Add muxer for TCP Routers
This commit is contained in:
parent
79aab5aab8
commit
dad76e0478
39 changed files with 2661 additions and 901 deletions
328
pkg/muxer/tcp/mux.go
Normal file
328
pkg/muxer/tcp/mux.go
Normal file
|
@ -0,0 +1,328 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/traefik/traefik/v2/pkg/ip"
|
||||
"github.com/traefik/traefik/v2/pkg/log"
|
||||
"github.com/traefik/traefik/v2/pkg/rules"
|
||||
"github.com/traefik/traefik/v2/pkg/tcp"
|
||||
"github.com/traefik/traefik/v2/pkg/types"
|
||||
"github.com/vulcand/predicate"
|
||||
)
|
||||
|
||||
var tcpFuncs = map[string]func(*matchersTree, ...string) error{
|
||||
"HostSNI": hostSNI,
|
||||
"ClientIP": clientIP,
|
||||
}
|
||||
|
||||
// ParseHostSNI extracts the HostSNIs declared in a rule.
|
||||
// This is a first naive implementation used in TCP routing.
|
||||
func ParseHostSNI(rule string) ([]string, error) {
|
||||
var matchers []string
|
||||
for matcher := range tcpFuncs {
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
|
||||
parser, err := rules.NewParser(matchers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parse, err := parser.Parse(rule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buildTree, ok := parse.(rules.TreeBuilder)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error while parsing rule %s", rule)
|
||||
}
|
||||
|
||||
return buildTree().ParseMatchers([]string{"HostSNI"}), nil
|
||||
}
|
||||
|
||||
// ConnData contains TCP connection metadata.
|
||||
type ConnData struct {
|
||||
serverName string
|
||||
remoteIP string
|
||||
}
|
||||
|
||||
// NewConnData builds a connData struct from the given parameters.
|
||||
func NewConnData(serverName string, conn tcp.WriteCloser) (ConnData, error) {
|
||||
remoteIP, _, err := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
if err != nil {
|
||||
return ConnData{}, fmt.Errorf("error while parsing remote address %q: %w", conn.RemoteAddr().String(), err)
|
||||
}
|
||||
|
||||
// as per https://datatracker.ietf.org/doc/html/rfc6066:
|
||||
// > The hostname is represented as a byte string using ASCII encoding without a trailing dot.
|
||||
// so there is no need to trim a potential trailing dot
|
||||
serverName = types.CanonicalDomain(serverName)
|
||||
|
||||
return ConnData{
|
||||
serverName: types.CanonicalDomain(serverName),
|
||||
remoteIP: remoteIP,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Muxer defines a muxer that handles TCP routing with rules.
|
||||
type Muxer struct {
|
||||
routes []*route
|
||||
parser predicate.Parser
|
||||
}
|
||||
|
||||
// NewMuxer returns a TCP muxer.
|
||||
func NewMuxer() (*Muxer, error) {
|
||||
var matcherNames []string
|
||||
for matcherName := range tcpFuncs {
|
||||
matcherNames = append(matcherNames, matcherName)
|
||||
}
|
||||
|
||||
parser, err := rules.NewParser(matcherNames)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while creating rules parser: %w", err)
|
||||
}
|
||||
|
||||
return &Muxer{parser: parser}, nil
|
||||
}
|
||||
|
||||
// Match returns the handler of the first route matching the connection metadata.
|
||||
func (m Muxer) Match(meta ConnData) tcp.Handler {
|
||||
for _, route := range m.routes {
|
||||
if route.matchers.match(meta) {
|
||||
return route.handler
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddRoute adds a new route, associated to the given handler, at the given
|
||||
// priority, to the muxer.
|
||||
func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error {
|
||||
// Special case for when the catchAll fallback is present.
|
||||
// When no user-defined priority is found, the lowest computable priority minus one is used,
|
||||
// in order to make the fallback the last to be evaluated.
|
||||
if priority == 0 && rule == "HostSNI(`*`)" {
|
||||
priority = -1
|
||||
}
|
||||
|
||||
// Default value, which means the user has not set it, so we'll compute it.
|
||||
if priority == 0 {
|
||||
priority = len(rule)
|
||||
}
|
||||
|
||||
parse, err := m.parser.Parse(rule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
|
||||
}
|
||||
|
||||
buildTree, ok := parse.(rules.TreeBuilder)
|
||||
if !ok {
|
||||
return fmt.Errorf("error while parsing rule %s", rule)
|
||||
}
|
||||
|
||||
var matchers matchersTree
|
||||
err = addRule(&matchers, buildTree())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newRoute := &route{
|
||||
handler: handler,
|
||||
priority: priority,
|
||||
matchers: matchers,
|
||||
}
|
||||
m.routes = append(m.routes, newRoute)
|
||||
|
||||
sort.Sort(routes(m.routes))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addRule(tree *matchersTree, rule *rules.Tree) error {
|
||||
switch rule.Matcher {
|
||||
case "and", "or":
|
||||
tree.operator = rule.Matcher
|
||||
tree.left = &matchersTree{}
|
||||
err := addRule(tree.left, rule.RuleLeft)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tree.right = &matchersTree{}
|
||||
return addRule(tree.right, rule.RuleRight)
|
||||
default:
|
||||
err := rules.CheckRule(rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tcpFuncs[rule.Matcher](tree, rule.Value...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rule.Not {
|
||||
matcherFunc := tree.matcher
|
||||
tree.matcher = func(meta ConnData) bool {
|
||||
return !matcherFunc(meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasRoutes returns whether the muxer has routes.
|
||||
func (m *Muxer) HasRoutes() bool {
|
||||
return len(m.routes) > 0
|
||||
}
|
||||
|
||||
// routes implements sort.Interface.
|
||||
type routes []*route
|
||||
|
||||
// Len implements sort.Interface.
|
||||
func (r routes) Len() int { return len(r) }
|
||||
|
||||
// Swap implements sort.Interface.
|
||||
func (r routes) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||
|
||||
// Less implements sort.Interface.
|
||||
func (r routes) Less(i, j int) bool { return r[i].priority > r[j].priority }
|
||||
|
||||
// route holds the matchers to match TCP route,
|
||||
// and the handler that will serve the connection.
|
||||
type route struct {
|
||||
// matchers tree structure reflecting the rule.
|
||||
matchers matchersTree
|
||||
// handler responsible for handling the route.
|
||||
handler tcp.Handler
|
||||
|
||||
// Used to disambiguate between two (or more) rules that would both match for a
|
||||
// given request.
|
||||
// Computed from the matching rule length, if not user-set.
|
||||
priority int
|
||||
}
|
||||
|
||||
// matcher is a matcher func used to match connection properties.
|
||||
type matcher func(meta ConnData) bool
|
||||
|
||||
// matchersTree represents the matchers tree structure.
|
||||
type matchersTree struct {
|
||||
// If matcher is not nil, it means that this matcherTree is a leaf of the tree.
|
||||
// It is therefore mutually exclusive with left and right.
|
||||
matcher matcher
|
||||
// operator to combine the evaluation of left and right leaves.
|
||||
operator string
|
||||
// Mutually exclusive with matcher.
|
||||
left *matchersTree
|
||||
right *matchersTree
|
||||
}
|
||||
|
||||
func (m *matchersTree) match(meta ConnData) bool {
|
||||
if m == nil {
|
||||
// This should never happen as it should have been detected during parsing.
|
||||
log.WithoutContext().Warnf("Rule matcher is nil")
|
||||
return false
|
||||
}
|
||||
|
||||
if m.matcher != nil {
|
||||
return m.matcher(meta)
|
||||
}
|
||||
|
||||
switch m.operator {
|
||||
case "or":
|
||||
return m.left.match(meta) || m.right.match(meta)
|
||||
case "and":
|
||||
return m.left.match(meta) && m.right.match(meta)
|
||||
default:
|
||||
// This should never happen as it should have been detected during parsing.
|
||||
log.WithoutContext().Warnf("Invalid rule operator %s", m.operator)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func clientIP(tree *matchersTree, clientIPs ...string) error {
|
||||
checker, err := ip.NewChecker(clientIPs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initialize IP Checker for \"ClientIP\" matcher: %w", err)
|
||||
}
|
||||
|
||||
tree.matcher = func(meta ConnData) bool {
|
||||
if meta.remoteIP == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
ok, err := checker.Contains(meta.remoteIP)
|
||||
if err != nil {
|
||||
log.WithoutContext().Warnf("\"ClientIP\" matcher: could not match remote address: %v", err)
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var almostFQDN = regexp.MustCompile(`^[[:alnum:]\.-]+$`)
|
||||
|
||||
// hostSNI checks if the SNI Host of the connection match the matcher host.
|
||||
func hostSNI(tree *matchersTree, hosts ...string) error {
|
||||
if len(hosts) == 0 {
|
||||
return errors.New("empty value for \"HostSNI\" matcher is not allowed")
|
||||
}
|
||||
|
||||
for i, host := range hosts {
|
||||
// Special case to allow global wildcard
|
||||
if host == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !almostFQDN.MatchString(host) {
|
||||
return fmt.Errorf("invalid value for \"HostSNI\" matcher, %q is not a valid hostname", host)
|
||||
}
|
||||
|
||||
hosts[i] = strings.ToLower(host)
|
||||
}
|
||||
|
||||
tree.matcher = func(meta ConnData) bool {
|
||||
// Since a HostSNI(`*`) rule has been provided as catchAll for non-TLS TCP,
|
||||
// it allows matching with an empty serverName.
|
||||
// Which is why we make sure to take that case into account before
|
||||
// checking meta.serverName.
|
||||
if hosts[0] == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
if meta.serverName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
if host == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
if host == meta.serverName {
|
||||
return true
|
||||
}
|
||||
|
||||
// trim trailing period in case of FQDN
|
||||
host = strings.TrimSuffix(host, ".")
|
||||
if host == meta.serverName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
776
pkg/muxer/tcp/mux_test.go
Normal file
776
pkg/muxer/tcp/mux_test.go
Normal file
|
@ -0,0 +1,776 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v2/pkg/tcp"
|
||||
)
|
||||
|
||||
type fakeConn struct {
|
||||
call map[string]int
|
||||
remoteAddr net.Addr
|
||||
}
|
||||
|
||||
func (f *fakeConn) Read(b []byte) (n int, err error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (f *fakeConn) Write(b []byte) (n int, err error) {
|
||||
f.call[string(b)]++
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (f *fakeConn) Close() error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (f *fakeConn) LocalAddr() net.Addr {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (f *fakeConn) RemoteAddr() net.Addr {
|
||||
return f.remoteAddr
|
||||
}
|
||||
|
||||
func (f *fakeConn) SetDeadline(t time.Time) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (f *fakeConn) SetReadDeadline(t time.Time) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (f *fakeConn) SetWriteDeadline(t time.Time) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (f *fakeConn) CloseWrite() error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func Test_addTCPRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
serverName string
|
||||
remoteAddr string
|
||||
routeErr bool
|
||||
matchErr bool
|
||||
}{
|
||||
{
|
||||
desc: "no tree",
|
||||
routeErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule with no matcher",
|
||||
rule: "rulewithnotmatcher",
|
||||
routeErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Empty HostSNI rule",
|
||||
rule: "HostSNI()",
|
||||
serverName: "foobar",
|
||||
routeErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Empty HostSNI rule",
|
||||
rule: "HostSNI(``)",
|
||||
serverName: "foobar",
|
||||
routeErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI rule matching",
|
||||
rule: "HostSNI(`foobar`)",
|
||||
serverName: "foobar",
|
||||
},
|
||||
{
|
||||
desc: "Valid negative HostSNI rule matching",
|
||||
rule: "!HostSNI(`bar`)",
|
||||
serverName: "foobar",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI rule matching with alternative case",
|
||||
rule: "hostsni(`foobar`)",
|
||||
serverName: "foobar",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI rule matching with alternative case",
|
||||
rule: "HOSTSNI(`foobar`)",
|
||||
serverName: "foobar",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI rule not matching",
|
||||
rule: "HostSNI(`foobar`)",
|
||||
serverName: "bar",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid negative HostSNI rule not matching",
|
||||
rule: "!HostSNI(`bar`)",
|
||||
serverName: "bar",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Empty ClientIP rule",
|
||||
rule: "ClientIP()",
|
||||
routeErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Empty ClientIP rule",
|
||||
rule: "ClientIP(``)",
|
||||
routeErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid ClientIP",
|
||||
rule: "ClientIP(`invalid`)",
|
||||
routeErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid remoteAddr",
|
||||
rule: "ClientIP(`10.0.0.1`)",
|
||||
remoteAddr: "not.an.IP:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule matching",
|
||||
rule: "ClientIP(`10.0.0.1`)",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid negative ClientIP rule matching",
|
||||
rule: "!ClientIP(`20.0.0.1`)",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule matching with alternative case",
|
||||
rule: "clientip(`10.0.0.1`)",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule matching with alternative case",
|
||||
rule: "CLIENTIP(`10.0.0.1`)",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule not matching",
|
||||
rule: "ClientIP(`10.0.0.1`)",
|
||||
remoteAddr: "10.0.0.2:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid negative ClientIP rule not matching",
|
||||
rule: "!ClientIP(`10.0.0.2`)",
|
||||
remoteAddr: "10.0.0.2:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule matching IPv6",
|
||||
rule: "ClientIP(`10::10`)",
|
||||
remoteAddr: "[10::10]:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid negative ClientIP rule matching IPv6",
|
||||
rule: "!ClientIP(`10::10`)",
|
||||
remoteAddr: "[::1]:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule not matching IPv6",
|
||||
rule: "ClientIP(`10::10`)",
|
||||
remoteAddr: "[::1]:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule matching multiple IPs",
|
||||
rule: "ClientIP(`10.0.0.1`, `10.0.0.0`)",
|
||||
remoteAddr: "10.0.0.0:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule matching CIDR",
|
||||
rule: "ClientIP(`11.0.0.0/24`)",
|
||||
remoteAddr: "11.0.0.0:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule not matching CIDR",
|
||||
rule: "ClientIP(`11.0.0.0/24`)",
|
||||
remoteAddr: "10.0.0.0:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule matching CIDR IPv6",
|
||||
rule: "ClientIP(`11::/16`)",
|
||||
remoteAddr: "[11::]:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule not matching CIDR IPv6",
|
||||
rule: "ClientIP(`11::/16`)",
|
||||
remoteAddr: "[10::]:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule matching multiple CIDR",
|
||||
rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0/16`)",
|
||||
remoteAddr: "10.0.0.0:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule not matching CIDR and matching IP",
|
||||
rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0`)",
|
||||
remoteAddr: "10.0.0.0:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid ClientIP rule matching CIDR and not matching IP",
|
||||
rule: "ClientIP(`11.0.0.0`, `10.0.0.0/16`)",
|
||||
remoteAddr: "10.0.0.0:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and ClientIP rule matching",
|
||||
rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
|
||||
serverName: "foobar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid negative HostSNI and ClientIP rule matching",
|
||||
rule: "!HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
|
||||
serverName: "foobar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and negative ClientIP rule matching",
|
||||
rule: "HostSNI(`foobar`) && !ClientIP(`10.0.0.2`)",
|
||||
serverName: "foobar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid negative HostSNI and negative ClientIP rule matching",
|
||||
rule: "!HostSNI(`bar`) && !ClientIP(`10.0.0.2`)",
|
||||
serverName: "foobar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid negative HostSNI or negative ClientIP rule matching",
|
||||
rule: "!(HostSNI(`bar`) || ClientIP(`10.0.0.2`))",
|
||||
serverName: "foobar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid negative HostSNI and negative ClientIP rule matching",
|
||||
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
|
||||
serverName: "foobar",
|
||||
remoteAddr: "10.0.0.2:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid negative HostSNI and negative ClientIP rule matching",
|
||||
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
|
||||
serverName: "bar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid negative HostSNI and negative ClientIP rule matching",
|
||||
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
|
||||
serverName: "bar",
|
||||
remoteAddr: "10.0.0.2:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid negative HostSNI and negative ClientIP rule matching",
|
||||
rule: "!(HostSNI(`bar`) && ClientIP(`10.0.0.2`))",
|
||||
serverName: "foobar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and ClientIP rule not matching",
|
||||
rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
|
||||
serverName: "bar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and ClientIP rule not matching",
|
||||
rule: "HostSNI(`foobar`) && ClientIP(`10.0.0.1`)",
|
||||
serverName: "foobar",
|
||||
remoteAddr: "10.0.0.2:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI or ClientIP rule matching",
|
||||
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
|
||||
serverName: "foobar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI or ClientIP rule matching",
|
||||
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
|
||||
serverName: "bar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI or ClientIP rule matching",
|
||||
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
|
||||
serverName: "foobar",
|
||||
remoteAddr: "10.0.0.2:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI or ClientIP rule not matching",
|
||||
rule: "HostSNI(`foobar`) || ClientIP(`10.0.0.1`)",
|
||||
serverName: "bar",
|
||||
remoteAddr: "10.0.0.2:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI x 3 OR rule matching",
|
||||
rule: "HostSNI(`foobar`) || HostSNI(`foo`) || HostSNI(`bar`)",
|
||||
serverName: "foobar",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI x 3 OR rule not matching",
|
||||
rule: "HostSNI(`foobar`) || HostSNI(`foo`) || HostSNI(`bar`)",
|
||||
serverName: "baz",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and ClientIP Combined rule matching",
|
||||
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
|
||||
serverName: "foobar",
|
||||
remoteAddr: "10.0.0.2:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and ClientIP Combined rule matching",
|
||||
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
|
||||
serverName: "bar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and ClientIP Combined rule not matching",
|
||||
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
|
||||
serverName: "bar",
|
||||
remoteAddr: "10.0.0.2:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and ClientIP Combined rule not matching",
|
||||
rule: "HostSNI(`foobar`) || HostSNI(`bar`) && ClientIP(`10.0.0.1`)",
|
||||
serverName: "baz",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and ClientIP complex combined rule matching",
|
||||
rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
|
||||
serverName: "bar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and ClientIP complex combined rule not matching",
|
||||
rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
|
||||
serverName: "baz",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and ClientIP complex combined rule not matching",
|
||||
rule: "(HostSNI(`foobar`) || HostSNI(`bar`)) && (ClientIP(`10.0.0.1`) || ClientIP(`10.0.0.2`))",
|
||||
serverName: "bar",
|
||||
remoteAddr: "10.0.0.3:80",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI and ClientIP more complex (but absurd) combined rule matching",
|
||||
rule: "(HostSNI(`foobar`) || (HostSNI(`bar`) && !HostSNI(`foobar`))) && ((ClientIP(`10.0.0.1`) && !ClientIP(`10.0.0.2`)) || ClientIP(`10.0.0.2`)) ",
|
||||
serverName: "bar",
|
||||
remoteAddr: "10.0.0.1:80",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
msg := "BYTES"
|
||||
handler := tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
||||
_, err := conn.Write([]byte(msg))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
router, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = router.AddRoute(test.rule, 0, handler)
|
||||
if test.routeErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
addr := "0.0.0.0:0"
|
||||
if test.remoteAddr != "" {
|
||||
addr = test.remoteAddr
|
||||
}
|
||||
|
||||
conn := &fakeConn{
|
||||
call: map[string]int{},
|
||||
remoteAddr: fakeAddr{addr: addr},
|
||||
}
|
||||
|
||||
connData, err := NewConnData(test.serverName, conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
matchingHandler := router.Match(connData)
|
||||
if test.matchErr {
|
||||
require.Nil(t, matchingHandler)
|
||||
return
|
||||
}
|
||||
|
||||
require.NotNil(t, matchingHandler)
|
||||
|
||||
matchingHandler.ServeTCP(conn)
|
||||
|
||||
n, ok := conn.call[msg]
|
||||
assert.Equal(t, n, 1)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAddr struct {
|
||||
addr string
|
||||
}
|
||||
|
||||
func (f fakeAddr) String() string {
|
||||
return f.addr
|
||||
}
|
||||
|
||||
func (f fakeAddr) Network() string {
|
||||
panic("Implement me")
|
||||
}
|
||||
|
||||
func TestParseHostSNI(t *testing.T) {
|
||||
testCases := []struct {
|
||||
description string
|
||||
expression string
|
||||
domain []string
|
||||
errorExpected bool
|
||||
}{
|
||||
{
|
||||
description: "Unknown rule",
|
||||
expression: "Foobar(`foo.bar`,`test.bar`)",
|
||||
errorExpected: true,
|
||||
},
|
||||
{
|
||||
description: "Many hostSNI rules",
|
||||
expression: "HostSNI(`foo.bar`,`test.bar`)",
|
||||
domain: []string{"foo.bar", "test.bar"},
|
||||
},
|
||||
{
|
||||
description: "Many hostSNI rules upper",
|
||||
expression: "HOSTSNI(`foo.bar`,`test.bar`)",
|
||||
domain: []string{"foo.bar", "test.bar"},
|
||||
},
|
||||
{
|
||||
description: "Many hostSNI rules lower",
|
||||
expression: "hostsni(`foo.bar`,`test.bar`)",
|
||||
domain: []string{"foo.bar", "test.bar"},
|
||||
},
|
||||
{
|
||||
description: "No hostSNI rule",
|
||||
expression: "ClientIP(`10.1`)",
|
||||
},
|
||||
{
|
||||
description: "HostSNI rule and another rule",
|
||||
expression: "HostSNI(`foo.bar`) && ClientIP(`10.1`)",
|
||||
domain: []string{"foo.bar"},
|
||||
},
|
||||
{
|
||||
description: "HostSNI rule to lower and another rule",
|
||||
expression: "HostSNI(`Foo.Bar`) && ClientIP(`10.1`)",
|
||||
domain: []string{"foo.bar"},
|
||||
},
|
||||
{
|
||||
description: "HostSNI rule with no domain",
|
||||
expression: "HostSNI() && ClientIP(`10.1`)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.expression, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
domains, err := ParseHostSNI(test.expression)
|
||||
|
||||
if test.errorExpected {
|
||||
require.Errorf(t, err, "unable to parse correctly the domains in the HostSNI rule from %q", test.expression)
|
||||
} else {
|
||||
require.NoError(t, err, "%s: Error while parsing domain.", test.expression)
|
||||
}
|
||||
|
||||
assert.EqualValues(t, test.domain, domains, "%s: Error parsing domains from expression.", test.expression)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_HostSNI(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
ruleHosts []string
|
||||
serverName string
|
||||
buildErr bool
|
||||
matchErr bool
|
||||
}{
|
||||
{
|
||||
desc: "Empty",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Non ASCII host",
|
||||
ruleHosts: []string{"héhé"},
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Not Matching hosts",
|
||||
ruleHosts: []string{"foobar"},
|
||||
serverName: "bar",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Matching globing host `*`",
|
||||
ruleHosts: []string{"*"},
|
||||
serverName: "foobar",
|
||||
},
|
||||
{
|
||||
desc: "Matching globing host `*` and empty serverName",
|
||||
ruleHosts: []string{"*"},
|
||||
serverName: "",
|
||||
},
|
||||
{
|
||||
desc: "Matching globing host `*` and another non matching host",
|
||||
ruleHosts: []string{"foo", "*"},
|
||||
serverName: "bar",
|
||||
},
|
||||
{
|
||||
desc: "Matching globing host `*` and another non matching host, and empty servername",
|
||||
ruleHosts: []string{"foo", "*"},
|
||||
serverName: "",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Not Matching globing host with subdomain",
|
||||
ruleHosts: []string{"*.bar"},
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Not Matching host with trailing dot with ",
|
||||
ruleHosts: []string{"foobar."},
|
||||
serverName: "foobar.",
|
||||
},
|
||||
{
|
||||
desc: "Matching host with trailing dot",
|
||||
ruleHosts: []string{"foobar."},
|
||||
serverName: "foobar",
|
||||
},
|
||||
{
|
||||
desc: "Matching hosts",
|
||||
ruleHosts: []string{"foobar"},
|
||||
serverName: "foobar",
|
||||
},
|
||||
{
|
||||
desc: "Matching hosts with subdomains",
|
||||
ruleHosts: []string{"foo.bar"},
|
||||
serverName: "foo.bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
matcherTree := &matchersTree{}
|
||||
err := hostSNI(matcherTree, test.ruleHosts...)
|
||||
if test.buildErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
meta := ConnData{
|
||||
serverName: test.serverName,
|
||||
}
|
||||
|
||||
assert.Equal(t, test.matchErr, !matcherTree.match(meta))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ClientIP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
ruleCIDRs []string
|
||||
remoteIP string
|
||||
buildErr bool
|
||||
matchErr bool
|
||||
}{
|
||||
{
|
||||
desc: "Empty",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Malformed CIDR",
|
||||
ruleCIDRs: []string{"héhé"},
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Not matching empty remote IP",
|
||||
ruleCIDRs: []string{"20.20.20.20"},
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Not matching IP",
|
||||
ruleCIDRs: []string{"20.20.20.20"},
|
||||
remoteIP: "10.10.10.10",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Matching IP",
|
||||
ruleCIDRs: []string{"10.10.10.10"},
|
||||
remoteIP: "10.10.10.10",
|
||||
},
|
||||
{
|
||||
desc: "Not matching multiple IPs",
|
||||
ruleCIDRs: []string{"20.20.20.20", "30.30.30.30"},
|
||||
remoteIP: "10.10.10.10",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Matching multiple IPs",
|
||||
ruleCIDRs: []string{"10.10.10.10", "20.20.20.20", "30.30.30.30"},
|
||||
remoteIP: "20.20.20.20",
|
||||
},
|
||||
{
|
||||
desc: "Not matching CIDR",
|
||||
ruleCIDRs: []string{"20.0.0.0/24"},
|
||||
remoteIP: "10.10.10.10",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Matching CIDR",
|
||||
ruleCIDRs: []string{"20.0.0.0/8"},
|
||||
remoteIP: "20.10.10.10",
|
||||
},
|
||||
{
|
||||
desc: "Not matching multiple CIDRs",
|
||||
ruleCIDRs: []string{"10.0.0.0/24", "20.0.0.0/24"},
|
||||
remoteIP: "10.10.10.10",
|
||||
matchErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Matching multiple CIDRs",
|
||||
ruleCIDRs: []string{"10.0.0.0/8", "20.0.0.0/8"},
|
||||
remoteIP: "20.10.10.10",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
matchersTree := &matchersTree{}
|
||||
err := clientIP(matchersTree, test.ruleCIDRs...)
|
||||
if test.buildErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
meta := ConnData{
|
||||
remoteIP: test.remoteIP,
|
||||
}
|
||||
|
||||
assert.Equal(t, test.matchErr, !matchersTree.match(meta))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Priority(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rules map[string]int
|
||||
serverName string
|
||||
remoteIP string
|
||||
expectedRule string
|
||||
}{
|
||||
{
|
||||
desc: "One matching rule, calculated priority",
|
||||
rules: map[string]int{
|
||||
"HostSNI(`bar`)": 0,
|
||||
"HostSNI(`foobar`)": 0,
|
||||
},
|
||||
expectedRule: "HostSNI(`bar`)",
|
||||
serverName: "bar",
|
||||
},
|
||||
{
|
||||
desc: "One matching rule, custom priority",
|
||||
rules: map[string]int{
|
||||
"HostSNI(`foobar`)": 0,
|
||||
"HostSNI(`bar`)": 10000,
|
||||
},
|
||||
expectedRule: "HostSNI(`foobar`)",
|
||||
serverName: "foobar",
|
||||
},
|
||||
{
|
||||
desc: "Two matching rules, calculated priority",
|
||||
rules: map[string]int{
|
||||
"HostSNI(`foobar`)": 0,
|
||||
"HostSNI(`foobar`, `bar`)": 0,
|
||||
},
|
||||
expectedRule: "HostSNI(`foobar`, `bar`)",
|
||||
serverName: "foobar",
|
||||
},
|
||||
{
|
||||
desc: "Two matching rules, custom priority",
|
||||
rules: map[string]int{
|
||||
"HostSNI(`foobar`)": 10000,
|
||||
"HostSNI(`foobar`, `bar`)": 0,
|
||||
},
|
||||
expectedRule: "HostSNI(`foobar`)",
|
||||
serverName: "foobar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
matchedRule := ""
|
||||
for rule, priority := range test.rules {
|
||||
rule := rule
|
||||
err := muxer.AddRoute(rule, priority, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
||||
matchedRule = rule
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
handler := muxer.Match(ConnData{
|
||||
serverName: test.serverName,
|
||||
remoteIP: test.remoteIP,
|
||||
})
|
||||
require.NotNil(t, handler)
|
||||
|
||||
handler.ServeTCP(nil)
|
||||
assert.Equal(t, test.expectedRule, matchedRule)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue