blob: edf69bcb6bbc65daa600290c63ef41f2eabbeb50 [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
41 "git.monogon.dev/source/nexantic.git/core/internal/common"
42 "git.monogon.dev/source/nexantic.git/core/internal/common/supervisor"
43 "git.monogon.dev/source/nexantic.git/core/internal/launch"
44 "git.monogon.dev/source/nexantic.git/core/internal/network/dhcp"
45)
46
47var switchIP = net.IP{10, 1, 0, 1}
48var switchSubnetMask = net.CIDRMask(24, 32)
49
50// defaultLeaseOptions sets the lease options needed to properly configure connectivity to nanoswitch
51func defaultLeaseOptions(reply *dhcpv4.DHCPv4) {
52 reply.GatewayIPAddr = switchIP
53 reply.UpdateOption(dhcpv4.OptDNS(net.IPv4(10, 42, 0, 3))) // SLIRP fake DNS server
54 reply.UpdateOption(dhcpv4.OptRouter(switchIP))
Lorenz Brun21b039b2020-11-25 16:00:39 +010055 reply.UpdateOption(dhcpv4.OptIPAddressLeaseTime(12 * time.Hour))
Lorenz Brun52f7f292020-06-24 16:42:02 +020056 reply.UpdateOption(dhcpv4.OptSubnetMask(switchSubnetMask))
57}
58
59// runDHCPServer runs an extremely minimal DHCP server with most options hardcoded, a wrapping bump allocator for the
60// IPs, 12h Lease timeout and no support for DHCP collision detection.
61func runDHCPServer(link netlink.Link) supervisor.Runnable {
62 currentIP := net.IP{10, 1, 0, 1}
63
64 return func(ctx context.Context) error {
65 laddr := net.UDPAddr{
66 IP: net.IPv4(0, 0, 0, 0),
67 Port: 67,
68 }
69 server, err := server4.NewServer(link.Attrs().Name, &laddr, func(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
70 if m == nil {
71 return
72 }
73 reply, err := dhcpv4.NewReplyFromRequest(m)
74 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +010075 supervisor.Logger(ctx).Warningf("Failed to generate DHCP reply: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +020076 return
77 }
78 reply.UpdateOption(dhcpv4.OptServerIdentifier(switchIP))
79 reply.ServerIPAddr = switchIP
80
81 switch m.MessageType() {
82 case dhcpv4.MessageTypeDiscover:
83 reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
84 defaultLeaseOptions(reply)
85 currentIP[3]++ // Works only because it's a /24
86 reply.YourIPAddr = currentIP
Serge Bazanskic7359672020-10-30 16:38:57 +010087 supervisor.Logger(ctx).Infof("Replying with DHCP IP %s", reply.YourIPAddr.String())
Lorenz Brun52f7f292020-06-24 16:42:02 +020088 case dhcpv4.MessageTypeRequest:
89 reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
90 defaultLeaseOptions(reply)
91 reply.YourIPAddr = m.RequestedIPAddress()
92 case dhcpv4.MessageTypeRelease, dhcpv4.MessageTypeDecline:
93 supervisor.Logger(ctx).Info("Ignoring Release/Decline")
94 }
95 if _, err := conn.WriteTo(reply.ToBytes(), peer); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +010096 supervisor.Logger(ctx).Warningf("Cannot reply to client: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +020097 }
98 })
99 if err != nil {
100 return err
101 }
102 supervisor.Signal(ctx, supervisor.SignalHealthy)
103 go func() {
104 <-ctx.Done()
105 server.Close()
106 }()
107 return server.Serve()
108 }
109}
110
111// userspaceProxy listens on port and proxies all TCP connections to the same port on targetIP
112func userspaceProxy(targetIP net.IP, port uint16) supervisor.Runnable {
113 return func(ctx context.Context) error {
114 logger := supervisor.Logger(ctx)
115 tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(0, 0, 0, 0), Port: int(port)})
116 if err != nil {
117 return err
118 }
119 supervisor.Signal(ctx, supervisor.SignalHealthy)
120 go func() {
121 <-ctx.Done()
122 tcpListener.Close()
123 }()
124 for {
125 conn, err := tcpListener.AcceptTCP()
126 if err != nil {
127 if ctx.Err() != nil {
128 return ctx.Err()
129 }
130 return err
131 }
132 go func(conn *net.TCPConn) {
133 defer conn.Close()
134 upstreamConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: targetIP, Port: int(port)})
135 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100136 logger.Infof("Userspace proxy failed to connect to upstream: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200137 return
138 }
139 defer upstreamConn.Close()
140 go io.Copy(upstreamConn, conn)
141 io.Copy(conn, upstreamConn)
142 }(conn)
143 }
144
145 }
146}
147
148// addNetworkRoutes sets up routing from DHCP
149func addNetworkRoutes(link netlink.Link, addr net.IPNet, gw net.IP) error {
150 if err := netlink.AddrReplace(link, &netlink.Addr{IPNet: &addr}); err != nil {
151 return fmt.Errorf("failed to add DHCP address to network interface \"%v\": %w", link.Attrs().Name, err)
152 }
153
154 if gw.IsUnspecified() {
155 return nil
156 }
157
158 route := &netlink.Route{
159 Dst: &net.IPNet{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(0, 0, 0, 0)},
160 Gw: gw,
161 Scope: netlink.SCOPE_UNIVERSE,
162 }
163 if err := netlink.RouteAdd(route); err != nil {
164 return fmt.Errorf("could not add default route: netlink.RouteAdd(%+v): %v", route, err)
165 }
166 return nil
167}
168
169// nfifname converts an interface name into 16 bytes padded with zeroes (for nftables)
170func nfifname(n string) []byte {
171 b := make([]byte, 16)
172 copy(b, []byte(n+"\x00"))
173 return b
174}
175
176func main() {
Serge Bazanskic7359672020-10-30 16:38:57 +0100177 supervisor.New(context.Background(), func(ctx context.Context) error {
Lorenz Brun52f7f292020-06-24 16:42:02 +0200178 logger := supervisor.Logger(ctx)
179 logger.Info("Starting NanoSwitch, a tiny TOR switch emulator")
180
181 // Set up target filesystems.
182 for _, el := range []struct {
183 dir string
184 fs string
185 flags uintptr
186 }{
187 {"/sys", "sysfs", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
188 {"/proc", "proc", unix.MS_NOEXEC | unix.MS_NOSUID | unix.MS_NODEV},
189 {"/dev", "devtmpfs", unix.MS_NOEXEC | unix.MS_NOSUID},
190 {"/dev/pts", "devpts", unix.MS_NOEXEC | unix.MS_NOSUID},
191 } {
192 if err := os.Mkdir(el.dir, 0755); err != nil && !os.IsExist(err) {
193 return fmt.Errorf("could not make %s: %w", el.dir, err)
194 }
195 if err := unix.Mount(el.fs, el.dir, el.fs, el.flags, ""); err != nil {
196 return fmt.Errorf("could not mount %s on %s: %w", el.fs, el.dir, err)
197 }
198 }
199
200 c := &nftables.Conn{}
201
202 links, err := netlink.LinkList()
203 if err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100204 logger.Fatalf("Failed to list links: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200205 }
206 var externalLink netlink.Link
207 var vmLinks []netlink.Link
208 for _, link := range links {
209 attrs := link.Attrs()
210 if link.Type() == "device" && len(attrs.HardwareAddr) > 0 {
211 if attrs.Flags&net.FlagUp != net.FlagUp {
212 netlink.LinkSetUp(link) // Attempt to take up all ethernet links
213 }
214 if bytes.Equal(attrs.HardwareAddr, launch.HostInterfaceMAC) {
215 externalLink = link
216 } else {
217 vmLinks = append(vmLinks, link)
218 }
219 }
220 }
221 vmBridgeLink := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: "vmbridge", Flags: net.FlagUp}}
222 if err := netlink.LinkAdd(vmBridgeLink); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100223 logger.Fatalf("Failed to create vmbridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200224 }
225 for _, link := range vmLinks {
226 if err := netlink.LinkSetMaster(link, vmBridgeLink); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100227 logger.Fatalf("Failed to add VM interface to bridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200228 }
Serge Bazanskic7359672020-10-30 16:38:57 +0100229 logger.Infof("Assigned interface %s to bridge", link.Attrs().Name)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200230 }
231 if err := netlink.AddrReplace(vmBridgeLink, &netlink.Addr{IPNet: &net.IPNet{IP: switchIP, Mask: switchSubnetMask}}); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100232 logger.Fatalf("Failed to assign static IP to vmbridge: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200233 }
234 if externalLink != nil {
235 nat := c.AddTable(&nftables.Table{
236 Family: nftables.TableFamilyIPv4,
237 Name: "nat",
238 })
239
240 postrouting := c.AddChain(&nftables.Chain{
241 Name: "postrouting",
242 Hooknum: nftables.ChainHookPostrouting,
243 Priority: nftables.ChainPriorityNATSource,
244 Table: nat,
245 Type: nftables.ChainTypeNAT,
246 })
247
248 // Masquerade/SNAT all traffic going out of the external interface
249 c.AddRule(&nftables.Rule{
250 Table: nat,
251 Chain: postrouting,
252 Exprs: []expr.Any{
253 &expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
254 &expr.Cmp{
255 Op: expr.CmpOpEq,
256 Register: 1,
257 Data: nfifname(externalLink.Attrs().Name),
258 },
259 &expr.Masq{},
260 },
261 })
262
263 if err := c.Flush(); err != nil {
264 panic(err)
265 }
266
267 dhcpClient := dhcp.New()
268 supervisor.Run(ctx, "dhcp-client", dhcpClient.Run(externalLink))
269 if err := ioutil.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1\n"), 0644); err != nil {
Serge Bazanskic7359672020-10-30 16:38:57 +0100270 logger.Fatalf("Failed to write ip forwards: %v", err)
Lorenz Brun52f7f292020-06-24 16:42:02 +0200271 }
272 status, err := dhcpClient.Status(ctx, true)
273 if err != nil {
274 return err
275 }
276
277 if err := addNetworkRoutes(externalLink, status.Address, status.Gateway); err != nil {
278 return err
279 }
280 } else {
281 logger.Info("No upstream interface detected")
282 }
283 supervisor.Run(ctx, "dhcp-server", runDHCPServer(vmBridgeLink))
284 supervisor.Run(ctx, "proxy-ext1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.ExternalServicePort))
285 supervisor.Run(ctx, "proxy-dbg1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.DebugServicePort))
286 supervisor.Run(ctx, "proxy-k8s-api1", userspaceProxy(net.IPv4(10, 1, 0, 2), common.KubernetesAPIPort))
287 supervisor.Signal(ctx, supervisor.SignalHealthy)
288 supervisor.Signal(ctx, supervisor.SignalDone)
289 return nil
290 })
291 select {}
292}