@@ -80,31 +80,60 @@ printf '%s\n' "$SANDBOX_RESOLV_CONF" > "$TEST_RESOLVER_OUTPUT"
8080STUB
8181cat > " $TEST_DIR /bin/getent" << 'STUB '
8282#!/usr/bin/env bash
83- printf ' 192.0.2.99 stale-address\n'
83+ [[ -z "${TEST_RUNNER_ADDRESS- 192.0.2.99}" ]] || printf '%s stale-address\n' "${TEST_RUNNER_ADDRESS-192.0.2.99}"
8484STUB
85- chmod +x " $TEST_DIR /bin/launcher" " $TEST_DIR /bin/getent"
85+ # The relay stub records its endpoints; readiness comes from the fake /proc/net.
86+ cat > " $TEST_DIR /bin/socat" << 'STUB '
87+ #!/usr/bin/env bash
88+ printf '%s\n' "$*" >> "$TEST_SOCAT_CALLS"
89+ STUB
90+ chmod +x " $TEST_DIR /bin/launcher" " $TEST_DIR /bin/getent" " $TEST_DIR /bin/socat"
91+ # 192.0.2.99:53 bound for UDP and listening for TCP, in /proc/net byte order.
92+ mkdir " $TEST_DIR /proc-net-bound" " $TEST_DIR /proc-net-unbound"
93+ printf ' sl local_address rem_address st\n 0: 630200C0:0035 00000000:0000 07 00000000:00000000\n' > " $TEST_DIR /proc-net-bound/udp"
94+ printf ' sl local_address rem_address st\n 0: 630200C0:0035 00000000:0000 0A 00000000:00000000\n' > " $TEST_DIR /proc-net-bound/tcp"
95+ printf ' sl local_address rem_address st\n' | tee " $TEST_DIR /proc-net-unbound/udp" > " $TEST_DIR /proc-net-unbound/tcp"
8696# libkrun appends every guest environment entry to the kernel command line,
8797# which linux-loader limits to single-line printable ASCII. Docker's generated
8898# resolv.conf is multi-line with comments, so the entrypoint must forward only
8999# the resolver directives, joined by the separator guest-dns.sh expands.
90100run_entrypoint () {
91- local resolv_conf=" $1 "
101+ local resolv_conf=" $1 " proc_net= " ${2 :- $TEST_DIR / proc-net-bound} "
92102 sed -e " s|/usr/local/bin/launcher|$TEST_DIR /bin/launcher|g" \
93103 -e " s|/etc/resolv.conf|$resolv_conf |g" \
104+ -e " s|/proc/net/|$proc_net /|g" \
105+ -e ' s|RESOLVER_RELAY_READY_ATTEMPTS=50|RESOLVER_RELAY_READY_ATTEMPTS=3|' \
94106 " $ROOT /launcher/entrypoint.sh" > " $TEST_DIR /entrypoint.sh"
95- rm -f " $TEST_DIR /forwarded"
107+ rm -f " $TEST_DIR /forwarded" " $TEST_DIR /socat-calls "
96108 PATH=" $TEST_DIR /bin:$PATH " \
97109 EGRESS_GATEWAY_URL=' https://egress_gateway:3190/base' \
98110 FILE_SERVER_URL=' http://[::1]:3000/base' \
99111 SANDBOX_FORWARD_TARGET=' tool_call_server:3033' \
100112 LAUNCHER_FILTER_VSOCK_ENOTCONN=false \
101113 TEST_RESOLVER_OUTPUT=" $TEST_DIR /forwarded" \
114+ TEST_SOCAT_CALLS=" $TEST_DIR /socat-calls" \
102115 bash " $TEST_DIR /entrypoint.sh"
103116}
104117# Independent reference for the expected handoff: directives only, whitespace
105- # collapsed, joined by the separator.
118+ # collapsed, joined by the separator. The first loopback nameserver becomes the
119+ # relay address; other loopback nameservers are unreachable and dropped.
106120encoded_reference () {
107- LC_ALL=C awk ' /^[[:space:]]*(nameserver|search|domain|options|sortlist)[[:space:]]/ { sub(/\r$/, ""); $1 = $1; print }' " $1 " | paste -sd ' |' -
121+ LC_ALL=C awk -v relay=192.0.2.99 ' /^[[:space:]]*(nameserver|search|domain|options|sortlist)[[:space:]]/ {
122+ sub(/\r$/, ""); $1 = $1
123+ address = tolower($2)
124+ if ($1 == "nameserver" && (address ~ /^127\./ || address ~ /^[0:]*:0*1$/ || address ~ /^::ffff:127\./)) { if (loop == "") loop = $2; if ($2 != loop) next; $2 = relay }
125+ print
126+ }' " $1 " | paste -sd ' |' -
127+ }
128+ # The relays start in the background, so wait for both before comparing. Both
129+ # bound idle children; the TCP listener also caps concurrent children.
130+ assert_relayed () {
131+ local udp_target=" $1 " tcp_target=" $2 " attempt
132+ for (( attempt = 0 ; attempt < 50 ; attempt++ )) ; do
133+ [[ -e " $TEST_DIR /socat-calls" && " $( wc -l < " $TEST_DIR /socat-calls" ) " -ge 2 ]] && break
134+ sleep 0.1
135+ done
136+ [[ " $( LC_ALL=C sort " $TEST_DIR /socat-calls" ) " == " $( printf -- ' -T 30 TCP4-LISTEN:53,bind=192.0.2.99,reuseaddr,fork,max-children=64 %s\n-T 5 UDP4-RECVFROM:53,bind=192.0.2.99,fork %s' " $tcp_target " " $udp_target " ) " ]]
108137}
109138cat > " $TEST_DIR /docker-resolv.conf" << 'RESOLV '
110139# Generated by Docker Engine.
@@ -119,24 +148,92 @@ options ndots:0
119148# Overrides: []
120149# Option ndots from: internal
121150RESOLV
151+ # The guest keeps loopback traffic local, so Docker's embedded resolver is
152+ # relayed from the runner address and the guest gets that address instead.
122153run_entrypoint " $TEST_DIR /docker-resolv.conf"
123- [[ " $( cat " $TEST_DIR /forwarded" ) " == ' nameserver 127 .0.0.11 |options ndots:0' ]]
154+ [[ " $( cat " $TEST_DIR /forwarded" ) " == ' nameserver 192 .0.2.99 |options ndots:0' ]]
124155[[ " $( cat " $TEST_DIR /forwarded" ) " == " $( encoded_reference " $TEST_DIR /docker-resolv.conf" ) " ]]
156+ assert_relayed UDP4-SENDTO:127.0.0.11:53 TCP4:127.0.0.11:53
125157[[ " $( wc -l < " $TEST_DIR /forwarded" ) " == 1 ]]
126158if LC_ALL=C grep -q ' [^ -~]' " $TEST_DIR /forwarded" ; then
127159 echo ' forwarded resolver contains bytes the kernel command line rejects' >&2 ; exit 1
128160fi
129- # The guest restores exactly the directives Docker supplied .
161+ # The guest restores the Docker directives with the relayed nameserver .
130162rm -rf " $TEST_DIR /guest/run/codeapi-resolver"
131163SANDBOX_RESOLV_CONF=" $( cat " $TEST_DIR /forwarded" ) " configure_guest_dns " $TEST_DIR /guest"
132- printf ' nameserver 127 .0.0.11 \noptions ndots:0\n' > " $TEST_DIR /expected"
164+ printf ' nameserver 192 .0.2.99 \noptions ndots:0\n' > " $TEST_DIR /expected"
133165cmp " $TEST_DIR /expected" " $TEST_DIR /guest/etc/resolv.conf"
134166
167+ # Only one loopback resolver can be relayed; other loopback entries would time
168+ # out in the guest, while routable nameservers keep their order.
169+ printf ' nameserver 127.0.0.53\r\nnameserver 10.0.0.2\nnameserver\t127.0.0.11\nsearch example.internal\n' > " $TEST_DIR /multi-loopback-resolv.conf"
170+ run_entrypoint " $TEST_DIR /multi-loopback-resolv.conf"
171+ [[ " $( cat " $TEST_DIR /forwarded" ) " == ' nameserver 192.0.2.99|nameserver 10.0.0.2|search example.internal' ]]
172+ [[ " $( cat " $TEST_DIR /forwarded" ) " == " $( encoded_reference " $TEST_DIR /multi-loopback-resolv.conf" ) " ]]
173+ assert_relayed UDP4-SENDTO:127.0.0.53:53 TCP4:127.0.0.53:53
174+
175+ # An IPv6 loopback resolver is just as unreachable from the guest; the IPv4
176+ # relay forwards to it over IPv6.
177+ printf ' nameserver ::1\noptions ndots:0\n' > " $TEST_DIR /ipv6-loopback-resolv.conf"
178+ run_entrypoint " $TEST_DIR /ipv6-loopback-resolv.conf"
179+ [[ " $( cat " $TEST_DIR /forwarded" ) " == ' nameserver 192.0.2.99|options ndots:0' ]]
180+ [[ " $( cat " $TEST_DIR /forwarded" ) " == " $( encoded_reference " $TEST_DIR /ipv6-loopback-resolv.conf" ) " ]]
181+ assert_relayed ' UDP6-SENDTO:[::1]:53' ' TCP6:[::1]:53'
182+ printf ' nameserver 127.0.0.11\nnameserver ::1\n' > " $TEST_DIR /mixed-loopback-resolv.conf"
183+ run_entrypoint " $TEST_DIR /mixed-loopback-resolv.conf"
184+ [[ " $( cat " $TEST_DIR /forwarded" ) " == ' nameserver 192.0.2.99' ]]
185+ assert_relayed UDP4-SENDTO:127.0.0.11:53 TCP4:127.0.0.11:53
186+ # Every spelling glibc accepts for a loopback resolver is relayed to its
187+ # canonical address: compressed or zero-padded ::1, IPv4-mapped 127/8 in any
188+ # IPv6 form, and inet_aton(3) IPv4 forms.
189+ while read -r spelling udp_target tcp_target; do
190+ printf ' nameserver %s\nnameserver 10.0.0.2\n' " $spelling " > " $TEST_DIR /spelled-loopback-resolv.conf"
191+ run_entrypoint " $TEST_DIR /spelled-loopback-resolv.conf"
192+ [[ " $( cat " $TEST_DIR /forwarded" ) " == ' nameserver 192.0.2.99|nameserver 10.0.0.2' ]]
193+ assert_relayed " $udp_target " " $tcp_target "
194+ done << 'SPELLINGS '
195+ 0:0:0:0:0:0:0:1 UDP6-SENDTO:[::1]:53 TCP6:[::1]:53
196+ 0000::0001 UDP6-SENDTO:[::1]:53 TCP6:[::1]:53
197+ ::1%lo UDP6-SENDTO:[::1]:53 TCP6:[::1]:53
198+ ::FFFF:127.0.0.11 UDP4-SENDTO:127.0.0.11:53 TCP4:127.0.0.11:53
199+ 0:0:0:0:0:ffff:127.0.0.11 UDP4-SENDTO:127.0.0.11:53 TCP4:127.0.0.11:53
200+ ::ffff:7f00:b UDP4-SENDTO:127.0.0.11:53 TCP4:127.0.0.11:53
201+ 127.1 UDP4-SENDTO:127.0.0.1:53 TCP4:127.0.0.1:53
202+ 2130706443 UDP4-SENDTO:127.0.0.11:53 TCP4:127.0.0.11:53
203+ 0x7f.0.0.0xb UDP4-SENDTO:127.0.0.11:53 TCP4:127.0.0.11:53
204+ 0177.0.0.013 UDP4-SENDTO:127.0.0.11:53 TCP4:127.0.0.11:53
205+ SPELLINGS
206+ # Spellings of the same loopback resolver collapse into one relayed entry.
207+ printf ' nameserver 127.0.0.11\nnameserver ::ffff:7f00:b\nnameserver 127.0.0.53\n' > " $TEST_DIR /same-loopback-resolv.conf"
208+ run_entrypoint " $TEST_DIR /same-loopback-resolv.conf"
209+ [[ " $( cat " $TEST_DIR /forwarded" ) " == ' nameserver 192.0.2.99|nameserver 192.0.2.99' ]]
210+ assert_relayed UDP4-SENDTO:127.0.0.11:53 TCP4:127.0.0.11:53
211+ # Routable resolvers and addresses glibc rejects are forwarded unchanged, without
212+ # a relay: 0127 is octal 87, and ::7f00:1 is not IPv4-mapped.
213+ printf ' nameserver 2001:db8::1\nnameserver fd00::11\nnameserver ::ffff:10.0.0.2\nnameserver ::7f00:1\nnameserver 0127.0.0.1\nnameserver 127.0.0.256\nnameserver 1:2:3:4:5:6:7:8:\nnameserver 127..1\n' > " $TEST_DIR /routable-resolv.conf"
214+ run_entrypoint " $TEST_DIR /routable-resolv.conf"
215+ [[ " $( cat " $TEST_DIR /forwarded" ) " == ' nameserver 2001:db8::1|nameserver fd00::11|nameserver ::ffff:10.0.0.2|nameserver ::7f00:1|nameserver 0127.0.0.1|nameserver 127.0.0.256|nameserver 1:2:3:4:5:6:7:8:|nameserver 127..1' ]]
216+ [[ ! -e " $TEST_DIR /socat-calls" ]]
217+
218+ # A relay that never listens, or a runner without a routable address, fails
219+ # startup instead of booting a guest that cannot resolve service names.
220+ if run_entrypoint " $TEST_DIR /docker-resolv.conf" " $TEST_DIR /proc-net-unbound" 2> " $TEST_DIR /entrypoint-error" ; then
221+ echo ' started launcher without a listening resolver relay' >&2 ; exit 1
222+ fi
223+ grep -q ' guest DNS relay could not listen on 192.0.2.99:53' " $TEST_DIR /entrypoint-error"
224+ [[ ! -e " $TEST_DIR /forwarded" ]]
225+ if TEST_RUNNER_ADDRESS=' ' run_entrypoint " $TEST_DIR /docker-resolv.conf" 2> " $TEST_DIR /entrypoint-error" ; then
226+ echo ' started launcher without a resolver relay address' >&2 ; exit 1
227+ fi
228+ grep -q ' no non-loopback IPv4 address to relay loopback resolver 127.0.0.11' " $TEST_DIR /entrypoint-error"
229+ [[ ! -e " $TEST_DIR /forwarded" && ! -e " $TEST_DIR /socat-calls" ]]
230+
135231# Kubernetes resolvers: tabs, CRLF, trailing spaces, and unknown keywords are
136232# normalized away; search domains and options survive intact.
137233printf ' nameserver\t10.96.0.10 \r\nsearch tenant.svc.cluster.local svc.cluster.local cluster.local\n; resolver comment\nlookup file bind\noptions ndots:5\n' > " $TEST_DIR /k8s-resolv.conf" # leak-check:allow
138234run_entrypoint " $TEST_DIR /k8s-resolv.conf"
139235[[ " $( cat " $TEST_DIR /forwarded" ) " == ' nameserver 10.96.0.10|search tenant.svc.cluster.local svc.cluster.local cluster.local|options ndots:5' ]] # leak-check:allow
236+ [[ ! -e " $TEST_DIR /socat-calls" ]]
140237
141238# The runner's own resolver must round-trip through the same reference.
142239run_entrypoint /etc/resolv.conf
0 commit comments