5 Commits

Author SHA1 Message Date
Alexey71 2f4651508c Revert changes fakeSNI 2026-05-14 11:14:52 +03:00
Alexey71 e7d9ae4706 Update go 2026-05-14 11:14:32 +03:00
Alexey71 7546402687 Update readme/x509roots 2026-05-04 10:26:58 +03:00
xteamlyer 63b9203be4 Some change 2026-04-30 19:04:28 +03:00
Alexey71 b99a41e040 Update readme 2026-04-26 20:02:03 +03:00
10 changed files with 234 additions and 541 deletions
+50 -37
View File
@@ -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 -country EU
``` ```
``` ```
$ ./opera-proxy -api-proxy-list-url https://example.com/proxies.txt -api-proxy-file 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: 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 | | Argument | Type | Description |
| -------- | ---- | ----------- | | -------- | ---- | ----------- |
| api-address | String | override IP address of api2.sec-tunnel.com | | -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-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-client-version | String | client version reported to SurfEasy API (default "Stable 114.0.5282.21") |
| api-login | String | SurfEasy API login (default "se0316") | | -api-login | String | SurfEasy API login (default "se0316") |
| api-password | String | SurfEasy API password (default "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II") | | -api-password | String | SurfEasy API password (default "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II") |
| api-proxy | String | additional proxy server used to access SurfEasy API | | -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-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-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-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") | | -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") | | -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/`) | | -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 | | -cafile | String | use custom CA certificate bundle file |
| config | String | read configuration from file with space-separated keys and values | | -config | String | read configuration from file with space-separated keys and values |
| country | String | desired proxy location (default "EU") | | -country | String | desired proxy location (default "EU") |
| discover-csv | String | read proxy endpoints from CSV instead of SurfEasy discover API | | -discover-csv | String | read proxy endpoints from CSV instead of SurfEasy discover API |
| dp-export | - | export configuration for dumbproxy | | -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 | | -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` |
| init-retries | Number | number of attempts for initialization steps, zero for unlimited retry | | -fake-SNI | String | domain name to use as SNI in communications with servers |
| init-retry-interval | Duration | delay between initialization retries (default 5s) | | -init-retries | Number | number of attempts for initialization steps, zero for unlimited retry |
| list-countries | - | list available countries and exit | | -init-retry-interval | Duration | delay between initialization retries (default 5s) |
| list-proxies | - | output proxy list and exit | | -list-countries | - | list available countries and exit |
| override-proxy-address | string | use fixed proxy address instead of server address returned by SurfEasy API | | -list-proxies | - | output proxy list and exit |
| 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 | | -override-proxy-address | string | use fixed proxy address instead of server address returned by SurfEasy API |
| 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` |
| 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 |
| refresh | Duration | login refresh interval (default 4h0m0s) | | -proxy-blacklist | String | path to file with blacklisted proxy addresses, one `host[:port]` per line |
| refresh-retry | Duration | login refresh retry interval (default 5s) | | -refresh | Duration | login refresh interval (default 4h0m0s) |
| server-selection | Enum | server selection policy (first/random/fastest) (default fastest) | | -refresh-retry | Duration | login refresh retry interval (default 5s) |
| server-selection-dl-limit | Number | restrict amount of downloaded data per connection by fastest server selection | | -server-selection | Enum | server selection policy (first/random/fastest) (default fastest) |
| 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-dl-limit | Number | restrict amount of downloaded data per connection by fastest server selection |
| server-selection-timeout | Duration | timeout given for server selection function to produce result (default 30s) | | -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`) |
| timeout | Duration | timeout for network operations (default 10s) | | -server-selection-timeout | Duration | timeout given for server selection function to produce result (default 30s) |
| verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) | | -socks-mode | - | listen for SOCKS requests instead of HTTP |
| version | - | show program version and exit | | -timeout | Duration | timeout for network operations (default 10s) |
| socks-mode | - | listen for SOCKS requests instead of HTTP | | -verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |
| -version | - | show program version and exit |
+14 -3
View File
@@ -115,9 +115,14 @@ func (d *ProxyDialer) DialContext(ctx context.Context, network, address string)
return nil, err return nil, err
} }
if uTLSServerName != "" { if uTLSServerName != "" {
// Custom cert verification logic: // Custom TLS verification strategy:
// DO NOT send SNI extension of TLS ClientHello // - Do NOT send SNI in ClientHello (use fakeSNI, may be empty string).
// DO peer certificate verification against specified servername // - 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{ conn = tls.Client(conn, &tls.Config{
ServerName: fakeSNI, ServerName: fakeSNI,
InsecureSkipVerify: true, InsecureSkipVerify: true,
@@ -186,6 +191,12 @@ func (d *ProxyDialer) Address() (string, error) {
return d.address() 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) { func readResponse(r io.Reader, req *http.Request) (*http.Response, error) {
endOfResponse := []byte("\r\n\r\n") endOfResponse := []byte("\r\n\r\n")
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
+3 -3
View File
@@ -2,13 +2,13 @@ module github.com/Alexey71/opera-proxy
go 1.25.0 go 1.25.0
toolchain go1.26.2 toolchain go1.26.3
require ( require (
github.com/Alexey71/go-http-digest-auth-client v1.1.3 github.com/Alexey71/go-http-digest-auth-client v1.1.3
github.com/Alexey71/go-multierror v1.1.3 github.com/Alexey71/go-multierror v1.1.3
github.com/ncruces/go-dns v1.3.3 github.com/ncruces/go-dns v1.3.3
github.com/things-go/go-socks5 v0.1.1 github.com/things-go/go-socks5 v0.1.1
golang.org/x/crypto/x509roots/fallback v0.0.0-20260423152011-b9e53593a607 golang.org/x/crypto/x509roots/fallback v0.0.0-20260511143831-44decbfe70e2
golang.org/x/net v0.53.0 golang.org/x/net v0.54.0
) )
+4 -4
View File
@@ -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/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 h1:48hy9cHEXPKeG91G/g4n8zW4uynzPUQy/FkcrJ7r5AY=
github.com/things-go/go-socks5 v0.1.1/go.mod h1:1YBHVYG7Oli5ae+Pwkp630cPAwY1pjUPmohO1n0Emg0= 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-20260511143831-44decbfe70e2 h1:7Y5FZkvYs5XMyG0VS/pONmKIgD9+9eqcm1DGar541SA=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260423152011-b9e53593a607/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/crypto/x509roots/fallback v0.0.0-20260511143831-44decbfe70e2/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+40 -27
View File
@@ -19,19 +19,35 @@ import (
const ( const (
COPY_BUF = 128 * 1024 COPY_BUF = 128 * 1024
BAD_REQ_MSG = "Bad Request\n" 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 { type ProxyHandler struct {
logger *clog.CondLogger logger *clog.CondLogger
dialer dialer.ContextDialer dialer dialer.ContextDialer
httptransport http.RoundTripper 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{ httptransport := &http.Transport{
MaxIdleConns: 100, MaxIdleConns: TRANSPORT_MAX_IDLE_CONNS,
IdleConnTimeout: 90 * time.Second, MaxIdleConnsPerHost: TRANSPORT_MAX_IDLE_CONNS_PER_HOST,
IdleConnTimeout: TRANSPORT_IDLE_CONN_TIMEOUT,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
DialContext: dialer.DialContext, DialContext: dialer.DialContext,
@@ -40,7 +56,6 @@ func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger, fakeS
logger: logger, logger: logger,
dialer: dialer, dialer: dialer,
httptransport: httptransport, 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 { if req.ProtoMajor == 0 || req.ProtoMajor == 1 {
// Upgrade client connection // Upgrade client connection
localconn, rw, err := hijack(wr) localconn, _, err := hijack(wr)
if err != nil { if err != nil {
s.logger.Error("Can't hijack client connection: %v", err) s.logger.Error("Can't hijack client connection: %v", err)
http.Error(wr, "Can't hijack client connection", http.StatusInternalServerError) 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 // Inform client connection is built
fmt.Fprintf(localconn, "HTTP/%d.%d 200 OK\r\n\r\n", req.ProtoMajor, req.ProtoMinor) fmt.Fprintf(localconn, "HTTP/%d.%d 200 OK\r\n\r\n", req.ProtoMajor, req.ProtoMinor)
clientReader := io.Reader(localconn) proxy(req.Context(), localconn, conn)
if rw != nil && rw.Reader.Buffered() > 0 {
clientReader = io.MultiReader(rw.Reader, localconn)
}
proxy(req.Context(), localconn, clientReader, conn, s.fakeSNI)
} else if req.ProtoMajor == 2 { } else if req.ProtoMajor == 2 {
wr.Header()["Date"] = nil wr.Header()["Date"] = nil
wr.WriteHeader(http.StatusOK) wr.WriteHeader(http.StatusOK)
flush(wr) flush(wr)
proxyh2(req.Context(), req.Body, wr, conn, s.fakeSNI) proxyh2(req.Context(), req.Body, wr, conn)
} else { } else {
s.logger.Error("Unsupported protocol version: %s", req.Proto) s.logger.Error("Unsupported protocol version: %s", req.Proto)
http.Error(wr, "Unsupported protocol version.", http.StatusBadRequest) 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{} wg := sync.WaitGroup{}
ltr := func(dst net.Conn, src io.Reader) { cpy := func(dst, src net.Conn) {
defer wg.Done() defer wg.Done()
copyWithSNIRewrite(dst, src, fakeSNI) // Grab a pooled buffer for this copy direction.
dst.Close() bufp := copyBufPool.Get().(*[]byte)
} defer copyBufPool.Put(bufp)
rtl := func(dst, src net.Conn) { io.CopyBuffer(dst, src, *bufp)
defer wg.Done()
io.Copy(dst, src)
dst.Close() dst.Close()
} }
wg.Add(2) wg.Add(2)
go ltr(right, leftReader) go cpy(left, right)
go rtl(left, right) go cpy(right, left)
groupdone := make(chan struct{}) groupdone := make(chan struct{})
go func() { go func() {
wg.Wait() wg.Wait()
@@ -152,11 +161,13 @@ func proxy(ctx context.Context, left net.Conn, leftReader io.Reader, right net.C
return 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{} wg := sync.WaitGroup{}
ltr := func(dst net.Conn, src io.Reader) { ltr := func(dst net.Conn, src io.Reader) {
defer wg.Done() defer wg.Done()
copyWithSNIRewrite(dst, src, fakeSNI) bufp := copyBufPool.Get().(*[]byte)
defer copyBufPool.Put(bufp)
io.CopyBuffer(dst, src, *bufp)
dst.Close() dst.Close()
} }
rtl := func(dst io.Writer, src io.Reader) { 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) { 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 { for {
bread, read_err := body.Read(buf) bread, read_err := body.Read(*bufp)
var write_err error var write_err error
if bread > 0 { if bread > 0 {
_, write_err = wr.Write(buf[:bread]) _, write_err = wr.Write((*bufp)[:bread])
flush(wr) flush(wr)
} }
if read_err != nil || write_err != nil { if read_err != nil || write_err != nil {
+4 -4
View File
@@ -13,7 +13,7 @@ import (
"github.com/things-go/go-socks5/statute" "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{ opts := []socks5.Option{
socks5.WithLogger(socks5.NewLogger(logger)), socks5.WithLogger(socks5.NewLogger(logger)),
socks5.WithRule( socks5.WithRule(
@@ -23,13 +23,13 @@ func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger, fakeSNI str
), ),
socks5.WithResolver(DummySocksResolver{}), socks5.WithResolver(DummySocksResolver{}),
socks5.WithConnectHandle(func(ctx context.Context, writer io.Writer, request *socks5.Request) error { 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 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()) target, err := upstream.DialContext(ctx, "tcp", request.DestAddr.String())
if err != nil { if err != nil {
reply := statute.RepHostUnreachable 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) return fmt.Errorf("writer is %T, expected net.Conn", writer)
} }
proxy(ctx, clientConn, request.Reader, target, fakeSNI) proxy(ctx, clientConn, target)
return nil return nil
} }
-227
View File
@@ -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)
}
-133
View File
@@ -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 ""
}
+93 -62
View File
@@ -11,7 +11,6 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -42,6 +41,15 @@ const (
PROXY_SUFFIX = "sec-tunnel.com" PROXY_SUFFIX = "sec-tunnel.com"
DefaultDiscoverCSVFallback = "proxies.csv" DefaultDiscoverCSVFallback = "proxies.csv"
DefaultProxyBypassFallback = "proxy-bypass.txt" 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) { 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.BoolVar(&args.socksMode, "socks-mode", false, "listen for SOCKS requests instead of HTTP")
flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity "+ flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity "+
"(10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical)") "(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.BoolVar(&args.showVersion, "version", false, "show program version and exit")
flag.StringVar(&args.proxy, "proxy", "", "sets base proxy to use for all dial-outs. "+ flag.StringVar(&args.proxy, "proxy", "", "sets base proxy to use for all dial-outs. "+
"Format: <http|https|socks5|socks5h>://[login:password@]host[:port] "+ "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.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.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.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.DurationVar(&args.serverSelectionTimeout, "server-selection-timeout", DEFAULT_SERVER_SELECTION_TIMEOUT,
flag.StringVar(&args.serverSelectionTestURL, "server-selection-test-url", "https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js", "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") "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.Func("config", "read configuration from file with space-separated keys and values", readConfig)
flag.Parse() flag.Parse()
flag.Visit(func(f *flag.Flag) { 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) 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) { func normalizeAPIProxy(raw string) (string, error) {
raw = strings.TrimSpace(raw) raw = strings.TrimSpace(raw)
if raw == "" { if raw == "" {
@@ -441,27 +505,25 @@ func newSEClient(args *CLIArgs, baseDialer dialer.ContextDialer, caPool *x509.Ce
seclientDialer = dialer.NewResolvingDialer(resolver, seclientDialer) seclientDialer = dialer.NewResolvingDialer(resolver, seclientDialer)
} }
// Dialing w/o SNI, receiving self-signed certificate, so skip verification. // TLS config for the API connection: SNI suppressed (or faked), cert
// Either way we'll validate certificate of actual proxy server. // 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{ tlsConfig := &tls.Config{
ServerName: args.fakeSNI, ServerName: args.fakeSNI,
InsecureSkipVerify: true, InsecureSkipVerify: true,
} }
seclient, err := se.NewSEClient(args.apiLogin, args.apiPassword, &http.Transport{
DialContext: seclientDialer.DialContext, seclient, err := se.NewSEClient(args.apiLogin, args.apiPassword, buildAPITransport(
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { seclientDialer.DialContext,
func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := seclientDialer.DialContext(ctx, network, addr) conn, err := seclientDialer.DialContext(ctx, network, addr)
if err != nil { if err != nil {
return conn, err return conn, err
} }
return tls.Client(conn, tlsConfig), nil 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 { if err != nil {
return nil, fmt.Errorf("unable to construct SEClient: %w", err) return nil, fmt.Errorf("unable to construct SEClient: %w", err)
} }
@@ -659,30 +721,9 @@ func run() int {
KeepAlive: 30 * time.Second, KeepAlive: 30 * time.Second,
} }
caPool := x509.NewCertPool() caPool, exitCode := buildCAPool(args.caFile, mainLogger)
if args.caFile != "" { if exitCode != 0 {
certs, err := ioutil.ReadFile(args.caFile) return exitCode
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)
}
}
} }
xproxy.RegisterDialerType("http", proxyFromURLWrapper) xproxy.RegisterDialerType("http", proxyFromURLWrapper)
@@ -793,12 +834,12 @@ func run() int {
if args.apiProxy != "" { if args.apiProxy != "" {
apiProxyURL, err := url.Parse(args.apiProxy) apiProxyURL, err := url.Parse(args.apiProxy)
if err != nil { 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 return 6
} }
pxDialer, err := xproxy.FromURL(apiProxyURL, seclientDialer) pxDialer, err := xproxy.FromURL(apiProxyURL, seclientDialer)
if err != nil { 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 return 7
} }
seclientDialer = pxDialer.(dialer.ContextDialer) seclientDialer = pxDialer.(dialer.ContextDialer)
@@ -806,12 +847,12 @@ func run() int {
if args.apiAddress != "" { if args.apiAddress != "" {
seclientDialer = dialer.NewFixedDialer(args.apiAddress, seclientDialer) seclientDialer = dialer.NewFixedDialer(args.apiAddress, seclientDialer)
} else if len(args.bootstrapDNS.values) > 0 { } 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 { if err != nil {
mainLogger.Critical("Unable to instantiate DNS resolver: %v", err) mainLogger.Critical("Unable to instantiate DNS resolver: %v", err)
return 4 return 4
} }
seclientDialer = dialer.NewResolvingDialer(resolver, seclientDialer) seclientDialer = dialer.NewResolvingDialer(res, seclientDialer)
} }
// Dialing w/o SNI, receiving self-signed certificate, so skip verification. // Dialing w/o SNI, receiving self-signed certificate, so skip verification.
@@ -959,9 +1000,7 @@ func run() int {
ss = dialer.NewFastestServerSelectionFunc( ss = dialer.NewFastestServerSelectionFunc(
args.serverSelectionTestURL, args.serverSelectionTestURL,
args.serverSelectionDLLimit, args.serverSelectionDLLimit,
&tls.Config{ &tls.Config{RootCAs: caPool},
RootCAs: caPool,
},
) )
default: default:
panic("unhandled server selection value got past parsing") panic("unhandled server selection value got past parsing")
@@ -1012,8 +1051,7 @@ func run() int {
mainLogger.Info("Refreshing login...") mainLogger.Info("Refreshing login...")
reqCtx, cl := context.WithTimeout(ctx, args.timeout) reqCtx, cl := context.WithTimeout(ctx, args.timeout)
defer cl() defer cl()
err := seclient.Login(reqCtx) if err := seclient.Login(reqCtx); err != nil {
if err != nil {
mainLogger.Error("Login refresh failed: %v", err) mainLogger.Error("Login refresh failed: %v", err)
return err return err
} }
@@ -1022,8 +1060,7 @@ func run() int {
mainLogger.Info("Refreshing device password...") mainLogger.Info("Refreshing device password...")
reqCtx, cl = context.WithTimeout(ctx, args.timeout) reqCtx, cl = context.WithTimeout(ctx, args.timeout)
defer cl() defer cl()
err = seclient.DeviceGeneratePassword(reqCtx) if err := seclient.DeviceGeneratePassword(reqCtx); err != nil {
if err != nil {
mainLogger.Error("Device password refresh failed: %v", err) mainLogger.Error("Device password refresh failed: %v", err)
return err return err
} }
@@ -1033,15 +1070,15 @@ func run() int {
mainLogger.Info("Starting proxy server...") mainLogger.Info("Starting proxy server...")
if args.socksMode { if args.socksMode {
socks, initError := handler.NewSocksServer(handlerDialer, socksLogger, args.fakeSNI) socks, initError := handler.NewSocksServer(handlerDialer, socksLogger)
if initError != nil { if initError != nil {
mainLogger.Critical("Failed to start: %v", err) mainLogger.Critical("Failed to start: %v", initError)
return 16 return 16
} }
mainLogger.Info("Init complete.") mainLogger.Info("Init complete.")
err = socks.ListenAndServe("tcp", args.bindAddress) err = socks.ListenAndServe("tcp", args.bindAddress)
} else { } else {
h := handler.NewProxyHandler(handlerDialer, proxyLogger, args.fakeSNI) h := handler.NewProxyHandler(handlerDialer, proxyLogger)
mainLogger.Info("Init complete.") mainLogger.Info("Init complete.")
err = http.ListenAndServe(args.bindAddress, h) err = http.ListenAndServe(args.bindAddress, h)
} }
@@ -1155,10 +1192,7 @@ func dpExport(ips []se.SEIPEntry, seclient *se.SEClient, sni string) int {
u := url.URL{ u := url.URL{
Scheme: "https", Scheme: "https",
User: creds, User: creds,
Host: net.JoinHostPort( Host: net.JoinHostPort(ip.IP, strconv.Itoa(int(ip.Ports[0]))),
ip.IP,
strconv.Itoa(int(ip.Ports[0])),
),
RawQuery: url.Values{ RawQuery: url.Values{
"sni": []string{sni}, "sni": []string{sni},
"peername": []string{fmt.Sprintf("%s%d.%s", strings.ToLower(ip.Geo.CountryCode), i, PROXY_SUFFIX)}, "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 { if gotOne {
key = "#proxy" key = "#proxy"
} }
wr.Write([]string{ wr.Write([]string{key, u.String()})
key,
u.String(),
})
gotOne = true gotOne = true
} }
return 0 return 0
+26 -41
View File
@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url" "net/url"
@@ -16,10 +15,10 @@ import (
) )
const ( const (
ANON_EMAIL_LOCALPART_BYTES = 32 ANON_EMAIL_LOCALPART_BYTES = 32
ANON_PASSWORD_BYTES = 20 ANON_PASSWORD_BYTES = 20
DEVICE_ID_BYTES = 20 DEVICE_ID_BYTES = 20
READ_LIMIT int64 = 128 * 1024 READ_LIMIT int64 = 128 * 1024
) )
type SEEndpoints struct { type SEEndpoints struct {
@@ -73,9 +72,10 @@ type SEClient struct {
type StrKV map[string]string type StrKV map[string]string
// Instantiates SurfEasy client with default settings and given API keys. // NewSEClient instantiates a SurfEasy client.
// Optional `transport` parameter allows to override HTTP transport used // apiUsername/apiSecret are the application-level Digest Auth credentials
// for HTTP calls // 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) { func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*SEClient, error) {
if transport == nil { if transport == nil {
transport = http.DefaultTransport transport = http.DefaultTransport
@@ -83,7 +83,7 @@ func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*S
rng := rand.New(RandomSource) rng := rand.New(RandomSource)
device_id, err := randomCapitalHexString(rng, DEVICE_ID_BYTES) deviceID, err := randomCapitalHexString(rng, DEVICE_ID_BYTES)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -93,17 +93,15 @@ func NewSEClient(apiUsername, apiSecret string, transport http.RoundTripper) (*S
return nil, err return nil, err
} }
res := &SEClient{ return &SEClient{
httpClient: &http.Client{ httpClient: &http.Client{
Jar: jar, Jar: jar,
Transport: dac.NewDigestTransport(apiUsername, apiSecret, transport), Transport: dac.NewDigestTransport(apiUsername, apiSecret, transport),
}, },
Settings: DefaultSESettings, Settings: DefaultSESettings,
rng: rng, rng: rng,
DeviceID: device_id, DeviceID: deviceID,
} }, nil
return res, nil
} }
func (c *SEClient) ResetCookies() error { func (c *SEClient) ResetCookies() error {
@@ -125,6 +123,8 @@ func (c *SEClient) AnonRegister(ctx context.Context) error {
return err 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.SubscriberEmail = fmt.Sprintf("%s@%s.best.vpn", localPart, c.Settings.ClientType)
c.SubscriberPassword = capitalHexSHA1(c.SubscriberEmail) 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 { func (c *SEClient) register(ctx context.Context) error {
err := c.resetCookies() if err := c.resetCookies(); err != nil {
if err != nil {
return err return err
} }
var regRes SERegisterSubscriberResponse var regRes SERegisterSubscriberResponse
err = c.rpcCall(ctx, c.Settings.Endpoints.RegisterSubscriber, StrKV{ err := c.rpcCall(ctx, c.Settings.Endpoints.RegisterSubscriber, StrKV{
"email": c.SubscriberEmail, "email": c.SubscriberEmail,
"password": c.SubscriberPassword, "password": c.SubscriberPassword,
}, &regRes) }, &regRes)
@@ -225,13 +224,12 @@ func (c *SEClient) Login(ctx context.Context) error {
c.Mux.Lock() c.Mux.Lock()
defer c.Mux.Unlock() defer c.Mux.Unlock()
err := c.resetCookies() if err := c.resetCookies(); err != nil {
if err != nil {
return err return err
} }
var loginRes SESubscriberLoginResponse var loginRes SESubscriberLoginResponse
err = c.rpcCall(ctx, c.Settings.Endpoints.SubscriberLogin, StrKV{ err := c.rpcCall(ctx, c.Settings.Endpoints.SubscriberLogin, StrKV{
"login": c.SubscriberEmail, "login": c.SubscriberEmail,
"password": c.SubscriberPassword, "password": c.SubscriberPassword,
"client_type": c.Settings.ClientType, "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 { 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 { for k, v := range params {
input[k] = []string{v} input[k] = []string{v}
} }
req, err := http.NewRequestWithContext( req, err := http.NewRequestWithContext(ctx, "POST", endpoint,
ctx, strings.NewReader(input.Encode()))
"POST",
endpoint,
strings.NewReader(input.Encode()),
)
if err != nil { if err != nil {
return err return err
} }
@@ -310,26 +304,17 @@ func (c *SEClient) rpcCall(ctx context.Context, endpoint string, params map[stri
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
cleanupBody(resp.Body)
return fmt.Errorf("bad http status: %s, headers: %#v", resp.Status, resp.Header) return fmt.Errorf("bad http status: %s, headers: %#v", resp.Status, resp.Header)
} }
decoder := json.NewDecoder(resp.Body) err = json.NewDecoder(resp.Body).Decode(res)
err = decoder.Decode(res)
cleanupBody(resp.Body) cleanupBody(resp.Body)
return err
if err != nil {
return err
}
return nil
} }
// Does cleanup of HTTP response in order to make it reusable by keep-alive // cleanupBody drains and closes an HTTP response body to allow connection reuse.
// logic of HTTP client
func cleanupBody(body io.ReadCloser) { func cleanupBody(body io.ReadCloser) {
io.Copy(ioutil.Discard, &io.LimitedReader{ io.Copy(io.Discard, &io.LimitedReader{R: body, N: READ_LIMIT})
R: body,
N: READ_LIMIT,
})
body.Close() body.Close()
} }