Skip to content

Commit

Permalink
ability to listen on multiple http or https ports (#127)
Browse files Browse the repository at this point in the history
* untested prototype

* working bugfixes

* working

* fixed some tests
  • Loading branch information
mosajjal authored Sep 7, 2024
1 parent 742be28 commit 366871e
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 53 deletions.
5 changes: 5 additions & 0 deletions cmd/sniproxy/config.defaults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# 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
# NOTE: if you're using SOCKS, avoid using UDP for upstream DNS
upstream_dns: udp://8.8.8.8:53
# enable send DNS through socks5
upstream_dns_over_socks5: false
Expand All @@ -25,8 +26,12 @@ general:
tls_key:
# HTTP Port to listen on. Should remain 80 in most cases. use :80 to listen on both IPv4 and IPv6
bind_http: "0.0.0.0:80"
# bind additional ports for HTTP. a list of portsor ranges separated by commas. example: "8080,8081-8083". follows the same listen address as bind_http
bind_http_additional: ["8080","8081-8083"]
# HTTPS Port to listen on. Should remain 443 in most cases
bind_https: "0.0.0.0:443"
# bind additional ports for HTTPS. a list of portsor ranges separated by commas. example: "8443,8444-8446". follows the same listen address as bind_https
bind_https_additional: ["8443","8444-8446"]
# 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
Expand Down
22 changes: 20 additions & 2 deletions cmd/sniproxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ func main() {
c.TLSCert = generalConfig.String("tls_cert")
c.TLSKey = generalConfig.String("tls_key")
c.BindHTTP = generalConfig.String("bind_http")
c.BindHTTPAdditional = generalConfig.Strings("bind_http_additional")
c.BindHTTPS = generalConfig.String("bind_https")
c.BindHTTPSAdditional = generalConfig.Strings("bind_https_additional")
c.Interface = generalConfig.String("interface")
c.PreferredVersion = generalConfig.String("preferred_version")

Expand Down Expand Up @@ -242,8 +244,24 @@ func main() {
return
}

go sniproxy.RunHTTP(&c, logger.With().Str("service", "http").Logger())
go sniproxy.RunHTTPS(&c, logger.With().Str("service", "https").Logger())
// get a list of http and https binds
if err := c.SetBindHTTPListeners(logger); err != nil {
logger.Error().Msgf("error setting up HTTP listeners: %v", err)
return
}
logger.Info().Msgf("HTTP listeners: %v", c.BindHTTPListeners)
if err := c.SetBindHTTPSListeners(logger); err != nil {
logger.Error().Msgf("error setting up HTTPS listeners: %v", err)
return
}
logger.Info().Msgf("HTTPS listeners: %v", c.BindHTTPSListeners)

for _, addr := range c.BindHTTPListeners {
go sniproxy.RunHTTP(&c, addr, logger.With().Str("service", "http").Str("listener", addr).Logger())
}
for _, addr := range c.BindHTTPSListeners {
go sniproxy.RunHTTPS(&c, addr, logger.With().Str("service", "https").Str("listener", addr).Logger())
}
go sniproxy.RunDNS(&c, logger.With().Str("service", "dns").Logger())

// wait forever. TODO: add signal handling here
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ require (
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.59.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.5.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.46.0 // indirect
github.com/redis/go-redis/v9 v9.6.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quic-go/qpack v0.5.0 h1:jldbr38Ef/swDfxtvNvvUIYNg5LNm3Oa9W+IZvCm4q0=
github.com/quic-go/qpack v0.5.0/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y=
github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
Expand Down
129 changes: 106 additions & 23 deletions pkg/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"net/netip"
"net/url"
"strconv"
"strings"

"github.com/mosajjal/sniproxy/v2/pkg/acl"
"github.com/rcrowley/go-metrics"
Expand All @@ -13,22 +15,26 @@ import (
)

type Config struct {
PublicIPv4 string `yaml:"public_ipv4"`
PublicIPv6 string `yaml:"public_ipv6"`
UpstreamDNS string `yaml:"upstream_dns"`
UpstreamDNSOverSocks5 bool `yaml:"upstream_dns_over_socks5"`
UpstreamSOCKS5 string `yaml:"upstream_socks5"`
BindDNSOverUDP string `yaml:"bind_dns_over_udp"`
BindDNSOverTCP string `yaml:"bind_dns_over_tcp"`
BindDNSOverTLS string `yaml:"bind_dns_over_tls"`
BindDNSOverQuic string `yaml:"bind_dns_over_quic"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
BindHTTP string `yaml:"bind_http"`
BindHTTPS string `yaml:"bind_https"`
Interface string `yaml:"interface"`
BindPrometheus string `yaml:"bind_prometheus"`
AllowConnToLocal bool `yaml:"allow_conn_to_local"`
PublicIPv4 string `yaml:"public_ipv4"`
PublicIPv6 string `yaml:"public_ipv6"`
UpstreamDNS string `yaml:"upstream_dns"`
UpstreamDNSOverSocks5 bool `yaml:"upstream_dns_over_socks5"`
UpstreamSOCKS5 string `yaml:"upstream_socks5"`
BindDNSOverUDP string `yaml:"bind_dns_over_udp"`
BindDNSOverTCP string `yaml:"bind_dns_over_tcp"`
BindDNSOverTLS string `yaml:"bind_dns_over_tls"`
BindDNSOverQuic string `yaml:"bind_dns_over_quic"`
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
BindHTTP string `yaml:"bind_http"`
BindHTTPAdditional []string `yaml:"bind_http_additional"`
BindHTTPListeners []string `yaml:"-"` // compiled list of bind_http and bind_http_additional listen addresses
BindHTTPS string `yaml:"bind_https"`
BindHTTPSAdditional []string `yaml:"bind_https_additional"`
BindHTTPSListeners []string `yaml:"-"` // compiled list of bind_https and bind_https_additional listen addresses
Interface string `yaml:"interface"`
BindPrometheus string `yaml:"bind_prometheus"`
AllowConnToLocal bool `yaml:"allow_conn_to_local"`

Acl []acl.ACL `yaml:"-"`

Expand Down Expand Up @@ -87,21 +93,98 @@ func (c *Config) SetDNSClient(logger zerolog.Logger) error {
var dnsProxy string
var dnsClient *DNSClient
// if upstream socks5 is not provided or upstream dns over socks5 is disabled, disable socks5 for dns
if c.UpstreamSOCKS5 == "" || !c.UpstreamDNSOverSocks5 {
if c.UpstreamSOCKS5 != "" && !c.UpstreamDNSOverSocks5 {
logger.Debug().Msg("disabling socks5 for dns because either upstream socks5 is not provided or upstream dns over socks5 is disabled")
dnsProxy = ""
} else {
dnsProxy = c.UpstreamSOCKS5
var err error
dnsClient, err = NewDNSClient(c, c.UpstreamDNS, true, dnsProxy)
}
var err error
dnsClient, err = NewDNSClient(c, c.UpstreamDNS, true, dnsProxy)
if err != nil {
logger.Error().Msgf("error setting up dns client with socks5 proxy, falling back to direct DNS client: %v", err)
dnsClient, err = NewDNSClient(c, c.UpstreamDNS, false, "")
if err != nil {
logger.Error().Msgf("error setting up dns client with socks5 proxy, falling back to direct DNS client: %v", err)
dnsClient, err = NewDNSClient(c, c.UpstreamDNS, false, "")
return fmt.Errorf("error setting up dns client: %v", err)
}
}
c.DnsClient = *dnsClient
return nil
}

// parseRanges parses a range of ports or a single port. It returns a list of ports
func parseRanges(portRange ...string) ([]int, error) {
var ports []int

for _, portRange := range portRange {

if strings.Index(portRange, "-") == -1 {
port, err := strconv.Atoi(portRange)
if err != nil {
return nil, fmt.Errorf("error parsing port: %v", err)
}
ports = append(ports, port)
} else {
num1Str := strings.Split(portRange, "-")[0]
num2Str := strings.Split(portRange, "-")[1]
// convert both numbers to integers

num1, err := strconv.Atoi(num1Str)
if err != nil {
return fmt.Errorf("error setting up dns client: %v", err)
return nil, fmt.Errorf("error parsing port range: %v", err)
}
num2, err := strconv.Atoi(num2Str)
if err != nil {
return nil, fmt.Errorf("error parsing port range: %v", err)
}
for i := num1; i <= num2; i++ {
ports = append(ports, i)
}
}
}
c.DnsClient = *dnsClient
return ports, nil
}

// parseBinders parses a bind address and a list of additional ports or port ranges
func parseBinders(bind string, additional []string) ([]string, error) {
// get the bind address from bind
bindAddPort, err := netip.ParseAddrPort(bind)
if err != nil {
return nil, fmt.Errorf("error parsing bind address: %v", err)
}
bindAddresses := []string{bindAddPort.String()}

// now all the ranges must be parsed, and each of them converted into a bind address and added to the list
portRange, err := parseRanges(additional...)
if err != nil {
return nil, fmt.Errorf("error parsing bind address range: %v", err)
}
for _, port := range portRange {
bindAddresses = append(bindAddresses, fmt.Sprintf("%s:%d", bindAddPort.Addr(), port))
}
return bindAddresses, nil
}

// SetBindHTTPListeners sets up a list of bind addresses for HTTP
// it gets the bind address from bind_http as 0.0.0.0:80 format
// and the additional bind addresses from bind_http_additional as a list of ports or port ranges
// such as 8080, 8081-8083, 8085
// when this function is called, it will compile the list of bind addresses and store it in BindHTTPListeners
func (c *Config) SetBindHTTPListeners(logger zerolog.Logger) error {
bindAddresses, err := parseBinders(c.BindHTTP, c.BindHTTPAdditional)
if err != nil {
return fmt.Errorf("error parsing bind addresses for HTTP: %v", err)
}
c.BindHTTPListeners = bindAddresses
return nil
}

// SetBindHTTPSListeners sets up a list of bind addresses for HTTPS
func (c *Config) SetBindHTTPSListeners(logger zerolog.Logger) error {
bindAddresses, err := parseBinders(c.BindHTTPS, c.BindHTTPSAdditional)
if err != nil {
return fmt.Errorf("error parsing bind addresses for HTTPS: %v", err)
}
c.BindHTTPSListeners = bindAddresses
return nil
}
32 changes: 18 additions & 14 deletions pkg/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/url"
"strings"
"sync"
"time"

rdns "github.com/folbricht/routedns"

Expand Down Expand Up @@ -148,7 +149,7 @@ func (dnsc DNSClient) lookupDomain(domain string, version string) (netip.Addr, e
return dnsc.lookupDomain4(domain)
case "ipv6only":
return dnsc.lookupDomain6(domain)
case "ipv4", "4", "0":
case "ipv4", "4", "0", "":
// try with ipv4, if there's any error, try with ipv6
ip, err := dnsc.lookupDomain4(domain)
if err != nil {
Expand Down Expand Up @@ -186,6 +187,7 @@ func (dnsc DNSClient) lookupDomain4(domain string) (netip.Addr, error) {
}
return netip.IPv4Unspecified(), fmt.Errorf("[DNS] Unknown type %s", dns.TypeToString[rAddrDNS[0].Header().Rrtype])
}

func (dnsc DNSClient) lookupDomain6(domain string) (netip.Addr, error) {
if !strings.HasSuffix(domain, ".") {
domain = domain + "."
Expand Down Expand Up @@ -323,12 +325,13 @@ func getDialerFromProxyURL(proxyURL *url.URL) (*rdns.Dialer, error) {
dialer = &net.Dialer{}
if proxyURL != nil && proxyURL.Host != "" {
// create a net dialer with proxy
var auth *proxy.Auth
auth := new(proxy.Auth)
if proxyURL.User != nil {
auth = new(proxy.Auth)
auth.User = proxyURL.User.Username()
if p, ok := proxyURL.User.Password(); ok {
auth.Password = p
} else {
auth.Password = ""
}
}
c, err := socks5.NewClient(proxyURL.Host, auth.User, auth.Password, 0, 5) // 0 and 5 are borrowed from routedns pr
Expand Down Expand Up @@ -378,15 +381,16 @@ func NewDNSClient(C *Config, uri string, skipVerify bool, proxy string) (*DNSCli

var ldarr net.IP
if parsedURL.Scheme == "udp6" {
ldarr = C.pickSrcAddr(6)
ldarr = C.pickSrcAddr("ipv6only")
} else {
ldarr = C.pickSrcAddr(4)
ldarr = C.pickSrcAddr("ipv4only")
}

opt := rdns.DNSClientOptions{
LocalAddr: ldarr,
UDPSize: 1300,
Dialer: *dialer,
LocalAddr: ldarr,
UDPSize: 1300,
Dialer: *dialer,
QueryTimeout: 10 * time.Second, //TODO: make this configurable
}
id, err := rdns.NewDNSClient("id", Address, "udp", opt)
if err != nil {
Expand All @@ -403,9 +407,9 @@ func NewDNSClient(C *Config, uri string, skipVerify bool, proxy string) (*DNSCli

var ldarr net.IP
if parsedURL.Scheme == "tcp6" {
ldarr = C.pickSrcAddr(6)
ldarr = C.pickSrcAddr("ipv6only")
} else {
ldarr = C.pickSrcAddr(4)
ldarr = C.pickSrcAddr("ipv4only")
}

Address := rdns.AddressWithDefault(host, port)
Expand All @@ -427,10 +431,10 @@ func NewDNSClient(C *Config, uri string, skipVerify bool, proxy string) (*DNSCli
var ldarr net.IP
bootstrapAddr := "1.1.1.1"
if parsedURL.Scheme == "tls6" || parsedURL.Scheme == "tcp-tls6" {
ldarr = C.pickSrcAddr(6)
ldarr = C.pickSrcAddr("ipv6only")
bootstrapAddr = "2606:4700:4700::1111"
} else {
ldarr = C.pickSrcAddr(4)
ldarr = C.pickSrcAddr("ipv4only")
}

opt := rdns.DoTClientOptions{
Expand All @@ -456,7 +460,7 @@ func NewDNSClient(C *Config, uri string, skipVerify bool, proxy string) (*DNSCli
TLSConfig: tlsConfig,
BootstrapAddr: "1.1.1.1", //TODO: make this configurable
Transport: transport,
LocalAddr: C.pickSrcAddr(4), //TODO:support IPv6
LocalAddr: C.pickSrcAddr("ipv4only"), //TODO:support IPv6
Dialer: *dialer,
}
id, err := rdns.NewDoHClient("id", parsedURL.String(), opt)
Expand All @@ -473,7 +477,7 @@ func NewDNSClient(C *Config, uri string, skipVerify bool, proxy string) (*DNSCli

opt := rdns.DoQClientOptions{
TLSConfig: tlsConfig,
LocalAddr: C.pickSrcAddr(4), //TODO:support IPv6
LocalAddr: C.pickSrcAddr("ipv4only"), //TODO:support IPv6
// Dialer: *dialer, // BUG: not yet supported
}
id, err := rdns.NewDoQClient("id", parsedURL.Host, opt)
Expand Down
17 changes: 12 additions & 5 deletions pkg/dns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ func TestDNSClient_lookupDomain4(t *testing.T) {
client *DNSClient
name string
domain string
want net.IP
want []net.IP
wantErr bool
}{
{client: dnsc, name: "test1", domain: "ident.me", want: net.IPv4(49, 12, 234, 183), wantErr: false},
{client: dnsc, name: "test1", domain: "ifconfig.me", want: net.IPv4(34, 117, 118, 44), wantErr: false},
{client: dnsc, name: "test1", domain: "ident.me", want: []net.IP{net.IPv4(49, 12, 234, 183)}, wantErr: false},
{client: dnsc, name: "test2", domain: "one.one.one.one", want: []net.IP{net.IPv4(1, 1, 1, 1), net.IPv4(1, 0, 0, 1)}, wantErr: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -31,8 +31,15 @@ func TestDNSClient_lookupDomain4(t *testing.T) {
t.Errorf("DNSClient.lookupDomain4() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !got.Equal(tt.want) {

// check if the returned IP is in the list of expected IPs
found := false
for _, w := range tt.want {
if got.Equal(w) {
found = true
break
}
}
if !found {
t.Errorf("DNSClient.lookupDomain4() = %v, want %v", got, tt.want)
}
})
Expand Down
Loading

0 comments on commit 366871e

Please sign in to comment.