diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f5f85e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/bin/ +capture.pcap +tmp/ +*.exe diff --git a/Makefile b/Makefile index 3513336..d02dafa 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ LDFLAGS = -ldflags '-s -w' .PHONY: build -build: host-switch +build: host-switch vm-switch bin/host-switch.exe: GOOS=windows go build $(LDFLAGS) -o $@ ./cmd/host @@ -9,6 +9,12 @@ bin/host-switch.exe: .PHONY: host-switch host-switch: bin/host-switch.exe +bin/vm-switch: + GOOS=linux go build $(LDFLAGS) -o $@ ./cmd/vm + +.PHONY: vm-switch +vm-switch: bin/vm-switch + .PHONY: fmt fmt: gofmt -l -s -w . diff --git a/cmd/vm/switch_linux.go b/cmd/vm/switch_linux.go new file mode 100644 index 0000000..65ee49b --- /dev/null +++ b/cmd/vm/switch_linux.go @@ -0,0 +1,275 @@ +/* +Copyright © 2023 SUSE LLC +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "encoding/binary" + "flag" + "fmt" + "io" + "net" + "os" + "os/exec" + "os/signal" + "syscall" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/sirupsen/logrus" + "github.com/songgao/packets/ethernet" + "github.com/songgao/water" + "github.com/vishvananda/netlink" + "gvisor.dev/gvisor/pkg/tcpip/header" +) + +var ( + debug bool + tapIface string +) + +const ( + defaultTapDevice = "eth0" + defaultMacAddr = "5a:94:ef:e4:0c:ee" + maxMTU = 4000 +) + +func main() { + flag.BoolVar(&debug, "debug", true, "enable debug flag") + flag.StringVar(&tapIface, "tap-interface", defaultTapDevice, "tap interface name, eg. eth0, eth1") + flag.Parse() + + if debug { + logrus.SetLevel(logrus.DebugLevel) + } + + // the FD is passed-in as an extra arg from exec.Command + // of the parent process. This is for the AF_VSOCK connection that + // is handed over from the default namespace to Rancher Desktop's + // network namespace, the logic behind this approach is because + // AF_VSOCK is affected by network namespaces, therefore we need + // to open it before entering a new namespace (via unshare/nsenter) + connFile := os.NewFile(uintptr(3), "vsock connection") + connFile.Close() + + logrus.Debugf("using a AF_VSOCK connection file from default namespace: %v", connFile) + + // this should never happen + if err := checkForExistingIface(tapIface); err != nil { + logrus.Fatal(err) + } + + // catch user issued signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + // try every second until we get DHCP + retryTicker := time.NewTicker(time.Second) + for { + ctx, cancel := context.WithCancel(context.Background()) + select { + case s := <-sigChan: + logrus.Errorf("signal caught: %v", s) + cancel() + os.Exit(1) + case <-retryTicker.C: + if err := run(ctx, cancel, connFile); err != nil { + logrus.Error(err) + } + } + } +} + +func run(ctx context.Context, cancel context.CancelFunc, connFile io.ReadWriteCloser) error { + tap, err := water.New(water.Config{ + DeviceType: water.TAP, + PlatformSpecificParams: water.PlatformSpecificParams{ + Name: tapIface, + }, + }) + if err != nil { + logrus.Fatalf("creating tap device %v failed: %s", tapIface, err) + } + logrus.Debugf("created tap device %s: %v", tapIface, tap) + + defer func() { + connFile.Close() + tap.Close() + logrus.Debugf("closed tap device: %s", tapIface) + }() + + if err := linkUp(tapIface, defaultMacAddr); err != nil { + logrus.Fatalf("setting mac address [%s] for %s tap device failed: %s", defaultMacAddr, tapIface, err) + } + if err := loopbackUp(); err != nil { + logrus.Fatalf("enabling loop back device failed: %s", err) + } + + logrus.Debugf("setup complete for tap interface %s(%s) + loopback", tapIface, defaultMacAddr) + + errCh := make(chan error, 1) + go tx(ctx, connFile, tap, errCh, maxMTU) + go rx(ctx, connFile, tap, errCh, maxMTU) + go func() { + if err := dhcp(ctx, tapIface); err != nil { + errCh <- fmt.Errorf("dhcp error: %w", err) + cancel() + } + }() + + return <-errCh +} + +func loopbackUp() error { + lo, err := netlink.LinkByName("lo") + if err != nil { + return err + } + + return netlink.LinkSetUp(lo) +} + +func linkUp(iface, mac string) error { + link, err := netlink.LinkByName(iface) + if err != nil { + return err + } + if mac == "" { + return netlink.LinkSetUp(link) + } + hw, err := net.ParseMAC(mac) + if err != nil { + return err + } + if err := netlink.LinkSetHardwareAddr(link, hw); err != nil { + return err + } + + logrus.Debugf("successful link setup %+v\n", link) + return netlink.LinkSetUp(link) +} + +func dhcp(ctx context.Context, iface string) error { + if _, err := exec.LookPath("udhcpc"); err == nil { // busybox dhcp client + cmd := exec.CommandContext(ctx, "udhcpc", "-f", "-q", "-i", iface, "-v") + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return cmd.Run() + } + cmd := exec.CommandContext(ctx, "dhclient", "-4", "-d", "-v", iface) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return cmd.Run() +} + +func rx(ctx context.Context, conn io.Writer, tap *water.Interface, errCh chan error, mtu int) { + logrus.Info("waiting for packets...") + var frame ethernet.Frame + for { + select { + case <-ctx.Done(): + logrus.Info("exiting rx goroutine") + return + default: + frame.Resize(mtu) + n, err := tap.Read([]byte(frame)) + if err != nil { + errCh <- fmt.Errorf("reading packet from tap failed: %w", err) + return + } + frame = frame[:n] + + size := make([]byte, 2) + binary.LittleEndian.PutUint16(size, uint16(n)) + + if _, err := conn.Write(size); err != nil { + errCh <- fmt.Errorf("writing size to the socket failed: %w", err) + return + } + if _, err := conn.Write(frame); err != nil { + errCh <- fmt.Errorf("writing packet to the socket failed: %w", err) + return + } + + if debug { + packet := gopacket.NewPacket(frame, layers.LayerTypeEthernet, gopacket.Default) + logrus.Infof("wrote packet (vm -> host %d): %s", size, packet.String()) + } + } + } +} + +func tx(ctx context.Context, conn io.Reader, tap *water.Interface, errCh chan error, mtu int) { + sizeBuf := make([]byte, 2) + buf := make([]byte, mtu+header.EthernetMinimumSize) + + for { + select { + case <-ctx.Done(): + logrus.Info("exiting tx goroutine") + return + default: + n, err := io.ReadFull(conn, sizeBuf) + if err != nil { + errCh <- fmt.Errorf("reading size from socket failed: %w", err) + return + } + if n != 2 { + errCh <- fmt.Errorf("unexpected size %d", n) + return + } + size := int(binary.LittleEndian.Uint16(sizeBuf[0:2])) + + if cap(buf) < size { + buf = make([]byte, size) + } + + n, err = io.ReadFull(conn, buf[:size]) + if err != nil { + errCh <- fmt.Errorf("reading payload from socket failed: %w", err) + return + } + if n == 0 || n != size { + errCh <- fmt.Errorf("unexpected size %d != %d", n, size) + return + } + + if _, err := tap.Write(buf[:size]); err != nil { + errCh <- fmt.Errorf("writing packet to tap failed: %w", err) + return + } + + if debug { + packet := gopacket.NewPacket(buf[:size], layers.LayerTypeEthernet, gopacket.Default) + logrus.Infof("read packet (host -> vm %d): %s", size, packet.String()) + } + } + } +} + +func checkForExistingIface(ifName string) error { + // equivalent to: `ip link show` + links, err := netlink.LinkList() + if err != nil { + return fmt.Errorf("getting link devices failed: %w", err) + } + + for _, link := range links { + if link.Attrs().Name == ifName { + return fmt.Errorf("%s interface already exist, exiting now", ifName) + } + } + return nil +} diff --git a/go.mod b/go.mod index 9ed5302..72cdc7d 100644 --- a/go.mod +++ b/go.mod @@ -6,28 +6,31 @@ require ( github.com/Microsoft/go-winio v0.6.0 github.com/containers/gvisor-tap-vsock v0.5.0 github.com/dustin/go-humanize v1.0.0 + github.com/google/gopacket v1.1.19 github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 github.com/rancher-sandbox/rancher-desktop-host-resolver v0.1.5 github.com/sirupsen/logrus v1.9.0 + github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 + github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 + github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852 golang.org/x/sync v0.1.0 + golang.org/x/sys v0.4.0 + gvisor.dev/gvisor v0.0.0-20221216231429-a78e892a26d2 ) -require github.com/pkg/errors v0.9.1 // indirect - require ( github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/google/btree v1.0.1 // indirect - github.com/google/gopacket v1.1.19 // indirect github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f // indirect github.com/miekg/dns v1.1.50 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper v0.0.0-20220712232929-bac01a348036 // indirect github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect + github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect golang.org/x/crypto v0.1.0 // indirect golang.org/x/mod v0.6.0 // indirect golang.org/x/net v0.4.0 // indirect - golang.org/x/sys v0.4.0 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect golang.org/x/tools v0.2.0 // indirect - gvisor.dev/gvisor v0.0.0-20221216231429-a78e892a26d2 // indirect inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 // indirect ) diff --git a/go.sum b/go.sum index ec1cc03..da0f373 100644 --- a/go.sum +++ b/go.sum @@ -54,12 +54,21 @@ github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0 github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 h1:1zN6ImoqhSJhN8hGXFaJlSC8msLmIbX8bFqOfWLKw0w= +github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091/go.mod h1:N20Z5Y8oye9a7HmytmZ+tr8Q2vlP0tAHP13kTHzwvQY= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852 h1:cPXZWzzG0NllBLdjWoD1nDfaqu98YMv+OneaKc8sPOA= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -98,6 +107,8 @@ golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=