chore: some changes

This commit is contained in:
xteamlyer
2026-04-24 13:20:19 +03:00
committed by Alexey71
parent 8952eae4e2
commit 1f29e12cf0
10 changed files with 175 additions and 68 deletions
+3 -3
View File
@@ -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
+9 -6
View File
@@ -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
+14 -3
View File
@@ -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{}
+3 -3
View File
@@ -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
)
+2
View File
@@ -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=
+36 -12
View File
@@ -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 {
+1 -1
View File
@@ -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"
)
+76 -32
View File
@@ -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: <http|https|socks5|socks5h>://[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
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}
+25 -8
View File
@@ -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,
})