├── .gitignore ├── logger └── logger.go ├── mystery_resolver.go ├── .github ├── workflows │ └── go.yml └── dependabot.yml ├── proto_test.go ├── LICENSE ├── proto.go ├── socks5_server.go ├── mr_worldwide.go ├── scale_util.go ├── go.mod ├── proxy.go ├── parse_test.go ├── stats.go ├── internal ├── randtls │ └── shim.go └── scaler │ ├── scaler_test.go │ └── scaler.go ├── list_management.go ├── conductor.go ├── daemons.go ├── dispense.go ├── parse.go ├── example └── main.go ├── getters.go ├── mystery_dialer.go ├── debug.go ├── README.md ├── prox5_test.go ├── validator_engine.go ├── setters.go ├── go.sum └── defs.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.txt 3 | *.list 4 | *.swp 5 | *.save 6 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | type Logger interface { 4 | Printf(format string, a ...interface{}) 5 | Errorf(format string, a ...interface{}) 6 | } 7 | -------------------------------------------------------------------------------- /mystery_resolver.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | /* 4 | import ( 5 | "context" 6 | "net" 7 | 8 | "inet.af/netaddr" 9 | ) 10 | 11 | type dnsCacheEntry []netaddr.IP 12 | 13 | var dnsCache = make(map[string]dnsCacheEntry) 14 | 15 | func (pe *ProxyEngine) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) { 16 | var result net.IP 17 | for { 18 | select { 19 | case <-ctx.Done(): 20 | return ctx, nil, ctx.Err() 21 | } 22 | } 23 | } 24 | */ 25 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches: [ "main", "development" ] 5 | pull_request: 6 | branches: [ "main", "development" ] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: 1.22 16 | - name: deps 17 | run: go mod tidy -x 18 | - name: vet 19 | run: go vet ./... 20 | - name: test 21 | run: go test -v -coverprofile=coverage.txt -covermode=atomic ./... 22 | - name: codecov 23 | run: bash <(curl -s https://codecov.io/bash) 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | target-branch: development 8 | groups: 9 | deps: 10 | applies-to: version-updates 11 | patterns: 12 | - "*" 13 | - package-ecosystem: "github-actions" # See documentation for possible values 14 | directory: "/" # Location of package manifests 15 | schedule: 16 | interval: "daily" 17 | target-branch: development 18 | groups: 19 | action-deps: 20 | applies-to: version-updates 21 | patterns: 22 | - "*" 23 | -------------------------------------------------------------------------------- /proto_test.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestImmutableProto(t *testing.T) { 8 | prt := newImmutableProto() 9 | if prt.Get() != ProtoNull { 10 | t.Fatal("expected protonull") 11 | } 12 | prt.set(ProtoSOCKS5) 13 | if prt.Get() != ProtoSOCKS5 { 14 | t.Fatal("expected socks5 proto") 15 | } 16 | prt.set(ProtoSOCKS4) 17 | if prt.Get() != ProtoSOCKS5 { 18 | t.Fatal("expected socks5 proto still after trying to set twice") 19 | } 20 | str := strs.Get() 21 | defer strs.MustPut(str) 22 | prt.Get().writeProtoString(str) 23 | if str.String() != "socks5" { 24 | t.Fatalf("expected socks5://, got %s", str.String()) 25 | } 26 | str.MustReset() 27 | prt.Get().writeProtoURI(str) 28 | if str.String() != "socks5://" { 29 | t.Fatalf("expected socks5://, got %s", str.String()) 30 | } 31 | str.MustReset() 32 | ptrstr := prt.Get().String() 33 | if ptrstr != "socks5" { 34 | t.Fatalf("expected socks5://, got %s", ptrstr) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 yung innanet (kayos@tcp.direct) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /proto.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | 7 | "git.tcp.direct/kayos/common/pool" 8 | ) 9 | 10 | type ProxyProtocol int8 11 | 12 | const ( 13 | // ProtoNull is a null value for ProxyProtocol. 14 | ProtoNull ProxyProtocol = iota 15 | ProtoSOCKS4 16 | ProtoSOCKS4a 17 | ProtoSOCKS5 18 | ProtoHTTP 19 | ) 20 | 21 | var protoMap = map[ProxyProtocol]string{ 22 | ProtoSOCKS5: "socks5", ProtoNull: "unknown", ProtoSOCKS4: "socks4", ProtoSOCKS4a: "socks4a", 23 | } 24 | 25 | func (p ProxyProtocol) String() string { 26 | return protoMap[p] 27 | } 28 | 29 | type proto struct { 30 | proto *atomic.Value 31 | // immutable 32 | *sync.Once 33 | } 34 | 35 | func newImmutableProto() proto { 36 | p := proto{ 37 | proto: &atomic.Value{}, 38 | Once: &sync.Once{}, 39 | } 40 | p.proto.Store(ProtoNull) 41 | return p 42 | } 43 | 44 | func (p *proto) Get() ProxyProtocol { 45 | return p.proto.Load().(ProxyProtocol) 46 | } 47 | 48 | func (p *proto) set(proxyproto ProxyProtocol) { 49 | p.Do(func() { 50 | p.proto.Store(proxyproto) 51 | }) 52 | } 53 | 54 | func (p ProxyProtocol) writeProtoString(builder *pool.String) { 55 | builder.MustWriteString(p.String()) 56 | } 57 | 58 | func (p ProxyProtocol) writeProtoURI(builder *pool.String) { 59 | p.writeProtoString(builder) 60 | builder.MustWriteString("://") 61 | } 62 | -------------------------------------------------------------------------------- /socks5_server.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "sync" 5 | 6 | "git.tcp.direct/kayos/go-socks5" 7 | 8 | "git.tcp.direct/kayos/common/pool" 9 | ) 10 | 11 | var strs = pool.NewStringFactory() 12 | 13 | type cpool struct { 14 | *sync.Pool 15 | } 16 | 17 | var bufs = cpool{ 18 | Pool: &sync.Pool{ 19 | New: func() interface{} { 20 | return make([]byte, 32*1024) 21 | }, 22 | }, 23 | } 24 | 25 | func (c cpool) Get() []byte { 26 | return c.Pool.Get().([]byte) 27 | } 28 | 29 | func (c cpool) Put(cc []byte) { 30 | c.Pool.Put(cc) 31 | } 32 | 33 | // StartSOCKS5Server starts our rotating proxy SOCKS5 server. 34 | // listen is standard Go listen string, e.g: "127.0.0.1:1080". 35 | // username and password are used for authenticatig to the SOCKS5 server. 36 | func (p5 *ProxyEngine) StartSOCKS5Server(listen, username, password string) error { 37 | opts := []socks5.Option{ 38 | socks5.WithBufferPool(bufs), 39 | socks5.WithLogger(p5.DebugLogger), 40 | socks5.WithDial(p5.DialContext), 41 | } 42 | if username != "" && password != "" { 43 | cator := socks5.UserPassAuthenticator{Credentials: socks5.StaticCredentials{username: password}} 44 | opts = append(opts, socks5.WithAuthMethods([]socks5.Authenticator{cator})) 45 | } 46 | 47 | server := socks5.NewServer(opts...) 48 | 49 | buf := strs.Get() 50 | buf.MustWriteString("listening for SOCKS5 connections on ") 51 | buf.MustWriteString(listen) 52 | p5.dbgPrint(buf) 53 | 54 | return server.ListenAndServe("tcp", listen) 55 | } 56 | -------------------------------------------------------------------------------- /mr_worldwide.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | func (p5 *ProxyEngine) newHTTPClient() any { 11 | timeout := p5.GetServerTimeout() 12 | 13 | hc := &http.Client{ 14 | Transport: &http.Transport{ 15 | DialContext: p5.DialContext, 16 | TLSClientConfig: &tls.Config{InsecureSkipVerify: p5.GetHTTPTLSVerificationStatus() == false}, //nolint:gosec 17 | // TLSHandshakeTimeout: p5.GetServerTimeout(), 18 | DisableKeepAlives: true, 19 | DisableCompression: false, 20 | // MaxIdleConnsPerHost: 5, 21 | // MaxConnsPerHost: 0, 22 | // IdleConnTimeout: 0, 23 | // ResponseHeaderTimeout: 0, 24 | // ExpectContinueTimeout: 0, 25 | // TLSNextProto: nil, 26 | // ProxyConnectHeader: nil, 27 | // GetProxyConnectHeader: nil, 28 | // MaxResponseHeaderBytes: 0, 29 | // WriteBufferSize: 0, 30 | // ReadBufferSize: 0, 31 | // ForceAttemptHTTP2: false, 32 | }, 33 | } 34 | 35 | if timeout != time.Duration(0) { 36 | hc.Timeout = timeout 37 | } 38 | return hc 39 | } 40 | 41 | // GetHTTPClient retrieves a pointer to an http.Client powered by mysteryDialer. 42 | func (p5 *ProxyEngine) GetHTTPClient() *http.Client { 43 | if p5.httpOptsDirty.Load() { 44 | p5.httpClients = &sync.Pool{ 45 | New: p5.newHTTPClient, 46 | } 47 | p5.httpOptsDirty.Store(false) 48 | } 49 | return p5.httpClients.Get().(*http.Client) 50 | } 51 | 52 | // RoundTrip is Mr. WorldWide. Obviously. See: https://pkg.go.dev/net/http#RoundTripper 53 | func (p5 *ProxyEngine) RoundTrip(req *http.Request) (*http.Response, error) { 54 | return p5.GetHTTPClient().Do(req) 55 | } 56 | -------------------------------------------------------------------------------- /scale_util.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "strconv" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | func (p5 *ProxyEngine) scaleDbg() { 10 | if !p5.DebugEnabled() { 11 | return 12 | } 13 | msg := strs.Get() 14 | msg.MustWriteString("job spawner auto scaling, new count: ") 15 | msg.MustWriteString(strconv.Itoa(p5.pool.Cap())) 16 | p5.dbgPrint(msg) 17 | } 18 | 19 | func (p5 *ProxyEngine) scale() (sleep bool) { 20 | select { 21 | case <-p5.scaleTimer.C: 22 | bad := int64(0) 23 | totalBadNow := p5.GetTotalBad() 24 | accountedFor := p5.stats.badAccounted.Load() 25 | netFactors := totalBadNow - accountedFor 26 | if time.Since(*p5.stats.accountingLastDone.Load()) > 5*time.Second && netFactors > 0 { 27 | bad = netFactors 28 | if p5.DebugEnabled() { 29 | p5.DebugLogger.Printf("accounting: %d bad - %d accounted for = %d net factors", 30 | totalBadNow, accountedFor, netFactors) 31 | } 32 | tnow := time.Now() 33 | p5.stats.accountingLastDone.Store(&tnow) 34 | } 35 | // this shouldn't happen..? 36 | if bad < 0 { 37 | panic("scale_util.go: bad < 0") 38 | } 39 | if p5.pool.IsClosed() { 40 | return 41 | } 42 | 43 | totalValidated := p5.GetTotalValidated() 44 | totalConsidered := p5.GetStatistics().Dispensed.Load() + bad 45 | 46 | // if we are considering more than we have validated, cap it at validated so that it registers properly. 47 | // additionally, signal the dialer to slow down a little. 48 | if totalConsidered >= totalValidated { 49 | sleep = true 50 | totalConsidered = totalValidated - atomic.LoadInt64(p5.scaler.Threshold)/2 51 | } 52 | 53 | if p5.scaler.ScaleAnts( 54 | p5.pool, 55 | totalValidated, 56 | totalConsidered, 57 | ) { 58 | p5.scaleDbg() 59 | } 60 | default: 61 | return 62 | } 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module git.tcp.direct/kayos/prox5 2 | 3 | go 1.19 4 | 5 | require ( 6 | git.tcp.direct/kayos/common v0.9.7 7 | git.tcp.direct/kayos/go-socks5 v0.3.0 8 | git.tcp.direct/kayos/socks v0.1.3 9 | github.com/gdamore/tcell/v2 v2.7.1 10 | github.com/miekg/dns v1.1.59 11 | github.com/ooni/oohttp v0.6.7 12 | github.com/orcaman/concurrent-map/v2 v2.0.1 13 | github.com/panjf2000/ants/v2 v2.9.1 14 | github.com/refraction-networking/utls v1.6.0 15 | github.com/rivo/tview v0.0.0-20230208211350-7dfff1ce7854 16 | github.com/yunginnanet/Rate5 v1.3.0 17 | golang.org/x/net v0.25.0 18 | ) 19 | 20 | require ( 21 | github.com/andybalholm/brotli v1.0.5 // indirect 22 | github.com/cloudflare/circl v1.3.6 // indirect 23 | github.com/gdamore/encoding v1.0.0 // indirect 24 | github.com/klauspost/compress v1.16.7 // indirect 25 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 26 | github.com/mattn/go-runewidth v0.0.15 // indirect 27 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 28 | github.com/quic-go/quic-go v0.37.4 // indirect 29 | github.com/rivo/uniseg v0.4.3 // indirect 30 | golang.org/x/crypto v0.23.0 // indirect 31 | golang.org/x/mod v0.16.0 // indirect 32 | golang.org/x/sys v0.20.0 // indirect 33 | golang.org/x/term v0.20.0 // indirect 34 | golang.org/x/text v0.15.0 // indirect 35 | golang.org/x/tools v0.19.0 // indirect 36 | nullprogram.com/x/rng v1.1.0 // indirect 37 | ) 38 | 39 | retract ( 40 | v1.2.2 41 | v1.2.1 42 | v0.9.55 43 | v0.9.54 44 | v0.9.53 45 | v0.9.52 46 | v0.9.51 47 | v0.9.45-0.20230829204418-17ccc7b9a09d 48 | v0.9.44 49 | v0.9.43 50 | v0.9.43 51 | v0.9.42 52 | v0.9.41 53 | v0.9.32-0.20230225131133-5840ef259e1e 54 | v0.9.32-0.20230225110043-d8f03bc8c6c8 55 | v0.9.32-0.20230225105305-4246210cda77 56 | v0.9.32-0.20230225104818-e7e934154ac2 57 | v0.9.31 58 | v0.9.5 59 | v0.9.5-0.20230205030612-6ed439457baa 60 | v0.9.5-0.20230131103931-25c45c57d5a1 61 | v0.9.5-0.20230131102049-cd7067a868b9 62 | v0.9.3 63 | v0.8.4 64 | ) 65 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "time" 5 | 6 | rl "github.com/yunginnanet/Rate5" 7 | ) 8 | 9 | // https://pkg.go.dev/github.com/yunginnanet/Rate5#Policy 10 | var defaultUseProxyRatelimiter = rl.Policy{ 11 | Window: 55, 12 | Burst: 55, 13 | } 14 | 15 | var defaultBadProxyRateLimiter = rl.Policy{ 16 | Window: 55, 17 | Burst: 10, 18 | } 19 | 20 | const ( 21 | stateUnlocked uint32 = iota 22 | stateLocked 23 | ) 24 | 25 | // Proxy represents an individual proxy 26 | type Proxy struct { 27 | // Endpoint is the address:port of the proxy that we connect to 28 | Endpoint string 29 | // ProxiedIP is the address that we end up having when making proxied requests through this proxy 30 | // TODO: parse this and store as flat int type 31 | ProxiedIP string 32 | // protocol is the version/Protocol (currently SOCKS* only) of the proxy 33 | protocol proto 34 | // lastValidated is the time this proxy was last verified working 35 | lastValidated time.Time 36 | // timesValidated is the amount of times the proxy has been validated. 37 | timesValidated int64 38 | // timesBad is the amount of times the proxy has been marked as bad. 39 | timesBad int64 40 | 41 | parent *ProxyEngine 42 | lock uint32 43 | } 44 | 45 | // UniqueKey is an implementation of the Identity interface from Rate5. 46 | // See: https://pkg.go.dev/github.com/yunginnanet/Rate5#Identity 47 | func (sock *Proxy) UniqueKey() string { 48 | return sock.Endpoint 49 | } 50 | 51 | // GetProto retrieves the known protocol value of the Proxy. 52 | func (sock *Proxy) GetProto() ProxyProtocol { 53 | return sock.protocol.Get() 54 | } 55 | 56 | // GetProto safely retrieves the protocol value of the Proxy. 57 | func (sock *Proxy) String() string { 58 | buf := strs.Get() 59 | defer strs.MustPut(buf) 60 | buf.MustWriteString(sock.GetProto().String()) 61 | buf.MustWriteString("://") 62 | buf.MustWriteString(sock.Endpoint) 63 | if sock.parent.GetServerTimeoutStr() != "-1" { 64 | buf.MustWriteString("?timeout=") 65 | buf.MustWriteString(sock.parent.GetServerTimeoutStr()) 66 | buf.MustWriteString("s") 67 | } 68 | return buf.String() 69 | } 70 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import "testing" 4 | 5 | func Test_filter(t *testing.T) { 6 | type args struct { 7 | in string 8 | } 9 | type test struct { 10 | name string 11 | args args 12 | wantFiltered string 13 | wantOk bool 14 | } 15 | var tests = []test{ 16 | { 17 | name: "simple", 18 | args: args{ 19 | in: "127.0.0.1:1080", 20 | }, 21 | wantFiltered: "127.0.0.1:1080", 22 | wantOk: true, 23 | }, 24 | { 25 | name: "withAuth", 26 | args: args{ 27 | in: "127.0.0.1:1080:user:pass", 28 | }, 29 | wantFiltered: "user:pass@127.0.0.1:1080", 30 | wantOk: true, 31 | }, 32 | { 33 | name: "withAuthAlt", 34 | args: args{ 35 | in: "user:pass@127.0.0.1:1080", 36 | }, 37 | wantFiltered: "user:pass@127.0.0.1:1080", 38 | wantOk: true, 39 | }, 40 | { 41 | name: "simpleDomain", 42 | args: args{ 43 | in: "yeet.com:1080", 44 | }, 45 | wantFiltered: "yeet.com:1080", 46 | wantOk: true, 47 | }, 48 | { 49 | name: "domainWithAuth", 50 | args: args{ 51 | in: "yeet.com:1080:user:pass", 52 | }, 53 | wantFiltered: "user:pass@yeet.com:1080", 54 | wantOk: true, 55 | }, 56 | { 57 | name: "ipv6", 58 | args: args{ 59 | in: "[fe80::2ef0:5dff:fe7f:c299]:1080", 60 | }, 61 | wantFiltered: "[fe80::2ef0:5dff:fe7f:c299]:1080", 62 | wantOk: true, 63 | }, 64 | { 65 | name: "ipv6WithAuth", 66 | args: args{ 67 | in: "[fe80::2ef0:5dff:fe7f:c299]:1080:user:pass", 68 | }, 69 | wantFiltered: "user:pass@[fe80::2ef0:5dff:fe7f:c299]:1080", 70 | wantOk: true, 71 | }, 72 | { 73 | name: "invalid", 74 | args: args{ 75 | in: "yeet", 76 | }, 77 | wantFiltered: "", 78 | wantOk: false, 79 | }, 80 | } 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | gotFiltered, gotOk := filter(tt.args.in) 84 | if gotFiltered != tt.wantFiltered { 85 | t.Errorf("filter() gotFiltered = %v, want %v", gotFiltered, tt.wantFiltered) 86 | } 87 | if gotOk != tt.wantOk { 88 | t.Errorf("filter() gotOk = %v, want %v", gotOk, tt.wantOk) 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | ) 7 | 8 | // Statistics is used to encapsulate various proxy engine stats 9 | type Statistics struct { 10 | // Valid4 is the amount of SOCKS4 proxies validated 11 | Valid4 *atomic.Int64 12 | // Valid4a is the amount of SOCKS4a proxies validated 13 | Valid4a *atomic.Int64 14 | // Valid5 is the amount of SOCKS5 proxies validated 15 | Valid5 *atomic.Int64 16 | // ValidHTTP is the amount of HTTP proxies validated 17 | ValidHTTP *atomic.Int64 18 | // Dispensed is a simple ticker to keep track of proxies dispensed via our getters 19 | Dispensed *atomic.Int64 20 | // Stale is the amount of proxies that failed our stale policy upon dispensing 21 | Stale *atomic.Int64 22 | // Checked is the amount of proxies we've checked. 23 | Checked *atomic.Int64 24 | // birthday represents the time we started checking proxies with this pool 25 | birthday *atomic.Pointer[time.Time] 26 | 27 | badAccounted *atomic.Int64 28 | accountingLastDone *atomic.Pointer[time.Time] 29 | } 30 | 31 | func (stats *Statistics) dispense() { 32 | stats.Dispensed.Add(1) 33 | } 34 | 35 | func (stats *Statistics) stale() { 36 | stats.Stale.Add(1) 37 | } 38 | 39 | func (stats *Statistics) v4() { 40 | stats.Valid4.Add(1) 41 | } 42 | 43 | func (stats *Statistics) v4a() { 44 | stats.Valid4a.Add(1) 45 | } 46 | 47 | func (stats *Statistics) v5() { 48 | stats.Valid5.Add(1) 49 | } 50 | 51 | func (stats *Statistics) http() { 52 | stats.ValidHTTP.Add(1) 53 | } 54 | 55 | // GetTotalValidated retrieves our grand total validated proxy count. 56 | func (p5 *ProxyEngine) GetTotalValidated() int64 { 57 | stats := p5.GetStatistics() 58 | 59 | total := int64(0) 60 | for _, val := range []*atomic.Int64{stats.Valid4a, stats.Valid4, stats.Valid5, stats.ValidHTTP} { 61 | total += val.Load() 62 | } 63 | return total 64 | } 65 | 66 | func (p5 *ProxyEngine) GetTotalBad() int64 { 67 | p5.badProx.Patrons.DeleteExpired() 68 | return int64(p5.badProx.Patrons.ItemCount()) 69 | } 70 | 71 | // GetUptime returns the total lifetime duration of our pool. 72 | func (stats *Statistics) GetUptime() time.Duration { 73 | return time.Since(*stats.birthday.Load()) 74 | } 75 | -------------------------------------------------------------------------------- /internal/randtls/shim.go: -------------------------------------------------------------------------------- 1 | package randtls 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net" 7 | 8 | uhttp "github.com/ooni/oohttp" 9 | utls "github.com/refraction-networking/utls" 10 | ) 11 | 12 | // See: https://github.com/ooni/oohttp/blob/main/example/example-utls/tls.go 13 | 14 | type adapter struct { 15 | *utls.UConn 16 | conn net.Conn 17 | } 18 | 19 | // Asserts that we follow the interface. 20 | var _ uhttp.TLSConn = &adapter{} 21 | 22 | // ConnectionState implements the tls.ConnectionState interface. 23 | func (c *adapter) ConnectionState() tls.ConnectionState { 24 | ustate := c.UConn.ConnectionState() 25 | return tls.ConnectionState{ 26 | Version: ustate.Version, 27 | HandshakeComplete: ustate.HandshakeComplete, 28 | DidResume: ustate.DidResume, 29 | CipherSuite: ustate.CipherSuite, 30 | NegotiatedProtocol: ustate.NegotiatedProtocol, 31 | NegotiatedProtocolIsMutual: ustate.NegotiatedProtocolIsMutual, 32 | ServerName: ustate.ServerName, 33 | PeerCertificates: ustate.PeerCertificates, 34 | VerifiedChains: ustate.VerifiedChains, 35 | SignedCertificateTimestamps: ustate.SignedCertificateTimestamps, 36 | OCSPResponse: ustate.OCSPResponse, 37 | TLSUnique: ustate.TLSUnique, 38 | } 39 | } 40 | 41 | // HandshakeContext implements TLSConn's HandshakeContext. 42 | func (c *adapter) HandshakeContext(ctx context.Context) error { 43 | errch := make(chan error, 1) 44 | go func() { 45 | errch <- c.UConn.Handshake() 46 | }() 47 | select { 48 | case err := <-errch: 49 | return err 50 | case <-ctx.Done(): 51 | return ctx.Err() 52 | } 53 | } 54 | 55 | // NetConn implements TLSConn's NetConn 56 | func (c *adapter) NetConn() net.Conn { 57 | return c.conn 58 | } 59 | 60 | // utlsFactory creates a new uTLS connection. 61 | func utlsFactory(conn net.Conn, config *tls.Config) uhttp.TLSConn { 62 | uConfig := &utls.Config{ 63 | RootCAs: config.RootCAs, 64 | NextProtos: config.NextProtos, 65 | ServerName: config.ServerName, 66 | InsecureSkipVerify: config.InsecureSkipVerify, 67 | DynamicRecordSizingDisabled: config.DynamicRecordSizingDisabled, 68 | } 69 | return &adapter{ 70 | UConn: utls.UClient(conn, uConfig, utls.HelloFirefox_55), 71 | conn: conn, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /list_management.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // LoadProxyTXT loads proxies from a given seed file and feeds them to the mapBuilder to be later queued automatically for validation. 12 | // Expects one of the following formats for each line: 13 | // - 127.0.0.1:1080 14 | // - 127.0.0.1:1080:user:pass 15 | // - yeet.com:1080 16 | // - yeet.com:1080:user:pass 17 | // - [fe80::2ef0:5dff:fe7f:c299]:1080 18 | // - [fe80::2ef0:5dff:fe7f:c299]:1080:user:pass 19 | func (p5 *ProxyEngine) LoadProxyTXT(seedFile string) (count int) { 20 | f, err := os.Open(seedFile) 21 | if err != nil { 22 | p5.dbgPrint(simpleString(err.Error())) 23 | return 0 24 | } 25 | 26 | defer func() { 27 | if err := f.Close(); err != nil { 28 | p5.dbgPrint(simpleString(err.Error())) 29 | } 30 | }() 31 | 32 | bs, err := io.ReadAll(f) 33 | if err != nil { 34 | p5.dbgPrint(simpleString(err.Error())) 35 | return 0 36 | } 37 | sockstr := string(bs) 38 | 39 | return p5.LoadMultiLineString(sockstr) 40 | } 41 | 42 | // LoadSingleProxy loads a SOCKS proxy into our map. 43 | // Expects one of the following formats: 44 | // - 127.0.0.1:1080 45 | // - 127.0.0.1:1080:user:pass 46 | // - yeet.com:1080 47 | // - yeet.com:1080:user:pass 48 | // - [fe80::2ef0:5dff:fe7f:c299]:1080 49 | // - [fe80::2ef0:5dff:fe7f:c299]:1080:user:pass 50 | func (p5 *ProxyEngine) LoadSingleProxy(sock string) bool { 51 | var ok bool 52 | if sock, ok = filter(sock); !ok { 53 | p5.dbgPrint(simpleString("invalid proxy format")) 54 | return false 55 | } 56 | if err := p5.loadSingleProxy(sock); err != nil { 57 | p5.dbgPrint(simpleString(err.Error())) 58 | return false 59 | } 60 | // p5.dbgPrint(simpleString("loaded proxy " + sock)) 61 | return true 62 | } 63 | 64 | func (p5 *ProxyEngine) loadSingleProxy(sock string) error { 65 | p, ok := p5.proxyMap.add(sock) 66 | if !ok { 67 | return errors.New("proxy already exists") 68 | } 69 | p5.Pending.add(p) 70 | return nil 71 | } 72 | 73 | // LoadMultiLineString loads a multiine string object with proxy per line. 74 | // Expects one of the following formats for each line: 75 | // - 127.0.0.1:1080 76 | // - 127.0.0.1:1080:user:pass 77 | // - yeet.com:1080 78 | // - yeet.com:1080:user:pass 79 | // - [fe80::2ef0:5dff:fe7f:c299]:1080 80 | // - [fe80::2ef0:5dff:fe7f:c299]:1080:user:pass 81 | func (p5 *ProxyEngine) LoadMultiLineString(socks string) int { 82 | var count int 83 | scan := bufio.NewScanner(strings.NewReader(socks)) 84 | for scan.Scan() { 85 | if err := p5.loadSingleProxy(scan.Text()); err != nil { 86 | continue 87 | } 88 | count++ 89 | } 90 | return count 91 | } 92 | 93 | // ClearSOCKSList clears the map of proxies that we have on record. 94 | // Other operations (proxies that are still in buffered channels) will continue. 95 | func (p5 *ProxyEngine) ClearSOCKSList() { 96 | p5.proxyMap.clear() 97 | } 98 | -------------------------------------------------------------------------------- /internal/scaler/scaler_test.go: -------------------------------------------------------------------------------- 1 | package scaler 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/panjf2000/ants/v2" 7 | ) 8 | 9 | var dummyPool *ants.Pool 10 | 11 | func init() { 12 | var err error 13 | dummyPool, err = ants.NewPool(5, ants.WithNonblocking(false)) 14 | if err != nil { 15 | panic(err) 16 | } 17 | } 18 | 19 | // I know this is a lot of lines unnecessarily. For a test, I don't care. 20 | // With that being said, this test could be much more exhaustive. 21 | // For now, this can serve as a sanity check. 22 | func TestNewAutoScaler(t *testing.T) { 23 | // debugSwitch = true 24 | as := NewAutoScaler(5, 50, 10) 25 | if as.IsOn() { 26 | t.Errorf("AutoScaler should be off by default") 27 | } 28 | if as.Max == nil { 29 | t.Fatalf("Max is nil") 30 | } 31 | if as.Threshold == nil { 32 | t.Fatalf("Threshold is nil") 33 | } 34 | if as.baseline == nil { 35 | t.Fatalf("old is nil") 36 | } 37 | if as.state != stateDisabled { 38 | t.Fatalf("state is not disabled") 39 | } 40 | if as.ScaleAnts(dummyPool, 0, 0) { 41 | t.Fatalf("ScaleAnts should return false") 42 | } 43 | if as.ScaleAnts(dummyPool, 0, 1) { 44 | t.Fatalf("ScaleAnts should return false") 45 | } 46 | if as.ScaleAnts(dummyPool, 10, 9) { 47 | t.Fatalf("ScaleAnts should return false") 48 | } 49 | as.Enable() 50 | if !as.IsOn() { 51 | t.Fatalf("AutoScaler should be on") 52 | } 53 | if as.state != stateIdle { 54 | t.Fatalf("state is not idle") 55 | } 56 | if !as.ScaleAnts(dummyPool, 10, 9) { 57 | t.Fatalf("ScaleAnts should return true") 58 | } 59 | if as.state != stateScalingUp { 60 | t.Fatalf("state is not scaling up") 61 | } 62 | if dummyPool.Cap() != 6 { 63 | t.Fatalf("Pool cap is not 6") 64 | } 65 | if !as.ScaleAnts(dummyPool, 11, 9) { 66 | t.Fatalf("ScaleAnts should return true") 67 | } 68 | if dummyPool.Cap() != 7 { 69 | t.Fatalf("Pool cap is not 7") 70 | } 71 | if !as.ScaleAnts(dummyPool, 12, 9) { 72 | t.Fatalf("ScaleAnts should return true") 73 | } 74 | if dummyPool.Cap() != 8 { 75 | t.Fatalf("Pool cap is not 8") 76 | } 77 | if !as.ScaleAnts(dummyPool, 13, 9) { 78 | t.Fatalf("ScaleAnts should return true") 79 | } 80 | if dummyPool.Cap() != 9 { 81 | t.Fatalf("Pool cap is not 9") 82 | } 83 | if !as.ScaleAnts(dummyPool, 21, 9) { 84 | t.Fatalf("ScaleAnts should return true") 85 | } 86 | if dummyPool.Cap() != 8 { 87 | t.Fatalf("Pool cap is not 8") 88 | } 89 | if !as.ScaleAnts(dummyPool, 21, 9) { 90 | t.Fatalf("ScaleAnts should return true") 91 | } 92 | if dummyPool.Cap() != 7 { 93 | t.Fatalf("Pool cap is not 7") 94 | } 95 | if !as.ScaleAnts(dummyPool, 21, 9) { 96 | t.Fatalf("ScaleAnts should return true") 97 | } 98 | if dummyPool.Cap() != 6 { 99 | t.Fatalf("Pool cap is not 6") 100 | } 101 | if !as.ScaleAnts(dummyPool, 21, 9) { 102 | t.Fatalf("ScaleAnts should return true") 103 | } 104 | if dummyPool.Cap() != 5 { 105 | t.Fatalf("Pool cap is not 5") 106 | } 107 | if as.ScaleAnts(dummyPool, 21, 9) { 108 | t.Fatalf("ScaleAnts should return false") 109 | } 110 | if dummyPool.Cap() != 5 { 111 | t.Fatalf("Pool cap is not 5") 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /conductor.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "errors" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | // engineState represents the current state of our ProxyEngine. 10 | type engineState uint32 11 | 12 | const ( 13 | // stateRunning means the proxy pool is currently taking in proxys and validating them, and is available to dispense proxies. 14 | stateRunning engineState = iota 15 | // statePaused means the proxy pool has been with ProxyEngine.Pause() and may be resumed with ProxyEngine.Resume() 16 | statePaused 17 | // stateNew means the proxy pool has never been started. 18 | stateNew 19 | ) 20 | 21 | // Start starts our proxy pool operations. Trying to start a running ProxyEngine will return an error. 22 | func (p5 *ProxyEngine) Start() error { 23 | if atomic.LoadUint32(&p5.Status) != uint32(stateNew) { 24 | p5.DebugLogger.Printf("proxy pool has been started before, resuming instead") 25 | return p5.Resume() 26 | } 27 | p5.DebugLogger.Printf("starting prox5") 28 | p5.startDaemons() 29 | return nil 30 | } 31 | 32 | /* 33 | Pause will cease the creation of any new proxy validation operations. 34 | - You will be able to start the proxy pool again with ProxyEngine.Resume(), it will have the same Statistics, options, and ratelimits. 35 | - During pause you are still able to dispense proxies. 36 | - Options may be changed and proxy lists may be loaded when paused. 37 | - Pausing an already paused ProxyEngine is a nonop. 38 | */ 39 | func (p5 *ProxyEngine) Pause() error { 40 | if !p5.IsRunning() { 41 | return errors.New("not running") 42 | } 43 | 44 | p5.dbgPrint(simpleString("pausing proxy pool")) 45 | 46 | // p5.quit() 47 | 48 | atomic.StoreUint32(&p5.Status, uint32(statePaused)) 49 | 50 | return nil 51 | } 52 | 53 | func (p5 *ProxyEngine) startDaemons() { 54 | go p5.jobSpawner() 55 | atomic.StoreUint32(&p5.Status, uint32(stateRunning)) 56 | p5.DebugLogger.Printf("prox5 started") 57 | } 58 | 59 | // Resume will resume pause proxy pool operations, attempting to resume a running ProxyEngine is returns an error. 60 | func (p5 *ProxyEngine) Resume() error { 61 | if p5.IsRunning() { 62 | return errors.New("already running") 63 | } 64 | // p5.ctx, p5.quit = context.WithCancel(context.Background()) 65 | p5.startDaemons() 66 | return nil 67 | } 68 | 69 | // CloseAllConns will (maybe) close all connections in progress by the dialers (including the SOCKS server if in use). 70 | // Note this does not effect the proxy pool, it will continue to operate as normal. 71 | // this is hacky FIXME 72 | func (p5 *ProxyEngine) CloseAllConns() { 73 | timeout := time.NewTicker(5 * time.Second) 74 | defer func() { 75 | timeout.Stop() 76 | select { 77 | case p5.conKiller <- struct{}{}: 78 | default: 79 | } 80 | }() 81 | for { 82 | select { 83 | case p5.conKiller <- struct{}{}: 84 | timeout.Reset(1 * time.Second) 85 | p5.DebugLogger.Printf("killed a connection") 86 | case <-p5.ctx.Done(): 87 | return 88 | case <-timeout.C: 89 | return 90 | } 91 | } 92 | } 93 | 94 | func (p5 *ProxyEngine) Close() error { 95 | p5.mu.Lock() 96 | defer p5.mu.Unlock() 97 | p5.quit() 98 | return p5.Pause() 99 | } 100 | -------------------------------------------------------------------------------- /daemons.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "time" 7 | 8 | "git.tcp.direct/kayos/common/entropy" 9 | cmap "github.com/orcaman/concurrent-map/v2" 10 | ) 11 | 12 | type proxyMap struct { 13 | plot cmap.ConcurrentMap[string, *Proxy] 14 | parent *ProxyEngine 15 | } 16 | 17 | func (sm proxyMap) add(sock string) (*Proxy, bool) { 18 | sm.plot.SetIfAbsent(sock, &Proxy{ 19 | Endpoint: sock, 20 | protocol: newImmutableProto(), 21 | lastValidated: time.UnixMilli(0), 22 | timesValidated: 0, 23 | timesBad: 0, 24 | parent: sm.parent, 25 | lock: stateUnlocked, 26 | }) 27 | 28 | return sm.plot.Get(sock) 29 | } 30 | 31 | func (sm proxyMap) delete(sock string) error { 32 | if _, ok := sm.plot.Get(sock); !ok { 33 | return errors.New("proxy not found") 34 | } 35 | sm.plot.Remove(sock) 36 | return nil 37 | } 38 | 39 | func (sm proxyMap) clear() { 40 | sm.plot.Clear() 41 | } 42 | 43 | func (p5 *ProxyEngine) recycling() int { 44 | if !p5.recycleMu.TryLock() { 45 | return 0 46 | } 47 | defer p5.recycleMu.Unlock() 48 | 49 | switch { 50 | case !p5.GetRecyclingStatus(), p5.proxyMap.plot.Count() < 1: 51 | return 0 52 | default: 53 | select { 54 | case <-p5.recycleTimer.C: 55 | break 56 | default: 57 | return 0 58 | } 59 | } 60 | 61 | var count = 0 62 | 63 | switch p5.GetRecyclerShuffleStatus() { 64 | case true: 65 | var tuples []cmap.Tuple[string, *Proxy] 66 | for tuple := range p5.proxyMap.plot.IterBuffered() { 67 | tuples = append(tuples, tuple) 68 | } 69 | entropy.GetOptimizedRand().Shuffle(len(tuples), func(i, j int) { 70 | tuples[i], tuples[j] = tuples[j], tuples[i] 71 | }) 72 | for _, tuple := range tuples { 73 | p5.Pending.add(tuple.Val) 74 | count++ 75 | } 76 | case false: 77 | for tuple := range p5.proxyMap.plot.IterBuffered() { 78 | p5.Pending.add(tuple.Val) 79 | count++ 80 | } 81 | } 82 | 83 | return count 84 | } 85 | 86 | func (p5 *ProxyEngine) jobSpawner() { 87 | p5.pool.Reboot() 88 | 89 | p5.dbgPrint(simpleString("job spawner started")) 90 | 91 | q := make(chan bool, 1) 92 | 93 | go func() { 94 | for { 95 | if !p5.IsRunning() { 96 | q <- true 97 | return 98 | } 99 | // select { 100 | // case <-p5.ctx.Done(): 101 | // default: 102 | // } 103 | p5.Pending.RLock() 104 | if p5.Pending.Len() < 1 { 105 | p5.Pending.RUnlock() 106 | count := p5.recycling() 107 | switch { 108 | case count > 0: 109 | buf := strs.Get() 110 | buf.MustWriteString("recycled ") 111 | buf.MustWriteString(strconv.Itoa(count)) 112 | buf.MustWriteString(" proxies from our map") 113 | p5.dbgPrint(buf) 114 | default: 115 | time.Sleep(time.Millisecond * 100) 116 | } 117 | continue 118 | } else { 119 | p5.Pending.RUnlock() 120 | } 121 | 122 | var sock *Proxy 123 | 124 | p5.Pending.Lock() 125 | switch p5.GetRecyclingStatus() { 126 | case true: 127 | el := p5.Pending.Front() 128 | p5.Pending.MoveToBack(el) 129 | sock = el.Value.(*Proxy) 130 | } 131 | p5.Pending.Unlock() 132 | 133 | _ = p5.scale() 134 | if err := p5.pool.Submit(sock.validate); err != nil { 135 | p5.dbgPrint(simpleString(err.Error())) 136 | } 137 | 138 | } 139 | }() 140 | 141 | <-q 142 | p5.dbgPrint(simpleString("job spawner paused")) 143 | p5.pool.Release() 144 | } 145 | -------------------------------------------------------------------------------- /dispense.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | ) 7 | 8 | func (p5 *ProxyEngine) getSocksStr(proto ProxyProtocol) string { 9 | var sock *Proxy 10 | var list *proxyList 11 | switch proto { 12 | case ProtoSOCKS4: 13 | list = &p5.Valids.SOCKS4 14 | case ProtoSOCKS4a: 15 | list = &p5.Valids.SOCKS4a 16 | case ProtoSOCKS5: 17 | list = &p5.Valids.SOCKS5 18 | case ProtoHTTP: 19 | list = &p5.Valids.HTTP 20 | } 21 | for { 22 | if list.Len() == 0 { 23 | p5.recycling() 24 | time.Sleep(250 * time.Millisecond) 25 | continue 26 | } 27 | list.Lock() 28 | sock = list.Remove(list.Front()).(*Proxy) 29 | list.Unlock() 30 | switch { 31 | case sock == nil: 32 | p5.recycling() 33 | time.Sleep(250 * time.Millisecond) 34 | continue 35 | case !p5.stillGood(sock): 36 | continue 37 | default: 38 | p5.stats.dispense() 39 | return sock.Endpoint 40 | } 41 | } 42 | } 43 | 44 | // Socks5Str gets a SOCKS5 proxy that we have fully verified (dialed and then retrieved our IP address from a what-is-my-ip endpoint. 45 | // Will block if one is not available! 46 | func (p5 *ProxyEngine) Socks5Str() string { 47 | return p5.getSocksStr(ProtoSOCKS5) 48 | } 49 | 50 | // Socks4Str gets a SOCKS4 proxy that we have fully verified. 51 | // Will block if one is not available! 52 | func (p5 *ProxyEngine) Socks4Str() string { 53 | return p5.getSocksStr(ProtoSOCKS4) 54 | } 55 | 56 | // Socks4aStr gets a SOCKS4 proxy that we have fully verified. 57 | // Will block if one is not available! 58 | func (p5 *ProxyEngine) Socks4aStr() string { 59 | return p5.getSocksStr(ProtoSOCKS4a) 60 | } 61 | 62 | // GetHTTPTunnel checks for an available HTTP CONNECT proxy in our pool. 63 | func (p5 *ProxyEngine) GetHTTPTunnel() string { 64 | return p5.getSocksStr(ProtoHTTP) 65 | } 66 | 67 | // GetAnySOCKS retrieves any version SOCKS proxy as a Proxy type 68 | // Will block if one is not available! 69 | func (p5 *ProxyEngine) GetAnySOCKS() *Proxy { 70 | defer p5.stats.dispense() 71 | 72 | for { 73 | var sock *Proxy 74 | select { 75 | case <-p5.ctx.Done(): 76 | return nil 77 | default: 78 | time.Sleep(2 * time.Millisecond) 79 | } 80 | for _, list := range p5.Valids.Slice() { 81 | list.RLock() 82 | if list.Len() < 1 { 83 | time.Sleep(15 * time.Millisecond) 84 | list.RUnlock() 85 | continue 86 | } 87 | 88 | list.RUnlock() 89 | sock = list.pop() 90 | switch { 91 | case sock == nil: 92 | p5.recycling() 93 | time.Sleep(50 * time.Millisecond) 94 | case p5.stillGood(sock): 95 | return sock 96 | default: 97 | } 98 | continue 99 | } 100 | } 101 | } 102 | 103 | func (p5 *ProxyEngine) stillGood(sock *Proxy) bool { 104 | if sock == nil { 105 | return false 106 | } 107 | if !atomic.CompareAndSwapUint32(&sock.lock, stateUnlocked, stateLocked) { 108 | return false 109 | } 110 | defer atomic.StoreUint32(&sock.lock, stateUnlocked) 111 | 112 | if p5.GetRemoveAfter() != -1 && atomic.LoadInt64(&sock.timesBad) > int64(p5.GetRemoveAfter()) { 113 | buf := strs.Get() 114 | buf.MustWriteString("deleting from map (too many failures): ") 115 | buf.MustWriteString(sock.Endpoint) 116 | p5.dbgPrint(buf) 117 | if err := p5.proxyMap.delete(sock.Endpoint); err != nil { 118 | p5.dbgPrint(simpleString(err.Error())) 119 | } 120 | return false 121 | } 122 | 123 | if p5.badProx.Peek(sock) { 124 | p5.msgBadProxRate(sock) 125 | return false 126 | } 127 | 128 | if time.Since(sock.lastValidated) > p5.opt.stale { 129 | buf := strs.Get() 130 | buf.MustWriteString("proxy stale: ") 131 | buf.MustWriteString(sock.Endpoint) 132 | p5.dbgPrint(buf) 133 | p5.stats.stale() 134 | return false 135 | } 136 | 137 | return true 138 | } 139 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/miekg/dns" 8 | "net/netip" 9 | ) 10 | 11 | func filterv6(in string) (filtered string, ok bool) { 12 | split := strings.Split(in, "]:") 13 | if len(split) < 2 { 14 | 15 | return "", false 16 | } 17 | split2 := strings.Split(split[1], ":") 18 | switch len(split2) { 19 | case 0: 20 | combo, err := netip.ParseAddrPort(buildProxyString("", "", split[0], split2[0], true)) 21 | if err == nil { 22 | return combo.String(), true 23 | } 24 | case 1: 25 | concat := buildProxyString("", "", split[0], split2[0], true) 26 | combo, err := netip.ParseAddrPort(concat) 27 | if err == nil { 28 | return combo.String(), true 29 | } 30 | default: 31 | _, err := netip.ParseAddrPort(buildProxyString("", "", split[0], split2[0], true)) 32 | if err == nil { 33 | return buildProxyString(split2[1], split2[2], split[0], split2[0], true), true 34 | } 35 | } 36 | return "", true 37 | } 38 | 39 | func isNumber(s string) bool { 40 | _, err := strconv.Atoi(s) 41 | return err == nil 42 | } 43 | 44 | func buildProxyString(username, password, address, port string, v6 bool) (result string) { 45 | builder := strs.Get() 46 | defer strs.MustPut(builder) 47 | if username != "" && password != "" { 48 | builder.MustWriteString(username) 49 | builder.MustWriteString(":") 50 | builder.MustWriteString(password) 51 | builder.MustWriteString("@") 52 | } 53 | builder.MustWriteString(address) 54 | if v6 { 55 | builder.MustWriteString("]") 56 | } 57 | builder.MustWriteString(":") 58 | builder.MustWriteString(port) 59 | return builder.String() 60 | } 61 | 62 | func filter(in string) (filtered string, ok bool) { //nolint:cyclop 63 | if !strings.Contains(in, ":") { 64 | return "", false 65 | } 66 | split := strings.Split(in, ":") 67 | 68 | if len(split) < 2 { 69 | return "", false 70 | } 71 | switch len(split) { 72 | case 2: 73 | _, isDomain := dns.IsDomainName(split[0]) 74 | if isDomain && isNumber(split[1]) { 75 | return in, true 76 | } 77 | combo, err := netip.ParseAddrPort(in) 78 | if err != nil { 79 | return "", false 80 | } 81 | return combo.String(), true 82 | case 3: 83 | if !strings.Contains(in, "@") { 84 | return "", false 85 | } 86 | split := strings.Split(in, "@") 87 | if !strings.Contains(split[0], ":") { 88 | return "", false 89 | } 90 | splitAuth := strings.Split(split[0], ":") 91 | splitServ := strings.Split(split[1], ":") 92 | _, isDomain := dns.IsDomainName(splitServ[0]) 93 | if isDomain && isNumber(splitServ[1]) { 94 | return buildProxyString(splitAuth[0], splitAuth[1], 95 | splitServ[0], splitServ[1], false), true 96 | } 97 | if _, err := netip.ParseAddrPort(split[1]); err == nil { 98 | return buildProxyString(splitAuth[0], splitAuth[1], 99 | splitServ[0], splitServ[1], false), true 100 | } 101 | case 4: 102 | _, isDomain := dns.IsDomainName(split[0]) 103 | if isDomain && isNumber(split[1]) { 104 | return buildProxyString(split[2], split[3], split[0], split[1], false), true 105 | } 106 | _, isDomain = dns.IsDomainName(split[2]) 107 | if isDomain && isNumber(split[3]) { 108 | return buildProxyString(split[0], split[1], split[2], split[3], false), true 109 | } 110 | if _, err := netip.ParseAddrPort(split[2] + ":" + split[3]); err == nil { 111 | return buildProxyString(split[0], split[1], split[2], split[3], false), true 112 | } 113 | if _, err := netip.ParseAddrPort(split[0] + ":" + split[1]); err == nil { 114 | return buildProxyString(split[2], split[3], split[0], split[1], false), true 115 | } 116 | default: 117 | if !strings.Contains(in, "[") || !strings.Contains(in, "]:") { 118 | return "", false 119 | } 120 | } 121 | return filterv6(in) 122 | } 123 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/gdamore/tcell/v2" 9 | // "github.com/haxii/socks5" 10 | 11 | "git.tcp.direct/kayos/prox5" 12 | 13 | "github.com/rivo/tview" 14 | ) 15 | 16 | var swamp *prox5.ProxyEngine 17 | 18 | type socksLogger struct{} 19 | 20 | var socklog = socksLogger{} 21 | 22 | func StartUpstreamProxy(listen string) { 23 | if err := swamp.StartSOCKS5Server(listen, "", ""); err != nil { 24 | panic(err) 25 | } 26 | } 27 | 28 | func init() { 29 | swamp = prox5.NewProxyEngine() 30 | swamp.SetMaxWorkers(5) 31 | swamp.EnableDebug() 32 | swamp.SetDebugLogger(socklog) 33 | swamp.DisableDebugRedaction() 34 | // swamp.EnableDebugRedaction() 35 | swamp.EnableAutoScaler() 36 | go StartUpstreamProxy("127.0.0.1:1555") 37 | 38 | count := swamp.LoadProxyTXT(os.Args[1]) 39 | if count < 1 { 40 | socklog.Printf("file contained no valid SOCKS host:port combos") 41 | os.Exit(1) 42 | } 43 | 44 | if err := swamp.Start(); err != nil { 45 | panic(err) 46 | } 47 | 48 | socklog.Printf("[USAGE] q: quit | d: debug | p: pause/unpause") 49 | } 50 | 51 | const statsFmt = ">>>>>-----<<<<<\n>>>>>Prox5<<<<<\n>>>>>-----<<<<<\n\nUptime: %s\n\nValidated: %d\nDispensed: %d\n\nMaximum Workers: %d\nActive Workers: %d\nAsleep Workers: %d\n\nAutoScale: %s\nSOCKS5 listening on 127.0.0.1:1555\n\n----------\n%s" 52 | 53 | var ( 54 | background *tview.TextView 55 | window *tview.Modal 56 | app *tview.Application 57 | ) 58 | 59 | var last string 60 | 61 | func currentString(lastMessage string) string { 62 | if lastMessage != last && lastMessage != "" { 63 | last = lastMessage 64 | } 65 | if lastMessage == "" { 66 | lastMessage = last 67 | } 68 | if swamp == nil { 69 | return "" 70 | } 71 | stats := swamp.GetStatistics() 72 | wMax, wRun, wIdle := swamp.GetWorkers() 73 | return fmt.Sprintf(statsFmt, 74 | stats.GetUptime().Round(time.Second), swamp.GetTotalValidated(), 75 | stats.Dispensed, wMax, wRun, wIdle, swamp.GetAutoScalerStateString(), lastMessage) 76 | } 77 | 78 | func (s socksLogger) Errorf(format string, a ...interface{}) { 79 | s.Printf(format, a...) 80 | } 81 | 82 | func (s socksLogger) Printf(format string, a ...interface{}) { 83 | if app == nil { 84 | return 85 | } 86 | msg := fmt.Sprintf(format, a...) 87 | if msg == "" { 88 | return 89 | } 90 | app.QueueUpdateDraw(func() { 91 | window.SetText(currentString(msg)) 92 | }) 93 | } 94 | 95 | func (s socksLogger) Print(str string) { 96 | if app == nil { 97 | return 98 | } 99 | app.QueueUpdateDraw(func() { 100 | window.SetText(currentString(str)) 101 | }) 102 | } 103 | 104 | func buttons(buttonIndex int, buttonLabel string) { 105 | switch buttonIndex { 106 | case 0: 107 | app.Stop() 108 | case 1: 109 | if swamp.IsRunning() { 110 | err := swamp.Pause() 111 | if err != nil { 112 | socklog.Printf(err.Error()) 113 | } 114 | } else { 115 | if err := swamp.Resume(); err != nil { 116 | socklog.Printf(err.Error()) 117 | } 118 | } 119 | case 2: 120 | swamp.SetMaxWorkers(swamp.GetMaxWorkers() + 1) 121 | case 3: 122 | swamp.SetMaxWorkers(swamp.GetMaxWorkers() - 1) 123 | default: 124 | app.Stop() 125 | } 126 | } 127 | 128 | func main() { 129 | app = tview.NewApplication() 130 | 131 | go func() { 132 | for { 133 | time.Sleep(500 * time.Millisecond) 134 | app.QueueUpdateDraw(func() { 135 | window.SetText(currentString("")) 136 | }) 137 | app.Sync() 138 | } 139 | }() 140 | 141 | window = tview.NewModal(). 142 | SetText(currentString("Initialize")). 143 | AddButtons([]string{"Quit", "Pause", "+", "-"}). 144 | SetDoneFunc(buttons). 145 | SetBackgroundColor(tcell.ColorBlack).SetTextColor(tcell.ColorWhite) 146 | 147 | modal := func(p tview.Primitive, width, height int) tview.Primitive { 148 | return tview.NewFlex(). 149 | AddItem(nil, 0, 1, false). 150 | AddItem(tview.NewFlex().SetDirection(tview.FlexRow). 151 | AddItem(nil, 0, 1, false). 152 | AddItem(p, height, 1, true). 153 | AddItem(nil, 0, 1, false), width, 1, true). 154 | AddItem(nil, 0, 1, false) 155 | } 156 | 157 | background = tview.NewTextView(). 158 | SetTextColor(tcell.ColorGray).SetTextAlign(tview.AlignLeft) 159 | 160 | pages := tview.NewPages(). 161 | AddPage("background", background, true, true). 162 | AddPage("window", modal(window, 150, 50), true, true) 163 | 164 | if err := app.SetRoot(pages, false).Run(); err != nil { 165 | panic(err) 166 | } 167 | swamp.SetDebugLogger(socklog) 168 | 169 | // Initialize() 170 | } 171 | -------------------------------------------------------------------------------- /getters.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "strconv" 5 | "sync/atomic" 6 | "time" 7 | 8 | "git.tcp.direct/kayos/common/entropy" 9 | ) 10 | 11 | // GetStatistics returns all Statistics atomics. 12 | func (p5 *ProxyEngine) GetStatistics() Statistics { 13 | p5.mu.RLock() 14 | defer p5.mu.RUnlock() 15 | return p5.stats 16 | } 17 | 18 | // RandomUserAgent retrieves a random user agent from our list in string form. 19 | func (p5 *ProxyEngine) RandomUserAgent() string { 20 | p5.mu.RLock() 21 | defer p5.mu.RUnlock() 22 | return entropy.RandomStrChoice(p5.opt.userAgents) 23 | } 24 | 25 | // GetRandomEndpoint returns a random whatismyip style endpoint from our ProxyEngine's options 26 | func (p5 *ProxyEngine) GetRandomEndpoint() string { 27 | p5.mu.RLock() 28 | defer p5.mu.RUnlock() 29 | return entropy.RandomStrChoice(p5.opt.checkEndpoints) 30 | } 31 | 32 | // GetStaleTime returns the duration of time after which a proxy will be considered "stale". 33 | func (p5 *ProxyEngine) GetStaleTime() time.Duration { 34 | p5.opt.RLock() 35 | defer p5.opt.RUnlock() 36 | return p5.opt.stale 37 | } 38 | 39 | // GetValidationTimeout returns the current value of validationTimeout. 40 | func (p5 *ProxyEngine) GetValidationTimeout() time.Duration { 41 | p5.opt.RLock() 42 | defer p5.opt.RUnlock() 43 | return p5.opt.validationTimeout 44 | } 45 | 46 | // GetValidationTimeoutStr returns the current value of validationTimeout (in seconds string). 47 | func (p5 *ProxyEngine) GetValidationTimeoutStr() string { 48 | p5.opt.RLock() 49 | defer p5.opt.RUnlock() 50 | timeout := p5.opt.validationTimeout 51 | return strconv.Itoa(int(timeout / time.Second)) 52 | } 53 | 54 | // GetServerTimeout returns the current value of serverTimeout. 55 | func (p5 *ProxyEngine) GetServerTimeout() time.Duration { 56 | p5.opt.RLock() 57 | defer p5.opt.RUnlock() 58 | return p5.opt.serverTimeout 59 | } 60 | 61 | // GetServerTimeoutStr returns the current value of serverTimeout (in seconds string). 62 | func (p5 *ProxyEngine) GetServerTimeoutStr() string { 63 | p5.opt.RLock() 64 | defer p5.opt.RUnlock() 65 | timeout := p5.opt.serverTimeout 66 | if timeout == time.Duration(0) { 67 | return "-1" 68 | } 69 | return strconv.Itoa(int(timeout / time.Second)) 70 | } 71 | 72 | // GetMaxWorkers returns maximum amount of workers that validate proxies concurrently. Note this is read-only during runtime. 73 | func (p5 *ProxyEngine) GetMaxWorkers() int { 74 | return p5.pool.Cap() 75 | } 76 | 77 | // IsRunning returns true if our background goroutines defined in daemons.go are currently operational 78 | func (p5 *ProxyEngine) IsRunning() bool { 79 | return atomic.LoadUint32(&p5.Status) == 0 80 | } 81 | 82 | // GetRecyclingStatus retrieves the current recycling status, see EnableRecycling. 83 | func (p5 *ProxyEngine) GetRecyclingStatus() bool { 84 | p5.opt.RLock() 85 | defer p5.opt.RUnlock() 86 | return p5.opt.recycle 87 | } 88 | 89 | // GetWorkers retrieves pond worker Statistics: 90 | // - return MaxWorkers, RunningWorkers, IdleWorkers 91 | func (p5 *ProxyEngine) GetWorkers() (maxWorkers, runningWorkers, idleWorkers int) { 92 | p5.mu.RLock() 93 | defer p5.mu.RUnlock() 94 | return p5.pool.Cap(), p5.pool.Running(), p5.pool.Free() 95 | } 96 | 97 | // GetRemoveAfter retrieves the removeafter policy, the amount of times a recycled proxy is marked as bad until it is removed entirely. 98 | // - returns -1 if recycling is disabled. 99 | func (p5 *ProxyEngine) GetRemoveAfter() int { 100 | p5.mu.RLock() 101 | defer p5.mu.RUnlock() 102 | if !p5.opt.recycle { 103 | return -1 104 | } 105 | return p5.opt.removeafter 106 | } 107 | 108 | // GetDialerBailout retrieves the dialer bailout policy. See SetDialerBailout for more info. 109 | func (p5 *ProxyEngine) GetDialerBailout() int { 110 | p5.mu.RLock() 111 | defer p5.mu.RUnlock() 112 | return p5.opt.dialerBailout 113 | } 114 | 115 | // TODO: Document middleware concept 116 | 117 | func (p5 *ProxyEngine) GetDispenseMiddleware() func(*Proxy) (*Proxy, bool) { 118 | p5.mu.RLock() 119 | defer p5.mu.RUnlock() 120 | return p5.dispenseMiddleware 121 | } 122 | 123 | func (p5 *ProxyEngine) GetRecyclerShuffleStatus() bool { 124 | p5.mu.RLock() 125 | defer p5.mu.RUnlock() 126 | return p5.opt.shuffle 127 | } 128 | 129 | func (p5 *ProxyEngine) GetAutoScalerStatus() bool { 130 | return p5.scaler.IsOn() 131 | } 132 | 133 | func (p5 *ProxyEngine) GetAutoScalerStateString() string { 134 | return p5.scaler.StateString() 135 | } 136 | 137 | func (p5 *ProxyEngine) GetDebugRedactStatus() bool { 138 | p5.mu.RLock() 139 | defer p5.mu.RUnlock() 140 | return p5.opt.redact 141 | } 142 | 143 | func (p5 *ProxyEngine) GetHTTPTLSVerificationStatus() bool { 144 | p5.mu.RLock() 145 | defer p5.mu.RUnlock() 146 | return p5.opt.tlsVerify 147 | } 148 | -------------------------------------------------------------------------------- /mystery_dialer.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net" 8 | "os" 9 | "sync/atomic" 10 | "time" 11 | 12 | "git.tcp.direct/kayos/socks" 13 | ) 14 | 15 | // DialContext is a simple stub adapter to implement a net.Dialer. 16 | func (p5 *ProxyEngine) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { 17 | return p5.mysteryDialer(ctx, network, addr) 18 | } 19 | 20 | // Dial is a simple stub adapter to implement a net.Dialer. 21 | func (p5 *ProxyEngine) Dial(network, addr string) (net.Conn, error) { 22 | return p5.mysteryDialer(context.Background(), network, addr) 23 | } 24 | 25 | // DialTimeout is a simple stub adapter to implement a net.Dialer with a timeout. 26 | func (p5 *ProxyEngine) DialTimeout(network, addr string, timeout time.Duration) (net.Conn, error) { 27 | ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(timeout)) 28 | defer cancel() 29 | nc, err := p5.mysteryDialer(ctx, network, addr) 30 | return nc, err 31 | } 32 | 33 | func (p5 *ProxyEngine) addTimeout(socksString string) string { 34 | tout := strs.Get() 35 | tout.MustWriteString(socksString) 36 | tout.MustWriteString("?timeout=") 37 | tout.MustWriteString(p5.GetServerTimeoutStr()) 38 | _, _ = tout.WriteRune('s') 39 | socksString = tout.String() 40 | strs.MustPut(tout) 41 | return socksString 42 | } 43 | 44 | func (p5 *ProxyEngine) isEmpty() bool { 45 | if p5.GetStatistics().Checked.Load() == 0 { 46 | return true 47 | } 48 | // if stats.Valid5.Load()+stats.Valid4.Load()+stats.Valid4a.Load()+stats.ValidHTTP.Load() == 0 { 49 | if p5.GetTotalValidated() == 0 { 50 | return true 51 | } 52 | return false 53 | } 54 | 55 | var ErrNoProxies = fmt.Errorf("no proxies available") 56 | 57 | func (p5 *ProxyEngine) popSockAndLockIt(ctx context.Context) (*Proxy, error) { 58 | if p5.isEmpty() { 59 | p5.scale() 60 | return nil, ErrNoProxies 61 | } 62 | sock := p5.GetAnySOCKS() 63 | select { 64 | case <-ctx.Done(): 65 | return nil, fmt.Errorf("context done: %w", ctx.Err()) 66 | default: 67 | // 68 | } 69 | if sock == nil { 70 | return nil, nil 71 | } 72 | if atomic.CompareAndSwapUint32(&sock.lock, stateUnlocked, stateLocked) { 73 | // p5.msgGotLock(socksString) 74 | return sock, nil 75 | } 76 | switch sock.GetProto() { 77 | case ProtoSOCKS5: 78 | p5.Valids.SOCKS5.add(sock) 79 | case ProtoSOCKS4: 80 | p5.Valids.SOCKS4.add(sock) 81 | case ProtoSOCKS4a: 82 | p5.Valids.SOCKS4a.add(sock) 83 | case ProtoHTTP: 84 | p5.Valids.HTTP.add(sock) 85 | default: 86 | return nil, fmt.Errorf("unknown protocol: %s", sock.GetProto()) 87 | } 88 | 89 | return nil, nil 90 | } 91 | 92 | func (p5 *ProxyEngine) announceDial(network, addr string) { 93 | s := strs.Get() 94 | s.MustWriteString("prox5 dialing: ") 95 | s.MustWriteString(network) 96 | s.MustWriteString("://") 97 | if p5.opt.redact { 98 | s.MustWriteString("[redacted]") 99 | } else { 100 | s.MustWriteString(addr) 101 | } 102 | s.MustWriteString(addr) 103 | s.MustWriteString("...") 104 | p5.dbgPrint(s) 105 | } 106 | 107 | // mysteryDialer is a dialer function that will use a different proxy for every request. 108 | // If you're looking for this function, it has been unexported. Use Dial, DialTimeout, or DialContext instead. 109 | func (p5 *ProxyEngine) mysteryDialer(ctx context.Context, network, addr string) (net.Conn, error) { 110 | p5.announceDial(network, addr) 111 | 112 | if p5.isEmpty() { 113 | // p5.dbgPrint(simpleString("prox5: no proxies available")) 114 | return nil, ErrNoProxies 115 | } 116 | 117 | timeout := time.NewTimer(p5.GetServerTimeout()) 118 | defer timeout.Stop() 119 | 120 | // pull down proxies from channel until we get a proxy good enough for our spoiled asses 121 | var count = 0 122 | for { 123 | maxBail := p5.GetDialerBailout() 124 | switch { 125 | case count > maxBail: 126 | return nil, fmt.Errorf("giving up after %d tries", maxBail) 127 | case ctx.Err() != nil: 128 | return nil, fmt.Errorf("context error: %w", ctx.Err()) 129 | default: 130 | select { 131 | case <-ctx.Done(): 132 | return nil, fmt.Errorf("context done: %w", ctx.Err()) 133 | case <-p5.ctx.Done(): 134 | return nil, fmt.Errorf("prox5 closed: %w", p5.ctx.Err()) 135 | case <-p5.conKiller: 136 | return nil, fmt.Errorf("prox5 closed: %w", io.ErrClosedPipe) 137 | case <-timeout.C: 138 | return nil, fmt.Errorf("timeout: %w, %w", io.ErrClosedPipe, os.ErrDeadlineExceeded) 139 | default: 140 | } 141 | } 142 | var sock *Proxy 143 | for { 144 | if p5.scale() { 145 | time.Sleep(5 * time.Millisecond) 146 | } 147 | var err error 148 | sock, err = p5.popSockAndLockIt(ctx) 149 | if err != nil { 150 | // println(err.Error()) 151 | return nil, err 152 | } 153 | if sock != nil { 154 | break 155 | } 156 | } 157 | socksString := sock.String() 158 | var ok bool 159 | if sock, ok = p5.dispenseMiddleware(sock); !ok { 160 | atomic.StoreUint32(&sock.lock, stateUnlocked) 161 | p5.msgFailedMiddleware(socksString) 162 | continue 163 | } 164 | p5.msgTry(socksString) 165 | atomic.StoreUint32(&sock.lock, stateUnlocked) 166 | dialSocks := socks.Dial(socksString) 167 | conn, err := dialSocks(network, addr) 168 | if err != nil { 169 | count++ 170 | p5.msgUnableToReach(socksString, addr, err) 171 | continue 172 | } 173 | p5.msgUsingProxy(socksString) 174 | go func() { 175 | select { 176 | case <-ctx.Done(): 177 | _ = conn.Close() 178 | case <-p5.conKiller: 179 | _ = conn.Close() 180 | case <-p5.ctx.Done(): 181 | _ = conn.Close() 182 | } 183 | }() 184 | return conn, nil 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /internal/scaler/scaler.go: -------------------------------------------------------------------------------- 1 | package scaler 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync/atomic" 7 | 8 | "github.com/panjf2000/ants/v2" 9 | ) 10 | 11 | type autoScalerState uint32 12 | 13 | // for tests 14 | var debugSwitch = false 15 | 16 | func init() { 17 | if os.Getenv("PROX5_SCALER_DEBUG") != "" { 18 | debugSwitch = true 19 | } 20 | } 21 | 22 | func debug(msg string) { 23 | if debugSwitch { 24 | println(msg) 25 | } 26 | } 27 | 28 | func noopMsg(validated, dispensed int64, as *AutoScaler) string { 29 | if !debugSwitch { 30 | return "" 31 | } 32 | return fmt.Sprintf("noop: validated: %d, dispensed: %d, mod: %d, max: %d, threshold: %d", 33 | validated, dispensed, atomic.LoadInt64(as.mod), atomic.LoadInt64(as.Max), atomic.LoadInt64(as.Threshold)) 34 | } 35 | 36 | const ( 37 | stateDisabled autoScalerState = iota 38 | stateIdle 39 | stateScalingUp 40 | stateScalingDown 41 | ) 42 | 43 | type AutoScaler struct { 44 | Max *int64 45 | state autoScalerState 46 | baseline *int64 47 | mod *int64 48 | Threshold *int64 49 | } 50 | 51 | func NewAutoScaler(baseline int, max int, differenceThreshold int) *AutoScaler { 52 | zero64 := int64(0) 53 | max64 := int64(max) 54 | diff64 := int64(differenceThreshold) 55 | baseline64 := int64(baseline) 56 | return &AutoScaler{ 57 | baseline: &baseline64, 58 | state: stateDisabled, 59 | mod: &zero64, 60 | Max: &max64, 61 | Threshold: &diff64, 62 | } 63 | } 64 | 65 | func (as *AutoScaler) Disable() { 66 | atomic.StoreUint32((*uint32)(&as.state), uint32(stateDisabled)) 67 | } 68 | 69 | func (as *AutoScaler) Enable() { 70 | atomic.StoreUint32((*uint32)(&as.state), uint32(stateIdle)) 71 | } 72 | 73 | func (as *AutoScaler) IsOn() bool { 74 | return !atomic.CompareAndSwapUint32((*uint32)(&as.state), uint32(stateDisabled), uint32(stateDisabled)) 75 | } 76 | 77 | func (as *AutoScaler) StateString() string { 78 | switch autoScalerState(atomic.LoadUint32((*uint32)(&as.state))) { 79 | case stateDisabled: 80 | return "disabled" 81 | case stateIdle: 82 | return "idle" 83 | case stateScalingUp: 84 | return "scaling up" 85 | case stateScalingDown: 86 | return "scaling down" 87 | default: 88 | return "unknown" 89 | } 90 | } 91 | 92 | func (as *AutoScaler) SetMax(max int) { 93 | atomic.StoreInt64(as.Max, int64(max)) 94 | } 95 | 96 | func (as *AutoScaler) SetThreshold(threshold int) { 97 | atomic.StoreInt64(as.Threshold, int64(threshold)) 98 | } 99 | 100 | func (as *AutoScaler) SetBaseline(baseline int) { 101 | atomic.StoreInt64(as.baseline, int64(baseline)) 102 | } 103 | 104 | // ScaleAnts scales the pool, it returns true if the pool scale has been changed, and false if not. 105 | func (as *AutoScaler) ScaleAnts(pool *ants.Pool, validated int64, dispensed int64) bool { 106 | if dispensed > validated { 107 | // consider panicing here... 108 | debug("dispensed > validated (FUBAR)") 109 | dispensed = validated 110 | } 111 | if atomic.LoadInt64(as.mod) < 0 { 112 | panic("scaler.go: scaler mod is negative") 113 | } 114 | if !as.IsOn() { 115 | debug("AutoScaler is off") 116 | // try to get us back to baseline if the scaler is disabled but we're not there yet. 117 | switch { 118 | case atomic.LoadInt64(as.mod) == 0: 119 | debug("off and not dirty") 120 | return false 121 | case atomic.LoadInt64(as.mod) > 0: 122 | debug("off: mod > 0") 123 | // we're dirty, but the scaler is off, so we need to get back to baseline 124 | if !(pool.Cap() > int(atomic.LoadInt64(as.baseline))) { 125 | debug("off: mod > 0, but pool is at baseline...") 126 | return false 127 | } else { 128 | debug("off and dirty: pool cap > baseline, scaling down") 129 | pool.Tune(pool.Cap() - 1) 130 | atomic.AddInt64(as.mod, -1) 131 | return true 132 | } 133 | default: 134 | debug("off: default (no-op)") 135 | } 136 | return false 137 | } 138 | 139 | sPtr := (*uint32)(&as.state) 140 | 141 | idle := atomic.CompareAndSwapUint32(sPtr, uint32(stateIdle), uint32(stateIdle)) 142 | 143 | needScaleUp := (validated-dispensed < atomic.LoadInt64(as.Threshold)) && 144 | (atomic.LoadInt64(as.mod) < atomic.LoadInt64(as.Max)) 145 | 146 | needScaleDown := atomic.LoadInt64(as.mod) > 0 && 147 | ((validated - dispensed) > atomic.LoadInt64(as.Threshold)) 148 | 149 | noop := ((idle && !needScaleUp && !needScaleDown) || 150 | (needScaleUp && atomic.LoadInt64(as.mod) >= atomic.LoadInt64(as.Max)) || 151 | (validated < atomic.LoadInt64(as.Threshold))) && atomic.LoadInt64(as.mod) == 0 152 | 153 | switch { 154 | case noop: 155 | debug(noopMsg(validated, dispensed, as)) 156 | return false 157 | case ((!needScaleUp && !needScaleDown) || atomic.LoadInt64(as.mod) == 0) && !idle: 158 | debug("not scaling up or down or mod is 0, and not idle, setting idle") 159 | atomic.StoreUint32(sPtr, uint32(stateIdle)) 160 | return false 161 | case needScaleUp && atomic.CompareAndSwapUint32(sPtr, uint32(stateIdle), uint32(stateScalingUp)): 162 | debug("scaling up") 163 | atomic.AddInt64(as.mod, 1) 164 | pool.Tune(pool.Cap() + 1) 165 | return true 166 | case needScaleUp && atomic.CompareAndSwapUint32(sPtr, uint32(stateScalingUp), uint32(stateScalingUp)): 167 | debug("scaling up (already scaling up)") 168 | atomic.AddInt64(as.mod, 1) 169 | pool.Tune(pool.Cap() + 1) 170 | return true 171 | case needScaleUp && atomic.CompareAndSwapUint32(sPtr, uint32(stateScalingDown), uint32(stateScalingUp)): 172 | debug("scaling up (was scaling down)") 173 | atomic.AddInt64(as.mod, 1) 174 | pool.Tune(pool.Cap() + 1) 175 | return true 176 | case needScaleDown && atomic.CompareAndSwapUint32(sPtr, uint32(stateScalingUp), uint32(stateScalingDown)): 177 | debug("scaling down (was scaling up)") 178 | atomic.AddInt64(as.mod, -1) 179 | pool.Tune(pool.Cap() - 1) 180 | return true 181 | case needScaleDown && atomic.CompareAndSwapUint32(sPtr, uint32(stateIdle), uint32(stateScalingDown)): 182 | debug("scaling down (was idle)") 183 | atomic.AddInt64(as.mod, -1) 184 | pool.Tune(pool.Cap() - 1) 185 | return true 186 | case needScaleDown && atomic.CompareAndSwapUint32(sPtr, uint32(stateScalingDown), uint32(stateScalingDown)): 187 | debug("scaling down (already scaling down)") 188 | atomic.AddInt64(as.mod, -1) 189 | pool.Tune(pool.Cap() - 1) 190 | return true 191 | default: 192 | debug("default (no-op)") 193 | return false 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "sync/atomic" 7 | 8 | "git.tcp.direct/kayos/common/pool" 9 | ) 10 | 11 | var ( 12 | debugStatus *uint32 13 | debugHardLock = &sync.RWMutex{} 14 | ) 15 | 16 | func init() { 17 | dd := debugDisabled 18 | debugStatus = &dd 19 | } 20 | 21 | const ( 22 | debugEnabled uint32 = iota 23 | debugDisabled 24 | 25 | stamp = "[prox5] " 26 | ) 27 | 28 | type SocksLogger struct { 29 | parent *ProxyEngine 30 | } 31 | 32 | // Printf is used to handle socks server logging. 33 | func (s SocksLogger) Printf(format string, a ...interface{}) { 34 | buf := strs.Get() 35 | buf.MustWriteString(fmt.Sprintf(format, a...)) 36 | s.parent.dbgPrint(buf) 37 | } 38 | 39 | type basicPrinter struct{} 40 | 41 | func (b *basicPrinter) Print(a ...any) { 42 | if len(a) == 0 { 43 | return 44 | } 45 | str := fmt.Sprint(a...) 46 | if useDebugChannel { 47 | debugChan <- str 48 | return 49 | } 50 | buf := strs.Get() 51 | buf.MustWriteString(stamp) 52 | buf.MustWriteString(str) 53 | strs.MustPut(buf) 54 | } 55 | 56 | func (b *basicPrinter) Printf(format string, items ...any) { 57 | b.Print(fmt.Sprintf(format, items...)) 58 | } 59 | 60 | func (b *basicPrinter) Errorf(format string, items ...any) { 61 | b.Printf(format, items...) 62 | } 63 | 64 | // DebugEnabled returns the current state of our debug switch. 65 | func (p5 *ProxyEngine) DebugEnabled() bool { 66 | debugHardLock.RLock() 67 | defer debugHardLock.RUnlock() 68 | return atomic.CompareAndSwapUint32(debugStatus, debugEnabled, debugEnabled) 69 | } 70 | 71 | // EnableDebug enables printing of verbose messages during operation 72 | func (p5 *ProxyEngine) EnableDebug() { 73 | atomic.StoreUint32(debugStatus, debugEnabled) 74 | p5.dbgPrint(simpleString("prox5 debug enabled")) 75 | } 76 | 77 | // DisableDebug enables printing of verbose messages during operation. 78 | // WARNING: if you are using a DebugChannel, you must read all of the messages in the channel's cache or this will block. 79 | func (p5 *ProxyEngine) DisableDebug() { 80 | atomic.StoreUint32(debugStatus, debugDisabled) 81 | } 82 | 83 | func simpleString(s string) *pool.String { 84 | buf := strs.Get() 85 | buf.MustWriteString(s) 86 | return buf 87 | } 88 | 89 | func (p5 *ProxyEngine) dbgPrint(builder *pool.String) { 90 | defer strs.MustPut(builder) 91 | if !p5.DebugEnabled() { 92 | return 93 | } 94 | p5.DebugLogger.Printf(builder.String()) 95 | return 96 | } 97 | 98 | func (p5 *ProxyEngine) msgUnableToReach(socksString, target string, err error) { 99 | if !p5.DebugEnabled() { 100 | return 101 | } 102 | buf := strs.Get() 103 | buf.MustWriteString("unable to reach ") 104 | if p5.opt.redact { 105 | buf.MustWriteString("[redacted]") 106 | } else { 107 | buf.MustWriteString(target) 108 | } 109 | buf.MustWriteString(" with ") 110 | buf.MustWriteString(socksString) 111 | if !p5.opt.redact { 112 | buf.MustWriteString(": ") 113 | buf.MustWriteString(err.Error()) 114 | } 115 | buf.MustWriteString(", cycling...") 116 | p5.dbgPrint(buf) 117 | } 118 | 119 | func (p5 *ProxyEngine) msgUsingProxy(socksString string) { 120 | if !p5.DebugEnabled() { 121 | return 122 | } 123 | buf := strs.Get() 124 | if p5.GetDebugRedactStatus() { 125 | socksString = "(redacted)" 126 | } 127 | buf.MustWriteString("mysteryDialer using socks: ") 128 | buf.MustWriteString(socksString) 129 | p5.dbgPrint(buf) 130 | } 131 | 132 | func (p5 *ProxyEngine) msgFailedMiddleware(socksString string) { 133 | if !p5.DebugEnabled() { 134 | return 135 | } 136 | buf := strs.Get() 137 | buf.MustWriteString("failed middleware check, ") 138 | buf.MustWriteString(socksString) 139 | buf.MustWriteString(", cycling...") 140 | p5.dbgPrint(buf) 141 | } 142 | 143 | func (p5 *ProxyEngine) msgTry(socksString string) { 144 | if !p5.DebugEnabled() { 145 | return 146 | } 147 | if p5.GetDebugRedactStatus() { 148 | socksString = "(redacted)" 149 | } 150 | buf := strs.Get() 151 | buf.MustWriteString("try dial with: ") 152 | buf.MustWriteString(socksString) 153 | p5.dbgPrint(buf) 154 | } 155 | 156 | func (p5 *ProxyEngine) msgCantGetLock(socksString string, putback bool) { 157 | if !p5.DebugEnabled() { 158 | return 159 | } 160 | if p5.GetDebugRedactStatus() { 161 | socksString = "(redacted)" 162 | } 163 | buf := strs.Get() 164 | buf.MustWriteString("can't get lock for ") 165 | buf.MustWriteString(socksString) 166 | if putback { 167 | buf.MustWriteString(", putting back in queue") 168 | } 169 | p5.dbgPrint(buf) 170 | } 171 | 172 | func (p5 *ProxyEngine) msgGotLock(socksString string) { 173 | if !p5.DebugEnabled() { 174 | return 175 | } 176 | if p5.GetDebugRedactStatus() { 177 | socksString = "(redacted)" 178 | } 179 | buf := strs.Get() 180 | buf.MustWriteString("got lock for ") 181 | buf.MustWriteString(socksString) 182 | p5.dbgPrint(buf) 183 | } 184 | 185 | func (p5 *ProxyEngine) msgChecked(sock *Proxy, success bool) { 186 | if !p5.DebugEnabled() { 187 | return 188 | } 189 | pstr := sock.Endpoint 190 | if p5.GetDebugRedactStatus() { 191 | pstr = "(redacted)" 192 | } 193 | buf := strs.Get() 194 | if !success { 195 | buf.MustWriteString("failed to verify: ") 196 | buf.MustWriteString(pstr) 197 | p5.dbgPrint(buf) 198 | return 199 | } 200 | buf.MustWriteString("verified ") 201 | buf.MustWriteString(pstr) 202 | buf.MustWriteString(" as ") 203 | buf.MustWriteString(sock.protocol.Get().String()) 204 | buf.MustWriteString(" proxy") 205 | p5.dbgPrint(buf) 206 | } 207 | 208 | func (p5 *ProxyEngine) msgBadProxRate(sock *Proxy) { 209 | if !p5.DebugEnabled() { 210 | return 211 | } 212 | if p5.lastBadProxAnnnounced.Load().(string) == sock.Endpoint { 213 | return 214 | } 215 | sockString := sock.Endpoint 216 | if p5.GetDebugRedactStatus() { 217 | sockString = "(redacted)" 218 | } 219 | buf := strs.Get() 220 | buf.MustWriteString("badProx ratelimited: ") 221 | buf.MustWriteString(sockString) 222 | p5.dbgPrint(buf) 223 | p5.lastBadProxAnnnounced.Store(sock.Endpoint) 224 | } 225 | 226 | // ------------ 227 | 228 | var ( 229 | debugChan chan string 230 | useDebugChannel bool 231 | ) 232 | 233 | // DebugChannel will return a channel which will receive debug messages once debug is enabled. 234 | // This will alter the flow of debug messages, they will no longer print to console, they will be pushed into this channel. 235 | // Make sure you pull from the channel eventually to avoid build up of blocked goroutines. 236 | // 237 | // Deprecated: use DebugLogger instead. This will be removed in a future version. 238 | func (p5 *ProxyEngine) DebugChannel() chan string { 239 | debugChan = make(chan string, 100) 240 | useDebugChannel = true 241 | return debugChan 242 | } 243 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

Prox5

3 | 4 | ### SOCKS5/4/4a validating proxy pool + SOCKS5 server 5 | 6 | Animated Screenshot of Prox5 Example 7 | 8 | [![GoDoc](https://godoc.org/git.tcp.direct/kayos/prox5?status.svg)](https://pkg.go.dev/git.tcp.direct/kayos/prox5) [![Go Report Card](https://goreportcard.com/badge/github.com/yunginnanet/prox5)](https://goreportcard.com/report/github.com/yunginnanet/prox5) [![IRC](https://img.shields.io/badge/ircd.chat-%23tcpdirect-blue.svg)](ircs://ircd.chat:6697/#tcpdirect) [![Test Status](https://github.com/yunginnanet/prox5/actions/workflows/go.yml/badge.svg)](https://github.com/yunginnanet/prox5/actions/workflows/go.yml) ![five](https://img.shields.io/badge/fhjones-55555-blue) 9 | 10 | `import git.tcp.direct/kayos/prox5` 11 | 12 | Prox5 is a golang library for managing, validating, and utilizing a very large amount of arbitrary SOCKS proxies. 13 | 14 | Notably it features interface compatible dialer functions that dial out from different proxies for every connection, and a SOCKS5 server that utilizes those functions. 15 | 16 |
17 | 18 | --- 19 | 20 | > [!CAUTION] 21 | > 22 | > **Using prox5 to proxy connections from certain offsec tools may cause denial of service.** Please spazz out responsibly. 23 | > ### e.g: https://youtu.be/qVRFnxjD7o8 24 | --- 25 | 26 | ## Table of Contents 27 | 28 | 1. [Overview](#overview) 29 | 1. [Validation Engine](#validation-engine) 30 | 1. [Auto Scaler](#auto-scaler) 31 | 1. [Rate Limiting](#rate-limiting) 32 | 1. [Accessing Validated Proxies](#accessing-validated-proxies) 33 | 1. [Additional info](#additional-info) 34 | 1. [The Secret Sauce](#the-secret-sauce) 35 | 1. [External Integrations](#external-integrations) 36 | 1. [Status and Final Thoughts](#status-and-final-thoughts) 37 | 38 | --- 39 | 40 | ## Overview 41 | 42 | ### Validation Engine 43 | 44 | 1) TCP Dial to the endpoint, if successful, reuse net.Conn down for step 2 45 | 2) HTTPS GET request to a list of IP echo endpoints 46 | 3) Store the IP address discovered during step 2 47 | 4) Allocate a new `prox5.Proxy` type || update an existing one, store latest info 48 | 5) Enqueue a pointer to this `proxy.Proxy` instance, instantiating it for further use 49 | 50 | ### Auto Scaler 51 | 52 | The validation has an optional auto scale feature that allows for the automatic tuning of validation worker count as more proxies are dispensed. 53 | This feature is still new, but seems to work well. It can be enabled with `[...].EnableAutoScaler()`. 54 | 55 | Please refer to the autoscale related items within [the documentation](https://pkg.go.dev/git.tcp.direct/kayos/prox5) for more info. 56 | 57 | ### Rate Limiting 58 | 59 | Using [Rate5](https://github.com/yunginnanet/Rate5), prox5 naturally reduces the frequency of proxies that fail to validate. It does this by reducing the frequency proxies are accepted into the validation pipeline the more they fail to verify or fail to successfully connect to an endpoint. This is not yet adjustable, but will be soon. See [the documentation for Rate5](https://pkg.go.dev/git.tcp.direct/kayos/rate5), and the source code for this project (defs.go is a good place to start) for more info. 60 | 61 | ### Accessing Validated Proxies 62 | 63 | - Retrieve validated 4/4a/5 proxies as simple strings for generic use 64 | - Use one of the dialer functions with any golang code that calls for a net.Dialer 65 | - Spin up a SOCKS5 server that will then make rotating use of your validated proxies 66 | 67 | --- 68 | 69 | ## Additional info 70 | 71 | ### The Secret Sauce 72 | 73 | What makes Prox5 special is largely the Mystery Dialer. This dialer satisfies the net.Dialer and ContextDialer interfaces. The implementation is a little bit different from your average dialer. Here's roughly what happens when you dial out with a ProxyEngine; 74 | 75 | - Loads up a previously verified proxy 76 | - Attempts to make connection with the dial endpoint using said proxy 77 | - Upon failure, prox5: 78 | - repeats this process *mid-dial* 79 | - does not drop connection to the client 80 | - Once a proxy has been successfully used to connect to the target endpoint, prox5 passes the same net.Conn onto the client 81 | 82 | ### External Integrations 83 | 84 |
85 | Mullvad 86 | 87 | 88 | Take a look at [mullsox](https://git.tcp.direct/kayos/mullsox) for an easy way to access all of the mullvad proxies reachable from any one VPN endpoint. It is trivial to feed the results of `GetAndVerifySOCKS` into prox5. 89 | 90 | Here's a snippet that should just about get you there: 91 | 92 | ```golang 93 | package main 94 | 95 | import ( 96 | "os" 97 | "time" 98 | 99 | "git.tcp.direct/kayos/mullsox" 100 | "git.tcp.direct/kayos/prox5" 101 | ) 102 | 103 | func main() { 104 | p5 := prox5.NewProxyEngine() 105 | mc := mullsox.NewChecker() 106 | 107 | if err := mc.Update(); err != nil { 108 | println(err.Error()) 109 | return 110 | } 111 | 112 | incoming, _ := mc.GetAndVerifySOCKS() 113 | 114 | var count = 0 115 | for line := range incoming { 116 | if p5.LoadSingleProxy(line.String()) { 117 | count++ 118 | } 119 | } 120 | 121 | if count == 0 { 122 | println("failed to load any proxies") 123 | return 124 | } 125 | 126 | if err := p5.Start(); err != nil { 127 | println(err.Error()) 128 | return 129 | } 130 | 131 | go func() { 132 | if err := p5.StartSOCKS5Server("127.0.0.1:42069", "", ""); err != nil { 133 | println(err.Error()) 134 | os.Exit(1) 135 | } 136 | }() 137 | 138 | time.Sleep(time.Millisecond * 500) 139 | 140 | println("proxies loaded and socks server started") 141 | } 142 | ``` 143 |
144 | 145 |
146 | ProxyBonanza 147 | 148 | Take a look at [ProxyGonanza](https://git.tcp.direct/kayos/proxygonanza) 149 | 150 | _(TODO: code example here)_ 151 | 152 |
153 | 154 | --- 155 | 156 | ## Status and Final Thoughts 157 | 158 | **This project is in development.** 159 | 160 | It "works" and has been used in "production", but still needs some love. 161 | 162 | Please break it and let me know what broke. 163 | 164 | 165 | The way you choose to use this lib is yours. The API is fairly extensive for you to be able to customize runtime configuration without having to do any surgery. 166 | 167 | Things like the amount of validation workers that are concurrently operating, timeouts, and proxy re-use policies may be tuned in real-time. 168 | 169 | --- 170 | 171 |
172 | 173 | # **Please see [the docs](https://pkg.go.dev/git.tcp.direct/kayos/prox5) and the [example](example/main.go) for more details.** 174 | 175 |
176 | -------------------------------------------------------------------------------- /prox5_test.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "net/http" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "sync/atomic" 15 | "testing" 16 | "time" 17 | 18 | "git.tcp.direct/kayos/common/entropy" 19 | "git.tcp.direct/kayos/go-socks5" 20 | ) 21 | 22 | var failures = &atomic.Int64{} 23 | 24 | func init() { 25 | _ = os.Setenv("PROX5_SCALER_DEBUG", "1") 26 | failures.Store(0) 27 | } 28 | 29 | type randomFail struct { 30 | t *testing.T 31 | failedCount int64 32 | maxFail int64 33 | 34 | failOneOutOf int 35 | } 36 | 37 | func (rf *randomFail) fail() bool { 38 | if rf.failOneOutOf == 0 { 39 | return false 40 | } 41 | 42 | doFail := entropy.GetOptimizedRand().Intn(rf.failOneOutOf) == 1 43 | 44 | if !doFail { 45 | return false 46 | } 47 | atomic.AddInt64(&rf.failedCount, 1) 48 | rf.t.Logf("random SOCKS failure triggered, total fail count: %d", rf.failedCount) 49 | if rf.maxFail > 0 && atomic.LoadInt64(&rf.failedCount) > rf.maxFail { 50 | rf.t.Errorf("[FAIL] random SOCKS failure triggered too many times, total fail count: %d", rf.failedCount) 51 | } 52 | 53 | failures.Add(1) 54 | return true 55 | } 56 | 57 | type dummyHTTPServer struct { 58 | t *testing.T 59 | net.Listener 60 | } 61 | 62 | func timeNowJSON() []byte { 63 | js, _ := time.Now().MarshalJSON() 64 | return js 65 | } 66 | 67 | func newDummyHTTPSServer(t *testing.T, port int) { 68 | t.Helper() 69 | dtcp := &dummyHTTPServer{t: t} 70 | var err error 71 | if dtcp.Listener, err = net.Listen("tcp", ":"+strconv.Itoa(port)); err != nil && !errors.Is(err, net.ErrClosed) { 72 | t.Fatal(err) 73 | } 74 | go func() { 75 | if err = http.Serve(dtcp, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 76 | time.Sleep(time.Duration(entropy.RNG(300)) * time.Millisecond) 77 | if _, err = w.Write(timeNowJSON()); err != nil { 78 | t.Error("[FAIL] http server failed to write JSON: " + err.Error()) 79 | } 80 | })); err != nil && !errors.Is(err, net.ErrClosed) { 81 | t.Error("[FAIL] http.Serve error: " + err.Error()) 82 | } 83 | }() 84 | 85 | t.Cleanup(func() { 86 | _ = dtcp.Close() 87 | }) 88 | 89 | t.Logf("dummy HTTPS server listening on port %d", port) 90 | 91 | } 92 | 93 | var ErrRandomFail = errors.New("random failure") 94 | 95 | func dummySOCKSServer(t *testing.T, port int, rf ...*randomFail) { 96 | t.Helper() 97 | var failure = &randomFail{t: t, failedCount: int64(0), failOneOutOf: 0} 98 | if len(rf) > 0 { 99 | failure = rf[0] 100 | } 101 | 102 | dialer := func(ctx context.Context, network, addr string) (net.Conn, error) { 103 | if failure.fail() { 104 | return nil, ErrRandomFail 105 | } 106 | time.Sleep(time.Duration(entropy.GetOptimizedRand().Intn(300)) * time.Millisecond) 107 | return net.Dial(network, addr) 108 | } 109 | 110 | server := socks5.NewServer(socks5.WithDial(dialer)) 111 | go func() { 112 | err := server.ListenAndServe("tcp", "127.0.0.1:"+strconv.Itoa(port)) 113 | if err != nil && !errors.Is(err, net.ErrClosed) { 114 | t.Error("[FAIL] socks server failure: " + err.Error()) 115 | } 116 | }() 117 | } 118 | 119 | type p5TestLogger struct { 120 | t *testing.T 121 | } 122 | 123 | func (tl p5TestLogger) Errorf(format string, args ...interface{}) { 124 | tl.t.Logf("[ERROR] "+format, args...) 125 | } 126 | func (tl p5TestLogger) Printf(format string, args ...interface{}) { 127 | val := fmt.Sprintf(format, args...) 128 | if strings.Contains(val, "failed to verify") { 129 | failures.Add(1) 130 | } 131 | tl.t.Logf("[PRINT] " + val) 132 | } 133 | func (tl p5TestLogger) Print(args ...interface{}) { 134 | val := fmt.Sprintf("%+v", args...) 135 | if strings.Contains(val, "failed to verify") { 136 | failures.Add(1) 137 | } 138 | tl.t.Log("[PRINT] " + val) 139 | } 140 | func TestProx5(t *testing.T) { 141 | numTest := 100 142 | if envCount := os.Getenv("PROX5_TEST_COUNT"); envCount != "" { 143 | n, e := strconv.Atoi(envCount) 144 | if e != nil { 145 | t.Skip(e.Error()) 146 | } 147 | numTest = n 148 | } 149 | for i := 0; i < numTest; i++ { 150 | dummySOCKSServer(t, 5555+i, &randomFail{ 151 | t: t, 152 | failedCount: int64(0), 153 | failOneOutOf: entropy.RNG(200), 154 | maxFail: 50, 155 | }) 156 | time.Sleep(time.Millisecond * 5) 157 | } 158 | newDummyHTTPSServer(t, 8055) 159 | time.Sleep(time.Millisecond * 350) 160 | p5 := NewProxyEngine() 161 | p5.SetAndEnableDebugLogger(p5TestLogger{t: t}) 162 | p5.SetMaxWorkers(10) 163 | p5.EnableAutoScaler() 164 | p5.SetAutoScalerThreshold(10) 165 | // p5.SetValidationTimeout(200 * time.Millisecond) 166 | p5.SetAutoScalerMaxScale(100) 167 | // p5.DisableRecycling() 168 | p5.SetRemoveAfter(2) 169 | var index = 5555 170 | 171 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) 172 | defer cancel() 173 | 174 | var once = &sync.Once{} 175 | 176 | check5 := func() { 177 | if err := p5.Pause(); err != nil { 178 | t.Errorf("[FAIL] failed to pause: %s", err.Error()) 179 | } 180 | time.Sleep(time.Second * 1) 181 | got := p5.GetTotalValidated() 182 | want := 55 - failures.Load() 183 | if got != want { 184 | t.Logf("[WARN] total validated proxies does not match expected, got: %d, expected: %d", 185 | got, want) 186 | } 187 | if err := p5.Resume(); err != nil { 188 | t.Errorf("[FAIL] failed to resume: %s", err.Error()) 189 | } 190 | } 191 | 192 | load := func() { 193 | if index > 5555+numTest { 194 | return 195 | } 196 | entropy.RandSleepMS(150) 197 | p5.LoadSingleProxy("127.0.0.1:" + strconv.Itoa(index)) 198 | if index == 5555+55 { 199 | once.Do(check5) 200 | } 201 | index++ 202 | } 203 | 204 | var successCount int64 = 0 205 | 206 | makeReq := func() { 207 | select { 208 | case <-ctx.Done(): 209 | return 210 | default: 211 | 212 | } 213 | resp, err := p5.GetHTTPClient().Get("http://127.0.0.1:8055") 214 | if err != nil && !errors.Is(err, ErrNoProxies) && !errors.Is(err, net.ErrClosed) { 215 | t.Error("[FAIL] " + err.Error()) 216 | } 217 | if err != nil && errors.Is(err, ErrNoProxies) { 218 | return 219 | } 220 | if resp == nil { 221 | return 222 | } 223 | b, e := io.ReadAll(resp.Body) 224 | if e != nil && !errors.Is(e, net.ErrClosed) { 225 | t.Log("[WARN] " + e.Error()) 226 | } 227 | t.Logf("got proxied response: %s", string(b)) 228 | atomic.AddInt64(&successCount, 1) 229 | } 230 | 231 | ticker := time.NewTicker(time.Millisecond * 100) 232 | 233 | if err := p5.Start(); err != nil { 234 | t.Fatal(err) 235 | } 236 | 237 | wait := 0 238 | 239 | testLoop: 240 | for { 241 | select { 242 | case <-ctx.Done(): 243 | successCountFinal := atomic.LoadInt64(&successCount) 244 | if successCountFinal < 10 { 245 | t.Fatal("no successful requests") 246 | } 247 | t.Logf("total successful requests: %d", successCountFinal) 248 | p5.CloseAllConns() 249 | break testLoop 250 | case <-ticker.C: 251 | // pre-warm 252 | wait++ 253 | if wait >= 50 { 254 | go makeReq() 255 | } 256 | default: 257 | load() 258 | } 259 | } 260 | cancel() 261 | if err := p5.Close(); err != nil { 262 | t.Fatal(err) 263 | } 264 | // let the proxy engine close gracefully 265 | time.Sleep(time.Second * 5) 266 | } 267 | -------------------------------------------------------------------------------- /validator_engine.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "errors" 7 | "io" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "sync" 13 | "sync/atomic" 14 | "time" 15 | 16 | "git.tcp.direct/kayos/socks" 17 | "golang.org/x/net/proxy" 18 | ) 19 | 20 | var headerPool = sync.Pool{ 21 | New: func() interface{} { 22 | hdr := make(http.Header) 23 | hdr["User-Agent"] = []string{""} 24 | hdr["Accept"] = []string{"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"} 25 | hdr["Accept-Language"] = []string{"en-US,en;q=0.5"} 26 | hdr["Accept-Encoding"] = []string{"gzip, deflate, br"} 27 | return hdr 28 | }, 29 | } 30 | 31 | func (p5 *ProxyEngine) prepHTTP() (*http.Client, *http.Transport, *http.Request, error) { 32 | req, err := http.NewRequest("GET", p5.GetRandomEndpoint(), bytes.NewBuffer([]byte(""))) 33 | if err != nil { 34 | return nil, nil, nil, err 35 | } 36 | headers := headerPool.Get().(http.Header) 37 | headers["User-Agent"] = []string{p5.RandomUserAgent()} 38 | 39 | var client = &http.Client{} 40 | var transporter = &http.Transport{ 41 | DisableKeepAlives: true, 42 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec 43 | TLSHandshakeTimeout: p5.GetValidationTimeout(), 44 | } 45 | 46 | return client, transporter, req, err 47 | } 48 | 49 | func (sock *Proxy) bad() { 50 | atomic.AddInt64(&sock.timesBad, 1) 51 | } 52 | 53 | func (sock *Proxy) good() { 54 | atomic.AddInt64(&sock.timesValidated, 1) 55 | sock.lastValidated = time.Now() 56 | } 57 | 58 | func httpEndpoint(hmd *handMeDown) (func(r *http.Request) (*url.URL, error), error) { 59 | s := strs.Get() 60 | defer strs.MustPut(s) 61 | s.MustWriteString("http://") 62 | s.MustWriteString(hmd.sock.Endpoint) 63 | purl, err := url.Parse(s.String()) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return http.ProxyURL(purl), nil 68 | } 69 | 70 | func (p5 *ProxyEngine) bakeHTTP(hmd *handMeDown) (client *http.Client, req *http.Request, err error) { 71 | builder := strs.Get() 72 | builder.MustWriteString(hmd.protoCheck.String()) 73 | builder.MustWriteString("://") 74 | builder.MustWriteString(hmd.sock.Endpoint) 75 | builder.MustWriteString("/?timeout=") 76 | builder.MustWriteString(p5.GetValidationTimeoutStr()) 77 | builder.MustWriteString("s") 78 | dialSocks := socks.DialWithConn(builder.String(), hmd.conn) 79 | strs.MustPut(builder) 80 | 81 | var transport *http.Transport 82 | 83 | client, transport, req, err = p5.prepHTTP() 84 | if err != nil { 85 | if req != nil && req.Header != nil { 86 | headerPool.Put(req.Header) 87 | } 88 | return 89 | } 90 | 91 | if hmd.protoCheck != ProtoHTTP { 92 | transport.Dial = dialSocks 93 | client.Transport = transport 94 | return 95 | } 96 | 97 | proxyURL, err := httpEndpoint(hmd) 98 | if err != nil { 99 | if req != nil && req.Header != nil { 100 | headerPool.Put(req.Header) 101 | } 102 | return 103 | } 104 | 105 | transport.Proxy = proxyURL 106 | return 107 | } 108 | 109 | func (p5 *ProxyEngine) validate(hmd *handMeDown) (string, error) { 110 | var ( 111 | client *http.Client 112 | req *http.Request 113 | err error 114 | ) 115 | 116 | client, req, err = p5.bakeHTTP(hmd) 117 | if err != nil { 118 | return "", err 119 | } 120 | 121 | resp, err := client.Do(req) 122 | defer func() { 123 | if req != nil && req.Header != nil { 124 | headerPool.Put(req.Header) 125 | } 126 | }() 127 | if err != nil { 128 | return "", err 129 | } 130 | 131 | rbody, err := io.ReadAll(resp.Body) 132 | _ = resp.Body.Close() 133 | return string(rbody), err 134 | } 135 | 136 | func (p5 *ProxyEngine) anothaOne() { 137 | p5.stats.Checked.Add(1) 138 | } 139 | 140 | type handMeDown struct { 141 | sock *Proxy 142 | protoCheck ProxyProtocol 143 | conn net.Conn 144 | under proxy.Dialer 145 | } 146 | 147 | func (hmd *handMeDown) Dial(network, addr string) (c net.Conn, err error) { 148 | if hmd.conn.LocalAddr().Network() != network { 149 | return hmd.under.Dial(network, addr) 150 | } 151 | if hmd.conn.RemoteAddr().String() != addr { 152 | return hmd.under.Dial(network, addr) 153 | } 154 | return hmd.conn, nil 155 | } 156 | 157 | func (p5 *ProxyEngine) announceValidating(sock *Proxy, presplit string) { 158 | if sock == nil { 159 | return 160 | } 161 | s := strs.Get() 162 | s.MustWriteString("validating ") 163 | s.MustWriteString(sock.GetProto().String()) 164 | s.MustWriteString("://") 165 | s.MustWriteString(presplit) 166 | p5.dbgPrint(s) 167 | 168 | } 169 | 170 | func (p5 *ProxyEngine) singleProxyCheck(sock *Proxy, protocol ProxyProtocol) error { 171 | defer p5.anothaOne() 172 | split := strings.Split(sock.Endpoint, "@") 173 | endpoint := split[0] 174 | if len(split) == 2 { 175 | endpoint = split[1] 176 | } 177 | 178 | // p5.announceValidating(sock, endpoint) 179 | 180 | conn, err := net.DialTimeout("tcp", endpoint, p5.GetValidationTimeout()) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | hmd := &handMeDown{sock: sock, conn: conn, under: proxy.Direct, protoCheck: protocol} 186 | 187 | resp, err := p5.validate(hmd) 188 | if err != nil { 189 | p5.badProx.Check(sock) 190 | return err 191 | } 192 | 193 | if newip := net.ParseIP(resp); newip == nil { 194 | p5.badProx.Check(sock) 195 | return errors.New("bad response from http request: " + resp) 196 | } 197 | 198 | sock.ProxiedIP = resp 199 | 200 | return nil 201 | } 202 | 203 | func (sock *Proxy) validate() { 204 | if sock == nil || sock.parent == nil { 205 | return 206 | } 207 | if !atomic.CompareAndSwapUint32(&sock.lock, stateUnlocked, stateLocked) { 208 | return 209 | } 210 | defer atomic.StoreUint32(&sock.lock, stateUnlocked) 211 | 212 | select { 213 | case <-sock.parent.ctx.Done(): 214 | return 215 | default: 216 | } 217 | 218 | pe := sock.parent 219 | if pe.useProx.Check(sock) { 220 | // s.dbgPrint("useProx ratelimited: " + sock.Endpoint ) 221 | return 222 | } 223 | 224 | // determined as bad, won't try again until it expires from that cache 225 | if pe.badProx.Peek(sock) { 226 | pe.msgBadProxRate(sock) 227 | return 228 | } 229 | 230 | // TODO: consider giving the option for verbose logging of this stuff? 231 | 232 | switch { 233 | case sock.timesValidated == 0, sock.protocol.Get() == ProtoNull: 234 | // try to use the proxy with all 3 SOCKS versions 235 | for tryProto := range protoMap { 236 | if tryProto == ProtoNull { 237 | continue 238 | } 239 | select { 240 | case <-pe.ctx.Done(): 241 | return 242 | default: 243 | if err := pe.singleProxyCheck(sock, tryProto); err != nil { 244 | // if the proxy is no good, we continue on to the next. 245 | continue 246 | } 247 | sock.protocol.set(tryProto) 248 | break 249 | } 250 | } 251 | default: 252 | if err := pe.singleProxyCheck(sock, sock.GetProto()); err != nil { 253 | sock.bad() 254 | pe.badProx.Check(sock) 255 | return 256 | } 257 | } 258 | 259 | switch sock.protocol.Get() { 260 | case ProtoSOCKS4, ProtoSOCKS4a, ProtoSOCKS5, ProtoHTTP: 261 | pe.msgChecked(sock, true) 262 | default: 263 | pe.msgChecked(sock, false) 264 | sock.bad() 265 | pe.badProx.Check(sock) 266 | return 267 | } 268 | 269 | sock.good() 270 | pe.tally(sock) 271 | } 272 | 273 | func (p5 *ProxyEngine) tally(sock *Proxy) bool { 274 | var target proxyList 275 | switch sock.protocol.Get() { 276 | case ProtoSOCKS4: 277 | p5.stats.v4() 278 | target = p5.Valids.SOCKS4 279 | case ProtoSOCKS4a: 280 | p5.stats.v4a() 281 | target = p5.Valids.SOCKS4a 282 | case ProtoSOCKS5: 283 | p5.stats.v5() 284 | target = p5.Valids.SOCKS5 285 | case ProtoHTTP: 286 | p5.stats.http() 287 | target = p5.Valids.HTTP 288 | default: 289 | return false 290 | } 291 | target.Lock() 292 | target.PushBack(sock) 293 | target.Unlock() 294 | return true 295 | } 296 | -------------------------------------------------------------------------------- /setters.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "time" 5 | 6 | "git.tcp.direct/kayos/prox5/logger" 7 | ) 8 | 9 | // AddUserAgents appends to the list of useragents we randomly choose from during proxied requests 10 | func (p5 *ProxyEngine) AddUserAgents(uagents []string) { 11 | p5.mu.Lock() 12 | p5.opt.userAgents = append(p5.opt.userAgents, uagents...) 13 | p5.mu.Unlock() 14 | p5.DebugLogger.Printf("added %d useragents to cycle through for proxy validation", len(uagents)) 15 | } 16 | 17 | // SetUserAgents sets the list of useragents we randomly choose from during proxied requests 18 | func (p5 *ProxyEngine) SetUserAgents(uagents []string) { 19 | p5.mu.Lock() 20 | p5.opt.userAgents = uagents 21 | p5.mu.Unlock() 22 | p5.DebugLogger.Printf("set %d useragents to cycle through for proxy validation", len(uagents)) 23 | } 24 | 25 | // SetCheckEndpoints replaces the running list of whatismyip style endpoitns for validation. (must return only the WAN IP) 26 | func (p5 *ProxyEngine) SetCheckEndpoints(newendpoints []string) { 27 | p5.mu.Lock() 28 | p5.opt.checkEndpoints = newendpoints 29 | p5.mu.Unlock() 30 | p5.DebugLogger.Printf("set %d check endpoints for proxy validations", len(newendpoints)) 31 | } 32 | 33 | // AddCheckEndpoints appends entries to the running list of whatismyip style endpoitns for validation. (must return only the WAN IP) 34 | func (p5 *ProxyEngine) AddCheckEndpoints(endpoints []string) { 35 | p5.mu.Lock() 36 | p5.opt.checkEndpoints = append(p5.opt.checkEndpoints, endpoints...) 37 | p5.mu.Unlock() 38 | p5.DebugLogger.Printf("added %d check endpoints for proxy validations", len(endpoints)) 39 | } 40 | 41 | // SetStaleTime replaces the duration of time after which a proxy will be considered "stale". stale proxies will be skipped upon retrieval. 42 | func (p5 *ProxyEngine) SetStaleTime(newtime time.Duration) { 43 | p5.opt.Lock() 44 | p5.opt.stale = newtime 45 | p5.opt.Unlock() 46 | p5.DebugLogger.Printf("prox5 stale time set to %s", newtime) 47 | } 48 | 49 | // SetValidationTimeout sets the validationTimeout option. 50 | func (p5 *ProxyEngine) SetValidationTimeout(timeout time.Duration) { 51 | p5.opt.Lock() 52 | p5.opt.validationTimeout = timeout 53 | p5.opt.Unlock() 54 | p5.DebugLogger.Printf("prox5 validation timeout set to %s", timeout) 55 | } 56 | 57 | // SetServerTimeout sets the serverTimeout option. 58 | // * serverTimeout defines the timeout for outgoing connections made with the mysteryDialer. 59 | // * To disable timeout on outgoing mysteryDialer connections, set this to time.Duration(0). 60 | func (p5 *ProxyEngine) SetServerTimeout(timeout time.Duration) { 61 | p5.opt.Lock() 62 | p5.opt.serverTimeout = timeout 63 | p5.opt.Unlock() 64 | p5.DebugLogger.Printf("prox5 server timeout set to %s", timeout) 65 | } 66 | 67 | // SetMaxWorkers set the maximum workers for proxy checking. 68 | func (p5 *ProxyEngine) SetMaxWorkers(num int) { 69 | if p5.isEmpty() && num < 2 { 70 | p5.DebugLogger. 71 | Printf("prox5 cannot set max workers to %d, minimum is 2 until we have some valid proxies", num) 72 | num = 2 73 | } 74 | p5.pool.Tune(num) 75 | p5.scaler.SetBaseline(num) 76 | 77 | } 78 | 79 | // EnableRecycling enables recycling used proxies back into the pending channel for revalidation after dispensed. 80 | func (p5 *ProxyEngine) EnableRecycling() { 81 | p5.opt.Lock() 82 | p5.opt.recycle = true 83 | p5.opt.Unlock() 84 | p5.DebugLogger.Printf("prox5 recycling enabled") 85 | } 86 | 87 | // DisableRecycling disables recycling used proxies back into the pending channel for revalidation after dispensed. 88 | func (p5 *ProxyEngine) DisableRecycling() { 89 | p5.opt.Lock() 90 | p5.opt.recycle = false 91 | p5.opt.Unlock() 92 | p5.DebugLogger.Printf("prox5 recycling disabled") 93 | } 94 | 95 | // SetRemoveAfter sets the removeafter policy, the amount of times a recycled proxy is marked as bad before it is removed entirely. 96 | // - Default is 10 97 | // - To disable deleting entirely, set this value to -1 98 | // - Only applies when recycling is enabled 99 | func (p5 *ProxyEngine) SetRemoveAfter(timesfailed int) { 100 | p5.opt.Lock() 101 | p5.opt.removeafter = timesfailed 102 | p5.opt.Unlock() 103 | p5.DebugLogger.Printf("prox5 removeafter policy set to %d", timesfailed) 104 | } 105 | 106 | // SetDialerBailout sets the amount of times the mysteryDialer will dial out and fail before it bails out. 107 | // - The dialer will attempt to redial a destination with a different proxy a specified amount of times before it gives up 108 | func (p5 *ProxyEngine) SetDialerBailout(dialattempts int) { 109 | p5.opt.Lock() 110 | p5.opt.dialerBailout = dialattempts 111 | p5.opt.Unlock() 112 | p5.DebugLogger.Printf("prox5 dialer bailout set to %d", dialattempts) 113 | } 114 | 115 | // SetDispenseMiddleware will add a function that sits within the dialing process of the mysteryDialer and anyhing using it. 116 | // This means this function will be called mid-dial during connections. Return true to approve proxy, false to skip it. 117 | // Take care modiying the proxy in-flight as it is a pointer. 118 | func (p5 *ProxyEngine) SetDispenseMiddleware(f func(*Proxy) (*Proxy, bool)) { 119 | p5.mu.Lock() 120 | p5.dispenseMiddleware = f 121 | p5.mu.Unlock() 122 | p5.DebugLogger.Printf("prox5 dispense middleware set") 123 | } 124 | 125 | // SetDebugLogger sets the debug logger for the ProxyEngine. See the Logger interface for implementation details. 126 | // 127 | // Deprecated: use SetLogger instead. This will be removed in a future version. 128 | func (p5 *ProxyEngine) SetDebugLogger(l logger.Logger) { 129 | p5.SetLogger(l) 130 | } 131 | 132 | // SetLogger sets the debug logger for the ProxyEngine. See the Logger interface for implementation details. 133 | func (p5 *ProxyEngine) SetLogger(l logger.Logger) { 134 | debugHardLock.Lock() 135 | p5.mu.Lock() 136 | p5.DebugLogger = l 137 | p5.mu.Unlock() 138 | debugHardLock.Unlock() 139 | p5.DebugLogger.Printf("prox5 debug logger set") 140 | } 141 | 142 | func (p5 *ProxyEngine) SetAndEnableDebugLogger(l logger.Logger) { 143 | p5.SetLogger(l) 144 | p5.EnableDebug() 145 | } 146 | 147 | // EnableAutoScaler enables the autoscaler. 148 | // This will automatically scale up the number of workers based on the threshold of dial attempts versus validated proxies. 149 | func (p5 *ProxyEngine) EnableAutoScaler() { 150 | p5.scaler.Enable() 151 | p5.DebugLogger.Printf("prox5 autoscaler enabled") 152 | } 153 | 154 | // DisableAutoScaler disables the autoscaler. 155 | func (p5 *ProxyEngine) DisableAutoScaler() { 156 | p5.scaler.Disable() 157 | p5.DebugLogger.Printf("prox5 autoscaler disabled") 158 | } 159 | 160 | // SetAutoScalerMaxScale sets the relative maximum amount that the autoscaler will scale up. 161 | func (p5 *ProxyEngine) SetAutoScalerMaxScale(max int) { 162 | p5.scaler.SetMax(max) 163 | p5.DebugLogger.Printf("prox5 autoscaler max scale set to %d", max) 164 | } 165 | 166 | // SetAutoScalerThreshold sets the threshold of validated proxies versus dials that will trigger the autoscaler. 167 | func (p5 *ProxyEngine) SetAutoScalerThreshold(threshold int) { 168 | p5.scaler.SetThreshold(threshold) 169 | p5.DebugLogger.Printf("prox5 autoscaler threshold set to %d", threshold) 170 | } 171 | 172 | func (p5 *ProxyEngine) EnableDebugRedaction() { 173 | p5.opt.Lock() 174 | p5.opt.redact = true 175 | p5.opt.Unlock() 176 | p5.DebugLogger.Printf("[redacted]") 177 | } 178 | 179 | func (p5 *ProxyEngine) DisableDebugRedaction() { 180 | p5.opt.Lock() 181 | p5.opt.redact = false 182 | p5.opt.Unlock() 183 | p5.DebugLogger.Printf("prox5 redaction disabled") 184 | } 185 | 186 | func (p5 *ProxyEngine) EnableRecyclerShuffling() { 187 | p5.opt.Lock() 188 | p5.opt.shuffle = true 189 | p5.opt.Unlock() 190 | p5.DebugLogger.Printf("prox5 recycler shuffling enabled") 191 | } 192 | 193 | func (p5 *ProxyEngine) DisableRecyclerShuffling() { 194 | p5.opt.Lock() 195 | p5.opt.shuffle = false 196 | p5.opt.Unlock() 197 | p5.DebugLogger.Printf("prox5 recycler shuffling disabled") 198 | } 199 | 200 | func (p5 *ProxyEngine) EnableHTTPClientTLSVerification() { 201 | p5.opt.Lock() 202 | p5.opt.tlsVerify = true 203 | p5.opt.Unlock() 204 | p5.DebugLogger.Printf("prox5 HTTP client TLS verification enabled") 205 | } 206 | 207 | func (p5 *ProxyEngine) DisableHTTPClientTLSVerification() { 208 | p5.opt.Lock() 209 | p5.opt.tlsVerify = false 210 | p5.opt.Unlock() 211 | p5.DebugLogger.Printf("prox5 HTTP client TLS verification disabled") 212 | } 213 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | git.tcp.direct/kayos/common v0.9.7 h1:k2k3fvvEFN9JV+0nyVWLoV8cGRDAhS/8ECO9tEKN+to= 2 | git.tcp.direct/kayos/common v0.9.7/go.mod h1:mmTOIi7k99yygTa1FSOZNoFEEbSTOQV/QpTLUaQU9Tk= 3 | git.tcp.direct/kayos/go-socks5 v0.3.0 h1:nCsYM0ttPZHGAVVG8zFEy2ZTxoSyPp5ld1YSy3zyWDQ= 4 | git.tcp.direct/kayos/go-socks5 v0.3.0/go.mod h1:6Dw8lhiA+dqCY6CvPBtMyXMug9+spSAXC7+lrqItXf0= 5 | git.tcp.direct/kayos/socks v0.1.3 h1:IUogYkM9q+UXEvvAQ5nJJVF03EixLYwbpGnc5FIcH1g= 6 | git.tcp.direct/kayos/socks v0.1.3/go.mod h1:1qQP+wLpoKzINcI7NYRvt7Q0Y5k2dmpe+YtmvlFGNbg= 7 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 8 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 9 | github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= 10 | github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 15 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 16 | github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc= 17 | github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= 18 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 19 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 20 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 21 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 22 | github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364 h1:5XxdakFhqd9dnXoAZy1Mb2R/DZ6D1e+0bGC/JhucGYI= 23 | github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= 24 | github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 25 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 26 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 27 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 28 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 29 | github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= 30 | github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= 31 | github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= 32 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= 33 | github.com/ooni/oohttp v0.6.7 h1:wmCjx9+gzx7p1xc/kMAmgXSgXKu7G8CAmil4Zii3g10= 34 | github.com/ooni/oohttp v0.6.7/go.mod h1:Vipww76rE6i/Lyd+M8gec/ixPrsyPti1J8xTyqzFIHA= 35 | github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= 36 | github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= 37 | github.com/panjf2000/ants/v2 v2.9.1 h1:Q5vh5xohbsZXGcD6hhszzGqB7jSSc2/CRr3QKIga8Kw= 38 | github.com/panjf2000/ants/v2 v2.9.1/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I= 39 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 40 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 41 | github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= 42 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 43 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 44 | github.com/quic-go/quic-go v0.37.4 h1:ke8B73yMCWGq9MfrCCAw0Uzdm7GaViC3i39dsIdDlH4= 45 | github.com/quic-go/quic-go v0.37.4/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU= 46 | github.com/refraction-networking/utls v1.6.0 h1:X5vQMqVx7dY7ehxxqkFER/W6DSjy8TMqSItXm8hRDYQ= 47 | github.com/refraction-networking/utls v1.6.0/go.mod h1:kHJ6R9DFFA0WsRgBM35iiDku4O7AqPR6y79iuzW7b10= 48 | github.com/rivo/tview v0.0.0-20230208211350-7dfff1ce7854 h1:/IIOjnKLbuO5YtZUZaJVw9fc062ChPlaGWEBmJ6jyGY= 49 | github.com/rivo/tview v0.0.0-20230208211350-7dfff1ce7854/go.mod h1:lBUy/T5kyMudFzWUH/C2moN+NlU5qF505vzOyINXuUQ= 50 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 51 | github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= 52 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 54 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 55 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 56 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 57 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 58 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 59 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 60 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 61 | github.com/yunginnanet/Rate5 v1.3.0 h1:URptaJxOx/TtgddbO6C5f5U+MXSAARN3GYlNYEXdw0Y= 62 | github.com/yunginnanet/Rate5 v1.3.0/go.mod h1:f0r66kVQZojRqUgVdLC/CKexMlF0nUDAmd01tBeF4Ms= 63 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 64 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 65 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 66 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 67 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 68 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 69 | golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= 70 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 71 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 72 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 73 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 74 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 75 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 76 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 77 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 78 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 79 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 80 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 81 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 82 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 83 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 84 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 85 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 86 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 87 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 88 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 89 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 90 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 91 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 92 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 93 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 94 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 95 | golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= 96 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 97 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 98 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 99 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 100 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 101 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 102 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 103 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 104 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 105 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 106 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 107 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 108 | golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= 109 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 110 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 111 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 112 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 113 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 114 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 115 | nullprogram.com/x/rng v1.1.0 h1:SMU7DHaQSWtKJNTpNFIFt8Wd/KSmOuSDPXrMFp/UMro= 116 | nullprogram.com/x/rng v1.1.0/go.mod h1:glGw6V87vyfawxCzqOABL3WfL95G65az9Z2JZCylCkg= 117 | -------------------------------------------------------------------------------- /defs.go: -------------------------------------------------------------------------------- 1 | package prox5 2 | 3 | import ( 4 | "container/list" 5 | "context" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "git.tcp.direct/kayos/common/entropy" 11 | cmap "github.com/orcaman/concurrent-map/v2" 12 | "github.com/panjf2000/ants/v2" 13 | rl "github.com/yunginnanet/Rate5" 14 | 15 | "git.tcp.direct/kayos/prox5/internal/scaler" 16 | "git.tcp.direct/kayos/prox5/logger" 17 | ) 18 | 19 | type proxyList struct { 20 | *list.List 21 | *sync.RWMutex 22 | } 23 | 24 | func (pl *proxyList) add(p *Proxy) { 25 | pl.Lock() 26 | defer pl.Unlock() 27 | pl.PushBack(p) 28 | } 29 | 30 | func (pl *proxyList) pop() *Proxy { 31 | pl.Lock() 32 | if pl.Len() < 1 { 33 | pl.Unlock() 34 | return nil 35 | } 36 | p := pl.Remove(pl.Front()).(*Proxy) 37 | pl.Unlock() 38 | return p 39 | } 40 | 41 | // ProxyChannels will likely be unexported in the future. 42 | type ProxyChannels struct { 43 | // SOCKS5 is a constant stream of verified SOCKS5 proxies 44 | SOCKS5 proxyList 45 | // SOCKS4 is a constant stream of verified SOCKS4 proxies 46 | SOCKS4 proxyList 47 | // SOCKS4a is a constant stream of verified SOCKS5 proxies 48 | SOCKS4a proxyList 49 | // HTTP is a constant stream of verified SOCKS5 proxies 50 | HTTP proxyList 51 | } 52 | 53 | // Slice returns a slice of all proxyLists in ProxyChannels, note that HTTP is not included. 54 | func (pc ProxyChannels) Slice() []*proxyList { 55 | lists := []*proxyList{&pc.SOCKS5, &pc.SOCKS4, &pc.SOCKS4a} 56 | entropy.GetOptimizedRand().Shuffle(3, func(i, j int) { 57 | lists[i], lists[j] = lists[j], lists[i] 58 | }) 59 | return lists 60 | } 61 | 62 | // ProxyEngine represents a proxy pool 63 | type ProxyEngine struct { 64 | Valids ProxyChannels 65 | DebugLogger logger.Logger 66 | 67 | // stats holds the Statistics for ProxyEngine 68 | stats Statistics 69 | 70 | Status uint32 71 | 72 | // Pending is a constant stream of proxy strings to be verified 73 | Pending proxyList 74 | 75 | // see: https://pkg.go.dev/github.com/yunginnanet/Rate5 76 | useProx *rl.Limiter 77 | badProx *rl.Limiter 78 | 79 | dispenseMiddleware func(*Proxy) (*Proxy, bool) 80 | 81 | ctx context.Context 82 | quit context.CancelFunc 83 | 84 | httpOptsDirty *atomic.Bool 85 | httpClients *sync.Pool 86 | 87 | proxyMap *proxyMap 88 | 89 | // reaper sync.Pool 90 | 91 | conKiller chan struct{} 92 | 93 | recycleMu *sync.Mutex 94 | mu *sync.RWMutex 95 | pool *ants.Pool 96 | 97 | scaler *scaler.AutoScaler 98 | scaleTimer *time.Ticker 99 | 100 | recycleTimer *time.Ticker 101 | 102 | lastBadProxAnnnounced *atomic.Value 103 | 104 | opt *config 105 | runningdaemons int32 106 | conductor chan bool 107 | } 108 | 109 | var ( 110 | defaultStaleTime = 30 * time.Minute 111 | defaultWorkerCount = 20 112 | defaultBailout = 20 113 | defaultRemoveAfter = 25 114 | // Note: I've chosen to use https here exclusively assuring all validated proxies are SSL capable. 115 | defaultChecks = []string{ 116 | "https://wtfismyip.com/text", 117 | "https://myexternalip.com/raw", 118 | "https://ipinfo.io/ip", 119 | "https://api.ipify.org/", 120 | "https://icanhazip.com/", 121 | "https://ifconfig.me/ip", 122 | "https://www.trackip.net/ip", 123 | "https://checkip.amazonaws.com/", 124 | } 125 | ) 126 | 127 | // Returns a pointer to our default options (modified and accessed later through concurrent safe getters and setters) 128 | func defOpt() *config { 129 | sm := &config{ 130 | useProxConfig: defaultUseProxyRatelimiter, 131 | badProxConfig: defaultBadProxyRateLimiter, 132 | 133 | checkEndpoints: defaultChecks, 134 | userAgents: defaultUserAgents, 135 | RWMutex: &sync.RWMutex{}, 136 | removeafter: defaultRemoveAfter, 137 | recycle: true, 138 | debug: true, 139 | dialerBailout: defaultBailout, 140 | stale: defaultStaleTime, 141 | maxWorkers: defaultWorkerCount, 142 | redact: false, 143 | tlsVerify: false, 144 | shuffle: true, 145 | } 146 | sm.validationTimeout = time.Duration(9) * time.Second 147 | sm.serverTimeout = time.Duration(15) * time.Second 148 | return sm 149 | } 150 | 151 | // config holds our configuration for ProxyEngine instances. 152 | // This is implemented as a pointer, and should be interacted with via the setter and getter functions. 153 | type config struct { 154 | // stale is the amount of time since verification that qualifies a proxy going stale. 155 | // if a stale proxy is drawn during the use of our getter functions, it will be skipped. 156 | stale time.Duration 157 | // userAgents contains a list of userAgents to be randomly drawn from for proxied requests, this should be supplied via SetUserAgents 158 | userAgents []string 159 | // debug when enabled will print results as they come in 160 | debug bool 161 | // checkEndpoints includes web services that respond with (just) the WAN IP of the connection for validation purposes 162 | checkEndpoints []string 163 | // maxWorkers determines the maximum amount of workers used for checking proxies 164 | maxWorkers int 165 | // validationTimeout defines the timeout for proxy validation operations. 166 | // This will apply for both the initial quick check (dial), and the second check (HTTP GET). 167 | validationTimeout time.Duration 168 | // serverTimeout defines the timeout for outgoing connections made with the mysteryDialer. 169 | serverTimeout time.Duration 170 | // dialerBailout defines the amount of times a dial atttempt can fail before giving up and returning an error. 171 | dialerBailout int 172 | // redact when enabled will redact the target string from the debug output 173 | redact bool 174 | // recycle determines whether or not we recycle proxies pack into the pending channel after we dispense them 175 | recycle bool 176 | // remove proxy from recycling after being marked bad this many times 177 | removeafter int 178 | // shuffle determines whether or not we shuffle proxies when we recycle them. 179 | shuffle bool 180 | // tlsVerify determines whether or not we verify the TLS certificate of the endpoints the http client connects to. 181 | tlsVerify bool 182 | 183 | // TODO: make getters and setters for these 184 | useProxConfig rl.Policy 185 | badProxConfig rl.Policy 186 | 187 | *sync.RWMutex 188 | } 189 | 190 | // NewDefaultSwamp returns a new ProxyEngine instance. 191 | // 192 | // Deprecated: use NewProxyEngine instead. 193 | func NewDefaultSwamp() *Swamp { 194 | return &Swamp{NewProxyEngine()} 195 | } 196 | 197 | // Swamp is a deprecated alias for ProxyEngine 198 | // 199 | // Deprecated: use ProxyEngine instead. 200 | type Swamp struct { 201 | *ProxyEngine 202 | } 203 | 204 | // NewProxyEngine returns a ProxyEngine with default options. 205 | // After calling this you may use the various "setters" to change the options before calling ProxyEngine.Start(). 206 | func NewProxyEngine() *ProxyEngine { 207 | p5 := &ProxyEngine{ 208 | stats: Statistics{ 209 | birthday: &atomic.Pointer[time.Time]{}, 210 | accountingLastDone: &atomic.Pointer[time.Time]{}, 211 | }, 212 | DebugLogger: &basicPrinter{}, 213 | 214 | opt: defOpt(), 215 | lastBadProxAnnnounced: &atomic.Value{}, 216 | 217 | conductor: make(chan bool), 218 | mu: &sync.RWMutex{}, 219 | recycleMu: &sync.Mutex{}, 220 | httpOptsDirty: &atomic.Bool{}, 221 | conKiller: make(chan struct{}, 1), 222 | Status: uint32(stateNew), 223 | } 224 | 225 | tnow := time.Now() 226 | p5.stats.birthday.Store(&tnow) 227 | p5.stats.accountingLastDone.Store(&tnow) 228 | 229 | p5.lastBadProxAnnnounced.Store("") 230 | p5.httpOptsDirty.Store(false) 231 | p5.httpClients = &sync.Pool{New: func() interface{} { return p5.newHTTPClient() }} 232 | 233 | stats := []**atomic.Int64{ 234 | &p5.stats.Valid4, &p5.stats.Valid4a, &p5.stats.Valid5, &p5.stats.ValidHTTP, &p5.stats.Dispensed, 235 | &p5.stats.Checked, &p5.stats.badAccounted, &p5.stats.Stale, 236 | } 237 | for _, i := range stats { 238 | *i = &atomic.Int64{} 239 | } 240 | 241 | lists := []*proxyList{&p5.Valids.SOCKS5, &p5.Valids.SOCKS4, &p5.Valids.SOCKS4a, &p5.Valids.HTTP, &p5.Pending} 242 | for _, c := range lists { 243 | *c = proxyList{ 244 | List: &list.List{}, 245 | RWMutex: &sync.RWMutex{}, 246 | } 247 | } 248 | 249 | p5.dispenseMiddleware = func(p *Proxy) (*Proxy, bool) { 250 | return p, true 251 | } 252 | p5.ctx, p5.quit = context.WithCancel(context.Background()) 253 | // p5.conCtx, p5.killConns = context.WithCancel(context.Background()) 254 | p5.proxyMap = newProxyMap(p5) 255 | 256 | atomic.StoreUint32(&p5.Status, uint32(stateNew)) 257 | atomic.StoreInt32(&p5.runningdaemons, 0) 258 | 259 | p5.useProx = rl.NewCustomLimiter(p5.opt.useProxConfig) 260 | p5.badProx = rl.NewCustomLimiter(p5.opt.badProxConfig) 261 | 262 | var err error 263 | p5.pool, err = ants.NewPool(p5.opt.maxWorkers, ants.WithOptions(ants.Options{ 264 | ExpiryDuration: 2 * time.Minute, 265 | PanicHandler: p5.pondPanic, 266 | })) 267 | 268 | p5.scaler = scaler.NewAutoScaler(p5.opt.maxWorkers, p5.opt.maxWorkers+100, 50) 269 | p5.scaleTimer = time.NewTicker(1 * time.Second) 270 | p5.recycleTimer = time.NewTicker(500 * time.Millisecond) 271 | 272 | if err != nil { 273 | buf := strs.Get() 274 | buf.MustWriteString("CRITICAL: ") 275 | buf.MustWriteString(err.Error()) 276 | p5.dbgPrint(buf) 277 | panic(err) 278 | } 279 | 280 | return p5 281 | } 282 | 283 | func newProxyMap(pe *ProxyEngine) *proxyMap { 284 | return &proxyMap{ 285 | plot: cmap.New[*Proxy](), 286 | parent: pe, 287 | } 288 | } 289 | 290 | func (p5 *ProxyEngine) pondPanic(p interface{}) { 291 | panic(p) 292 | // pe.dbgPrint("Worker panic: " + fmt.Sprintf("%v", p)) 293 | } 294 | 295 | // defaultUserAgents is a small list of user agents to use during validation. 296 | var defaultUserAgents = []string{ 297 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", 298 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", 299 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36", 300 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv109.0) Gecko/20100101 Firefox/115.0", 301 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36", 302 | "Mozilla/5.0 (X11; Linux x86_64; rv109.0) Gecko/20100101 Firefox/115.0", 303 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", 304 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv109.0) Gecko/20100101 Firefox/115.0", 305 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36", 306 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.15", 307 | "Mozilla/5.0 (Windows NT 10.0; rv109.0) Gecko/20100101 Firefox/115.0", 308 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv109.0) Gecko/20100101 Firefox/116.0", 309 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.82", 310 | "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv109.0) Gecko/20100101 Firefox/115.0", 311 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.1 Safari/605.1.15", 312 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188", 313 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv109.0) Gecko/20100101 Firefox/114.0", 314 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36", 315 | "Mozilla/5.0 (X11; Linux x86_64; rv102.0) Gecko/20100101 Firefox/102.0", 316 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.183", 317 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67", 318 | "Mozilla/5.0 (X11; Linux x86_64; rv109.0) Gecko/20100101 Firefox/114.0", 319 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 OPR/100.0.0.0", 320 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv102.0) Gecko/20100101 Firefox/102.0", 321 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv109.0) Gecko/20100101 Firefox/114.0", 322 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.79", 323 | "Mozilla/5.0 (X11; Linux x86_64; rv109.0) Gecko/20100101 Firefox/116.0", 324 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36", 325 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15", 326 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36", 327 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 OPR/99.0.0.0", 328 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", 329 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15", 330 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36", 331 | "Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", 332 | "Mozilla/5.0 (Windows NT 10.0; rv102.0) Gecko/20100101 Firefox/102.0", 333 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36", 334 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Safari/605.1.15", 335 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", 336 | "Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5666.197 Safari/537.36", 337 | "Mozilla/5.0 (Windows NT 10.0; rv109.0) Gecko/20100101 Firefox/116.0", 338 | "Mozilla/5.0 (Windows NT 10.0; rv114.0) Gecko/20100101 Firefox/114.0", 339 | "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv109.0) Gecko/20100101 Firefox/114.0", 340 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36", 341 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.1 Safari/605.1.15", 342 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36", 343 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv109.0) Gecko/20100101 Firefox/116.0", 344 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.86", 345 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36", 346 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv109.0) Gecko/20100101 Firefox/113.0", 347 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36", 348 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15", 349 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 YaBrowser/23.5.4.674 Yowser/2.5 Safari/537.36", 350 | "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv109.0) Gecko/20100101 Firefox/116.0", 351 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36", 352 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36", 353 | } 354 | --------------------------------------------------------------------------------