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,71 @@
|
||||
package preset
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParsePrefix accepts every spelling of a subnet mask the editor is required to
|
||||
// take — "24", "/24" and "255.255.255.0" — and returns the prefix length.
|
||||
//
|
||||
// Only contiguous masks are accepted. 255.255.0.255 is a typo, not a netmask,
|
||||
// and letting it through would produce a netsh call that fails confusingly.
|
||||
func ParsePrefix(s string) (int, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return 0, fmt.Errorf("empty mask")
|
||||
}
|
||||
s = strings.TrimPrefix(s, "/")
|
||||
|
||||
if strings.Contains(s, ".") {
|
||||
addr, err := netip.ParseAddr(s)
|
||||
if err != nil || !addr.Is4() {
|
||||
return 0, fmt.Errorf("%q is not a dotted-quad netmask", s)
|
||||
}
|
||||
b := addr.As4()
|
||||
mask := uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
|
||||
|
||||
ones := 0
|
||||
for ones < 32 && mask&(1<<uint(31-ones)) != 0 {
|
||||
ones++
|
||||
}
|
||||
// Everything below the leading run of ones must be zero.
|
||||
if ones < 32 && mask<<uint(ones) != 0 {
|
||||
return 0, fmt.Errorf("%q is not a contiguous netmask", s)
|
||||
}
|
||||
return ones, nil
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%q is not a prefix length", s)
|
||||
}
|
||||
if n < 0 || n > 32 {
|
||||
return 0, fmt.Errorf("prefix /%d out of range", n)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// MaskString renders a prefix length as the dotted-quad netmask netsh expects
|
||||
// on the command line. netsh will not take "/24".
|
||||
func MaskString(prefix int) string {
|
||||
if prefix < 0 || prefix > 32 {
|
||||
prefix = 32
|
||||
}
|
||||
var mask uint32
|
||||
if prefix > 0 {
|
||||
mask = ^uint32(0) << uint(32-prefix)
|
||||
}
|
||||
return fmt.Sprintf("%d.%d.%d.%d", mask>>24&0xff, mask>>16&0xff, mask>>8&0xff, mask&0xff)
|
||||
}
|
||||
|
||||
// FormatPrefix renders a prefix in whichever notation the user last chose.
|
||||
// dotted controls the style; see config.Settings.MaskStyle.
|
||||
func FormatPrefix(prefix int, dotted bool) string {
|
||||
if dotted {
|
||||
return MaskString(prefix)
|
||||
}
|
||||
return "/" + strconv.Itoa(prefix)
|
||||
}
|
||||
Reference in New Issue
Block a user