From 1f29e12cf0ae64d8905db5a49f19473a3d6f2d6e Mon Sep 17 00:00:00 2001 From: xteamlyer <29282888+xteamlyer@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:20:19 +0300 Subject: [PATCH] chore: some changes --- .github/workflows/build.yml | 6 +- README.md | 15 +++-- dialer/upstream.go | 17 +++++- go.mod | 6 +- go.sum | 2 + handler/handler.go | 48 ++++++++++++---- handler/socks.go | 2 +- main.go | 108 +++++++++++++++++++++++++----------- renovate.json | 6 ++ seclient/seclient.go | 33 ++++++++--- 10 files changed, 175 insertions(+), 68 deletions(-) create mode 100644 renovate.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6fc056b..d5c55a3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,12 +14,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v6 with: go-version: 'stable' - @@ -35,7 +35,7 @@ jobs: VERSION=${{steps.tag.outputs.tag}} - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v3 with: files: bin/* fail_on_unmatched_files: true diff --git a/README.md b/README.md index 61ba091..dff1fa9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ -opera-proxy -=========== +# opera-proxy fork + +- Original author - [Snawoot](https://github.com/Snawoot) +- Fork based - [Alexey71](https://github.com/Alexey71) +- Some ideas - [SLY-F0X](https://github.com/SLY-F0X) Standalone Opera VPN client. @@ -8,10 +11,10 @@ By default the application listens on 127.0.0.1:18080. ## Features -* Cross-platform (Windows/Mac OS/Linux/Android (via shell)/\*BSD) -* Uses TLS for secure communication with upstream proxies -* Zero configuration -* Simple and straightforward +- Cross-platform (Windows/Mac OS/Linux/Android (via shell)/\*BSD) +- Uses TLS for secure communication with upstream proxies +- Zero configuration +- Simple and straightforward ## Installation diff --git a/dialer/upstream.go b/dialer/upstream.go index 56eed3c..3e5ea15 100644 --- a/dialer/upstream.go +++ b/dialer/upstream.go @@ -115,9 +115,14 @@ func (d *ProxyDialer) DialContext(ctx context.Context, network, address string) return nil, err } if uTLSServerName != "" { - // Custom cert verification logic: - // DO NOT send SNI extension of TLS ClientHello - // DO peer certificate verification against specified servername + // Custom TLS verification strategy: + // - Do NOT send SNI in ClientHello (use fakeSNI, may be empty string). + // - Verify the peer certificate against the real server name using + // the explicit caPool (Mozilla NSS bundle via bundle.Roots()). + // + // No cross-signed intermediate injection needed: bundle.Roots() already + // contains USERTrust ECC CA as a trusted root, so Go's chain builder + // resolves Opera's certificate chain without any manual patching. conn = tls.Client(conn, &tls.Config{ ServerName: fakeSNI, InsecureSkipVerify: true, @@ -186,6 +191,12 @@ func (d *ProxyDialer) Address() (string, error) { return d.address() } +// readResponse reads an HTTP/1.1 response from the raw conn after a CONNECT +// request. It reads byte-by-byte until the \r\n\r\n header terminator is found, +// then hands the accumulated bytes to http.ReadResponse. +// +// Note: byte-by-byte reading is intentional — we must not over-read past the +// end of headers into the tunneled TLS stream. func readResponse(r io.Reader, req *http.Request) (*http.Response, error) { endOfResponse := []byte("\r\n\r\n") buf := &bytes.Buffer{} diff --git a/go.mod b/go.mod index 01dde98..44a0608 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ -module github.com/Alexey71/opera-proxy +module github.com/xteamlyer/opera-proxy go 1.25.0 -toolchain go1.25.9 +toolchain go1.26.2 require ( github.com/Alexey71/go-http-digest-auth-client v1.1.3 github.com/Alexey71/go-multierror v1.1.3 github.com/ncruces/go-dns v1.3.3 github.com/things-go/go-socks5 v0.1.1 - golang.org/x/crypto/x509roots/fallback v0.0.0-20260413170323-a8e9237a216b + golang.org/x/crypto/x509roots/fallback v0.0.0-20260423152011-b9e53593a607 golang.org/x/net v0.53.0 ) diff --git a/go.sum b/go.sum index 8e4ecd7..2267ece 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/things-go/go-socks5 v0.1.1 h1:48hy9cHEXPKeG91G/g4n8zW4uynzPUQy/FkcrJ7 github.com/things-go/go-socks5 v0.1.1/go.mod h1:1YBHVYG7Oli5ae+Pwkp630cPAwY1pjUPmohO1n0Emg0= golang.org/x/crypto/x509roots/fallback v0.0.0-20260413170323-a8e9237a216b h1:ZG2SxTKsx1w3pUpOMD9dliRYnhWC5R5jmL6UDPCbYj4= golang.org/x/crypto/x509roots/fallback v0.0.0-20260413170323-a8e9237a216b/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260423152011-b9e53593a607 h1:WlWLkLEVGjGS9LziRuLo1Ut+UkpzbXxFQh94eUUVjeg= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260423152011-b9e53593a607/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/handler/handler.go b/handler/handler.go index 09c4793..ceacf6a 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -12,32 +12,49 @@ import ( "sync" "time" - "github.com/Alexey71/opera-proxy/dialer" - clog "github.com/Alexey71/opera-proxy/log" + "github.com/xteamlyer/opera-proxy/dialer" + clog "github.com/xteamlyer/opera-proxy/log" ) const ( COPY_BUF = 128 * 1024 BAD_REQ_MSG = "Bad Request\n" + + // Reduced idle pool: the proxy handler makes upstream connections per request, + // not persistent keep-alive sessions. 10 total / 2 per host is plenty and + // avoids leaking hundreds of idle goroutines/sockets under bursty traffic. + TRANSPORT_MAX_IDLE_CONNS = 10 + TRANSPORT_MAX_IDLE_CONNS_PER_HOST = 2 + TRANSPORT_IDLE_CONN_TIMEOUT = 60 * time.Second ) +// copyBufPool reuses 128 KiB buffers for bidirectional data relay, +// avoiding per-connection heap allocations. +var copyBufPool = sync.Pool{ + New: func() any { + b := make([]byte, COPY_BUF) + return &b + }, +} + type ProxyHandler struct { logger *clog.CondLogger dialer dialer.ContextDialer httptransport http.RoundTripper } -func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger) *ProxyHandler { +func NewProxyHandler(d dialer.ContextDialer, logger *clog.CondLogger) *ProxyHandler { httptransport := &http.Transport{ - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, + MaxIdleConns: TRANSPORT_MAX_IDLE_CONNS, + MaxIdleConnsPerHost: TRANSPORT_MAX_IDLE_CONNS_PER_HOST, + IdleConnTimeout: TRANSPORT_IDLE_CONN_TIMEOUT, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, - DialContext: dialer.DialContext, + DialContext: d.DialContext, } return &ProxyHandler{ logger: logger, - dialer: dialer, + dialer: d, httptransport: httptransport, } } @@ -119,7 +136,10 @@ func proxy(ctx context.Context, left, right net.Conn) { wg := sync.WaitGroup{} cpy := func(dst, src net.Conn) { defer wg.Done() - io.Copy(dst, src) + // Grab a pooled buffer for this copy direction. + bufp := copyBufPool.Get().(*[]byte) + defer copyBufPool.Put(bufp) + io.CopyBuffer(dst, src, *bufp) dst.Close() } wg.Add(2) @@ -145,7 +165,9 @@ func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer wg := sync.WaitGroup{} ltr := func(dst net.Conn, src io.Reader) { defer wg.Done() - io.Copy(dst, src) + bufp := copyBufPool.Get().(*[]byte) + defer copyBufPool.Put(bufp) + io.CopyBuffer(dst, src, *bufp) dst.Close() } rtl := func(dst io.Writer, src io.Reader) { @@ -226,12 +248,14 @@ func flush(flusher interface{}) bool { } func copyBody(wr io.Writer, body io.Reader) { - buf := make([]byte, COPY_BUF) + // Use pooled buffer to avoid per-call allocation. + bufp := copyBufPool.Get().(*[]byte) + defer copyBufPool.Put(bufp) for { - bread, read_err := body.Read(buf) + bread, read_err := body.Read(*bufp) var write_err error if bread > 0 { - _, write_err = wr.Write(buf[:bread]) + _, write_err = wr.Write((*bufp)[:bread]) flush(wr) } if read_err != nil || write_err != nil { diff --git a/handler/socks.go b/handler/socks.go index 57037ac..b4b4e5e 100644 --- a/handler/socks.go +++ b/handler/socks.go @@ -5,7 +5,7 @@ import ( "log" "net" - "github.com/Alexey71/opera-proxy/dialer" + "github.com/xteamlyer/opera-proxy/dialer" "github.com/things-go/go-socks5" ) diff --git a/main.go b/main.go index 43b6676..ede8a09 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,6 @@ import ( "flag" "fmt" "io" - "io/ioutil" "log" "net" "net/http" @@ -23,12 +22,12 @@ import ( xproxy "golang.org/x/net/proxy" - "github.com/Alexey71/opera-proxy/clock" - "github.com/Alexey71/opera-proxy/dialer" - "github.com/Alexey71/opera-proxy/handler" - clog "github.com/Alexey71/opera-proxy/log" - "github.com/Alexey71/opera-proxy/resolver" - se "github.com/Alexey71/opera-proxy/seclient" + "github.com/xteamlyer/opera-proxy/clock" + "github.com/xteamlyer/opera-proxy/dialer" + "github.com/xteamlyer/opera-proxy/handler" + clog "github.com/xteamlyer/opera-proxy/log" + "github.com/xteamlyer/opera-proxy/resolver" + se "github.com/xteamlyer/opera-proxy/seclient" _ "golang.org/x/crypto/x509roots/fallback" "golang.org/x/crypto/x509roots/fallback/bundle" @@ -37,6 +36,15 @@ import ( const ( API_DOMAIN = "api2.sec-tunnel.com" PROXY_SUFFIX = "sec-tunnel.com" + + // Default timeouts increased to reduce premature API errors on slow networks. + DEFAULT_TIMEOUT = 30 * time.Second + DEFAULT_SERVER_SELECTION_TIMEOUT = 60 * time.Second + + // Reduced idle connection pool to lower resource usage on embedded/low-RAM hosts. + HTTP_MAX_IDLE_CONNS = 10 + HTTP_MAX_IDLE_CONNS_PER_HOST = 3 + HTTP_IDLE_CONN_TIMEOUT = 60 * time.Second ) func perror(msg string) { @@ -112,6 +120,9 @@ type CLIArgs struct { proxy string apiLogin string apiPassword string + // randomAPICreds: when true, ignore --api-login/--api-password and + // generate a fresh cryptographically-random credential pair on every run. + randomAPICreds bool apiAddress string apiClientType string apiClientVersion string @@ -156,7 +167,8 @@ func parse_args() *CLIArgs { flag.BoolVar(&args.socksMode, "socks-mode", false, "listen for SOCKS requests instead of HTTP") flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity "+ "(10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical)") - flag.DurationVar(&args.timeout, "timeout", 10*time.Second, "timeout for network operations") + flag.DurationVar(&args.timeout, "timeout", DEFAULT_TIMEOUT, + "timeout for network operations (default raised to 30s for better API reliability)") flag.BoolVar(&args.showVersion, "version", false, "show program version and exit") flag.StringVar(&args.proxy, "proxy", "", "sets base proxy to use for all dial-outs. "+ "Format: ://[login:password@]host[:port] "+ @@ -164,8 +176,13 @@ func parse_args() *CLIArgs { flag.StringVar(&args.apiClientVersion, "api-client-version", se.DefaultSESettings.ClientVersion, "client version reported to SurfEasy API") flag.StringVar(&args.apiClientType, "api-client-type", se.DefaultSESettings.ClientType, "client type reported to SurfEasy API") flag.StringVar(&args.apiUserAgent, "api-user-agent", se.DefaultSESettings.UserAgent, "user agent reported to SurfEasy API") - flag.StringVar(&args.apiLogin, "api-login", "se0316", "SurfEasy API login") - flag.StringVar(&args.apiPassword, "api-password", "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II", "SurfEasy API password") + flag.StringVar(&args.apiLogin, "api-login", "se0316", + "SurfEasy API login (ignored when -random-api-creds is set)") + flag.StringVar(&args.apiPassword, "api-password", "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II", + "SurfEasy API password (ignored when -random-api-creds is set)") + flag.BoolVar(&args.randomAPICreds, "random-api-creds", false, + "generate a random API login/password pair on every run instead of using -api-login/-api-password; "+ + "reduces fingerprinting and avoids credential-reuse bans") flag.StringVar(&args.apiAddress, "api-address", "", fmt.Sprintf("override IP address of %s", API_DOMAIN)) flag.StringVar(&args.apiProxy, "api-proxy", "", "additional proxy server used to access SurfEasy API") flag.Var(args.bootstrapDNS, "bootstrap-dns", @@ -180,10 +197,13 @@ func parse_args() *CLIArgs { flag.StringVar(&args.fakeSNI, "fake-SNI", "", "domain name to use as SNI in communications with servers") flag.StringVar(&args.overrideProxyAddress, "override-proxy-address", "", "use fixed proxy address instead of server address returned by SurfEasy API") flag.Var(&args.serverSelection, "server-selection", "server selection policy (first/random/fastest)") - flag.DurationVar(&args.serverSelectionTimeout, "server-selection-timeout", 30*time.Second, "timeout given for server selection function to produce result") - flag.StringVar(&args.serverSelectionTestURL, "server-selection-test-url", "https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js", + flag.DurationVar(&args.serverSelectionTimeout, "server-selection-timeout", DEFAULT_SERVER_SELECTION_TIMEOUT, + "timeout given for server selection function to produce result (default raised to 60s)") + flag.StringVar(&args.serverSelectionTestURL, "server-selection-test-url", + "https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js", "URL used for download benchmark by fastest server selection policy") - flag.Int64Var(&args.serverSelectionDLLimit, "server-selection-dl-limit", 0, "restrict amount of downloaded data per connection by fastest server selection") + flag.Int64Var(&args.serverSelectionDLLimit, "server-selection-dl-limit", 0, + "restrict amount of downloaded data per connection by fastest server selection") flag.Func("config", "read configuration from file with space-separated keys and values", readConfig) flag.Parse() if args.country == "" { @@ -200,10 +220,27 @@ func proxyFromURLWrapper(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error) if !ok { return nil, errors.New("only context dialers are accepted") } - return dialer.ProxyDialerFromURL(u, cdialer) } +// buildAPITransport returns an http.Transport tuned for infrequent API calls: +// reduced idle pool (saves goroutines/sockets), no forced HTTP/2. +func buildAPITransport( + dialCtx func(context.Context, string, string) (net.Conn, error), + dialTLSCtx func(context.Context, string, string) (net.Conn, error), +) *http.Transport { + return &http.Transport{ + DialContext: dialCtx, + DialTLSContext: dialTLSCtx, + ForceAttemptHTTP2: false, + MaxIdleConns: HTTP_MAX_IDLE_CONNS, + MaxIdleConnsPerHost: HTTP_MAX_IDLE_CONNS_PER_HOST, + IdleConnTimeout: HTTP_IDLE_CONN_TIMEOUT, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } +} + func run() int { args := parse_args() if args.showVersion { @@ -232,7 +269,8 @@ func run() int { caPool := x509.NewCertPool() if args.caFile != "" { - certs, err := ioutil.ReadFile(args.caFile) + // os.ReadFile replaces deprecated ioutil.ReadFile + certs, err := os.ReadFile(args.caFile) if err != nil { mainLogger.Error("Can't load CA file: %v", err) return 15 @@ -290,35 +328,44 @@ func run() int { mainLogger.Info("Using fixed API host address = %s", args.apiAddress) seclientDialer = dialer.NewFixedDialer(args.apiAddress, seclientDialer) } else if len(args.bootstrapDNS.values) > 0 { - resolver, err := resolver.FastFromURLs(caPool, args.bootstrapDNS.values...) + res, err := resolver.FastFromURLs(caPool, args.bootstrapDNS.values...) if err != nil { mainLogger.Critical("Unable to instantiate DNS resolver: %v", err) return 4 } - seclientDialer = dialer.NewResolvingDialer(resolver, seclientDialer) + seclientDialer = dialer.NewResolvingDialer(res, seclientDialer) } // Dialing w/o SNI, receiving self-signed certificate, so skip verification. - // Either way we'll validate certificate of actual proxy server. + // Either way we validate the certificate of the actual proxy server. tlsConfig := &tls.Config{ ServerName: args.fakeSNI, InsecureSkipVerify: true, } - seclient, err := se.NewSEClient(args.apiLogin, args.apiPassword, &http.Transport{ - DialContext: seclientDialer.DialContext, - DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + + // Resolve which API credentials to use. + apiLogin := args.apiLogin + apiPassword := args.apiPassword + if args.randomAPICreds { + var err error + apiLogin, apiPassword, err = se.GenerateRandomAPICreds() + if err != nil { + mainLogger.Critical("Failed to generate random API credentials: %v", err) + return 17 + } + mainLogger.Info("Using randomly generated API credentials (login prefix: %.8s...)", apiLogin) + } + + seclient, err := se.NewSEClient(apiLogin, apiPassword, buildAPITransport( + seclientDialer.DialContext, + func(ctx context.Context, network, addr string) (net.Conn, error) { conn, err := seclientDialer.DialContext(ctx, network, addr) if err != nil { return conn, err } return tls.Client(conn, tlsConfig), nil }, - ForceAttemptHTTP2: true, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }) + )) if err != nil { mainLogger.Critical("Unable to construct SEClient: %v", err) return 8 @@ -473,7 +520,7 @@ func run() int { if args.socksMode { socks, initError := handler.NewSocksServer(handlerDialer, socksLogger) if initError != nil { - mainLogger.Critical("Failed to start: %v", err) + mainLogger.Critical("Failed to start: %v", initError) return 16 } mainLogger.Info("Init complete.") @@ -557,10 +604,7 @@ func dpExport(ips []se.SEIPEntry, seclient *se.SEClient, sni string) int { if gotOne { key = "#proxy" } - wr.Write([]string{ - key, - u.String(), - }) + wr.Write([]string{key, u.String()}) gotOne = true } return 0 diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..5db72dd --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +} diff --git a/seclient/seclient.go b/seclient/seclient.go index fb1c7d5..bbf9c00 100644 --- a/seclient/seclient.go +++ b/seclient/seclient.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "math/rand" "net/http" "net/url" @@ -20,6 +19,10 @@ const ( ANON_PASSWORD_BYTES = 20 DEVICE_ID_BYTES = 20 READ_LIMIT int64 = 128 * 1024 + + // Byte length for randomly generated API credential components. + RANDOM_API_LOGIN_BYTES = 16 + RANDOM_API_PASSWORD_BYTES = 24 ) type SEEndpoints struct { @@ -73,9 +76,24 @@ type SEClient struct { type StrKV map[string]string -// Instantiates SurfEasy client with default settings and given API keys. -// Optional `transport` parameter allows to override HTTP transport used -// for HTTP calls +// GenerateRandomAPICreds returns a cryptographically-random (login, password) pair +// suitable for use as SurfEasy digest-auth credentials. Call this instead of +// using the hardcoded defaults when -random-api-creds is set. +func GenerateRandomAPICreds() (login, password string, err error) { + rng := rand.New(RandomSource) + login, err = randomCapitalHexString(rng, RANDOM_API_LOGIN_BYTES) + if err != nil { + return "", "", fmt.Errorf("generate random api login: %w", err) + } + password, err = randomCapitalHexString(rng, RANDOM_API_PASSWORD_BYTES) + if err != nil { + return "", "", fmt.Errorf("generate random api password: %w", err) + } + return login, password, nil +} + +// NewSEClient instantiates a SurfEasy client with default settings and given API keys. +// Optional transport parameter allows overriding the HTTP transport used for API calls. func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*SEClient, error) { if transport == nil { transport = http.DefaultTransport @@ -293,7 +311,7 @@ func (c *SEClient) RpcCall(ctx context.Context, endpoint string, params map[stri } func (c *SEClient) rpcCall(ctx context.Context, endpoint string, params map[string]string, res interface{}) error { - input := make(url.Values) + input := make(url.Values, len(params)) for k, v := range params { input[k] = []string{v} } @@ -330,10 +348,9 @@ func (c *SEClient) rpcCall(ctx context.Context, endpoint string, params map[stri return nil } -// Does cleanup of HTTP response in order to make it reusable by keep-alive -// logic of HTTP client +// cleanupBody drains and closes an HTTP response body to allow connection reuse. func cleanupBody(body io.ReadCloser) { - io.Copy(ioutil.Discard, &io.LimitedReader{ + io.Copy(io.Discard, &io.LimitedReader{ R: body, N: READ_LIMIT, })