Compare commits

...

8 Commits

6 changed files with 131 additions and 18 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,20 +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
@ -24,13 +24,16 @@ archives:
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

@ -30,3 +30,15 @@ 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)"
```

26
main.go
View File

@ -4,25 +4,29 @@ import (
"encoding/json"
"flag"
"fmt"
"github.com/go-ping/ping"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"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() {
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)
best := servers[bestIndex]
@ -40,16 +44,15 @@ func main() {
}
func selectBestServerIndex(servers []server, country string) int {
best := servers[0].Hostname
bestIndex := -1
var bestPing time.Duration
for i, server := range servers {
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
}
}
}
@ -69,6 +72,9 @@ 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 {