package config import ( "fmt" "log/slog" "net" "strings" "github.com/sower-proxy/deferlog/v2" "github.com/sower-proxy/sower/pkg/upstreamtls" ) type RemoteTLSConfig struct { ServerName string `usage:"override upstream TLS server name (SNI)"` ClientHello string `usage:"uTLS client hello: chrome, firefox, ios, android, edge, safari, 360, qq, randomized, randomized_alpn, randomized_no_alpn, golang"` InsecureSkipVerify bool `default:"false" usage:"skip upstream TLS certificate verification"` } type RemoteConfig struct { Type string `default:"sower" required:"true" usage:"option: sower/socks5"` Addr string `required:"true" usage:"proxy address, eg: proxy.com or proxy.com:443"` // Password is sent verbatim to the upstream proxy. It stays a plain // string (not deferlog.Password): the base64-or-plain heuristic in // deferlog.Password would silently corrupt passwords that happen to be // valid canonical base64, breaking auth against servers that compare // byte-for-byte. Password string `usage:"remote proxy password"` TLS RemoteTLSConfig `flag:"tls"` } // SowerConfig represents the configuration for sower client type SowerConfig struct { LogLevel slog.Level `default:"info" usage:"log level: debug, info, warn, error"` // IgnoreAdminState skips the admin state file at startup. It is the // escape hatch when a persisted override breaks the service. IgnoreAdminState bool `flag:"ignore-admin-state" usage:"ignore the admin state file at startup"` Remote RemoteConfig DNS struct { Disable bool `default:"false" usage:"disable DNS proxy"` Serve string `usage:"dns server ip"` Serve6 string `usage:"dns server ipv6, eg: ::1"` ServeIface string `usage:"use the IP in the net interface, if serve ip not setted. eg: eth0"` Upstream string `usage:"upstream dns server"` Fallback string `default:"223.5.5.5" required:"true" usage:"fallback dns server"` // Reverse is the DNS server used to resolve client IPs to hostnames // for the traffic console. Typically the local LAN resolver (e.g. // dnsmasq) so LAN leases and tailnet names resolve; empty disables // reverse lookups and the console shows raw IPs. Reverse string `usage:"reverse dns server for client hostname lookup"` } Socks5 struct { Disable bool `default:"false" usage:"disable sock5 proxy"` Addr string `default:"127.0.0.1:1080" usage:"socks5 listen address"` } `flag:"socks5"` Admin struct { Disable bool `default:"true" usage:"disable admin web server"` Addr string `default:"127.0.0.1:19090" usage:"admin web server listen address"` // Admin console password. Accepts plain text verbatim, or canonical // base64 with an explicit "b64:" prefix (e.g. "b64:c2VjcmV0"). Plain // values are never reinterpreted, so a password that happens to be // valid base64 stays exactly as written. Password deferlog.Password `usage:"admin web server password; empty generates a random one at startup"` SessionFile string `default:"/etc/sower/sessions.json" usage:"persist admin sessions to this file; one sower process only"` DisableSessionPersistence bool `default:"false" usage:"disable admin session persistence"` CookieSecure bool `default:"false" usage:"set Secure on admin session cookies behind a TLS-terminating proxy"` StateFile string `default:"/etc/sower/admin-state.json" usage:"persist admin rule and config changes to this file, empty disables persistence"` } `flag:"admin"` Router struct { Block struct { File string `usage:"block list file, local file or remote"` FilePrefix string `default:"**." usage:"parsed as 'line_text'"` FileSkipRules []string `usage:"rules to skip when loading block list file"` Rules []string `usage:"block list rules"` } Direct struct { File string `usage:"direct list file, local file or remote"` FilePrefix string `default:"**." usage:"parsed as 'line_text'"` FileSkipRules []string `usage:"rules to skip when loading direct list file"` Rules []string `usage:"direct list rules"` } Proxy struct { File string `usage:"proxy list file, local file or remote"` FilePrefix string `default:"**." usage:"parsed as 'line_text'"` FileSkipRules []string `usage:"rules to skip when loading proxy list file"` Rules []string `usage:"proxy list rules"` } Country struct { MMDB string `usage:"mmdb file"` File string `usage:"CIDR block list file, local file or remote"` FilePrefix string `default:"" usage:"parsed as 'line_text'"` Rules []string `usage:"CIDR list rules"` } } } // AdminSessionFile returns an empty path only when persistence is explicitly // disabled. The session file is not safe to share across multiple processes. func (c SowerConfig) AdminSessionFile() string { if c.Admin.DisableSessionPersistence { return "" } return c.Admin.SessionFile } // Validate implements the validation interface for SowerConfig func (c *SowerConfig) Validate() error { switch c.Remote.Type { case "sower", "socks5": default: return fmt.Errorf("unsupported remote type %q", c.Remote.Type) } if c.Remote.Type == "socks5" && c.Remote.Password != "" { // The SOCKS5 client transport only negotiates no-auth (RFC 1928); // a configured password would be silently ignored and the upstream // handshake would fail only when it demands authentication. return fmt.Errorf("remote password is not supported for socks5 upstreams; remove remote.password or use remote.type = \"sower\"") } remoteHost, err := validateRemoteAddr(c.Remote.Type, c.Remote.Addr) if err != nil { return err } if c.Remote.TLS.ClientHello != "" { if err := upstreamtls.ValidateClientHello(c.Remote.TLS.ClientHello); err != nil { return err } } if c.DNS.ServeIface != "" { iface, err := net.InterfaceByName(c.DNS.ServeIface) if err != nil { return fmt.Errorf("get interface: %w", err) } addrs, err := iface.Addrs() if err != nil { return fmt.Errorf("get interface addresses: %w", err) } for _, addr := range addrs { ip, _, err := net.ParseCIDR(addr.String()) if err != nil { return fmt.Errorf("parse interface IP: %w", err) } if ip.To4() != nil { // ipv4 if c.DNS.Serve == "" { c.DNS.Serve = ip.String() } } else if ip.IsGlobalUnicast() { // ipv6 must be global unicast if c.DNS.Serve6 == "" { c.DNS.Serve6 = ip.String() } } } } if err := validateOptionalIP("dns serve", c.DNS.Serve); err != nil { return err } if err := validateOptionalIP("dns serve6", c.DNS.Serve6); err != nil { return err } if err := validateOptionalIP("dns upstream", c.DNS.Upstream); err != nil { return err } if err := validateRequiredIP("dns fallback", c.DNS.Fallback); err != nil { return err } if err := validateOptionalIP("dns reverse", c.DNS.Reverse); err != nil { return err } if !c.DNS.Disable && c.DNS.Serve == "" { return fmt.Errorf("dns serve ip and serve interface not set") } if !c.Socks5.Disable { if _, _, err := net.SplitHostPort(c.Socks5.Addr); err != nil { return fmt.Errorf("invalid socks5 listen address %q: %w", c.Socks5.Addr, err) } } if !c.Admin.Disable && c.Admin.Addr != "" { host, port, err := net.SplitHostPort(c.Admin.Addr) if err != nil || host == "" { return fmt.Errorf("invalid admin listen address %q", c.Admin.Addr) } // The admin console can share the DNS HTTP proxy listener on port 80 // (admin.addr == dns.serve:80), but it cannot share the HTTPS or DNS // listeners, which speak different protocols. if !c.DNS.Disable && c.DNS.Serve != "" && host == c.DNS.Serve { switch port { case "443", "53": return fmt.Errorf("admin listen address %q conflicts with the %s proxy listener on %s", c.Admin.Addr, port, c.DNS.Serve) } } } c.Router.Direct.Rules = append(c.Router.Direct.Rules, remoteHost, "**.in-addr.arpa", "**.ip6.arpa") return nil } func validateRemoteAddr(remoteType, addr string) (string, error) { switch remoteType { case "socks5": host, _, err := net.SplitHostPort(addr) if err != nil { return "", fmt.Errorf("invalid remote socks5 address %q: %w", addr, err) } if host == "" { return "", fmt.Errorf("invalid remote socks5 address %q", addr) } return host, nil default: host, _, err := splitRemoteAddr(addr, "443") if err != nil { return "", fmt.Errorf("invalid remote tls address %q: %w", addr, err) } return host, nil } } func splitRemoteAddr(addr, defaultPort string) (string, string, error) { if addr == "" { return "", "", fmt.Errorf("empty address") } if host, port, err := net.SplitHostPort(addr); err == nil { if host == "" || port == "" { return "", "", fmt.Errorf("invalid address %q", addr) } return host, port, nil } if strings.HasPrefix(addr, "[") || strings.HasSuffix(addr, "]") { return "", "", fmt.Errorf("invalid address %q", addr) } // A bare IPv6 literal is a valid host with the default port. Anything // else that still contains a colon is a malformed host:port (e.g. a // bare IPv6 with a port, which needs [addr]:port) or a garbage string. if ip := net.ParseIP(addr); ip != nil { return addr, defaultPort, nil } if strings.Contains(addr, ":") { return "", "", fmt.Errorf("invalid address %q: IPv6 with a port must use [addr]:port", addr) } return addr, defaultPort, nil } func validateOptionalIP(name, value string) error { if value == "" { return nil } return validateRequiredIP(name, value) } func validateRequiredIP(name, value string) error { if net.ParseIP(value) == nil { return fmt.Errorf("invalid %s %q", name, value) } return nil }