m/node/allocs: split new package off m/node

The metropolis/node package contains various unrelated things. One of
these things are node-wide allocations of identifiers.
These are moved here to the new metropolis/node/allocs package.
Additionally, the constants are renamed to move the type of identifier
in front.

Change-Id: Ibc841ca64d4450a7054463e6d2d44753a118e07a
Reviewed-on: https://review.monogon.dev/c/monogon/+/4566
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/cli/metroctl/BUILD.bazel b/metropolis/cli/metroctl/BUILD.bazel
index b280aea..826a90b 100644
--- a/metropolis/cli/metroctl/BUILD.bazel
+++ b/metropolis/cli/metroctl/BUILD.bazel
@@ -82,6 +82,7 @@
         "//metropolis/cli/flagdefs",
         "//metropolis/cli/metroctl/core",
         "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/rpc",
         "//metropolis/node/core/rpc/resolver",
         "//metropolis/proto/api",
diff --git a/metropolis/cli/metroctl/cmd_node_metrics.go b/metropolis/cli/metroctl/cmd_node_metrics.go
index 648fe60..8e2e51f 100644
--- a/metropolis/cli/metroctl/cmd_node_metrics.go
+++ b/metropolis/cli/metroctl/cmd_node_metrics.go
@@ -13,7 +13,7 @@
 	"github.com/spf13/cobra"
 
 	"source.monogon.dev/metropolis/cli/metroctl/core"
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/proto/api"
 )
 
@@ -76,7 +76,7 @@
 		client := http.Client{
 			Transport: transport,
 		}
-		res, err := client.Get(fmt.Sprintf("https://%s/metrics/%s", net.JoinHostPort(n.Status.ExternalAddress, common.MetricsPort.PortString()), args[1]))
+		res, err := client.Get(fmt.Sprintf("https://%s/metrics/%s", net.JoinHostPort(n.Status.ExternalAddress, allocs.PortMetrics.PortString()), args[1]))
 		if err != nil {
 			return fmt.Errorf("metrics HTTP request failed: %w", err)
 		}
diff --git a/metropolis/cli/metroctl/core/BUILD.bazel b/metropolis/cli/metroctl/core/BUILD.bazel
index e9c2117..46520c2 100644
--- a/metropolis/cli/metroctl/core/BUILD.bazel
+++ b/metropolis/cli/metroctl/core/BUILD.bazel
@@ -14,7 +14,7 @@
     deps = [
         "//go/logging",
         "//metropolis/installer/install",
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/curator/proto/api",
         "//metropolis/node/core/rpc",
         "//metropolis/node/core/rpc/resolver",
diff --git a/metropolis/cli/metroctl/core/config.go b/metropolis/cli/metroctl/core/config.go
index 38862af..71283b0 100644
--- a/metropolis/cli/metroctl/core/config.go
+++ b/metropolis/cli/metroctl/core/config.go
@@ -24,7 +24,7 @@
 	clientapi "k8s.io/client-go/tools/clientcmd/api"
 
 	"source.monogon.dev/go/logging"
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 )
 
 const (
@@ -233,7 +233,7 @@
 
 	var u url.URL
 	u.Scheme = "https"
-	u.Host = net.JoinHostPort(server, node.KubernetesAPIWrappedPort.PortString())
+	u.Host = net.JoinHostPort(server, allocs.PortKubernetesAPIWrapped.PortString())
 
 	// HACK: the Metropolis node certificates only contain the node ID as a SAN. This
 	// means that we can't use some 'global' identifier as the TLSServerName below
diff --git a/metropolis/cli/metroctl/core/rpc.go b/metropolis/cli/metroctl/core/rpc.go
index a8f933c..cd5678d 100644
--- a/metropolis/cli/metroctl/core/rpc.go
+++ b/metropolis/cli/metroctl/core/rpc.go
@@ -15,7 +15,7 @@
 	"golang.org/x/net/proxy"
 	"google.golang.org/grpc"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/rpc"
 	"source.monogon.dev/metropolis/node/core/rpc/resolver"
 	"source.monogon.dev/metropolis/proto/api"
@@ -45,7 +45,7 @@
 		return nil, fmt.Errorf("no cluster endpoints specified")
 	}
 	for _, eps := range c.Endpoints {
-		ep := resolver.NodeByHostPort(eps, uint16(node.CuratorServicePort))
+		ep := resolver.NodeByHostPort(eps, uint16(allocs.PortCuratorService))
 		r.AddEndpoint(ep)
 	}
 	opts = append(opts, grpc.WithResolvers(r))
@@ -76,7 +76,7 @@
 	creds := rpc.NewAuthenticatedCredentials(tlsc, rpc.WantRemoteCluster(ca), rpc.WantRemoteNode(nodeId))
 	dialOpts = append(dialOpts, grpc.WithTransportCredentials(creds))
 
-	endpoint := net.JoinHostPort(nodeAddr, node.NodeManagementPort.PortString())
+	endpoint := net.JoinHostPort(nodeAddr, allocs.PortNodeManagement.PortString())
 	return grpc.NewClient(endpoint, dialOpts...)
 }
 
diff --git a/metropolis/node/BUILD.bazel b/metropolis/node/BUILD.bazel
index 6168e48..e69d00a 100644
--- a/metropolis/node/BUILD.bazel
+++ b/metropolis/node/BUILD.bazel
@@ -9,19 +9,12 @@
 go_library(
     name = "node",
     srcs = [
-        "ids.go",
         "labels.go",
-        "net_ips.go",
-        "net_protocols.go",
         "net_status.go",
-        "ports.go",
         "validation.go",
     ],
     importpath = "source.monogon.dev/metropolis/node",
-    visibility = [
-        "//metropolis:__subpackages__",
-        "@io_k8s_kubernetes//pkg/registry:__subpackages__",
-    ],
+    visibility = ["//metropolis:__subpackages__"],
     deps = ["//metropolis/proto/common"],
 )
 
diff --git a/metropolis/node/allocs/BUILD.bazel b/metropolis/node/allocs/BUILD.bazel
new file mode 100644
index 0000000..f2ead45
--- /dev/null
+++ b/metropolis/node/allocs/BUILD.bazel
@@ -0,0 +1,17 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "allocs",
+    srcs = [
+        "doc.go",
+        "ids.go",
+        "net_ips.go",
+        "net_protocols.go",
+        "ports.go",
+    ],
+    importpath = "source.monogon.dev/metropolis/node/allocs",
+    visibility = [
+        "//metropolis:__subpackages__",
+        "@io_k8s_kubernetes//pkg/registry:__subpackages__",
+    ],
+)
diff --git a/metropolis/node/allocs/doc.go b/metropolis/node/allocs/doc.go
new file mode 100644
index 0000000..dcc6960
--- /dev/null
+++ b/metropolis/node/allocs/doc.go
@@ -0,0 +1,6 @@
+// Copyright The Monogon Project Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package allocs contains allocations of various types of identifiers used in a
+// node. By tracking these centrally, we can avoid collisions.
+package allocs
diff --git a/metropolis/node/ids.go b/metropolis/node/allocs/ids.go
similarity index 80%
rename from metropolis/node/ids.go
rename to metropolis/node/allocs/ids.go
index c9a9aa0..51d74fd 100644
--- a/metropolis/node/ids.go
+++ b/metropolis/node/allocs/ids.go
@@ -1,11 +1,11 @@
 // Copyright The Monogon Project Authors.
 // SPDX-License-Identifier: Apache-2.0
 
-package node
+package allocs
 
 // These are UID/GID constants for components inside the Metropolis node
 // code.
 const (
-	RootUid = 0
-	TimeUid = 100
+	UidRoot = 0
+	UidTime = 100
 )
diff --git a/metropolis/node/net_ips.go b/metropolis/node/allocs/net_ips.go
similarity index 83%
rename from metropolis/node/net_ips.go
rename to metropolis/node/allocs/net_ips.go
index 415f84d..b0d19a0 100644
--- a/metropolis/node/net_ips.go
+++ b/metropolis/node/allocs/net_ips.go
@@ -1,7 +1,7 @@
 // Copyright The Monogon Project Authors.
 // SPDX-License-Identifier: Apache-2.0
 
-package node
+package allocs
 
 import "net"
 
@@ -9,5 +9,5 @@
 var (
 	// Used by //metropolis/node/kubernetes as the DNS server IP for containers.
 	// Link-local IP space, 77 for ASCII M(onogon), 53 for DNS port.
-	ContainerDNSIP = net.IPv4(169, 254, 77, 53)
+	IPContainerDNS = net.IPv4(169, 254, 77, 53)
 )
diff --git a/metropolis/node/net_protocols.go b/metropolis/node/allocs/net_protocols.go
similarity index 97%
rename from metropolis/node/net_protocols.go
rename to metropolis/node/allocs/net_protocols.go
index c4c3f64..360f763 100644
--- a/metropolis/node/net_protocols.go
+++ b/metropolis/node/allocs/net_protocols.go
@@ -1,7 +1,7 @@
 // Copyright The Monogon Project Authors.
 // SPDX-License-Identifier: Apache-2.0
 
-package node
+package allocs
 
 // These are netlink protocol numbers used internally for various netlink
 // resource (e.g. route) owners/manager.
