Skip to content
Closed
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 @@ -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 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 @@ -1123,6 +1144,7 @@ func (x *XdsServer) buildListener() *listenerv3.Listener {
},
},
},
AdditionalAddresses: dualStackAdditionalAddresses(uint32(x.ingressPort)),
FilterChains: []*listenerv3.FilterChain{
{
Filters: []*listenerv3.Filter{
Expand Down Expand Up @@ -1182,6 +1204,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
97 changes: 97 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,100 @@ 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.
# Retry the whole pod, not just the query. Asking once right after the rollout
# fails on a brand-new cluster, and retrying inside the pod does not rescue it:
# measured here, a pod that fails keeps failing for 36s while a fresh pod 10s
# later resolves first try. So the outer loop is the one that matters; the
# inner ones only absorb a slow answer. busybox nslookup has no timeout flag,
# so every budget below is attempts, not seconds.
probe=""
for probe_attempt in 1 2 3 4; do
probe="$(kubectl --context="${KUBECTL_CONTEXT}" run "coredns-probe-$$-${probe_attempt}" \
--rm --attach --quiet --restart=Never --image=busybox:1.36 --command -- \
sh -c "echo PROBE_RAN
for i in 1 2 3; do
out=\$(nslookup storage.googleapis.com 2>&1) && { echo RESOLVE_OK; break; }
echo \"resolve attempt failed: \$(echo \"\$out\" | tail -2 | tr '\n' ' ')\"
sleep 3
done
for i in 1 2 3; do
wget -q -T10 -O/dev/null http://${reg_name}:5000/v2/ && { echo REGISTRY_OK; break; }
sleep 3
done")" || true
[[ "${probe}" == *RESOLVE_OK* && "${probe}" == *REGISTRY_OK* ]] && break
if [[ "${probe_attempt}" != 4 ]]; then
echo " the cluster is not resolving yet; re-probing (${probe_attempt}/4)..."
sleep 10
fi
done
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
echo " probe output was:" >&2
while IFS= read -r line; do echo " ${line}" >&2; done <<<"${probe}"
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
Loading
Loading