diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index c38ad0d4b..852c78d54 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -107,7 +107,8 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { if sni, ok := m.(MatchServerName); ok { for _, sniName := range sni { // index for fast lookups during handshakes - indexedBySNI[sniName] = append(indexedBySNI[sniName], p) + indexName := asciiServerNameForMatch(sniName) + indexedBySNI[indexName] = append(indexedBySNI[indexName], p) } } } @@ -118,7 +119,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { // filter policies by SNI first, if possible, to speed things up // when there may be lots of policies possiblePolicies := cp - if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok { + if indexedPolicies, ok := indexedBySNI[asciiServerNameForMatch(hello.ServerName)]; ok { possiblePolicies = indexedPolicies } diff --git a/modules/caddytls/connpolicy_test.go b/modules/caddytls/connpolicy_test.go index 82ecbc40d..b3c091d47 100644 --- a/modules/caddytls/connpolicy_test.go +++ b/modules/caddytls/connpolicy_test.go @@ -15,6 +15,8 @@ package caddytls import ( + "context" + "crypto/tls" "encoding/json" "fmt" "reflect" @@ -24,6 +26,40 @@ import ( "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) +func TestConnectionPolicyIDNSNIMatcherFastPath(t *testing.T) { + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + targetTLSConfig := &tls.Config{ClientAuth: tls.RequireAnyClientCert} + policies := ConnectionPolicies{ + { + matchers: []ConnectionMatcher{MatchServerName{"つ.Localhost"}}, + TLSConfig: targetTLSConfig, + }, + } + + const sniFastPathThreshold = 30 + for i := len(policies); i < sniFastPathThreshold; i++ { + policies = append(policies, &ConnectionPolicy{ + matchers: []ConnectionMatcher{MatchServerName{fmt.Sprintf("example-%d.localhost", i)}}, + TLSConfig: &tls.Config{}, + }) + } + policies = append(policies, &ConnectionPolicy{ + matchers: []ConnectionMatcher{MatchServerName{"xn--k9j.localhost"}}, + TLSConfig: &tls.Config{ClientAuth: tls.NoClientCert}, + }) + + tlsConfig := policies.TLSConfig(ctx) + got, err := tlsConfig.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "XN--K9J.LOCALHOST"}) + if err != nil { + t.Fatalf("GetConfigForClient() error = %v", err) + } + if got != targetTLSConfig { + t.Fatalf("expected Unicode IDN policy to match before later punycode policy") + } +} + func TestClientAuthenticationUnmarshalCaddyfileWithDirectiveName(t *testing.T) { const test_der_1 = `MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==` const test_cert_file_1 = "../../caddytest/caddy.ca.cer" diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index dfbec94cc..597450ef7 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -28,6 +28,7 @@ import ( "github.com/caddyserver/certmagic" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "golang.org/x/net/idna" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -69,15 +70,45 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { repl = caddy.NewReplacer() } + serverName := asciiServerNameForMatch(hello.ServerName) for _, name := range m { - rs := repl.ReplaceAll(name, "") - if certmagic.MatchWildcard(hello.ServerName, rs) { + rs := asciiServerNameForMatch(repl.ReplaceAll(name, "")) + if certmagic.MatchWildcard(serverName, rs) { return true } } return false } +func asciiServerNameForMatch(name string) string { + if name == "" { + return name + } + + // SNI is ASCII on the wire, but config can use Unicode IDNs. + ascii, err := idna.ToASCII(name) + if err == nil { + return strings.ToLower(ascii) + } + + if !strings.Contains(name, "*") { + return strings.ToLower(name) + } + + labels := strings.Split(name, ".") + for i, label := range labels { + if label == "" || label == "*" { + continue + } + ascii, err := idna.ToASCII(label) + if err != nil { + return strings.ToLower(name) + } + labels[i] = strings.ToLower(ascii) + } + return strings.Join(labels, ".") +} + // UnmarshalCaddyfile sets up the MatchServerName from Caddyfile tokens. Syntax: // // sni diff --git a/modules/caddytls/matchers_test.go b/modules/caddytls/matchers_test.go index 824f72070..8b597b188 100644 --- a/modules/caddytls/matchers_test.go +++ b/modules/caddytls/matchers_test.go @@ -79,6 +79,26 @@ func TestServerNameMatcher(t *testing.T) { input: "sub2.sub.example.com", expect: true, }, + { + names: []string{"つ.localhost"}, + input: "xn--k9j.localhost", + expect: true, + }, + { + names: []string{"つ.Localhost"}, + input: "XN--K9J.LOCALHOST", + expect: true, + }, + { + names: []string{"*.つ.localhost"}, + input: "sub.xn--k9j.localhost", + expect: true, + }, + { + names: []string{"*.つ.Localhost"}, + input: "Sub.XN--K9J.LOCALHOST", + expect: true, + }, } { chi := &tls.ClientHelloInfo{ServerName: tc.input} actual := MatchServerName(tc.names).Match(chi)