diff --git a/metropolis/node/allocs/ports.go b/metropolis/node/allocs/ports.go
new file mode 100644
index 0000000..4595733
--- /dev/null
+++ b/metropolis/node/allocs/ports.go
@@ -0,0 +1,130 @@
+// Copyright The Monogon Project Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package allocs
+
+import (
+	"strconv"
+)
+
+// Port is a TCP and/or UDP port number reserved for and used by Metropolis
+// node code.
+type Port uint16
+
+const (
+	// PortCuratorService is the TCP port on which the Curator listens for gRPC
+	// calls and services Management/AAA/Curator RPCs.
+	PortCuratorService Port = 7835
+	// PortConsensus is the TCP port on which etcd listens for peer traffic.
+	PortConsensus Port = 7834
+	// PortDebugService is the TCP port on which the debug service serves gRPC
+	// traffic. This is only available in debug builds.
+	PortDebugService Port = 7837
+	// PortWireGuard is the UDP port on which the Wireguard Kubernetes network
+	// overlay listens for incoming peer traffic.
+	PortWireGuard Port = 7838
+	// PortNodeManagement is the TCP port on which the node-local management service
+	// serves gRPC traffic for NodeManagement.
+	PortNodeManagement Port = 7839
+	// PortMetrics is the TCP port on which the Metrics Service exports
+	// Prometheus-compatible metrics for this node, secured using TLS and the
+	// Cluster/Node certificates.
+	PortMetrics Port = 7840
+	// PortMetricsNodeListener is the TCP port on which the Prometheus node_exporter
+	// runs, bound to 127.0.0.1. The Metrics Service proxies traffic to it from the
+	// public PortMetrics.
+	PortMetricsNodeListener Port = 7841
+	// PortMetricsEtcdListener is the TCP port on which the etcd exporter
+	// runs, bound to 127.0.0.1. The metrics service proxies traffic to it from the
+	// public PortMetrics.
+	PortMetricsEtcdListener Port = 7842
+	// PortMetricsKubeSchedulerListener is the TCP port on which the proxy for
+	// the kube-scheduler runs, bound to 127.0.0.1. The metrics service proxies
+	// traffic to it from the public PortMetrics.
+	PortMetricsKubeSchedulerListener Port = 7843
+	// PortMetricsKubeControllerManagerListener is the TCP port on which the
+	// proxy for the controller-manager runs, bound to 127.0.0.1. The metrics
+	// service proxies traffic to it from the public PortMetrics.
+	PortMetricsKubeControllerManagerListener Port = 7844
+	// PortMetricsKubeAPIServerListener is the TCP port on which the
+	// proxy for the api-server runs, bound to 127.0.0.1. The metrics
+	// service proxies traffic to it from the public PortMetrics.
+	PortMetricsKubeAPIServerListener Port = 7845
+	// PortMetricsContainerdListener is the TCP port on which the
+	// containerd metrics endpoint, bound to 127.0.0.1, is exposed.
+	PortMetricsContainerdListener Port = 7846
+	// PortKubernetesAPI is the TCP port on which the Kubernetes API is
+	// exposed.
+	PortKubernetesAPI Port = 6443
+	// PortKubernetesAPIWrapped is the TCP port on which the Metropolis
+	// authenticating proxy for the Kubernetes API is exposed.
+	PortKubernetesAPIWrapped Port = 6444
+	// PortKubernetesWorkerLocalAPI is the TCP port on which Kubernetes worker nodes
+	// run a loadbalancer to access the cluster's API servers before cluster
+	// networking is available. This port is only bound to 127.0.0.1.
+	PortKubernetesWorkerLocalAPI Port = 6445
+	// PortDebugger is the port on which the delve debugger runs (on debug
+	// builds only). Not to be confused with PortDebugService.
+	PortDebugger Port = 2345
+)
+
+var SystemPorts = []Port{
+	PortCuratorService,
+	PortConsensus,
+	PortDebugService,
+	PortWireGuard,
+	PortNodeManagement,
+	PortMetrics,
+	PortMetricsNodeListener,
+	PortMetricsEtcdListener,
+	PortMetricsKubeSchedulerListener,
+	PortMetricsKubeControllerManagerListener,
+	PortMetricsKubeAPIServerListener,
+	PortMetricsContainerdListener,
+	PortKubernetesAPI,
+	PortKubernetesAPIWrapped,
+	PortKubernetesWorkerLocalAPI,
+	PortDebugger,
+}
+
+func (p Port) String() string {
+	switch p {
+	case PortCuratorService:
+		return "curator"
+	case PortConsensus:
+		return "consensus"
+	case PortDebugService:
+		return "debug"
+	case PortWireGuard:
+		return "wireguard"
+	case PortNodeManagement:
+		return "node-mgmt"
+	case PortMetrics:
+		return "metrics"
+	case PortMetricsNodeListener:
+		return "metrics-node-exporter"
+	case PortMetricsEtcdListener:
+		return "metrics-etcd"
+	case PortMetricsKubeSchedulerListener:
+		return "metrics-kubernetes-scheduler"
+	case PortMetricsKubeControllerManagerListener:
+		return "metrics-kubernetes-controller-manager"
+	case PortMetricsKubeAPIServerListener:
+		return "metrics-kubernetes-api-server"
+	case PortMetricsContainerdListener:
+		return "metrics-containerd"
+	case PortKubernetesAPI:
+		return "kubernetes-api"
+	case PortKubernetesAPIWrapped:
+		return "kubernetes-api-wrapped"
+	case PortKubernetesWorkerLocalAPI:
+		return "kubernetes-worker-local-api"
+	case PortDebugger:
+		return "delve"
+	}
+	return "unknown"
+}
+
+func (p Port) PortString() string {
+	return strconv.Itoa(int(p))
+}
diff --git a/metropolis/node/core/BUILD.bazel b/metropolis/node/core/BUILD.bazel
index db96103..6fdf353 100644
--- a/metropolis/node/core/BUILD.bazel
+++ b/metropolis/node/core/BUILD.bazel
@@ -22,7 +22,7 @@
     visibility = ["//visibility:private"],
     deps = [
         "//go/logging",
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/cluster",
         "//metropolis/node/core/devmgr",
         "//metropolis/node/core/localstorage",
diff --git a/metropolis/node/core/consensus/BUILD.bazel b/metropolis/node/core/consensus/BUILD.bazel
index 5c58c2a..6532f4f 100644
--- a/metropolis/node/core/consensus/BUILD.bazel
+++ b/metropolis/node/core/consensus/BUILD.bazel
@@ -13,7 +13,7 @@
     visibility = ["//:__subpackages__"],
     deps = [
         "//go/logging",
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/consensus/client",
         "//metropolis/node/core/localstorage",
         "//osbase/event",
diff --git a/metropolis/node/core/consensus/configuration.go b/metropolis/node/core/consensus/configuration.go
index 8c4bd06..dfd70f2 100644
--- a/metropolis/node/core/consensus/configuration.go
+++ b/metropolis/node/core/consensus/configuration.go
@@ -15,7 +15,7 @@
 	clientv3 "go.etcd.io/etcd/client/v3"
 	"go.etcd.io/etcd/server/v3/embed"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/localstorage"
 	"source.monogon.dev/osbase/pki"
 )
@@ -90,7 +90,7 @@
 // over TLS. This requires TLS credentials to be present on disk, and will be
 // disabled for bootstrapping the instance.
 func (c *Config) build(enablePeers bool) *embed.Config {
-	port := int(node.ConsensusPort)
+	port := int(allocs.PortConsensus)
 	if p := c.testOverrides.externalPort; p != 0 {
 		port = p
 	}
@@ -98,7 +98,7 @@
 	if c.testOverrides.externalAddress != "" {
 		host = c.testOverrides.externalAddress
 	}
-	etcdPort := int(node.MetricsEtcdListenerPort)
+	etcdPort := int(allocs.PortMetricsEtcdListener)
 	if p := c.testOverrides.etcdMetricsPort; p != 0 {
 		etcdPort = p
 	}
diff --git a/metropolis/node/core/consensus/consensus.go b/metropolis/node/core/consensus/consensus.go
index f6addd8..f00a7a9 100644
--- a/metropolis/node/core/consensus/consensus.go
+++ b/metropolis/node/core/consensus/consensus.go
@@ -33,12 +33,12 @@
 //   | node-foo            |
 //   |---------------------|
 //   | .--------------------.
-//   | | etcd               |<---etcd/TLS--.   (node.ConsensusPort)
+//   | | etcd               |<---etcd/TLS--.   (allocs.PortConsensus)
 //   | '--------------------'              |
 //   |     ^ Domain Socket |               |
 //   |     | etcd/plain    |               |
 //   | .--------------------.              |
-//   | | curator            |<---gRPC/TLS----. (node.CuratorServicePort)
+//   | | curator            |<---gRPC/TLS----. (allocs.PortCuratorService)
 //   | '--------------------'              | |
 //   |     ^ Domain Socket |               | |
 //   |     | gRPC/plain    |               | |
diff --git a/metropolis/node/core/consensus/status.go b/metropolis/node/core/consensus/status.go
index 0886531..5bf0416 100644
--- a/metropolis/node/core/consensus/status.go
+++ b/metropolis/node/core/consensus/status.go
@@ -13,7 +13,7 @@
 
 	clientv3 "go.etcd.io/etcd/client/v3"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/consensus/client"
 	"source.monogon.dev/osbase/event"
 	"source.monogon.dev/osbase/pki"
@@ -103,7 +103,7 @@
 
 	var extraNames []string
 	name := nodeID
-	port := int(node.ConsensusPort)
+	port := int(allocs.PortConsensus)
 	for _, opt := range opts {
 		if opt.externalAddress != "" {
 			name = opt.externalAddress
diff --git a/metropolis/node/core/curator/BUILD.bazel b/metropolis/node/core/curator/BUILD.bazel
index 4ff34f1..42cdffa 100644
--- a/metropolis/node/core/curator/BUILD.bazel
+++ b/metropolis/node/core/curator/BUILD.bazel
@@ -26,6 +26,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/consensus",
         "//metropolis/node/core/consensus/client",
         "//metropolis/node/core/curator/proto/api",
@@ -72,7 +73,7 @@
     ],
     embed = [":curator"],
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/consensus",
         "//metropolis/node/core/consensus/client",
         "//metropolis/node/core/curator/proto/api",
diff --git a/metropolis/node/core/curator/impl_follower.go b/metropolis/node/core/curator/impl_follower.go
index 96a9385..b6d59e6 100644
--- a/metropolis/node/core/curator/impl_follower.go
+++ b/metropolis/node/core/curator/impl_follower.go
@@ -9,7 +9,7 @@
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/consensus/client"
 	cpb "source.monogon.dev/metropolis/node/core/curator/proto/api"
 	"source.monogon.dev/metropolis/node/core/identity"
@@ -73,7 +73,7 @@
 		err = srv.Send(&cpb.GetCurrentLeaderResponse{
 			LeaderNodeId: lock.NodeId,
 			LeaderHost:   node.status.ExternalAddress,
-			LeaderPort:   int32(common.CuratorServicePort),
+			LeaderPort:   int32(allocs.PortCuratorService),
 			ThisNodeId:   f.followerID,
 		})
 		if err != nil {
diff --git a/metropolis/node/core/curator/impl_leader_curator.go b/metropolis/node/core/curator/impl_leader_curator.go
index bcf7d42..8fa651f 100644
--- a/metropolis/node/core/curator/impl_leader_curator.go
+++ b/metropolis/node/core/curator/impl_leader_curator.go
@@ -20,6 +20,7 @@
 	tpb "google.golang.org/protobuf/types/known/timestamppb"
 
 	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/consensus"
 	ipb "source.monogon.dev/metropolis/node/core/curator/proto/api"
 	"source.monogon.dev/metropolis/node/core/identity"
@@ -601,7 +602,7 @@
 	err = srv.Send(&ipb.GetCurrentLeaderResponse{
 		LeaderNodeId: l.leaderID,
 		LeaderHost:   host,
-		LeaderPort:   int32(common.CuratorServicePort),
+		LeaderPort:   int32(allocs.PortCuratorService),
 		ThisNodeId:   l.leaderID,
 	})
 	if err != nil {
diff --git a/metropolis/node/core/curator/impl_leader_test.go b/metropolis/node/core/curator/impl_leader_test.go
index 7e31aeb..1621be6 100644
--- a/metropolis/node/core/curator/impl_leader_test.go
+++ b/metropolis/node/core/curator/impl_leader_test.go
@@ -30,7 +30,7 @@
 	"google.golang.org/protobuf/types/known/timestamppb"
 	"k8s.io/utils/ptr"
 
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/consensus"
 	"source.monogon.dev/metropolis/node/core/consensus/client"
 	ipb "source.monogon.dev/metropolis/node/core/curator/proto/api"
@@ -1416,7 +1416,7 @@
 	if want, got := cl.localNodeID, res.ThisNodeId; want != got {
 		t.Errorf("Wanted local node ID %q, got %q", want, got)
 	}
-	if want, got := int32(common.CuratorServicePort), res.LeaderPort; want != got {
+	if want, got := int32(allocs.PortCuratorService), res.LeaderPort; want != got {
 		t.Errorf("Wanted leader port %d, got %d", want, got)
 	}
 }
diff --git a/metropolis/node/core/curator/listener.go b/metropolis/node/core/curator/listener.go
index cb79cd8..114a02c 100644
--- a/metropolis/node/core/curator/listener.go
+++ b/metropolis/node/core/curator/listener.go
@@ -12,7 +12,7 @@
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/keepalive"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/consensus"
 	"source.monogon.dev/metropolis/node/core/consensus/client"
 	cpb "source.monogon.dev/metropolis/node/core/curator/proto/api"
@@ -89,7 +89,7 @@
 		PermitWithoutStream: true,
 	}))
 	srv := grpc.NewServer(opts...)
-	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", node.CuratorServicePort))
+	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", allocs.PortCuratorService))
 	if err != nil {
 		return fmt.Errorf("failed to listen on curator socket: %w", err)
 	}
diff --git a/metropolis/node/core/debug_service_enabled.go b/metropolis/node/core/debug_service_enabled.go
index ff20a55..9a9a02d 100644
--- a/metropolis/node/core/debug_service_enabled.go
+++ b/metropolis/node/core/debug_service_enabled.go
@@ -18,13 +18,13 @@
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/localstorage"
 	"source.monogon.dev/metropolis/node/core/mgmt"
 	"source.monogon.dev/metropolis/node/core/roleserve"
 	"source.monogon.dev/osbase/logtree"
 	"source.monogon.dev/osbase/supervisor"
 
-	common "source.monogon.dev/metropolis/node"
 	apb "source.monogon.dev/metropolis/proto/api"
 )
 
@@ -47,7 +47,7 @@
 	}
 	dbgSrv := grpc.NewServer()
 	apb.RegisterNodeDebugServiceServer(dbgSrv, dbg)
