Networking: Dial/Listen, linear sockets, deterministic sim
A socket is the operating system's handle for one end of a network conversation. Programs read and write it much like a file, through a file descriptor. Most programs use one of two protocols. TCP gives a reliable, ordered stream of bytes over a connection: one side listens on a port (a 16-bit number that identifies a service on a machine, such as 443 for HTTPS), the other connects, and the listener accepts the connection. UDP sends independent datagrams, single packets that may be lost, duplicated or reordered, with no connection at all. A socket behaves like a file with two complications: there is a peer at the other end who can misbehave without your program's cooperation, and the data arrives in whatever pieces the network chooses. std::net treats sockets the way the filesystem chapter treats files. You need a capability before you can connect or listen, and every connection is a resource that cannot be leaked. This page covers the split between Dial and Listen, the byte-stream loop, datagrams, readiness and the reactor, and a simulator whose seeded short reads break naive readers on purpose.
Dial and Listen: two audit questions
The filesystem is one capability. The network is two: Dial for outbound connections ("may this code call out?") and Listen for inbound ones ("may this code accept connections and become a service?"). The split is not about protocols, since TCP and UDP both appear on both sides. It is about the threat model, and it matches what a reviewer asks when reading a package manifest. A DNS client library should never be able to bind a listening port. A request handler should never be able to open an outbound connection. Two words in [caps] allowed answer both questions without reading the code.
use std::net;
fn serve_one(listen: &Listen) -> Result[u64, (NetErr, OsErr)] !{io, block} {
let mut lst = listen.bind("svc")?;
let mut conn = match lst.try_accept() {
Ok(Some(c)) => c,
Ok(None) => { lst.close(); return Ok(0u64); }
Err(e) => { lst.close(); return Err(e); }
};
let mut buf = [0u8; 8];
let result = match conn.try_read(&mut buf) {
Ok(Some(n)) => Ok(widen[u64](n)),
Ok(None) => Ok(0u64),
Err(e) => Err(e),
};
let _c = conn.close();
lst.close();
result
}
test nothing_listens_yet (mut sim: Sim) {
match sim.dial.connect("svc-not-yet-bound") {
Ok(c) => { let _x = c.close(); assert false; }
Err((e, _)) => { assert e == NetErr::Refused; }
}
}TCP and UDP answer the same audit question ("talk on the network"), so they share the two capabilities and differ only in methods. The transport is chosen by the address string: "host:port" is TCP, an IPv6 peer is written "[::1]:53", and a unix: prefix selects a Unix-domain socket at the same connect and bind calls. A name that resolves to several addresses is tried in the resolver's order, and dial.resolve(host, &mut out) gives you the list itself.
Every fallible network operation answers Result[T, (NetErr, OsErr)]. Branch on the NetErr (Refused, Reset, Denied, WouldBlock, Invalid and so on) and print the OsErr, which carries the operating system's own error identity. "Nothing yet" is never an error: a try_ operation answers Ok(None) when there is nothing to do, and a bounded wait answers Ok(None) or Ok(false) when it expires.
There is a third network capability, and the reason it exists is itself an audit argument. sys.ping sends unprivileged ICMP echo requests: ping.send(addr) takes a bare IPv4 literal, never a name, and ping.recv(timeout_ms) answers Ok(Some(addr)) with the address that replied, or Ok(None) when the wait expires. It is separate from Dial because the ability to sweep a network does not follow from the ability to make outbound connections. Dial reaches hosts you know; Ping discovers hosts you do not. Both operations need a mutable root, fn main(mut sys: Sys).
Conn is a linear resource
Conn, Listener, UdpSock and Poller must each be consumed exactly once on every path, and forgetting one is the same compile error as an unclosed File. Here is a complete loopback round trip against the deterministic simulator used throughout this page:
use std::net;
test loopback_echo (mut sim: Sim) {
region r {
let msg = alloc in r [75u8, 79u8, 76u8, 65u8]; // "KOLA"
let mut lst = sim.listen.bind("svc").unwrap();
let mut client = sim.dial.connect("svc").unwrap();
let mut server = lst.try_accept().unwrap().unwrap();
let _s = net::send_all(&mut client, msg);
let buf: &r mut Vec[u8] = alloc in r mut Vec::new();
match net::recv_exact(&mut server, buf, 4usize, 10u64) {
Ok(ok) => { assert ok; assert buf == msg; }
Err(_) => { assert false; }
}
let _c1 = client.close();
let _c2 = server.close();
lst.close();
}
}When a connection is handed to a worker thread it moves; it is never borrowed across threads (Threads and channels). There is no shared memory for two threads to fight over a socket in.
try_accept never waits. With no connection pending it answers Ok(None), so a real server loops on it or registers the listener with a Poller. The test above can unwrap the first try_accept because the simulator delivers the connect before the accept. Do not copy that shortcut into code that runs against real sockets.
The byte-stream loop: send_all, recv_exact and end of stream
TCP delivers a byte stream, not a sequence of messages. One write on one side is not one read on the other, and a single write may accept fewer bytes than you offered. net::send_all(&mut c, bytes) loops until every byte is written. net::recv_exact(&mut c, out, want, timeout_ms) waits for readiness and reads until it has exactly want bytes; it answers Ok(true) when it has them, Ok(false) if the peer closed first, and Err with WouldBlock if one wait timed out. The third piece is the end of the stream: try_read answering Ok(Some(0)) means the peer is gone, and that is an ordinary value:
test peer_close_is_eof (mut sim: Sim) {
let mut lst = sim.listen.bind("svc").unwrap();
let mut client = sim.dial.connect("svc").unwrap();
let mut server = lst.try_accept().unwrap().unwrap();
let _c = client.close();
let mut buf = [0u8; 8];
match server.try_read(&mut buf) {
Ok(Some(n)) => { assert n == 0usize; }
Ok(None) => { assert false; }
Err(_) => { assert false; }
}
let _s = server.close();
lst.close();
}A peer's behaviour is untrusted input, so nothing a peer does (refusing, resetting, closing early, sending one byte at a time) ever traps your process. It is always a Result or a distinguishable end of stream. The language enforces this for your own code too: a parameter marked untrusted may not appear in a contract, because a req is a claim about the caller, wire bytes have no caller, and a trap on them would be a remote denial of service.
c.shutdown_write() closes only the sending direction: the peer reads your queued bytes and then end of stream, while you can still read its reply. You still owe the close.
Bounding the write side
send_all waits as long as the peer likes. A peer that stops reading fills its receive window and then your send buffer, and the loop then waits indefinitely. Writes are the place nobody expects to hang, so this is easy to miss in a client whose reads are all carefully bounded.
net::send_all_timeout(c, bytes, ms) bounds each wait for the socket to accept more data, the same contract recv_exact has on the read side. It is not a deadline for the whole call: a peer that keeps draining, however slowly, keeps it alive. Err with WouldBlock means one wait passed with nothing accepted. For a single write there is c.write_timeout(bytes, ms), which answers Ok(None) if nothing could be queued in time.
use std::net;
pub fn request(c: &mut Conn, msg: &[u8], out: &mut [u8], ms: u64) -> Result[u64, (NetErr, OsErr)] !{io, block} {
net::send_all_timeout(c, msg, ms)?;
match c.wait_readable(ms)? {
true => match c.try_read(out)? {
Some(n) => Ok(widen[u64](n)),
None => Err((NetErr::WouldBlock, OsErr::Other)),
},
false => Err((NetErr::WouldBlock, OsErr::Other)),
}
}
test bounded_request (mut sim: Sim) {
let mut lst = sim.listen.bind("svc").unwrap();
let mut client = sim.dial.connect("svc").unwrap();
let mut server = lst.try_accept().unwrap().unwrap();
let _s = net::send_all(&mut server, &b"pong").unwrap();
let mut buf = [0u8; 8];
match request(&mut client, &b"ping", &mut buf, 50u64) {
Ok(n) => { assert n > 0u64; }
Err(_) => { assert false; }
}
let _c1 = client.close();
let _c2 = server.close();
lst.close();
}Every bounded wait limits one wait, never a sequence of them. A loop of bounded reads can run forever against a peer that answers just inside each timeout. For a real deadline, take clock.now() before the loop and compare it on every iteration.
Bounded connecting
connect to an address nobody listens on fails at once with Refused: the other side actively said no. A host that is down or firewalled never answers at all, and a plain connect waits for the operating system's own timeout. connect_timeout(addr, ms) answers Result[Option[Conn], (NetErr, OsErr)], with Ok(None) when the time runs out. In the simulator an unbound address behaves differently for the two calls, which lets a test exercise the "nobody ever answers" case deliberately:
test connect_timeout_and_connect_differ (mut sim: Sim) {
match sim.dial.connect_timeout("dead-peer", 5u64) {
Ok(Some(c)) => { let _x = c.close(); assert false; }
Ok(None) => {}
Err((e, _)) => { assert e == NetErr::WouldBlock; }
}
match sim.dial.connect("dead-peer") {
Ok(c) => { let _x = c.close(); assert false; }
Err((e, _)) => { assert e == NetErr::Refused; }
}
}Neither call draws randomness, so the outcome is the same at every seed.
Unix-domain sockets
A Unix-domain socket is a socket between processes on the same machine, addressed by a path in the filesystem instead of a host and port. A unix: address such as "unix:/tmp/svc.sock" selects one at the same connect and bind calls, with no new capability and no new method. One difference matters: closing a Unix-domain listener removes its filesystem entry, because the kernel does not reclaim a socket path the way it reclaims a port. That removal happens under the Listen authority granted at bind, not through an Fs capability.
use std::net;
test uds_roundtrip (mut sim: Sim) {
region r {
let msg = alloc in r [79u8, 75u8]; // "OK"
let mut lst = sim.listen.bind("unix:/tmp/kola-wiki.sock").unwrap();
let mut client = sim.dial.connect("unix:/tmp/kola-wiki.sock").unwrap();
let mut server = lst.try_accept().unwrap().unwrap();
let _s = net::send_all(&mut client, msg);
let buf: &r mut Vec[u8] = alloc in r mut Vec::new();
match net::recv_exact(&mut server, buf, 2usize, 10u64) {
Ok(ok) => { assert ok; assert buf == msg; }
Err(_) => { assert false; }
}
let _c1 = client.close();
let _c2 = server.close();
lst.close();
}
}Readiness waits
A readiness wait asks the operating system to put the program to sleep until a socket can be read or written without blocking, or until a timeout passes. It is the kola form of the poll system call. Every readiness wait has one shape. wait_readable(timeout_ms) and wait_writable(timeout_ms) answer Result[bool, (NetErr, OsErr)]: Ok(true) when ready, Ok(false) when the wait expired. A closed or failed socket counts as ready, so the next read reports the end of stream or the error. timeout_ms is a u64 waited out in full, and the reserved literal NO_DEADLINE waits with no limit. c.peer_closed() asks whether the peer has gone without reading or waiting.
c.wait_ready(timeout_ms) gives both answers from one probe, as Ready::Data, Ready::Closed or Ready::Timeout. Prefer it on a path that runs for every request, since wait_readable followed by peer_closed costs two probes.
use std::net;
test wait_ready_three_ways (mut sim: Sim) {
let mut lst = sim.listen.bind("svc").unwrap();
let mut client = sim.dial.connect("svc").unwrap();
let mut server = lst.try_accept().unwrap().unwrap();
assert server.wait_ready(5) == Ok(Ready::Timeout);
let _w = net::send_all(&mut client, &b"hi");
assert server.wait_ready(5) == Ok(Ready::Data);
let mut buf = [0u8; 8];
let mut got = 0usize;
while got < 2 {
match server.try_read(&mut buf) { Ok(Some(n)) => { got = got + n; } _ => { assert false; } }
}
let _c = client.close();
assert server.wait_ready(5) == Ok(Ready::Closed);
assert client_gone(&server);
let _s = server.close();
lst.close();
}
fn client_gone(c: &Conn) -> bool !{io} {
c.peer_closed() == Ok(true)
}UDP: the same two capabilities
Datagrams get no capability of their own, because they answer the same audit questions. The split is applied inside the protocol instead. listen.bind_udp(addr) claims a port that unsolicited datagrams can reach, so it is Listen authority. dial.send_to(&sock, addr, bytes) is Dial authority, exercised on every send: the socket is an argument to a method on Dial, so code that sends cannot exist without a &Dial in scope. A socket that both receives and sends needs both budget lines. A pure client that only wants replies to its own queries uses dial.connect_udp(peer), which binds an ephemeral port connected to that peer so nothing unsolicited can arrive, and needs only Dial; it sends with dial.send(&sock, bytes).
use std::net;
test one_send_is_one_recv (mut sim: Sim) {
let mut a = sim.listen.bind_udp("peer-a").unwrap();
let mut b = sim.listen.bind_udp("peer-b").unwrap();
let n = sim.dial.send_to(&a, "peer-b", &b"HI!").unwrap();
assert n == 3usize;
let mut buf = [0u8; 8];
let (got, sender) = b.try_recv_from(&mut buf).unwrap().unwrap();
assert got == 3usize;
assert sender == "peer-a";
let rn = sim.dial.send_to(&b, sender, &buf[0..3]).unwrap();
assert rn == 3usize;
a.close();
b.close();
}A few differences from the stream side are deliberate. A zero-length datagram is Ok(Some((0, addr))), not end of stream, since UDP has none. try_recv_from returns the sender's address as a new string, which is allocated by the same rule as an f"..." literal (into the destination's region, else the innermost open region); a per-request region keeps a long-running server from accumulating one address per datagram. Broadcast is its own method, send_broadcast, rather than a socket flag, so broadcast traffic can be found with a search. join_multicast belongs to Listen, because joining a group widens the set of senders that can reach the socket.
Tuning and path MTU
Two pieces of background help here. First, the kernel keeps a receive queue for each UDP socket; datagrams that arrive while the program is busy wait there, and once it is full new ones are dropped. Second, every network path has a maximum transmission unit (MTU), the largest packet it carries, often 1500 bytes on Ethernet. A larger IP packet is either split into fragments, which is slow and fragile, or refused, and routers report the refusal back with an ICMP message (ICMP is the protocol IP uses for control and error messages). Path MTU discovery is the process of finding the largest size that gets through. Several UDP operations exist for servers and tunnels that care about these details:
listen.set_recv_buffer(&sock, bytes)sizes the kernel queue where unsolicited datagrams wait while your program is busy, and answers the size the kernel reports back. Linux reports twice the request (its bookkeeping) and caps it atnet.core.rmem_maxunless the process holdsCAP_NET_ADMIN; macOS reports the request. Compare the answer with what you asked for to detect a cap. It isListenauthority because it reserves kernel memory for inbound traffic.dial.send_to_pmtu(&sock, addr, bytes, mode)anddial.send_pmtu(&sock, bytes, mode)aresend_toandsendwith a per-send path-MTU discovery mode:Pmtu::Dontlets the datagram fragment,Pmtu::Dosets "don't fragment" and refuses anything larger than the route MTU, andPmtu::Probesets "don't fragment" while ignoring the learned MTU. A datagram that is too big answersErrwithNetErr::TooBig, and nothing is sent.dial.send_to_pmtu_gso(&sock, addr, data, segment, mode)sends a run of datagrams to one address in one call:datais cut everysegmentbytes (the last piece may be shorter), up to 64 datagrams and 65,507 bytes. On Linux it is a singlesendmsgwithUDP_SEGMENT, so a network card with UDP segmentation offload does the cutting; where the kernel refuses that, the datagrams go out one by one.Ok(n)counts the bytes of the datagrams accepted.dial.route_mtu(&sock, addr)asks the route MTU without sending anything, andsock.try_recv_error(&mut buf)drains one queued ICMP or local error as aPathError. Once the error queue is active,try_recv_fromand the path-MTU sends may answerNetErr::Pendingto tell you an error is waiting there.dial.connect_udp_iface(addr, ifname)isconnect_udppinned to one network interface.
The path-MTU operations are Linux-only: on other hosts and in the simulator they answer NetErr::Invalid. The own backend does not implement set_recv_buffer or send_to_pmtu_gso yet and refuses them by name, so a program that uses them builds through the C backend.
test udp_tuning_under_the_simulator (mut sim: Sim) {
let mut a = sim.listen.bind_udp("peer-a").unwrap();
// The simulator answers the request as the queue size.
assert sim.listen.set_recv_buffer(&a, 1048576) == Ok(1048576usize);
// Path-MTU operations need a real Linux socket.
match sim.dial.send_to_pmtu_gso(&a, "peer-b", &b"abcdef", 3, Pmtu::Do) {
Ok(_) => { assert false; }
Err((e, _)) => { assert e == NetErr::Invalid; }
}
a.close();
}Many sockets, one wait: Poller
A server with many connections cannot afford one blocked thread per connection, and checking each connection in turn does not scale past a handful. The usual answer is a reactor: register every socket with one object, make one wait call that returns when any of them is ready, then handle the ready ones (this is what epoll on Linux and kqueue on macOS provide). Poller::new() creates a linear readiness reactor. It has one registration method per kind of handle: add for a Conn, add_listener, add_udp, add_chan for a channel receiver, add_child for a child process, add_device for a TUN device and add_signal for a process signal. Each answers a slot id. wait(timeout_ms) answers how many registered handles are ready (Ok(0) on expiry), next_ready() hands out their ids one at a time, and remove(id) frees a slot for reuse. The poller only observes readiness; it never owns the handles registered with it, and it has no fixed limit on how many it can hold.
test one_wait_serves_many_sockets (mut sim: Sim) {
let mut lst = sim.listen.bind("svc").unwrap();
let mut c1 = sim.dial.connect("svc").unwrap();
let mut c2 = sim.dial.connect("svc").unwrap();
let mut s1 = lst.try_accept().unwrap().unwrap();
let mut s2 = lst.try_accept().unwrap().unwrap();
let mut poller = Poller::new();
let id1 = poller.add(&s1).unwrap();
let id2 = poller.add(&s2).unwrap();
// Nothing written yet: a bounded wait expires with nothing ready.
assert poller.wait(10u64).unwrap() == 0u64;
let _w = c1.write(&b"x");
assert poller.wait(10u64).unwrap() == 1u64;
match poller.next_ready() {
Some(id) => { assert id == id1; assert id != id2; }
None => { assert false; }
}
match poller.next_ready() { Some(_) => { assert false; } None => {} }
poller.close();
let _a = s1.close();
let _b = s2.close();
let _d = c1.close();
let _e = c2.close();
lst.close();
}
test poller_slots_are_reused (mut sim: Sim) {
let mut lst = sim.listen.bind("svc").unwrap();
let mut u = sim.listen.bind_udp("dgram").unwrap();
let mut poller = Poller::new();
let id1 = poller.add_listener(&lst).unwrap();
let id2 = poller.add_udp(&u).unwrap();
let _r = poller.remove(id1);
let id3 = poller.add_listener(&lst).unwrap(); // the freed slot is reused
assert id3 == id1 && id2 != id1;
poller.close();
lst.close();
u.close();
}poller.add_signal(&sys.proc, Signal::Term) turns SIGTERM, SIGINT or SIGHUP into an ordinary readiness event, with no callback (it uses signalfd on Linux and kqueue on macOS). Under the simulator, sim.signal(Signal::Term) delivers a registered signal deterministically.
Sharing a port between processes
A supervisor that binds a port once and hands it to its workers uses l.dup(), which creates a second owner of the same listening socket, together with SpawnSlot::Listen when spawning (Command-line programs covers spawning). listen.bind_reuseport(addr) is different: it makes the port shareable by any process that also asks for it, and the kernel splits connections among them with no error on either side. Use it only for several workers of one service. Plain bind reports AddrInUse and already handles restarting while old connections are in TIME_WAIT.
Narrowing Dial for a worker
A capability moves exactly once, so with one Dial a process could have a reactor that connects or a worker that connects, not both. Cloning Dial would solve that and weaken the guarantee. kola attenuates instead. dial.to_host(host, port) returns a new Dial that may reach exactly that endpoint, and the holder keeps the original. The restriction is checked at run time on every operation that takes an address (connect, connect_timeout, connect_udp, send_to, send_broadcast), and a refused call answers Err with NetErr::Denied, never a trap. It restricts by name, not by address, so reaching the same host by its IP address is refused too. Narrowing an already narrowed Dial to a different endpoint yields one that reaches nothing; narrowing never widens.
dial.split() covers the case that needs a second handle without narrowing. send_to is a Dial operation, so a thread that owns a bound UdpSock cannot reply on it without owning a Dial too. A split Dial reaches exactly what its parent reaches.
The host is given as bytes so that [caps] dial_hosts in the manifest can list the literal for kola audit. That budget covers to_host only: a pack holding a full Dial can still connect anywhere.
test a_narrowed_dial_reaches_one_endpoint_only (mut sim: Sim) {
let mut lst = sim.listen.bind("svc").unwrap();
let mut other = sim.listen.bind("other").unwrap();
let narrow = sim.dial.to_host(&b"svc", 0u16);
// The original Dial still reaches everything.
let c = sim.dial.connect("other").unwrap();
let _c = c.close();
match narrow.connect("other") {
Ok(c2) => { let _y = c2.close(); assert false; }
Err((e, _)) => { assert e == NetErr::Denied; }
}
lst.close();
other.close();
}Deterministic testing with SimNet
Under kola test, sim.dial and sim.listen run against a seeded in-memory network, SimNet. Its seeded fault is the short read: each read returns a random-length prefix of what is available, which reproduces on purpose the defining property of TCP. A reader written correctly, reassembling with recv_exact until it has the whole message, round-trips exactly however the bytes are cut:
use std::net;
test reassembles_across_seeded_short_reads (mut sim: Sim) {
region r {
let msg: &r mut Vec[u8] = alloc in r mut Vec::new();
for k in 0..40 {
msg.push(narrow[u8](widen[u64](k)).unwrap());
}
let mut lst = sim.listen.bind("svc").unwrap();
let mut client = sim.dial.connect("svc").unwrap();
let mut server = lst.try_accept().unwrap().unwrap();
let _s = net::send_all(&mut client, msg);
let got: &r mut Vec[u8] = alloc in r mut Vec::new();
match net::recv_exact(&mut server, got, 40usize, 50u64) {
Ok(ok) => { assert ok; assert got == msg; }
Err(_) => { assert false; }
}
let _c = client.close();
let _s2 = server.close();
lst.close();
}
}That test passes at every seed. Now write the reader naively, with one wait and one read into a fixed buffer, trusting that it holds the whole message. The file checks, and the test fails:
use std::net;
test naive_reader_can_tear (mut sim: Sim) {
region r {
let msg = alloc in r [107u8, 111u8, 108u8, 97u8];
let mut lst = sim.listen.bind("svc").unwrap();
let mut client = sim.dial.connect("svc").unwrap();
let mut server = lst.try_accept().unwrap().unwrap();
match net::send_all(&mut client, msg) { Ok(_) => {} Err(_) => { assert false; } }
let mut buf = [0u8; 4];
match server.wait_readable(10u64) {
Ok(true) => {
match server.try_read(&mut buf) {
Ok(Some(n)) => { assert n == 4usize; }
Ok(None) => { assert false; }
Err(_) => { assert false; }
}
}
Ok(false) => { assert false; }
Err(_) => { assert false; }
}
let _c = client.close();
let _s = server.close();
lst.close();
}
}$ kola test --interp --seed 7 naive.kl
naive.kl: test naive_reader_can_tear ... FAILED (seed 7)
error[card §8]: assertion failed
--> naive.kl:14:38
|
14 | Ok(Some(n)) => { assert n == 4usize; }
| ^^^^^^
0 passed, 1 failed (interpreted, JIT backend)With a 4-byte message there is little room for a short read to still look complete, so this fails at most seeds. kola test --seed N replays a particular failure exactly. A test that passes across seeds shows that the protocol handles a byte stream correctly, not that it got lucky once.
Real sockets
The same code runs on real sockets. Dial and Listen (TCP, UDP, Unix-domain sockets and the Poller) are implemented by both the interpreter and the C backend, so kola run, kola run --native and kola build all use the operating system's sockets, while kola test with a (mut sim: Sim) test uses SimNet. A test that declares (mut sys: Sys) instead runs live, with real sockets on both executors.
Simulation has limits. SimNet models fragmentation and connection state deterministically; it does not model kernel send buffers, real readiness timing or a hostile peer's full behaviour. When a claim depends on those, add a live test. And remember the try_accept difference from earlier: under the simulator a spawned thread does not run until its parent yields, so a server thread's first try_accept always finds the client waiting, while on a real system it may answer Ok(None). A seed sweep cannot find that; only the native run can.
Lower layers
Some programs need more than sockets. Each of these is its own capability with its own section in STD-CARD, and none is reachable through Dial or Listen:
Packet sockets on an owned TUN device (Linux). A TUN device is a virtual network interface whose packets go to a program instead of a cable; VPNs are built on them. With a granted TUN
Device,listen.bind_packet,bind_udp_packetandbind_transparent_packetbind sockets to that device's interface. APacketUdpreceives a datagram together with aPacketReply, which can answer only the captured peer from the captured source and interface.Raw IP (
sys.raw_ip, Linux).raw.socket()opens a linearRawIpSock, andsendtakes one complete IPv4 or IPv6 packet whose header picks the family and destination. It is separate fromDialbecause whoever writes the header can choose any source address.Netlink (
sys.netlink, Linux).netlink.socket(family)opens aNetlinkSocklimited to one granted family (link,neigh,addr,routeand others) for talking to the kernel's routing and neighbour tables. Netlink is the Linux interface programs such asipuse to configure the network. Underkola testa family runs against a simulated model and never reaches the host kernel.