Merge current v2.7 into master
This commit is contained in:
commit
521109d3f2
60 changed files with 2136 additions and 529 deletions
|
@ -16,7 +16,7 @@ import (
|
|||
"github.com/traefik/traefik/v2/pkg/version"
|
||||
)
|
||||
|
||||
// collectorURL URL where the stats are send.
|
||||
// collectorURL URL where the stats are sent.
|
||||
const collectorURL = "https://collect.traefik.io/9vxmmkcdmalbdi635d4jgc5p5rx0h7h8"
|
||||
|
||||
// Collected data.
|
||||
|
@ -30,16 +30,30 @@ type data struct {
|
|||
|
||||
// Collect anonymous data.
|
||||
func Collect(staticConfiguration *static.Configuration) error {
|
||||
anonConfig, err := redactor.Anonymize(staticConfiguration)
|
||||
buf, err := createBody(staticConfiguration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := makeHTTPClient().Post(collectorURL, "application/json; charset=utf-8", buf)
|
||||
if resp != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func createBody(staticConfiguration *static.Configuration) (*bytes.Buffer, error) {
|
||||
anonConfig, err := redactor.Anonymize(staticConfiguration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.WithoutContext().Infof("Anonymous stats sent to %s: %s", collectorURL, anonConfig)
|
||||
|
||||
hashConf, err := hashstructure.Hash(staticConfiguration, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := &data{
|
||||
|
@ -53,15 +67,10 @@ func Collect(staticConfiguration *static.Configuration) error {
|
|||
buf := new(bytes.Buffer)
|
||||
err = json.NewEncoder(buf).Encode(data)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := makeHTTPClient().Post(collectorURL, "application/json; charset=utf-8", buf)
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
return err
|
||||
return buf, err
|
||||
}
|
||||
|
||||
func makeHTTPClient() *http.Client {
|
||||
|
|
21
pkg/collector/collector_test.go
Normal file
21
pkg/collector/collector_test.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package collector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v2/pkg/config/static"
|
||||
)
|
||||
|
||||
func Test_createBody(t *testing.T) {
|
||||
var staticConfiguration static.Configuration
|
||||
|
||||
err := hydrate(&staticConfiguration)
|
||||
require.NoError(t, err)
|
||||
|
||||
buffer, err := createBody(&staticConfiguration)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEmpty(t, buffer)
|
||||
}
|
166
pkg/collector/hydration_test.go
Normal file
166
pkg/collector/hydration_test.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
package collector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/traefik/paerser/types"
|
||||
)
|
||||
|
||||
const (
|
||||
sliceItemNumber = 2
|
||||
mapItemNumber = 2
|
||||
defaultString = "foobar"
|
||||
defaultNumber = 42
|
||||
defaultBool = true
|
||||
defaultMapKeyPrefix = "name"
|
||||
)
|
||||
|
||||
func hydrate(element interface{}) error {
|
||||
field := reflect.ValueOf(element)
|
||||
return fill(field)
|
||||
}
|
||||
|
||||
func fill(field reflect.Value) error {
|
||||
switch field.Kind() {
|
||||
case reflect.Struct:
|
||||
if err := setStruct(field); err != nil {
|
||||
return err
|
||||
}
|
||||
case reflect.Ptr:
|
||||
if err := setPointer(field); err != nil {
|
||||
return err
|
||||
}
|
||||
case reflect.Slice:
|
||||
if err := setSlice(field); err != nil {
|
||||
return err
|
||||
}
|
||||
case reflect.Map:
|
||||
if err := setMap(field); err != nil {
|
||||
return err
|
||||
}
|
||||
case reflect.Interface:
|
||||
if err := fill(field.Elem()); err != nil {
|
||||
return err
|
||||
}
|
||||
case reflect.String:
|
||||
setTyped(field, defaultString)
|
||||
case reflect.Int:
|
||||
setTyped(field, defaultNumber)
|
||||
case reflect.Int8:
|
||||
setTyped(field, int8(defaultNumber))
|
||||
case reflect.Int16:
|
||||
setTyped(field, int16(defaultNumber))
|
||||
case reflect.Int32:
|
||||
setTyped(field, int32(defaultNumber))
|
||||
case reflect.Int64:
|
||||
switch field.Type() {
|
||||
case reflect.TypeOf(types.Duration(time.Second)):
|
||||
setTyped(field, int64(defaultNumber*int(time.Second)))
|
||||
default:
|
||||
setTyped(field, int64(defaultNumber))
|
||||
}
|
||||
case reflect.Uint:
|
||||
setTyped(field, uint(defaultNumber))
|
||||
case reflect.Uint8:
|
||||
setTyped(field, uint8(defaultNumber))
|
||||
case reflect.Uint16:
|
||||
setTyped(field, uint16(defaultNumber))
|
||||
case reflect.Uint32:
|
||||
setTyped(field, uint32(defaultNumber))
|
||||
case reflect.Uint64:
|
||||
setTyped(field, uint64(defaultNumber))
|
||||
case reflect.Bool:
|
||||
setTyped(field, defaultBool)
|
||||
case reflect.Float32:
|
||||
setTyped(field, float32(defaultNumber))
|
||||
case reflect.Float64:
|
||||
setTyped(field, float64(defaultNumber))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setTyped(field reflect.Value, i interface{}) {
|
||||
baseValue := reflect.ValueOf(i)
|
||||
if field.Kind().String() == field.Type().String() {
|
||||
field.Set(baseValue)
|
||||
} else {
|
||||
field.Set(baseValue.Convert(field.Type()))
|
||||
}
|
||||
}
|
||||
|
||||
func setMap(field reflect.Value) error {
|
||||
field.Set(reflect.MakeMap(field.Type()))
|
||||
|
||||
for i := 0; i < mapItemNumber; i++ {
|
||||
baseKeyName := makeKeyName(field.Type().Elem())
|
||||
key := reflect.ValueOf(fmt.Sprintf("%s%d", baseKeyName, i))
|
||||
|
||||
// generate value
|
||||
ptrType := reflect.PtrTo(field.Type().Elem())
|
||||
ptrValue := reflect.New(ptrType)
|
||||
if err := fill(ptrValue); err != nil {
|
||||
return err
|
||||
}
|
||||
value := ptrValue.Elem().Elem()
|
||||
|
||||
field.SetMapIndex(key, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeKeyName(typ reflect.Type) string {
|
||||
switch typ.Kind() {
|
||||
case reflect.Ptr:
|
||||
return typ.Elem().Name()
|
||||
case reflect.String,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Bool, reflect.Float32, reflect.Float64:
|
||||
return defaultMapKeyPrefix
|
||||
default:
|
||||
return typ.Name()
|
||||
}
|
||||
}
|
||||
|
||||
func setStruct(field reflect.Value) error {
|
||||
for i := 0; i < field.NumField(); i++ {
|
||||
fld := field.Field(i)
|
||||
stFld := field.Type().Field(i)
|
||||
|
||||
if !stFld.IsExported() || fld.Kind() == reflect.Func {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := fill(fld); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setSlice(field reflect.Value) error {
|
||||
field.Set(reflect.MakeSlice(field.Type(), sliceItemNumber, sliceItemNumber))
|
||||
for j := 0; j < field.Len(); j++ {
|
||||
if err := fill(field.Index(j)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setPointer(field reflect.Value) error {
|
||||
if field.IsNil() {
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
if err := fill(field.Elem()); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := fill(field.Elem()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
53
pkg/config/static/hub.go
Normal file
53
pkg/config/static/hub.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package static
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/traefik/traefik/v2/pkg/log"
|
||||
"github.com/traefik/traefik/v2/pkg/provider/hub"
|
||||
)
|
||||
|
||||
func (c *Configuration) initHubProvider() error {
|
||||
// Hub provider is an experimental feature. It requires the experimental flag to be enabled before continuing.
|
||||
if c.Experimental == nil || !c.Experimental.Hub {
|
||||
return errors.New("the experimental flag for Hub is not set")
|
||||
}
|
||||
|
||||
if _, ok := c.EntryPoints[hub.TunnelEntrypoint]; !ok {
|
||||
var ep EntryPoint
|
||||
ep.SetDefaults()
|
||||
ep.Address = ":9901"
|
||||
c.EntryPoints[hub.TunnelEntrypoint] = &ep
|
||||
log.WithoutContext().Infof("The entryPoint %q is created on port 9901 to allow exposition of services.", hub.TunnelEntrypoint)
|
||||
}
|
||||
|
||||
if c.Hub.TLS == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Hub.TLS.Insecure && (c.Hub.TLS.CA != "" || c.Hub.TLS.Cert != "" || c.Hub.TLS.Key != "") {
|
||||
return errors.New("mTLS configuration for Hub and insecure TLS for Hub are mutually exclusive")
|
||||
}
|
||||
|
||||
if !c.Hub.TLS.Insecure && (c.Hub.TLS.CA == "" || c.Hub.TLS.Cert == "" || c.Hub.TLS.Key == "") {
|
||||
return errors.New("incomplete mTLS configuration for Hub")
|
||||
}
|
||||
|
||||
if c.Hub.TLS.Insecure {
|
||||
log.WithoutContext().Warn("Hub is in `insecure` mode. Do not run in production with this setup.")
|
||||
}
|
||||
|
||||
if _, ok := c.EntryPoints[hub.APIEntrypoint]; !ok {
|
||||
var ep EntryPoint
|
||||
ep.SetDefaults()
|
||||
ep.Address = ":9900"
|
||||
c.EntryPoints[hub.APIEntrypoint] = &ep
|
||||
log.WithoutContext().Infof("The entryPoint %q is created on port 9900 to allow Traefik to communicate with the Hub Agent for Traefik.", hub.APIEntrypoint)
|
||||
}
|
||||
|
||||
c.EntryPoints[hub.APIEntrypoint].HTTP.TLS = &TLSConfig{
|
||||
Options: "traefik-hub",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package static
|
||||
|
||||
// Pilot Configuration related to Traefik Pilot.
|
||||
// Deprecated.
|
||||
type Pilot struct {
|
||||
Token string `description:"Traefik Pilot token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
|
||||
Dashboard bool `description:"Enable Traefik Pilot in the dashboard." json:"dashboard,omitempty" toml:"dashboard,omitempty" yaml:"dashboard,omitempty"`
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package static
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"strings"
|
||||
|
@ -78,7 +77,6 @@ type Configuration struct {
|
|||
|
||||
CertificatesResolvers map[string]CertificateResolver `description:"Certificates resolvers configuration." json:"certificatesResolvers,omitempty" toml:"certificatesResolvers,omitempty" yaml:"certificatesResolvers,omitempty" export:"true"`
|
||||
|
||||
// Deprecated.
|
||||
Pilot *Pilot `description:"Traefik Pilot configuration." json:"pilot,omitempty" toml:"pilot,omitempty" yaml:"pilot,omitempty" export:"true"`
|
||||
|
||||
Hub *hub.Provider `description:"Traefik Hub configuration." json:"hub,omitempty" toml:"hub,omitempty" yaml:"hub,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
|
@ -201,7 +199,7 @@ type Providers struct {
|
|||
// It also takes care of maintaining backwards compatibility.
|
||||
func (c *Configuration) SetEffectiveConfiguration() {
|
||||
// Creates the default entry point if needed
|
||||
if len(c.EntryPoints) == 0 || (c.Hub != nil && len(c.EntryPoints) == 1 && c.EntryPoints[c.Hub.EntryPoint] != nil) {
|
||||
if !c.hasUserDefinedEntrypoint() {
|
||||
ep := &EntryPoint{Address: ":80"}
|
||||
ep.SetDefaults()
|
||||
// TODO: double check this tomorrow
|
||||
|
@ -287,6 +285,21 @@ func (c *Configuration) SetEffectiveConfiguration() {
|
|||
c.initACMEProvider()
|
||||
}
|
||||
|
||||
func (c *Configuration) hasUserDefinedEntrypoint() bool {
|
||||
if len(c.EntryPoints) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch len(c.EntryPoints) {
|
||||
case 1:
|
||||
return c.EntryPoints[hub.TunnelEntrypoint] == nil
|
||||
case 2:
|
||||
return c.EntryPoints[hub.TunnelEntrypoint] == nil || c.EntryPoints[hub.APIEntrypoint] == nil
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Configuration) initACMEProvider() {
|
||||
for _, resolver := range c.CertificatesResolvers {
|
||||
if resolver.ACME != nil {
|
||||
|
@ -297,46 +310,6 @@ func (c *Configuration) initACMEProvider() {
|
|||
legolog.Logger = stdlog.New(log.WithoutContext().WriterLevel(logrus.DebugLevel), "legolog: ", 0)
|
||||
}
|
||||
|
||||
func (c *Configuration) initHubProvider() error {
|
||||
// Hub provider is an experimental feature. Require the experimental flag to be enabled before continuing.
|
||||
if c.Experimental == nil || !c.Experimental.Hub {
|
||||
return errors.New("experimental flag for Hub not set")
|
||||
}
|
||||
|
||||
if c.Hub.TLS == nil {
|
||||
return errors.New("no TLS configuration defined for Hub")
|
||||
}
|
||||
|
||||
if c.Hub.TLS.Insecure && (c.Hub.TLS.CA != "" || c.Hub.TLS.Cert != "" || c.Hub.TLS.Key != "") {
|
||||
return errors.New("mTLS configuration for Hub and insecure TLS for Hub are mutually exclusive")
|
||||
}
|
||||
|
||||
if !c.Hub.TLS.Insecure && (c.Hub.TLS.CA == "" || c.Hub.TLS.Cert == "" || c.Hub.TLS.Key == "") {
|
||||
return errors.New("incomplete mTLS configuration for Hub")
|
||||
}
|
||||
|
||||
if c.Hub.TLS.Insecure {
|
||||
log.WithoutContext().Warn("Hub is in `insecure` mode. Do not run in production with this setup.")
|
||||
}
|
||||
|
||||
// Creates the internal Hub entry point if needed.
|
||||
if c.Hub.EntryPoint == hub.DefaultEntryPointName {
|
||||
if _, ok := c.EntryPoints[hub.DefaultEntryPointName]; !ok {
|
||||
var ep EntryPoint
|
||||
ep.SetDefaults()
|
||||
ep.Address = ":9900"
|
||||
c.EntryPoints[hub.DefaultEntryPointName] = &ep
|
||||
log.WithoutContext().Infof("The entryPoint %q is created on port 9900 to allow Traefik to communicate with the Hub Agent for Traefik.", hub.DefaultEntryPointName)
|
||||
}
|
||||
}
|
||||
|
||||
c.EntryPoints[c.Hub.EntryPoint].HTTP.TLS = &TLSConfig{
|
||||
Options: "traefik-hub",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateConfiguration validate that configuration is coherent.
|
||||
func (c *Configuration) ValidateConfiguration() error {
|
||||
var acmeEmail string
|
||||
|
|
88
pkg/config/static/static_config_test.go
Normal file
88
pkg/config/static/static_config_test.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package static
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/traefik/traefik/v2/pkg/provider/hub"
|
||||
)
|
||||
|
||||
func TestHasEntrypoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
entryPoints map[string]*EntryPoint
|
||||
assert assert.BoolAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "no user defined entryPoints",
|
||||
assert: assert.False,
|
||||
},
|
||||
{
|
||||
desc: "user defined entryPoints",
|
||||
entryPoints: map[string]*EntryPoint{
|
||||
"foo": {},
|
||||
},
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "user defined entryPoints + hub entryPoint (tunnel)",
|
||||
entryPoints: map[string]*EntryPoint{
|
||||
"foo": {},
|
||||
hub.TunnelEntrypoint: {},
|
||||
},
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "hub entryPoint (tunnel)",
|
||||
entryPoints: map[string]*EntryPoint{
|
||||
hub.TunnelEntrypoint: {},
|
||||
},
|
||||
assert: assert.False,
|
||||
},
|
||||
{
|
||||
desc: "user defined entryPoints + hub entryPoint (api)",
|
||||
entryPoints: map[string]*EntryPoint{
|
||||
"foo": {},
|
||||
hub.APIEntrypoint: {},
|
||||
},
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "hub entryPoint (api)",
|
||||
entryPoints: map[string]*EntryPoint{
|
||||
hub.APIEntrypoint: {},
|
||||
},
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "user defined entryPoints + hub entryPoints (tunnel, api)",
|
||||
entryPoints: map[string]*EntryPoint{
|
||||
"foo": {},
|
||||
hub.TunnelEntrypoint: {},
|
||||
hub.APIEntrypoint: {},
|
||||
},
|
||||
assert: assert.True,
|
||||
},
|
||||
{
|
||||
desc: "hub entryPoints (tunnel, api)",
|
||||
entryPoints: map[string]*EntryPoint{
|
||||
hub.TunnelEntrypoint: {},
|
||||
hub.APIEntrypoint: {},
|
||||
},
|
||||
assert: assert.False,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &Configuration{
|
||||
EntryPoints: test.entryPoints,
|
||||
}
|
||||
|
||||
test.assert(t, cfg.hasUserDefinedEntrypoint())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -95,32 +95,21 @@ func NewMuxer() (*Muxer, error) {
|
|||
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 {
|
||||
// Match returns the handler of the first route matching the connection metadata,
|
||||
// and whether the match is exactly from the rule HostSNI(*).
|
||||
func (m Muxer) Match(meta ConnData) (tcp.Handler, bool) {
|
||||
for _, route := range m.routes {
|
||||
if route.matchers.match(meta) {
|
||||
return route.handler
|
||||
return route.handler, route.catchAll
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
@ -131,16 +120,36 @@ func (m *Muxer) AddRoute(rule string, priority int, handler tcp.Handler) error {
|
|||
return fmt.Errorf("error while parsing rule %s", rule)
|
||||
}
|
||||
|
||||
ruleTree := buildTree()
|
||||
|
||||
var matchers matchersTree
|
||||
err = addRule(&matchers, buildTree())
|
||||
err = addRule(&matchers, ruleTree)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var catchAll bool
|
||||
if ruleTree.RuleLeft == nil && ruleTree.RuleRight == nil && len(ruleTree.Value) == 1 {
|
||||
catchAll = ruleTree.Value[0] == "*" && strings.EqualFold(ruleTree.Matcher, "HostSNI")
|
||||
}
|
||||
|
||||
// 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 && catchAll {
|
||||
priority = -1
|
||||
}
|
||||
|
||||
// Default value, which means the user has not set it, so we'll compute it.
|
||||
if priority == 0 {
|
||||
priority = len(rule)
|
||||
}
|
||||
|
||||
newRoute := &route{
|
||||
handler: handler,
|
||||
priority: priority,
|
||||
matchers: matchers,
|
||||
catchAll: catchAll,
|
||||
priority: priority,
|
||||
}
|
||||
m.routes = append(m.routes, newRoute)
|
||||
|
||||
|
@ -207,9 +216,10 @@ type route struct {
|
|||
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.
|
||||
// catchAll indicates whether the route rule has exactly the catchAll value (HostSNI(`*`)).
|
||||
catchAll bool
|
||||
// priority is used to disambiguate between two (or more) rules that would
|
||||
// all match for a given request.
|
||||
// Computed from the matching rule length, if not user-set.
|
||||
priority int
|
||||
}
|
||||
|
|
|
@ -474,7 +474,7 @@ func Test_addTCPRoute(t *testing.T) {
|
|||
connData, err := NewConnData(test.serverName, conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
matchingHandler := router.Match(connData)
|
||||
matchingHandler, _ := router.Match(connData)
|
||||
if test.matchErr {
|
||||
require.Nil(t, matchingHandler)
|
||||
return
|
||||
|
@ -568,6 +568,54 @@ func TestParseHostSNI(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_HostSNICatchAll(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
isCatchAll bool
|
||||
}{
|
||||
{
|
||||
desc: "HostSNI(`foobar`) is not catchAll",
|
||||
rule: "HostSNI(`foobar`)",
|
||||
},
|
||||
{
|
||||
desc: "HostSNI(`*`) is catchAll",
|
||||
rule: "HostSNI(`*`)",
|
||||
isCatchAll: true,
|
||||
},
|
||||
{
|
||||
desc: "HOSTSNI(`*`) is catchAll",
|
||||
rule: "HOSTSNI(`*`)",
|
||||
isCatchAll: true,
|
||||
},
|
||||
{
|
||||
desc: `HostSNI("*") is catchAll`,
|
||||
rule: `HostSNI("*")`,
|
||||
isCatchAll: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, catchAll := muxer.Match(ConnData{
|
||||
serverName: "foobar",
|
||||
})
|
||||
require.NotNil(t, handler)
|
||||
assert.Equal(t, test.isCatchAll, catchAll)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_HostSNI(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
|
@ -934,7 +982,7 @@ func Test_Priority(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
handler := muxer.Match(ConnData{
|
||||
handler, _ := muxer.Match(ConnData{
|
||||
serverName: test.serverName,
|
||||
remoteIP: test.remoteIP,
|
||||
})
|
||||
|
|
|
@ -538,7 +538,7 @@ func (p *Provider) addCertificateForDomain(domain types.Domain, certificate, key
|
|||
// The second (RenewInterval) is the interval between renew attempts.
|
||||
func getCertificateRenewDurations(certificatesDuration int) (time.Duration, time.Duration) {
|
||||
switch {
|
||||
case certificatesDuration >= 265*24: // >= 1 year
|
||||
case certificatesDuration >= 365*24: // >= 1 year
|
||||
return 4 * 30 * 24 * time.Hour, 7 * 24 * time.Hour // 4 month, 1 week
|
||||
case certificatesDuration >= 3*30*24: // >= 90 days
|
||||
return 30 * 24 * time.Hour, 24 * time.Hour // 30 days, 1 day
|
||||
|
|
|
@ -608,11 +608,17 @@ func Test_getCertificateRenewDurations(t *testing.T) {
|
|||
expectRenewInterval: time.Minute,
|
||||
},
|
||||
{
|
||||
desc: "1 Year certificates: 2 months renew period, 1 week renew interval",
|
||||
desc: "1 Year certificates: 4 months renew period, 1 week renew interval",
|
||||
certificatesDurations: 24 * 365,
|
||||
expectRenewPeriod: time.Hour * 24 * 30 * 4,
|
||||
expectRenewInterval: time.Hour * 24 * 7,
|
||||
},
|
||||
{
|
||||
desc: "265 Days certificates: 30 days renew period, 1 day renew interval",
|
||||
certificatesDurations: 24 * 265,
|
||||
expectRenewPeriod: time.Hour * 24 * 30,
|
||||
expectRenewInterval: time.Hour * 24,
|
||||
},
|
||||
{
|
||||
desc: "90 Days certificates: 30 days renew period, 1 day renew interval",
|
||||
certificatesDurations: 24 * 90,
|
||||
|
|
|
@ -392,6 +392,13 @@ func (p *Provider) lookupEc2Instances(ctx context.Context, client *awsClient, cl
|
|||
|
||||
for _, container := range resp.ContainerInstances {
|
||||
instanceIds[aws.StringValue(container.Ec2InstanceId)] = aws.StringValue(container.ContainerInstanceArn)
|
||||
// Disallow Instance IDs of the form mi-*
|
||||
// This prevents considering external instances in ECS Anywhere setups
|
||||
// and getting InvalidInstanceID.Malformed error when calling the describe-instances endpoint.
|
||||
if strings.HasPrefix(aws.StringValue(container.Ec2InstanceId), "mi-") {
|
||||
continue
|
||||
}
|
||||
|
||||
instanceArns = append(instanceArns, container.Ec2InstanceId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
|
@ -101,7 +102,7 @@ func (h *handler) handleDiscoverIP(rw http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
func (h *handler) doDiscoveryReq(ctx context.Context, ip, port, nonce string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s:%s", ip, port), http.NoBody)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s", net.JoinHostPort(ip, port)), http.NoBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
|
|
@ -17,13 +17,15 @@ import (
|
|||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
// DefaultEntryPointName is the name of the default internal entry point.
|
||||
const DefaultEntryPointName = "traefik-hub"
|
||||
// Entrypoints created for Hub.
|
||||
const (
|
||||
APIEntrypoint = "traefikhub-api"
|
||||
TunnelEntrypoint = "traefikhub-tunl"
|
||||
)
|
||||
|
||||
// Provider holds configurations of the provider.
|
||||
type Provider struct {
|
||||
EntryPoint string `description:"Entrypoint that exposes data for Traefik Hub. It should be a dedicated one, and not used by any router." json:"entryPoint,omitempty" toml:"entryPoint,omitempty" yaml:"entryPoint,omitempty" export:"true"`
|
||||
TLS *TLS `description:"TLS configuration for mTLS communication between Traefik and Hub Agent." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
|
||||
TLS *TLS `description:"TLS configuration for mTLS communication between Traefik and Hub Agent." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
|
||||
|
||||
server *http.Server
|
||||
}
|
||||
|
@ -36,11 +38,6 @@ type TLS struct {
|
|||
Key ttls.FileOrContent `description:"The TLS key for Traefik Proxy as a TLS client." json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty" loggable:"false"`
|
||||
}
|
||||
|
||||
// SetDefaults sets the default values.
|
||||
func (p *Provider) SetDefaults() {
|
||||
p.EntryPoint = DefaultEntryPointName
|
||||
}
|
||||
|
||||
// Init the provider.
|
||||
func (p *Provider) Init() error {
|
||||
return nil
|
||||
|
@ -48,10 +45,15 @@ func (p *Provider) Init() error {
|
|||
|
||||
// Provide allows the hub provider to provide configurations to traefik using the given configuration channel.
|
||||
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
|
||||
if p.TLS == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return fmt.Errorf("listener: %w", err)
|
||||
}
|
||||
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
client, err := createAgentClient(p.TLS)
|
||||
|
@ -59,7 +61,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Poo
|
|||
return fmt.Errorf("creating Hub Agent HTTP client: %w", err)
|
||||
}
|
||||
|
||||
p.server = &http.Server{Handler: newHandler(p.EntryPoint, port, configurationChan, p.TLS, client)}
|
||||
p.server = &http.Server{Handler: newHandler(APIEntrypoint, port, configurationChan, p.TLS, client)}
|
||||
|
||||
// TODO: this is going to be leaky (because no context to make it terminate)
|
||||
// if/when Provide lifecycle differs with Traefik lifecycle.
|
||||
|
@ -70,7 +72,7 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Poo
|
|||
}
|
||||
}()
|
||||
|
||||
exposeAPIAndMetrics(configurationChan, p.EntryPoint, port, p.TLS)
|
||||
exposeAPIAndMetrics(configurationChan, APIEntrypoint, port, p.TLS)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -413,7 +413,7 @@ func getServicePort(svc *corev1.Service, port intstr.IntOrString) (*corev1.Servi
|
|||
|
||||
if hasValidPort {
|
||||
log.WithoutContext().
|
||||
Warning("The port %d from IngressRoute doesn't match with ports defined in the ExternalName service %s/%s.", port, svc.Namespace, svc.Name)
|
||||
Warnf("The port %s from IngressRoute doesn't match with ports defined in the ExternalName service %s/%s.", port, svc.Namespace, svc.Name)
|
||||
}
|
||||
|
||||
return &corev1.ServicePort{Port: port.IntVal}, nil
|
||||
|
|
|
@ -93,7 +93,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
|
|||
return
|
||||
}
|
||||
|
||||
handler := r.muxerTCP.Match(connData)
|
||||
handler, _ := r.muxerTCP.Match(connData)
|
||||
// If there is a handler matching the connection metadata,
|
||||
// we let it handle the connection.
|
||||
if handler != nil {
|
||||
|
@ -133,7 +133,7 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
|
|||
}
|
||||
|
||||
if !tls {
|
||||
handler := r.muxerTCP.Match(connData)
|
||||
handler, _ := r.muxerTCP.Match(connData)
|
||||
switch {
|
||||
case handler != nil:
|
||||
handler.ServeTCP(r.GetConn(conn, peeked))
|
||||
|
@ -145,20 +145,38 @@ func (r *Router) ServeTCP(conn tcp.WriteCloser) {
|
|||
return
|
||||
}
|
||||
|
||||
handler := r.muxerTCPTLS.Match(connData)
|
||||
if handler != nil {
|
||||
handler.ServeTCP(r.GetConn(conn, peeked))
|
||||
// For real, the handler eventually used for HTTPS 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
|
||||
// (wrapped inside the returned handler) requested for the given HostSNI.
|
||||
handlerHTTPS, catchAllHTTPS := r.muxerHTTPS.Match(connData)
|
||||
if handlerHTTPS != nil && !catchAllHTTPS {
|
||||
// In order not to depart from the behavior in 2.6, we only allow an HTTPS router
|
||||
// to take precedence over a TCP-TLS router if it is _not_ an HostSNI(*) router (so
|
||||
// basically any router that has a specific HostSNI based rule).
|
||||
handlerHTTPS.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))
|
||||
// Contains also TCP TLS passthrough routes.
|
||||
handlerTCPTLS, catchAllTCPTLS := r.muxerTCPTLS.Match(connData)
|
||||
if handlerTCPTLS != nil && !catchAllTCPTLS {
|
||||
handlerTCPTLS.ServeTCP(r.GetConn(conn, peeked))
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback on HTTPS catchAll.
|
||||
// We end up here for e.g. an HTTPS router that only has a PathPrefix rule,
|
||||
// which under the scenes is counted as an HostSNI(*) rule.
|
||||
if handlerHTTPS != nil {
|
||||
handlerHTTPS.ServeTCP(r.GetConn(conn, peeked))
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback on TCP TLS catchAll.
|
||||
if handlerTCPTLS != nil {
|
||||
handlerTCPTLS.ServeTCP(r.GetConn(conn, peeked))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
919
pkg/server/router/tcp/router_test.go
Normal file
919
pkg/server/router/tcp/router_test.go
Normal file
|
@ -0,0 +1,919 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v2/pkg/config/dynamic"
|
||||
"github.com/traefik/traefik/v2/pkg/config/runtime"
|
||||
tcpmiddleware "github.com/traefik/traefik/v2/pkg/server/middleware/tcp"
|
||||
"github.com/traefik/traefik/v2/pkg/server/service/tcp"
|
||||
tcp2 "github.com/traefik/traefik/v2/pkg/tcp"
|
||||
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
|
||||
)
|
||||
|
||||
type applyRouter func(conf *runtime.Configuration)
|
||||
|
||||
type checkRouter func(addr string, timeout time.Duration) error
|
||||
|
||||
type httpForwarder struct {
|
||||
net.Listener
|
||||
connChan chan net.Conn
|
||||
errChan chan error
|
||||
}
|
||||
|
||||
func newHTTPForwarder(ln net.Listener) *httpForwarder {
|
||||
return &httpForwarder{
|
||||
Listener: ln,
|
||||
connChan: make(chan net.Conn),
|
||||
errChan: make(chan error),
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the Listener.
|
||||
func (h *httpForwarder) Close() error {
|
||||
h.errChan <- http.ErrServerClosed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeTCP uses the connection to serve it later in "Accept".
|
||||
func (h *httpForwarder) ServeTCP(conn tcp2.WriteCloser) {
|
||||
h.connChan <- conn
|
||||
}
|
||||
|
||||
// Accept retrieves a served connection in ServeTCP.
|
||||
func (h *httpForwarder) Accept() (net.Conn, error) {
|
||||
select {
|
||||
case conn := <-h.connChan:
|
||||
return conn, nil
|
||||
case err := <-h.errChan:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Test_Routing aims to settle the behavior between routers of different types on the same TCP entryPoint.
|
||||
// It has been introduced as a regression test following a fix on the v2.7 TCP Muxer.
|
||||
//
|
||||
// The routing precedence is roughly as follows:
|
||||
// - TCP over HTTP
|
||||
// - HTTPS over TCP-TLS
|
||||
//
|
||||
// Discrepancies for server sending first bytes support:
|
||||
// - On v2.6, it is possible as long as you have one and only one TCP Non-TLS HostSNI(`*`) router (so called CatchAllNoTLS) defined.
|
||||
// - On v2.7, it is possible as long as you have zero TLS/HTTPS router defined.
|
||||
//
|
||||
// Discrepancies in routing precedence between TCP and HTTP routers:
|
||||
// - TCP HostSNI(`*`) and HTTP Host(`foobar`)
|
||||
// - On v2.6 and v2.7, the TCP one takes precedence.
|
||||
//
|
||||
// - TCP ClientIP(`[::]`) and HTTP Host(`foobar`)
|
||||
// - On v2.6, ClientIP matcher doesn't exist.
|
||||
// - On v2.7, the TCP one takes precedence.
|
||||
//
|
||||
// Routing precedence between TCP-TLS and HTTPS routers (considering a request/connection with the servername "foobar"):
|
||||
// - TCP-TLS HostSNI(`*`) and HTTPS Host(`foobar`)
|
||||
// - On v2.6 and v2.7, the HTTPS one takes precedence.
|
||||
//
|
||||
// - TCP-TLS HostSNI(`foobar`) and HTTPS Host(`foobar`)
|
||||
// - On v2.6 and v2.7, the HTTPS one takes precedence (overriding the TCP-TLS one in v2.6).
|
||||
//
|
||||
// - TCP-TLS HostSNI(`*`) and HTTPS PathPrefix(`/`)
|
||||
// - On v2.6 and v2.7, the HTTPS one takes precedence (overriding the TCP-TLS one in v2.6).
|
||||
//
|
||||
// - TCP-TLS HostSNI(`foobar`) and HTTPS PathPrefix(`/`)
|
||||
// - On v2.6 and v2.7, the TCP-TLS one takes precedence.
|
||||
//
|
||||
func Test_Routing(t *testing.T) {
|
||||
// This listener simulates the backend service.
|
||||
// It is capable of switching into server first communication mode,
|
||||
// if the client takes to long to send the first bytes.
|
||||
tcpBackendListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
// This allows the closing of the TCP backend listener to happen last.
|
||||
t.Cleanup(func() {
|
||||
tcpBackendListener.Close()
|
||||
})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := tcpBackendListener.Accept()
|
||||
if err != nil {
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Temporary() {
|
||||
continue
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, 100)
|
||||
_, err = conn.Read(buf)
|
||||
|
||||
var opErr *net.OpError
|
||||
if err == nil {
|
||||
_, err = fmt.Fprint(conn, "TCP-CLIENT-FIRST")
|
||||
require.NoError(t, err)
|
||||
} else if errors.As(err, &opErr) && opErr.Timeout() {
|
||||
_, err = fmt.Fprint(conn, "TCP-SERVER-FIRST")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err = conn.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Configuration defining the TCP backend service, used by TCP routers later.
|
||||
conf := &runtime.Configuration{
|
||||
TCPServices: map[string]*runtime.TCPServiceInfo{
|
||||
"tcp": {
|
||||
TCPService: &dynamic.TCPService{
|
||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{
|
||||
Servers: []dynamic.TCPServer{
|
||||
{
|
||||
Address: tcpBackendListener.Addr().String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
serviceManager := tcp.NewManager(conf)
|
||||
|
||||
// Creates the tlsManager and defines the TLS 1.0 and 1.2 TLSOptions.
|
||||
tlsManager := traefiktls.NewManager()
|
||||
tlsManager.UpdateConfigs(
|
||||
context.Background(),
|
||||
map[string]traefiktls.Store{},
|
||||
map[string]traefiktls.Options{
|
||||
"default": {
|
||||
MaxVersion: "VersionTLS10",
|
||||
},
|
||||
"tls10": {
|
||||
MaxVersion: "VersionTLS10",
|
||||
},
|
||||
"tls12": {
|
||||
MinVersion: "VersionTLS12",
|
||||
MaxVersion: "VersionTLS12",
|
||||
},
|
||||
},
|
||||
[]*traefiktls.CertAndStores{})
|
||||
|
||||
middlewaresBuilder := tcpmiddleware.NewBuilder(conf.TCPMiddlewares)
|
||||
|
||||
manager := NewManager(conf, serviceManager, middlewaresBuilder,
|
||||
nil, nil, tlsManager)
|
||||
|
||||
type checkCase struct {
|
||||
checkRouter
|
||||
|
||||
desc string
|
||||
expectedError string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
routers []applyRouter
|
||||
checks []checkCase
|
||||
}{
|
||||
{
|
||||
desc: "No routers",
|
||||
routers: []applyRouter{},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP with client sending first bytes should fail",
|
||||
checkRouter: checkTCPClientFirst,
|
||||
expectedError: "i/o timeout",
|
||||
},
|
||||
{
|
||||
desc: "TCP with server sending first bytes should fail",
|
||||
checkRouter: checkTCPServerFirst,
|
||||
expectedError: "i/o timeout",
|
||||
},
|
||||
{
|
||||
desc: "HTTP request should be handled by HTTP service (404)",
|
||||
checkRouter: checkHTTP,
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS 1.0 connection should fail",
|
||||
checkRouter: checkTCPTLS10,
|
||||
expectedError: "i/o timeout",
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS 1.2 connection should fail",
|
||||
checkRouter: checkTCPTLS12,
|
||||
// The HTTPS forwarder catches the connection with the TLS 1.0 config,
|
||||
// because no matching routes are defined with the custom TLS Config.
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.0 request should be handled by HTTPS (HTTPS forwarder with tls 1.0 config) (404)",
|
||||
checkRouter: checkHTTPSTLS10,
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.2 request should fail",
|
||||
checkRouter: checkHTTPSTLS12,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Single TCP CatchAll router",
|
||||
routers: []applyRouter{routerTCPCatchAll},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP with client sending first bytes should be handled by TCP service",
|
||||
checkRouter: checkTCPClientFirst,
|
||||
},
|
||||
{
|
||||
desc: "TCP with server sending first bytes should be handled by TCP service",
|
||||
checkRouter: checkTCPServerFirst,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Single HTTP router",
|
||||
routers: []applyRouter{routerHTTP},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "HTTP request should be handled by HTTP service",
|
||||
checkRouter: checkHTTP,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Single TCP TLS router",
|
||||
routers: []applyRouter{routerTCPTLS},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP TLS 1.0 connection should fail",
|
||||
checkRouter: checkTCPTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS 1.2 connection should be handled by TCP service",
|
||||
checkRouter: checkTCPTLS12,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Single TCP TLS CatchAll router",
|
||||
routers: []applyRouter{routerTCPTLSCatchAll},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP TLS 1.0 connection should be handled by TCP service",
|
||||
checkRouter: checkTCPTLS10,
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS 1.2 connection should fail",
|
||||
checkRouter: checkTCPTLS12,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Single HTTPS router",
|
||||
routers: []applyRouter{routerHTTPS},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "HTTPS TLS 1.0 request should fail",
|
||||
checkRouter: checkHTTPSTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.2 request should be handled by HTTPS service",
|
||||
checkRouter: checkHTTPSTLS12,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Single HTTPS PathPrefix router",
|
||||
routers: []applyRouter{routerHTTPSPathPrefix},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "HTTPS TLS 1.0 request should be handled by HTTPS service",
|
||||
checkRouter: checkHTTPSTLS10,
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.2 request should fail",
|
||||
checkRouter: checkHTTPSTLS12,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "TCP CatchAll router && HTTP router",
|
||||
routers: []applyRouter{routerTCPCatchAll, routerHTTP},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP client sending first bytes should be handled by TCP service",
|
||||
checkRouter: checkTCPClientFirst,
|
||||
},
|
||||
{
|
||||
desc: "TCP server sending first bytes should be handled by TCP service",
|
||||
checkRouter: checkTCPServerFirst,
|
||||
},
|
||||
{
|
||||
desc: "HTTP request should fail, because handled by TCP service",
|
||||
checkRouter: checkHTTP,
|
||||
expectedError: "malformed HTTP response",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS CatchAll router && HTTP router",
|
||||
routers: []applyRouter{routerTCPTLS, routerHTTP},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP TLS 1.0 connection should fail",
|
||||
checkRouter: checkTCPTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS 1.2 connection should be handled by TCP service",
|
||||
checkRouter: checkTCPTLS12,
|
||||
},
|
||||
{
|
||||
desc: "HTTP request should be handled by HTTP service",
|
||||
checkRouter: checkHTTP,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "TCP CatchAll router && HTTPS router",
|
||||
routers: []applyRouter{routerTCPCatchAll, routerHTTPS},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP client sending first bytes should be handled by TCP service",
|
||||
checkRouter: checkTCPClientFirst,
|
||||
},
|
||||
{
|
||||
desc: "TCP server sending first bytes should timeout on clientHello",
|
||||
checkRouter: checkTCPServerFirst,
|
||||
expectedError: "i/o timeout",
|
||||
},
|
||||
{
|
||||
desc: "HTTP request should fail, because handled by TCP service",
|
||||
checkRouter: checkHTTP,
|
||||
expectedError: "malformed HTTP response",
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.0 request should be handled by HTTPS service",
|
||||
checkRouter: checkHTTPSTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.2 request should be handled by HTTPS service",
|
||||
checkRouter: checkHTTPSTLS12,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// We show that a not CatchAll HTTPS router takes priority over a TCP-TLS router.
|
||||
desc: "TCP TLS router && HTTPS router",
|
||||
routers: []applyRouter{routerTCPTLS, routerHTTPS},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP TLS 1.0 connection should fail",
|
||||
checkRouter: checkTCPTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS 1.2 connection should fail",
|
||||
checkRouter: checkTCPTLS12,
|
||||
// The connection is handled by the HTTPS router,
|
||||
// which has the correct TLS config,
|
||||
// but the HTTP server is detecting a malformed request which ends with a timeout.
|
||||
expectedError: "i/o timeout",
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.0 request should fail",
|
||||
checkRouter: checkHTTPSTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.2 request should be handled by HTTPS service",
|
||||
checkRouter: checkHTTPSTLS12,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// We show that a not CatchAll HTTPS router takes priority over a CatchAll TCP-TLS router.
|
||||
desc: "TCP TLS CatchAll router && HTTPS router",
|
||||
routers: []applyRouter{routerTCPCatchAll, routerHTTPS},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP TLS 1.0 connection should fail",
|
||||
checkRouter: checkTCPTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS 1.2 connection should fail",
|
||||
checkRouter: checkTCPTLS12,
|
||||
// The connection is handled by the HTTPS router,
|
||||
// which has the correct TLS config,
|
||||
// but the HTTP server is detecting a malformed request which ends with a timeout.
|
||||
expectedError: "i/o timeout",
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.0 request should fail",
|
||||
checkRouter: checkHTTPSTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.2 request should be handled by HTTPS service",
|
||||
checkRouter: checkHTTPSTLS12,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// We show that TCP-TLS router (not CatchAll) takes priority over non-Host rule HTTPS router (CatchAll).
|
||||
desc: "TCP TLS router && HTTPS Path prefix router",
|
||||
routers: []applyRouter{routerTCPTLS, routerHTTPSPathPrefix},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP TLS 1.0 connection should fail",
|
||||
checkRouter: checkTCPTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS 1.2 connection should be handled by TCP service",
|
||||
checkRouter: checkTCPTLS12,
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.0 request should fail",
|
||||
checkRouter: checkHTTPSTLS10,
|
||||
expectedError: "malformed HTTP response",
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.2 should fail",
|
||||
checkRouter: checkHTTPSTLS12,
|
||||
expectedError: "malformed HTTP response",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS router && TCP TLS CatchAll router",
|
||||
routers: []applyRouter{routerTCPTLS, routerTCPTLSCatchAll},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP TLS 1.0 connection should fail",
|
||||
checkRouter: checkTCPTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS 1.2 connection should be handled by TCP service",
|
||||
checkRouter: checkTCPTLS12,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "All routers, all checks",
|
||||
routers: []applyRouter{routerTCPCatchAll, routerHTTP, routerHTTPS, routerTCPTLS, routerTCPTLSCatchAll},
|
||||
checks: []checkCase{
|
||||
{
|
||||
desc: "TCP client sending first bytes should be handled by TCP service",
|
||||
checkRouter: checkTCPClientFirst,
|
||||
},
|
||||
{
|
||||
desc: "TCP server sending first bytes should timeout on clientHello",
|
||||
checkRouter: checkTCPServerFirst,
|
||||
expectedError: "i/o timeout",
|
||||
},
|
||||
{
|
||||
desc: "HTTP request should fail, because handled by TCP service",
|
||||
checkRouter: checkHTTP,
|
||||
expectedError: "malformed HTTP response",
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.0 request should fail",
|
||||
checkRouter: checkHTTPSTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "HTTPS TLS 1.2 request should be handled by HTTPS service",
|
||||
checkRouter: checkHTTPSTLS12,
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS 1.0 connection should fail",
|
||||
checkRouter: checkTCPTLS10,
|
||||
expectedError: "wrong TLS version",
|
||||
},
|
||||
{
|
||||
desc: "TCP TLS 1.2 connection should fail",
|
||||
checkRouter: checkTCPTLS12,
|
||||
// The connection is handled by the HTTPS router,
|
||||
// witch have the correct TLS config,
|
||||
// but the HTTP server is detecting a malformed request which ends with a timeout.
|
||||
expectedError: "i/o timeout",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dynConf := &runtime.Configuration{
|
||||
Routers: map[string]*runtime.RouterInfo{},
|
||||
TCPRouters: map[string]*runtime.TCPRouterInfo{},
|
||||
}
|
||||
|
||||
for _, router := range test.routers {
|
||||
router(dynConf)
|
||||
}
|
||||
|
||||
router, err := manager.buildEntryPointHandler(context.Background(), dynConf.TCPRouters, dynConf.Routers, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
epListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
// serverHTTP handler returns only the "HTTP" value as body for further checks.
|
||||
serverHTTP := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err = fmt.Fprint(w, "HTTP")
|
||||
require.NoError(t, err)
|
||||
}),
|
||||
}
|
||||
|
||||
stoppedHTTP := make(chan struct{})
|
||||
forwarder := newHTTPForwarder(epListener)
|
||||
go func() {
|
||||
defer close(stoppedHTTP)
|
||||
_ = serverHTTP.Serve(forwarder)
|
||||
}()
|
||||
|
||||
router.SetHTTPForwarder(forwarder)
|
||||
|
||||
// serverHTTPS handler returns only the "HTTPS" value as body for further checks.
|
||||
serverHTTPS := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err = fmt.Fprint(w, "HTTPS")
|
||||
require.NoError(t, err)
|
||||
}),
|
||||
}
|
||||
|
||||
stoppedHTTPS := make(chan struct{})
|
||||
httpsForwarder := newHTTPForwarder(epListener)
|
||||
go func() {
|
||||
defer close(stoppedHTTPS)
|
||||
_ = serverHTTPS.Serve(httpsForwarder)
|
||||
}()
|
||||
|
||||
router.SetHTTPSForwarder(httpsForwarder)
|
||||
|
||||
stoppedTCP := make(chan struct{})
|
||||
go func() {
|
||||
defer close(stoppedTCP)
|
||||
for {
|
||||
conn, err := epListener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tcpConn, ok := conn.(*net.TCPConn)
|
||||
if !ok {
|
||||
t.Error("not a write closer")
|
||||
}
|
||||
|
||||
router.ServeTCP(tcpConn)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, check := range test.checks {
|
||||
timeout := 2 * time.Second
|
||||
if check.timeout > 0 {
|
||||
timeout = check.timeout
|
||||
}
|
||||
|
||||
err := check.checkRouter(epListener.Addr().String(), timeout)
|
||||
|
||||
if check.expectedError != "" {
|
||||
require.NotNil(t, err, check.desc)
|
||||
assert.Contains(t, err.Error(), check.expectedError, check.desc)
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Nil(t, err, check.desc)
|
||||
}
|
||||
|
||||
epListener.Close()
|
||||
|
||||
<-stoppedTCP
|
||||
|
||||
serverHTTP.Close()
|
||||
serverHTTPS.Close()
|
||||
|
||||
<-stoppedHTTP
|
||||
<-stoppedHTTPS
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// routerTCPCatchAll configures a TCP CatchAll No TLS - HostSNI(`*`) router.
|
||||
func routerTCPCatchAll(conf *runtime.Configuration) {
|
||||
conf.TCPRouters["tcp-catchall"] = &runtime.TCPRouterInfo{
|
||||
TCPRouter: &dynamic.TCPRouter{
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "tcp",
|
||||
Rule: "HostSNI(`*`)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// routerHTTP configures an HTTP - Host(`foo.bar`) router.
|
||||
func routerHTTP(conf *runtime.Configuration) {
|
||||
conf.Routers["http"] = &runtime.RouterInfo{
|
||||
Router: &dynamic.Router{
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "http",
|
||||
Rule: "Host(`foo.bar`)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// routerTCPTLSCatchAll a TCP TLS CatchAll - HostSNI(`*`) router with TLS 1.0 config.
|
||||
func routerTCPTLSCatchAll(conf *runtime.Configuration) {
|
||||
conf.TCPRouters["tcp-tls-catchall"] = &runtime.TCPRouterInfo{
|
||||
TCPRouter: &dynamic.TCPRouter{
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "tcp",
|
||||
Rule: "HostSNI(`*`)",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{
|
||||
Options: "tls10",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// routerTCPTLS configures a TCP TLS - HostSNI(`foo.bar`) router with TLS 1.2 config.
|
||||
func routerTCPTLS(conf *runtime.Configuration) {
|
||||
conf.TCPRouters["tcp-tls"] = &runtime.TCPRouterInfo{
|
||||
TCPRouter: &dynamic.TCPRouter{
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "tcp",
|
||||
Rule: "HostSNI(`foo.bar`)",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{
|
||||
Options: "tls12",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// routerHTTPSPathPrefix configures an HTTPS - PathPrefix(`/`) router with TLS 1.0 config.
|
||||
func routerHTTPSPathPrefix(conf *runtime.Configuration) {
|
||||
conf.Routers["https"] = &runtime.RouterInfo{
|
||||
Router: &dynamic.Router{
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "http",
|
||||
Rule: "PathPrefix(`/`)",
|
||||
TLS: &dynamic.RouterTLSConfig{
|
||||
Options: "tls10",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// routerHTTPS configures an HTTPS - Host(`foo.bar`) router with TLS 1.2 config.
|
||||
func routerHTTPS(conf *runtime.Configuration) {
|
||||
conf.Routers["https-custom-tls"] = &runtime.RouterInfo{
|
||||
Router: &dynamic.Router{
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "http",
|
||||
Rule: "Host(`foo.bar`)",
|
||||
TLS: &dynamic.RouterTLSConfig{
|
||||
Options: "tls12",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// checkTCPClientFirst simulates a TCP client sending first bytes first.
|
||||
// It returns an error if it doesn't receive the expected response.
|
||||
func checkTCPClientFirst(addr string, timeout time.Duration) (err error) {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
closeErr := conn.Close()
|
||||
if closeErr != nil && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Fprint(conn, "HELLO")
|
||||
|
||||
err = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(buf.String(), "TCP-CLIENT-FIRST") {
|
||||
return fmt.Errorf("unexpected response: %s", buf.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkTCPServerFirst simulates a TCP client waiting for the server first bytes.
|
||||
// It returns an error if it doesn't receive the expected response.
|
||||
func checkTCPServerFirst(addr string, timeout time.Duration) (err error) {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
closeErr := conn.Close()
|
||||
if closeErr != nil && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
err = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(buf.String(), "TCP-SERVER-FIRST") {
|
||||
return fmt.Errorf("unexpected response: %s", buf.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkHTTP simulates an HTTP client.
|
||||
// It returns an error if it doesn't receive the expected response.
|
||||
func checkHTTP(addr string, timeout time.Duration) error {
|
||||
httpClient := &http.Client{Timeout: timeout}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://"+addr, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Host", "foo.bar")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(string(body), "HTTP") {
|
||||
return fmt.Errorf("unexpected response: %s", string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkTCPTLS simulates a TCP client connection.
|
||||
// It returns an error if it doesn't receive the expected response.
|
||||
func checkTCPTLS(addr string, timeout time.Duration, tlsVersion uint16) (err error) {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "foo.bar",
|
||||
MinVersion: tls.VersionTLS10,
|
||||
MaxVersion: tls.VersionTLS12,
|
||||
}
|
||||
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
closeErr := conn.Close()
|
||||
if closeErr != nil && err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
if conn.ConnectionState().Version != tlsVersion {
|
||||
return fmt.Errorf("wrong TLS version. wanted %X, got %X", tlsVersion, conn.ConnectionState().Version)
|
||||
}
|
||||
|
||||
fmt.Fprint(conn, "HELLO")
|
||||
|
||||
err = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(buf.String(), "TCP-CLIENT-FIRST") {
|
||||
return fmt.Errorf("unexpected response: %s", buf.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkTCPTLS10 simulates a TCP client connection with TLS 1.0.
|
||||
// It returns an error if it doesn't receive the expected response.
|
||||
func checkTCPTLS10(addr string, timeout time.Duration) error {
|
||||
return checkTCPTLS(addr, timeout, tls.VersionTLS10)
|
||||
}
|
||||
|
||||
// checkTCPTLS12 simulates a TCP client connection with TLS 1.2.
|
||||
// It returns an error if it doesn't receive the expected response.
|
||||
func checkTCPTLS12(addr string, timeout time.Duration) error {
|
||||
return checkTCPTLS(addr, timeout, tls.VersionTLS12)
|
||||
}
|
||||
|
||||
// checkHTTPS makes an HTTPS request and checks the given TLS.
|
||||
// It returns an error if it doesn't receive the expected response.
|
||||
func checkHTTPS(addr string, timeout time.Duration, tlsVersion uint16) error {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://"+addr, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Host", "foo.bar")
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "foo.bar",
|
||||
MinVersion: tls.VersionTLS10,
|
||||
MaxVersion: tls.VersionTLS12,
|
||||
},
|
||||
},
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.TLS.Version != tlsVersion {
|
||||
return fmt.Errorf("wrong TLS version. wanted %X, got %X", tlsVersion, resp.TLS.Version)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(string(body), "HTTPS") {
|
||||
return fmt.Errorf("unexpected response: %s", string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkHTTPSTLS10 makes an HTTP request with TLS version 1.0.
|
||||
// It returns an error if it doesn't receive the expected response.
|
||||
func checkHTTPSTLS10(addr string, timeout time.Duration) error {
|
||||
return checkHTTPS(addr, timeout, tls.VersionTLS10)
|
||||
}
|
||||
|
||||
// checkHTTPSTLS12 makes an HTTP request with TLS version 1.2.
|
||||
// It returns an error if it doesn't receive the expected response.
|
||||
func checkHTTPSTLS12(addr string, timeout time.Duration) error {
|
||||
return checkHTTPS(addr, timeout, tls.VersionTLS12)
|
||||
}
|
|
@ -14,33 +14,31 @@ import (
|
|||
// Proxy forwards a TCP request to a TCP service.
|
||||
type Proxy struct {
|
||||
address string
|
||||
target *net.TCPAddr
|
||||
tcpAddr *net.TCPAddr
|
||||
terminationDelay time.Duration
|
||||
proxyProtocol *dynamic.ProxyProtocol
|
||||
refreshTarget bool
|
||||
}
|
||||
|
||||
// NewProxy creates a new Proxy.
|
||||
func NewProxy(address string, terminationDelay time.Duration, proxyProtocol *dynamic.ProxyProtocol) (*Proxy, error) {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if proxyProtocol != nil && (proxyProtocol.Version < 1 || proxyProtocol.Version > 2) {
|
||||
return nil, fmt.Errorf("unknown proxyProtocol version: %d", proxyProtocol.Version)
|
||||
}
|
||||
|
||||
// enable the refresh of the target only if the address in not an IP
|
||||
refreshTarget := false
|
||||
if host, _, err := net.SplitHostPort(address); err == nil && net.ParseIP(host) == nil {
|
||||
refreshTarget = true
|
||||
// Creates the tcpAddr only for IP based addresses,
|
||||
// because there is no need to resolve the name on every new connection,
|
||||
// and building it should happen once.
|
||||
var tcpAddr *net.TCPAddr
|
||||
if host, _, err := net.SplitHostPort(address); err == nil && net.ParseIP(host) != nil {
|
||||
tcpAddr, err = net.ResolveTCPAddr("tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &Proxy{
|
||||
address: address,
|
||||
target: tcpAddr,
|
||||
refreshTarget: refreshTarget,
|
||||
tcpAddr: tcpAddr,
|
||||
terminationDelay: terminationDelay,
|
||||
proxyProtocol: proxyProtocol,
|
||||
}, nil
|
||||
|
@ -83,10 +81,14 @@ func (p *Proxy) ServeTCP(conn WriteCloser) {
|
|||
}
|
||||
|
||||
func (p Proxy) dialBackend() (*net.TCPConn, error) {
|
||||
if !p.refreshTarget {
|
||||
return net.DialTCP("tcp", nil, p.target)
|
||||
// Dial using directly the TCPAddr for IP based addresses.
|
||||
if p.tcpAddr != nil {
|
||||
return net.DialTCP("tcp", nil, p.tcpAddr)
|
||||
}
|
||||
|
||||
log.WithoutContext().Debugf("Dial with lookup to address %s", p.address)
|
||||
|
||||
// Dial with DNS lookup for host based addresses.
|
||||
conn, err := net.Dial("tcp", p.address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -176,16 +176,20 @@ func TestLookupAddress(t *testing.T) {
|
|||
testCases := []struct {
|
||||
desc string
|
||||
address string
|
||||
expectRefresh bool
|
||||
expectAddr assert.ComparisonAssertionFunc
|
||||
expectRefresh assert.ValueAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "IP doesn't need refresh",
|
||||
address: "8.8.4.4:53",
|
||||
desc: "IP doesn't need refresh",
|
||||
address: "8.8.4.4:53",
|
||||
expectAddr: assert.Equal,
|
||||
expectRefresh: assert.NotNil,
|
||||
},
|
||||
{
|
||||
desc: "Hostname needs refresh",
|
||||
address: "dns.google:53",
|
||||
expectRefresh: true,
|
||||
expectAddr: assert.NotEqual,
|
||||
expectRefresh: assert.Nil,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -197,16 +201,12 @@ func TestLookupAddress(t *testing.T) {
|
|||
proxy, err := NewProxy(test.address, 10*time.Millisecond, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, proxy.target)
|
||||
test.expectRefresh(t, proxy.tcpAddr)
|
||||
|
||||
conn, err := proxy.dialBackend()
|
||||
require.NoError(t, err)
|
||||
|
||||
if test.expectRefresh {
|
||||
assert.NotEqual(t, test.address, conn.RemoteAddr().String())
|
||||
} else {
|
||||
assert.Equal(t, test.address, conn.RemoteAddr().String())
|
||||
}
|
||||
test.expectAddr(t, test.address, conn.RemoteAddr().String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,6 +171,13 @@ func (m *Manager) Get(storeName, configName string) (*tls.Config, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
if store == nil {
|
||||
log.WithoutContext().Errorf("TLS: No certificate store found with this name: %q, closing connection", storeName)
|
||||
|
||||
// Same comment as above, as in the isACMETLS case.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
log.WithoutContext().Debugf("Serving default certificate for request: %q", domainToCheck)
|
||||
return store.DefaultCertificate, nil
|
||||
}
|
||||
|
|
|
@ -171,6 +171,36 @@ func TestManager_Get(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestManager_Get_GetCertificate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
expectedGetConfigErr require.ErrorAssertionFunc
|
||||
expectedCertificate assert.ValueAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "Get a default certificate from non-existing store",
|
||||
expectedGetConfigErr: require.Error,
|
||||
expectedCertificate: assert.Nil,
|
||||
},
|
||||
}
|
||||
|
||||
tlsManager := NewManager()
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config, err := tlsManager.Get("default", "foo")
|
||||
test.expectedGetConfigErr(t, err)
|
||||
|
||||
certificate, err := config.GetCertificate(&tls.ClientHelloInfo{})
|
||||
require.NoError(t, err)
|
||||
test.expectedCertificate(t, certificate)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientAuth(t *testing.T) {
|
||||
tlsConfigs := map[string]Options{
|
||||
"eca": {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue