Skip to content
Draft
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
370 changes: 370 additions & 0 deletions .github/workflows/e2e-ipv6.yaml

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions cmd/atenet/internal/router/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,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 uint32) []*listenerv3.AdditionalAddress {
return []*listenerv3.AdditionalAddress{
{
Address: &corev3.Address{
Address: &corev3.Address_SocketAddress{
SocketAddress: &corev3.SocketAddress{
Address: "::",
Ipv4Compat: false,
PortSpecifier: &corev3.SocketAddress_PortValue{
PortValue: port,
},
},
},
},
},
}
}

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

Expand All @@ -1120,6 +1141,7 @@ func (x *XdsServer) buildListener() *listenerv3.Listener {
},
},
},
AdditionalAddresses: dualStackAdditionalAddresses(uint32(x.ingressPort)),
FilterChains: []*listenerv3.FilterChain{
{
Filters: []*listenerv3.Filter{
Expand Down Expand Up @@ -1179,6 +1201,7 @@ func (x *XdsServer) buildHttpsListener() *listenerv3.Listener {
},
},
},
AdditionalAddresses: dualStackAdditionalAddresses(uint32(x.httpsPort)),
FilterChains: []*listenerv3.FilterChain{
{
Filters: []*listenerv3.Filter{
Expand Down
35 changes: 35 additions & 0 deletions cmd/atenet/internal/router/xds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,22 @@ func TestXdsServer_UpdateSnapshot(t *testing.T) {
if sa.GetAddress() != "0.0.0.0" {
t.Errorf("Expected address '0.0.0.0', got %s", sa.GetAddress())
}

addrs := l.GetAdditionalAddresses()
if len(addrs) == 0 {
t.Fatalf("Expected an additional address on %s, got none", IngressHTTPListener)
}

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

Expand Down Expand Up @@ -196,6 +212,25 @@ func TestXdsServer_UpdateSnapshot_WithHttps(t *testing.T) {
if sa.GetPortValue() != 8443 {
t.Errorf("Expected port 8443, got %d", sa.GetPortValue())
}
if sa.GetAddress() != "0.0.0.0" {
t.Errorf("Expected address '0.0.0.0', got %s", sa.GetAddress())
}

addrs := l.GetAdditionalAddresses()
if len(addrs) == 0 {
t.Fatalf("Expected an additional address on %s, got none", IngressHTTPSListener)
}

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

// Verify the TLS config references the serving cert via SDS rather
// than embedding it: inline filename DataSources are read only once
Expand Down
64 changes: 64 additions & 0 deletions hack/create-kind-cluster.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-kind}"
KUBECTL_CONTEXT="kind-${KIND_CLUSTER_NAME}"
reg_name="kind-registry"
reg_port="${KIND_REGISTRY_PORT:-5001}"
IPV6_DNS_UPSTREAM="${IPV6_DNS_UPSTREAM:-2001:4860:4860::8888 2001:4860:4860::8844}"

if [[ $# -gt 0 ]]; then
case "$1" in
Expand All @@ -31,6 +32,8 @@ if [[ $# -gt 0 ]]; then
echo "Configured through the environment:"
echo " KIND_CLUSTER_NAME Name of the cluster to create (default: kind)."
echo " IP_FAMILY Address families for pods and Services: ipv4, ipv6 or dual (default: ipv4)."
echo " IPV6_DNS_UPSTREAM Space-separated IPv6 resolvers CoreDNS forwards to when IP_FAMILY=ipv6"
echo " (default: Google Public DNS). Override where those are unreachable."
exit 0
;;
esac
Expand Down Expand Up @@ -196,6 +199,67 @@ if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${reg_name}
docker network connect "kind" "${reg_name}"
fi

# 4.5. Give CoreDNS an IPv6 forwarder and a registry entry (ipv6 only)
#
# CoreDNS runs dnsPolicy: Default, so it inherits the node's Docker-generated
# /etc/resolv.conf, which always names an IPv4 resolver. Pods here have no IPv4
# address, so without this every external lookup SERVFAILs and anything that
# fetches at runtime -- atelet pulling the gVisor tarball, for one -- never
# starts. Step 3 wired the registry into containerd on the *node*, which does
# not help a pod: atelet pulls actor images from its own netns, where
# "kind-registry" NXDOMAINs. Two Corefile clauses fix both.
if [[ "${IP_FAMILY}" == "ipv6" ]]; then
echo "Repointing CoreDNS at an IPv6 resolver and teaching it '${reg_name}'..."
reg_v6="$(docker inspect "${reg_name}" \
--format '{{.NetworkSettings.Networks.kind.GlobalIPv6Address}}')"
if [[ -z "${reg_v6}" ]]; then
echo "error: '${reg_name}' has no IPv6 address on the 'kind' network" >&2
exit 1
fi

corefile="$(kubectl --context="${KUBECTL_CONTEXT}" -n kube-system get cm coredns \
-o jsonpath='{.data.Corefile}')"
# fallthrough is load-bearing: without it every name that is not the registry
# NXDOMAINs, trading one outage for a worse one. Both sides are left unquoted
# -- bash 3.2 would splice the quotes in literally.
search="forward . /etc/resolv.conf"
replace="hosts {
${reg_v6} ${reg_name}
fallthrough
}
forward . ${IPV6_DNS_UPSTREAM}"
patched="${corefile/$search/$replace}"
if [[ "${patched}" == "${corefile}" ]]; then
echo "error: '${search}' not found in the CoreDNS Corefile" >&2
echo " a silent no-op here is the whole failure mode; inspect it by hand" >&2
exit 1
fi

# A YAML patch file avoids escaping the Corefile's newlines into JSON.
{ printf 'data:\n Corefile: |\n'; printf '%s\n' "${patched}" | sed 's/^/ /'; } \
> "${ROOT}/bin/coredns-patch.yaml"
kubectl --context="${KUBECTL_CONTEXT}" -n kube-system patch cm coredns \
--type=merge --patch-file "${ROOT}/bin/coredns-patch.yaml"
kubectl --context="${KUBECTL_CONTEXT}" -n kube-system rollout restart deploy/coredns
kubectl --context="${KUBECTL_CONTEXT}" -n kube-system rollout status deploy/coredns \
--timeout=120s

# Probe from a pod, never from the node: the node is dual-stack and resolves
# both names either way, so a node-side check proves nothing. The registry leg
# fetches rather than resolves, because the hosts entry above is AAAA-only and
# `nslookup kind-registry` fails on its A query even though every real client
# (getaddrinfo, and so containerd and atelet) is satisfied by the AAAA.
echo "Verifying DNS from a pod..."
if ! kubectl --context="${KUBECTL_CONTEXT}" run "coredns-probe-$$" \
--rm --attach --quiet --restart=Never --image=busybox:1.36 --command -- \
sh -c "nslookup storage.googleapis.com >/dev/null &&
wget -q -T10 -O/dev/null http://${reg_name}:5000/v2/"; then
echo "error: a pod cannot resolve an external name and reach '${reg_name}'" >&2
echo " IPV6_DNS_UPSTREAM is '${IPV6_DNS_UPSTREAM}'; set it to a reachable resolver" >&2
exit 1
fi
fi

# 5. Document the local registry in kube-public ConfigMap
echo "Documenting local registry in cluster..."
cat <<EOF | kubectl --context="${KUBECTL_CONTEXT}" apply -f -
Expand Down
7 changes: 5 additions & 2 deletions manifests/ate-install/atenet-egress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ data:
envoy.yaml: |
admin:
address:
socket_address: { address: 0.0.0.0, port_value: 15000 }
# ipv4_compat: the drainer dials this on IPv4 loopback (--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 }
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 +380,8 @@ metadata:
namespace: ate-system
spec:
type: ClusterIP
# Prefer, not Require: Require fails Service creation on a single-stack cluster.
ipFamilyPolicy: PreferDualStack
selector:
app: atenet-egress
ports:
Expand Down
7 changes: 6 additions & 1 deletion manifests/ate-install/atenet-router.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ data:
admin:
address:
socket_address:
address: 0.0.0.0
# ipv4_compat clears IPV6_V6ONLY, so this one socket serves both
# families; dataplane.go probes /ready over the IPv4 loopback.
address: "::"
ipv4_compat: true
port_value: 9901

node:
Expand Down Expand Up @@ -354,6 +357,8 @@ metadata:
namespace: ate-system
spec:
type: ClusterIP
# Prefer, not Require: Require fails Service creation on a single-stack cluster.
ipFamilyPolicy: PreferDualStack
selector:
app: atenet-router
ports:
Expand Down
Loading