initial
This commit is contained in:
commit
d312f07fa8
250
cmd/mullvadclosest/main.go
Normal file
250
cmd/mullvadclosest/main.go
Normal 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)
|
||||||
|
}
|
128
cmd/mullvadclosest/relays.go
Normal file
128
cmd/mullvadclosest/relays.go
Normal 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
9
embed.json
Normal 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
16336
relays.2024-06-04.json
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user