Scaffold ipswap: tray-driven Windows IP preset switcher
Implements the design in claude.md as a building skeleton: pure Go, no cgo, cross-compiles to a single Windows .exe from Linux. Architecture follows the spec's deliberate split. The fast path is native — tray icon, grouped submenus, a Win32 MessageBox showing a live before/after diff, then netsh. The slow path is an embedded web editor served on a random loopback port and opened in the default browser. Two decisions worth recording: Reads use GetAdaptersAddresses, writes use netsh. The spec left the enumeration mechanism open; parsing `netsh show config` breaks on a non-English Windows because the output is localised. DNS static-vs-DHCP origin is not exposed by that API, so it comes from one registry read. The netsh command plan is built in portable code. That puts the delete-every- existing-address step — the one that stops secondary addresses leaking across switches — under test without needing a Windows box. The editor requires the session token in a header for mutations, not just the cookie: any page in the browser can make it POST to 127.0.0.1 with the cookie attached, but it cannot set a header. The updater refuses to install a release that publishes no SHA256. Not yet done: no group picker for export (the API supports it), no single-instance guard, and internal/server/web/app.css is reconstructed from the description in claude.md rather than the canonical apointless.css. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# ipswap
|
||||
|
||||
A tray-resident Windows utility for switching a network adapter between saved
|
||||
static-IP configurations. Built for field and support work, where a laptop hops
|
||||
between customer subnets — broadcast control networks, media networks,
|
||||
management VLANs — many times a day.
|
||||
|
||||
Switching a preset is two clicks and no browser. Editing fifty presets happens
|
||||
in a real UI. Those are different problems, so they use different interfaces.
|
||||
|
||||
## How it works
|
||||
|
||||
**Fast path (native, no browser).** The tray icon holds a menu of presets
|
||||
grouped into submenus. Clicking one reads the adapter's live configuration,
|
||||
shows a Win32 message box with a before/after diff, and on Yes runs the netsh
|
||||
sequence. The tray tooltip then names the preset that is actually applied.
|
||||
|
||||
**Slow path (browser).** "Manage presets…" starts a local HTTP server on a
|
||||
random loopback port and opens your default browser to it. Full CRUD, adapter
|
||||
picker, import/export, settings. The server shuts itself down five minutes
|
||||
after the browser stops sending heartbeats, and on exit.
|
||||
|
||||
## Building
|
||||
|
||||
Pure Go, no cgo anywhere, so a Linux or macOS host cross-compiles the Windows
|
||||
binary directly — no Wine, no mingw, no fyne-cross.
|
||||
|
||||
```
|
||||
make build # -> bin/ipswap.exe
|
||||
make check # tests + vet + typecheck for both targets
|
||||
make dist # bin/ipswap.exe plus dist/SHA256SUMS
|
||||
```
|
||||
|
||||
`make build` regenerates `cmd/ipswap/resource_windows.syso` first. That object
|
||||
carries the icon, the version resource and the manifest — the manifest is the
|
||||
part that matters, because it is what asks for `asInvoker` and per-monitor DPI.
|
||||
|
||||
`GOOS=windows go vet ./...` is worth running on its own during development: the
|
||||
Windows-only files (netsh, user32, the registry) are most of the risk and a
|
||||
plain Linux build never looks at them. `make vet` does both targets.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
cmd/ipswap/ entrypoint, manifest, version resource
|
||||
internal/preset/ data model, store, prefix parsing, import/export
|
||||
internal/netcfg/ adapter reads (Win32) and the netsh command plan
|
||||
internal/tray/ tray menu, confirm-then-apply, icon
|
||||
internal/server/ the local editor server and its embedded web app
|
||||
internal/updater/ Gitea release check, download, verify, swap
|
||||
internal/dialog/ Win32 MessageBoxW wrappers
|
||||
internal/elevate/ token elevation check, relaunch via ShellExecuteW runas
|
||||
internal/desktop/ open-in-browser, HKCU Run key
|
||||
internal/config/ paths and settings
|
||||
internal/applog/ rotating log
|
||||
```
|
||||
|
||||
Two rules shape the split. Anything portable lives in portable code and is
|
||||
tested on any host — most usefully `netcfg.Plan`, which builds the exact netsh
|
||||
sequence and is covered without a Windows box. Anything Windows-specific has a
|
||||
`_windows.go` and an `_other.go`, so the whole tree stays buildable and
|
||||
vettable during development on Linux.
|
||||
|
||||
## Elevation
|
||||
|
||||
The manifest asks for `asInvoker`, not `requireAdministrator`. On a machine
|
||||
with UAC relaxed, or when the parent process is already elevated, that means
|
||||
zero prompts — including at login, if start-with-Windows is on.
|
||||
|
||||
Membership of the Administrators group is not the same as holding an elevated
|
||||
token: with UAC on, a member's process gets a filtered token and `netsh
|
||||
interface ipv4 set address` fails with "The requested operation requires
|
||||
elevation." So ipswap checks the token at startup, and if it is not elevated it
|
||||
adds a "Relaunch as administrator" item to the tray. A failed apply offers the
|
||||
same thing. Nothing blocks startup either way.
|
||||
|
||||
## Data
|
||||
|
||||
```
|
||||
%APPDATA%\ipswap\presets.json presets
|
||||
%APPDATA%\ipswap\config.json settings
|
||||
%APPDATA%\ipswap\ipswap.log rotating, 5 × 1 MB
|
||||
```
|
||||
|
||||
`presets.json` is the export format too, so a preset pack is just this file and
|
||||
stays hand-editable. See `examples/preset-pack.json`.
|
||||
|
||||
Masks are stored as an integer prefix. Both `/24` and `255.255.255.0` are
|
||||
accepted on input, in the editor and in a hand-written file; the display style
|
||||
is a single global setting.
|
||||
|
||||
## Why applies are destructive
|
||||
|
||||
After an apply, the adapter has exactly the addresses in the preset and nothing
|
||||
else.
|
||||
|
||||
`netsh interface ipv4 set address … static` replaces the primary address but
|
||||
leaves previously-added secondary addresses attached. A naive implementation
|
||||
therefore leaks addresses across switches — after visiting three presets the
|
||||
adapter is still carrying secondaries from the first two. So every apply
|
||||
enumerates the live addresses, deletes each one, then sets the primary and adds
|
||||
the preset's secondaries. `internal/netcfg/plan_test.go` pins that ordering.
|
||||
|
||||
Reads use `GetAdaptersAddresses`, not `netsh show config`: netsh's output is
|
||||
localised and parsing it breaks on a non-English Windows.
|
||||
|
||||
## Updates
|
||||
|
||||
Set an update repository in Settings (`https://host/owner/repo`) and ipswap
|
||||
checks its Gitea releases API at startup, in a goroutine, behind a short
|
||||
timeout, failing silently to the log — a laptop on a customer site usually
|
||||
cannot reach the host, and that is not an error worth showing.
|
||||
|
||||
A newer tag adds "Update available — vX.Y.Z" to the top of the tray menu.
|
||||
Clicking it downloads the `.exe`, verifies its SHA256 against the release, and
|
||||
hands off to a small batch helper that waits for ipswap to exit, swaps the
|
||||
binary and relaunches. **A release that publishes no checksum is refused** — an
|
||||
unverified binary that is about to be run is not worth the convenience. It
|
||||
never installs on its own.
|
||||
|
||||
## Not in v1
|
||||
|
||||
Revert / restore-previous / timed auto-revert; post-apply connectivity checks
|
||||
(the confirmation diff is the check); global hotkeys; IPv6; adapter matching by
|
||||
MAC address; per-preset scripts.
|
||||
Reference in New Issue
Block a user