Path: blob/main/components/ws-daemon/nsinsider/main.go
2496 views
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package main56import (7"fmt"8"io/ioutil"9"net"10"net/netip"11"os"12"time"13"unsafe"1415cli "github.com/urfave/cli/v2"16"golang.org/x/sys/unix"17"golang.org/x/xerrors"1819"github.com/gitpod-io/gitpod/common-go/log"20_ "github.com/gitpod-io/gitpod/common-go/nsenter"21"github.com/google/nftables"22"github.com/google/nftables/binaryutil"23"github.com/google/nftables/expr"24"github.com/vishvananda/netlink"25)2627func main() {28app := &cli.App{29Commands: []*cli.Command{30{31Name: "move-mount",32Usage: "calls move_mount with the pipe-fd to target",33Flags: []cli.Flag{34&cli.StringFlag{35Name: "target",36Required: true,37},38&cli.IntFlag{39Name: "pipe-fd",40Required: true,41},42},43Action: func(c *cli.Context) error {44return syscallMoveMount(c.Int("pipe-fd"), "", unix.AT_FDCWD, c.String("target"), flagMoveMountFEmptyPath)45},46},47{48Name: "open-tree",49Usage: "opens a and writes the resulting mountfd to the Unix pipe on the pipe-fd",50Flags: []cli.Flag{51&cli.StringFlag{52Name: "target",53Required: true,54},55&cli.IntFlag{56Name: "pipe-fd",57Required: true,58},59},60Action: func(c *cli.Context) error {61fd, err := syscallOpenTree(unix.AT_FDCWD, c.String("target"), flagOpenTreeClone|flagAtRecursive)62if err != nil {63return err64}6566err = unix.Sendmsg(c.Int("pipe-fd"), nil, unix.UnixRights(int(fd)), nil, 0)67if err != nil {68return err69}7071return nil72},73},74{75Name: "make-shared",76Usage: "makes a mount point shared",77Flags: []cli.Flag{78&cli.StringFlag{79Name: "target",80Required: true,81},82},83Action: func(c *cli.Context) error {84return unix.Mount("none", c.String("target"), "", unix.MS_SHARED, "")85},86},87{88Name: "mount-shiftfs-mark",89Usage: "mounts a shiftfs mark",90Flags: []cli.Flag{91&cli.StringFlag{92Name: "source",93Required: true,94},95&cli.StringFlag{96Name: "target",97Required: true,98},99},100Action: func(c *cli.Context) error {101return unix.Mount(c.String("source"), c.String("target"), "shiftfs", 0, "mark")102},103},104{105Name: "mount-proc",106Usage: "mounts proc",107Flags: []cli.Flag{108&cli.StringFlag{109Name: "target",110Required: true,111},112},113Action: func(c *cli.Context) error {114return unix.Mount("proc", c.String("target"), "proc", 0, "")115},116},117{118Name: "mount-sysfs",119Usage: "mounts sysfs",120Flags: []cli.Flag{121&cli.StringFlag{122Name: "target",123Required: true,124},125},126Action: func(c *cli.Context) error {127return unix.Mount("sysfs", c.String("target"), "sysfs", 0, "")128},129},130{131Name: "unmount",132Usage: "unmounts a mountpoint",133Flags: []cli.Flag{134&cli.StringFlag{135Name: "target",136Required: true,137},138},139Action: func(c *cli.Context) error {140return unix.Unmount(c.String("target"), 0)141},142},143{144Name: "prepare-dev",145Usage: "prepares a workspaces /dev directory",146Flags: []cli.Flag{147&cli.IntFlag{148Name: "uid",149Required: true,150},151&cli.IntFlag{152Name: "gid",153Required: true,154},155},156Action: func(c *cli.Context) error {157err := ioutil.WriteFile("/dev/kmsg", nil, 0644)158if err != nil {159return err160}161162_ = os.MkdirAll("/dev/net", 0755)163err = unix.Mknod("/dev/net/tun", 0666|unix.S_IFCHR, int(unix.Mkdev(10, 200)))164if err != nil {165return err166}167err = os.Chmod("/dev/net/tun", os.FileMode(0666))168if err != nil {169return err170}171err = os.Chown("/dev/net/tun", c.Int("uid"), c.Int("gid"))172if err != nil {173return err174}175176if _, err := os.Stat("/dev/fuse"); os.IsNotExist(err) {177err = unix.Mknod("/dev/fuse", 0666|unix.S_IFCHR, int(unix.Mkdev(10, 229)))178if err != nil {179return err180}181}182183err = os.Chmod("/dev/fuse", os.FileMode(0666))184if err != nil {185return err186}187err = os.Chown("/dev/fuse", c.Int("uid"), c.Int("gid"))188if err != nil {189return err190}191192return nil193},194},195{196Name: "setup-pair-veths",197Usage: "set up a pair of veths",198Flags: []cli.Flag{199&cli.IntFlag{200Name: "target-pid",201Required: true,202},203&cli.StringFlag{204Name: "workspace-cidr",205Required: true,206},207},208Action: func(c *cli.Context) error {209containerIf, vethIf, cethIf := "eth0", "veth0", "eth0"210networkCIDR := c.String("workspace-cidr")211212vethIp, cethIp, mask, err := processWorkspaceCIDR(networkCIDR)213if err != nil {214return xerrors.Errorf("parsing workspace CIDR (%v):%v", networkCIDR, err)215}216217vethIpNet := net.IPNet{218IP: vethIp,219Mask: mask.Mask,220}221222targetPid := c.Int("target-pid")223224eth0, err := netlink.LinkByName(containerIf)225if err != nil {226return xerrors.Errorf("cannot get container network device %s: %w", containerIf, err)227}228229veth := &netlink.Veth{230LinkAttrs: netlink.LinkAttrs{231Name: vethIf,232Flags: net.FlagUp,233MTU: eth0.Attrs().MTU,234},235PeerName: cethIf,236PeerNamespace: netlink.NsPid(targetPid),237}238if err := netlink.LinkAdd(veth); err != nil {239return xerrors.Errorf("link %q-%q netns failed: %v", vethIf, cethIf, err)240}241242vethLink, err := netlink.LinkByName(vethIf)243if err != nil {244return xerrors.Errorf("cannot found %q netns failed: %v", vethIf, err)245}246if err := netlink.AddrAdd(vethLink, &netlink.Addr{IPNet: &vethIpNet}); err != nil {247return xerrors.Errorf("failed to add IP address to %q: %v", vethIf, err)248}249if err := netlink.LinkSetUp(vethLink); err != nil {250return xerrors.Errorf("failed to enable %q: %v", vethIf, err)251}252253nc := &nftables.Conn{}254nat := nc.AddTable(&nftables.Table{255Family: nftables.TableFamilyIPv4,256Name: "nat",257})258259postrouting := nc.AddChain(&nftables.Chain{260Name: "postrouting",261Hooknum: nftables.ChainHookPostrouting,262Priority: nftables.ChainPriorityNATSource,263Table: nat,264Type: nftables.ChainTypeNAT,265})266267nc.AddRule(&nftables.Rule{268Table: nat,269Chain: postrouting,270Exprs: []expr.Any{271&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},272&expr.Cmp{273Op: expr.CmpOpEq,274Register: 1,275Data: []byte(fmt.Sprintf("%s\x00", containerIf)),276},277&expr.Masq{},278},279})280281prerouting := nc.AddChain(&nftables.Chain{282Name: "prerouting",283Hooknum: nftables.ChainHookPrerouting,284Priority: nftables.ChainPriorityNATDest,285Table: nat,286Type: nftables.ChainTypeNAT,287})288289// iif $containerIf tcp dport 1-65535 dnat to $cethIp:tcp dport290nc.AddRule(&nftables.Rule{291Table: nat,292Chain: prerouting,293Exprs: []expr.Any{294&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},295&expr.Cmp{296Op: expr.CmpOpEq,297Register: 1,298Data: []byte(containerIf + "\x00"),299},300301&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},302&expr.Cmp{303Op: expr.CmpOpEq,304Register: 1,305Data: []byte{unix.IPPROTO_TCP},306},307&expr.Payload{308DestRegister: 1,309Base: expr.PayloadBaseTransportHeader,310Offset: 2,311Len: 2,312},313314&expr.Cmp{315Op: expr.CmpOpGte,316Register: 1,317Data: []byte{0x00, 0x01},318},319&expr.Cmp{320Op: expr.CmpOpLte,321Register: 1,322Data: []byte{0xff, 0xff},323},324325&expr.Immediate{326Register: 2,327Data: cethIp.To4(),328},329&expr.NAT{330Type: expr.NATTypeDestNAT,331Family: unix.NFPROTO_IPV4,332RegAddrMin: 2,333RegProtoMin: 1,334},335},336})337if err := nc.Flush(); err != nil {338return xerrors.Errorf("failed to apply nftables: %v", err)339}340341return nil342},343},344{345Name: "setup-peer-veth",346Usage: "set up a peer veth",347Flags: []cli.Flag{348&cli.StringFlag{349Name: "workspace-cidr",350Required: true,351},352},353Action: func(c *cli.Context) error {354cethIf := "eth0"355356networkCIDR := c.String("workspace-cidr")357vethIp, cethIp, mask, err := processWorkspaceCIDR(networkCIDR)358if err != nil {359return xerrors.Errorf("parsing workspace CIDR (%v):%v", networkCIDR, err)360}361362cethIpNet := net.IPNet{363IP: cethIp,364Mask: mask.Mask,365}366367cethLink, err := netlink.LinkByName(cethIf)368if err != nil {369return xerrors.Errorf("cannot found %q netns failed: %v", cethIf, err)370}371if err := netlink.AddrAdd(cethLink, &netlink.Addr{IPNet: &cethIpNet}); err != nil {372return xerrors.Errorf("failed to add IP address to %q: %v", cethIf, err)373}374if err := netlink.LinkSetUp(cethLink); err != nil {375return xerrors.Errorf("failed to enable %q: %v", cethIf, err)376}377378lo, err := netlink.LinkByName("lo")379if err != nil {380return xerrors.Errorf("cannot found lo: %v", err)381}382if err := netlink.LinkSetUp(lo); err != nil {383return xerrors.Errorf("failed to enable lo: %v", err)384}385386defaultGw := netlink.Route{387Scope: netlink.SCOPE_UNIVERSE,388Gw: vethIp,389}390if err := netlink.RouteReplace(&defaultGw); err != nil {391return xerrors.Errorf("failed to set up default gw (%v): %v", vethIp.String(), err)392}393394return nil395},396},397{398Name: "enable-ip-forward",399Usage: "enable IPv4 forwarding",400Action: func(c *cli.Context) error {401return os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1"), 0644)402},403},404{405Name: "disable-ipv6",406Usage: "disable IPv6",407Action: func(c *cli.Context) error {408return os.WriteFile("/proc/sys/net/ipv6/conf/all/disable_ipv6", []byte("1"), 0644)409},410},411{412Name: "dump-network-info",413Usage: "dump network info",414Flags: []cli.Flag{415&cli.StringFlag{416Name: "tag",417},418},419Action: func(c *cli.Context) error {420links, err := netlink.LinkList()421if err != nil {422return xerrors.Errorf("cannot list network links: %v", err)423}424425tag := c.String("tag")426427for _, link := range links {428attrs := link.Attrs()429430ip4, _ := netlink.AddrList(link, netlink.FAMILY_V4)431ip6, _ := netlink.AddrList(link, netlink.FAMILY_V6)432433log.Infof("%v", struct {434Tag string435Name string436Type string437Ip4 []netlink.Addr438Ip6 []netlink.Addr439Flags net.Flags440MTU int441}{442Tag: tag,443Name: attrs.Name,444Type: link.Type(),445Ip4: ip4,446Ip6: ip6,447Flags: attrs.Flags,448MTU: attrs.MTU,449})450}451452return nil453},454},455{456Name: "setup-connection-limit",457Usage: "set up network connection rate limiting",458Flags: []cli.Flag{459&cli.IntFlag{460Name: "limit",461Required: true,462},463&cli.IntFlag{464Name: "bucketsize",465Required: false,466},467&cli.BoolFlag{468Name: "enforce",469Required: false,470},471},472Action: func(c *cli.Context) error {473const drop_stats = "ws-connection-drop-stats"474nftcon := nftables.Conn{}475476connLimit := c.Int("limit")477bucketSize := c.Int("bucketsize")478if bucketSize == 0 {479bucketSize = 1000480}481enforce := c.Bool("enforce")482483// nft add table ip gitpod484gitpodTable := nftcon.AddTable(&nftables.Table{485Family: nftables.TableFamilyIPv4,486Name: "gitpod",487})488489// nft add chain ip gitpod ratelimit { type filter hook postrouting priority 0 \; }490ratelimit := nftcon.AddChain(&nftables.Chain{491Table: gitpodTable,492Name: "ratelimit",493Type: nftables.ChainTypeFilter,494Hooknum: nftables.ChainHookPostrouting,495Priority: nftables.ChainPriorityFilter,496})497498// nft add counter gitpod connection_drop_stats499nftcon.AddObject(&nftables.CounterObj{500Table: gitpodTable,501Name: drop_stats,502})503504// nft add set gitpod ws-connections { type ipv4_addr; flags timeout, dynamic; }505set := &nftables.Set{506Table: gitpodTable,507Name: "ws-connections",508KeyType: nftables.TypeIPAddr,509Dynamic: true,510HasTimeout: true,511}512if err := nftcon.AddSet(set, nil); err != nil {513return err514}515516verdict := expr.VerdictAccept517if enforce {518verdict = expr.VerdictDrop519}520521// nft add rule ip gitpod ratelimit ip protocol tcp ct state new meter ws-connections522// '{ ip daddr & 0.0.0.0 timeout 1m limit rate over 3000/minute burst 1000 packets }' counter name ws-connection-drop-stats drop523nftcon.AddRule(&nftables.Rule{524// ip gitpod ratelimit525Table: gitpodTable,526Chain: ratelimit,527528Exprs: []expr.Any{529// ip protocol tcp530// get offset into network header and check if tcp531&expr.Payload{532DestRegister: 1,533Base: expr.PayloadBaseNetworkHeader,534Offset: uint32(9),535Len: uint32(1),536},537&expr.Cmp{538Register: 1,539Op: expr.CmpOpEq,540Data: []byte{unix.IPPROTO_TCP},541},542// ct state new543// get state from conntrack entry and check for 'new' (0x00000008)544&expr.Ct{545Key: expr.CtKeySTATE,546Register: 1,547SourceRegister: false,548},549&expr.Bitwise{550DestRegister: 1,551SourceRegister: 1,552Len: 4,553Mask: binaryutil.NativeEndian.PutUint32(expr.CtStateBitNEW),554Xor: binaryutil.NativeEndian.PutUint32(0),555},556&expr.Cmp{557Register: 1,558Op: expr.CmpOpNeq,559Data: []byte{0, 0, 0, 0},560},561// ip daddr & 0.0.0.0562// get the destination address and AND every address with zero563// to ensure that every address is placed into the same bucket564&expr.Payload{565DestRegister: 1,566Base: expr.PayloadBaseNetworkHeader,567Offset: uint32(16),568Len: uint32(4),569},570&expr.Bitwise{571DestRegister: 1,572SourceRegister: 1,573Len: 1,574Mask: []byte{0x00},575Xor: []byte{0x00},576},577// timeout 1m limit rate over 3000/minute burst 1000 packets578&expr.Dynset{579SrcRegKey: 1,580SetName: set.Name,581Operation: uint32(unix.NFT_DYNSET_OP_ADD),582Timeout: time.Duration(60 * time.Second),583Exprs: []expr.Any{584&expr.Limit{585Type: expr.LimitTypePkts,586Rate: uint64(connLimit),587Unit: expr.LimitTimeMinute,588Burst: uint32(bucketSize),589Over: true,590},591},592},593// counter name "ws-connection-drop-stats"594&expr.Objref{595Type: 1,596Name: drop_stats,597},598// drop599&expr.Verdict{600Kind: verdict,601},602},603})604605if err := nftcon.Flush(); err != nil {606return xerrors.Errorf("failed to apply connection limit: %v", err)607}608609return nil610},611},612},613}614615log.Init("nsinsider", "", true, false)616err := app.Run(os.Args)617if err != nil {618log.WithField("instanceId", os.Getenv("GITPOD_INSTANCE_ID")).WithField("args", os.Args).Fatal(err)619}620}621622func syscallMoveMount(fromDirFD int, fromPath string, toDirFD int, toPath string, flags uintptr) error {623fromPathP, err := unix.BytePtrFromString(fromPath)624if err != nil {625return err626}627toPathP, err := unix.BytePtrFromString(toPath)628if err != nil {629return err630}631632_, _, errno := unix.Syscall6(unix.SYS_MOVE_MOUNT, uintptr(fromDirFD), uintptr(unsafe.Pointer(fromPathP)), uintptr(toDirFD), uintptr(unsafe.Pointer(toPathP)), flags, 0)633if errno != 0 {634return errno635}636637return nil638}639640const (641// FlagMoveMountFEmptyPath: empty from path permitted: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/mount.h#L70642flagMoveMountFEmptyPath = 0x00000004643)644645func syscallOpenTree(dfd int, path string, flags uintptr) (fd uintptr, err error) {646p1, err := unix.BytePtrFromString(path)647if err != nil {648return 0, err649}650fd, _, errno := unix.Syscall(unix.SYS_OPEN_TREE, uintptr(dfd), uintptr(unsafe.Pointer(p1)), flags)651if errno != 0 {652return 0, errno653}654655return fd, nil656}657658const (659// FlagOpenTreeClone: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/mount.h#L62660flagOpenTreeClone = 1661// FlagAtRecursive: Apply to the entire subtree: https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/fcntl.h#L112662flagAtRecursive = 0x8000663)664665func processWorkspaceCIDR(networkCIDR string) (net.IP, net.IP, *net.IPNet, error) {666netIP, mask, err := net.ParseCIDR(networkCIDR)667if err != nil {668return nil, nil, nil, xerrors.Errorf("cannot configure workspace CIDR: %w", err)669}670671addr, err := netip.ParseAddr(netIP.String())672if err != nil {673return nil, nil, nil, xerrors.Errorf("cannot configure workspace CIDR: %w", err)674}675676vethIp := addr.Next()677if !vethIp.IsValid() {678return nil, nil, nil, xerrors.Errorf("workspace CIDR is not big enough (%v)", networkCIDR)679}680681cethIp := vethIp.Next()682if !cethIp.IsValid() {683return nil, nil, nil, xerrors.Errorf("workspace CIDR is not big enough (%v)", networkCIDR)684}685686return net.ParseIP(vethIp.String()), net.ParseIP(cethIp.String()), mask, nil687}688689690