m/test: implement SOCKS proxy in cluster tests

This uses the new socksproxy package to run a proxy server in the
nanoswitch, and uses it within tests to access the test cluster's nodes.

The cluster test code (and nanoswitch) still forward traffic to the
first node, but this will be gradually removed as SOCKS support is
implemented in metroctl and the debug tool. Forwards from host ports to
different node can then be implemented as part of the dbg tool (instead
of the cluster launch code) to maintain a simple interface during debug
and development.

We also use the opportunity to make the non-cluster launch code not
Metropolis specific (by removing an assumption that all ports on all
nodes are Metropolis ports). In the long term, we will probably remove
non-cluster launches entirely (or further turn this code into just being
a 'launch qemu' wrapper).

Change-Id: I9b321bde95ba74fbfaa695eaaad8f9974aba5372
Reviewed-on: https://review.monogon.dev/c/monogon/+/648
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/test/nanoswitch/socks.go b/metropolis/test/nanoswitch/socks.go
new file mode 100644
index 0000000..7b0278a
--- /dev/null
+++ b/metropolis/test/nanoswitch/socks.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"net"
+
+	"source.monogon.dev/metropolis/pkg/socksproxy"
+	"source.monogon.dev/metropolis/pkg/supervisor"
+)
+
+// ONCHANGE(//metropolis/test/launch/cluster:cluster.go): port must be kept in sync
+const SOCKSPort uint16 = 1080
+
+// socksHandler implements a socksproxy.Handler which permits and logs
+// connections to the nanoswitch network.
+type socksHandler struct{}
+
+func (s *socksHandler) Connect(ctx context.Context, req *socksproxy.ConnectRequest) *socksproxy.ConnectResponse {
+	logger := supervisor.Logger(ctx)
+	target := net.JoinHostPort(req.Address.String(), fmt.Sprintf("%d", req.Port))
+
+	if len(req.Address) != 4 {
+		logger.Warningf("Connect %s: wrong address type", target)
+		return &socksproxy.ConnectResponse{
+			Error: socksproxy.ReplyAddressTypeNotSupported,
+		}
+	}
+
+	addr := req.Address
+	switchCIDR := net.IPNet{
+		IP:   switchIP.Mask(switchSubnetMask),
+		Mask: switchSubnetMask,
+	}
+	if !switchCIDR.Contains(addr) || switchCIDR.IP.Equal(addr) {
+		logger.Warningf("Connect %s: not in switch network", target)
+		return &socksproxy.ConnectResponse{
+			Error: socksproxy.ReplyNetworkUnreachable,
+		}
+	}
+
+	con, err := net.Dial("tcp", target)
+	if err != nil {
+		logger.Warningf("Connect %s: dial failed: %v", target, err)
+		return &socksproxy.ConnectResponse{
+			Error: socksproxy.ReplyHostUnreachable,
+		}
+	}
+	res, err := socksproxy.ConnectResponseFromConn(con)
+	if err != nil {
+		logger.Warningf("Connect %s: could not make SOCKS response: %v", target, err)
+		return &socksproxy.ConnectResponse{
+			Error: socksproxy.ReplyGeneralFailure,
+		}
+	}
+	logger.Infof("Connect %s: established", target)
+	return res
+}
+
+// runSOCKSProxy starts a SOCKS proxy to the nanoswitchnetwork at SOCKSPort.
+func runSOCKSProxy(ctx context.Context) error {
+	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", SOCKSPort))
+	if err != nil {
+		return fmt.Errorf("failed to listen on :%d : %v", SOCKSPort, err)
+	}
+
+	h := &socksHandler{}
+	supervisor.Signal(ctx, supervisor.SignalHealthy)
+	return socksproxy.Serve(ctx, h, lis)
+}