From 50783d52763c9605c58df70706f160ed4f052db0 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 20 Nov 2019 17:14:42 -0500 Subject: [PATCH] fix #561, take 2 --- irc/client.go | 23 ++++++++++++++--------- irc/config.go | 18 +++++++++++++----- irc/gateways.go | 45 +++++++++++++++++++++++++++++++++++++++++++-- irc/server.go | 13 +++++++++++-- oragono.yaml | 4 ++++ 5 files changed, 85 insertions(+), 18 deletions(-) diff --git a/irc/client.go b/irc/client.go index 3b425287..95698fcb 100644 --- a/irc/client.go +++ b/irc/client.go @@ -190,7 +190,7 @@ type ClientDetails struct { } // RunClient sets up a new client and runs its goroutine. -func (server *Server) RunClient(conn clientConn) { +func (server *Server) RunClient(conn clientConn, proxyLine string) { var isBanned bool var banMsg string var realIP net.IP @@ -278,7 +278,7 @@ func (server *Server) RunClient(conn clientConn) { client.proxiedIP = session.proxiedIP server.stats.Add() - client.run(session) + client.run(session, proxyLine) } func (client *Client) doIdentLookup(conn net.Conn) { @@ -371,11 +371,9 @@ func (client *Client) t(originalString string) string { return languageManager.Translate(client.Languages(), originalString) } -// -// command goroutine -// - -func (client *Client) run(session *Session) { +// main client goroutine: read lines and execute the corresponding commands +// `proxyLine` is the PROXY-before-TLS line, if there was one +func (client *Client) run(session *Session, proxyLine string) { defer func() { if r := recover(); r != nil { @@ -414,7 +412,14 @@ func (client *Client) run(session *Session) { for { maxlenRest := session.MaxlenRest() - line, err := session.socket.Read() + var line string + var err error + if proxyLine == "" { + line, err = session.socket.Read() + } else { + line = proxyLine // pretend we're just now receiving the proxy-before-TLS line + proxyLine = "" + } if err != nil { quitMessage := "connection closed" if err == errReadQ { @@ -483,7 +488,7 @@ func (client *Client) run(session *Session) { break } else if session.client != client { // bouncer reattach - go session.client.run(session) + go session.client.run(session, "") break } } diff --git a/irc/config.go b/irc/config.go index 42be0a1d..5fb5ffe2 100644 --- a/irc/config.go +++ b/irc/config.go @@ -39,8 +39,9 @@ import ( // TLSListenConfig defines configuration options for listening on TLS. type TLSListenConfig struct { - Cert string - Key string + Cert string + Key string + Proxy bool } // This is the YAML-deserializable type of the value of the `Server.Listeners` map @@ -53,9 +54,10 @@ type listenerConfigBlock struct { // listenerConfig is the config governing a particular listener (bound address), // in particular whether it has TLS or Tor (or both) enabled. type listenerConfig struct { - TLSConfig *tls.Config - IsTor bool - IsSTSOnly bool + TLSConfig *tls.Config + IsTor bool + IsSTSOnly bool + IsTLSProxy bool } type AccountConfig struct { @@ -529,6 +531,7 @@ func (conf *Config) prepareListeners() (err error) { return err } lconf.TLSConfig = tlsConfig + lconf.IsTLSProxy = block.TLS.Proxy } listeners[addr] = lconf } @@ -848,5 +851,10 @@ func LoadConfig(filename string) (config *Config, err error) { return nil, err } + err = config.prepareListeners() + if err != nil { + return nil, fmt.Errorf("failed to prepare listeners: %v", err) + } + return config, nil } diff --git a/irc/gateways.go b/irc/gateways.go index 6b47d01d..5d1ddff6 100644 --- a/irc/gateways.go +++ b/irc/gateways.go @@ -10,6 +10,7 @@ import ( "fmt" "net" "strings" + "time" "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/utils" @@ -20,6 +21,13 @@ var ( errBadProxyLine = errors.New("Invalid PROXY/WEBIRC command") ) +const ( + // https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + // "a 108-byte buffer is always enough to store all the line and a trailing zero + // for string processing." + maxProxyLineLen = 107 +) + type webircConfig struct { PasswordString string `yaml:"password"` Password []byte `yaml:"password-bytes"` @@ -75,10 +83,10 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo client.stateMutex.Lock() defer client.stateMutex.Unlock() - session.proxiedIP = parsedProxiedIP client.proxiedIP = parsedProxiedIP - session.rawHostname = rawHostname client.rawHostname = rawHostname + session.proxiedIP = parsedProxiedIP + session.rawHostname = rawHostname client.cloakedHostname = cloakedHostname // nickmask will be updated when the client completes registration // set tls info @@ -118,3 +126,36 @@ func handleProxyCommand(server *Server, client *Client, session *Session, line s return errBadGatewayAddress } } + +// read a PROXY line one byte at a time, to ensure we don't read anything beyond +// that into a buffer, which would break the TLS handshake +func readRawProxyLine(conn net.Conn) (result string) { + // normally this is covered by ping timeouts, but we're doing this outside + // of the normal client goroutine: + conn.SetDeadline(time.Now().Add(time.Minute)) + defer conn.SetDeadline(time.Time{}) + + var buf [maxProxyLineLen]byte + oneByte := make([]byte, 1) + i := 0 + for i < maxProxyLineLen { + n, err := conn.Read(oneByte) + if err != nil { + return + } else if n == 1 { + buf[i] = oneByte[0] + if buf[i] == '\n' { + candidate := string(buf[0 : i+1]) + if strings.HasPrefix(candidate, "PROXY") { + return candidate + } else { + return + } + } + i += 1 + } + } + + // no \r\n, fail out + return +} diff --git a/irc/server.go b/irc/server.go index 31b8b24e..01e15665 100644 --- a/irc/server.go +++ b/irc/server.go @@ -307,6 +307,15 @@ func (server *Server) createListener(addr string, conf listenerConfig, bindMode listener.Close() return } else if err == nil { + var proxyLine string + if conf.IsTLSProxy { + proxyLine = readRawProxyLine(conn) + if proxyLine == "" { + server.logger.Error("internal", "bad TLS-proxy line from", addr) + conn.Close() + continue + } + } if conf.TLSConfig != nil { conn = tls.Server(conn, conf.TLSConfig) } @@ -315,7 +324,7 @@ func (server *Server) createListener(addr string, conf listenerConfig, bindMode Config: conf, } // hand off the connection - go server.RunClient(newConn) + go server.RunClient(newConn, proxyLine) } else { server.logger.Error("internal", "accept error", addr, err.Error()) } @@ -868,7 +877,7 @@ func (server *Server) loadDatastore(config *Config) error { func (server *Server) setupListeners(config *Config) (err error) { logListener := func(addr string, config listenerConfig) { server.logger.Info("listeners", - fmt.Sprintf("now listening on %s, tls=%t, tor=%t.", addr, (config.TLSConfig != nil), config.IsTor), + fmt.Sprintf("now listening on %s, tls=%t, tlsproxy=%t, tor=%t.", addr, (config.TLSConfig != nil), config.IsTLSProxy, config.IsTor), ) } diff --git a/oragono.yaml b/oragono.yaml index d6716738..9d09c523 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -30,6 +30,10 @@ server: tls: key: tls.key cert: tls.crt + # 'proxy' should typically be false. It's only for Kubernetes-style load + # balancing that does not terminate TLS, but sends an initial PROXY line + # in plaintext. + proxy: false # Example of a Unix domain socket for proxying: # "/tmp/oragono_sock":