Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Rules:
- Human has to write PR description
- If you create a new filter, predicate or dataclient a PR is fine, else a human has to write an issue.
- Always run `make fmt`, `make lint` and `make shortcheck` before committing.
- If `check-plugins` fail, then run `make clean` and try again.
- Use `git commit --signoff` to comply with [DCO](https://developercertificate.org/).

## Security
Expand Down
3 changes: 3 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Config struct {
MaxTCPListenerQueue int `yaml:"max-tcp-listener-queue"`
EnableCopyStreamPoolExperimental bool `yaml:"enable-copy-stream-pool"`
IgnoreTrailingSlash bool `yaml:"ignore-trailing-slash"`
UseHostTree bool `yaml:"use-host-tree"`
Insecure bool `yaml:"insecure"`
AllowInsecureBackends bool `yaml:"allow-insecure-backends"`
ProxyPreserveHost bool `yaml:"proxy-preserve-host"`
Expand Down Expand Up @@ -453,6 +454,7 @@ func NewConfig() *Config {
flag.IntVar(&cfg.MaxTCPListenerQueue, "max-tcp-listener-queue", 0, "sets hardcoded max queue size for TCP listener, normally calculated 10x concurrency with max TODO:50k")
flag.BoolVar(&cfg.EnableCopyStreamPoolExperimental, "enable-copy-stream-pool", false, "flag to use a pooled copy stream in the proxy. This is an optimization that is experimental and this option might disappear in the future")
flag.BoolVar(&cfg.IgnoreTrailingSlash, "ignore-trailing-slash", false, "flag indicating to ignore trailing slashes in paths when routing")
flag.BoolVar(&cfg.UseHostTree, "use-host-tree", false, "enable two-level host+path routing trie for routes using HostAny predicates")
flag.BoolVar(&cfg.Insecure, "insecure", false, "flag indicating to ignore the verification of the TLS certificates of the backend services")
flag.BoolVar(&cfg.AllowInsecureBackends, "allow-insecure-backends", false, "enables the per-route proxySSLVerifyOff() filter that skips TLS certificate verification for individual backends; disabled by default")
flag.BoolVar(&cfg.ProxyPreserveHost, "proxy-preserve-host", false, "flag indicating to preserve the incoming request 'Host' header in the outgoing requests")
Expand Down Expand Up @@ -995,6 +997,7 @@ func (c *Config) ToOptions() skipper.Options {
MaxTCPListenerQueue: c.MaxTCPListenerQueue,
EnableCopyStreamPoolExperimental: c.EnableCopyStreamPoolExperimental,
IgnoreTrailingSlash: c.IgnoreTrailingSlash,
UseHostTree: c.UseHostTree,
DevMode: c.DevMode,
SupportListener: c.SupportListener,
DebugListener: c.DebugListener,
Expand Down
10 changes: 10 additions & 0 deletions hostpathmux/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Package hostpathmux implements a two-level routing trie for HTTP routing.
//
// The outer level is keyed by exact hostname strings; the inner level is a
// pathmux.Tree keyed by request path. Routes indexed under WildcardHost ("*")
// are tried as a fallback when no per-host subtree produces a match.
//
// This is used by the routing package when UseHostTree is enabled to reduce
// the candidate set for leaf evaluation in workloads dominated by host-specific
// routes using the HostAny predicate.
package hostpathmux
51 changes: 51 additions & 0 deletions hostpathmux/tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package hostpathmux

import "github.com/zalando/skipper/pathmux"

// WildcardHost is the key used for routes that have no exact-host constraint.
// Routes added under this key are tried as a fallback when no per-host subtree
// produces a match.
const WildcardHost = "*"

// Tree is a two-level routing trie. The outer level is keyed by exact hostname
// strings; the inner level is a pathmux.Tree keyed by path.
type Tree struct {
hosts map[string]*pathmux.Tree
}

// New returns an empty Tree.
func New() *Tree {
return &Tree{hosts: make(map[string]*pathmux.Tree)}
}

// Add registers value at the given host+path combination.
// host must be a literal hostname (e.g. from HostAny) or WildcardHost.
// path follows the same wildcard syntax as pathmux.Tree.Add.
func (t *Tree) Add(host, path string, value any) error {
pt, ok := t.hosts[host]
if !ok {
pt = &pathmux.Tree{}
t.hosts[host] = pt
}
return pt.Add(path, value)
}

// Lookup finds the best match for the given host and path.
// It first queries the per-host subtree for host; on failure it falls back to
// the WildcardHost subtree. The Matcher is forwarded to pathmux.Tree.LookupMatcher
// for leaf selection.
func (t *Tree) Lookup(host, path string, m pathmux.Matcher) (any, []string, any) {
if pt, ok := t.hosts[host]; ok {
lv, params, extra := pt.LookupMatcher(path, m)
if lv != nil {
return lv, params, extra
}
}
if pt, ok := t.hosts[WildcardHost]; ok {
lv, params, extra := pt.LookupMatcher(path, m)
if lv != nil {
return lv, params, extra
}
}
return nil, nil, nil
}
140 changes: 140 additions & 0 deletions hostpathmux/tree_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package hostpathmux

import (
"testing"

"github.com/zalando/skipper/pathmux"
)

type trueMatcher struct{}

func (m *trueMatcher) Match(v any) (bool, any) { return true, v }

func lookup(t *Tree, host, path string) any {
lv, _, _ := t.Lookup(host, path, &trueMatcher{})
return lv
}

func TestAddAndLookupExactHost(t *testing.T) {
tree := New()
if err := tree.Add("a.example.org", "/foo", "v1"); err != nil {
t.Fatal(err)
}

if got := lookup(tree, "a.example.org", "/foo"); got != "v1" {
t.Errorf("want v1, got %v", got)
}
if got := lookup(tree, "b.example.org", "/foo"); got != nil {
t.Errorf("want nil for unknown host, got %v", got)
}
}

func TestLookupWildcardFallback(t *testing.T) {
tree := New()
if err := tree.Add(WildcardHost, "/foo", "wildcard"); err != nil {
t.Fatal(err)
}

if got := lookup(tree, "any.host", "/foo"); got != "wildcard" {
t.Errorf("want wildcard, got %v", got)
}
}

func TestHostBeforeWildcard(t *testing.T) {
tree := New()
if err := tree.Add("a.example.org", "/foo", "specific"); err != nil {
t.Fatal(err)
}
if err := tree.Add(WildcardHost, "/foo", "wildcard"); err != nil {
t.Fatal(err)
}

if got := lookup(tree, "a.example.org", "/foo"); got != "specific" {
t.Errorf("want specific, got %v", got)
}
if got := lookup(tree, "other.host", "/foo"); got != "wildcard" {
t.Errorf("want wildcard for other host, got %v", got)
}
}

func TestLookupPathWildcards(t *testing.T) {
tree := New()
if err := tree.Add("a.example.org", "/api/:id", "api"); err != nil {
t.Fatal(err)
}

lv, params, _ := tree.Lookup("a.example.org", "/api/42", &trueMatcher{})
if lv == nil {
t.Fatal("want match, got nil")
}
if len(params) == 0 || params[0] != "42" {
t.Errorf("want params [42], got %v", params)
}
}

func TestLookupNoMatch(t *testing.T) {
tree := New()
if err := tree.Add("a.example.org", "/foo", "v1"); err != nil {
t.Fatal(err)
}

lv, params, extra := tree.Lookup("a.example.org", "/bar", &trueMatcher{})
if lv != nil || params != nil || extra != nil {
t.Errorf("want all nil for non-matching path, got %v %v %v", lv, params, extra)
}
}

func TestLookupMatcherCanReject(t *testing.T) {
tree := New()
if err := tree.Add("a.example.org", "/foo", "v1"); err != nil {
t.Fatal(err)
}

rejectMatcher := &rejectAll{}
lv, _, _ := tree.Lookup("a.example.org", "/foo", rejectMatcher)
if lv != nil {
t.Errorf("want nil when matcher rejects, got %v", lv)
}
}

type rejectAll struct{}

func (r *rejectAll) Match(v any) (bool, any) { return false, nil }

func TestMultipleHostsInTree(t *testing.T) {
tree := New()
_ = tree.Add("a.test", "/foo", "a")
_ = tree.Add("b.test", "/foo", "b")
_ = tree.Add("c.test", "/bar", "c")

if got := lookup(tree, "a.test", "/foo"); got != "a" {
t.Errorf("want a, got %v", got)
}
if got := lookup(tree, "b.test", "/foo"); got != "b" {
t.Errorf("want b, got %v", got)
}
if got := lookup(tree, "c.test", "/bar"); got != "c" {
t.Errorf("want c, got %v", got)
}
if got := lookup(tree, "a.test", "/bar"); got != nil {
t.Errorf("want nil cross-host miss, got %v", got)
}
}

func TestMatcherExtraValuePropagated(t *testing.T) {
tree := New()
_ = tree.Add("h.test", "/x", "stored")

m := &extraMatcher{extra: "bonus"}
_, _, extra := tree.Lookup("h.test", "/x", m)
if extra != "bonus" {
t.Errorf("want extra=bonus, got %v", extra)
}
}

type extraMatcher struct{ extra any }

func (m *extraMatcher) Match(v any) (bool, any) { return true, m.extra }

// ensure Tree satisfies the pathmux.Matcher-consumer pattern at compile time
var _ pathmux.Matcher = (*trueMatcher)(nil)
6 changes: 6 additions & 0 deletions predicates/host/any.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,9 @@ func (*anySpec) Create(args []any) (routing.Predicate, error) {
func (ap *AnyPredicate) Match(r *http.Request) bool {
return slices.Contains(ap.hosts, r.Host)
}

// MatchHosts returns the list of hostnames this predicate matches exactly.
// It is used by the routing package to build a host-keyed index.
func (ap *AnyPredicate) MatchHosts() []string {
return ap.hosts
}
Loading
Loading