diff --git a/internal/firmware/native/native.go b/internal/firmware/native/native.go index 93b3f8ed..74b78686 100644 --- a/internal/firmware/native/native.go +++ b/internal/firmware/native/native.go @@ -108,6 +108,31 @@ func (n *Native) PID() int { return n.cmd.Process.Pid } +// args is the child's command line. +// +// The seed goes down as its low 32 bits. That is all the firmware ever read: +// the bridge parses --seed with strtoul into a uint32_t, and where a long is +// 64 bits the cast keeps the low word. Where a long is 32 bits - every Windows +// target - strtoul saturates at ULONG_MAX instead, so every node whose seed +// was above 2^32 was seeded 0xFFFFFFFF, and 57 of 58 nodes booted with one +// identity, private key included. Node 0's seed is the run seed, which fits, +// and was the one node apart. Masking here gives every platform the low +// word Linux and macOS always had, so no identity changes where the mesh +// already worked, and the published binaries need no rebuild to be right. +func (n *Native) args(bridgeAddr string) []string { + args := []string{"--bridge", bridgeAddr, "--seed", fmt.Sprint(uint32(n.Seed))} + if n.SF != 0 { + args = append(args, "--sf", fmt.Sprint(n.SF)) + } + if n.BandwidthKHz != 0 { + args = append(args, "--bw-khz", strconv.FormatFloat(n.BandwidthKHz, 'f', -1, 64)) + } + if n.CodingRate != 0 { + args = append(args, "--cr", fmt.Sprint(n.CodingRate)) + } + return args +} + func (n *Native) Start(ctx context.Context, bridgeAddr string) (err error) { path, err := firmware.FindNative(n.Path, n.Role) if err != nil { @@ -118,17 +143,7 @@ func (n *Native) Start(ctx context.Context, bridgeAddr string) (err error) { if n.cmd != nil { return errors.New("firmware: native node already started") } - args := []string{"--bridge", bridgeAddr, "--seed", fmt.Sprint(n.Seed)} - if n.SF != 0 { - args = append(args, "--sf", fmt.Sprint(n.SF)) - } - if n.BandwidthKHz != 0 { - args = append(args, "--bw-khz", strconv.FormatFloat(n.BandwidthKHz, 'f', -1, 64)) - } - if n.CodingRate != 0 { - args = append(args, "--cr", fmt.Sprint(n.CodingRate)) - } - cmd := exec.CommandContext(ctx, path, args...) + cmd := exec.CommandContext(ctx, path, n.args(bridgeAddr)...) // The same attributes the emulated backend gives its children: a // parent-death signal where the platform has one, and no console window // on Windows, where every node is a console program and a national diff --git a/internal/firmware/native/seed_internal_test.go b/internal/firmware/native/seed_internal_test.go new file mode 100644 index 00000000..f710a10c --- /dev/null +++ b/internal/firmware/native/seed_internal_test.go @@ -0,0 +1,46 @@ +package native + +import ( + "math" + "strconv" + "testing" +) + +// The engine strides node seeds by a 64-bit golden-ratio constant, so every +// node past the first carries a seed above 2^32. The bridge parses --seed with +// strtoul into a uint32_t, and on Windows, where a long is 32 bits, strtoul +// saturates: 57 of 58 nodes were seeded 0xFFFFFFFF and booted with one +// identity, private key included. What goes down the command line has to be +// a number that parser cannot saturate on, and it has to be the low word +// Linux and macOS were already keeping, so nothing changes where it worked. +func TestTheSeedOnTheCommandLineFitsTheFirmwaresParser(t *testing.T) { + // The engine's stride, as a variable so the arithmetic wraps the way the + // engine's does rather than failing as a constant expression. + var stride uint64 = 0x9E3779B97F4A7C15 + seeds := []uint64{4417, 9001 + 1*stride, 9001 + 57*stride, math.MaxUint64} + for _, seed := range seeds { + want := seed & math.MaxUint32 + n := &Native{Seed: seed} + args := n.args("127.0.0.1:1") + var got string + for i, a := range args { + if a == "--seed" && i+1 < len(args) { + got = args[i+1] + } + } + v, err := strconv.ParseUint(got, 10, 32) + if err != nil { + t.Fatalf("seed %d went down as %q, which a 32-bit parser refuses: %v", seed, got, err) + } + if v != want { + t.Errorf("seed %d went down as %d, want its low word %d", seed, v, want) + } + } + // And two nodes of one run still get two seeds: the stride's low word is + // as distinct as its high one. + a := (&Native{Seed: 9001 + 1*stride}).args("x")[3] + b := (&Native{Seed: 9001 + 2*stride}).args("x")[3] + if a == b { + t.Fatalf("two nodes went down with one seed %s", a) + } +}