Conversation
Five new optional subsystems with the MacIP-style build-tag gating pattern (`-tags ipx`, `-tags netbeui`, `-tags netbios`, `-tags smb`, `-tags shortname`, all rolled up under `-tags all`): - protocol/ipx, port/ipx, router/ipx, service/ipx: real IPX header encode/decode, port subscribing to all three Ethernet framings (Ethernet II 0x8137, raw 802.3 0x00FF, 802.2 LLC SAP 0xE0), socket router, RIP/SAP services registering sockets 0x0453/0x0452. - protocol/netbeui, port/netbeui: real NBF frame encode/decode (0xEFFF delimiter, 16/16 byte name pair), port building full Ethernet+LLC+NBF outbound frames and stripping link headers inbound. - protocol/netbios, service/netbios + over_ipx/over_netbeui/over_tcp: multi-transport NetBIOS service with per-transport CommandHandler fan-out and ordered Start/Stop with rollback on transport failure. - service/smb: SMB 1.0 server stub with NetBIOS CommandHandler implementation, VFS event-bus subscription for cross-protocol cache invalidation, anonymous-only authenticator, share config type matching `[SMB.Volumes.<name>]` TOML layout. - pkg/shortname: shared 8.3 mapping primitive (Mapper + Store + in-memory backing) used by SMB and AFP. cnid.Store now embeds shortname.Store so per-volume CNID databases also carry the shortname bindings without a circular import. - pkg/vfs: process-wide event bus (Create/Rename/Modify/Delete/Attr) with per-subscriber bounded channels and origin-tag loop avoidance, enabling future fsnotify integration and the cross-protocol service bus for file mutations. - port/rawlink/mux.go: FrameFilter / FrameConsumer types and the optional MultiplexableLink capability that IPX and NetBEUI ports consume to demux by EtherType / LLC SAP. cmd/classicstack: hook trios for each subsystem mirroring MacIP/AFP patterns; main.go now drives Start/Stop on each independently of the AppleTalk router (IPX → NetBEUI → NetBIOS → SMB), and parses 24 new CLI flags plus the matching TOML tables. server.toml.example gets commented-out templates for every new section. All five subsystems are stubs: send paths return ErrNotImplemented, inbound dispatch is logged-and-dropped, and no rawlink wiring is plumbed through main.go yet. Default builds remain identical in feature surface; tagged builds compile cleanly and unit tests cover encode/decode round-trips, lifecycle rollback, and bus loop avoidance. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Decouples AFP's local-FS implementation so SMB and any future service
can build a local-backed share by calling vfs.New("local_fs", ...)
without dragging in a single line of AFP. The host-filesystem
operations (ReadDir/Stat/CreateFile/OpenFile/Remove/Rename/DiskUsage)
now live in pkg/vfs.LocalFileSystem registered as the canonical
"local_fs" backend; AFP keeps the AFP-specific extensions
(CatSearch / ChildCount / DirAttributes / IsReadOnly /
SupportsCatSearch) on a thin wrapper that delegates the universal
methods to a vfs.FileSystem held through the interface, never the
concrete type.
Disk-usage syscall shims (statfs / GetDiskFreeSpaceEx) move with the
backend into pkg/vfs/disk_usage_{windows,other}.go so future backends
inherit them.
afp.File becomes a type alias for vfs.File (structurally identical
contracts) so handles flow across the boundary without an adapter.
The AFP wrapper's zero value remains usable: the embedded backend is
created lazily on first call so the ~50 existing test sites that
construct &LocalFileSystem{} directly continue to work.
All documented build combinations (default, ipx, netbeui, netbios,
smb, shortname, afp, "afp macgarden", "macip ipx netbeui netbios smb
shortname afp macgarden", all) compile clean. Full test suite
green at default and -tags all.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The stub commit had IPX and NetBEUI subscribing to a shared rawlink mux via rawlink.RegisterConsumer. That model assumes one read loop per interface fanning frames out to multiple consumers, with a generic FrameFilter struct synthesizing a union BPF expression. For ClassicStack's actual filter set — EtherType 0x809B for EtherTalk, 0x8137 / raw 802.3 / LLC SAP 0xE0 for IPX, LLC SAP 0xF0 for NetBEUI — the filters are non-overlapping, so the kernel never duplicates a frame across handles. The native model used by EtherTalk and MacIP today (one pcap handle per protocol, each with its own kernel BPF filter, each with its own read goroutine) is strictly cheaper and lets each protocol express its filter natively instead of through a generic struct that struggles with clauses like AARP's `(ether proto 0x806 and arp[2:2] == 0x80F3)`. Changes: - port/rawlink: remove FrameFilter, FrameConsumer, MultiplexableLink, and the RegisterConsumer helper entirely. The rawlink surface is now just RawLink (read/write/close), FilterableLink (BPF push), and MediumReporter (medium hint). port/rawlink/mux.go is deleted. - port/ipx/port.go: rewritten to own its read loop, push the IPX BPF filter via FilterableLink.SetFilter, and demux Ethernet II / raw 802.3 / LLC framings in software (one handle covers all three via the kernel filter; the second-level discriminator is a couple of bytes of arithmetic). Adds Start()/Stop() to the Port interface for explicit lifecycle. - port/netbeui/port.go: same shape as IPX — own read loop, push the NetBEUI BPF filter for LLC DSAP/SSAP=0xF0 with UI control byte, strip the 14+3 byte link header inbound, prepend it outbound. - port/rawlink/pcap.go: adds DefaultIPXConfig and DefaultNetBEUIConfig helpers shaped like DefaultEtherTalkConfig (promiscuous, immediate mode, 250ms timeout). - cmd/classicstack/ipx_enabled.go and netbeui_enabled.go: open their own pcap rawlinks via OpenPcap when an interface is configured, call port.Start in the hook's Start, port.Stop in the hook's Stop. IPX framing is parsed from the operator-facing string into the Framing constant. - main.go drops the dead Rawlink: nil arguments and lets each hook open its own handle. Tests: port-level read-loop tests with a synthetic channel-backed RawLink confirm IPX decodes Ethernet II / raw 802.3 / LLC framings, NetBEUI strips the LLC header and decodes the NBF body, and Send on both ports produces correctly framed Ethernet bytes. All build combinations (default, ipx, netbeui, ipx+netbeui+netbios+ smb, all) compile clean. Full test suite green at default and -tags all. Zero references to the removed mux surface remain in the tree. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Foundation for IPX live traffic. The router now holds the network number and node ID for the process, fills outbound source addresses on the way through Send, and discards inbound datagrams that aren't addressed to us (or to a broadcast address). None of this is visible to clients yet — it's the substrate that RIP and SAP need before either can do anything useful. router/ipx changes: - Router gains SetIdentity / Network / Node, plus a default network number routeripx.DefaultNetwork = 00000001 used when the operator hasn't configured one. Single-segment deployments work out of the box; multi-segment must configure explicitly. - Send now fills SrcNet and SrcNode (when zero) before forwarding to the attached port. Pre-set source fields are preserved so a future forwarding path can still override them. - Inbound enforces addressed-to-us: DstNet must match ours (or be zero, the "local segment, unknown" sentinel that name-claim broadcasts use), and DstNode must be ours or the all-ones broadcast. Frames that survive the kernel pcap filter but aren't for us are dropped silently. - BroadcastNode = FF:FF:FF:FF:FF:FF exported for SAP/RIP/NB-IPX to use when constructing broadcasts. cmd/classicstack/ipx_enabled changes: - parseIPXNetwork accepts an 8-hex-digit network number with an optional 0x prefix; empty input falls back to DefaultNetwork. - resolveIPXNodeFromInterface reads the host interface MAC via rawlink.DetectHostMACForPcapInterface and uses it as the IPX node ID — the standard Ethernet-IPX convention. Failure is a warning, not fatal; the router stays usable with a zero node for tests and loopback. - wireIPX calls router.SetIdentity once it knows both, and logs the resolved network/node at info level so operators can confirm what the router will announce. Tests: - router_test.go covers the address filter (us, broadcast, zero network, foreign network, foreign node, unregistered socket), the source-fill (zero fields filled, pre-set fields preserved, no-port error), and the duplicate-socket guard. - ipx_enabled_test.go covers parseIPXNetwork happy paths and rejection of invalid input. All builds (default, ipx, ipx+netbeui+netbios+smb, all) clean. Full test suite green at default and -tags all. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
We are not a router — we don't forward traffic and we maintain no table beyond our own single network. But to be a well-behaved IPX node we need to: 1. Respond to RIP requests so other nodes learn our network number. 2. Periodically broadcast our network so other nodes don't time us out of their tables. This commit lands both, with sensible defaults and a clock injection point so the broadcaster can be tested without sleeping in real time. service/ipx/rip_packet.go (new): - RIPPacket / RIPEntry types and EncodeRIP / DecodeRIP for the wire format documented in RFC 1551 / Novell IPX RIP. - RIPRequest / RIPResponse operation constants, RIPNetworkAny (0xFFFFFFFF) wildcard, RIPHopUnreachable (16) sentinel. - Decoder ignores trailing pad bytes that don't form a complete 8-byte entry, since IPX frames are commonly padded to the 60-byte Ethernet minimum. service/ipx/rip.go (rewrite): - HandleDatagram parses the RIP body. Requests get a unicast response to the requester; responses from other nodes are ignored (we don't maintain a routing table to update). - Wildcard requests (no entries, or any entry with network=0xFFFFFFFF) match: we respond with our own network as hops=1, ticks=1. - Request-with-specific-network only matches when the network is ours; otherwise we say nothing. - Periodic broadcaster goroutine emits an unsolicited RIP response for our network every Period (default 60s) to the broadcast node on socket 0x0453. First broadcast goes out immediately on Start so the segment learns about us without waiting a full period. - Outbound goes through router.Send so SrcNet/SrcNode are filled from the router's identity automatically. - Stop cancels the broadcaster context and waits for the loop to exit. Tests: - rip_packet_test: round-trip, minimal request, short-input rejection, trailing-pad tolerance. - rip_test: wildcard request gets answered, unknown-network request doesn't, RIP responses from other nodes are silently ignored, periodic broadcast cadence verified via a synthetic clock that closes a channel on demand instead of waiting 60s. All builds (default, ipx, ipx+netbeui+netbios+smb, all) clean. Full test suite green at default and -tags all. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t 0x0452 SAP is the announcement layer that makes a server visible in NetWare-aware clients' browse lists. Without it nothing finds us; with it we appear as soon as a client running IPX/SPX is on the segment. Higher layers (NetBIOS-over-IPX, when it claims a name) call Register to publish an advertisement. The SAP service holds the local registry, replies to inbound queries with matching entries, and broadcasts the registry every 60 seconds so clients pick us up without having to ask. service/ipx/sap_packet.go (new): - SAPPacket / SAPEntry types, EncodeSAP / DecodeSAP for the wire format documented in the NetWare protocol spec. - Operation codes: SAPGeneralQuery (1), SAPGeneralResponse (2), SAPNearestQuery (3), SAPNearestResponse (4). - Service-type constants: 0x0640 NetBIOS, 0x0004 file server, 0xFFFF wildcard. - 64-byte fixed-width entry (2-byte type + 48-byte zero-padded name + 4-byte network + 6-byte node + 2-byte socket + 2-byte hops). Encoder truncates names longer than 47 bytes; decoder trims trailing nulls. - SAPMaxEntriesPerPacket = 7 keeps a response under the 576-byte IPX MTU after the 30-byte IPX header and 2-byte op. service/ipx/sap.go (rewrite): - SAPService.Register fills Network/Node from the router's identity (and Hops=1) when the caller leaves them zero, so most local advertisements just say "this server, on this socket of mine". Returns a cancel func that removes the entry. - HandleDatagram dispatches: queries get a unicast response carrying matching local entries; responses from other agents are ignored. Wildcard (0xFFFF) service-type matches every registered entry; specific types match exact-equal. - SAPNearestQuery returns at most one entry (the first match) — semantics expected by Win9x's "Find Server" dialog. - Periodic broadcaster emits the registry to socket 0x0452 at the broadcast node every 60s (DefaultSAPPeriod). Splits across multiple packets when the registry exceeds 7 entries. First broadcast goes out immediately on Start. - Empty registry means no broadcast. Tests: - sap_packet_test: query and response round-trips; rejects oversized response; truncates long names; rejects truncated query bodies. - sap_test: Register fills identity from router (and respects pre-set fields); Cancel removes entries; specific-type and wildcard query handling; nearest-query returns one entry; empty-match query produces no response; periodic broadcast cadence verified via the synthetic-clock pattern from RIP. The hook already starts both RIP and SAP from Phase 1, so this commit is wire-visible the moment cfg.IPX.enabled is true on a configured interface — though no service is registered yet (that lands in Phase 5 when NetBIOS-over-IPX claims its name). All builds clean. Full test suite green at default and -tags all. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lays the wire-format foundation for NetBIOS-over-IPX (NWLink). The
NB-IPX protocol uses two distinct IPX packet types depending on
purpose:
- IPX type 20 ("NetBIOS broadcast / forwarding") for name service
operations (claim, query, in-conflict). Travels broadcast and
may traverse up to 8 routers.
- IPX type 4 ("Packet Exchange Protocol") on socket 0x0455 for
session-layer traffic. Carries the 16-byte NB-IPX session
header below.
protocol/netbios/netbios.go:
- Name (16-byte) gains structured construction. NewName uppercases,
truncates to 15 bytes, space-pads, and sets the type byte. Type
constants: 0x00 workstation, 0x20 file-server (SMB), 0x1E group.
- Name.String trims trailing spaces; Name.Type returns byte 15.
protocol/netbios/nbipx.go (new):
- NBIPXSessionHeader: the 16-byte session-family header carrying
ConnCtrlFlag, DataStreamType, SourceConnID, DestConnID, SendSeq,
TotalDataLen, Offset, DataLen, ConnCtrlByte. Encode/Decode round-
trip in big-endian.
- DataStreamType constants: NBIPXFindName, NBIPXNameRecognized,
NBIPXSessionInit/Confirm/End/EndAck, NBIPXStatusQuery/Response,
NBIPXDataAck/OnlyLast/FirstMiddle.
- ConnCtrlFlag bits: SYS, ACK, ATT, EOM.
- IPXTypeNetBIOS (0x14) and IPXTypePEP (0x04) constants name the
two IPX packet types NB-IPX uses.
- NBIPXNameServicePacket is the 16-byte body for type-20 broadcasts
carrying just a NetBIOS name. The function (claim vs query vs
conflict) is inferred from context, not from a wire opcode —
matches what NWLink clients actually emit.
Tests:
- NewName padding, uppercasing, truncation behaviour.
- Session header round-trip and short-input rejection.
- Name service body round-trip and short-input rejection.
The session machine (Phase 5C-E) and the name-claim broadcaster
with retry (Phase 5B) build on this codec.
All builds clean. Full test suite green at default and -tags all.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the NetBIOS-over-IPX name-claim handshake. On Start the transport broadcasts a type-20 IPX FindName carrying our chosen NetBIOS name (16-byte padded form, type 0x20 file-server) at 500ms × 6 cadence — the same retry shape NWLink and Win9x clients use. If silence reigns, we own the name and register it with SAP under SAPServiceTypeNetBIOS (0x0640) so other nodes browsing SAP discover us. If any node objects (sends back a type-20 packet carrying our name), we abort the claim before all retries lapse and leave SAP unregistered. This is the moment IPX-side discovery starts working on the wire. A Win9x client running IPX/SPX with NetBIOS-over-IPX enabled should see CLASSICSTACK in its browse list once we land here. SMB won't *talk* to that name until the session machine arrives in Phase 5C, but the segment-level visibility is real. service/netbios/over_ipx changes: - NewTransport(r, sap, name) replaces NewTransport(r). The SAP argument is a SAPRegistrar interface (one method, Register), so unit tests can substitute a fake without dragging the full SAP agent in. - claimAndAdvertise goroutine runs the retry loop, sleeping via the injected sleep func (synthetic clock in tests). On uncontested completion it calls SAP.Register and stores the cancel func for Stop to invoke. - HandleDatagram now dispatches by the IPX packet-type field: type 20 routes to handleNameService (which checks for objections during a pending claim); type 4 (PEP) is reserved for the session machine in Phase 5C and currently dropped. - handleNameService filters out our own loopback broadcasts (matching source net+node against the router's identity) before treating an inbound name-match as an objection — pcap on some drivers delivers our own broadcasts back to us. - Stop unregisters the SAP entry via the stored cancel func. - Empty (zero) name skips the claim entirely, useful for tests that want only the socket-level transport. cmd/classicstack changes: - IPXHook gains SAP() returning the SAP service so wireNetBIOS can pass it through to over_ipx.NewTransport. - ipx_disabled.go's stub satisfies the new interface with a nil SAPService. - wireNetBIOS builds the 16-byte NetBIOS name from cfg.NetBIOSServerName via netbiosproto.NewName(... , NameTypeFileServer) and threads it into NewTransport. Skips the IPX transport when SAP is unavailable (e.g. binary built without -tags ipx). Tests (service/netbios/over_ipx/transport_test.go): - Uncontested claim: synthetic clock drives the retry loop; three FindName broadcasts go out, then SAP receives one Register call carrying our name, type 0x0640, socket 0x0455. Every broadcast is type-20 to the broadcast node on socket 0x0455. - Contested claim: an inbound type-20 packet from a foreign source carrying our name triggers an objection; the goroutine exits before SAP.Register fires. - Self-loopback: an inbound type-20 packet from our own net+node carrying our name is ignored, the claim still succeeds. - Stop after register: the SAP cancel func is invoked exactly once. - Empty name skips the claim entirely (no broadcasts, no SAP). All builds (default, ipx, netbios, ipx+netbios, all) clean. Full test suite green at default and -tags all. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A live run of -tags all with [IPX] enabled but no interface set produced "ipx: no ports attached" errors from RIP and the NB-IPX name-claim broadcaster: the IPX router's identity was set but no port had been attached. The intended deployment runs every protocol on the same physical NIC, so blank IPX/NetBEUI interface should reuse [EtherTalk]'s. cmd/classicstack/main.go now resolves the IPX and NetBEUI interfaces before calling wireIPX/wireNetBEUI: if blank and EtherTalk has a device, fall back to EtherTalk's, log it at info level so it's visible in the startup banner. Operators who want a different NIC still set [IPX] interface = "..." explicitly. cmd/classicstack/smb_shares.go: the loader was already reading [SMB.Volumes.<key>] (the documented spelling) but the example file and most live configs in the wild used [SMB.Shares.<key>]. Accept both: prefer Volumes when present, fall through to Shares with a deprecation warning. Future commits may drop the alias. cmd/classicstack/ipx_enabled.go: the startup banner said "RIP+SAP registered (stub)" — left over from before Phases 1-3 landed real RIP and SAP. Renamed to "RIP+SAP active". server.toml.example: clarify that blank [IPX]/[NetBEUI] interface reuses [EtherTalk] device, and document the 8-hex-digit format and 00000001 default for internal_network. All builds clean. cmd/classicstack tests green at -tags all. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Project shipped under the ClassicStack brand; matching the icon filenames keeps the asset set consistent with the binary name. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire the shortname.Mapper into AFP path encoding so 8.3 names are exposed when UseShortnames is enabled, surface Get/LookupShort/Put on the CNID store interface (stub backend for the no-sqlite build), and add a CNID subscriber that listens on vfs.DefaultBus for rename/delete events from non-AFP origins. Refresh the bundled Sample Volume CNID DB to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add an IPX capture path (Capture.ipx in TOML, plumbed via IPXConfig.CapturePath)
that writes raw IPX frames to a pcap with DLT_EN10MB so wireshark can
decode them alongside the existing LocalTalk and EtherTalk captures. The
router gains tx/rx Debug lines and explains drops by reason ("network"
vs "node"), and DefaultNetwork is changed to 0.0.0.0 so a fresh
ClassicStack appears on the same "unknown" segment Win98/NWLink uses
before a NetWare assignment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build out the NetBEUI command set (protocol/netbeui/commands.go) and grow the over_netbeui transport with a real name table and session layer so NB-frame name claims and session establishment are handled end-to-end. Round out NB-IPX transport coverage and protocol-level decode/encode tests, and have the NetBEUI port carry a configurable source MAC so the NetBEUI transport can announce its own identity. NetBIOS service.go gains the SetCommandHandler/datagram glue SMB needs as a tenant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Expand the SMB service into a working SMB1 server with shared session state (state.go), wire it into NetBIOS for NBT-bound transports, and add a direct IPX transport (service/smb/over_ipx_direct) bound to socket 0x0550 for DOS clients that speak SMB straight over IPX. On the config side, make SMB the source of truth for the computer/workgroup identity and have NetBIOS inherit those values via normalizeSMBIdentity so operators do not have to set them in two places. main.go threads the IPX hook into wireSMB and the new IPX capture path through wireIPX. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Turn on the new SMB-over-IPX path in the operator's server.toml (IPX interface, NetBIOS over ipx transport, an SMB share, and an IPX pcap), trim the redundant netbios server_name/workgroup hints from the example template now that SMB is the identity source, and check in the SMB1 VFS MVP planning prompt under .github/prompts/. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop the Microsoft Open Specification PDFs/Word originals for MS-SMB, MS-CIFS, and MS-BRWS into spec/ alongside markdown extracts, plus an IEEE 802 framing reference. These are the authoritative sources the SMB1 server is being implemented against. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add .gitattributes so text files (Go, markdown, TOML, JSON, YAML, shell) are stored and checked out as LF on every platform, with .bat/.cmd/.ps1 keeping CRLF and binary asset extensions explicitly marked. Run git add --renormalize across the tree to refresh the few files whose indexed copy still had CRLF or stray gofmt drift, so subsequent diffs no longer light up with "LF will be replaced by CRLF" warnings. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Land the work that took us from a NEGOTIATE-only stub to working share enumeration, directory enumeration, and read-only file access from real Win9x/NT clients. Path resolution: every share-relative SMB path is now joined to the share's host root and case-folded against the underlying directory via resolveExistingPath, so client paths like "\Public\AUTOEXEC.BAT" resolve correctly even when the on-disk name differs in case or trailing-dot handling. Trans2: handle SMB_COM_TRANSACTION2 with FIND_FIRST2/FIND_NEXT2 at info level FILE_BOTH_DIR (0x0104), backed by a per-connection search table; handle FIND_CLOSE2 to release entries; handle the legacy SMB_COM_QUERY_INFORMATION (0x08) so Win9x browsing keeps working pre-Trans2. File I/O: implement SMB_COM_READ (0x0A), SMB_COM_SEEK (0x12), and fold a closeFID helper out of handleClose so AndX chains can reuse it. OpenAndX now honors openFunction's open-only bit, picks grantedAccess/action from the actual outcome, and pads ReadAndX data to a 2-byte boundary the way Samba does. Reject SMB_COM_READ_MPX with ERRSRV/ERRuseSTD so clients that ignore our cleared CAP_MPX_MODE flag fall back to plain SMB_COM_READ (mirrors Samba's reply_readbmpx in source3/smbd/reply.c). LockingAndX: walk AndX chains, supporting SMB_COM_CLOSE as the next command — clients commonly send LOCKING_ANDX,CLOSE in one PDU. Wire-error mapping: when the client did not set FLAGS2_NT_STATUS, translate our internal NTSTATUS-shaped codes back into the matching ERRDOS/ERRSRV pair via toWireErrorStatus, so DOS-era redirectors get the errors they expect (ERRbadfile, ERRnoaccess, ERRuseSTD, etc.). state.go gains fileHandle.offset to back SEEK; constants.go adds CommandSeek and CommandReadMPX; session_dispatch routes the new opcodes; server_test covers the new behaviors end-to-end. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
handleReadMPX now serves the requested file bytes inline (WCT=8 response with Offset, Count, Remaining=-1, DataLength, DataOffset, Pad, Data) per [MS-CIFS] 2.2.4.23. We return everything in a single response capped at negotiateMaxBufferSize, which fits a typical Win9x MPX read in one IPX datagram and avoids the prior round-trip via SMB_COM_READ. Returns STATUS_INVALID_HANDLE (mapped to ERRDOS/ERRbadfid on the wire) when the FID is not open, matching the rest of the file-I/O handlers. Also routes buildWriteMPXResponse through stampSMBResponseHeader for consistency with the other response builders in command_core.go. Test updates: - TestHandleReadMPXReturnsData verifies the new shape end-to-end. - TestHandleReadMPXInvalidFID covers the missing-FID error path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds NodeHandler and broadcast handler registration to the IPX router. Node-scoped handlers receive traffic addressed to a non-router-owned node ID and take precedence over socket dispatch; broadcast handlers run in addition to any matching socket handler. The MacIPX gateway uses these hooks to claim the pool of node IDs it hands out to Mac clients and to fan IPX broadcasts out to every registered listener. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a new service (build tag ipxgw|all) that registers NBP names of type "IPX Gateway" in the AppleTalk zones the router serves. MacIPX clients on Classic Mac OS can then see the gateway in their control panel. The wire format (DDP protocol 0x4E carrying a 1-byte opcode + encapsulated IPX or short control message) is observation-driven and documented in spec/15-macipx-gateway.md. Discovery only at this stage: encapsulation/relay is wired so we can capture what the MacIPX client speaks once it picks us. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Parses [IPXGW] section (enabled + Object:Zone bindings), invokes wireIPXGW alongside MacIP, and attaches the IPX router to the gateway once IPX wiring completes so the gateway can register node-scoped handlers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a long name is already a valid 8.3 candidate after uppercasing (1..8 base + 0..3 ext, all FAT-legal), return it verbatim instead of generating a ~N suffix. "FOOD" now maps to "FOOD" not "FOOD~1", which matches what Windows clients on the same share see. Also defaults Shortname.WindowsShortnames=true on Windows so the mapper consults the host's authoritative NTFS short name before falling back to derive83, and drops a dead sqlite-backend stub. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds SMB_COM_TREE_CONNECT (WCT=0 request, WCT=2 response with MaxBufferSize+TID) as used by Windows for Workgroups 3.11 and other CORE-dialect clients. Shares the share-resolution path with the AndX variant. Rewrites handleSearch (0x81) for proper paging: the first request carries a pattern, follow-ups carry an empty filename plus a 21-byte resume key whose ClientState (bytes 17-20) is echoed verbatim while ServerState (bytes 1-16) packs our SID and the last-returned index. The full match list is retained under the SID on the connection so continuations can resume; ERRDOS/ERRnofiles signals exhaustion. Treats MaxCount as per-response, not session-wide, per the errata entry — WfW 3.11 sends MaxCount=1 then 20 across calls and no real CIFS client honours the session-wide reading. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NICs and emulated adapters routinely drop sub-60-byte frames as runts. All outbound NetBEUI frames (UA, RR, I-frames, UI) are now zero-padded to ethernetMinFrameLen (60); the 802.3 length field stays at the LLC payload size so only trailing bytes are added. Also collapses ResponseCorrelator->RspCorrelator field references in tests (the deprecated alias survives) and uses errors.Is for sentinel-error checks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CLAUDE.md gains a numbered checklist for working with the codebase: confirm against /spec, use named consts, capture-driven debugging, attribution, gofmt/lint discipline. spec/errata.md is new: records two intentional CIFS deviations (SEARCH NUL-padding and per-response MaxCount) so the rationale lives next to the code that diverges from the spec. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Documents the new AppleTalk-to-IPX gateway: enabled flag plus the optional Object:Zone bindings list (defaulting to one binding per zone the router knows about when omitted). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the IPXGWHook interface and its enabled/disabled implementations behind the //go:build ipxgw tag. Router-only builds get the no-op stub; ipxgw|all builds get the real service. Companion to the gateway wiring landed earlier on this branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mechanical cleanup across the tree, no behavior changes: - errors.Is for sentinel-error comparisons (replaces == on wrapped errors that broke after %w-wrapping landed) - explicit _ = on Close/Write/SetDeadline returns that were previously ignored implicitly - nested-defer wrappers for Close so the deferred call sees a fresh *err variable, not the captured one - gofmt alignment, dropped trailing blank lines - package doc comments on ipx, netbios, service, over_ipx_direct - block-comment package docs converted to line-comment form - removed dead helpers (resolveForkMetadataBackend, categoryByName, countCategoryResultsOnPage, getCategoryItems, getDesktopDB, release, pathDir, errMacGardenNotFound, dirEntryFromFileInfo, buildTreeConnectResponse old form) and unused fields (extendedNetwork, tid on fileHandle, maxBufferSize on connState, headCacheEntry.err, sessionStateInit/Closing aliases) - routing_table: simplified De Morgan'd boolean - ipx rip/sap/netbios: collapsed sleep closures to time.After - ethertalk tap: pass OpenTAP directly instead of wrapping - afp server_calls: switch-case for UAM - nat/ipnat: net.JoinHostPort + strconv.Itoa instead of Sprintf - .gitignore: drop stray "classicstack" pattern that shadowed cmd/classicstack/ Go files when staging Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Contributor
Author
|
Okay tested:
While everything works, it's at best alpha quality and more testing is needed. There are significant changes in this release for user configuration, etc. Need full release notes and moving from v0.1 notes. |
Win9x clients address GetBackupListRequest to <workgroup><00> or <workgroup><1D>, and only accept the reply when it comes from the master-browser identity <workgroup><1D> per [MS-BRWS] 3.2.5.5. Our previous behavior of replying as <server><20> (file-server) caused the client to silently reject the backup list, retry up to six times, then re-run the election — every ~30s — leaving the Network Neighbourhood unable to enumerate ClassicStack as master. Observed in captures/ipx.pcap frames 161-189: ClassicStack correctly answers six GetBackupListResponse datagrams sourced from CLASSICSTACK<20>, then QEMU triggers a fresh BrowserElectionRequest because it never adopted us as master. backupListResponseSource now mirrors as <workgroup><1D> whenever the request destination matches our workgroup, regardless of the suffix on the inbound request. Tests updated to assert the new source name on both broadcast and directed reply paths. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The spec-compliant handleReadMPX from a7fa7ee returned the requested chunk in a single WCT=8 response (Offset, Count=DataLength, Remaining=0xFFFF, valid Pad+Data) per [MS-CIFS] 2.2.4.23. On the wire this looks correct, but Win9x over Direct IPX silently rejects each reply and retransmits the same Read MPX request at file offset 0 forever — see captures/ipx.pcap frames 365-393 (FID 0x0003, MID 35457, all at offset 0) and frames 415+ (FID 0x0004). Exact reason Win9x refuses the response is unknown — possibly a required multi-response streaming form or some MID/dialect quirk we have not reverse-engineered. Samba's reply_readbmpx in source3/smbd/reply.c has avoided the command for the same reason since the 1990s by returning ERRSRV/ERRuseSTD, which prompts the client to fall back to SMB_COM_READ (which we already serve correctly). Costs one extra round-trip per chunk; in exchange the transfer actually completes. - handleReadMPX now returns smbStatusUseStandard unconditionally. - TestHandleReadMPXReturnsData / TestHandleReadMPXInvalidFID replaced with TestHandleReadMPXReturnsUseStandard. - spec/errata.md documents the deviation per CLAUDE.md #5. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
PR aims to add protocol support for IPX, NetBeui + NetBIOS + SMB.
It's a work in progress.
Current state:
Some other changes: