Files
opera-proxy/main.go
T

652 lines
20 KiB
Go
Raw Normal View History

2021-03-25 20:45:48 +02:00
package main
import (
2024-08-01 12:01:24 +03:00
"bytes"
2021-03-27 00:43:33 +02:00
"context"
"crypto/tls"
"crypto/x509"
"encoding/csv"
2021-03-26 22:34:43 +02:00
"errors"
"flag"
"fmt"
2024-08-01 12:01:24 +03:00
"io"
"io/ioutil"
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"
2025-11-20 00:41:55 +02:00
"strconv"
"strings"
"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
2024-11-03 15:17:29 +02:00
"github.com/Snawoot/opera-proxy/clock"
"github.com/Snawoot/opera-proxy/dialer"
"github.com/Snawoot/opera-proxy/handler"
clog "github.com/Snawoot/opera-proxy/log"
2025-08-12 19:38:17 +03:00
"github.com/Snawoot/opera-proxy/resolver"
2021-03-27 00:43:33 +02:00
se "github.com/Snawoot/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 (
2024-10-05 19:01:39 +03:00
API_DOMAIN = "api2.sec-tunnel.com"
2021-03-28 22:21:23 +03:00
PROXY_SUFFIX = "sec-tunnel.com"
)
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
}
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 {
country string
listCountries bool
listProxies bool
2025-11-20 00:41:55 +02:00
dpExport bool
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
bootstrapDNS *CSVArg
refresh time.Duration
refreshRetry time.Duration
initRetries int
initRetryInterval time.Duration
caFile string
fakeSNI string
overrideProxyAddress string
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/",
},
},
serverSelection: serverSelectionArg{dialer.ServerSelectionFastest},
2024-08-01 12:01:24 +03:00
}
2021-03-26 22:40:27 +02:00
flag.StringVar(&args.country, "country", "EU", "desired proxy location")
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")
2025-11-20 00:41:55 +02:00
flag.BoolVar(&args.dpExport, "dp-export", false, "export configuration for dumbproxy")
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)")
flag.DurationVar(&args.timeout, "timeout", 10*time.Second, "timeout for network operations")
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")
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")
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")
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")
flag.DurationVar(&args.initRetryInterval, "init-retry-interval", 5*time.Second, "delay between initialization retries")
flag.StringVar(&args.caFile, "cafile", "", "use custom CA certificate bundle file")
2024-12-21 20:47:28 +02:00
flag.StringVar(&args.fakeSNI, "fake-SNI", "", "domain name to use as SNI in communications with servers")
flag.StringVar(&args.overrideProxyAddress, "override-proxy-address", "", "use fixed proxy address instead of server address returned by SurfEasy API")
flag.Var(&args.serverSelection, "server-selection", "server selection policy (first/random/fastest)")
flag.DurationVar(&args.serverSelectionTimeout, "server-selection-timeout", 30*time.Second, "timeout given for server selection function to produce result")
flag.StringVar(&args.serverSelectionTestURL, "server-selection-test-url", "https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js",
"URL used for download benchmark by fastest server selection policy")
flag.Int64Var(&args.serverSelectionDLLimit, "server-selection-dl-limit", 0, "restrict amount of downloaded data per connection by fastest server selection")
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()
if args.country == "" {
arg_fail("Country can't be empty string.")
}
2025-11-20 00:41:55 +02:00
if args.listCountries && args.listProxies || args.listCountries && args.dpExport || args.listProxies && args.dpExport {
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
}
func run() int {
args := parse_args()
if args.showVersion {
2025-11-20 01:43:07 +02:00
fmt.Println(version())
2021-03-26 22:34:43 +02:00
return 0
2021-03-25 22:09:36 +02:00
}
2021-03-26 21:52:09 +02:00
2024-11-03 15:17:29 +02:00
logWriter := clog.NewLogWriter(os.Stderr)
2021-03-26 22:34:43 +02:00
defer logWriter.Close()
2024-11-03 15:17:29 +02:00
mainLogger := clog.NewCondLogger(log.New(logWriter, "MAIN : ",
2021-03-26 22:34:43 +02:00
log.LstdFlags|log.Lshortfile),
args.verbosity)
2024-11-03 15:17:29 +02:00
proxyLogger := clog.NewCondLogger(log.New(logWriter, "PROXY : ",
2021-03-26 22:34:43 +02:00
log.LstdFlags|log.Lshortfile),
args.verbosity)
2025-04-11 23:20:07 +07:00
socksLogger := log.New(logWriter, "SOCKS : ",
log.LstdFlags|log.Lshortfile)
2021-03-26 22:34:43 +02:00
2025-11-20 04:33:12 +02:00
mainLogger.Info("opera-proxy client version %s is starting...", version())
2021-03-27 00:43:33 +02:00
2024-11-03 15:17:29 +02:00
var d dialer.ContextDialer = &net.Dialer{
2021-03-26 22:34:43 +02:00
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
2026-01-08 21:34:17 +02:00
caPool := x509.NewCertPool()
if args.caFile != "" {
certs, err := ioutil.ReadFile(args.caFile)
if err != nil {
mainLogger.Error("Can't load CA file: %v", err)
return 15
}
if ok := caPool.AppendCertsFromPEM(certs); !ok {
mainLogger.Error("Can't load certificates from CA file")
return 15
}
2026-01-08 21:34:17 +02:00
} else {
for c := range bundle.Roots() {
cert, err := x509.ParseCertificate(c.Certificate)
if err != nil {
mainLogger.Error("Unable to parse bundled certificate: %v", err)
return 15
}
if c.Constraint == nil {
caPool.AddCert(cert)
} else {
caPool.AddCertWithConstraint(cert, c.Constraint)
}
}
}
2025-10-10 15:25:12 +03:00
xproxy.RegisterDialerType("http", proxyFromURLWrapper)
xproxy.RegisterDialerType("https", proxyFromURLWrapper)
2021-03-26 22:34:43 +02:00
if args.proxy != "" {
proxyURL, err := url.Parse(args.proxy)
if err != nil {
mainLogger.Critical("Unable to parse base proxy URL: %v", err)
return 6
}
2024-11-03 15:17:29 +02:00
pxDialer, err := xproxy.FromURL(proxyURL, d)
2021-03-26 22:34:43 +02:00
if err != nil {
mainLogger.Critical("Unable to instantiate base proxy dialer: %v", err)
return 7
}
2024-11-03 15:17:29 +02:00
d = pxDialer.(dialer.ContextDialer)
2021-03-26 22:34:43 +02:00
}
2024-11-03 15:17:29 +02:00
seclientDialer := d
2025-10-10 15:25:12 +03:00
if args.apiProxy != "" {
apiProxyURL, err := url.Parse(args.apiProxy)
if err != nil {
mainLogger.Critical("Unable to parse base proxy URL: %v", err)
return 6
}
pxDialer, err := xproxy.FromURL(apiProxyURL, seclientDialer)
if err != nil {
mainLogger.Critical("Unable to instantiate base proxy dialer: %v", err)
return 7
}
seclientDialer = pxDialer.(dialer.ContextDialer)
}
2024-11-04 13:26:09 +02:00
if args.apiAddress != "" {
mainLogger.Info("Using fixed API host address = %s", 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 {
mainLogger.Critical("Unable to instantiate DNS resolver: %v", err)
return 4
2021-03-28 22:21:23 +03:00
}
2025-10-10 15:25:12 +03:00
seclientDialer = dialer.NewResolvingDialer(resolver, seclientDialer)
}
2021-03-27 01:48:24 +02: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{
2024-12-21 20:47:28 +02:00
ServerName: args.fakeSNI,
2021-03-27 01:48:24 +02:00
InsecureSkipVerify: true,
}
2021-03-27 00:43:33 +02:00
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)
2021-03-27 01:48:24 +02:00
if err != nil {
return conn, err
}
return tls.Client(conn, tlsConfig), nil
},
2021-03-27 00:43:33 +02:00
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
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
2021-03-27 00:43:33 +02:00
2024-11-05 15:54:05 +02:00
try := retryPolicy(args.initRetries, args.initRetryInterval, mainLogger)
err = try("anonymous registration", func() error {
ctx, cl := context.WithTimeout(context.Background(), args.timeout)
defer cl()
return seclient.AnonRegister(ctx)
})
2021-03-27 00:43:33 +02:00
if err != nil {
return 9
2021-03-26 21:52:09 +02:00
}
2021-03-26 22:09:37 +02:00
2024-11-05 15:54:05 +02:00
err = try("device registration", func() error {
ctx, cl := context.WithTimeout(context.Background(), args.timeout)
defer cl()
return seclient.RegisterDevice(ctx)
})
2021-03-26 22:09:37 +02:00
if err != nil {
2021-03-27 00:43:33 +02:00
return 10
}
2021-04-02 01:39:33 +03:00
if args.listCountries {
2024-11-05 15:54:05 +02:00
return printCountries(try, mainLogger, args.timeout, seclient)
2021-04-02 01:39:33 +03:00
}
2025-11-20 00:41:55 +02:00
var ips []se.SEIPEntry
if args.listProxies || args.dpExport {
err = try("discover", func() error {
ctx, cl := context.WithTimeout(context.Background(), args.timeout)
defer cl()
ips, err = seclient.Discover(ctx, fmt.Sprintf("\"%s\",,", args.country))
if err != nil {
return err
}
if len(ips) == 0 {
return errors.New("empty endpoints list!")
}
return nil
})
if err != nil {
return 12
}
if args.listProxies {
return printProxies(ips, seclient)
}
if args.dpExport {
return dpExport(ips, seclient, args.fakeSNI)
2025-11-20 00:41:55 +02:00
}
}
handlerDialerFactory := func(endpointAddr string) dialer.ContextDialer {
return dialer.NewProxyDialer(
dialer.WrapStringToCb(endpointAddr),
dialer.WrapStringToCb(fmt.Sprintf("%s0.%s", args.country, PROXY_SUFFIX)),
dialer.WrapStringToCb(args.fakeSNI),
func() (string, error) {
return dialer.BasicAuthHeader(seclient.GetProxyCredentials()), nil
},
caPool,
d)
}
var handlerDialer dialer.ContextDialer
2025-11-20 00:41:55 +02:00
if args.overrideProxyAddress == "" {
err = try("discover", func() error {
ctx, cl := context.WithTimeout(context.Background(), args.timeout)
defer cl()
res, err := seclient.Discover(ctx, fmt.Sprintf("\"%s\",,", args.country))
if err != nil {
return err
}
if len(res) == 0 {
return errors.New("empty endpoints list!")
}
2025-09-14 22:56:19 +03:00
mainLogger.Info("Discovered endpoints: %v. Starting server selection routine %q.", res, args.serverSelection.value)
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,
2025-09-20 18:11:00 +03:00
&tls.Config{
RootCAs: caPool,
},
)
default:
panic("unhandled server selection value got past parsing")
}
dialers := make([]dialer.ContextDialer, len(res))
for i, ep := range res {
dialers[i] = handlerDialerFactory(ep.NetAddr())
}
ctx, cl = context.WithTimeout(context.Background(), args.serverSelectionTimeout)
defer cl()
handlerDialer, err = ss(ctx, dialers)
2025-09-14 22:56:19 +03:00
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)
handlerDialer = handlerDialerFactory(sanitizedEndpoint)
mainLogger.Info("Endpoint override: %s", sanitizedEndpoint)
2021-04-02 01:39:33 +03:00
}
2024-11-03 15:17:29 +02:00
clock.RunTicker(context.Background(), args.refresh, args.refreshRetry, func(ctx context.Context) error {
mainLogger.Info("Refreshing login...")
2021-04-02 01:15:12 +03:00
reqCtx, cl := context.WithTimeout(ctx, args.timeout)
defer cl()
2021-04-02 01:15:12 +03:00
err := seclient.Login(reqCtx)
if err != nil {
2021-04-02 02:03:16 +03:00
mainLogger.Error("Login refresh failed: %v", err)
2021-04-23 13:01:53 +03:00
return err
}
mainLogger.Info("Login refreshed.")
2021-04-02 01:15:12 +03:00
mainLogger.Info("Refreshing device password...")
reqCtx, cl = context.WithTimeout(ctx, args.timeout)
defer cl()
err = seclient.DeviceGeneratePassword(reqCtx)
if err != nil {
2021-04-02 02:03:16 +03:00
mainLogger.Error("Device password refresh failed: %v", err)
2021-04-23 13:01:53 +03:00
return err
2021-04-02 01:15:12 +03:00
}
mainLogger.Info("Device password refreshed.")
2021-04-23 13:01:53 +03:00
return nil
})
2021-03-26 22:34:43 +02:00
mainLogger.Info("Starting proxy server...")
2025-04-11 23:20:07 +07:00
if args.socksMode {
socks, initError := handler.NewSocksServer(handlerDialer, socksLogger)
if initError != nil {
mainLogger.Critical("Failed to start: %v", err)
return 16
}
mainLogger.Info("Init complete.")
err = socks.ListenAndServe("tcp", args.bindAddress)
} else {
h := handler.NewProxyHandler(handlerDialer, proxyLogger)
mainLogger.Info("Init complete.")
err = http.ListenAndServe(args.bindAddress, h)
}
2021-03-26 22:34:43 +02:00
mainLogger.Critical("Server terminated with a reason: %v", err)
mainLogger.Info("Shutting down...")
return 0
}
2024-11-05 15:54:05 +02:00
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
})
2021-03-27 00:43:33 +02:00
if err != nil {
return 11
}
wr := csv.NewWriter(os.Stdout)
defer wr.Flush()
wr.Write([]string{"country code", "country name"})
for _, country := range list {
wr.Write([]string{country.CountryCode, country.Country})
}
return 0
}
func printProxies(ips []se.SEIPEntry, seclient *se.SEClient) int {
2021-03-27 00:43:33 +02:00
wr := csv.NewWriter(os.Stdout)
defer wr.Flush()
login, password := seclient.GetProxyCredentials()
fmt.Println("Proxy login:", login)
fmt.Println("Proxy password:", password)
2024-11-03 15:17:29 +02:00
fmt.Println("Proxy-Authorization:", dialer.BasicAuthHeader(login, password))
2021-03-27 00:43:33 +02:00
fmt.Println("")
wr.Write([]string{"host", "ip_address", "port"})
for i, ip := range ips {
for _, port := range ip.Ports {
wr.Write([]string{
2021-03-28 22:21:23 +03:00
fmt.Sprintf("%s%d.%s", strings.ToLower(ip.Geo.CountryCode), i, PROXY_SUFFIX),
ip.IP,
fmt.Sprintf("%d", port),
})
2021-03-27 00:43:33 +02:00
}
}
return 0
}
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,
Host: net.JoinHostPort(
ip.IP,
strconv.Itoa(int(ip.Ports[0])),
),
RawQuery: url.Values{
"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"
}
wr.Write([]string{
key,
u.String(),
})
gotOne = true
}
return 0
}
func sanitizeFixedProxyAddress(addr string) string {
if _, _, err := net.SplitHostPort(addr); err == nil {
return addr
}
return net.JoinHostPort(addr, "443")
}
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
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
}