1
0
Fork 0

Support ALPN for TCP + TLS routers

This commit is contained in:
Dmitry Sharshakov 2022-07-07 17:58:09 +03:00 committed by GitHub
parent aff334ffb4
commit 4dc379c601
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 228 additions and 34 deletions

View file

@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/traefik/traefik/v2/pkg/ip"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/rules"
@ -22,6 +23,7 @@ var tcpFuncs = map[string]func(*matchersTree, ...string) error{
"HostSNI": hostSNI,
"HostSNIRegexp": hostSNIRegexp,
"ClientIP": clientIP,
"ALPN": alpn,
}
// ParseHostSNI extracts the HostSNIs declared in a rule.
@ -54,10 +56,11 @@ func ParseHostSNI(rule string) ([]string, error) {
type ConnData struct {
serverName string
remoteIP string
alpnProtos []string
}
// NewConnData builds a connData struct from the given parameters.
func NewConnData(serverName string, conn tcp.WriteCloser) (ConnData, error) {
func NewConnData(serverName string, conn tcp.WriteCloser, alpnProtos []string) (ConnData, error) {
remoteIP, _, err := net.SplitHostPort(conn.RemoteAddr().String())
if err != nil {
return ConnData{}, fmt.Errorf("error while parsing remote address %q: %w", conn.RemoteAddr().String(), err)
@ -71,6 +74,7 @@ func NewConnData(serverName string, conn tcp.WriteCloser) (ConnData, error) {
return ConnData{
serverName: types.CanonicalDomain(serverName),
remoteIP: remoteIP,
alpnProtos: alpnProtos,
}, nil
}
@ -284,6 +288,33 @@ func clientIP(tree *matchersTree, clientIPs ...string) error {
return nil
}
// alpn checks if any of the connection ALPN protocols matches one of the matcher protocols.
func alpn(tree *matchersTree, protos ...string) error {
if len(protos) == 0 {
return errors.New("empty value for \"ALPN\" matcher is not allowed")
}
for _, proto := range protos {
if proto == tlsalpn01.ACMETLS1Protocol {
return fmt.Errorf("invalid protocol value for \"ALPN\" matcher, %q is not allowed", proto)
}
}
tree.matcher = func(meta ConnData) bool {
for _, proto := range meta.alpnProtos {
for _, filter := range protos {
if proto == filter {
return true
}
}
}
return false
}
return nil
}
var almostFQDN = regexp.MustCompile(`^[[:alnum:]\.-]+$`)
// hostSNI checks if the SNI Host of the connection match the matcher host.

View file

@ -1,10 +1,12 @@
package tcp
import (
"fmt"
"net"
"testing"
"time"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/tcp"
@ -58,6 +60,7 @@ func Test_addTCPRoute(t *testing.T) {
rule string
serverName string
remoteAddr string
protos []string
routeErr bool
matchErr bool
}{
@ -436,6 +439,66 @@ func Test_addTCPRoute(t *testing.T) {
serverName: "bar",
remoteAddr: "10.0.0.1:80",
},
{
desc: "Invalid ALPN rule matching ACME-TLS/1",
rule: fmt.Sprintf("ALPN(`%s`)", tlsalpn01.ACMETLS1Protocol),
protos: []string{"foo"},
routeErr: true,
},
{
desc: "Valid ALPN rule matching single protocol",
rule: "ALPN(`foo`)",
protos: []string{"foo"},
},
{
desc: "Valid ALPN rule matching ACME-TLS/1 protocol",
rule: "ALPN(`foo`)",
protos: []string{tlsalpn01.ACMETLS1Protocol},
matchErr: true,
},
{
desc: "Valid ALPN rule not matching single protocol",
rule: "ALPN(`foo`)",
protos: []string{"bar"},
matchErr: true,
},
{
desc: "Valid alternative case ALPN rule matching single protocol without another being supported",
rule: "ALPN(`foo`) && !alpn(`h2`)",
protos: []string{"foo", "bar"},
},
{
desc: "Valid alternative case ALPN rule not matching single protocol because of another being supported",
rule: "ALPN(`foo`) && !alpn(`h2`)",
protos: []string{"foo", "h2", "bar"},
matchErr: true,
},
{
desc: "Valid complex alternative case ALPN and HostSNI rule",
rule: "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))",
protos: []string{"foo", "bar"},
serverName: "foo",
},
{
desc: "Valid complex alternative case ALPN and HostSNI rule not matching by SNI",
rule: "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))",
protos: []string{"foo", "bar", "h2"},
serverName: "bar",
matchErr: true,
},
{
desc: "Valid complex alternative case ALPN and HostSNI rule matching by ALPN",
rule: "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))",
protos: []string{"foo", "bar"},
serverName: "bar",
},
{
desc: "Valid complex alternative case ALPN and HostSNI rule not matching by protos",
rule: "ALPN(`foo`) && (!alpn(`h2`) || hostsni(`foo`))",
protos: []string{"h2", "bar"},
serverName: "bar",
matchErr: true,
},
}
for _, test := range testCases {
@ -471,7 +534,7 @@ func Test_addTCPRoute(t *testing.T) {
remoteAddr: fakeAddr{addr: addr},
}
connData, err := NewConnData(test.serverName, conn)
connData, err := NewConnData(test.serverName, conn, test.protos)
require.NoError(t, err)
matchingHandler, _ := router.Match(connData)
@ -918,6 +981,75 @@ func Test_ClientIP(t *testing.T) {
}
}
func Test_ALPN(t *testing.T) {
testCases := []struct {
desc string
ruleALPNProtos []string
connProto string
buildErr bool
matchErr bool
}{
{
desc: "Empty",
buildErr: true,
},
{
desc: "ACME TLS proto",
ruleALPNProtos: []string{tlsalpn01.ACMETLS1Protocol},
buildErr: true,
},
{
desc: "Not matching empty proto",
ruleALPNProtos: []string{"h2"},
matchErr: true,
},
{
desc: "Not matching ALPN",
ruleALPNProtos: []string{"h2"},
connProto: "mqtt",
matchErr: true,
},
{
desc: "Matching ALPN",
ruleALPNProtos: []string{"h2"},
connProto: "h2",
},
{
desc: "Not matching multiple ALPNs",
ruleALPNProtos: []string{"h2", "mqtt"},
connProto: "h2c",
matchErr: true,
},
{
desc: "Matching multiple ALPNs",
ruleALPNProtos: []string{"h2", "h2c", "mqtt"},
connProto: "h2c",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
matchersTree := &matchersTree{}
err := alpn(matchersTree, test.ruleALPNProtos...)
if test.buildErr {
require.Error(t, err)
return
}
require.NoError(t, err)
meta := ConnData{
alpnProtos: []string{test.connProto},
}
assert.Equal(t, test.matchErr, !matchersTree.match(meta))
})
}
}
func Test_Priority(t *testing.T) {
testCases := []struct {
desc string