From f63f74284d4188ee2ce81a63d8349603014238e3 Mon Sep 17 00:00:00 2001 From: RouxAntoine Date: Mon, 1 Jan 2024 23:49:26 +0100 Subject: [PATCH] feature: create veth link between host and network namespace --- cmd/main.go | 55 ++++++++++++------- go.mod | 3 ++ go.sum | 6 +++ internal/netlink/netlink.go | 103 ++++++++++++++++++++++++++++++++++++ internal/netns/netns.go | 23 ++++---- internal/netns/nshandle.go | 42 +++++++-------- 6 files changed, 183 insertions(+), 49 deletions(-) create mode 100644 internal/netlink/netlink.go diff --git a/cmd/main.go b/cmd/main.go index ea1cf12..02cd9c7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,6 +3,7 @@ package main import ( + "antoine-roux.tk/projects/go/firecracker-netns/internal/netlink" "antoine-roux.tk/projects/go/firecracker-netns/internal/netns" "fmt" "net" @@ -11,26 +12,43 @@ import ( "runtime" ) -func main() { - // Lock the OS Thread, so we don't accidentally switch namespaces - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - handle, err := netns.New() +func setupEnv() int { + newNs, err := netns.New() if err != nil { fmt.Println("new ns error", err) + return 1 } - defer func(handle *netns.NsHandle) { + defer func(handle netns.NsHandle) { err := handle.Close() if err != nil { fmt.Println("close ns error", err) } - }(&handle) + }(newNs) - err = netns.Set(handle) + defer func(ns netns.NsHandle) { + err := netns.Delete(ns) + if err != nil { + fmt.Println("delete ns error", err) + } + }(newNs) + + vethPair, err := netlink.NewVethPair(newNs) if err != nil { - fmt.Println("set ns error", err) + fmt.Println("new Veth error", err) + return 1 + } + + defer func(veth *netlink.PairLink) { + err = veth.DeleteLink() + if err != nil { + fmt.Println("delete vethPair error", err) + } + }(vethPair) + + err = netns.Set(newNs) + if err != nil { + return 1 } // Do something with the network namespace @@ -45,17 +63,18 @@ func main() { cmd.Env = []string{"PS1=-[ns-process]- # "} - /* cmd.SysProcAttr = &syscall.SysProcAttr{ - Cloneflags: syscall.CLONE_NEWUTS, - }*/ - if err := cmd.Run(); err != nil { fmt.Printf("Error running the /bin/sh command - %s\n", err) os.Exit(1) } - err = netns.Delete(handle) - if err != nil { - fmt.Println("delete ns error", err) - } + return 0 +} + +func main() { + // Lock the OS Thread, so we don't accidentally switch namespaces + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + os.Exit(setupEnv()) } diff --git a/go.mod b/go.mod index 3e4e3e1..99556f0 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,8 @@ go 1.21.5 require ( github.com/docker/docker v24.0.7+incompatible github.com/hashicorp/go-version v1.6.0 + github.com/vishvananda/netlink v1.2.1-beta.2 golang.org/x/sys v0.15.0 ) + +require github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect diff --git a/go.sum b/go.sum index c9d1414..561652b 100644 --- a/go.sum +++ b/go.sum @@ -2,5 +2,11 @@ github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= +github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/netlink/netlink.go b/internal/netlink/netlink.go new file mode 100644 index 0000000..a115f86 --- /dev/null +++ b/internal/netlink/netlink.go @@ -0,0 +1,103 @@ +package netlink + +import ( + "antoine-roux.tk/projects/go/firecracker-netns/internal/netns" + "crypto/rand" + "fmt" + "github.com/vishvananda/netlink" +) + +// inspired by https://github.com/containernetworking/plugins/blob/main/pkg/ip/link_linux.go#L57 + +type Peer struct { + link netlink.Link + netNs netns.NsHandle +} + +type PairLink struct { + host Peer + guest Peer +} + +func NewVethPair(ns netns.NsHandle) (pair *PairLink, error error) { + + origins, err := netns.Get() + if err != nil { + return nil, err + } + + err = netns.Set(ns) + if err != nil { + return nil, err + } + + name, err := randomVethName() + if err != nil { + return nil, err + } + + peerName, err := randomVethName() + if err != nil { + return nil, err + } + + attrs := netlink.NewLinkAttrs() + attrs.Name = name + veth := &netlink.Veth{ + LinkAttrs: attrs, + PeerName: peerName, + PeerNamespace: netlink.NsFd(origins.Fd), + } + + if err := netlink.LinkAdd(veth); err != nil { + return nil, err + } + + // Re-fetch the container link to get its creation-time parameters, e.g. index and mac + fmt.Println(name) + veth2, err := netlink.LinkByName(name) + if err != nil { + netlink.LinkDel(veth) + return nil, err + } + + err = netns.Set(origins) + if err != nil { + return nil, err + } + + fmt.Println(peerName) + veth1, err := netlink.LinkByName(peerName) + if err != nil { + netlink.LinkDel(veth) + return nil, err + } + + return &PairLink{host: Peer{veth1, origins}, guest: Peer{veth2, ns}}, nil +} + +// DeleteLink removes an interface link. +func (p *PairLink) DeleteLink() error { + err := netns.Set(p.host.netNs) + if err != nil { + return fmt.Errorf("set original Ns for deleting veth %q: %v", p.host.link.Attrs().Name, err) + } + + if err := netlink.LinkDel(p.host.link); err != nil { + return fmt.Errorf("failed to delete %q: %v", p.host.link.Attrs().Name, err) + } + + return nil +} + +// randomVethName returns string "veth" with random prefix (hashed from entropy) +func randomVethName() (string, error) { + entropy := make([]byte, 4) + _, err := rand.Read(entropy) + if err != nil { + return "", fmt.Errorf("failed to generate random veth name: %v", err) + } + + // NetworkManager (recent versions) will ignore veth devices that start with "veth" + return fmt.Sprintf("veth%x", entropy), nil +} diff --git a/internal/netns/netns.go b/internal/netns/netns.go index 9dff324..0666b53 100644 --- a/internal/netns/netns.go +++ b/internal/netns/netns.go @@ -3,16 +3,19 @@ package netns // inspired from https://github.com/vishvananda/netns/blob/master/netns_linux.go#L95 import ( - "fmt" - "github.com/docker/docker/pkg/namesgenerator" - "golang.org/x/sys/unix" - "os" - "path" + "fmt" + "github.com/docker/docker/pkg/namesgenerator" + "golang.org/x/sys/unix" + "os" + "path" ) -// New create and persist a new namespace with random name +// New create and persist a new namespace with random Name func New() (handle NsHandle, error error) { - origins, _ := Get() + origins, err := Get() + if err != nil { + return None(), err + } defer deferClose(&origins, &error) if err := unix.Unshare(unix.CLONE_NEWNET); err != nil { @@ -26,7 +29,7 @@ func New() (handle NsHandle, error error) { err = Set(origins) if err != nil { - return NsHandle{fd: 0}, err + return NsHandle{Fd: 0}, err } err = newNs.persist() @@ -47,7 +50,7 @@ func deferClose(origins *NsHandle, e *error) { // Delete deletes a named network namespace func Delete(ns NsHandle) error { - namedPath := path.Join(bindMountPath, ns.name) + namedPath := path.Join(bindMountPath, ns.Name) err := unix.Unmount(namedPath, unix.MNT_DETACH) if err != nil { @@ -83,5 +86,5 @@ func GetFromPath(path string) (NsHandle, error) { // Set sets the current network namespace to the namespace represented // by NsHandle. func Set(ns NsHandle) error { - return unix.Setns(ns.fd, unix.CLONE_NEWNET) + return unix.Setns(ns.Fd, unix.CLONE_NEWNET) } diff --git a/internal/netns/nshandle.go b/internal/netns/nshandle.go index 8880a95..8aafabe 100644 --- a/internal/netns/nshandle.go +++ b/internal/netns/nshandle.go @@ -3,10 +3,10 @@ package netns // inspired from https://github.com/vishvananda/netns/blob/master/nshandle_linux.go#L11 import ( - "fmt" - "golang.org/x/sys/unix" - "os" - "path" + "fmt" + "golang.org/x/sys/unix" + "os" + "path" ) const bindMountPath = "/run/netns" @@ -14,12 +14,12 @@ const bindMountPath = "/run/netns" // NsHandle is a handle to a network namespace. It can be cast directly // to an int and used as a file descriptor. type NsHandle struct { - fd int - name string + Fd int + Name string } // persist and bind mount net namespace to `/run/netns` -func (ns NsHandle) persist() error { +func (ns *NsHandle) persist() error { if _, err := os.Stat(bindMountPath); os.IsNotExist(err) { err = os.MkdirAll(bindMountPath, 0o755) if err != nil { @@ -27,7 +27,7 @@ func (ns NsHandle) persist() error { } } - namedPath := path.Join(bindMountPath, ns.name) + namedPath := path.Join(bindMountPath, ns.Name) f, err := os.OpenFile(namedPath, os.O_CREATE|os.O_EXCL, 0o444) if err != nil { @@ -50,50 +50,50 @@ func (ns NsHandle) persist() error { // Equal determines if two network handles refer to the same network // namespace. This is done by comparing the device and inode that the // file descriptors point to. -func (ns NsHandle) Equal(other NsHandle) bool { - if ns == other { +func (ns *NsHandle) Equal(other NsHandle) bool { + if *ns == other { return true } var s1, s2 unix.Stat_t - if err := unix.Fstat(ns.fd, &s1); err != nil { + if err := unix.Fstat(ns.Fd, &s1); err != nil { return false } - if err := unix.Fstat(other.fd, &s2); err != nil { + if err := unix.Fstat(other.Fd, &s2); err != nil { return false } return (s1.Dev == s2.Dev) && (s1.Ino == s2.Ino) } // String shows the file descriptor number and its dev and inode. -func (ns NsHandle) String() string { - if ns.fd == -1 { +func (ns *NsHandle) String() string { + if ns.Fd == -1 { return "NS(none)" } var s unix.Stat_t - if err := unix.Fstat(ns.fd, &s); err != nil { + if err := unix.Fstat(ns.Fd, &s); err != nil { return fmt.Sprintf("NS(%d: unknown)", ns) } return fmt.Sprintf("NS(%d: %d, %d)", ns, s.Dev, s.Ino) } // IsOpen returns true if Close() has not been called. -func (ns NsHandle) IsOpen() bool { - return ns.fd != -1 +func (ns *NsHandle) IsOpen() bool { + return ns.Fd != -1 } // Close closes the NsHandle and resets its file descriptor to -1. // It is not safe to use an NsHandle after Close() is called. -func (ns NsHandle) Close() error { - if err := unix.Close(ns.fd); err != nil { +func (ns *NsHandle) Close() error { + if err := unix.Close(ns.Fd); err != nil { return err } - ns.fd = -1 + ns.Fd = -1 return nil } // None gets an empty (closed) NsHandle. func None() NsHandle { return NsHandle{ - fd: -1, + Fd: -1, } }