-	dbgLis, err := net.Listen("tcp", fmt.Sprintf(":%d", common.DebugServicePort))
+	dbgLis, err := net.Listen("tcp", fmt.Sprintf(":%d", allocs.PortDebugService))
 	if err != nil {
 		return fmt.Errorf("failed to listen on debug service: %w", err)
 	}
diff --git a/metropolis/node/core/delve_enabled.go b/metropolis/node/core/delve_enabled.go
index 54a64c7..038d1ac 100644
--- a/metropolis/node/core/delve_enabled.go
+++ b/metropolis/node/core/delve_enabled.go
@@ -8,12 +8,12 @@
 	"fmt"
 	"os/exec"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/network"
 )
 
 // initializeDebugger attaches Delve to ourselves and exposes it on
-// common.DebuggerPort
+// allocs.PortDebugger
 // This is coupled to compilation_mode=dbg because otherwise Delve doesn't have
 // the necessary DWARF debug info
 func initializeDebugger(networkSvc *network.Service) {
@@ -27,7 +27,7 @@
 		if err != nil {
 			panic(err)
 		}
-		dlvCmd := exec.Command("/dlv", "--headless=true", fmt.Sprintf("--listen=:%v", node.DebuggerPort),
+		dlvCmd := exec.Command("/dlv", "--headless=true", fmt.Sprintf("--listen=:%v", allocs.PortDebugger),
 			"--accept-multiclient", "--only-same-user=false", "attach", "--continue", "1", "/init")
 		if err := dlvCmd.Start(); err != nil {
 			panic(err)
diff --git a/metropolis/node/core/metrics/BUILD.bazel b/metropolis/node/core/metrics/BUILD.bazel
index bc94863..861d59f 100644
--- a/metropolis/node/core/metrics/BUILD.bazel
+++ b/metropolis/node/core/metrics/BUILD.bazel
@@ -11,7 +11,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//go/types/mapsets",
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/curator/proto/api",
         "//metropolis/node/core/curator/watcher",
         "//metropolis/node/core/identity",
@@ -32,7 +32,7 @@
         "xFakeExporterPath": "$(rlocationpath //metropolis/node/core/metrics/fake_exporter )",
     },
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/curator/proto/api",
         "//metropolis/test/util",
         "//osbase/supervisor",
diff --git a/metropolis/node/core/metrics/exporters.go b/metropolis/node/core/metrics/exporters.go
index 003a690..fe7044d 100644
--- a/metropolis/node/core/metrics/exporters.go
+++ b/metropolis/node/core/metrics/exporters.go
@@ -11,7 +11,7 @@
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/osbase/supervisor"
 )
 
@@ -36,7 +36,7 @@
 	Gatherer prometheus.Gatherer
 	// Port on which an exporter is/will be running to which metrics requests will be
 	// proxied to. Exactly one of Gatherer or Port must be set.
-	Port node.Port
+	Port allocs.Port
 	// Executable to run to start the exporter. If empty, no executable will be
 	// started.
 	Executable string
@@ -59,10 +59,10 @@
 	},
 	{
 		Name:       "node",
-		Port:       node.MetricsNodeListenerPort,
+		Port:       allocs.PortMetricsNodeListener,
 		Executable: "/metrics/bin/node_exporter",
 		Arguments: []string{
-			"--web.listen-address=127.0.0.1:" + node.MetricsNodeListenerPort.PortString(),
+			"--web.listen-address=127.0.0.1:" + allocs.PortMetricsNodeListener.PortString(),
 			"--collector.buddyinfo",
 			"--collector.zoneinfo",
 			"--collector.tcpstat",
@@ -77,23 +77,23 @@
 	},
 	{
 		Name: "etcd",
-		Port: node.MetricsEtcdListenerPort,
+		Port: allocs.PortMetricsEtcdListener,
 	},
 	{
 		Name: "kubernetes-scheduler",
-		Port: node.MetricsKubeSchedulerListenerPort,
+		Port: allocs.PortMetricsKubeSchedulerListener,
 	},
 	{
 		Name: "kubernetes-controller-manager",
-		Port: node.MetricsKubeControllerManagerListenerPort,
+		Port: allocs.PortMetricsKubeControllerManagerListener,
 	},
 	{
 		Name: "kubernetes-apiserver",
-		Port: node.MetricsKubeAPIServerListenerPort,
+		Port: allocs.PortMetricsKubeAPIServerListener,
 	},
 	{
 		Name: "containerd",
-		Port: node.MetricsContainerdListenerPort,
+		Port: allocs.PortMetricsContainerdListener,
 		Path: "/v1/metrics",
 	},
 }
diff --git a/metropolis/node/core/metrics/metrics.go b/metropolis/node/core/metrics/metrics.go
index 9abed54..e547c5d 100644
--- a/metropolis/node/core/metrics/metrics.go
+++ b/metropolis/node/core/metrics/metrics.go
@@ -12,7 +12,7 @@
 	"net/http"
 	"os/exec"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/identity"
 	"source.monogon.dev/osbase/supervisor"
 )
@@ -29,7 +29,7 @@
 // Each exporter is exposed on a separate path, /metrics/<name>, where <name> is
 // the name of the exporter.
 //
-// The HTTPS listener is bound to node.MetricsPort.
+// The HTTPS listener is bound to allocs.PortMetrics.
 type Service struct {
 	// Credentials used to run the TLS/HTTPS listener and verify incoming
 	// connections.
@@ -67,7 +67,7 @@
 		// anyone/anything with a valid cluster certificate to access them.
 	}
 
-	addr := net.JoinHostPort("", node.MetricsPort.PortString())
+	addr := net.JoinHostPort("", allocs.PortMetrics.PortString())
 	if s.enableDynamicAddr {
 		addr = ""
 	}
diff --git a/metropolis/node/core/metrics/metrics_test.go b/metropolis/node/core/metrics/metrics_test.go
index ebb59a9..d494222 100644
--- a/metropolis/node/core/metrics/metrics_test.go
+++ b/metropolis/node/core/metrics/metrics_test.go
@@ -20,7 +20,7 @@
 
 	apb "source.monogon.dev/metropolis/node/core/curator/proto/api"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/test/util"
 	"source.monogon.dev/osbase/supervisor"
 	"source.monogon.dev/osbase/test/freeport"
@@ -51,7 +51,7 @@
 		panic(err)
 	}
 	defer closer.Close()
-	port := node.Port(p)
+	port := allocs.Port(p)
 
 	return &Exporter{
 		Name:       name,
diff --git a/metropolis/node/core/mgmt/BUILD.bazel b/metropolis/node/core/mgmt/BUILD.bazel
index b95565b..24d0439 100644
--- a/metropolis/node/core/mgmt/BUILD.bazel
+++ b/metropolis/node/core/mgmt/BUILD.bazel
@@ -11,7 +11,7 @@
     importpath = "source.monogon.dev/metropolis/node/core/mgmt",
     visibility = ["//visibility:public"],
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/identity",
         "//metropolis/node/core/rpc",
         "//metropolis/node/core/update",
diff --git a/metropolis/node/core/mgmt/mgmt.go b/metropolis/node/core/mgmt/mgmt.go
index 78961f2..8bfb7ed 100644
--- a/metropolis/node/core/mgmt/mgmt.go
+++ b/metropolis/node/core/mgmt/mgmt.go
@@ -13,7 +13,7 @@
 
 	"google.golang.org/grpc"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/identity"
 	"source.monogon.dev/metropolis/node/core/rpc"
 	"source.monogon.dev/metropolis/node/core/update"
@@ -54,7 +54,7 @@
 	}
 	logger := supervisor.MustSubLogger(ctx, "rpc")
 	opts := sec.GRPCOptions(logger)
