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