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