-	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", node.NodeManagementPort))
+	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", allocs.PortNodeManagement))
 	if err != nil {
 		return fmt.Errorf("failed to listen on node management socket socket: %w", err)
 	}
diff --git a/metropolis/node/core/network/BUILD.bazel b/metropolis/node/core/network/BUILD.bazel
index 23fdd5d..a3f6066 100644
--- a/metropolis/node/core/network/BUILD.bazel
+++ b/metropolis/node/core/network/BUILD.bazel
@@ -16,6 +16,7 @@
         "//go/algorithm/toposort",
         "//go/logging",
         "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/network/ipam",
         "//metropolis/node/core/network/workloads",
         "//metropolis/node/core/productinfo",
diff --git a/metropolis/node/core/network/main.go b/metropolis/node/core/network/main.go
index 2dcf7bb..419c256 100644
--- a/metropolis/node/core/network/main.go
+++ b/metropolis/node/core/network/main.go
@@ -17,6 +17,7 @@
 	"github.com/vishvananda/netlink"
 
 	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/network/ipam"
 	"source.monogon.dev/metropolis/node/core/network/workloads"
 	"source.monogon.dev/osbase/event"
@@ -285,7 +286,7 @@
 			&expr.Cmp{
 				Op:       expr.CmpOpEq,
 				Register: 8,
-				Data:     binaryutil.NativeEndian.PutUint32(node.LinkGroupK8sPod),
+				Data:     binaryutil.NativeEndian.PutUint32(allocs.LinkGroupK8sPod),
 			},
 			&expr.Meta{
 				Key:      expr.MetaKeyOIFGROUP,
@@ -295,13 +296,13 @@
 			&expr.Cmp{
 				Op:       expr.CmpOpNeq,
 				Register: 8,
-				Data:     binaryutil.NativeEndian.PutUint32(node.LinkGroupK8sPod),
+				Data:     binaryutil.NativeEndian.PutUint32(allocs.LinkGroupK8sPod),
 			},
 			// Check if outgoing interface is not part of the overlay
 			&expr.Cmp{
 				Op:       expr.CmpOpNeq,
 				Register: 8,
-				Data:     binaryutil.NativeEndian.PutUint32(node.LinkGroupOverlay),
+				Data:     binaryutil.NativeEndian.PutUint32(allocs.LinkGroupOverlay),
 			},
 			&expr.Masq{},
 		},
diff --git a/metropolis/node/core/network/overlay/BUILD.bazel b/metropolis/node/core/network/overlay/BUILD.bazel
index 4054127..2941e9d 100644
--- a/metropolis/node/core/network/overlay/BUILD.bazel
+++ b/metropolis/node/core/network/overlay/BUILD.bazel
@@ -10,7 +10,7 @@
     importpath = "source.monogon.dev/metropolis/node/core/network/overlay",
     visibility = ["//visibility:public"],
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/curator/proto/api",
         "//metropolis/node/core/curator/watcher",
         "//metropolis/node/core/localstorage",
@@ -30,7 +30,7 @@
     srcs = ["overlay_test.go"],
     embed = [":overlay"],
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/curator/proto/api",
         "//metropolis/node/core/localstorage",
         "//metropolis/node/core/localstorage/declarative",
diff --git a/metropolis/node/core/network/overlay/overlay_test.go b/metropolis/node/core/network/overlay/overlay_test.go
index 4409e09..78a1424 100644
--- a/metropolis/node/core/network/overlay/overlay_test.go
+++ b/metropolis/node/core/network/overlay/overlay_test.go
@@ -16,7 +16,7 @@
 	"golang.zx2c4.com/wireguard/wgctrl"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/localstorage"
 	"source.monogon.dev/metropolis/node/core/localstorage/declarative"
 	"source.monogon.dev/metropolis/node/core/network/ipam"
@@ -309,7 +309,7 @@
 	if want, got := key, wgDev.PrivateKey.String(); want != got {
 		t.Errorf("Wireguard key mismatch, wanted %q, got %q", want, got)
 	}
