This commit is contained in:
Jeffrey Paul 2024-06-05 00:24:36 -07:00
commit d312f07fa8
4 changed files with 16723 additions and 0 deletions

250
cmd/mullvadclosest/main.go Normal file
View File

@ -0,0 +1,250 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"math"
"math/rand"
"net/http"
"os/user"
"sort"
"sync"
"time"
"github.com/schollz/progressbar/v3"
)
// RelayLatency holds a relay and its associated latency
type RelayLatency struct {
Relay Relay
Latency time.Duration
}
// MullvadIPResponse represents the response from the Mullvad IP check API
type MullvadIPResponse struct {
IP string `json:"ip"`
Country string `json:"country"`
City string `json:"city"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
MullvadExitIP bool `json:"mullvad_exit_ip"`
MullvadExitIPHostname string `json:"mullvad_exit_ip_hostname,omitempty"`
MullvadServerType string `json:"mullvad_server_type,omitempty"`
Blacklisted struct {
Blacklisted bool `json:"blacklisted"`
Results []string `json:"results"`
} `json:"blacklisted"`
Organization string `json:"organization"`
}
// worker function to process liveness checks
func livenessWorker(id int, jobs <-chan Relay, results chan<- RelayLatency, wg *sync.WaitGroup, bar *progressbar.ProgressBar) {
defer wg.Done()
for relay := range jobs {
isLive, latency, err := relay.CheckLiveness()
if err == nil && isLive {
results <- RelayLatency{Relay: relay, Latency: latency}
}
bar.Add(1)
}
}
// worker function to process latency checks
func latencyWorker(id int, jobs <-chan RelayLatency, results chan<- RelayLatency, wg *sync.WaitGroup, bar *progressbar.ProgressBar) {
defer wg.Done()
for relayLatency := range jobs {
latency, err := relayLatency.Relay.MeasureLatency()
if err == nil {
results <- RelayLatency{
Relay: relayLatency.Relay,
Latency: latency,
}
}
bar.Add(1)
}
}
// shuffle function to randomly shuffle a slice
func shuffle(relays []Relay) {
rand.Shuffle(len(relays), func(i, j int) {
relays[i], relays[j] = relays[j], relays[i]
})
}
func shuffleLatency(relays []RelayLatency) {
rand.Shuffle(len(relays), func(i, j int) {
relays[i], relays[j] = relays[j], relays[i]
})
}
// CollectRelayLatencies iterates over relays, pings them, and collects latencies
func CollectRelayLatencies(relays []Relay) []RelayLatency {
var wg sync.WaitGroup
livenessJobs := make(chan Relay, len(relays))
livenessResults := make(chan RelayLatency, len(relays))
bar := progressbar.Default(int64(len(relays)), "Checking liveness")
// Start 20 workers for liveness checks
numLivenessWorkers := 20
for w := 0; w < numLivenessWorkers; w++ {
wg.Add(1)
go livenessWorker(w, livenessJobs, livenessResults, &wg, bar)
}
// Shuffle the relays before liveness checks
shuffle(relays)
// Send relays to liveness jobs channel
for _, relay := range relays {
livenessJobs <- relay
}
close(livenessJobs)
// Wait for all liveness workers to finish
wg.Wait()
close(livenessResults)
// Collect live relays
var liveRelays []RelayLatency
for result := range livenessResults {
liveRelays = append(liveRelays, result)
}
// Now measure latency for live relays
var relayLatencies []RelayLatency
latencyJobs := make(chan RelayLatency, len(liveRelays))
latencyResults := make(chan RelayLatency, len(liveRelays))
bar = progressbar.Default(int64(len(liveRelays)), "Measuring latency")
// Start 30 workers for latency checks
numLatencyWorkers := 30
for w := 0; w < numLatencyWorkers; w++ {
wg.Add(1)
go latencyWorker(w, latencyJobs, latencyResults, &wg, bar)
}
// Shuffle the live relays before latency checks
shuffleLatency(liveRelays)
// Send live relays to latency jobs channel
for _, liveRelay := range liveRelays {
latencyJobs <- liveRelay
}
close(latencyJobs)
// Wait for all latency workers to finish
wg.Wait()
close(latencyResults)
// Collect latency results
for result := range latencyResults {
relayLatencies = append(relayLatencies, result)
}
return relayLatencies
}
// PrintRelayLatencies prints the sorted list of relay latencies
func PrintRelayLatencies(relayLatencies []RelayLatency, totalRelays int, deadRelays int) {
sort.Slice(relayLatencies, func(i, j int) bool {
return relayLatencies[i].Latency < relayLatencies[j].Latency
})
if len(relayLatencies) > 0 {
minLatency := relayLatencies[0]
maxLatency := relayLatencies[len(relayLatencies)-1]
medianLatency := relayLatencies[len(relayLatencies)/2]
var totalLatency time.Duration
for _, rl := range relayLatencies {
totalLatency += rl.Latency
}
meanLatency := totalLatency / time.Duration(len(relayLatencies))
var sumOfSquares time.Duration
for _, rl := range relayLatencies {
deviation := rl.Latency - meanLatency
sumOfSquares += deviation * deviation
}
stddevLatency := time.Duration(math.Sqrt(float64(sumOfSquares / time.Duration(len(relayLatencies)))))
fmt.Printf("Total relays: %d\n", totalRelays)
fmt.Printf("Live relays: %d\n", len(relayLatencies))
fmt.Printf("Dead relays: %d\n", deadRelays)
fmt.Printf("Min latency: %v (%s)\n", minLatency.Latency, minLatency.Relay.String())
fmt.Printf("Max latency: %v (%s)\n", maxLatency.Latency, maxLatency.Relay.String())
fmt.Printf("Median latency: %v (%s)\n", medianLatency.Latency, medianLatency.Relay.String())
fmt.Printf("Mean latency: %v\n", meanLatency)
fmt.Printf("Stddev latency: %v\n", stddevLatency)
fmt.Println()
}
fmt.Printf("%-20s %-20s %-30s %s\n", "Country", "City", "Hostname", "Latency")
for i, rl := range relayLatencies {
if i < 25 || i >= len(relayLatencies)-4 {
fmt.Printf("%-20s %-20s %-30s %v\n",
rl.Relay.Location.Country,
rl.Relay.Location.City,
rl.Relay.Hostname,
rl.Latency)
}
if i == 24 {
fmt.Println()
}
}
}
// CheckMullvadExitIP checks if the current IP is a Mullvad exit IP
func CheckMullvadExitIP() (bool, error) {
resp, err := http.Get("https://ipv4.am.i.mullvad.net/json")
if err != nil {
return false, fmt.Errorf("failed to make GET request: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false, fmt.Errorf("failed to read response body: %v", err)
}
var mullvadResp MullvadIPResponse
if err := json.Unmarshal(body, &mullvadResp); err != nil {
return false, fmt.Errorf("failed to parse JSON response: %v", err)
}
return mullvadResp.MullvadExitIP, nil
}
func main() {
currentUser, err := user.Current()
if err != nil {
panic("Failed to get current user")
}
if currentUser.Uid != "0" {
panic("This program must be run as root")
}
isMullvadExitIP, err := CheckMullvadExitIP()
if err != nil {
panic(fmt.Sprintf("Error checking Mullvad exit IP: %v", err))
}
if isMullvadExitIP {
fmt.Println("This program is designed to test latency between your actual IP and the Mullvad VPN servers. Please disconnect from the VPN and run the program again.")
return
}
relays, err := ParseRelayData()
if err != nil {
fmt.Printf("Error parsing relay data: %v\n", err)
return
}
totalRelays := len(relays)
relayLatencies := CollectRelayLatencies(relays)
deadRelays := totalRelays - len(relayLatencies)
PrintRelayLatencies(relayLatencies, totalRelays, deadRelays)
}

View File

@ -0,0 +1,128 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"time"
"github.com/go-ping/ping"
"sneak.berlin/go/mullvadclosest"
)
// Relay represents the structure of each relay in the JSON file
type Relay struct {
Hostname string `json:"hostname"`
Ipv4AddrIn string `json:"ipv4_addr_in"`
Ipv6AddrIn string `json:"ipv6_addr_in"`
IncludeInCountry bool `json:"include_in_country"`
Active bool `json:"active"`
Owned bool `json:"owned"`
Provider string `json:"provider"`
Weight int `json:"weight"`
EndpointData interface{} `json:"endpoint_data"` // Can be a string (e.g., "openvpn") or a nested struct
Location struct {
Country string `json:"country"`
CountryCode string `json:"country_code"`
City string `json:"city"`
CityCode string `json:"city_code"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
} `json:"location"`
}
// String returns a string representation of the Relay
func (r Relay) String() string {
return fmt.Sprintf("Relay(hostname=%s, ip=%s, country=%s)", r.Hostname, r.Ipv4AddrIn, r.Location.Country)
}
// CheckLiveness checks if the relay is live
func (r Relay) CheckLiveness() (bool, time.Duration, error) {
pinger, err := ping.NewPinger(r.Ipv4AddrIn)
if err != nil {
return false, 0, err
}
pinger.Count = 1
pinger.Timeout = 3 * time.Second // Increased timeout for the single ping
pinger.SetPrivileged(true)
pinger.Run()
stats := pinger.Statistics()
if stats.PacketsRecv == 0 {
return false, 0, nil
}
return true, stats.AvgRtt, nil
}
// MeasureLatency measures the minimum latency of the relay over 5 pings
func (r Relay) MeasureLatency() (time.Duration, error) {
pinger, err := ping.NewPinger(r.Ipv4AddrIn)
if err != nil {
return 0, err
}
pinger.Count = 5
pinger.Interval = 1 * time.Second // Adding interval between pings
pinger.Timeout = 10 * time.Second // Increased overall timeout
pinger.SetPrivileged(true)
pinger.Run()
stats := pinger.Statistics()
return stats.MinRtt, nil
}
// RelayData represents the structure of the JSON file
type RelayData struct {
Etag string `json:"etag"`
Countries []struct {
Name string `json:"name"`
Code string `json:"code"`
Cities []struct {
Name string `json:"name"`
Code string `json:"code"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Relays []Relay `json:"relays"`
} `json:"cities"`
} `json:"countries"`
}
// ParseRelayData parses the JSON file and returns a flat list of all relays
func ParseRelayData() ([]Relay, error) {
paths := []string{
"/var/cache/mullvad-vpn/relays.json",
"/Library/Caches/mullvad-vpn/relays.json",
`C:\ProgramData\Mullvad VPN\cache\relays.json`,
}
var filePath string
for _, path := range paths {
if _, err := os.Stat(path); err == nil {
filePath = path
break
}
}
var fileData []byte
if filePath == "" {
// use embedded cache if not on filesystem
fileData = mullvadclosest.RelayJSON
} else {
fileData, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %v", err)
}
}
var relayData RelayData
if err := json.Unmarshal(fileData, &relayData); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %v", err)
}
var relays []Relay
for _, country := range relayData.Countries {
for _, city := range country.Cities {
relays = append(relays, city.Relays...)
}
}
return relays, nil
}

9
embed.json Normal file
View File

@ -0,0 +1,9 @@
package mullvadclosest
import (
"embed"
)
//go:embed relays.2024-06-04.json
var RelayJSON embed.FS

16336
relays.2024-06-04.json Normal file

File diff suppressed because it is too large Load Diff