fabiolb / fabio

Consul Load-Balancing made simple
https://fabiolb.net
MIT License
7.26k stars 618 forks source link

TCP proxy: Get remote address? #392

Open tino opened 6 years ago

tino commented 6 years ago

I'm fairly new to working with raw TCP transmissions, but I've got a device that sends it what we need to parse. We do need the source ip to differentiate devices though. Is there a way to get the original address, not the one fabio is running on? Currently python's socketserver.BaseRequestHandler.client_address is set to the local address. Is perhaps the PROXY way that ELB uses available?

magiconair commented 6 years ago

I think the PROXY protocol is the most likely way to do this since the TCP proxy just shuffles a bunch of bytes. In that case this is #191 I can have another look at this since the TCP proxy has been around for a while :)

tino commented 6 years ago

That would be awesome! I'm up for working on a PR, but I've got no idea where to start atm ☺️

KEZHwMlXV1vFzs6QvY8v5WjX5 commented 5 years ago

this is what I tried using a patch from another issue from @lukas2511

looks like the core idea is doing this:

header := fmt.Sprintf("PROXY TCP4 %s %s %d %d\r\n", source_addr, dest_addr, source_port, dest_port)

From 7d4b660a500043aaa01947421253c7ee632ff3a3 Mon Sep 17 00:00:00 2001
From: "nobody" <does@notmatter.invalid>
Date: Fri, 18 Jan 2019 16:33:37 +0100
Subject: [PATCH] Added tcp+sni+proxy from https://github.com/lukas2511/fabio

---
 config/load.go               |   2 +-
 main.go                      |  13 +++
 proxy/tcp/sni_proxy_proxy.go | 168 +++++++++++++++++++++++++++++++++++
 3 files changed, 182 insertions(+), 1 deletion(-)
 create mode 100644 proxy/tcp/sni_proxy_proxy.go

diff --git a/config/load.go b/config/load.go
index 11f9b4d..7241f76 100644
--- a/config/load.go
+++ b/config/load.go
@@ -345,7 +345,7 @@ func parseListen(cfg map[string]string, cs map[string]CertSource, readTimeout, w
        case "proto":
            l.Proto = v
            switch l.Proto {
-           case "tcp", "tcp+sni", "http", "https", "grpc", "grpcs":
+           case "tcp", "tcp+sni", "tcp+sni+proxy", "http", "https", "grpc", "grpcs":
                // ok
            default:
                return Listen{}, fmt.Errorf("unknown protocol %q", v)
diff --git a/main.go b/main.go
index aa8e02c..8ebdba2 100644
--- a/main.go
+++ b/main.go
@@ -338,6 +338,19 @@ func startServers(cfg *config.Config) {
                    exit.Fatal("[FATAL] ", err)
                }
            }()
+       case "tcp+sni+proxy":
+           go func() {
+               h := &tcp.SNIProxyProxy{
+                   DialTimeout: cfg.Proxy.DialTimeout,
+                   Lookup:      lookupHostFn(cfg),
+                   Conn:        metrics.DefaultRegistry.GetCounter("tcp_sni.conn"),
+                   ConnFail:    metrics.DefaultRegistry.GetCounter("tcp_sni.connfail"),
+                   Noroute:     metrics.DefaultRegistry.GetCounter("tcp_sni.noroute"),
+               }
+               if err := proxy.ListenAndServeTCP(l, h, tlscfg); err != nil {
+                   exit.Fatal("[FATAL] ", err)
+               }
+           }()
        default:
            exit.Fatal("[FATAL] Invalid protocol ", l.Proto)
        }
