Fenrir fixes 2026-06-04#127
Merged
Merged
Conversation
…names
The server copied the wire-supplied RRQ/WRQ filename verbatim and handed it
straight to io.open() with no sanitization. Because TFTP is unauthenticated,
an off-path sender could request "../../etc/passwd" (read) or "/etc/cron.d/evil"
(write) and the raw traversal path reached an integrator-supplied,
possibly-filesystem-backed open() unchanged.
Add a component-aware wolftftp_filename_is_safe() in wolftftp_parse_request():
reject absolute paths (leading '/' or '\') and any ".." path component, while
still allowing relative subdirectories and dots embedded in a name ("fw..bin").
A rejected request fails parsing as WOLFTFTP_ERR_PACKET, so the server replies
with an error and never reaches io.open(). Document the (now-enforced) contract
on the open callback in the header.
tcp_rebuild_rx_sack() derived SACK blocks from the out-of-order cache and emitted them in descending-sequence order, so the first block was whichever island had the highest sequence rather than the segment that triggered the ACK. RFC 2018 sec.4 (2) requires the first SACK block to contain the segment that triggered the ACK (unless that segment advanced the cumulative ACK). With descending order the freshest island can be reported last and is the first to be dropped when the TCP option header lacks room for every block (e.g. four islands alongside a timestamp option, leaving room for only three blocks), delaying the peer's loss recovery. Pass the triggering segment's range into tcp_rebuild_rx_sack() from the store-OOO path and move the merged block containing it to rx_sack[0], keeping the remaining islands in descending order behind it. The consume/promote path advances the cumulative ACK, which is the RFC exemption, so it passes trig_len == 0 and keeps plain descending order.
nslookup() and wolfIP_dns_ptr_lookup() overwrote the shared dns_lookup_cb, dns_ptr_cb and dns_query_type fields before calling dns_send_query(), whose "dns_id != 0" busy-guard only fires afterwards. A second DNS API call made while a query was in flight was rejected with -16, but by then it had already replaced the in-flight query's callback/type state. When the original query's response arrived (its id still matched dns_id, since the second call never changed it) dns_callback() invoked the wrong handler with that response's rdata (A->A handler hijack), or the PTR path NULLed the A callback and flipped the query type, stalling the resolver until the retry timer gave up. Move the busy-guard ahead of any state mutation in both entry points: when dns_id != 0 return -16 before touching the shared fields. The guard inside dns_send_query() is retained as defence in depth.
raw_try_recv() delivered every protocol-matching IPv4 packet to a bound raw socket without consulting r->bound_local_ip or r->if_idx (the arriving interface was explicitly discarded with (void)if_idx). wolfIP_sock_bind() accepts a local IP and interface for raw sockets and records them, so a socket bound to one local address still captured traffic for every other destination, and a socket bound to one interface still saw traffic from all of them. With forwarding enabled raw_try_recv() runs on the IP input path, so a bound socket even saw transit traffic for unrelated destinations. The TCP/UDP receive paths already gate on bound_local_ip; raw sockets were the outlier. Add the same two guards inside the socket-match loop: skip when bound_local_ip is set and differs from the packet destination, and skip when if_idx is set and differs from the arriving interface (IPADDR_ANY / 0 keep "match any"). Unbound catch-all sockets are unaffected.
http_recv_cb's fail_close path freed hc->ssl, called wolfIP_sock_close(sd) and zeroed hc->client_sd, but never deregistered the socket callback. For an ESTABLISHED connection wolfIP_sock_close only begins the active close: it moves the socket to FIN_WAIT_1, returns -EAGAIN, and leaves ts->callback / ts->callback_arg pointing at this http_client slot. With client_sd zeroed the slot looks free, so the next accept reuses it for a new connection (fresh sd, fresh ssl) and re-registers the callback with the same &clients[i] arg. The half-closed socket still accepts data in FIN_WAIT_1, so a late segment on it makes wolfIP_poll fire the stale callback against the new connection's state - freeing its live TLS context and zeroing its client_sd, a use-after-free / repeatable denial of service against concurrent HTTPS clients. Deregister the callback on the lingering socket (wolfIP_register_callback with NULL cb/arg) before zeroing client_sd, so a segment arriving on the half-closed socket can no longer dispatch into a reused slot.
The WOLFIP_PACKET_SOCKETS TX loop in wolfIP_poll walked the queue with fifo_next() when the SENDING filter blocked a frame, but fifo_pop() only ever removes the tail (oldest) descriptor. With [A,B] queued and A blocked, the loop advanced to B, sent it, then popped A; the next fifo_peek() returned B again, so B was transmitted a second time and the SENDING filter re-fired for it. A was silently dropped and B sent twice. fifo has no way to remove an arbitrary descriptor, so the fifo_next()/fifo_pop() mix cannot be made correct. Drop the filter-blocked frame from the head with fifo_pop() and re-peek, matching firewall-drop semantics and sending each accepted frame exactly once. The sibling TX loops (TCP/UDP/ICMP/raw) already break on a filter block and were not touched.
wolfIP_recv_on() rebased if_idx from the physical parent to the matched VLAN sub-interface and stripped the tag in place before calling packet_try_recv(). Since packet_try_recv() matches sockets by if_idx, an AF_PACKET socket bound to the parent never matched and received zero tagged frames - blinding a parent-bound sniffer or IDS to all VLAN traffic - while wildcard listeners only saw the tag-stripped frame stamped with the sub-interface index, losing the VLAN membership information. Tap the physical interface with the original, still-tagged frame (parent if_idx) before the demux/strip, so parent-bound and wildcard sockets observe VLAN traffic in wire form. The existing post-demux delivery now runs in "exact interface" mode (new match_wildcard arg to packet_try_recv) so sockets bound explicitly to the sub-interface still get the tag-stripped copy while wildcard sockets are not delivered the same frame a second time. Non-VLAN and non-packet-socket paths are unchanged.
The strict-RPF loop in ip_recv's forwarding block skips the ingress interface (i == if_idx), so a source address equal to the router's own IP on that interface was never checked. An L2-adjacent attacker could send a packet with src set to the router's own LAN address and have it forwarded out another interface with the source unchanged, impersonating the router to downstream hosts. (Other interfaces' own IPs were already caught by the RPF subnet match, but the ingress interface's own /32 slipped through.) Add an explicit anti-spoof check before the strict-RPF loop: drop the packet if its source equals any configured interface address. The router originates such packets locally and never legitimately receives them from the wire. The check matches only the exact own /32, so legitimate hosts in the ingress subnet are still forwarded. Two existing forwarding tests used the router's own IP as a convenience source; they are updated to a real in-subnet host so they keep exercising the TTL- exceeded and ARP-queue forward paths.
…andled ip_recv dispatched proto=50 (ESP) to esp_transport_unwrap before the IP option-strip block. esp_transport_unwrap reads the ESP header (SPI, sequence, ICV, payload) at a fixed 20-byte-IP-header offset (ip->data) and derives esp_len from a fixed IP_HEADER_LEN. When the outer IPv4 packet carried options (IHL>5, e.g. Router-Alert), ip->data pointed into the options region: the SPI was read from option bytes, the SA lookup failed, and every such ESP packet was silently dropped regardless of SA registration. Move the ESP unwrap into the dispatch block, after the existing option-strip that normalizes the header to IHL=5. esp_transport_unwrap now always receives a 20-byte IP header, so the ESP header is at the offset it expects. Non-ESP and non-option paths are unchanged (the strip and protocol dispatch are identical; only the ESP step moved down).
…te policy ip_recv delivers to raw_try_recv before the RFC 7126 LSRR/SSRR source-route drop, so raw sockets observe source-routed packets the stack later refuses to forward or deliver. This was flagged as a possible policy-ordering issue, but it is the intended and correct behaviour: a raw socket is a passive ingress tap whose purpose is to see wire traffic - including hostile packets the stack itself rejects - which is exactly what monitoring/IDS use cases need. Moving the scan ahead of the tap would blind those listeners to source-routed attacks. raw_try_recv only copies the frame to the socket queue; it never parses options or honours a source route, so tapping early cannot cause the stack to act on one. Add a comment recording this intent (the alternative the report offered) rather than changing behaviour. No functional change.
…L2 trust wolfIP_vlan_delete wiped ll_dev[if_idx] and ipconf[if_idx] but left the ARP neighbor cache, the in-flight ARP request tracker, the queued-packet array and last_arp[if_idx] untouched. ARP neighbor entries are keyed only by (ip, if_idx) - there is no VID, generation counter, or liveness tie to the interface - and wolfIP_vlan_create immediately reuses the freed slot. So a new VLAN on the same if_idx silently inherited the deleted VLAN's IP->MAC mappings for up to ARP_AGING_TIMEOUT_MS, breaking per-VLAN L2 isolation without any ARP exchange on the new VID. After wiping the slot, evict every arp.neighbors[] and arp.pending[] / arp_pending[] entry whose if_idx matches the deleted slot and clear last_arp[if_idx], so the reused slot starts from a clean ARP state. Note: the report's dedupe group mentions a sibling multicast-membership variant (f_vlan_stale_mcast_1); that is a separate finding and is not touched here.
dhcp_xid was assigned once in dhcp_client_init and reused verbatim for every T1/T2 renewal DHCPREQUEST. Both the xid and the server-id are observable from the initial broadcast DORA exchange, and dhcp_parse_ack accepts a DHCPACK on matching xid + server-id while overwriting the gateway unconditionally. Since the RENEWING DHCPREQUEST is unicast to the server, a LAN-adjacent attacker who captured the initial DORA could blindly forge a renewal DHCPACK (same xid, real server-id, attacker-controlled router option) at every renewal without ever seeing the renewal request, redirecting routed traffic through their host. Generate a fresh wolfIP_getrandom() xid at the BOUND->RENEWING transition, so each renewal is a new transaction with an unpredictable id (RFC 2131). Retransmissions within the cycle and the RENEWING->REBINDING continuation keep the xid, as required. An attacker who only saw the initial DORA can no longer predict the renewal xid, so a replayed-xid ACK is rejected.
dhcp_parse_ack initialized lease_s to 0 and transitioned to DHCP_BOUND without checking that option 51 (IP Address Lease Time) was present. RFC 2131 makes this option mandatory in a DHCPACK. With it absent, dhcp_schedule_lease_timer no-ops on lease_s == 0, so the client binds with no renewal/rebind/expiry timer and treats a dynamic lease as permanent - it never renews and never notices the server reclaiming the address. Gate the BOUND transition on lease_s != 0. lease_s is only ever assigned by the LEASE_TIME option (and a <4-byte option already returns -1 earlier), so the check accepts only an ACK carrying a present, nonzero lease time and rejects both the missing and zero-duration cases; a rejected ACK is ignored and the client keeps waiting/retransmitting. Several existing tests built DHCPACK fixtures without a lease-time option and relied on the prior (non-compliant) acceptance; they are updated to include the mandatory option so they keep exercising their actual intent (offer/ack flow, renewing, rebinding, unknown-option skipping).
dhcp_bound(), dhcp_client_is_running() and dhcp_client_init() are public (in wolfip.h) but dereferenced the stack pointer without validating it, so a NULL argument crashed. Other public entry points in the stack already guard against NULL; these three were inconsistent. Return a deterministic value on NULL, matching the existing convention: the two predicates report 0 (not bound / not running) and dhcp_client_init() returns -WOLFIP_EINVAL.
…sockets icmp_input dispatched ICMP_ECHO_REPLY straight to icmp_try_recv without the wolfIP_if_for_local_ip() guard the sibling ECHO_REQUEST path applies. With forwarding disabled there is no general "is this dst ours" gate before local dispatch, and icmp_try_recv skips the per-socket destination check when the socket's local_ip is 0 (reachable during/after DHCP) and ignores the ingress interface. An L2-adjacent attacker could thus address an echo reply to our MAC with an arbitrary ip.dst and a guessed 16-bit echo id and have it delivered to an application ICMP socket (fake ping-reply / covert-channel injection). Mirror the ECHO_REQUEST guard onto the reply branch: drop broadcast/multicast destinations and any reply whose ip.dst is not one of our configured local IPs. This bounds delivery (including to wildcard local_ip==0 sockets) to replies actually addressed to us, matching the RFC 1122 s3.2.2.6 policy already applied to requests, TCP and UDP. An existing test bound an ICMP socket to 10.0.0.1 without configuring that IP on an interface; it now also configures the interface so it keeps exercising reply delivery under the destination check.
The UDP and ICMP arms of wolfIP_sock_bind wrote ts->src_port with the requested port/echo-id before calling the WOLFIP_FILT_BINDING filter callback, then rolled it back if the callback rejected the bind. The TCP arm already defers the assignment to the success path. wolfIP_filter_dispatch holds wolfip_filter_lock across the callback and makes any re-entrant filter dispatch return 0 (allow), so a callback that re-enters the stack via wolfIP_poll would have an ingress UDP/ICMP datagram matching local_ip:src_port delivered to the socket receive FIFO without WOLFIP_FILT_RECEIVING inspection - even when the bind is ultimately rejected. Move the src_port assignment after wolfIP_filter_notify_socket_event returns 0 in both arms, matching the TCP arm. The socket is no longer matchable by udp_try_recv / icmp_try_recv while the bind is being vetted. (Exploitation needs CONFIG_IPFILTER and a re-entrant filter callback; no in-tree callback does this, but it closes the ordering gap for such integrations.)
wolfIP_sock_recvfrom validated sockfd and s but never buf, then copied queued data into it for every socket family (TCP via queue_pop, UDP/ICMP/raw/packet via memcpy). A caller passing buf == NULL with a nonzero length, once a datagram had queued, dereferenced NULL and crashed. Reject a NULL buffer with nonzero length up front (before any family branch), returning -WOLFIP_EINVAL and consuming nothing. len == 0 (a no-op data-availability probe) remains allowed. This guards TCP, UDP, ICMP, raw and packet sockets uniformly, matching the existing sockfd/s validation.
The TCP arm of wolfIP_sock_connect set ts->sock.tcp.state = TCP_SYN_SENT and ts->remote_ip before validating that bound_local_ip still resolves to an interface. When the bound address had been removed/changed (via wolfIP_ipconfig_set_ex), the bound_match check returned -WOLFIP_EINVAL with the socket left in TCP_SYN_SENT - but no SYN was queued and no RTO timer started. Every later connect then hit the "state == TCP_SYN_SENT -> return EAGAIN" early path, wedging the socket permanently. The other connect error paths (filter reject, tcp_send_syn failure) already roll state back to CLOSED; only the binding-validation path did not. Resolve and validate the binding into locals first and only then commit state/remote_ip/if_idx/local_ip, mirroring the UDP arm. A failed validation now returns EINVAL with the socket still CLOSED, so a retry re-validates instead of returning a phantom in-flight SYN. The existing test only checked the return code; it now also asserts the socket stays CLOSED and that a second connect re-validates (EINVAL, not stuck EAGAIN).
igmp_input accepted any IGMP membership query that passed the length, checksum, type and group-is-multicast checks, ignoring ip->ttl and ip->dst. ip_recv's source filter only drops broadcast/multicast/zero sources, so an off-link or unicast-addressed query (e.g. TTL=64, dst=our unicast IP) reached igmp_input and solicited an IGMPv3 membership report per joined group - disclosing the host's multicast membership table and violating RFC 3376 §4.1. Add the RFC 3376 §4.1 query guards: drop the query unless ip->ttl == 1 (IGMP is link-local and never transits a router) and its destination is either all-hosts (224.0.0.1) for a general query or the queried group for a group-specific query. Both fields are already reachable via ip; no new parsing or helper is needed, and legitimate on-link queries (TTL 1, dst 224.0.0.1/group) are unaffected. Note: the report also suggested an on-link source-address check; that needs a new subnet helper and is largely redundant with the TTL==1 guard (a TTL==1 query cannot have transited a router), so it is not added here.
iphdr_set_checksum summed the whole header including whatever was already in ip->csum, so it produced a correct value only if the caller had zeroed the field first. All current callers do (explicitly or via memset), so this was not a live bug, but the implicit precondition is a footgun: a future caller that forgets to clear csum would emit a silently-wrong checksum. Zero ip->csum inside the setter before summing (RFC 1071). The setter is now correct-by-construction and idempotent (set/verify holds for any input, set;set;verify holds); existing callers that already clear the field are unaffected (the extra clear is a no-op).
… each tcp_store_ooo_segment treated only an exact (seq,len) match as a duplicate, so segments with distinct (seq,len) pairs that cover overlapping byte ranges each took a separate slot. With only TCP_OOO_MAX_SEGS=4 slots, an in-window injector (or a peer re-segmenting retransmissions) could fill the cache with four overlapping segments, after which every further legitimate out-of-order segment was silently dropped, degrading goodput until the in-order hole was refilled. Replace the exact-match dedup with an overlap check that coalesces the incoming segment into the overlapping slot: store the union of the two ranges in place (incoming bytes win in the overlap) when it fits a single slot. This bounds how many slots an overlapping cluster can occupy. A simple replace-on-overlap would have lost the non-overlapping tail of the cached segment (breaking the existing coalesce-on-consume behaviour); the union merge preserves it. When the union would exceed one slot the segments are kept separate, as before, and tcp_consume_ooo coalesces them on promotion.
The TS branch in tcp_parse_options accepted any option with olen >= TCP_OPTION_TS_LEN, so a SYN carrying kind 8 with length 11-40 still set ts_found, negotiated ts_enabled, and recorded TSval/TSEcr. RFC 7323 fixes the Timestamp option at length 10, and every other fixed-length option here (WS, MSS, SACK-permitted) already enforces an exact olen; only TS used >=. There is no memory-safety issue (the 8 read bytes are within the already-validated olen bounds), but accepting a non-canonical length violates the encode/decode roundtrip property. Require olen == TCP_OPTION_TS_LEN. When the length is wrong the branch is now skipped and opt += olen (already bounds-checked) advances parsing correctly, so the remaining options are unaffected. Test test_tcp_parse_options_timestamp_overlong_ignored injects a SYN with an olen=11 TS option and asserts ts_enabled stays 0; it fails pre-fix (ts_enabled==1) and passes after.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR is a roll-up of correctness and security hardening fixes across the wolfIP network stack (TCP, raw/packet sockets, DHCP/DNS, ICMP/IGMP, VLAN/ARP, ESP) along with new unit tests to lock in the corrected behaviors.
Changes:
- Tighten input validation and state transitions (e.g., TCP option parsing, TCP connect state mutation, IGMP/ICMP/DHCP validation, DNS in-flight query guarding).
- Fix packet processing behaviors in edge cases (e.g., VLAN demux delivery to AF_PACKET sockets, ESP unwrap with IPv4 options, ARP cache purge on VLAN delete, raw socket receive bind contract).
- Add/adjust unit tests covering the new validation and regression fixes.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/wolfip.c | Implements the bulk of protocol/socket fixes (raw/packet sockets, TCP SACK/OOO, IGMP/ICMP/DHCP/DNS, VLAN/ARP, ESP ordering, checksum idempotence, TX filter behavior). |
| src/tftp/wolftftp.h | Documents filename sanitization expectations for the server open callback. |
| src/tftp/wolftftp.c | Adds request filename sanitization to block traversal/absolute paths before invoking IO callbacks. |
| src/http/httpd.c | Deregisters callbacks on lingering sockets to avoid stale callback_arg use after close. |
| src/test/unit/unit.c | Registers new unit tests covering the added/changed behaviors. |
| src/test/unit/unit_shared.c | Adds an additional DNS callback used by the new in-flight query regression test. |
| src/test/unit/unit_tests_vlan.c | Adds VLAN packet-socket delivery tests and VLAN delete ARP purge regression coverage. |
| src/test/unit/unit_tests_tftp.c | Adds unit test for path traversal/absolute path rejection in TFTP RRQ/WRQ parsing. |
| src/test/unit/unit_tests_tcp_state.c | Adds regression test ensuring overlong TCP Timestamp options do not negotiate TS. |
| src/test/unit/unit_tests_tcp_ack.c | Updates DHCP ACK tests for mandatory lease-time requirement; adjusts forwarding tests for self-IP spoof drop; adds TCP SACK/OOO regression tests. |
| src/test/unit/unit_tests_proto.c | Adds tests for checksum idempotence, raw socket bind filtering, and packet-socket TX filter drop behavior. |
| src/test/unit/unit_tests_multicast.c | Adds IGMP spoofed-query drop coverage (TTL/destination validation). |
| src/test/unit/unit_tests_ip_arp_recv.c | Adds forwarding regression test for dropping packets spoofed with the router’s own source IP. |
| src/test/unit/unit_tests_dns_dhcp.c | Adds tests for recvfrom NULL buffer rejection, ICMP echo-reply dst validation, and DNS in-flight query state protection; updates DHCP message builders for lease-time. |
| src/test/unit/unit_tests_dhcp_edges.c | Adds DHCP renewal xid rerandomization and DHCPACK lease-time mandatory/zero rejection tests; adds NULL-safe public DHCP API test. |
| src/test/unit/unit_tests_branches.c | Adds bind TOCTOU regression tests for deferring UDP/ICMP src_port commit until filters approve. |
| src/test/unit/unit_tests_api.c | Adds regression assertions that failed TCP connect validation does not wedge the socket in SYN_SENT. |
| src/test/unit/unit_esp.c | Adds ESP transport regression test for IHL>5 outer IPv4 headers (options) being unwrapped correctly. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… check
wolftftp_filename_is_safe() blocked leading '/' and '\' absolute paths and
'..' components, but a Windows drive-letter path ("C:\windows\system32") or a
drive-relative path ("C:foo") slipped through: the first segment is "C:", which
is neither a separator-led absolute path nor a ".." component, so the name
reached io.open(). NTFS alternate data streams ("name:stream") had the same
gap. All three hinge on ':', which is never valid in a portable filename, so
reject it wherever it appears.
Adds the drive-letter, drive-relative, and ADS cases to
test_tftp_parse_request_rejects_path_traversal; they fail before the fix
(the names are accepted) and pass after, on both the RRQ and WRQ paths.
The autocov job installs gcovr 7.0 from apt (ubuntu-noble). That version does not recognize the "<count>:<lineno>-block <n>" lines emitted by the runner's gcov and aborts with "UnknownLineType ... -block 0" before any coverage gate runs, failing the whole job. gcovr 7.1+ parses these lines, which is why it reproduces only on the CI toolchain. Add --gcov-ignore-parse-errors=all to every gcovr invocation (the Makefile coverage/autocov targets and the workflow JSON step), alongside the existing --gcov-ignore-errors flag. The auxiliary -block lines are skipped while the canonical "count:lineno:" and function records still parse, so function coverage is unchanged: default 224/224, vlan 227/227, multicast 238/238.
gasbytes
approved these changes
Jun 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
bf2c625 F-5488: reject overlong TCP Timestamp options instead of accepting them
6e1f584 F-4949: coalesce overlapping OOO segments instead of consuming a slot each
58f791b F-5490: make iphdr_set_checksum clear the csum field before computing
99cbd86 F-5904: validate IGMP query TTL and destination in igmp_input
eae67c1 F-5479: validate TCP connect local binding before mutating socket state
57e8aff F-5495: reject a NULL receive buffer in wolfIP_sock_recvfrom
9d05b09 F-5069: defer UDP/ICMP bind src_port commit until the filter approves
5d62a3e F-5733: validate ECHO_REPLY destination IP before delivering to ICMP sockets
0c050fa F-5485: null-check the public DHCP helper APIs
71e81b8 F-5482: reject a DHCPACK that lacks the mandatory lease-time option
129d267 F-5698: re-randomize the DHCP xid for each renewal cycle
1aa3587 F-4948: purge ARP state for a VLAN slot on delete to stop cross-VLAN L2 trust
435a976 F-5484: document that raw sockets intentionally tap before source-route policy
b9e84e3 F-5784: strip IP options before ESP unwrap so IHL>5 ESP packets are handled
16e20df F-5697: never forward a packet whose source is one of our own IPs
7b9e310 F-5011: deliver 802.1Q-tagged frames to parent-bound AF_PACKET sockets
ebd7109 F-4501: drop filter-blocked packet-socket TX frames instead of resending
ae1f904 F-5696: deregister httpd callback on the lingering socket in fail_close
62bdc9f F-5070: enforce raw socket bound_local_ip and if_idx on receive
1ed5dc9 F-4946: guard DNS lookups against clobbering in-flight query state
227c30e F-5481: put the ACK-triggering segment in the first SACK block
6202a32 F-5009: reject path traversal and absolute paths in TFTP RRQ/WRQ filenames