Open tino opened 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 :)
That would be awesome! I'm up for working on a PR, but I've got no idea where to start atm ☺️
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.
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.
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?