-	if want, got := int(common.WireGuardPort), wgDev.ListenPort; want != got {
+	if want, got := int(allocs.PortWireGuard), wgDev.ListenPort; want != got {
 		t.Errorf("Wireguard port mismatch, wanted %d, got %d", want, got)
 	}
 
@@ -366,7 +366,7 @@
 			if want, got := pkeys[i].PublicKey().String(), wgDev.Peers[i].PublicKey.String(); want != got {
 				t.Errorf("Peer %d should have key %q, got %q", i, want, got)
 			}
-			if want, got := fmt.Sprintf("10.100.%d.1:%s", i, common.WireGuardPort.PortString()), wgDev.Peers[i].Endpoint.String(); want != got {
+			if want, got := fmt.Sprintf("10.100.%d.1:%s", i, allocs.PortWireGuard.PortString()), wgDev.Peers[i].Endpoint.String(); want != got {
 				t.Errorf("Peer %d should have endpoint %q, got %q", i, want, got)
 			}
 			if want, got := 2, len(wgDev.Peers[i].AllowedIPs); want != got {
@@ -409,7 +409,7 @@
 		if want, got := pkeys[0].PublicKey().String(), wgDev.Peers[0].PublicKey.String(); want != got {
 			t.Errorf("Peer 0 should have key %q, got %q", want, got)
 		}
-		if want, got := fmt.Sprintf("10.100.0.3:%s", common.WireGuardPort.PortString()), wgDev.Peers[0].Endpoint.String(); want != got {
+		if want, got := fmt.Sprintf("10.100.0.3:%s", allocs.PortWireGuard.PortString()), wgDev.Peers[0].Endpoint.String(); want != got {
 			t.Errorf("Peer 0 should have endpoint %q, got %q", want, got)
 		}
 		if want, got := 1, len(wgDev.Peers[0].AllowedIPs); want != got {
diff --git a/metropolis/node/core/network/overlay/wireguard.go b/metropolis/node/core/network/overlay/wireguard.go
index 5054553..c830dd5 100644
--- a/metropolis/node/core/network/overlay/wireguard.go
+++ b/metropolis/node/core/network/overlay/wireguard.go
@@ -12,7 +12,7 @@
 	"golang.zx2c4.com/wireguard/wgctrl"
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	ipb "source.monogon.dev/metropolis/node/core/curator/proto/api"
 	"source.monogon.dev/metropolis/node/core/localstorage"
 )
@@ -89,7 +89,7 @@
 		}
 	}
 
-	wgInterface := &netlink.Wireguard{LinkAttrs: netlink.LinkAttrs{Name: clusterNetDeviceName, Flags: net.FlagUp, Group: common.LinkGroupOverlay}}
+	wgInterface := &netlink.Wireguard{LinkAttrs: netlink.LinkAttrs{Name: clusterNetDeviceName, Flags: net.FlagUp, Group: allocs.LinkGroupOverlay}}
 	if err := netlink.LinkAdd(wgInterface); err != nil {
 		return fmt.Errorf("when adding network interface: %w", err)
 	}
@@ -100,7 +100,7 @@
 	}
 	s.wgClient = wgClient
 
-	listenPort := int(common.WireGuardPort)
+	listenPort := int(allocs.PortWireGuard)
 	if err := s.wgClient.ConfigureDevice(clusterNetDeviceName, wgtypes.Config{
 		PrivateKey: &s.privKey,
 		ListenPort: &listenPort,
@@ -111,7 +111,7 @@
 	if err := netlink.RouteAdd(&netlink.Route{
 		Dst:       clusterNet,
 		LinkIndex: wgInterface.Index,
-		Protocol:  netlink.RouteProtocol(common.ProtocolOverlay),
+		Protocol:  netlink.RouteProtocol(allocs.ProtocolOverlay),
 	}); err != nil && !os.IsExist(err) {
 		return fmt.Errorf("when creating cluster route: %w", err)
 	}
@@ -145,7 +145,7 @@
 			}
 			allowedIPs = append(allowedIPs, *podNet)
 		}
-		endpoint := net.UDPAddr{Port: int(common.WireGuardPort), IP: addressParsed}
+		endpoint := net.UDPAddr{Port: int(allocs.PortWireGuard), IP: addressParsed}
 		configs = append(configs, wgtypes.PeerConfig{
 			PublicKey:         pubkeyParsed,
 			Endpoint:          &endpoint,
diff --git a/metropolis/node/core/network/workloads/BUILD.bazel b/metropolis/node/core/network/workloads/BUILD.bazel
index e031fca..6ae3132 100644
--- a/metropolis/node/core/network/workloads/BUILD.bazel
+++ b/metropolis/node/core/network/workloads/BUILD.bazel
@@ -6,7 +6,7 @@
     importpath = "source.monogon.dev/metropolis/node/core/network/workloads",
     visibility = ["//visibility:public"],
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/network/ipam",
         "//metropolis/node/core/network/workloads/spec",
         "//osbase/event",
diff --git a/metropolis/node/core/network/workloads/workloads.go b/metropolis/node/core/network/workloads/workloads.go
index dfe3eca..fae175c 100644
--- a/metropolis/node/core/network/workloads/workloads.go
+++ b/metropolis/node/core/network/workloads/workloads.go
@@ -19,7 +19,7 @@
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/network/ipam"
 	wlapi "source.monogon.dev/metropolis/node/core/network/workloads/spec"
 	"source.monogon.dev/osbase/event"
@@ -192,7 +192,7 @@
 	}
 
 	linkAttrs := netlink.NewLinkAttrs()
-	linkAttrs.Group = node.LinkGroupK8sPod
+	linkAttrs.Group = allocs.LinkGroupK8sPod
 	linkAttrs.Name = intf
 	linkAttrs.HardwareAddr = firstHopMAC
 
@@ -307,7 +307,7 @@
 	if err != nil {
 		return nil, status.Errorf(codes.Unavailable, "error getting interface for deletion: %v", err)
 	}
-	if hostIf.Attrs().Group != node.LinkGroupK8sPod {
+	if hostIf.Attrs().Group != allocs.LinkGroupK8sPod {
 		return nil, status.Errorf(codes.InvalidArgument, "refusing to delete interface not belonging to workload, has group %d", hostIf.Attrs().Group)
 	}
 	// Routes and addresses do not need to be cleaned up as Linux already takes
diff --git a/metropolis/node/core/roleserve/BUILD.bazel b/metropolis/node/core/roleserve/BUILD.bazel
index c753683..b170ec2 100644
--- a/metropolis/node/core/roleserve/BUILD.bazel
+++ b/metropolis/node/core/roleserve/BUILD.bazel
@@ -18,7 +18,7 @@
     importpath = "source.monogon.dev/metropolis/node/core/roleserve",
     visibility = ["//visibility:public"],
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/consensus",
         "//metropolis/node/core/curator",
         "//metropolis/node/core/curator/proto/api",
@@ -64,7 +64,7 @@
         "source.monogon.dev/metropolis/node/core/productinfo.path": "$(rlocationpath //metropolis/node:product_info )",
     },
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/consensus",
         "//metropolis/node/core/curator",
         "//metropolis/node/core/curator/proto/api",
diff --git a/metropolis/node/core/roleserve/roleserve.go b/metropolis/node/core/roleserve/roleserve.go
index 918b466..6ddda1e 100644
--- a/metropolis/node/core/roleserve/roleserve.go
+++ b/metropolis/node/core/roleserve/roleserve.go
@@ -42,7 +42,7 @@
 	"context"
 	"crypto/ed25519"
 
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/curator"
 	"source.monogon.dev/metropolis/node/core/identity"
 	"source.monogon.dev/metropolis/node/core/localstorage"
@@ -215,8 +215,8 @@
 func (s *Service) ProvideBootstrapData(data *BootstrapData) {
 	// This is the first time we have the node ID, tell the resolver that it's
 	// available on the loopback interface.
-	s.Resolver.AddOverride(data.Node.ID, resolver.NodeByHostPort("127.0.0.1", uint16(common.CuratorServicePort)))
-	s.Resolver.AddEndpoint(resolver.NodeByHostPort("127.0.0.1", uint16(common.CuratorServicePort)))
+	s.Resolver.AddOverride(data.Node.ID, resolver.NodeByHostPort("127.0.0.1", uint16(allocs.PortCuratorService)))
+	s.Resolver.AddEndpoint(resolver.NodeByHostPort("127.0.0.1", uint16(allocs.PortCuratorService)))
 
 	s.bootstrapData.Set(data)
 }
@@ -224,7 +224,7 @@
 func (s *Service) ProvideRegisterData(credentials identity.NodeCredentials, directory *cpb.ClusterDirectory) {
 	// This is the first time we have the node ID, tell the resolver that it's
 	// available on the loopback interface.
-	s.Resolver.AddOverride(credentials.ID(), resolver.NodeByHostPort("127.0.0.1", uint16(common.CuratorServicePort)))
+	s.Resolver.AddOverride(credentials.ID(), resolver.NodeByHostPort("127.0.0.1", uint16(allocs.PortCuratorService)))
 	// Also tell the resolver about all the existing nodes in the cluster we just
 	// registered into. The directory passed here was used to issue the initial
 	// Register call, which means at least one of the nodes was running the control
@@ -241,7 +241,7 @@
 func (s *Service) ProvideJoinData(credentials identity.NodeCredentials, directory *cpb.ClusterDirectory) {
 	// This is the first time we have the node ID, tell the resolver that it's
 	// available on the loopback interface.
-	s.Resolver.AddOverride(credentials.ID(), resolver.NodeByHostPort("127.0.0.1", uint16(common.CuratorServicePort)))
+	s.Resolver.AddOverride(credentials.ID(), resolver.NodeByHostPort("127.0.0.1", uint16(allocs.PortCuratorService)))
 	// Also tell the resolver about all the existing nodes in the cluster we just
 	// joined into. The directory passed here was used to issue the initial
 	// Join call, which means at least one of the nodes was running the control
diff --git a/metropolis/node/core/roleserve/worker_statuspush.go b/metropolis/node/core/roleserve/worker_statuspush.go
index 62355bc..5b7ee71 100644
--- a/metropolis/node/core/roleserve/worker_statuspush.go
+++ b/metropolis/node/core/roleserve/worker_statuspush.go
@@ -12,7 +12,7 @@
 	"github.com/google/uuid"
 	"google.golang.org/protobuf/encoding/prototext"
 
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/network"
 	"source.monogon.dev/metropolis/node/core/productinfo"
 	"source.monogon.dev/osbase/event"
@@ -102,7 +102,7 @@
 			if status.RunningCurator == nil && lcp.exists() {
 				supervisor.Logger(ctx).Infof("Got new local curator state: running")
 				status.RunningCurator = &cpb.NodeStatus_RunningCurator{
-					Port: int32(common.CuratorServicePort),
+					Port: int32(allocs.PortCuratorService),
 				}
 				changed = true
 			}
diff --git a/metropolis/node/core/roleserve/worker_statuspush_test.go b/metropolis/node/core/roleserve/worker_statuspush_test.go
index 40764f2..0f25f3b 100644
--- a/metropolis/node/core/roleserve/worker_statuspush_test.go
+++ b/metropolis/node/core/roleserve/worker_statuspush_test.go
@@ -18,7 +18,7 @@
 	"google.golang.org/grpc/test/bufconn"
 	"google.golang.org/protobuf/testing/protocmp"
 
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/consensus"
 	"source.monogon.dev/metropolis/node/core/curator"
 	"source.monogon.dev/metropolis/node/core/productinfo"
@@ -182,7 +182,7 @@
 		{NodeId: nodeID, Status: &cpb.NodeStatus{
 			ExternalAddress: "192.0.2.11",
 			RunningCurator: &cpb.NodeStatus_RunningCurator{
-				Port: int32(common.CuratorServicePort),
+				Port: int32(allocs.PortCuratorService),
 			},
 			Version: productInfo.Version,
 			BootId:  []byte{1, 2, 3},
diff --git a/metropolis/node/core/rpc/resolver/BUILD.bazel b/metropolis/node/core/rpc/resolver/BUILD.bazel
index 6438c62..0b041f6 100644
--- a/metropolis/node/core/rpc/resolver/BUILD.bazel
+++ b/metropolis/node/core/rpc/resolver/BUILD.bazel
@@ -11,7 +11,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//go/logging",
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/curator/proto/api",
         "//metropolis/node/core/curator/watcher",
         "//metropolis/proto/common",
diff --git a/metropolis/node/core/rpc/resolver/resolver.go b/metropolis/node/core/rpc/resolver/resolver.go
index c865130..95c5837 100644
--- a/metropolis/node/core/rpc/resolver/resolver.go
+++ b/metropolis/node/core/rpc/resolver/resolver.go
@@ -17,7 +17,7 @@
 	"google.golang.org/grpc/keepalive"
 
 	"source.monogon.dev/go/logging"
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/curator/watcher"
 
 	apb "source.monogon.dev/metropolis/node/core/curator/proto/api"
@@ -144,13 +144,13 @@
 	if m, _ := regexp.MatchString(`metropolis-[a-f0-9]+`, id); !m {
 		return nil, fmt.Errorf("invalid node ID")
 	}
-	return NodeByHostPort(id, uint16(common.CuratorServicePort)), nil
+	return NodeByHostPort(id, uint16(allocs.PortCuratorService)), nil
 }
 
 // NodeAtAddressWithDefaultPort returns a NodeEndpoint referencing the default
 // control plane port (the Curator port) of a node at a given address.
 func NodeAtAddressWithDefaultPort(host string) *NodeEndpoint {
-	return NodeByHostPort(host, uint16(common.CuratorServicePort))
+	return NodeByHostPort(host, uint16(allocs.PortCuratorService))
 }
 
 // NodeByHostPort returns a NodeEndpoint for a fully specified host + port pair.
diff --git a/metropolis/node/core/time/BUILD.bazel b/metropolis/node/core/time/BUILD.bazel
index f113153..3f318e0 100644
--- a/metropolis/node/core/time/BUILD.bazel
+++ b/metropolis/node/core/time/BUILD.bazel
@@ -6,7 +6,7 @@
     importpath = "source.monogon.dev/metropolis/node/core/time",
     visibility = ["//visibility:public"],
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//osbase/fileargs",
         "//osbase/supervisor",
     ],
diff --git a/metropolis/node/core/time/time.go b/metropolis/node/core/time/time.go
index 23400ef..d7cd721 100644
--- a/metropolis/node/core/time/time.go
+++ b/metropolis/node/core/time/time.go
@@ -20,7 +20,7 @@
 	"strconv"
 	"strings"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/osbase/fileargs"
 	"source.monogon.dev/osbase/supervisor"
 )
@@ -53,8 +53,8 @@
 	cmd := exec.CommandContext(ctx,
 		"/time/chrony",
 		"-d",
-		"-i", strconv.Itoa(node.TimeUid),
-		"-g", strconv.Itoa(node.TimeUid),
+		"-i", strconv.Itoa(allocs.UidTime),
+		"-g", strconv.Itoa(allocs.UidTime),
 		"-f", args.ArgPath("chrony.conf", []byte(config)),
 	)
 	cmd.Stdout = supervisor.RawLogger(ctx)
diff --git a/metropolis/node/kubernetes/BUILD.bazel b/metropolis/node/kubernetes/BUILD.bazel
index 3245964..69722b3 100644
--- a/metropolis/node/kubernetes/BUILD.bazel
+++ b/metropolis/node/kubernetes/BUILD.bazel
@@ -21,6 +21,7 @@
         "//go/logging",
         "//go/net/tinylb",
         "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/consensus",
         "//metropolis/node/core/curator/proto/api",
         "//metropolis/node/core/curator/watcher",
diff --git a/metropolis/node/kubernetes/apiproxy.go b/metropolis/node/kubernetes/apiproxy.go
index 01990ff..a09796a 100644
--- a/metropolis/node/kubernetes/apiproxy.go
+++ b/metropolis/node/kubernetes/apiproxy.go
@@ -8,7 +8,7 @@
 	"net"
 
 	"source.monogon.dev/go/net/tinylb"
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	ipb "source.monogon.dev/metropolis/node/core/curator/proto/api"
 	"source.monogon.dev/metropolis/node/core/curator/watcher"
 	"source.monogon.dev/osbase/event/memory"
@@ -39,7 +39,7 @@
 		},
 		OnNewUpdated: func(new *ipb.Node) error {
 			set.Insert(new.Id, &tinylb.SimpleTCPBackend{
-				Remote: net.JoinHostPort(new.Status.ExternalAddress, node.KubernetesAPIPort.PortString()),
+				Remote: net.JoinHostPort(new.Status.ExternalAddress, allocs.PortKubernetesAPI.PortString()),
 			})
 			val.Set(set.Clone())
 			return nil
diff --git a/metropolis/node/kubernetes/apiserver.go b/metropolis/node/kubernetes/apiserver.go
index 5471e02..427d059 100644
--- a/metropolis/node/kubernetes/apiserver.go
+++ b/metropolis/node/kubernetes/apiserver.go
@@ -18,7 +18,7 @@
 	"k8s.io/kubernetes/plugin/pkg/admission/security/podsecurity"
 	podsecurityadmissionv1 "k8s.io/pod-security-admission/admission/api/v1"
 
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/localstorage"
 	"source.monogon.dev/metropolis/node/kubernetes/pki"
 	"source.monogon.dev/osbase/fileargs"
@@ -142,7 +142,7 @@
 			pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: s.idCA})),
 		"--enable-admission-plugins=NodeRestriction",
 		"--enable-aggregator-routing=true",
