mirror of
https://github.com/Alexey71/opera-proxy.git
synced 2026-05-15 07:01:00 +00:00
feat: try add bypass
This commit is contained in:
@@ -0,0 +1,339 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BypassDialer routes connections based on the destination address:
|
||||||
|
// - addresses matching any bypass pattern go directly (via directDialer)
|
||||||
|
// - all other addresses go through the proxy (via proxyDialer)
|
||||||
|
//
|
||||||
|
// Supported pattern formats (case-insensitive):
|
||||||
|
//
|
||||||
|
// 192.168.1.1 — exact IP (any port)
|
||||||
|
// 192.168.0.0/16 — CIDR block (any port)
|
||||||
|
// example.com — exact hostname (any port)
|
||||||
|
// *.example.com — wildcard: any subdomain
|
||||||
|
// localhost:8080 — hostname + specific port
|
||||||
|
// 192.168.1.1:443 — IP + specific port
|
||||||
|
// 192.168.0.0/24:443 — CIDR + specific port
|
||||||
|
//
|
||||||
|
// SOCKS5 + DNS note:
|
||||||
|
// When used in SOCKS5 mode, hostname patterns only match if the SOCKS client
|
||||||
|
// sends the hostname directly (ATYP=0x03 / SOCKS5h mode). Most clients resolve
|
||||||
|
// DNS themselves and send an IP address (ATYP=0x01) — in that case only CIDR
|
||||||
|
// rules match. Call ResolvePatterns() once after construction to pre-resolve
|
||||||
|
// hostnames into CIDRs so IP-based bypass works correctly.
|
||||||
|
type BypassDialer struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
patterns []bypassPattern
|
||||||
|
directDialer ContextDialer
|
||||||
|
proxyDialer ContextDialer
|
||||||
|
logger bypassLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
// bypassLogger matches the signature of *clog.CondLogger so it can be passed
|
||||||
|
// directly without an adapter.
|
||||||
|
type bypassLogger interface {
|
||||||
|
Info(fmt string, args ...interface{}) error
|
||||||
|
Debug(fmt string, args ...interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// noopLogger silently discards all log messages.
|
||||||
|
type noopLogger struct{}
|
||||||
|
|
||||||
|
func (noopLogger) Info(string, ...interface{}) error { return nil }
|
||||||
|
func (noopLogger) Debug(string, ...interface{}) error { return nil }
|
||||||
|
|
||||||
|
type bypassPattern struct {
|
||||||
|
raw string
|
||||||
|
port string // "" = any port
|
||||||
|
hostKind patternKind
|
||||||
|
cidr *net.IPNet // kindCIDR
|
||||||
|
host string // kindExact | kindWildcard
|
||||||
|
}
|
||||||
|
|
||||||
|
type patternKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
kindExact patternKind = iota
|
||||||
|
kindWildcard // *.suffix
|
||||||
|
kindCIDR
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewBypassDialer compiles patterns and returns a BypassDialer.
|
||||||
|
// logger may be nil (logging disabled).
|
||||||
|
func NewBypassDialer(patterns []string, directDialer, proxyDialer ContextDialer, logger bypassLogger) (*BypassDialer, error) {
|
||||||
|
if logger == nil {
|
||||||
|
logger = noopLogger{}
|
||||||
|
}
|
||||||
|
compiled := make([]bypassPattern, 0, len(patterns))
|
||||||
|
for _, raw := range patterns {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, err := parseBypassPattern(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("bypass pattern %q: %w", raw, err)
|
||||||
|
}
|
||||||
|
compiled = append(compiled, p)
|
||||||
|
}
|
||||||
|
return &BypassDialer{
|
||||||
|
patterns: compiled,
|
||||||
|
directDialer: directDialer,
|
||||||
|
proxyDialer: proxyDialer,
|
||||||
|
logger: logger,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolvePatterns pre-resolves hostname and wildcard patterns into their IP
|
||||||
|
// addresses and adds them as CIDR /32 (or /128) rules. This is needed when
|
||||||
|
// the SOCKS5 client resolves DNS itself and sends IP addresses.
|
||||||
|
//
|
||||||
|
// resolver is the DNS function to use. Pass nil to use net.DefaultResolver.
|
||||||
|
// On Android with CGO disabled, net.DefaultResolver may fail because Go's
|
||||||
|
// pure-Go resolver reads /etc/resolv.conf which may not exist. In that case
|
||||||
|
// pass a custom resolver backed by the bootstrap DNS already configured in
|
||||||
|
// the proxy (see buildCustomResolver in main.go).
|
||||||
|
//
|
||||||
|
// Wildcard TLD patterns (e.g. *.ru) are skipped — resolving a TLD returns
|
||||||
|
// nothing useful. List specific domains instead (e.g. yandex.ru, *.yandex.ru).
|
||||||
|
//
|
||||||
|
// The resolved IPs supplement the original patterns; hostname matching
|
||||||
|
// continues to work for clients that send hostnames.
|
||||||
|
func (bd *BypassDialer) ResolvePatterns(ctx context.Context, resolver *net.Resolver) {
|
||||||
|
if resolver == nil {
|
||||||
|
resolver = net.DefaultResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot current patterns so we don't iterate over appended results.
|
||||||
|
bd.mu.RLock()
|
||||||
|
snapshot := make([]bypassPattern, len(bd.patterns))
|
||||||
|
copy(snapshot, bd.patterns)
|
||||||
|
bd.mu.RUnlock()
|
||||||
|
|
||||||
|
var newPatterns []bypassPattern
|
||||||
|
for _, p := range snapshot {
|
||||||
|
if p.hostKind != kindExact && p.hostKind != kindWildcard {
|
||||||
|
continue // CIDR patterns need no resolution
|
||||||
|
}
|
||||||
|
if net.ParseIP(p.host) != nil {
|
||||||
|
continue // already an IP literal
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname := p.host
|
||||||
|
if p.hostKind == kindWildcard {
|
||||||
|
// p.host = ".example.com" — strip leading dot to get the apex
|
||||||
|
hostname = strings.TrimPrefix(p.host, ".")
|
||||||
|
|
||||||
|
// Skip TLD-only wildcards like "*.ru" — resolving "ru" yields
|
||||||
|
// nothing meaningful and pollutes the pattern list.
|
||||||
|
if !strings.Contains(hostname, ".") {
|
||||||
|
bd.logger.Debug("bypass: skipping TLD wildcard %q (use exact domains instead)", p.raw)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ips, err := resolver.LookupIPAddr(ctx, hostname)
|
||||||
|
if err != nil {
|
||||||
|
bd.logger.Info("bypass: resolve %q failed: %v (hostname-only matching)", hostname, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, ia := range ips {
|
||||||
|
bits := 32
|
||||||
|
if ia.IP.To4() == nil {
|
||||||
|
bits = 128
|
||||||
|
}
|
||||||
|
_, cidr, err := net.ParseCIDR(fmt.Sprintf("%s/%d", ia.IP.String(), bits))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newPatterns = append(newPatterns, bypassPattern{
|
||||||
|
raw: fmt.Sprintf("resolved:%s→%s", hostname, ia.IP),
|
||||||
|
port: p.port,
|
||||||
|
hostKind: kindCIDR,
|
||||||
|
cidr: cidr,
|
||||||
|
})
|
||||||
|
bd.logger.Debug("bypass: resolved %q → %s", hostname, ia.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newPatterns) > 0 {
|
||||||
|
bd.mu.Lock()
|
||||||
|
bd.patterns = append(bd.patterns, newPatterns...)
|
||||||
|
bd.mu.Unlock()
|
||||||
|
bd.logger.Info("bypass: pre-resolved %d IP(s) from hostname patterns (total rules: %d)",
|
||||||
|
len(newPatterns), len(bd.patterns))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBypassPattern(raw string) (bypassPattern, error) {
|
||||||
|
if raw == "" {
|
||||||
|
return bypassPattern{}, fmt.Errorf("empty pattern")
|
||||||
|
}
|
||||||
|
|
||||||
|
p := bypassPattern{raw: raw}
|
||||||
|
hostPart := raw
|
||||||
|
|
||||||
|
if strings.HasPrefix(raw, "[") {
|
||||||
|
// IPv6 bracket notation: [::1]:443
|
||||||
|
h, port, err := net.SplitHostPort(raw)
|
||||||
|
if err != nil {
|
||||||
|
return p, fmt.Errorf("invalid bracketed address: %w", err)
|
||||||
|
}
|
||||||
|
p.port = port
|
||||||
|
p.hostKind = kindExact
|
||||||
|
p.host = strings.ToLower(h)
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
colonCount := strings.Count(raw, ":")
|
||||||
|
if colonCount > 1 {
|
||||||
|
// Bare IPv6, no port
|
||||||
|
hostPart = raw
|
||||||
|
} else if colonCount == 1 {
|
||||||
|
lastColon := strings.LastIndex(raw, ":")
|
||||||
|
possiblePort := raw[lastColon+1:]
|
||||||
|
if isValidPort(possiblePort) {
|
||||||
|
hostPart = raw[:lastColon]
|
||||||
|
p.port = possiblePort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPart = strings.ToLower(hostPart)
|
||||||
|
|
||||||
|
// CIDR?
|
||||||
|
if strings.Contains(hostPart, "/") {
|
||||||
|
_, cidr, err := net.ParseCIDR(hostPart)
|
||||||
|
if err != nil {
|
||||||
|
return p, fmt.Errorf("invalid CIDR %q: %w", hostPart, err)
|
||||||
|
}
|
||||||
|
p.hostKind = kindCIDR
|
||||||
|
p.cidr = cidr
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard hostname?
|
||||||
|
if strings.HasPrefix(hostPart, "*.") {
|
||||||
|
suffix := hostPart[1:] // ".example.com"
|
||||||
|
if strings.ContainsAny(suffix, "*?") {
|
||||||
|
return p, fmt.Errorf("only leading wildcard (*.) is supported")
|
||||||
|
}
|
||||||
|
p.hostKind = kindWildcard
|
||||||
|
p.host = suffix
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact hostname or IP literal
|
||||||
|
p.hostKind = kindExact
|
||||||
|
p.host = hostPart
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p bypassPattern) matches(host, port string) bool {
|
||||||
|
if p.port != "" && p.port != port {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
host = strings.ToLower(host)
|
||||||
|
switch p.hostKind {
|
||||||
|
case kindCIDR:
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
return ip != nil && p.cidr.Contains(ip)
|
||||||
|
case kindWildcard:
|
||||||
|
// p.host = ".example.com"
|
||||||
|
return strings.HasSuffix(host, p.host) && len(host) > len(p.host)
|
||||||
|
case kindExact:
|
||||||
|
if host == p.host {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// IP literal comparison (normalises IPv4-mapped IPv6 etc.)
|
||||||
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
|
if pip := net.ParseIP(p.host); pip != nil {
|
||||||
|
return ip.Equal(pip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd *BypassDialer) shouldBypass(addr string) (bool, string) {
|
||||||
|
host, port, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
host = addr
|
||||||
|
port = ""
|
||||||
|
}
|
||||||
|
host = strings.Trim(host, "[]")
|
||||||
|
|
||||||
|
bd.mu.RLock()
|
||||||
|
defer bd.mu.RUnlock()
|
||||||
|
for _, p := range bd.patterns {
|
||||||
|
if p.matches(host, port) {
|
||||||
|
return true, p.raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd *BypassDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
bypass, matchedRule := bd.shouldBypass(addr)
|
||||||
|
if bypass {
|
||||||
|
bd.logger.Debug("bypass: DIRECT %s (rule: %s)", addr, matchedRule)
|
||||||
|
return bd.directDialer.DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
bd.logger.Debug("bypass: PROXY %s", addr)
|
||||||
|
return bd.proxyDialer.DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bd *BypassDialer) Dial(network, addr string) (net.Conn, error) {
|
||||||
|
return bd.DialContext(context.Background(), network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBypassFile reads bypass patterns from a text file.
|
||||||
|
// Empty lines and lines starting with # are ignored.
|
||||||
|
// Inline comments (preceded by whitespace + #) are stripped.
|
||||||
|
// Windows line endings (\r\n) are handled transparently by bufio.Scanner.
|
||||||
|
func LoadBypassFile(filename string) ([]string, error) {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var patterns []string
|
||||||
|
sc := bufio.NewScanner(f)
|
||||||
|
for sc.Scan() {
|
||||||
|
line := strings.TrimSpace(sc.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Strip inline comment: " # comment"
|
||||||
|
if idx := strings.Index(line, " #"); idx >= 0 {
|
||||||
|
line = strings.TrimSpace(line[:idx])
|
||||||
|
}
|
||||||
|
if line != "" {
|
||||||
|
patterns = append(patterns, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patterns, sc.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidPort(s string) bool {
|
||||||
|
if s == "" || len(s) > 5 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
for _, c := range s {
|
||||||
|
if c < '0' || c > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n = n*10 + int(c-'0')
|
||||||
|
}
|
||||||
|
return n > 0 && n <= 65535
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBypassPatternMatches(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
pattern string
|
||||||
|
host string
|
||||||
|
port string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"192.168.1.1", "192.168.1.1", "443", true},
|
||||||
|
{"192.168.1.1", "192.168.1.2", "443", false},
|
||||||
|
{"192.168.0.0/16", "192.168.1.100", "80", true},
|
||||||
|
{"192.168.0.0/16", "10.0.0.1", "80", false},
|
||||||
|
{"192.168.0.0/24:443", "192.168.0.5", "443", true},
|
||||||
|
{"192.168.0.0/24:443", "192.168.0.5", "80", false},
|
||||||
|
{"192.168.0.0/24:443", "10.0.0.1", "443", false},
|
||||||
|
{"example.com", "example.com", "443", true},
|
||||||
|
{"example.com", "sub.example.com", "443", false},
|
||||||
|
{"Example.COM", "example.com", "80", true},
|
||||||
|
{"*.example.com", "sub.example.com", "443", true},
|
||||||
|
{"*.example.com", "deep.sub.example.com", "443", true},
|
||||||
|
{"*.example.com", "example.com", "443", false},
|
||||||
|
{"*.example.com", "notexample.com", "443", false},
|
||||||
|
{"localhost:8080", "localhost", "8080", true},
|
||||||
|
{"localhost:8080", "localhost", "9090", false},
|
||||||
|
{"example.com", "example.com", "", true},
|
||||||
|
{"example.com", "example.com", "8080", true},
|
||||||
|
// TLD wildcard: only matches subdomains, not the TLD itself
|
||||||
|
{"*.ru", "yandex.ru", "443", true},
|
||||||
|
{"*.ru", "www.yandex.ru", "443", true},
|
||||||
|
{"*.ru", "ru", "443", false},
|
||||||
|
// Exact domain
|
||||||
|
{"yandex.ru", "yandex.ru", "80", true},
|
||||||
|
{"yandex.ru", "www.yandex.ru", "80", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
p, err := parseBypassPattern(tc.pattern)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("parseBypassPattern(%q) error: %v", tc.pattern, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
got := p.matches(tc.host, tc.port)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("pattern=%q host=%q port=%q: got %v, want %v",
|
||||||
|
tc.pattern, tc.host, tc.port, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldBypass(t *testing.T) {
|
||||||
|
bd, err := NewBypassDialer(
|
||||||
|
[]string{"192.168.0.0/24", "*.internal", "exact.host:8080"},
|
||||||
|
nil, nil, nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
addr string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"192.168.0.1:443", true},
|
||||||
|
{"192.168.1.1:443", false},
|
||||||
|
{"foo.internal:443", true},
|
||||||
|
{"internal:443", false},
|
||||||
|
{"exact.host:8080", true},
|
||||||
|
{"exact.host:9090", false},
|
||||||
|
{"google.com:443", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
got, _ := bd.shouldBypass(tc.addr)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("shouldBypass(%q) = %v, want %v", tc.addr, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBypassPatternErrors(t *testing.T) {
|
||||||
|
bad := []string{
|
||||||
|
"",
|
||||||
|
"192.168.0.0/999",
|
||||||
|
"**.example.com",
|
||||||
|
"*.*.example.com",
|
||||||
|
}
|
||||||
|
for _, raw := range bad {
|
||||||
|
_, err := parseBypassPattern(raw)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("parseBypassPattern(%q) expected error, got nil", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTLDWildcardSkippedInResolve(t *testing.T) {
|
||||||
|
// *.ru should still match hostnames even though resolution is skipped
|
||||||
|
bd, err := NewBypassDialer([]string{"*.ru"}, nil, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, _ := bd.shouldBypass("yandex.ru:443")
|
||||||
|
if !got {
|
||||||
|
t.Error("*.ru should match yandex.ru:443 via hostname matching")
|
||||||
|
}
|
||||||
|
got, _ = bd.shouldBypass("ru:443")
|
||||||
|
if got {
|
||||||
|
t.Error("*.ru should NOT match bare 'ru'")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
SILENT = 60 // suppress all output, including critical errors
|
||||||
CRITICAL = 50
|
CRITICAL = 50
|
||||||
ERROR = 40
|
ERROR = 40
|
||||||
WARNING = 30
|
WARNING = 30
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ type CLIArgs struct {
|
|||||||
caFile string
|
caFile string
|
||||||
fakeSNI string
|
fakeSNI string
|
||||||
overrideProxyAddress string
|
overrideProxyAddress string
|
||||||
|
proxyBypass []string
|
||||||
|
proxyBypassFile string
|
||||||
serverSelection serverSelectionArg
|
serverSelection serverSelectionArg
|
||||||
serverSelectionTimeout time.Duration
|
serverSelectionTimeout time.Duration
|
||||||
serverSelectionTestURL string
|
serverSelectionTestURL string
|
||||||
@@ -163,7 +165,7 @@ func parse_args() *CLIArgs {
|
|||||||
flag.StringVar(&args.bindAddress, "bind-address", "127.0.0.1:18080", "proxy listen address")
|
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")
|
flag.BoolVar(&args.socksMode, "socks-mode", false, "listen for SOCKS requests instead of HTTP")
|
||||||
flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity "+
|
flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity "+
|
||||||
"(10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical)")
|
"(10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical, 60 - silent)")
|
||||||
flag.DurationVar(&args.timeout, "timeout", DEFAULT_TIMEOUT,
|
flag.DurationVar(&args.timeout, "timeout", DEFAULT_TIMEOUT,
|
||||||
"timeout for network operations")
|
"timeout for network operations")
|
||||||
flag.BoolVar(&args.showVersion, "version", false, "show program version and exit")
|
flag.BoolVar(&args.showVersion, "version", false, "show program version and exit")
|
||||||
@@ -188,6 +190,18 @@ func parse_args() *CLIArgs {
|
|||||||
flag.StringVar(&args.caFile, "cafile", "", "use custom CA certificate bundle file")
|
flag.StringVar(&args.caFile, "cafile", "", "use custom CA certificate bundle file")
|
||||||
flag.StringVar(&args.fakeSNI, "fake-SNI", "", "domain name to use as SNI in communications with servers")
|
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.StringVar(&args.overrideProxyAddress, "override-proxy-address", "", "use fixed proxy address instead of server address returned by SurfEasy API")
|
||||||
|
flag.StringVar(&args.proxyBypassFile, "proxy-bypass-file", "", "path to file with bypass patterns, one per line (comments with #)")
|
||||||
|
flag.Func("proxy-bypass", "comma-separated bypass patterns; matched addresses connect directly.\n"+
|
||||||
|
"Formats: hostname (example.com), wildcard (*.example.com), IP (1.2.3.4), CIDR (10.0.0.0/8), any with optional :port suffix",
|
||||||
|
func(s string) error {
|
||||||
|
for _, part := range strings.Split(s, ",") {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if part != "" {
|
||||||
|
args.proxyBypass = append(args.proxyBypass, part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
flag.Var(&args.serverSelection, "server-selection", "server selection policy (first/random/fastest)")
|
flag.Var(&args.serverSelection, "server-selection", "server selection policy (first/random/fastest)")
|
||||||
flag.DurationVar(&args.serverSelectionTimeout, "server-selection-timeout", DEFAULT_SERVER_SELECTION_TIMEOUT,
|
flag.DurationVar(&args.serverSelectionTimeout, "server-selection-timeout", DEFAULT_SERVER_SELECTION_TIMEOUT,
|
||||||
"timeout given for server selection function to produce result")
|
"timeout given for server selection function to produce result")
|
||||||
@@ -283,7 +297,14 @@ func run() int {
|
|||||||
proxyLogger := clog.NewCondLogger(log.New(logWriter, "PROXY : ",
|
proxyLogger := clog.NewCondLogger(log.New(logWriter, "PROXY : ",
|
||||||
log.LstdFlags|log.Lshortfile),
|
log.LstdFlags|log.Lshortfile),
|
||||||
args.verbosity)
|
args.verbosity)
|
||||||
socksLogger := log.New(logWriter, "SOCKS : ",
|
// When verbosity >= SILENT, discard socks5 library logs entirely.
|
||||||
|
// go-socks5 writes through a raw *log.Logger which bypasses CondLogger,
|
||||||
|
// so we swap the writer to io.Discard to guarantee silence.
|
||||||
|
socksLogWriter := io.Writer(logWriter)
|
||||||
|
if args.verbosity >= clog.SILENT {
|
||||||
|
socksLogWriter = io.Discard
|
||||||
|
}
|
||||||
|
socksLogger := log.New(socksLogWriter, "SOCKS : ",
|
||||||
log.LstdFlags|log.Lshortfile)
|
log.LstdFlags|log.Lshortfile)
|
||||||
|
|
||||||
mainLogger.Info("opera-proxy client version %s is starting...", version())
|
mainLogger.Info("opera-proxy client version %s is starting...", version())
|
||||||
@@ -292,6 +313,10 @@ func run() int {
|
|||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: 30 * time.Second,
|
||||||
}
|
}
|
||||||
|
// rawDialer is the bare net.Dialer before any -proxy wrapping.
|
||||||
|
// BypassDialer uses this as directDialer so that bypassed connections
|
||||||
|
// genuinely go direct — not through the base proxy.
|
||||||
|
rawDialer := d
|
||||||
|
|
||||||
caPool, exitCode := buildCAPool(args.caFile, mainLogger)
|
caPool, exitCode := buildCAPool(args.caFile, mainLogger)
|
||||||
if exitCode != 0 {
|
if exitCode != 0 {
|
||||||
@@ -484,6 +509,33 @@ func run() int {
|
|||||||
mainLogger.Info("Endpoint override: %s", sanitizedEndpoint)
|
mainLogger.Info("Endpoint override: %s", sanitizedEndpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load bypass patterns from file if specified.
|
||||||
|
if args.proxyBypassFile != "" {
|
||||||
|
filePatterns, err := dialer.LoadBypassFile(args.proxyBypassFile)
|
||||||
|
if err != nil {
|
||||||
|
mainLogger.Critical("Unable to load bypass file: %v", err)
|
||||||
|
return 13
|
||||||
|
}
|
||||||
|
args.proxyBypass = append(args.proxyBypass, filePatterns...)
|
||||||
|
}
|
||||||
|
if len(args.proxyBypass) > 0 {
|
||||||
|
mainLogger.Info("Bypass rules (%d): %v", len(args.proxyBypass), args.proxyBypass)
|
||||||
|
bypassDialer, err := dialer.NewBypassDialer(args.proxyBypass, rawDialer, handlerDialer, mainLogger)
|
||||||
|
if err != nil {
|
||||||
|
mainLogger.Critical("Unable to configure bypass rules: %v", err)
|
||||||
|
return 13
|
||||||
|
}
|
||||||
|
// Pre-resolve hostname/wildcard patterns to IPs so that bypass works
|
||||||
|
// even when the SOCKS5 client resolves DNS itself and sends IP addresses.
|
||||||
|
// 20s timeout: Android may take longer to get network ready at daemon startup.
|
||||||
|
// nil resolver = net.DefaultResolver; on Android with CGO=0 this may fail
|
||||||
|
// for some patterns — those will still match by hostname if client sends SOCKS5h.
|
||||||
|
resolveCtx, resolveCancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
bypassDialer.ResolvePatterns(resolveCtx, nil)
|
||||||
|
resolveCancel()
|
||||||
|
handlerDialer = bypassDialer
|
||||||
|
}
|
||||||
|
|
||||||
clock.RunTicker(context.Background(), args.refresh, args.refreshRetry, func(ctx context.Context) error {
|
clock.RunTicker(context.Background(), args.refresh, args.refreshRetry, func(ctx context.Context) error {
|
||||||
mainLogger.Info("Refreshing login...")
|
mainLogger.Info("Refreshing login...")
|
||||||
reqCtx, cl := context.WithTimeout(ctx, args.timeout)
|
reqCtx, cl := context.WithTimeout(ctx, args.timeout)
|
||||||
@@ -602,6 +654,7 @@ func sanitizeFixedProxyAddress(addr string) string {
|
|||||||
return net.JoinHostPort(addr, "443")
|
return net.JoinHostPort(addr, "443")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
os.Exit(run())
|
os.Exit(run())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user