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
25 changes: 25 additions & 0 deletions cmd/atenet/internal/router/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,27 @@ func (x *XdsServer) buildTracing() *hcmv3.HttpConnectionManager_Tracing {
}
}

// dualStackAdditionalAddresses returns the IPv6 half of a dual-stack ingress
// listener, to pair with a primary 0.0.0.0 socket on the same port. Ipv4Compat
// stays false: clearing IPV6_V6ONLY would collide with that primary.
func dualStackAdditionalAddresses(port int) []*listenerv3.AdditionalAddress {
return []*listenerv3.AdditionalAddress{
{
Address: &corev3.Address{
Address: &corev3.Address_SocketAddress{
SocketAddress: &corev3.SocketAddress{
Address: "::",
Ipv4Compat: false,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to sync up the configuration in the YAML with the configuration here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the comment here might have confused me. This is talking about two different sockets? If it is, can you remove the comment above.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, two different sockets. This is the router's ingress data-plane listener; the ipv4_compat: true you saw in the YAML is the Envoy admin socket on 9901. I've cut the comment down to the one line explaining why Ipv4Compat is false here — the cross-reference to the admin socket was the confusing part and it's gone.

On the original question: the admin sockets can't take this shape anyway, since bootstrap.v3.Admin has a single address field and no additional_addresses — one :: socket with ipv4_compat is the only way to serve both families there. The egress :443 listener is a real Listener, so that one could match the ingress shape if you'd rather have a single idiom across listeners. Happy to do it; it's a few lines plus the test that asserts the current shape.

PortSpecifier: &corev3.SocketAddress_PortValue{
PortValue: uint32(port),
},
},
},
},
},
}
}

