Skip to content

Commit 68c9f93

Browse files
authored
fix: Relay Loopback Resolvers Into KVM Guests (#314)
* 🧭 fix: Relay Loopback Resolvers Into KVM Guests The KVM launcher forwards the runner's /etc/resolv.conf into the libkrun guest. Under Docker Compose that names the embedded resolver 127.0.0.11, but the TSI guest kernel keeps loopback traffic on its own loopback device, so every service-name lookup in the guest failed. Generated-file uploads and session marker reads to egress_gateway were dropped with ConnectionRefused or FailedToOpenSocket, while connections by IP worked. When the runner has a loopback nameserver, relay it over UDP and TCP port 53 from the runner's own IPv4 address with socat and hand the guest that address instead. Further loopback nameservers are dropped, routable resolvers such as Kubernetes cluster DNS are forwarded unchanged, and startup fails if the relay cannot listen or the runner has no routable address. * 🧭 fix: Bound the Guest DNS Relay and Relay IPv6 Loopback Give both relays an idle timeout and cap concurrent TCP children. socat 1.8 fails every UDP-RECVFROM child given max-children, so the UDP relay relies on its short idle timeout. Treat a `::1` nameserver as loopback too, relaying it over IPv6 from the same runner address instead of forwarding an address the guest would only answer locally. * 🧭 fix: Recognize Every Loopback Resolver Spelling Classify nameservers as loopback by value rather than by the canonical `::1` string: zero-padded or compressed IPv6 loopback and IPv4-mapped 127.0.0.0/8 are relayed too, forwarding over IPv6 to the address exactly as written. Routable IPv6 resolvers stay untouched. * 🧭 fix: Parse Nameserver Addresses Before Loopback Detection Replace string matching with real parsing. IPv6 addresses are expanded to their eight groups (compressed zeros, dotted IPv4 tails), and IPv4 addresses are read in the inet_aton(3) forms glibc accepts in resolv.conf. Then ::1 and IPv4-mapped or plain 127.0.0.0/8 are relayed in any spelling. Mapped addresses are relayed over IPv4 to their canonical form. Spellings of the same resolver collapse into one relayed entry, and addresses glibc rejects are forwarded unchanged.
1 parent 26d1ab8 commit 68c9f93

3 files changed

Lines changed: 282 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,17 @@ read-only root disk does not need modification at boot. Rebuild the runner
245245
image to pick up this layout change. A missing resolver handoff fails startup
246246
rather than leaving the guest with an unrelated public DNS server.
247247

248+
The guest kernel keeps loopback traffic on its own loopback device instead of
249+
proxying it through TSI, so it cannot reach a loopback resolver such as Docker's
250+
embedded `127.0.0.11`. The launcher entrypoint relays the first loopback
251+
nameserver (`127.0.0.0/8`, `::1` or IPv4-mapped `127.0.0.0/8`, in any spelling
252+
glibc accepts) over UDP and TCP port 53 from the runner's
253+
own IPv4 address with `socat`, and forwards that address to the guest in its
254+
place; further loopback nameservers are dropped, and startup fails if the relay
255+
cannot listen. Relay children exit when idle, and the TCP relay caps its
256+
concurrent children. Routable nameservers, including Kubernetes cluster DNS, are forwarded
257+
unchanged.
258+
248259
libkrun delivers the guest environment on the kernel command line, which only
249260
carries single-line printable ASCII and is capped at 2048 bytes by the guest
250261
kernel. The launcher entrypoint therefore forwards only the `nameserver`,

‎launcher/entrypoint.sh‎

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,129 @@ set -e
1212
# joined by a separator that api/src/guest-dns.sh expands back into lines.
1313
RESOLV_FIELD_SEPARATOR='|'
1414

15+
# The guest kernel keeps loopback traffic on its own loopback device instead of
16+
# proxying it through TSI, so a loopback resolver such as Docker's embedded
17+
# 127.0.0.11 never answers inside the guest. Relay it from this container's own
18+
# address, which the guest reaches through TSI, and hand the guest that address.
19+
# The relay forks per datagram and per connection, so bound how long an idle
20+
# child may live, and how many TCP children may run at once. socat 1.8 fails
21+
# every UDP-RECVFROM child given max-children, so the UDP relay relies on its
22+
# short idle timeout. Only the runner network reaches the relay: NsJail jobs in
23+
# the guest run in their own network namespace.
24+
RESOLVER_RELAY_READY_ATTEMPTS=50
25+
RESOLVER_RELAY_MAX_CHILDREN=64
26+
RESOLVER_RELAY_UDP_IDLE_SECONDS=5
27+
RESOLVER_RELAY_TCP_IDLE_SECONDS=30
28+
29+
# One inet_aton(3) part: hex with 0x, octal with a leading 0, else decimal.
30+
inet_aton_part() {
31+
case "$1" in
32+
0x*) [[ "$1" =~ ^0x[0-9a-f]+$ ]] && echo $((16#${1#0x})) ;;
33+
0*) [[ "$1" =~ ^0[0-7]*$ ]] && echo $((8#$1)) ;;
34+
*) [[ "$1" =~ ^[0-9]+$ ]] && echo $((10#$1)) ;;
35+
esac
36+
}
37+
38+
# Numeric value of an IPv4 address in the inet_aton(3) forms glibc accepts in
39+
# resolv.conf: one to four parts, the last filling the remaining bytes.
40+
ipv4_value() {
41+
local parts part value=0 index last
42+
IFS=. read -ra parts <<< "$1"
43+
[[ "$1" != *. ]] && (( ${#parts[@]} >= 1 && ${#parts[@]} <= 4 )) || return 1
44+
last=$(( ${#parts[@]} - 1 ))
45+
for ((index = 0; index < last; index++)); do
46+
part="$(inet_aton_part "${parts[index]}")" || return 1
47+
(( part <= 255 )) || return 1
48+
value=$(( value | part << (24 - 8 * index) ))
49+
done
50+
part="$(inet_aton_part "${parts[last]}")" || return 1
51+
(( part < 1 << (32 - 8 * last) )) || return 1
52+
echo $(( value | part ))
53+
}
54+
55+
# The eight 16-bit groups of an IPv6 address in decimal, accepting compressed
56+
# zeros and a dotted IPv4 tail.
57+
ipv6_groups() {
58+
local address="$1" head tail octets octet group groups=() tail_groups=() fill
59+
if [[ "$address" =~ ^(.*:)([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
60+
address="${BASH_REMATCH[1]}"
61+
IFS=. read -ra octets <<< "${BASH_REMATCH[2]}"
62+
for octet in "${octets[@]}"; do
63+
[[ "$octet" =~ ^(0|[1-9][0-9]{0,2})$ ]] && (( octet <= 255 )) || return 1
64+
done
65+
address+="$(printf '%x:%x' $(( octets[0] << 8 | octets[1] )) $(( octets[2] << 8 | octets[3] )))"
66+
fi
67+
[[ "$address" =~ ^[0-9a-f:]+$ && "$address" != :[!:]* && "$address" != *[!:]: && "$address" != *:::* ]] || return 1
68+
if [[ "$address" == *::* ]]; then
69+
head="${address%%::*}"
70+
tail="${address#*::}"
71+
[[ "$tail" != *::* ]] || return 1
72+
[ -z "$head" ] || IFS=: read -ra groups <<< "$head"
73+
[ -z "$tail" ] || IFS=: read -ra tail_groups <<< "$tail"
74+
fill=$(( 8 - ${#groups[@]} - ${#tail_groups[@]} ))
75+
(( fill >= 1 )) || return 1
76+
for ((; fill > 0; fill--)); do groups+=(0); done
77+
groups+=("${tail_groups[@]}")
78+
else
79+
IFS=: read -ra groups <<< "$address"
80+
fi
81+
(( ${#groups[@]} == 8 )) || return 1
82+
for group in "${groups[@]}"; do
83+
[[ "$group" =~ ^[0-9a-f]{1,4}$ ]] || return 1
84+
printf '%d ' $((16#$group))
85+
done
86+
}
87+
88+
# Canonical form of a loopback nameserver in any spelling glibc accepts: IPv4
89+
# 127.0.0.0/8, IPv6 ::1, or IPv4-mapped 127.0.0.0/8 (relayed as IPv4). Fails for
90+
# every other address.
91+
canonical_loopback() {
92+
local address="${1,,}" text groups value
93+
address="${address%%\%*}"
94+
if [[ "$address" == *:* ]]; then
95+
text="$(ipv6_groups "$address")" || return 1
96+
read -ra groups <<< "$text"
97+
if [ "${groups[*]}" = '0 0 0 0 0 0 0 1' ]; then
98+
echo ::1
99+
return 0
100+
fi
101+
[ "${groups[*]:0:6}" = '0 0 0 0 0 65535' ] || return 1
102+
value=$(( groups[6] << 16 | groups[7] ))
103+
else
104+
value="$(ipv4_value "$address")" || return 1
105+
fi
106+
(( value >> 24 == 127 )) || return 1
107+
printf '%d.%d.%d.%d' $(( value >> 24 )) $(( value >> 16 & 255 )) $(( value >> 8 & 255 )) $(( value & 255 ))
108+
}
109+
110+
loopback_nameserver() {
111+
local line words
112+
while IFS= read -r line || [ -n "$line" ]; do
113+
read -ra words <<< "${line%$'\r'}"
114+
if [ "${words[0]:-}" = nameserver ] && canonical_loopback "${words[1]:-}"; then
115+
return 0
116+
fi
117+
done < "$1"
118+
}
119+
120+
runner_address() {
121+
getent ahostsv4 "$(cat /proc/sys/kernel/hostname)" | awk '$1 !~ /^127\./ { print $1; exit }'
122+
}
123+
15124
encode_resolv_conf() {
125+
local loopback="${1:-}" relay="${2:-}"
16126
local LC_ALL=C
17-
local line words encoded=''
127+
local line words canonical encoded=''
18128
while IFS= read -r line || [ -n "$line" ]; do
19129
line="${line%$'\r'}"
20130
if [[ ! "$line" =~ ^[[:space:]]*(nameserver|search|domain|options|sortlist)[[:space:]] ]]; then
21131
continue
22132
fi
23133
read -ra words <<< "$line"
134+
if [ "${words[0]}" = nameserver ] && canonical="$(canonical_loopback "${words[1]}")"; then
135+
[ "$canonical" = "$loopback" ] || continue
136+
words[1]="$relay"
137+
fi
24138
line="${words[*]}"
25139
if [[ "$line" == *[!' '-'~']* || "$line" == *[\"$RESOLV_FIELD_SEPARATOR]* ]]; then
26140
echo "ERROR: runner /etc/resolv.conf line cannot cross the kernel command line: $line" >&2
@@ -31,13 +145,62 @@ encode_resolv_conf() {
31145
printf '%s' "$encoded"
32146
}
33147

34-
SANDBOX_RESOLV_CONF="$(encode_resolv_conf < /etc/resolv.conf)"
148+
proc_net_address() {
149+
local a b c d
150+
IFS=. read -r a b c d <<< "$1"
151+
printf '%02X%02X%02X%02X:0035' "$d" "$c" "$b" "$a"
152+
}
153+
154+
start_resolver_relay() {
155+
local nameserver="$1" address="$2" local_address udp_pid tcp_pid attempt
156+
local udp_target="UDP4-SENDTO:$nameserver:53" tcp_target="TCP4:$nameserver:53"
157+
if [ "$nameserver" = ::1 ]; then
158+
udp_target='UDP6-SENDTO:[::1]:53'
159+
tcp_target='TCP6:[::1]:53'
160+
fi
161+
socat -T "$RESOLVER_RELAY_UDP_IDLE_SECONDS" \
162+
"UDP4-RECVFROM:53,bind=$address,fork" "$udp_target" &
163+
udp_pid=$!
164+
socat -T "$RESOLVER_RELAY_TCP_IDLE_SECONDS" \
165+
"TCP4-LISTEN:53,bind=$address,reuseaddr,fork,max-children=$RESOLVER_RELAY_MAX_CHILDREN" "$tcp_target" &
166+
tcp_pid=$!
167+
local_address="$(proc_net_address "$address")"
168+
for ((attempt = 0; attempt < RESOLVER_RELAY_READY_ATTEMPTS; attempt++)); do
169+
if grep -q " $local_address " /proc/net/udp && grep -q " $local_address 00000000:0000 0A " /proc/net/tcp; then
170+
echo "Relaying guest DNS from $address:53 to loopback resolver $nameserver"
171+
return 0
172+
fi
173+
if ! kill -0 "$udp_pid" "$tcp_pid" 2>/dev/null; then
174+
break
175+
fi
176+
sleep 0.1
177+
done
178+
kill "$udp_pid" "$tcp_pid" 2>/dev/null || true
179+
echo "ERROR: guest DNS relay could not listen on $address:53 for loopback resolver $nameserver" >&2
180+
return 1
181+
}
182+
183+
LOOPBACK_NAMESERVER="$(loopback_nameserver /etc/resolv.conf)"
184+
RESOLVER_RELAY_ADDRESS=''
185+
if [ -n "$LOOPBACK_NAMESERVER" ]; then
186+
RESOLVER_RELAY_ADDRESS="$(runner_address)"
187+
if [ -z "$RESOLVER_RELAY_ADDRESS" ]; then
188+
echo "ERROR: runner has no non-loopback IPv4 address to relay loopback resolver $LOOPBACK_NAMESERVER" >&2
189+
exit 1
190+
fi
191+
fi
192+
193+
SANDBOX_RESOLV_CONF="$(encode_resolv_conf "$LOOPBACK_NAMESERVER" "$RESOLVER_RELAY_ADDRESS" < /etc/resolv.conf)"
35194
export SANDBOX_RESOLV_CONF
36195
if [[ "$RESOLV_FIELD_SEPARATOR$SANDBOX_RESOLV_CONF" != *"${RESOLV_FIELD_SEPARATOR}nameserver "[!\#]* ]]; then
37196
echo 'ERROR: runner /etc/resolv.conf has no nameserver' >&2
38197
exit 1
39198
fi
40199

200+
if [ -n "$LOOPBACK_NAMESERVER" ]; then
201+
start_resolver_relay "$LOOPBACK_NAMESERVER" "$RESOLVER_RELAY_ADDRESS"
202+
fi
203+
41204
if [ "${LAUNCHER_FILTER_VSOCK_ENOTCONN:-true}" = "true" ]; then
42205
# libkrun can emit this benign TSI/vsock teardown line after the guest has
43206
# already closed its side of the socket. It contains the word "error", so

‎tests/kvm_guest_dns.sh‎

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,31 +80,60 @@ printf '%s\n' "$SANDBOX_RESOLV_CONF" > "$TEST_RESOLVER_OUTPUT"
8080
STUB
8181
cat > "$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}"
8484
STUB
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.
90100
run_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.
106120
encoded_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
}
109138
cat > "$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
121150
RESOLV
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.
122153
run_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 ]]
126158
if LC_ALL=C grep -q '[^ -~]' "$TEST_DIR/forwarded"; then
127159
echo 'forwarded resolver contains bytes the kernel command line rejects' >&2; exit 1
128160
fi
129-
# The guest restores exactly the directives Docker supplied.
161+
# The guest restores the Docker directives with the relayed nameserver.
130162
rm -rf "$TEST_DIR/guest/run/codeapi-resolver"
131163
SANDBOX_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"
133165
cmp "$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.
137233
printf '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
138234
run_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.
142239
run_entrypoint /etc/resolv.conf

0 commit comments

Comments
 (0)