Repository navigation
Conversation
The OS-stack NAT terminates each Mac TCP connection itself, but sent every segment toward the Mac exactly once. LocalTalk loses frames (a busy 512K Mac's driver drops datagrams it has no buffer for), so one lost segment hung the connection for good: the Mac sent duplicate ACKs and the NAT ignored them. The SYN-ACK and FIN were also sent once, a retransmitted SYN was ignored, and data from the Mac was written to the server whatever its sequence number, so a Mac retransmit reached the server twice. TCP flows now: - keep the data sent toward the Mac until it is acknowledged, and resend the oldest segment on three duplicate ACKs (RFC 5681), on a partial ACK during recovery (RFC 6582, the Mac keeps no out-of-order data) and on a timeout (1 s, doubling to 16 s; reset after 8 resends) - resend the FIN until it is acknowledged, and stay open until the Mac has closed its side, so the Mac's FIN is ACKed - answer a retransmitted SYN with the SYN-ACK again - write only in-order bytes from the Mac to the server: retransmits are trimmed or dropped, out-of-order data is dropped, and either is answered with an ACK for the next byte expected Tested on an emulated Mac 512Ke (Snow, System 6.0.8, with a small MacIP TCP stack) over LToUDP, dropping 5% of the gateway's frames: a 5.6 KB gopher fetch completed 1 of 5 times before (the rest hung until the app's 90 s idle timeout) and 8 of 8 times after, in 2.0-4.6 s. Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
Claude generated commit/PR after running across a bug while testing ClassicStack on a slimmed down TCP/IP stack on a Macintosh 512Ke.
Summary
In MacIP NAT mode, the OS-stack NAT (
port/nat/ipnat.go) terminates each Mac TCP connection itself, but it sent everysegment toward the Mac exactly once. LocalTalk loses frames, so one lost segment, SYN-ACK or FIN hung the connection
for good. The NAT also wrote the Mac's retransmits to the server a second time. This PR adds retransmission toward the
Mac and only writes in-order bytes from the Mac to the server. All changes are in
port/nat/ipnat.go, and the testsare in a new
port/nat/ipnat_test.go.Problem
tcpServerReadLoopsends data and the FIN once and keeps no copy. Duplicate ACKs from the Mac are ignored, and thereis no retransmission timer. One lost frame hangs the connection for good.
if _, exists := n.tcpFlows[key]; exists { return }), so alost SYN-ACK hangs the connect.
handleTCPdoesmacSeq += len(payload)and writes the payload to the server whatever its sequence number. Aretransmit from the Mac reaches the server twice, and the NAT then ACKs bytes the Mac never sent. A strict client
ignores an ACK above its
snd_nxt, so it never sees its data acknowledged and keeps resending it. Each copy iswritten to the server again.
LocalTalk does lose frames. Besides collisions, a 512K Mac's driver drops any datagram it has no buffer for while the
application is busy. On a clean LToUDP link, 2 of about 18 connections from an emulated 512Ke hung this way.
The comments in #20's
core/service/macip/tcp.goandmacip.godescribe the same failure: OSNAT "does notretransmit, so ... a single dropped segment DEADLOCKED the flow (capture ltoudp-netboot.pcap)".
Repro
The client is a small MacIP TCP stack on an emulated Macintosh 512Ke (Snow, System 6.0.8) over LocalTalk. The stack
keeps one segment in flight, with MSS 546 and a 1092-byte window. It talks to ClassicStack 3e8248a (
-tags macip,[MacIP] mode = "nat") over LToUDP in a private network namespace. nftables drops 5% of ClassicStack's LToUDP frames:The test is a gopher fetch of
floodgap.com:70/(5,639 bytes), repeated.Before the fix, the first connection lost the NAT's ACK of the 2-byte request. The capture below uses relative
sequence numbers. It was taken on the LToUDP side after the drop rule, so a dropped frame doesn't appear:
What's changed
Data toward the Mac
unackeduntil the Mac ACKs it. The oldest segment is resent:lost one is gone too;
ourSeqare ignored.Connection setup and close
closed its side, or for at most 1 minute, so the Mac's FIN is ACKed.
Data from the Mac
out-of-order data is dropped. Any data or FIN segment is answered with an ACK for the next byte expected.
The window logic is unchanged: data in flight stays within the Mac's advertised window. Retransmission state is per
flow and guarded by the existing
flow.mu. A generation counter stops a timer callback that fires late from acting.Results
The same rig at 5% loss, before and after the fix. Times are as reported by the client application:
Here is a recovery after the fix. Two segments were lost. The timer resent the first, and the partial ACK resent the
second 7 ms later:
Other results after the fix:
fetch completed.
help(4 pages) andcowsay. 8of ClassicStack's frames were dropped and 4 data segments were resent. The session never stalled.
timer repaired it in 1 s. Before the fix, that drop would have hung the fetch.
Testing
The new
port/nat/ipnat_test.godrivesForwardandtcpServerReadLoop. It uses anet.Pipeas the server and arouter stub that records what is routed to the Mac. It tests that:
answered with the right ACK;
The following checks pass on the branch, which is based on main at 3e8248a (Go 1.25.11, linux/amd64):
go test -race -count=1 ./port/nat/, run 3 times.go test -count=1 ./...: 41 packages ok, 0 failed.go test -tags all -race -count=1 ./...: 47 packages ok, 0 failed.go vet ./...andgo vet -tags all ./...are clean.go buildwith-tags macip,-tags "afp macip"and-tags allsucceeds.golangci-lint run --build-tags=all ./port/nat/...(v2.5.0) reports 0 issues, andgofmt -lis clean.Notes
adapter/macipgw/nat/osnat.go, and the change ports over directly. Branchnat-tcp-retransmit-on-pr20has it on top of 1.0.0-RC: hexagonal core/adapter/compose rewrite #20 if that's more useful.is outstanding, cover the case seen here.
🤖 Generated with Claude Code