test: the network is checked by the suite now, and /proc/netif says what it did
The TCP/IP stack landed with its end-to-end test performed by hand: start a listener, boot, type nettest, read the output. That is evidence once and nothing afterwards, and it is exactly the kind of check that quietly stops being run. bootcheck -Net does the whole exchange. A listener runs on this host on a port picked by binding one and letting go, the guest is told to connect out to 10.0.2.2 at that port, and both ends are asserted: the guest reports what it received, the listener reports what arrived. One side alone proves much less than it looks -- a stack that completes a handshake and then drops every byte satisfies any check made from inside the guest. The listener is a background job, a separate process, because this harness has one hard rule: nothing may block the writer feeding QEMU's stdin. A socket accept is precisely the wait that deadlocked two earlier attempts at reading QEMU's output. It answers the protocol nettest speaks and then closes -- the close is load-bearing, since nettest reads until end of file and a listener that stayed open would hang the guest rather than fail it. The NIC is spelled out (-netdev user + -device e1000) rather than left to QEMU's default. Other runs can take the default and still answer "did a driver bind"; a run that asserts on an address should be reading its own configuration. /proc/netif is new, and the address is why. Until now the only report was a line on the console at boot, which userland cannot read and a test can only find by scraping the transcript. It gives interface, MAC, state, address, netmask, gateway, MTU and frame counts. Named netif rather than net on purpose: /proc/net is a *directory* on Linux, and a plain file there would give ENOTDIR to anything reaching for /proc/net/dev, which is worse than not having it. The counters are kept at the lwIP seam rather than in the driver, because a frame the driver received and the netif dropped never reached the stack. Verified: 38 [ok], no failures. Eleven of them are new -- lwIP up, DHCP obtaining 10.0.2.15 from SLIRP (a number nothing in the guest knows in advance, so it is the exchange completing rather than a guess), the four /proc/netif fields, frames counted in both directions, the handshake, the reply read back, and the listener confirming HELLO-FROM-SCHOS arrived. One bug found by writing it: $netPort was chosen after the command list was built, so the guest ran `nettest 10.0.2.2` with no port and printed its usage. An unset PowerShell variable interpolates to nothing rather than failing, which is worth remembering next time a command in this file looks right and behaves oddly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c0806d1b64
commit
f46fb41966
6 changed files with 268 additions and 1 deletions
|
|
@ -51,6 +51,15 @@ param(
|
|||
# it exists for -- one directory per process -- is exactly what nothing
|
||||
# else in this suite touches.
|
||||
[switch]$Ps,
|
||||
# TCP/IP. A listener is started on this host and the guest is told to
|
||||
# connect out to it, which is the only arrangement that checks both
|
||||
# directions from both ends: the guest reports what it received, and the
|
||||
# listener reports what arrived. A stack that completed a handshake and
|
||||
# then dropped every byte would pass any check made from one side alone.
|
||||
#
|
||||
# Under QEMU's user-mode networking the host is 10.0.2.2 seen from inside,
|
||||
# and SLIRP connects to the host's loopback -- so the listener binds there.
|
||||
[switch]$Net,
|
||||
[int]$BootSeconds = 20,
|
||||
[string]$Qemu
|
||||
)
|
||||
|
|
@ -230,6 +239,21 @@ if ($Usb) {
|
|||
if ($Sock) {
|
||||
$commands += 'socktest'
|
||||
}
|
||||
if ($Net) {
|
||||
# A free loopback port, found by binding one and letting go, so two runs on
|
||||
# one machine do not collide. It has to be chosen here rather than with the
|
||||
# rest of the network setup below: the command line that uses it is built
|
||||
# in this block, and an unset variable interpolates to nothing -- which is
|
||||
# how nettest first ran with no port at all and reported its usage.
|
||||
$l = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
|
||||
$l.Start(); $script:netPort = $l.LocalEndpoint.Port; $l.Stop()
|
||||
|
||||
# The interface first, so a failure below reads as "the address never
|
||||
# arrived" rather than "TCP is broken" -- two very different bugs that look
|
||||
# identical from a failed connect.
|
||||
$commands += 'cat /proc/netif'
|
||||
$commands += "nettest 10.0.2.2 $netPort"
|
||||
}
|
||||
if ($Ps) {
|
||||
$commands += 'ls /proc'
|
||||
$commands += 'cat /proc/1/stat'
|
||||
|
|
@ -297,7 +321,56 @@ $monPort = if ($Usb -or $Evdev) {
|
|||
|
||||
$monitor = if ($Usb -or $Evdev) { @("-monitor tcp:127.0.0.1:$monPort,server,nowait") } else { @() }
|
||||
|
||||
$psi.Arguments = ($drives + $usbDevices + $monitor + @(
|
||||
# The network run: an explicit NIC, and a listener on this host for the guest
|
||||
# to talk to.
|
||||
#
|
||||
# The NIC is spelled out rather than left to QEMU's default. Every other run
|
||||
# gets the default one and that is fine for "did a driver bind", but a check
|
||||
# that asserts on an address wants to be reading its own configuration rather
|
||||
# than whatever the machine type happens to supply this year.
|
||||
$netDevices = if ($Net) {
|
||||
@('-netdev user,id=net0', '-device e1000,netdev=net0')
|
||||
} else { @() }
|
||||
|
||||
# The listener runs in a background job -- a separate process -- rather than on
|
||||
# a thread here. This script's one hard rule is that nothing may block the
|
||||
# writer feeding QEMU's stdin, and a socket accept is exactly the kind of wait
|
||||
# that has deadlocked this harness before.
|
||||
#
|
||||
# It answers the protocol cmd/nettest speaks: read what the guest sends, send a
|
||||
# line back, close. The close matters -- nettest reads until end of file, so a
|
||||
# listener that stayed open would hang the guest rather than fail it.
|
||||
$netJob = if ($Net) {
|
||||
Start-Job -ArgumentList $netPort -ScriptBlock {
|
||||
param($port)
|
||||
$l = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $port)
|
||||
$l.Start()
|
||||
# Generous, because it has to cover the whole boot and every command
|
||||
# before nettest, under TCG. Nothing waits on this deadline: the job is
|
||||
# torn down by the caller either way.
|
||||
$deadline = (Get-Date).AddSeconds(900)
|
||||
while (-not $l.Pending()) {
|
||||
if ((Get-Date) -gt $deadline) { $l.Stop(); return '' }
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
$c = $l.AcceptTcpClient()
|
||||
$s = $c.GetStream()
|
||||
$s.ReadTimeout = 30000
|
||||
$buf = New-Object byte[] 512
|
||||
$got = ''
|
||||
try {
|
||||
$n = $s.Read($buf, 0, $buf.Length)
|
||||
if ($n -gt 0) { $got = [Text.Encoding]::ASCII.GetString($buf, 0, $n) }
|
||||
} catch { }
|
||||
$reply = [Text.Encoding]::ASCII.GetBytes("HELLO-FROM-HOST`n")
|
||||
try { $s.Write($reply, 0, $reply.Length); $s.Flush() } catch { }
|
||||
$c.Close()
|
||||
$l.Stop()
|
||||
return $got
|
||||
}
|
||||
} else { $null }
|
||||
|
||||
$psi.Arguments = ($drives + $usbDevices + $netDevices + $monitor + @(
|
||||
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
|
||||
'-display none'
|
||||
"-chardev stdio,id=c0,logfile=`"$log`""
|
||||
|
|
@ -493,6 +566,17 @@ try {
|
|||
Remove-Item -Force $pidfile -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# What the listener on this host received, if there was one. Collected after
|
||||
# QEMU is gone: the job either finished when the guest closed the connection or
|
||||
# is still sitting in accept, and the second case is a failure to report rather
|
||||
# than one to wait out.
|
||||
$netFromGuest = ''
|
||||
if ($Net -and $netJob) {
|
||||
Wait-Job $netJob -Timeout 20 | Out-Null
|
||||
$netFromGuest = (Receive-Job $netJob) -join ''
|
||||
Remove-Job $netJob -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if (-not (Test-Path $log)) { throw "no serial log produced at $log" }
|
||||
$text = Get-Content $log -Raw
|
||||
if ($null -eq $text) { $text = '' }
|
||||
|
|
@ -651,6 +735,33 @@ if ($Sock) {
|
|||
Expect 'socktest: ALL CHECKS PASSED' 'every socket check passed'
|
||||
}
|
||||
|
||||
if ($Net) {
|
||||
# The stack came up at all. Cheap, and it separates "lwIP never started"
|
||||
# from every other network failure below.
|
||||
Expect 'net: lwIP \d+\.\d+\.\d+ up' 'the TCP/IP stack started'
|
||||
# 10.0.2.15 is the address QEMU's user-mode networking hands out, so this
|
||||
# is DHCP genuinely completing -- discover, offer, request, ack -- and not
|
||||
# a hardcoded guess: nothing in the guest knows that number in advance.
|
||||
Expect 'net: 10\.0\.2\.15/' 'DHCP obtained an address from SLIRP'
|
||||
Expect '(?m)^state\s+up' '/proc/netif reports the interface up'
|
||||
Expect '(?m)^address\s+10\.0\.2\.15' '/proc/netif reports the address it was given'
|
||||
Expect '(?m)^gateway\s+10\.0\.2\.2' '/proc/netif reports the gateway'
|
||||
# Frames in both directions. DHCP alone guarantees these are non-zero, so a
|
||||
# zero here means the counters are wrong rather than the network.
|
||||
Expect '(?m)^rx_frames\s+[1-9]' 'frames arrived and reached the stack'
|
||||
Expect '(?m)^tx_frames\s+[1-9]' 'frames were transmitted'
|
||||
|
||||
# And now real traffic. Each of these is a different part of the exchange,
|
||||
# so a failure says which one: the handshake, the guest's receive path, and
|
||||
# the guest's send path as seen from outside.
|
||||
Expect 'nettest: connected' 'a TCP connection out of the guest completed'
|
||||
Expect 'nettest: OK' 'the guest read a reply back off that connection'
|
||||
Expect 'HELLO-FROM-HOST' 'and the reply is the host''s bytes'
|
||||
ExpectThat ($netFromGuest -match 'HELLO-FROM-SCHOS') `
|
||||
'the listener on this host received what the guest sent' `
|
||||
"the listener read '$($netFromGuest -replace '\s+', ' ')'"
|
||||
}
|
||||
|
||||
if ($Ps) {
|
||||
# /proc is synthetic, so a per-process directory is the thing to prove.
|
||||
# `ls /proc` prints in columns, so pid 1 is a token on a line rather than a
|
||||
|
|
|
|||
Loading…
Reference in a new issue