Compare commits

..

26 Commits

Author SHA1 Message Date
2b05df2b75 Fix some error logging - Msg() needs to be called for exiting with exit 1 2022-03-19 17:00:48 +01:00
415cfa8ee7 recover gracefully if no server found 2022-03-12 11:45:41 +01:00
0bf9fc463a Update README.md 2022-03-12 11:28:58 +01:00
44cfbce5f1 add flag -l to set log-level 2022-03-12 11:24:27 +01:00
3c17eded13 remove redundant '-' from .goreleaser.yaml 2022-03-07 21:45:27 +01:00
d3a3969aee remove build folder after linting to enable clean state repo for goreleaser 2022-03-07 21:38:48 +01:00
7f01d016a4 remove linter binary after linting to enable clean state repo for goreleaser 2022-03-07 20:09:48 +01:00
5f3f234fff add linter and make file (#2) 2022-03-07 20:03:32 +01:00
18f5d74402 optimize build 2022-03-07 19:26:58 +01:00
71611fd754 simplify & optimize code 2022-03-07 19:24:11 +01:00
310f59e03c describe usage in script 2022-03-06 19:57:31 +01:00
a6992b2373 archive binaries without version number, use commit timestamp as mod_timestamp 2022-03-06 19:31:40 +01:00
31859569fc fix typo in README 2022-03-06 19:25:06 +01:00
d31571fc50 cleanup (country default now comes from flag) 2022-03-06 19:20:21 +01:00
5eefe15b3b Update README.md 2022-03-06 19:10:33 +01:00
dbb84f0c0c add support to select server type 2022-03-06 19:06:59 +01:00
6c839d7196 Create LICENSE (#1) 2022-03-06 18:33:24 +01:00
d2de3225b0 add support for selecting a country via CLI flag 2022-03-06 18:03:36 +01:00
130a048394 fix design after adding badge in README.md 2022-03-06 17:47:47 +01:00
0be7f66582 updated README.md with badge 2022-03-06 17:46:55 +01:00
0f014cf9f3 updated README.md with usage information 2022-03-06 17:37:44 +01:00
230e89e2b7 allow full json output for best server using flag -o json 2022-03-06 17:36:21 +01:00
20e1092604 unshallow checkout to always have access to the latest tag during release 2022-03-06 17:16:09 +01:00
2998e1d39d Create README.md 2022-03-06 17:09:40 +01:00
5821d890b8 only give short server (e.g. de5) as output 2022-03-06 16:51:45 +01:00
2dfdb1ad74 set log-level to info, change output to only the best server name 2022-03-06 16:42:58 +01:00
7 changed files with 223 additions and 31 deletions

View File

@ -9,6 +9,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v2
@ -18,6 +20,12 @@ jobs:
- name: Test
run: go test -v ./...
- name: Lint source code
run: |
make tools lint
rm -rf .bin/
rm -rf dist/
- name: Create release tag
run: |
git tag "v$(git show -s --format=%cd --date=format:%Y%m%d.%H%M%S)"

26
.golangci.yaml Normal file
View File

@ -0,0 +1,26 @@
linters:
disable-all: true
enable:
- deadcode
- errcheck
- gofmt
- goimports
- gosimple
- ineffassign
- misspell
- staticcheck
- structcheck
- unconvert
- unused
- varcheck
- govet
linters-settings:
goimports:
local-prefixes: github.com/bastiandoetsch/mullvad-best-server
output:
format: tab
run:
deadline: 10m

View File

@ -1,19 +1,20 @@
project_name: mullvad-best-server
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- env:
- flags:
- -trimpath
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
mod_timestamp: "{{ .CommitTimestamp }}"
archives:
- replacements:
darwin: Darwin
@ -21,10 +22,15 @@ archives:
windows: Windows
386: i386
amd64: x86_64
format: binary
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Bastian Doetsch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

60
Makefile Normal file
View File

@ -0,0 +1,60 @@
# project variables
PROJECT_NAME := mullvad-best-server
# build variables
.DEFAULT_GOAL = lint
BUILD_DIR := dist
DEV_GOARCH := $(shell go env GOARCH)
DEV_GOOS := $(shell go env GOOS)
## tools: Install required tooling.
.PHONY: tools
tools:
ifeq (,$(wildcard ./.bin/golangci-lint*))
@curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b .bin/ v1.44.2
else
@echo "==> Required tooling is already installed"
endif
## clean: Delete the build directory
.PHONY: clean
clean:
@echo "==> Removing '$(BUILD_DIR)' directory..."
@rm -rf $(BUILD_DIR)
## lint: Lint code with golangci-lint.
.PHONY: lint
lint: tools
@echo "==> Linting code with 'golangci-lint'..."
@.bin/golangci-lint run ./...
## test: Run all unit tests.
.PHONY: test
test:
@echo "==> Running unit tests..."
@mkdir -p $(BUILD_DIR)
@go test -count=1 -v -cover -coverprofile=$(BUILD_DIR)/coverage.out -parallel=4 ./...
## build: Build binary for default local system's OS and architecture.
.PHONY: build
build:
@echo "==> Building binary..."
@echo " running go build for GOOS=$(DEV_GOOS) GOARCH=$(DEV_GOARCH)"
# workaround for missing .exe extension on Windows
ifeq ($(OS),Windows_NT)
@go build -o $(BUILD_DIR)/$(PROJECT_NAME).$(DEV_GOOS).$(DEV_GOARCH).exe
else
@go build -o $(BUILD_DIR)/$(PROJECT_NAME).$(DEV_GOOS).$(DEV_GOARCH)
endif
.PHONY: run
run:
@echo "==> Running $(PROJECT_NAME)"
@go run main.go
help: Makefile
@echo "Usage: make <command>"
@echo ""
@echo "Commands:"
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# mullvad-best-server
![Build](https://github.com/bastiandoetsch/mullvad-best-server/actions/workflows/go.yml/badge.svg)
Determines the mullvad.net server with the lowest latency.
## Installation
Download binary from releases for your platform and unpack.
## Usage
### Default usage
Execute `mullvad-best-server`. It outputs the code, e.g. `de05`. You can then connect to it with e.g. wireguard using the normal shell scripts.
### Command line parameters
```angular2html
Usage of dist/mullvad-best-server_darwin_amd64/mullvad-best-server:
-c string
Server country code, e.g. ch for Switzerland (default "ch")
-l string
Log level. Allowed values: trace, debug, info, warn, error, fatal, panic (default "info")
-o string
Output format. 'json' outputs server json
-t string
Server type, e.g. wireguard (default "wireguard")
```
If you want the full server information, execute `mullvad-best-server -o json`. It returns the full json output of the server information.
The `-c` flag allows to give a country code. Else `ch` will be used.
## Background
The program uses `https://api.mullvad.net/www/relays/<SERVER_TYPE>/` to get the current server list, pings the ones with the right country
and outputs the server with the lowest ping.
## Integration into a script
I use it on my router like this (yes, I know I could have done the whole thing with jq and shell scripting, but wanted to use go for maintainability).
```
#!/bin/sh
set -e
LATEST_RELEASE=$(curl -sSL https://api.github.com/repos/bastiandoetsch/mullvad-best-server/releases/latest | jq -r '.assets[]| .browser_download_url' | grep Linux_arm64)
curl -sSL $LATEST_RELEASE > /root/mullvad-best-server
chmod +x /root/mullvad-best-server
/usr/bin/wg-quick down $(wg show|grep interface | cut -d: -f2) || echo "nothing to shut down"
/usr/bin/wg-quick up "mullvad-$(/root/mullvad-best-server -c de)"
```

71
main.go
View File

@ -2,38 +2,60 @@ package main
import (
"encoding/json"
"github.com/go-ping/ping"
"github.com/rs/zerolog/log"
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/go-ping/ping"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var pings = make(map[string]time.Duration)
func main() {
var outputFlag = flag.String("o", "", "Output format. 'json' outputs server json")
var countryFlag = flag.String("c", "ch", "Server country code, e.g. ch for Switzerland")
var typeFlag = flag.String("t", "wireguard", "Server type, e.g. wireguard")
var logLevel = flag.String("l", "info", "Log level. Allowed values: trace, debug, info, warn, error, fatal, panic")
flag.Parse()
servers := getServers()
bestIndex := selectBestServerIndex(servers)
log.Info().Interface("best", servers[bestIndex]).Msg("Best Latency Server found.")
level, err := zerolog.ParseLevel(*logLevel)
if err != nil {
log.Fatal().Err(err).Msg("Unable to set log level")
}
zerolog.SetGlobalLevel(level)
servers := getServers(*typeFlag)
bestIndex := selectBestServerIndex(servers, *countryFlag)
if bestIndex == -1 {
log.Fatal().Str("country", *countryFlag).Msg("No servers for country found.")
}
best := servers[bestIndex]
log.Debug().Interface("server", best).Msg("Best latency server found.")
hostname := strings.TrimSuffix(best.Hostname, "-wireguard")
if *outputFlag != "json" {
fmt.Println(hostname)
} else {
serverJson, err := json.Marshal(best)
if err != nil {
log.Fatal().Err(err).Msg("Couldn't marshal server information to Json")
}
fmt.Println(string(serverJson))
}
}
func selectBestServerIndex(servers []server) int {
best := servers[0].Hostname
bestIndex := 0
allowedCountries := map[string]string{}
allowedCountries["de"] = "1"
allowedCountries["ch"] = "1"
allowedCountries["at"] = "1"
func selectBestServerIndex(servers []server, country string) int {
bestIndex := -1
var bestPing time.Duration
for i, server := range servers {
if server.Active && allowedCountries[server.CountryCode] != "" {
if server.Active && server.CountryCode == country {
duration, err := serverLatency(server)
if err == nil {
pings[server.Hostname] = duration
if best == "" || pings[best] > pings[server.Hostname] {
best = server.Hostname
if bestIndex == -1 || bestPing > duration {
bestIndex = i
bestPing = duration
}
}
}
@ -41,10 +63,10 @@ func selectBestServerIndex(servers []server) int {
return bestIndex
}
func getServers() []server {
resp, err := http.Get("https://api.mullvad.net/www/relays/wireguard/")
func getServers(serverType string) []server {
resp, err := http.Get("https://api.mullvad.net/www/relays/" + serverType + "/")
if err != nil {
log.Fatal().Err(err)
log.Fatal().Err(err).Msg("Couldn't retrieve servers")
}
responseBody, err := ioutil.ReadAll(resp.Body)
defer func(Body io.ReadCloser) {
@ -53,10 +75,13 @@ func getServers() []server {
log.Err(err)
}
}(resp.Body)
if err != nil {
log.Fatal().Err(err)
}
var servers []server
err = json.Unmarshal(responseBody, &servers)
if err != nil {
log.Fatal().Err(err)
log.Fatal().Err(err).Msg("couldn't unmarshall server json")
}
return servers
}
@ -69,7 +94,7 @@ func serverLatency(s server) (time.Duration, error) {
}
var duration time.Duration
pinger.OnRecv = func(pkt *ping.Packet) {
log.Info().Str("Server", s.Hostname).IPAddr("IP", pkt.IPAddr.IP).Dur("RTT", pkt.Rtt).Msg("Added server latency.")
log.Debug().Str("Server", s.Hostname).IPAddr("IP", pkt.IPAddr.IP).Dur("RTT", pkt.Rtt).Msg("Added server latency.")
duration = pkt.Rtt
}
err = pinger.Run()