Fix reverse DNS slave: strip trailing newline from input

The GANL adapter sends IP addresses to the slave with a trailing '\n'
delimiter (stream framing), but slave.cpp passed the raw buffer —
including the newline — to getaddrinfo(), which rejects it. This caused
reverse DNS to silently fail, falling back to numeric IPs.

Latent since b7fec8bc8 (2012) when inet_addr() was replaced by
getaddrinfo(); activated by a6f579c30 (2026-03-03) when GANL replaced
the old SOCK_DGRAM transport with stream pipes.

Adds testcases/tools/SlaveDNS with 11 protocol-level tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stephen Dennis 2026-03-18 19:35:00 -06:00
parent 7b9e415896
commit 982b82fbc8
2 changed files with 171 additions and 0 deletions

View file

@ -207,6 +207,19 @@ int main(int argc, char *argv[])
}
arg[len] = '\0';
// Strip trailing whitespace (the GANL adapter sends a newline
// delimiter after the IP address).
//
while (len > 0 && (arg[len-1] == '\n' || arg[len-1] == '\r' || arg[len-1] == ' '))
{
arg[--len] = '\0';
}
if (len == 0)
{
continue;
}
child = fork();
switch (child)
{

158
testcases/tools/SlaveDNS Executable file
View file

@ -0,0 +1,158 @@
#!/bin/sh
#
# SlaveDNS - Verify the reverse DNS slave binary works correctly.
#
# Tests the slave binary's protocol: write an IP address, read back
# "<ip> <hostname>\n". Uses both with-newline and without-newline
# inputs to match the GANL adapter's actual wire format.
#
# Exit 0 on success, 1 on failure.
#
BIN=../mux/game/bin
SLAVE=$BIN/slave
PASS=0
FAIL=0
TOTAL=0
passed() {
PASS=$((PASS + 1))
TOTAL=$((TOTAL + 1))
echo " PASS: $1"
}
failed() {
FAIL=$((FAIL + 1))
TOTAL=$((TOTAL + 1))
echo " FAIL: $1"
}
echo "=== Reverse DNS Slave Tests ==="
echo ""
# ------------------------------------------------------------------
# Test 0: Binary exists and is executable
# ------------------------------------------------------------------
if [ -x "$SLAVE" ]; then
passed "slave binary exists and is executable"
else
failed "slave binary missing or not executable ($SLAVE)"
echo ""
echo "Result: FAIL (slave binary not found)"
exit 1
fi
# ------------------------------------------------------------------
# Test 1: Loopback without trailing newline (bare IP)
# ------------------------------------------------------------------
RESULT=$(printf "127.0.0.1" | timeout 10 "$SLAVE" 2>/dev/null)
if echo "$RESULT" | grep -q "^127\.0\.0\.1 "; then
passed "127.0.0.1 (no newline) resolves (got: $(echo "$RESULT" | tr -d '\n'))"
else
failed "127.0.0.1 (no newline) unexpected output (got: '$RESULT')"
fi
# ------------------------------------------------------------------
# Test 2: Loopback WITH trailing newline (GANL wire format)
#
# The GANL adapter appends '\n' after the IP. The slave must strip
# the newline before passing it to getaddrinfo().
# ------------------------------------------------------------------
RESULT_NL=$(printf "127.0.0.1\n" | timeout 10 "$SLAVE" 2>/dev/null)
if echo "$RESULT_NL" | grep -q "^127\.0\.0\.1 "; then
passed "127.0.0.1 (with newline) resolves (got: $(echo "$RESULT_NL" | tr -d '\n'))"
else
failed "127.0.0.1 (with newline) unexpected output (got: '$RESULT_NL')"
fi
# ------------------------------------------------------------------
# Test 3: Response format — at least two space-separated fields
# ------------------------------------------------------------------
NFIELDS=$(echo "$RESULT" | head -1 | awk '{print NF}')
if [ "$NFIELDS" -ge 2 ] 2>/dev/null; then
passed "response has >= 2 fields (ip + hostname)"
else
failed "response field count unexpected (got $NFIELDS fields)"
fi
# ------------------------------------------------------------------
# Test 4: IP field in response matches input
# ------------------------------------------------------------------
RESP_IP=$(echo "$RESULT" | head -1 | awk '{print $1}')
if [ "$RESP_IP" = "127.0.0.1" ]; then
passed "response IP matches input (127.0.0.1)"
else
failed "response IP mismatch (expected 127.0.0.1, got '$RESP_IP')"
fi
# ------------------------------------------------------------------
# Test 5: With-newline and without-newline produce the same result
# ------------------------------------------------------------------
CLEAN1=$(echo "$RESULT" | head -1 | tr -s ' ')
CLEAN2=$(echo "$RESULT_NL" | head -1 | tr -s ' ')
if [ "$CLEAN1" = "$CLEAN2" ]; then
passed "newline/no-newline produce identical output"
else
failed "newline/no-newline differ ('$CLEAN1' vs '$CLEAN2')"
fi
# ------------------------------------------------------------------
# Test 6: RFC 5737 documentation address (192.0.2.1) — no PTR
# ------------------------------------------------------------------
RESULT2=$(printf "192.0.2.1\n" | timeout 10 "$SLAVE" 2>/dev/null)
RESP2_IP=$(echo "$RESULT2" | head -1 | awk '{print $1}')
RESP2_HOST=$(echo "$RESULT2" | head -1 | awk '{print $2}')
if [ "$RESP2_IP" = "192.0.2.1" ]; then
passed "192.0.2.1 echoes back IP field correctly"
else
failed "192.0.2.1 IP field mismatch (got '$RESP2_IP')"
fi
if [ -n "$RESP2_HOST" ]; then
passed "192.0.2.1 returned a hostname field ('$RESP2_HOST')"
else
failed "192.0.2.1 returned empty hostname"
fi
# ------------------------------------------------------------------
# Test 7: Multiple lookups in a single session
# ------------------------------------------------------------------
RESULT3=$(printf "127.0.0.1\n192.0.2.1\n" | timeout 15 "$SLAVE" 2>/dev/null)
NLINES=$(echo "$RESULT3" | grep -c '.')
if [ "$NLINES" -ge 2 ] 2>/dev/null; then
passed "multiple lookups returned $NLINES results"
else
failed "multiple lookups returned $NLINES results (expected >= 2)"
fi
# ------------------------------------------------------------------
# Test 8: Slave exits cleanly when stdin closes (EOF)
# ------------------------------------------------------------------
printf "" | timeout 5 "$SLAVE" >/dev/null 2>&1
if [ $? -eq 0 ]; then
passed "slave exits cleanly on EOF"
else
failed "slave did not exit cleanly on EOF (exit code $?)"
fi
# ------------------------------------------------------------------
# Test 9: Empty/whitespace-only input is ignored (no crash)
# ------------------------------------------------------------------
RESULT4=$(printf "\n\n \n" | timeout 5 "$SLAVE" 2>/dev/null)
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
passed "whitespace-only input handled gracefully"
else
failed "whitespace-only input caused exit code $EXIT_CODE"
fi
# ------------------------------------------------------------------
# Summary
# ------------------------------------------------------------------
echo ""
echo "=== DNS Slave: $PASS/$TOTAL passed, $FAIL failed ==="
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0