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)