-
Notifications
You must be signed in to change notification settings - Fork 619
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
Comments
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:
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. |
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?The text was updated successfully, but these errors were encountered: