From f58fc5b102f42b93eff846930dce125567bfee1e Mon Sep 17 00:00:00 2001 From: RouxAntoine Date: Mon, 1 Jan 2024 10:55:49 +0100 Subject: [PATCH] feature: create netns and execute sh commands into it --- Makefile | 38 ++++++++++++--- cmd/.gitkeep | 0 cmd/main.go | 58 +++++++++++++++++++++- go.mod | 6 +++ go.sum | 6 +++ internal/.gitkeep | 0 internal/netns/netns.go | 87 +++++++++++++++++++++++++++++++++ internal/netns/nshandle.go | 99 ++++++++++++++++++++++++++++++++++++++ internal/version/dev.go | 24 +++++++++ internal/version/prod.go | 30 ++++++++++++ 10 files changed, 340 insertions(+), 8 deletions(-) delete mode 100644 cmd/.gitkeep create mode 100644 go.sum delete mode 100644 internal/.gitkeep create mode 100644 internal/netns/netns.go create mode 100644 internal/netns/nshandle.go create mode 100644 internal/version/dev.go create mode 100644 internal/version/prod.go diff --git a/Makefile b/Makefile index 629b3f4..163ed5b 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,37 @@ -.PHONY: build run +.PHONY: build run ci get-alpine-rootfs +.EXPORT_ALL_VARIABLES: -build: - go build -o ./out/main cmd/main.go +GOARCH=amd64 +#GOARCH=arm +#GOOS=darwin +GOOS=linux -run: - @chmod +x ./out/main - ./out/main +LDFLAGS=-w -s -X antoine-roux.tk/projects/go/firecracker-netns/internal/version.Version=$$(git rev-list -1 HEAD) +GOBUILDFLAGS=-tags dev + +EXEC=out/main + +build: out/alpine-minirootfs-3.19.0-x86_64.tar.gz $(EXEC) + +run: $(EXEC) + @chmod +x $(EXEC) + $(EXEC) ci: golangci-lint run --fix + +get-alpine-rootfs: out/alpine-minirootfs-3.19.0-x86_64.tar.gz + +publish: + scp $(EXEC) sf314:~/firecracker/ + +dependencies: + go mod download + go mod verify + +$(EXEC): cmd/main.go dependencies + @echo "build for os $$GOOS and arch $$GOARCH" + go build -o $@ -ldflags="$(LDFLAGS)" $(GOBUILDFLAGS) $< + +out/alpine-minirootfs-3.19.0-x86_64.tar.gz: + wget -O $@ https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/x86_64/alpine-minirootfs-3.19.0-x86_64.tar.gz diff --git a/cmd/.gitkeep b/cmd/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/cmd/main.go b/cmd/main.go index dcec099..ea1cf12 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,7 +1,61 @@ +//go:build linux + package main -import "fmt" +import ( + "antoine-roux.tk/projects/go/firecracker-netns/internal/netns" + "fmt" + "net" + "os" + "os/exec" + "runtime" +) func main() { - fmt.Println("Hello world !") + // Lock the OS Thread, so we don't accidentally switch namespaces + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + handle, err := netns.New() + if err != nil { + fmt.Println("new ns error", err) + } + + defer func(handle *netns.NsHandle) { + err := handle.Close() + if err != nil { + fmt.Println("close ns error", err) + } + }(&handle) + + err = netns.Set(handle) + if err != nil { + fmt.Println("set ns error", err) + } + + // Do something with the network namespace + interfaces, _ := net.Interfaces() + fmt.Printf("Interfaces: %v\n", interfaces) + + cmd := exec.Command("/bin/sh") + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + 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) + } } diff --git a/go.mod b/go.mod index aba5c13..3e4e3e1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module antoine-roux.tk/projects/go/firecracker-netns go 1.21.5 + +require ( + github.com/docker/docker v24.0.7+incompatible + github.com/hashicorp/go-version v1.6.0 + golang.org/x/sys v0.15.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c9d1414 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +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= +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/.gitkeep b/internal/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/internal/netns/netns.go b/internal/netns/netns.go new file mode 100644 index 0000000..9dff324 --- /dev/null +++ b/internal/netns/netns.go @@ -0,0 +1,87 @@ +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" +) + +// New create and persist a new namespace with random name +func New() (handle NsHandle, error error) { + origins, _ := Get() + defer deferClose(&origins, &error) + + if err := unix.Unshare(unix.CLONE_NEWNET); err != nil { + return None(), err + } + + newNs, err := Get() + if err != nil { + return None(), err + } + + err = Set(origins) + if err != nil { + return NsHandle{fd: 0}, err + } + + err = newNs.persist() + if err != nil { + defer deferClose(&newNs, &error) + return newNs, err + } + + return newNs, nil +} + +func deferClose(origins *NsHandle, e *error) { + // return main error, or defer action error if occurred + if err := origins.Close(); e == nil && err != nil { + e = &err + } +} + +// Delete deletes a named network namespace +func Delete(ns NsHandle) error { + namedPath := path.Join(bindMountPath, ns.name) + + err := unix.Unmount(namedPath, unix.MNT_DETACH) + if err != nil { + return err + } + + return os.Remove(namedPath) +} + +// Get gets a handle to the current threads network namespace. +func Get() (NsHandle, error) { + return GetFromPath(GetPath()) +} + +// GetPath gets path to the current threads network namespace. +func GetPath() string { + tid := unix.Gettid() + pid := os.Getpid() + return fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid) +} + +// GetFromPath gets a handle to a network namespace +// identified by the path +func GetFromPath(path string) (NsHandle, error) { + fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC, 0) + if err != nil { + return None(), err + } + name := namesgenerator.GetRandomName(0) + return NsHandle{fd, name}, nil +} + +// 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) +} diff --git a/internal/netns/nshandle.go b/internal/netns/nshandle.go new file mode 100644 index 0000000..8880a95 --- /dev/null +++ b/internal/netns/nshandle.go @@ -0,0 +1,99 @@ +package netns + +// inspired from https://github.com/vishvananda/netns/blob/master/nshandle_linux.go#L11 + +import ( + "fmt" + "golang.org/x/sys/unix" + "os" + "path" +) + +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 +} + +// persist and bind mount net namespace to `/run/netns` +func (ns NsHandle) persist() error { + if _, err := os.Stat(bindMountPath); os.IsNotExist(err) { + err = os.MkdirAll(bindMountPath, 0o755) + if err != nil { + return err + } + } + + namedPath := path.Join(bindMountPath, ns.name) + + f, err := os.OpenFile(namedPath, os.O_CREATE|os.O_EXCL, 0o444) + if err != nil { + return err + } + err = f.Close() + if err != nil { + return err + } + + nsPath := GetPath() + err = unix.Mount(nsPath, namedPath, "bind", unix.MS_BIND, "") + if err != nil { + return err + } + + return nil +} + +// 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 { + return true + } + var s1, s2 unix.Stat_t + if err := unix.Fstat(ns.fd, &s1); err != nil { + return false + } + 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 { + return "NS(none)" + } + var s unix.Stat_t + 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 +} + +// 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 { + return err + } + ns.fd = -1 + return nil +} + +// None gets an empty (closed) NsHandle. +func None() NsHandle { + return NsHandle{ + fd: -1, + } +} diff --git a/internal/version/dev.go b/internal/version/dev.go new file mode 100644 index 0000000..7404f94 --- /dev/null +++ b/internal/version/dev.go @@ -0,0 +1,24 @@ +//go:build dev + +package version + +import ( + "fmt" +) + +// Version ... +var Version = "0.1.0" + +// Prerelease such as "dev" (in development), "beta", "rc1", etc. +var Prerelease = "dev" + +// Header is the header name used to send the current in http requests. +const Header = "Weather-Version" + +// String returns the complete version string, including prerelease +func String() string { + if Prerelease != "" { + return fmt.Sprintf("%s-%s", Version, Prerelease) + } + return Version +} diff --git a/internal/version/prod.go b/internal/version/prod.go new file mode 100644 index 0000000..cd98a13 --- /dev/null +++ b/internal/version/prod.go @@ -0,0 +1,30 @@ +//go:build prod + +package version + +import ( + "fmt" + + version "github.com/hashicorp/go-version" +) + +// Version ... +var Version = "1.0.0" + +// Prerelease such as "dev" (in development), "beta", "rc1", etc. +var Prerelease = "prod" + +// SemVer management +var SemVer *version.Version + +func init() { + SemVer = version.Must(version.NewSemver(fmt.Sprintf("%s-%s", Version, Prerelease))) +} + +// Header is the header name used to send the current in http requests. +const Header = "Weather-Version" + +// String returns the complete version string, including prerelease +func String() string { + return SemVer.String() +}