summaryrefslogtreecommitdiff
path: root/sni.go
diff options
context:
space:
mode:
authorDavid Anderson <[email protected]>2017-07-06 01:08:29 -0700
committerDave Anderson <[email protected]>2017-07-06 21:36:14 -0700
commitc6a0996ce0f3db7b5c3e16e04c9e664936077c97 (patch)
tree74cda35a42bfd547c07032e9c37af6ddf4e2591e /sni.go
parent815c9425f1ad46ffd3a3fb1bbefc05440072e4a4 (diff)
Support configurable routing of ACME tls-sni-01 challenges.
By design, the tls-sni-01 challenge does not reveal information about the domain being verified, so the proxy cannot "naively" route such requests. Instead, it probes the Targets of all SNI routes, looking for one that responds plausibly to the challenge hostname, and routes the client connection to that. ACME support can be turned off by inserting AddStopAcmeSearch in the route chain. Subsequently registered SNI routes will not be probed by ACME challenges.
Diffstat (limited to 'sni.go')
-rw-r--r--sni.go95
1 files changed, 95 insertions, 0 deletions
diff --git a/sni.go b/sni.go
index f0128bf..50ab599 100644
--- a/sni.go
+++ b/sni.go
@@ -17,9 +17,11 @@ package tcpproxy
import (
"bufio"
"bytes"
+ "context"
"crypto/tls"
"io"
"net"
+ "strings"
)
// AddSNIRoute appends a route to the ipPort listener that says if the
@@ -27,11 +29,31 @@ import (
// dest. If it doesn't match, rule processing continues for any
// additional routes on ipPort.
//
+// By default, the proxy will route all ACME tls-sni-01 challenges
+// received on ipPort to all SNI dests. You can disable ACME routing
+// with AddStopACMESearch.
+//
// The ipPort is any valid net.Listen TCP address.
func (p *Proxy) AddSNIRoute(ipPort, sni string, dest Target) {
+ cfg := p.configFor(ipPort)
+ if !cfg.stopACME {
+ if len(cfg.acmeTargets) == 0 {
+ p.addRoute(ipPort, &acmeMatch{cfg})
+ }
+ cfg.acmeTargets = append(cfg.acmeTargets, dest)
+ }
+
p.addRoute(ipPort, sniMatch{sni, dest})
}
+// AddStopACMESearch prevents ACME probing of subsequent SNI routes.
+// Any ACME challenges on ipPort for SNI routes previously added
+// before this call will still be proxied to all possible SNI
+// backends.
+func (p *Proxy) AddStopACMESearch(ipPort string) {
+ p.configFor(ipPort).stopACME = true
+}
+
type sniMatch struct {
sni string
target Target
@@ -44,6 +66,79 @@ func (m sniMatch) match(br *bufio.Reader) Target {
return nil
}
+// acmeMatch matches "*.acme.invalid" ACME tls-sni-01 challenges and
+// searches for a Target in cfg.acmeTargets that has the challenge
+// response.
+type acmeMatch struct {
+ cfg *config
+}
+
+func (m *acmeMatch) match(br *bufio.Reader) Target {
+ sni := clientHelloServerName(br)
+ if !strings.HasSuffix(sni, ".acme.invalid") {
+ return nil
+ }
+
+ // TODO: cache. ACME issuers will hit multiple times in a short
+ // burst for each issuance event. A short TTL cache + singleflight
+ // should have an excellent hit rate.
+ // TODO: maybe an acme-specific timeout as well?
+ // TODO: plumb context upwards?
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ ch := make(chan Target, len(m.cfg.acmeTargets))
+ for _, target := range m.cfg.acmeTargets {
+ go tryACME(ctx, ch, target, sni)
+ }
+ for range m.cfg.acmeTargets {
+ if target := <-ch; target != nil {
+ return target
+ }
+ }
+
+ // No target was happy with the provided challenge.
+ return nil
+}
+
+func tryACME(ctx context.Context, ch chan<- Target, dest Target, sni string) {
+ var ret Target
+ defer func() { ch <- ret }()
+
+ conn, targetConn := net.Pipe()
+ defer conn.Close()
+ go dest.HandleConn(targetConn)
+
+ deadline, ok := ctx.Deadline()
+ if ok {
+ conn.SetDeadline(deadline)
+ }
+
+ client := tls.Client(conn, &tls.Config{
+ ServerName: sni,
+ InsecureSkipVerify: true,
+ })
+ if err := client.Handshake(); err != nil {
+ // TODO: log?
+ return
+ }
+ certs := client.ConnectionState().PeerCertificates
+ if len(certs) == 0 {
+ // TODO: log?
+ return
+ }
+ // acme says the first cert offered by the server must match the
+ // challenge hostname.
+ if err := certs[0].VerifyHostname(sni); err != nil {
+ // TODO: log?
+ return
+ }
+
+ // Target presented what looks like a valid challenge
+ // response, send it back to the matcher.
+ ret = dest
+}
+
// clientHelloServerName returns the SNI server name inside the TLS ClientHello,
// without consuming any bytes from br.
// On any error, the empty string is returned.