blob: bfd3323d08c5b53062809083534569dc0732c961 [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.
Serge Bazanskibe742842022-04-04 13:18:50 +020023//
24// It also has built-in userspace proxying support for accessing the first
25// node's services, as well as a SOCKS proxy to access all nodes within the
26// network.
Lorenz Brun52f7f292020-06-24 16:42:02 +020027package main
28
29import (
30 "bytes"
31 "context"
32 "fmt"
33 "io"
Lorenz Brun52f7f292020-06-24 16:42:02 +020034 "net"
35 "os"
36 "time"
37
38 "github.com/google/nftables"
39 "github.com/google/nftables/expr"
40 "github.com/insomniacslk/dhcp/dhcpv4"
41 "github.com/insomniacslk/dhcp/dhcpv4/server4"
42 "github.com/vishvananda/netlink"
Lorenz Brun52f7f292020-06-24 16:42:02 +020043 "golang.org/x/sys/unix"
44
Serge Bazanski31370b02021-01-07 16:31:14 +010045 common "source.monogon.dev/metropolis/node"
46 "source.monogon.dev/metropolis/node/core/network/dhcp4c"
47 dhcpcb "source.monogon.dev/metropolis/node/core/network/dhcp4c/callback"
48 "source.monogon.dev/metropolis/pkg/logtree"
49 "source.monogon.dev/metropolis/pkg/supervisor"
50 "source.monogon.dev/metropolis/test/launch"
Lorenz Brun52f7f292020-06-24 16:42:02 +020051)
52
53var switchIP = net.IP{10, 1, 0, 1}
54var switchSubnetMask = net.CIDRMask(24, 32)
55
Serge Bazanski216fe7b2021-05-21 18:36:16 +020056// defaultLeaseOptions sets the lease options needed to properly configure
57// connectivity to nanoswitch.
Lorenz Brun52f7f292020-06-24 16:42:02 +020058func defaultLeaseOptions(reply *dhcpv4.DHCPv4) {
59 reply.GatewayIPAddr = switchIP
Serge Bazanski216fe7b2021-05-21 18:36:16 +020060 // SLIRP fake DNS server.
61 reply.UpdateOption(dhcpv4.OptDNS(net.IPv4(10, 42, 0, 3)))
Lorenz Brun52f7f292020-06-24 16:42:02 +020062 reply.UpdateOption(dhcpv4.OptRouter(switchIP))
Serge Bazanski216fe7b2021-05-21 18:36:16 +020063 // Make sure we exercise our DHCP client in E2E tests.
64 reply.UpdateOption(dhcpv4.OptIPAddressLeaseTime(30 * time.Second))
Lorenz Brun52f7f292020-06-24 16:42:02 +020065 reply.UpdateOption(dhcpv4.OptSubnetMask(switchSubnetMask))
66}
67
Serge Bazanski216fe7b2021-05-21 18:36:16 +020068// runDHCPServer runs an extremely minimal DHCP server with most options
69// hardcoded, a wrapping bump allocator for the IPs, 30 second lease timeout
70// and no support for DHCP collision detection.
Lorenz Brun52f7f292020-06-24 16:42:02 +020071func runDHCPServer(link netlink.Link) supervisor.Runnable {
Serge Bazanski3e5e5802022-06-21 13:46:31 +020072 currentIP := net.IP{10, 1, 0, 2}
Lorenz Brun52f7f292020-06-24 16:42:02 +020073
Serge Bazanskid279dc02022-05-06 12:17:42 +020074 // Map from stringified MAC address to IP address, allowing handing out the
75 // same IP to a given MAC on re-discovery.
76 leases := make(map[string]net.IP)
77
Lorenz Brun52f7f292020-06-24 16:42:02 +020078 return func(ctx context.Context) error {
79 laddr := net.UDPAddr{
80 IP: net.IPv4(0, 0, 0, 0),
81 Port: 67,
82 }
83 server, err := server4.NewServer(link.Attrs().Name, &laddr, func(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
84 if m == nil {
85 return
86 }
87 reply, err := dhcpv4.NewReplyFromRequest(m)
88 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +010089 supervisor.Logger(ctx).Warningf("Failed to generate DHCP reply: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +020090 return
91 }
92 reply.UpdateOption(dhcpv4.OptServerIdentifier(switchIP))
93 reply.ServerIPAddr = switchIP
94
95 switch m.MessageType() {
96 case dhcpv4.MessageTypeDiscover:
97 reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
98 defaultLeaseOptions(reply)
Serge Bazanskid279dc02022-05-06 12:17:42 +020099 hwaddr := m.ClientHWAddr.String()
100 // Either hand out already allocated address from leases, or allocate new.
101 if ip, ok := leases[hwaddr]; ok {
102 reply.YourIPAddr = ip
103 } else {
Serge Bazanski3e5e5802022-06-21 13:46:31 +0200104 leases[hwaddr] = net.ParseIP(currentIP.String())
105 reply.YourIPAddr = leases[hwaddr]
Serge Bazanskid279dc02022-05-06 12:17:42 +0200106 currentIP[3]++ // Works only because it's a /24
107 }
108 supervisor.Logger(ctx).Infof("Replying with DHCP IP %s to %s", reply.YourIPAddr.String(), hwaddr)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200109 case dhcpv4.MessageTypeRequest:
110 reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
111 defaultLeaseOptions(reply)
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100112 if m.RequestedIPAddress() != nil {
113 reply.YourIPAddr = m.RequestedIPAddress()
114 } else {
115 reply.YourIPAddr = m.ClientIPAddr
116 }
Lorenz Brun52f7f292020-06-24 16:42:02 +0200117 case dhcpv4.MessageTypeRelease, dhcpv4.MessageTypeDecline:
118 supervisor.Logger(ctx).Info("Ignoring Release/Decline")
119 }
120 if _, err := conn.WriteTo(reply.ToBytes(), peer); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100121 supervisor.Logger(ctx).Warningf("Cannot reply to client: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200122 }
123 })
124 if err != nil {
125 return err
126 }
127 supervisor.Signal(ctx, supervisor.SignalHealthy)
128 go func() {
129 <-ctx.Done()
130 server.Close()
131 }()
132 return server.Serve()
133 }
134}
135
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200136// userspaceProxy listens on port and proxies all TCP connections to the same
137// port on targetIP
Serge Bazanski52304a82021-10-29 16:56:18 +0200138func userspaceProxy(targetIP net.IP, port common.Port) supervisor.Runnable {
Lorenz Brun52f7f292020-06-24 16:42:02 +0200139 return func(ctx context.Context) error {
140 logger := supervisor.Logger(ctx)
141 tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(0, 0, 0, 0), Port: int(port)})
142 if err != nil {
143 return err
144 }
145 supervisor.Signal(ctx, supervisor.SignalHealthy)
146 go func() {
147 <-ctx.Done()
148 tcpListener.Close()
149 }()
150 for {
151 conn, err := tcpListener.AcceptTCP()
152 if err != nil {
153 if ctx.Err() != nil {
154 return ctx.Err()
155 }
156 return err
157 }
158 go func(conn *net.TCPConn) {
159 defer conn.Close()
160 upstreamConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: targetIP, Port: int(port)})
161 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100162 logger.Infof("Userspace proxy failed to connect to upstream: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200163 return
164 }
165 defer upstreamConn.Close()
166 go io.Copy(upstreamConn, conn)
167 io.Copy(conn, upstreamConn)
168 }(conn)
169 }
170
171 }
172}
173
174// addNetworkRoutes sets up routing from DHCP
175func addNetworkRoutes(link netlink.Link, addr net.IPNet, gw net.IP) error {
176 if err := netlink.AddrReplace(link, &netlink.Addr{IPNet: &addr}); err != nil {
177 return fmt.Errorf("failed to add DHCP address to network interface \"%v\": %w", link.Attrs().Name, err)
178 }
179
180 if gw.IsUnspecified() {
181 return nil
182 }
183
184 route := &netlink.Route{
185 Dst: &net.IPNet{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(0, 0, 0, 0)},
186 Gw: gw,
187 Scope: netlink.SCOPE_UNIVERSE,
188 }
189 if err := netlink.RouteAdd(route); err != nil {
190 return fmt.Errorf("could not add default route: netlink.RouteAdd(%+v): %v", route, err)
191 }
192 return nil
193}
194
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200195// nfifname converts an interface name into 16 bytes padded with zeroes (for
196// nftables)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200197func nfifname(n string) []byte {
198 b := make([]byte, 16)
199 copy(b, []byte(n+"\x00"))
200 return b
201}
202
203func main() {
Lorenz Brundf952412020-12-21 14:59:36 +0100204 lt := logtree.New()
205 reader, err := lt.Read("", logtree.WithChildren(), logtree.WithStream())
206 if err != nil {
207 panic(fmt.Errorf("could not set up root log reader: %v", err))
208 }
209 go func() {
210 for p := range reader.Stream {
211 fmt.Fprintf(os.Stderr, "%s\n", p.String())
212 }
213 }()
Serge Bazanskic7359672020-10-30 16:38:57 +0100214 supervisor.New(context.Background(), func(ctx context.Context) error {
Lorenz Brun52f7f292020-06-24 16:42:02 +0200215 logger := supervisor.Logger(ctx)
216 logger.Info("Starting NanoSwitch, a tiny TOR switch emulator")
217
218 // Set up target filesystems.
219 for _, el := range []struct {
220 dir string
221 fs string
222 flags uintptr
223 }{
224 {"/sys", "sysfs", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
225 {"/proc", "proc", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
226 {"/dev", "devtmpfs", unix.MS_NOEXEC | unix.MS_NOSUID},
227 {"/dev/pts", "devpts", unix.MS_NOEXEC | unix.MS_NOSUID},
228 } {
229 if err := os.Mkdir(el.dir, 0755); err != nil && !os.IsExist(err) {
230 return fmt.Errorf("could not make %s: %w", el.dir, err)
231 }
232 if err := unix.Mount(el.fs, el.dir, el.fs, el.flags, ""); err != nil {
233 return fmt.Errorf("could not mount %s on %s: %w", el.fs, el.dir, err)
234 }
235 }
236
237 c := &nftables.Conn{}
238
239 links, err := netlink.LinkList()
240 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100241 logger.Fatalf("Failed to list links: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200242 }
243 var externalLink netlink.Link
244 var vmLinks []netlink.Link
245 for _, link := range links {
246 attrs := link.Attrs()
247 if link.Type() == "device" && len(attrs.HardwareAddr) > 0 {
248 if attrs.Flags&net.FlagUp != net.FlagUp {
249 netlink.LinkSetUp(link) // Attempt to take up all ethernet links
250 }
251 if bytes.Equal(attrs.HardwareAddr, launch.HostInterfaceMAC) {
252 externalLink = link
253 } else {
254 vmLinks = append(vmLinks, link)
255 }
256 }
257 }
258 vmBridgeLink := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: "vmbridge", Flags: net.FlagUp}}
259 if err := netlink.LinkAdd(vmBridgeLink); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100260 logger.Fatalf("Failed to create vmbridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200261 }
262 for _, link := range vmLinks {
263 if err := netlink.LinkSetMaster(link, vmBridgeLink); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100264 logger.Fatalf("Failed to add VM interface to bridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200265 }
Serge Bazanskic7359672020-10-30 16:38:57 +0100266 logger.Infof("Assigned interface %s to bridge", link.Attrs().Name)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200267 }
268 if err := netlink.AddrReplace(vmBridgeLink, &netlink.Addr{IPNet: &net.IPNet{IP: switchIP, Mask: switchSubnetMask}}); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100269 logger.Fatalf("Failed to assign static IP to vmbridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200270 }
271 if externalLink != nil {
272 nat := c.AddTable(&nftables.Table{
273 Family: nftables.TableFamilyIPv4,
274 Name: "nat",
275 })
276
277 postrouting := c.AddChain(&nftables.Chain{
278 Name: "postrouting",
279 Hooknum: nftables.ChainHookPostrouting,
280 Priority: nftables.ChainPriorityNATSource,
281 Table: nat,
282 Type: nftables.ChainTypeNAT,
283 })
284
285 // Masquerade/SNAT all traffic going out of the external interface
286 c.AddRule(&nftables.Rule{
287 Table: nat,
288 Chain: postrouting,
289 Exprs: []expr.Any{
290 &expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
291 &expr.Cmp{
292 Op: expr.CmpOpEq,
293 Register: 1,
294 Data: nfifname(externalLink.Attrs().Name),
295 },
296 &expr.Masq{},
297 },
298 })
299
300 if err := c.Flush(); err != nil {
301 panic(err)
302 }
303
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100304 netIface := &net.Interface{
305 Name: externalLink.Attrs().Name,
306 MTU: externalLink.Attrs().MTU,
307 Index: externalLink.Attrs().Index,
308 Flags: externalLink.Attrs().Flags,
309 HardwareAddr: externalLink.Attrs().HardwareAddr,
310 }
311 dhcpClient, err := dhcp4c.NewClient(netIface)
312 if err != nil {
313 logger.Fatalf("Failed to create DHCP client: %v", err)
314 }
315 dhcpClient.RequestedOptions = []dhcpv4.OptionCode{dhcpv4.OptionRouter}
Lorenz Brunfdb73222021-12-13 05:19:25 +0100316 dhcpClient.LeaseCallback = dhcpcb.Compose(dhcpcb.ManageIP(externalLink), dhcpcb.ManageRoutes(externalLink))
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100317 supervisor.Run(ctx, "dhcp-client", dhcpClient.Run)
Lorenz Brun764a2de2021-11-22 16:26:36 +0100318 if err := os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1\n"), 0644); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100319 logger.Fatalf("Failed to write ip forwards: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200320 }
Lorenz Brun52f7f292020-06-24 16:42:02 +0200321 } else {
322 logger.Info("No upstream interface detected")
323 }
324 supervisor.Run(ctx, "dhcp-server", runDHCPServer(vmBridgeLink))
Serge Bazanski66e58952021-10-05 17:06:56 +0200325 supervisor.Run(ctx, "proxy-cur1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.CuratorServicePort))
Lorenz Brun52f7f292020-06-24 16:42:02 +0200326 supervisor.Run(ctx, "proxy-dbg1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.DebugServicePort))
327 supervisor.Run(ctx, "proxy-k8s-api1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.KubernetesAPIPort))
Lorenz Bruncc078df2021-12-23 11:51:55 +0100328 supervisor.Run(ctx, "proxy-k8s-api-wrapped1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.KubernetesAPIWrappedPort))
Serge Bazanskibe742842022-04-04 13:18:50 +0200329 supervisor.Run(ctx, "socks", runSOCKSProxy)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200330 supervisor.Signal(ctx, supervisor.SignalHealthy)
331 supervisor.Signal(ctx, supervisor.SignalDone)
332 return nil
Lorenz Brundf952412020-12-21 14:59:36 +0100333 }, supervisor.WithExistingLogtree(lt))
Lorenz Brun52f7f292020-06-24 16:42:02 +0200334 select {}
335}