TUN-6801: Add punycode alternatives for ingress rules

This commit is contained in:
Devin Carr 2022-09-22 16:19:06 -07:00
parent be0305ec58
commit b3e26420c0
4 changed files with 98 additions and 32 deletions

View File

@ -11,6 +11,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"golang.org/x/net/idna"
"github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/ingress/middleware" "github.com/cloudflare/cloudflared/ingress/middleware"
@ -275,6 +276,16 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
return Ingress{}, err return Ingress{}, err
} }
isCatchAllRule := (r.Hostname == "" || r.Hostname == "*") && r.Path == ""
punycodeHostname := ""
if !isCatchAllRule {
punycode, err := idna.Lookup.ToASCII(r.Hostname)
// Don't provide the punycode hostname if it is the same as the original hostname
if err == nil && punycode != r.Hostname {
punycodeHostname = punycode
}
}
var pathRegexp *Regexp var pathRegexp *Regexp
if r.Path != "" { if r.Path != "" {
var err error var err error
@ -286,11 +297,12 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
} }
rules[i] = Rule{ rules[i] = Rule{
Hostname: r.Hostname, Hostname: r.Hostname,
Service: service, punycodeHostname: punycodeHostname,
Path: pathRegexp, Service: service,
Handlers: handlers, Path: pathRegexp,
Config: cfg, Handlers: handlers,
Config: cfg,
} }
} }
return Ingress{Rules: rules, Defaults: defaults}, nil return Ingress{Rules: rules, Defaults: defaults}, nil

View File