diff --git a/proxy/tcp/sni_proxy_proxy.go b/proxy/tcp/sni_proxy_proxy.go
new file mode 100644
index 0000000..370bb5d
--- /dev/null
+++ b/proxy/tcp/sni_proxy_proxy.go
@@ -0,0 +1,168 @@
+package tcp
+
+import (
+   "bufio"
+   "fmt"
+   "io"
+   "log"
+   "net"
+   "time"
+
+   "github.com/fabiolb/fabio/metrics"
+   "github.com/fabiolb/fabio/route"
+)
+
+// SNIProxyProxy implements an SNI aware TCP proxy using Proxy protocol
+// which captures the TLS client hello, extracts the host name and uses it
+// for finding the upstream server. Then it sends a PROXY Protocol header,
+// replays the ClientHello message and copies data transparently allowing
+// to route a TLS connection based on the SNI header without decrypting it.
+type SNIProxyProxy struct {
+   // DialTimeout sets the timeout for establishing the outbound
+   // connection.
+   DialTimeout time.Duration
+
+   // Lookup returns a target host for the given server name.
+   // The proxy will panic if this value is nil.
+   Lookup func(host string) *route.Target
+
+   // Conn counts the number of connections.
+   Conn metrics.Counter
+
+   // ConnFail counts the failed upstream connection attempts.
+   ConnFail metrics.Counter
+
+   // Noroute counts the failed Lookup() calls.
+   Noroute metrics.Counter
+}
+
+func (p *SNIProxyProxy) ServeTCP(in net.Conn) error {
+   defer in.Close()
+
+   if p.Conn != nil {
+       p.Conn.Inc(1)
+   }
+
+   tlsReader := bufio.NewReader(in)
+   tlsHeaders, err := tlsReader.Peek(9)
+   if err != nil {
+       log.Print("[DEBUG] tcp+sni+proxy: TLS handshake failed (failed to peek data)")
+       if p.ConnFail != nil {
+           p.ConnFail.Inc(1)
+       }
+       return err
+   }
+
+   bufferSize, err := clientHelloBufferSize(tlsHeaders)
+   if err != nil {
+       log.Printf("[DEBUG] tcp+sni+proxy: TLS handshake failed (%s)", err)
+       if p.ConnFail != nil {
+           p.ConnFail.Inc(1)
+       }
+       return err
+   }
+
+   data := make([]byte, bufferSize)
+   _, err = io.ReadFull(tlsReader, data)
+   if err != nil {
+       log.Printf("[DEBUG] tcp+sni+proxy: TLS handshake failed (%s)", err)
+       if p.ConnFail != nil {
+           p.ConnFail.Inc(1)
+       }
+       return err
+   }
+
+   // readServerName wants only the handshake message so ignore the first
+   // 5 bytes which is the TLS record header
+   host, ok := readServerName(data[5:])
+   if !ok {
+       log.Print("[DEBUG] tcp+sni+proxy: TLS handshake failed (unable to parse client hello)")
+       if p.ConnFail != nil {
+           p.ConnFail.Inc(1)
+       }
+       return nil
+   }
+
+   if host == "" {
+       log.Print("[DEBUG] tcp+sni+proxy: server_name missing")
+       if p.ConnFail != nil {
+           p.ConnFail.Inc(1)
+       }
+       return nil
+   }
+
+   t := p.Lookup(host)
+   if t == nil {
+       if p.Noroute != nil {
+           p.Noroute.Inc(1)
+       }
+       return nil
+   }
+   addr := t.URL.Host
+
+   if t.AccessDeniedTCP(in) {
+       return nil
+   }
+
+   out, err := net.DialTimeout("tcp", addr, p.DialTimeout)
+   if err != nil {
+       log.Print("[WARN] tcp+sni+proxy: cannot connect to upstream ", addr)
+       if p.ConnFail != nil {
+           p.ConnFail.Inc(1)
+       }
+       return err
+   }
+   defer out.Close()
+
+   // send PROXY protocol header
+   source_addr, source_port, err := net.SplitHostPort(in.RemoteAddr().String())
+   if err != nil {
+       log.Print("[WARN] tcp+sni+proxy: parsing source address has failed. ", err)
+       return err
+   }
+
+   dest_addr, dest_port, err := net.SplitHostPort(addr)
+   if err != nil {
+       log.Print("[WARN] tcp+sni+proxy: parsing destination address has failed. ", err)
+       return err
+   }
+
+   header := fmt.Sprintf("PROXY TCP4 %s %s %d %d\r\n", source_addr, dest_addr, source_port, dest_port)
+   _, err = out.Write([]byte(header))
+   if err != nil {
+       log.Print("[WARN] tcp+sni+proxy: sending PROXY protocol header failed. ", err)
+       return err
+   }
+
+   // write the data already read from the connection
+   n, err := out.Write(data)
+   if err != nil {
+       log.Print("[WARN] tcp+sni+proxy: copy client hello failed. ", err)
+       if p.ConnFail != nil {
+           p.ConnFail.Inc(1)
+       }
+       return err
+   }
+
+   errc := make(chan error, 2)
+   cp := func(dst io.Writer, src io.Reader, c metrics.Counter) {
+       errc <- copyBuffer(dst, src, c)
+   }
+
+   // rx measures the traffic to the upstream server (in <- out)
+   // tx measures the traffic from the upstream server (out <- in)
+   rx := metrics.DefaultRegistry.GetCounter(t.TimerName + ".rx")
+   tx := metrics.DefaultRegistry.GetCounter(t.TimerName + ".tx")
+
+   // we've received the ClientHello already
+   rx.Inc(int64(n))
+
+   go cp(in, out, rx)
+   go cp(out, in, tx)
+   err = <-errc
+   if err != nil && err != io.EOF {
+       log.Print("[WARN]: tcp+sni+proxy:  ", err)
+       return err
+   }
+   return nil
+}
-- 
2.20.1

unfortunately the nginx ingress controller we used does not seem to understand what fabio sends him. A test using haproxy with send-proxy option was working.

KEZHwMlXV1vFzs6QvY8v5WjX5 commented 5 years ago

ok this could be it

-       header := fmt.Sprintf("PROXY TCP4 %s %s %d %d\r\n", source_addr, dest_addr, source_port, dest_port)
+       header := fmt.Sprintf("PROXY TCP4 %s %s %s %s\r\n", source_addr, dest_addr, source_port, dest_port)

works for me now.