Skip to content

Commit

Permalink
Merge pull request #28 from mysteriumnetwork/feature/multiple-upstreams
Browse files Browse the repository at this point in the history
Support of multiple upstream proxies
  • Loading branch information
Waldz authored Dec 22, 2022
2 parents 5edb42d + f0f42a0 commit 3335f4c
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 89 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ Let's assume:
docker run -d --restart=always --name forwarder --network host --cap-add NET_ADMIN mysteriumnetwork/openvpn-forwarder \
--proxy.bind=0.0.0.0:8443 \
--proxy.allow=0.0.0.0/0 \
--proxy.upstream-url="https://superproxy.com:443" \
--filter.hostnames="ipinfo.io"
--proxy.upstream-url="https://superproxy1.com:8443" \
--filter.hostnames="ipinfo.io" \
--proxy.upstream-url="http://superproxy2.com:8080" \
--filter.zones="ipify.org"
```

2. Redirect HTTP ports to forwarder:
Expand Down
1 change: 1 addition & 0 deletions magefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func Run() error {
return sh.RunV(buildPath,
"--log.level=trace",
"--proxy.bind=:8443",
"--proxy.allow=127.0.0.1",
"--proxy.upstream-url=http://superproxy.com:8080",
"--proxy.user=",
"--proxy.pass=",
Expand Down
202 changes: 143 additions & 59 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main

import (
"flag"
"fmt"
"net"
"net/url"
"os"
Expand All @@ -36,38 +37,12 @@ var logLevel = flag.String("log.level", log.InfoStr, "Set the logging level (tra
var proxyAddr = flag.String("proxy.bind", ":8443", "Proxy address for incoming connections")
var proxyAllow = FlagArray("proxy.allow", `Proxy allows connection from these addresses only (separated by comma - "10.13.0.1,10.13.0.0/16")`)
var proxyAPIAddr = flag.String("proxy.api-bind", ":8000", "HTTP proxy API address")
var proxyUpstreamURL = flag.String(
"proxy.upstream-url",
"",
`Upstream HTTPS proxy where to forward traffic (e.g. "http://superproxy.com:8080")`,
)
var proxyUser = flag.String("proxy.user", "", "HTTP proxy auth user")
var proxyPass = flag.String("proxy.pass", "", "HTTP proxy auth password")
var proxyCountry = flag.String("proxy.country", "", "HTTP proxy country targeting")
var upstreamConfigs = FlagUpstreamConfig()
var proxyMapPort = FlagArray(
"proxy.port-map",
`Explicitly map source port to destination port (separated by comma - "8443:443,18443:8443")`,
)

var stickyStoragePath = flag.String("stickiness-db-path", proxy.MemoryStorage, "Path to the database for stickiness mapping")

var filterHostnames = FlagArray(
"filter.hostnames",
`Explicitly forward just several hostnames (separated by comma - "ipinfo.io,ipify.org")`,
)
var filterZones = FlagArray(
"filter.zones",
`Explicitly forward just several DNS zones. A zone of "example.com" matches "example.com" and all of its subdomains. (separated by comma - "ipinfo.io,ipify.org")`,
)
var excludeHostnames = FlagArray(
"exclude.hostnames",
`Exclude from forwarding several hostnames (separated by comma - "ipinfo.io,ipify.org")`,
)
var excludeZones = FlagArray(
"exclude.zones",
`Exclude from forwarding several DNS zones. A zone of "example.com" matches "example.com" and all of its subdomains. (separated by comma - "ipinfo.io,ipify.org")`,
)

var enableDomainTracer = flag.Bool("enable-domain-tracer", false, "Enable tracing domain names from requests")

type domainTracker interface {
Expand All @@ -79,12 +54,6 @@ func main() {
flag.Parse()
setLoggerFormat(*logLevel)

dialerUpstreamURL, err := url.Parse(*proxyUpstreamURL)
if err != nil {
_ = log.Criticalf("Invalid upstream URL: %s", *proxyUpstreamURL)
os.Exit(1)
}

sm, err := proxy.NewStickyMapper(*stickyStoragePath)
if err != nil {
_ = log.Criticalf("Failed to create sticky mapper, %v", err)
Expand All @@ -99,35 +68,41 @@ func main() {
apiServer := api.NewServer(*proxyAPIAddr, sm, domainTracer)
go apiServer.ListenAndServe()

dialerUpstream := proxy.NewDialerHTTPConnect(proxy.DialerDirect, dialerUpstreamURL.Host, *proxyUser, *proxyPass, *proxyCountry)

var dialer netproxy.Dialer
if len(*filterHostnames) > 0 || len(*filterZones) > 0 {
dialerUpstreamFiltered := netproxy.NewPerHost(proxy.DialerDirect, dialerUpstream)
for _, host := range *filterHostnames {
log.Infof("Redirecting: %s -> %s", host, dialerUpstreamURL)
dialerUpstreamFiltered.AddHost(host)
}
for _, zone := range *filterZones {
log.Infof("Redirecting: *.%s -> %s", zone, dialerUpstreamURL)
dialerUpstreamFiltered.AddZone(zone)
for _, upstreamConfig := range upstreamConfigs.configs {
var dialerDefault netproxy.Dialer = proxy.DialerDirect
if dialer != nil {
dialerDefault = dialer
}
dialer = dialerUpstreamFiltered
} else {
dialer = dialerUpstream
log.Infof("Redirecting: * -> %s", dialerUpstreamURL)
}
if len(*excludeHostnames) > 0 || len(*excludeZones) > 0 {
dialerUpstreamExcluded := netproxy.NewPerHost(dialer, proxy.DialerDirect)
for _, host := range *excludeHostnames {
log.Infof("Excluding: %s -> %s", host, dialerUpstreamURL)
dialerUpstreamExcluded.AddHost(host)
dialerUpstream := proxy.NewDialerHTTPConnect(proxy.DialerDirect, upstreamConfig.url, upstreamConfig.user, upstreamConfig.password, upstreamConfig.country)

if len(upstreamConfig.filterHostnames) > 0 || len(upstreamConfig.filterZones) > 0 {
dialerUpstreamFiltered := netproxy.NewPerHost(dialerDefault, dialerUpstream)
for _, host := range upstreamConfig.filterHostnames {
log.Infof("Redirecting: %s -> %s", host, upstreamConfig.url)
dialerUpstreamFiltered.AddHost(host)
}
for _, zone := range upstreamConfig.filterZones {
log.Infof("Redirecting: *.%s -> %s", zone, upstreamConfig.url)
dialerUpstreamFiltered.AddZone(zone)
}
dialer = dialerUpstreamFiltered
} else {
dialer = dialerUpstream
log.Infof("Redirecting: * -> %s", upstreamConfig.url)
}
for _, zone := range *excludeZones {
log.Infof("Excluding: *.%s -> %s", zone, dialerUpstreamURL)
dialerUpstreamExcluded.AddZone(zone)
if len(upstreamConfig.excludeHostnames) > 0 || len(upstreamConfig.excludeZones) > 0 {
dialerUpstreamExcluded := netproxy.NewPerHost(dialer, dialerDefault)
for _, host := range upstreamConfig.excludeHostnames {
log.Infof("Excluding: %s -> %s", host, upstreamConfig.url)
dialerUpstreamExcluded.AddHost(host)
}
for _, zone := range upstreamConfig.excludeZones {
log.Infof("Excluding: *.%s -> %s", zone, upstreamConfig.url)
dialerUpstreamExcluded.AddZone(zone)
}
dialer = dialerUpstreamExcluded
}
dialer = dialerUpstreamExcluded
}

allowedSubnets, allowedIPs, err := parseAllowedAddresses(*proxyAllow)
Expand All @@ -140,7 +115,7 @@ func main() {
_ = log.Criticalf("Failed to parse port map: %v", err)
os.Exit(1)
}
proxyServer := proxy.NewServer(allowedSubnets, allowedIPs, dialer, dialerUpstreamURL, sm, domainTracer, portMap)
proxyServer := proxy.NewServer(allowedSubnets, allowedIPs, dialer, sm, domainTracer, portMap)

var wg sync.WaitGroup
for p := range portMap {
Expand Down Expand Up @@ -218,3 +193,112 @@ func (flag *flagArray) Set(s string) error {
})
return nil
}

type flagUpstreamConfig struct {
url *url.URL
user string
password string
country string
filterHostnames flagArray
filterZones flagArray
excludeHostnames flagArray
excludeZones flagArray
}

// FlagUpstreamConfig defines list of configure upstream proxies.
func FlagUpstreamConfig() *flagUpstreamConfigs {
fuc := &flagUpstreamConfigs{
configs: []flagUpstreamConfig{
{},
},
configCurrent: 0,
}
flag.Func(
"proxy.upstream-url",
`Upstream HTTPS proxy where to forward traffic (e.g. "http://superproxy.com:8080")`,
fuc.parseUpstreamUrl,
)
flag.Func("proxy.user", "HTTPS proxy auth user", fuc.parseUpstreamUser)
flag.Func("proxy.pass", "HTTP proxy auth password", fuc.parseUpstreamPass)
flag.Func("proxy.country", "HTTP proxy country targeting", fuc.parseUpstreamCountry)
flag.Func(
"filter.hostnames",
`Explicitly forward just several hostnames (separated by comma - "ipinfo.io,ipify.org")`,
fuc.parseFilterHostnames,
)
flag.Func(
"filter.zones",
`Explicitly forward just several DNS zones. A zone of "example.com" matches "example.com" and all of its subdomains. (separated by comma - "ipinfo.io,ipify.org")`,
fuc.parseFilterZones,
)
flag.Func(
"exclude.hostnames",
`Exclude from forwarding several hostnames (separated by comma - "ipinfo.io,ipify.org")`,
fuc.parseExcludeHostnames,
)
flag.Func(
"exclude.zones",
`Exclude from forwarding several DNS zones. A zone of "example.com" matches "example.com" and all of its subdomains. (separated by comma - "ipinfo.io,ipify.org")`,
fuc.parseExcludeZones,
)

return fuc
}

type flagUpstreamConfigs struct {
configs []flagUpstreamConfig
configCurrent int
}

func (fuc *flagUpstreamConfigs) current() *flagUpstreamConfig {
return &fuc.configs[fuc.configCurrent]
}

func (fuc *flagUpstreamConfigs) increment() {
fuc.configs = append(fuc.configs, flagUpstreamConfig{})
fuc.configCurrent++
}

func (fuc *flagUpstreamConfigs) parseUpstreamUrl(s string) error {
upstreamUrl, err := url.Parse(s)
if err != nil {
return fmt.Errorf("invalid upstream URL: %s. %v", s, err)
}

if fuc.configs[fuc.configCurrent].url != nil {
fuc.increment()
}
fuc.configs[fuc.configCurrent].url = upstreamUrl
return nil
}

func (fuc *flagUpstreamConfigs) parseUpstreamUser(s string) error {
fuc.configs[fuc.configCurrent].user = s
return nil
}

func (fuc *flagUpstreamConfigs) parseUpstreamPass(s string) error {
fuc.configs[fuc.configCurrent].password = s
return nil
}

func (fuc *flagUpstreamConfigs) parseUpstreamCountry(s string) error {
fuc.configs[fuc.configCurrent].country = s
return nil
}

func (fuc *flagUpstreamConfigs) parseFilterHostnames(s string) error {
return fuc.configs[fuc.configCurrent].filterHostnames.Set(s)
}

func (fuc *flagUpstreamConfigs) parseFilterZones(s string) error {
return fuc.configs[fuc.configCurrent].filterZones.Set(s)
}

func (fuc *flagUpstreamConfigs) parseExcludeHostnames(s string) error {
return fuc.configs[fuc.configCurrent].excludeHostnames.Set(s)
}

func (fuc *flagUpstreamConfigs) parseExcludeZones(s string) error {
return fuc.configs[fuc.configCurrent].excludeZones.Set(s)
}
26 changes: 18 additions & 8 deletions proxy/dialer_http_connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,33 @@ package proxy

import (
"bufio"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/url"

"github.com/pkg/errors"
netproxy "golang.org/x/net/proxy"
)

// NewDialerHTTPConnect returns a new Dialer that dials through the provided
// proxy server's network and address.
func NewDialerHTTPConnect(forwardDialer netproxy.Dialer, forwardAddress, user, pass, country string) *dialerHTTPConnect {
func NewDialerHTTPConnect(forwardDialer netproxy.Dialer, forwardUrl *url.URL, user, pass, country string) *dialerHTTPConnect {
return &dialerHTTPConnect{
forwardDialer: forwardDialer,
forwardAddress: forwardAddress,
user: user,
pass: pass,
country: country,
forwardDialer: forwardDialer,
forwardURL: forwardUrl,
user: user,
pass: pass,
country: country,
}
}

type dialerHTTPConnect struct {
forwardDialer netproxy.Dialer
forwardAddress string
forwardURL *url.URL
user, pass, country string
}

Expand All @@ -55,7 +57,15 @@ type Connection struct {

// Dial makes actual connection to specified address through intermediate HTTP proxy
func (dialer *dialerHTTPConnect) Dial(network, address string) (net.Conn, error) {
conn, err := dialer.forwardDialer.Dial(network, dialer.forwardAddress)
conn, err := dialer.forwardDialer.Dial(network, dialer.forwardURL.Host)

if dialer.forwardURL.Scheme == "https" {
tlsConn := tls.Client(conn.(net.Conn), &tls.Config{ServerName: dialer.forwardURL.Hostname()})
if err := tlsConn.Handshake(); err != nil {
return nil, errors.Wrap(err, "failed to perform TLS handshake")
}
conn = tlsConn
}

return &Connection{
Conn: conn,
Expand Down
Loading

0 comments on commit 3335f4c

Please sign in to comment.