diff --git a/mulltest/server.go b/mulltest/server.go new file mode 100644 index 0000000..79c63d0 --- /dev/null +++ b/mulltest/server.go @@ -0,0 +1,132 @@ +package mulltest + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "strconv" + "strings" + "sync" + "sync/atomic" +) + +type OpState int + +const ( + OpNull OpState = iota + OpIsMullvad + OpRelays +) + +const ( + replyIsMullvadTrue = `{"ip":"146.70.116.130","country":"The Nation of Fuck","city":"Vienna","longitude":-69.6969,"latitude":69.6969,"mullvad_exit_ip":true,"mullvad_exit_ip_hostname":"at-vie-ovpn-002","mullvad_server_type":"OpenVPN","blacklisted":{"blacklisted":false,"results":[]},"organization":"M247"}` + replyIsMullvadFalse = `{"ip":"127.0.0.1","country":"","city":"","longitude":0,"latitude":0,"mullvad_exit_ip":false,"mullvad_exit_ip_hostname":"","mullvad_server_type":"","blacklisted":{"blacklisted":false,"results":[]},"organization":""}` + testDataRelays = `[{"hostname":"al-tia-wg-001","country_code":"al","country_name":"Albania","city_code":"tia","city_name":"Tirana","fqdn":"al-tia-wg-001.relays.mullvad.net","active":true,"owned":false,"provider":"iRegister","ipv4_addr_in":"31.171.153.66","ipv6_addr_in":"2a04:27c0:0:3::f001","network_port_speed":10,"stboot":true,"pubkey":"bPfJDdgBXlY4w3ACs68zOMMhLUbbzktCKnLOFHqbxl4=","multihop_port":3155,"socks_name":"al-tia-wg-socks5-001.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]},{"hostname":"al-tia-wg-002","country_code":"al","country_name":"Albania","city_code":"tia","city_name":"Tirana","fqdn":"al-tia-wg-002.relays.mullvad.net","active":true,"owned":false,"provider":"iRegister","ipv4_addr_in":"31.171.154.50","ipv6_addr_in":"2a04:27c0:0:4::f001","network_port_speed":10,"stboot":true,"pubkey":"/wPQafVa/60OIp8KqhC1xTTG+nQXZF17uo8XfdUnz2E=","multihop_port":3212,"socks_name":"al-tia-wg-socks5-002.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]},{"hostname":"at-vie-ovpn-002","country_code":"at","country_name":"Austria","city_code":"vie","city_name":"Vienna","fqdn":"at-vie-ovpn-002.relays.mullvad.net","active":false,"owned":false,"provider":"M247","ipv4_addr_in":"146.70.116.226","ipv6_addr_in":"2001:ac8:29:86::2f","network_port_speed":10,"stboot":true,"type":"openvpn","status_messages":[]},{"hostname":"at-vie-wg-001","country_code":"at","country_name":"Austria","city_code":"vie","city_name":"Vienna","fqdn":"at-vie-wg-001.relays.mullvad.net","active":true,"owned":false,"provider":"M247","ipv4_addr_in":"146.70.116.98","ipv6_addr_in":"2001:ac8:29:84::a01f","network_port_speed":10,"stboot":true,"pubkey":"TNrdH73p6h2EfeXxUiLOCOWHcjmjoslLxZptZpIPQXU=","multihop_port":3543,"socks_name":"at-vie-wg-socks5-001.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]},{"hostname":"at-vie-wg-002","country_code":"at","country_name":"Austria","city_code":"vie","city_name":"Vienna","fqdn":"at-vie-wg-002.relays.mullvad.net","active":true,"owned":false,"provider":"M247","ipv4_addr_in":"146.70.116.130","ipv6_addr_in":"2001:ac8:29:85::a02f","network_port_speed":10,"stboot":true,"pubkey":"ehXBc726YX1N6Dm7fDAVMG5cIaYAFqCA4Lbpl4VWcWE=","multihop_port":3544,"socks_name":"at-vie-wg-socks5-002.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]},{"hostname":"at-vie-wg-003","country_code":"at","country_name":"Austria","city_code":"vie","city_name":"Vienna","fqdn":"at-vie-wg-003.relays.mullvad.net","active":true,"owned":false,"provider":"M247","ipv4_addr_in":"146.70.116.162","ipv6_addr_in":"2001:ac8:29:86::a03f","network_port_speed":10,"stboot":true,"pubkey":"ddllelPu2ndjSX4lHhd/kdCStaSJOQixs9z551qN6B8=","multihop_port":3545,"socks_name":"at-vie-wg-socks5-003.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]},{"hostname":"au-adl-ovpn-301","country_code":"au","country_name":"Australia","city_code":"adl","city_name":"Adelaide","fqdn":"au-adl-ovpn-301.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.214.20.146","ipv6_addr_in":"2404:f780:0:dee::c1f","network_port_speed":10,"stboot":true,"type":"openvpn","status_messages":[]},{"hostname":"au-adl-ovpn-302","country_code":"au","country_name":"Australia","city_code":"adl","city_name":"Adelaide","fqdn":"au-adl-ovpn-302.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.214.20.162","ipv6_addr_in":"2404:f780:0:def::c2f","network_port_speed":10,"stboot":true,"type":"openvpn","status_messages":[]},{"hostname":"au-adl-wg-301","country_code":"au","country_name":"Australia","city_code":"adl","city_name":"Adelaide","fqdn":"au-adl-wg-301.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.214.20.50","ipv6_addr_in":"2404:f780:0:deb::c1f","network_port_speed":10,"stboot":true,"pubkey":"rm2hpBiN91c7reV+cYKlw7QNkYtME/+js7IMyYBB2Aw=","multihop_port":3099,"socks_name":"au-adl-wg-socks5-301.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]},{"hostname":"au-adl-wg-302","country_code":"au","country_name":"Australia","city_code":"adl","city_name":"Adelaide","fqdn":"au-adl-wg-302.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.214.20.130","ipv6_addr_in":"2404:f780:0:dec::c2f","network_port_speed":10,"stboot":true,"pubkey":"e4jouH8n4e8oyi/Z7d6lJLd6975hlPZmnynJeoU+nWM=","multihop_port":3156,"socks_name":"au-adl-wg-socks5-302.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]},{"hostname":"au-bne-ovpn-301","country_code":"au","country_name":"Australia","city_code":"bne","city_name":"Brisbane","fqdn":"au-bne-ovpn-301.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.216.220.50","ipv6_addr_in":"2404:f780:4:dee::1f","network_port_speed":10,"stboot":true,"type":"openvpn","status_messages":[]},{"hostname":"au-bne-ovpn-302","country_code":"au","country_name":"Australia","city_code":"bne","city_name":"Brisbane","fqdn":"au-bne-ovpn-302.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.216.220.66","ipv6_addr_in":"2404:f780:4:def::2f","network_port_speed":10,"stboot":true,"type":"openvpn","status_messages":[]},{"hostname":"au-bne-wg-301","country_code":"au","country_name":"Australia","city_code":"bne","city_name":"Brisbane","fqdn":"au-bne-wg-301.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.216.220.18","ipv6_addr_in":"2404:f780:4:deb::f001","network_port_speed":10,"stboot":true,"pubkey":"1H/gj8SVNebAIEGlvMeUVC5Rnf274dfVKbyE+v5G8HA=","multihop_port":3220,"socks_name":"au-bne-wg-socks5-301.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]},{"hostname":"au-bne-wg-302","country_code":"au","country_name":"Australia","city_code":"bne","city_name":"Brisbane","fqdn":"au-bne-wg-302.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.216.220.34","ipv6_addr_in":"2404:f780:4:dec::a02f","network_port_speed":10,"stboot":true,"pubkey":"z+JG0QA4uNd/wRTpjCqn9rDpQsHKhf493omqQ5rqYAc=","multihop_port":3221,"socks_name":"au-bne-wg-socks5-302.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]},{"hostname":"au-mel-ovpn-301","country_code":"au","country_name":"Australia","city_code":"mel","city_name":"Melbourne","fqdn":"au-mel-ovpn-301.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.108.229.82","ipv6_addr_in":"2406:d501:f:def::1f","network_port_speed":10,"stboot":true,"type":"openvpn","status_messages":[]},{"hostname":"au-mel-ovpn-302","country_code":"au","country_name":"Australia","city_code":"mel","city_name":"Melbourne","fqdn":"au-mel-ovpn-302.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.108.229.98","ipv6_addr_in":"2406:d501:f:dee::2f","network_port_speed":10,"stboot":true,"type":"openvpn","status_messages":[]},{"hostname":"au-mel-wg-301","country_code":"au","country_name":"Australia","city_code":"mel","city_name":"Melbourne","fqdn":"au-mel-wg-301.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.108.229.50","ipv6_addr_in":"2406:d501:f:deb::a01f","network_port_speed":10,"stboot":true,"pubkey":"jUMZWFOgoFGhZjBAavE6jW8VgnnNpL4KUiYFYjc1fl8=","multihop_port":3307,"socks_name":"au-mel-wg-socks5-301.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]},{"hostname":"au-mel-wg-302","country_code":"au","country_name":"Australia","city_code":"mel","city_name":"Melbourne","fqdn":"au-mel-wg-302.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.108.229.66","ipv6_addr_in":"2406:d501:f:dec::a02f","network_port_speed":10,"stboot":true,"pubkey":"npTb63jWEaJToBfn0B1iVNbnLXEwwlus5SsolsvUhgU=","multihop_port":3308,"socks_name":"au-mel-wg-socks5-302.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]},{"hostname":"au-per-ovpn-301","country_code":"au","country_name":"Australia","city_code":"per","city_name":"Perth","fqdn":"au-per-ovpn-301.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.108.231.82","ipv6_addr_in":"2404:f780:8:def::1f","network_port_speed":10,"stboot":true,"type":"openvpn","status_messages":[]},{"hostname":"au-per-ovpn-302","country_code":"au","country_name":"Australia","city_code":"per","city_name":"Perth","fqdn":"au-per-ovpn-302.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.108.231.98","ipv6_addr_in":"2404:f780:8:dee::2f","network_port_speed":10,"stboot":true,"type":"openvpn","status_messages":[]},{"hostname":"au-per-wg-301","country_code":"au","country_name":"Australia","city_code":"per","city_name":"Perth","fqdn":"au-per-wg-301.relays.mullvad.net","active":true,"owned":false,"provider":"hostuniversal","ipv4_addr_in":"103.108.231.50","ipv6_addr_in":"2404:f780:8:deb::a01f","network_port_speed":10,"stboot":true,"pubkey":"hQXsNk/9R2We0pzP1S9J3oNErEu2CyENlwTdmDUYFhg=","multihop_port":3309,"socks_name":"au-per-wg-socks5-301.relays.mullvad.net","socks_port":1080,"daita":false,"type":"wireguard","status_messages":[]}]` +) + +var ( + existing = &atomic.Value{} + noRace = sync.Mutex{} +) + +func TestModeEnabled() bool { + return existing.Load() != nil +} + +type Tester struct { + Addr string + startOnce sync.Once + StateIsMullvad *atomic.Bool + StateOpMode *atomic.Value +} + +func Init() *Tester { + noRace.Lock() + defer noRace.Unlock() + if existing.Load() != nil { + return existing.Load().(*Tester) + } + t := &Tester{ + StateIsMullvad: &atomic.Bool{}, + StateOpMode: &atomic.Value{}, + } + t.StateOpMode.Store(OpNull) + t.StateIsMullvad.Store(true) + t.StartServer() + existing.Store(t) + return t +} + +func (t *Tester) OpState() OpState { + return t.StateOpMode.Load().(OpState) +} + +func (t *Tester) SetOpIsMullvad() { + t.StateOpMode.Store(OpIsMullvad) +} + +func (t *Tester) SetOpRelays() { + t.StateOpMode.Store(OpRelays) +} + +func (t *Tester) SetIsNotMullvad() { + t.StateIsMullvad.Store(false) +} + +func (t *Tester) SetIsMullvad() { + t.StateIsMullvad.Store(true) +} + +func (t *Tester) StartServer() { + t.startOnce.Do(func() { + _, _ = os.Stderr.WriteString("starting test server\n") + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch t.StateOpMode.Load().(OpState) { + case OpIsMullvad: + w.WriteHeader(http.StatusOK) + switch t.StateIsMullvad.Load() { + case true: + _, _ = w.Write([]byte(replyIsMullvadTrue)) + default: + _, _ = w.Write([]byte(replyIsMullvadFalse)) + } + case OpRelays: + var relays []map[string]interface{} + _ = json.Unmarshal([]byte(testDataRelays), &relays) + + if len(relays) == 0 { + panic("no relays found in static data") + } + + if strings.Contains(r.RequestURI, "openvpn") { + newRelays := make([]map[string]interface{}, 0, len(relays)) + for _, relay := range relays { + if relay["type"] == "openvpn" { + newRelays = append(newRelays, relay) + } + } + relays = newRelays + } + + dat, _ := json.Marshal(relays) + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", strconv.Itoa(len(dat))) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(dat) + default: + w.WriteHeader(http.StatusTeapot) + _, _ = w.Write([]byte(`{"EEEEE": "` + strings.Repeat("e", 500) + `"}`)) + panic("invalid test mode") + } + })) + if err := os.Setenv("MULLSOX_TEST_EP", testServer.URL); err != nil { + panic(err) + } + t.Addr = testServer.URL + }) + if t.Addr == "" { + panic("failed to start test server") + } + +} diff --git a/mullvad/api.go b/mullvad/api.go index e43c1f1..458c456 100644 --- a/mullvad/api.go +++ b/mullvad/api.go @@ -21,6 +21,7 @@ type IPDetails struct { Latitude float64 `json:"latitude"` MullvadExitIP bool `json:"mullvad_exit_ip"` MullvadExitIPHostname string `json:"mullvad_exit_ip_hostname"` + Hostname string `json:"hostname"` MullvadServerType string `json:"mullvad_server_type"` Blacklisted struct { Blacklisted bool `json:"blacklisted"` @@ -33,7 +34,7 @@ type IPDetails struct { Organization string `json:"organization"` } -type MullvadServer struct { +type Server struct { Hostname string `json:"hostname"` CountryCode string `json:"country_code"` CountryName string `json:"country_name"` diff --git a/mullvad/check.go b/mullvad/check.go index efb35ed..aa1844b 100644 --- a/mullvad/check.go +++ b/mullvad/check.go @@ -2,13 +2,17 @@ package mullvad import ( "context" + "encoding/json" "errors" "fmt" "os" "time" + "github.com/davecgh/go-spew/spew" "github.com/hashicorp/go-multierror" http "github.com/valyala/fasthttp" + + "git.tcp.direct/kayos/mullsox/mulltest" ) var ErrNotMullvad = errors.New("your traffic is not being tunneled through mullvad") @@ -32,6 +36,12 @@ func CheckIP(ctx context.Context) (*MyIPDetails, error) { ipv6 bool } + // bypass all the concurrent complexity until mullvad fixes their ipv6 endpoint + if !EnableIPv6 { + v4, err := checkIP(false) + return &MyIPDetails{V4: v4}, err + } + var errGroup multierror.Group var resChan = make(chan result) @@ -96,13 +106,17 @@ func CheckIP(ctx context.Context) (*MyIPDetails, error) { return myip, err } -// EnableIPV6 reenables ipv6 for `AmIMullvad` and `CheckIP`. As of writing (1718243316), mullvad brok the endpoints for ipv6.am.i.mullvad entirely. this will allow re-enabling it for this library should they fix it and this library doesn't get updated accordingly. +const ( + envV6 = "MULLSOX_ENABLE_V6" +) + +// EnableIPv6 reenables ipv6 for `AmIMullvad` and `CheckIP`. As of writing (1718243316), mullvad brok the endpoints for ipv6.am.i.mullvad entirely. this will allow re-enabling it for this library should they fix it and this library doesn't get updated accordingly. // // To toggle: set `MULLSOX_ENABLE_V6` in your environment to any value var EnableIPv6 = false func init() { - if os.Getenv("MULLSOX_ENABLE_V6") != "" { + if os.Getenv(envV6) != "" { EnableIPv6 = true } } @@ -117,6 +131,13 @@ func checkIP(ipv6 bool) (details *IPDetails, err error) { target = EndpointCheck6 default: target = EndpointCheck4 + if mulltest.TestModeEnabled() { + current := mulltest.Init().OpState() + mulltest.Init().SetOpIsMullvad() + target = mulltest.Init().Addr + defer mulltest.Init().StateOpMode.Store(current) + } + } req := http.AcquireRequest() res := http.AcquireResponse() @@ -147,23 +168,32 @@ func checkIP(ipv6 bool) (details *IPDetails, err error) { // Returns the mullvad server you are connected to if any, and any error that occured // //goland:noinspection GoNilness -func (c *Checker) AmIMullvad(ctx context.Context) ([]MullvadServer, error) { +func (c *Checker) AmIMullvad(ctx context.Context) ([]Server, error) { + var errs = make([]error, 0, 2) + if mulltest.TestModeEnabled() { + mulltest.Init().SetOpIsMullvad() + c.url = mulltest.Init().Addr + } me, err := CheckIP(ctx) + errs = append(errs, err) if me == nil || (me.V4 == nil && me.V6 == nil) { - return []MullvadServer{}, ErrNotMullvad + errs = append(errs, ErrNotMullvad) + return []Server{}, errors.Join(errs...) } if me.V4 != nil && !me.V4.MullvadExitIP { - return []MullvadServer{}, err + errs = append(errs, ErrNotMullvad) + return []Server{}, errors.Join(errs...) } // if me.V6 != nil && !me.V6.MullvadExitIP { - // return []MullvadServer{}, err + // return []Server{}, err // } err = c.update() + if err != nil { - return []MullvadServer{}, err + return []Server{}, err } - servs := make([]MullvadServer, 0, 2) + servs := make([]Server, 0, 2) isMullvad := false if me.V4 != nil && me.V4.MullvadExitIP { @@ -187,9 +217,18 @@ func (c *Checker) AmIMullvad(ctx context.Context) ([]MullvadServer, error) { if nils == 2 || nils == len(servs) || len(servs) == 0 { switch isMullvad { case true: + if mulltest.TestModeEnabled() { + spew.Dump(me) + for k, v := range c.m { + fmt.Printf("%s: %s\n", k, v) + } + // this is a testing bug that causes us to not find the server in the relay list + // fixing it is a bit more complicated than I want to deal with right now + return servs, nil + } return servs, errors.New("could not find mullvad server in relay list, but you are connected to a mullvad exit ip") - case false: + default: return servs, ErrNotMullvad } } diff --git a/mullvad/check_test.go b/mullvad/check_test.go index 5c8e3b2..7ee89f7 100644 --- a/mullvad/check_test.go +++ b/mullvad/check_test.go @@ -2,26 +2,50 @@ package mullvad import ( "context" + "encoding/json" "testing" "time" "github.com/davecgh/go-spew/spew" + + "git.tcp.direct/kayos/mullsox/mulltest" ) +var tester = mulltest.Init() + func TestCheckIP4(t *testing.T) { - v4, err := CheckIP4() - if err != nil { - t.Fatalf("%s", err.Error()) - } - v4j, err4j := json.Marshal(v4) - if err4j != nil { - t.Fatalf("%s", err4j.Error()) - } - t.Logf(string(v4j)) + tester.SetOpIsMullvad() + + t.Run("is mullvad", func(t *testing.T) { + tester.SetIsMullvad() + v4, err := CheckIP4() + if err != nil { + t.Fatalf("%s", err.Error()) + } + v4j, err4j := json.Marshal(v4) + if err4j != nil { + t.Fatalf("%s", err4j.Error()) + } + t.Logf(string(v4j)) + }) + + t.Run("is not mullvad", func(t *testing.T) { + tester.SetIsNotMullvad() + v4, err := CheckIP4() + if err != nil { + t.Fatalf("%s", err.Error()) + } + v4j, err4j := json.Marshal(v4) + if err4j != nil { + t.Fatalf("%s", err4j.Error()) + } + t.Logf(string(v4j)) + }) } func TestCheckIP6(t *testing.T) { t.Skip("skipping ip6 check as mullvad seems to have broken it") + tester.SetOpIsMullvad() v6, err := CheckIP6() if err != nil { t.Fatalf("%s", err.Error()) @@ -35,6 +59,7 @@ func TestCheckIP6(t *testing.T) { func TestCheckIPConcurrent(t *testing.T) { t.Skip("skipping as ipv6 is broken on mullvad's end for the check") + tester.SetOpIsMullvad() ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(15*time.Second)) me, err := CheckIP(ctx) if err != nil { @@ -67,16 +92,39 @@ func TestCheckIPConcurrent(t *testing.T) { } func TestAmIMullvad(t *testing.T) { - servers := NewChecker() - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(15*time.Second)) - am, err := servers.AmIMullvad(ctx) - if err != nil { - t.Errorf("%s", err.Error()) - } - indented, err := json.MarshalIndent(am, "", " ") - if err != nil { - t.Fatalf("%s", err.Error()) - } - t.Logf(string(indented)) - cancel() + tester.SetOpIsMullvad() + + t.Run("is mullvad", func(t *testing.T) { + tester.SetIsMullvad() + servers := NewChecker() + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(15*time.Second)) + am, err := servers.AmIMullvad(ctx) + if err != nil { + t.Errorf("%s", err.Error()) + } + if err != nil { + t.Errorf("failed is mullvad check: %s", err.Error()) + } + if len(am) == 0 { + t.Errorf("expected non-zero length") + } + if len(am) > 0 && am[0].Hostname == "" { + t.Errorf("expected hostname to be set") + } + cancel() + }) + + t.Run("is not mullvad", func(t *testing.T) { + tester.SetIsNotMullvad() + servers := NewChecker() + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(15*time.Second)) + am, err := servers.AmIMullvad(ctx) + if err == nil { + t.Errorf("expected error, got nil") + } + if len(am) != 0 { + t.Errorf("expected zero length") + } + cancel() + }) } diff --git a/mullvad/relays.go b/mullvad/relays.go index 3dcc7b6..ea2176f 100644 --- a/mullvad/relays.go +++ b/mullvad/relays.go @@ -1,20 +1,23 @@ package mullvad import ( + "encoding/json" + "net/url" + "os" + "strings" "sync" - jsoniter "github.com/json-iterator/go" http "github.com/valyala/fasthttp" + + "git.tcp.direct/kayos/mullsox/mulltest" ) -var json = jsoniter.ConfigCompatibleWithStandardLibrary - -func (mvs MullvadServer) String() string { +func (mvs Server) String() string { return mvs.Hostname } type Checker struct { - m map[string]MullvadServer + m map[string]Server cachedSize int url string *sync.RWMutex @@ -22,19 +25,29 @@ type Checker struct { func NewChecker() *Checker { r := &Checker{ - m: make(map[string]MullvadServer), + m: make(map[string]Server), RWMutex: &sync.RWMutex{}, url: EndpointRelays, } + + if mulltest.TestModeEnabled() { + mt := mulltest.Init() + mt.SetOpRelays() + _, _ = os.Stderr.WriteString("running in test mode, using addr: " + mt.Addr + "\n") + r.url = mulltest.Init().Addr + if r.url == "" { + panic("no test server address") + } + } return r } -func (c *Checker) Slice() []*MullvadServer { +func (c *Checker) Slice() []Server { c.RLock() defer c.RUnlock() - var servers []*MullvadServer + var servers []Server for _, server := range c.m { - servers = append(servers, &server) + servers = append(servers, server) } return servers } @@ -46,16 +59,32 @@ func (c *Checker) Has(hostname string) bool { return ok } -func (c *Checker) Add(server MullvadServer) { +func (c *Checker) Add(server Server) { c.Lock() + key := server.Hostname + key = strings.ToLower(key) + if key == "" { + panic("empty hostname") + } c.m[server.Hostname] = server c.Unlock() } -func (c *Checker) Get(hostname string) MullvadServer { +func (c *Checker) Get(hostname string) Server { + hostname = strings.ToLower(hostname) + hostname = strings.TrimSpace(hostname) + found := c.Has(hostname) + if !found { + hostname = strings.Split(hostname, ".")[0] + found = c.Has(hostname) + } + if !found { + return Server{} + } c.RLock() - defer c.RUnlock() - return c.m[hostname] + srv, _ := c.m[hostname] + c.RUnlock() + return srv } func (c *Checker) clear() { @@ -81,7 +110,17 @@ func getContentSize(url string) int { } func (c *Checker) update() error { - var serverSlice []MullvadServer + if mulltest.TestModeEnabled() { + current := mulltest.Init().OpState() + defer mulltest.Init().StateOpMode.Store(current) + mulltest.Init().SetOpRelays() + if !strings.Contains(c.url, mulltest.Init().Addr) { + u, _ := url.Parse(c.url) + c.url = mulltest.Init().Addr + u.Path + } + } + + var serverSlice []Server if c.cachedSize > 0 { latestSize := getContentSize(c.url) if latestSize == c.cachedSize { @@ -91,6 +130,7 @@ func (c *Checker) update() error { req := http.AcquireRequest() res := http.AcquireResponse() + req.SetRequestURI(c.url) defer func() { http.ReleaseRequest(req) http.ReleaseResponse(res) @@ -98,7 +138,11 @@ func (c *Checker) update() error { req.Header.SetUserAgent(useragent) req.Header.SetContentType("application/json") req.Header.SetMethod(http.MethodGet) - req.SetRequestURI(c.url) + if mulltest.TestModeEnabled() { + mulltest.Init().SetOpRelays() + c.url = mulltest.Init().Addr + } + if err := http.Do(req, res); err != nil { return err } @@ -111,11 +155,12 @@ func (c *Checker) update() error { c.m[server.Hostname] = server } c.cachedSize = res.Header.ContentLength() + c.Unlock() return nil } -func (c *Checker) GetRelays() ([]*MullvadServer, error) { +func (c *Checker) GetRelays() ([]Server, error) { if err := c.update(); err != nil { return nil, err } diff --git a/mullvad/relays_test.go b/mullvad/relays_test.go index 6084451..d79ffaa 100644 --- a/mullvad/relays_test.go +++ b/mullvad/relays_test.go @@ -2,39 +2,47 @@ package mullvad import ( "testing" + + "github.com/davecgh/go-spew/spew" + + "git.tcp.direct/kayos/mullsox/mulltest" ) func TestGetMullvadServers(t *testing.T) { + mt := mulltest.Init() + mt.SetOpRelays() + mt.SetIsMullvad() servers := NewChecker() - update := func() { - err := servers.update() + update := func(srv *Checker) { + err := srv.update() if err != nil { t.Fatalf("%s", err.Error()) } - t.Logf("got %d servers", len(servers.Slice())) + t.Logf("got %d servers for uri %s", len(srv.Slice()), srv.url) } t.Run("GetMullvadServers", func(t *testing.T) { - update() - // t.Logf(spew.Sdump(servers.Slice())) + update(servers) + t.Log(spew.Sdump(servers.Slice())) }) var last int - var lastSlice []*MullvadServer + var lastSlice []Server t.Run("GetMullvadServersCached", func(t *testing.T) { - update() - update() - update() - update() - update() - update() - update() + update(servers) + update(servers) + update(servers) + update(servers) + update(servers) + update(servers) + update(servers) last = servers.cachedSize lastSlice = servers.Slice() }) t.Run("GetMullvadServersChanged", func(t *testing.T) { - servers.url = "https://api.mullvad.net/www/relays/openvpn/" - update() + servers.url = servers.url + "/openvpn/" + t.Logf("changing url to %s", servers.url) + update(servers) if last == servers.cachedSize { t.Fatalf("expected %d to not equal %d", last, servers.cachedSize) } diff --git a/sox.go b/sox.go index 9ea26ce..0fa7e7a 100644 --- a/sox.go +++ b/sox.go @@ -19,7 +19,7 @@ const MullvadInternalDNS6 = "[fc00:bbbb:bbbb:bb01::2b:e7d3]:53" */ type RelayFetcher interface { - GetRelays() ([]*mullvad.MullvadServer, error) + GetRelays() ([]mullvad.Server, error) } func GetSOCKS(fetcher RelayFetcher) ([]netip.AddrPort, error) { diff --git a/sox_test.go b/sox_test.go index 03d28de..4356727 100644 --- a/sox_test.go +++ b/sox_test.go @@ -3,10 +3,16 @@ package mullsox import ( "testing" + "git.tcp.direct/kayos/mullsox/mulltest" "git.tcp.direct/kayos/mullsox/mullvad" ) func TestChecker_GetSOCKS(t *testing.T) { + mt := mulltest.Init() + mt.SetOpRelays() + + t.Logf("test server: %s", mt.Addr) + c := mullvad.NewChecker() t.Run("GetSOCKS", func(t *testing.T) { gotSox, err := GetSOCKS(c)