Skip to content

Commit b979419

Browse files
ccie18643claude
andcommitted
docs: add host-refinements backlog plan (post-3.0.8 optional items)
Durable plan for the "do refinements one by one" track: what shipped (UDP + RAW SO_RCVBUF), and the remaining optional items (R1 ping SO_RCVBUF, R2 setsockopt-honored sweep, R3 SO_SNDBUF/SNDTIMEO accounting, R4 IPv6 SSM source filters, R5 CLI JSON, R6 HyStart++, R7 DF-guarded PLPMTUD probe) with scope, files, tests-first plan, effort/risk, and recommended ordering. None block a 3.0.8 release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a014080 commit b979419

1 file changed

Lines changed: 290 additions & 0 deletions

File tree

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
# Host-stack refinements backlog (post-3.0.8 optional items)
2+
3+
| Field | Value |
4+
|------------|-----------------------------------------------------------------------|
5+
| Status | **IN PROGRESS** — 2 of N shipped. Opened 2026-07-19 on `PyTCP_3_0_8`. |
6+
| Branch | `PyTCP_3_0_8` |
7+
| Scope | Optional host-scope refinements deferred out of the 3.0.8 cut. **None are host-conformance gaps** — 3.0.8 is host-feature-complete. These are polish / Linux-parity completeness. |
8+
| Rule | Every item is **tests-first (red tests before implementation)**, `make lint` clean, adherence + docs in lockstep. See `.claude/rules/feature_implementation.md`. |
9+
10+
This is the working backlog for the "do refinements one by one" track.
11+
Items are ordered by ascending prerequisite-weight, not value. Pick the
12+
next unchecked item, write the failing test(s) first, implement the
13+
minimal change, verify, commit (one concern per commit), hold pushes
14+
until the user says "push".
15+
16+
---
17+
18+
## Shipped (this track)
19+
20+
- [x] **UDP `SO_RCVBUF` enforcement** — commit `d51abda9`. `process_udp_packet`
21+
drops an inbound datagram whose payload would push the queued receive
22+
bytes past the cap (Linux `sk_rcvqueues_full`), enforced only when
23+
`SO_RCVBUF` is set; unset stays unbounded. 3 unit tests in
24+
`test__runtime__socket__udp__socket.py::TestUdpSocketReceive`.
25+
- [x] **RAW `SO_RCVBUF` enforcement** — commit `a0140806`. Same guard in
26+
`process_raw_packet` (`raw__data`). 2 unit tests in
27+
`test__runtime__socket__raw__socket.py::TestRawSocketRcvbuf`.
28+
29+
**The canonical SO_RCVBUF guard pattern** (mirror for any new datagram socket):
30+
31+
```python
32+
with self._lock__io:
33+
if self._closed:
34+
return
35+
if self._so_rcvbuf is not None:
36+
queued = sum(len(md.<data_attr>) for md in self._packet_rx_md)
37+
if queued + len(packet_rx_md.<data_attr>) > self._so_rcvbuf:
38+
__debug__ and log("socket", f"...Dropped: SO_RCVBUF {self._so_rcvbuf} exceeded")
39+
return
40+
self._packet_rx_md.append(packet_rx_md)
41+
self._packet_rx_md_ready.release()
42+
self._signal_readable()
43+
```
44+
45+
`_so_rcvbuf` lives on the base `socket` (runtime/socket/__init__.py, set by
46+
`_sol_socket_setsockopt`). Enforce-only-when-set = zero regression risk.
47+
48+
---
49+
50+
## Remaining items
51+
52+
### R1 — Ping socket `SO_RCVBUF` (small, has a prerequisite)
53+
54+
- **Why deferred:** `PingSocket.setsockopt` (runtime/socket/ping__socket.py
55+
~222) only handles `IP_RECVTTL` / `IPV6_RECVHOPLIMIT` — it does **not**
56+
route `SO_RCVBUF` to the base `_sol_socket_setsockopt`, so enforcing the
57+
guard in `process_echo_reply` would be dead code (`_so_rcvbuf` stays
58+
`None`).
59+
- **Scope:** (1) add SOL_SOCKET routing to `PingSocket.setsockopt` (mirror
60+
udp/raw: `if isinstance(value, int) and level == SOL_SOCKET and
61+
self._sol_socket_setsockopt(optname, value): return`); (2) add the
62+
SO_RCVBUF guard to `process_echo_reply` (~381, data attr `icmp__data`).
63+
- **Tests-first:** there is **no** ping-socket unit test today. Create
64+
`packages/pytcp/pytcp/tests/unit/runtime/socket/test__runtime__socket__ping__socket.py`
65+
with a lean fixture: patch `pytcp.runtime.socket.ping__socket.log` and
66+
`...ping__socket.stack.icmp_echo_sockets` to a fresh `dict` (the only two
67+
module deps — `_allocate_echo_id` reads the dict). Construct
68+
`PingSocket(family=AddressFamily.INET4, protocol=IpProto.ICMP4)`,
69+
`addCleanup(s.close)`. `PingMetadata` fields: `ip__ver`,
70+
`ip__remote_address`, `ip__ttl`, `icmp__data`. Red tests: over-cap drop
71+
+ unset-unbounded (mirror the RAW tests). Also a test that
72+
`setsockopt(SOL_SOCKET, SO_RCVBUF, n)` now actually sets `_so_rcvbuf`.
73+
- **Effort:** ~1 hr. **Risk:** low. **Value:** low (ping is one reply per
74+
request), but closes the SO_RCVBUF symmetry.
75+
76+
### R2 — `setsockopt`-honored + errno-exactness sweep (medium, incremental)
77+
78+
- **Why:** several options are accepted-and-stored but not enforced, and
79+
some error paths don't match Linux errno exactly. This is the "make every
80+
setsockopt actually bite" item.
81+
- **Approach:** audit each `case` in the setsockopt handlers in
82+
`runtime/socket/__init__.py` (`_sol_socket_setsockopt`,
83+
`_ipproto_ip_setsockopt`, `_ipproto_ipv6_setsockopt`) plus the per-flavour
84+
overrides (udp/raw/ping). For each option ask: is it read anywhere? If
85+
stored-only, either wire it or document why it's inert. Also verify
86+
`ENOPROTOOPT` / `EINVAL` / `ENOTCONN` are raised where Linux raises them.
87+
- **Tests-first:** one red test per option-that-should-bite-but-doesn't,
88+
asserting the behavioural effect (not just the stored value). This is a
89+
series of small red-tests-first commits — good "one by one" cadence.
90+
- **Known members of this bucket:** `SO_SNDBUF` (see R3), `SO_SNDTIMEO`
91+
(see R3), `X3` listen()-on-unbound → `EINVAL` (breaks examples; land as
92+
an explicit breaking-change commit + update `examples/`).
93+
- **Effort:** open-ended (do a few per session). **Risk:** low per fix.
94+
95+
### R3 — `SO_SNDBUF` accounting + `SO_SNDTIMEO` (medium-large, coupled)
96+
97+
- **Why deferred:** UDP `send` hands the datagram straight to the shared TX
98+
ring (`send_udp_packet`), with **no per-socket send buffer**. Linux
99+
`SO_SNDBUF` bounds `sk_wmem_alloc`; PyTCP has no such accounting.
100+
- **Scope:** build a per-socket outstanding-send-bytes counter
101+
(increment on enqueue-to-TX, decrement on TX completion — needs a
102+
completion signal from the TX ring the socket can observe), bound it by
103+
`_so_sndbuf`, and on overflow either `EAGAIN` (non-blocking) or
104+
block up to `SO_SNDTIMEO`. `SO_SNDTIMEO` is only meaningful once this
105+
exists — do them together.
106+
- **Tests-first:** red tests for over-SO_SNDBUF → EAGAIN, and blocking →
107+
timeout after SO_SNDTIMEO.
108+
- **Effort:** medium-large (the TX-completion signal is the hard part).
109+
**Risk:** medium (touches the TX path). **Prereq:** understand how the
110+
TX ring signals completion; there may be no per-datagram completion today.
111+
112+
### R4 — IPv6 per-socket source filters = MLDv2 SSM track (large, highest value)
113+
114+
- **Why:** IPv6 has only any-source `IPV6_JOIN_GROUP` / `IPV6_LEAVE_GROUP`
115+
(handled at runtime/socket/__init__.py ~1029). IPv4 has the full
116+
source-specific set. This is the **IPv6 analogue of the shipped IGMPv3
117+
SSM feature** — see `docs/refactor/igmp_source_specific_multicast.md`
118+
(Phases 1–5) as the exact template.
119+
- **The IPv4 machinery to mirror:**
120+
- `packages/pytcp/pytcp/lib/ip4_multicast_filter.py`
121+
`Ip4MulticastFilter` (INCLUDE/EXCLUDE mode + source set + `merge`).
122+
- `_ip4_multicast_filters: dict[Ip4Address, Ip4MulticastFilter]` on the
123+
packet handler (runtime/packet_handler/__init__.py ~324); the merged
124+
§3.2 reception filter via `_ip4_multicast_filter_for`.
125+
- setsockopt opts at runtime/socket/__init__.py ~783–957:
126+
`IP_ADD_SOURCE_MEMBERSHIP` / `IP_DROP_SOURCE_MEMBERSHIP` /
127+
`IP_BLOCK_SOURCE` / `IP_UNBLOCK_SOURCE`.
128+
- IGMPv3 state-change source records (ALLOW_NEW_SOURCES /
129+
BLOCK_OLD_SOURCES / the CHANGE_TO_* forms) in the IGMP TX handler.
130+
- RX source-delivery filter `ip_mc_sf_allow` (`Ip4MulticastFilter.allows`)
131+
gating per-socket delivery for **UDP and RAW** (RAW needed the
132+
`RawMetadata.socket_ids` wildcard-combo enumeration — mirror for v6).
133+
- **The IPv6 side to build (Phases mirroring the IGMP track):**
134+
- P1: `packages/pytcp/pytcp/lib/ip6_multicast_filter.py` (`Ip6MulticastFilter`,
135+
a straight `Ip6Address` copy of the v4 value type). Unit tests.
136+
- P2: new opts `IPV6_ADD_SOURCE_MEMBERSHIP` / `IPV6_DROP_SOURCE_MEMBERSHIP`
137+
(and/or the protocol-independent `MCAST_JOIN_SOURCE_GROUP` family) as
138+
`IpV6Option` enum members + bare aliases (see `.claude/rules/enums.md`
139+
§2.2). Wire the setsockopt cases.
140+
- P3: `_ip6_multicast_filters` dict on the handler + `_ip6_multicast_filter_for`.
141+
- P4: MLDv2 source records on TX — `Icmp6Mld2MulticastAddressRecordType`
142+
already has `ALLOW_NEW_SOURCES` (5) / `BLOCK_OLD_SOURCES` (6) /
143+
`CHANGE_TO_INCLUDE` (3) / `CHANGE_TO_EXCLUDE` (4). Emit the state-change
144+
source records on filter transitions (mirror `_send_igmp_state_change`;
145+
the MLDv2 leave path added in `de3d3213` is the sibling to extend).
146+
- P5: RX source-delivery filter for IPv6 UDP + RAW (`Ip6MulticastFilter.allows`).
147+
- Adherence: update `docs/rfc/icmp6/rfc3810__mld2/adherence.md` (§4.2.12 /
148+
§5.1 / §5.2 source records) + a socket-parity note, in lockstep.
149+
- **Effort:** large (multi-phase, mirrors a whole shipped track). **Risk:**
150+
medium. **Value:** highest — real v4/v6 parity.
151+
152+
### R5 — CLI polish: `pytcp address -j` JSON output (small, self-contained)
153+
154+
- **Scope:** add `-j`/`--json` to the `pytcp address` subcommand (and
155+
optionally `ss`/`route`/`neighbor` for parity), emitting machine-readable
156+
JSON alongside the human table. CLI lives under `packages/pytcp/pytcp/cli/`.
157+
- **Tests-first:** unit tests under
158+
`packages/pytcp/pytcp/tests/unit/cli/` asserting the JSON shape.
159+
- **Effort:** ~1–2 hrs. **Risk:** low. **Value:** low-ish (nice for scripting).
160+
161+
### R6 — HyStart++ (RFC 9406) remaining work
162+
163+
- **Scope:** the deferred pieces noted in the rfc9406 adherence record
164+
(~6–8 hrs estimated). Read `docs/rfc/tcp/rfc9406*/adherence.md` for the
165+
exact deferred rows before starting.
166+
- **Effort:** medium. **Risk:** medium (CC path). **Value:** medium.
167+
168+
### R7 — DF-guarded TCP PLPMTUD probe (small, closes a "Phase 3c-min" residual)
169+
170+
- **Why:** the TCP probe-emit path (`session/tcp__session__tx.py`) ships
171+
probe-**sized** segments but does not set DF, so RFC 8899 §3 #2 (DF=1 on
172+
the probe) is still "Phase 3c" (honest note left in
173+
`docs/rfc/tcp/rfc8899__dplpmtud/adherence.md` and rfc4821). Set DF on the
174+
emitted probe segment so a black-hole is detected by loss rather than
175+
relying only on ack-feedback sizing.
176+
- **Tests-first:** extend
177+
`test__tcp__session__plpmtud_probe_emit.py` to assert the probe segment
178+
carries DF=1 (IPv4) / is size-capped without fragmentation (IPv6).
179+
- **Effort:** small–medium. **Risk:** medium (probe-loss interaction with
180+
RTO). **Value:** completes RFC 8899 §3 #2 / §4.1 fully.
181+
182+
### R8 — `IP_RECVERR` / `IPV6_RECVERR` error queue over the daemon boundary (medium)
183+
184+
- **Why:** the per-socket ICMP error queue + `recvmsg(MSG_ERRQUEUE)` works
185+
**in-process**, but the daemon data bridge does not pump the error queue
186+
across the AF_UNIX boundary, so a daemon-backed drop-in client cannot read
187+
ICMP errors via `MSG_ERRQUEUE`. Source: `kernel_userspace_separation.md`
188+
deferred list.
189+
- **Scope:** extend the daemon IPC protocol so an `MSG_ERRQUEUE` `recvmsg`
190+
is serviced across the boundary (the in-process path already builds the
191+
`sock_extended_err` cmsg via `runtime/socket/error_queue.py`). Wire the
192+
error-queue drain into the daemon's per-socket bridge.
193+
- **Tests-first:** integration test under `tests/integration/ipc/` driving
194+
an ICMP error to a daemon-backed UDP socket and asserting the client's
195+
`recvmsg(MSG_ERRQUEUE)` returns the `IP_RECVERR` cmsg.
196+
- **Effort:** medium. **Risk:** medium (IPC protocol surface). **Value:**
197+
medium (completes the drop-in's error-reporting parity).
198+
199+
### R9 — Selectable / cancelable `accept` over the daemon (medium)
200+
201+
- **Why:** a client disconnecting mid-`accept` leaves the daemon dispatch
202+
thread polling until server stop (Phase-4 daemon limitation noted in
203+
`kernel_userspace_separation.md`).
204+
- **Scope:** make the daemon-side accept wait cancelable (wake on client
205+
disconnect / a cancellation signal) so the dispatch thread doesn't spin.
206+
- **Tests-first:** integration test — connect a client, issue accept, drop
207+
the client, assert the dispatch thread returns/cleans up promptly.
208+
- **Effort:** medium. **Risk:** medium (threading/lifecycle). **Value:**
209+
medium (daemon robustness).
210+
211+
### R10 — MLDv1 Report suppression + `mld.version` force knob (small)
212+
213+
- **Why:** two deferred-with-rationale items from `mld_version_fallback.md`:
214+
RFC 2710 §4 Report suppression (a host that hears another host's Report for
215+
a group suppresses its own — an optimization, not done for MLDv2 either),
216+
and an `mld.version` force sysctl (Linux extension, the IPv6 analogue of
217+
the shipped `igmp.version` knob).
218+
- **Scope:** (a) suppression: on RX of a peer MLDv1 Report for a group in
219+
v1 compat mode, cancel this host's pending Report for that group; (b) the
220+
knob: add `mld.version` via the `sysctl_knob` skill workflow, consumed by
221+
`_mld_host_compatibility_mode` (mirror `igmp.version` /
222+
`IGMP__FORCE_VERSION`).
223+
- **Tests-first:** integration tests in `tests/integration/protocols/icmp6/`.
224+
- **Effort:** small each. **Risk:** low. **Value:** low (optimization +
225+
operator knob). The knob has clear parity value; suppression is marginal.
226+
227+
### R11 — RFC 6724 `ip.policy_table` sysctl override (small-medium)
228+
229+
- **Why:** the RFC 6724 §2.1 policy table (source/dest address selection
230+
precedence/label) is hard-coded; Linux exposes it as a writable table
231+
(`/proc/sys/net/ipv6/...` / `ip addrlabel`). Source:
232+
`rfc6724_source_selection.md` non-blocking arc extension.
233+
- **Scope:** make the policy table operator-overridable (a sysctl entry or a
234+
small dedicated API), re-resolved live per the qualified-module-access
235+
pattern.
236+
- **Tests-first:** unit tests asserting source-selection changes when the
237+
policy table is overridden.
238+
- **Effort:** small-medium. **Risk:** low. **Value:** low-medium (rarely
239+
tuned on a host).
240+
241+
### Ongoing hygiene (not a discrete scheduled item)
242+
243+
- **On-touch enum migrations** — bare `FOO: int = N` constants that represent
244+
one-of-a-set should become enum members on touch (see `.claude/rules/enums.md`
245+
§5). Not a dedicated commit; fix opportunistically when editing a file.
246+
247+
---
248+
249+
## Out of scope for this backlog (deliberately excluded)
250+
251+
These are **not** host-scope refinements and are tracked elsewhere; listed
252+
so this backlog's boundary is explicit.
253+
254+
- **Consumer-blocked** (need a DNS resolver / DDNS / HTTP agent PyTCP does
255+
not have): RFC 4702 Client FQDN, RFC 3203 FORCERENEW, RFC 8910 Captive
256+
Portal (DHCPv4 Phase 9); RFC 6724 §6 destination-address selection;
257+
the RDNSS / DNSSL runtime consumer (wire codec is parse-ready).
258+
- **Deliberate won't-do (scope decisions):** DCTCP (RFC 8257), L4S
259+
(RFC 9331 / 8311), TCP-AO (RFC 5925), Eifel (RFC 4015), CWV (RFC 7661),
260+
F-RTO (RFC 5682); `dup` / `dup2`, `socketpair`, hostname-in-`bind`/
261+
`connect`; RFC 4884 extended ICMP.
262+
- **Phase-2 router track** (its own future major version): IP forwarding
263+
data path, ICMP Redirect emit, forward-path TTL-decrement + Time
264+
Exceeded, IGMPv3 / MLDv2 querier role, Proxy ARP, RH0 disable-knob,
265+
Router-Alert interception, PMTU-on-transit, RFC 1812 requirements, Link
266+
API `up()` / `down()` (needs multi-interface).
267+
268+
---
269+
270+
## Recommended ordering
271+
272+
1. **R1** (ping SO_RCVBUF) — finishes the SO_RCVBUF symmetry cleanly.
273+
2. Small self-contained wins, any order: **R5** (CLI JSON), **R10** (mld
274+
knob + suppression), **R11** (RFC 6724 policy table), or dip into **R2**
275+
(setsockopt sweep).
276+
3. **R4** (IPv6 SSM) — the big-value track; do it as its own phased effort.
277+
4. Medium items as appetite allows: **R3** (SO_SNDBUF/SNDTIMEO), **R6**
278+
(HyStart++), **R7** (DF-guarded probe), **R8** (IP_RECVERR over daemon),
279+
**R9** (cancelable accept).
280+
281+
None block a 3.0.8 release. Everything here can equally slip to 3.0.9.
282+
283+
---
284+
285+
## Git state at handoff (2026-07-19)
286+
287+
- Branch `PyTCP_3_0_8`. Pushed through `af2719ea` (MLDv2 leave + doc
288+
reconciliations).
289+
- **Unpushed:** `d51abda9` (UDP SO_RCVBUF) + `a0140806` (RAW SO_RCVBUF).
290+
Hold until the user says "push".

0 commit comments

Comments
 (0)