2021-03-25 20:45:48 +02:00
package main
import (
2026-04-26 15:35:01 +03:00
"bufio"
2024-08-01 12:01:24 +03:00
"bytes"
2021-03-27 00:43:33 +02:00
"context"
2021-03-27 02:48:07 +02:00
"crypto/tls"
2021-04-28 17:56:53 +03:00
"crypto/x509"
2021-03-27 02:48:07 +02:00
"encoding/csv"
2021-03-26 22:34:43 +02:00
"errors"
"flag"
"fmt"
2024-08-01 12:01:24 +03:00
"io"
2021-03-25 20:45:48 +02:00
"log"
2021-03-26 22:34:43 +02:00
"net"
"net/http"
"net/url"
"os"
2025-11-20 01:43:07 +02:00
"runtime/debug"
2026-04-26 15:35:01 +03:00
"sort"
2025-11-20 00:41:55 +02:00
"strconv"
2021-03-27 01:42:32 +02:00
"strings"
2026-04-26 15:35:01 +03:00
"sync"
2021-03-27 02:48:07 +02:00
"time"
2021-03-25 20:45:48 +02:00
2021-03-26 22:34:43 +02:00
xproxy "golang.org/x/net/proxy"
2021-03-27 00:43:33 +02:00
2026-03-25 15:26:39 +03:00
"github.com/Alexey71/opera-proxy/clock"
"github.com/Alexey71/opera-proxy/dialer"
"github.com/Alexey71/opera-proxy/handler"
clog "github.com/Alexey71/opera-proxy/log"
"github.com/Alexey71/opera-proxy/resolver"
se "github.com/Alexey71/opera-proxy/seclient"
2026-01-08 18:14:22 +02:00
_ "golang.org/x/crypto/x509roots/fallback"
2026-01-08 21:34:17 +02:00
"golang.org/x/crypto/x509roots/fallback/bundle"
2021-03-25 20:45:48 +02:00
)
2021-03-28 22:21:23 +03:00
const (
2026-04-26 15:35:01 +03:00
API_DOMAIN = "api2.sec-tunnel.com"
PROXY_SUFFIX = "sec-tunnel.com"
DefaultDiscoverCSVFallback = "proxies.csv"
DefaultProxyBypassFallback = "proxy-bypass.txt"
2026-04-30 19:04:28 +03:00
// Default timeouts increased to reduce premature API errors on slow networks.
DEFAULT_TIMEOUT = 30 * time . Second
DEFAULT_SERVER_SELECTION_TIMEOUT = 60 * time . Second
// Reduced idle connection pool to lower resource usage on embedded/low-RAM hosts.
HTTP_MAX_IDLE_CONNS = 10
HTTP_MAX_IDLE_CONNS_PER_HOST = 3
HTTP_IDLE_CONN_TIMEOUT = 60 * time . Second
2021-03-28 22:21:23 +03:00
)
2021-03-26 22:34:43 +02:00
func perror ( msg string ) {
fmt . Fprintln ( os . Stderr , "" )
fmt . Fprintln ( os . Stderr , msg )
}
func arg_fail ( msg string ) {
perror ( msg )
perror ( "Usage:" )
flag . PrintDefaults ( )
os . Exit ( 2 )
}
2024-08-01 12:01:24 +03:00
type CSVArg struct {
values [ ] string
}
func ( a * CSVArg ) String ( ) string {
if len ( a . values ) == 0 {
return ""
}
buf := new ( bytes . Buffer )
wr := csv . NewWriter ( buf )
wr . Write ( a . values )
wr . Flush ( )
return strings . TrimRight ( buf . String ( ) , "\n" )
}
func ( a * CSVArg ) Set ( line string ) error {
rd := csv . NewReader ( strings . NewReader ( line ) )
rd . FieldsPerRecord = - 1
rd . TrimLeadingSpace = true
values , err := rd . Read ( )
if err == io . EOF {
a . values = nil
return nil
}
if err != nil {
return fmt . Errorf ( "unable to parse comma-separated argument: %w" , err )
}
a . values = values
return nil
}
2025-09-14 15:16:02 +03:00
type serverSelectionArg struct {
value dialer . ServerSelection
}
func ( a * serverSelectionArg ) Set ( s string ) error {
v , err := dialer . ParseServerSelection ( s )
if err != nil {
return err
}
a . value = v
return nil
}
func ( a * serverSelectionArg ) String ( ) string {
return a . value . String ( )
}
2021-03-26 22:34:43 +02:00
type CLIArgs struct {
2025-09-14 15:16:02 +03:00
country string
2026-04-26 15:35:01 +03:00
countryExplicit bool
discoverCSV string
2025-09-14 15:16:02 +03:00
listCountries bool
listProxies bool
2026-04-26 15:35:01 +03:00
listProxiesAll bool
listProxiesAllOut string
fetchFreeProxyOut string
fetchFreeProxyURL string
estimateProxySpeed bool
sortProxiesBy string
2025-11-20 00:41:55 +02:00
dpExport bool
2026-04-26 15:35:01 +03:00
discoverRepeat int
2025-09-14 15:16:02 +03:00
bindAddress string
socksMode bool
verbosity int
timeout time . Duration
showVersion bool
proxy string
apiLogin string
apiPassword string
apiAddress string
apiClientType string
apiClientVersion string
apiUserAgent string
2025-10-10 15:25:12 +03:00
apiProxy string
2026-04-26 15:35:01 +03:00
apiProxyFile string
apiProxyListURL string
apiProxyParallel int
2025-09-14 15:16:02 +03:00
bootstrapDNS * CSVArg
refresh time . Duration
refreshRetry time . Duration
initRetries int
initRetryInterval time . Duration
caFile string
fakeSNI string
2026-04-26 15:35:01 +03:00
proxyBypass * CSVArg
proxyBlacklistFile string
proxyBlacklist map [ string ] struct { }
2025-09-14 15:16:02 +03:00
overrideProxyAddress string
2026-04-26 15:35:01 +03:00
proxySpeedTestURL string
proxySpeedTimeout time . Duration
proxySpeedDLLimit int64
2025-09-14 15:16:02 +03:00
serverSelection serverSelectionArg
serverSelectionTimeout time . Duration
serverSelectionTestURL string
serverSelectionDLLimit int64
2021-03-26 22:34:43 +02:00
}
2024-08-01 12:01:24 +03:00
func parse_args ( ) * CLIArgs {
args := & CLIArgs {
bootstrapDNS : & CSVArg {
values : [ ] string {
"https://1.1.1.3/dns-query" ,
2024-08-01 12:49:41 +03:00
"https://8.8.8.8/dns-query" ,
"https://dns.google/dns-query" ,
2024-08-01 12:01:24 +03:00
"https://security.cloudflare-dns.com/dns-query" ,
2025-01-04 15:25:27 +02:00
"https://fidelity.vm-0.com/q" ,
2024-08-01 12:01:24 +03:00
"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/" ,
} ,
} ,
2026-04-26 15:35:01 +03:00
proxyBypass : & CSVArg { } ,
2025-09-14 15:16:02 +03:00
serverSelection : serverSelectionArg { dialer . ServerSelectionFastest } ,
2024-08-01 12:01:24 +03:00
}
2026-04-26 15:35:01 +03:00
flag . StringVar ( & args . country , "country" , "EU" , "desired proxy location; for list-proxies-all modes supports comma-separated codes or ALL" )
flag . StringVar ( & args . discoverCSV , "discover-csv" , "" , "read proxy endpoints from CSV instead of SurfEasy discover API" )
2021-03-27 00:43:33 +02:00
flag . BoolVar ( & args . listCountries , "list-countries" , false , "list available countries and exit" )
flag . BoolVar ( & args . listProxies , "list-proxies" , false , "output proxy list and exit" )
2026-04-26 15:35:01 +03:00
flag . BoolVar ( & args . listProxiesAll , "list-proxies-all" , false , "output proxy list for all countries and exit" )
flag . StringVar ( & args . listProxiesAllOut , "list-proxies-all-out" , "" , "write proxy list CSV to file" )
flag . StringVar ( & args . fetchFreeProxyOut , "fetch-freeproxy-out" , "" , "download proxy list from advanced.name/freeproxy and save it as a text file with one ip:port per line" )
flag . StringVar ( & args . fetchFreeProxyURL , "fetch-freeproxy-url" , "https://advanced.name/freeproxy" , "source URL for -fetch-freeproxy-out" )
flag . BoolVar ( & args . estimateProxySpeed , "estimate-proxy-speed" , false , "measure proxy response time for proxy list output" )
flag . StringVar ( & args . sortProxiesBy , "sort-proxies-by" , "speed" , "proxy list sort order: speed, country, ip" )
2025-11-20 00:41:55 +02:00
flag . BoolVar ( & args . dpExport , "dp-export" , false , "export configuration for dumbproxy" )
2026-04-26 15:35:01 +03:00
flag . IntVar ( & args . discoverRepeat , "discover-repeat" , 1 , "number of repeated discover requests to aggregate and deduplicate" )
2025-04-11 23:20:07 +07:00
flag . StringVar ( & args . bindAddress , "bind-address" , "127.0.0.1:18080" , "proxy listen address" )
flag . BoolVar ( & args . socksMode , "socks-mode" , false , "listen for SOCKS requests instead of HTTP" )
2021-03-26 22:34:43 +02:00
flag . IntVar ( & args . verbosity , "verbosity" , 20 , "logging verbosity " +
"(10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical)" )
2026-04-30 19:04:28 +03:00
flag . DurationVar ( & args . timeout , "timeout" , DEFAULT_TIMEOUT ,
"timeout for network operations" )
2021-03-26 22:34:43 +02:00
flag . BoolVar ( & args . showVersion , "version" , false , "show program version and exit" )
2021-03-26 22:40:27 +02:00
flag . StringVar ( & args . proxy , "proxy" , "" , "sets base proxy to use for all dial-outs. " +
"Format: <http|https|socks5|socks5h>://[login:password@]host[:port] " +
2021-03-26 22:34:43 +02:00
"Examples: http://user:password@192.168.1.1:3128, socks5://10.0.0.1:1080" )
2026-04-26 15:35:01 +03:00
flag . Var ( args . proxyBypass , "proxy-bypass" , "comma-separated list of destination host or URL patterns that should bypass proxying and connect directly; matching is case-insensitive and supports * in hostnames" )
2024-10-06 00:08:17 +03:00
flag . StringVar ( & args . apiClientVersion , "api-client-version" , se . DefaultSESettings . ClientVersion , "client version reported to SurfEasy API" )
flag . StringVar ( & args . apiClientType , "api-client-type" , se . DefaultSESettings . ClientType , "client type reported to SurfEasy API" )
flag . StringVar ( & args . apiUserAgent , "api-user-agent" , se . DefaultSESettings . UserAgent , "user agent reported to SurfEasy API" )
2021-03-27 00:43:33 +02:00
flag . StringVar ( & args . apiLogin , "api-login" , "se0316" , "SurfEasy API login" )
flag . StringVar ( & args . apiPassword , "api-password" , "SILrMEPBmJuhomxWkfm3JalqHX2Eheg1YhlEZiMh8II" , "SurfEasy API password" )
2021-03-28 22:21:23 +03:00
flag . StringVar ( & args . apiAddress , "api-address" , "" , fmt . Sprintf ( "override IP address of %s" , API_DOMAIN ) )
2025-10-10 15:25:12 +03:00
flag . StringVar ( & args . apiProxy , "api-proxy" , "" , "additional proxy server used to access SurfEasy API" )
2026-04-26 15:35:01 +03:00
flag . StringVar ( & args . apiProxyFile , "api-proxy-file" , "" , "path to text file with candidate proxy servers for SurfEasy API access, one per line; proxies are tried in order until init/discover succeeds" )
flag . StringVar ( & args . apiProxyListURL , "api-proxy-list-url" , "" , "URL of a text file with candidate proxy servers for SurfEasy API access; falls back to -api-proxy-file if download fails" )
flag . IntVar ( & args . apiProxyParallel , "api-proxy-parallel" , 5 , "number of API proxy candidates tested in parallel when -api-proxy-file is used" )
2024-08-01 12:01:24 +03:00
flag . Var ( args . bootstrapDNS , "bootstrap-dns" ,
2025-08-12 19:38:17 +03:00
"comma-separated list of DNS/DoH/DoT resolvers for initial discovery of SurfEasy API address. " +
2025-08-13 16:22:16 +03:00
"Supported schemes are: dns://, https://, tls://, tcp://. " +
2025-08-12 19:38:17 +03:00
"Examples: https://1.1.1.1/dns-query,tls://9.9.9.9:853" )
2021-03-30 16:29:54 +03:00
flag . DurationVar ( & args . refresh , "refresh" , 4 * time . Hour , "login refresh interval" )
2021-04-23 13:01:53 +03:00
flag . DurationVar ( & args . refreshRetry , "refresh-retry" , 5 * time . Second , "login refresh retry interval" )
2024-11-05 15:59:10 +02:00
flag . IntVar ( & args . initRetries , "init-retries" , 0 , "number of attempts for initialization steps, zero for unlimited retry" )
2025-01-27 14:59:22 +02:00
flag . DurationVar ( & args . initRetryInterval , "init-retry-interval" , 5 * time . Second , "delay between initialization retries" )
2021-04-28 17:56:53 +03:00
flag . StringVar ( & args . caFile , "cafile" , "" , "use custom CA certificate bundle file" )
2026-04-26 15:35:01 +03:00
flag . StringVar ( & args . fakeSNI , "fake-SNI" , "" , "domain name to use as SNI in outbound TLS and tunneled TLS ClientHello where possible" )
flag . StringVar ( & args . proxyBlacklistFile , "proxy-blacklist" , "" , "path to file with blacklisted proxy addresses, one host[:port] per line" )
2025-01-27 14:59:22 +02:00
flag . StringVar ( & args . overrideProxyAddress , "override-proxy-address" , "" , "use fixed proxy address instead of server address returned by SurfEasy API" )
2026-04-26 15:35:01 +03:00
flag . StringVar ( & args . proxySpeedTestURL , "proxy-speed-test-url" , "https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js" ,
"URL used to measure proxy response time" )
flag . DurationVar ( & args . proxySpeedTimeout , "proxy-speed-timeout" , 15 * time . Second , "timeout for a single proxy speed measurement" )
flag . Int64Var ( & args . proxySpeedDLLimit , "proxy-speed-dl-limit" , 262144 , "limit of downloaded bytes for proxy speed measurement" )
2025-09-14 15:16:02 +03:00
flag . Var ( & args . serverSelection , "server-selection" , "server selection policy (first/random/fastest)" )
2026-04-30 19:04:28 +03:00
flag . DurationVar ( & args . serverSelectionTimeout , "server-selection-timeout" , DEFAULT_SERVER_SELECTION_TIMEOUT ,
"timeout given for server selection function to produce result" )
flag . StringVar ( & args . serverSelectionTestURL , "server-selection-test-url" ,
"https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js" ,
2025-09-14 15:16:02 +03:00
"URL used for download benchmark by fastest server selection policy" )
2026-04-30 19:04:28 +03:00
flag . Int64Var ( & args . serverSelectionDLLimit , "server-selection-dl-limit" , 0 ,
"restrict amount of downloaded data per connection by fastest server selection" )
2026-01-06 14:17:31 +02:00
flag . Func ( "config" , "read configuration from file with space-separated keys and values" , readConfig )
2021-03-26 22:34:43 +02:00
flag . Parse ( )
2026-04-26 15:35:01 +03:00
flag . Visit ( func ( f * flag . Flag ) {
if f . Name == "country" {
args . countryExplicit = true
}
} )
2021-03-26 22:34:43 +02:00
if args . country == "" {
arg_fail ( "Country can't be empty string." )
}
2026-04-26 15:35:01 +03:00
if args . discoverRepeat < 1 {
arg_fail ( "discover-repeat must be >= 1." )
}
if args . apiProxyParallel < 1 {
arg_fail ( "api-proxy-parallel must be >= 1." )
}
switch args . sortProxiesBy {
case "speed" , "country" , "ip" :
default :
arg_fail ( "sort-proxies-by must be one of: speed, country, ip." )
}
if args . listProxiesAllOut != "" {
args . listProxiesAll = true
}
if args . listProxiesAll {
args . estimateProxySpeed = true
}
if args . listCountries && args . listProxies ||
args . listCountries && args . listProxiesAll ||
args . listCountries && args . dpExport ||
args . listCountries && args . fetchFreeProxyOut != "" ||
args . listProxies && args . listProxiesAll ||
args . listProxies && args . dpExport ||
args . listProxies && args . fetchFreeProxyOut != "" ||
args . listProxiesAll && args . dpExport ||
args . listProxiesAll && args . fetchFreeProxyOut != "" ||
args . dpExport && args . fetchFreeProxyOut != "" {
2025-11-20 00:41:55 +02:00
arg_fail ( "mutually exclusive output arguments were provided" )
2021-03-25 20:45:48 +02:00
}
2021-03-26 22:34:43 +02:00
return args
}
2021-03-25 20:45:48 +02:00
2021-03-26 22:34:43 +02:00
func proxyFromURLWrapper ( u * url . URL , next xproxy . Dialer ) ( xproxy . Dialer , error ) {
2024-11-03 15:17:29 +02:00
cdialer , ok := next . ( dialer . ContextDialer )
2021-03-26 22:34:43 +02:00
if ! ok {
return nil , errors . New ( "only context dialers are accepted" )
2021-03-25 20:45:48 +02:00
}
2021-03-25 22:09:36 +02:00
2024-11-03 15:17:29 +02:00
return dialer . ProxyDialerFromURL ( u , cdialer )
2021-03-26 22:34:43 +02:00
}
2026-04-30 19:04:28 +03:00
// buildAPITransport returns an http.Transport tuned for infrequent API calls:
// reduced idle pool (saves goroutines/sockets), no forced HTTP/2.
func buildAPITransport (
dialCtx func ( context . Context , string , string ) ( net . Conn , error ) ,
dialTLSCtx func ( context . Context , string , string ) ( net . Conn , error ) ,
) * http . Transport {
return & http . Transport {
DialContext : dialCtx ,
DialTLSContext : dialTLSCtx ,
ForceAttemptHTTP2 : false ,
MaxIdleConns : HTTP_MAX_IDLE_CONNS ,
MaxIdleConnsPerHost : HTTP_MAX_IDLE_CONNS_PER_HOST ,
IdleConnTimeout : HTTP_IDLE_CONN_TIMEOUT ,
TLSHandshakeTimeout : 10 * time . Second ,
ExpectContinueTimeout : 1 * time . Second ,
}
}
// buildCAPool constructs the x509 cert pool used for all TLS verification.
// When -cafile is given, only that file is loaded (useful for custom/corporate CAs).
// Otherwise the bundled Mozilla NSS root store is used, which includes all
// major roots and supports AddCertWithConstraint for name-constrained CAs —
// strictly better than a plain PEM file.
func buildCAPool ( caFile string , logger * clog . CondLogger ) ( * x509 . CertPool , int ) {
pool := x509 . NewCertPool ( )
if caFile != "" {
certs , err := os . ReadFile ( caFile )
if err != nil {
logger . Error ( "Can't load CA file: %v" , err )
return nil , 15
}
if ok := pool . AppendCertsFromPEM ( certs ) ; ! ok {
logger . Error ( "Can't load certificates from CA file" )
return nil , 15
}
return pool , 0
}
for c := range bundle . Roots ( ) {
cert , err := x509 . ParseCertificate ( c . Certificate )
if err != nil {
logger . Error ( "Unable to parse bundled certificate: %v" , err )
return nil , 15
}
if c . Constraint == nil {
pool . AddCert ( cert )
} else {
pool . AddCertWithConstraint ( cert , c . Constraint )
}
}
return pool , 0
}
2026-04-26 15:35:01 +03:00
func normalizeAPIProxy ( raw string ) ( string , error ) {
raw = strings . TrimSpace ( raw )
if raw == "" {
return "" , nil
}
if strings . Contains ( raw , "://" ) {
return raw , nil
}
if strings . Contains ( raw , "@" ) {
return "http://" + raw , nil
2021-03-25 22:09:36 +02:00
}
2021-03-26 21:52:09 +02:00
2026-04-26 15:35:01 +03:00
parts := strings . Split ( raw , ":" )
if len ( parts ) == 4 {
host := strings . TrimSpace ( parts [ 0 ] )
port := strings . TrimSpace ( parts [ 1 ] )
user := strings . TrimSpace ( parts [ 2 ] )
password := strings . TrimSpace ( parts [ 3 ] )
if host == "" || port == "" || user == "" {
return "" , fmt . Errorf ( "invalid proxy entry %q" , raw )
}
proxyURL := & url . URL {
Scheme : "http" ,
Host : net . JoinHostPort ( host , port ) ,
User : url . UserPassword ( user , password ) ,
}
return proxyURL . String ( ) , nil
}
2021-03-26 22:34:43 +02:00
2026-04-26 15:35:01 +03:00
if len ( parts ) == 2 {
host := strings . TrimSpace ( parts [ 0 ] )
port := strings . TrimSpace ( parts [ 1 ] )
if host == "" || port == "" {
return "" , fmt . Errorf ( "invalid proxy entry %q" , raw )
}
return "http://" + net . JoinHostPort ( host , port ) , nil
}
2021-03-26 22:34:43 +02:00
2026-04-26 15:35:01 +03:00
return "" , fmt . Errorf ( "unsupported proxy entry format %q" , raw )
}
2021-03-27 00:43:33 +02:00
2026-04-26 15:35:01 +03:00
func loadAPIProxyListFromReader ( r io . Reader , source string ) ( [ ] string , error ) {
scanner := bufio . NewScanner ( r )
proxies := make ( [ ] string , 0 )
seen := make ( map [ string ] struct { } )
for scanner . Scan ( ) {
line := strings . TrimSpace ( scanner . Text ( ) )
if line == "" || strings . HasPrefix ( line , "#" ) {
continue
}
proxy , err := normalizeAPIProxy ( line )
if err != nil {
return nil , fmt . Errorf ( "invalid API proxy entry %q from %s: %w" , line , source , err )
}
if _ , ok := seen [ proxy ] ; ok {
continue
}
seen [ proxy ] = struct { } { }
proxies = append ( proxies , proxy )
2021-03-26 22:34:43 +02:00
}
2026-04-26 15:35:01 +03:00
if err := scanner . Err ( ) ; err != nil {
return nil , fmt . Errorf ( "unable to read API proxy list from %s: %w" , source , err )
}
if len ( proxies ) == 0 {
return nil , fmt . Errorf ( "no API proxies found in %s" , source )
}
return proxies , nil
}
2021-03-27 02:48:07 +02:00
2026-04-26 15:35:01 +03:00
func loadAPIProxyList ( filename string ) ( [ ] string , error ) {
f , err := os . Open ( filename )
if err != nil {
return nil , fmt . Errorf ( "unable to open API proxy list %q: %w" , filename , err )
}
defer f . Close ( )
return loadAPIProxyListFromReader ( f , fmt . Sprintf ( "file %q" , filename ) )
}
func loadProxyBypassList ( filename string ) ( [ ] string , error ) {
f , err := os . Open ( filename )
if err != nil {
return nil , err
}
defer f . Close ( )
scanner := bufio . NewScanner ( f )
seen := make ( map [ string ] struct { } )
patterns := make ( [ ] string , 0 )
for scanner . Scan ( ) {
line := scanner . Text ( )
if idx := strings . Index ( line , "#" ) ; idx >= 0 {
line = line [ : idx ]
2025-09-14 15:16:02 +03:00
}
2026-04-26 15:35:01 +03:00
line = strings . TrimSpace ( line )
if line == "" {
continue
2025-09-14 15:16:02 +03:00
}
2026-04-26 15:35:01 +03:00
var parsed CSVArg
if err := parsed . Set ( line ) ; err != nil {
return nil , fmt . Errorf ( "unable to parse proxy bypass entry %q from %s: %w" , line , filename , err )
}
for _ , value := range parsed . values {
pattern := strings . TrimSpace ( value )
if pattern == "" {
continue
2026-01-08 21:34:17 +02:00
}
2026-04-26 15:35:01 +03:00
if _ , ok := seen [ pattern ] ; ok {
continue
2026-01-08 21:34:17 +02:00
}
2026-04-26 15:35:01 +03:00
seen [ pattern ] = struct { } { }
patterns = append ( patterns , pattern )
2026-01-08 21:34:17 +02:00
}
2025-09-14 15:16:02 +03:00
}
2026-04-26 15:35:01 +03:00
if err := scanner . Err ( ) ; err != nil {
return nil , fmt . Errorf ( "unable to read proxy bypass list %q: %w" , filename , err )
}
return patterns , nil
}
2025-09-14 15:16:02 +03:00
2026-04-26 15:35:01 +03:00
func loadAPIProxyListFromURL ( listURL string , transport http . RoundTripper , timeout time . Duration ) ( [ ] string , error ) {
client := & http . Client {
Transport : transport ,
Timeout : timeout ,
}
req , err := http . NewRequest ( http . MethodGet , listURL , nil )
if err != nil {
return nil , fmt . Errorf ( "unable to create request for API proxy list URL %q: %w" , listURL , err )
}
req . Header . Set ( "User-Agent" , "opera-proxy api-proxy-list fetcher/1.0" )
resp , err := client . Do ( req )
if err != nil {
return nil , fmt . Errorf ( "unable to download API proxy list from %q: %w" , listURL , err )
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "unable to download API proxy list from %q: unexpected HTTP status %s" , listURL , resp . Status )
2021-03-26 22:34:43 +02:00
}
2026-04-26 15:35:01 +03:00
return loadAPIProxyListFromReader ( resp . Body , fmt . Sprintf ( "URL %q" , listURL ) )
}
2021-03-26 22:34:43 +02:00
2026-04-26 15:35:01 +03:00
func newSEClient ( args * CLIArgs , baseDialer dialer . ContextDialer , caPool * x509 . CertPool , apiProxy string ) ( * se . SEClient , error ) {
seclientDialer := baseDialer
if apiProxy != "" {
apiProxyURL , err := url . Parse ( apiProxy )
2025-10-10 15:25:12 +03:00
if err != nil {
2026-04-26 15:35:01 +03:00
return nil , fmt . Errorf ( "unable to parse API proxy URL %q: %w" , apiProxy , err )
2025-10-10 15:25:12 +03:00
}
pxDialer , err := xproxy . FromURL ( apiProxyURL , seclientDialer )
if err != nil {
2026-04-26 15:35:01 +03:00
return nil , fmt . Errorf ( "unable to instantiate API proxy dialer %q: %w" , apiProxy , err )
2025-10-10 15:25:12 +03:00
}
seclientDialer = pxDialer . ( dialer . ContextDialer )
}
2024-11-04 13:26:09 +02:00
if args . apiAddress != "" {
2025-10-10 15:25:12 +03:00
seclientDialer = dialer . NewFixedDialer ( args . apiAddress , seclientDialer )
2024-11-04 13:26:09 +02:00
} else if len ( args . bootstrapDNS . values ) > 0 {
2026-01-08 22:38:17 +02:00
resolver , err := resolver . FastFromURLs ( caPool , args . bootstrapDNS . values ... )
2024-11-04 13:26:09 +02:00
if err != nil {
2026-04-26 15:35:01 +03:00
return nil , fmt . Errorf ( "unable to instantiate DNS resolver: %w" , err )
2021-03-28 22:21:23 +03:00
}
2025-10-10 15:25:12 +03:00
seclientDialer = dialer . NewResolvingDialer ( resolver , seclientDialer )
2021-03-27 02:48:07 +02:00
}
2026-04-30 19:04:28 +03:00
// TLS config for the API connection: SNI suppressed (or faked), cert
// verification is skipped at the TLS layer because the API endpoint uses
// a self-signed cert — actual peer verification happens in VerifyConnection
// inside ProxyDialer for the proxy connections.
2021-03-27 01:48:24 +02:00
tlsConfig := & tls . Config {
2024-12-21 20:47:28 +02:00
ServerName : args . fakeSNI ,
2021-03-27 01:48:24 +02:00
InsecureSkipVerify : true ,
}
2026-04-30 19:04:28 +03:00
seclient , err := se . NewSEClient ( args . apiLogin , args . apiPassword , buildAPITransport (
seclientDialer . DialContext ,
func ( ctx context . Context , network , addr string ) ( net . Conn , error ) {
2021-03-27 02:48:07 +02:00
conn , err := seclientDialer . DialContext ( ctx , network , addr )
2021-03-27 01:48:24 +02:00
if err != nil {
return conn , err
}
return tls . Client ( conn , tlsConfig ) , nil
} ,
2026-04-30 19:04:28 +03:00
) )
2021-03-27 00:43:33 +02:00
if err != nil {
2026-04-26 15:35:01 +03:00
return nil , fmt . Errorf ( "unable to construct SEClient: %w" , err )
2021-03-26 22:34:43 +02:00
}
2024-11-05 15:54:05 +02:00
seclient . Settings . ClientType = args . apiClientType
seclient . Settings . ClientVersion = args . apiClientVersion
seclient . Settings . UserAgent = args . apiUserAgent
2026-04-26 15:35:01 +03:00
return seclient , nil
}
2021-03-27 00:43:33 +02:00
2026-04-26 15:35:01 +03:00
type apiProxyCandidateResult struct {
candidate string
client * se . SEClient
ips [ ] se . SEIPEntry
countries [ ] se . SEGeoEntry
err error
}
2024-11-05 15:54:05 +02:00
2026-04-26 15:35:01 +03:00
func callWithTimeout ( parent context . Context , timeout time . Duration , f func ( context . Context ) error ) error {
ctx , cl := context . WithTimeout ( parent , timeout )
defer cl ( )
return f ( ctx )
}
func testAPIProxyCandidate ( ctx context . Context , args * CLIArgs , baseDialer dialer . ContextDialer , caPool * x509 . CertPool , candidate string , needDiscover bool , needCountries bool ) apiProxyCandidateResult {
result := apiProxyCandidateResult { candidate : candidate }
client , err := newSEClient ( args , baseDialer , caPool , candidate )
2021-03-27 00:43:33 +02:00
if err != nil {
2026-04-26 15:35:01 +03:00
result . err = err
return result
2021-03-26 21:52:09 +02:00
}
2021-03-26 22:09:37 +02:00
2026-04-26 15:35:01 +03:00
if err := callWithTimeout ( ctx , args . timeout , client . AnonRegister ) ; err != nil {
result . err = fmt . Errorf ( "anonymous registration failed: %w" , err )
return result
2021-03-27 00:43:33 +02:00
}
2026-04-26 15:35:01 +03:00
if err := callWithTimeout ( ctx , args . timeout , client . RegisterDevice ) ; err != nil {
result . err = fmt . Errorf ( "device registration failed: %w" , err )
return result
2021-04-02 01:39:33 +03:00
}
2026-04-26 15:35:01 +03:00
if needCountries {
var countries [ ] se . SEGeoEntry
if err := callWithTimeout ( ctx , args . timeout , func ( reqCtx context . Context ) error {
var geoErr error
countries , geoErr = client . GeoList ( reqCtx )
return geoErr
} ) ; err != nil {
result . err = fmt . Errorf ( "geo list request failed: %w" , err )
return result
2025-11-20 00:41:55 +02:00
}
2026-04-26 15:35:01 +03:00
result . countries = countries
}
if needDiscover {
var ips [ ] se . SEIPEntry
if args . listProxiesAll {
ips , err = discoverAllCountriesWithContext ( ctx , args , client , nil , args . country )
} else {
ips , err = discoverCountryWithContext ( ctx , args , client , nil , args . country )
2025-11-20 00:41:55 +02:00
}
2026-04-26 15:35:01 +03:00
if err != nil {
result . err = fmt . Errorf ( "discover request failed: %w" , err )
return result
2025-11-20 00:41:55 +02:00
}
2026-04-26 15:35:01 +03:00
if len ( ips ) == 0 {
result . err = errors . New ( "discover request returned an empty endpoints list" )
return result
}
result . ips = ips
2025-11-20 00:41:55 +02:00
}
2026-04-26 15:35:01 +03:00
result . client = client
return result
}
func selectAPIProxyCandidate ( args * CLIArgs , baseDialer dialer . ContextDialer , caPool * x509 . CertPool , logger * clog . CondLogger , candidates [ ] string , needDiscover bool , needCountries bool ) ( apiProxyCandidateResult , error ) {
if len ( candidates ) == 0 {
return apiProxyCandidateResult { } , errors . New ( "no API proxy candidates provided" )
2025-09-14 15:16:02 +03:00
}
2026-04-26 15:35:01 +03:00
parallelism := args . apiProxyParallel
if parallelism > len ( candidates ) {
parallelism = len ( candidates )
}
2025-09-14 15:16:02 +03:00
2026-04-26 15:35:01 +03:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2025-09-14 22:56:19 +03:00
2026-04-26 15:35:01 +03:00
type candidateJob struct {
idx int
candidate string
}
jobs := make ( chan candidateJob )
results := make ( chan apiProxyCandidateResult , len ( candidates ) )
var wg sync . WaitGroup
worker := func ( ) {
defer wg . Done ( )
for job := range jobs {
if ctx . Err ( ) != nil {
return
2025-09-14 22:56:19 +03:00
}
2026-04-26 15:35:01 +03:00
logger . Info ( "Trying API proxy candidate #%d/%d: %s" , job . idx + 1 , len ( candidates ) , job . candidate )
result := testAPIProxyCandidate ( ctx , args , baseDialer , caPool , job . candidate , needDiscover , needCountries )
select {
case results <- result :
case <- ctx . Done ( ) :
return
2025-09-14 22:56:19 +03:00
}
2025-09-14 15:16:02 +03:00
}
2021-04-02 01:39:33 +03:00
}
2026-04-26 15:35:01 +03:00
wg . Add ( parallelism )
for i := 0 ; i < parallelism ; i ++ {
go worker ( )
}
2021-04-02 01:15:12 +03:00
2026-04-26 15:35:01 +03:00
go func ( ) {
defer close ( jobs )
for idx , candidate := range candidates {
select {
case jobs <- candidateJob { idx : idx , candidate : candidate } :
case <- ctx . Done ( ) :
return
}
2021-04-02 01:15:12 +03:00
}
2026-04-26 15:35:01 +03:00
} ( )
2021-03-30 16:29:54 +03:00
2026-04-26 15:35:01 +03:00
var lastErr error
for i := 0 ; i < len ( candidates ) ; i ++ {
result := <- results
if result . err == nil {
logger . Info ( "Using API proxy candidate: %s" , result . candidate )
cancel ( )
wg . Wait ( )
return result , nil
2025-04-11 23:20:07 +07:00
}
2026-04-26 15:35:01 +03:00
lastErr = result . err
logger . Warning ( "API proxy candidate %s failed: %v" , result . candidate , result . err )
2021-03-27 00:43:33 +02:00
}
2026-04-26 15:35:01 +03:00
cancel ( )
wg . Wait ( )
if lastErr == nil {
lastErr = errors . New ( "all API proxy candidates failed" )
2021-03-27 00:43:33 +02:00
}
2026-04-26 15:35:01 +03:00
return apiProxyCandidateResult { } , lastErr
2021-03-27 00:43:33 +02:00
}
2026-04-26 15:35:01 +03:00
func run ( ) int {
args := parse_args ( )
if args . showVersion {
fmt . Println ( version ( ) )
return 0
2021-03-27 00:43:33 +02:00
}
2026-04-26 15:35:01 +03:00
if args . fetchFreeProxyOut != "" {
count , err := fetchFreeProxyToFile ( args . fetchFreeProxyURL , args . fetchFreeProxyOut , args . timeout )
if err != nil {
fmt . Fprintf ( os . Stderr , "failed to fetch free proxy list: %v\n" , err )
return 19
}
fmt . Printf ( "Saved %d proxies to %s\n" , count , args . fetchFreeProxyOut )
return 0
}
logWriter := clog . NewLogWriter ( os . Stderr )
defer logWriter . Close ( )
mainLogger := clog . NewCondLogger ( log . New ( logWriter , "MAIN : " ,
log . LstdFlags | log . Lshortfile ) ,
args . verbosity )
proxyLogger := clog . NewCondLogger ( log . New ( logWriter , "PROXY : " ,
log . LstdFlags | log . Lshortfile ) ,
args . verbosity )
socksLogger := log . New ( logWriter , "SOCKS : " ,
log . LstdFlags | log . Lshortfile )
mainLogger . Info ( "opera-proxy client version %s is starting..." , version ( ) )
proxyBlacklist , err := loadProxyBlacklist ( args . proxyBlacklistFile )
if err != nil {
mainLogger . Error ( "Can't load proxy blacklist file %q: %v" , args . proxyBlacklistFile , err )
return 18
}
args . proxyBlacklist = proxyBlacklist
if len ( args . proxyBlacklist ) > 0 {
mainLogger . Info ( "Loaded %d blacklisted proxy endpoints from %s." , len ( args . proxyBlacklist ) , args . proxyBlacklistFile )
}
var d dialer . ContextDialer = & net . Dialer {
Timeout : 30 * time . Second ,
KeepAlive : 30 * time . Second ,
}
2026-04-30 19:04:28 +03:00
caPool , exitCode := buildCAPool ( args . caFile , mainLogger )
if exitCode != 0 {
return exitCode
2026-04-26 15:35:01 +03:00
}
xproxy . RegisterDialerType ( "http" , proxyFromURLWrapper )
xproxy . RegisterDialerType ( "https" , proxyFromURLWrapper )
directDialer := d
if args . proxy != "" {
proxyURL , err := url . Parse ( args . proxy )
if err != nil {
mainLogger . Critical ( "Unable to parse base proxy URL: %v" , err )
return 6
}
pxDialer , err := xproxy . FromURL ( proxyURL , d )
if err != nil {
mainLogger . Critical ( "Unable to instantiate base proxy dialer: %v" , err )
return 7
}
d = pxDialer . ( dialer . ContextDialer )
}
if len ( args . proxyBypass . values ) == 0 {
fallbackPatterns , err := loadProxyBypassList ( proxyBypassPathForFallback ( ) )
if err == nil {
args . proxyBypass . values = fallbackPatterns
} else if ! errors . Is ( err , os . ErrNotExist ) {
mainLogger . Critical ( "Unable to load proxy bypass list: %v" , err )
return 7
}
}
mainLogger . Info ( "Proxy bypass loaded: %d rule(s)." , len ( args . proxyBypass . values ) )
try := retryPolicy ( args . initRetries , args . initRetryInterval , mainLogger )
if args . apiAddress != "" {
mainLogger . Info ( "Using fixed API host address = %s" , args . apiAddress )
}
var (
seclient * se . SEClient
ips [ ] se . SEIPEntry
preloadedDiscovery bool
)
if args . apiProxyListURL != "" || args . apiProxyFile != "" {
var (
candidates [ ] string
err error
)
if args . apiProxyListURL != "" {
downloadTransport := & http . Transport {
DialContext : d . DialContext ,
ForceAttemptHTTP2 : true ,
MaxIdleConns : 100 ,
IdleConnTimeout : 90 * time . Second ,
TLSHandshakeTimeout : 10 * time . Second ,
ExpectContinueTimeout : 1 * time . Second ,
}
candidates , err = loadAPIProxyListFromURL ( args . apiProxyListURL , downloadTransport , args . timeout )
if err != nil {
if args . apiProxyFile == "" {
mainLogger . Critical ( "Unable to load API proxy list from URL: %v" , err )
return 8
}
mainLogger . Warning ( "Unable to load API proxy list from URL %q, falling back to file %q: %v" , args . apiProxyListURL , args . apiProxyFile , err )
}
}
if len ( candidates ) == 0 {
candidates , err = loadAPIProxyList ( args . apiProxyFile )
if err != nil {
mainLogger . Critical ( "Unable to load API proxy list: %v" , err )
return 8
}
}
if args . apiProxy != "" {
explicitProxy , err := normalizeAPIProxy ( args . apiProxy )
if err != nil {
mainLogger . Critical ( "Unable to parse explicit API proxy: %v" , err )
return 6
}
filtered := make ( [ ] string , 0 , len ( candidates ) + 1 )
filtered = append ( filtered , explicitProxy )
for _ , candidate := range candidates {
if candidate == explicitProxy {
continue
}
filtered = append ( filtered , candidate )
}
candidates = filtered
}
parallelism := args . apiProxyParallel
if parallelism > len ( candidates ) {
parallelism = len ( candidates )
}
mainLogger . Info ( "Loaded %d API proxy candidates. Testing up to %d in parallel." , len ( candidates ) , parallelism )
needDiscover := args . listProxies || args . listProxiesAll || args . dpExport || args . overrideProxyAddress == ""
result , err := selectAPIProxyCandidate ( args , d , caPool , mainLogger , candidates , needDiscover , args . listCountries )
if err != nil {
mainLogger . Critical ( "All API proxy candidates failed. Last error: %v" , err )
return 12
}
seclient = result . client
ips = result . ips
preloadedDiscovery = len ( ips ) > 0
args . apiProxy = result . candidate
if args . listCountries {
return printCountryList ( result . countries )
}
} else {
seclientDialer := d
if args . apiProxy != "" {
apiProxyURL , err := url . Parse ( args . apiProxy )
if err != nil {
2026-04-30 19:04:28 +03:00
mainLogger . Critical ( "Unable to parse api-proxy URL: %v" , err )
2026-04-26 15:35:01 +03:00
return 6
}
pxDialer , err := xproxy . FromURL ( apiProxyURL , seclientDialer )
if err != nil {
2026-04-30 19:04:28 +03:00
mainLogger . Critical ( "Unable to instantiate api-proxy dialer: %v" , err )
2026-04-26 15:35:01 +03:00
return 7
}
seclientDialer = pxDialer . ( dialer . ContextDialer )
}
if args . apiAddress != "" {
seclientDialer = dialer . NewFixedDialer ( args . apiAddress , seclientDialer )
} else if len ( args . bootstrapDNS . values ) > 0 {
2026-04-30 19:04:28 +03:00
res , err := resolver . FastFromURLs ( caPool , args . bootstrapDNS . values ... )
2026-04-26 15:35:01 +03:00
if err != nil {
mainLogger . Critical ( "Unable to instantiate DNS resolver: %v" , err )
return 4
}
2026-04-30 19:04:28 +03:00
seclientDialer = dialer . NewResolvingDialer ( res , seclientDialer )
2026-04-26 15:35:01 +03:00
}
// Dialing w/o SNI, receiving self-signed certificate, so skip verification.
// Either way we'll validate certificate of actual proxy server.
tlsConfig := & tls . Config {
ServerName : args . fakeSNI ,
InsecureSkipVerify : true ,
}
seclient , err = se . NewSEClient ( args . apiLogin , args . apiPassword , & http . Transport {
DialContext : seclientDialer . DialContext ,
DialTLSContext : func ( ctx context . Context , network , addr string ) ( net . Conn , error ) {
conn , err := seclientDialer . DialContext ( ctx , network , addr )
if err != nil {
return conn , err
}
return tls . Client ( conn , tlsConfig ) , nil
} ,
ForceAttemptHTTP2 : true ,
MaxIdleConns : 100 ,
IdleConnTimeout : 90 * time . Second ,
TLSHandshakeTimeout : 10 * time . Second ,
ExpectContinueTimeout : 1 * time . Second ,
} )
if err != nil {
mainLogger . Critical ( "Unable to construct SEClient: %v" , err )
return 8
}
seclient . Settings . ClientType = args . apiClientType
seclient . Settings . ClientVersion = args . apiClientVersion
seclient . Settings . UserAgent = args . apiUserAgent
err = try ( "anonymous registration" , func ( ) error {
ctx , cl := context . WithTimeout ( context . Background ( ) , args . timeout )
defer cl ( )
return seclient . AnonRegister ( ctx )
} )
if err != nil {
return 9
}
err = try ( "device registration" , func ( ) error {
ctx , cl := context . WithTimeout ( context . Background ( ) , args . timeout )
defer cl ( )
return seclient . RegisterDevice ( ctx )
} )
if err != nil {
return 10
}
if args . listCountries {
return printCountries ( try , mainLogger , args . timeout , seclient )
}
}
proxyTLSServerName := func ( entry se . SEIPEntry ) string {
if strings . TrimSpace ( entry . Host ) != "" {
return entry . Host
}
return fmt . Sprintf ( "%s0.%s" , strings . ToLower ( entry . Geo . CountryCode ) , PROXY_SUFFIX )
}
handlerDialerFactory := func ( entry se . SEIPEntry , endpointAddr string ) dialer . ContextDialer {
return dialer . NewProxyDialer (
dialer . WrapStringToCb ( endpointAddr ) ,
dialer . WrapStringToCb ( proxyTLSServerName ( entry ) ) ,
dialer . WrapStringToCb ( args . fakeSNI ) ,
func ( ) ( string , error ) {
return dialer . BasicAuthHeader ( seclient . GetProxyCredentials ( ) ) , nil
} ,
caPool ,
d )
}
if args . listProxies || args . listProxiesAll || args . dpExport {
if ! preloadedDiscovery {
err = try ( "discover" , func ( ) error {
var discoverErr error
if args . listProxiesAll {
ips , discoverErr = discoverAllCountries ( args , seclient , mainLogger )
} else {
ips , discoverErr = discoverCountry ( args , seclient , mainLogger , args . country )
}
if discoverErr != nil {
return discoverErr
}
if len ( ips ) == 0 {
return errors . New ( "empty endpoints list!" )
}
return nil
} )
if err != nil {
return 12
}
}
if args . listProxies || args . listProxiesAll {
var speedResults map [ proxyEndpointKey ] proxySpeedResult
if args . estimateProxySpeed {
mainLogger . Info ( "Measuring proxy response time for %d endpoints using %q." , countProxyPorts ( ips ) , args . proxySpeedTestURL )
speedResults = benchmarkProxyEndpoints ( args , ips , caPool , mainLogger , handlerDialerFactory )
}
if args . listProxiesAllOut != "" {
if err := writeProxyCSV ( args . listProxiesAllOut , ips , seclient , speedResults , args . sortProxiesBy ) ; err != nil {
mainLogger . Critical ( "Unable to write proxy CSV: %v" , err )
return 17
}
fmt . Printf ( "Proxy list saved to %s\n" , args . listProxiesAllOut )
return 0
}
return printProxies ( ips , seclient , speedResults , args . sortProxiesBy )
}
if args . dpExport {
return dpExport ( ips , seclient , args . fakeSNI )
}
}
var handlerDialer dialer . ContextDialer
if args . overrideProxyAddress == "" {
if ! preloadedDiscovery {
err = try ( "discover" , func ( ) error {
res , err := discoverCountry ( args , seclient , mainLogger , args . country )
if err != nil {
return err
}
if len ( res ) == 0 {
return errors . New ( "empty endpoints list!" )
}
ips = res
return nil
} )
if err != nil {
return 12
}
}
mainLogger . Info ( "Discovered endpoints: %v. Starting server selection routine %q." , ips , args . serverSelection . value )
err = func ( ) error {
var ss dialer . SelectionFunc
switch args . serverSelection . value {
case dialer . ServerSelectionFirst :
ss = dialer . SelectFirst
case dialer . ServerSelectionRandom :
ss = dialer . SelectRandom
case dialer . ServerSelectionFastest :
ss = dialer . NewFastestServerSelectionFunc (
args . serverSelectionTestURL ,
args . serverSelectionDLLimit ,
2026-04-30 19:04:28 +03:00
& tls . Config { RootCAs : caPool } ,
2026-04-26 15:35:01 +03:00
)
default :
panic ( "unhandled server selection value got past parsing" )
}
dialers := make ( [ ] dialer . ContextDialer , len ( ips ) )
for i , ep := range ips {
dialers [ i ] = handlerDialerFactory ( ep , ep . NetAddr ( ) )
}
ctx , cl := context . WithTimeout ( context . Background ( ) , args . serverSelectionTimeout )
defer cl ( )
handlerDialer , err = ss ( ctx , dialers )
if err != nil {
return err
}
if addresser , ok := handlerDialer . ( interface { Address ( ) ( string , error ) } ) ; ok {
if epAddr , err := addresser . Address ( ) ; err == nil {
mainLogger . Info ( "Selected endpoint address: %s" , epAddr )
}
}
return nil
} ( )
if err != nil {
return 12
}
} else {
sanitizedEndpoint := sanitizeFixedProxyAddress ( args . overrideProxyAddress )
if _ , ok := args . proxyBlacklist [ sanitizedEndpoint ] ; ok {
mainLogger . Critical ( "Endpoint override %s is blacklisted." , sanitizedEndpoint )
return 12
}
handlerDialer = handlerDialerFactory ( se . SEIPEntry {
Geo : se . SEGeoEntry {
CountryCode : args . country ,
} ,
} , sanitizedEndpoint )
mainLogger . Info ( "Endpoint override: %s" , sanitizedEndpoint )
}
if len ( args . proxyBypass . values ) > 0 {
bypassDialer , err := dialer . NewBypassDialer ( args . proxyBypass . values , directDialer , handlerDialer )
if err != nil {
mainLogger . Critical ( "Unable to configure proxy bypass rules: %v" , err )
return 7
}
handlerDialer = bypassDialer
}
clock . RunTicker ( context . Background ( ) , args . refresh , args . refreshRetry , func ( ctx context . Context ) error {
mainLogger . Info ( "Refreshing login..." )
reqCtx , cl := context . WithTimeout ( ctx , args . timeout )
defer cl ( )
2026-04-30 19:04:28 +03:00
if err := seclient . Login ( reqCtx ) ; err != nil {
2026-04-26 15:35:01 +03:00
mainLogger . Error ( "Login refresh failed: %v" , err )
return err
}
mainLogger . Info ( "Login refreshed." )
mainLogger . Info ( "Refreshing device password..." )
reqCtx , cl = context . WithTimeout ( ctx , args . timeout )
defer cl ( )
2026-04-30 19:04:28 +03:00
if err := seclient . DeviceGeneratePassword ( reqCtx ) ; err != nil {
2026-04-26 15:35:01 +03:00
mainLogger . Error ( "Device password refresh failed: %v" , err )
return err
}
mainLogger . Info ( "Device password refreshed." )
return nil
} )
mainLogger . Info ( "Starting proxy server..." )
if args . socksMode {
socks , initError := handler . NewSocksServer ( handlerDialer , socksLogger , args . fakeSNI )
if initError != nil {
2026-04-30 19:04:28 +03:00
mainLogger . Critical ( "Failed to start: %v" , initError )
2026-04-26 15:35:01 +03:00
return 16
}
mainLogger . Info ( "Init complete." )
err = socks . ListenAndServe ( "tcp" , args . bindAddress )
} else {
h := handler . NewProxyHandler ( handlerDialer , proxyLogger , args . fakeSNI )
mainLogger . Info ( "Init complete." )
err = http . ListenAndServe ( args . bindAddress , h )
}
mainLogger . Critical ( "Server terminated with a reason: %v" , err )
mainLogger . Info ( "Shutting down..." )
return 0
}
func printCountryList ( list [ ] se . SEGeoEntry ) int {
wr := csv . NewWriter ( os . Stdout )
defer wr . Flush ( )
if err := wr . Write ( [ ] string { "country code" , "country name" } ) ; err != nil {
fmt . Fprintf ( os . Stderr , "failed to write country list: %v\n" , err )
return 1
}
for _ , country := range list {
if err := wr . Write ( [ ] string { country . CountryCode , country . Country } ) ; err != nil {
fmt . Fprintf ( os . Stderr , "failed to write country list: %v\n" , err )
return 1
}
}
return 0
}
func printCountries ( try func ( string , func ( ) error ) error , logger * clog . CondLogger , timeout time . Duration , seclient * se . SEClient ) int {
var list [ ] se . SEGeoEntry
err := try ( "geolist" , func ( ) error {
ctx , cl := context . WithTimeout ( context . Background ( ) , timeout )
defer cl ( )
l , err := seclient . GeoList ( ctx )
list = l
return err
} )
if err != nil {
return 11
}
return printCountryList ( list )
}
func printProxies ( ips [ ] se . SEIPEntry , seclient * se . SEClient , speedResults map [ proxyEndpointKey ] proxySpeedResult , sortBy string ) int {
login , password := seclient . GetProxyCredentials ( )
fmt . Println ( "Proxy login:" , login )
fmt . Println ( "Proxy password:" , password )
fmt . Println ( "Proxy-Authorization:" , dialer . BasicAuthHeader ( login , password ) )
fmt . Println ( "" )
if err := emitProxyCSV ( os . Stdout , ips , speedResults , sortBy ) ; err != nil {
fmt . Fprintf ( os . Stderr , "failed to write proxy CSV: %v\n" , err )
return 1
}
return 0
}
func writeProxyCSV ( filename string , ips [ ] se . SEIPEntry , seclient * se . SEClient , speedResults map [ proxyEndpointKey ] proxySpeedResult , sortBy string ) error {
f , err := os . Create ( filename )
if err != nil {
return err
}
defer f . Close ( )
return emitProxyCSV ( f , ips , speedResults , sortBy )
}
func emitProxyCSV ( w io . Writer , ips [ ] se . SEIPEntry , speedResults map [ proxyEndpointKey ] proxySpeedResult , sortBy string ) error {
wr := csv . NewWriter ( w )
defer wr . Flush ( )
header := [ ] string { "country_code" , "country_name" , "host" , "ip_address" , "port" }
includeSpeed := speedResults != nil
if includeSpeed {
header = append ( header , "speed_ms" , "speed_status" )
}
if err := wr . Write ( header ) ; err != nil {
return err
}
rows := buildProxyRows ( ips , speedResults , sortBy )
for _ , rowData := range rows {
row := [ ] string {
rowData . CountryCode ,
rowData . CountryName ,
rowData . Host ,
rowData . IP ,
fmt . Sprintf ( "%d" , rowData . Port ) ,
}
if includeSpeed {
speedMs := ""
status := "not_tested"
if rowData . HasSpeed {
if rowData . Speed . Err == nil {
speedMs = fmt . Sprintf ( "%d" , rowData . Speed . Duration . Milliseconds ( ) )
}
status = rowData . Speed . Status ( )
}
row = append ( row , speedMs , status )
}
if err := wr . Write ( row ) ; err != nil {
return err
}
}
wr . Flush ( )
return wr . Error ( )
2021-03-27 00:43:33 +02:00
}
2025-11-20 01:33:12 +02:00
func dpExport ( ips [ ] se . SEIPEntry , seclient * se . SEClient , sni string ) int {
2025-11-20 00:41:55 +02:00
wr := csv . NewWriter ( os . Stdout )
wr . Comma = ' '
defer wr . Flush ( )
creds := url . UserPassword ( seclient . GetProxyCredentials ( ) )
var gotOne bool
for i , ip := range ips {
if len ( ip . Ports ) == 0 {
continue
}
u := url . URL {
Scheme : "https" ,
User : creds ,
2026-04-30 19:04:28 +03:00
Host : net . JoinHostPort ( ip . IP , strconv . Itoa ( int ( ip . Ports [ 0 ] ) ) ) ,
2025-11-20 00:41:55 +02:00
RawQuery : url . Values {
2025-11-20 01:33:12 +02:00
"sni" : [ ] string { sni } ,
2025-11-20 00:41:55 +02:00
"peername" : [ ] string { fmt . Sprintf ( "%s%d.%s" , strings . ToLower ( ip . Geo . CountryCode ) , i , PROXY_SUFFIX ) } ,
} . Encode ( ) ,
}
key := "proxy"
if gotOne {
key = "#proxy"
}
2026-04-30 19:04:28 +03:00
wr . Write ( [ ] string { key , u . String ( ) } )
2025-11-20 00:41:55 +02:00
gotOne = true
}
return 0
}
2025-09-14 15:16:02 +03:00
func sanitizeFixedProxyAddress ( addr string ) string {
if _ , _ , err := net . SplitHostPort ( addr ) ; err == nil {
return addr
}
return net . JoinHostPort ( addr , "443" )
}
2026-04-26 15:35:01 +03:00
func loadProxyBlacklist ( filename string ) ( map [ string ] struct { } , error ) {
if filename == "" {
return nil , nil
}
f , err := os . Open ( filename )
if err != nil {
return nil , err
}
defer f . Close ( )
blacklist := make ( map [ string ] struct { } )
scanner := bufio . NewScanner ( f )
for scanner . Scan ( ) {
line := scanner . Text ( )
if idx := strings . Index ( line , "#" ) ; idx >= 0 {
line = line [ : idx ]
}
line = strings . TrimSpace ( line )
if line == "" {
continue
}
blacklist [ sanitizeFixedProxyAddress ( line ) ] = struct { } { }
}
if err := scanner . Err ( ) ; err != nil {
return nil , err
}
return blacklist , nil
}
func filterBlacklistedProxyEntries ( entries [ ] se . SEIPEntry , blacklist map [ string ] struct { } ) ( [ ] se . SEIPEntry , [ ] string ) {
if len ( blacklist ) == 0 {
return entries , nil
}
filtered := make ( [ ] se . SEIPEntry , 0 , len ( entries ) )
blocked := make ( [ ] string , 0 )
blockedSeen := make ( map [ string ] struct { } )
appendBlocked := func ( addr string ) {
if _ , ok := blockedSeen [ addr ] ; ok {
return
}
blockedSeen [ addr ] = struct { } { }
blocked = append ( blocked , addr )
}
for _ , entry := range entries {
if len ( entry . Ports ) == 0 {
addr := net . JoinHostPort ( entry . IP , "443" )
if _ , ok := blacklist [ addr ] ; ok {
appendBlocked ( addr )
continue
}
filtered = append ( filtered , entry )
continue
}
ports := make ( [ ] uint16 , 0 , len ( entry . Ports ) )
for _ , port := range entry . Ports {
addr := net . JoinHostPort ( entry . IP , strconv . Itoa ( int ( port ) ) )
if _ , ok := blacklist [ addr ] ; ok {
appendBlocked ( addr )
continue
}
ports = append ( ports , port )
}
if len ( ports ) == 0 {
continue
}
filtered = append ( filtered , se . SEIPEntry {
Geo : entry . Geo ,
IP : entry . IP ,
Ports : ports ,
} )
}
return filtered , blocked
}
type proxyEndpointKey struct {
countryCode string
ip string
port uint16
}
type proxySpeedResult struct {
Duration time . Duration
Err error
}
type proxyListRow struct {
CountryCode string
CountryName string
Host string
IP string
Port uint16
Speed proxySpeedResult
HasSpeed bool
}
func ( r proxySpeedResult ) Status ( ) string {
if r . Err == nil {
return "ok"
}
return r . Err . Error ( )
}
func parseCountryFilters ( raw string ) ( [ ] string , bool ) {
parts := strings . Split ( raw , "," )
res := make ( [ ] string , 0 , len ( parts ) )
seen := make ( map [ string ] struct { } )
for _ , part := range parts {
country := strings . ToUpper ( strings . TrimSpace ( part ) )
if country == "" {
continue
}
if country == "ALL" || country == "*" {
return nil , true
}
if _ , ok := seen [ country ] ; ok {
continue
}
seen [ country ] = struct { } { }
res = append ( res , country )
}
return res , false
}
func proxyEndpointAddrs ( entries [ ] se . SEIPEntry ) [ ] string {
addrs := make ( [ ] string , 0 , countProxyPorts ( entries ) )
for _ , entry := range entries {
if len ( entry . Ports ) == 0 {
addrs = append ( addrs , net . JoinHostPort ( entry . IP , "443" ) )
continue
}
for _ , port := range entry . Ports {
addrs = append ( addrs , net . JoinHostPort ( entry . IP , strconv . Itoa ( int ( port ) ) ) )
}
}
return addrs
}
func discoverCountryWithContext ( parent context . Context , args * CLIArgs , seclient * se . SEClient , logger * clog . CondLogger , countryCode string ) ( [ ] se . SEIPEntry , error ) {
if args . discoverCSV != "" {
return loadProxyEntriesFromCSV ( args . discoverCSV , countryCode , false , args . countryExplicit , args . proxyBlacklist )
}
seen := make ( map [ proxyEndpointKey ] struct { } )
aggregated := make ( [ ] se . SEIPEntry , 0 )
requestedGeo := fmt . Sprintf ( "\"%s\",," , countryCode )
for attempt := 1 ; attempt <= args . discoverRepeat ; attempt ++ {
ctx , cl := context . WithTimeout ( parent , args . timeout )
res , err := seclient . Discover ( ctx , requestedGeo )
cl ( )
if err != nil {
if isSurfEasyDiscover801 ( err ) {
fallbackCSV := discoverCSVPathForFallback ( args )
if logger != nil {
logger . Warning ( "Discover API returned 801 for country %s, falling back to CSV %q." , countryCode , fallbackCSV )
}
res , csvErr := loadProxyEntriesFromCSV ( fallbackCSV , countryCode , false , args . countryExplicit , args . proxyBlacklist )
if csvErr != nil {
return nil , fmt . Errorf ( "discover API returned 801 and CSV fallback %q failed: %w" , fallbackCSV , csvErr )
}
return res , nil
}
return nil , err
}
if logger != nil {
logger . Info ( "Discover for country %s returned %d endpoints on pass #%d: %v" , countryCode , len ( res ) , attempt , proxyEndpointAddrs ( res ) )
}
res , blocked := filterBlacklistedProxyEntries ( res , args . proxyBlacklist )
if logger != nil && len ( blocked ) > 0 {
logger . Info ( "Discover for country %s skipped %d blacklisted endpoints on pass #%d: %v" , countryCode , len ( blocked ) , attempt , blocked )
}
aggregated = appendUniqueProxies ( aggregated , res , seen )
}
sortProxyEntries ( aggregated )
return aggregated , nil
}
func discoverCountry ( args * CLIArgs , seclient * se . SEClient , logger * clog . CondLogger , countryCode string ) ( [ ] se . SEIPEntry , error ) {
return discoverCountryWithContext ( context . Background ( ) , args , seclient , logger , countryCode )
}
func discoverAllCountriesWithContext ( parent context . Context , args * CLIArgs , seclient * se . SEClient , logger * clog . CondLogger , countryFilter string ) ( [ ] se . SEIPEntry , error ) {
if args . discoverCSV != "" {
return loadProxyEntriesFromCSV ( args . discoverCSV , countryFilter , true , args . countryExplicit , args . proxyBlacklist )
}
ctx , cl := context . WithTimeout ( parent , args . timeout )
countries , err := seclient . GeoList ( ctx )
cl ( )
if err != nil {
return nil , err
}
filters , allCountries := parseCountryFilters ( countryFilter )
if ! args . countryExplicit {
allCountries = true
filters = nil
}
allowed := make ( map [ string ] struct { } , len ( filters ) )
for _ , country := range filters {
allowed [ country ] = struct { } { }
}
all := make ( [ ] se . SEIPEntry , 0 )
seen := make ( map [ proxyEndpointKey ] struct { } )
for _ , country := range countries {
if ! allCountries {
if _ , ok := allowed [ strings . ToUpper ( country . CountryCode ) ] ; ! ok {
continue
}
}
res , err := discoverCountryWithContext ( parent , args , seclient , logger , country . CountryCode )
if err != nil {
return nil , fmt . Errorf ( "discover failed for country %s: %w" , country . CountryCode , err )
}
all = appendUniqueProxies ( all , res , seen )
}
if len ( all ) == 0 && ! allCountries {
return nil , fmt . Errorf ( "no countries matched filter %q" , args . country )
}
sortProxyEntries ( all )
return all , nil
}
func discoverAllCountries ( args * CLIArgs , seclient * se . SEClient , logger * clog . CondLogger ) ( [ ] se . SEIPEntry , error ) {
return discoverAllCountriesWithContext ( context . Background ( ) , args , seclient , logger , args . country )
}
func appendUniqueProxies ( dst , src [ ] se . SEIPEntry , seen map [ proxyEndpointKey ] struct { } ) [ ] se . SEIPEntry {
for _ , entry := range src {
if len ( entry . Ports ) == 0 {
key := proxyEndpointKey {
countryCode : entry . Geo . CountryCode ,
ip : entry . IP ,
port : 443 ,
}
if _ , ok := seen [ key ] ; ok {
continue
}
seen [ key ] = struct { } { }
dst = append ( dst , se . SEIPEntry {
Geo : entry . Geo ,
Host : entry . Host ,
IP : entry . IP ,
Ports : [ ] uint16 { 443 } ,
} )
continue
}
ports := make ( [ ] uint16 , 0 , len ( entry . Ports ) )
for _ , port := range entry . Ports {
key := proxyEndpointKey {
countryCode : entry . Geo . CountryCode ,
ip : entry . IP ,
port : port ,
}
if _ , ok := seen [ key ] ; ok {
continue
}
seen [ key ] = struct { } { }
ports = append ( ports , port )
}
if len ( ports ) == 0 {
continue
}
dst = append ( dst , se . SEIPEntry {
Geo : entry . Geo ,
Host : entry . Host ,
IP : entry . IP ,
Ports : ports ,
} )
}
return dst
}
func isSurfEasyDiscover801 ( err error ) bool {
if err == nil {
return false
}
return strings . Contains ( err . Error ( ) , "code=801" )
}
func discoverCSVPathForFallback ( args * CLIArgs ) string {
if args . discoverCSV != "" {
return args . discoverCSV
}
return DefaultDiscoverCSVFallback
}
func proxyBypassPathForFallback ( ) string {
return DefaultProxyBypassFallback
}
func loadProxyEntriesFromCSV ( filename string , countryFilter string , allowAll bool , countryExplicit bool , blacklist map [ string ] struct { } ) ( [ ] se . SEIPEntry , error ) {
f , err := os . Open ( filename )
if err != nil {
return nil , err
}
defer f . Close ( )
reader := csv . NewReader ( f )
reader . FieldsPerRecord = - 1
reader . TrimLeadingSpace = true
header , err := reader . Read ( )
if err != nil {
return nil , err
}
indexByName := make ( map [ string ] int , len ( header ) )
for i , name := range header {
indexByName [ strings . ToLower ( strings . TrimSpace ( name ) ) ] = i
}
requiredColumns := [ ] string { "country_code" , "ip_address" , "port" }
for _ , col := range requiredColumns {
if _ , ok := indexByName [ col ] ; ! ok {
return nil , fmt . Errorf ( "proxy CSV %q is missing required column %q" , filename , col )
}
}
filters , allCountries := parseCountryFilters ( countryFilter )
if allowAll && ! countryExplicit {
allCountries = true
filters = nil
}
allowedCountries := make ( map [ string ] struct { } , len ( filters ) )
for _ , country := range filters {
allowedCountries [ country ] = struct { } { }
}
seen := make ( map [ proxyEndpointKey ] struct { } )
entries := make ( [ ] se . SEIPEntry , 0 )
lineNo := 1
fieldValue := func ( record [ ] string , column string ) ( string , error ) {
idx := indexByName [ column ]
if idx >= len ( record ) {
return "" , fmt . Errorf ( "proxy CSV %q line %d is missing value for %s" , filename , lineNo , column )
}
return strings . TrimSpace ( record [ idx ] ) , nil
}
for {
record , err := reader . Read ( )
if err == io . EOF {
break
}
if err != nil {
return nil , fmt . Errorf ( "failed to read proxy CSV %q at line %d: %w" , filename , lineNo + 1 , err )
}
lineNo ++
countryCodeValue , err := fieldValue ( record , "country_code" )
if err != nil {
return nil , err
}
countryCode := strings . ToUpper ( countryCodeValue )
if countryCode == "" {
return nil , fmt . Errorf ( "proxy CSV %q line %d has empty country_code" , filename , lineNo )
}
if ! allCountries {
if _ , ok := allowedCountries [ countryCode ] ; ! ok {
continue
}
}
ipAddr , err := fieldValue ( record , "ip_address" )
if err != nil {
return nil , err
}
if ipAddr == "" {
return nil , fmt . Errorf ( "proxy CSV %q line %d has empty ip_address" , filename , lineNo )
}
if net . ParseIP ( ipAddr ) == nil {
return nil , fmt . Errorf ( "proxy CSV %q line %d has invalid ip_address %q" , filename , lineNo , ipAddr )
}
portValue , err := fieldValue ( record , "port" )
if err != nil {
return nil , err
}
portNum , err := strconv . ParseUint ( portValue , 10 , 16 )
if err != nil {
return nil , fmt . Errorf ( "proxy CSV %q line %d has invalid port %q: %w" , filename , lineNo , portValue , err )
}
countryName := ""
if idx , ok := indexByName [ "country_name" ] ; ok && idx < len ( record ) {
countryName = strings . TrimSpace ( record [ idx ] )
}
hostName := ""
if idx , ok := indexByName [ "host" ] ; ok && idx < len ( record ) {
hostName = strings . TrimSpace ( record [ idx ] )
}
entry := se . SEIPEntry {
Geo : se . SEGeoEntry {
CountryCode : countryCode ,
Country : countryName ,
} ,
Host : hostName ,
IP : ipAddr ,
Ports : [ ] uint16 { uint16 ( portNum ) } ,
}
entries = appendUniqueProxies ( entries , [ ] se . SEIPEntry { entry } , seen )
}
entries , _ = filterBlacklistedProxyEntries ( entries , blacklist )
if len ( entries ) == 0 {
if allowAll {
if allCountries {
return nil , fmt . Errorf ( "no proxy endpoints found in CSV %q" , filename )
}
return nil , fmt . Errorf ( "no proxy endpoints from CSV %q matched country filter %q" , filename , countryFilter )
}
return nil , fmt . Errorf ( "no proxy endpoints from CSV %q matched country %q" , filename , countryFilter )
}
sortProxyEntries ( entries )
return entries , nil
}
func sortProxyEntries ( entries [ ] se . SEIPEntry ) {
for i := range entries {
sort . Slice ( entries [ i ] . Ports , func ( a , b int ) bool {
return entries [ i ] . Ports [ a ] < entries [ i ] . Ports [ b ]
} )
}
sort . Slice ( entries , func ( i , j int ) bool {
if entries [ i ] . Geo . CountryCode != entries [ j ] . Geo . CountryCode {
return entries [ i ] . Geo . CountryCode < entries [ j ] . Geo . CountryCode
}
if entries [ i ] . IP != entries [ j ] . IP {
return entries [ i ] . IP < entries [ j ] . IP
}
leftPort , rightPort := uint16 ( 443 ) , uint16 ( 443 )
if len ( entries [ i ] . Ports ) > 0 {
leftPort = entries [ i ] . Ports [ 0 ]
}
if len ( entries [ j ] . Ports ) > 0 {
rightPort = entries [ j ] . Ports [ 0 ]
}
return leftPort < rightPort
} )
}
func countProxyPorts ( entries [ ] se . SEIPEntry ) int {
total := 0
for _ , entry := range entries {
if len ( entry . Ports ) == 0 {
total ++
continue
}
total += len ( entry . Ports )
}
return total
}
func buildProxyRows ( ips [ ] se . SEIPEntry , speedResults map [ proxyEndpointKey ] proxySpeedResult , sortBy string ) [ ] proxyListRow {
rows := make ( [ ] proxyListRow , 0 , countProxyPorts ( ips ) )
for i , ip := range ips {
ports := ip . Ports
if len ( ports ) == 0 {
ports = [ ] uint16 { 443 }
}
for _ , port := range ports {
row := proxyListRow {
CountryCode : ip . Geo . CountryCode ,
CountryName : ip . Geo . Country ,
Host : ip . Host ,
IP : ip . IP ,
Port : port ,
}
if row . Host == "" {
row . Host = fmt . Sprintf ( "%s%d.%s" , strings . ToLower ( ip . Geo . CountryCode ) , i , PROXY_SUFFIX )
}
if speedResults != nil {
result , ok := speedResults [ proxyEndpointKey {
countryCode : ip . Geo . CountryCode ,
ip : ip . IP ,
port : port ,
} ]
row . HasSpeed = ok
if ok {
row . Speed = result
}
}
rows = append ( rows , row )
}
}
sortProxyRows ( rows , sortBy )
return rows
}
func sortProxyRows ( rows [ ] proxyListRow , sortBy string ) {
sort . SliceStable ( rows , func ( i , j int ) bool {
left , right := rows [ i ] , rows [ j ]
switch sortBy {
case "country" :
if left . CountryCode != right . CountryCode {
return left . CountryCode < right . CountryCode
}
if left . CountryName != right . CountryName {
return left . CountryName < right . CountryName
}
if left . IP != right . IP {
return left . IP < right . IP
}
return left . Port < right . Port
case "ip" :
if left . IP != right . IP {
return left . IP < right . IP
}
if left . Port != right . Port {
return left . Port < right . Port
}
return left . CountryCode < right . CountryCode
default :
leftOK := left . HasSpeed && left . Speed . Err == nil
rightOK := right . HasSpeed && right . Speed . Err == nil
if leftOK != rightOK {
return leftOK
}
if leftOK && rightOK && left . Speed . Duration != right . Speed . Duration {
return left . Speed . Duration < right . Speed . Duration
}
if left . CountryCode != right . CountryCode {
return left . CountryCode < right . CountryCode
}
if left . IP != right . IP {
return left . IP < right . IP
}
return left . Port < right . Port
}
} )
}
func benchmarkProxyEndpoints ( args * CLIArgs , ips [ ] se . SEIPEntry , caPool * x509 . CertPool , logger * clog . CondLogger , dialerFactory func ( se . SEIPEntry , string ) dialer . ContextDialer ) map [ proxyEndpointKey ] proxySpeedResult {
results := make ( map [ proxyEndpointKey ] proxySpeedResult )
var mu sync . Mutex
var wg sync . WaitGroup
sem := make ( chan struct { } , 8 )
for _ , entry := range ips {
ports := entry . Ports
if len ( ports ) == 0 {
ports = [ ] uint16 { 443 }
}
for _ , port := range ports {
key := proxyEndpointKey {
countryCode : entry . Geo . CountryCode ,
ip : entry . IP ,
port : port ,
}
endpoint := net . JoinHostPort ( entry . IP , strconv . Itoa ( int ( port ) ) )
wg . Add ( 1 )
go func ( entry se . SEIPEntry , key proxyEndpointKey , countryCode , endpoint string ) {
defer wg . Done ( )
sem <- struct { } { }
defer func ( ) { <- sem } ( )
start := time . Now ( )
ctx , cl := context . WithTimeout ( context . Background ( ) , args . proxySpeedTimeout )
err := probeProxyEndpoint ( ctx , dialerFactory ( entry , endpoint ) , args . proxySpeedTestURL , args . proxySpeedDLLimit , & tls . Config {
RootCAs : caPool ,
} )
cl ( )
result := proxySpeedResult {
Duration : time . Since ( start ) ,
Err : err ,
}
mu . Lock ( )
results [ key ] = result
mu . Unlock ( )
if err == nil {
logger . Info ( "Speed probe for %s via %s completed in %d ms." , countryCode , endpoint , result . Duration . Milliseconds ( ) )
} else {
logger . Warning ( "Speed probe for %s via %s failed: %v" , countryCode , endpoint , err )
}
} ( entry , key , entry . Geo . CountryCode , endpoint )
}
}
wg . Wait ( )
return results
}
func probeProxyEndpoint ( ctx context . Context , upstream dialer . ContextDialer , targetURL string , dlLimit int64 , tlsClientConfig * tls . Config ) error {
httpClient := http . Client {
Transport : & http . Transport {
MaxIdleConns : 100 ,
IdleConnTimeout : 90 * time . Second ,
TLSHandshakeTimeout : 10 * time . Second ,
ExpectContinueTimeout : 1 * time . Second ,
DialContext : upstream . DialContext ,
TLSClientConfig : tlsClientConfig ,
ForceAttemptHTTP2 : true ,
} ,
}
req , err := http . NewRequestWithContext ( ctx , http . MethodGet , targetURL , nil )
if err != nil {
return err
}
resp , err := httpClient . Do ( req )
if err != nil {
return err
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
return fmt . Errorf ( "http_%d" , resp . StatusCode )
}
var reader io . Reader = resp . Body
if dlLimit > 0 {
reader = io . LimitReader ( reader , dlLimit )
}
_ , err = io . Copy ( io . Discard , reader )
return err
}
2021-03-26 22:34:43 +02:00
func main ( ) {
os . Exit ( run ( ) )
2021-03-25 20:45:48 +02:00
}
2024-11-05 15:54:05 +02:00
func retryPolicy ( retries int , retryInterval time . Duration , logger * clog . CondLogger ) func ( string , func ( ) error ) error {
return func ( name string , f func ( ) error ) error {
var err error
2025-01-27 14:59:22 +02:00
for i := 1 ; retries <= 0 || i <= retries ; i ++ {
2024-11-05 15:54:05 +02:00
if i > 1 {
logger . Warning ( "Retrying action %q in %v..." , name , retryInterval )
time . Sleep ( retryInterval )
}
logger . Info ( "Attempting action %q, attempt #%d..." , name , i )
err = f ( )
if err == nil {
logger . Info ( "Action %q succeeded on attempt #%d" , name , i )
return nil
}
logger . Warning ( "Action %q failed: %v" , name , err )
}
logger . Critical ( "All attempts for action %q have failed. Last error: %v" , name , err )
return err
}
}
2025-11-20 01:43:07 +02:00
2026-01-06 14:17:31 +02:00
func readConfig ( filename string ) error {
f , err := os . Open ( filename )
if err != nil {
return fmt . Errorf ( "unable to open config file %q: %w" , filename , err )
}
defer f . Close ( )
r := csv . NewReader ( f )
r . Comma = ' '
r . Comment = '#'
r . FieldsPerRecord = - 1
r . TrimLeadingSpace = true
r . ReuseRecord = true
for {
record , err := r . Read ( )
if err == io . EOF {
break
}
if err != nil {
return fmt . Errorf ( "configuration file parsing failed: %w" , err )
}
switch len ( record ) {
case 0 :
continue
case 1 :
if err := flag . Set ( record [ 0 ] , "true" ) ; err != nil {
line , _ := r . FieldPos ( 0 )
return fmt . Errorf ( "error parsing config file %q at line %d (%#v): %w" , filename , line , record , err )
}
case 2 :
if err := flag . Set ( record [ 0 ] , record [ 1 ] ) ; err != nil {
line , _ := r . FieldPos ( 0 )
return fmt . Errorf ( "error parsing config file %q at line %d (%#v): %w" , filename , line , record , err )
}
default :
unified := strings . Join ( record [ 1 : ] , " " )
if err := flag . Set ( record [ 0 ] , unified ) ; err != nil {
line , _ := r . FieldPos ( 0 )
return fmt . Errorf ( "error parsing config file %q at line %d (%#v): %w" , filename , line , record , err )
}
}
}
return nil
}
2025-11-20 01:43:07 +02:00
func version ( ) string {
bi , ok := debug . ReadBuildInfo ( )
if ! ok {
return "unknown"
}
return bi . Main . Version
}