Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-daemon/nsinsider/main.go
2496 views
1
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package main
6
7
import (
8
"fmt"
9
"io/ioutil"
10
"net"
11
"net/netip"
12
"os"
13
"time"
14
"unsafe"
15
16
cli "github.com/urfave/cli/v2"
17
"golang.org/x/sys/unix"
18
"golang.org/x/xerrors"
19
20
"github.com/gitpod-io/gitpod/common-go/log"
21
_ "github.com/gitpod-io/gitpod/common-go/nsenter"
22
"github.com/google/nftables"
23
"github.com/google/nftables/binaryutil"
24
"github.com/google/nftables/expr"
25
"github.com/vishvananda/netlink"
26
)
27
28
func main() {
29
app := &cli.App{
30
Commands: []*cli.Command{
31
{
32
Name: "move-mount",
33
Usage: "calls move_mount with the pipe-fd to target",
34
Flags: []cli.Flag{
35
&cli.StringFlag{
36
Name: "target",
37
Required: true,
38
},
39
&cli.IntFlag{
40
Name: "pipe-fd",
41
Required: true,
42
},
43
},
44
Action: func(c *cli.Context) error {
45
return syscallMoveMount(c.Int("pipe-fd"), "", unix.AT_FDCWD, c.String("target"), flagMoveMountFEmptyPath)
46
},
47
},
48
{
49
Name: "open-tree",
50
Usage: "opens a and writes the resulting mountfd to the Unix pipe on the pipe-fd",
51
Flags: []cli.Flag{
52
&cli.StringFlag{
53
Name: "target",
54
Required: true,
55
},
56
&cli.IntFlag{
57
Name: "pipe-fd",
58
Required: true,
59
},
60
},
61
Action: func(c *cli.Context) error {
62
fd, err := syscallOpenTree(unix.AT_FDCWD, c.String("target"), flagOpenTreeClone|flagAtRecursive)
63
if err != nil {
64
return err
65
}
66
67
err = unix.Sendmsg(c.Int("pipe-fd"), nil, unix.UnixRights(int(fd)), nil, 0)
68
if err != nil {
69
return err
70
}
71
72
return nil
73
},
74
},
75
{
76
Name: "make-shared",
77
Usage: "makes a mount point shared",
78
Flags: []cli.Flag{
79
&cli.StringFlag{
80
Name: "target",
81
Required: true,
82
},
83
},
84
Action: func(c *cli.Context) error {
85
return unix.Mount("none", c.String("target"), "", unix.MS_SHARED, "")
86
},
87
},
88
{
89
Name: "mount-shiftfs-mark",
90
Usage: "mounts a shiftfs mark",
91
Flags: []cli.Flag{
92
&cli.StringFlag{
93
Name: "source",
94
Required: true,
95
},
96
&cli.StringFlag{
97
Name: "target",
98
Required: true,
99
},
100
},
101
Action: func(c *cli.Context) error {
102
return unix.Mount(c.String("source"), c.String("target"), "shiftfs", 0, "mark")
103
},
104
},
105
{
106
Name: "mount-proc",
107
Usage: "mounts proc",
108
Flags: []cli.Flag{
109
&cli.StringFlag{
110
Name: "target",
111
Required: true,
112
},
113
},
114
Action: func(c *cli.Context) error {
115
return unix.Mount("proc", c.String("target"), "proc", 0, "")
116
},
117
},
118
{
119
Name: "mount-sysfs",
120
Usage: "mounts sysfs",
121
Flags: []cli.Flag{
122
&cli.StringFlag{
123
Name: "target",
124
Required: true,
125
},
126
},
127
Action: func(c *cli.Context) error {
128
return unix.Mount("sysfs", c.String("target"), "sysfs", 0, "")
129
},
130
},
131
{
132
Name: "unmount",
133
Usage: "unmounts a mountpoint",
134
Flags: []cli.Flag{
135
&cli.StringFlag{
136
Name: "target",
137
Required: true,
138
},
139
},
140
Action: func(c *cli.Context) error {
141
return unix.Unmount(c.String("target"), 0)
142
},
143
},
144
{
145
Name: "prepare-dev",
146
Usage: "prepares a workspaces /dev directory",
147
Flags: []cli.Flag{
148
&cli.IntFlag{
149
Name: "uid",
150
Required: true,
151
},
152
&cli.IntFlag{
153
Name: "gid",
154
Required: true,
155
},
156
},
157
Action: func(c *cli.Context) error {
158
err := ioutil.WriteFile("/dev/kmsg", nil, 0644)
159
if err != nil {
160
return err
161
}
162
163
_ = os.MkdirAll("/dev/net", 0755)
164
err = unix.Mknod("/dev/net/tun", 0666|unix.S_IFCHR, int(unix.Mkdev(10, 200)))
165
if err != nil {
166
return err
167
}
168
err = os.Chmod("/dev/net/tun", os.FileMode(0666))
169
if err != nil {
170
return err
171
}
172
err = os.Chown("/dev/net/tun", c.Int("uid"), c.Int("gid"))
173
if err != nil {
174
return err
175
}
176
177
if _, err := os.Stat("/dev/fuse"); os.IsNotExist(err) {
178
err = unix.Mknod("/dev/fuse", 0666|unix.S_IFCHR, int(unix.Mkdev(10, 229)))
179
if err != nil {
180
return err
181
}
182
}
183
184
err = os.Chmod("/dev/fuse", os.FileMode(0666))
185
if err != nil {
186
return err
187
}
188
err = os.Chown("/dev/fuse", c.Int("uid"), c.Int("gid"))
189
if err != nil {
190
return err
191
}
192
193
return nil
194
},
195
},
196
{
197
Name: "setup-pair-veths",
198
Usage: "set up a pair of veths",
199
Flags: []cli.Flag{
200
&cli.IntFlag{
201
Name: "target-pid",
202
Required: true,
203
},
204
&cli.StringFlag{
205
Name: "workspace-cidr",
206
Required: true,
207
},
208
},
209
Action: func(c *cli.Context) error {
210
containerIf, vethIf, cethIf := "eth0", "veth0", "eth0"
211
networkCIDR := c.String("workspace-cidr")
212
213
vethIp, cethIp, mask, err := processWorkspaceCIDR(networkCIDR)
214
if err != nil {
215
return xerrors.Errorf("parsing workspace CIDR (%v):%v", networkCIDR, err)
216
}
217
218
vethIpNet := net.IPNet{
219
IP: vethIp,
220
Mask: mask.Mask,
221
}
222
223
targetPid := c.Int("target-pid")
224
225
eth0, err := netlink.LinkByName(containerIf)
226
if err != nil {
227
return xerrors.Errorf("cannot get container network device %s: %w", containerIf, err)
228
}
229
230
veth := &netlink.Veth{
231
LinkAttrs: netlink.LinkAttrs{
232
Name: vethIf,
233
Flags: net.FlagUp,
234
MTU: eth0.Attrs().MTU,
235
},
236
PeerName: cethIf,
237
PeerNamespace: netlink.NsPid(targetPid),
238
}
239
if err := netlink.LinkAdd(veth); err != nil {
240
return xerrors.Errorf("link %q-%q netns failed: %v", vethIf, cethIf, err)
241
}
242
243
vethLink, err := netlink.LinkByName(vethIf)
244
if err != nil {
245
return xerrors.Errorf("cannot found %q netns failed: %v", vethIf, err)
246
}
247
if err := netlink.AddrAdd(vethLink, &netlink.Addr{IPNet: &vethIpNet}); err != nil {
248
return xerrors.Errorf("failed to add IP address to %q: %v", vethIf, err)
249
}
250
if err := netlink.LinkSetUp(vethLink); err != nil {
251
return xerrors.Errorf("failed to enable %q: %v", vethIf, err)
252
}
253
254
nc := &nftables.Conn{}
255
nat := nc.AddTable(&nftables.Table{
256
Family: nftables.TableFamilyIPv4,
257
Name: "nat",
258
})
259
260
postrouting := nc.AddChain(&nftables.Chain{
261
Name: "postrouting",
262
Hooknum: nftables.ChainHookPostrouting,
263
Priority: nftables.ChainPriorityNATSource,
264
Table: nat,
265
Type: nftables.ChainTypeNAT,
266
})
267
268
nc.AddRule(&nftables.Rule{
269
Table: nat,
270
Chain: postrouting,
271
Exprs: []expr.Any{
272
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
273
&expr.Cmp{
274
Op: expr.CmpOpEq,
275
Register: 1,
276
Data: []byte(fmt.Sprintf("%s\x00", containerIf)),
277
},
278
&expr.Masq{},
279
},
280
})
281
282
prerouting := nc.AddChain(&nftables.Chain{
283
Name: "prerouting",
284
Hooknum: nftables.ChainHookPrerouting,
285
Priority: nftables.ChainPriorityNATDest,
286
Table: nat,
287
Type: nftables.ChainTypeNAT,
288
})
289
290
// iif $containerIf tcp dport 1-65535 dnat to $cethIp:tcp dport
291
nc.AddRule(&nftables.Rule{
292
Table: nat,
293
Chain: prerouting,
294
Exprs: []expr.Any{
295
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
296
&expr.Cmp{
297
Op: expr.CmpOpEq,
298
Register: 1,
299
Data: []byte(containerIf + "\x00"),
300
},
301
302
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
303
&expr.Cmp{
304
Op: expr.CmpOpEq,
305
Register: 1,
306
Data: []byte{unix.IPPROTO_TCP},
307
},
308
&expr.Payload{
309
DestRegister: 1,
310
Base: expr.PayloadBaseTransportHeader,
311
Offset: 2,
312
Len: 2,
313
},
314
315
&expr.Cmp{
316
Op: expr.CmpOpGte,
317
Register: 1,
318
Data: []byte{0x00, 0x01},
319
},
320
&expr.Cmp{
321
Op: expr.CmpOpLte,
322
Register: 1,
323
Data: []byte{0xff, 0xff},
324
},
325
326
&expr.Immediate{
327
Register: 2,
328
Data: cethIp.To4(),
329
},
330
&expr.NAT{
331
Type: expr.NATTypeDestNAT,
332
Family: unix.NFPROTO_IPV4,
333
RegAddrMin: 2,
334
RegProtoMin: 1,
335
},
336
},
337
})
338
if err := nc.Flush(); err != nil {
339
return xerrors.Errorf("failed to apply nftables: %v", err)
340
}
341
342
return nil
343
},
344
},
345
{
346
Name: "setup-peer-veth",
347
Usage: "set up a peer veth",
348
Flags: []cli.Flag{
349
&cli.StringFlag{
350
Name: "workspace-cidr",
351
Required: true,
352
},
353
},
354
Action: func(c *cli.Context) error {
355
cethIf := "eth0"
356
357
networkCIDR := c.String("workspace-cidr")
358
vethIp, cethIp, mask, err := processWorkspaceCIDR(networkCIDR)
359
if err != nil {
360
return xerrors.Errorf("parsing workspace CIDR (%v):%v", networkCIDR, err)
361
}
362
363
cethIpNet := net.IPNet{
364
IP: cethIp,
365
Mask: mask.Mask,
366
}
367
368
cethLink, err := netlink.LinkByName(cethIf)
369
if err != nil {
370
return xerrors.Errorf("cannot found %q netns failed: %v", cethIf, err)
371
}
372
if err := netlink.AddrAdd(cethLink, &netlink.Addr{IPNet: &cethIpNet}); err != nil {
373
return xerrors.Errorf("failed to add IP address to %q: %v", cethIf, err)
374
}
375
if err := netlink.LinkSetUp(cethLink); err != nil {
376
return xerrors.Errorf("failed to enable %q: %v", cethIf, err)
377
}
378
379
lo, err := netlink.LinkByName("lo")
380
if err != nil {
381
return xerrors.Errorf("cannot found lo: %v", err)
382
}
383
if err := netlink.LinkSetUp(lo); err != nil {
384
return xerrors.Errorf("failed to enable lo: %v", err)
385
}
386
387
defaultGw := netlink.Route{
388
Scope: netlink.SCOPE_UNIVERSE,
389
Gw: vethIp,
390
}
391
if err := netlink.RouteReplace(&defaultGw); err != nil {
392
return xerrors.Errorf("failed to set up default gw (%v): %v", vethIp.String(), err)
393
}
394
395
return nil
396
},
397
},
398
{
399
Name: "enable-ip-forward",
400
Usage: "enable IPv4 forwarding",
401
Action: func(c *cli.Context) error {
402
return os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1"), 0644)
403
},
404
},
405
{
406
Name: "disable-ipv6",
407
Usage: "disable IPv6",
408
Action: func(c *cli.Context) error {
409
return os.WriteFile("/proc/sys/net/ipv6/conf/all/disable_ipv6", []byte("1"), 0644)
410
},
411
},
412
{
413
Name: "dump-network-info",
414
Usage: "dump network info",
415
Flags: []cli.Flag{
416
&cli.StringFlag{
417
Name: "tag",
418
},
419
},
420
Action: func(c *cli.Context) error {
421
links, err := netlink.LinkList()
422
if err != nil {
423
return xerrors.Errorf("cannot list network links: %v", err)
424
}
425
426
tag := c.String("tag")
427
428
for _, link := range links {
429
attrs := link.Attrs()
430
431
ip4, _ := netlink.AddrList(link, netlink.FAMILY_V4)
432
ip6, _ := netlink.AddrList(link, netlink.FAMILY_V6)
433
434
log.Infof("%v", struct {
435
Tag string
436
Name string
437
Type string
438
Ip4 []netlink.Addr
439
Ip6 []netlink.Addr
440
Flags net.Flags
441
MTU int
442
}{
443
Tag: tag,
444
Name: attrs.Name,
445
Type: link.Type(),
446
Ip4: ip4,
447
Ip6: ip6,
448
Flags: attrs.Flags,
449
MTU: attrs.MTU,
450
})
451
}
452
453
return nil
454
},
455
},
456
{
457
Name: "setup-connection-limit",
458
Usage: "set up network connection rate limiting",
459
Flags: []cli.Flag{
460
&cli.IntFlag{
461
Name: "limit",
462
Required: true,
463
},
464
&cli.IntFlag{
465
Name: "bucketsize",
466
Required: false,
467
},
468
&cli.BoolFlag{
469
Name: "enforce",
470
Required: false,
471
},
472
},
473
Action: func(c *cli.Context) error {
474
const drop_stats = "ws-connection-drop-stats"
475
nftcon := nftables.Conn{}
476
477
connLimit := c.Int("limit")
478
bucketSize := c.Int("bucketsize")
479
if bucketSize == 0 {
480
bucketSize = 1000
481
}
482
enforce := c.Bool("enforce")
483
484
// nft add table ip gitpod
485
gitpodTable := nftcon.AddTable(&nftables.Table{
486
Family: nftables.TableFamilyIPv4,
487
Name: "gitpod",
488
})
489
490
// nft add chain ip gitpod ratelimit { type filter hook postrouting priority 0 \; }
491
ratelimit := nftcon.AddChain(&nftables.Chain{
492
Table: gitpodTable,
493
Name: "ratelimit",
494
Type: nftables.ChainTypeFilter,
495
Hooknum: nftables.ChainHookPostrouting,
496
Priority: nftables.ChainPriorityFilter,
497
})
498
499
// nft add counter gitpod connection_drop_stats
500
nftcon.AddObject(&nftables.CounterObj{
501
Table: gitpodTable,
502
Name: drop_stats,
503
})
504
505
// nft add set gitpod ws-connections { type ipv4_addr; flags timeout, dynamic; }
506
set := &nftables.Set{
507
Table: gitpodTable,
508
Name: "ws-connections",
509
KeyType: nftables.TypeIPAddr,
510
Dynamic: true,
511
HasTimeout: true,
512
}
513
if err := nftcon.AddSet(set, nil); err != nil {
514
return err
515
}
516
517
verdict := expr.VerdictAccept
518
if enforce {
519
verdict = expr.VerdictDrop
520
}
521
522
// nft add rule ip gitpod ratelimit ip protocol tcp ct state new meter ws-connections
523
// '{ ip daddr & 0.0.0.0 timeout 1m limit rate over 3000/minute burst 1000 packets }' counter name ws-connection-drop-stats drop
524
nftcon.AddRule(&nftables.Rule{
525
// ip gitpod ratelimit
526
Table: gitpodTable,
527
Chain: ratelimit,
528
529
Exprs: []expr.Any{
530
// ip protocol tcp
531
// get offset into network header and check if tcp
532
&expr.Payload{
533
DestRegister: 1,
534
Base: expr.PayloadBaseNetworkHeader,
535
Offset: uint32(9),
536
Len: uint32(1),
537
},
538
&expr.Cmp{
539
Register: 1,
540
Op: expr.CmpOpEq,
541
Data: []byte{unix.IPPROTO_TCP},
542
},
543
// ct state new
544
// get state from conntrack entry and check for 'new' (0x00000008)
545
&expr.Ct{
546
Key: expr.CtKeySTATE,
547
Register: 1,
548
SourceRegister: false,
549
},
550
&expr.Bitwise{
551
DestRegister: 1,
552
SourceRegister: 1,
553
Len: 4,
554
Mask: binaryutil.NativeEndian.PutUint32(expr.CtStateBitNEW),
555
Xor: binaryutil.NativeEndian.PutUint32(0),
556
},
557
&expr.Cmp{
558
Register: 1,
559
Op: expr.CmpOpNeq,
560
Data: []byte{0, 0, 0, 0},
561
},
562
// ip daddr & 0.0.0.0
563
// get the destination address and AND every address with zero
564
// to ensure that every address is placed into the same bucket
565
&expr.Payload{
566
DestRegister: 1,
567
Base: expr.PayloadBaseNetworkHeader,
568
Offset: uint32(16),
569
Len: uint32(4),
570
},
571
&expr.Bitwise{
572
DestRegister: 1,
573
SourceRegister: 1,
574
Len: 1,
575
Mask: []byte{0x00},
576
Xor: []byte{0x00},
577
},
578
// timeout 1m limit rate over 3000/minute burst 1000 packets
579
&expr.Dynset{
580
SrcRegKey: 1,
581
SetName: set.Name,
582
Operation: uint32(unix.NFT_DYNSET_OP_ADD),
583
Timeout: time.Duration(60 * time.Second),
584
Exprs: []expr.Any{
585
&expr.Limit{
586
Type: expr.LimitTypePkts,
587
Rate: uint64(connLimit),
588
Unit: expr.LimitTimeMinute,
589
Burst: uint32(bucketSize),
590
Over: true,
591
},
592
},
593
},
594
// counter name "ws-connection-drop-stats"
595
&expr.Objref{
596
Type: 1,
597
Name: drop_stats,
598
},
599
// drop
600
&expr.Verdict{
601
Kind: verdict,
602
},
603
},
604
})
605
606
if err := nftcon.Flush(); err != nil {
607
return xerrors.Errorf("failed to apply connection limit: %v", err)
608
}
609
610
return nil
611
},
612
},
613
},
614
}
615
616
log.Init("nsinsider", "", true, false)
617
err := app.Run(os.Args)
618
if err != nil {
619
log.WithField("instanceId", os.Getenv("GITPOD_INSTANCE_ID")).WithField("args", os.Args).Fatal(err)
620
}
621
}
622
623
func syscallMoveMount(fromDirFD int, fromPath string, toDirFD int, toPath string, flags uintptr) error {
624
fromPathP, err := unix.BytePtrFromString(fromPath)
625
if err != nil {
626
return err
627
}
628
toPathP, err := unix.BytePtrFromString(toPath)
629
if err != nil {
630
return err
631
}
632
633
_, _, errno := unix.Syscall6(unix.SYS_MOVE_MOUNT, uintptr(fromDirFD), uintptr(unsafe.Pointer(fromPathP)), uintptr(toDirFD), uintptr(unsafe.Pointer(toPathP)), flags, 0)
634
if errno != 0 {
635
return errno
636
}
637
638
return nil
639
}
640
641
const (
642
// FlagMoveMountFEmptyPath: empty from path permitted: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/mount.h#L70
643
flagMoveMountFEmptyPath = 0x00000004
644
)
645
646
func syscallOpenTree(dfd int, path string, flags uintptr) (fd uintptr, err error) {
647
p1, err := unix.BytePtrFromString(path)
648
if err != nil {
649
return 0, err
650
}
651
fd, _, errno := unix.Syscall(unix.SYS_OPEN_TREE, uintptr(dfd), uintptr(unsafe.Pointer(p1)), flags)
652
if errno != 0 {
653
return 0, errno
654
}
655
656
return fd, nil
657
}
658
659
const (
660
// FlagOpenTreeClone: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/mount.h#L62
661
flagOpenTreeClone = 1
662
// FlagAtRecursive: Apply to the entire subtree: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/fcntl.h#L112
663
flagAtRecursive = 0x8000
664
)
665
666
func processWorkspaceCIDR(networkCIDR string) (net.IP, net.IP, *net.IPNet, error) {
667
netIP, mask, err := net.ParseCIDR(networkCIDR)
668
if err != nil {
669
return nil, nil, nil, xerrors.Errorf("cannot configure workspace CIDR: %w", err)
670
}
671
672
addr, err := netip.ParseAddr(netIP.String())
673
if err != nil {
674
return nil, nil, nil, xerrors.Errorf("cannot configure workspace CIDR: %w", err)
675
}
676
677
vethIp := addr.Next()
678
if !vethIp.IsValid() {
679
return nil, nil, nil, xerrors.Errorf("workspace CIDR is not big enough (%v)", networkCIDR)
680
}
681
682
cethIp := vethIp.Next()
683
if !cethIp.IsValid() {
684
return nil, nil, nil, xerrors.Errorf("workspace CIDR is not big enough (%v)", networkCIDR)
685
}
686
687
return net.ParseIP(vethIp.String()), net.ParseIP(cethIp.String()), mask, nil
688
}
689
690