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
74 changes: 74 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,77 @@ 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
#
# CoreDNS runs dnsPolicy: Default, inheriting the node's IPv4 resolver, which
# no pod here can reach, so external lookups SERVFAIL. Step 3's registry
# wiring is node-side, so it misses atelet too: that pull runs in atelet's own
# netns, where "kind-registry" NXDOMAINs.
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 but the registry
# NXDOMAINs, cluster.local included -- one outage traded for a worse one.
search="forward . /etc/resolv.conf"
replace="hosts {
${reg_v6} ${reg_name}
fallthrough
}
forward . ${IPV6_DNS_UPSTREAM}"
# Unquoted on purpose: bash 3.2 splices the quotes in literally. First match
# only, and the emitted block inherits the matched line's indentation.
patched="${corefile/$search/$replace}"
if [[ "${patched}" == "${corefile}" ]]; then
echo "error: '${search}' not found in the CoreDNS Corefile" >&2
echo " the Corefile layout changed upstream; update this block" >&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, not the node: the node is dual-stack and passes either
# way. The registry leg fetches rather than resolves -- the hosts entry is
# AAAA-only, which fails nslookup's A query but satisfies getaddrinfo.
echo "Verifying DNS from a pod..."
# Markers, not the exit status: --attach returns only the last leg's.
# PROBE_RAN separates a failed leg from a pod that never ran -- a different fix.
probe="$(kubectl --context="${KUBECTL_CONTEXT}" run "coredns-probe-$$" \
--rm --attach --quiet --restart=Never --image=busybox:1.36 --command -- \
sh -c "echo PROBE_RAN
nslookup storage.googleapis.com >/dev/null && echo RESOLVE_OK
wget -q -T10 -O/dev/null http://${reg_name}:5000/v2/ && echo REGISTRY_OK")" || true
if [[ "${probe}" != *PROBE_RAN* ]]; then
echo "error: the probe pod never ran, so CoreDNS is unverified" >&2
echo " check that it scheduled and that 'busybox:1.36' pulled" >&2
exit 1
fi
if [[ "${probe}" != *RESOLVE_OK* ]]; then
echo "error: a pod cannot resolve an external name" >&2
echo " IPV6_DNS_UPSTREAM is '${IPV6_DNS_UPSTREAM}'; set it to a reachable resolver" >&2
exit 1
fi
if [[ "${probe}" != *REGISTRY_OK* ]]; then
echo "error: DNS works but a pod cannot reach '${reg_name}' at [${reg_v6}]:5000" >&2
echo " check the registry container is up and on the 'kind' network" >&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