Skip to content
This repository has been archived by the owner on Sep 12, 2024. It is now read-only.

Add vm-switch core logic #4

Merged
merged 2 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/bin/
capture.pcap
tmp/
*.exe
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
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

.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 .
Expand Down
275 changes: 275 additions & 0 deletions cmd/vm/switch_linux.go
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have options for (non-default) mtu, mac addr?

Copy link
Member Author

@Nino-K Nino-K Feb 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same mac address is also being used in the host switch so making this configurable is kind of pointless, also, the arp message is isolated to a network namespace so mac address conflict is not an option. As for the mtu, the default standard value for most networks is 1500 bytes (although we are setting it to 4000), we can always add an extra arg to configure this if there is a demand for it or if we ever decide to support jumbo frames but for now, it should be fine.


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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, if you want to let deferred things run (given that this is main()), one way would be:

func main() {
  exitCode := 0
  defer os.Exit(exitCode)
…
  // in this code here
  exitCode = 1
  return

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connFile is probably better named conn (because the fact that it's a file is opaque).

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but I wonder if we can use a pure-go 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
}
13 changes: 8 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down