From b4a58d150864f059728b35b058fedd72dc443d3e Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Thu, 12 Dec 2024 20:51:13 +0100 Subject: [PATCH] Make nebula config generation deterministic --- go/daemon/children/nebula.go | 26 ++++++++++++---------- go/yamlutil/ordered_map.go | 39 +++++++++++++++++++++++++++++++++ go/yamlutil/ordered_map_test.go | 20 +++++++++++++++++ 3 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 go/yamlutil/ordered_map.go create mode 100644 go/yamlutil/ordered_map_test.go diff --git a/go/daemon/children/nebula.go b/go/daemon/children/nebula.go index d8dff6a..3b4e6f7 100644 --- a/go/daemon/children/nebula.go +++ b/go/daemon/children/nebula.go @@ -8,6 +8,7 @@ import ( "isle/daemon/daecommon" "isle/nebula" "isle/toolkit" + "isle/yamlutil" "net" "path/filepath" "strconv" @@ -51,19 +52,18 @@ func waitForNebula( return ctx.Err() } -// TODO this needs to be produce a deterministic config value. func nebulaConfig( networkConfig daecommon.NetworkConfig, hostBootstrap bootstrap.Bootstrap, ) ( - map[string]any, error, + any, error, ) { var ( lighthouseHostIPs []string - staticHostMap = map[string][]string{} + staticHostMap = yamlutil.OrderedMap[string, []string]{} ) - for _, host := range hostBootstrap.Hosts { + for _, host := range hostBootstrap.HostsOrdered() { if host.Nebula.PublicAddr == "" { continue @@ -105,18 +105,20 @@ func nebulaConfig( ) } - config := map[string]any{ - "pki": map[string]string{ + type m = yamlutil.OrderedMap[string, any] + + config := m{ + "pki": m{ "ca": string(caCertPEM), "cert": string(hostCertPEM), "key": string(hostKeyPEM), }, "static_host_map": staticHostMap, - "punchy": map[string]bool{ + "punchy": m{ "punch": true, "respond": true, }, - "tun": map[string]any{ + "tun": m{ "dev": nebula.GetDeviceName(hostBootstrap.NetworkCreationParams.ID), }, "firewall": firewall, @@ -124,12 +126,12 @@ func nebulaConfig( if publicAddr := networkConfig.VPN.PublicAddr; publicAddr == "" { - config["listen"] = map[string]string{ + config["listen"] = m{ "host": "0.0.0.0", "port": "0", } - config["lighthouse"] = map[string]any{ + config["lighthouse"] = m{ "hosts": lighthouseHostIPs, } @@ -149,12 +151,12 @@ func nebulaConfig( host = "0.0.0.0" } - config["listen"] = map[string]string{ + config["listen"] = m{ "host": host, "port": port, } - config["lighthouse"] = map[string]any{ + config["lighthouse"] = m{ "hosts": []string{}, "am_lighthouse": true, } diff --git a/go/yamlutil/ordered_map.go b/go/yamlutil/ordered_map.go new file mode 100644 index 0000000..0a7a4ef --- /dev/null +++ b/go/yamlutil/ordered_map.go @@ -0,0 +1,39 @@ +package yamlutil + +import ( + "cmp" + "slices" + + "gopkg.in/yaml.v3" +) + +// OrderedMap is like a normal map, except that when it is marshaled to yaml it +// will do so with its keys in ascending order. This makes it useful when +// generating output which needs to be deterministic. +type OrderedMap[K comparable, V any] map[K]V + +func (m OrderedMap[K, V]) MarshalYAML() (any, error) { + type wrapped map[K]V + + var n yaml.Node + if err := n.Encode(wrapped(m)); err != nil { + return nil, err + } + + pairs := make([][2]*yaml.Node, len(n.Content)/2) + for i := range pairs { + pairs[i][0] = n.Content[i*2] + pairs[i][1] = n.Content[i*2+1] + } + + slices.SortFunc(pairs, func(a, b [2]*yaml.Node) int { + return cmp.Compare(a[0].Value, b[0].Value) + }) + + for i := range pairs { + n.Content[i*2] = pairs[i][0] + n.Content[i*2+1] = pairs[i][1] + } + + return n, nil +} diff --git a/go/yamlutil/ordered_map_test.go b/go/yamlutil/ordered_map_test.go new file mode 100644 index 0000000..a8cb942 --- /dev/null +++ b/go/yamlutil/ordered_map_test.go @@ -0,0 +1,20 @@ +package yamlutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestOrderedMap(t *testing.T) { + m := OrderedMap[string, int]{ + "a": 1, + "b": 2, + "c": 3, + } + + b, err := yaml.Marshal(m) + assert.NoError(t, err) + assert.Equal(t, "a: 1\nb: 2\nc: 3\n", string(b)) +}