// Copyright 2015 Light Code Labs, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package caddytls import ( "crypto/tls" "fmt" "log" "net" "strings" "github.com/caddyserver/caddy/telemetry" "github.com/mholt/certmagic" ) // configGroup is a type that keys configs by their hostname // (hostnames can have wildcard characters; use the getConfig // method to get a config by matching its hostname). type configGroup map[string]*Config // getConfig gets the config by the first key match for hello. // In other words, "sub.foo.bar" will get the config for "*.foo.bar" // if that is the closest match. If no match is found, the first // (random) config will be loaded, which will defer any TLS alerts // to the certificate validation (this may or may not be ideal; // let's talk about it if this becomes problematic). // // This function follows nearly the same logic to lookup // a hostname as the getCertificate function uses. func (cg configGroup) getConfig(hello *tls.ClientHelloInfo) *Config { name := certmagic.NormalizedName(hello.ServerName) if name == "" { name = certmagic.NormalizedName(certmagic.Default.DefaultServerName) } // if SNI is empty, prefer matching IP address (it is // more specific than a "catch-all" configuration) if name == "" && hello.Conn != nil { addr := hello.Conn.LocalAddr().String() ip, _, err := net.SplitHostPort(addr) if err == nil { addr = ip } if config, ok := cg[addr]; ok { return config } } // otherwise, try an exact match if config, ok := cg[name]; ok { return config } // then try replacing labels in the name with // wildcards until we get a match labels := strings.Split(name, ".") for i := range labels { labels[i] = "*" candidate := strings.Join(labels, ".") if config, ok := cg[candidate]; ok { return config } } // try a config that matches all names - this // is needed to match configs defined without // a specific host, like ":443", when SNI is // a non-empty value if config, ok := cg[""]; ok { return config } // failover with a random config: this is necessary // because we might be needing to solve a TLS-ALPN // ACME challenge for a name that we don't have a // TLS configuration for; any config will do for // this purpose for _, config := range cg { // important! disable on-demand TLS so we don't // try to get certificates for unrecognized names; // this requires a careful pointer dance... first // make shallow copies of the structs if config.Manager != nil && config.Manager.OnDemand != nil { cfgCopy := *config mgrCopy := *config.Manager tlsCfgCopy := config.tlsConfig.Clone() // then turn off on-demand TLS mgrCopy.OnDemand = nil // then change the copies; make sure the // GetCertificate callback is updated so // it points to our modified config cfgCopy.Manager = &mgrCopy tlsCfgCopy.GetCertificate = mgrCopy.GetCertificate cfgCopy.tlsConfig = tlsCfgCopy // finally, return the reconstructed config return &cfgCopy } // if on-demand TLS was not enabled, we should // be able to use this config directly return config } log.Printf("[ERROR] No TLS configuration available for ClientHello with ServerName: %s", hello.ServerName) return nil } // GetConfigForClient gets a TLS configuration satisfying clientHello. // In getting the configuration, it abides the rules and settings // defined in the Config that matches clientHello.ServerName. If no // tls.Config is set on the matching Config, a nil value is returned. // // This method is safe for use as a tls.Config.GetConfigForClient callback. func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls.Config, error) { config := cg.getConfig(clientHello) if config != nil { return config.tlsConfig, nil } return nil, nil } // ClientHelloInfo is our own version of the standard lib's // tls.ClientHelloInfo. As of May 2018, any fields populated // by the Go standard library are not guaranteed to have their // values in the original order as on the wire. type ClientHelloInfo struct { Version uint16 `json:"version,omitempty"` CipherSuites []uint16 `json:"cipher_suites,omitempty"` Extensions []uint16 `json:"extensions,omitempty"` CompressionMethods []byte `json:"compression,omitempty"` Curves []tls.CurveID `json:"curves,omitempty"` Points []uint8 `json:"points,omitempty"` // Whether a couple of fields are unknown; if not, the key will encode // differently to reflect that, as opposed to being known empty values. // (some fields may be unknown depending on what package is being used; // i.e. the Go standard lib doesn't expose some things) // (very important to NOT encode these to JSON) ExtensionsUnknown bool `json:"-"` CompressionMethodsUnknown bool `json:"-"` } // Key returns a standardized string form of the data in info, // useful for identifying duplicates. func (info ClientHelloInfo) Key() string { extensions, compressionMethods := "?", "?" if !info.ExtensionsUnknown { extensions = fmt.Sprintf("%x", info.Extensions) } if !info.CompressionMethodsUnknown { compressionMethods = fmt.Sprintf("%x", info.CompressionMethods) } return telemetry.FastHash([]byte(fmt.Sprintf("%x-%x-%s-%s-%x-%x", info.Version, info.CipherSuites, extensions, compressionMethods, info.Curves, info.Points))) } // ClientHelloTelemetry determines whether to report // TLS ClientHellos to telemetry. Disable if doing // it from a different package. var ClientHelloTelemetry = true