net: recvfrom says who sent it, and a datagram is delivered whole
The second of the two things the stack owed. recvfrom accepted the address arguments and ignored them, so every caller was told nothing about where a datagram came from -- which for UDP is most of the point: a server that cannot see the sender cannot reply to it. Fixing it properly meant fixing something worse that was sitting underneath. UDP payloads went into the same byte ring TCP uses, with no record of where one datagram ended and the next began, so two datagrams that arrived together came back from one read as one. That is a bug whose appearance depends on timing and whose blame lands on the sender. Datagrams now carry a header alongside the bytes: source address, source port, length. Sixteen of them per socket, in a ring beside the existing one. Whole buffers would have been 16 sockets x 16 datagrams x an MTU of static memory for something almost always empty; only the boundaries need keeping, and those are eight bytes each. A datagram too big for the buffer it is read into is truncated and the remainder discarded, rather than left in the ring to be delivered as the head of the next one. One that arrives with the queue full is dropped whole -- that is UDP's own contract, and half a datagram in the ring would corrupt every one after. An empty queue and a zero-length datagram are kept apart, because a sender really can send zero bytes and the right answer to that is 0 rather than "nothing has arrived yet". sendto grew the mirror image: a destination address on a datagram socket, which is what a server replying to whoever just wrote to it needs. On TCP a destination is ignored rather than refused -- the connection decides where the bytes go, and programs pass one out of habit. For a stream, recvfrom's source is read from the pcb: it is the peer, and it is the same answer for every byte. Verified: bootcheck -Net now covers UDP too, 45 [ok] and no failures. The address check is real rather than cosmetic -- nettest deliberately leaves its sockaddr uninitialised, so a kernel that filled in nothing would print stack rubbish instead of 10.0.2.2 and the port the responder is actually on. The boundaries are checked by byte count: the responder sends 20 bytes and then 16, and the guest reports reading 20 and then 16. Treated as a stream the first read would have taken all 36. Left for later, and deliberately: accept() still does not report the peer's address, and getsockname/getpeername do not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3ad8114208
commit
a9ff1166b5
6 changed files with 433 additions and 24 deletions
|
|
@ -6,13 +6,21 @@
|
|||
// exercises ARP, IP, the TCP handshake, the send and receive paths, the window
|
||||
// and the close, in one exchange.
|
||||
//
|
||||
// nettest <a.b.c.d> <port>
|
||||
// nettest [-u] <a.b.c.d> <port>
|
||||
//
|
||||
// Under QEMU's user-mode networking the host is 10.0.2.2, so pointing this at a
|
||||
// listener there is the intended use:
|
||||
//
|
||||
// host$ printf 'HELLO-FROM-HOST\n' | nc -l -p 9000
|
||||
// guest# nettest 10.0.2.2 9000
|
||||
//
|
||||
// -u does the same exchange over UDP, and checks the one thing TCP cannot: that
|
||||
// recvfrom reports who sent the datagram. On a connected stream the source is
|
||||
// known before the first byte arrives, so a kernel that filled the address in
|
||||
// with anything at all would look correct there.
|
||||
//
|
||||
// host$ nc -u -l -p 9000
|
||||
// guest# nettest -u 10.0.2.2 9000
|
||||
|
||||
#include "types.h"
|
||||
#include "user.h"
|
||||
|
|
@ -80,11 +88,110 @@ parse_port(const char *s)
|
|||
return (*s == '\0' && v > 0 && v < 65536) ? v : -1;
|
||||
}
|
||||
|
||||
// The UDP half. One datagram out, one back, and the source of the reply
|
||||
// printed -- which is the part that has no equivalent over TCP.
|
||||
static void
|
||||
udp_exchange(uint ip, int port, const char *ipstr)
|
||||
{
|
||||
int fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd < 0) {
|
||||
puts_("nettest: udp socket failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
struct sockaddr_in to;
|
||||
for (int i = 0; i < (int)sizeof(to); i++)
|
||||
((char *)&to)[i] = 0;
|
||||
to.sin_family = AF_INET;
|
||||
to.sin_port = hton16((ushort)port);
|
||||
to.sin_addr = ip;
|
||||
|
||||
const char *msg = "HELLO-UDP-FROM-SCHOS\n";
|
||||
int len = 0;
|
||||
while (msg[len]) len++;
|
||||
|
||||
puts_("nettest: udp sending to "); puts_(ipstr);
|
||||
puts_(":"); putn_(port); puts_("\n");
|
||||
|
||||
if (sendto(fd, msg, len, 0, (const struct sockaddr *)&to, sizeof(to)) != len) {
|
||||
puts_("nettest: udp send failed\n");
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Deliberately not zeroed first. If the kernel does not fill this in, the
|
||||
// address printed is whatever was on the stack, and a test that asserts on
|
||||
// 10.0.2.2 catches that -- where a zeroed struct would quietly read as
|
||||
// 0.0.0.0 and look like a mere formatting problem.
|
||||
struct sockaddr_in from;
|
||||
int fromlen = sizeof(from);
|
||||
char buf[512];
|
||||
|
||||
int n = recvfrom(fd, buf, sizeof(buf) - 1, 0,
|
||||
(struct sockaddr *)&from, &fromlen);
|
||||
if (n <= 0) {
|
||||
puts_("nettest: udp FAILED, nothing came back\n");
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
buf[n] = '\0';
|
||||
|
||||
puts_("nettest: udp received "); putn_(n); puts_(" bytes: ");
|
||||
puts_(buf);
|
||||
if (buf[0] && buf[n - 1] != '\n')
|
||||
puts_("\n");
|
||||
|
||||
if (fromlen < (int)sizeof(from)) {
|
||||
puts_("nettest: udp FAILED, no source address returned\n");
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Network order, most significant byte first, which is how parse_ip built
|
||||
// it -- so the same shifts unpack it.
|
||||
uint a = from.sin_addr;
|
||||
puts_("nettest: udp source ");
|
||||
putn_((int)(a & 0xFF)); puts_(".");
|
||||
putn_((int)((a >> 8) & 0xFF)); puts_(".");
|
||||
putn_((int)((a >> 16) & 0xFF));puts_(".");
|
||||
putn_((int)((a >> 24) & 0xFF));
|
||||
puts_(":");
|
||||
putn_((int)hton16(from.sin_port));
|
||||
puts_("\n");
|
||||
|
||||
// A second datagram, read separately, because message boundaries are the
|
||||
// whole difference between UDP and TCP. The responder sends two; if the
|
||||
// kernel kept them as a byte stream the first read would have swallowed
|
||||
// both, and the byte counts printed above and below are what says which
|
||||
// happened.
|
||||
int m = recvfrom(fd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&from,
|
||||
&fromlen);
|
||||
close(fd);
|
||||
if (m <= 0) {
|
||||
puts_("nettest: udp FAILED, no second datagram\n");
|
||||
return;
|
||||
}
|
||||
buf[m] = '\0';
|
||||
puts_("nettest: udp second "); putn_(m); puts_(" bytes: ");
|
||||
puts_(buf);
|
||||
if (buf[0] && buf[m - 1] != '\n')
|
||||
puts_("\n");
|
||||
|
||||
puts_("nettest: udp OK\n");
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
int udp = 0;
|
||||
|
||||
if (argc == 4 && argv[1][0] == '-' && argv[1][1] == 'u' && argv[1][2] == '\0') {
|
||||
udp = 1;
|
||||
argv++;
|
||||
argc--;
|
||||
}
|
||||
if (argc != 3) {
|
||||
puts_("usage: nettest <a.b.c.d> <port>\n");
|
||||
puts_("usage: nettest [-u] <a.b.c.d> <port>\n");
|
||||
exit();
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +202,11 @@ main(int argc, char *argv[])
|
|||
exit();
|
||||
}
|
||||
|
||||
if (udp) {
|
||||
udp_exchange(ip, port, argv[1]);
|
||||
exit();
|
||||
}
|
||||
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
puts_("nettest: socket failed\n");
|
||||
|
|
|
|||
Loading…
Reference in a new issue