1
0
Fork 0

Add muxer for TCP Routers

This commit is contained in:
Daniel Tomcej 2022-03-17 11:02:08 -06:00 committed by GitHub
parent 79aab5aab8
commit dad76e0478
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 2661 additions and 901 deletions

View file

@ -52,6 +52,7 @@ type TCPRouter struct {
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`
Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"`
Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"`
TLS *RouterTCPTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}

View file

@ -176,11 +176,13 @@ func TestDecodeConfiguration(t *testing.T) {
"traefik.tcp.middlewares.Middleware0.ipwhitelist.sourcerange": "foobar, fiibar",
"traefik.tcp.middlewares.Middleware2.inflightconn.amount": "42",
"traefik.tcp.routers.Router0.rule": "foobar",
"traefik.tcp.routers.Router0.priority": "42",
"traefik.tcp.routers.Router0.entrypoints": "foobar, fiibar",
"traefik.tcp.routers.Router0.service": "foobar",
"traefik.tcp.routers.Router0.tls.passthrough": "false",
"traefik.tcp.routers.Router0.tls.options": "foo",
"traefik.tcp.routers.Router1.rule": "foobar",
"traefik.tcp.routers.Router1.priority": "42",
"traefik.tcp.routers.Router1.entrypoints": "foobar, fiibar",
"traefik.tcp.routers.Router1.service": "foobar",
"traefik.tcp.routers.Router1.tls.options": "foo",
@ -211,8 +213,9 @@ func TestDecodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@ -223,8 +226,9 @@ func TestDecodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@ -699,8 +703,9 @@ func TestEncodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@ -711,8 +716,9 @@ func TestEncodeConfiguration(t *testing.T) {
"foobar",
"fiibar",
},
Service: "foobar",
Rule: "foobar",
Service: "foobar",
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTCPTLSConfig{
Passthrough: false,
Options: "foo",
@ -1333,11 +1339,13 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.TCP.Middlewares.Middleware0.IPWhiteList.SourceRange": "foobar, fiibar",
"traefik.TCP.Middlewares.Middleware2.InFlightConn.Amount": "42",
"traefik.TCP.Routers.Router0.Rule": "foobar",
"traefik.TCP.Routers.Router0.Priority": "42",
"traefik.TCP.Routers.Router0.EntryPoints": "foobar, fiibar",
"traefik.TCP.Routers.Router0.Service": "foobar",
"traefik.TCP.Routers.Router0.TLS.Passthrough": "false",
"traefik.TCP.Routers.Router0.TLS.Options": "foo",
"traefik.TCP.Routers.Router1.Rule": "foobar",
"traefik.TCP.Routers.Router1.Priority": "42",
"traefik.TCP.Routers.Router1.EntryPoints": "foobar, fiibar",
"traefik.TCP.Routers.Router1.Service": "foobar",
"traefik.TCP.Routers.Router1.TLS.Passthrough": "false",

View file

@ -1,4 +1,4 @@
package rules
package http
import (
"fmt"
@ -10,11 +10,14 @@ import (
"github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v2/pkg/rules"
"github.com/vulcand/predicate"
)
var funcs = map[string]func(*mux.Route, ...string) error{
"Host": host,
const hostMatcher = "Host"
var httpFuncs = map[string]func(*mux.Route, ...string) error{
hostMatcher: host,
"HostHeader": host,
"HostRegexp": hostRegexp,
"ClientIP": clientIP,
@ -26,33 +29,38 @@ var funcs = map[string]func(*mux.Route, ...string) error{
"Query": query,
}
// Router handle routing with rules.
type Router struct {
// Muxer handles routing with rules.
type Muxer struct {
*mux.Router
parser predicate.Parser
}
// NewRouter returns a new router instance.
func NewRouter() (*Router, error) {
parser, err := newParser()
// NewMuxer returns a new muxer instance.
func NewMuxer() (*Muxer, error) {
var matchers []string
for matcher := range httpFuncs {
matchers = append(matchers, matcher)
}
parser, err := rules.NewParser(matchers)
if err != nil {
return nil, err
}
return &Router{
return &Muxer{
Router: mux.NewRouter().SkipClean(true),
parser: parser,
}, nil
}
// AddRoute add a new route to the router.
func (r *Router) AddRoute(rule string, priority int, handler http.Handler) error {
func (r *Muxer) AddRoute(rule string, priority int, handler http.Handler) error {
parse, err := r.parser.Parse(rule)
if err != nil {
return fmt.Errorf("error while parsing rule %s: %w", rule, err)
}
buildTree, ok := parse.(treeBuilder)
buildTree, ok := parse.(rules.TreeBuilder)
if !ok {
return fmt.Errorf("error while parsing rule %s", rule)
}
@ -72,23 +80,40 @@ func (r *Router) AddRoute(rule string, priority int, handler http.Handler) error
return nil
}
type tree struct {
matcher string
not bool
value []string
ruleLeft *tree
ruleRight *tree
// ParseDomains extract domains from rule.
func ParseDomains(rule string) ([]string, error) {
var matchers []string
for matcher := range httpFuncs {
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{hostMatcher}), nil
}
func path(route *mux.Route, paths ...string) error {
rt := route.Subrouter()
for _, path := range paths {
tmpRt := rt.Path(path)
if tmpRt.GetError() != nil {
return tmpRt.GetError()
if err := rt.Path(path).GetError(); err != nil {
return err
}
}
return nil
}
@ -96,11 +121,11 @@ func pathPrefix(route *mux.Route, paths ...string) error {
rt := route.Subrouter()
for _, path := range paths {
tmpRt := rt.PathPrefix(path)
if tmpRt.GetError() != nil {
return tmpRt.GetError()
if err := rt.PathPrefix(path).GetError(); err != nil {
return err
}
}
return nil
}
@ -220,33 +245,34 @@ func query(route *mux.Route, query ...string) error {
return route.GetError()
}
func addRuleOnRouter(router *mux.Router, rule *tree) error {
switch rule.matcher {
func addRuleOnRouter(router *mux.Router, rule *rules.Tree) error {
switch rule.Matcher {
case "and":
route := router.NewRoute()
err := addRuleOnRoute(route, rule.ruleLeft)
err := addRuleOnRoute(route, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRoute(route, rule.ruleRight)
return addRuleOnRoute(route, rule.RuleRight)
case "or":
err := addRuleOnRouter(router, rule.ruleLeft)
err := addRuleOnRouter(router, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRouter(router, rule.ruleRight)
return addRuleOnRouter(router, rule.RuleRight)
default:
err := checkRule(rule)
err := rules.CheckRule(rule)
if err != nil {
return err
}
if rule.not {
return not(funcs[rule.matcher])(router.NewRoute(), rule.value...)
if rule.Not {
return not(httpFuncs[rule.Matcher])(router.NewRoute(), rule.Value...)
}
return funcs[rule.matcher](router.NewRoute(), rule.value...)
return httpFuncs[rule.Matcher](router.NewRoute(), rule.Value...)
}
}
@ -264,48 +290,36 @@ func not(m func(*mux.Route, ...string) error) func(*mux.Route, ...string) error
}
}
func addRuleOnRoute(route *mux.Route, rule *tree) error {
switch rule.matcher {
func addRuleOnRoute(route *mux.Route, rule *rules.Tree) error {
switch rule.Matcher {
case "and":
err := addRuleOnRoute(route, rule.ruleLeft)
err := addRuleOnRoute(route, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRoute(route, rule.ruleRight)
return addRuleOnRoute(route, rule.RuleRight)
case "or":
subRouter := route.Subrouter()
err := addRuleOnRouter(subRouter, rule.ruleLeft)
err := addRuleOnRouter(subRouter, rule.RuleLeft)
if err != nil {
return err
}
return addRuleOnRouter(subRouter, rule.ruleRight)
return addRuleOnRouter(subRouter, rule.RuleRight)
default:
err := checkRule(rule)
err := rules.CheckRule(rule)
if err != nil {
return err
}
if rule.not {
return not(funcs[rule.matcher])(route, rule.value...)
if rule.Not {
return not(httpFuncs[rule.Matcher])(route, rule.Value...)
}
return funcs[rule.matcher](route, rule.value...)
}
}
func checkRule(rule *tree) error {
if len(rule.value) == 0 {
return fmt.Errorf("no args for matcher %s", rule.matcher)
return httpFuncs[rule.Matcher](route, rule.Value...)
}
for _, v := range rule.value {
if len(v) == 0 {
return fmt.Errorf("empty args for matcher %s, %v", rule.matcher, rule.value)
}
}
return nil
}
// IsASCII checks if the given string contains only ASCII characters.

View file

@ -1,4 +1,4 @@
package rules
package http
import (
"net/http"
@ -635,10 +635,10 @@ func Test_addRoute(t *testing.T) {
t.Parallel()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
router, err := NewRouter()
muxer, err := NewMuxer()
require.NoError(t, err)
err = router.AddRoute(test.rule, 0, handler)
err = muxer.AddRoute(test.rule, 0, handler)
if test.expectedError {
require.Error(t, err)
} else {
@ -659,7 +659,7 @@ func Test_addRoute(t *testing.T) {
for key, value := range test.headers {
req.Header.Set(key, value)
}
reqHost.ServeHTTP(w, req, router.ServeHTTP)
reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
results[calledURL] = w.Code
}
assert.Equal(t, test.expected, results)
@ -787,7 +787,7 @@ func Test_addRoutePriority(t *testing.T) {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
router, err := NewRouter()
muxer, err := NewMuxer()
require.NoError(t, err)
for _, route := range test.cases {
@ -796,16 +796,16 @@ func Test_addRoutePriority(t *testing.T) {
w.Header().Set("X-From", route.xFrom)
})
err := router.AddRoute(route.rule, route.priority, handler)
err := muxer.AddRoute(route.rule, route.priority, handler)
require.NoError(t, err, route.rule)
}
router.SortRoutes()
muxer.SortRoutes()
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, test.path, nil)
router.ServeHTTP(w, req)
muxer.ServeHTTP(w, req)
assert.Equal(t, test.expected, w.Header().Get("X-From"))
})
@ -900,44 +900,42 @@ func TestParseDomains(t *testing.T) {
errorExpected bool
}{
{
description: "Many host rules",
expression: "Host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
errorExpected: false,
description: "Unknown rule",
expression: "Foobar(`foo.bar`,`test.bar`)",
errorExpected: true,
},
{
description: "Many host rules upper",
expression: "HOST(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
errorExpected: false,
description: "Several host rules",
expression: "Host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "Many host rules lower",
expression: "host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
errorExpected: false,
description: "Several host rules upper",
expression: "HOST(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "No host rule",
expression: "Path(`/test`)",
errorExpected: false,
description: "Several host rules lower",
expression: "host(`foo.bar`,`test.bar`)",
domain: []string{"foo.bar", "test.bar"},
},
{
description: "Host rule and another rule",
expression: "Host(`foo.bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
errorExpected: false,
description: "No host rule",
expression: "Path(`/test`)",
},
{
description: "Host rule to trim and another rule",
expression: "Host(`Foo.Bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
errorExpected: false,
description: "Host rule and another rule",
expression: "Host(`foo.bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
},
{
description: "Host rule with no domain",
expression: "Host() && Path(`/test`)",
errorExpected: false,
description: "Host rule to trim and another rule",
expression: "Host(`Foo.Bar`) && Path(`/test`)",
domain: []string{"foo.bar"},
},
{
description: "Host rule with no domain",
expression: "Host() && Path(`/test`)",
},
}

328
pkg/muxer/tcp/mux.go Normal file
View 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
View 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)
})
}
}

View file

@ -21,7 +21,8 @@ import (
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/rules"
httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http"
tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp"
"github.com/traefik/traefik/v2/pkg/safe"
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
"github.com/traefik/traefik/v2/pkg/types"
@ -423,7 +424,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
})
}
} else {
domains, err := rules.ParseHostSNI(route.Rule)
domains, err := tcpmuxer.ParseHostSNI(route.Rule)
if err != nil {
logger.Errorf("Error parsing domains in provider ACME: %v", err)
continue
@ -452,7 +453,7 @@ func (p *Provider) watchNewDomains(ctx context.Context) {
})
}
} else {
domains, err := rules.ParseDomains(route.Rule)
domains, err := httpmuxer.ParseDomains(route.Rule)
if err != nil {
log.FromContext(ctxRouter).Errorf("Error parsing domains in provider ACME: %v", err)
continue

View file

@ -98,6 +98,7 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client
EntryPoints: ingressRouteTCP.Spec.EntryPoints,
Middlewares: mds,
Rule: route.Match,
Priority: route.Priority,
Service: serviceName,
}

View file

@ -17,6 +17,7 @@ type IngressRouteTCPSpec struct {
// RouteTCP contains the set of routes.
type RouteTCP struct {
Match string `json:"match"`
Priority int `json:"priority,omitempty"`
Services []ServiceTCP `json:"services,omitempty"`
// Middlewares contains references to MiddlewareTCP resources.
Middlewares []ObjectReference `json:"middlewares,omitempty"`

View file

@ -1,7 +1,7 @@
package rules
import (
"errors"
"fmt"
"strings"
"github.com/vulcand/predicate"
@ -12,121 +12,29 @@ const (
or = "or"
)
type treeBuilder func() *tree
// TreeBuilder defines the type for a Tree builder.
type TreeBuilder func() *Tree
// ParseDomains extract domains from rule.
func ParseDomains(rule string) ([]string, error) {
parser, err := newParser()
if err != nil {
return nil, err
}
parse, err := parser.Parse(rule)
if err != nil {
return nil, err
}
buildTree, ok := parse.(treeBuilder)
if !ok {
return nil, errors.New("cannot parse")
}
return lower(parseDomain(buildTree())), nil
// Tree represents the rules' tree structure.
type Tree struct {
Matcher string
Not bool
Value []string
RuleLeft *Tree
RuleRight *Tree
}
// ParseHostSNI extracts the HostSNIs declared in a rule.
// This is a first naive implementation used in TCP routing.
func ParseHostSNI(rule string) ([]string, error) {
parser, err := newTCPParser()
if err != nil {
return nil, err
}
parse, err := parser.Parse(rule)
if err != nil {
return nil, err
}
buildTree, ok := parse.(treeBuilder)
if !ok {
return nil, errors.New("cannot parse")
}
return lower(parseDomain(buildTree())), nil
}
func lower(slice []string) []string {
var lowerStrings []string
for _, value := range slice {
lowerStrings = append(lowerStrings, strings.ToLower(value))
}
return lowerStrings
}
func parseDomain(tree *tree) []string {
switch tree.matcher {
case and, or:
return append(parseDomain(tree.ruleLeft), parseDomain(tree.ruleRight)...)
case "Host", "HostSNI":
return tree.value
default:
return nil
}
}
func andFunc(left, right treeBuilder) treeBuilder {
return func() *tree {
return &tree{
matcher: and,
ruleLeft: left(),
ruleRight: right(),
}
}
}
func orFunc(left, right treeBuilder) treeBuilder {
return func() *tree {
return &tree{
matcher: or,
ruleLeft: left(),
ruleRight: right(),
}
}
}
func invert(t *tree) *tree {
switch t.matcher {
case or:
t.matcher = and
t.ruleLeft = invert(t.ruleLeft)
t.ruleRight = invert(t.ruleRight)
case and:
t.matcher = or
t.ruleLeft = invert(t.ruleLeft)
t.ruleRight = invert(t.ruleRight)
default:
t.not = !t.not
}
return t
}
func notFunc(elem treeBuilder) treeBuilder {
return func() *tree {
return invert(elem())
}
}
func newParser() (predicate.Parser, error) {
// NewParser constructs a parser for the given matchers.
func NewParser(matchers []string) (predicate.Parser, error) {
parserFuncs := make(map[string]interface{})
for matcherName := range funcs {
for _, matcherName := range matchers {
matcherName := matcherName
fn := func(value ...string) treeBuilder {
return func() *tree {
return &tree{
matcher: matcherName,
value: value,
fn := func(value ...string) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: matcherName,
Value: value,
}
}
}
@ -146,28 +54,85 @@ func newParser() (predicate.Parser, error) {
})
}
func newTCPParser() (predicate.Parser, error) {
parserFuncs := make(map[string]interface{})
// FIXME quircky way of waiting for new rules
matcherName := "HostSNI"
fn := func(value ...string) treeBuilder {
return func() *tree {
return &tree{
matcher: matcherName,
value: value,
}
func andFunc(left, right TreeBuilder) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: and,
RuleLeft: left(),
RuleRight: right(),
}
}
parserFuncs[matcherName] = fn
parserFuncs[strings.ToLower(matcherName)] = fn
parserFuncs[strings.ToUpper(matcherName)] = fn
parserFuncs[strings.Title(strings.ToLower(matcherName))] = fn
return predicate.NewParser(predicate.Def{
Operators: predicate.Operators{
OR: orFunc,
},
Functions: parserFuncs,
})
}
func orFunc(left, right TreeBuilder) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: or,
RuleLeft: left(),
RuleRight: right(),
}
}
}
func invert(t *Tree) *Tree {
switch t.Matcher {
case or:
t.Matcher = and
t.RuleLeft = invert(t.RuleLeft)
t.RuleRight = invert(t.RuleRight)
case and:
t.Matcher = or
t.RuleLeft = invert(t.RuleLeft)
t.RuleRight = invert(t.RuleRight)
default:
t.Not = !t.Not
}
return t
}
func notFunc(elem TreeBuilder) TreeBuilder {
return func() *Tree {
return invert(elem())
}
}
// ParseMatchers returns the subset of matchers in the Tree matching the given matchers.
func (tree *Tree) ParseMatchers(matchers []string) []string {
switch tree.Matcher {
case and, or:
return append(tree.RuleLeft.ParseMatchers(matchers), tree.RuleRight.ParseMatchers(matchers)...)
default:
for _, matcher := range matchers {
if tree.Matcher == matcher {
return lower(tree.Value)
}
}
return nil
}
}
// CheckRule validates the given rule.
func CheckRule(rule *Tree) error {
if len(rule.Value) == 0 {
return fmt.Errorf("no args for matcher %s", rule.Matcher)
}
for _, v := range rule.Value {
if len(v) == 0 {
return fmt.Errorf("empty args for matcher %s, %v", rule.Matcher, rule.Value)
}
}
return nil
}
func lower(slice []string) []string {
var lowerStrings []string
for _, value := range slice {
lowerStrings = append(lowerStrings, strings.ToLower(value))
}
return lowerStrings
}

301
pkg/rules/parser_test.go Normal file
View file

@ -0,0 +1,301 @@
package rules
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testTree = Tree + CheckErr
type testTree struct {
Matcher string
Not bool
Value []string
RuleLeft *testTree
RuleRight *testTree
// CheckErr allow knowing if a Tree has a rule error.
CheckErr bool
}
func TestRuleMatch(t *testing.T) {
matchers := []string{"m"}
testCases := []struct {
desc string
rule string
tree testTree
matchers []string
values []string
expectParseErr bool
}{
{
desc: "No rule",
rule: "",
expectParseErr: true,
},
{
desc: "No matcher in rule",
rule: "m",
expectParseErr: true,
},
{
desc: "No value in rule",
rule: "m()",
tree: testTree{
Matcher: "m",
Value: []string{},
CheckErr: true,
},
},
{
desc: "Empty value in rule",
rule: "m(``)",
tree: testTree{
Matcher: "m",
Value: []string{""},
CheckErr: true,
},
matchers: []string{"m"},
values: []string{""},
},
{
desc: "One value in rule with and",
rule: "m(`1`) &&",
expectParseErr: true,
},
{
desc: "One value in rule with or",
rule: "m(`1`) ||",
expectParseErr: true,
},
{
desc: "One value in rule with missing back tick",
rule: "m(`1)",
expectParseErr: true,
},
{
desc: "One value in rule with missing opening parenthesis",
rule: "m(`1`))",
expectParseErr: true,
},
{
desc: "One value in rule with missing closing parenthesis",
rule: "(m(`1`)",
expectParseErr: true,
},
{
desc: "One value in rule",
rule: "m(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "One value in rule with superfluous parenthesis",
rule: "(m(`1`))",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Rule with CAPS matcher",
rule: "M(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Invalid matcher in rule",
rule: "w(`1`)",
expectParseErr: true,
},
{
desc: "Invalid matchers",
rule: "m(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"not-m"},
},
{
desc: "Two value in rule",
rule: "m(`1`, `2`)",
tree: testTree{
Matcher: "m",
Value: []string{"1", "2"},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Not one value in rule",
rule: "!m(`1`)",
tree: testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Two value in rule with and",
rule: "m(`1`) && m(`2`)",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with or",
rule: "m(`1`) || m(`2`)",
tree: testTree{
Matcher: "or",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with and negated",
rule: "!(m(`1`) && m(`2`))",
tree: testTree{
Matcher: "or",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Not: true,
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with or negated",
rule: "!(m(`1`) || m(`2`))",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Not: true,
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "No value in rule",
rule: "m(`1`) && m()",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{},
CheckErr: true,
},
},
matchers: []string{"m"},
values: []string{"1"},
},
}
parser, err := NewParser(matchers)
require.NoError(t, err)
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
parse, err := parser.Parse(test.rule)
if test.expectParseErr {
require.Error(t, err)
return
}
require.NoError(t, err)
treeBuilder, ok := parse.(TreeBuilder)
require.True(t, ok)
tree := treeBuilder()
checkEquivalence(t, &test.tree, tree)
assert.Equal(t, test.values, tree.ParseMatchers(test.matchers))
})
}
}
func checkEquivalence(t *testing.T, expected *testTree, actual *Tree) {
t.Helper()
if actual == nil {
return
}
if actual.RuleLeft != nil {
checkEquivalence(t, expected.RuleLeft, actual.RuleLeft)
}
if actual.RuleRight != nil {
checkEquivalence(t, expected.RuleRight, actual.RuleRight)
}
assert.Equal(t, expected.Matcher, actual.Matcher)
assert.Equal(t, expected.Not, actual.Not)
assert.Equal(t, expected.Value, actual.Value)
t.Logf("%+v -> %v", actual, CheckRule(actual))
if expected.CheckErr {
assert.Error(t, CheckRule(actual))
} else {
assert.NoError(t, CheckRule(actual))
}
}

View file

@ -13,7 +13,7 @@ import (
metricsMiddle "github.com/traefik/traefik/v2/pkg/middlewares/metrics"
"github.com/traefik/traefik/v2/pkg/middlewares/recovery"
"github.com/traefik/traefik/v2/pkg/middlewares/tracing"
"github.com/traefik/traefik/v2/pkg/rules"
httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http"
"github.com/traefik/traefik/v2/pkg/server/middleware"
"github.com/traefik/traefik/v2/pkg/server/provider"
)
@ -102,7 +102,7 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.RouterInfo) (http.Handler, error) {
router, err := rules.NewRouter()
muxer, err := httpmuxer.NewMuxer()
if err != nil {
return nil, err
}
@ -118,7 +118,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
continue
}
err = router.AddRoute(routerConfig.Rule, routerConfig.Priority, handler)
err = muxer.AddRoute(routerConfig.Rule, routerConfig.Priority, handler)
if err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
@ -126,14 +126,14 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
}
}
router.SortRoutes()
muxer.SortRoutes()
chain := alice.New()
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return recovery.New(ctx, next)
})
return chain.Then(router)
return chain.Then(muxer)
}
func (m *Manager) buildRouterHandler(ctx context.Context, routerName string, routerConfig *runtime.RouterInfo) (http.Handler, error) {

View file

@ -0,0 +1,365 @@
package tcp
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
"github.com/traefik/traefik/v2/pkg/config/runtime"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/middlewares/snicheck"
httpmuxer "github.com/traefik/traefik/v2/pkg/muxer/http"
tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp"
"github.com/traefik/traefik/v2/pkg/server/provider"
tcpservice "github.com/traefik/traefik/v2/pkg/server/service/tcp"
"github.com/traefik/traefik/v2/pkg/tcp"
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
)
type middlewareBuilder interface {
BuildChain(ctx context.Context, names []string) *tcp.Chain
}
// NewManager Creates a new Manager.
func NewManager(conf *runtime.Configuration,
serviceManager *tcpservice.Manager,
middlewaresBuilder middlewareBuilder,
httpHandlers map[string]http.Handler,
httpsHandlers map[string]http.Handler,
tlsManager *traefiktls.Manager,
) *Manager {
return &Manager{
serviceManager: serviceManager,
middlewaresBuilder: middlewaresBuilder,
httpHandlers: httpHandlers,
httpsHandlers: httpsHandlers,
tlsManager: tlsManager,
conf: conf,
}
}
// Manager is a route/router manager.
type Manager struct {
serviceManager *tcpservice.Manager
middlewaresBuilder middlewareBuilder
httpHandlers map[string]http.Handler
httpsHandlers map[string]http.Handler
tlsManager *traefiktls.Manager
conf *runtime.Configuration
}
func (m *Manager) getTCPRouters(ctx context.Context, entryPoints []string) map[string]map[string]*runtime.TCPRouterInfo {
if m.conf != nil {
return m.conf.GetTCPRoutersByEntryPoints(ctx, entryPoints)
}
return make(map[string]map[string]*runtime.TCPRouterInfo)
}
func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*runtime.RouterInfo {
if m.conf != nil {
return m.conf.GetRoutersByEntryPoints(ctx, entryPoints, tls)
}
return make(map[string]map[string]*runtime.RouterInfo)
}
// BuildHandlers builds the handlers for the given entrypoints.
func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) map[string]*Router {
entryPointsRouters := m.getTCPRouters(rootCtx, entryPoints)
entryPointsRoutersHTTP := m.getHTTPRouters(rootCtx, entryPoints, true)
entryPointHandlers := make(map[string]*Router)
for _, entryPointName := range entryPoints {
entryPointName := entryPointName
routers := entryPointsRouters[entryPointName]
ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName))
handler, err := m.buildEntryPointHandler(ctx, routers, entryPointsRoutersHTTP[entryPointName], m.httpHandlers[entryPointName], m.httpsHandlers[entryPointName])
if err != nil {
log.FromContext(ctx).Error(err)
continue
}
entryPointHandlers[entryPointName] = handler
}
return entryPointHandlers
}
type nameAndConfig struct {
routerName string // just so we have it as additional information when logging
TLSConfig *tls.Config
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP, handlerHTTPS http.Handler) (*Router, error) {
// Build a new Router.
router, err := NewRouter()
if err != nil {
return nil, err
}
router.SetHTTPHandler(handlerHTTP)
defaultTLSConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, traefiktls.DefaultTLSConfigName)
if err != nil {
log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
}
// Keyed by domain. The source of truth for doing SNI checking, and for what TLS
// options will actually be used for the connection.
// As soon as there's (at least) two different tlsOptions found for the same domain,
// we set the value to the default TLS conf.
tlsOptionsForHost := map[string]string{}
// Keyed by domain, then by options reference.
// As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS
// options that occur for a given host name, so that later on we can set relevant
// errors and logging for all the routers concerned (i.e. wrongly configured).
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
for routerHTTPName, routerHTTPConfig := range configsHTTP {
if routerHTTPConfig.TLS == nil {
continue
}
ctxRouter := log.With(provider.AddInContext(ctx, routerHTTPName), log.Str(log.RouterName, routerHTTPName))
logger := log.FromContext(ctxRouter)
tlsOptionsName := traefiktls.DefaultTLSConfigName
if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
}
domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule)
if err != nil {
routerErr := fmt.Errorf("invalid rule %s, error: %w", routerHTTPConfig.Rule, err)
routerHTTPConfig.AddError(routerErr, true)
logger.Error(routerErr)
continue
}
if len(domains) == 0 {
// Extra Host(*) rule, for HTTPS routers with no Host rule, and for requests for
// which the SNI does not match _any_ of the other existing routers Host. This is
// only about choosing the TLS configuration. The actual routing will be done
// further on by the HTTPS handler. See examples below.
router.AddHTTPTLSConfig("*", defaultTLSConf)
// The server name (from a Host(SNI) rule) is the only parameter (available in
// HTTP routing rules) on which we can map a TLS config, because it is the only one
// accessible before decryption (we obtain it during the ClientHello). Therefore,
// when a router has no Host rule, it does not make any sense to specify some TLS
// options. Consequently, when it comes to deciding what TLS config will be used,
// for a request that will match an HTTPS router with no Host rule, the result will
// depend on the _others_ existing routers (their Host rule, to be precise), and
// the TLS options associated with them, even though they don't match the incoming
// request. Consider the following examples:
// # conf1
// httpRouter1:
// rule: PathPrefix("/foo")
// # Wherever the request comes from, the TLS config used will be the default one, because of the Host(*) fallback.
// # conf2
// httpRouter1:
// rule: PathPrefix("/foo")
//
// httpRouter2:
// rule: Host("foo.com") && PathPrefix("/bar")
// tlsoptions: myTLSOptions
// # When a request for "/foo" comes, even though it won't be routed by
// httpRouter2, if its SNI is set to foo.com, myTLSOptions will be used for the TLS
// connection. Otherwise, it will fallback to the default TLS config.
logger.Warnf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule)
}
tlsConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName)
if err != nil {
routerHTTPConfig.AddError(err, true)
logger.Error(err)
continue
}
for _, domain := range domains {
// domain is already in lower case thanks to the domain parsing
if tlsOptionsForHostSNI[domain] == nil {
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
}
tlsOptionsForHostSNI[domain][tlsOptionsName] = nameAndConfig{
routerName: routerHTTPName,
TLSConfig: tlsConf,
}
if name, ok := tlsOptionsForHost[domain]; ok && name != tlsOptionsName {
// Different tlsOptions on the same domain, so fallback to default
tlsOptionsForHost[domain] = traefiktls.DefaultTLSConfigName
} else {
tlsOptionsForHost[domain] = tlsOptionsName
}
}
}
sniCheck := snicheck.New(tlsOptionsForHost, handlerHTTPS)
router.SetHTTPSHandler(sniCheck, defaultTLSConf)
logger := log.FromContext(ctx)
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
if len(tlsConfigs) == 1 {
var optionsName string
var config *tls.Config
for k, v := range tlsConfigs {
optionsName = k
config = v.TLSConfig
break
}
logger.Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName)
router.AddHTTPTLSConfig(hostSNI, config)
} else {
routers := make([]string, 0, len(tlsConfigs))
for _, v := range tlsConfigs {
configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS options instead", hostSNI), false)
routers = append(routers, v.routerName)
}
logger.Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
router.AddHTTPTLSConfig(hostSNI, defaultTLSConf)
}
}
for routerName, routerConfig := range configs {
ctxRouter := log.With(provider.AddInContext(ctx, routerName), log.Str(log.RouterName, routerName))
logger := log.FromContext(ctxRouter)
if routerConfig.Service == "" {
err := errors.New("the service is missing on the router")
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
if routerConfig.Rule == "" {
err := errors.New("router has no rule")
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
handler, err := m.buildTCPHandler(ctxRouter, routerConfig)
if err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
domains, err := tcpmuxer.ParseHostSNI(routerConfig.Rule)
if err != nil {
routerErr := fmt.Errorf("invalid rule: %q , %w", routerConfig.Rule, err)
routerConfig.AddError(routerErr, true)
logger.Error(routerErr)
continue
}
// HostSNI Rule, but TLS not set on the router, which is an error.
// However, we allow the HostSNI(*) exception.
if len(domains) > 0 && routerConfig.TLS == nil && domains[0] != "*" {
routerErr := fmt.Errorf("invalid rule: %q , has HostSNI matcher, but no TLS on router", routerConfig.Rule)
routerConfig.AddError(routerErr, true)
logger.Error(routerErr)
}
if routerConfig.TLS == nil {
logger.Debugf("Adding route for %q", routerConfig.Rule)
if err := router.AddRoute(routerConfig.Rule, routerConfig.Priority, handler); err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
}
continue
}
if routerConfig.TLS.Passthrough {
logger.Debugf("Adding Passthrough route for %q", routerConfig.Rule)
if err := router.AddRouteTLS(routerConfig.Rule, routerConfig.Priority, handler, nil); err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
}
continue
}
for _, domain := range domains {
if httpmuxer.IsASCII(domain) {
continue
}
asciiError := fmt.Errorf("invalid domain name value %q, non-ASCII characters are not allowed", domain)
routerConfig.AddError(asciiError, true)
logger.Error(asciiError)
}
tlsOptionsName := routerConfig.TLS.Options
if len(tlsOptionsName) == 0 {
tlsOptionsName = traefiktls.DefaultTLSConfigName
}
if tlsOptionsName != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, tlsOptionsName)
}
tlsConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName)
if err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
// Now that the Rule is not just about the Host, we could theoretically have a config like:
// router1:
// rule: HostSNI(foo.com) && ClientIP(IP1)
// tlsOption: tlsOne
// router2:
// rule: HostSNI(foo.com) && ClientIP(IP2)
// tlsOption: tlsTwo
// i.e. same HostSNI but different tlsOptions
// This is only applicable if the muxer can decide about the routing _before_
// telling the client about the tlsConf (i.e. before the TLS HandShake). This seems
// to be the case so far with the existing matchers (HostSNI, and ClientIP), so
// it's all good. Otherwise, we would have to do as for HTTPS, i.e. disallow
// different TLS configs for the same HostSNIs.
logger.Debugf("Adding TLS route for %q", routerConfig.Rule)
if err := router.AddRouteTLS(routerConfig.Rule, routerConfig.Priority, handler, tlsConf); err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
}
}
return router, nil
}
func (m *Manager) buildTCPHandler(ctx context.Context, router *runtime.TCPRouterInfo) (tcp.Handler, error) {
var qualifiedNames []string
for _, name := range router.Middlewares {
qualifiedNames = append(qualifiedNames, provider.GetQualifiedName(ctx, name))
}
router.Middlewares = qualifiedNames
if router.Service == "" {
return nil, errors.New("the service is missing on the router")
}
sHandler, err := m.serviceManager.BuildTCP(ctx, router.Service)
if err != nil {
return nil, err
}
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
return tcp.NewChain().Extend(*mHandler).Then(sHandler)
}

