From 63b9203be472e589e73ec146f13779177b49f4cd Mon Sep 17 00:00:00 2001 From: xteamlyer Date: Thu, 30 Apr 2026 19:04:28 +0300 Subject: [PATCH] Some change --- dialer/upstream.go | 17 ++++- handler/handler.go | 43 +++++++++--- main.go | 151 ++++++++++++++++++++++++++----------------- seclient/seclient.go | 67 ++++++++----------- 4 files changed, 165 insertions(+), 113 deletions(-) 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/handler/handler.go b/handler/handler.go index 420d61f..5807854 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -19,8 +19,24 @@ import ( 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 @@ -28,17 +44,18 @@ type ProxyHandler struct { fakeSNI string } -func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger, fakeSNI string) *ProxyHandler { +func NewProxyHandler(d dialer.ContextDialer, logger *clog.CondLogger, fakeSNI string) *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, fakeSNI: fakeSNI, } @@ -130,7 +147,10 @@ func proxy(ctx context.Context, left net.Conn, leftReader io.Reader, right net.C } rtl := 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) @@ -156,6 +176,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() + bufp := copyBufPool.Get().(*[]byte) + defer copyBufPool.Put(bufp) + io.CopyBuffer(dst, src, *bufp) copyWithSNIRewrite(dst, src, fakeSNI) dst.Close() } @@ -237,12 +260,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/main.go b/main.go index 43de79b..6345804 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,6 @@ import ( "flag" "fmt" "io" - "io/ioutil" "log" "net" "net/http" @@ -42,6 +41,15 @@ const ( PROXY_SUFFIX = "sec-tunnel.com" DefaultDiscoverCSVFallback = "proxies.csv" DefaultProxyBypassFallback = "proxy-bypass.txt" + + // 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) { @@ -188,7 +196,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") 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] "+ @@ -221,10 +230,13 @@ func parse_args() *CLIArgs { flag.DurationVar(&args.proxySpeedTimeout, "proxy-speed-timeout", 15*time.Second, "timeout for a single proxy speed measurement") flag.Int64Var(&args.proxySpeedDLLimit, "proxy-speed-dl-limit", 262144, "limit of downloaded bytes for proxy speed measurement") 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") + 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() flag.Visit(func(f *flag.Flag) { @@ -276,6 +288,58 @@ func proxyFromURLWrapper(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error) 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, + } +} + +// buildCAPool constructs the x509 cert pool used for all TLS verification. +// When -cafile is given, only that file is loaded (useful for custom/corporate CAs). +// Otherwise the bundled Mozilla NSS root store is used, which includes all +// major roots and supports AddCertWithConstraint for name-constrained CAs — +// strictly better than a plain PEM file. +func buildCAPool(caFile string, logger *clog.CondLogger) (*x509.CertPool, int) { + pool := x509.NewCertPool() + if caFile != "" { + certs, err := os.ReadFile(caFile) + if err != nil { + logger.Error("Can't load CA file: %v", err) + return nil, 15 + } + if ok := pool.AppendCertsFromPEM(certs); !ok { + logger.Error("Can't load certificates from CA file") + return nil, 15 + } + return pool, 0 + } + for c := range bundle.Roots() { + cert, err := x509.ParseCertificate(c.Certificate) + if err != nil { + logger.Error("Unable to parse bundled certificate: %v", err) + return nil, 15 + } + if c.Constraint == nil { + pool.AddCert(cert) + } else { + pool.AddCertWithConstraint(cert, c.Constraint) + } + } + return pool, 0 +} + func normalizeAPIProxy(raw string) (string, error) { raw = strings.TrimSpace(raw) if raw == "" { @@ -441,27 +505,25 @@ func newSEClient(args *CLIArgs, baseDialer dialer.ContextDialer, caPool *x509.Ce seclientDialer = dialer.NewResolvingDialer(resolver, seclientDialer) } - // Dialing w/o SNI, receiving self-signed certificate, so skip verification. - // Either way we'll validate certificate of actual proxy server. + // TLS config for the API connection: SNI suppressed (or faked), cert + // verification is skipped at the TLS layer because the API endpoint uses + // a self-signed cert — actual peer verification happens in VerifyConnection + // inside ProxyDialer for the proxy connections. 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) { + + seclient, err := se.NewSEClient(args.apiLogin, args.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 { return nil, fmt.Errorf("unable to construct SEClient: %w", err) } @@ -659,30 +721,9 @@ func run() int { KeepAlive: 30 * time.Second, } - caPool := x509.NewCertPool() - if args.caFile != "" { - certs, err := ioutil.ReadFile(args.caFile) - if err != nil { - mainLogger.Error("Can't load CA file: %v", err) - return 15 - } - if ok := caPool.AppendCertsFromPEM(certs); !ok { - mainLogger.Error("Can't load certificates from CA file") - return 15 - } - } else { - for c := range bundle.Roots() { - cert, err := x509.ParseCertificate(c.Certificate) - if err != nil { - mainLogger.Error("Unable to parse bundled certificate: %v", err) - return 15 - } - if c.Constraint == nil { - caPool.AddCert(cert) - } else { - caPool.AddCertWithConstraint(cert, c.Constraint) - } - } + caPool, exitCode := buildCAPool(args.caFile, mainLogger) + if exitCode != 0 { + return exitCode } xproxy.RegisterDialerType("http", proxyFromURLWrapper) @@ -793,12 +834,12 @@ func run() int { if args.apiProxy != "" { apiProxyURL, err := url.Parse(args.apiProxy) if err != nil { - mainLogger.Critical("Unable to parse base proxy URL: %v", err) + mainLogger.Critical("Unable to parse api-proxy URL: %v", err) return 6 } pxDialer, err := xproxy.FromURL(apiProxyURL, seclientDialer) if err != nil { - mainLogger.Critical("Unable to instantiate base proxy dialer: %v", err) + mainLogger.Critical("Unable to instantiate api-proxy dialer: %v", err) return 7 } seclientDialer = pxDialer.(dialer.ContextDialer) @@ -806,12 +847,12 @@ func run() int { if 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. @@ -959,9 +1000,7 @@ func run() int { ss = dialer.NewFastestServerSelectionFunc( args.serverSelectionTestURL, args.serverSelectionDLLimit, - &tls.Config{ - RootCAs: caPool, - }, + &tls.Config{RootCAs: caPool}, ) default: panic("unhandled server selection value got past parsing") @@ -1012,8 +1051,7 @@ func run() int { mainLogger.Info("Refreshing login...") reqCtx, cl := context.WithTimeout(ctx, args.timeout) defer cl() - err := seclient.Login(reqCtx) - if err != nil { + if err := seclient.Login(reqCtx); err != nil { mainLogger.Error("Login refresh failed: %v", err) return err } @@ -1022,8 +1060,7 @@ func run() int { mainLogger.Info("Refreshing device password...") reqCtx, cl = context.WithTimeout(ctx, args.timeout) defer cl() - err = seclient.DeviceGeneratePassword(reqCtx) - if err != nil { + if err := seclient.DeviceGeneratePassword(reqCtx); err != nil { mainLogger.Error("Device password refresh failed: %v", err) return err } @@ -1035,7 +1072,7 @@ func run() int { if args.socksMode { socks, initError := handler.NewSocksServer(handlerDialer, socksLogger, args.fakeSNI) if initError != nil { - mainLogger.Critical("Failed to start: %v", err) + mainLogger.Critical("Failed to start: %v", initError) return 16 } mainLogger.Info("Init complete.") @@ -1155,10 +1192,7 @@ func dpExport(ips []se.SEIPEntry, seclient *se.SEClient, sni string) int { u := url.URL{ Scheme: "https", User: creds, - Host: net.JoinHostPort( - ip.IP, - strconv.Itoa(int(ip.Ports[0])), - ), + Host: net.JoinHostPort(ip.IP, strconv.Itoa(int(ip.Ports[0]))), RawQuery: url.Values{ "sni": []string{sni}, "peername": []string{fmt.Sprintf("%s%d.%s", strings.ToLower(ip.Geo.CountryCode), i, PROXY_SUFFIX)}, @@ -1168,10 +1202,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/seclient/seclient.go b/seclient/seclient.go index d1490a0..d756ead 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" @@ -16,10 +15,10 @@ import ( ) const ( - ANON_EMAIL_LOCALPART_BYTES = 32 - ANON_PASSWORD_BYTES = 20 - DEVICE_ID_BYTES = 20 - READ_LIMIT int64 = 128 * 1024 + ANON_EMAIL_LOCALPART_BYTES = 32 + ANON_PASSWORD_BYTES = 20 + DEVICE_ID_BYTES = 20 + READ_LIMIT int64 = 128 * 1024 ) type SEEndpoints struct { @@ -73,9 +72,10 @@ 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 +// NewSEClient instantiates a SurfEasy client. +// apiUsername/apiSecret are the application-level Digest Auth credentials +// embedded in every Opera client — they are NOT per-user and must not be randomised. +// transport may be nil (uses http.DefaultTransport). func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*SEClient, error) { if transport == nil { transport = http.DefaultTransport @@ -83,7 +83,7 @@ func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*S rng := rand.New(RandomSource) - device_id, err := randomCapitalHexString(rng, DEVICE_ID_BYTES) + deviceID, err := randomCapitalHexString(rng, DEVICE_ID_BYTES) if err != nil { return nil, err } @@ -93,17 +93,15 @@ func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*S return nil, err } - res := &SEClient{ + return &SEClient{ httpClient: &http.Client{ Jar: jar, Transport: dac.NewDigestTransport(apiUsername, apiSecret, transport), }, Settings: DefaultSESettings, rng: rng, - DeviceID: device_id, - } - - return res, nil + DeviceID: deviceID, + }, nil } func (c *SEClient) ResetCookies() error { @@ -125,6 +123,8 @@ func (c *SEClient) AnonRegister(ctx context.Context) error { return err } + // Each run generates a fresh random subscriber identity — this is the + // actual anonymisation layer. The API-level credentials above are fixed. c.SubscriberEmail = fmt.Sprintf("%s@%s.best.vpn", localPart, c.Settings.ClientType) c.SubscriberPassword = capitalHexSHA1(c.SubscriberEmail) @@ -138,13 +138,12 @@ func (c *SEClient) Register(ctx context.Context) error { } func (c *SEClient) register(ctx context.Context) error { - err := c.resetCookies() - if err != nil { + if err := c.resetCookies(); err != nil { return err } var regRes SERegisterSubscriberResponse - err = c.rpcCall(ctx, c.Settings.Endpoints.RegisterSubscriber, StrKV{ + err := c.rpcCall(ctx, c.Settings.Endpoints.RegisterSubscriber, StrKV{ "email": c.SubscriberEmail, "password": c.SubscriberPassword, }, ®Res) @@ -225,13 +224,12 @@ func (c *SEClient) Login(ctx context.Context) error { c.Mux.Lock() defer c.Mux.Unlock() - err := c.resetCookies() - if err != nil { + if err := c.resetCookies(); err != nil { return err } var loginRes SESubscriberLoginResponse - err = c.rpcCall(ctx, c.Settings.Endpoints.SubscriberLogin, StrKV{ + err := c.rpcCall(ctx, c.Settings.Endpoints.SubscriberLogin, StrKV{ "login": c.SubscriberEmail, "password": c.SubscriberPassword, "client_type": c.Settings.ClientType, @@ -287,16 +285,12 @@ 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} } - req, err := http.NewRequestWithContext( - ctx, - "POST", - endpoint, - strings.NewReader(input.Encode()), - ) + req, err := http.NewRequestWithContext(ctx, "POST", endpoint, + strings.NewReader(input.Encode())) if err != nil { return err } @@ -310,26 +304,17 @@ func (c *SEClient) rpcCall(ctx context.Context, endpoint string, params map[stri } if resp.StatusCode != http.StatusOK { + cleanupBody(resp.Body) return fmt.Errorf("bad http status: %s, headers: %#v", resp.Status, resp.Header) } - decoder := json.NewDecoder(resp.Body) - err = decoder.Decode(res) + err = json.NewDecoder(resp.Body).Decode(res) cleanupBody(resp.Body) - - if err != nil { - return err - } - - return nil + return err } -// 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{ - R: body, - N: READ_LIMIT, - }) + io.Copy(io.Discard, &io.LimitedReader{R: body, N: READ_LIMIT}) body.Close() }