Add nanoswitch and cluster testing

Adds nanoswitch and the `switched-multi2` launch target to launch two Smalltown instances on a switched
network and enroll them into a single cluster. Nanoswitch contains a Linux bridge and a minimal DHCP server
and connects to the two Smalltown instances over virtual Ethernet cables. Also moves out the DHCP client into
a package since nanoswitch needs it.

Test Plan:
Manually tested using `bazel run //:launch -- switched-multi2` and observing that the second VM
(whose serial port is mapped to stdout) prints that it is enrolled. Also validated by `bazel run //core/cmd/dbg -- kubectl get node -o wide` returning two ready nodes.

X-Origin-Diff: phab/D572
GitOrigin-RevId: 9f6e2b3d8268749dd81588205646ae3976ad14b3
diff --git a/core/internal/common/setup.go b/core/internal/common/setup.go
index db00692..531b688 100644
--- a/core/internal/common/setup.go
+++ b/core/internal/common/setup.go
@@ -16,6 +16,13 @@
 
 package common
 
+import (
+	"crypto/ed25519"
+	"encoding/hex"
+	"errors"
+	"strings"
+)
+
 type (
 	SmalltownState string
 )
@@ -38,3 +45,14 @@
 	// Node is fully provisioned.
 	StateJoined SmalltownState = "enrolled"
 )
+
+func NameFromIDKey(pubKey ed25519.PublicKey) string {
+	return "smalltown-" + hex.EncodeToString(pubKey[:16])
+}
+
+func IDKeyPrefixFromName(name string) ([]byte, error) {
+	if !strings.HasPrefix(name, "smalltown-") {
+		return []byte{}, errors.New("invalid name")
+	}
+	return hex.DecodeString(strings.TrimPrefix(name, "smalltown-"))
+}