View file

@ -175,6 +175,7 @@ func TestRuntimeConfiguration(t *testing.T) {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`foo.bar`)",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
},
@ -234,6 +235,7 @@ func TestRuntimeConfiguration(t *testing.T) {
EntryPoints: []string{"web"},
Service: "wrong-service",
Rule: "HostSNI(`bar.foo`)",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
"bar": {
@ -241,6 +243,7 @@ func TestRuntimeConfiguration(t *testing.T) {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`foo.bar`)",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
},
@ -266,6 +269,32 @@ func TestRuntimeConfiguration(t *testing.T) {
},
expectedError: 2,
},
{
desc: "Router with HostSNI but no TLS",
tcpServiceConfig: map[string]*runtime.TCPServiceInfo{
"foo-service": {
TCPService: &dynamic.TCPService{
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "127.0.0.1:80",
},
},
},
},
},
},
tcpRouterConfig: map[string]*runtime.TCPRouterInfo{
"foo": {
TCPRouter: &dynamic.TCPRouter{
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "HostSNI(`bar.foo`)",
},
},
},
expectedError: 1,
},
}
for _, test := range testCases {

View file

@ -1,291 +1,364 @@
package tcp
import (
"context"
"bufio"
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"time"
"github.com/traefik/traefik/v2/pkg/config/runtime"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/middlewares/snicheck"
"github.com/traefik/traefik/v2/pkg/rules"
"github.com/traefik/traefik/v2/pkg/server/provider"
tcpservice "github.com/traefik/traefik/v2/pkg/server/service/tcp"
tcpmuxer "github.com/traefik/traefik/v2/pkg/muxer/tcp"
"github.com/traefik/traefik/v2/pkg/tcp"
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
)
type middlewareBuilder interface {
BuildChain(ctx context.Context, names []string) *tcp.Chain
const defaultBufSize = 4096
// Router is a TCP router.
type Router struct {
// Contains TCP routes.
muxerTCP tcpmuxer.Muxer
// Contains TCP TLS routes.
muxerTCPTLS tcpmuxer.Muxer
// Contains HTTPS routes.
muxerHTTPS tcpmuxer.Muxer
// Forwarder handlers.
// Handles all HTTP requests.
httpForwarder tcp.Handler
// Handles (indirectly through muxerHTTPS, or directly) all HTTPS requests.
httpsForwarder tcp.Handler
// Neither is used directly, but they are held here, and recreated on config
// reload, so that they can be passed to the Switcher at the end of the config
// reload phase.
httpHandler http.Handler
httpsHandler http.Handler
// TLS configs.
httpsTLSConfig *tls.Config // default TLS config
hostHTTPTLSConfig map[string]*tls.Config // TLS configs keyed by SNI
}
// NewManager Creates a new Manager.
func NewManager(conf *runtime.Configuration,
serviceManager *tcpservice.Manager,
middlewaresBuilder middlewareBuilder,
httpHandlers map[string]http.Handler,
httpsHandlers map[string]http.Handler,
tlsManager *traefiktls.Manager,
) *Manager {
return &Manager{
serviceManager: serviceManager,
middlewaresBuilder: middlewaresBuilder,
httpHandlers: httpHandlers,
httpsHandlers: httpsHandlers,
tlsManager: tlsManager,
conf: conf,
}
}
// Manager is a route/router manager.
type Manager struct {
serviceManager *tcpservice.Manager
middlewaresBuilder middlewareBuilder
httpHandlers map[string]http.Handler
httpsHandlers map[string]http.Handler
tlsManager *traefiktls.Manager
conf *runtime.Configuration
}
func (m *Manager) getTCPRouters(ctx context.Context, entryPoints []string) map[string]map[string]*runtime.TCPRouterInfo {
if m.conf != nil {
return m.conf.GetTCPRoutersByEntryPoints(ctx, entryPoints)
}
return make(map[string]map[string]*runtime.TCPRouterInfo)
}
func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls bool) map[string]map[string]*runtime.RouterInfo {
if m.conf != nil {
return m.conf.GetRoutersByEntryPoints(ctx, entryPoints, tls)
}
return make(map[string]map[string]*runtime.RouterInfo)
}
// BuildHandlers builds the handlers for the given entrypoints.
func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) map[string]*tcp.Router {
entryPointsRouters := m.getTCPRouters(rootCtx, entryPoints)
entryPointsRoutersHTTP := m.getHTTPRouters(rootCtx, entryPoints, true)
entryPointHandlers := make(map[string]*tcp.Router)
for _, entryPointName := range entryPoints {
entryPointName := entryPointName
routers := entryPointsRouters[entryPointName]
ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName))
handler, err := m.buildEntryPointHandler(ctx, routers, entryPointsRoutersHTTP[entryPointName], m.httpHandlers[entryPointName], m.httpsHandlers[entryPointName])
if err != nil {
log.FromContext(ctx).Error(err)
continue
}
entryPointHandlers[entryPointName] = handler
}
return entryPointHandlers
}
type nameAndConfig struct {
routerName string // just so we have it as additional information when logging
TLSConfig *tls.Config
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*runtime.TCPRouterInfo, configsHTTP map[string]*runtime.RouterInfo, handlerHTTP, handlerHTTPS http.Handler) (*tcp.Router, error) {
router := &tcp.Router{}
router.HTTPHandler(handlerHTTP)
defaultTLSConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, traefiktls.DefaultTLSConfigName)
if err != nil {
log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
}
if len(configsHTTP) > 0 {
router.AddRouteHTTPTLS("*", defaultTLSConf)
}
// Keyed by domain, then by options reference.
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
tlsOptionsForHost := map[string]string{}
for routerHTTPName, routerHTTPConfig := range configsHTTP {
if routerHTTPConfig.TLS == nil {
continue
}
ctxRouter := log.With(provider.AddInContext(ctx, routerHTTPName), log.Str(log.RouterName, routerHTTPName))
logger := log.FromContext(ctxRouter)
tlsOptionsName := traefiktls.DefaultTLSConfigName
if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
}
domains, err := rules.ParseDomains(routerHTTPConfig.Rule)
if err != nil {
routerErr := fmt.Errorf("invalid rule %s, error: %w", routerHTTPConfig.Rule, err)
routerHTTPConfig.AddError(routerErr, true)
logger.Debug(routerErr)
continue
}
if len(domains) == 0 {
logger.Warnf("No domain found in rule %v, the TLS options applied for this router will depend on the hostSNI of each request", routerHTTPConfig.Rule)
}
for _, domain := range domains {
tlsConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName)
if err != nil {
routerHTTPConfig.AddError(err, true)
logger.Debug(err)
continue
}
// domain is already in lower case thanks to the domain parsing
if tlsOptionsForHostSNI[domain] == nil {
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
}
tlsOptionsForHostSNI[domain][tlsOptionsName] = nameAndConfig{
routerName: routerHTTPName,
TLSConfig: tlsConf,
}
if name, ok := tlsOptionsForHost[domain]; ok && name != tlsOptionsName {
// Different tlsOptions on the same domain fallback to default
tlsOptionsForHost[domain] = traefiktls.DefaultTLSConfigName
} else {
tlsOptionsForHost[domain] = tlsOptionsName
}
}
}
sniCheck := snicheck.New(tlsOptionsForHost, handlerHTTPS)
router.HTTPSHandler(sniCheck, defaultTLSConf)
logger := log.FromContext(ctx)
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
if len(tlsConfigs) == 1 {
var optionsName string
var config *tls.Config
for k, v := range tlsConfigs {
optionsName = k
config = v.TLSConfig
break
}
logger.Debugf("Adding route for %s with TLS options %s", hostSNI, optionsName)
router.AddRouteHTTPTLS(hostSNI, config)
} else {
routers := make([]string, 0, len(tlsConfigs))
for _, v := range tlsConfigs {
configsHTTP[v.routerName].AddError(fmt.Errorf("found different TLS options for routers on the same host %v, so using the default TLS options instead", hostSNI), false)
routers = append(routers, v.routerName)
}
logger.Warnf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
router.AddRouteHTTPTLS(hostSNI, defaultTLSConf)
}
}
for routerName, routerConfig := range configs {
ctxRouter := log.With(provider.AddInContext(ctx, routerName), log.Str(log.RouterName, routerName))
logger := log.FromContext(ctxRouter)
if routerConfig.Service == "" {
err := errors.New("the service is missing on the router")
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
if routerConfig.Rule == "" {
err := errors.New("router has no rule")
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
handler, err := m.buildTCPHandler(ctxRouter, routerConfig)
if err != nil {
routerConfig.AddError(err, true)
logger.Error(err)
continue
}
domains, err := rules.ParseHostSNI(routerConfig.Rule)
if err != nil {
routerErr := fmt.Errorf("unknown rule %s", routerConfig.Rule)
routerConfig.AddError(routerErr, true)
logger.Error(routerErr)
continue
}
for _, domain := range domains {
logger.Debugf("Adding route %s on TCP", domain)
switch {
case routerConfig.TLS != nil:
if !rules.IsASCII(domain) {
asciiError := fmt.Errorf("invalid domain name value %q, non-ASCII characters are not allowed", domain)
routerConfig.AddError(asciiError, true)
logger.Debug(asciiError)
continue
}
if routerConfig.TLS.Passthrough {
router.AddRoute(domain, handler)
continue
}
tlsOptionsName := routerConfig.TLS.Options
if len(tlsOptionsName) == 0 {
tlsOptionsName = traefiktls.DefaultTLSConfigName
}
if tlsOptionsName != traefiktls.DefaultTLSConfigName {
tlsOptionsName = provider.GetQualifiedName(ctxRouter, tlsOptionsName)
}
tlsConf, err := m.tlsManager.Get(traefiktls.DefaultTLSStoreName, tlsOptionsName)
if err != nil {
routerConfig.AddError(err, true)
logger.Debug(err)
continue
}
router.AddRouteTLS(domain, handler, tlsConf)
case domain == "*":
router.AddCatchAllNoTLS(handler)
default:
logger.Warn("TCP Router ignored, cannot specify a Host rule without TLS")
}
}
}
return router, nil
}
func (m *Manager) buildTCPHandler(ctx context.Context, router *runtime.TCPRouterInfo) (tcp.Handler, error) {
var qualifiedNames []string
for _, name := range router.Middlewares {
qualifiedNames = append(qualifiedNames, provider.GetQualifiedName(ctx, name))
}
router.Middlewares = qualifiedNames
if router.Service == "" {
return nil, errors.New("the service is missing on the router")
}
sHandler, err := m.serviceManager.BuildTCP(ctx, router.Service)
// NewRouter returns a new TCP router.
func NewRouter() (*Router, error) {
muxTCP, err := tcpmuxer.NewMuxer()
if err != nil {
return nil, err
}
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
muxTCPTLS, err := tcpmuxer.NewMuxer()
if err != nil {
return nil, err
}
return tcp.NewChain().Extend(*mHandler).Then(sHandler)
muxHTTPS, err := tcpmuxer.NewMuxer()
if err != nil {
return nil, err
}
return &Router{
muxerTCP: *muxTCP,
muxerTCPTLS: *muxTCPTLS,
muxerHTTPS: *muxHTTPS,
}, nil
}
// GetTLSGetClientInfo is called after a ClientHello is received from a client.
func (r *Router) GetTLSGetClientInfo() func(info *tls.ClientHelloInfo) (*tls.Config, error) {
return func(info *tls.ClientHelloInfo) (*tls.Config, error) {
if tlsConfig, ok := r.hostHTTPTLSConfig[info.ServerName]; ok {
return tlsConfig, nil
}
return r.httpsTLSConfig, nil
}
}
// ServeTCP forwards the connection to the right TCP/HTTP handler.
func (r *Router) ServeTCP(conn tcp.WriteCloser) {
// Handling Non-TLS TCP connection early if there is neither HTTP(S) nor TLS
// routers on the entryPoint, and if there is at least one non-TLS TCP router.
// In the case of a non-TLS TCP client (that does not "send" first), we would
// block forever on clientHelloServerName, which is why we want to detect and
// handle that case first and foremost.
if r.muxerTCP.HasRoutes() && !r.muxerTCPTLS.HasRoutes() && !r.muxerHTTPS.HasRoutes() {
connData, err := tcpmuxer.NewConnData("", conn)
if err != nil {
log.WithoutContext().Errorf("Error while reading TCP connection data: %v", err)
conn.Close()
return
}
handler := r.muxerTCP.Match(connData)
// If there is a handler matching the connection metadata,
// we let it handle the connection.
if handler != nil {
handler.ServeTCP(conn)
return
}
// Otherwise, we keep going because:
// 1) we could be in the case where we have HTTP routers.
// 2) if it is an HTTPS request, even though we do not have any TLS routers,
// we still need to reply with a 404.
}
// FIXME -- Check if ProxyProtocol changes the first bytes of the request
br := bufio.NewReader(conn)
serverName, tls, peeked, err := clientHelloServerName(br)
if err != nil {
conn.Close()
return
}
// Remove read/write deadline and delegate this to underlying tcp server (for now only handled by HTTP Server)
err = conn.SetReadDeadline(time.Time{})
if err != nil {
log.WithoutContext().Errorf("Error while setting read deadline: %v", err)
}
err = conn.SetWriteDeadline(time.Time{})
if err != nil {
log.WithoutContext().Errorf("Error while setting write deadline: %v", err)
}
connData, err := tcpmuxer.NewConnData(serverName, conn)
if err != nil {
log.WithoutContext().Errorf("Error while reading TCP connection data: %v", err)
conn.Close()
return
}
if !tls {
handler := r.muxerTCP.Match(connData)
switch {
case handler != nil:
handler.ServeTCP(r.GetConn(conn, peeked))
case r.httpForwarder != nil:
r.httpForwarder.ServeTCP(r.GetConn(conn, peeked))
default:
conn.Close()
}
return
}
handler := r.muxerTCPTLS.Match(connData)
if handler != nil {
handler.ServeTCP(r.GetConn(conn, peeked))
return
}
// for real, the handler returned here is (almost) always the same:
// it is the httpsForwarder that is used for all HTTPS connections that match
// (which is also incidentally the same used in the last block below for 404s).
// The added value from doing Match, is to find and use the specific TLS config
// requested for the given HostSNI.
handler = r.muxerHTTPS.Match(connData)
if handler != nil {
handler.ServeTCP(r.GetConn(conn, peeked))
return
}
// needed to handle 404s for HTTPS, as well as all non-Host (e.g. PathPrefix) matches.
if r.httpsForwarder != nil {
r.httpsForwarder.ServeTCP(r.GetConn(conn, peeked))
return
}
conn.Close()
}
// AddRoute defines a handler for the given rule.
func (r *Router) AddRoute(rule string, priority int, target tcp.Handler) error {
return r.muxerTCP.AddRoute(rule, priority, target)
}
// AddRouteTLS defines a handler for a given rule and sets the matching tlsConfig.
func (r *Router) AddRouteTLS(rule string, priority int, target tcp.Handler, config *tls.Config) error {
// TLS PassThrough
if config == nil {
return r.muxerTCPTLS.AddRoute(rule, priority, target)
}
return r.muxerTCPTLS.AddRoute(rule, priority, &tcp.TLSHandler{
Next: target,
Config: config,
})
}
// AddHTTPTLSConfig defines a handler for a given sniHost and sets the matching tlsConfig.
func (r *Router) AddHTTPTLSConfig(sniHost string, config *tls.Config) {
if r.hostHTTPTLSConfig == nil {
r.hostHTTPTLSConfig = map[string]*tls.Config{}
}
r.hostHTTPTLSConfig[sniHost] = config
}
// GetConn creates a connection proxy with a peeked string.
func (r *Router) GetConn(conn tcp.WriteCloser, peeked string) tcp.WriteCloser {
// FIXME should it really be on Router ?
conn = &Conn{
Peeked: []byte(peeked),
WriteCloser: conn,
}
return conn
}
// GetHTTPHandler gets the attached http handler.
func (r *Router) GetHTTPHandler() http.Handler {
return r.httpHandler
}
// GetHTTPSHandler gets the attached https handler.
func (r *Router) GetHTTPSHandler() http.Handler {
return r.httpsHandler
}
// SetHTTPForwarder sets the tcp handler that will forward the connections to an http handler.
func (r *Router) SetHTTPForwarder(handler tcp.Handler) {
r.httpForwarder = handler
}
// SetHTTPSForwarder sets the tcp handler that will forward the TLS connections to an http handler.
func (r *Router) SetHTTPSForwarder(handler tcp.Handler) {
for sniHost, tlsConf := range r.hostHTTPTLSConfig {
// muxerHTTPS only contains single HostSNI rules (and no other kind of rules),
// so there's no need for specifying a priority for them.
err := r.muxerHTTPS.AddRoute("HostSNI(`"+sniHost+"`)", 0, &tcp.TLSHandler{
Next: handler,
Config: tlsConf,
})
if err != nil {
log.WithoutContext().Errorf("Error while adding route for host: %w", err)
}
}
r.httpsForwarder = &tcp.TLSHandler{
Next: handler,
Config: r.httpsTLSConfig,
}
}
// SetHTTPHandler attaches http handlers on the router.
func (r *Router) SetHTTPHandler(handler http.Handler) {
r.httpHandler = handler
}
// SetHTTPSHandler attaches https handlers on the router.
func (r *Router) SetHTTPSHandler(handler http.Handler, config *tls.Config) {
r.httpsHandler = handler
r.httpsTLSConfig = config
}
// Conn is a connection proxy that handles Peeked bytes.
type Conn struct {
// Peeked are the bytes that have been read from Conn for the
// purposes of route matching, but have not yet been consumed
// by Read calls. It set to nil by Read when fully consumed.
Peeked []byte
// Conn is the underlying connection.
// It can be type asserted against *net.TCPConn or other types
// as needed. It should not be read from directly unless
// Peeked is nil.
tcp.WriteCloser
}
// Read reads bytes from the connection (using the buffer prior to actually reading).
func (c *Conn) Read(p []byte) (n int, err error) {
if len(c.Peeked) > 0 {
n = copy(p, c.Peeked)
c.Peeked = c.Peeked[n:]
if len(c.Peeked) == 0 {
c.Peeked = nil
}
return n, nil
}
return c.WriteCloser.Read(p)
}
// clientHelloServerName returns the SNI server name inside the TLS ClientHello,
// without consuming any bytes from br.
// On any error, the empty string is returned.
func clientHelloServerName(br *bufio.Reader) (string, bool, string, error) {
hdr, err := br.Peek(1)
if err != nil {
var opErr *net.OpError
if !errors.Is(err, io.EOF) && (!errors.As(err, &opErr) || opErr.Timeout()) {
log.WithoutContext().Errorf("Error while Peeking first byte: %s", err)
}
return "", false, "", err
}
// No valid TLS record has a type of 0x80, however SSLv2 handshakes
// start with a uint16 length where the MSB is set and the first record
// is always < 256 bytes long. Therefore typ == 0x80 strongly suggests
// an SSLv2 client.
const recordTypeSSLv2 = 0x80
const recordTypeHandshake = 0x16
if hdr[0] != recordTypeHandshake {
if hdr[0] == recordTypeSSLv2 {
// we consider SSLv2 as TLS and it will be refused by real TLS handshake.
return "", true, getPeeked(br), nil
}
return "", false, getPeeked(br), nil // Not TLS.
}
const recordHeaderLen = 5
hdr, err = br.Peek(recordHeaderLen)
if err != nil {
log.Errorf("Error while Peeking hello: %s", err)
return "", false, getPeeked(br), nil
}
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
if recordHeaderLen+recLen > defaultBufSize {
br = bufio.NewReaderSize(br, recordHeaderLen+recLen)
}
helloBytes, err := br.Peek(recordHeaderLen + recLen)
if err != nil {
log.Errorf("Error while Hello: %s", err)
return "", true, getPeeked(br), nil
}
sni := ""
server := tls.Server(sniSniffConn{r: bytes.NewReader(helloBytes)}, &tls.Config{
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
sni = hello.ServerName
return nil, nil
},
})
_ = server.Handshake()
return sni, true, getPeeked(br), nil
}
func getPeeked(br *bufio.Reader) string {
peeked, err := br.Peek(br.Buffered())
if err != nil {
log.Errorf("Could not get anything: %s", err)
return ""
}
return string(peeked)
}
// sniSniffConn is a net.Conn that reads from r, fails on Writes,
// and crashes otherwise.
type sniSniffConn struct {
r io.Reader
net.Conn // nil; crash on any unexpected use
}
// Read reads from the underlying reader.
func (c sniSniffConn) Read(p []byte) (int, error) { return c.r.Read(p) }
// Write crashes all the time.
func (sniSniffConn) Write(p []byte) (int, error) { return 0, io.EOF }

View file

@ -8,16 +8,15 @@ import (
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/metrics"
"github.com/traefik/traefik/v2/pkg/server/middleware"
middlewaretcp "github.com/traefik/traefik/v2/pkg/server/middleware/tcp"
tcpmiddleware "github.com/traefik/traefik/v2/pkg/server/middleware/tcp"
"github.com/traefik/traefik/v2/pkg/server/router"
routertcp "github.com/traefik/traefik/v2/pkg/server/router/tcp"
routerudp "github.com/traefik/traefik/v2/pkg/server/router/udp"
tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
udprouter "github.com/traefik/traefik/v2/pkg/server/router/udp"
"github.com/traefik/traefik/v2/pkg/server/service"
"github.com/traefik/traefik/v2/pkg/server/service/tcp"
"github.com/traefik/traefik/v2/pkg/server/service/udp"
tcpCore "github.com/traefik/traefik/v2/pkg/tcp"
"github.com/traefik/traefik/v2/pkg/tls"
udpCore "github.com/traefik/traefik/v2/pkg/udp"
udptypes "github.com/traefik/traefik/v2/pkg/udp"
)
// RouterFactory the factory of TCP/UDP routers.
@ -64,7 +63,7 @@ func NewRouterFactory(staticConfiguration static.Configuration, managerFactory *
}
// CreateRouters creates new TCPRouters and UDPRouters.
func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string]*tcpCore.Router, map[string]udpCore.Handler) {
func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string]*tcprouter.Router, map[string]udptypes.Handler) {
ctx := context.Background()
// HTTP
@ -82,14 +81,14 @@ func (f *RouterFactory) CreateRouters(rtConf *runtime.Configuration) (map[string
// TCP
svcTCPManager := tcp.NewManager(rtConf)
middlewaresTCPBuilder := middlewaretcp.NewBuilder(rtConf.TCPMiddlewares)
middlewaresTCPBuilder := tcpmiddleware.NewBuilder(rtConf.TCPMiddlewares)
rtTCPManager := routertcp.NewManager(rtConf, svcTCPManager, middlewaresTCPBuilder, handlersNonTLS, handlersTLS, f.tlsManager)
rtTCPManager := tcprouter.NewManager(rtConf, svcTCPManager, middlewaresTCPBuilder, handlersNonTLS, handlersTLS, f.tlsManager)
routersTCP := rtTCPManager.BuildHandlers(ctx, f.entryPointsTCP)
// UDP
svcUDPManager := udp.NewManager(rtConf)
rtUDPManager := routerudp.NewManager(rtConf, svcUDPManager)
rtUDPManager := udprouter.NewManager(rtConf, svcUDPManager)
routersUDP := rtUDPManager.BuildHandlers(ctx, f.entryPointsUDP)
rtConf.PopulateUsedBy()

View file

@ -22,6 +22,7 @@ import (
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v2/pkg/safe"
"github.com/traefik/traefik/v2/pkg/server/router"
tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
"github.com/traefik/traefik/v2/pkg/tcp"
"github.com/traefik/traefik/v2/pkg/types"
"golang.org/x/net/http2"
@ -114,7 +115,7 @@ func (eps TCPEntryPoints) Stop() {
}
// Switch the TCP routers.
func (eps TCPEntryPoints) Switch(routersTCP map[string]*tcp.Router) {
func (eps TCPEntryPoints) Switch(routersTCP map[string]*tcprouter.Router) {
for entryPointName, rt := range routersTCP {
eps[entryPointName].SwitchRouter(rt)
}
@ -141,7 +142,7 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos
return nil, fmt.Errorf("error preparing server: %w", err)
}
rt := &tcp.Router{}
rt := &tcprouter.Router{}
reqDecorator := requestdecorator.New(hostResolverConfig)
@ -150,7 +151,7 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos
return nil, fmt.Errorf("error preparing http server: %w", err)
}
rt.HTTPForwarder(httpServer.Forwarder)
rt.SetHTTPForwarder(httpServer.Forwarder)
httpsServer, err := createHTTPServer(ctx, listener, configuration, false, reqDecorator)
if err != nil {
@ -162,7 +163,7 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos
return nil, fmt.Errorf("error preparing http3 server: %w", err)
}
rt.HTTPSForwarder(httpsServer.Forwarder)
rt.SetHTTPSForwarder(httpsServer.Forwarder)
tcpSwitcher := &tcp.HandlerSwitcher{}
tcpSwitcher.Switch(rt)
@ -181,7 +182,7 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos
// Start starts the TCP server.
func (e *TCPEntryPoint) Start(ctx context.Context) {
logger := log.FromContext(ctx)
logger.Debugf("Start TCP Server")
logger.Debug("Starting TCP Server")
if e.http3Server != nil {
go func() { _ = e.http3Server.Start() }()
@ -301,8 +302,8 @@ func (e *TCPEntryPoint) Shutdown(ctx context.Context) {
}
// SwitchRouter switches the TCP router handler.
func (e *TCPEntryPoint) SwitchRouter(rt *tcp.Router) {
rt.HTTPForwarder(e.httpServer.Forwarder)
func (e *TCPEntryPoint) SwitchRouter(rt *tcprouter.Router) {
rt.SetHTTPForwarder(e.httpServer.Forwarder)
httpHandler := rt.GetHTTPHandler()
if httpHandler == nil {
@ -311,7 +312,7 @@ func (e *TCPEntryPoint) SwitchRouter(rt *tcp.Router) {
e.httpServer.Switcher.UpdateHandler(httpHandler)
rt.HTTPSForwarder(e.httpsServer.Forwarder)
rt.SetHTTPSForwarder(e.httpsServer.Forwarder)
httpsHandler := rt.GetHTTPSHandler()
if httpsHandler == nil {

View file

@ -13,7 +13,7 @@ import (
"github.com/lucas-clemente/quic-go/http3"
"github.com/traefik/traefik/v2/pkg/config/static"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/tcp"
tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
)
type http3server struct {
@ -77,7 +77,7 @@ func (e *http3server) Start() error {
return e.Serve(e.http3conn)
}
func (e *http3server) Switch(rt *tcp.Router) {
func (e *http3server) Switch(rt *tcprouter.Router) {
e.lock.Lock()
defer e.lock.Unlock()

View file

@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/config/static"
"github.com/traefik/traefik/v2/pkg/tcp"
tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
)
@ -94,11 +94,13 @@ func TestHTTP3AdvertisedPort(t *testing.T) {
}, nil)
require.NoError(t, err)
router := &tcp.Router{}
router.AddRouteHTTPTLS("*", &tls.Config{
router, err := tcprouter.NewRouter()
require.NoError(t, err)
router.AddHTTPTLSConfig("*", &tls.Config{
Certificates: []tls.Certificate{tlsCert},
})
router.HTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
router.SetHTTPSHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}), nil)

View file

@ -15,12 +15,13 @@ import (
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/config/static"
tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
"github.com/traefik/traefik/v2/pkg/tcp"
)
func TestShutdownHijacked(t *testing.T) {
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
router := &tcprouter.Router{}
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
conn, _, err := rw.(http.Hijacker).Hijack()
require.NoError(t, err)
@ -33,8 +34,8 @@ func TestShutdownHijacked(t *testing.T) {
}
func TestShutdownHTTP(t *testing.T) {
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
router := &tcprouter.Router{}
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
time.Sleep(time.Second)
}))
@ -43,8 +44,10 @@ func TestShutdownHTTP(t *testing.T) {
}
func TestShutdownTCP(t *testing.T) {
router := &tcp.Router{}
router.AddCatchAllNoTLS(tcp.HandlerFunc(func(conn tcp.WriteCloser) {
router, err := tcprouter.NewRouter()
require.NoError(t, err)
err = router.AddRoute("HostSNI(`*`)", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
for {
_, err := http.ReadRequest(bufio.NewReader(conn))
@ -58,11 +61,12 @@ func TestShutdownTCP(t *testing.T) {
require.NoError(t, err)
}
}))
require.NoError(t, err)
testShutdown(t, router)
}
func testShutdown(t *testing.T, router *tcp.Router) {
func testShutdown(t *testing.T, router *tcprouter.Router) {
t.Helper()
epConfig := &static.EntryPointsTransport{}
@ -135,7 +139,7 @@ func testShutdown(t *testing.T, router *tcp.Router) {
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func startEntrypoint(entryPoint *TCPEntryPoint, router *tcp.Router) (net.Conn, error) {
func startEntrypoint(entryPoint *TCPEntryPoint, router *tcprouter.Router) (net.Conn, error) {
go entryPoint.Start(context.Background())
entryPoint.SwitchRouter(router)
@ -165,8 +169,8 @@ func TestReadTimeoutWithoutFirstByte(t *testing.T) {
}, nil)
require.NoError(t, err)
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
router := &tcprouter.Router{}
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
@ -201,8 +205,8 @@ func TestReadTimeoutWithFirstByte(t *testing.T) {
}, nil)
require.NoError(t, err)
router := &tcp.Router{}
router.HTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
router := &tcprouter.Router{}
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))

View file

@ -93,7 +93,7 @@ func (m *Mirroring) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if errors.Is(err, errBodyTooLarge) {
req.Body = io.NopCloser(io.MultiReader(bytes.NewReader(bytesRead), req.Body))
m.handler.ServeHTTP(rw, req)
logger.Debugf("no mirroring, request body larger than allowed size")
logger.Debug("no mirroring, request body larger than allowed size")
return
}

View file

@ -1,286 +0,0 @@
package tcp
import (
"bufio"
"bytes"
"crypto/tls"
"errors"
"io"
"net"
"net/http"
"strings"
"time"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/types"
)
const defaultBufSize = 4096
// Router is a TCP router.
type Router struct {
routingTable map[string]Handler
httpForwarder Handler
httpsForwarder Handler
httpHandler http.Handler
httpsHandler http.Handler
httpsTLSConfig *tls.Config // default TLS config
catchAllNoTLS Handler
hostHTTPTLSConfig map[string]*tls.Config // TLS configs keyed by SNI
}
// GetTLSGetClientInfo is called after a ClientHello is received from a client.
func (r *Router) GetTLSGetClientInfo() func(info *tls.ClientHelloInfo) (*tls.Config, error) {
return func(info *tls.ClientHelloInfo) (*tls.Config, error) {
if tlsConfig, ok := r.hostHTTPTLSConfig[info.ServerName]; ok {
return tlsConfig, nil
}
return r.httpsTLSConfig, nil
}
}
// ServeTCP forwards the connection to the right TCP/HTTP handler.
func (r *Router) ServeTCP(conn WriteCloser) {
// FIXME -- Check if ProxyProtocol changes the first bytes of the request
if r.catchAllNoTLS != nil && len(r.routingTable) == 0 {
r.catchAllNoTLS.ServeTCP(conn)
return
}
br := bufio.NewReader(conn)
serverName, tls, peeked, err := clientHelloServerName(br)
if err != nil {
conn.Close()
return
}
// Remove read/write deadline and delegate this to underlying tcp server (for now only handled by HTTP Server)
err = conn.SetReadDeadline(time.Time{})
if err != nil {
log.WithoutContext().Errorf("Error while setting read deadline: %v", err)
}
err = conn.SetWriteDeadline(time.Time{})
if err != nil {
log.WithoutContext().Errorf("Error while setting write deadline: %v", err)
}
if !tls {
switch {
case r.catchAllNoTLS != nil:
r.catchAllNoTLS.ServeTCP(r.GetConn(conn, peeked))
case r.httpForwarder != nil:
r.httpForwarder.ServeTCP(r.GetConn(conn, peeked))
default:
conn.Close()
}
return
}
// FIXME Optimize and test the routing table before helloServerName
serverName = types.CanonicalDomain(serverName)
if r.routingTable != nil && serverName != "" {
if target, ok := r.routingTable[serverName]; ok {
target.ServeTCP(r.GetConn(conn, peeked))
return
}
}
// FIXME Needs tests
if target, ok := r.routingTable["*"]; ok {
target.ServeTCP(r.GetConn(conn, peeked))
return
}
if r.httpsForwarder != nil {
r.httpsForwarder.ServeTCP(r.GetConn(conn, peeked))
} else {
conn.Close()
}
}
// AddRoute defines a handler for a given sniHost (* is the only valid option).
func (r *Router) AddRoute(sniHost string, target Handler) {
if r.routingTable == nil {
r.routingTable = map[string]Handler{}
}
r.routingTable[strings.ToLower(sniHost)] = target
}
// AddRouteTLS defines a handler for a given sniHost and sets the matching tlsConfig.
func (r *Router) AddRouteTLS(sniHost string, target Handler, config *tls.Config) {
r.AddRoute(sniHost, &TLSHandler{
Next: target,
Config: config,
})
}
// AddRouteHTTPTLS defines a handler for a given sniHost and sets the matching tlsConfig.
func (r *Router) AddRouteHTTPTLS(sniHost string, config *tls.Config) {
if r.hostHTTPTLSConfig == nil {
r.hostHTTPTLSConfig = map[string]*tls.Config{}
}
r.hostHTTPTLSConfig[sniHost] = config
}
// AddCatchAllNoTLS defines the fallback tcp handler.
func (r *Router) AddCatchAllNoTLS(handler Handler) {
r.catchAllNoTLS = handler
}
// GetConn creates a connection proxy with a peeked string.
func (r *Router) GetConn(conn WriteCloser, peeked string) WriteCloser {
// FIXME should it really be on Router ?
conn = &Conn{
Peeked: []byte(peeked),
WriteCloser: conn,
}
return conn
}
// GetHTTPHandler gets the attached http handler.
func (r *Router) GetHTTPHandler() http.Handler {
return r.httpHandler
}
// GetHTTPSHandler gets the attached https handler.
func (r *Router) GetHTTPSHandler() http.Handler {
return r.httpsHandler
}
// HTTPForwarder sets the tcp handler that will forward the connections to an http handler.
func (r *Router) HTTPForwarder(handler Handler) {
r.httpForwarder = handler
}
// HTTPSForwarder sets the tcp handler that will forward the TLS connections to an http handler.
func (r *Router) HTTPSForwarder(handler Handler) {
for sniHost, tlsConf := range r.hostHTTPTLSConfig {
r.AddRouteTLS(sniHost, handler, tlsConf)
}
r.httpsForwarder = &TLSHandler{
Next: handler,
Config: r.httpsTLSConfig,
}
}
// HTTPHandler attaches http handlers on the router.
func (r *Router) HTTPHandler(handler http.Handler) {
r.httpHandler = handler
}
// HTTPSHandler attaches https handlers on the router.
func (r *Router) HTTPSHandler(handler http.Handler, config *tls.Config) {
r.httpsHandler = handler
r.httpsTLSConfig = config
}
// Conn is a connection proxy that handles Peeked bytes.
type Conn struct {
// Peeked are the bytes that have been read from Conn for the
// purposes of route matching, but have not yet been consumed
// by Read calls. It set to nil by Read when fully consumed.
Peeked []byte
// Conn is the underlying connection.
// It can be type asserted against *net.TCPConn or other types
// as needed. It should not be read from directly unless
// Peeked is nil.
WriteCloser
}
// Read reads bytes from the connection (using the buffer prior to actually reading).
func (c *Conn) Read(p []byte) (n int, err error) {
if len(c.Peeked) > 0 {
n = copy(p, c.Peeked)
c.Peeked = c.Peeked[n:]
if len(c.Peeked) == 0 {
c.Peeked = nil
}
return n, nil
}
return c.WriteCloser.Read(p)
}
// clientHelloServerName returns the SNI server name inside the TLS ClientHello,
// without consuming any bytes from br.
// On any error, the empty string is returned.
func clientHelloServerName(br *bufio.Reader) (string, bool, string, error) {
hdr, err := br.Peek(1)
if err != nil {
var opErr *net.OpError
if !errors.Is(err, io.EOF) && (!errors.As(err, &opErr) || opErr.Timeout()) {
log.WithoutContext().Debugf("Error while Peeking first byte: %s", err)
}
return "", false, "", err
}
// No valid TLS record has a type of 0x80, however SSLv2 handshakes
// start with a uint16 length where the MSB is set and the first record
// is always < 256 bytes long. Therefore typ == 0x80 strongly suggests
// an SSLv2 client.
const recordTypeSSLv2 = 0x80
const recordTypeHandshake = 0x16
if hdr[0] != recordTypeHandshake {
if hdr[0] == recordTypeSSLv2 {
// we consider SSLv2 as TLS and it will be refuse by real TLS handshake.
return "", true, getPeeked(br), nil
}
return "", false, getPeeked(br), nil // Not TLS.
}
const recordHeaderLen = 5
hdr, err = br.Peek(recordHeaderLen)
if err != nil {
log.Errorf("Error while Peeking hello: %s", err)
return "", false, getPeeked(br), nil
}
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
if recordHeaderLen+recLen > defaultBufSize {
br = bufio.NewReaderSize(br, recordHeaderLen+recLen)
}
helloBytes, err := br.Peek(recordHeaderLen + recLen)
if err != nil {
log.Errorf("Error while Hello: %s", err)
return "", true, getPeeked(br), nil
}
sni := ""
server := tls.Server(sniSniffConn{r: bytes.NewReader(helloBytes)}, &tls.Config{
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
sni = hello.ServerName
return nil, nil
},
})
_ = server.Handshake()
return sni, true, getPeeked(br), nil
}
func getPeeked(br *bufio.Reader) string {
peeked, err := br.Peek(br.Buffered())
if err != nil {
log.Errorf("Could not get anything: %s", err)
return ""
}
return string(peeked)
}
// sniSniffConn is a net.Conn that reads from r, fails on Writes,
// and crashes otherwise.
type sniSniffConn struct {
r io.Reader
net.Conn // nil; crash on any unexpected use
}
// Read reads from the underlying reader.
func (c sniSniffConn) Read(p []byte) (int, error) { return c.r.Read(p) }
// Write crashes all the time.
func (sniSniffConn) Write(p []byte) (int, error) { return 0, io.EOF }