blob: 39e50265e84c44e32fa37f7bc7a0cfcc53e552f0 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Lorenz Brun52f7f292020-06-24 16:42:02 +02002// SPDX-License-Identifier: Apache-2.0
Lorenz Brun52f7f292020-06-24 16:42:02 +02003
4// nanoswitch is a virtualized switch/router combo intended for testing.
Serge Bazanski216fe7b2021-05-21 18:36:16 +02005// It uses the first interface as an external interface to connect to the host
6// and pass traffic in and out. All other interfaces are switched together and
7// served by a built-in DHCP server. Traffic from that network to the
8// SLIRP/external network is SNATed as the host-side SLIRP ignores routed
9// packets.
Serge Bazanskibe742842022-04-04 13:18:50 +020010//
11// It also has built-in userspace proxying support for accessing the first
12// node's services, as well as a SOCKS proxy to access all nodes within the
13// network.
Lorenz Brun52f7f292020-06-24 16:42:02 +020014package main
15
16import (
17 "bytes"
18 "context"
19 "fmt"
20 "io"
Lorenz Brun52f7f292020-06-24 16:42:02 +020021 "net"
22 "os"
23 "time"
24
25 "github.com/google/nftables"
26 "github.com/google/nftables/expr"
27 "github.com/insomniacslk/dhcp/dhcpv4"
28 "github.com/insomniacslk/dhcp/dhcpv4/server4"
29 "github.com/vishvananda/netlink"
Lorenz Brun52f7f292020-06-24 16:42:02 +020030 "golang.org/x/sys/unix"
31
Serge Bazanski31370b02021-01-07 16:31:14 +010032 common "source.monogon.dev/metropolis/node"
33 "source.monogon.dev/metropolis/node/core/network/dhcp4c"
34 dhcpcb "source.monogon.dev/metropolis/node/core/network/dhcp4c/callback"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020035 "source.monogon.dev/osbase/logtree"
36 "source.monogon.dev/osbase/supervisor"
Tim Windelschmidtd0cdb572025-03-27 17:18:39 +010037 "source.monogon.dev/osbase/test/qemu"
Lorenz Brun52f7f292020-06-24 16:42:02 +020038)
39
40var switchIP = net.IP{10, 1, 0, 1}
41var switchSubnetMask = net.CIDRMask(24, 32)
42
Serge Bazanski216fe7b2021-05-21 18:36:16 +020043// defaultLeaseOptions sets the lease options needed to properly configure
44// connectivity to nanoswitch.
Lorenz Brun52f7f292020-06-24 16:42:02 +020045func defaultLeaseOptions(reply *dhcpv4.DHCPv4) {
46 reply.GatewayIPAddr = switchIP
Serge Bazanski216fe7b2021-05-21 18:36:16 +020047 // SLIRP fake DNS server.
48 reply.UpdateOption(dhcpv4.OptDNS(net.IPv4(10, 42, 0, 3)))
Lorenz Brun52f7f292020-06-24 16:42:02 +020049 reply.UpdateOption(dhcpv4.OptRouter(switchIP))
Serge Bazanski216fe7b2021-05-21 18:36:16 +020050 // Make sure we exercise our DHCP client in E2E tests.
51 reply.UpdateOption(dhcpv4.OptIPAddressLeaseTime(30 * time.Second))
Lorenz Brun52f7f292020-06-24 16:42:02 +020052 reply.UpdateOption(dhcpv4.OptSubnetMask(switchSubnetMask))
53}
54
Serge Bazanski216fe7b2021-05-21 18:36:16 +020055// runDHCPServer runs an extremely minimal DHCP server with most options
56// hardcoded, a wrapping bump allocator for the IPs, 30 second lease timeout
57// and no support for DHCP collision detection.
Lorenz Brun52f7f292020-06-24 16:42:02 +020058func runDHCPServer(link netlink.Link) supervisor.Runnable {
Serge Bazanski3e5e5802022-06-21 13:46:31 +020059 currentIP := net.IP{10, 1, 0, 2}
Lorenz Brun52f7f292020-06-24 16:42:02 +020060
Serge Bazanskid279dc02022-05-06 12:17:42 +020061 // Map from stringified MAC address to IP address, allowing handing out the
62 // same IP to a given MAC on re-discovery.
63 leases := make(map[string]net.IP)
64
Lorenz Brun52f7f292020-06-24 16:42:02 +020065 return func(ctx context.Context) error {
66 laddr := net.UDPAddr{
67 IP: net.IPv4(0, 0, 0, 0),
68 Port: 67,
69 }
70 server, err := server4.NewServer(link.Attrs().Name, &laddr, func(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
71 if m == nil {
72 return
73 }
74 reply, err := dhcpv4.NewReplyFromRequest(m)
75 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +010076 supervisor.Logger(ctx).Warningf("Failed to generate DHCP reply: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +020077 return
78 }
79 reply.UpdateOption(dhcpv4.OptServerIdentifier(switchIP))
80 reply.ServerIPAddr = switchIP
81
82 switch m.MessageType() {
83 case dhcpv4.MessageTypeDiscover:
84 reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
85 defaultLeaseOptions(reply)
Serge Bazanskid279dc02022-05-06 12:17:42 +020086 hwaddr := m.ClientHWAddr.String()
87 // Either hand out already allocated address from leases, or allocate new.
88 if ip, ok := leases[hwaddr]; ok {
89 reply.YourIPAddr = ip
90 } else {
Serge Bazanski3e5e5802022-06-21 13:46:31 +020091 leases[hwaddr] = net.ParseIP(currentIP.String())
92 reply.YourIPAddr = leases[hwaddr]
Serge Bazanskid279dc02022-05-06 12:17:42 +020093 currentIP[3]++ // Works only because it's a /24
94 }
95 supervisor.Logger(ctx).Infof("Replying with DHCP IP %s to %s", reply.YourIPAddr.String(), hwaddr)
Lorenz Brun52f7f292020-06-24 16:42:02 +020096 case dhcpv4.MessageTypeRequest:
97 reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
98 defaultLeaseOptions(reply)
Lorenz Brundbac6cc2020-11-30 10:57:26 +010099 if m.RequestedIPAddress() != nil {
100 reply.YourIPAddr = m.RequestedIPAddress()
101 } else {
102 reply.YourIPAddr = m.ClientIPAddr
103 }
Lorenz Brun52f7f292020-06-24 16:42:02 +0200104 case dhcpv4.MessageTypeRelease, dhcpv4.MessageTypeDecline:
105 supervisor.Logger(ctx).Info("Ignoring Release/Decline")
106 }
107 if _, err := conn.WriteTo(reply.ToBytes(), peer); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100108 supervisor.Logger(ctx).Warningf("Cannot reply to client: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200109 }
110 })
111 if err != nil {
112 return err
113 }
114 supervisor.Signal(ctx, supervisor.SignalHealthy)
115 go func() {
116 <-ctx.Done()
117 server.Close()
118 }()
119 return server.Serve()
120 }
121}
122
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200123// userspaceProxy listens on port and proxies all TCP connections to the same
124// port on targetIP
Serge Bazanski52304a82021-10-29 16:56:18 +0200125func userspaceProxy(targetIP net.IP, port common.Port) supervisor.Runnable {
Lorenz Brun52f7f292020-06-24 16:42:02 +0200126 return func(ctx context.Context) error {
127 logger := supervisor.Logger(ctx)
128 tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(0, 0, 0, 0), Port: int(port)})
129 if err != nil {
130 return err
131 }
132 supervisor.Signal(ctx, supervisor.SignalHealthy)
133 go func() {
134 <-ctx.Done()
135 tcpListener.Close()
136 }()
137 for {
138 conn, err := tcpListener.AcceptTCP()
139 if err != nil {
140 if ctx.Err() != nil {
141 return ctx.Err()
142 }
143 return err
144 }
145 go func(conn *net.TCPConn) {
146 defer conn.Close()
147 upstreamConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: targetIP, Port: int(port)})
148 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100149 logger.Infof("Userspace proxy failed to connect to upstream: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200150 return
151 }
152 defer upstreamConn.Close()
153 go io.Copy(upstreamConn, conn)
154 io.Copy(conn, upstreamConn)
155 }(conn)
156 }
157
158 }
159}
160
161// addNetworkRoutes sets up routing from DHCP
162func addNetworkRoutes(link netlink.Link, addr net.IPNet, gw net.IP) error {
163 if err := netlink.AddrReplace(link, &netlink.Addr{IPNet: &addr}); err != nil {
164 return fmt.Errorf("failed to add DHCP address to network interface \"%v\": %w", link.Attrs().Name, err)
165 }
166
167 if gw.IsUnspecified() {
168 return nil
169 }
170
171 route := &netlink.Route{
172 Dst: &net.IPNet{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(0, 0, 0, 0)},
173 Gw: gw,
174 Scope: netlink.SCOPE_UNIVERSE,
175 }
176 if err := netlink.RouteAdd(route); err != nil {
Tim Windelschmidtadcf5d72024-05-21 13:46:25 +0200177 return fmt.Errorf("could not add default route: netlink.RouteAdd(%+v): %w", route, err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200178 }
179 return nil
180}
181
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200182// nfifname converts an interface name into 16 bytes padded with zeroes (for
183// nftables)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200184func nfifname(n string) []byte {
185 b := make([]byte, 16)
Tim Windelschmidt5e460a92024-04-11 01:33:09 +0200186 copy(b, n+"\x00")
Lorenz Brun52f7f292020-06-24 16:42:02 +0200187 return b
188}
189
190func main() {
Lorenz Brundf952412020-12-21 14:59:36 +0100191 lt := logtree.New()
192 reader, err := lt.Read("", logtree.WithChildren(), logtree.WithStream())
193 if err != nil {
Tim Windelschmidt6b207812024-04-22 19:13:15 +0200194 panic(fmt.Sprintf("could not set up root log reader: %v", err))
Lorenz Brundf952412020-12-21 14:59:36 +0100195 }
196 go func() {
197 for p := range reader.Stream {
Serge Bazanskicc4e96a2023-03-16 17:57:14 +0100198 fmt.Fprintf(os.Stderr, "%s\n", p.ConciseString(nil, 120))
Lorenz Brundf952412020-12-21 14:59:36 +0100199 }
200 }()
Serge Bazanskic7359672020-10-30 16:38:57 +0100201 supervisor.New(context.Background(), func(ctx context.Context) error {
Lorenz Brun52f7f292020-06-24 16:42:02 +0200202 logger := supervisor.Logger(ctx)
203 logger.Info("Starting NanoSwitch, a tiny TOR switch emulator")
204
205 // Set up target filesystems.
206 for _, el := range []struct {
207 dir string
208 fs string
209 flags uintptr
210 }{
211 {"/sys", "sysfs", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
212 {"/proc", "proc", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
213 {"/dev", "devtmpfs", unix.MS_NOEXEC | unix.MS_NOSUID},
214 {"/dev/pts", "devpts", unix.MS_NOEXEC | unix.MS_NOSUID},
215 } {
216 if err := os.Mkdir(el.dir, 0755); err != nil && !os.IsExist(err) {
217 return fmt.Errorf("could not make %s: %w", el.dir, err)
218 }
219 if err := unix.Mount(el.fs, el.dir, el.fs, el.flags, ""); err != nil {
220 return fmt.Errorf("could not mount %s on %s: %w", el.fs, el.dir, err)
221 }
222 }
223
224 c := &nftables.Conn{}
225
226 links, err := netlink.LinkList()
227 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100228 logger.Fatalf("Failed to list links: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200229 }
230 var externalLink netlink.Link
231 var vmLinks []netlink.Link
232 for _, link := range links {
233 attrs := link.Attrs()
234 if link.Type() == "device" && len(attrs.HardwareAddr) > 0 {
235 if attrs.Flags&net.FlagUp != net.FlagUp {
236 netlink.LinkSetUp(link) // Attempt to take up all ethernet links
237 }
Tim Windelschmidtd0cdb572025-03-27 17:18:39 +0100238 if bytes.Equal(attrs.HardwareAddr, qemu.HostInterfaceMAC) {
Lorenz Brun52f7f292020-06-24 16:42:02 +0200239 externalLink = link
240 } else {
241 vmLinks = append(vmLinks, link)
242 }
243 }
244 }
245 vmBridgeLink := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: "vmbridge", Flags: net.FlagUp}}
246 if err := netlink.LinkAdd(vmBridgeLink); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100247 logger.Fatalf("Failed to create vmbridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200248 }
249 for _, link := range vmLinks {
250 if err := netlink.LinkSetMaster(link, vmBridgeLink); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100251 logger.Fatalf("Failed to add VM interface to bridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200252 }
Serge Bazanskic7359672020-10-30 16:38:57 +0100253 logger.Infof("Assigned interface %s to bridge", link.Attrs().Name)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200254 }
255 if err := netlink.AddrReplace(vmBridgeLink, &netlink.Addr{IPNet: &net.IPNet{IP: switchIP, Mask: switchSubnetMask}}); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100256 logger.Fatalf("Failed to assign static IP to vmbridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200257 }
258 if externalLink != nil {
259 nat := c.AddTable(&nftables.Table{
260 Family: nftables.TableFamilyIPv4,
261 Name: "nat",
262 })
263
264 postrouting := c.AddChain(&nftables.Chain{
265 Name: "postrouting",
266 Hooknum: nftables.ChainHookPostrouting,
267 Priority: nftables.ChainPriorityNATSource,
268 Table: nat,
269 Type: nftables.ChainTypeNAT,
270 })
271
272 // Masquerade/SNAT all traffic going out of the external interface
273 c.AddRule(&nftables.Rule{
274 Table: nat,
275 Chain: postrouting,
276 Exprs: []expr.Any{
277 &expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
278 &expr.Cmp{
279 Op: expr.CmpOpEq,
280 Register: 1,
281 Data: nfifname(externalLink.Attrs().Name),
282 },
283 &expr.Masq{},
284 },
285 })
286
287 if err := c.Flush(); err != nil {
288 panic(err)
289 }
290
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100291 netIface := &net.Interface{
292 Name: externalLink.Attrs().Name,
293 MTU: externalLink.Attrs().MTU,
294 Index: externalLink.Attrs().Index,
295 Flags: externalLink.Attrs().Flags,
296 HardwareAddr: externalLink.Attrs().HardwareAddr,
297 }
298 dhcpClient, err := dhcp4c.NewClient(netIface)
299 if err != nil {
300 logger.Fatalf("Failed to create DHCP client: %v", err)
301 }
302 dhcpClient.RequestedOptions = []dhcpv4.OptionCode{dhcpv4.OptionRouter}
Lorenz Brunfdb73222021-12-13 05:19:25 +0100303 dhcpClient.LeaseCallback = dhcpcb.Compose(dhcpcb.ManageIP(externalLink), dhcpcb.ManageRoutes(externalLink))
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100304 supervisor.Run(ctx, "dhcp-client", dhcpClient.Run)
Lorenz Brun764a2de2021-11-22 16:26:36 +0100305 if err := os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1\n"), 0644); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100306 logger.Fatalf("Failed to write ip forwards: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200307 }
Lorenz Brun52f7f292020-06-24 16:42:02 +0200308 } else {
309 logger.Info("No upstream interface detected")
310 }
311 supervisor.Run(ctx, "dhcp-server", runDHCPServer(vmBridgeLink))
Serge Bazanski66e58952021-10-05 17:06:56 +0200312 supervisor.Run(ctx, "proxy-cur1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.CuratorServicePort))
Lorenz Brun52f7f292020-06-24 16:42:02 +0200313 supervisor.Run(ctx, "proxy-dbg1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.DebugServicePort))
314 supervisor.Run(ctx, "proxy-k8s-api1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.KubernetesAPIPort))
Lorenz Bruncc078df2021-12-23 11:51:55 +0100315 supervisor.Run(ctx, "proxy-k8s-api-wrapped1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.KubernetesAPIWrappedPort))
Serge Bazanskibe742842022-04-04 13:18:50 +0200316 supervisor.Run(ctx, "socks", runSOCKSProxy)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200317 supervisor.Signal(ctx, supervisor.SignalHealthy)
318 supervisor.Signal(ctx, supervisor.SignalDone)
319 return nil
Lorenz Brundf952412020-12-21 14:59:36 +0100320 }, supervisor.WithExistingLogtree(lt))
Lorenz Brun52f7f292020-06-24 16:42:02 +0200321 select {}
322}