Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TCP proxy: Get remote address? #392

Open
tino opened this issue Nov 20, 2017 · 4 comments
Open

TCP proxy: Get remote address? #392

tino opened this issue Nov 20, 2017 · 4 comments
Milestone

Comments

@tino
Copy link
Contributor

tino commented Nov 20, 2017

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
Copy link
Contributor

magiconair commented Nov 20, 2017

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 :)

@magiconair magiconair added this to the Unplanned milestone Nov 20, 2017
@tino
Copy link
Contributor Author

tino commented Nov 28, 2017

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

@KEZHwMlXV1vFzs6QvY8v5WjX5
Copy link

KEZHwMlXV1vFzs6QvY8v5WjX5 commented Jan 21, 2019

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" <[email protected]>
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
Copy link

KEZHwMlXV1vFzs6QvY8v5WjX5 commented Jan 21, 2019

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants