From 8e9af0e72b51859b067cab6ac38057008babece0 Mon Sep 17 00:00:00 2001 From: Ali Mosajjal Date: Thu, 9 Nov 2023 09:54:30 +1300 Subject: [PATCH] added cmd back --- .gitignore | 1 - cmd/sniproxy/config.defaults.yaml | 107 ++++++++++ cmd/sniproxy/main.go | 333 ++++++++++++++++++++++++++++++ 3 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 cmd/sniproxy/config.defaults.yaml create mode 100644 cmd/sniproxy/main.go diff --git a/.gitignore b/.gitignore index 5d79290..e585625 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,6 @@ _testmain.go *.exe *.test *.prof -sniproxy config.yaml .TODO.md diff --git a/cmd/sniproxy/config.defaults.yaml b/cmd/sniproxy/config.defaults.yaml new file mode 100644 index 0000000..5a8609f --- /dev/null +++ b/cmd/sniproxy/config.defaults.yaml @@ -0,0 +1,107 @@ +# you can use environment variables to override the settings provided int he default config or the config file as well +# to do that use the `SNIPROXY_` prefix, followed by the full tree of the parameter you need to update, separated by two underscores (__) +# for example, if you want the dns server to be bounded to 0.0.0.0:5555 rather than the default 0.0.0.0:53, you run the sniproxy (or the container) +# with the following environment variable +# SNIPROXY_GENERAL__BIND_DNS_OVER_UDP=0.0.0.0:5555 +# note that there are 2 underscores between general and BIND_DNS_OVER_UDP +general: + # Upsteam DNS URI. examples: Upstream DNS URI. examples: udp://1.1.1.1:53, tcp://1.1.1.1:53, tcp-tls://1.1.1.1:853, https://dns.google/dns-query + upstream_dns: udp://8.8.8.8:53 + # enable send DNS through socks5 + upstream_dns_over_socks5: false + # Use a SOCKS proxy for upstream HTTP/HTTPS traffic. Example: socks5://admin: + upstream_socks5: + # DNS Port to listen on. Should remain 53 in most cases. MUST NOT be empty + bind_dns_over_udp: "0.0.0.0:53" + # enable DNS over TCP. empty disables it. example: "127.0.0.1:53" + bind_dns_over_tcp: + # enable DNS over TLS. empty disables it. example: "127.0.0.1:853" + bind_dns_over_tls: + # enable DNS over QUIC. empty disables it. example: "127.0.0.1:8853" + bind_dns_over_quic: + # Path to the certificate for DoH, DoT and DoQ. eg: /tmp/mycert.pem + tls_cert: + # Path to the certificate key for DoH, DoT and DoQ. eg: /tmp/mycert.key + tls_key: + # HTTP Port to listen on. Should remain 80 in most cases + bind_http: "0.0.0.0:80" + # HTTPS Port to listen on. Should remain 443 in most cases + bind_https: "0.0.0.0:443" + # Enable prometheus endpoint on IP:PORT. example: 127.0.0.1:8080. Always exposes /metrics and only supports HTTP + bind_prometheus: + # Interface used for outbound TLS connections. uses OS prefered one if empty + interface: + # Public IPv4 of the server, reply address of DNS A queries + public_ipv4: + # Public IPv6 of the server, reply address of DNS AAAA queries + public_ipv6: + # allow connections from sniproxy to RFC1918 addresses. default is false. + allow_conn_to_local: false + # log level for the application. choices: debug, info, warn, error + # by default, the logs are colored so they are not suited for logging to a file. + # in order to disable colors, set NO_COLOR=true in the environment variables + log_level: info + +acl: + # geoip filtering + # + # the logic is as follows: + # 1. if mmdb is not loaded or not available, it's fail-open (allow by default) + # 2. if the IP can't be resolved to a country, it's rejected + # 3. if the country is in the blocked list, it's rejected + # 4. if the country is in the allowed list, it's allowed + # note that the reject list is checked first and takes priority over the allow list + # if the IP's country doesn't match any of the above, it's allowed if the blocked list is not empty + # for example, if the blockedlist is [US] and the allowedlist is empty, a connection from + # CA will be allowed. but if blockedlist is empty and allowedlist is [US], a connection from + # CA will be rejected. + geoip: + enabled: false + # priority of the geoip filter. lower priority means it's checked first, meaning it can be ovveriden by other ACLs with higehr priority number. + priority: 10 + # strictly blocked countries + blocked: + # allowed countries + allowed: + # Path to the MMDB file. eg: /tmp/Country.mmdb, https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb + path: + # Interval to re-fetch the MMDB file + refresh_interval: 24h0m0s + # domain filtering + domain: + enabled: false # false means ALL domains will be allowed to go through the proxy + # priority of the domain filter. lower priority means it's checked first. if multiple filters have the same priority, they're checked in random order + priority: 20 + # Path to the domain list. eg: /tmp/domainlist.csv. Look at the example file for the format. + path: + # Interval to re-fetch the domain list + refresh_interval: 1h0m0s + # IP/CIDR filtering + cidr: + enabled: false + # priority of the cidr filter. lower priority means it's checked first. if multiple filters have the same priority, they're checked in random order + priority: 30 + # Path to the CIDR list. eg: /tmp/cidr.csv. Look at the example file for the format. + path: + # Interval to re-fetch the domain list + refresh_interval: 1h0m0s + # FQDN override. This ACL is used to override the destination IP to not be the one resolved by the upstream DNS or the proxy itself, rather a custom IP and port + # if the destination is HTTP, it uses tls_cert and tls_key certificate to terminate the original connection. + override: + enabled: false + # priority of the override filter. lower priority means it's checked first. if multiple filters have the same priority, they're checked in random order + priority: 40 + # override rules. unlike others, this one does not require a path to a file. it's a map of FQDNs wildcards to IPs and ports. only HTTPS is supported + # currently, these rules are checked with a simple for loop and string matching, + # so it's not suited for a large number of rules. if you have a big list of rules + # use a reverse proxy in front of sniproxy rather than using sniproxy as a reverse proxy + rules: + "one.one.one.one": "1.1.1.1:443" + "google.com": "8.8.8.8:443" + # enable listening on DoH on a specific SNI. example: "myawesomedoh.example.com". empty disables it. If you need DoH to be enabled and don't want + # any other overrides, enable this ACL with empty rules. DoH SNI will add a default rule and start. + doh_sni: "myawesomedoh.example.com" + # Path to the certificate for handling tls decryption. eg: /tmp/mycert.pem + tls_cert: + # Path to the certificate key handling tls decryption. eg: /tmp/mycert.key + tls_key: diff --git a/cmd/sniproxy/main.go b/cmd/sniproxy/main.go new file mode 100644 index 0000000..3ba00c3 --- /dev/null +++ b/cmd/sniproxy/main.go @@ -0,0 +1,333 @@ +package main + +import ( + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/knadh/koanf" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/rawbytes" + "github.com/rs/zerolog" + "github.com/txthinking/socks5" + + prometheusmetrics "github.com/deathowl/go-metrics-prometheus" + "github.com/prometheus/client_golang/prometheus" + "github.com/rcrowley/go-metrics" + + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/spf13/cobra" + + _ "embed" + stdlog "log" + + "github.com/miekg/dns" + "golang.org/x/net/proxy" + + sniproxy "github.com/mosajjal/sniproxy/v2/pkg" + "github.com/mosajjal/sniproxy/v2/pkg/acl" + "github.com/mosajjal/sniproxy/v2/pkg/doh" +) + +var c sniproxy.Config + +var ( + version string = "v2-UNKNOWN" + commit string = "NOT PROVIDED" + envPrefix string = "SNIPROXY_" // used as the prefix to read env variables at runtime +) + +//go:embed config.defaults.yaml +var defaultConfig []byte + +// disable colors in logging if NO_COLOR is set +var nocolorLog = strings.ToLower(os.Getenv("NO_COLOR")) == "true" +var logger = zerolog.New(os.Stderr).With().Timestamp().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339, NoColor: nocolorLog}) + +func getPublicIPv4() (string, error) { + conn, err := net.Dial("udp", "8.8.8.8:53") + if err != nil { + return "", err + } + defer conn.Close() + localAddr := conn.LocalAddr().String() + idx := strings.LastIndex(localAddr, ":") + ipaddr := localAddr[0:idx] + if !net.ParseIP(ipaddr).IsPrivate() { + return ipaddr, nil + } + externalIP := "" + // trying to get the public IP from multiple sources to see if they match. + resp, err := http.Get("https://myexternalip.com/raw") + if err == nil { + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err == nil { + externalIP = string(body) + } + + if externalIP != "" { + return externalIP, nil + } + logger.Error().Msg("Could not automatically find the public IPv4 address. Please specify it in the configuration.") + + } + return "", nil +} + +func cleanIPv6(ip string) string { + ip = strings.TrimPrefix(ip, "[") + ip = strings.TrimSuffix(ip, "]") + return ip +} + +func getPublicIPv6() (string, error) { + conn, err := net.Dial("udp6", "[2001:4860:4860::8888]:53") + if err != nil { + return "", err + } + defer conn.Close() + localAddr := conn.LocalAddr().String() + idx := strings.LastIndex(localAddr, ":") + ipaddr := localAddr[0:idx] + if !net.ParseIP(ipaddr).IsPrivate() { + return cleanIPv6(ipaddr), nil + } + externalIP := "" + // trying to get the public IP from multiple sources to see if they match. + resp, err := http.Get("https://6.ident.me") + if err == nil { + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err == nil { + externalIP = string(body) + } + + // backup method of getting a public IP + if externalIP == "" { + // dig +short -6 myip.opendns.com aaaa @2620:0:ccc::2 + dnsRes, err := c.DnsClient.PerformExternalAQuery("myip.opendns.com.", dns.TypeAAAA) + if err != nil { + return "", err + } + externalIP = dnsRes[0].(*dns.AAAA).AAAA.String() + } + + if externalIP != "" { + return cleanIPv6(externalIP), nil + } + logger.Error().Msg("Could not automatically find the public IPv6 address. Please specify it in the configuration.") + + } + return "", nil +} + +func main() { + + cmd := &cobra.Command{ + Use: "sniproxy", + Short: "SNI Proxy with Embedded DNS Server", + Run: func(command *cobra.Command, args []string) { + + }, + } + flags := cmd.Flags() + config := flags.StringP("config", "c", "", "path to YAML configuration file") + _ = flags.Bool("defaultconfig", false, "write the default config yaml file to stdout") + _ = flags.BoolP("version", "v", false, "show version info and exit") + if err := cmd.Execute(); err != nil { + logger.Error().Msgf("failed to execute command: %s", err) + return + } + if flags.Changed("help") { + return + } + if flags.Changed("version") { + fmt.Printf("sniproxy version %s, commit %s\n", version, commit) + return + } + if flags.Changed("defaultconfig") { + fmt.Fprintf(os.Stdout, string(defaultConfig)) + return + } + + k := koanf.New(".") + // load the defaults + if err := k.Load(rawbytes.Provider(defaultConfig), yaml.Parser()); err != nil { + panic(err) + } + if *config != "" { + if err := k.Load(file.Provider(*config), yaml.Parser()); err != nil { + panic(err) + } + } + // load environment variables starting with envPrefix + k.Load(env.Provider(envPrefix, ".", func(s string) string { + return strings.Replace(strings.ToLower( + strings.TrimPrefix(s, envPrefix)), "__", ".", -1) + }), nil) + + logger.Info().Msgf("starting sniproxy. version %s, commit %s", version, commit) + + // verify and load config + generalConfig := k.Cut("general") + + stdlog.SetFlags(0) + stdlog.SetOutput(logger) + + switch l := generalConfig.String("log_level"); l { + case "debug": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + logger = logger.With().Caller().Logger() + case "info": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + logger = logger.With().Caller().Logger() + case "warn": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "error": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + default: + zerolog.SetGlobalLevel(zerolog.InfoLevel) + } + + c.UpstreamDNS = generalConfig.String("upstream_dns") + c.UpstreamDNSOverSocks5 = generalConfig.Bool("upstream_dns_over_socks5") + c.UpstreamSOCKS5 = generalConfig.String("upstream_socks5") + c.BindDNSOverUDP = generalConfig.String("bind_dns_over_udp") + c.BindDNSOverTCP = generalConfig.String("bind_dns_over_tcp") + c.BindDNSOverTLS = generalConfig.String("bind_dns_over_tls") + c.BindDNSOverQuic = generalConfig.String("bind_dns_over_quic") + c.TLSCert = generalConfig.String("tls_cert") + c.TLSKey = generalConfig.String("tls_key") + c.BindHTTP = generalConfig.String("bind_http") + c.BindHTTPS = generalConfig.String("bind_https") + c.Interface = generalConfig.String("interface") + c.PublicIPv4 = generalConfig.String("public_ipv4") + if c.PublicIPv4 == "" { + c.PublicIPv4, _ = getPublicIPv4() + } + c.PublicIPv6 = generalConfig.String("public_ipv6") + if c.PublicIPv6 == "" { + c.PublicIPv6, _ = getPublicIPv6() + } + c.BindPrometheus = generalConfig.String("prometheus") + c.AllowConnToLocal = generalConfig.Bool("allow_conn_to_local") + + var err error + c.Acl, err = acl.StartACLs(&logger, k) + if err != nil { + logger.Error().Msgf("failed to start ACLs: %s", err) + return + } + + // set up metrics + c.RecievedDNS = metrics.GetOrRegisterCounter("dns.requests.recieved", metrics.DefaultRegistry) + c.ProxiedDNS = metrics.GetOrRegisterCounter("dns.requests.proxied", metrics.DefaultRegistry) + c.RecievedHTTP = metrics.GetOrRegisterCounter("http.requests.recieved", metrics.DefaultRegistry) + c.ProxiedHTTP = metrics.GetOrRegisterCounter("http.requests.proxied", metrics.DefaultRegistry) + c.RecievedHTTPS = metrics.GetOrRegisterCounter("https.requests.recieved", metrics.DefaultRegistry) + c.ProxiedHTTPS = metrics.GetOrRegisterCounter("https.requests.proxied", metrics.DefaultRegistry) + + if c.BindPrometheus != "" { + p := prometheusmetrics.NewPrometheusProvider(metrics.DefaultRegistry, "sniproxy", c.PublicIPv4, prometheus.DefaultRegisterer, 1*time.Second) + go p.UpdatePrometheusMetrics() + go func() { + http.Handle("/metrics", promhttp.Handler()) + logger.Info().Str( + "address", c.BindPrometheus, + ).Msg("starting metrics server") + if err := http.ListenAndServe(c.BindPrometheus, promhttp.Handler()); err != nil { + logger.Error().Msgf("%s", err) + } + }() + } + + if c.PublicIPv4 != "" { + logger.Info().Str("public_ip", c.PublicIPv4).Msg("server info") + } else { + logger.Error().Msg("Could not automatically determine public IPv4. you should provide it manually using --publicIPv4") + } + + if c.PublicIPv6 != "" { + logger.Info().Str("public_ip", c.PublicIPv6).Msg("server info") + } else { + logger.Error().Msg("Could not automatically determine public IPv6. you should provide it manually using --publicIPv6") + } + + // generate self-signed certificate if not provided + if c.TLSCert == "" && c.TLSKey == "" { + _, _, err := doh.GenerateSelfSignedCertKey(c.PublicIPv4, nil, nil, os.TempDir()) + logger.Info().Msg("certificate was not provided, generating a self signed cert in temp directory") + if err != nil { + logger.Error().Msgf("error while generating self-signed cert: %s", err) + } + c.TLSCert = filepath.Join(os.TempDir(), c.PublicIPv4+".crt") + c.TLSKey = filepath.Join(os.TempDir(), c.PublicIPv4+".key") + } + + // Finds source addr for outbound connections if interface is not empty + if c.Interface != "" { + logger.Info().Msgf("Using interface %s", c.Interface) + ief, err := net.InterfaceByName(c.Interface) + if err != nil { + logger.Error().Msg(err.Error()) + } + addrs, err := ief.Addrs() + if err != nil { + logger.Error().Msg(err.Error()) + } + c.SourceAddr = net.ParseIP(addrs[0].String()) + + } + + if c.UpstreamSOCKS5 != "" { + uri, err := url.Parse(c.UpstreamSOCKS5) + if err != nil { + logger.Error().Msg(err.Error()) + } + if uri.Scheme != "socks5" { + logger.Error().Msg("only SOCKS5 is supported") + return + } + + logger.Info().Msgf("Using an upstream SOCKS5 proxy: %s", uri.Host) + socksAuth := new(proxy.Auth) + socksAuth.User = uri.User.Username() + socksAuth.Password, _ = uri.User.Password() + c.Dialer, err = socks5.NewClient(uri.Host, socksAuth.User, socksAuth.Password, 60, 60) + if err != nil { + logger.Error().Msg(err.Error()) + } + } else { + c.Dialer = proxy.Direct + } + + dnsProxy := c.UpstreamSOCKS5 + if c.UpstreamSOCKS5 != "" && !c.UpstreamDNSOverSocks5 { + logger.Debug().Msg("disabling socks5 for dns") + dnsProxy = "" + } + tmp, err := sniproxy.NewDNSClient(&c, c.UpstreamDNS, true, dnsProxy) + if err != nil { + logger.Error().Msgf("error setting up dns client, removing proxy if provided: %v", err) + tmp, err = sniproxy.NewDNSClient(&c, c.UpstreamDNS, false, "") + if err != nil { + logger.Error().Msgf("error setting up dns client: %v", err) + return + } + } + c.DnsClient = *tmp + go sniproxy.RunHTTP(&c, logger) + go sniproxy.RunHTTPS(&c, logger.With().Str("service", "https").Logger()) + go sniproxy.RunDNS(&c, logger.With().Str("service", "dns").Logger()) + + select {} +}