Compare commits

...

16 Commits

Author SHA1 Message Date
8aedb82176 added window information to the README.md 2022-07-28 20:32:43 -05:00
91ba7a4dfa fix #4 by sending a priviledged raw ICMP ping 2022-07-28 20:19:49 -05:00
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
8 changed files with 189 additions and 70 deletions

View File

@ -19,7 +19,13 @@ 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,13 +22,18 @@ 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:
exclude:
- '^docs:'
- '^test:'
- '^test:'

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/^/ /'

View File

@ -1,7 +1,12 @@
# mullvad-best-server
![Build](https://github.com/bastiandoetsch/mullvad-best-server/actions/workflows/go.yml/badge.svg)
Determines the mullvat.net wireguard server with the lowest latency.
Determines the mullvad.net server with the lowest latency on macOS and Linux. On Windows, it can only check if the server is up.
The reason lies with the golang network libraries, according to the `go-ping` library, that is used under the hood for pinging:
```
Please note that accessing packet TTL values is not supported due to limitations in the Go x/net/ipv4 and x/net/ipv6 packages.
```
## Installation
@ -17,6 +22,8 @@ Execute `mullvad-best-server`. It outputs the code, e.g. `de05`. You can then co
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
@ -28,5 +35,17 @@ 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
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)"
```

14
go.mod
View File

@ -3,13 +3,15 @@ module github.com/bastiandoetsch/mullvad-best-server
go 1.17
require (
github.com/go-ping/ping v0.0.0-20211130115550-779d1e919534
github.com/rs/zerolog v1.26.1
github.com/go-ping/ping v1.1.0
github.com/rs/zerolog v1.27.0
)
require (
github.com/google/uuid v1.2.0 // indirect
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
)

52
go.sum
View File

@ -1,41 +1,33 @@
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/go-ping/ping v0.0.0-20211130115550-779d1e919534 h1:dhy9OQKGBh4zVXbjwbxxHjRxMJtLXj3zfgpBYQaR4Q4=
github.com/go-ping/ping v0.0.0-20211130115550-779d1e919534/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462 h1:UreQrH7DbFXSi9ZFox6FNT3WBooWmdANpU+IfkT1T4I=
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

56
main.go
View File

@ -4,27 +4,35 @@ import (
"encoding/json"
"flag"
"fmt"
"github.com/go-ping/ping"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"io"
"io/ioutil"
"net/http"
"runtime"
"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() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
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()
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)
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")
@ -33,32 +41,25 @@ func main() {
} else {
serverJson, err := json.Marshal(best)
if err != nil {
log.Fatal().Err(err)
log.Fatal().Err(err).Msg("Couldn't marshal server information to Json")
}
fmt.Println(string(serverJson))
}
}
func selectBestServerIndex(servers []server, country *string) int {
best := servers[0].Hostname
func selectBestServerIndex(servers []server, country string) int {
bestIndex := -1
allowedCountries := map[string]string{}
if *country == "" {
allowedCountries["de"] = "1"
allowedCountries["ch"] = "1"
allowedCountries["at"] = "1"
} else {
allowedCountries[*country] = "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 bestIndex == -1 || pings[best] > pings[server.Hostname] {
best = server.Hostname
if bestIndex == -1 || bestPing > duration {
bestIndex = i
bestPing = duration
}
} else {
log.Err(err).Msg("Error determining the server latency via ping.")
}
}
}
@ -68,7 +69,7 @@ func selectBestServerIndex(servers []server, country *string) int {
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) {
@ -77,16 +78,23 @@ func getServers(serverType string) []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
}
//goland:noinspection GoBoolExpressions
func serverLatency(s server) (time.Duration, error) {
pinger, err := ping.NewPinger(s.Ipv4AddrIn)
if runtime.GOOS == "windows" {
pinger.SetPrivileged(true)
}
pinger.Count = 1
if err != nil {
return 0, err