tcp stream (und damit: tls passthrough) ist implementiert
This commit is contained in:
1 parent
6a5cb95f92
commit
fdcd06c93f
16 files changed
+1008
-75
No files matched your search
+141
-14
@@ -13,6 +13,9 @@
|
||||
// Mahnstufe 1/2 zeigt er einen Hinweis und leitet per
|
||||
// <meta http-equiv="refresh"> zurück; ein Cookie lässt den Besucher
|
||||
// danach über einen zusätzlichen Router direkt zum Original durch.
|
||||
// - TCP (nur TLS-Passthrough, Feature tcp_stream): HostSNI mit expliziten
|
||||
// eigenen Hosts, nur Entrypoint websecure. Ohne Interceptor entfällt die
|
||||
// Route ab Paywall und bei jeder Wartung; Hinweisstufen lassen durch.
|
||||
//
|
||||
// Verstöße entfernen den betroffenen Router und werden als Violation
|
||||
// gemeldet (für das Audit-Log); der Rest des Knotens bleibt aktiv.
|
||||
@@ -56,6 +59,11 @@ const (
|
||||
// Das Präfix "sultan__" ist reserviert (Mandanten-ID "sultan" ist verboten).
|
||||
const InterceptorService = "sultan__interceptor"
|
||||
|
||||
// TCPEntryPoint ist der einzige Entrypoint für TCP-Router (TLS-Passthrough
|
||||
// per SNI auf :443). Sultan setzt ihn immer; ohne Angabe läge ein TCP-Router
|
||||
// in Traefik auf allen Entrypoints.
|
||||
const TCPEntryPoint = "websecure"
|
||||
|
||||
// Header, mit denen der Interceptor seinen Auftrag erhält. Traefik setzt sie
|
||||
// per customRequestHeaders und überschreibt damit Werte des Besuchers.
|
||||
const (
|
||||
@@ -164,6 +172,8 @@ const (
|
||||
ViolationFeature ViolationKind = "feature"
|
||||
ViolationForbiddenMiddleware ViolationKind = "forbidden_middleware"
|
||||
ViolationMiddlewareTarget ViolationKind = "middleware_target"
|
||||
ViolationTCPPassthrough ViolationKind = "tcp_passthrough"
|
||||
ViolationEntryPoint ViolationKind = "entrypoint"
|
||||
)
|
||||
|
||||
// Violation ist ein Verstoß eines Routers (Originalname).
|
||||
@@ -254,14 +264,21 @@ func (e *Engine) Apply(in Input) (Result, error) {
|
||||
at := now()
|
||||
stage := StageFor(in.Tenant, in.License, at)
|
||||
res := Result{Config: &traefik.Configuration{}, Stage: stage}
|
||||
if stage == StageBlocked || in.NodeDrained || in.Config == nil || in.Config.HTTP == nil {
|
||||
if stage == StageBlocked || in.NodeDrained || in.Config == nil || (in.Config.HTTP == nil && in.Config.TCP == nil) {
|
||||
return res, nil
|
||||
}
|
||||
src := in.Config.HTTP
|
||||
if src == nil {
|
||||
src = &traefik.HTTPConfiguration{}
|
||||
}
|
||||
tcpSrc := in.Config.TCP
|
||||
if tcpSrc == nil {
|
||||
tcpSrc = &traefik.TCPConfiguration{}
|
||||
}
|
||||
|
||||
// 1. Router einzeln prüfen (sortiert, damit Ergebnis und Routenlimit
|
||||
// deterministisch sind).
|
||||
var accepted []string
|
||||
var accepted, acceptedTCP []string
|
||||
for _, name := range sortedKeys(src.Routers) {
|
||||
blocked, observed := e.checkRouter(name, src.Routers[name], src, in.License)
|
||||
res.Violations = append(res.Violations, observed...)
|
||||
@@ -271,21 +288,44 @@ func (e *Engine) Apply(in Input) (Result, error) {
|
||||
}
|
||||
accepted = append(accepted, name)
|
||||
}
|
||||
|
||||
// 2. Routenlimit: überzählige Router (nach Namen) verwerfen.
|
||||
if !in.License.AllowsRoutes(len(accepted)) {
|
||||
for _, name := range accepted[in.License.MaxRoutes:] {
|
||||
res.Violations = append(res.Violations, Violation{Kind: ViolationRouteLimit, Router: name,
|
||||
Detail: fmt.Sprintf("max_routes = %d", in.License.MaxRoutes)})
|
||||
for _, name := range sortedKeys(tcpSrc.Routers) {
|
||||
if v := e.checkTCPRouter(name, tcpSrc.Routers[name], tcpSrc, in.License); v != nil {
|
||||
res.Violations = append(res.Violations, *v)
|
||||
continue
|
||||
}
|
||||
accepted = accepted[:in.License.MaxRoutes]
|
||||
acceptedTCP = append(acceptedTCP, name)
|
||||
}
|
||||
if len(accepted) == 0 {
|
||||
return res, nil
|
||||
|
||||
// 2. Routenlimit über HTTP- und TCP-Router: HTTP zuerst, überzählige
|
||||
// (nach Namen) verwerfen.
|
||||
if !in.License.AllowsRoutes(len(accepted) + len(acceptedTCP)) {
|
||||
limit := func(names []string, max int) []string {
|
||||
max = min(max, len(names))
|
||||
for _, name := range names[max:] {
|
||||
res.Violations = append(res.Violations, Violation{Kind: ViolationRouteLimit, Router: name,
|
||||
Detail: fmt.Sprintf("max_routes = %d", in.License.MaxRoutes)})
|
||||
}
|
||||
return names[:max]
|
||||
}
|
||||
accepted = limit(accepted, in.License.MaxRoutes)
|
||||
acceptedTCP = limit(acceptedTCP, in.License.MaxRoutes-len(accepted))
|
||||
}
|
||||
|
||||
// 3. Ausgabe mit Namensraum und ggf. Umleitung zum Interceptor.
|
||||
prefix := in.Tenant.ID + "__" + in.NodeID + "__"
|
||||
if len(accepted) > 0 {
|
||||
out, err := e.httpOutput(in, src, accepted, stage, prefix, at)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
res.Config.HTTP = out
|
||||
}
|
||||
res.Config.TCP = tcpOutput(in, tcpSrc, acceptedTCP, stage, prefix, at)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// httpOutput erzeugt die HTTP-Konfiguration der angenommenen Router.
|
||||
func (e *Engine) httpOutput(in Input, src *traefik.HTTPConfiguration, accepted []string, stage Stage, prefix string, at time.Time) (*traefik.HTTPConfiguration, error) {
|
||||
out := &traefik.HTTPConfiguration{
|
||||
Routers: map[string]*traefik.Router{},
|
||||
Services: map[string]*traefik.Service{},
|
||||
@@ -324,7 +364,7 @@ func (e *Engine) Apply(in Input) (Result, error) {
|
||||
}
|
||||
if routerStage != StageNormal {
|
||||
if e.InterceptorURL == "" {
|
||||
return Result{}, ErrNoInterceptor
|
||||
return nil, ErrNoInterceptor
|
||||
}
|
||||
if slices.Contains(ackStages, routerStage) {
|
||||
// Durchlass nach bestätigtem Hinweis: gleiche Regel + Cookie,
|
||||
@@ -369,8 +409,43 @@ func (e *Engine) Apply(in Input) (Result, error) {
|
||||
if len(out.Middlewares) == 0 {
|
||||
out.Middlewares = nil
|
||||
}
|
||||
res.Config = &traefik.Configuration{HTTP: out}
|
||||
return res, nil
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// tcpOutput erzeugt die TCP-Konfiguration der angenommenen Router. Ohne HTTP
|
||||
// gibt es keinen Interceptor: Hinweisstufen lassen durch, ab Paywall und bei
|
||||
// jeder Wartung (auch Healthcheck, getrennter Knoten) entfällt die Route.
|
||||
func tcpOutput(in Input, src *traefik.TCPConfiguration, accepted []string, stage Stage, prefix string, at time.Time) *traefik.TCPConfiguration {
|
||||
if stage == StagePaywall || stage == StageLanding {
|
||||
return nil
|
||||
}
|
||||
out := &traefik.TCPConfiguration{
|
||||
Routers: map[string]*traefik.TCPRouter{},
|
||||
Services: map[string]*traefik.TCPService{},
|
||||
}
|
||||
for _, name := range accepted {
|
||||
r := src.Routers[name]
|
||||
if _, inMaint := maintenanceFor(in, r.Service, at); inMaint {
|
||||
continue
|
||||
}
|
||||
nr := *r
|
||||
nr.EntryPoints = []string{TCPEntryPoint}
|
||||
nr.TLS = &traefik.TCPRouterTLS{Passthrough: true}
|
||||
if isExternal(r.Service) {
|
||||
nr.Service = r.Service
|
||||
} else {
|
||||
nr.Service = prefix + r.Service
|
||||
out.Services[nr.Service] = copyTCPService(src.Services[r.Service])
|
||||
}
|
||||
out.Routers[prefix+name] = &nr
|
||||
}
|
||||
if len(out.Routers) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(out.Services) == 0 {
|
||||
out.Services = nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// maintenanceFor liefert die geltende Wartung eines Services: zuerst die des
|
||||
@@ -482,6 +557,50 @@ func (e *Engine) checkRouterRules(name string, r *traefik.Router, src *traefik.H
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// checkTCPRouter prüft einen TCP-Router: nur mit Feature tcp_stream, nur
|
||||
// TLS-Passthrough auf TCPEntryPoint, nur explizite eigene Hosts per HostSNI.
|
||||
func (e *Engine) checkTCPRouter(name string, r *traefik.TCPRouter, src *traefik.TCPConfiguration, l license.License) *Violation {
|
||||
v := func(kind ViolationKind, format string, args ...any) *Violation {
|
||||
return &Violation{Kind: kind, Router: name, Detail: "TCP: " + fmt.Sprintf(format, args...)}
|
||||
}
|
||||
if !l.HasFeature(license.FeatureTCPStream) {
|
||||
return v(ViolationFeature, "TCP-Router erfordert %s", license.FeatureTCPStream)
|
||||
}
|
||||
|
||||
hosts, err := HostsFromSNIRule(r.Rule)
|
||||
switch {
|
||||
case errors.Is(err, ErrUnconstrained):
|
||||
return v(ViolationCatchAll, "Regel %q nicht auf explizite Hosts beschränkt", r.Rule)
|
||||
case errors.Is(err, ErrHostRegexp):
|
||||
return v(ViolationHostRegexp, "Regel %q", r.Rule)
|
||||
case err != nil:
|
||||
return v(ViolationRuleSyntax, "%v", err)
|
||||
}
|
||||
for _, h := range hosts {
|
||||
if !l.AllowsDomain(h) {
|
||||
return v(ViolationDomain, "Host %q nicht in allowed_domains", h)
|
||||
}
|
||||
}
|
||||
|
||||
if r.TLS == nil || !r.TLS.Passthrough {
|
||||
return v(ViolationTCPPassthrough, "nur tls.passthrough=true wird unterstützt")
|
||||
}
|
||||
for _, ep := range r.EntryPoints {
|
||||
if ep != TCPEntryPoint {
|
||||
return v(ViolationEntryPoint, "Entrypoint %q nicht erlaubt (nur %s)", ep, TCPEntryPoint)
|
||||
}
|
||||
}
|
||||
|
||||
if isExternal(r.Service) {
|
||||
if !slices.Contains(e.AllowedExternalRefs, r.Service) {
|
||||
return v(ViolationExternalRef, "Service %q", r.Service)
|
||||
}
|
||||
} else if src.Services[r.Service] == nil {
|
||||
return v(ViolationUnknownRef, "Service %q nicht auf diesem Knoten definiert", r.Service)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isExternal(ref string) bool { return strings.Contains(ref, "@") }
|
||||
|
||||
func sortedKeys[V any](m map[string]V) []string {
|
||||
@@ -507,6 +626,14 @@ func copyService(s *traefik.Service) *traefik.Service {
|
||||
return c
|
||||
}
|
||||
|
||||
func copyTCPService(s *traefik.TCPService) *traefik.TCPService {
|
||||
c := &traefik.TCPService{}
|
||||
if s.LoadBalancer != nil {
|
||||
c.LoadBalancer = &traefik.TCPServersLoadBalancer{Servers: slices.Clone(s.LoadBalancer.Servers)}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func copyMiddleware(m traefik.Middleware) traefik.Middleware {
|
||||
return traefik.Middleware(copyMap(map[string]any(m)))
|
||||
}
|
||||
|
||||
+33
-7
@@ -24,6 +24,29 @@ var (
|
||||
// Hosts schränken nicht ein (sie schließen nur aus). Positives HostRegexp
|
||||
// ist nicht erlaubt, weil sich ein Regex nicht gegen die Lizenz prüfen lässt.
|
||||
func HostsFromRule(rule string) ([]string, error) {
|
||||
return hostsFromRule(rule, "host", "hostregexp")
|
||||
}
|
||||
|
||||
// HostsFromSNIRule ist das Gegenstück für TCP-Regeln: Jeder ||-Zweig muss
|
||||
// durch HostSNI(...) beschränkt sein, HostSNIRegexp ist verboten. Erlaubt
|
||||
// sind nur explizite Hosts – HostSNI(`*`) und Wildcards ergeben
|
||||
// ErrUnconstrained.
|
||||
func HostsFromSNIRule(rule string) ([]string, error) {
|
||||
hosts, err := hostsFromRule(rule, "hostsni", "hostsniregexp")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, h := range hosts {
|
||||
if strings.Contains(h, "*") {
|
||||
return nil, fmt.Errorf("%w: %q ist kein expliziter Host", ErrUnconstrained, h)
|
||||
}
|
||||
}
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
// hostsFromRule parst rule; hostMatcher/regexpMatcher sind die Matcher-Namen
|
||||
// in Kleinschreibung.
|
||||
func hostsFromRule(rule, hostMatcher, regexpMatcher string) ([]string, error) {
|
||||
toks, err := lex(rule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -36,7 +59,8 @@ func HostsFromRule(rule string) ([]string, error) {
|
||||
if p.peek().kind != tEOF {
|
||||
return nil, p.errorf("unerwartetes %q", p.peek().text)
|
||||
}
|
||||
constrained, hosts, err := analyze(root, true)
|
||||
a := analyzer{host: hostMatcher, regexp: regexpMatcher}
|
||||
constrained, hosts, err := a.analyze(root, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -48,29 +72,31 @@ func HostsFromRule(rule string) ([]string, error) {
|
||||
|
||||
// analyze ermittelt, ob ein Ausdruck auf Hosts beschränkt ist, und sammelt
|
||||
// die Hosts aus positiven (nicht negierten) Host-Matchern.
|
||||
func analyze(n *node, positive bool) (constrained bool, hosts []string, err error) {
|
||||
type analyzer struct{ host, regexp string }
|
||||
|
||||
func (a analyzer) analyze(n *node, positive bool) (constrained bool, hosts []string, err error) {
|
||||
switch n.op {
|
||||
case opCall:
|
||||
switch strings.ToLower(n.name) {
|
||||
case "host":
|
||||
case a.host:
|
||||
if positive {
|
||||
return true, n.args, nil
|
||||
}
|
||||
case "hostregexp":
|
||||
case a.regexp:
|
||||
if positive {
|
||||
return false, nil, ErrHostRegexp
|
||||
}
|
||||
}
|
||||
return false, nil, nil
|
||||
case opNot:
|
||||
_, h, err := analyze(n.kids[0], !positive)
|
||||
_, h, err := a.analyze(n.kids[0], !positive)
|
||||
return false, h, err
|
||||
default: // opAnd, opOr
|
||||
c1, h1, err := analyze(n.kids[0], positive)
|
||||
c1, h1, err := a.analyze(n.kids[0], positive)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
c2, h2, err := analyze(n.kids[1], positive)
|
||||
c2, h2, err := a.analyze(n.kids[1], positive)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sultan/pkg/license"
|
||||
"sultan/pkg/store"
|
||||
"sultan/pkg/traefik"
|
||||
)
|
||||
|
||||
func TestHostsFromSNIRule(t *testing.T) {
|
||||
tests := map[string][]string{
|
||||
"HostSNI(`a.de`)": {"a.de"},
|
||||
"HostSNI(`a.de`) || HostSNI(`b.de`)": {"a.de", "b.de"},
|
||||
"HostSNI(`a.de`, `b.de`)": {"a.de", "b.de"},
|
||||
"hostsni(`a.de`) && ClientIP(`10.0.0.0/8`)": {"a.de"},
|
||||
"HostSNI(`a.de`) && (ALPN(`h2`) || ALPN(`acme`))": {"a.de"},
|
||||
"(HostSNI(`a.de`) || HostSNI(`b.de`)) && !ClientIP(`1.2.3.4`)": {"a.de", "b.de"},
|
||||
}
|
||||
for rule, want := range tests {
|
||||
got, err := HostsFromSNIRule(rule)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unerwarteter Fehler %v", rule, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s: Hosts = %v, erwartet %v", rule, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nur explizite Hosts: HostSNI(`*`) wäre ein Catch-all für den ganzen
|
||||
// Entrypoint, Wildcards wie *.x.de versteht Traefik in HostSNI ohnehin nicht.
|
||||
func TestHostsFromSNIRule_Rejected(t *testing.T) {
|
||||
tests := map[string]error{
|
||||
"HostSNI(`*`)": ErrUnconstrained,
|
||||
"HostSNI(`*.lab.kunde.de`)": ErrUnconstrained,
|
||||
"HostSNI(`a.de`) || HostSNI(`*`)": ErrUnconstrained,
|
||||
"ClientIP(`10.0.0.0/8`)": ErrUnconstrained,
|
||||
"HostSNI(`a.de`) || ALPN(`h2`)": ErrUnconstrained,
|
||||
"Host(`a.de`)": ErrUnconstrained, // HTTP-Matcher zählt hier nicht
|
||||
"HostSNIRegexp(`^.+\\.kunde\\.de$`)": ErrHostRegexp,
|
||||
"HostSNI(`a.de`) && HostSNIRegexp(`x`)": ErrHostRegexp,
|
||||
"HostSNI(`a.de`": ErrSyntax,
|
||||
}
|
||||
for rule, want := range tests {
|
||||
if _, err := HostsFromSNIRule(rule); !errors.Is(err, want) {
|
||||
t.Errorf("%s: erwartet %v, bekommen %v", rule, want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Umgekehrt schränkt HostSNI eine HTTP-Regel nicht ein.
|
||||
func TestHostsFromRule_IgnoresHostSNI(t *testing.T) {
|
||||
if _, err := HostsFromRule("HostSNI(`a.de`)"); !errors.Is(err, ErrUnconstrained) {
|
||||
t.Errorf("erwartet ErrUnconstrained, bekommen %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func caLabels() map[string]string {
|
||||
return map[string]string{
|
||||
"traefik.tcp.routers.ca.rule": "HostSNI(`ca.lab.kunde.de`)",
|
||||
"traefik.tcp.routers.ca.entrypoints": "websecure",
|
||||
"traefik.tcp.routers.ca.tls.passthrough": "true",
|
||||
"traefik.tcp.services.ca.loadbalancer.server.port": "9000",
|
||||
}
|
||||
}
|
||||
|
||||
func tcpInput(t *testing.T, labels map[string]string) Input {
|
||||
in := input(t, labels)
|
||||
in.License = lic(license.TierPro, 10, license.FeatureTCPStream)
|
||||
return in
|
||||
}
|
||||
|
||||
func TestApply_TCPPassthrough(t *testing.T) {
|
||||
res, err := engine().Apply(tcpInput(t, caLabels()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res.Violations) != 0 {
|
||||
t.Errorf("Violations = %+v", res.Violations)
|
||||
}
|
||||
want := &traefik.Configuration{TCP: &traefik.TCPConfiguration{
|
||||
Routers: map[string]*traefik.TCPRouter{
|
||||
"kunde__node-1__ca": {
|
||||
EntryPoints: []string{TCPEntryPoint},
|
||||
Service: "kunde__node-1__ca",
|
||||
Rule: "HostSNI(`ca.lab.kunde.de`)",
|
||||
TLS: &traefik.TCPRouterTLS{Passthrough: true},
|
||||
},
|
||||
},
|
||||
Services: map[string]*traefik.TCPService{
|
||||
"kunde__node-1__ca": {LoadBalancer: &traefik.TCPServersLoadBalancer{Servers: []traefik.TCPServer{{Address: "100.64.0.42:9000"}}}},
|
||||
},
|
||||
}}
|
||||
if !reflect.DeepEqual(res.Config, want) {
|
||||
t.Errorf("Konfiguration weicht ab\n got: %+v\nwant: %+v", res.Config.TCP, want.TCP)
|
||||
}
|
||||
}
|
||||
|
||||
// Ohne entrypoints-Label würde Traefik den Router auf alle Entrypoints legen
|
||||
// (auch :80) – Sultan setzt immer websecure.
|
||||
func TestApply_TCPEntryPointDefault(t *testing.T) {
|
||||
labels := caLabels()
|
||||
delete(labels, "traefik.tcp.routers.ca.entrypoints")
|
||||
res, err := engine().Apply(tcpInput(t, labels))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := res.Config.TCP.Routers["kunde__node-1__ca"]
|
||||
if r == nil || !reflect.DeepEqual(r.EntryPoints, []string{TCPEntryPoint}) {
|
||||
t.Errorf("Router = %+v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_TCPViolations(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
mutate func(map[string]string, *Input)
|
||||
want ViolationKind
|
||||
}{
|
||||
"ohne Feature": {func(_ map[string]string, in *Input) {
|
||||
in.License = lic(license.TierPro, 10)
|
||||
}, ViolationFeature},
|
||||
"ohne Passthrough": {func(l map[string]string, _ *Input) {
|
||||
delete(l, "traefik.tcp.routers.ca.tls.passthrough")
|
||||
l["traefik.tcp.routers.ca.tls"] = "true"
|
||||
}, ViolationTCPPassthrough},
|
||||
"Passthrough aus": {func(l map[string]string, _ *Input) {
|
||||
l["traefik.tcp.routers.ca.tls.passthrough"] = "false"
|
||||
}, ViolationTCPPassthrough},
|
||||
"anderer Entrypoint": {func(l map[string]string, _ *Input) {
|
||||
l["traefik.tcp.routers.ca.entrypoints"] = "websecure,postgres"
|
||||
}, ViolationEntryPoint},
|
||||
"Catch-all": {func(l map[string]string, _ *Input) {
|
||||
l["traefik.tcp.routers.ca.rule"] = "HostSNI(`*`)"
|
||||
}, ViolationCatchAll},
|
||||
"Wildcard": {func(l map[string]string, _ *Input) {
|
||||
l["traefik.tcp.routers.ca.rule"] = "HostSNI(`*.lab.kunde.de`)"
|
||||
}, ViolationCatchAll},
|
||||
"Regexp": {func(l map[string]string, _ *Input) {
|
||||
l["traefik.tcp.routers.ca.rule"] = "HostSNIRegexp(`.+`)"
|
||||
}, ViolationHostRegexp},
|
||||
"Syntax": {func(l map[string]string, _ *Input) {
|
||||
l["traefik.tcp.routers.ca.rule"] = "HostSNI(`ca.lab.kunde.de`"
|
||||
}, ViolationRuleSyntax},
|
||||
"fremde Domain": {func(l map[string]string, _ *Input) {
|
||||
l["traefik.tcp.routers.ca.rule"] = "HostSNI(`ca.lab.kunde.de`) || HostSNI(`ca.fremd.de`)"
|
||||
}, ViolationDomain},
|
||||
"externer Service": {func(l map[string]string, _ *Input) {
|
||||
l["traefik.tcp.routers.ca.service"] = "ca@file"
|
||||
}, ViolationExternalRef},
|
||||
"unbekannter Service": {func(l map[string]string, _ *Input) {
|
||||
l["traefik.tcp.routers.ca.service"] = "andere"
|
||||
}, ViolationUnknownRef},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
labels := caLabels()
|
||||
in := tcpInput(t, labels)
|
||||
tt.mutate(labels, &in)
|
||||
in.Config = cfg(t, labels)
|
||||
res, err := engine().Apply(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(kinds(res.Violations), []ViolationKind{tt.want}) || res.Violations[0].Router != "ca" {
|
||||
t.Errorf("Violations = %+v, erwartet %s", res.Violations, tt.want)
|
||||
}
|
||||
if res.Config.TCP != nil {
|
||||
t.Errorf("Router trotz Verstoß übernommen: %+v", res.Config.TCP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Die Domainprüfung erlaubt einzelne Hosts unter einer Wildcard aus
|
||||
// allowed_domains (*.lab.kunde.de).
|
||||
func TestApply_TCPHostUnderLicenseWildcard(t *testing.T) {
|
||||
in := tcpInput(t, caLabels())
|
||||
in.License.AllowedDomains = []string{"*.lab.kunde.de"}
|
||||
res, err := engine().Apply(in)
|
||||
if err != nil || len(res.Violations) != 0 || res.Config.TCP == nil {
|
||||
t.Errorf("res=%+v violations=%+v err=%v", res.Config, res.Violations, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TCP-Router zählen gegen max_routes; HTTP-Router werden zuerst vergeben.
|
||||
func TestApply_TCPRouteLimit(t *testing.T) {
|
||||
labels := caLabels()
|
||||
for k, v := range wpLabels() {
|
||||
labels[k] = v
|
||||
}
|
||||
in := tcpInput(t, labels)
|
||||
in.License.MaxRoutes = 1
|
||||
res, err := engine().Apply(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Config.HTTP == nil || len(res.Config.HTTP.Routers) != 1 || res.Config.TCP != nil {
|
||||
t.Errorf("erwartet nur den HTTP-Router: %+v / %+v", res.Config.HTTP, res.Config.TCP)
|
||||
}
|
||||
if !reflect.DeepEqual(kinds(res.Violations), []ViolationKind{ViolationRouteLimit}) || res.Violations[0].Router != "ca" {
|
||||
t.Errorf("Violations = %+v", res.Violations)
|
||||
}
|
||||
|
||||
in.License.MaxRoutes = 2
|
||||
res, _ = engine().Apply(in)
|
||||
if res.Config.TCP == nil || len(res.Violations) != 0 {
|
||||
t.Errorf("max_routes=2: %+v %+v", res.Config.TCP, res.Violations)
|
||||
}
|
||||
}
|
||||
|
||||
// Ohne Interceptor gilt: Hinweisstufen lassen durch, ab Paywall und bei jeder
|
||||
// Wartung ist die Route weg.
|
||||
func TestApply_TCPStages(t *testing.T) {
|
||||
future := now.Add(time.Hour)
|
||||
tests := map[string]struct {
|
||||
mutate func(*Input)
|
||||
keep bool
|
||||
}{
|
||||
"normal": {func(*Input) {}, true},
|
||||
"Mahnstufe 1": {func(in *Input) { in.Tenant.DunningLevel = 1 }, true},
|
||||
"Mahnstufe 2": {func(in *Input) { in.Tenant.DunningLevel = 2 }, true},
|
||||
"Paywall": {func(in *Input) { in.Tenant.DunningLevel = 3 }, false},
|
||||
"ausgesetzt": {func(in *Input) { in.Tenant.Status = store.TenantSuspendedPayment }, false},
|
||||
"abgelaufen": {func(in *Input) { in.Tenant.Status = store.TenantExpired }, false},
|
||||
"Quarantäne": {func(in *Input) { in.Tenant.Status = store.TenantQuarantined }, false},
|
||||
"gedraint": {func(in *Input) { in.NodeDrained = true }, false},
|
||||
"Knotenwartung": {func(in *Input) { in.Maintenance = &Maintenance{Until: future} }, false},
|
||||
"Knotenwartung vorbei": {func(in *Input) {
|
||||
in.Maintenance = &Maintenance{Until: now.Add(-time.Minute)}
|
||||
}, true},
|
||||
"Dienstwartung": {func(in *Input) { in.ServiceMaintenance = map[string]Maintenance{"ca": {}} }, false},
|
||||
"Healthcheck": {func(in *Input) { in.DownServices = map[string]bool{"ca": true} }, false},
|
||||
"getrennt": {func(in *Input) { in.Disconnected = true }, false},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
in := tcpInput(t, caLabels())
|
||||
tt.mutate(&in)
|
||||
res, err := (&Engine{now: engine().now}).Apply(in) // ohne Interceptor: TCP braucht keinen
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := res.Config.TCP != nil; got != tt.keep {
|
||||
t.Errorf("Route vorhanden = %v, erwartet %v", got, tt.keep)
|
||||
}
|
||||
if len(res.Violations) != 0 {
|
||||
t.Errorf("Violations = %+v", res.Violations)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply_TCPDoesNotMutateInput(t *testing.T) {
|
||||
labels := caLabels()
|
||||
delete(labels, "traefik.tcp.routers.ca.entrypoints")
|
||||
in := tcpInput(t, labels)
|
||||
before := cfg(t, labels)
|
||||
if _, err := engine().Apply(in); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(in.Config, before) {
|
||||
t.Error("Eingabe-Konfiguration wurde verändert")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user