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:
@@ -115,6 +115,9 @@ type TLSConfig struct {
|
|||||||
Validity time.Duration `yaml:",omitempty" json:"validity,omitempty"`
|
Validity time.Duration `yaml:",omitempty" json:"validity,omitempty"`
|
||||||
CommonName string `yaml:"commonName,omitempty" json:"commonName,omitempty"`
|
CommonName string `yaml:"commonName,omitempty" json:"commonName,omitempty"`
|
||||||
Organization string `yaml:",omitempty" json:"organization,omitempty"`
|
Organization string `yaml:",omitempty" json:"organization,omitempty"`
|
||||||
|
|
||||||
|
RejectUnknownSNI bool `yaml:"rejectUnknownSNI,omitempty" json:"rejectUnknownSNI,omitempty"`
|
||||||
|
ServerNames []string `yaml:"serverNames,omitempty" json:"serverNames,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TLSOptions struct {
|
type TLSOptions struct {
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
|||||||
if tlsConfig == nil {
|
if tlsConfig == nil {
|
||||||
tlsConfig = parsing.DefaultTLSConfig().Clone()
|
tlsConfig = parsing.DefaultTLSConfig().Clone()
|
||||||
tls_util.SetTLSOptions(tlsConfig, tlsCfg.Options)
|
tls_util.SetTLSOptions(tlsConfig, tlsCfg.Options)
|
||||||
|
tls_util.RejectUnknownSNIConfig(tlsConfig, tlsCfg.RejectUnknownSNI, tlsCfg.ServerNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
authers := auth_parser.List(cfg.Listener.Auther, cfg.Listener.Authers...)
|
authers := auth_parser.List(cfg.Listener.Auther, cfg.Listener.Authers...)
|
||||||
@@ -252,6 +253,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
|||||||
if tlsConfig == nil {
|
if tlsConfig == nil {
|
||||||
tlsConfig = parsing.DefaultTLSConfig().Clone()
|
tlsConfig = parsing.DefaultTLSConfig().Clone()
|
||||||
tls_util.SetTLSOptions(tlsConfig, tlsCfg.Options)
|
tls_util.SetTLSOptions(tlsConfig, tlsCfg.Options)
|
||||||
|
tls_util.RejectUnknownSNIConfig(tlsConfig, tlsCfg.RejectUnknownSNI, tlsCfg.ServerNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
authers = auth_parser.List(cfg.Handler.Auther, cfg.Handler.Authers...)
|
authers = auth_parser.List(cfg.Handler.Auther, cfg.Handler.Authers...)
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ func LoadServerConfig(config *config.TLSConfig) (*tls.Config, error) {
|
|||||||
|
|
||||||
SetTLSOptions(cfg, config.Options)
|
SetTLSOptions(cfg, config.Options)
|
||||||
|
|
||||||
|
RejectUnknownSNIConfig(cfg, config.RejectUnknownSNI, config.ServerNames)
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,6 +456,34 @@ func GenerateCertificate(serverName string, validity time.Duration, caCert *x509
|
|||||||
return x509.ParseCertificate(raw)
|
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
|
// https://pkg.go.dev/crypto#PrivateKey
|
||||||
type privateKey interface {
|
type privateKey interface {
|
||||||
Public() crypto.PublicKey
|
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