blob: 6d0b6bb2e6be9e03ec87eb0da71a98dab5c779f0 [file] [log] [blame]
Lorenz Brun52f7f292020-06-24 16:42:02 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17// nanoswitch is a virtualized switch/router combo intended for testing.
Serge Bazanski216fe7b2021-05-21 18:36:16 +020018// It uses the first interface as an external interface to connect to the host
19// and pass traffic in and out. All other interfaces are switched together and
20// served by a built-in DHCP server. Traffic from that network to the
21// SLIRP/external network is SNATed as the host-side SLIRP ignores routed
22// packets.
Lorenz Brun52f7f292020-06-24 16:42:02 +020023// It also has built-in userspace proxying support for debugging.
24package main
25
26import (
27 "bytes"
28 "context"
29 "fmt"
30 "io"
Lorenz Brun52f7f292020-06-24 16:42:02 +020031 "net"
32 "os"
33 "time"
34
35 "github.com/google/nftables"
36 "github.com/google/nftables/expr"
37 "github.com/insomniacslk/dhcp/dhcpv4"
38 "github.com/insomniacslk/dhcp/dhcpv4/server4"
39 "github.com/vishvananda/netlink"
Lorenz Brun52f7f292020-06-24 16:42:02 +020040 "golang.org/x/sys/unix"
41
Serge Bazanski31370b02021-01-07 16:31:14 +010042 common "source.monogon.dev/metropolis/node"
43 "source.monogon.dev/metropolis/node/core/network/dhcp4c"
44 dhcpcb "source.monogon.dev/metropolis/node/core/network/dhcp4c/callback"
45 "source.monogon.dev/metropolis/pkg/logtree"
46 "source.monogon.dev/metropolis/pkg/supervisor"
47 "source.monogon.dev/metropolis/test/launch"
Lorenz Brun52f7f292020-06-24 16:42:02 +020048)
49
50var switchIP = net.IP{10, 1, 0, 1}
51var switchSubnetMask = net.CIDRMask(24, 32)
52
Serge Bazanski216fe7b2021-05-21 18:36:16 +020053// defaultLeaseOptions sets the lease options needed to properly configure
54// connectivity to nanoswitch.
Lorenz Brun52f7f292020-06-24 16:42:02 +020055func defaultLeaseOptions(reply *dhcpv4.DHCPv4) {
56 reply.GatewayIPAddr = switchIP
Serge Bazanski216fe7b2021-05-21 18:36:16 +020057 // SLIRP fake DNS server.
58 reply.UpdateOption(dhcpv4.OptDNS(net.IPv4(10, 42, 0, 3)))
Lorenz Brun52f7f292020-06-24 16:42:02 +020059 reply.UpdateOption(dhcpv4.OptRouter(switchIP))
Serge Bazanski216fe7b2021-05-21 18:36:16 +020060 // Make sure we exercise our DHCP client in E2E tests.
61 reply.UpdateOption(dhcpv4.OptIPAddressLeaseTime(30 * time.Second))
Lorenz Brun52f7f292020-06-24 16:42:02 +020062 reply.UpdateOption(dhcpv4.OptSubnetMask(switchSubnetMask))
63}
64
Serge Bazanski216fe7b2021-05-21 18:36:16 +020065// runDHCPServer runs an extremely minimal DHCP server with most options
66// hardcoded, a wrapping bump allocator for the IPs, 30 second lease timeout
67// and no support for DHCP collision detection.
Lorenz Brun52f7f292020-06-24 16:42:02 +020068func runDHCPServer(link netlink.Link) supervisor.Runnable {
69 currentIP := net.IP{10, 1, 0, 1}
70
71 return func(ctx context.Context) error {
72 laddr := net.UDPAddr{
73 IP: net.IPv4(0, 0, 0, 0),
74 Port: 67,
75 }
76 server, err := server4.NewServer(link.Attrs().Name, &laddr, func(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
77 if m == nil {
78 return
79 }
80 reply, err := dhcpv4.NewReplyFromRequest(m)
81 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +010082 supervisor.Logger(ctx).Warningf("Failed to generate DHCP reply: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +020083 return
84 }
85 reply.UpdateOption(dhcpv4.OptServerIdentifier(switchIP))
86 reply.ServerIPAddr = switchIP
87
88 switch m.MessageType() {
89 case dhcpv4.MessageTypeDiscover:
90 reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
91 defaultLeaseOptions(reply)
92 currentIP[3]++ // Works only because it's a /24
93 reply.YourIPAddr = currentIP
Serge Bazanskic7359672020-10-30 16:38:57 +010094 supervisor.Logger(ctx).Infof("Replying with DHCP IP %s", reply.YourIPAddr.String())
Lorenz Brun52f7f292020-06-24 16:42:02 +020095 case dhcpv4.MessageTypeRequest:
96 reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
97 defaultLeaseOptions(reply)
Lorenz Brundbac6cc2020-11-30 10:57:26 +010098 if m.RequestedIPAddress() != nil {
99 reply.YourIPAddr = m.RequestedIPAddress()
100 } else {
101 reply.YourIPAddr = m.ClientIPAddr
102 }
Lorenz Brun52f7f292020-06-24 16:42:02 +0200103 case dhcpv4.MessageTypeRelease, dhcpv4.MessageTypeDecline:
104 supervisor.Logger(ctx).Info("Ignoring Release/Decline")
105 }
106 if _, err := conn.WriteTo(reply.ToBytes(), peer); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100107 supervisor.Logger(ctx).Warningf("Cannot reply to client: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200108 }
109 })
110 if err != nil {
111 return err
112 }
113 supervisor.Signal(ctx, supervisor.SignalHealthy)
114 go func() {
115 <-ctx.Done()
116 server.Close()
117 }()
118 return server.Serve()
119 }
120}
121
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200122// userspaceProxy listens on port and proxies all TCP connections to the same
123// port on targetIP
Serge Bazanski52304a82021-10-29 16:56:18 +0200124func userspaceProxy(targetIP net.IP, port common.Port) supervisor.Runnable {
Lorenz Brun52f7f292020-06-24 16:42:02 +0200125 return func(ctx context.Context) error {
126 logger := supervisor.Logger(ctx)
127 tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(0, 0, 0, 0), Port: int(port)})
128 if err != nil {
129 return err
130 }
131 supervisor.Signal(ctx, supervisor.SignalHealthy)
132 go func() {
133 <-ctx.Done()
134 tcpListener.Close()
135 }()
136 for {
137 conn, err := tcpListener.AcceptTCP()
138 if err != nil {
139 if ctx.Err() != nil {
140 return ctx.Err()
141 }
142 return err
143 }
144 go func(conn *net.TCPConn) {
145 defer conn.Close()
146 upstreamConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: targetIP, Port: int(port)})
147 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100148 logger.Infof("Userspace proxy failed to connect to upstream: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200149 return
150 }
151 defer upstreamConn.Close()
152 go io.Copy(upstreamConn, conn)
153 io.Copy(conn, upstreamConn)
154 }(conn)
155 }
156
157 }
158}
159
160// addNetworkRoutes sets up routing from DHCP
161func addNetworkRoutes(link netlink.Link, addr net.IPNet, gw net.IP) error {
162 if err := netlink.AddrReplace(link, &netlink.Addr{IPNet: &addr}); err != nil {
163 return fmt.Errorf("failed to add DHCP address to network interface \"%v\": %w", link.Attrs().Name, err)
164 }
165
166 if gw.IsUnspecified() {
167 return nil
168 }
169
170 route := &netlink.Route{
171 Dst: &net.IPNet{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(0, 0, 0, 0)},
172 Gw: gw,
173 Scope: netlink.SCOPE_UNIVERSE,
174 }
175 if err := netlink.RouteAdd(route); err != nil {
176 return fmt.Errorf("could not add default route: netlink.RouteAdd(%+v): %v", route, err)
177 }
178 return nil
179}
180
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200181// nfifname converts an interface name into 16 bytes padded with zeroes (for
182// nftables)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200183func nfifname(n string) []byte {
184 b := make([]byte, 16)
185 copy(b, []byte(n+"\x00"))
186 return b
187}
188
189func main() {
Lorenz Brundf952412020-12-21 14:59:36 +0100190 lt := logtree.New()
191 reader, err := lt.Read("", logtree.WithChildren(), logtree.WithStream())
192 if err != nil {
193 panic(fmt.Errorf("could not set up root log reader: %v", err))
194 }
195 go func() {
196 for p := range reader.Stream {
197 fmt.Fprintf(os.Stderr, "%s\n", p.String())
198 }
199 }()
Serge Bazanskic7359672020-10-30 16:38:57 +0100200 supervisor.New(context.Background(), func(ctx context.Context) error {
Lorenz Brun52f7f292020-06-24 16:42:02 +0200201 logger := supervisor.Logger(ctx)
202 logger.Info("Starting NanoSwitch, a tiny TOR switch emulator")
203
204 // Set up target filesystems.
205 for _, el := range []struct {
206 dir string
207 fs string
208 flags uintptr
209 }{
210 {"/sys", "sysfs", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
211 {"/proc", "proc", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
212 {"/dev", "devtmpfs", unix.MS_NOEXEC | unix.MS_NOSUID},
213 {"/dev/pts", "devpts", unix.MS_NOEXEC | unix.MS_NOSUID},
214 } {
215 if err := os.Mkdir(el.dir, 0755); err != nil && !os.IsExist(err) {
216 return fmt.Errorf("could not make %s: %w", el.dir, err)
217 }
218 if err := unix.Mount(el.fs, el.dir, el.fs, el.flags, ""); err != nil {
219 return fmt.Errorf("could not mount %s on %s: %w", el.fs, el.dir, err)
220 }
221 }
222
223 c := &nftables.Conn{}
224
225 links, err := netlink.LinkList()
226 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100227 logger.Fatalf("Failed to list links: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200228 }
229 var externalLink netlink.Link
230 var vmLinks []netlink.Link
231 for _, link := range links {
232 attrs := link.Attrs()
233 if link.Type() == "device" && len(attrs.HardwareAddr) > 0 {
234 if attrs.Flags&net.FlagUp != net.FlagUp {
235 netlink.LinkSetUp(link) // Attempt to take up all ethernet links
236 }
237 if bytes.Equal(attrs.HardwareAddr, launch.HostInterfaceMAC) {
238 externalLink = link
239 } else {
240 vmLinks = append(vmLinks, link)
241 }
242 }
243 }
244 vmBridgeLink := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: "vmbridge", Flags: net.FlagUp}}
245 if err := netlink.LinkAdd(vmBridgeLink); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100246 logger.Fatalf("Failed to create vmbridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200247 }
248 for _, link := range vmLinks {
249 if err := netlink.LinkSetMaster(link, vmBridgeLink); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100250 logger.Fatalf("Failed to add VM interface to bridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200251 }
Serge Bazanskic7359672020-10-30 16:38:57 +0100252 logger.Infof("Assigned interface %s to bridge", link.Attrs().Name)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200253 }
254 if err := netlink.AddrReplace(vmBridgeLink, &netlink.Addr{IPNet: &net.IPNet{IP: switchIP, Mask: switchSubnetMask}}); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100255 logger.Fatalf("Failed to assign static IP to vmbridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200256 }
257 if externalLink != nil {
258 nat := c.AddTable(&nftables.Table{
259 Family: nftables.TableFamilyIPv4,
260 Name: "nat",
261 })
262
263 postrouting := c.AddChain(&nftables.Chain{
264 Name: "postrouting",
265 Hooknum: nftables.ChainHookPostrouting,
266 Priority: nftables.ChainPriorityNATSource,
267 Table: nat,
268 Type: nftables.ChainTypeNAT,
269 })
270
271 // Masquerade/SNAT all traffic going out of the external interface
272 c.AddRule(&nftables.Rule{
273 Table: nat,
274 Chain: postrouting,
275 Exprs: []expr.Any{
276 &expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
277 &expr.Cmp{
278 Op: expr.CmpOpEq,
279 Register: 1,
280 Data: nfifname(externalLink.Attrs().Name),
281 },
282 &expr.Masq{},
283 },
284 })
285
286 if err := c.Flush(); err != nil {
287 panic(err)
288 }
289
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100290 netIface := &net.Interface{
291 Name: externalLink.Attrs().Name,
292 MTU: externalLink.Attrs().MTU,
293 Index: externalLink.Attrs().Index,
294 Flags: externalLink.Attrs().Flags,
295 HardwareAddr: externalLink.Attrs().HardwareAddr,
296 }
297 dhcpClient, err := dhcp4c.NewClient(netIface)
298 if err != nil {
299 logger.Fatalf("Failed to create DHCP client: %v", err)
300 }
301 dhcpClient.RequestedOptions = []dhcpv4.OptionCode{dhcpv4.OptionRouter}
Lorenz Brunfdb73222021-12-13 05:19:25 +0100302 dhcpClient.LeaseCallback = dhcpcb.Compose(dhcpcb.ManageIP(externalLink), dhcpcb.ManageRoutes(externalLink))
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100303 supervisor.Run(ctx, "dhcp-client", dhcpClient.Run)
Lorenz Brun764a2de2021-11-22 16:26:36 +0100304 if err := os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1\n"), 0644); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100305 logger.Fatalf("Failed to write ip forwards: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200306 }
Lorenz Brun52f7f292020-06-24 16:42:02 +0200307 } else {
308 logger.Info("No upstream interface detected")
309 }
310 supervisor.Run(ctx, "dhcp-server", runDHCPServer(vmBridgeLink))
Serge Bazanski66e58952021-10-05 17:06:56 +0200311 supervisor.Run(ctx, "proxy-cur1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.CuratorServicePort))
Lorenz Brun52f7f292020-06-24 16:42:02 +0200312 supervisor.Run(ctx, "proxy-dbg1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.DebugServicePort))
313 supervisor.Run(ctx, "proxy-k8s-api1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.KubernetesAPIPort))
314 supervisor.Signal(ctx, supervisor.SignalHealthy)
315 supervisor.Signal(ctx, supervisor.SignalDone)
316 return nil
Lorenz Brundf952412020-12-21 14:59:36 +0100317 }, supervisor.WithExistingLogtree(lt))
Lorenz Brun52f7f292020-06-24 16:42:02 +0200318 select {}
319}