@marcel91200 / caddy Mirror · read-only / commits / 9505c0ba
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 11:00 UTC

caddytls: match IDN SNI in connection policies (#7742)

Zen Dodd committed May 21, 2026 at 03:52 UTC 9505c0baa0ab9539b506b46be05bb6b4ddf3e461
4 files changed +92 -4
modules/caddytls/connpolicy.go
+3 -2
@@ -107,7 +107,8 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
107 if sni, ok := m.(MatchServerName); ok {
108 for _, sniName := range sni {
109 // index for fast lookups during handshakes
110 - indexedBySNI[sniName] = append(indexedBySNI[sniName], p)
110 + indexName := asciiServerNameForMatch(sniName)
111 + indexedBySNI[indexName] = append(indexedBySNI[indexName], p)
112 }
113 }
114 }
@@ -118,7 +119,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
119 // filter policies by SNI first, if possible, to speed things up
120 // when there may be lots of policies
121 possiblePolicies := cp
121 - if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok {
122 + if indexedPolicies, ok := indexedBySNI[asciiServerNameForMatch(hello.ServerName)]; ok {
123 possiblePolicies = indexedPolicies
124 }
125
modules/caddytls/connpolicy_test.go
+36
@@ -15,6 +15,8 @@
15 package caddytls
16
17 import (
18 + "context"
19 + "crypto/tls"
20 "encoding/json"
21 "fmt"
22 "reflect"
@@ -24,6 +26,40 @@ import (
26 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
27 )
28
29 +func TestConnectionPolicyIDNSNIMatcherFastPath(t *testing.T) {
30 + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
31 + defer cancel()
32 +
33 + targetTLSConfig := &tls.Config{ClientAuth: tls.RequireAnyClientCert}
34 + policies := ConnectionPolicies{
35 + {
36 + matchers: []ConnectionMatcher{MatchServerName{"つ.Localhost"}},
37 + TLSConfig: targetTLSConfig,
38 + },
39 + }
40 +
41 + const sniFastPathThreshold = 30
42 + for i := len(policies); i < sniFastPathThreshold; i++ {
43 + policies = append(policies, &ConnectionPolicy{
44 + matchers: []ConnectionMatcher{MatchServerName{fmt.Sprintf("example-%d.localhost", i)}},
45 + TLSConfig: &tls.Config{},
46 + })
47 + }
48 + policies = append(policies, &ConnectionPolicy{
49 + matchers: []ConnectionMatcher{MatchServerName{"xn--k9j.localhost"}},
50 + TLSConfig: &tls.Config{ClientAuth: tls.NoClientCert},
51 + })
52 +
53 + tlsConfig := policies.TLSConfig(ctx)
54 + got, err := tlsConfig.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "XN--K9J.LOCALHOST"})
55 + if err != nil {
56 + t.Fatalf("GetConfigForClient() error = %v", err)
57 + }
58 + if got != targetTLSConfig {
59 + t.Fatalf("expected Unicode IDN policy to match before later punycode policy")
60 + }
61 +}
62 +
63 func TestClientAuthenticationUnmarshalCaddyfileWithDirectiveName(t *testing.T) {
64 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==`
65 const test_cert_file_1 = "../../caddytest/caddy.ca.cer"
modules/caddytls/matchers.go
+33 -2
@@ -28,6 +28,7 @@ import (
28 "github.com/caddyserver/certmagic"
29 "go.uber.org/zap"
30 "go.uber.org/zap/zapcore"
31 + "golang.org/x/net/idna"
32
33 "github.com/caddyserver/caddy/v2"
34 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -69,15 +70,45 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
70 repl = caddy.NewReplacer()
71 }
72
73 + serverName := asciiServerNameForMatch(hello.ServerName)
74 for _, name := range m {
73 - rs := repl.ReplaceAll(name, "")
74 - if certmagic.MatchWildcard(hello.ServerName, rs) {
75 + rs := asciiServerNameForMatch(repl.ReplaceAll(name, ""))
76 + if certmagic.MatchWildcard(serverName, rs) {
77 return true
78 }
79 }
80 return false
81 }
82
83 +func asciiServerNameForMatch(name string) string {
84 + if name == "" {
85 + return name
86 + }
87 +
88 + // SNI is ASCII on the wire, but config can use Unicode IDNs.
89 + ascii, err := idna.ToASCII(name)
90 + if err == nil {
91 + return strings.ToLower(ascii)
92 + }
93 +
94 + if !strings.Contains(name, "*") {
95 + return strings.ToLower(name)
96 + }
97 +
98 + labels := strings.Split(name, ".")
99 + for i, label := range labels {
100 + if label == "" || label == "*" {
101 + continue
102 + }
103 + ascii, err := idna.ToASCII(label)
104 + if err != nil {
105 + return strings.ToLower(name)
106 + }
107 + labels[i] = strings.ToLower(ascii)
108 + }
109 + return strings.Join(labels, ".")
110 +}
111 +
112 // UnmarshalCaddyfile sets up the MatchServerName from Caddyfile tokens. Syntax:
113 //
114 // sni <domains...>
modules/caddytls/matchers_test.go
+20
@@ -79,6 +79,26 @@ func TestServerNameMatcher(t *testing.T) {
79 input: "sub2.sub.example.com",
80 expect: true,
81 },
82 + {
83 + names: []string{"つ.localhost"},
84 + input: "xn--k9j.localhost",
85 + expect: true,
86 + },
87 + {
88 + names: []string{"つ.Localhost"},
89 + input: "XN--K9J.LOCALHOST",
90 + expect: true,
91 + },
92 + {
93 + names: []string{"*.つ.localhost"},
94 + input: "sub.xn--k9j.localhost",
95 + expect: true,
96 + },
97 + {
98 + names: []string{"*.つ.Localhost"},
99 + input: "Sub.XN--K9J.LOCALHOST",
100 + expect: true,
101 + },
102 } {
103 chi := &tls.ClientHelloInfo{ServerName: tc.input}
104 actual := MatchServerName(tc.names).Match(chi)