├── output.png ├── .gitignore ├── main_pcap.go ├── utils ├── must.go ├── limit.go ├── collect2slice.go ├── ttlset_test.go └── ttlset.go ├── main.go ├── scan ├── geoip │ ├── geoip_test.go │ ├── geoip.go │ ├── cf.go │ ├── ipsb.go │ └── ipwho.go ├── socks5 │ ├── socks5_test.go │ └── socks5.go ├── tcpscanner │ ├── scanner.go │ ├── pcap │ │ ├── physical_windows_test.go │ │ ├── physical.go │ │ ├── iphelpapi_windows.go │ │ ├── types64_windows.go │ │ ├── physical_windows.go │ │ └── pcap.go │ └── system │ │ └── system.go └── scan.go ├── README.md ├── pool ├── pool.go └── worker.go ├── convert ├── clash_test.go └── clash.go ├── go.mod ├── cmd └── cli │ ├── report.go │ └── cli.go ├── proxy ├── report.go └── proxy.go └── go.sum /output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dn-11/proxyScan/HEAD/output.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | proxies.yaml 3 | proxy_test_results.txt 4 | *proxyScan* -------------------------------------------------------------------------------- /main_pcap.go: -------------------------------------------------------------------------------- 1 | //go:build !nopcap 2 | 3 | package main 4 | 5 | import _ "github.com/dn-11/proxyScan/scan/tcpscanner/pcap" 6 | -------------------------------------------------------------------------------- /utils/must.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func Must[T any](res T, err error) T { 4 | if err != nil { 5 | panic(err) 6 | } 7 | return res 8 | } 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dn-11/proxyScan/cmd/cli" 5 | 6 | // import scanner plugins 7 | _ "github.com/dn-11/proxyScan/scan/tcpscanner/system" 8 | ) 9 | 10 | func main() { 11 | cli.Cli() 12 | } 13 | -------------------------------------------------------------------------------- /scan/geoip/geoip_test.go: -------------------------------------------------------------------------------- 1 | package geoip 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func TestGetGeo(t *testing.T) { 9 | geo, err := GetGeo("127.0.0.1:7890") 10 | if err != nil { 11 | t.Error(err) 12 | return 13 | } 14 | t.Log(geo) 15 | } 16 | 17 | func TestEndpoint(t *testing.T) { 18 | for i, f := range tryOrder { 19 | res := f(http.DefaultClient) 20 | if res != nil { 21 | t.Log(res) 22 | } else { 23 | t.Logf("No geoip found in %d", i) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /utils/limit.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "golang.org/x/time/rate" 5 | "log" 6 | "math" 7 | "time" 8 | ) 9 | 10 | func ParseLimiter(r int) *rate.Limiter { 11 | var interval rate.Limit 12 | var b int 13 | if r <= 0 { 14 | log.Printf("rate unlimited mode") 15 | interval = rate.Inf 16 | b = math.MaxInt 17 | } else { 18 | log.Printf("rate %d/s", r) 19 | interval = rate.Every(time.Second / time.Duration(r)) 20 | b = max(r/200, 1) 21 | } 22 | 23 | return rate.NewLimiter(interval, b) 24 | } 25 | -------------------------------------------------------------------------------- /scan/socks5/socks5_test.go: -------------------------------------------------------------------------------- 1 | package socks5 2 | 3 | import ( 4 | "github.com/txthinking/socks5" 5 | "net/netip" 6 | "testing" 7 | ) 8 | 9 | func TestGetInfo(t *testing.T) { 10 | res := GetInfo(netip.MustParseAddrPort("172.16.4.6:18080")) 11 | t.Log(*res) 12 | } 13 | 14 | func TestRawDNS(t *testing.T) { 15 | //ExampleServer() 16 | c, err := socks5.NewClient("127.0.0.1:7890", "", "", 10, 10) 17 | if err != nil { 18 | t.Error(err) 19 | return 20 | } 21 | err = testUDPByDNS(c) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /utils/collect2slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type Collector[T any] struct { 4 | C chan T 5 | slice []T 6 | done chan struct{} 7 | } 8 | 9 | func NewCollector[T any]() *Collector[T] { 10 | c := &Collector[T]{ 11 | C: make(chan T, 16), 12 | done: make(chan struct{}), 13 | } 14 | go func() { 15 | for v := range c.C { 16 | c.slice = append(c.slice, v) 17 | } 18 | c.done <- struct{}{} 19 | }() 20 | return c 21 | } 22 | 23 | func (c *Collector[T]) Return() []T { 24 | close(c.C) 25 | <-c.done 26 | return c.slice 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProxyScan 2 | 3 | 很多配网小子喜欢在路由器上配代理,同时把 WAN 方向的访问打开了,尤其是在高校网段。 4 | 5 | 因此编写了这样一个工具来警醒各位正确配置防火墙的重要性。 6 | 7 | ![output.png](./output.png) 8 | 9 | # Start 10 | 11 | ```shell 12 | go install github.com/dn-11/proxyScan@latest 13 | sudo proxyScan -prefix 0.0.0.0/0 -pcap -report 14 | ``` 15 | 16 | `-pcap` 模式需要 root 权限。`-report` 选项会连接到扫描出的代理,做一下对 `cloudflare` 的测速和不同方向的 `ip` 出口测试。 17 | 18 | 建议在 Linux 上运行,效率比 windows 上高十倍起码。 19 | 20 | # 兼容性 21 | 22 | **NO WINDOWS XP OR EARLIER VERSIONS SUPPORTED** 23 | 24 | 不支持 Windows XP 及更早版本 25 | 26 | # 测速 27 | 28 | 测速基准:腾讯云上海2c2g SA5 100Mbps pcap 模式 无速率限制 每个 IP 扫 9 个常用端口 29 | 30 | 由于长时间跑会被限速,因此跑 /12 直接换算 /0 时间,约为 54 小时 31 | -------------------------------------------------------------------------------- /pool/pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | type Pool struct { 4 | Size int 5 | Buffer int 6 | tasks chan func() 7 | workers []*Worker 8 | } 9 | 10 | func NewDefaultPool() *Pool { 11 | p := &Pool{Size: 128, Buffer: 1024} 12 | p.Init() 13 | return p 14 | } 15 | 16 | func (p *Pool) Init() { 17 | p.tasks = make(chan func(), p.Buffer) 18 | p.workers = make([]*Worker, p.Size) 19 | for i := 0; i < p.Size; i++ { 20 | p.workers[i] = NewWorker(p.tasks) 21 | go p.workers[i].Run() 22 | } 23 | } 24 | 25 | func (p *Pool) Submit(f func()) { 26 | p.tasks <- f 27 | } 28 | 29 | func (p *Pool) Close() { 30 | for _, w := range p.workers { 31 | w.Cancel() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /scan/tcpscanner/scanner.go: -------------------------------------------------------------------------------- 1 | package tcpscanner 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/netip" 7 | ) 8 | 9 | var list = make(map[string]func(ctx context.Context, rate int) (Scanner, error)) 10 | 11 | func Register(name string, f func(ctx context.Context, rate int) (Scanner, error)) { 12 | list[name] = f 13 | } 14 | 15 | var ErrScannerNotFound = errors.New("scanner not found") 16 | 17 | func Get(name string, ctx context.Context, rate int) (Scanner, error) { 18 | if f, ok := list[name]; ok { 19 | return f(ctx, rate) 20 | } 21 | return nil, ErrScannerNotFound 22 | } 23 | 24 | type Scanner interface { 25 | Alive() chan netip.AddrPort 26 | Send(netip.AddrPort) 27 | End() 28 | } 29 | -------------------------------------------------------------------------------- /pool/worker.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "context" 5 | "log" 6 | ) 7 | 8 | type Worker struct { 9 | ctx context.Context 10 | cancel context.CancelFunc 11 | 12 | Func chan func() 13 | } 14 | 15 | func NewWorker(f chan func()) *Worker { 16 | w := &Worker{Func: f} 17 | w.ctx, w.cancel = context.WithCancel(context.Background()) 18 | return w 19 | } 20 | 21 | func (w *Worker) Run() { 22 | defer func() { 23 | if r := recover(); r != nil { 24 | log.Println("Recovered ", r) 25 | } 26 | }() 27 | 28 | for { 29 | select { 30 | case f := <-w.Func: 31 | if f == nil { 32 | return 33 | } 34 | f() 35 | case <-w.ctx.Done(): 36 | return 37 | } 38 | } 39 | } 40 | 41 | func (w *Worker) Cancel() { 42 | w.cancel() 43 | } 44 | -------------------------------------------------------------------------------- /convert/clash_test.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "bytes" 5 | "github.com/dn-11/proxyScan/scan/socks5" 6 | "net/netip" 7 | "testing" 8 | ) 9 | 10 | func TestClashTmpl(t *testing.T) { 11 | var buf bytes.Buffer 12 | err := clashTmpl.Execute(&buf, &socks5.Result{ 13 | AddrPort: netip.MustParseAddrPort("127.0.0.1:7890"), 14 | Success: true, 15 | UDP: true, 16 | }) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | t.Log(buf.String()) 21 | } 22 | 23 | func TestClashTmpl2(t *testing.T) { 24 | var buf bytes.Buffer 25 | err := clashTmpl.Execute(&buf, &socks5.Result{ 26 | AddrPort: netip.MustParseAddrPort("127.0.0.1:1"), 27 | Success: true, 28 | UDP: true, 29 | }) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | t.Log(buf.String()) 34 | } 35 | -------------------------------------------------------------------------------- /scan/tcpscanner/pcap/physical_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package pcap 4 | 5 | import ( 6 | "github.com/libp2p/go-netroute" 7 | "net" 8 | "testing" 9 | ) 10 | 11 | func TestResolve(t *testing.T) { 12 | c, _ := netroute.New() 13 | iface, gw, _, err := c.Route(net.ParseIP("1.1.1.1")) 14 | if err != nil { 15 | t.Error(err) 16 | return 17 | } 18 | address, err := resolveHardwareAddress(iface, gw) 19 | if err != nil { 20 | t.Error(err) 21 | return 22 | } 23 | t.Log(address) 24 | } 25 | 26 | func TestOpenLive(t *testing.T) { 27 | c, _ := netroute.New() 28 | iface, _, _, err := c.Route(net.ParseIP("1.1.1.1")) 29 | if err != nil { 30 | t.Error(err) 31 | return 32 | } 33 | _, err = openLive(iface) 34 | if err != nil { 35 | t.Error(err) 36 | return 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scan/tcpscanner/pcap/physical.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package pcap 4 | 5 | import ( 6 | "fmt" 7 | "github.com/mdlayher/arp" 8 | "github.com/yaklang/pcap" 9 | "net" 10 | "net/netip" 11 | ) 12 | 13 | func resolveHardwareAddress(iface *net.Interface, addr net.IP) (net.HardwareAddr, error) { 14 | arpc, err := arp.Dial(iface) 15 | if err != nil { 16 | return nil, fmt.Errorf("arp dial: %v", err) 17 | } 18 | dstmac, err := arpc.Resolve(netip.MustParseAddr(addr.String())) 19 | if err != nil { 20 | return nil, fmt.Errorf("arp resolve: %v", err) 21 | } 22 | return dstmac, nil 23 | } 24 | 25 | func openLive(iface *net.Interface) (*pcap.Handle, error) { 26 | fmt.Printf("open live: %v\n", iface.Name) 27 | handle, err := pcap.OpenLive(iface.Name, 1600, true, pcap.BlockForever) 28 | if err != nil { 29 | return nil, fmt.Errorf("open live: %v", err) 30 | } 31 | return handle, nil 32 | } 33 | -------------------------------------------------------------------------------- /scan/geoip/geoip.go: -------------------------------------------------------------------------------- 1 | package geoip 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/net/proxy" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type GeoIP struct { 11 | City string 12 | Country string 13 | ASOrg string 14 | } 15 | 16 | const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0" 17 | 18 | var ( 19 | TestTimeout = time.Second * 5 20 | ) 21 | 22 | var tryOrder = []func(*http.Client) *GeoIP{ 23 | CloudFlare, IPsb, ipWho, 24 | } 25 | 26 | func GetGeo(addrPort string) (*GeoIP, error) { 27 | dialer, err := proxy.SOCKS5("tcp", addrPort, nil, proxy.Direct) 28 | if err != nil { 29 | return nil, err 30 | } 31 | dialerCtx, ok := dialer.(proxy.ContextDialer) 32 | if !ok { 33 | return nil, err 34 | } 35 | c := &http.Client{ 36 | Transport: &http.Transport{ 37 | DialContext: dialerCtx.DialContext, 38 | }, 39 | Timeout: TestTimeout, 40 | } 41 | 42 | for _, f := range tryOrder { 43 | if geo := f(c); geo != nil { 44 | return geo, nil 45 | } 46 | } 47 | 48 | return nil, fmt.Errorf("no geoip found") 49 | } 50 | -------------------------------------------------------------------------------- /utils/ttlset_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestTTLSet1(t *testing.T) { 10 | set := NewTTLSet[int](time.Second) 11 | set.Add(1) 12 | set.Add(2) 13 | assert.Equal(t, true, set.Exist(1)) 14 | time.Sleep(time.Second) 15 | assert.Equal(t, false, set.Exist(1)) 16 | set.Add(1) 17 | set.Add(2) 18 | wait := make(chan struct{}) 19 | go func() { 20 | t1 := time.Now() 21 | set.Wait() 22 | dur := time.Since(t1) 23 | assert.Equal(t, true, dur > time.Duration(float64(time.Second)*1.4)) 24 | wait <- struct{}{} 25 | }() 26 | time.Sleep(time.Second / 2) 27 | set.Add(3) 28 | <-wait 29 | } 30 | func TestTTLSet2(t *testing.T) { 31 | set := NewTTLSet[int](time.Second) 32 | go func() { 33 | for i := 0; i < 1000; i++ { 34 | set.Add(i) 35 | time.Sleep(time.Millisecond) 36 | } 37 | }() 38 | go func() { 39 | for i := 0; i < 1000; i++ { 40 | set.Exist(i) 41 | time.Sleep(time.Millisecond) 42 | } 43 | }() 44 | begin := time.Now() 45 | time.Sleep(time.Second / 2) 46 | set.Wait() 47 | t.Log(time.Since(begin)) 48 | } 49 | -------------------------------------------------------------------------------- /scan/geoip/cf.go: -------------------------------------------------------------------------------- 1 | package geoip 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | type cloudFlareResp struct { 12 | Ip string `json:"ip"` 13 | City string `json:"city"` 14 | Country string `json:"country"` 15 | Flag string `json:"flag"` 16 | CountryRegion string `json:"countryRegion"` 17 | Region string `json:"region"` 18 | Latitude string `json:"latitude"` 19 | Longitude string `json:"longitude"` 20 | AsOrganization string `json:"asOrganization"` 21 | } 22 | 23 | func CloudFlare(c *http.Client) *GeoIP { 24 | req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://cloudflare-ip.html.zone/geo?_t=%d", time.Now().UnixMilli()), nil) 25 | if err != nil { 26 | log.Printf("cloudflare geo request: %v", err) 27 | return nil 28 | } 29 | req.Header.Set("User-Agent", userAgent) 30 | resp, err := c.Do(req) 31 | if err != nil { 32 | log.Printf("cloudflare do request: %v", err) 33 | return nil 34 | } 35 | 36 | var cfResp cloudFlareResp 37 | if err := json.NewDecoder(resp.Body).Decode(&cfResp); err != nil { 38 | log.Printf("cloudflare decode response: %v", err) 39 | return nil 40 | } 41 | 42 | return &GeoIP{ 43 | City: cfResp.City, 44 | Country: cfResp.Country, 45 | ASOrg: cfResp.AsOrganization, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dn-11/proxyScan 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/google/gopacket v1.1.19 7 | github.com/libp2p/go-netroute v0.2.1 8 | github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875 9 | github.com/miekg/dns v1.1.51 10 | github.com/stretchr/testify v1.9.0 11 | github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 12 | github.com/yaklang/pcap v1.0.3 13 | golang.org/x/net v0.25.0 14 | golang.org/x/sys v0.20.0 15 | golang.org/x/text v0.15.0 16 | golang.org/x/time v0.5.0 17 | gopkg.in/yaml.v3 v3.0.1 18 | ) 19 | 20 | require ( 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/google/go-cmp v0.5.9 // indirect 23 | github.com/josharian/native v1.0.0 // indirect 24 | github.com/kr/pretty v0.1.0 // indirect 25 | github.com/kr/text v0.2.0 // indirect 26 | github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 // indirect 27 | github.com/mdlayher/packet v1.0.0 // indirect 28 | github.com/mdlayher/socket v0.2.1 // indirect 29 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect 32 | golang.org/x/mod v0.13.0 // indirect 33 | golang.org/x/sync v0.5.0 // indirect 34 | golang.org/x/tools v0.14.0 // indirect 35 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /convert/clash.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "github.com/dn-11/proxyScan/scan/geoip" 8 | "github.com/dn-11/proxyScan/scan/socks5" 9 | "text/template" 10 | ) 11 | 12 | type ClashSocks5Proxy struct { 13 | Name string `yaml:"name"` 14 | Type string `yaml:"type"` 15 | Server string `yaml:"server"` 16 | Port int `yaml:"port"` 17 | Udp bool `yaml:"udp"` 18 | } 19 | 20 | var ErrInvalidSocks5Result = errors.New("invalid input") 21 | 22 | var clashTmpl = template.Must(template.New("clash").Funcs(template.FuncMap{ 23 | "geo": func(addrPort string) *geoip.GeoIP { 24 | pos, err := geoip.GetGeo(addrPort) 25 | if err != nil { 26 | return nil 27 | } 28 | return pos 29 | }, 30 | }).Parse(`{{ $pos := geo .AddrPort.String }} 31 | {{- with $pos -}} 32 | [{{ .Country }}{{ if and (ne "" .Country) (ne "" .City) }}-{{ end }}{{ .City }}] 33 | {{- $pos.ASOrg }}({{$.AddrPort}}) 34 | {{- else -}} 35 | [Unknown]{{ .AddrPort }} 36 | {{- end }}`)) 37 | 38 | func ToClash(res *socks5.Result) *ClashSocks5Proxy { 39 | var ( 40 | buf bytes.Buffer 41 | name string 42 | ) 43 | if err := clashTmpl.Execute(&buf, res); err != nil { 44 | name = fmt.Sprintf("[Unknown]%s", res.AddrPort.String()) 45 | } else { 46 | name = buf.String() 47 | } 48 | 49 | return &ClashSocks5Proxy{ 50 | Name: name, 51 | Type: "socks5", 52 | Server: res.AddrPort.Addr().String(), 53 | Port: int(res.AddrPort.Port()), 54 | Udp: res.UDP, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /scan/tcpscanner/system/system.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "context" 5 | "github.com/dn-11/proxyScan/scan/tcpscanner" 6 | "github.com/dn-11/proxyScan/utils" 7 | "golang.org/x/time/rate" 8 | "log" 9 | "net" 10 | "net/netip" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | func init() { 16 | tcpscanner.Register("system", NewScanner) 17 | } 18 | 19 | const WaitTimeout = 2 * time.Second 20 | 21 | type Scanner struct { 22 | alive chan netip.AddrPort 23 | end bool 24 | 25 | ctx context.Context 26 | limiter *rate.Limiter 27 | wg sync.WaitGroup 28 | } 29 | 30 | func (c *Scanner) Alive() chan netip.AddrPort { 31 | return c.alive 32 | } 33 | 34 | func (c *Scanner) Send(addrPort netip.AddrPort) { 35 | if c.end { 36 | return 37 | } 38 | err := c.limiter.Wait(c.ctx) 39 | if err != nil { 40 | log.Printf("limiter wait: %v", err) 41 | return 42 | } 43 | c.wg.Add(1) 44 | go func() { 45 | defer c.wg.Done() 46 | conn, err := net.DialTimeout("tcp", addrPort.String(), WaitTimeout) 47 | if err != nil { 48 | return 49 | } 50 | conn.Close() 51 | c.alive <- addrPort 52 | }() 53 | } 54 | 55 | func (c *Scanner) End() { 56 | c.end = true 57 | c.wg.Wait() 58 | close(c.alive) 59 | } 60 | 61 | func NewScanner(ctx context.Context, rate int) (tcpscanner.Scanner, error) { 62 | return &Scanner{ 63 | alive: make(chan netip.AddrPort, 1024), 64 | end: false, 65 | limiter: utils.ParseLimiter(rate), 66 | ctx: ctx, 67 | }, nil 68 | } 69 | 70 | var _ tcpscanner.Scanner = (*Scanner)(nil) 71 | -------------------------------------------------------------------------------- /scan/geoip/ipsb.go: -------------------------------------------------------------------------------- 1 | package geoip 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | type ipsbResp struct { 10 | Organization string `json:"organization"` 11 | Longitude float64 `json:"longitude"` 12 | City string `json:"city"` 13 | Timezone string `json:"timezone"` 14 | Isp string `json:"isp"` 15 | Offset int `json:"offset"` 16 | Region string `json:"region"` 17 | Asn int `json:"asn"` 18 | AsnOrganization string `json:"asn_organization"` 19 | Country string `json:"country"` 20 | Ip string `json:"ip"` 21 | Latitude float64 `json:"latitude"` 22 | PostalCode string `json:"postal_code"` 23 | ContinentCode string `json:"continent_code"` 24 | CountryCode string `json:"country_code"` 25 | RegionCode string `json:"region_code"` 26 | } 27 | 28 | func IPsb(c *http.Client) *GeoIP { 29 | req, err := http.NewRequest(http.MethodGet, "https://api-ipv4.ip.sb/geoip", nil) 30 | if err != nil { 31 | log.Printf("ip.sb geo request: %v", err) 32 | return nil 33 | } 34 | 35 | req.Header.Set("User-Agent", userAgent) 36 | resp, err := c.Do(req) 37 | if err != nil { 38 | log.Printf("ip.sb do request: %v", err) 39 | return nil 40 | } 41 | 42 | var ipResp ipsbResp 43 | if err := json.NewDecoder(resp.Body).Decode(&ipResp); err != nil { 44 | log.Printf("ip.sb decode response: %v", err) 45 | return nil 46 | } 47 | 48 | return &GeoIP{ 49 | City: ipResp.City, 50 | Country: ipResp.Country, 51 | ASOrg: ipResp.AsnOrganization, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cmd/cli/report.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/dn-11/proxyScan/proxy" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | type ProxyConfig struct { 13 | Proxies []struct { 14 | Name string `yaml:"name"` 15 | Type string `yaml:"type"` 16 | Server string `yaml:"server"` 17 | Port int `yaml:"port"` 18 | Username string `yaml:"username,omitempty"` 19 | Password string `yaml:"password,omitempty"` 20 | } `yaml:"proxies"` 21 | } 22 | 23 | func GenerateReport() { 24 | // Read proxy list from scan results 25 | configFile := "proxies.yaml" 26 | data, err := os.ReadFile(configFile) 27 | if err != nil { 28 | log.Fatalf("Failed to read config file: %v", err) 29 | } 30 | 31 | var config ProxyConfig 32 | if err := yaml.Unmarshal(data, &config); err != nil { 33 | log.Fatalf("Failed to parse config file: %v", err) 34 | } 35 | 36 | // Build proxy address list 37 | var proxies []string 38 | for _, p := range config.Proxies { 39 | proxyAddr := fmt.Sprintf("%s:%d", p.Server, p.Port) 40 | proxies = append(proxies, proxyAddr) 41 | } 42 | 43 | if len(proxies) == 0 { 44 | log.Println("No available proxies found") 45 | return 46 | } 47 | 48 | // Create proxy tester 49 | tester := proxy.NewProxyTester(nil, proxies) 50 | 51 | // Run tests 52 | log.Println("Starting proxy tests...") 53 | results := tester.Run() 54 | 55 | // Generate report 56 | report := proxy.NewReport(results) 57 | if err := report.GenerateTXT("proxy_test_results.txt"); err != nil { 58 | log.Fatalf("Failed to generate report: %v", err) 59 | } 60 | 61 | log.Println("Proxy testing completed, results saved to proxy_test_results.txt") 62 | } 63 | -------------------------------------------------------------------------------- /scan/tcpscanner/pcap/iphelpapi_windows.go: -------------------------------------------------------------------------------- 1 | package pcap 2 | 3 | import ( 4 | "golang.org/x/sys/windows" 5 | "reflect" 6 | "unsafe" 7 | ) 8 | 9 | var ( 10 | modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") 11 | 12 | procGetIpNetTable = modiphlpapi.NewProc("GetIpNetTable") 13 | procGetIfTable2 = modiphlpapi.NewProc("GetIfTable2") 14 | procFreeMibTable = modiphlpapi.NewProc("FreeMibTable") 15 | ) 16 | 17 | func getIpNetTable(ipNetTable PMIB_IPNETTABLE, sizePoint *uint32, order bool) error { 18 | r1, _, lastErr := procGetIpNetTable.Call(convertAddr(ipNetTable), convertAddr(sizePoint), convertAddr(&order)) 19 | if r1 != 0 { 20 | return lastErr 21 | } 22 | return nil 23 | } 24 | 25 | func getIfTable2(ifTable *PMIB_IF_TABLE2) error { 26 | r1, _, lastErr := procGetIfTable2.Call(convertAddr(ifTable)) 27 | if r1 != 0 { 28 | return lastErr 29 | } 30 | return nil 31 | } 32 | 33 | func freeMibTable(ifTable PMIB_IF_TABLE2) error { 34 | _, _, err := procFreeMibTable.Call(convertAddr(ifTable)) 35 | return err 36 | } 37 | 38 | func convertAddr[T any](in *T) uintptr { 39 | return uintptr(unsafe.Pointer(in)) 40 | } 41 | 42 | func forceConvert[T any, V any](ptr *V) *T { 43 | return (*T)(unsafe.Pointer(ptr)) 44 | } 45 | 46 | // convert wchar to []byte 47 | // input MUST be point to sized byte Array 48 | // example: *[111]byte 49 | func wchar2bytes[T any](wchar *T) []byte { 50 | t := reflect.TypeFor[T]() 51 | if t.Kind() != reflect.Array || t.Elem().Kind() != reflect.Uint16 { 52 | panic("wchar2bytes: need array of uint16") 53 | } 54 | hdr := reflect.SliceHeader{ 55 | Data: uintptr(unsafe.Pointer(wchar)), 56 | Len: int(t.Size() * 2), 57 | Cap: int(t.Size() * 2), 58 | } 59 | s := *(*[]byte)(unsafe.Pointer(&hdr)) 60 | for i := 0; i < len(s); i += 2 { 61 | if s[i] == 0 && s[i+1] == 0 { 62 | return s[:i] 63 | } 64 | } 65 | return s 66 | } 67 | -------------------------------------------------------------------------------- /utils/ttlset.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | // TTLSet a set that can set ttl 10 | // Attention: this is not a thread-safe set 11 | type TTLSet[K comparable] struct { 12 | old map[K]time.Time 13 | m map[K]time.Time 14 | 15 | expireAll atomic.Pointer[time.Time] 16 | ttl time.Duration 17 | t *time.Timer 18 | lock sync.Mutex 19 | } 20 | 21 | func NewTTLSet[K comparable](ttl time.Duration) *TTLSet[K] { 22 | s := &TTLSet[K]{ 23 | old: make(map[K]time.Time), 24 | m: make(map[K]time.Time), 25 | ttl: ttl, 26 | t: time.NewTimer(ttl), 27 | } 28 | now := new(time.Time) 29 | *now = time.Now() 30 | s.expireAll.Store(now) 31 | return s 32 | } 33 | 34 | func (s *TTLSet[K]) Add(k K) { 35 | s.checkRotate(true) 36 | 37 | expire := time.Now().Add(s.ttl) 38 | s.updateExpireAll(expire) 39 | 40 | s.lock.Lock() 41 | defer s.lock.Unlock() 42 | s.m[k] = expire 43 | } 44 | 45 | func (s *TTLSet[K]) Exist(k K) bool { 46 | s.checkRotate(true) 47 | 48 | entry, ok := s.old[k] 49 | if ok && entry.After(time.Now()) { 50 | return true 51 | } 52 | 53 | s.lock.Lock() 54 | defer s.lock.Unlock() 55 | entry, ok = s.m[k] 56 | return ok && entry.After(time.Now()) 57 | } 58 | 59 | // Wait all key to be expired 60 | func (s *TTLSet[K]) Wait() { 61 | s.t.Stop() 62 | s.checkRotate(false) 63 | 64 | for { 65 | offset := s.expireAll.Load().Sub(time.Now()) 66 | if offset <= 0 { 67 | break 68 | } 69 | time.Sleep(offset) 70 | } 71 | } 72 | 73 | func (s *TTLSet[K]) checkRotate(rotate bool) { 74 | select { 75 | case <-s.t.C: 76 | if rotate { 77 | s.old = s.m 78 | s.m = make(map[K]time.Time) 79 | } 80 | default: 81 | } 82 | } 83 | 84 | func (s *TTLSet[K]) updateExpireAll(t time.Time) { 85 | for { 86 | old := s.expireAll.Load() 87 | if old.After(t) { 88 | return 89 | } 90 | if s.expireAll.CompareAndSwap(old, &t) { 91 | return 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /scan/geoip/ipwho.go: -------------------------------------------------------------------------------- 1 | package geoip 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type ipWhoResp struct { 11 | Ip string `json:"ip"` 12 | Success bool `json:"success"` 13 | Type string `json:"type"` 14 | Continent string `json:"continent"` 15 | ContinentCode string `json:"continent_code"` 16 | Country string `json:"country"` 17 | CountryCode string `json:"country_code"` 18 | Region string `json:"region"` 19 | RegionCode string `json:"region_code"` 20 | City string `json:"city"` 21 | Latitude float64 `json:"latitude"` 22 | Longitude float64 `json:"longitude"` 23 | IsEu bool `json:"is_eu"` 24 | Postal string `json:"postal"` 25 | CallingCode string `json:"calling_code"` 26 | Capital string `json:"capital"` 27 | Borders string `json:"borders"` 28 | Flag struct { 29 | Img string `json:"img"` 30 | Emoji string `json:"emoji"` 31 | EmojiUnicode string `json:"emoji_unicode"` 32 | } `json:"flag"` 33 | Connection struct { 34 | Asn int `json:"asn"` 35 | Org string `json:"org"` 36 | Isp string `json:"isp"` 37 | Domain string `json:"domain"` 38 | } `json:"connection"` 39 | Timezone struct { 40 | Id string `json:"id"` 41 | Abbr string `json:"abbr"` 42 | IsDst bool `json:"is_dst"` 43 | Offset int `json:"offset"` 44 | Utc string `json:"utc"` 45 | CurrentTime time.Time `json:"current_time"` 46 | } `json:"timezone"` 47 | } 48 | 49 | func ipWho(c *http.Client) *GeoIP { 50 | req, err := http.NewRequest(http.MethodGet, "https://ipwho.is/", nil) 51 | if err != nil { 52 | log.Printf("ipwho geo request: %v", err) 53 | return nil 54 | } 55 | 56 | req.Header.Set("User-Agent", userAgent) 57 | resp, err := c.Do(req) 58 | if err != nil { 59 | log.Printf("ipwho do request: %v", err) 60 | return nil 61 | } 62 | 63 | var ipResp ipWhoResp 64 | if err := json.NewDecoder(resp.Body).Decode(&ipResp); err != nil { 65 | log.Printf("ipwho decode response: %v", err) 66 | return nil 67 | } 68 | 69 | return &GeoIP{ 70 | City: ipResp.City, 71 | Country: ipResp.Country, 72 | ASOrg: ipResp.Connection.Org, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /scan/socks5/socks5.go: -------------------------------------------------------------------------------- 1 | package socks5 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/miekg/dns" 7 | "github.com/txthinking/socks5" 8 | "log" 9 | "net" 10 | "net/http" 11 | "net/netip" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | var ( 17 | TestURL = "http://www.gstatic.com/generate_204" 18 | TestTimeout = time.Second * 5 19 | TestUDPAddrPort = "1.1.1.1:53" 20 | ) 21 | 22 | type Result struct { 23 | AddrPort netip.AddrPort 24 | Success bool 25 | UDP bool 26 | } 27 | 28 | func GetInfo(addrPort netip.AddrPort) *Result { 29 | res := &Result{ 30 | AddrPort: addrPort, 31 | Success: false, 32 | UDP: false, 33 | } 34 | sc, err := socks5.NewClient(addrPort.String(), "", "", 15, 15) 35 | if err != nil { 36 | log.Printf("[-] new socks5 client failed (addr=%s): %v", addrPort, err) 37 | return res 38 | } 39 | 40 | c := http.Client{ 41 | Transport: &http.Transport{ 42 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 43 | return sc.Dial(network, addr) 44 | }, 45 | }, 46 | Timeout: TestTimeout, 47 | } 48 | 49 | resp, err := c.Get(TestURL) 50 | defer c.CloseIdleConnections() 51 | if err != nil || resp == nil { 52 | return res 53 | } 54 | res.Success = true 55 | 56 | if err := testUDPByDNS(sc); err != nil { 57 | log.Printf("[-] test udp failed (addr=%s): %v", addrPort, err) 58 | return res 59 | } 60 | 61 | res.UDP = true 62 | return res 63 | } 64 | 65 | func testUDPByDNS(c *socks5.Client) error { 66 | conn, err := c.Dial("udp", TestUDPAddrPort) 67 | if err != nil { 68 | return err 69 | } 70 | conn.SetDeadline(time.Now().Add(TestTimeout)) 71 | 72 | msg := &dns.Msg{} 73 | msg.SetQuestion(dns.Fqdn("example.com"), dns.TypeA) 74 | data, err := msg.Pack() 75 | n, err := conn.Write(data) 76 | if err != nil { 77 | return err 78 | } 79 | if n != len(data) { 80 | return errors.New("write length error, expect: " + strconv.Itoa(len(data)) + ", actual: " + strconv.Itoa(n)) 81 | } 82 | 83 | var buf [1024]byte 84 | recLen, err := conn.Read(buf[:]) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | recMsg := &dns.Msg{} 90 | if err := recMsg.Unpack(buf[:recLen]); err != nil { 91 | return err 92 | } 93 | 94 | if len(recMsg.Answer) == 0 { 95 | return errors.New("no answer") 96 | } 97 | 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /scan/tcpscanner/pcap/types64_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows && amd64 2 | 3 | package pcap 4 | 5 | import ( 6 | "golang.org/x/sys/windows" 7 | ) 8 | 9 | const IF_MAX_STRING_SIZE = 256 10 | 11 | type _MIB_IPNETROW_LH struct { 12 | dwIndex uint32 13 | dwPhysAddrLen uint32 14 | bPhysAddr [windows.MAXLEN_PHYSADDR]byte 15 | dwAddr uint32 16 | dwType uint32 17 | } 18 | 19 | type _MIB_IPNETTABLE struct { 20 | dwNumEntries uint32 21 | table [1]_MIB_IPNETROW_LH 22 | } 23 | 24 | type PMIB_IPNETTABLE *_MIB_IPNETTABLE 25 | 26 | type PULONG *uint32 27 | 28 | type _MIB_IF_ROW2 struct { 29 | InterfaceLuid windows.LUID 30 | InterfaceIndex uint32 31 | InterfaceGuid windows.GUID 32 | Alias [IF_MAX_STRING_SIZE + 1]uint16 33 | Description [IF_MAX_STRING_SIZE + 1]uint16 34 | PhysicalAddressLength uint32 35 | PhysicalAddress [windows.MAXLEN_PHYSADDR]uint8 36 | PermanentPhysicalAddress [windows.MAXLEN_PHYSADDR]uint8 37 | Mtu uint32 38 | Type uint32 39 | TunnelType uint32 40 | MediaType uint32 41 | PhysicalMediumType uint32 42 | AccessType uint32 43 | DirectionType uint32 44 | InterfaceAndOperStatusFlags uint8 45 | OperStatus uint32 46 | AdminStatus uint32 47 | MediaConnectState uint32 48 | NetworkGuid windows.GUID 49 | ConnectionType uint32 50 | TransmitLinkSpeed uint64 51 | ReceiveLinkSpeed uint64 52 | InOctets uint64 53 | InUcastPkts uint64 54 | InNUcastPkts uint64 55 | InDiscards uint64 56 | InErrors uint64 57 | InUnknownProtos uint64 58 | InUcastOctets uint64 59 | InMulticastOctets uint64 60 | InBroadcastOctets uint64 61 | OutOctets uint64 62 | OutUcastPkts uint64 63 | OutNUcastPkts uint64 64 | OutDiscards uint64 65 | OutErrors uint64 66 | OutUcastOctets uint64 67 | OutMulticastOctets uint64 68 | OutBroadcastOctets uint64 69 | OutQLen uint64 70 | IDONTKNOWWHY [48]byte 71 | } 72 | 73 | type _MIB_IF_TABLE2 struct { 74 | NumEntries uint32 75 | Table [1]_MIB_IF_ROW2 76 | } 77 | 78 | type PMIB_IF_TABLE2 *_MIB_IF_TABLE2 79 | -------------------------------------------------------------------------------- /scan/tcpscanner/pcap/physical_windows.go: -------------------------------------------------------------------------------- 1 | package pcap 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "github.com/yaklang/pcap" 8 | "golang.org/x/sys/windows" 9 | "golang.org/x/text/encoding/unicode" 10 | "net" 11 | "slices" 12 | "unsafe" 13 | ) 14 | 15 | // because the ip we query is gw, so it must exist in the arp table normally 16 | func resolveHardwareAddress(iface *net.Interface, addr net.IP) (net.HardwareAddr, error) { 17 | size := uint32(0) 18 | var buffer []byte 19 | 20 | // It may exist timing issues, so we retry 3 times 21 | retry := 0 22 | for { 23 | // get buffer size 24 | _ = getIpNetTable(nil, &size, false) 25 | buffer = make([]byte, size) 26 | if err := getIpNetTable(forceConvert[_MIB_IPNETTABLE, byte](&buffer[0]), &size, false); err != nil && !errors.Is(err, windows.ERROR_SUCCESS) { 27 | if !errors.Is(err, windows.ERROR_INSUFFICIENT_BUFFER) { 28 | return nil, err 29 | } 30 | retry++ 31 | if retry > 3 { 32 | return nil, err 33 | } 34 | continue 35 | } 36 | break 37 | } 38 | res := forceConvert[_MIB_IPNETTABLE, byte](&buffer[0]) 39 | ipnettable := unsafe.Slice(&res.table[0], res.dwNumEntries) 40 | uintip := binary.LittleEndian.Uint32(addr.To4()) 41 | for _, row := range ipnettable { 42 | if row.dwAddr == uintip { 43 | // in ethernet, the hardware address is 6 bytes 44 | return row.bPhysAddr[:6], nil 45 | } 46 | } 47 | 48 | return nil, errors.New("not found") 49 | } 50 | 51 | func openLive(iface *net.Interface) (*pcap.Handle, error) { 52 | var ifTable PMIB_IF_TABLE2 53 | if err := getIfTable2(&ifTable); err != nil { 54 | return nil, err 55 | } 56 | defer func(ifTable PMIB_IF_TABLE2) { 57 | err := freeMibTable(ifTable) 58 | if !errors.Is(err, windows.ERROR_SUCCESS) { 59 | fmt.Printf("freeMibTable may leaks memory: %v\n", err) 60 | } 61 | }(ifTable) 62 | ifTableSlice := unsafe.Slice(&ifTable.Table[0], ifTable.NumEntries) 63 | idx := slices.IndexFunc(ifTableSlice, func(i _MIB_IF_ROW2) bool { return i.InterfaceIndex == uint32(iface.Index) }) 64 | if idx == -1 { 65 | return nil, errors.New("interface not found") 66 | } 67 | found := ifTableSlice[idx] 68 | path := fmt.Sprintf("\\Device\\NPF_%s", found.InterfaceGuid.String()) 69 | ifs, err := pcap.FindAllDevs() 70 | if err != nil { 71 | return nil, err 72 | } 73 | idx = slices.IndexFunc(ifs, func(i pcap.Interface) bool { return i.Name == path }) 74 | if idx == -1 { 75 | return nil, errors.New("interface not found") 76 | } 77 | name, err := wchar2string(&found.Alias) 78 | if err != nil { 79 | name = iface.Name 80 | } 81 | fmt.Printf("open live: %s\n", name) 82 | return pcap.OpenLive(ifs[idx].Name, 1600, true, 0) 83 | } 84 | 85 | func wchar2string[T any](wchar *T) (string, error) { 86 | bytes, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder().Bytes(wchar2bytes(wchar)) 87 | if err != nil { 88 | return "", err 89 | } 90 | return string(bytes), nil 91 | } 92 | -------------------------------------------------------------------------------- /proxy/report.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Report struct { 11 | Results []ProxyResult 12 | TestURL string 13 | } 14 | 15 | func NewReport(results []ProxyResult) *Report { 16 | return &Report{ 17 | Results: results, 18 | TestURL: "https://speed.cloudflare.com/__down?bytes=10000000", // 10MB test file 19 | } 20 | } 21 | 22 | func (r *Report) GenerateTXT(filename string) error { 23 | file, err := os.Create(filename) 24 | if err != nil { 25 | return err 26 | } 27 | defer file.Close() 28 | 29 | // Write header 30 | fmt.Fprintf(file, "Test Time: %s\n", time.Now().Format("2006-01-02 15:04:05")) 31 | fmt.Fprintf(file, "Test URL: %s\n", r.TestURL) 32 | fmt.Fprintf(file, "Proxy Count: %d\n\n", len(r.Results)) 33 | 34 | // Write test results 35 | fmt.Fprintln(file, "=== Test Results ===\n") 36 | availableCount := 0 37 | for _, result := range r.Results { 38 | fmt.Fprintf(file, "%s:\n", result.Proxy) 39 | fmt.Fprintf(file, " Status: %s\n", result.Status) 40 | if result.Error != "" { 41 | fmt.Fprintf(file, " Error: %s\n", result.Error) 42 | } 43 | if result.Latency != "" { 44 | fmt.Fprintf(file, " Latency: %s\n", result.Latency) 45 | } 46 | if result.DownloadSpeed != "" { 47 | fmt.Fprintf(file, " Download Speed: %s\n", result.DownloadSpeed) 48 | } 49 | if result.TotalBytes != "" { 50 | fmt.Fprintf(file, " Total Bytes: %s\n", result.TotalBytes) 51 | } 52 | if result.DownloadTime != "" { 53 | fmt.Fprintf(file, " Download Time: %s\n", result.DownloadTime) 54 | } 55 | 56 | // Write IP information 57 | if len(result.IPInfo.Same) > 0 { 58 | fmt.Fprintln(file, " === IP Information ===") 59 | for field, value := range result.IPInfo.Same { 60 | fmt.Fprintf(file, " %s: %s (Sources: %s)\n", field, value.Value, strings.Join(value.Sources, ", ")) 61 | } 62 | } 63 | 64 | if len(result.IPInfo.Different) > 0 { 65 | fmt.Fprintln(file, " === Different IP Information ===") 66 | for field, values := range result.IPInfo.Different { 67 | fmt.Fprintf(file, " %s:\n", field) 68 | for _, value := range values { 69 | fmt.Fprintf(file, " - %s (Sources: %s)\n", value.Value, strings.Join(value.Sources, ", ")) 70 | } 71 | } 72 | } 73 | 74 | fmt.Fprintln(file, "\n"+strings.Repeat("=", 50)+"\n") 75 | 76 | // Count available proxies 77 | if result.Status == "Available" && result.Error == "" { 78 | availableCount++ 79 | } 80 | } 81 | 82 | // Write statistics 83 | fmt.Fprintln(file, "\n=== Test Statistics ===") 84 | fmt.Fprintf(file, "Total Proxies: %d\n", len(r.Results)) 85 | fmt.Fprintf(file, "Available Proxies: %d\n", availableCount) 86 | fmt.Fprintf(file, "Unavailable Proxies: %d\n", len(r.Results)-availableCount) 87 | if len(r.Results) > 0 { 88 | availabilityRate := float64(availableCount) / float64(len(r.Results)) * 100 89 | fmt.Fprintf(file, "Availability Rate: %.2f%%\n", availabilityRate) 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /scan/scan.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "context" 5 | "github.com/dn-11/proxyScan/pool" 6 | "github.com/dn-11/proxyScan/scan/socks5" 7 | "github.com/dn-11/proxyScan/scan/tcpscanner" 8 | _ "github.com/dn-11/proxyScan/scan/tcpscanner/system" 9 | "github.com/dn-11/proxyScan/utils" 10 | "log" 11 | "net/http" 12 | "net/netip" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | type Scanner struct { 18 | ScannerType string 19 | TestUrl string 20 | TestCallback func(resp *http.Response) bool 21 | TestTimeout time.Duration 22 | PortScanRate int 23 | } 24 | 25 | func Default() *Scanner { 26 | return &Scanner{ 27 | ScannerType: "system", 28 | TestUrl: "http://www.gstatic.com/generate_204", 29 | TestTimeout: time.Second * 15, 30 | PortScanRate: 3000, 31 | } 32 | } 33 | 34 | func ipGenerator(prefixs []netip.Prefix) func(func(addr netip.Addr)) { 35 | return func(yield func(addr netip.Addr)) { 36 | t := time.NewTicker(3 * time.Second) 37 | all := 0 38 | current := 0 39 | for _, prefix := range prefixs { 40 | all += 1 << (32 - prefix.Bits()) 41 | } 42 | for _, prefix := range prefixs { 43 | count := 1 << (32 - prefix.Bits()) 44 | ip := prefix.Masked().Addr() 45 | for i := 0; i < count; i++ { 46 | yield(ip) 47 | ip = ip.Next() 48 | select { 49 | case <-t.C: 50 | log.Printf("IP Generator %d/%d(%f%%)\n", current, all, float64(current)/float64(all)*100) 51 | default: 52 | } 53 | current++ 54 | } 55 | } 56 | } 57 | } 58 | 59 | func (s *Scanner) ScanSocks5(prefixs []netip.Prefix, port []int) []*socks5.Result { 60 | c := utils.NewCollector[netip.AddrPort]() 61 | 62 | addrCount := 0 63 | for _, prefix := range prefixs { 64 | addrCount += 1 << (32 - prefix.Bits()) 65 | } 66 | 67 | sc, err := tcpscanner.Get(s.ScannerType, context.Background(), s.PortScanRate) 68 | if err != nil { 69 | log.Fatalf("get scanner failed: %v", err) 70 | } 71 | 72 | done := make(chan struct{}) 73 | go func() { 74 | for addrPort := range sc.Alive() { 75 | log.Println("[+]", addrPort.String()) 76 | c.C <- addrPort 77 | } 78 | done <- struct{}{} 79 | }() 80 | 81 | ipGenerator(prefixs)(func(addr netip.Addr) { 82 | for _, pt := range port { 83 | sc.Send(netip.AddrPortFrom(addr, uint16(pt))) 84 | } 85 | }) 86 | log.Println("wait for tcp scan.") 87 | sc.End() 88 | <-done 89 | 90 | log.Println("tcp scan done.") 91 | aliveTCPAddrs := c.Return() 92 | 93 | log.Println("start socks5 scan with 128 threads.") 94 | p := pool.Pool{Size: 128, Buffer: 128} 95 | p.Init() 96 | defer p.Close() 97 | res := utils.NewCollector[*socks5.Result]() 98 | var wg sync.WaitGroup 99 | wg.Add(len(aliveTCPAddrs)) 100 | for _, addrPort := range aliveTCPAddrs { 101 | p.Submit(func() { 102 | defer wg.Done() 103 | info := socks5.GetInfo(addrPort) 104 | if info.Success { 105 | res.C <- info 106 | log.Printf("[+] socks5 %s", addrPort.String()) 107 | } else { 108 | log.Printf("[-] not socks5 or too slow %s", addrPort.String()) 109 | } 110 | }) 111 | } 112 | 113 | log.Println("wait for socks5 scan.") 114 | wg.Wait() 115 | log.Println("socks5 scan done.") 116 | return res.Return() 117 | } 118 | -------------------------------------------------------------------------------- /cmd/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/netip" 7 | "os" 8 | "os/signal" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/dn-11/proxyScan/convert" 16 | "github.com/dn-11/proxyScan/scan" 17 | "gopkg.in/yaml.v3" 18 | ) 19 | 20 | func Cli() { 21 | var ( 22 | Prefix string 23 | Port string 24 | TestURL string 25 | Output string 26 | Pcap bool 27 | Rate int 28 | Report bool 29 | ) 30 | 31 | flag.StringVar(&Prefix, "prefix", "", "prefix") 32 | flag.StringVar(&Port, "port", "10808,10809,20170-20172,7890-7893", "split by , use - for range, eg: 10808,10809,20171-20172,7890-7893") 33 | flag.StringVar(&TestURL, "url", "http://www.gstatic.com/generate_204", "") 34 | flag.StringVar(&Output, "output", "proxies.yaml", "output file") 35 | flag.BoolVar(&Pcap, "pcap", false, "use pcap") 36 | flag.IntVar(&Rate, "rate", 3000, "rate, -1 for unlimited") 37 | flag.BoolVar(&Report, "report", false, "generate proxy test report") 38 | flag.Parse() 39 | 40 | // assert rate 41 | if !(Rate == -1 || Rate > 0) { 42 | log.Fatal("rate must be -1 or >0") 43 | } 44 | // parse prefix 45 | var prefixs []netip.Prefix 46 | for _, prefix := range strings.Split(Prefix, ",") { 47 | prefixs = append(prefixs, netip.MustParsePrefix(prefix)) 48 | } 49 | 50 | // parse port 51 | var ports []int 52 | for _, port := range strings.Split(Port, ",") { 53 | if strings.Contains(port, "-") { 54 | strings.Split(port, "-") 55 | start, err := strconv.Atoi(strings.Split(port, "-")[0]) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | end, err := strconv.Atoi(strings.Split(port, "-")[1]) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | for i := start; i <= end; i++ { 64 | ports = append(ports, i) 65 | } 66 | } else { 67 | i, err := strconv.Atoi(port) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | ports = append(ports, i) 72 | } 73 | } 74 | 75 | s := scan.Default() 76 | s.TestUrl = TestURL 77 | s.PortScanRate = Rate 78 | if Pcap { 79 | s.ScannerType = "pcap" 80 | } 81 | 82 | // setup signal handling 83 | sigChan := make(chan os.Signal, 1) 84 | doneChan := make(chan struct{}) 85 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 86 | 87 | // start scanning 88 | go func() { 89 | list := s.ScanSocks5(prefixs, ports) 90 | 91 | // generate output 92 | output := make(map[string][]*convert.ClashSocks5Proxy) 93 | output["proxies"] = make([]*convert.ClashSocks5Proxy, 0, len(list)) 94 | for _, addr := range list { 95 | output["proxies"] = append(output["proxies"], convert.ToClash(addr)) 96 | } 97 | 98 | log.Printf("total %d proxies", len(list)) 99 | data, err := yaml.Marshal(output) 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | abs, err := filepath.Abs(Output) 104 | if err != nil { 105 | log.Fatalf("failed to resolve absolute path for output: %v", err) 106 | } 107 | log.Printf("output to %s", abs) 108 | err = os.WriteFile(Output, data, 0644) 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | 113 | // Wait a moment to ensure file is written 114 | time.Sleep(1 * time.Second) 115 | close(doneChan) 116 | }() 117 | 118 | // wait for signal or scan completion 119 | select { 120 | case <-sigChan: 121 | log.Println("received termination signal, stopping scan...") 122 | log.Println("scan stopped") 123 | os.Exit(0) 124 | case <-doneChan: 125 | log.Println("scan completed") 126 | } 127 | 128 | // generate report if -report flag is specified 129 | if Report { 130 | // Wait a moment to ensure file is written 131 | time.Sleep(1 * time.Second) 132 | GenerateReport() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /scan/tcpscanner/pcap/pcap.go: -------------------------------------------------------------------------------- 1 | package pcap 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "github.com/dn-11/proxyScan/scan/tcpscanner" 9 | "github.com/dn-11/proxyScan/utils" 10 | "github.com/google/gopacket" 11 | "github.com/google/gopacket/layers" 12 | "github.com/libp2p/go-netroute" 13 | "github.com/yaklang/pcap" 14 | "golang.org/x/time/rate" 15 | "io" 16 | "log" 17 | "math/rand/v2" 18 | "net" 19 | "net/netip" 20 | "time" 21 | ) 22 | 23 | func init() { 24 | tcpscanner.Register("pcap", NewScanner) 25 | } 26 | 27 | type Scanner struct { 28 | ctx context.Context 29 | cancelRead context.CancelFunc 30 | 31 | limiter *rate.Limiter 32 | handle *pcap.Handle 33 | pending *utils.TTLSet[netip.AddrPort] 34 | end bool 35 | 36 | srcIP net.IP 37 | linkLayer *layers.Ethernet 38 | alive chan netip.AddrPort 39 | } 40 | 41 | func (t *Scanner) Alive() chan netip.AddrPort { 42 | return t.alive 43 | } 44 | 45 | func (t *Scanner) End() { 46 | t.end = true 47 | t.pending.Wait() 48 | t.cancelRead() 49 | } 50 | 51 | var _ tcpscanner.Scanner = (*Scanner)(nil) 52 | 53 | func NewScanner(ctx context.Context, r int) (tcpscanner.Scanner, error) { 54 | // find route 55 | router, err := netroute.New() 56 | if err != nil { 57 | return nil, fmt.Errorf("get router: %v", err) 58 | } 59 | iface, gw, src, err := router.Route(net.ParseIP("1.1.1.1")) 60 | if err != nil { 61 | return nil, fmt.Errorf("get public route: %v", err) 62 | } 63 | // build link layer 64 | dstmac, err := resolveHardwareAddress(iface, gw) 65 | linkLayer := &layers.Ethernet{ 66 | SrcMAC: iface.HardwareAddr, 67 | DstMAC: dstmac, 68 | EthernetType: layers.EthernetTypeIPv4, 69 | } 70 | // open pcap 71 | h, err := openLive(iface) 72 | if err != nil { 73 | return nil, fmt.Errorf("open live error: %v", err) 74 | } 75 | if err := h.SetBPFFilter("tcp and src host not " + src.String()); err != nil { 76 | return nil, fmt.Errorf("set bpf filter: %v", err) 77 | } 78 | ctxRead, cancelRead := context.WithCancel(ctx) 79 | scanner := &Scanner{ 80 | alive: make(chan netip.AddrPort, 1024), 81 | handle: h, 82 | limiter: utils.ParseLimiter(r), 83 | ctx: ctx, 84 | pending: utils.NewTTLSet[netip.AddrPort](time.Second * 5), 85 | srcIP: src, 86 | cancelRead: cancelRead, 87 | linkLayer: linkLayer, 88 | } 89 | go scanner.recLoop(ctxRead) 90 | return scanner, nil 91 | } 92 | 93 | func (t *Scanner) Send(addr netip.AddrPort) { 94 | if t.end { 95 | log.Println("calling Send after ended is not allowed.") 96 | return 97 | } 98 | 99 | linkLayer := &layers.Ethernet{ 100 | SrcMAC: bytes.Clone(t.linkLayer.SrcMAC), 101 | DstMAC: bytes.Clone(t.linkLayer.DstMAC), 102 | EthernetType: t.linkLayer.EthernetType, 103 | } 104 | 105 | networkLayer := &layers.IPv4{ 106 | Version: 4, 107 | IHL: 0, 108 | TOS: 0, 109 | Length: 0, 110 | Id: uint16(rand.IntN(65535)), 111 | Flags: 0x2, 112 | FragOffset: 0, 113 | TTL: 128, 114 | Protocol: layers.IPProtocolTCP, 115 | Checksum: 0, 116 | SrcIP: t.srcIP, 117 | DstIP: addr.Addr().AsSlice(), 118 | Options: nil, 119 | } 120 | 121 | transportLayer := &layers.TCP{ 122 | SrcPort: layers.TCPPort(rand.IntN(55535) + 10000), 123 | DstPort: layers.TCPPort(addr.Port()), 124 | Seq: rand.Uint32(), 125 | Ack: 0, 126 | DataOffset: 0, 127 | Window: uint16(rand.IntN(10000) + 10000), 128 | Checksum: 0, 129 | SYN: true, 130 | } 131 | 132 | if err := transportLayer.SetNetworkLayerForChecksum(networkLayer); err != nil { 133 | log.Println("set network layer for checksum error: ", err) 134 | } 135 | 136 | buf := gopacket.NewSerializeBuffer() 137 | if err := gopacket.SerializeLayers(buf, gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true}, 138 | linkLayer, networkLayer, transportLayer); err != nil { 139 | log.Println("serialize layers error: ", err) 140 | return 141 | } 142 | 143 | if err := t.limiter.Wait(t.ctx); err != nil { 144 | if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { 145 | log.Printf("limiter: %v", err) 146 | } 147 | return 148 | } 149 | 150 | if err := t.handle.WritePacketData(buf.Bytes()); err != nil { 151 | log.Printf("write packet fail: %v", err) 152 | return 153 | } 154 | 155 | t.pending.Add(addr) 156 | } 157 | 158 | func (t *Scanner) recLoop(ctx context.Context) { 159 | defer close(t.alive) 160 | for { 161 | select { 162 | case <-ctx.Done(): 163 | return 164 | default: 165 | data, _, err := t.handle.ZeroCopyReadPacketData() 166 | if err != nil { 167 | if errors.Is(err, io.EOF) { 168 | return 169 | } 170 | log.Printf("read packet error: %v", err) 171 | } 172 | // tcp syn-ack is short, skip big packet 173 | if len(data) > 100 { 174 | continue 175 | } 176 | 177 | pk := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.Default) 178 | nwLayer, ok := pk.NetworkLayer().(*layers.IPv4) 179 | if !ok { 180 | continue 181 | } 182 | ip := nwLayer.SrcIP 183 | tcpLayer, ok := pk.TransportLayer().(*layers.TCP) 184 | if !ok { 185 | continue 186 | } 187 | if !(tcpLayer.SYN && tcpLayer.ACK) { 188 | continue 189 | } 190 | netipip, ok := netip.AddrFromSlice(ip.To4()) 191 | if !ok { 192 | continue 193 | } 194 | addrPort := netip.AddrPortFrom(netipip, uint16(tcpLayer.SrcPort)) 195 | if t.pending.Exist(addrPort) { 196 | t.alive <- addrPort 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 6 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 7 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 9 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 10 | github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= 11 | github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 12 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 16 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 17 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 18 | github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU= 19 | github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ= 20 | github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875 h1:ql8x//rJsHMjS+qqEag8n3i4azw1QneKh5PieH9UEbY= 21 | github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875/go.mod h1:kfOoFJuHWp76v1RgZCb9/gVUc7XdY877S2uVYbNliGc= 22 | github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 h1:2oDp6OOhLxQ9JBoUuysVz9UZ9uI6oLUbvAZu0x8o+vE= 23 | github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118/go.mod h1:ZFUnHIVchZ9lJoWoEGUg8Q3M4U8aNNWA3CVSUTkW4og= 24 | github.com/mdlayher/packet v1.0.0 h1:InhZJbdShQYt6XV2GPj5XHxChzOfhJJOMbvnGAmOfQ8= 25 | github.com/mdlayher/packet v1.0.0/go.mod h1:eE7/ctqDhoiRhQ44ko5JZU2zxB88g+JH/6jmnjzPjOU= 26 | github.com/mdlayher/socket v0.2.1 h1:F2aaOwb53VsBE+ebRS9bLd7yPOfYUMC8lOODdCBDY6w= 27 | github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E= 28 | github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo= 29 | github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= 30 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 31 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 32 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 33 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 34 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 35 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 36 | github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= 37 | github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= 38 | github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301 h1:d/Wr/Vl/wiJHc3AHYbYs5I3PucJvRuw3SvbmlIRf+oM= 39 | github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301/go.mod h1:ntmMHL/xPq1WLeKiw8p/eRATaae6PiVRNipHFJxI8PM= 40 | github.com/yaklang/pcap v1.0.3 h1:AU2w5l156RfzUj+WLAVnkBTgkellor8NawrXttW3kiA= 41 | github.com/yaklang/pcap v1.0.3/go.mod h1:rrkYQ3AJ3pFh4ShmkuTu9ZTFRI4LWIi9jYQjiupgV8c= 42 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 43 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 44 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 45 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 46 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 47 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 48 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 49 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 50 | golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= 51 | golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 52 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 53 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 54 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 55 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 56 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 57 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 58 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 59 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 60 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 61 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 62 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 65 | golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= 66 | golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 67 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 68 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 75 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 76 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 77 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 78 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 79 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 80 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 81 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 82 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 83 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 84 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 85 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 86 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 87 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 88 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 89 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 90 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 91 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 92 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 93 | golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= 94 | golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= 95 | golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 96 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 97 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 98 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 99 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 100 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 101 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 102 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 103 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 104 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "sort" 11 | "strings" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | const ( 17 | testURL = "https://speed.cloudflare.com/__down?bytes=10000000" // 10MB test file 18 | ) 19 | 20 | type IPInfo struct { 21 | IP string `json:"ip"` 22 | Country string `json:"country"` 23 | Region string `json:"region"` 24 | City string `json:"city"` 25 | Org string `json:"org"` 26 | ASN string `json:"asn"` 27 | Source string `json:"source"` 28 | } 29 | 30 | type FieldValue struct { 31 | Value string `json:"value"` 32 | Sources []string `json:"sources"` 33 | } 34 | 35 | type IPInfoResult struct { 36 | Same map[string]FieldValue `json:"same"` 37 | Different map[string][]FieldValue `json:"different"` 38 | AllSources []string `json:"all_sources"` 39 | } 40 | 41 | type ProxyResult struct { 42 | Proxy string `json:"proxy"` 43 | Status string `json:"status"` 44 | IPInfo IPInfoResult `json:"ip_info"` 45 | Latency string `json:"latency"` 46 | DownloadSpeed string `json:"download_speed"` 47 | DownloadSpeedMB float64 `json:"download_speed_mb"` 48 | TotalBytes string `json:"total_bytes"` 49 | DownloadTime string `json:"download_time"` 50 | Error string `json:"error"` 51 | } 52 | 53 | type IPCheckAPI struct { 54 | Name string 55 | URL string 56 | Fields map[string]string 57 | ParseFunc func(string) (IPInfoResult, error) 58 | } 59 | 60 | var ipCheckAPIs = []IPCheckAPI{ 61 | { 62 | Name: "speedtestcn", 63 | URL: "https://api-v3.speedtest.cn/ip", 64 | Fields: map[string]string{ 65 | "ip": "data.ip", 66 | "country": "data.country", 67 | "region": "data.province", 68 | "city": "data.city", 69 | "org": "data.isp", 70 | "asn": "data.operator", 71 | }, 72 | }, 73 | { 74 | Name: "ipip", 75 | URL: "https://myip.ipip.net/", 76 | ParseFunc: func(text string) (IPInfoResult, error) { 77 | parts := strings.Split(text, ":") 78 | if len(parts) < 3 { 79 | return IPInfoResult{}, fmt.Errorf("invalid response format") 80 | } 81 | ip := strings.TrimSpace(parts[1]) 82 | location := strings.TrimSpace(parts[2]) 83 | locationParts := strings.Split(location, " ") 84 | 85 | result := IPInfoResult{ 86 | Same: make(map[string]FieldValue), 87 | Different: make(map[string][]FieldValue), 88 | AllSources: []string{"ipip"}, 89 | } 90 | 91 | result.Same["ip"] = FieldValue{ 92 | Value: ip, 93 | Sources: []string{"ipip"}, 94 | } 95 | 96 | if len(locationParts) > 0 { 97 | result.Same["country"] = FieldValue{ 98 | Value: locationParts[0], 99 | Sources: []string{"ipip"}, 100 | } 101 | } 102 | 103 | if len(locationParts) > 1 { 104 | result.Same["region"] = FieldValue{ 105 | Value: locationParts[1], 106 | Sources: []string{"ipip"}, 107 | } 108 | } 109 | 110 | if len(locationParts) > 2 { 111 | result.Same["city"] = FieldValue{ 112 | Value: locationParts[2], 113 | Sources: []string{"ipip"}, 114 | } 115 | } 116 | 117 | if len(locationParts) > 3 { 118 | result.Same["org"] = FieldValue{ 119 | Value: locationParts[len(locationParts)-1], 120 | Sources: []string{"ipip"}, 121 | } 122 | } 123 | 124 | return result, nil 125 | }, 126 | }, 127 | { 128 | Name: "ip.sb", 129 | URL: "https://api.ip.sb/geoip", 130 | Fields: map[string]string{ 131 | "ip": "ip", 132 | "country": "country", 133 | "region": "region", 134 | "city": "city", 135 | "org": "organization", 136 | "asn": "asn_organization", 137 | }, 138 | }, 139 | { 140 | Name: "ipinfo", 141 | URL: "https://ipinfo.io/json", 142 | Fields: map[string]string{ 143 | "ip": "ip", 144 | "country": "country", 145 | "region": "region", 146 | "city": "city", 147 | "org": "org", 148 | "asn": "asn", 149 | }, 150 | }, 151 | { 152 | Name: "ipapi", 153 | // Public token from ip.skk.moe 154 | URL: "https://ipinfo.io/json?token=ba0234c01f79d3", 155 | Fields: map[string]string{ 156 | "ip": "ip", 157 | "country": "country_name", 158 | "region": "region", 159 | "city": "city", 160 | "org": "org", 161 | "asn": "asn", 162 | }, 163 | }, 164 | { 165 | Name: "ip-api", 166 | // Public token from ip.skk.moe 167 | URL: "https://pro.ip-api.com/json/?fields=16985625&key=EEKS6bLi6D91G1p", 168 | Fields: map[string]string{ 169 | "ip": "query", 170 | "country": "country", 171 | "region": "regionName", 172 | "city": "city", 173 | "org": "org", 174 | "asn": "as", 175 | }, 176 | }, 177 | { 178 | Name: "cf(skkmoe)", 179 | URL: "https://ip.skk.moe/cdn-cgi/trace", 180 | ParseFunc: func(text string) (IPInfoResult, error) { 181 | result := IPInfoResult{ 182 | Same: make(map[string]FieldValue), 183 | Different: make(map[string][]FieldValue), 184 | AllSources: []string{"cf(skkmoe)"}, 185 | } 186 | 187 | // Parse IP 188 | if ipIndex := strings.Index(text, "ip="); ipIndex != -1 { 189 | ipEnd := strings.Index(text[ipIndex:], "\n") 190 | if ipEnd != -1 { 191 | result.Same["ip"] = FieldValue{ 192 | Value: text[ipIndex+3 : ipIndex+ipEnd], 193 | Sources: []string{"cf(skkmoe)"}, 194 | } 195 | } 196 | } 197 | 198 | // Parse country 199 | if locIndex := strings.Index(text, "loc="); locIndex != -1 { 200 | locEnd := strings.Index(text[locIndex:], "\n") 201 | if locEnd != -1 { 202 | result.Same["country"] = FieldValue{ 203 | Value: text[locIndex+4 : locIndex+locEnd], 204 | Sources: []string{"cf(skkmoe)"}, 205 | } 206 | } 207 | } 208 | 209 | return result, nil 210 | }, 211 | }, 212 | { 213 | Name: "cf(chatgpt)", 214 | URL: "https://chatgpt.com/cdn-cgi/trace", 215 | ParseFunc: func(text string) (IPInfoResult, error) { 216 | result := IPInfoResult{ 217 | Same: make(map[string]FieldValue), 218 | Different: make(map[string][]FieldValue), 219 | AllSources: []string{"cf(chatgpt)"}, 220 | } 221 | 222 | // Parse IP 223 | if ipIndex := strings.Index(text, "ip="); ipIndex != -1 { 224 | ipEnd := strings.Index(text[ipIndex:], "\n") 225 | if ipEnd != -1 { 226 | result.Same["ip"] = FieldValue{ 227 | Value: text[ipIndex+3 : ipIndex+ipEnd], 228 | Sources: []string{"cf(chatgpt)"}, 229 | } 230 | } 231 | } 232 | 233 | // Parse country 234 | if locIndex := strings.Index(text, "loc="); locIndex != -1 { 235 | locEnd := strings.Index(text[locIndex:], "\n") 236 | if locEnd != -1 { 237 | result.Same["country"] = FieldValue{ 238 | Value: text[locIndex+4 : locIndex+locEnd], 239 | Sources: []string{"cf(chatgpt)"}, 240 | } 241 | } 242 | } 243 | 244 | return result, nil 245 | }, 246 | }, 247 | { 248 | Name: "cf(cp)", 249 | URL: "https://cp.cloudflare.com/cdn-cgi/trace", 250 | ParseFunc: func(text string) (IPInfoResult, error) { 251 | result := IPInfoResult{ 252 | Same: make(map[string]FieldValue), 253 | Different: make(map[string][]FieldValue), 254 | AllSources: []string{"cf(cp)"}, 255 | } 256 | 257 | // Parse IP 258 | if ipIndex := strings.Index(text, "ip="); ipIndex != -1 { 259 | ipEnd := strings.Index(text[ipIndex:], "\n") 260 | if ipEnd != -1 { 261 | result.Same["ip"] = FieldValue{ 262 | Value: text[ipIndex+3 : ipIndex+ipEnd], 263 | Sources: []string{"cf(cp)"}, 264 | } 265 | } 266 | } 267 | 268 | // Parse country 269 | if locIndex := strings.Index(text, "loc="); locIndex != -1 { 270 | locEnd := strings.Index(text[locIndex:], "\n") 271 | if locEnd != -1 { 272 | result.Same["country"] = FieldValue{ 273 | Value: text[locIndex+4 : locIndex+locEnd], 274 | Sources: []string{"cf(cp)"}, 275 | } 276 | } 277 | } 278 | 279 | return result, nil 280 | }, 281 | }, 282 | { 283 | Name: "ipwhois", 284 | URL: "https://ipwho.is/", 285 | Fields: map[string]string{ 286 | "ip": "ip", 287 | "country": "country", 288 | "region": "region", 289 | "city": "city", 290 | "org": "connection.org", 291 | "asn": "connection.asn", 292 | }, 293 | }, 294 | } 295 | 296 | type ProxyTester struct { 297 | proxies []string 298 | client *http.Client 299 | ctx context.Context 300 | } 301 | 302 | func NewProxyTester(ctx context.Context, proxies []string) *ProxyTester { 303 | // Remove duplicate proxy list 304 | uniqueProxies := make(map[string]struct{}) 305 | for _, proxy := range proxies { 306 | uniqueProxies[proxy] = struct{}{} 307 | } 308 | 309 | // Convert the deduplicated proxy list to a slice 310 | deduplicatedProxies := make([]string, 0, len(uniqueProxies)) 311 | for proxy := range uniqueProxies { 312 | deduplicatedProxies = append(deduplicatedProxies, proxy) 313 | } 314 | 315 | return &ProxyTester{ 316 | proxies: deduplicatedProxies, 317 | client: &http.Client{ 318 | Timeout: 30 * time.Second, 319 | }, 320 | ctx: ctx, 321 | } 322 | } 323 | 324 | func (t *ProxyTester) TestProxy(proxy string) ProxyResult { 325 | proxyURL, err := url.Parse(fmt.Sprintf("http://%s", proxy)) 326 | if err != nil { 327 | return ProxyResult{ 328 | Proxy: proxy, 329 | Status: "Unavailable", 330 | Error: fmt.Sprintf("Invalid proxy URL: %v", err), 331 | } 332 | } 333 | 334 | transport := &http.Transport{ 335 | Proxy: http.ProxyURL(proxyURL), 336 | } 337 | client := &http.Client{ 338 | Transport: transport, 339 | Timeout: 30 * time.Second, 340 | } 341 | 342 | result := ProxyResult{ 343 | Proxy: proxy, 344 | Status: "Available", 345 | } 346 | 347 | // Test latency 348 | startTime := time.Now() 349 | resp, err := client.Get(testURL) 350 | if err != nil { 351 | result.Status = "Unavailable" 352 | result.Error = fmt.Sprintf("Connection failed: %v", err) 353 | return result 354 | } 355 | defer resp.Body.Close() 356 | 357 | if resp.StatusCode != http.StatusOK { 358 | result.Status = "Unavailable" 359 | result.Error = fmt.Sprintf("HTTP status: %d", resp.StatusCode) 360 | return result 361 | } 362 | 363 | latency := time.Since(startTime).Milliseconds() 364 | result.Latency = fmt.Sprintf("%dms", latency) 365 | 366 | // Test download speed 367 | startTime = time.Now() 368 | totalBytes := int64(0) 369 | buf := make([]byte, 1024) 370 | for { 371 | n, err := resp.Body.Read(buf) 372 | if err != nil && err != io.EOF { 373 | result.Status = "Unavailable" 374 | result.Error = fmt.Sprintf("Download failed: %v", err) 375 | return result 376 | } 377 | if n == 0 { 378 | break 379 | } 380 | totalBytes += int64(n) 381 | } 382 | downloadTime := time.Since(startTime).Seconds() 383 | if downloadTime == 0 { 384 | result.Status = "Unavailable" 385 | result.Error = "Download time is zero" 386 | return result 387 | } 388 | downloadSpeed := float64(totalBytes) / downloadTime 389 | result.DownloadSpeed = formatSpeed(downloadSpeed) 390 | result.DownloadSpeedMB = downloadSpeed / (1024 * 1024) 391 | result.TotalBytes = fmt.Sprintf("%.2fMB", float64(totalBytes)/(1024*1024)) 392 | result.DownloadTime = fmt.Sprintf("%.2fs", downloadTime) 393 | 394 | // Get IP information 395 | ipInfo, err := t.getIPInfo(client) 396 | if err != nil { 397 | result.IPInfo = IPInfoResult{ 398 | Same: map[string]FieldValue{ 399 | "ip": {Value: "Failed to get", Sources: []string{}}, 400 | "country": {Value: "Failed to get", Sources: []string{}}, 401 | "region": {Value: "Failed to get", Sources: []string{}}, 402 | "city": {Value: "Failed to get", Sources: []string{}}, 403 | "org": {Value: "Failed to get", Sources: []string{}}, 404 | "asn": {Value: "Failed to get", Sources: []string{}}, 405 | }, 406 | Different: make(map[string][]FieldValue), 407 | AllSources: []string{}, 408 | } 409 | } else { 410 | result.IPInfo = ipInfo 411 | } 412 | 413 | return result 414 | } 415 | 416 | func (t *ProxyTester) getIPInfo(client *http.Client) (IPInfoResult, error) { 417 | var results []IPInfoResult 418 | var mu sync.Mutex 419 | var wg sync.WaitGroup 420 | 421 | // Set request headers 422 | headers := map[string]string{ 423 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 424 | } 425 | 426 | for _, api := range ipCheckAPIs { 427 | wg.Add(1) 428 | go func(api IPCheckAPI) { 429 | defer wg.Done() 430 | 431 | // Create request 432 | req, err := http.NewRequest("GET", api.URL, nil) 433 | if err != nil { 434 | return 435 | } 436 | 437 | // Add headers 438 | for k, v := range headers { 439 | req.Header.Set(k, v) 440 | } 441 | 442 | // Send request 443 | resp, err := client.Do(req) 444 | if err != nil { 445 | return 446 | } 447 | defer resp.Body.Close() 448 | 449 | // Read response body 450 | body, err := io.ReadAll(resp.Body) 451 | if err != nil { 452 | return 453 | } 454 | 455 | var info IPInfoResult 456 | if api.ParseFunc != nil { 457 | info, err = api.ParseFunc(string(body)) 458 | if err != nil { 459 | return 460 | } 461 | } else { 462 | var data map[string]interface{} 463 | if err := json.Unmarshal(body, &data); err != nil { 464 | return 465 | } 466 | 467 | info = IPInfoResult{ 468 | Same: make(map[string]FieldValue), 469 | Different: make(map[string][]FieldValue), 470 | AllSources: []string{api.Name}, 471 | } 472 | 473 | for field, path := range api.Fields { 474 | value := getFieldValue(data, path) 475 | if value != "" { 476 | info.Same[field] = FieldValue{ 477 | Value: value, 478 | Sources: []string{api.Name}, 479 | } 480 | } 481 | } 482 | } 483 | 484 | // Check if valid information was obtained 485 | if len(info.Same) == 0 { 486 | return 487 | } 488 | 489 | mu.Lock() 490 | results = append(results, info) 491 | mu.Unlock() 492 | }(api) 493 | } 494 | 495 | wg.Wait() 496 | 497 | if len(results) == 0 { 498 | return IPInfoResult{}, fmt.Errorf("Failed to get IP information") 499 | } 500 | 501 | // Analyze results 502 | fieldValues := make(map[string]map[string][]string) 503 | for _, result := range results { 504 | for field, info := range result.Same { 505 | if _, ok := fieldValues[field]; !ok { 506 | fieldValues[field] = make(map[string][]string) 507 | } 508 | fieldValues[field][info.Value] = append(fieldValues[field][info.Value], info.Sources...) 509 | } 510 | } 511 | 512 | // Build result 513 | finalResult := IPInfoResult{ 514 | Same: make(map[string]FieldValue), 515 | Different: make(map[string][]FieldValue), 516 | AllSources: make([]string, 0), 517 | } 518 | 519 | // Find same and different results 520 | for field, values := range fieldValues { 521 | if len(values) == 1 { 522 | // All APIs return same value 523 | for value, sources := range values { 524 | finalResult.Same[field] = FieldValue{ 525 | Value: value, 526 | Sources: sources, 527 | } 528 | finalResult.AllSources = append(finalResult.AllSources, sources...) 529 | } 530 | } else { 531 | // Different APIs return different values 532 | var diffs []FieldValue 533 | for value, sources := range values { 534 | diffs = append(diffs, FieldValue{ 535 | Value: value, 536 | Sources: sources, 537 | }) 538 | finalResult.AllSources = append(finalResult.AllSources, sources...) 539 | } 540 | finalResult.Different[field] = diffs 541 | } 542 | } 543 | 544 | // Deduplicate and sort all sources 545 | sourceMap := make(map[string]bool) 546 | for _, source := range finalResult.AllSources { 547 | sourceMap[source] = true 548 | } 549 | finalResult.AllSources = make([]string, 0, len(sourceMap)) 550 | for source := range sourceMap { 551 | finalResult.AllSources = append(finalResult.AllSources, source) 552 | } 553 | sort.Strings(finalResult.AllSources) 554 | 555 | return finalResult, nil 556 | } 557 | 558 | func getFieldValue(data map[string]interface{}, fieldPath string) string { 559 | if fieldPath == "" { 560 | return "" 561 | } 562 | parts := strings.Split(fieldPath, ".") 563 | var value interface{} = data 564 | for _, part := range parts { 565 | if m, ok := value.(map[string]interface{}); ok { 566 | value = m[part] 567 | } else { 568 | return "" 569 | } 570 | } 571 | if str, ok := value.(string); ok { 572 | return str 573 | } 574 | return "" 575 | } 576 | 577 | func (t *ProxyTester) Run() []ProxyResult { 578 | var wg sync.WaitGroup 579 | results := make([]ProxyResult, len(t.proxies)) 580 | sem := make(chan struct{}, 10) // Limit concurrency 581 | 582 | for i, proxy := range t.proxies { 583 | wg.Add(1) 584 | sem <- struct{}{} 585 | go func(i int, proxy string) { 586 | defer wg.Done() 587 | defer func() { <-sem }() 588 | results[i] = t.TestProxy(proxy) 589 | }(i, proxy) 590 | } 591 | 592 | wg.Wait() 593 | 594 | // Sort by download speed 595 | sort.Slice(results, func(i, j int) bool { 596 | return results[i].DownloadSpeedMB > results[j].DownloadSpeedMB 597 | }) 598 | 599 | return results 600 | } 601 | 602 | func formatSpeed(bytesPerSecond float64) string { 603 | if bytesPerSecond == 0 { 604 | return "0 B/s" 605 | } 606 | sizeName := []string{"B/s", "KB/s", "MB/s", "GB/s"} 607 | i := 0 608 | for bytesPerSecond >= 1024 && i < len(sizeName)-1 { 609 | bytesPerSecond /= 1024 610 | i++ 611 | } 612 | return fmt.Sprintf("%.2f %s", bytesPerSecond, sizeName[i]) 613 | } 614 | --------------------------------------------------------------------------------