Skip to content

Commit

Permalink
Merge pull request #27 from mysteriumnetwork/feature/non-sni-support
Browse files Browse the repository at this point in the history
Handle HTTPS requests without SNI
  • Loading branch information
Waldz authored Dec 20, 2022
2 parents 302d7f7 + 4341003 commit 5edb42d
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 40 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ Let's assume:
1. Run forwarder as a Docker container:
```bash
docker run -d --restart=always --name forwarder --network host --cap-add NET_ADMIN mysteriumnetwork/openvpn-forwarder \
--proxy.bind=127.0.0.1:8443 \
--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"
```

2. Redirect HTTP ports to forwarder:
```bash
iptables -t nat -A PREROUTING -p tcp -m multiport --dports 80,443 -j DNAT --to-destination 127.0.0.1:8443
iptables -t nat -A PREROUTING -p tcp -m multiport --dports 80,443 -j REDIRECT --to-ports 8443
```

3. Forwarder redirects HTTP traffic to upstream HTTPS proxy (this case just hostname 'ipinfo.io'):
Expand Down Expand Up @@ -150,7 +151,8 @@ If you need to forward non standard port too, the following steps required:
1. Start OpenVPN forwarder with the `--proxy.port-map` flag:
```bash
docker run -d --restart=always --name forwarder --network host --cap-add NET_ADMIN mysteriumnetwork/openvpn-forwarder \
--proxy.bind=127.0.0.1:8443 \
--proxy.bind=0.0.0.0:8443 \
--proxy.allow=0.0.0.0/0 \
--proxy.upstream-url="https://superproxy.com:443" \
--proxy.port-map=18443:8443,1234:1234
```
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ services:
command: >
--log.level=trace
--proxy.bind=:8443
--proxy.allow=0.0.0.0/0
--proxy.upstream-url=http://superproxy.com:8080
--proxy.user=
--proxy.pass=
Expand Down
24 changes: 23 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (

var logLevel = flag.String("log.level", log.InfoStr, "Set the logging level (trace, debug, info, warn, error, critical)")
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",
Expand Down Expand Up @@ -129,12 +130,17 @@ func main() {
dialer = dialerUpstreamExcluded
}

allowedSubnets, allowedIPs, err := parseAllowedAddresses(*proxyAllow)
if err != nil {
_ = log.Criticalf("Failed to parse allowed addresses: %v", err)
os.Exit(1)
}
portMap, err := parsePortMap(*proxyMapPort, *proxyAddr)
if err != nil {
_ = log.Criticalf("Failed to parse port map: %v", err)
os.Exit(1)
}
proxyServer := proxy.NewServer(dialer, dialerUpstreamURL, sm, domainTracer, portMap)
proxyServer := proxy.NewServer(allowedSubnets, allowedIPs, dialer, dialerUpstreamURL, sm, domainTracer, portMap)

var wg sync.WaitGroup
for p := range portMap {
Expand All @@ -159,6 +165,22 @@ func setLoggerFormat(levelStr string) {
log.ReplaceLogger(logger)
}

func parseAllowedAddresses(addresses flagArray) (subnets []*net.IPNet, ips []net.IP, _ error) {
for _, address := range addresses {
if _, subnet, err := net.ParseCIDR(address); err == nil {
subnets = append(subnets, subnet)
continue
}
if ip := net.ParseIP(address); ip != nil {
ips = append(ips, ip)
continue
}
return nil, nil, errors.Errorf("invalid subnet or IP: %s", address)
}

return subnets, ips, nil
}

func parsePortMap(ports flagArray, proxyAddr string) (map[string]string, error) {
_, port, err := net.SplitHostPort(proxyAddr)
if err != nil {
Expand Down
135 changes: 101 additions & 34 deletions proxy/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ type domainTracker interface {
}

type proxyServer struct {
dialer netproxy.Dialer
sm StickyMapper
dt domainTracker
upstream *url.URL
portMap map[string]string
allowedSubnets []*net.IPNet
allowedIPs []net.IP
dialer netproxy.Dialer
sm StickyMapper
dt domainTracker
upstream *url.URL
portMap map[string]string
}

// StickyMapper represent connection stickiness storage.
Expand All @@ -56,13 +58,23 @@ type StickyMapper interface {
}

// NewServer returns new instance of HTTP transparent proxy server
func NewServer(upstreamDialer netproxy.Dialer, upstreamHost *url.URL, mapper StickyMapper, dt domainTracker, portMap map[string]string) *proxyServer {
func NewServer(
allowedSubnets []*net.IPNet,
allowedIPs []net.IP,
upstreamDialer netproxy.Dialer,
upstreamHost *url.URL,
mapper StickyMapper,
dt domainTracker,
portMap map[string]string,
) *proxyServer {
return &proxyServer{
dialer: upstreamDialer,
sm: mapper,
dt: dt,
upstream: upstreamHost,
portMap: portMap,
allowedSubnets: allowedSubnets,
allowedIPs: allowedIPs,
dialer: upstreamDialer,
sm: mapper,
dt: dt,
upstream: upstreamHost,
portMap: portMap,
}
}

Expand Down Expand Up @@ -97,45 +109,72 @@ func (s *proxyServer) handler(l net.Listener, f func(c *Context)) {
if !ok {
err = fmt.Errorf("non-TCP connection: %T", connMux.Conn)
}
clientAddr, ok := connTCP.RemoteAddr().(*net.TCPAddr)
if !ok {
err = fmt.Errorf("non-TCP address: %T", connTCP.RemoteAddr())
continue
}
if err != nil {
_ = log.Errorf("Error accepting new connection. %v", err)
s.logError(fmt.Sprintf("Error accepting new connection. %v", err), &c)
continue
}

clientAddrAllowed := false
for _, subnet := range s.allowedSubnets {
if subnet.Contains(clientAddr.IP) {
clientAddrAllowed = true
break
}
}
for _, ip := range s.allowedIPs {
if ip.Equal(clientAddr.IP) {
clientAddrAllowed = true
break
}
}
if !clientAddrAllowed {
s.logWarn(fmt.Sprintf("Access restricted from address %s", clientAddr.IP.String()), &c)
continue
}

c.connOriginalDst, err = getOriginalDst(connTCP)
if err != nil {
_ = log.Errorf("Error recovering original destination address. %v", err)
if c.connOriginalDst.String() == c.conn.LocalAddr().String() {
c.connOriginalDst = nil
}

go func() {
f(&c)
c.conn.Close()

if c.connOriginalDst == nil {
s.logWarn("Failure recovering original destination address. Are you redirecting from same host network?", &c)
}
}()
}
}

func (s *proxyServer) serveHTTP(c *Context) {
req, err := http.ReadRequest(bufio.NewReader(c.conn))
if err != nil {
_ = log.Errorf("Failed to read HTTP request: %v", err)
s.logAccess(fmt.Sprintf("Failed to accept new HTTP request: %v", err), c)
return
}

c.destinationHost = req.Host
c.destinationAddress = s.authorityAddr("http", c.destinationHost)
s.accessLog("HTTP request", c)
s.logAccess("HTTP request", c)

conn, err := s.connectTo(c.conn, c.destinationAddress)
if err != nil {
_ = log.Errorf("Error establishing connection to %s: %v", c.destinationAddress, err)
s.logError(fmt.Sprintf("Failed to establishing connection. %v", err), c)
return
}
defer conn.Close()

if req.Method == http.MethodConnect {
c.conn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
} else if err := req.Write(conn); err != nil {
_ = log.Errorf("Failed to forward HTTP request to %s: %v", c.destinationAddress, err)
s.logError(fmt.Sprintf("Failed to forward HTTP request. %v", err), c)
return
}

Expand Down Expand Up @@ -164,35 +203,39 @@ func (s *proxyServer) serveTLS(c *Context) {
// For some malformed TLS connection vhost.TLS could panic.
// We don't care about a single failed request, service should keep working.
if r := recover(); r != nil {
_ = log.Error("Recovered panic in serveTLS", r)
s.logError(fmt.Sprintf("Recovered panic in serveTLS. %v", r), c)
}
}()

tlsConn, err := vhost.TLS(c.conn)
if err != nil {
_ = log.Errorf("Error accepting new TLS connection - %v", err)
s.logError(fmt.Sprintf("Failed to accept new TLS request. %v", err), c)
return
}
defer tlsConn.Close()

if tlsConn.Host() == "" {
_ = log.Error("Cannot support non-SNI enabled TLS sessions")
return
}
if tlsConn.Host() != "" {
_, port, err := net.SplitHostPort(tlsConn.LocalAddr().String())
if err != nil {
s.logError("Cannot parse local address", c)
return
}

_, port, err := net.SplitHostPort(tlsConn.LocalAddr().String())
if err != nil {
_ = log.Error("Cannot parse local address")
c.destinationHost = tlsConn.Host() + ":" + port
c.destinationAddress = s.authorityAddr("https", c.destinationHost)
} else if c.connOriginalDst != nil {
c.destinationHost = ""
c.destinationAddress = c.connOriginalDst.String()
s.logWarn("Cannon parse SNI in TLS request", c)
} else {
s.logError("Cannot support non-SNI enabled TLS sessions", c)
return
}

c.destinationHost = tlsConn.Host() + ":" + port
c.destinationAddress = s.authorityAddr("https", c.destinationHost)
s.accessLog("HTTPS request", c)
s.logAccess("HTTPS request", c)

conn, err := s.connectTo(c.conn, c.destinationAddress)
if err != nil {
_ = log.Errorf("Error establishing connection to %s: %v", c.destinationAddress, err)
s.logError(fmt.Sprintf("Failed to establishing connection. %v", err), c)
return
}
defer conn.Close()
Expand Down Expand Up @@ -291,13 +334,37 @@ func getSockOpt(s int, level int, optname int, optval unsafe.Pointer, optlen *ui
return
}

func (s *proxyServer) accessLog(message string, c *Context) {
func (s *proxyServer) logAccess(message string, c *Context) {
log.Tracef(
"%s [client_addr=%s, dest_addr=%s, original_dest_addr=%s destination_host=%s, destination_addr=%s]",
message,
c.conn.RemoteAddr().String(),
c.conn.LocalAddr().String(),
c.connOriginalDst,
c.connOriginalDst.String(),
c.destinationHost,
c.destinationAddress,
)
}

func (s *proxyServer) logError(message string, c *Context) {
_ = log.Errorf(
"%s [client_addr=%s, dest_addr=%s, original_dest_addr=%s destination_host=%s, destination_addr=%s]",
message,
c.conn.RemoteAddr().String(),
c.conn.LocalAddr().String(),
c.connOriginalDst.String(),
c.destinationHost,
c.destinationAddress,
)
}

func (s *proxyServer) logWarn(message string, c *Context) {
_ = log.Warnf(
"%s [client_addr=%s, dest_addr=%s, original_dest_addr=%s destination_host=%s, destination_addr=%s]",
message,
c.conn.RemoteAddr().String(),
c.conn.LocalAddr().String(),
c.connOriginalDst.String(),
c.destinationHost,
c.destinationAddress,
)
Expand Down
4 changes: 2 additions & 2 deletions proxy/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func Test_Server_ServeHTTP(t *testing.T) {

req, _ := http.NewRequest("GET", "http://domain.com", nil)

proxyServer := NewServer(upstreamDialer, &url.URL{}, &stickyMapperStub{}, &noopTracer{}, nil)
proxyServer := NewServer(nil, []net.IP{net.ParseIP("::1")}, upstreamDialer, &url.URL{}, &stickyMapperStub{}, &noopTracer{}, nil)
proxyAddr := listenAndServe(proxyServer)

proxyURL, _ := url.Parse("http://" + proxyAddr)
Expand All @@ -67,7 +67,7 @@ func Test_Server_AuthHeaderAdded(t *testing.T) {

req, _ := http.NewRequest("GET", "http://domain.com", nil)

proxyServer := NewServer(upstreamDialer, &url.URL{}, &stickyMapperStub{}, &noopTracer{}, nil)
proxyServer := NewServer(nil, []net.IP{net.ParseIP("::1")}, upstreamDialer, &url.URL{}, &stickyMapperStub{}, &noopTracer{}, nil)
proxyAddr := listenAndServe(proxyServer)

proxyURL, _ := url.Parse("http://" + proxyAddr)
Expand Down

0 comments on commit 5edb42d

Please sign in to comment.