1
0
Fork 0

Merge current v2.7 into master

This commit is contained in:
romain 2022-05-30 12:14:26 +02:00
commit 521109d3f2
60 changed files with 2136 additions and 529 deletions

View file

@ -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 {

View 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)
}

View 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
View 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
}

View file

@ -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"`

View file

@ -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

View 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())
})
}
}

View file

@ -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
}

View file

@ -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,
})

View file

@ -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

View file

@ -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,

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View 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)
}

View file

@ -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

View file

@ -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())
})
}
}

View file

@ -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
}

View file

@ -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": {