@ -130,6 +130,36 @@ ingress:
}, },
}, },
}, },
{
name: "Unicode domain",
args: args{rawYAML: `
ingress:
- hostname: môô.cloudflare.com
service: https://localhost:8000
- service: https://localhost:8001
`},
want: []Rule{
{
Hostname: "môô.cloudflare.com",
punycodeHostname: "xn--m-xgaa.cloudflare.com",
Service: &httpService{url: localhost8000},
Config: defaultConfig,
},
{
Service: &httpService{url: localhost8001},
Config: defaultConfig,
},
},
},
{
name: "Invalid unicode domain",
args: args{rawYAML: fmt.Sprintf(`
ingress:
- hostname: %s
service: https://localhost:8000
`, string(rune(0xd8f3))+".cloudflare.com")},
wantErr: true,
},
{ {
name: "Invalid service", name: "Invalid service",
args: args{rawYAML: ` args: args{rawYAML: `

View File

@ -14,6 +14,9 @@ type Rule struct {
// Requests for this hostname will be proxied to this rule's service. // Requests for this hostname will be proxied to this rule's service.
Hostname string `json:"hostname"` Hostname string `json:"hostname"`
// punycodeHostname is an additional optional hostname converted to punycode.
punycodeHostname string
// Path is an optional regex that can specify path-driven ingress rules. // Path is an optional regex that can specify path-driven ingress rules.
Path *Regexp `json:"path"` Path *Regexp `json:"path"`
@ -50,9 +53,18 @@ func (r Rule) MultiLineString() string {
// Matches checks if the rule matches a given hostname/path combination. // Matches checks if the rule matches a given hostname/path combination.
func (r *Rule) Matches(hostname, path string) bool { func (r *Rule) Matches(hostname, path string) bool {
hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, hostname) hostMatch := false
if r.Hostname == "" || r.Hostname == "*" {
hostMatch = true
} else {
hostMatch = matchHost(r.Hostname, hostname)
}
punycodeHostMatch := false
if r.punycodeHostname != "" {
punycodeHostMatch = matchHost(r.punycodeHostname, hostname)
}
pathMatch := r.Path == nil || r.Path.Regexp == nil || r.Path.Regexp.MatchString(path) pathMatch := r.Path == nil || r.Path.Regexp == nil || r.Path.Regexp.MatchString(path)
return hostMatch && pathMatch return (hostMatch || punycodeHostMatch) && pathMatch
} }
// Regexp adds unmarshalling from json for regexp.Regexp // Regexp adds unmarshalling from json for regexp.Regexp

View File

@ -14,23 +14,18 @@ import (
) )
func Test_rule_matches(t *testing.T) { func Test_rule_matches(t *testing.T) {
type fields struct {
Hostname string
Path *Regexp
Service OriginService
}
type args struct { type args struct {
requestURL *url.URL requestURL *url.URL
} }
tests := []struct { tests := []struct {
name string name string
fields fields rule Rule
args args args args
want bool want bool
}{ }{
{ {
name: "Just hostname, pass", name: "Just hostname, pass",
fields: fields{ rule: Rule{
Hostname: "example.com", Hostname: "example.com",
}, },
args: args{ args: args{
@ -38,9 +33,31 @@ func Test_rule_matches(t *testing.T) {
}, },
want: true, want: true,
}, },
{
name: "Unicode hostname with unicode request, pass",
rule: Rule{
Hostname: "môô.cloudflare.com",
punycodeHostname: "xn--m-xgaa.cloudflare.com",
},
args: args{
requestURL: MustParseURL(t, "https://môô.cloudflare.com"),
},
want: true,
},
{
name: "Unicode hostname with punycode request, pass",
rule: Rule{
Hostname: "môô.cloudflare.com",
punycodeHostname: "xn--m-xgaa.cloudflare.com",
},
args: args{
requestURL: MustParseURL(t, "https://xn--m-xgaa.cloudflare.com"),
},
want: true,
},
{ {
name: "Entire hostname is wildcard, should match everything", name: "Entire hostname is wildcard, should match everything",
fields: fields{ rule: Rule{
Hostname: "*", Hostname: "*",
}, },
args: args{ args: args{
@ -50,7 +67,7 @@ func Test_rule_matches(t *testing.T) {
}, },
{ {
name: "Just hostname, fail", name: "Just hostname, fail",
fields: fields{ rule: Rule{
Hostname: "example.com", Hostname: "example.com",
}, },
args: args{ args: args{
@ -60,7 +77,7 @@ func Test_rule_matches(t *testing.T) {
}, },
{ {
name: "Just wildcard hostname, pass", name: "Just wildcard hostname, pass",
fields: fields{ rule: Rule{
Hostname: "*.example.com", Hostname: "*.example.com",
}, },
args: args{ args: args{
@ -70,7 +87,7 @@ func Test_rule_matches(t *testing.T) {
}, },
{ {
name: "Just wildcard hostname, fail", name: "Just wildcard hostname, fail",
fields: fields{ rule: Rule{
Hostname: "*.example.com", Hostname: "*.example.com",
}, },
args: args{ args: args{
@ -80,7 +97,7 @@ func Test_rule_matches(t *testing.T) {
}, },
{ {
name: "Just wildcard outside of subdomain in hostname, fail", name: "Just wildcard outside of subdomain in hostname, fail",
fields: fields{ rule: Rule{
Hostname: "*example.com", Hostname: "*example.com",
}, },
args: args{ args: args{
@ -90,7 +107,7 @@ func Test_rule_matches(t *testing.T) {
}, },
{ {
name: "Wildcard over multiple subdomains", name: "Wildcard over multiple subdomains",
fields: fields{ rule: Rule{
Hostname: "*.example.com", Hostname: "*.example.com",
}, },
args: args{ args: args{
@ -100,7 +117,7 @@ func Test_rule_matches(t *testing.T) {
}, },
{ {
name: "Hostname and path", name: "Hostname and path",
fields: fields{ rule: Rule{
Hostname: "*.example.com", Hostname: "*.example.com",
Path: &Regexp{Regexp: regexp.MustCompile("/static/.*\\.html")}, Path: &Regexp{Regexp: regexp.MustCompile("/static/.*\\.html")},
}, },
@ -111,7 +128,7 @@ func Test_rule_matches(t *testing.T) {
}, },
{ {
name: "Hostname and empty Regex", name: "Hostname and empty Regex",
fields: fields{ rule: Rule{
Hostname: "example.com", Hostname: "example.com",
Path: &Regexp{}, Path: &Regexp{},
}, },
@ -122,7 +139,7 @@ func Test_rule_matches(t *testing.T) {
}, },
{ {
name: "Hostname and nil path", name: "Hostname and nil path",
fields: fields{ rule: Rule{
Hostname: "example.com", Hostname: "example.com",
Path: nil, Path: nil,
}, },
@ -134,13 +151,8 @@ func Test_rule_matches(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := Rule{
Hostname: tt.fields.Hostname,
Path: tt.fields.Path,
Service: tt.fields.Service,
}
u := tt.args.requestURL u := tt.args.requestURL
if got := r.Matches(u.Hostname(), u.Path); got != tt.want { if got := tt.rule.Matches(u.Hostname(), u.Path); got != tt.want {
t.Errorf("rule.matches() = %v, want %v", got, tt.want) t.Errorf("rule.matches() = %v, want %v", got, tt.want)
} }
}) })