-		fmt.Sprintf("--secure-port=%d", common.KubernetesAPIPort),
+		fmt.Sprintf("--secure-port=%d", allocs.PortKubernetesAPI),
 		fmt.Sprintf("--etcd-servers=unix:///%s:0", s.EphemeralConsensusDirectory.ClientSocket.FullPath()),
 		args.FileOpt("--kubelet-client-certificate", "kubelet-client-cert.pem",
 			pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: s.kubeletClientCert})),
diff --git a/metropolis/node/kubernetes/authproxy/BUILD.bazel b/metropolis/node/kubernetes/authproxy/BUILD.bazel
index 263e846..57d950e 100644
--- a/metropolis/node/kubernetes/authproxy/BUILD.bazel
+++ b/metropolis/node/kubernetes/authproxy/BUILD.bazel
@@ -6,7 +6,7 @@
     importpath = "source.monogon.dev/metropolis/node/kubernetes/authproxy",
     visibility = ["//visibility:public"],
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/identity",
         "//metropolis/node/kubernetes/pki",
         "//osbase/supervisor",
diff --git a/metropolis/node/kubernetes/authproxy/authproxy.go b/metropolis/node/kubernetes/authproxy/authproxy.go
index 7fdef76..93737ec 100644
--- a/metropolis/node/kubernetes/authproxy/authproxy.go
+++ b/metropolis/node/kubernetes/authproxy/authproxy.go
@@ -20,7 +20,7 @@
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/identity"
 	"source.monogon.dev/metropolis/node/kubernetes/pki"
 	"source.monogon.dev/osbase/supervisor"
@@ -73,7 +73,7 @@
 		return err
 	}
 
-	internalAPIServer := net.JoinHostPort("localhost", node.KubernetesAPIPort.PortString())
+	internalAPIServer := net.JoinHostPort("localhost", allocs.PortKubernetesAPI.PortString())
 	standardProxy := httputil.NewSingleHostReverseProxy(&url.URL{
 		Scheme: "https",
 		Host:   internalAPIServer,
@@ -119,7 +119,7 @@
 	clientCAs := x509.NewCertPool()
 	clientCAs.AddCert(s.Node.ClusterCA())
 	server := &http.Server{
-		Addr: ":" + node.KubernetesAPIWrappedPort.PortString(),
+		Addr: ":" + allocs.PortKubernetesAPIWrapped.PortString(),
 		TLSConfig: &tls.Config{
 			MinVersion:   tls.VersionTLS12,
 			NextProtos:   []string{"h2", "http/1.1"},
diff --git a/metropolis/node/kubernetes/metricsproxy/BUILD.bazel b/metropolis/node/kubernetes/metricsproxy/BUILD.bazel
index 7f89450..186bd57 100644
--- a/metropolis/node/kubernetes/metricsproxy/BUILD.bazel
+++ b/metropolis/node/kubernetes/metricsproxy/BUILD.bazel
@@ -6,7 +6,7 @@
     importpath = "source.monogon.dev/metropolis/node/kubernetes/metricsproxy",
     visibility = ["//visibility:public"],
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/kubernetes/pki",
         "//osbase/supervisor",
         "@io_k8s_kubernetes//cmd/kubeadm/app/constants",
diff --git a/metropolis/node/kubernetes/metricsproxy/metricsproxy.go b/metropolis/node/kubernetes/metricsproxy/metricsproxy.go
index 1b98fa8..c098fac 100644
--- a/metropolis/node/kubernetes/metricsproxy/metricsproxy.go
+++ b/metropolis/node/kubernetes/metricsproxy/metricsproxy.go
@@ -17,7 +17,7 @@
 
 	"k8s.io/kubernetes/cmd/kubeadm/app/constants"
 
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/kubernetes/pki"
 	"source.monogon.dev/osbase/supervisor"
 )
@@ -30,9 +30,9 @@
 type kubernetesExporter struct {
 	Name string
 	// TargetPort on which this exporter is running.
-	TargetPort node.Port
+	TargetPort allocs.Port
 	// TargetPort on which the unauthenticated exporter should run.
-	ListenPort node.Port
+	ListenPort allocs.Port
 	// ServerName used to verify the tls connection.
 	ServerName string
 }
@@ -42,19 +42,19 @@
 	{
 		Name:       "kubernetes-scheduler",
 		TargetPort: constants.KubeSchedulerPort,
-		ListenPort: node.MetricsKubeSchedulerListenerPort,
+		ListenPort: allocs.PortMetricsKubeSchedulerListener,
 		ServerName: "kube-scheduler.local",
 	},
 	{
 		Name:       "kubernetes-controller-manager",
 		TargetPort: constants.KubeControllerManagerPort,
-		ListenPort: node.MetricsKubeControllerManagerListenerPort,
+		ListenPort: allocs.PortMetricsKubeControllerManagerListener,
 		ServerName: "kube-controller-manager.local",
 	},
 	{
 		Name:       "kubernetes-apiserver",
-		TargetPort: node.KubernetesAPIPort,
-		ListenPort: node.MetricsKubeAPIServerListenerPort,
+		TargetPort: allocs.PortKubernetesAPI,
+		ListenPort: allocs.PortMetricsKubeAPIServerListener,
 		ServerName: "kubernetes",
 	},
 }
diff --git a/metropolis/node/kubernetes/networkpolicy/BUILD.bazel b/metropolis/node/kubernetes/networkpolicy/BUILD.bazel
index 287427e..15fa223 100644
--- a/metropolis/node/kubernetes/networkpolicy/BUILD.bazel
+++ b/metropolis/node/kubernetes/networkpolicy/BUILD.bazel
@@ -8,7 +8,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//go/logging",
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//osbase/supervisor",
         "@io_k8s_api//core/v1:core",
         "@io_k8s_client_go//informers",
diff --git a/metropolis/node/kubernetes/networkpolicy/networkpolicy.go b/metropolis/node/kubernetes/networkpolicy/networkpolicy.go
index c43ef40..04a2fcd 100644
--- a/metropolis/node/kubernetes/networkpolicy/networkpolicy.go
+++ b/metropolis/node/kubernetes/networkpolicy/networkpolicy.go
@@ -19,7 +19,7 @@
 	"k8s.io/kubectl/pkg/scheme"
 
 	"source.monogon.dev/go/logging"
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/osbase/supervisor"
 )
 
@@ -75,7 +75,7 @@
 	eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: c.Kubernetes.CoreV1().Events("")})
 	recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "npc"})
 
-	nft, err := nftctrl.New(recorder, node.LinkGroupK8sPod)
+	nft, err := nftctrl.New(recorder, allocs.LinkGroupK8sPod)
 	if err != nil {
 		return fmt.Errorf("failed to create nftables controller: %w", err)
 	}
diff --git a/metropolis/node/kubernetes/pki/BUILD.bazel b/metropolis/node/kubernetes/pki/BUILD.bazel
index 60121c7..85e0d2f 100644
--- a/metropolis/node/kubernetes/pki/BUILD.bazel
+++ b/metropolis/node/kubernetes/pki/BUILD.bazel
@@ -6,7 +6,7 @@
     importpath = "source.monogon.dev/metropolis/node/kubernetes/pki",
     visibility = ["//metropolis/node:__subpackages__"],
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/consensus",
         "//osbase/pki",
         "@io_etcd_go_etcd_client_v3//:client",
diff --git a/metropolis/node/kubernetes/pki/kubernetes.go b/metropolis/node/kubernetes/pki/kubernetes.go
index 2d67875..234be44 100644
--- a/metropolis/node/kubernetes/pki/kubernetes.go
+++ b/metropolis/node/kubernetes/pki/kubernetes.go
@@ -26,7 +26,7 @@
 	"k8s.io/client-go/tools/clientcmd"
 	configapi "k8s.io/client-go/tools/clientcmd/api"
 
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/consensus"
 	opki "source.monogon.dev/osbase/pki"
 )
@@ -225,10 +225,10 @@
 	// KubernetesAPIEndpointForWorker points Kubernetes workers to connect to a
 	// locally-running apiproxy, which in turn loadbalances the connection to
 	// controller nodes running in the cluster.
-	KubernetesAPIEndpointForWorker = KubernetesAPIEndpoint(fmt.Sprintf("https://127.0.0.1:%d", common.KubernetesWorkerLocalAPIPort))
+	KubernetesAPIEndpointForWorker = KubernetesAPIEndpoint(fmt.Sprintf("https://127.0.0.1:%d", allocs.PortKubernetesWorkerLocalAPI))
 	// KubernetesAPIEndpointForController points Kubernetes controllers to connect to
 	// the locally-running API server.
-	KubernetesAPIEndpointForController = KubernetesAPIEndpoint(fmt.Sprintf("https://127.0.0.1:%d", common.KubernetesAPIPort))
+	KubernetesAPIEndpointForController = KubernetesAPIEndpoint(fmt.Sprintf("https://127.0.0.1:%d", allocs.PortKubernetesAPI))
 )
 
 // KubeconfigRaw emits a Kubeconfig for a given set of certificates, private key,
diff --git a/metropolis/node/kubernetes/service_worker.go b/metropolis/node/kubernetes/service_worker.go
index 5e28788..0eb6435 100644
--- a/metropolis/node/kubernetes/service_worker.go
+++ b/metropolis/node/kubernetes/service_worker.go
@@ -16,7 +16,7 @@
 	"k8s.io/client-go/tools/clientcmd"
 
 	"source.monogon.dev/go/net/tinylb"
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/localstorage"
 	"source.monogon.dev/metropolis/node/core/metrics"
 	"source.monogon.dev/metropolis/node/core/network"
