blob: 3ab662be3b148324f619c8152eae36f48badf935 [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.
18// It uses the first interface as an external interface to connect to the host and pass traffic in and out. All other
19// interfaces are switched together and served by a built-in DHCP server. Traffic from that network to the
20// SLIRP/external network is SNATed as the host-side SLIRP ignores routed packets.
21// It also has built-in userspace proxying support for debugging.
22package main
23
24import (
25 "bytes"
26 "context"
27 "fmt"
28 "io"
29 "io/ioutil"
30 "net"
31 "os"
32 "time"
33
34 "github.com/google/nftables"
35 "github.com/google/nftables/expr"
36 "github.com/insomniacslk/dhcp/dhcpv4"
37 "github.com/insomniacslk/dhcp/dhcpv4/server4"
38 "github.com/vishvananda/netlink"
Lorenz Brun52f7f292020-06-24 16:42:02 +020039 "golang.org/x/sys/unix"
40
Serge Bazanski31370b02021-01-07 16:31:14 +010041 common "source.monogon.dev/metropolis/node"
42 "source.monogon.dev/metropolis/node/core/network/dhcp4c"
43 dhcpcb "source.monogon.dev/metropolis/node/core/network/dhcp4c/callback"
44 "source.monogon.dev/metropolis/pkg/logtree"
45 "source.monogon.dev/metropolis/pkg/supervisor"
46 "source.monogon.dev/metropolis/test/launch"
Lorenz Brun52f7f292020-06-24 16:42:02 +020047)
48
49var switchIP = net.IP{10, 1, 0, 1}
50var switchSubnetMask = net.CIDRMask(24, 32)
51
52// defaultLeaseOptions sets the lease options needed to properly configure connectivity to nanoswitch
53func defaultLeaseOptions(reply *dhcpv4.DHCPv4) {
54 reply.GatewayIPAddr = switchIP
55 reply.UpdateOption(dhcpv4.OptDNS(net.IPv4(10, 42, 0, 3))) // SLIRP fake DNS server
56 reply.UpdateOption(dhcpv4.OptRouter(switchIP))
Lorenz Brundbac6cc2020-11-30 10:57:26 +010057 reply.UpdateOption(dhcpv4.OptIPAddressLeaseTime(30 * time.Second)) // Make sure we exercise our DHCP client in E2E tests
Lorenz Brun52f7f292020-06-24 16:42:02 +020058 reply.UpdateOption(dhcpv4.OptSubnetMask(switchSubnetMask))
59}
60
61// runDHCPServer runs an extremely minimal DHCP server with most options hardcoded, a wrapping bump allocator for the
Serge Bazanski553ab2b2020-12-21 13:59:33 +010062// IPs, 30 second lease timeout and no support for DHCP collision detection.
Lorenz Brun52f7f292020-06-24 16:42:02 +020063func runDHCPServer(link netlink.Link) supervisor.Runnable {
64 currentIP := net.IP{10, 1, 0, 1}
65
66 return func(ctx context.Context) error {
67 laddr := net.UDPAddr{
68 IP: net.IPv4(0, 0, 0, 0),
69 Port: 67,
70 }
71 server, err := server4.NewServer(link.Attrs().Name, &laddr, func(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
72 if m == nil {
73 return
74 }
75 reply, err := dhcpv4.NewReplyFromRequest(m)
76 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +010077 supervisor.Logger(ctx).Warningf("Failed to generate DHCP reply: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +020078 return
79 }
80 reply.UpdateOption(dhcpv4.OptServerIdentifier(switchIP))
81 reply.ServerIPAddr = switchIP
82
83 switch m.MessageType() {
84 case dhcpv4.MessageTypeDiscover:
85 reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
86 defaultLeaseOptions(reply)
87 currentIP[3]++ // Works only because it's a /24
88 reply.YourIPAddr = currentIP
Serge Bazanskic7359672020-10-30 16:38:57 +010089 supervisor.Logger(ctx).Infof("Replying with DHCP IP %s", reply.YourIPAddr.String())
Lorenz Brun52f7f292020-06-24 16:42:02 +020090 case dhcpv4.MessageTypeRequest:
91 reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
92 defaultLeaseOptions(reply)
Lorenz Brundbac6cc2020-11-30 10:57:26 +010093 if m.RequestedIPAddress() != nil {
94 reply.YourIPAddr = m.RequestedIPAddress()
95 } else {
96 reply.YourIPAddr = m.ClientIPAddr
97 }
Lorenz Brun52f7f292020-06-24 16:42:02 +020098 case dhcpv4.MessageTypeRelease, dhcpv4.MessageTypeDecline:
99 supervisor.Logger(ctx).Info("Ignoring Release/Decline")
100 }
101 if _, err := conn.WriteTo(reply.ToBytes(), peer); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100102 supervisor.Logger(ctx).Warningf("Cannot reply to client: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200103 }
104 })
105 if err != nil {
106 return err
107 }
108 supervisor.Signal(ctx, supervisor.SignalHealthy)
109 go func() {
110 <-ctx.Done()
111 server.Close()
112 }()
113 return server.Serve()
114 }
115}
116
117// userspaceProxy listens on port and proxies all TCP connections to the same port on targetIP
118func userspaceProxy(targetIP net.IP, port uint16) supervisor.Runnable {
119 return func(ctx context.Context) error {
120 logger := supervisor.Logger(ctx)
121 tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(0, 0, 0, 0), Port: int(port)})
122 if err != nil {
123 return err
124 }
125 supervisor.Signal(ctx, supervisor.SignalHealthy)
126 go func() {
127 <-ctx.Done()
128 tcpListener.Close()
129 }()
130 for {
131 conn, err := tcpListener.AcceptTCP()
132 if err != nil {
133 if ctx.Err() != nil {
134 return ctx.Err()
135 }
136 return err
137 }
138 go func(conn *net.TCPConn) {
139 defer conn.Close()
140 upstreamConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: targetIP, Port: int(port)})
141 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100142 logger.Infof("Userspace proxy failed to connect to upstream: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200143 return
144 }
145 defer upstreamConn.Close()
146 go io.Copy(upstreamConn, conn)
147 io.Copy(conn, upstreamConn)
148 }(conn)
149 }
150
151 }
152}
153
154// addNetworkRoutes sets up routing from DHCP
155func addNetworkRoutes(link netlink.Link, addr net.IPNet, gw net.IP) error {
156 if err := netlink.AddrReplace(link, &netlink.Addr{IPNet: &addr}); err != nil {
157 return fmt.Errorf("failed to add DHCP address to network interface \"%v\": %w", link.Attrs().Name, err)
158 }
159
160 if gw.IsUnspecified() {
161 return nil
162 }
163
164 route := &netlink.Route{
165 Dst: &net.IPNet{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(0, 0, 0, 0)},
166 Gw: gw,
167 Scope: netlink.SCOPE_UNIVERSE,
168 }
169 if err := netlink.RouteAdd(route); err != nil {
170 return fmt.Errorf("could not add default route: netlink.RouteAdd(%+v): %v", route, err)
171 }
172 return nil
173}
174
175// nfifname converts an interface name into 16 bytes padded with zeroes (for nftables)
176func nfifname(n string) []byte {
177 b := make([]byte, 16)
178 copy(b, []byte(n+"\x00"))
179 return b
180}
181
182func main() {
Lorenz Brundf952412020-12-21 14:59:36 +0100183 lt := logtree.New()
184 reader, err := lt.Read("", logtree.WithChildren(), logtree.WithStream())
185 if err != nil {
186 panic(fmt.Errorf("could not set up root log reader: %v", err))
187 }
188 go func() {
189 for p := range reader.Stream {
190 fmt.Fprintf(os.Stderr, "%s\n", p.String())
191 }
192 }()
Serge Bazanskic7359672020-10-30 16:38:57 +0100193 supervisor.New(context.Background(), func(ctx context.Context) error {
Lorenz Brun52f7f292020-06-24 16:42:02 +0200194 logger := supervisor.Logger(ctx)
195 logger.Info("Starting NanoSwitch, a tiny TOR switch emulator")
196
197 // Set up target filesystems.
198 for _, el := range []struct {
199 dir string
200 fs string
201 flags uintptr
202 }{
203 {"/sys", "sysfs", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
204 {"/proc", "proc", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
205 {"/dev", "devtmpfs", unix.MS_NOEXEC | unix.MS_NOSUID},
206 {"/dev/pts", "devpts", unix.MS_NOEXEC | unix.MS_NOSUID},
207 } {
208 if err := os.Mkdir(el.dir, 0755); err != nil && !os.IsExist(err) {
209 return fmt.Errorf("could not make %s: %w", el.dir, err)
210 }
211 if err := unix.Mount(el.fs, el.dir, el.fs, el.flags, ""); err != nil {
212 return fmt.Errorf("could not mount %s on %s: %w", el.fs, el.dir, err)
213 }
214 }
215
216 c := &nftables.Conn{}
217
218 links, err := netlink.LinkList()
219 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100220 logger.Fatalf("Failed to list links: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200221 }
222 var externalLink netlink.Link
223 var vmLinks []netlink.Link
224 for _, link := range links {
225 attrs := link.Attrs()
226 if link.Type() == "device" && len(attrs.HardwareAddr) > 0 {
227 if attrs.Flags&net.FlagUp != net.FlagUp {
228 netlink.LinkSetUp(link) // Attempt to take up all ethernet links
229 }
230 if bytes.Equal(attrs.HardwareAddr, launch.HostInterfaceMAC) {
231 externalLink = link
232 } else {
233 vmLinks = append(vmLinks, link)
234 }
235 }
236 }
237 vmBridgeLink := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: "vmbridge", Flags: net.FlagUp}}
238 if err := netlink.LinkAdd(vmBridgeLink); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100239 logger.Fatalf("Failed to create vmbridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200240 }
241 for _, link := range vmLinks {
242 if err := netlink.LinkSetMaster(link, vmBridgeLink); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100243 logger.Fatalf("Failed to add VM interface to bridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200244 }
Serge Bazanskic7359672020-10-30 16:38:57 +0100245 logger.Infof("Assigned interface %s to bridge", link.Attrs().Name)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200246 }
247 if err := netlink.AddrReplace(vmBridgeLink, &netlink.Addr{IPNet: &net.IPNet{IP: switchIP, Mask: switchSubnetMask}}); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100248 logger.Fatalf("Failed to assign static IP to vmbridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200249 }
250 if externalLink != nil {
251 nat := c.AddTable(&nftables.Table{
252 Family: nftables.TableFamilyIPv4,
253 Name: "nat",
254 })
255
256 postrouting := c.AddChain(&nftables.Chain{
257 Name: "postrouting",
258 Hooknum: nftables.ChainHookPostrouting,
259 Priority: nftables.ChainPriorityNATSource,
260 Table: nat,
261 Type: nftables.ChainTypeNAT,
262 })
263
264 // Masquerade/SNAT all traffic going out of the external interface
265 c.AddRule(&nftables.Rule{
266 Table: nat,
267 Chain: postrouting,
268 Exprs: []expr.Any{
269 &expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
270 &expr.Cmp{
271 Op: expr.CmpOpEq,
272 Register: 1,
273 Data: nfifname(externalLink.Attrs().Name),
274 },
275 &expr.Masq{},
276 },
277 })
278
279 if err := c.Flush(); err != nil {
280 panic(err)
281 }
282
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100283 netIface := &net.Interface{
284 Name: externalLink.Attrs().Name,
285 MTU: externalLink.Attrs().MTU,
286 Index: externalLink.Attrs().Index,
287 Flags: externalLink.Attrs().Flags,
288 HardwareAddr: externalLink.Attrs().HardwareAddr,
289 }
290 dhcpClient, err := dhcp4c.NewClient(netIface)
291 if err != nil {
292 logger.Fatalf("Failed to create DHCP client: %v", err)
293 }
294 dhcpClient.RequestedOptions = []dhcpv4.OptionCode{dhcpv4.OptionRouter}
295 dhcpClient.LeaseCallback = dhcpcb.Compose(dhcpcb.ManageIP(externalLink), dhcpcb.ManageDefaultRoute(externalLink))
296 supervisor.Run(ctx, "dhcp-client", dhcpClient.Run)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200297 if err := ioutil.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1\n"), 0644); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100298 logger.Fatalf("Failed to write ip forwards: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200299 }
Lorenz Brun52f7f292020-06-24 16:42:02 +0200300 } else {
301 logger.Info("No upstream interface detected")
302 }
303 supervisor.Run(ctx, "dhcp-server", runDHCPServer(vmBridgeLink))
304 supervisor.Run(ctx, "proxy-ext1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.ExternalServicePort))
305 supervisor.Run(ctx, "proxy-dbg1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.DebugServicePort))
306 supervisor.Run(ctx, "proxy-k8s-api1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.KubernetesAPIPort))
307 supervisor.Signal(ctx, supervisor.SignalHealthy)
308 supervisor.Signal(ctx, supervisor.SignalDone)
309 return nil
Lorenz Brundf952412020-12-21 14:59:36 +0100310 }, supervisor.WithExistingLogtree(lt))
Lorenz Brun52f7f292020-06-24 16:42:02 +0200311 select {}
312}