Add new API settings

This commit is contained in:
Photo
2026-04-26 15:35:01 +03:00
parent 726a68d22b
commit 91ea01b43b
17 changed files with 2803 additions and 156 deletions
+10
View File
@@ -0,0 +1,10 @@
31.59.20.176:6754:oxcestpe:2yoo1hy06sho
198.23.239.134:6540:oxcestpe:2yoo1hy06sho
45.38.107.97:6014:oxcestpe:2yoo1hy06sho
107.172.163.27:6543:oxcestpe:2yoo1hy06sho
198.105.121.200:6462:oxcestpe:2yoo1hy06sho
216.10.27.159:6837:oxcestpe:2yoo1hy06sho
142.111.67.146:5611:oxcestpe:2yoo1hy06sho
191.96.254.138:6185:oxcestpe:2yoo1hy06sho
31.58.9.4:6077:oxcestpe:2yoo1hy06sho
23.26.71.145:5628:oxcestpe:2yoo1hy06sho
+45 -1
View File
@@ -60,6 +60,44 @@ eu2.sec-tunnel.com,77.111.247.51,443
eu3.sec-tunnel.com,77.111.244.22,443 eu3.sec-tunnel.com,77.111.244.22,443
``` ```
You can also skip the SurfEasy discover request and take endpoints from an existing CSV, while still using the normal server selection logic including `-server-selection fastest`:
```
$ ./opera-proxy -discover-csv proxies.csv -server-selection fastest
```
If direct SurfEasy API access is unstable, you can point discovery at a text file with fallback proxies. The app will read `proxies.txt`, test API proxies in parallel, and stop on the first one that successfully completes init/discover. File entries may be plain `host:port`, URL-style `http://user:pass@host:port`, or `host:port:user:pass` like `10proxies.txt`:
```
$ ./opera-proxy -api-proxy-file proxies.txt -country EU
```
By default it tests up to 5 candidates at once. You can change that with `-api-proxy-parallel`:
```
$ ./opera-proxy -api-proxy-file proxies.txt -api-proxy-parallel 5 -country EU
```
You can also download the proxy list from a URL. If the download fails, the app can fall back to a local file:
```
$ ./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
```
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:
```
$ ./opera-proxy -country EU -proxy-bypass "api2.sec-tunnel.com,*.example.com,https://download.test.local/list.txt"
```
If `-proxy-bypass` is not passed, the app also tries to read `proxy-bypass.txt` from the current working directory. The file supports one pattern per line, comments via `#`, and comma-separated values on the same line.
If SurfEasy discover returns API error `801`, the app also automatically tries `proxies.csv` from the current working directory, even when `-discover-csv` was not passed.
## List of arguments ## List of arguments
| Argument | Type | Description | | Argument | Type | Description |
@@ -70,19 +108,25 @@ eu3.sec-tunnel.com,77.111.244.22,443
| 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-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") | | 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 |
| dp-export | - | export configuration for dumbproxy | | dp-export | - | export configuration for dumbproxy |
| fake-SNI | String | domain name to use as SNI in communications with servers | | 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-retries | Number | number of attempts for initialization steps, zero for unlimited retry |
| init-retry-interval | Duration | delay between initialization retries (default 5s) | | init-retry-interval | Duration | delay between initialization retries (default 5s) |
| list-countries | - | list available countries and exit | | list-countries | - | list available countries and exit |
| list-proxies | - | output proxy list 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 | | 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` | | 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 | Duration | login refresh interval (default 4h0m0s) |
| refresh-retry | Duration | login refresh retry interval (default 5s) | | refresh-retry | Duration | login refresh retry interval (default 5s) |
+133
View File
@@ -0,0 +1,133 @@
package dialer
import (
"context"
"fmt"
"net"
"net/url"
"path"
"strings"
)
type bypassPattern struct {
raw string
host string
}
type BypassDialer struct {
direct ContextDialer
proxied ContextDialer
patterns []bypassPattern
}
func NewBypassDialer(patterns []string, direct, proxied ContextDialer) (*BypassDialer, error) {
compiled := make([]bypassPattern, 0, len(patterns))
for _, raw := range patterns {
hostPattern, err := normalizeBypassPattern(raw)
if err != nil {
return nil, fmt.Errorf("invalid proxy bypass pattern %q: %w", raw, err)
}
if _, err := path.Match(hostPattern, ""); err != nil {
return nil, fmt.Errorf("invalid proxy bypass pattern %q: %w", raw, err)
}
compiled = append(compiled, bypassPattern{
raw: raw,
host: hostPattern,
})
}
return &BypassDialer{
direct: direct,
proxied: proxied,
patterns: compiled,
}, nil
}
func (d *BypassDialer) Dial(network, address string) (net.Conn, error) {
return d.DialContext(context.Background(), network, address)
}
func (d *BypassDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
if d.shouldBypass(address) {
return d.direct.DialContext(ctx, network, address)
}
return d.proxied.DialContext(ctx, network, address)
}
func (d *BypassDialer) shouldBypass(address string) bool {
host := normalizeDialTarget(address)
if host == "" {
return false
}
for _, pattern := range d.patterns {
matched, err := path.Match(pattern.host, host)
if err != nil {
continue
}
if matched {
return true
}
}
return false
}
func normalizeBypassPattern(raw string) (string, error) {
pattern := strings.TrimSpace(raw)
if pattern == "" {
return "", fmt.Errorf("empty pattern")
}
switch {
case strings.Contains(pattern, "://"):
parsed, err := url.Parse(pattern)
if err != nil {
return "", fmt.Errorf("unable to parse URL: %w", err)
}
if parsed.Host == "" {
return "", fmt.Errorf("URL does not contain a host")
}
pattern = parsed.Hostname()
case strings.HasPrefix(pattern, "//"):
parsed, err := url.Parse("https:" + pattern)
if err != nil {
return "", fmt.Errorf("unable to parse URL: %w", err)
}
if parsed.Host == "" {
return "", fmt.Errorf("URL does not contain a host")
}
pattern = parsed.Hostname()
case strings.ContainsAny(pattern, "/?"):
parsed, err := url.Parse("https://" + strings.TrimLeft(pattern, "/"))
if err != nil {
return "", fmt.Errorf("unable to parse URL-like pattern: %w", err)
}
if parsed.Host == "" {
return "", fmt.Errorf("pattern does not contain a host")
}
pattern = parsed.Hostname()
default:
if host, _, err := net.SplitHostPort(pattern); err == nil {
pattern = host
}
}
pattern = strings.Trim(pattern, "[]")
pattern = strings.ToLower(pattern)
if pattern == "" {
return "", fmt.Errorf("empty host pattern")
}
return pattern, nil
}
func normalizeDialTarget(address string) string {
host := strings.TrimSpace(address)
if host == "" {
return ""
}
if parsed, err := url.Parse(host); err == nil && parsed.Host != "" {
host = parsed.Hostname()
} else if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
host = strings.Trim(host, "[]")
return strings.ToLower(host)
}
+67
View File
@@ -0,0 +1,67 @@
package dialer
import (
"context"
"errors"
"net"
"testing"
)
type recordingDialer struct {
name string
addresses []string
}
func (d *recordingDialer) Dial(network, address string) (net.Conn, error) {
return d.DialContext(context.Background(), network, address)
}
func (d *recordingDialer) DialContext(_ context.Context, _ string, address string) (net.Conn, error) {
d.addresses = append(d.addresses, address)
return nil, errors.New(d.name)
}
func TestBypassDialerRoutesByHostPattern(t *testing.T) {
direct := &recordingDialer{name: "direct"}
proxied := &recordingDialer{name: "proxied"}
d, err := NewBypassDialer([]string{
"api2.sec-tunnel.com",
"*.example.com",
"https://already.url.test/some/path",
}, direct, proxied)
if err != nil {
t.Fatalf("NewBypassDialer() error = %v", err)
}
tests := []struct {
address string
wantDirect bool
description string
}{
{address: "api2.sec-tunnel.com:443", wantDirect: true, description: "exact host"},
{address: "cdn.example.com:8443", wantDirect: true, description: "wildcard host"},
{address: "ALREADY.URL.TEST:443", wantDirect: true, description: "case insensitive"},
{address: "other.test:443", wantDirect: false, description: "unmatched host"},
}
for _, tt := range tests {
_, err := d.DialContext(context.Background(), "tcp", tt.address)
if err == nil {
t.Fatalf("%s: expected dial error", tt.description)
}
if tt.wantDirect && err.Error() != "direct" {
t.Fatalf("%s: expected direct dialer, got %v", tt.description, err)
}
if !tt.wantDirect && err.Error() != "proxied" {
t.Fatalf("%s: expected proxied dialer, got %v", tt.description, err)
}
}
}
func TestNewBypassDialerRejectsInvalidPattern(t *testing.T) {
_, err := NewBypassDialer([]string{" "}, &recordingDialer{}, &recordingDialer{})
if err == nil {
t.Fatal("expected invalid pattern error")
}
}
+130
View File
@@ -0,0 +1,130 @@
package main
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"time"
)
var (
freeProxyIPPattern = regexp.MustCompile(`data-ip="([^"]+)"`)
freeProxyPortPattern = regexp.MustCompile(`data-port="([^"]+)"`)
)
func fetchFreeProxyToFile(sourceURL, outPath string, timeout time.Duration) (int, error) {
proxies, err := fetchFreeProxyList(sourceURL, timeout)
if err != nil {
return 0, err
}
if len(proxies) == 0 {
return 0, fmt.Errorf("no proxies found at %s", sourceURL)
}
if err := os.WriteFile(outPath, []byte(strings.Join(proxies, "\n")+"\n"), 0o644); err != nil {
return 0, err
}
return len(proxies), nil
}
func fetchFreeProxyList(sourceURL string, timeout time.Duration) ([]string, error) {
client := &http.Client{Timeout: timeout}
seen := make(map[string]struct{})
proxies := make([]string, 0, 256)
for page := 1; ; page++ {
pageURL, err := freeProxyPageURL(sourceURL, page)
if err != nil {
return nil, err
}
pageProxies, err := fetchFreeProxyPage(client, pageURL)
if err != nil {
return nil, fmt.Errorf("page %d: %w", page, err)
}
if len(pageProxies) == 0 {
break
}
for _, proxy := range pageProxies {
if _, ok := seen[proxy]; ok {
continue
}
seen[proxy] = struct{}{}
proxies = append(proxies, proxy)
}
}
return proxies, nil
}
func freeProxyPageURL(rawURL string, page int) (string, error) {
parsed, err := url.Parse(rawURL)
if err != nil {
return "", fmt.Errorf("parse url: %w", err)
}
query := parsed.Query()
if page <= 1 {
query.Del("page")
} else {
query.Set("page", fmt.Sprintf("%d", page))
}
parsed.RawQuery = query.Encode()
return parsed.String(), nil
}
func fetchFreeProxyPage(client *http.Client, pageURL string) ([]string, error) {
req, err := http.NewRequest(http.MethodGet, pageURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "opera-proxy freeproxy fetcher/1.0")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
ipMatches := freeProxyIPPattern.FindAllSubmatch(body, -1)
portMatches := freeProxyPortPattern.FindAllSubmatch(body, -1)
rowCount := min(len(ipMatches), len(portMatches))
proxies := make([]string, 0, rowCount)
for i := 0; i < rowCount; i++ {
ip, err := freeProxyDecodeBase64(string(ipMatches[i][1]))
if err != nil {
return nil, fmt.Errorf("decode ip on row %d: %w", i+1, err)
}
port, err := freeProxyDecodeBase64(string(portMatches[i][1]))
if err != nil {
return nil, fmt.Errorf("decode port on row %d: %w", i+1, err)
}
proxies = append(proxies, ip+":"+port)
}
return proxies, nil
}
func freeProxyDecodeBase64(value string) (string, error) {
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return "", err
}
return string(decoded), nil
}
+93
View File
@@ -0,0 +1,93 @@
package handler
import (
"context"
"errors"
"io"
"log"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/Snawoot/opera-proxy/dialer"
clog "github.com/Snawoot/opera-proxy/log"
)
type recordingDialer struct {
name string
addresses []string
}
func (d *recordingDialer) Dial(network, address string) (net.Conn, error) {
return d.DialContext(context.Background(), network, address)
}
func (d *recordingDialer) DialContext(_ context.Context, _ string, address string) (net.Conn, error) {
d.addresses = append(d.addresses, address)
return nil, errors.New(d.name)
}
func testProxyLogger() *clog.CondLogger {
return clog.NewCondLogger(log.New(io.Discard, "", 0), clog.CRITICAL)
}
func TestProxyHandlerBypassesHTTPRequestsByTargetHost(t *testing.T) {
direct := &recordingDialer{name: "direct"}
proxied := &recordingDialer{name: "proxied"}
bypassDialer, err := dialer.NewBypassDialer([]string{"*.example.com"}, direct, proxied)
if err != nil {
t.Fatalf("NewBypassDialer() error = %v", err)
}
h := NewProxyHandler(bypassDialer, testProxyLogger(), "")
req := httptest.NewRequest(http.MethodGet, "http://check.example.com/path", nil)
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != http.StatusInternalServerError {
t.Fatalf("ServeHTTP() status = %d, want %d", rr.Code, http.StatusInternalServerError)
}
if len(direct.addresses) != 1 || direct.addresses[0] != "check.example.com:80" {
t.Fatalf("direct dialer addresses = %#v, want []string{\"check.example.com:80\"}", direct.addresses)
}
if len(proxied.addresses) != 0 {
t.Fatalf("proxied dialer should not be used, got %#v", proxied.addresses)
}
}
func TestProxyHandlerBypassesConnectRequestsByTargetHost(t *testing.T) {
direct := &recordingDialer{name: "direct"}
proxied := &recordingDialer{name: "proxied"}
bypassDialer, err := dialer.NewBypassDialer([]string{"*.example.com"}, direct, proxied)
if err != nil {
t.Fatalf("NewBypassDialer() error = %v", err)
}
h := NewProxyHandler(bypassDialer, testProxyLogger(), "")
req := &http.Request{
Method: http.MethodConnect,
URL: &url.URL{Host: "secure.example.com:443"},
Host: "secure.example.com:443",
RequestURI: "secure.example.com:443",
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
}
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != http.StatusBadGateway {
t.Fatalf("ServeHTTP() status = %d, want %d", rr.Code, http.StatusBadGateway)
}
if len(direct.addresses) != 1 || direct.addresses[0] != "secure.example.com:443" {
t.Fatalf("direct dialer addresses = %#v, want []string{\"secure.example.com:443\"}", direct.addresses)
}
if len(proxied.addresses) != 0 {
t.Fatalf("proxied dialer should not be used, got %#v", proxied.addresses)
}
}
+21 -10
View File
@@ -25,9 +25,10 @@ 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) *ProxyHandler { func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger, fakeSNI string) *ProxyHandler {
httptransport := &http.Transport{ httptransport := &http.Transport{
MaxIdleConns: 100, MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
@@ -39,6 +40,7 @@ func NewProxyHandler(dialer dialer.ContextDialer, logger *clog.CondLogger) *Prox
logger: logger, logger: logger,
dialer: dialer, dialer: dialer,
httptransport: httptransport, httptransport: httptransport,
fakeSNI: fakeSNI,
} }
} }
@@ -53,7 +55,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, _, err := hijack(wr) localconn, rw, 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)
@@ -64,12 +66,16 @@ 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)
proxy(req.Context(), localconn, conn) 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)
} 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) proxyh2(req.Context(), req.Body, wr, conn, s.fakeSNI)
} 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)
@@ -115,16 +121,21 @@ func (s *ProxyHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
} }
} }
func proxy(ctx context.Context, left, right net.Conn) { func proxy(ctx context.Context, left net.Conn, leftReader io.Reader, right net.Conn, fakeSNI string) {
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
cpy := func(dst, src net.Conn) { ltr := func(dst net.Conn, src io.Reader) {
defer wg.Done()
copyWithSNIRewrite(dst, src, fakeSNI)
dst.Close()
}
rtl := func(dst, src net.Conn) {
defer wg.Done() defer wg.Done()
io.Copy(dst, src) io.Copy(dst, src)
dst.Close() dst.Close()
} }
wg.Add(2) wg.Add(2)
go cpy(left, right) go ltr(right, leftReader)
go cpy(right, left) go rtl(left, right)
groupdone := make(chan struct{}) groupdone := make(chan struct{})
go func() { go func() {
wg.Wait() wg.Wait()
@@ -141,11 +152,11 @@ func proxy(ctx context.Context, left, right net.Conn) {
return return
} }
func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer, right net.Conn) { func proxyh2(ctx context.Context, leftreader io.ReadCloser, leftwriter io.Writer, right net.Conn, fakeSNI string) {
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()
io.Copy(dst, src) copyWithSNIRewrite(dst, src, fakeSNI)
dst.Close() dst.Close()
} }
rtl := func(dst io.Writer, src io.Reader) { rtl := func(dst io.Writer, src io.Reader) {
+39 -2
View File
@@ -2,14 +2,18 @@ package handler
import ( import (
"context" "context"
"fmt"
"io"
"log" "log"
"net" "net"
"strings"
"github.com/Alexey71/opera-proxy/dialer" "github.com/Alexey71/opera-proxy/dialer"
"github.com/things-go/go-socks5" "github.com/things-go/go-socks5"
"github.com/things-go/go-socks5/statute"
) )
func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger) (*socks5.Server, error) { func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger, fakeSNI string) (*socks5.Server, error) {
opts := []socks5.Option{ opts := []socks5.Option{
socks5.WithLogger(socks5.NewLogger(logger)), socks5.WithLogger(socks5.NewLogger(logger)),
socks5.WithRule( socks5.WithRule(
@@ -17,12 +21,45 @@ func NewSocksServer(dialer dialer.ContextDialer, logger *log.Logger) (*socks5.Se
EnableConnect: true, EnableConnect: true,
}, },
), ),
socks5.WithDial(dialer.DialContext),
socks5.WithResolver(DummySocksResolver{}), socks5.WithResolver(DummySocksResolver{}),
socks5.WithConnectHandle(func(ctx context.Context, writer io.Writer, request *socks5.Request) error {
return handleSocksConnect(ctx, writer, request, dialer, fakeSNI)
}),
} }
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 {
target, err := upstream.DialContext(ctx, "tcp", request.DestAddr.String())
if err != nil {
reply := statute.RepHostUnreachable
msg := err.Error()
if strings.Contains(msg, "refused") {
reply = statute.RepConnectionRefused
} else if strings.Contains(msg, "network is unreachable") {
reply = statute.RepNetworkUnreachable
}
if sendErr := socks5.SendReply(writer, reply, nil); sendErr != nil {
return fmt.Errorf("failed to send reply, %v", sendErr)
}
return fmt.Errorf("connect to %v failed, %v", request.RawDestAddr, err)
}
if err := socks5.SendReply(writer, statute.RepSuccess, target.LocalAddr()); err != nil {
target.Close()
return fmt.Errorf("failed to send reply, %v", err)
}
clientConn, ok := writer.(net.Conn)
if !ok {
target.Close()
return fmt.Errorf("writer is %T, expected net.Conn", writer)
}
proxy(ctx, clientConn, request.Reader, target, fakeSNI)
return nil
}
type DummySocksResolver struct{} type DummySocksResolver struct{}
func (_ DummySocksResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) { func (_ DummySocksResolver) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) {
+227
View File
@@ -0,0 +1,227 @@
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
@@ -0,0 +1,133 @@
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 ""
}
+1370 -131
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
package main
import (
"os"
"path/filepath"
"reflect"
"testing"
)
func TestLoadProxyBypassList(t *testing.T) {
dir := t.TempDir()
filename := filepath.Join(dir, "proxy-bypass.txt")
content := "" +
"# comment only\n" +
"api2.sec-tunnel.com\n" +
"*.example.com, https://download.test.local/list.txt\n" +
"api2.sec-tunnel.com # duplicate\n" +
"\n"
if err := os.WriteFile(filename, []byte(content), 0o644); err != nil {
t.Fatalf("os.WriteFile() error = %v", err)
}
got, err := loadProxyBypassList(filename)
if err != nil {
t.Fatalf("loadProxyBypassList() error = %v", err)
}
want := []string{
"api2.sec-tunnel.com",
"*.example.com",
"https://download.test.local/list.txt",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("loadProxyBypassList() = %#v, want %#v", got, want)
}
}
+21
View File
@@ -0,0 +1,21 @@
country_code,country_name,host,ip_address,port,speed_ms,speed_status
EU,,eu14.sec-tunnel.com,77.111.247.43,443,566,ok
EU,,eu15.sec-tunnel.com,77.111.247.44,443,577,ok
EU,,eu17.sec-tunnel.com,77.111.247.55,443,587,ok
EU,,eu13.sec-tunnel.com,77.111.247.233,443,600,ok
EU,,eu16.sec-tunnel.com,77.111.247.46,443,606,ok
EU,,eu19.sec-tunnel.com,77.111.247.57,443,607,ok
EU,,eu12.sec-tunnel.com,77.111.247.232,443,622,ok
EU,,eu18.sec-tunnel.com,77.111.247.56,443,651,ok
AM,,am1.sec-tunnel.com,77.111.246.14,443,1269,ok
AM,,am2.sec-tunnel.com,77.111.246.60,443,1269,ok
AM,,am0.sec-tunnel.com,77.111.246.126,443,1427,ok
EU,,eu11.sec-tunnel.com,77.111.244.37,443,1429,ok
EU,,eu10.sec-tunnel.com,77.111.244.210,443,1535,ok
AS,,as8.sec-tunnel.com,77.111.245.16,443,2200,ok
AS,,as5.sec-tunnel.com,77.111.245.10,443,2263,ok
AS,,as7.sec-tunnel.com,77.111.245.15,443,2359,ok
AS,,as6.sec-tunnel.com,77.111.245.12,443,2430,ok
AS,,as9.sec-tunnel.com,77.111.245.17,443,3300,ok
AM,,am3.sec-tunnel.com,77.111.246.62,443,11270,ok
AM,,am4.sec-tunnel.com,77.111.246.64,443,11654,ok
1 country_code country_name host ip_address port speed_ms speed_status
2 EU eu14.sec-tunnel.com 77.111.247.43 443 566 ok
3 EU eu15.sec-tunnel.com 77.111.247.44 443 577 ok
4 EU eu17.sec-tunnel.com 77.111.247.55 443 587 ok
5 EU eu13.sec-tunnel.com 77.111.247.233 443 600 ok
6 EU eu16.sec-tunnel.com 77.111.247.46 443 606 ok
7 EU eu19.sec-tunnel.com 77.111.247.57 443 607 ok
8 EU eu12.sec-tunnel.com 77.111.247.232 443 622 ok
9 EU eu18.sec-tunnel.com 77.111.247.56 443 651 ok
10 AM am1.sec-tunnel.com 77.111.246.14 443 1269 ok
11 AM am2.sec-tunnel.com 77.111.246.60 443 1269 ok
12 AM am0.sec-tunnel.com 77.111.246.126 443 1427 ok
13 EU eu11.sec-tunnel.com 77.111.244.37 443 1429 ok
14 EU eu10.sec-tunnel.com 77.111.244.210 443 1535 ok
15 AS as8.sec-tunnel.com 77.111.245.16 443 2200 ok
16 AS as5.sec-tunnel.com 77.111.245.10 443 2263 ok
17 AS as7.sec-tunnel.com 77.111.245.15 443 2359 ok
18 AS as6.sec-tunnel.com 77.111.245.12 443 2430 ok
19 AS as9.sec-tunnel.com 77.111.245.17 443 3300 ok
20 AM am3.sec-tunnel.com 77.111.246.62 443 11270 ok
21 AM am4.sec-tunnel.com 77.111.246.64 443 11654 ok
+436
View File
@@ -0,0 +1,436 @@
106.51.185.233:8080
213.169.33.10:8001
103.227.187.1:6080
131.161.68.41:35944
61.49.87.3:80
203.28.67.74:8080
152.42.201.82:1080
82.115.60.66:80
24.152.58.108:999
190.60.47.62:999
176.88.168.107:8080
38.51.207.184:999
103.136.171.145:8080
118.69.186.75:1452
200.35.34.134:999
204.157.251.178:999
185.76.240.94:10001
176.88.166.207:8080
51.159.28.39:80
112.203.192.108:8082
2.27.32.81:3128
79.137.204.101:1080
46.107.230.122:1080
8.209.239.31:30000
179.49.113.225:999
193.43.159.163:8080
186.248.87.172:5678
138.68.235.51:80
118.70.151.55:1080
79.101.106.74:33608
58.33.109.114:2021
103.78.83.22:8083
185.76.240.97:10001
192.252.211.197:14921
168.138.202.218:3128
77.91.77.220:3128
103.132.52.229:8080
185.76.240.99:10001
138.84.65.117:8081
187.111.144.102:8080
181.94.197.37:8080
185.242.107.91:3129
5.102.109.41:999
185.76.240.91:10001
31.97.100.157:8080
114.111.151.41:80
5.196.101.18:3128
102.68.135.147:8080
194.87.215.166:9443
14.232.166.150:5678
185.114.73.2:1080
8.212.177.126:8080
103.153.246.54:8181
103.46.186.161:8080
201.234.50.194:3128
210.223.44.230:3128
176.61.151.123:80
213.131.85.28:1976
45.92.108.112:80
135.148.120.6:80
108.161.135.118:80
138.124.99.216:8888
103.103.146.149:7080
79.137.17.104:80
185.85.111.18:80
112.198.22.122:80
80.78.73.142:65530
94.23.9.170:80
104.225.220.233:80
133.18.234.13:80
34.135.166.24:80
195.231.69.203:80
193.105.62.11:58973
186.26.95.249:61445
174.104.115.21:80
35.209.198.222:80
167.99.124.118:80
201.222.50.218:80
174.138.119.88:80
185.76.241.223:10001
159.223.225.118:8888
163.172.167.48:80
65.108.103.19:80
46.17.47.48:80
13.80.134.180:80
154.43.62.22:80
190.119.132.61:80
46.21.186.252:1080
147.231.163.133:80
124.108.6.20:8085
5.161.103.41:88
143.198.135.176:80
103.205.64.153:80
190.110.226.122:80
37.187.74.125:80
185.76.241.158:10001
103.151.20.131:80
66.111.113.34:80
183.110.216.159:8090
31.220.78.244:80
95.214.9.93:3128
23.247.136.254:80
185.76.240.64:10001
194.102.38.53:80
62.149.165.171:80
185.76.240.60:10001
103.65.237.92:5678
138.91.159.185:80
219.93.101.62:80
185.76.240.61:10001
154.65.39.7:80
79.174.12.190:80
197.221.240.240:80
51.79.135.131:8080
152.53.65.215:8090
200.174.198.32:8888
167.99.236.14:80
27.34.242.98:80
141.147.9.254:80
13.230.49.39:8080
149.248.215.39:8081
113.160.132.26:8080
8.219.97.248:80
209.135.168.41:80
203.23.238.21:80
41.220.16.213:80
163.172.53.142:80
34.44.49.215:80
152.230.215.123:80
211.38.188.120:9080
35.225.22.61:80
45.146.163.31:80
213.177.2.55:80
41.220.16.209:80
196.1.93.16:80
197.221.249.196:80
195.26.224.135:80
219.249.37.107:8382
168.222.254.88:3128
156.38.112.11:80
212.231.191.23:80
170.64.170.204:8080
45.167.125.21:999
102.223.9.53:80
69.70.244.34:80
97.74.87.226:80
190.119.132.62:80
219.65.73.81:80
210.177.178.148:80
87.255.196.143:80
41.220.16.218:80
219.93.101.63:80
219.93.101.60:80
85.214.204.79:80
39.109.113.97:4090
196.223.129.21:80
34.81.160.132:80
38.60.196.214:80
194.71.227.149:80
197.221.240.178:80
78.28.152.113:80
41.220.16.208:80
154.65.39.8:80
197.221.234.252:80
197.221.249.198:80
41.220.16.215:80
193.181.35.106:8118
46.249.100.124:80
194.150.110.134:80
147.91.22.150:80
159.65.221.25:80
139.162.200.213:80
150.136.163.51:80
91.132.92.231:80
93.127.163.52:80
91.132.92.150:80
34.140.137.151:80
198.111.166.184:80
192.73.244.36:80
66.135.16.53:80
89.58.55.33:80
151.236.24.38:80
207.180.254.198:8080
212.47.232.28:80
84.39.112.144:3128
202.133.88.173:80
80.74.54.148:3128
109.236.88.82:80
178.156.224.42:3128
195.91.129.101:1337
103.90.66.19:1080
187.19.127.246:8011
138.36.199.14:4153
41.223.234.116:37259
110.93.206.62:5678
194.61.24.198:1080
103.37.82.134:39873
103.136.107.70:1080
14.241.241.185:4145
110.232.86.221:1080
83.218.186.22:5678
103.174.122.197:8199
110.235.248.142:1080
69.48.201.94:80
31.43.194.184:1080
119.148.47.166:22122
76.81.6.107:31008
103.88.169.106:33149
195.138.65.34:5678
109.166.207.162:3629
91.247.92.63:5678
81.228.44.187:65530
181.115.75.102:5678
113.53.29.228:13629
103.111.136.82:8199
168.253.92.93:10808
138.68.247.51:1080
36.37.244.41:5678
103.156.14.234:1080
160.22.22.85:5678
45.236.152.21:4145
177.136.124.47:56113
111.67.103.162:1080
128.199.37.92:1080
202.70.80.153:5678
192.248.95.98:54126
34.122.187.196:80
186.47.213.158:5678
46.146.216.44:1080
87.117.11.57:1080
20.64.237.168:1080
202.51.103.154:5678
198.0.198.132:54321
163.47.37.190:1080
125.24.156.113:7080
139.255.63.170:14888
46.30.46.133:3128
103.39.245.106:1080
202.84.76.190:5678
193.233.254.82:1080
206.189.154.204:8888
31.43.63.70:4145
199.229.254.129:4145
187.157.30.202:4153
114.130.153.122:58080
192.252.214.20:15864
144.124.227.145:8080
203.189.135.73:1080
213.169.33.8:8001
202.5.53.145:9355
103.172.120.102:8097
198.8.94.174:39078
202.40.188.2:1088
192.111.135.17:18302
192.252.209.158:4145
124.83.114.43:8081
41.21.182.179:5678
103.175.127.230:60606
103.102.12.134:1111
103.19.58.151:8080
45.79.203.254:48388
143.137.116.72:1080
103.120.221.129:8083
202.154.241.199:808
103.99.110.222:5678
103.151.74.5:2025
5.134.48.59:8080
202.154.19.63:8083
103.17.246.60:1080
103.156.248.78:8080
202.74.243.182:228
97.105.12.186:4153
103.163.244.106:1080
102.212.44.151:12354
163.53.204.178:9813
157.15.40.251:7777
79.132.136.58:3128
50.238.47.86:32100
192.111.135.18:18301
103.132.52.32:8080
47.237.169.183:1080
202.181.16.45:52929
192.252.209.155:14455
94.247.241.70:51006
65.21.201.149:8080
147.45.167.84:3128
193.233.254.61:1080
5.255.123.43:1080
192.111.139.165:4145
20.64.246.197:1080
114.130.175.146:1088
194.67.99.223:1080
91.108.243.203:3128
91.150.189.122:60647
192.252.210.233:4145
157.66.16.52:8080
91.99.182.39:4000
31.43.33.56:4153
8.220.143.77:1080
45.148.30.212:3128
89.43.133.146:8080
168.110.52.228:3128
103.56.115.156:7890
181.236.221.138:4145
103.167.156.202:1080
103.183.19.34:3128
41.139.147.86:5678
113.192.31.17:8080
203.189.141.138:1080
103.97.140.157:1080
37.60.237.110:30001
65.108.203.36:18080
192.252.216.81:4145
192.111.129.150:4145
103.153.63.223:1080
103.189.63.149:53053
198.8.94.170:4145
38.76.200.182:58367
162.240.19.30:80
185.76.240.178:10001
41.169.151.154:4153
103.180.123.27:8080
5.161.50.82:8118
121.135.144.141:8096
75.84.71.14:80
193.23.194.147:3128
185.76.241.162:10001
196.1.97.198:80
185.76.240.208:10001
185.76.240.45:10001
46.47.197.210:3128
23.88.88.102:80
45.12.151.226:2829
196.1.93.10:80
185.76.240.203:10001
150.107.140.238:3128
41.184.92.220:80
8.209.255.13:3128
143.42.66.91:80
46.29.162.166:80
41.184.92.219:80
23.88.88.105:443
116.203.139.209:5678
176.236.37.132:1080
8.211.152.121:10000
20.57.134.61:1080
202.62.34.102:1080
195.133.250.173:3128
110.235.247.105:1080
103.112.234.65:5678
212.23.69.121:1080
109.120.151.2:1080
203.189.154.80:1080
213.149.154.213:5678
117.102.115.154:4153
202.4.117.136:12321
103.239.52.100:1080
210.16.86.105:5678
168.232.213.9:4153
20.120.246.129:1080
31.13.239.4:5678
31.10.83.158:8080
27.123.254.158:6969
23.133.196.12:9000
202.166.196.105:48293
102.36.127.53:1080
154.79.248.156:5678
95.179.255.75:1080
119.148.51.30:22122
191.7.208.86:8080
122.252.179.66:5678
171.249.163.170:1452
49.151.177.202:8082
107.219.228.250:7777
113.160.188.21:1080
103.18.47.79:4145
223.25.110.37:8199
190.60.40.242:999
81.12.157.98:5678
115.127.112.178:1080
103.10.99.110:5678
124.105.55.176:30906
130.193.123.34:5678
160.19.41.60:80
31.42.6.125:5678
190.102.233.100:999
158.160.215.167:8123
103.206.68.241:1080
27.147.153.34:10888
123.200.8.6:1081
113.161.6.170:1080
185.14.149.33:4145
103.239.201.50:58765
165.0.100.149:5678
118.174.233.113:4153
110.74.195.152:1080
164.138.205.232:8080
80.78.74.8:65530
185.95.186.165:48293
202.138.240.249:8080
45.81.146.7:8080
163.61.254.104:1111
103.39.51.184:8088
107.172.102.234:40621
197.164.101.13:1981
181.48.243.194:4153
82.209.251.53:45678
1.179.172.45:31225
209.14.108.98:999
120.28.193.126:8082
38.58.170.10:999
103.66.62.177:8080
205.177.85.130:39593
103.30.211.34:80
91.211.115.127:1080
202.58.77.194:8031
37.26.86.206:4145
62.4.37.104:60606
91.147.235.162:4153
185.76.240.33:10001
43.153.28.68:3128
185.76.240.57:10001
185.76.240.12:10001
185.76.241.132:10001
185.76.241.232:10001
185.76.240.19:10001
167.71.196.28:8080
185.76.240.177:10001
185.76.240.18:10001
185.76.240.47:10001
172.236.140.124:3128
185.76.240.55:10001
185.76.240.201:10001
91.121.63.51:1080
62.113.119.14:8080
213.154.2.210:3128
+19
View File
@@ -0,0 +1,19 @@
# комментарий
*.ru
#avito
avito.ru
*.avito.ru
avito.st
cdn.avito.ru
img.avito.ru
static.avito.ru
#ozon
ozon.ru
ozon.com
*.ozon.ru
ozonusercontent.com
ozon-images.ru
ozon-st.com
cdn.ozon.ru
+17
View File
@@ -12,6 +12,22 @@ const (
SE_STATUS_OK int64 = 0 SE_STATUS_OK int64 = 0
) )
type APIError struct {
Code int64
Message string
}
func (e *APIError) Error() string {
return fmt.Sprintf("API responded with error message: code=%d, msg=%q", e.Code, e.Message)
}
func newAPIError(status SEStatusPair) error {
return &APIError{
Code: status.Code,
Message: status.Message,
}
}
type SEStatusPair struct { type SEStatusPair struct {
Code int64 Code int64
Message string Message string
@@ -85,6 +101,7 @@ type SEGeoListResponse struct {
type SEIPEntry struct { type SEIPEntry struct {
Geo SEGeoEntry `json:"geo"` Geo SEGeoEntry `json:"geo"`
Host string `json:"host,omitempty"`
IP string `json:"ip"` IP string `json:"ip"`
Ports []uint16 `json:"ports"` Ports []uint16 `json:"ports"`
} }
+6 -12
View File
@@ -153,8 +153,7 @@ func (c *SEClient) register(ctx context.Context) error {
} }
if regRes.Status.Code != SE_STATUS_OK { if regRes.Status.Code != SE_STATUS_OK {
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"", return newAPIError(regRes.Status)
regRes.Status.Code, regRes.Status.Message)
} }
return nil return nil
} }
@@ -174,8 +173,7 @@ func (c *SEClient) RegisterDevice(ctx context.Context) error {
} }
if regRes.Status.Code != SE_STATUS_OK { if regRes.Status.Code != SE_STATUS_OK {
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"", return newAPIError(regRes.Status)
regRes.Status.Code, regRes.Status.Message)
} }
c.AssignedDeviceID = regRes.Data.DeviceID c.AssignedDeviceID = regRes.Data.DeviceID
@@ -197,8 +195,7 @@ func (c *SEClient) GeoList(ctx context.Context) ([]SEGeoEntry, error) {
} }
if geoListRes.Status.Code != SE_STATUS_OK { if geoListRes.Status.Code != SE_STATUS_OK {
return nil, fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"", return nil, newAPIError(geoListRes.Status)
geoListRes.Status.Code, geoListRes.Status.Message)
} }
return geoListRes.Data.Geos, nil return geoListRes.Data.Geos, nil
@@ -218,8 +215,7 @@ func (c *SEClient) Discover(ctx context.Context, requestedGeo string) ([]SEIPEnt
} }
if discoverRes.Status.Code != SE_STATUS_OK { if discoverRes.Status.Code != SE_STATUS_OK {
return nil, fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"", return nil, newAPIError(discoverRes.Status)
discoverRes.Status.Code, discoverRes.Status.Message)
} }
return discoverRes.Data.IPs, nil return discoverRes.Data.IPs, nil
@@ -245,8 +241,7 @@ func (c *SEClient) Login(ctx context.Context) error {
} }
if loginRes.Status.Code != SE_STATUS_OK { if loginRes.Status.Code != SE_STATUS_OK {
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"", return newAPIError(loginRes.Status)
loginRes.Status.Code, loginRes.Status.Message)
} }
return nil return nil
} }
@@ -264,8 +259,7 @@ func (c *SEClient) DeviceGeneratePassword(ctx context.Context) error {
} }
if genRes.Status.Code != SE_STATUS_OK { if genRes.Status.Code != SE_STATUS_OK {
return fmt.Errorf("API responded with error message: code=%d, msg=\"%s\"", return newAPIError(genRes.Status)
genRes.Status.Code, genRes.Status.Message)
} }
c.DevicePassword = genRes.Data.DevicePassword c.DevicePassword = genRes.Data.DevicePassword