├── LICENSE ├── README.md ├── example.nd ├── netdef └── main.go ├── netdefine.go └── util.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Whyrusleeping 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netdef 2 | 3 | A virtual network management tool. 4 | 5 | ## Installation 6 | To install the cli tool, just run: 7 | ``` 8 | go get -u github.com/whyrusleeping/go-netdef/netdef 9 | ``` 10 | 11 | ## Dependencies 12 | This only runs on linux right now, and requires a working installation of `openvswitch`. 13 | 14 | ## Usage 15 | Netdef can be used as a package, but it also provides a command line tool 16 | that operates on json formatted 'netdef' specifications. An example such file might look like: 17 | 18 | ```json 19 | { 20 | "networks": [{ 21 | "name":"seattle", 22 | "iprange": "10.1.1.0/24" 23 | }], 24 | "peers": [ 25 | { 26 | "name":"wolf", 27 | "links": { 28 | "seattle": {}, 29 | } 30 | }, 31 | { 32 | "name":"bear", 33 | "links": { 34 | "seattle": { 35 | "latency": "10ms" 36 | } 37 | } 38 | } 39 | ], 40 | } 41 | ``` 42 | 43 | This specification defines a network named 'seattle' that allocates ip 44 | addresses for its peers in the 10.1.1.0/24 subnet, and two peers named 'wolf' 45 | and 'bear'. It then defines that 'wolf' has a link to 'seattle' with default 46 | settings, and that 'bear' has a link to 'seattle' with a 10ms latency on it. 47 | 48 | To create this network, save the json to a file `example.nd` and run: 49 | ``` 50 | sudo netdef create example.nd 51 | ``` 52 | 53 | This will set up all the namespaces needed and switches, and connect them as described. 54 | Once setup, you can run commands on a given peer by doing: 55 | ``` 56 | sudo ip netns exec wolf ping 10.1.1.2 57 | ``` 58 | 59 | To teardown the network, run: 60 | ``` 61 | sudo netdef cleanup example.nd 62 | ``` 63 | 64 | ## TODO 65 | Theres a lot more I want to do here, this is a partial list (roughly in order of priority): 66 | - [ ] Actually implement latencies/bandwidth/packet-loss with go-ctrlnet 67 | - [ ] Better permissions (running everything as root sucks) 68 | - [ ] Network to network links 69 | - [ ] Better validation of config 70 | - [ ] Multi-host namespaces (via openvswitch) 71 | - [ ] Different 'bridge' implementations (i.e. brctl) 72 | 73 | ## License 74 | MIT 75 | -------------------------------------------------------------------------------- /example.nd: -------------------------------------------------------------------------------- 1 | { 2 | "networks": [{ 3 | "name":"wild", 4 | "iprange": "10.1.1.0/24" 5 | }], 6 | "peers": [ 7 | { 8 | "name":"wolf", 9 | "links": { 10 | "wild": {} 11 | } 12 | }, 13 | { 14 | "name":"bear", 15 | "links": { 16 | "wild": { 17 | "latency": "10ms" 18 | } 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /netdef/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/urfave/cli" 9 | "github.com/whyrusleeping/go-netdef" 10 | ) 11 | 12 | func readConfig(path string) (*netdef.Config, error) { 13 | fi, err := os.Open(path) 14 | if err != nil { 15 | return nil, err 16 | } 17 | defer fi.Close() 18 | 19 | cfg := &netdef.Config{} 20 | if err = json.NewDecoder(fi).Decode(cfg); err != nil { 21 | return nil, err 22 | } 23 | 24 | return cfg, nil 25 | } 26 | 27 | func writeRender(path string, r *netdef.RenderedNetwork) error { 28 | fi, err := os.Create(path) 29 | if err != nil { 30 | return err 31 | } 32 | defer fi.Close() 33 | if err := json.NewEncoder(fi).Encode(r); err != nil { 34 | return err 35 | } 36 | return nil 37 | } 38 | 39 | func main() { 40 | app := cli.NewApp() 41 | 42 | create := cli.Command{ 43 | Name: "create", 44 | Flags: []cli.Flag{ 45 | cli.StringFlag{ 46 | Name: "output", 47 | Value: "config.render.json", 48 | Usage: "Path to write out the rendered configuration", 49 | }, 50 | }, 51 | Action: func(c *cli.Context) error { 52 | if c.Args().First() == "" { 53 | return fmt.Errorf("must specify netdef configuration file") 54 | } 55 | 56 | cfg, err := readConfig(c.Args().First()) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | r, err := cfg.Create() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | err = writeRender(c.String("output"), r) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return nil 72 | }, 73 | } 74 | 75 | cleanup := cli.Command{ 76 | Name: "cleanup", 77 | Action: func(c *cli.Context) error { 78 | if c.Args().First() == "" { 79 | return fmt.Errorf("must specify netdef configuration file") 80 | } 81 | 82 | fi, err := os.Open(c.Args().First()) 83 | if err != nil { 84 | return err 85 | } 86 | defer fi.Close() 87 | 88 | var r netdef.RenderedNetwork 89 | if err := json.NewDecoder(fi).Decode(&r); err != nil { 90 | return err 91 | } 92 | 93 | if err := r.Cleanup(); err != nil { 94 | return err 95 | } 96 | 97 | return nil 98 | }, 99 | } 100 | 101 | app.Commands = []cli.Command{ 102 | create, 103 | cleanup, 104 | } 105 | 106 | app.RunAndExitOnError() 107 | } 108 | -------------------------------------------------------------------------------- /netdefine.go: -------------------------------------------------------------------------------- 1 | package netdef 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io/ioutil" 8 | "math/big" 9 | "net" 10 | "os" 11 | "os/exec" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/pkg/errors" 18 | "github.com/whyrusleeping/go-ctrlnet" 19 | ) 20 | 21 | func callBin(args ...string) error { 22 | _, err := exec.LookPath(args[0]) 23 | if err != nil { 24 | return errors.Wrap(err, "looking up binary failed") 25 | } 26 | 27 | cmd := exec.Command(args[0], args[1:]...) 28 | out, err := cmd.CombinedOutput() 29 | if err != nil { 30 | return fmt.Errorf("%s (exit code %d)", strings.TrimRight(string(out), "\n"), cmd.ProcessState.ExitCode()) 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // freshInterfaceName creates a unique interface name based on prefix that does 37 | // not collide with any existing interfaces. 38 | func freshInterfaceName(prefix string) (string, error) { 39 | ifaces, err := net.Interfaces() 40 | if err != nil { 41 | return "", err 42 | } 43 | names := make([]string, len(ifaces)) 44 | for i, iface := range ifaces { 45 | names[i] = iface.Name 46 | } 47 | return freshName(prefix, names), nil 48 | } 49 | 50 | var vethRegexp = regexp.MustCompile(`^[0-9]+: ([a-z0-9]+)(@[a-z0-9]+)?:.+`) 51 | 52 | // getVethNames is a helper function to poll for veth interfaces. 53 | func getVethNames() ([]string, error) { 54 | cmd := exec.Command("ip", "link", "show", "type", "veth") 55 | out, err := cmd.CombinedOutput() 56 | if err != nil { 57 | return nil, err 58 | } 59 | buf := bytes.NewReader(out) 60 | scanner := bufio.NewScanner(buf) 61 | ret := make([]string, 0) 62 | for scanner.Scan() { 63 | match := vethRegexp.FindStringSubmatch(scanner.Text()) 64 | if match != nil { 65 | ret = append(ret, match[1]) 66 | } 67 | } 68 | return ret, nil 69 | } 70 | 71 | // freshVethName creates a unique veth name based on prefix that does not 72 | // collide with any existing veths. 73 | func freshVethName(prefix string) (string, error) { 74 | names, err := getVethNames() 75 | if err != nil { 76 | return "", err 77 | } 78 | return freshName(prefix, names), nil 79 | } 80 | 81 | // freshNamespaceName creates a unique netns name that does not collide with any 82 | // exising namespaces. 83 | func freshNamespaceName(prefix string) (string, error) { 84 | files, err := ioutil.ReadDir("/var/run/netns") 85 | if err == nil { 86 | // Successfully opened the directory 87 | names := make([]string, len(files)) 88 | for i, file := range files { 89 | if file.IsDir() { 90 | continue 91 | } 92 | names[i] = file.Name() 93 | } 94 | return freshName(prefix, names), nil 95 | } else if os.IsNotExist(err) { 96 | // Directory doesn't exist, assume there are no namespaces 97 | names := make([]string, 0) 98 | return freshName(prefix, names), nil 99 | } else { 100 | // Real error 101 | return "", err 102 | } 103 | } 104 | 105 | // freshName generates a new name based on prefix that does not collide with any 106 | // names in existing. 107 | func freshName(prefix string, existing []string) string { 108 | found := false 109 | max := uint64(0) 110 | for _, name := range existing { 111 | if strings.HasPrefix(name, prefix) { 112 | found = true 113 | numstr := name[len(prefix):] 114 | num, err := strconv.ParseUint(numstr, 10, 64) 115 | if err != nil { 116 | continue 117 | } 118 | if num > max { 119 | max = num 120 | } 121 | } 122 | } 123 | if found { 124 | max++ 125 | } 126 | return fmt.Sprintf("%s%d", prefix, max) 127 | } 128 | 129 | func (r *RenderedNetwork) freshNetworkName(name string) (string, error) { 130 | bridgename, err := r.freshInterfaceName("Bridge") 131 | if err != nil { 132 | return "", err 133 | } 134 | r.subnets[name] = bridgename 135 | return bridgename, nil 136 | } 137 | 138 | func (r *RenderedNetwork) freshInterfaceName(typ string) (string, error) { 139 | prefix := r.prefixes[typ] 140 | bridgename, err := freshInterfaceName(prefix) 141 | if err != nil { 142 | return "", err 143 | } 144 | return bridgename, nil 145 | } 146 | 147 | func (r *RenderedNetwork) freshVethName(typ string) (string, error) { 148 | prefix := r.prefixes[typ] 149 | bridgename, err := freshVethName(prefix) 150 | if err != nil { 151 | return "", err 152 | } 153 | return bridgename, nil 154 | } 155 | 156 | // CreateNamespace creates a unique namespace and, if successful, logs a mapping 157 | // of the configuration name to the generated namespace name. 158 | func (r *RenderedNetwork) CreateNamespace(name string) error { 159 | freshname, err := freshNamespaceName(r.prefixes["Namespace"]) 160 | if err != nil { 161 | return err 162 | } 163 | err = callBin("ip", "netns", "add", freshname) 164 | if err == nil { 165 | r.Namespaces[name] = freshname 166 | } 167 | return err 168 | } 169 | 170 | // DeleteNamespace deletes an internet namespace. 171 | func (r *RenderedNetwork) DeleteNamespace(name string) error { 172 | err := callBin("ip", "netns", "del", name) 173 | if err == nil { 174 | delete(r.Namespaces, name) 175 | } 176 | return err 177 | } 178 | 179 | // CreateBridge creates a new bridge with openvswitch. 180 | func (r *RenderedNetwork) CreateBridge(name string) error { 181 | err := callBin("ovs-vsctl", "add-br", name) 182 | if err == nil { 183 | r.Bridges[name] = struct{}{} 184 | } 185 | return err 186 | } 187 | 188 | // DeleteBridge deletes a bridge with openvswitch. 189 | func (r *RenderedNetwork) DeleteBridge(name string) error { 190 | err := callBin("ovs-vsctl", "del-br", name) 191 | if err == nil { 192 | delete(r.Bridges, name) 193 | } 194 | return err 195 | } 196 | 197 | // BridgeAddPort adds a port to an openvswitch bridge. 198 | func (r *RenderedNetwork) BridgeAddPort(bridge, ifname string) error { 199 | return callBin("ovs-vsctl", "add-port", bridge, ifname) 200 | } 201 | 202 | // PortSetParameter sets a variable for a given port. 203 | func (r *RenderedNetwork) PortSetParameter(port, param, val string) error { 204 | typeStr := fmt.Sprintf("%s=%s", param, val) 205 | return callBin("ovs-vsctl", "set", "interface", port, typeStr) 206 | } 207 | 208 | // PortSetOption sets an option for a given port. 209 | func (r *RenderedNetwork) PortSetOption(port, option, peer string) error { 210 | param := fmt.Sprintf("options:%s", option) 211 | return r.PortSetParameter(port, param, peer) 212 | } 213 | 214 | // PatchBridges creates patch ports on two interfaces and peers them, 215 | // effectively connecting two openvswitch bridges. 216 | func (r *RenderedNetwork) PatchBridges(a, b string, l *LinkOpts) error { 217 | ab, err := r.freshVethName("Port") 218 | if err != nil { 219 | return errors.Wrap(err, "creating fresh port name") 220 | } 221 | if err = r.CreateVeth(ab); err != nil { 222 | return errors.Wrap(err, "creating port") 223 | } 224 | ba, err := r.freshVethName("Port") 225 | if err != nil { 226 | return errors.Wrap(err, "creating fresh port name") 227 | } 228 | if err = r.CreateVeth(ba); err != nil { 229 | return errors.Wrap(err, "creating port") 230 | } 231 | if err = r.BridgeAddPort(a, ab); err != nil { 232 | return errors.Wrap(err, "adding port") 233 | } 234 | if err = r.PortSetParameter(ab, "type", "patch"); err != nil { 235 | return errors.Wrap(err, "configuring port type") 236 | } 237 | if err = r.PortSetOption(ab, "peer", ba); err != nil { 238 | return errors.Wrap(err, "configuring port options") 239 | } 240 | if err = r.BridgeAddPort(b, ba); err != nil { 241 | return errors.Wrap(err, "adding port") 242 | } 243 | if err = r.PortSetParameter(ba, "type", "patch"); err != nil { 244 | return errors.Wrap(err, "configuring port type") 245 | } 246 | if err = r.PortSetOption(ba, "peer", ab); err != nil { 247 | return errors.Wrap(err, "configuring port options") 248 | } 249 | if l != nil { 250 | if err = l.Apply(ab); err != nil { 251 | return errors.Wrap(err, "setting patch link options") 252 | } 253 | } 254 | 255 | return nil 256 | } 257 | 258 | // NetNsExec executes a command within a network namespace. 259 | func (r *RenderedNetwork) NetNsExec(ns string, cmdn string, nsargs ...string) error { 260 | args := []string{"ip", "netns", "exec", ns, cmdn} 261 | args = append(args, nsargs...) 262 | return callBin(args...) 263 | } 264 | 265 | // SetDev updates the state of a network device. 266 | func (r *RenderedNetwork) SetDev(dev string, state string) error { 267 | return callBin("ip", "link", "set", "dev", dev, state) 268 | } 269 | 270 | // CreateVeth creates a new veth interface. 271 | func (r *RenderedNetwork) CreateVeth(a string) error { 272 | err := callBin("ip", "link", "add", a, "type", "veth") 273 | if err == nil { 274 | r.Interfaces[a] = struct{}{} 275 | } 276 | return err 277 | } 278 | 279 | // CreateVethPair creates a new pair of veth interfaces that are connected. 280 | func (r *RenderedNetwork) CreateVethPair(a, b string) error { 281 | err := callBin("ip", "link", "add", a, "type", "veth", "peer", "name", b) 282 | if err == nil { 283 | r.Interfaces[a] = struct{}{} 284 | r.Interfaces[b] = struct{}{} 285 | } 286 | return err 287 | } 288 | 289 | // DeleteInterface deletes a network interface. 290 | func (r *RenderedNetwork) DeleteInterface(name string) error { 291 | err := callBin("ip", "link", "del", name) 292 | if err == nil { 293 | delete(r.Interfaces, name) 294 | } 295 | return err 296 | } 297 | 298 | // AssignVethToNamespace moves a veth into a network namespace. 299 | func (r *RenderedNetwork) AssignVethToNamespace(veth, ns string) error { 300 | err := callBin("ip", "link", "set", veth, "netns", ns) 301 | if err == nil { 302 | delete(r.Interfaces, veth) 303 | } 304 | return err 305 | } 306 | 307 | // Config describes a network configuration. From a Config, netdef can create a 308 | // RenderedNetwork, representing the set of actual namespaces, bridges, and veth 309 | // interfaces created in executing a configuration. 310 | type Config struct { 311 | // Networks is a slice of descriptions of subnets. 312 | Networks []Network 313 | // Peers is a slice of descriptions of peers which will manifest as 314 | // namespaces with the desired connectivity configuraiton. 315 | Peers []Peer 316 | // Prefixes is a user-configurable map of prefixes for the various network 317 | // constructs created by this library. If it is not provided, the nil value 318 | // will be replaced with defaults. The valid keys are: 319 | // - Bridge (default "br") 320 | // - Interface (default "veth") 321 | // - Patch (default "patch") 322 | // - Port (default "tap") 323 | // - Namespace (default "ns") 324 | Prefixes map[string]string 325 | } 326 | 327 | // Network describes a subnet configuration. 328 | type Network struct { 329 | // Name of the subnet, used only in configuration, not actual rendering. 330 | Name string 331 | // IpRange is a string representation of a class C or D IP range. 332 | IpRange string 333 | // Links is a map of subnets this network is connected to to the link 334 | // options that describe the physical qualities of the link. 335 | Links map[string]*LinkOpts 336 | // BindMask is a default subnet mask for all peers created on this network. 337 | BindMask string 338 | 339 | ipnet *net.IPNet 340 | nextIp int64 341 | } 342 | 343 | // RenderedNetwork describes the actual changes made to a host operating system 344 | // in executing a configuration. This exists primarily for cleaning up rendered 345 | // network configurations. 346 | type RenderedNetwork struct { 347 | // Bridges is a set of bridges created by a Config. 348 | Bridges map[string]struct{} 349 | // Namespaces is a map of peer names to the namespaces created for them. 350 | Namespaces map[string]string 351 | // Interfaces ia set of veths created in the global namespace. Typically 352 | // these will all be ports to openvswitch bridges. 353 | Interfaces map[string]struct{} 354 | 355 | subnets map[string]string 356 | prefixes map[string]string 357 | } 358 | 359 | // NewRenderedNetwork initializes a RenderedNetwork based on the prefixes 360 | // supplied by the Config. 361 | func (c *Config) NewRenderedNetwork() *RenderedNetwork { 362 | r := &RenderedNetwork{ 363 | Bridges: make(map[string]struct{}), 364 | Namespaces: make(map[string]string), 365 | Interfaces: make(map[string]struct{}), 366 | subnets: make(map[string]string), 367 | prefixes: map[string]string{ 368 | "Bridge": "br", 369 | "Interface": "veth", 370 | "Patch": "patch", 371 | "Port": "tap", 372 | "Namespace": "ns", 373 | }, 374 | } 375 | 376 | if c.Prefixes != nil { 377 | for k, v := range c.Prefixes { 378 | r.prefixes[k] = v 379 | } 380 | } 381 | 382 | return r 383 | } 384 | 385 | // GetNextIp returns the next IPv4 address in the Network's IpRange. 386 | func (n *Network) GetNextIp(mask string) (string, error) { 387 | ip := n.ipnet.IP 388 | 389 | // TODO: better algorithm for this all. github.com/apparentlymart/go-cidr looks decent 390 | n.nextIp++ 391 | 392 | ipn := big.NewInt(0).SetBytes([]byte(ip)) 393 | ipn.Add(ipn, big.NewInt(n.nextIp)) 394 | 395 | b := ipn.Bytes() 396 | subnetMask := net.IPMask(net.ParseIP(mask)) 397 | if subnetMask == nil { 398 | subnetMask = net.IPMask(net.ParseIP(n.BindMask)) 399 | if subnetMask == nil { 400 | subnetMask = n.ipnet.Mask 401 | } 402 | } 403 | out := net.IPNet{ 404 | IP: net.IPv4(b[0], b[1], b[2], b[3]), 405 | Mask: subnetMask, 406 | } 407 | return out.String(), nil 408 | } 409 | 410 | // Peer describes a peer to be rendered into a network namespace. 411 | type Peer struct { 412 | // Name of the peer. 413 | Name string 414 | // A map of subnets this peer is connected to and their link properties. 415 | Links map[string]*LinkOpts 416 | // The default subnet mask for this peer. 417 | BindMask string 418 | } 419 | 420 | // LinkOpts describes a physical network connection. 421 | type LinkOpts struct { 422 | // Latency of the interface. 423 | Latency string 424 | // Jitter of the interface. 425 | Jitter string 426 | // Bandwidth available to the interface. 427 | Bandwidth string 428 | // PacketLoss rate of the interface. 429 | PacketLoss string 430 | 431 | lset *ctrlnet.LinkSettings 432 | } 433 | 434 | // Parse parses human readable LinkOpts into openvswitch ready LinkSettings. 435 | func (lo *LinkOpts) Parse() error { 436 | lo.lset = new(ctrlnet.LinkSettings) 437 | 438 | if lo.Latency != "" { 439 | lat, err := time.ParseDuration(lo.Latency) 440 | if err != nil { 441 | return err 442 | } 443 | 444 | lo.lset.Latency = uint(lat.Nanoseconds() / 1000000) 445 | } 446 | 447 | if lo.Jitter != "" { 448 | jit, err := time.ParseDuration(lo.Jitter) 449 | if err != nil { 450 | return err 451 | } 452 | 453 | lo.lset.Jitter = uint(jit.Nanoseconds() / 1000000) 454 | } 455 | 456 | bw, err := ParseHumanLinkRate(lo.Bandwidth) 457 | if err != nil { 458 | return err 459 | } 460 | lo.lset.Bandwidth = bw 461 | 462 | pl, err := ParsePercentage(lo.PacketLoss) 463 | if err != nil { 464 | return err 465 | } 466 | 467 | lo.lset.PacketLoss = uint8(pl) 468 | 469 | return nil 470 | } 471 | 472 | // Apply configures an interface to have the specified settings. It is all or 473 | // nothing, so a user must configure all aspects of the LinkOpts for this method 474 | // to have an effect. 475 | func (lo *LinkOpts) Apply(iface string) error { 476 | if lo.Bandwidth == "" && lo.PacketLoss == "" && lo.Jitter == "" && lo.Latency == "" { 477 | return nil 478 | } 479 | 480 | if lo.lset == nil { 481 | return fmt.Errorf("linkopts has not been parsed for iface %s", iface) 482 | } 483 | 484 | return ctrlnet.SetLink(iface, lo.lset) 485 | } 486 | 487 | // Create realizes a Config as a RenderedNetwork, tracking the side effects in 488 | // the RenderedNetwork. 489 | func (cfg *Config) Create() (*RenderedNetwork, error) { 490 | nets := make(map[string]*Network) 491 | for i := range cfg.Networks { 492 | n := cfg.Networks[i] 493 | if _, ok := nets[n.Name]; ok { 494 | return nil, fmt.Errorf("duplicate network name: %s", n.Name) 495 | } 496 | 497 | _, ipn, err := net.ParseCIDR(n.IpRange) 498 | if err != nil { 499 | return nil, err 500 | } 501 | 502 | n.ipnet = ipn 503 | nets[n.Name] = &n 504 | } 505 | 506 | peers := make(map[string]bool) 507 | for _, p := range cfg.Peers { 508 | _, ok := peers[p.Name] 509 | if ok { 510 | return nil, fmt.Errorf("duplicate peer name: %s", p.Name) 511 | } 512 | peers[p.Name] = true 513 | 514 | for net, l := range p.Links { 515 | if _, ok := nets[net]; !ok { 516 | return nil, fmt.Errorf("peer %s has link to non-existent network %q", p.Name, net) 517 | } 518 | 519 | if l == nil { 520 | continue 521 | } 522 | if err := l.Parse(); err != nil { 523 | return nil, err 524 | } 525 | } 526 | } 527 | 528 | for name, net := range nets { 529 | for targetNet, l := range net.Links { 530 | if _, ok := nets[targetNet]; !ok { 531 | return nil, fmt.Errorf("network %s has link to non-existent network %s", name, targetNet) 532 | } 533 | 534 | if l == nil { 535 | continue 536 | } 537 | if err := l.Parse(); err != nil { 538 | return nil, err 539 | } 540 | } 541 | } 542 | 543 | r := cfg.NewRenderedNetwork() 544 | 545 | for n := range nets { 546 | bridgename, err := r.freshNetworkName(n) 547 | if err != nil { 548 | return r, errors.Wrap(err, "generating network name") 549 | } 550 | if err := r.CreateBridge(bridgename); err != nil { 551 | return r, errors.Wrap(err, "creating bridge") 552 | } 553 | } 554 | 555 | for name, net := range nets { 556 | bridge := r.subnets[name] 557 | for targetNet, l := range net.Links { 558 | targetBridge := r.subnets[targetNet] 559 | if err := r.PatchBridges(bridge, targetBridge, l); err != nil { 560 | return r, errors.Wrap(err, "patching bridges") 561 | } 562 | } 563 | } 564 | 565 | for _, p := range cfg.Peers { 566 | if err := r.CreateNamespace(p.Name); err != nil { 567 | return r, err 568 | } 569 | ns := r.Namespaces[p.Name] 570 | 571 | for net, l := range p.Links { 572 | bridge := r.subnets[net] 573 | lnA, err := r.freshVethName("Interface") 574 | if err != nil { 575 | return r, errors.Wrap(err, "generate interface name") 576 | } 577 | lnB, err := r.freshVethName("Port") 578 | if err != nil { 579 | return r, errors.Wrap(err, "generate port name") 580 | } 581 | 582 | if err := r.CreateVethPair(lnA, lnB); err != nil { 583 | return r, errors.Wrap(err, "create veth pair") 584 | } 585 | 586 | if err := r.BridgeAddPort(bridge, lnB); err != nil { 587 | return r, errors.Wrap(err, "bridge add port") 588 | } 589 | 590 | if err := r.AssignVethToNamespace(lnA, ns); err != nil { 591 | return r, errors.Wrap(err, "failed to assign veth to namespace") 592 | } 593 | 594 | if err := r.NetNsExec(ns, "ip", "link", "set", "dev", "lo", "up"); err != nil { 595 | return r, errors.Wrap(err, "set ns link up") 596 | } 597 | 598 | if err := r.NetNsExec(ns, "ip", "link", "set", "dev", lnA, "up"); err != nil { 599 | return r, errors.Wrap(err, "set ns link up") 600 | } 601 | 602 | if err := r.SetDev(lnB, "up"); err != nil { 603 | return r, err 604 | } 605 | 606 | next, err := nets[net].GetNextIp(p.BindMask) 607 | if err != nil { 608 | return r, err 609 | } 610 | 611 | if err := r.NetNsExec(ns, "ip", "addr", "add", next, "dev", lnA); err != nil { 612 | return r, err 613 | } 614 | 615 | if l == nil { 616 | continue 617 | } 618 | if err := l.Apply(lnB); err != nil { 619 | return r, err 620 | } 621 | } 622 | } 623 | 624 | return r, nil 625 | } 626 | 627 | // Cleanup reverses the changes made by calling Create on a Config. 628 | func (r *RenderedNetwork) Cleanup() error { 629 | for iface := range r.Interfaces { 630 | if err := r.DeleteInterface(iface); err != nil { 631 | return err 632 | } 633 | } 634 | 635 | for _, ns := range r.Namespaces { 636 | if err := r.DeleteNamespace(ns); err != nil { 637 | return err 638 | } 639 | } 640 | 641 | for br := range r.Bridges { 642 | if err := r.DeleteBridge(br); err != nil { 643 | return err 644 | } 645 | } 646 | 647 | return nil 648 | } 649 | 650 | /* 651 | func main() { 652 | cfg := &Config{ 653 | Networks: []Network{ 654 | { 655 | Name: "homenetwork", 656 | IpRange: "10.1.1.0/24", 657 | BindMask: "255.255.0.0", 658 | }, 659 | { 660 | Name: "officenetwork", 661 | IpRange: "10.1.2.0/24", 662 | Links: map[string]*LinkOpts{ 663 | "homenetwork": nil, 664 | }, 665 | }, 666 | }, 667 | Peers: []Peer{ 668 | { 669 | Name: "c1", 670 | Links: map[string]*LinkOpts{ 671 | "homenetwork": &LinkOpts{}, 672 | }, 673 | BindMask: "255.255.0.0", 674 | }, 675 | { 676 | Name: "c2", 677 | Links: map[string]*LinkOpts{ 678 | "officenetwork": &LinkOpts{ 679 | Latency: "50ms", 680 | }, 681 | }, 682 | }, 683 | }, 684 | } 685 | 686 | r, err := cfg.Create() 687 | if err != nil { 688 | panic(err) 689 | } 690 | 691 | scanner := bufio.NewScanner(os.Stdin) 692 | scanner.Scan() 693 | 694 | if err := r.Cleanup(); err != nil { 695 | panic(err) 696 | } 697 | } 698 | */ 699 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package netdef 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | var prefixes = map[byte]uint{ 11 | 'k': 1024, 12 | 'm': 1024 * 1024, 13 | 'g': 1024 * 1024 * 1024, 14 | 't': 1024 * 1024 * 1024 * 1024, 15 | } 16 | 17 | func ParseHumanLinkRate(s string) (uint, error) { 18 | if s == "" { 19 | return 0, nil 20 | } 21 | 22 | if len(s) < 4 { 23 | return 0, fmt.Errorf("invalid rate limit string") 24 | } 25 | 26 | if !strings.HasSuffix(s, "bit") { 27 | return 0, fmt.Errorf("link rate string must end in 'bit'") 28 | } 29 | 30 | s = s[:len(s)-3] 31 | mul := uint(1) 32 | if !unicode.IsDigit(rune(s[len(s)-1])) { 33 | m, ok := prefixes[s[len(s)-1]] 34 | if !ok { 35 | return 0, fmt.Errorf("invalid metric prefix: %c", s[len(s)-1]) 36 | } 37 | 38 | mul = m 39 | s = s[:len(s)-1] 40 | } 41 | 42 | val, err := strconv.Atoi(s) 43 | if err != nil { 44 | return 0, err 45 | } 46 | 47 | return uint(val) * mul, nil 48 | } 49 | 50 | func ParsePercentage(s string) (uint, error) { 51 | if s == "" { 52 | return 0, nil 53 | } 54 | 55 | if !strings.HasSuffix(s, "%") { 56 | return 0, fmt.Errorf("percentage strings must end in %%") 57 | } 58 | 59 | v, err := strconv.Atoi(s[:len(s)-1]) 60 | if err != nil { 61 | return 0, err 62 | } 63 | 64 | // TODO: maybe bound to [0-100]? 65 | return uint(v), nil 66 | } 67 | --------------------------------------------------------------------------------