├── .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 |
7 |
8 | [](https://pkg.go.dev/git.tcp.direct/kayos/prox5) [](https://goreportcard.com/report/github.com/yunginnanet/prox5) [](ircs://ircd.chat:6697/#tcpdirect) [](https://github.com/yunginnanet/prox5/actions/workflows/go.yml) 
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 |