diff --git a/.gitignore b/.gitignore index 34285a8..082fd49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # --- Binaries & Build Results --- # Binaries built by 'go build' +scanner *.exe *.exe~ *.dll diff --git a/bin/sing-box b/bin/sing-box new file mode 100755 index 0000000..7bf90ad Binary files /dev/null and b/bin/sing-box differ diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..1b5c54f --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "fmt" + "os" + "sync" + "time" + + "find-me-internet/internal/filter" + "find-me-internet/internal/parser" + "find-me-internet/internal/tester" +) + +const ( + SingBoxPath = "./bin/sing-box" // Make sure this exists! + TestTarget = "http://cp.cloudflare.com" + MaxWorkers = 10 +) + +func main() { + // 0. Setup + // Check if binary exists + if _, err := os.Stat(SingBoxPath); os.IsNotExist(err) { + fmt.Printf("Error: sing-box binary not found at %s\n", SingBoxPath) + return + } + + // Mock Input Data (Replace with file reading logic later) + rawLinks := []string{ + "vless://4525c260-df3c-4f62-b8f1-f4f5f305694b@66.81.247.155:443?encryption=none&security=tls&sni=yyzsuabw9e3qd5ud7ihi5dxm96oglnsvr83cjojnm1efncfhr9ucordq.zjde5.de5.net&fp=chrome&insecure=0&allowInsecure=0&type=ws&host=yyzsuabw9e3qd5ud7ihi5dxm96oglnsvr83cjojnm1efncfhr9ucordq.zjde5.de5.net&path=%2F%3Fed#%DA%86%D9%86%D9%84%20%D8%AA%D9%84%DA%AF%D8%B1%D8%A7%D9%85%20%3A%20%40CroSs_Guildd%F0%9F%92%8A", + "vless://efdb2890-6dd7-4e65-8984-f0b1d3ae4e01@here-we-go-again.embeddedonline.org:443?encryption=none&security=tls&sni=here-we-go-again.embeddedonline.org&fp=chrome&alpn=http%2F1.1&insecure=0&allowInsecure=0&type=ws&host=here-we-go-again.embeddedonline.org&path=%2FJ1jTS0GMxqS0Atmd5x#here-we-go-again.embeddedonline.org%20tls%20WS%20direct%20vless", + // Add more links here... + } + + fmt.Printf("Loaded %d links. Starting scan...\n", len(rawLinks)) + + // Pipelines + netFilter := filter.NewPipeline(2 * time.Second) + boxRunner := tester.NewRunner(SingBoxPath, TestTarget, 5*time.Second) + + // Concurrency Controls + var wg sync.WaitGroup + semaphore := make(chan struct{}, MaxWorkers) // Limit active Sing-box instances + + // Results + successCount := 0 + var mu sync.Mutex + + startTotal := time.Now() + + for _, link := range rawLinks { + wg.Add(1) + + go func(raw string) { + defer wg.Done() + + // --- STAGE 1: PARSE --- + proxy, err := parser.ParseLink(raw) + if err != nil { + // fmt.Printf("Invalid Link: %v\n", err) + return + } + + // --- STAGE 2: CHEAP FILTER --- + // Check TCP and TLS Handshake first + if !netFilter.Check(proxy) { + // fmt.Printf("[DEAD] %s:%d\n", proxy.Address, proxy.Port) + return + } + + // --- STAGE 3: EXPENSIVE TEST --- + semaphore <- struct{}{} // Acquire worker slot + err = boxRunner.Test(proxy) + <-semaphore // Release worker slot + + if err != nil { + fmt.Printf("[FAIL] %s (%v)\n", proxy.SNI, err) + return + } + + // --- SUCCESS --- + mu.Lock() + successCount++ + mu.Unlock() + + fmt.Printf("✅ [OK] %s | Latency: %dms | Type: %s\n", + proxy.Address, proxy.Latency.Milliseconds(), proxy.Type) + + }(link) + } + + wg.Wait() + fmt.Printf("\n--- Scan Complete in %s ---\n", time.Since(startTotal)) + fmt.Printf("Valid Proxies Found: %d\n", successCount) +} \ No newline at end of file diff --git a/go.mod b/go.mod index c471882..04fa476 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module find-me-internet go 1.25.6 +require github.com/gvcgo/vpnparser v0.2.7 + require ( atomicgo.dev/cursor v0.1.1 // indirect atomicgo.dev/keyboard v0.2.9 // indirect @@ -11,12 +13,10 @@ require ( github.com/charmbracelet/lipgloss v0.8.0 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gogf/gf/v2 v2.6.1 // indirect github.com/gookit/color v1.5.3 // indirect github.com/gvcgo/goutils v0.8.5 // indirect - github.com/gvcgo/vpnparser v0.2.7 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.6 // indirect @@ -26,10 +26,7 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/pterm/pterm v0.12.62 // indirect github.com/rivo/uniseg v0.4.4 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/urfave/cli/v2 v2.25.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opentelemetry.io/otel v1.15.1 // indirect go.opentelemetry.io/otel/trace v1.15.1 // indirect golang.org/x/sys v0.15.0 // indirect diff --git a/go.sum b/go.sum index 47d162d..4ebff3e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= atomicgo.dev/cursor v0.1.1 h1:0t9sxQomCTRh5ug+hAMCs59x/UmC9QL6Ci5uosINKD4= atomicgo.dev/cursor v0.1.1/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= @@ -13,6 +15,8 @@ github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSr github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= @@ -23,18 +27,29 @@ github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gogf/gf/v2 v2.6.1 h1:n/cfXM506WjhPa6Z1CEDuHNM1XZ7C8JzSDPn2AfuxgQ= github.com/gogf/gf/v2 v2.6.1/go.mod h1:x2XONYcI4hRQ/4gMNbWHmZrNzSEIg20s2NULbzom5k0= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE= github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= +github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= github.com/gvcgo/goutils v0.8.5 h1:0QPLOl5lfjV9vIAUnB5MPze5YpfMkrnxUW512ZjH82Q= github.com/gvcgo/goutils v0.8.5/go.mod h1:g/gPJxbpSiLK0q8a1qpkaf6ec3LOcxb7jj1ekDsKqzY= github.com/gvcgo/vpnparser v0.2.7 h1:+uezF5c00ROSKsD6a3ysmx3BdHxzI1ICkzuWeCHDQww= @@ -42,8 +57,12 @@ github.com/gvcgo/vpnparser v0.2.7/go.mod h1:JQwo6gDtzYAQgO0o63FvP0db/eO+7QKy3TnT github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= @@ -51,6 +70,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -61,6 +82,9 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= @@ -75,33 +99,36 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= -github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8= go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc= +go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY= +go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY= go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -139,6 +166,7 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/filter/filter.go b/internal/filter/filter.go index db1a143..cc3468a 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -2,9 +2,11 @@ package filter import ( "crypto/tls" - "find-me-internet/internal/model" "net" "time" + "strconv" + + "find-me-internet/internal/model" ) // Pipeline represents the filter configuration diff --git a/internal/parser/parser.go b/internal/parser/parser.go index cc1f3fd..9d262fe 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -1,9 +1,8 @@ package parser import ( + "encoding/json" "fmt" - "net" - "strconv" "strings" "find-me-internet/internal/model" @@ -11,44 +10,61 @@ import ( "github.com/gvcgo/vpnparser/pkgs/outbound" ) -// ParseLink converts a raw proxy string (vless://, vmess://) into our internal Model +// tempConfig allows us to extract deep fields from the Sing-box JSON +type tempConfig struct { + Transport struct { + Type string `json:"type"` + } `json:"transport"` + TLS struct { + ServerName string `json:"server_name"` + } `json:"tls"` +} + func ParseLink(raw string) (*model.Proxy, error) { raw = strings.TrimSpace(raw) if raw == "" { return nil, fmt.Errorf("empty link") } - // 1. Use vpnparser to decode the link - item, err := outbound.ParseRawUriToProxyItem(raw, outbound.ClientTypeSingBox) - if err != nil { - return nil, fmt.Errorf("parse failed: %w", err) - } + // 1. Parse Raw Link + // We omit the second argument to let the library use default parsing + item := outbound.ParseRawUriToProxyItem(raw) if item == nil { return nil, fmt.Errorf("unknown protocol or invalid link") } - // 2. Map to our Internal Model - // The library returns a ProxyItem struct. We extract what we need for the "Cheap Checks". + // 2. Initialize Proxy Model p := &model.Proxy{ RawLink: raw, Address: item.Address, Port: item.Port, - Network: item.Network, - SNI: item.Sni, } - // 3. Determine Protocol (The library stores this in Protocol field) - switch strings.ToLower(item.Protocol) { + // 3. Extract SNI and Network from the Outbound JSON + // The library packs the details into 'item.Outbound' string + if item.Outbound != "" { + var cfg tempConfig + if err := json.Unmarshal([]byte(item.Outbound), &cfg); err == nil { + p.Network = cfg.Transport.Type + p.SNI = cfg.TLS.ServerName + } + } + + // Fallback: If JSON extraction failed but we have a generic "Host" (sometimes used as SNI) + // Note: 'item.Host' doesn't exist either, so we rely solely on JSON extraction above. + + // 4. Map Protocol (Library uses 'Scheme') + switch strings.ToLower(item.Scheme) { case "vless": p.Type = model.TypeVLESS if strings.Contains(raw, "reality") { - p.Type = model.TypeVLESS // We treat Reality as VLESS with special TLS options + p.Type = model.TypeVLESS // Reality is technically VLESS } case "vmess": p.Type = model.TypeVMess case "trojan": p.Type = model.TypeTrojan - case "shadowsocks": + case "shadowsocks", "ss": p.Type = model.TypeShadowsocks default: p.Type = model.TypeUnknown diff --git a/internal/tester/config.go b/internal/tester/config.go new file mode 100644 index 0000000..d121352 --- /dev/null +++ b/internal/tester/config.go @@ -0,0 +1,70 @@ +package tester + +import ( + "encoding/json" + "fmt" + + "find-me-internet/internal/model" + + "github.com/gvcgo/vpnparser/pkgs/outbound" +) + +// SingBoxConfig is the minimal structure Sing-box expects +type SingBoxConfig struct { + Log LogConfig `json:"log"` + Inbounds []InboundConfig `json:"inbounds"` + Outbounds []interface{} `json:"outbounds"` // Interface because structure varies +} + +type LogConfig struct { + Level string `json:"level"` + Output string `json:"output,omitempty"` + Disabled bool `json:"disabled"` +} + +type InboundConfig struct { + Type string `json:"type"` + Tag string `json:"tag"` + Listen string `json:"listen"` + ListenPort int `json:"listen_port"` +} + +// GenerateConfig creates a JSON config string for Sing-box +func GenerateConfig(p *model.Proxy, localPort int) ([]byte, error) { + // 1. Convert the Raw Link directly to Sing-box Outbound Object + item := outbound.ParseRawUriToProxyItem(p.RawLink, outbound.SingBox) + if item == nil { + return nil, fmt.Errorf("failed to parse link for config generation") + } + + outboundJsonStr := item.GetOutbound() + var sbOutbound interface{} + if err := json.Unmarshal([]byte(outboundJsonStr), &sbOutbound); err != nil { + return nil, fmt.Errorf("failed to parse sing-box outbound json: %w", err) + } + + // 2. Wrap it in the full config structure + config := SingBoxConfig{ + Log: LogConfig{ + Level: "panic", // Silence all logs to keep console clean + Disabled: true, + }, + Inbounds: []InboundConfig{ + { + Type: "mixed", // Supports both SOCKS5 and HTTP + Tag: "in-local", + Listen: "127.0.0.1", + ListenPort: localPort, + }, + }, + Outbounds: []interface{}{ + sbOutbound, // The Proxy being tested + map[string]string{ + "type": "direct", + "tag": "direct", + }, + }, + } + + return json.MarshalIndent(config, "", " ") +} \ No newline at end of file diff --git a/internal/tester/runner.go b/internal/tester/runner.go new file mode 100644 index 0000000..c2a6afd --- /dev/null +++ b/internal/tester/runner.go @@ -0,0 +1,143 @@ +package tester + +import ( + "context" + "fmt" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "time" + + "find-me-internet/internal/model" +) + +// Runner handles the execution of Sing-box +type Runner struct { + BinPath string + TestURL string + Timeout time.Duration +} + +func NewRunner(binPath string, testURL string, timeout time.Duration) *Runner { + return &Runner{ + BinPath: binPath, + TestURL: testURL, + Timeout: timeout, + } +} + +// Test performs the full latency check +func (r *Runner) Test(p *model.Proxy) error { + // 1. Get a random free port + port, err := getFreePort() + if err != nil { + return fmt.Errorf("no free ports: %v", err) + } + + // 2. Generate Config + configData, err := GenerateConfig(p, port) + if err != nil { + return err + } + + // 3. Write Config to Temp File + // specific name helps debugging if needed: config_.json + configName := filepath.Join(os.TempDir(), fmt.Sprintf("sb_config_%d.json", port)) + if err := os.WriteFile(configName, configData, 0644); err != nil { + return err + } + defer os.Remove(configName) // Cleanup after test + + // 4. Start Sing-box Process + ctx, cancel := context.WithTimeout(context.Background(), r.Timeout+2*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, r.BinPath, "run", "-c", configName) + // cmd.Stdout = os.Stdout // Uncomment for debugging + // cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start sing-box: %v", err) + } + + // Ensure process is killed when function exits + defer func() { + if cmd.Process != nil { + cmd.Process.Kill() + } + }() + + // 5. Wait for Sing-box to initialize + // A smart retry loop is better than a fixed sleep + proxyReady := waitForPort(port, 2*time.Second) + if !proxyReady { + return fmt.Errorf("sing-box did not start in time") + } + + // 6. Perform HTTP Latency Test + latency, err := r.measureLatency(port) + if err != nil { + return err + } + + // 7. Success! Update the model + p.Latency = latency + return nil +} + +// measureLatency makes the actual HTTP request +func (r *Runner) measureLatency(port int) (time.Duration, error) { + proxyUrl, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", port)) + + client := &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyUrl), + DisableKeepAlives: true, + }, + Timeout: r.Timeout, + } + + start := time.Now() + resp, err := client.Get(r.TestURL) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + // Check for valid response codes (200 OK or 204 No Content) + if resp.StatusCode != 200 && resp.StatusCode != 204 { + return 0, fmt.Errorf("bad status code: %d", resp.StatusCode) + } + + return time.Since(start), nil +} + +// Helpers +func getFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + +func waitForPort(port int, timeout time.Duration) bool { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 100*time.Millisecond) + if err == nil { + conn.Close() + return true + } + time.Sleep(50 * time.Millisecond) + } + return false +} \ No newline at end of file