mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-24 00:19:53 -04:00
tls: Refactor internals related to TLS configurations (#1466)
* tls: Refactor TLS config innards with a few minor syntax changes muststaple -> must_staple "http2 off" -> "alpn" with list of ALPN values * Fix typo * Fix QUIC handler * Inline struct field assignments
This commit is contained in:
@@ -79,7 +79,7 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
|
||||
cfg.TLS.Enabled = true
|
||||
cfg.Addr.Scheme = "https"
|
||||
if loadCertificates && caddytls.HostQualifies(cfg.Addr.Host) {
|
||||
_, err := caddytls.CacheManagedCertificate(cfg.Addr.Host, cfg.TLS)
|
||||
_, err := cfg.TLS.CacheManagedCertificate(cfg.Addr.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ type tlsHandler struct {
|
||||
// Halderman, et. al. in "The Security Impact of HTTPS Interception" (NDSS '17):
|
||||
// https://jhalderm.com/pub/papers/interception-ndss17.pdf
|
||||
func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if h.listener == nil {
|
||||
h.next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
h.listener.helloInfosMu.RLock()
|
||||
info := h.listener.helloInfos[r.RemoteAddr]
|
||||
h.listener.helloInfosMu.RUnlock()
|
||||
@@ -78,63 +83,62 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// clientHelloConn reads the ClientHello
|
||||
// and stores it in the attached listener.
|
||||
type clientHelloConn struct {
|
||||
net.Conn
|
||||
readHello bool
|
||||
listener *tlsHelloListener
|
||||
readHello bool // whether ClientHello has been read
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
// Read reads from c.Conn (by letting the standard library
|
||||
// do the reading off the wire), with the exception of
|
||||
// getting a copy of the ClientHello so it can parse it.
|
||||
func (c *clientHelloConn) Read(b []byte) (n int, err error) {
|
||||
if !c.readHello {
|
||||
// Read the header bytes.
|
||||
hdr := make([]byte, 5)
|
||||
n, err := io.ReadFull(c.Conn, hdr)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Get the length of the ClientHello message and read it as well.
|
||||
length := uint16(hdr[3])<<8 | uint16(hdr[4])
|
||||
hello := make([]byte, int(length))
|
||||
n, err = io.ReadFull(c.Conn, hello)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Parse the ClientHello and store it in the map.
|
||||
rawParsed := parseRawClientHello(hello)
|
||||
c.listener.helloInfosMu.Lock()
|
||||
c.listener.helloInfos[c.Conn.RemoteAddr().String()] = rawParsed
|
||||
c.listener.helloInfosMu.Unlock()
|
||||
|
||||
// Since we buffered the header and ClientHello, pretend we were
|
||||
// never here by lining up the buffered values to be read with a
|
||||
// custom connection type, followed by the rest of the actual
|
||||
// underlying connection.
|
||||
mr := io.MultiReader(bytes.NewReader(hdr), bytes.NewReader(hello), c.Conn)
|
||||
mc := multiConn{Conn: c.Conn, reader: mr}
|
||||
|
||||
c.Conn = mc
|
||||
|
||||
c.readHello = true
|
||||
// if we've already read the ClientHello, pass thru
|
||||
if c.readHello {
|
||||
return c.Conn.Read(b)
|
||||
}
|
||||
return c.Conn.Read(b)
|
||||
}
|
||||
|
||||
// multiConn is a net.Conn that reads from the
|
||||
// given reader instead of the wire directly. This
|
||||
// is useful when some of the connection has already
|
||||
// been read (like the TLS Client Hello) and the
|
||||
// reader is a io.MultiReader that starts with
|
||||
// the contents of the buffer.
|
||||
type multiConn struct {
|
||||
net.Conn
|
||||
reader io.Reader
|
||||
}
|
||||
// we let the standard lib read off the wire for us, and
|
||||
// tee that into our buffer so we can read the ClientHello
|
||||
tee := io.TeeReader(c.Conn, c.buf)
|
||||
n, err = tee.Read(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if c.buf.Len() < 5 {
|
||||
return // need to read more bytes for header
|
||||
}
|
||||
|
||||
// Read reads from mc.reader.
|
||||
func (mc multiConn) Read(b []byte) (n int, err error) {
|
||||
return mc.reader.Read(b)
|
||||
// read the header bytes
|
||||
hdr := make([]byte, 5)
|
||||
_, err = io.ReadFull(c.buf, hdr)
|
||||
if err != nil {
|
||||
return // this would be highly unusual and sad
|
||||
}
|
||||
|
||||
// get length of the ClientHello message and read it
|
||||
length := int(uint16(hdr[3])<<8 | uint16(hdr[4]))
|
||||
if c.buf.Len() < length {
|
||||
return // need to read more bytes
|
||||
}
|
||||
hello := make([]byte, length)
|
||||
_, err = io.ReadFull(c.buf, hello)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.buf = nil // buffer no longer needed
|
||||
|
||||
// parse the ClientHello and store it in the map
|
||||
rawParsed := parseRawClientHello(hello)
|
||||
c.listener.helloInfosMu.Lock()
|
||||
c.listener.helloInfos[c.Conn.RemoteAddr().String()] = rawParsed
|
||||
c.listener.helloInfosMu.Unlock()
|
||||
|
||||
c.readHello = true
|
||||
return
|
||||
}
|
||||
|
||||
// parseRawClientHello parses data which contains the raw
|
||||
@@ -279,7 +283,7 @@ func (l *tlsHelloListener) Accept() (net.Conn, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
helloConn := &clientHelloConn{Conn: conn, listener: l}
|
||||
helloConn := &clientHelloConn{Conn: conn, listener: l, buf: new(bytes.Buffer)}
|
||||
return tls.Server(helloConn, l.config), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestHeuristicFunctions(t *testing.T) {
|
||||
// clientHello pairs a User-Agent string to its ClientHello message.
|
||||
type clientHello struct {
|
||||
userAgent string
|
||||
helloHex string
|
||||
helloHex string // do NOT include the header, just the ClientHello message
|
||||
}
|
||||
|
||||
// clientHellos groups samples of true (real) ClientHellos by the
|
||||
@@ -158,7 +158,12 @@ func TestHeuristicFunctions(t *testing.T) {
|
||||
},
|
||||
{
|
||||
// IE 11 on Windows 7, this connection was intercepted by Blue Coat
|
||||
helloHex: "010000b1030358a3f3bae627f464da8cb35976b88e9119640032d41e62a107d608ed8d3e62b9000034c028c027c014c013009f009e009d009cc02cc02bc024c023c00ac009003d003c0035002f006a004000380032000a0013000500040100005400000014001200000f66696e6572706978656c732e636f6d000500050100000000000a00080006001700180019000b00020100000d0014001206010603040105010201040305030203020200170000ff01000100",
|
||||
helloHex: `010000b1030358a3f3bae627f464da8cb35976b88e9119640032d41e62a107d608ed8d3e62b9000034c028c027c014c013009f009e009d009cc02cc02bc024c023c00ac009003d003c0035002f006a004000380032000a0013000500040100005400000014001200000f66696e6572706978656c732e636f6d000500050100000000000a00080006001700180019000b00020100000d0014001206010603040105010201040305030203020200170000ff01000100`,
|
||||
},
|
||||
{
|
||||
// Firefox 51.0.1 being intercepted by burp 1.7.17
|
||||
userAgent: "(TODO)",
|
||||
helloHex: `010000d8030358a92f4daca95acc2f6a10a9c50d736135eae39406d3090238464540d482677600003ac023c027003cc025c02900670040c009c013002fc004c00e00330032c02bc02f009cc02dc031009e00a2c008c012000ac003c00d0016001300ff01000075000a0034003200170001000300130015000600070009000a0018000b000c0019000d000e000f001000110002001200040005001400080016000b00020100000d00180016060306010503050104030401040202030201020201010000001700150000126a61677561722e6b796877616e612e6f7267`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -31,40 +31,47 @@ type Server struct {
|
||||
connTimeout time.Duration // max time to wait for a connection before force stop
|
||||
tlsGovChan chan struct{} // close to stop the TLS maintenance goroutine
|
||||
vhosts *vhostTrie
|
||||
tlsConfig caddytls.ConfigGroup
|
||||
}
|
||||
|
||||
// ensure it satisfies the interface
|
||||
var _ caddy.GracefulServer = new(Server)
|
||||
|
||||
var defaultALPN = []string{"h2", "http/1.1"}
|
||||
|
||||
// makeTLSConfig extracts TLS settings from each site config to
|
||||
// build a tls.Config usable in Caddy HTTP servers. The returned
|
||||
// config will be nil if TLS is disabled for these sites.
|
||||
func makeTLSConfig(group []*SiteConfig) (*tls.Config, error) {
|
||||
var tlsConfigs []*caddytls.Config
|
||||
for i := range group {
|
||||
if HTTP2 && len(group[i].TLS.ALPN) == 0 {
|
||||
// if no application-level protocol was configured up to now,
|
||||
// default to HTTP/2, then HTTP/1.1 if necessary
|
||||
group[i].TLS.ALPN = defaultALPN
|
||||
}
|
||||
tlsConfigs = append(tlsConfigs, group[i].TLS)
|
||||
}
|
||||
return caddytls.MakeTLSConfig(tlsConfigs)
|
||||
}
|
||||
|
||||
// NewServer creates a new Server instance that will listen on addr
|
||||
// and will serve the sites configured in group.
|
||||
func NewServer(addr string, group []*SiteConfig) (*Server, error) {
|
||||
s := &Server{
|
||||
Server: makeHTTPServer(addr, group),
|
||||
Server: makeHTTPServerWithTimeouts(addr, group),
|
||||
vhosts: newVHostTrie(),
|
||||
sites: group,
|
||||
connTimeout: GracefulTimeout,
|
||||
}
|
||||
|
||||
s.Server.Handler = s // this is weird, but whatever
|
||||
tlsh := &tlsHandler{next: s.Server.Handler}
|
||||
s.Server.ConnState = func(c net.Conn, cs http.ConnState) {
|
||||
// when a connection closes or is hijacked, delete its entry
|
||||
// in the map, because we are done with it.
|
||||
if tlsh.listener != nil {
|
||||
if cs == http.StateHijacked || cs == http.StateClosed {
|
||||
tlsh.listener.helloInfosMu.Lock()
|
||||
delete(tlsh.listener.helloInfos, c.RemoteAddr().String())
|
||||
tlsh.listener.helloInfosMu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disable HTTP/2 if desired
|
||||
if !HTTP2 {
|
||||
s.Server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
|
||||
// extract TLS settings from each site config to build
|
||||
// a tls.Config, which will not be nil if TLS is enabled
|
||||
tlsConfig, err := makeTLSConfig(group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.Server.TLSConfig = tlsConfig
|
||||
|
||||
// Enable QUIC if desired
|
||||
if QUIC {
|
||||
@@ -72,41 +79,36 @@ func NewServer(addr string, group []*SiteConfig) (*Server, error) {
|
||||
s.Server.Handler = s.wrapWithSvcHeaders(s.Server.Handler)
|
||||
}
|
||||
|
||||
// Set up TLS configuration
|
||||
tlsConfigs := make(caddytls.ConfigGroup)
|
||||
var allConfigs []*caddytls.Config
|
||||
|
||||
for _, site := range group {
|
||||
|
||||
if err := site.TLS.Build(tlsConfigs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsConfigs[site.TLS.Hostname] = site.TLS
|
||||
allConfigs = append(allConfigs, site.TLS)
|
||||
}
|
||||
|
||||
// Check if configs are valid
|
||||
if err := caddytls.CheckConfigs(allConfigs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.tlsConfig = tlsConfigs
|
||||
|
||||
if caddytls.HasTLSEnabled(allConfigs) {
|
||||
s.Server.TLSConfig = &tls.Config{
|
||||
GetConfigForClient: s.tlsConfig.GetConfigForClient,
|
||||
GetCertificate: s.tlsConfig.GetCertificate,
|
||||
}
|
||||
}
|
||||
|
||||
// As of Go 1.7, HTTP/2 is enabled only if NextProtos includes the string "h2"
|
||||
if HTTP2 && s.Server.TLSConfig != nil && len(s.Server.TLSConfig.NextProtos) == 0 {
|
||||
s.Server.TLSConfig.NextProtos = []string{"h2"}
|
||||
}
|
||||
|
||||
// if TLS is enabled, make sure we prepare the Server accordingly
|
||||
if s.Server.TLSConfig != nil {
|
||||
s.Server.Handler = tlsh
|
||||
// wrap the HTTP handler with a handler that does MITM detection
|
||||
tlsh := &tlsHandler{next: s.Server.Handler}
|
||||
s.Server.Handler = tlsh // this needs to be the "outer" handler when Serve() is called, for type assertion
|
||||
|
||||
// when Serve() creates the TLS listener later, that listener should
|
||||
// be adding a reference the ClientHello info to a map; this callback
|
||||
// will be sure to clear out that entry when the connection closes.
|
||||
s.Server.ConnState = func(c net.Conn, cs http.ConnState) {
|
||||
// when a connection closes or is hijacked, delete its entry
|
||||
// in the map, because we are done with it.
|
||||
if tlsh.listener != nil {
|
||||
if cs == http.StateHijacked || cs == http.StateClosed {
|
||||
tlsh.listener.helloInfosMu.Lock()
|
||||
delete(tlsh.listener.helloInfos, c.RemoteAddr().String())
|
||||
tlsh.listener.helloInfosMu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// As of Go 1.7, if the Server's TLSConfig is not nil, HTTP/2 is enabled only
|
||||
// if TLSConfig.NextProtos includes the string "h2"
|
||||
if HTTP2 && len(s.Server.TLSConfig.NextProtos) == 0 {
|
||||
// some experimenting shows that this NextProtos must have at least
|
||||
// one value that overlaps with the NextProtos of any other tls.Config
|
||||
// that is returned from GetConfigForClient; if there is no overlap,
|
||||
// the connection will fail (as of Go 1.8, Feb. 2017).
|
||||
s.Server.TLSConfig.NextProtos = defaultALPN
|
||||
}
|
||||
}
|
||||
|
||||
// Compile custom middleware for every site (enables virtual hosting)
|
||||
@@ -122,6 +124,61 @@ func NewServer(addr string, group []*SiteConfig) (*Server, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// makeHTTPServerWithTimeouts makes an http.Server from the group of
|
||||
// configs in a way that configures timeouts (or, if not set, it uses
|
||||
// the default timeouts) by combining the configuration of each
|
||||
// SiteConfig in the group. (Timeouts are important for mitigating
|
||||
// slowloris attacks.)
|
||||
func makeHTTPServerWithTimeouts(addr string, group []*SiteConfig) *http.Server {
|
||||
// find the minimum duration configured for each timeout
|
||||
var min Timeouts
|
||||
for _, cfg := range group {
|
||||
if cfg.Timeouts.ReadTimeoutSet &&
|
||||
(!min.ReadTimeoutSet || cfg.Timeouts.ReadTimeout < min.ReadTimeout) {
|
||||
min.ReadTimeoutSet = true
|
||||
min.ReadTimeout = cfg.Timeouts.ReadTimeout
|
||||
}
|
||||
if cfg.Timeouts.ReadHeaderTimeoutSet &&
|
||||
(!min.ReadHeaderTimeoutSet || cfg.Timeouts.ReadHeaderTimeout < min.ReadHeaderTimeout) {
|
||||
min.ReadHeaderTimeoutSet = true
|
||||
min.ReadHeaderTimeout = cfg.Timeouts.ReadHeaderTimeout
|
||||
}
|
||||
if cfg.Timeouts.WriteTimeoutSet &&
|
||||
(!min.WriteTimeoutSet || cfg.Timeouts.WriteTimeout < min.WriteTimeout) {
|
||||
min.WriteTimeoutSet = true
|
||||
min.WriteTimeout = cfg.Timeouts.WriteTimeout
|
||||
}
|
||||
if cfg.Timeouts.IdleTimeoutSet &&
|
||||
(!min.IdleTimeoutSet || cfg.Timeouts.IdleTimeout < min.IdleTimeout) {
|
||||
min.IdleTimeoutSet = true
|
||||
min.IdleTimeout = cfg.Timeouts.IdleTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// for the values that were not set, use defaults
|
||||
if !min.ReadTimeoutSet {
|
||||
min.ReadTimeout = defaultTimeouts.ReadTimeout
|
||||
}
|
||||
if !min.ReadHeaderTimeoutSet {
|
||||
min.ReadHeaderTimeout = defaultTimeouts.ReadHeaderTimeout
|
||||
}
|
||||
if !min.WriteTimeoutSet {
|
||||
min.WriteTimeout = defaultTimeouts.WriteTimeout
|
||||
}
|
||||
if !min.IdleTimeoutSet {
|
||||
min.IdleTimeout = defaultTimeouts.IdleTimeout
|
||||
}
|
||||
|
||||
// set the final values on the server and return it
|
||||
return &http.Server{
|
||||
Addr: addr,
|
||||
ReadTimeout: min.ReadTimeout,
|
||||
ReadHeaderTimeout: min.ReadHeaderTimeout,
|
||||
WriteTimeout: min.WriteTimeout,
|
||||
IdleTimeout: min.IdleTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) wrapWithSvcHeaders(previousHandler http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
s.quicServer.SetQuicHeaders(w.Header())
|
||||
@@ -390,62 +447,6 @@ var defaultTimeouts = Timeouts{
|
||||
IdleTimeout: 2 * time.Minute,
|
||||
}
|
||||
|
||||
// makeHTTPServer makes an http.Server from the group of configs
|
||||
// in a way that configures timeouts (or, if not set, it uses the
|
||||
// default timeouts) and other http.Server properties by combining
|
||||
// the configuration of each SiteConfig in the group. (Timeouts
|
||||
// are important for mitigating slowloris attacks.)
|
||||
func makeHTTPServer(addr string, group []*SiteConfig) *http.Server {
|
||||
s := &http.Server{Addr: addr}
|
||||
|
||||
// find the minimum duration configured for each timeout
|
||||
var min Timeouts
|
||||
for _, cfg := range group {
|
||||
if cfg.Timeouts.ReadTimeoutSet &&
|
||||
(!min.ReadTimeoutSet || cfg.Timeouts.ReadTimeout < min.ReadTimeout) {
|
||||
min.ReadTimeoutSet = true
|
||||
min.ReadTimeout = cfg.Timeouts.ReadTimeout
|
||||
}
|
||||
if cfg.Timeouts.ReadHeaderTimeoutSet &&
|
||||
(!min.ReadHeaderTimeoutSet || cfg.Timeouts.ReadHeaderTimeout < min.ReadHeaderTimeout) {
|
||||
min.ReadHeaderTimeoutSet = true
|
||||
min.ReadHeaderTimeout = cfg.Timeouts.ReadHeaderTimeout
|
||||
}
|
||||
if cfg.Timeouts.WriteTimeoutSet &&
|
||||
(!min.WriteTimeoutSet || cfg.Timeouts.WriteTimeout < min.WriteTimeout) {
|
||||
min.WriteTimeoutSet = true
|
||||
min.WriteTimeout = cfg.Timeouts.WriteTimeout
|
||||
}
|
||||
if cfg.Timeouts.IdleTimeoutSet &&
|
||||
(!min.IdleTimeoutSet || cfg.Timeouts.IdleTimeout < min.IdleTimeout) {
|
||||
min.IdleTimeoutSet = true
|
||||
min.IdleTimeout = cfg.Timeouts.IdleTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// for the values that were not set, use defaults
|
||||
if !min.ReadTimeoutSet {
|
||||
min.ReadTimeout = defaultTimeouts.ReadTimeout
|
||||
}
|
||||
if !min.ReadHeaderTimeoutSet {
|
||||
min.ReadHeaderTimeout = defaultTimeouts.ReadHeaderTimeout
|
||||
}
|
||||
if !min.WriteTimeoutSet {
|
||||
min.WriteTimeout = defaultTimeouts.WriteTimeout
|
||||
}
|
||||
if !min.IdleTimeoutSet {
|
||||
min.IdleTimeout = defaultTimeouts.IdleTimeout
|
||||
}
|
||||
|
||||
// set the final values on the server
|
||||
s.ReadTimeout = min.ReadTimeout
|
||||
s.ReadHeaderTimeout = min.ReadHeaderTimeout
|
||||
s.WriteTimeout = min.WriteTimeout
|
||||
s.IdleTimeout = min.IdleTimeout
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
|
||||
// connections. It's used by ListenAndServe and ListenAndServeTLS so
|
||||
// dead TCP connections (e.g. closing laptop mid-download) eventually
|
||||
|
||||
@@ -92,7 +92,7 @@ func TestMakeHTTPServer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
} {
|
||||
actual := makeHTTPServer("127.0.0.1:9005", tc.group)
|
||||
actual := makeHTTPServerWithTimeouts("127.0.0.1:9005", tc.group)
|
||||
|
||||
if got, want := actual.Addr, "127.0.0.1:9005"; got != want {
|
||||
t.Errorf("Test %d: Expected Addr=%s, but was %s", i, want, got)
|
||||
|
||||
Reference in New Issue
Block a user