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