caddytls: match IDN SNI in connection policies (#7742)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Failing after 1m41s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 3m29s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m12s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 4m17s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m12s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 2m20s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m51s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m20s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m19s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m43s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m40s
Lint / govulncheck (push) Successful in 2m41s
Lint / dependency-review (push) Failing after 1m29s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m12s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Failing after 25m52s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled

This commit is contained in:
Zen Dodd
2026-05-21 03:52:28 +10:00
committed by GitHub
parent ad912569b5
commit 9505c0baa0
4 changed files with 92 additions and 4 deletions
+3 -2
View File
@@ -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
}
+36
View File
@@ -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"
+33 -2
View File
@@ -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 <domains...>
+20
View File
@@ -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)