func (x *XdsServer) buildListener() *listenerv3.Listener {
hcm := x.buildHcm("ingress_http", true)

Expand All @@ -1123,6 +1144,7 @@ func (x *XdsServer) buildListener() *listenerv3.Listener {
},
},
},
AdditionalAddresses: dualStackAdditionalAddresses(x.ingressPort),
FilterChains: []*listenerv3.FilterChain{
{
Filters: []*listenerv3.Filter{
Expand Down Expand Up @@ -1182,6 +1204,7 @@ func (x *XdsServer) buildHttpsListener() *listenerv3.Listener {
},
},
},
AdditionalAddresses: dualStackAdditionalAddresses(x.httpsPort),
FilterChains: []*listenerv3.FilterChain{
{
Filters: []*listenerv3.Filter{
Expand Down Expand Up @@ -1213,6 +1236,7 @@ func (x *XdsServer) buildConnectTerminateListener() *listenerv3.Listener {
},
},
},
AdditionalAddresses: dualStackAdditionalAddresses(x.connectPlainTextPort),
FilterChains: []*listenerv3.FilterChain{
{
Filters: []*listenerv3.Filter{
Expand Down Expand Up @@ -1246,6 +1270,7 @@ func (x *XdsServer) buildConnectTerminateTLSListener() *listenerv3.Listener {
},
},
},
AdditionalAddresses: dualStackAdditionalAddresses(x.connectTLSPort),
FilterChains: []*listenerv3.FilterChain{
{
Filters: []*listenerv3.Filter{
Expand Down
52 changes: 35 additions & 17 deletions cmd/atenet/internal/router/xds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,36 @@ import (
"github.com/agent-substrate/substrate/internal/atunnel"
)

// assertDualStackIngress checks an ingress listener keeps its 0.0.0.0 primary
// and gains exactly one "::" socket on the same port.
func assertDualStackIngress(t *testing.T, l *listenerv3.Listener, wantPort uint32) {
t.Helper()

sa := l.GetAddress().GetSocketAddress()
if sa.GetAddress() != "0.0.0.0" {
t.Errorf("Expected address '0.0.0.0', got %s", sa.GetAddress())
}
if sa.GetPortValue() != wantPort {
t.Errorf("Expected port %d, got %d", wantPort, sa.GetPortValue())
}

addrs := l.GetAdditionalAddresses()
if len(addrs) != 1 {
t.Fatalf("Expected 1 additional address on %s, got %d", l.GetName(), len(addrs))
}

asa := addrs[0].GetAddress().GetSocketAddress()
if asa.GetAddress() != "::" {
t.Errorf("Expected additional address '::', got %s", asa.GetAddress())
}
if asa.GetIpv4Compat() {
t.Error("Expected additional address Ipv4Compat to be false")
}
if asa.GetPortValue() != wantPort {
t.Errorf("Expected additional port %d, got %d", wantPort, asa.GetPortValue())
}
}

func TestXdsServer_UpdateSnapshot(t *testing.T) {
server := NewXdsServer(18000)
server.SetConfig(8081, 50052, "10.0.0.1")
Expand Down Expand Up @@ -150,14 +180,7 @@ func TestXdsServer_UpdateSnapshot(t *testing.T) {
if raw, exists := listenersMap[IngressHTTPListener]; !exists {
t.Errorf("Listener name '%s' is missing from snapshot listeners", IngressHTTPListener)
} else {
l := raw.(*listenerv3.Listener)
sa := l.GetAddress().GetSocketAddress()
if sa.GetPortValue() != 8081 {
t.Errorf("Expected port 8081, got %d", sa.GetPortValue())
}
if sa.GetAddress() != "0.0.0.0" {
t.Errorf("Expected address '0.0.0.0', got %s", sa.GetAddress())
}
assertDualStackIngress(t, raw.(*listenerv3.Listener), 8081)
}
}

Expand Down Expand Up @@ -192,10 +215,7 @@ func TestXdsServer_UpdateSnapshot_WithHttps(t *testing.T) {
t.Errorf("Listener name '%s' is missing from snapshot listeners", IngressHTTPSListener)
} else {
l := raw.(*listenerv3.Listener)
sa := l.GetAddress().GetSocketAddress()
if sa.GetPortValue() != 8443 {
t.Errorf("Expected port 8443, got %d", sa.GetPortValue())
}
assertDualStackIngress(t, l, 8443)

// Verify the TLS config references the serving cert via SDS rather
// than embedding it: inline filename DataSources are read only once
Expand Down Expand Up @@ -354,16 +374,14 @@ func TestXdsServer_UpdateSnapshot_WithConnect(t *testing.T) {
}
if raw, exists := listenersMap["connect_terminate"]; !exists {
t.Error("connect_terminate listener missing")
} else if sa := raw.(*listenerv3.Listener).GetAddress().GetSocketAddress(); sa.GetPortValue() != 8081 {
t.Errorf("Expected connect_terminate port 8081, got %d", sa.GetPortValue())
} else {
assertDualStackIngress(t, raw.(*listenerv3.Listener), 8081)
}
if raw, exists := listenersMap["connect_terminate_tls"]; !exists {
t.Error("connect_terminate_tls listener missing")
} else {
l := raw.(*listenerv3.Listener)
if sa := l.GetAddress().GetSocketAddress(); sa.GetPortValue() != 8444 {
t.Errorf("Expected connect_terminate_tls port 8444, got %d", sa.GetPortValue())
}
assertDualStackIngress(t, l, 8444)
ts := l.GetFilterChains()[0].GetTransportSocket()
if ts.GetName() != "envoy.transport_sockets.tls" {
t.Errorf("Expected connect_terminate_tls to be TLS-wrapped, got transport socket %q", ts.GetName())
Expand Down
86 changes: 86 additions & 0 deletions hack/verify/atenet-admin-bind.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Dropping ipv4_compat from a gateway's Envoy admin socket fails silently: the
# drain sequence reads the refused IPv4 loopback dial as "Envoy already exited"
# and reports a drain it never performed. No Go test reads these manifests.

set -o errexit -o nounset -o pipefail

ROOT="$(git rev-parse --show-toplevel)"
cd "${ROOT}"

# Each admin block is bounded by the first line that dedents back to its own
# key, and is judged on its own. A fixed context window silently truncates as
# the block grows, and a whole-file match lets one compliant block satisfy the
# check on behalf of another that has regressed. Comments are stripped so that
# prose about ipv4_compat cannot stand in for the setting.
read -r -d '' AWK_ADMIN_BIND <<'EOF' || true
function flush() {
if (!open) {
return
}
open = 0
if (block !~ /"::"/) {
printf "%s:%d: Envoy admin socket does not bind \"::\"; an IPv6-primary pod cannot be probed\n", FILENAME, start
} else if (block !~ /ipv4_compat: true/) {
printf "%s:%d: Envoy admin socket binds \"::\" without ipv4_compat; IPv4 loopback dials will be refused\n", FILENAME, start
}
}
match($0, /[^ ]/) && substr($0, RSTART) ~ /^admin:[[:space:]]*$/ {
flush()
open = 1
indent = RSTART - 1
start = FNR
block = ""
next
}
open {
if ($0 ~ /^[[:space:]]*$/) {
next
}
if (match($0, /[^ ]/) - 1 <= indent) {
flush()
next
}
line = $0
sub(/^[[:space:]]*#.*/, "", line)
sub(/[[:space:]]#.*/, "", line)
block = block line "\n"
}
END {
flush()
if (!start) {
printf "%s: no Envoy admin block found; this check needs updating\n", FILENAME
}
}
EOF

rc=0
for f in manifests/ate-install/atenet-router.yaml manifests/ate-install/atenet-egress.yaml; do
if [[ ! -f "${f}" ]]; then
echo "${f}: not found; this check needs updating" >&2
rc=1
continue
fi
problems="$(awk "${AWK_ADMIN_BIND}" "${f}")"
if [[ -n "${problems}" ]]; then
echo "${problems}" >&2
rc=1
fi
done

exit "${rc}"
8 changes: 6 additions & 2 deletions manifests/ate-install/atenet-egress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ data:
envoy.yaml: |
admin:
address:
socket_address: { address: 0.0.0.0, port_value: 15000 }
# ipv4_compat is load-bearing: see --envoy-admin-address below.
socket_address: { address: "::", ipv4_compat: true, port_value: 15000 }
static_resources:
listeners:
- name: egress
address:
socket_address: { address: 0.0.0.0, port_value: 443 }
# ipv4_compat rather than a second socket: IPv4 peers arrive as
# ::ffff: and nothing here reads the peer -- identity is the cert.
socket_address: { address: "::", ipv4_compat: true, port_value: 443 }
filter_chains:
# Named so ext_proc can read it back as xds.filter_chain_name. Must
# match EgressFilterChainName in
Expand Down Expand Up @@ -379,6 +382,7 @@ metadata:
namespace: ate-system
spec:
type: ClusterIP
ipFamilyPolicy: PreferDualStack
selector:
app: atenet-egress
ports:
Expand Down
5 changes: 4 additions & 1 deletion manifests/ate-install/atenet-router.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ data:
admin:
address:
socket_address:
address: 0.0.0.0
# ipv4_compat is load-bearing: dataplane.go probes /ready over IPv4 loopback.
address: "::"
ipv4_compat: true
port_value: 9901

node:
Expand Down Expand Up @@ -354,6 +356,7 @@ metadata:
namespace: ate-system
spec:
type: ClusterIP
ipFamilyPolicy: PreferDualStack
selector:
app: atenet-router
ports:
Expand Down
Loading