@@ -66,7 +66,7 @@
 	// available apiservers, and Kubernetes components do not implement client-side
 	// load-balancing.
 	err := supervisor.Run(ctx, "apiproxy", func(ctx context.Context) error {
-		lis, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", node.KubernetesWorkerLocalAPIPort))
+		lis, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", allocs.PortKubernetesWorkerLocalAPI))
 		if err != nil {
 			return fmt.Errorf("failed to listen: %w", err)
 		}
@@ -95,7 +95,7 @@
 		ClusterDomain:      s.c.ClusterDomain,
 		KubeletDirectory:   &s.c.Root.Data.Kubernetes.Kubelet,
 		EphemeralDirectory: &s.c.Root.Ephemeral,
-		ClusterDNS:         []net.IP{node.ContainerDNSIP},
+		ClusterDNS:         []net.IP{allocs.IPContainerDNS},
 	}
 
 	// Gather all required material to send over for certficiate issuance to the
@@ -229,16 +229,16 @@
 	// //metropolis/node/core/roleserve/worker_kubernetes.go.
 	s.c.Network.DNS.SetHandler("kubernetes", dnsService)
 
-	if err := s.c.Network.AddLoopbackIP(node.ContainerDNSIP); err != nil {
+	if err := s.c.Network.AddLoopbackIP(allocs.IPContainerDNS); err != nil {
 		return fmt.Errorf("failed to add local IP for container DNS: %w", err)
 	}
 	defer func() {
-		if err := s.c.Network.ReleaseLoopbackIP(node.ContainerDNSIP); err != nil {
+		if err := s.c.Network.ReleaseLoopbackIP(allocs.IPContainerDNS); err != nil {
 			supervisor.Logger(ctx).Errorf("Failed to release local IP for container DNS: %v", err)
 		}
 	}()
 	runDNSListener := func(ctx context.Context) error {
-		return s.c.Network.DNS.RunListenerAddr(ctx, net.JoinHostPort(node.ContainerDNSIP.String(), "53"))
+		return s.c.Network.DNS.RunListenerAddr(ctx, net.JoinHostPort(allocs.IPContainerDNS.String(), "53"))
 	}
 
 	kvmDevicePlugin := kvmdevice.Plugin{
diff --git a/metropolis/node/ports.go b/metropolis/node/ports.go
deleted file mode 100644
index 014eb90..0000000
--- a/metropolis/node/ports.go
+++ /dev/null
@@ -1,130 +0,0 @@
-// Copyright The Monogon Project Authors.
-// SPDX-License-Identifier: Apache-2.0
-
-package node
-
-import (
-	"strconv"
-)
-
-// Port is a TCP and/or UDP port number reserved for and used by Metropolis
-// node code.
-type Port uint16
-
-const (
-	// CuratorServicePort is the TCP port on which the Curator listens for gRPC
-	// calls and services Management/AAA/Curator RPCs.
-	CuratorServicePort Port = 7835
-	// ConsensusPort is the TCP port on which etcd listens for peer traffic.
-	ConsensusPort Port = 7834
-	// DebugServicePort is the TCP port on which the debug service serves gRPC
-	// traffic. This is only available in debug builds.
-	DebugServicePort Port = 7837
-	// WireGuardPort is the UDP port on which the Wireguard Kubernetes network
-	// overlay listens for incoming peer traffic.
-	WireGuardPort Port = 7838
-	// NodeManagementPort is the TCP port on which the node-local management service
-	// serves gRPC traffic for NodeManagement.
-	NodeManagementPort Port = 7839
-	// MetricsPort is the TCP port on which the Metrics Service exports
-	// Prometheus-compatible metrics for this node, secured using TLS and the
-	// Cluster/Node certificates.
-	MetricsPort Port = 7840
-	// MetricsNodeListenerPort is the TCP port on which the Prometheus node_exporter
-	// runs, bound to 127.0.0.1. The Metrics Service proxies traffic to it from the
-	// public MetricsPort.
-	MetricsNodeListenerPort Port = 7841
-	// MetricsEtcdListenerPort is the TCP port on which the etcd exporter
-	// runs, bound to 127.0.0.1. The metrics service proxies traffic to it from the
-	// public MetricsPort.
-	MetricsEtcdListenerPort Port = 7842
-	// MetricsKubeSchedulerListenerPort is the TCP port on which the proxy for
-	// the kube-scheduler runs, bound to 127.0.0.1. The metrics service proxies
-	// traffic to it from the public MetricsPort.
-	MetricsKubeSchedulerListenerPort Port = 7843
-	// MetricsKubeControllerManagerListenerPort is the TCP port on which the
-	// proxy for the controller-manager runs, bound to 127.0.0.1. The metrics
-	// service proxies traffic to it from the public MetricsPort.
-	MetricsKubeControllerManagerListenerPort Port = 7844
-	// MetricsKubeAPIServerListenerPort is the TCP port on which the
-	// proxy for the api-server runs, bound to 127.0.0.1. The metrics
-	// service proxies traffic to it from the public MetricsPort.
-	MetricsKubeAPIServerListenerPort Port = 7845
-	// MetricsContainerdListenerPort is the TCP port on which the
-	// containerd metrics endpoint, bound to 127.0.0.1, is exposed.
-	MetricsContainerdListenerPort Port = 7846
-	// KubernetesAPIPort is the TCP port on which the Kubernetes API is
-	// exposed.
-	KubernetesAPIPort Port = 6443
-	// KubernetesAPIWrappedPort is the TCP port on which the Metropolis
-	// authenticating proxy for the Kubernetes API is exposed.
-	KubernetesAPIWrappedPort Port = 6444
-	// KubernetesWorkerLocalAPIPort is the TCP port on which Kubernetes worker nodes
-	// run a loadbalancer to access the cluster's API servers before cluster
-	// networking is available. This port is only bound to 127.0.0.1.
-	KubernetesWorkerLocalAPIPort Port = 6445
-	// DebuggerPort is the port on which the delve debugger runs (on debug
-	// builds only). Not to be confused with DebugServicePort.
-	DebuggerPort Port = 2345
-)
-
-var SystemPorts = []Port{
-	CuratorServicePort,
-	ConsensusPort,
-	DebugServicePort,
-	WireGuardPort,
-	NodeManagementPort,
-	MetricsPort,
-	MetricsNodeListenerPort,
-	MetricsEtcdListenerPort,
-	MetricsKubeSchedulerListenerPort,
-	MetricsKubeControllerManagerListenerPort,
-	MetricsKubeAPIServerListenerPort,
-	MetricsContainerdListenerPort,
-	KubernetesAPIPort,
-	KubernetesAPIWrappedPort,
-	KubernetesWorkerLocalAPIPort,
-	DebuggerPort,
-}
-
-func (p Port) String() string {
-	switch p {
-	case CuratorServicePort:
-		return "curator"
-	case ConsensusPort:
-		return "consensus"
-	case DebugServicePort:
-		return "debug"
-	case WireGuardPort:
-		return "wireguard"
-	case NodeManagementPort:
-		return "node-mgmt"
-	case MetricsPort:
-		return "metrics"
-	case MetricsNodeListenerPort:
-		return "metrics-node-exporter"
-	case MetricsEtcdListenerPort:
-		return "metrics-etcd"
-	case MetricsKubeSchedulerListenerPort:
-		return "metrics-kubernetes-scheduler"
-	case MetricsKubeControllerManagerListenerPort:
-		return "metrics-kubernetes-controller-manager"
-	case MetricsKubeAPIServerListenerPort:
-		return "metrics-kubernetes-api-server"
-	case MetricsContainerdListenerPort:
-		return "metrics-containerd"
-	case KubernetesAPIPort:
-		return "kubernetes-api"
-	case KubernetesAPIWrappedPort:
-		return "kubernetes-api-wrapped"
-	case KubernetesWorkerLocalAPIPort:
-		return "kubernetes-worker-local-api"
-	case DebuggerPort:
-		return "delve"
-	}
-	return "unknown"
-}
-
-func (p Port) PortString() string {
-	return strconv.Itoa(int(p))
-}
diff --git a/metropolis/test/e2e/suites/core/BUILD.bazel b/metropolis/test/e2e/suites/core/BUILD.bazel
index 9671f9f..35d73ec 100644
--- a/metropolis/test/e2e/suites/core/BUILD.bazel
+++ b/metropolis/test/e2e/suites/core/BUILD.bazel
@@ -16,7 +16,7 @@
         "xTestImagesManifestPath": "$(rlocationpath //metropolis/test/e2e:testimages_manifest )",
     },
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/rpc",
         "//metropolis/proto/api",
         "//metropolis/proto/common",
diff --git a/metropolis/test/e2e/suites/core/run_test.go b/metropolis/test/e2e/suites/core/run_test.go
index f6fd24c..587e173 100644
--- a/metropolis/test/e2e/suites/core/run_test.go
+++ b/metropolis/test/e2e/suites/core/run_test.go
@@ -23,7 +23,7 @@
 	apb "source.monogon.dev/metropolis/proto/api"
 	cpb "source.monogon.dev/metropolis/proto/common"
 
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/metropolis/node/core/rpc"
 	mlaunch "source.monogon.dev/metropolis/test/launch"
 	"source.monogon.dev/metropolis/test/localregistry"
@@ -101,7 +101,7 @@
 
 	// Dial first node's curator.
 	creds := rpc.NewAuthenticatedCredentials(cluster.Owner, rpc.WantInsecure())
-	remote := net.JoinHostPort(cluster.Nodes[cluster.NodeIDs[0]].ManagementAddress, common.CuratorServicePort.PortString())
+	remote := net.JoinHostPort(cluster.Nodes[cluster.NodeIDs[0]].ManagementAddress, allocs.PortCuratorService.PortString())
 	cl, err := grpc.NewClient(remote, grpc.WithContextDialer(cluster.DialNode), grpc.WithTransportCredentials(creds))
 	if err != nil {
 		t.Fatalf("failed to create first node's curator client: %v", err)
@@ -163,7 +163,7 @@
 		}
 		u := url.URL{
 			Scheme: "https",
-			Host:   net.JoinHostPort(cluster.NodeIDs[0], common.MetricsPort.PortString()),
+			Host:   net.JoinHostPort(cluster.NodeIDs[0], allocs.PortMetrics.PortString()),
 			Path:   "/metrics/node",
 		}
 		res, err := cl.Get(u.String())
diff --git a/metropolis/test/e2e/suites/kubernetes/BUILD.bazel b/metropolis/test/e2e/suites/kubernetes/BUILD.bazel
index a25bbe8..8091d67 100644
--- a/metropolis/test/e2e/suites/kubernetes/BUILD.bazel
+++ b/metropolis/test/e2e/suites/kubernetes/BUILD.bazel
@@ -35,6 +35,7 @@
     },
     deps = [
         "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/proto/api",
         "//metropolis/proto/common",
         "//metropolis/test/e2e/connectivity",
diff --git a/metropolis/test/e2e/suites/kubernetes/run_test.go b/metropolis/test/e2e/suites/kubernetes/run_test.go
index 8333567..64da63e 100644
--- a/metropolis/test/e2e/suites/kubernetes/run_test.go
+++ b/metropolis/test/e2e/suites/kubernetes/run_test.go
@@ -32,6 +32,7 @@
 	"k8s.io/utils/ptr"
 
 	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	apb "source.monogon.dev/metropolis/proto/api"
 	cpb "source.monogon.dev/metropolis/proto/common"
 	"source.monogon.dev/metropolis/test/e2e/connectivity"
@@ -620,7 +621,7 @@
 		}
 		u := url.URL{
 			Scheme: "https",
-			Host:   net.JoinHostPort(cluster.NodeIDs[1], common.MetricsPort.PortString()),
+			Host:   net.JoinHostPort(cluster.NodeIDs[1], allocs.PortMetrics.PortString()),
 			Path:   "/metrics/containerd",
 		}
 		res, err := cl.Get(u.String())
diff --git a/metropolis/test/launch/BUILD.bazel b/metropolis/test/launch/BUILD.bazel
index b2530c5..f93cda8 100644
--- a/metropolis/test/launch/BUILD.bazel
+++ b/metropolis/test/launch/BUILD.bazel
@@ -48,6 +48,7 @@
         "//metropolis/cli/metroctl/core",
         "//metropolis/installer/install",
         "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/node/core/curator/proto/api",
         "//metropolis/node/core/rpc",
         "//metropolis/node/core/rpc/resolver",
diff --git a/metropolis/test/launch/cli/launch-cluster/BUILD.bazel b/metropolis/test/launch/cli/launch-cluster/BUILD.bazel
index 339ea4c..0e9795b 100644
--- a/metropolis/test/launch/cli/launch-cluster/BUILD.bazel
+++ b/metropolis/test/launch/cli/launch-cluster/BUILD.bazel
@@ -14,7 +14,7 @@
     deps = [
         "//metropolis/cli/flagdefs",
         "//metropolis/cli/metroctl/core",
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//metropolis/proto/common",
         "//metropolis/test/launch",
         "@io_bazel_rules_go//go/runfiles",
diff --git a/metropolis/test/launch/cli/launch-cluster/main.go b/metropolis/test/launch/cli/launch-cluster/main.go
index 1aec556..e94f0cf 100644
--- a/metropolis/test/launch/cli/launch-cluster/main.go
+++ b/metropolis/test/launch/cli/launch-cluster/main.go
@@ -20,7 +20,7 @@
 
 	"source.monogon.dev/metropolis/cli/flagdefs"
 	metroctl "source.monogon.dev/metropolis/cli/metroctl/core"
-	"source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	cpb "source.monogon.dev/metropolis/proto/common"
 	mlaunch "source.monogon.dev/metropolis/test/launch"
 )
@@ -183,7 +183,7 @@
 	apiserver := cl.Nodes[cl.NodeIDs[0]].ManagementAddress
 	// Wait for the API server to start listening.
 	for {
-		conn, err := cl.DialNode(ctx, net.JoinHostPort(apiserver, node.KubernetesAPIWrappedPort.PortString()))
+		conn, err := cl.DialNode(ctx, net.JoinHostPort(apiserver, allocs.PortKubernetesAPIWrapped.PortString()))
 		if err == nil {
 			conn.Close()
 			break
diff --git a/metropolis/test/launch/cluster.go b/metropolis/test/launch/cluster.go
index c77a101..c6711c8 100644
--- a/metropolis/test/launch/cluster.go
+++ b/metropolis/test/launch/cluster.go
@@ -41,6 +41,7 @@
 	"k8s.io/client-go/rest"
 	"k8s.io/utils/ptr"
 
+	"source.monogon.dev/metropolis/node/allocs"
 	ipb "source.monogon.dev/metropolis/node/core/curator/proto/api"
 	apb "source.monogon.dev/metropolis/proto/api"
 	cpb "source.monogon.dev/metropolis/proto/common"
@@ -141,17 +142,17 @@
 }
 
 // NodePorts is the list of ports a fully operational Metropolis node listens on
-var NodePorts = []node.Port{
-	node.ConsensusPort,
+var NodePorts = []allocs.Port{
+	allocs.PortConsensus,
 
-	node.CuratorServicePort,
-	node.DebugServicePort,
+	allocs.PortCuratorService,
+	allocs.PortDebugService,
 
-	node.KubernetesAPIPort,
-	node.KubernetesAPIWrappedPort,
-	node.CuratorServicePort,
-	node.DebuggerPort,
-	node.MetricsPort,
+	allocs.PortKubernetesAPI,
+	allocs.PortKubernetesAPIWrapped,
+	allocs.PortCuratorService,
+	allocs.PortDebugger,
+	allocs.PortMetrics,
 }
 
 // setupRuntime creates the node's QEMU runtime directory, together with all
@@ -579,10 +580,10 @@
 // ClusterPorts contains all ports handled by Nanoswitch.
 var ClusterPorts = []uint16{
 	// Forwarded to the first node.
-	uint16(node.CuratorServicePort),
-	uint16(node.DebugServicePort),
-	uint16(node.KubernetesAPIPort),
-	uint16(node.KubernetesAPIWrappedPort),
+	uint16(allocs.PortCuratorService),
+	uint16(allocs.PortDebugService),
+	uint16(allocs.PortKubernetesAPI),
+	uint16(allocs.PortKubernetesAPIWrapped),
 
 	// SOCKS proxy to the switch network
 	SOCKSPort,
@@ -694,7 +695,7 @@
 // information as NodeInCluster.
 func firstConnection(ctx context.Context, socksDialer proxy.Dialer) (*tls.Certificate, *NodeInCluster, error) {
 	// Dial external service.
-	remote := fmt.Sprintf("10.1.0.2:%s", node.CuratorServicePort.PortString())
+	remote := fmt.Sprintf("10.1.0.2:%s", allocs.PortCuratorService.PortString())
 	initCreds, err := rpc.NewEphemeralCredentials(InsecurePrivateKey, rpc.WantInsecure())
 	if err != nil {
 		return nil, nil, fmt.Errorf("NewEphemeralCredentials: %w", err)
@@ -1320,7 +1321,7 @@
 		panic(err)
 	}
 
-	host := net.JoinHostPort(c.NodeIDs[0], node.KubernetesAPIWrappedPort.PortString())
+	host := net.JoinHostPort(c.NodeIDs[0], allocs.PortKubernetesAPIWrapped.PortString())
 	clientConfig := rest.Config{
 		Host: host,
 		TLSClientConfig: rest.TLSClientConfig{
diff --git a/metropolis/test/nanoswitch/BUILD.bazel b/metropolis/test/nanoswitch/BUILD.bazel
index ef80101..58555b5 100644
--- a/metropolis/test/nanoswitch/BUILD.bazel
+++ b/metropolis/test/nanoswitch/BUILD.bazel
@@ -10,7 +10,7 @@
     importpath = "source.monogon.dev/metropolis/test/nanoswitch",
     visibility = ["//visibility:private"],
     deps = [
-        "//metropolis/node",
+        "//metropolis/node/allocs",
         "//osbase/bringup",
         "//osbase/net/dhcp4c",
         "//osbase/net/dhcp4c/callback",
diff --git a/metropolis/test/nanoswitch/nanoswitch.go b/metropolis/test/nanoswitch/nanoswitch.go
index 78d8efe..56f11a8 100644
--- a/metropolis/test/nanoswitch/nanoswitch.go
+++ b/metropolis/test/nanoswitch/nanoswitch.go
@@ -28,7 +28,7 @@
 	"github.com/insomniacslk/dhcp/dhcpv4/server4"
 	"github.com/vishvananda/netlink"
 
-	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/allocs"
 	"source.monogon.dev/osbase/bringup"
 	"source.monogon.dev/osbase/net/dhcp4c"
 	dhcpcb "source.monogon.dev/osbase/net/dhcp4c/callback"
@@ -128,7 +128,7 @@
 
 // userspaceProxy listens on port and proxies all TCP connections to the same
 // port on targetIP
-func userspaceProxy(targetIP net.IP, port common.Port) supervisor.Runnable {
+func userspaceProxy(targetIP net.IP, port allocs.Port) supervisor.Runnable {
 	return func(ctx context.Context) error {
 		logger := supervisor.Logger(ctx)
 		tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(0, 0, 0, 0), Port: int(port)})
@@ -289,10 +289,10 @@
 		logger.Info("No upstream interface detected")
 	}
 	supervisor.Run(ctx, "dhcp-server", runDHCPServer(vmBridgeLink))
-	supervisor.Run(ctx, "proxy-cur1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.CuratorServicePort))
-	supervisor.Run(ctx, "proxy-dbg1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.DebugServicePort))
-	supervisor.Run(ctx, "proxy-k8s-api1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.KubernetesAPIPort))
-	supervisor.Run(ctx, "proxy-k8s-api-wrapped1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.KubernetesAPIWrappedPort))
+	supervisor.Run(ctx, "proxy-cur1", userspaceProxy(net.IPv4(10, 1, 0, 2), allocs.PortCuratorService))
+	supervisor.Run(ctx, "proxy-dbg1", userspaceProxy(net.IPv4(10, 1, 0, 2), allocs.PortDebugService))
+	supervisor.Run(ctx, "proxy-k8s-api1", userspaceProxy(net.IPv4(10, 1, 0, 2), allocs.PortKubernetesAPI))
+	supervisor.Run(ctx, "proxy-k8s-api-wrapped1", userspaceProxy(net.IPv4(10, 1, 0, 2), allocs.PortKubernetesAPIWrapped))
 	supervisor.Run(ctx, "socks", runSOCKSProxy)
 	supervisor.Signal(ctx, supervisor.SignalHealthy)
 	supervisor.Signal(ctx, supervisor.SignalDone)