mirror of
https://github.com/Alexey71/opera-proxy.git
synced 2026-05-14 22:50:59 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f4651508c | |||
| e7d9ae4706 | |||
| 7546402687 | |||
| 63b9203be4 | |||
| b99a41e040 |
@@ -83,11 +83,23 @@ You can also download the proxy list from a URL. If the download fails, the app
|
||||
```
|
||||
$ ./opera-proxy -api-proxy-list-url https://example.com/proxies.txt -country EU
|
||||
```
|
||||
|
||||
```
|
||||
$ ./opera-proxy -api-proxy-list-url https://example.com/proxies.txt -api-proxy-file proxies.txt -country EU
|
||||
```
|
||||
|
||||
You can free download proxy servers (default `https://advanced.name/freeproxy`) into a file named `proxies.txt`, use `-fetch-freeproxy-out`. The file name and path can be anything (`D:\myproxy.txt`, `xxxxx.txt`). By default, the `proxies.txt` file is created alongside the `opera-proxy` binary.
|
||||
|
||||
```
|
||||
$ ./opera-proxy -fetch-freeproxy-out proxies.txt
|
||||
```
|
||||
|
||||
You can also run two commands sequentially. The first command will download the proxies and save them to proxies.txt. The second command will launch opera-proxy using the proxies you downloaded.
|
||||
|
||||
```
|
||||
$ ./opera-proxy -fetch-freeproxy-out proxies.txt
|
||||
$ ./opera-proxy -api-proxy-file proxies.txt -country EU
|
||||
```
|
||||
|
||||
If you want selected destinations to go directly instead of through the Opera proxy, use `-proxy-bypass`. It accepts a comma-separated list of host or URL patterns and supports `*` in hostnames:
|
||||
|
||||
```
|
||||
@@ -102,40 +114,41 @@ If SurfEasy discover returns API error `801`, the app also automatically tries `
|
||||
|
||||
| Argument | Type | Description |
|
||||
| -------- | ---- | ----------- |
|
||||
| api-address | String | override IP address of api2.sec-tunnel.com |
|
||||
| api-client-type | String | client type reported to SurfEasy API (default "se0316") |
|
||||
| api-client-version | String | client version reported to SurfEasy API (default "Stable 114.0.5282.21") |
|
||||
| api-login | String | SurfEasy API login (default "se0316") |
|
||||
| api-password | String | SurfEasy API password (default "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II") |
|
||||
| api-proxy | String | additional proxy server used to access SurfEasy API |
|
||||
| api-proxy-file | String | path to text file with candidate proxy servers for SurfEasy API access, one per line; proxies are tried in order until init/discover succeeds |
|
||||
| api-proxy-list-url | String | URL of a text file with candidate proxy servers for SurfEasy API access; falls back to `-api-proxy-file` if download fails |
|
||||
| api-proxy-parallel | Number | number of API proxy candidates tested in parallel when `-api-proxy-file` is used (default 5) |
|
||||
| api-user-agent | String | user agent reported to SurfEasy API (default "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0") |
|
||||
| bind-address | String | proxy listen address (default "127.0.0.1:18080") |
|
||||
| bootstrap-dns | String | Comma-separated list of DNS/DoH/DoT resolvers for initial discovery of SurfEasy API address. Supported schemes are: `dns://`, `https://`, `tls://`, `tcp://`. Examples: `https://1.1.1.1/dns-query`, `tls://9.9.9.9:853` (default `https://1.1.1.3/dns-query,https://8.8.8.8/dns-query,https://dns.google/dns-query,https://security.cloudflare-dns.com/dns-query,https://fidelity.vm-0.com/q,https://wikimedia-dns.org/dns-query,https://dns.adguard-dns.com/dns-query,https://dns.quad9.net/dns-query,https://doh.cleanbrowsing.org/doh/adult-filter/`) |
|
||||
| cafile | String | use custom CA certificate bundle file |
|
||||
| config | String | read configuration from file with space-separated keys and values |
|
||||
| country | String | desired proxy location (default "EU") |
|
||||
| discover-csv | String | read proxy endpoints from CSV instead of SurfEasy discover API |
|
||||
| dp-export | - | export configuration for dumbproxy |
|
||||
| fake-SNI | String | domain name to use as SNI in outbound TLS and in tunneled TLS ClientHello when possible |
|
||||
| init-retries | Number | number of attempts for initialization steps, zero for unlimited retry |
|
||||
| init-retry-interval | Duration | delay between initialization retries (default 5s) |
|
||||
| list-countries | - | list available countries and exit |
|
||||
| list-proxies | - | output proxy list and exit |
|
||||
| override-proxy-address | string | use fixed proxy address instead of server address returned by SurfEasy API |
|
||||
| proxy-bypass | String | comma-separated list of destination host or URL patterns that should bypass proxying and connect directly; matching is case-insensitive and supports `*` in hostnames; if omitted, `proxy-bypass.txt` from the current working directory is loaded automatically when present |
|
||||
| proxy-blacklist | String | path to file with blacklisted proxy addresses, one `host[:port]` per line |
|
||||
| proxy | String | sets base proxy to use for all dial-outs. Format: `<http\|https\|socks5\|socks5h>://[login:password@]host[:port]` Examples: `http://user:password@192.168.1.1:3128`, `socks5://10.0.0.1:1080` |
|
||||
| refresh | Duration | login refresh interval (default 4h0m0s) |
|
||||
| refresh-retry | Duration | login refresh retry interval (default 5s) |
|
||||
| server-selection | Enum | server selection policy (first/random/fastest) (default fastest) |
|
||||
| server-selection-dl-limit | Number | restrict amount of downloaded data per connection by fastest server selection |
|
||||
| server-selection-test-url | String | URL used for download benchmark by fastest server selection policy (default `https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js`) |
|
||||
| server-selection-timeout | Duration | timeout given for server selection function to produce result (default 30s) |
|
||||
| timeout | Duration | timeout for network operations (default 10s) |
|
||||
| verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |
|
||||
| version | - | show program version and exit |
|
||||
| socks-mode | - | listen for SOCKS requests instead of HTTP |
|
||||
| -api-address | String | override IP address of api2.sec-tunnel.com |
|
||||
| -api-client-type | String | client type reported to SurfEasy API (default "se0316") |
|
||||
| -api-client-version | String | client version reported to SurfEasy API (default "Stable 114.0.5282.21") |
|
||||
| -api-login | String | SurfEasy API login (default "se0316") |
|
||||
| -api-password | String | SurfEasy API password (default "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II") |
|
||||
| -api-proxy | String | additional proxy server used to access SurfEasy API |
|
||||
| -api-proxy-file | String | path to text file with candidate proxy servers for SurfEasy API access, one per line; proxies are tried in order until init/discover succeeds |
|
||||
| -api-proxy-list-url | String | URL of a text file with candidate proxy servers for SurfEasy API access; falls back to `-api-proxy-file` if download fails |
|
||||
| -api-proxy-parallel | Number | number of API proxy candidates tested in parallel when `-api-proxy-file` is used (default 5) |
|
||||
| -api-user-agent | String | user agent reported to SurfEasy API (default "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0") |
|
||||
| -bind-address | String | proxy listen address (default "127.0.0.1:18080") |
|
||||
| -bootstrap-dns | String | Comma-separated list of DNS/DoH/DoT resolvers for initial discovery of SurfEasy API address. Supported schemes are: `dns://`, `https://`, `tls://`, `tcp://`. Examples: `https://1.1.1.1/dns-query`, `tls://9.9.9.9:853` (default `https://1.1.1.3/dns-query,https://8.8.8.8/dns-query,https://dns.google/dns-query,https://security.cloudflare-dns.com/dns-query,https://fidelity.vm-0.com/q,https://wikimedia-dns.org/dns-query,https://dns.adguard-dns.com/dns-query,https://dns.quad9.net/dns-query,https://doh.cleanbrowsing.org/doh/adult-filter/`) |
|
||||
| -cafile | String | use custom CA certificate bundle file |
|
||||
| -config | String | read configuration from file with space-separated keys and values |
|
||||
| -country | String | desired proxy location (default "EU") |
|
||||
| -discover-csv | String | read proxy endpoints from CSV instead of SurfEasy discover API |
|
||||
| -dp-export | - | export configuration for dumbproxy |
|
||||
| -fetch-freeproxy-out | - | download proxy list from `https://advanced.name/freeproxy` and save it as a text file with one ip:port per line. Examples: `-fetch-freeproxy-out proxies.txt` or `-fetch-freeproxy-out D:\myproxy.txt` |
|
||||
| -fake-SNI | String | domain name to use as SNI in communications with servers |
|
||||
| -init-retries | Number | number of attempts for initialization steps, zero for unlimited retry |
|
||||
| -init-retry-interval | Duration | delay between initialization retries (default 5s) |
|
||||
| -list-countries | - | list available countries and exit |
|
||||
| -list-proxies | - | output proxy list and exit |
|
||||
| -override-proxy-address | string | use fixed proxy address instead of server address returned by SurfEasy API |
|
||||
| -proxy | String | sets base proxy to use for all dial-outs. Format: `<http\|https\|socks5\|socks5h>://[login:password@]host[:port]` Examples: `http://user:password@192.168.1.1:3128`, `socks5://10.0.0.1:1080` |
|
||||
| -proxy-bypass | String | comma-separated list of destination host or URL patterns that should bypass proxying and connect directly; matching is case-insensitive and supports `*` in hostnames; if omitted, `proxy-bypass.txt` from the current working directory is loaded automatically when present |
|
||||
| -proxy-blacklist | String | path to file with blacklisted proxy addresses, one `host[:port]` per line |
|
||||
| -refresh | Duration | login refresh interval (default 4h0m0s) |
|
||||
| -refresh-retry | Duration | login refresh retry interval (default 5s) |
|
||||
| -server-selection | Enum | server selection policy (first/random/fastest) (default fastest) |
|
||||
| -server-selection-dl-limit | Number | restrict amount of downloaded data per connection by fastest server selection |
|
||||
| -server-selection-test-url | String | URL used for download benchmark by fastest server selection policy (default `https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js`) |
|
||||
| -server-selection-timeout | Duration | timeout given for server selection function to produce result (default 30s) |
|
||||
| -socks-mode | - | listen for SOCKS requests instead of HTTP |
|
||||
| -timeout | Duration | timeout for network operations (default 10s) |
|
||||
| -verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |
|
||||
| -version | - | show program version and exit |
|
||||
|
||||
|
||||
+14
-3
@@ -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{}
|
||||
|
||||
@@ -2,13 +2,13 @@ module github.com/Alexey71/opera-proxy
|
||||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.26.2
|
||||
toolchain go1.26.3
|
||||
|
||||
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-20260423152011-b9e53593a607
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260511143831-44decbfe70e2
|
||||
golang.org/x/net v0.54.0
|
||||
)
|
||||
|
||||
@@ -12,9 +12,9 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/things-go/go-socks5 v0.1.1 h1:48hy9cHEXPKeG91G/g4n8zW4uynzPUQy/FkcrJ7r5AY=
|
||||
github.com/things-go/go-socks5 v0.1.1/go.mod h1:1YBHVYG7Oli5ae+Pwkp630cPAwY1pjUPmohO1n0Emg0=
|
||||
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=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260511143831-44decbfe70e2 h1:7Y5FZkvYs5XMyG0VS/pONmKIgD9+9eqcm1DGar541SA=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260511143831-44decbfe70e2/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+40
-27
@@ -19,19 +19,35 @@ 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
|
||||
httptransport http.RoundTripper
|
||||
fakeSNI string
|
||||
}
|
||||
|
||||
func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger, fakeSNI string) *ProxyHandler {
|
||||
func NewProxyHandler(dialer 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,
|
||||
@@ -40,7 +56,6 @@ func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger, fakeS
|
||||
logger: logger,
|
||||
dialer: dialer,
|
||||
httptransport: httptransport,
|
||||
fakeSNI: fakeSNI,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +70,7 @@ func (s *ProxyHandler) HandleTunnel(wr http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if req.ProtoMajor == 0 || req.ProtoMajor == 1 {
|
||||
// Upgrade client connection
|
||||
localconn, rw, err := hijack(wr)
|
||||
localconn, _, err := hijack(wr)
|
||||
if err != nil {
|
||||
s.logger.Error("Can't hijack client connection: %v", err)
|
||||
http.Error(wr, "Can't hijack client connection", http.StatusInternalServerError)
|
||||
@@ -66,16 +81,12 @@ func (s *ProxyHandler) HandleTunnel(wr http.ResponseWriter, req *http.Request) {
|
||||
// Inform client connection is built
|
||||
fmt.Fprintf(localconn, "HTTP/%d.%d 200 OK\r\n\r\n", req.ProtoMajor, req.ProtoMinor)
|
||||
|
||||
clientReader := io.Reader(localconn)
|
||||
if rw != nil && rw.Reader.Buffered() > 0 {
|
||||
clientReader = io.MultiReader(rw.Reader, localconn)
|
||||
}
|
||||
proxy(req.Context(), localconn, clientReader, conn, s.fakeSNI)
|
||||
proxy(req.Context(), localconn, conn)
|
||||
} else if req.ProtoMajor == 2 {
|
||||
wr.Header()["Date"] = nil
|
||||
wr.WriteHeader(http.StatusOK)
|
||||
flush(wr)
|
||||
proxyh2(req.Context(), req.Body, wr, conn, s.fakeSNI)
|
||||
proxyh2(req.Context(), req.Body, wr, conn)
|
||||
} else {
|
||||
s.logger.Error("Unsupported protocol version: %s", req.Proto)
|
||||
http.Error(wr, "Unsupported protocol version.", http.StatusBadRequest)
|
||||
@@ -121,21 +132,19 @@ func (s *ProxyHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func proxy(ctx context.Context, left net.Conn, leftReader io.Reader, right net.Conn, fakeSNI string) {
|
||||
func proxy(ctx context.Context, left, right net.Conn) {
|
||||
wg := sync.WaitGroup{}
|
||||
ltr := func(dst net.Conn, src io.Reader) {
|
||||
cpy := func(dst, src net.Conn) {
|
||||
defer wg.Done()
|
||||
copyWithSNIRewrite(dst, src, fakeSNI)
|
||||
dst.Close()
|
||||
}
|
||||
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)
|
||||
go ltr(right, leftReader)
|
||||
go rtl(left, right)
|
||||
go cpy(left, right)
|
||||
go cpy(right, left)
|
||||
groupdone := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
@@ -152,11 +161,13 @@ func proxy(ctx context.Context, left net.Conn, leftReader io.Reader, right net.C
|
||||
return
|
||||
}
|
||||
|
||||
func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer, right net.Conn, fakeSNI string) {
|
||||
func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer, right net.Conn) {
|
||||
wg := sync.WaitGroup{}
|
||||
ltr := func(dst net.Conn, src io.Reader) {
|
||||
defer wg.Done()
|
||||
copyWithSNIRewrite(dst, src, fakeSNI)
|
||||
bufp := copyBufPool.Get().(*[]byte)
|
||||
defer copyBufPool.Put(bufp)
|
||||
io.CopyBuffer(dst, src, *bufp)
|
||||
dst.Close()
|
||||
}
|
||||
rtl := func(dst io.Writer, src io.Reader) {
|
||||
@@ -237,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 {
|
||||
|
||||
+4
-4
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/things-go/go-socks5/statute"
|
||||
)
|
||||
|
||||
func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger, fakeSNI string) (*socks5.Server, error) {
|
||||
func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger) (*socks5.Server, error) {
|
||||
opts := []socks5.Option{
|
||||
socks5.WithLogger(socks5.NewLogger(logger)),
|
||||
socks5.WithRule(
|
||||
@@ -23,13 +23,13 @@ func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger, fakeSNI str
|
||||
),
|
||||
socks5.WithResolver(DummySocksResolver{}),
|
||||
socks5.WithConnectHandle(func(ctx context.Context, writer io.Writer, request *socks5.Request) error {
|
||||
return handleSocksConnect(ctx, writer, request, dialer, fakeSNI)
|
||||
return handleSocksConnect(ctx, writer, request, dialer)
|
||||
}),
|
||||
}
|
||||
return socks5.NewServer(opts...), nil
|
||||
}
|
||||
|
||||
func handleSocksConnect(ctx context.Context, writer io.Writer, request *socks5.Request, upstream dialer.ContextDialer, fakeSNI string) error {
|
||||
func handleSocksConnect(ctx context.Context, writer io.Writer, request *socks5.Request, upstream dialer.ContextDialer) error {
|
||||
target, err := upstream.DialContext(ctx, "tcp", request.DestAddr.String())
|
||||
if err != nil {
|
||||
reply := statute.RepHostUnreachable
|
||||
@@ -56,7 +56,7 @@ func handleSocksConnect(ctx context.Context, writer io.Writer, request *socks5.R
|
||||
return fmt.Errorf("writer is %T, expected net.Conn", writer)
|
||||
}
|
||||
|
||||
proxy(ctx, clientConn, request.Reader, target, fakeSNI)
|
||||
proxy(ctx, clientConn, target)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
tlsRecordHeaderLen = 5
|
||||
tlsHandshakeHeaderLen = 4
|
||||
tlsRecordTypeHandshake = 0x16
|
||||
tlsHandshakeTypeClientHello = 0x01
|
||||
tlsExtensionServerName = 0x0000
|
||||
)
|
||||
|
||||
func copyWithSNIRewrite(dst io.Writer, src io.Reader, fakeSNI string) error {
|
||||
fakeSNI = strings.TrimSpace(fakeSNI)
|
||||
if fakeSNI == "" {
|
||||
_, err := io.Copy(dst, src)
|
||||
return err
|
||||
}
|
||||
|
||||
br, ok := src.(*bufio.Reader)
|
||||
if !ok {
|
||||
br = bufio.NewReader(src)
|
||||
}
|
||||
|
||||
header, err := br.Peek(tlsRecordHeaderLen)
|
||||
if err != nil {
|
||||
_, copyErr := io.Copy(dst, br)
|
||||
if copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !looksLikeTLSClientHelloRecord(header) {
|
||||
_, err = io.Copy(dst, br)
|
||||
return err
|
||||
}
|
||||
|
||||
recordLen := int(binary.BigEndian.Uint16(header[3:5]))
|
||||
record := make([]byte, tlsRecordHeaderLen+recordLen)
|
||||
n, err := io.ReadFull(br, record)
|
||||
if err != nil {
|
||||
if n > 0 {
|
||||
if _, writeErr := dst.Write(record[:n]); writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
_, copyErr := io.Copy(dst, br)
|
||||
return copyErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if rewritten, ok := rewriteTLSClientHelloRecordServerName(record, fakeSNI); ok {
|
||||
record = rewritten
|
||||
}
|
||||
|
||||
if _, err := dst.Write(record); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(dst, br)
|
||||
return err
|
||||
}
|
||||
|
||||
func looksLikeTLSClientHelloRecord(header []byte) bool {
|
||||
return len(header) >= tlsRecordHeaderLen &&
|
||||
header[0] == tlsRecordTypeHandshake &&
|
||||
header[1] == 0x03 &&
|
||||
header[2] <= 0x04
|
||||
}
|
||||
|
||||
func rewriteTLSClientHelloRecordServerName(record []byte, fakeSNI string) ([]byte, bool) {
|
||||
if len(record) < tlsRecordHeaderLen+tlsHandshakeHeaderLen {
|
||||
return nil, false
|
||||
}
|
||||
if !looksLikeTLSClientHelloRecord(record[:tlsRecordHeaderLen]) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
payload := record[tlsRecordHeaderLen:]
|
||||
if len(payload) < tlsHandshakeHeaderLen || payload[0] != tlsHandshakeTypeClientHello {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
handshakeLen := readUint24(payload[1:4])
|
||||
if handshakeLen > len(payload)-tlsHandshakeHeaderLen {
|
||||
// ClientHello is fragmented across multiple TLS records.
|
||||
return nil, false
|
||||
}
|
||||
|
||||
hello := payload[tlsHandshakeHeaderLen : tlsHandshakeHeaderLen+handshakeLen]
|
||||
offset := 0
|
||||
if !skipLen(hello, &offset, 2+32) {
|
||||
return nil, false
|
||||
}
|
||||
if !skipOpaque8(hello, &offset) {
|
||||
return nil, false
|
||||
}
|
||||
if !skipOpaque16(hello, &offset) {
|
||||
return nil, false
|
||||
}
|
||||
if !skipOpaque8(hello, &offset) {
|
||||
return nil, false
|
||||
}
|
||||
if offset == len(hello) {
|
||||
return nil, false
|
||||
}
|
||||
if offset+2 > len(hello) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
extensionsLenOffset := offset
|
||||
extensionsLen := int(binary.BigEndian.Uint16(hello[offset : offset+2]))
|
||||
offset += 2
|
||||
if offset+extensionsLen > len(hello) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
extensionsEnd := offset + extensionsLen
|
||||
for offset+4 <= extensionsEnd {
|
||||
extStart := offset
|
||||
extType := binary.BigEndian.Uint16(hello[offset : offset+2])
|
||||
extLen := int(binary.BigEndian.Uint16(hello[offset+2 : offset+4]))
|
||||
offset += 4
|
||||
if offset+extLen > extensionsEnd {
|
||||
return nil, false
|
||||
}
|
||||
if extType != tlsExtensionServerName {
|
||||
offset += extLen
|
||||
continue
|
||||
}
|
||||
|
||||
extDataStart := offset
|
||||
extDataEnd := offset + extLen
|
||||
extData := hello[extDataStart:extDataEnd]
|
||||
if len(extData) < 5 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
serverNameListLen := int(binary.BigEndian.Uint16(extData[:2]))
|
||||
if 2+serverNameListLen > len(extData) {
|
||||
return nil, false
|
||||
}
|
||||
if extData[2] != 0x00 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
nameLen := int(binary.BigEndian.Uint16(extData[3:5]))
|
||||
if 5+nameLen > len(extData) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
tail := extData[5+nameLen:]
|
||||
newExtData := make([]byte, 2+1+2+len(fakeSNI)+len(tail))
|
||||
binary.BigEndian.PutUint16(newExtData[:2], uint16(1+2+len(fakeSNI)+len(tail)))
|
||||
newExtData[2] = 0x00
|
||||
binary.BigEndian.PutUint16(newExtData[3:5], uint16(len(fakeSNI)))
|
||||
copy(newExtData[5:], fakeSNI)
|
||||
copy(newExtData[5+len(fakeSNI):], tail)
|
||||
|
||||
helloStart := tlsRecordHeaderLen + tlsHandshakeHeaderLen
|
||||
extLenFieldStart := helloStart + extStart + 2
|
||||
extDataAbsStart := helloStart + extDataStart
|
||||
extDataAbsEnd := helloStart + extDataEnd
|
||||
extensionsLenFieldStart := helloStart + extensionsLenOffset
|
||||
|
||||
delta := len(newExtData) - len(extData)
|
||||
newRecord := make([]byte, 0, len(record)+delta)
|
||||
newRecord = append(newRecord, record[:extDataAbsStart]...)
|
||||
newRecord = append(newRecord, newExtData...)
|
||||
newRecord = append(newRecord, record[extDataAbsEnd:]...)
|
||||
|
||||
binary.BigEndian.PutUint16(newRecord[3:5], uint16(len(payload)+delta))
|
||||
writeUint24(newRecord[6:9], handshakeLen+delta)
|
||||
binary.BigEndian.PutUint16(newRecord[extensionsLenFieldStart:extensionsLenFieldStart+2], uint16(extensionsLen+delta))
|
||||
binary.BigEndian.PutUint16(newRecord[extLenFieldStart:extLenFieldStart+2], uint16(extLen+delta))
|
||||
|
||||
return newRecord, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func readUint24(b []byte) int {
|
||||
return int(b[0])<<16 | int(b[1])<<8 | int(b[2])
|
||||
}
|
||||
|
||||
func writeUint24(dst []byte, v int) {
|
||||
dst[0] = byte(v >> 16)
|
||||
dst[1] = byte(v >> 8)
|
||||
dst[2] = byte(v)
|
||||
}
|
||||
|
||||
func skipLen(b []byte, offset *int, n int) bool {
|
||||
if *offset+n > len(b) {
|
||||
return false
|
||||
}
|
||||
*offset += n
|
||||
return true
|
||||
}
|
||||
|
||||
func skipOpaque8(b []byte, offset *int) bool {
|
||||
if *offset >= len(b) {
|
||||
return false
|
||||
}
|
||||
l := int(b[*offset])
|
||||
*offset++
|
||||
return skipLen(b, offset, l)
|
||||
}
|
||||
|
||||
func skipOpaque16(b []byte, offset *int) bool {
|
||||
if *offset+2 > len(b) {
|
||||
return false
|
||||
}
|
||||
l := int(binary.BigEndian.Uint16(b[*offset : *offset+2]))
|
||||
*offset += 2
|
||||
return skipLen(b, offset, l)
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRewriteTLSClientHelloRecordServerName(t *testing.T) {
|
||||
record := buildClientHelloRecord("example.com")
|
||||
|
||||
rewritten, ok := rewriteTLSClientHelloRecordServerName(record, "fake.example")
|
||||
if !ok {
|
||||
t.Fatal("expected ClientHello record to be rewritten")
|
||||
}
|
||||
|
||||
if got := extractServerName(t, rewritten); got != "fake.example" {
|
||||
t.Fatalf("unexpected SNI after rewrite: got %q", got)
|
||||
}
|
||||
if got := int(binary.BigEndian.Uint16(rewritten[3:5])); got != len(rewritten)-tlsRecordHeaderLen {
|
||||
t.Fatalf("unexpected TLS record length: got %d want %d", got, len(rewritten)-tlsRecordHeaderLen)
|
||||
}
|
||||
if got := readUint24(rewritten[6:9]); got != len(rewritten)-tlsRecordHeaderLen-tlsHandshakeHeaderLen {
|
||||
t.Fatalf("unexpected handshake length: got %d want %d", got, len(rewritten)-tlsRecordHeaderLen-tlsHandshakeHeaderLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyWithSNIRewritePreservesTrailingBytes(t *testing.T) {
|
||||
record := buildClientHelloRecord("example.com")
|
||||
stream := append(append([]byte{}, record...), []byte("tail")...)
|
||||
|
||||
var dst bytes.Buffer
|
||||
if err := copyWithSNIRewrite(&dst, bytes.NewReader(stream), "fake.example"); err != nil {
|
||||
t.Fatalf("copyWithSNIRewrite returned error: %v", err)
|
||||
}
|
||||
|
||||
out := dst.Bytes()
|
||||
recordLen := int(binary.BigEndian.Uint16(out[3:5])) + tlsRecordHeaderLen
|
||||
if got := extractServerName(t, out[:recordLen]); got != "fake.example" {
|
||||
t.Fatalf("unexpected SNI in output stream: got %q", got)
|
||||
}
|
||||
if tail := string(out[recordLen:]); tail != "tail" {
|
||||
t.Fatalf("unexpected trailing bytes: got %q", tail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteTLSClientHelloRecordServerNameSkipsFragmentedHello(t *testing.T) {
|
||||
record := buildClientHelloRecord("example.com")
|
||||
record[6] = 0x7f
|
||||
record[7] = 0xff
|
||||
record[8] = 0xff
|
||||
|
||||
if _, ok := rewriteTLSClientHelloRecordServerName(record, "fake.example"); ok {
|
||||
t.Fatal("expected fragmented ClientHello rewrite to be skipped")
|
||||
}
|
||||
}
|
||||
|
||||
func buildClientHelloRecord(serverName string) []byte {
|
||||
sniExtData := make([]byte, 2+1+2+len(serverName))
|
||||
binary.BigEndian.PutUint16(sniExtData[:2], uint16(1+2+len(serverName)))
|
||||
sniExtData[2] = 0x00
|
||||
binary.BigEndian.PutUint16(sniExtData[3:5], uint16(len(serverName)))
|
||||
copy(sniExtData[5:], serverName)
|
||||
|
||||
sniExt := makeExtension(tlsExtensionServerName, sniExtData)
|
||||
otherExt := makeExtension(0x002b, []byte{0x02, 0x03, 0x04})
|
||||
extensions := append(sniExt, otherExt...)
|
||||
|
||||
hello := make([]byte, 0, 128)
|
||||
hello = append(hello, 0x03, 0x03)
|
||||
hello = append(hello, bytes.Repeat([]byte{0x11}, 32)...)
|
||||
hello = append(hello, 0x00)
|
||||
hello = append(hello, 0x00, 0x02, 0x13, 0x01)
|
||||
hello = append(hello, 0x01, 0x00)
|
||||
hello = append(hello, byte(len(extensions)>>8), byte(len(extensions)))
|
||||
hello = append(hello, extensions...)
|
||||
|
||||
record := make([]byte, 0, tlsRecordHeaderLen+tlsHandshakeHeaderLen+len(hello))
|
||||
record = append(record, tlsRecordTypeHandshake, 0x03, 0x03)
|
||||
recordLen := tlsHandshakeHeaderLen + len(hello)
|
||||
record = append(record, byte(recordLen>>8), byte(recordLen))
|
||||
record = append(record, tlsHandshakeTypeClientHello)
|
||||
record = append(record, byte(len(hello)>>16), byte(len(hello)>>8), byte(len(hello)))
|
||||
record = append(record, hello...)
|
||||
return record
|
||||
}
|
||||
|
||||
func makeExtension(extType uint16, data []byte) []byte {
|
||||
ext := make([]byte, 4+len(data))
|
||||
binary.BigEndian.PutUint16(ext[:2], extType)
|
||||
binary.BigEndian.PutUint16(ext[2:4], uint16(len(data)))
|
||||
copy(ext[4:], data)
|
||||
return ext
|
||||
}
|
||||
|
||||
func extractServerName(t *testing.T, record []byte) string {
|
||||
t.Helper()
|
||||
|
||||
payload := record[tlsRecordHeaderLen:]
|
||||
handshakeLen := readUint24(payload[1:4])
|
||||
hello := payload[tlsHandshakeHeaderLen : tlsHandshakeHeaderLen+handshakeLen]
|
||||
|
||||
offset := 2 + 32
|
||||
sessionIDLen := int(hello[offset])
|
||||
offset++
|
||||
offset += sessionIDLen
|
||||
|
||||
cipherSuitesLen := int(binary.BigEndian.Uint16(hello[offset : offset+2]))
|
||||
offset += 2 + cipherSuitesLen
|
||||
|
||||
compressionMethodsLen := int(hello[offset])
|
||||
offset++
|
||||
offset += compressionMethodsLen
|
||||
|
||||
extensionsLen := int(binary.BigEndian.Uint16(hello[offset : offset+2]))
|
||||
offset += 2
|
||||
extensionsEnd := offset + extensionsLen
|
||||
for offset+4 <= extensionsEnd {
|
||||
extType := binary.BigEndian.Uint16(hello[offset : offset+2])
|
||||
extLen := int(binary.BigEndian.Uint16(hello[offset+2 : offset+4]))
|
||||
offset += 4
|
||||
if extType != tlsExtensionServerName {
|
||||
offset += extLen
|
||||
continue
|
||||
}
|
||||
extData := hello[offset : offset+extLen]
|
||||
nameLen := int(binary.BigEndian.Uint16(extData[3:5]))
|
||||
return string(extData[5 : 5+nameLen])
|
||||
}
|
||||
|
||||
t.Fatal("server_name extension not found")
|
||||
return ""
|
||||
}
|
||||
@@ -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: <http|https|socks5|socks5h>://[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
|
||||
}
|
||||
@@ -1033,15 +1070,15 @@ func run() int {
|
||||
|
||||
mainLogger.Info("Starting proxy server...")
|
||||
if args.socksMode {
|
||||
socks, initError := handler.NewSocksServer(handlerDialer, socksLogger, args.fakeSNI)
|
||||
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.")
|
||||
err = socks.ListenAndServe("tcp", args.bindAddress)
|
||||
} else {
|
||||
h := handler.NewProxyHandler(handlerDialer, proxyLogger, args.fakeSNI)
|
||||
h := handler.NewProxyHandler(handlerDialer, proxyLogger)
|
||||
mainLogger.Info("Init complete.")
|
||||
err = http.ListenAndServe(args.bindAddress, h)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+26
-41
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user