From f6442e9276625746dbdac8dd939620a51ed8f0b3 Mon Sep 17 00:00:00 2001 From: mesutoezdil Date: Wed, 29 Jul 2026 21:47:01 +0200 Subject: [PATCH 1/2] fix: match actor DNS names case-insensitively at the router DNS lookups are case-insensitive (RFC 4343), so a client that resolves "MyActor.MySpace.actors.resources.substrate.ate.dev" reaches the router with that spelling preserved in Host/:authority. ParseActorDNSName compared it byte for byte against a strictly lower-case suffix and name pattern, so the request was rejected with a 404 even though its DNS lookup had resolved to that very actor. Fold the name to lower case before parsing. Actor and atespace names are always lower case, so a folded name either addresses the same actor its lookup resolved to or fails validation as before. --- cmd/atenet/internal/router/extproc_in_test.go | 6 ++++++ internal/resources/actorref.go | 9 ++++++++- internal/resources/actorref_test.go | 7 +++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/cmd/atenet/internal/router/extproc_in_test.go b/cmd/atenet/internal/router/extproc_in_test.go index bbb7a8f9e..31b61a04a 100644 --- a/cmd/atenet/internal/router/extproc_in_test.go +++ b/cmd/atenet/internal/router/extproc_in_test.go @@ -151,6 +151,12 @@ func TestParseActorRef(t *testing.T) { want: resources.ActorRef{Atespace: "team-a", Name: "my-actor"}, wantErr: false, }, + { + name: "mixed-case host", + host: "My-Actor.Team-A.actors.resources.substrate.ate.dev", + want: resources.ActorRef{Atespace: "team-a", Name: "my-actor"}, + wantErr: false, + }, { name: "missing atespace label", host: "my-actor.actors.resources.substrate.ate.dev", diff --git a/internal/resources/actorref.go b/internal/resources/actorref.go index 7d287ad8a..b37a52b2d 100644 --- a/internal/resources/actorref.go +++ b/internal/resources/actorref.go @@ -72,8 +72,15 @@ func ActorRefFromActor(a *ateapipb.Actor) ActorRef { } // ParseActorDNSName parses a DNS name for a given actor. +// +// The name is folded to lower case first. DNS lookups are case-insensitive +// (RFC 4343), so a client that resolved "MyActor.MySpace." reaches us +// with that spelling preserved in the Host header, while actor and atespace +// names are always lower case. Folding keeps the request addressed to the same +// actor its DNS lookup resolved to instead of failing to parse. func ParseActorDNSName(name string) (ActorRef, error) { - rest, found := strings.CutSuffix(strings.TrimSuffix(name, "."), "."+ActorDNSSuffix) + normalized := strings.ToLower(strings.TrimSuffix(name, ".")) + rest, found := strings.CutSuffix(normalized, "."+ActorDNSSuffix) if !found { return ActorRef{}, fmt.Errorf("invalid actor DNS name: must end with %s, got %q", ActorDNSSuffix, name) } diff --git a/internal/resources/actorref_test.go b/internal/resources/actorref_test.go index 63b74069f..cd0556f6d 100644 --- a/internal/resources/actorref_test.go +++ b/internal/resources/actorref_test.go @@ -56,8 +56,11 @@ func TestParseActorDNSName(t *testing.T) { {"valid trailing dot", "act-1.team-a.actors.resources.substrate.ate.dev.", ActorRef{Atespace: "team-a", Name: "act-1"}, false}, {"wrong suffix", "act-1.team-a.example.com", ActorRef{}, true}, {"missing atespace", "act-1.actors.resources.substrate.ate.dev", ActorRef{}, true}, - {"invalid actor name", "ACT-1.team-a.actors.resources.substrate.ate.dev", ActorRef{}, true}, - {"invalid atespace", "act-1.TEAM.actors.resources.substrate.ate.dev", ActorRef{}, true}, + {"mixed-case actor name", "ACT-1.team-a.actors.resources.substrate.ate.dev", ActorRef{Atespace: "team-a", Name: "act-1"}, false}, + {"mixed-case atespace", "act-1.TEAM-A.actors.resources.substrate.ate.dev", ActorRef{Atespace: "team-a", Name: "act-1"}, false}, + {"mixed-case suffix", "act-1.team-a.Actors.Resources.Substrate.Ate.Dev", ActorRef{Atespace: "team-a", Name: "act-1"}, false}, + {"invalid actor name", "act_1.team-a.actors.resources.substrate.ate.dev", ActorRef{}, true}, + {"invalid atespace", "act-1.team_a.actors.resources.substrate.ate.dev", ActorRef{}, true}, {"host:port not accepted", "act-1.team-a.actors.resources.substrate.ate.dev:8080", ActorRef{}, true}, {"empty", "", ActorRef{}, true}, } From d6e0ea7f1864281cb6e75bba811a5e4a2e5ff7ed Mon Sep 17 00:00:00 2001 From: mesutoezdil Date: Thu, 30 Jul 2026 22:47:48 +0200 Subject: [PATCH 2/2] resources: fold only ASCII case in the actor DNS name --- internal/resources/actorref.go | 17 ++++++++++++++++- internal/resources/actorref_test.go | 3 +++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/resources/actorref.go b/internal/resources/actorref.go index b37a52b2d..15362a825 100644 --- a/internal/resources/actorref.go +++ b/internal/resources/actorref.go @@ -71,6 +71,21 @@ func ActorRefFromActor(a *ateapipb.Actor) ActorRef { } } +// lowerASCII folds A-Z and leaves every other byte alone. Resource names are +// DNS-1123 labels, so ASCII is the whole alphabet here, and strings.ToLower +// would additionally fold characters outside it onto ASCII letters (the Kelvin +// sign U+212A onto "k", U+017F onto "s"), letting a non-ASCII host reach an +// actor under a spelling that is not its name. +func lowerASCII(s string) string { + b := []byte(s) + for i, c := range b { + if c >= 'A' && c <= 'Z' { + b[i] = c + ('a' - 'A') + } + } + return string(b) +} + // ParseActorDNSName parses a DNS name for a given actor. // // The name is folded to lower case first. DNS lookups are case-insensitive @@ -79,7 +94,7 @@ func ActorRefFromActor(a *ateapipb.Actor) ActorRef { // names are always lower case. Folding keeps the request addressed to the same // actor its DNS lookup resolved to instead of failing to parse. func ParseActorDNSName(name string) (ActorRef, error) { - normalized := strings.ToLower(strings.TrimSuffix(name, ".")) + normalized := lowerASCII(strings.TrimSuffix(name, ".")) rest, found := strings.CutSuffix(normalized, "."+ActorDNSSuffix) if !found { return ActorRef{}, fmt.Errorf("invalid actor DNS name: must end with %s, got %q", ActorDNSSuffix, name) diff --git a/internal/resources/actorref_test.go b/internal/resources/actorref_test.go index cd0556f6d..a7c8707b0 100644 --- a/internal/resources/actorref_test.go +++ b/internal/resources/actorref_test.go @@ -59,6 +59,9 @@ func TestParseActorDNSName(t *testing.T) { {"mixed-case actor name", "ACT-1.team-a.actors.resources.substrate.ate.dev", ActorRef{Atespace: "team-a", Name: "act-1"}, false}, {"mixed-case atespace", "act-1.TEAM-A.actors.resources.substrate.ate.dev", ActorRef{Atespace: "team-a", Name: "act-1"}, false}, {"mixed-case suffix", "act-1.team-a.Actors.Resources.Substrate.Ate.Dev", ActorRef{Atespace: "team-a", Name: "act-1"}, false}, + // strings.ToLower would fold the Kelvin sign onto "k" and hand "act-1k" a + // request addressed to a name no actor can have. + {"non-ASCII actor name", "act-1K.team-a.actors.resources.substrate.ate.dev", ActorRef{}, true}, {"invalid actor name", "act_1.team-a.actors.resources.substrate.ate.dev", ActorRef{}, true}, {"invalid atespace", "act-1.team_a.actors.resources.substrate.ate.dev", ActorRef{}, true}, {"host:port not accepted", "act-1.team-a.actors.resources.substrate.ate.dev:8080", ActorRef{}, true},