feat(tls): add rejectUnknownSNI support for TLS listeners
Adds RejectUnknownSNI and ServerNames fields to TLSConfig. When enabled, TLS handshakes with missing or non-allowlisted SNI are rejected at the handshake level via GetConfigForClient before any certificate is sent. Works across all TLS-based listener types (tls, mtls, ws, mws, http2, grpc, http3) since they share the same *tls.Config from LoadServerConfig.
This commit is contained in:
@@ -165,6 +165,8 @@ func LoadServerConfig(config *config.TLSConfig) (*tls.Config, error) {
|
||||
|
||||
SetTLSOptions(cfg, config.Options)
|
||||
|
||||
RejectUnknownSNIConfig(cfg, config.RejectUnknownSNI, config.ServerNames)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -454,6 +456,34 @@ func GenerateCertificate(serverName string, validity time.Duration, caCert *x509
|
||||
return x509.ParseCertificate(raw)
|
||||
}
|
||||
|
||||
// RejectUnknownSNIConfig sets a GetConfigForClient callback on cfg that rejects
|
||||
// TLS handshakes when the SNI is missing or not in the allowed list.
|
||||
// If allowList is empty, only missing/empty SNI is rejected (all named SNIs
|
||||
// are allowed). If allowList is populated, any SNI not in the list is rejected.
|
||||
func RejectUnknownSNIConfig(cfg *tls.Config, rejectUnknown bool, allowList []string) {
|
||||
if !rejectUnknown {
|
||||
return
|
||||
}
|
||||
cfg.GetConfigForClient = func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
if hello.ServerName == "" {
|
||||
return nil, errors.New("SNI is required but not provided")
|
||||
}
|
||||
if len(allowList) > 0 {
|
||||
found := false
|
||||
for _, name := range allowList {
|
||||
if strings.EqualFold(hello.ServerName, name) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("SNI %q is not allowed", hello.ServerName)
|
||||
}
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
}
|
||||
|
||||
// https://pkg.go.dev/crypto#PrivateKey
|
||||
type privateKey interface {
|
||||
Public() crypto.PublicKey
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRejectUnknownSNIConfig_Disabled(t *testing.T) {
|
||||
cfg := &tls.Config{}
|
||||
RejectUnknownSNIConfig(cfg, false, nil)
|
||||
if cfg.GetConfigForClient != nil {
|
||||
t.Error("GetConfigForClient should be nil when rejectUnknownSNI is false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectUnknownSNIConfig_EmptyServerName(t *testing.T) {
|
||||
cfg := &tls.Config{}
|
||||
RejectUnknownSNIConfig(cfg, true, nil)
|
||||
if cfg.GetConfigForClient == nil {
|
||||
t.Fatal("GetConfigForClient should be set")
|
||||
}
|
||||
|
||||
_, err := cfg.GetConfigForClient(&tls.ClientHelloInfo{})
|
||||
if err == nil {
|
||||
t.Error("expected error for empty ServerName")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectUnknownSNIConfig_NamedAllowed(t *testing.T) {
|
||||
cfg := &tls.Config{}
|
||||
RejectUnknownSNIConfig(cfg, true, nil)
|
||||
if cfg.GetConfigForClient == nil {
|
||||
t.Fatal("GetConfigForClient should be set")
|
||||
}
|
||||
|
||||
result, err := cfg.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "example.com"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result != cfg {
|
||||
t.Error("expected the original config to be returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectUnknownSNIConfig_WithAllowList_Match(t *testing.T) {
|
||||
cfg := &tls.Config{}
|
||||
RejectUnknownSNIConfig(cfg, true, []string{"example.com", "test.org"})
|
||||
if cfg.GetConfigForClient == nil {
|
||||
t.Fatal("GetConfigForClient should be set")
|
||||
}
|
||||
|
||||
result, err := cfg.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "example.com"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result != cfg {
|
||||
t.Error("expected the original config to be returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectUnknownSNIConfig_WithAllowList_NoMatch(t *testing.T) {
|
||||
cfg := &tls.Config{}
|
||||
RejectUnknownSNIConfig(cfg, true, []string{"example.com", "test.org"})
|
||||
if cfg.GetConfigForClient == nil {
|
||||
t.Fatal("GetConfigForClient should be set")
|
||||
}
|
||||
|
||||
_, err := cfg.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "evil.com"})
|
||||
if err == nil {
|
||||
t.Error("expected error for non-matching ServerName")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectUnknownSNIConfig_WithAllowList_CaseInsensitive(t *testing.T) {
|
||||
cfg := &tls.Config{}
|
||||
RejectUnknownSNIConfig(cfg, true, []string{"Example.COM"})
|
||||
if cfg.GetConfigForClient == nil {
|
||||
t.Fatal("GetConfigForClient should be set")
|
||||
}
|
||||
|
||||
result, err := cfg.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "example.com"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result != cfg {
|
||||
t.Error("expected the original config to be returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectUnknownSNIConfig_WithAllowList_EmptySNI(t *testing.T) {
|
||||
cfg := &tls.Config{}
|
||||
RejectUnknownSNIConfig(cfg, true, []string{"example.com"})
|
||||
if cfg.GetConfigForClient == nil {
|
||||
t.Fatal("GetConfigForClient should be set")
|
||||
}
|
||||
|
||||
_, err := cfg.GetConfigForClient(&tls.ClientHelloInfo{})
|
||||
if err == nil {
|
||||
t.Error("expected error for empty ServerName when allowList is set")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user