Merge pull request #1 from yunginnanet/dev
This commit is contained in:
commit
360c08de49
17
.github/workflows/go.yml
vendored
Normal file
17
.github/workflows/go.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
name: test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
- name: test
|
||||
run: go test -v ./...
|
||||
|
@ -1,80 +0,0 @@
|
||||
package mullsox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func TestCheckIP6(t *testing.T) {
|
||||
v6, err := CheckIP6()
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
v6j, err6j := json.Marshal(v6)
|
||||
if err6j != nil {
|
||||
t.Fatalf("%s", err6j.Error())
|
||||
}
|
||||
t.Logf(string(v6j))
|
||||
}
|
||||
|
||||
func TestCheckIPConcurrent(t *testing.T) {
|
||||
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(15*time.Second))
|
||||
me, err := CheckIP(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
v4j, err4j := json.Marshal(me.V4)
|
||||
if err4j != nil {
|
||||
t.Fatalf("%s", err4j.Error())
|
||||
}
|
||||
v6j, err6j := json.Marshal(me.V6)
|
||||
if err6j != nil {
|
||||
t.Fatalf("%s", err6j.Error())
|
||||
}
|
||||
unmarshaled := &MyIPDetails{}
|
||||
unv4 := &IPDetails{}
|
||||
unv6 := &IPDetails{}
|
||||
|
||||
if err = json.Unmarshal(v4j, unv4); err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
if err = json.Unmarshal(v6j, unv6); err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
unmarshaled.V4 = unv4
|
||||
unmarshaled.V6 = unv6
|
||||
|
||||
t.Logf(spew.Sdump(unmarshaled.V4))
|
||||
t.Logf(spew.Sdump(unmarshaled.V6))
|
||||
cancel()
|
||||
}
|
||||
|
||||
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.Fatalf("%s", err.Error())
|
||||
}
|
||||
indented, err := json.MarshalIndent(am, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
t.Logf(string(indented))
|
||||
cancel()
|
||||
}
|
132
mulltest/server.go
Normal file
132
mulltest/server.go
Normal file
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
||||
package mullsox
|
||||
package mullvad
|
||||
|
||||
const useragent = "mullsox/0.0.1"
|
||||
|
||||
@ -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"`
|
@ -1,15 +1,22 @@
|
||||
package mullsox
|
||||
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")
|
||||
|
||||
type MyIPDetails struct {
|
||||
V4 *IPDetails `json:"ipv4,omitempty"`
|
||||
V6 *IPDetails `json:"ipv6,omitempty"`
|
||||
@ -29,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)
|
||||
|
||||
@ -93,13 +106,38 @@ func CheckIP(ctx context.Context) (*MyIPDetails, error) {
|
||||
return myip, err
|
||||
}
|
||||
|
||||
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(envV6) != "" {
|
||||
EnableIPv6 = true
|
||||
}
|
||||
}
|
||||
|
||||
func checkIP(ipv6 bool) (details *IPDetails, err error) {
|
||||
var target string
|
||||
switch ipv6 {
|
||||
case true:
|
||||
if !EnableIPv6 {
|
||||
return &IPDetails{}, nil
|
||||
}
|
||||
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()
|
||||
@ -130,39 +168,70 @@ 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)
|
||||
if me == nil || me.V4 == nil && me.V6 == nil || err != nil {
|
||||
return MullvadServer{}, err
|
||||
errs = append(errs, err)
|
||||
if me == nil || (me.V4 == nil && me.V6 == nil) {
|
||||
errs = append(errs, ErrNotMullvad)
|
||||
return []Server{}, errors.Join(errs...)
|
||||
}
|
||||
if me.V4 != nil && !me.V4.MullvadExitIP {
|
||||
return MullvadServer{}, err
|
||||
}
|
||||
if me.V6 != nil && !me.V6.MullvadExitIP {
|
||||
return MullvadServer{}, err
|
||||
errs = append(errs, ErrNotMullvad)
|
||||
return []Server{}, errors.Join(errs...)
|
||||
}
|
||||
// if me.V6 != nil && !me.V6.MullvadExitIP {
|
||||
// return []Server{}, err
|
||||
// }
|
||||
|
||||
err = c.update()
|
||||
|
||||
err = c.Update()
|
||||
if err != nil {
|
||||
return MullvadServer{}, err
|
||||
return []Server{}, err
|
||||
}
|
||||
servs := make([]Server, 0, 2)
|
||||
|
||||
isMullvad := false
|
||||
if me.V4 != nil && me.V4.MullvadExitIP {
|
||||
isMullvad = true
|
||||
if c.Has(me.V4.MullvadExitIPHostname) {
|
||||
return c.Get(me.V4.MullvadExitIPHostname), nil
|
||||
servs = append(servs, c.Get(me.V4.MullvadExitIPHostname))
|
||||
}
|
||||
}
|
||||
if me.V6 != nil && me.V6.MullvadExitIP {
|
||||
isMullvad = true
|
||||
if c.Has(me.V6.MullvadExitIPHostname) {
|
||||
return c.Get(me.V6.MullvadExitIPHostname), nil
|
||||
servs = append(servs, c.Get(me.V6.MullvadExitIPHostname))
|
||||
}
|
||||
}
|
||||
if isMullvad {
|
||||
return MullvadServer{},
|
||||
nils := 0
|
||||
for _, srv := range servs {
|
||||
if srv.Hostname == "" {
|
||||
nils++
|
||||
}
|
||||
}
|
||||
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")
|
||||
default:
|
||||
return servs, ErrNotMullvad
|
||||
}
|
||||
return MullvadServer{}, nil
|
||||
}
|
||||
|
||||
return servs, nil
|
||||
}
|
130
mullvad/check_test.go
Normal file
130
mullvad/check_test.go
Normal file
@ -0,0 +1,130 @@
|
||||
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) {
|
||||
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())
|
||||
}
|
||||
v6j, err6j := json.Marshal(v6)
|
||||
if err6j != nil {
|
||||
t.Fatalf("%s", err6j.Error())
|
||||
}
|
||||
t.Logf(string(v6j))
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
v4j, err4j := json.Marshal(me.V4)
|
||||
if err4j != nil {
|
||||
t.Fatalf("%s", err4j.Error())
|
||||
}
|
||||
v6j, err6j := json.Marshal(me.V6)
|
||||
if err6j != nil {
|
||||
t.Fatalf("%s", err6j.Error())
|
||||
}
|
||||
unmarshaled := &MyIPDetails{}
|
||||
unv4 := &IPDetails{}
|
||||
unv6 := &IPDetails{}
|
||||
|
||||
if err = json.Unmarshal(v4j, unv4); err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
if err = json.Unmarshal(v6j, unv6); err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
unmarshaled.V4 = unv4
|
||||
unmarshaled.V6 = unv6
|
||||
|
||||
t.Logf(spew.Sdump(unmarshaled.V4))
|
||||
t.Logf(spew.Sdump(unmarshaled.V6))
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestAmIMullvad(t *testing.T) {
|
||||
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()
|
||||
})
|
||||
}
|
@ -1,20 +1,23 @@
|
||||
package mullsox
|
||||
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,17 +25,27 @@ 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)
|
||||
}
|
||||
@ -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() {
|
||||
@ -80,8 +109,18 @@ func getContentSize(url string) int {
|
||||
return res.Header.ContentLength()
|
||||
}
|
||||
|
||||
func (c *Checker) Update() error {
|
||||
var serverSlice []MullvadServer
|
||||
func (c *Checker) update() error {
|
||||
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,6 +155,14 @@ func (c *Checker) Update() error {
|
||||
c.m[server.Hostname] = server
|
||||
}
|
||||
c.cachedSize = res.Header.ContentLength()
|
||||
|
||||
c.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Checker) GetRelays() ([]Server, error) {
|
||||
if err := c.update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.Slice(), nil
|
||||
}
|
@ -1,40 +1,48 @@
|
||||
package mullsox
|
||||
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)
|
||||
}
|
200
sox.go
200
sox.go
@ -2,92 +2,133 @@ package mullsox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.tcp.direct/kayos/mullsox/mulltest"
|
||||
"git.tcp.direct/kayos/mullsox/mullvad"
|
||||
)
|
||||
|
||||
func persistentResolver(hostname string) []netip.Addr {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
var ips []netip.Addr
|
||||
if hostname == "" {
|
||||
return ips
|
||||
}
|
||||
for n := 0; n < 5; n++ {
|
||||
var err error
|
||||
var res []netip.Addr
|
||||
go func() {
|
||||
res, err = net.DefaultResolver.LookupNetIP(ctx, "ip", hostname)
|
||||
if err == nil && res != nil && len(res) > 0 {
|
||||
ips = res
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
<-ctx.Done()
|
||||
return ips
|
||||
/*
|
||||
const MullvadInternalDNS4 = "10.64.0.1:53"
|
||||
const MullvadInternalDNS6 = "[fc00:bbbb:bbbb:bb01::2b:e7d3]:53"
|
||||
*/
|
||||
|
||||
type RelayFetcher interface {
|
||||
GetRelays() ([]mullvad.Server, error)
|
||||
}
|
||||
|
||||
func (c *Checker) GetSOCKS() (sox []netip.AddrPort, err error) {
|
||||
if err = c.Update(); err != nil {
|
||||
return
|
||||
func GetSOCKS(fetcher RelayFetcher) ([]netip.AddrPort, error) {
|
||||
relays, rerr := fetcher.GetRelays()
|
||||
switch {
|
||||
case rerr != nil:
|
||||
return nil, rerr
|
||||
case len(relays) == 0:
|
||||
return nil, fmt.Errorf("no relays found")
|
||||
default:
|
||||
}
|
||||
|
||||
var (
|
||||
done = make(chan struct{})
|
||||
errs = make(chan error, len(relays))
|
||||
multiErr error
|
||||
)
|
||||
|
||||
var tmpMap = make(map[netip.AddrPort]struct{})
|
||||
var tmpMapMu = &sync.RWMutex{}
|
||||
wg := &sync.WaitGroup{}
|
||||
for _, serv := range c.m {
|
||||
wg.Add(1)
|
||||
go func(endpoint *MullvadServer) {
|
||||
var resolved = make(chan netip.AddrPort, 1)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
wg.Add(len(relays))
|
||||
for _, serv := range relays {
|
||||
go func(host string, port int) {
|
||||
defer wg.Done()
|
||||
ips := persistentResolver(endpoint.SocksName)
|
||||
for _, ip := range ips {
|
||||
port := uint16(endpoint.SocksPort)
|
||||
ips, err := net.DefaultResolver.LookupIP(ctx, "ip", host)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return
|
||||
}
|
||||
for _, ipa := range ips {
|
||||
port := uint16(port)
|
||||
if port == 0 {
|
||||
port = 1080
|
||||
}
|
||||
ap := netip.AddrPortFrom(ip, port)
|
||||
tmpMapMu.RLock()
|
||||
_, ok := tmpMap[ap]
|
||||
if ap.IsValid() && ap.Port() > 0 && !ok {
|
||||
sox = append(sox, ap)
|
||||
tmpMapMu.RUnlock()
|
||||
tmpMapMu.Lock()
|
||||
tmpMap[ap] = struct{}{}
|
||||
tmpMapMu.Unlock()
|
||||
ip, err := netip.ParseAddr(ipa.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
tmpMapMu.RUnlock()
|
||||
if !ap.IsValid() {
|
||||
err = fmt.Errorf("invalid address/port combo: %s", ap.String())
|
||||
}
|
||||
}
|
||||
}(&serv)
|
||||
}
|
||||
wg.Wait()
|
||||
ap := netip.AddrPortFrom(ip, port)
|
||||
if ap.IsValid() && ap.Port() > 0 {
|
||||
resolved <- ap
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case !ap.IsValid():
|
||||
errs <- fmt.Errorf("invalid address/port combo: %s", ap.String())
|
||||
continue
|
||||
case ap.Port() == 0:
|
||||
errs <- fmt.Errorf("invalid port: %d", ap.Port())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}(serv.SocksName, serv.SocksPort)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
var sox = make([]netip.AddrPort, 0, len(relays))
|
||||
for {
|
||||
select {
|
||||
case ap := <-resolved:
|
||||
tmpMapMu.RLock()
|
||||
_, ok := tmpMap[ap]
|
||||
tmpMapMu.RUnlock()
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
tmpMapMu.Lock()
|
||||
tmpMap[ap] = struct{}{}
|
||||
sox = append(sox, ap)
|
||||
tmpMapMu.Unlock()
|
||||
case err := <-errs:
|
||||
multiErr = errors.Join(multiErr, err)
|
||||
case <-done:
|
||||
return sox, multiErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Checker) GetAndVerifySOCKS() (chan netip.AddrPort, chan error) {
|
||||
sox, err := c.GetSOCKS()
|
||||
var errs = make(chan error, len(sox)+1)
|
||||
var verified = make(chan netip.AddrPort, len(sox))
|
||||
if err != nil || len(sox) == 0 {
|
||||
errs <- err
|
||||
close(errs)
|
||||
return nil, errs
|
||||
func checker(candidate netip.AddrPort, verified chan netip.AddrPort, errs chan error, working *int64) {
|
||||
atomic.AddInt64(working, 1)
|
||||
defer func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
atomic.AddInt64(working, -1)
|
||||
}()
|
||||
if !candidate.IsValid() {
|
||||
errs <- fmt.Errorf("invalid address/port combo: %s", candidate.String())
|
||||
return
|
||||
}
|
||||
if mulltest.TestModeEnabled() {
|
||||
addruri := mulltest.Init().Addr
|
||||
if addruri == "" {
|
||||
panic("no test server address")
|
||||
}
|
||||
serv := strings.TrimSuffix(strings.Split(addruri, "http://")[1], "/")
|
||||
candidate = netip.MustParseAddrPort(serv)
|
||||
}
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(sox))
|
||||
for _, prx := range sox {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
go func(prx netip.AddrPort) {
|
||||
defer wg.Done()
|
||||
var conn net.Conn
|
||||
conn, err = net.DialTimeout("tcp", prx.String(), 10*time.Second)
|
||||
conn, err := net.DialTimeout("tcp", candidate.String(), 15*time.Second)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
}
|
||||
@ -95,12 +136,41 @@ func (c *Checker) GetAndVerifySOCKS() (chan netip.AddrPort, chan error) {
|
||||
_ = conn.Close()
|
||||
}
|
||||
if err == nil {
|
||||
verified <- prx
|
||||
verified <- candidate
|
||||
}
|
||||
}(prx)
|
||||
}
|
||||
|
||||
func GetAndVerifySOCKS(fetcher RelayFetcher) (chan netip.AddrPort, chan error) {
|
||||
sox, err := GetSOCKS(fetcher)
|
||||
var errs = make(chan error, len(sox)+1)
|
||||
switch {
|
||||
case len(sox) == 0:
|
||||
err = fmt.Errorf("no relays found")
|
||||
fallthrough
|
||||
case err != nil:
|
||||
go func() {
|
||||
errs <- err
|
||||
}()
|
||||
return nil, errs
|
||||
default:
|
||||
}
|
||||
|
||||
var (
|
||||
verified = make(chan netip.AddrPort, len(sox))
|
||||
working = new(int64)
|
||||
)
|
||||
atomic.StoreInt64(working, 0)
|
||||
|
||||
for _, prx := range sox {
|
||||
for atomic.LoadInt64(working) > 10 {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
checker(prx, verified, errs, working)
|
||||
}
|
||||
go func() {
|
||||
wg.Wait()
|
||||
for atomic.LoadInt64(working) > 0 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
close(errs)
|
||||
close(verified)
|
||||
}()
|
||||
|
20
sox_test.go
20
sox_test.go
@ -2,15 +2,20 @@ package mullsox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.tcp.direct/kayos/mullsox/mulltest"
|
||||
"git.tcp.direct/kayos/mullsox/mullvad"
|
||||
)
|
||||
|
||||
func TestChecker_GetSOCKS(t *testing.T) {
|
||||
c := NewChecker()
|
||||
mt := mulltest.Init()
|
||||
mt.SetOpRelays()
|
||||
|
||||
t.Logf("test server: %s", mt.Addr)
|
||||
|
||||
c := mullvad.NewChecker()
|
||||
t.Run("GetSOCKS", func(t *testing.T) {
|
||||
if err := c.Update(); err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
gotSox, err := c.GetSOCKS()
|
||||
gotSox, err := GetSOCKS(c)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -22,10 +27,7 @@ func TestChecker_GetSOCKS(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("GetAndVerifySOCKS", func(t *testing.T) {
|
||||
if err := c.Update(); err != nil {
|
||||
t.Fatalf("%s", err.Error())
|
||||
}
|
||||
gotSox, errs := c.GetAndVerifySOCKS()
|
||||
gotSox, errs := GetAndVerifySOCKS(c)
|
||||
count := 0
|
||||
for sox := range gotSox {
|
||||
select {
|
||||
|
Loading…
Reference in New Issue
Block a user