traefik/pkg/muxer/tcp/matcher.go
2024-06-05 00:05:37 +02:00

134 lines
3.2 KiB
Go

package tcp
import (
"fmt"
"regexp"
"strings"
"unicode/utf8"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/ip"
)
var tcpFuncs = map[string]func(*matchersTree, ...string) error{
"ALPN": expect1Parameter(alpn),
"ClientIP": expect1Parameter(clientIP),
"HostSNI": expect1Parameter(hostSNI),
"HostSNIRegexp": expect1Parameter(hostSNIRegexp),
}
func expect1Parameter(fn func(*matchersTree, ...string) error) func(*matchersTree, ...string) error {
return func(route *matchersTree, s ...string) error {
if len(s) != 1 {
return fmt.Errorf("unexpected number of parameters; got %d, expected 1", len(s))
}
return fn(route, s...)
}
}
// alpn checks if any of the connection ALPN protocols matches one of the matcher protocols.
func alpn(tree *matchersTree, protos ...string) error {
proto := protos[0]
if proto == tlsalpn01.ACMETLS1Protocol {
return fmt.Errorf("invalid protocol value for ALPN matcher, %q is not allowed", proto)
}
tree.matcher = func(meta ConnData) bool {
for _, alpnProto := range meta.alpnProtos {
if alpnProto == proto {
return true
}
}
return false
}
return nil
}
func clientIP(tree *matchersTree, clientIP ...string) error {
checker, err := ip.NewChecker(clientIP)
if err != nil {
return fmt.Errorf("initializing IP checker for ClientIP matcher: %w", err)
}
tree.matcher = func(meta ConnData) bool {
ok, err := checker.Contains(meta.remoteIP)
if err != nil {
log.Warn().Err(err).Msg("ClientIP matcher: could not match remote address")
return false
}
return ok
}
return nil
}
var hostOrIP = regexp.MustCompile(`^[[:alnum:]\.\-\:]+$`)
// hostSNI checks if the SNI Host of the connection match the matcher host.
func hostSNI(tree *matchersTree, hosts ...string) error {
host := hosts[0]
if host == "*" {
// Since a HostSNI(`*`) rule has been provided as catchAll for non-TLS TCP,
// it allows matching with an empty serverName.
tree.matcher = func(meta ConnData) bool { return true }
return nil
}
if !hostOrIP.MatchString(host) {
return fmt.Errorf("invalid value for HostSNI matcher, %q is not a valid hostname", host)
}
tree.matcher = func(meta ConnData) bool {
if meta.serverName == "" {
return false
}
if host == meta.serverName {
return true
}
// trim trailing period in case of FQDN
host = strings.TrimSuffix(host, ".")
return host == meta.serverName
}
return nil
}
// hostSNIRegexp checks if the SNI Host of the connection matches the matcher host regexp.
func hostSNIRegexp(tree *matchersTree, templates ...string) error {
template := templates[0]
if !isASCII(template) {
return fmt.Errorf("invalid value for HostSNIRegexp matcher, %q is not a valid hostname", template)
}
re, err := regexp.Compile(template)
if err != nil {
return fmt.Errorf("compiling HostSNIRegexp matcher: %w", err)
}
tree.matcher = func(meta ConnData) bool {
return re.MatchString(meta.serverName)
}
return nil
}
// isASCII checks if the given string contains only ASCII characters.
func isASCII(s string) bool {
for i := range len(s) {
if s[i] >= utf8.RuneSelf {
return false
}
}
return true
}