mirror of
https://github.com/Alexey71/opera-proxy.git
synced 2026-05-13 14:11:00 +00:00
Add new API settings
This commit is contained in:
@@ -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
|
||||||
@@ -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) |
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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 ""
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
+436
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user