From 0187c3c4bc2c00ede763b32326201c7bfac4f71f Mon Sep 17 00:00:00 2001 From: milan-zededa <83634241+milan-zededa@users.noreply.github.com> Date: Wed, 30 Aug 2023 14:36:47 +0200 Subject: [PATCH] Traffic control for Eden-SDN (#880) With this patch, Eden-SDN now allows to apply traffic control individually for every network port. This can be used to model poor network connectivity and observe how EVE is able to deal with such challenging conditions. Signed-off-by: Milan Lenco --- sdn/examples/poor-network/README.md | 24 ++ sdn/examples/poor-network/device-config.json | 67 ++++++ sdn/examples/poor-network/network-model.json | 66 ++++++ sdn/vm/api/netModel.go | 33 +++ sdn/vm/cmd/sdnagent/config.go | 23 ++ sdn/vm/cmd/sdnagent/parse.go | 16 ++ sdn/vm/pkg/configitems/ifHandle.go | 2 +- sdn/vm/pkg/configitems/registry.go | 1 + sdn/vm/pkg/configitems/tc.go | 220 +++++++++++++++++++ sdn/vm/pkg/configitems/typenames.go | 2 + 10 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 sdn/examples/poor-network/README.md create mode 100644 sdn/examples/poor-network/device-config.json create mode 100644 sdn/examples/poor-network/network-model.json create mode 100644 sdn/vm/pkg/configitems/tc.go diff --git a/sdn/examples/poor-network/README.md b/sdn/examples/poor-network/README.md new file mode 100644 index 000000000..773ec3a1f --- /dev/null +++ b/sdn/examples/poor-network/README.md @@ -0,0 +1,24 @@ +# SDN Example with emulated poor network connectivity + +Eden-SDN Network Model allows to configure traffic control individually for every network port. +Included is traffic shaping, i.e. limiting traffic to meet but not exceed a configured rate, +and emulating network impairments, such as packet delay, loss, corruption, reordering, etc. +This can be used to simulate poor network connectivity and observe how EVE is able to deal +with such challenging conditions. + +In this example, traffic control parameters are set for the single and only network interface. +The intention is to model rather poor network connection with a low bandwidth and a high +percentage of packet loss or corruption. For the purposes of the showcase, we set every +available traffic control attribute to a specific non-default value. + +Run the example with: + +```shell +make clean && make build-tests +./eden config add default +./eden config set default --key sdn.disable --value false +./eden setup +./eden start --sdn-network-model $(pwd)/sdn/examples/poor-network/network-model.json +./eden eve onboard +./eden controller edge-node set-config --file $(pwd)/sdn/examples/poor-network/device-config.json +``` diff --git a/sdn/examples/poor-network/device-config.json b/sdn/examples/poor-network/device-config.json new file mode 100644 index 000000000..d604157d7 --- /dev/null +++ b/sdn/examples/poor-network/device-config.json @@ -0,0 +1,67 @@ +{ + "deviceIoList": [ + { + "ptype": 1, + "phylabel": "eth0", + "phyaddrs": { + "Ifname": "eth0" + }, + "logicallabel": "eth0", + "assigngrp": "eth0", + "usage": 1, + "usagePolicy": { + "freeUplink": true + } + } + ], + "networks": [ + { + "id": "6605d17b-3273-4108-8e6e-4965441ebe01", + "type": 4, + "ip": { + "dhcp": 4 + } + } + ], + "systemAdapterList": [ + { + "name": "eth0", + "uplink": true, + "networkUUID": "6605d17b-3273-4108-8e6e-4965441ebe01" + } + ], + "configItems": [ + { + "key": "network.fallback.any.eth", + "value": "disabled" + }, + { + "key": "newlog.allow.fastupload", + "value": "true" + }, + { + "key": "timer.config.interval", + "value": "10" + }, + { + "key": "timer.location.app.interval", + "value": "10" + }, + { + "key": "timer.location.cloud.interval", + "value": "300" + }, + { + "key": "app.allow.vnc", + "value": "true" + }, + { + "key": "timer.download.retry", + "value": "60" + }, + { + "key": "debug.default.loglevel", + "value": "debug" + } + ] +} diff --git a/sdn/examples/poor-network/network-model.json b/sdn/examples/poor-network/network-model.json new file mode 100644 index 000000000..bf4bdb652 --- /dev/null +++ b/sdn/examples/poor-network/network-model.json @@ -0,0 +1,66 @@ +{ + "ports": [ + { + "logicalLabel": "eveport0", + "adminUP": true, + "trafficControl": { + "delay": 250, + "delayJitter": 50, + "lossProbability": 20, + "corruptProbability": 5, + "duplicateProbability": 10, + "reorderProbability": 30, + "rateLimit": 512, + "queueLimit": 1024, + "burstLimit": 64 + } + } + ], + "bridges": [ + { + "logicalLabel": "bridge0", + "ports": ["eveport0"] + } + ], + "networks": [ + { + "logicalLabel": "network0", + "bridge": "bridge0", + "subnet": "172.22.12.0/24", + "gwIP": "172.22.12.1", + "dhcp": { + "enable": true, + "ipRange": { + "fromIP": "172.22.12.10", + "toIP": "172.22.12.20" + }, + "domainName": "sdn", + "privateDNS": ["my-dns-server"] + }, + "router": { + "outsideReachability": true, + "reachableEndpoints": ["my-dns-server"] + } + } + ], + "endpoints": { + "dnsServers": [ + { + "logicalLabel": "my-dns-server", + "fqdn": "my-dns-server.sdn", + "subnet": "10.16.16.0/24", + "ip": "10.16.16.25", + "staticEntries": [ + { + "fqdn": "mydomain.adam", + "ip": "adam-ip" + } + ], + "upstreamServers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + ] + } +} \ No newline at end of file diff --git a/sdn/vm/api/netModel.go b/sdn/vm/api/netModel.go index 4c675734a..dfe4d7cb3 100644 --- a/sdn/vm/api/netModel.go +++ b/sdn/vm/api/netModel.go @@ -96,6 +96,39 @@ type Port struct { AdminUP bool `json:"adminUP"` // EVEConnect : plug the other side of the port into a given EVE instance. EVEConnect EVEConnect `json:"eveConnect"` + // TC : traffic control. + TC TrafficControl `json:"trafficControl"` +} + +// TrafficControl allows to control traffic going through a port. +// It can be used to emulate slow and faulty networks. +type TrafficControl struct { + // Delay refers to the duration, measured in milliseconds, by which each packet + // will be delayed. + Delay uint32 `json:"delay"` + // DelayJitter : jitter in milliseconds added to the delay. + DelayJitter uint32 `json:"delayJitter"` + // LossProbability : probability of a packet loss (in percent). + LossProbability uint8 `json:"lossProbability"` + // CorruptProbability : probability of a packet corruption (in percent). + CorruptProbability uint8 `json:"corruptProbability"` + // DuplicateProbability : probability of a packet duplication (in percent). + DuplicateProbability uint8 `json:"duplicateProbability"` + // ReorderProbability represents the percentage probability of a packet's order + // being modified within the queue. + ReorderProbability uint8 `json:"reorderProbability"` + // RateLimit represents the maximum speed, measured in kilobytes per second, + // at which traffic can flow through the port. + RateLimit uint32 `json:"rateLimit"` + // QueueLimit : number of kilobytes that can be queued before being sent further. + // Packets that would exceed the queue size are dropped. + // Mandatory if RateLimit is set. + QueueLimit uint32 `json:"queueLimit"` + // BurstLimit represents the maximum amount of data, measured in kilobytes, + // that can be sent or received in a short burst or interval, temporarily exceeding + // the rate limit. + // Mandatory if RateLimit is set. + BurstLimit uint32 `json:"burstLimit"` } // ItemType diff --git a/sdn/vm/cmd/sdnagent/config.go b/sdn/vm/cmd/sdnagent/config.go index ed3b4ba0e..d2c46af51 100644 --- a/sdn/vm/cmd/sdnagent/config.go +++ b/sdn/vm/cmd/sdnagent/config.go @@ -20,6 +20,7 @@ const ( // *SG are names of sub-graphs. configGraphName = "SDN-Config" physicalIfsSG = "Physical-Interfaces" + trafficControlSG = "Traffic-Control" hostConnectivitySG = "Host-Connectivity" bridgesSG = "Bridges" firewallSG = "Firewall" @@ -92,6 +93,7 @@ func (a *agent) updateIntendedState() { a.intendedState = dg.New(graphArgs) a.intendedState.PutSubGraph(a.getIntendedPhysIfs()) a.intendedState.PutSubGraph(a.getIntendedHostConnectivity()) + a.intendedState.PutSubGraph(a.getIntendedTrafficControl()) a.intendedState.PutSubGraph(a.getIntendedBridges()) a.intendedState.PutSubGraph(a.getIntendedFirewall()) for _, network := range a.netModel.Networks { @@ -183,6 +185,27 @@ func (a *agent) getIntendedHostConnectivity() dg.Graph { return intendedCfg } +func (a *agent) getIntendedTrafficControl() dg.Graph { + graphArgs := dg.InitArgs{Name: trafficControlSG} + intendedCfg := dg.New(graphArgs) + emptyTC := api.TrafficControl{} + for _, port := range a.netModel.Ports { + if port.TC == emptyTC { + continue + } + // MAC address is already validated + mac, _ := net.ParseMAC(port.MAC) + intendedCfg.PutItem(configitems.TrafficControl{ + TrafficControl: port.TC, + PhysIf: configitems.PhysIf{ + LogicalLabel: port.LogicalLabel, + MAC: mac, + }, + }, nil) + } + return intendedCfg +} + func (a *agent) getIntendedBridges() dg.Graph { graphArgs := dg.InitArgs{Name: bridgesSG} intendedCfg := dg.New(graphArgs) diff --git a/sdn/vm/cmd/sdnagent/parse.go b/sdn/vm/cmd/sdnagent/parse.go index a1735c0cb..4a81c3cab 100644 --- a/sdn/vm/cmd/sdnagent/parse.go +++ b/sdn/vm/cmd/sdnagent/parse.go @@ -120,6 +120,22 @@ func (a *agent) validatePorts(netModel *parsedNetModel) (err error) { return } } + + // QueueLimit and BurstLimit are mandatory when RateLimit is set. + for _, port := range netModel.Ports { + if port.TC.RateLimit != 0 { + if port.TC.QueueLimit == 0 { + err = fmt.Errorf("RateLimit set for port %s without QueueLimit", + port.LogicalLabel) + return + } + if port.TC.BurstLimit == 0 { + err = fmt.Errorf("RateLimit set for port %s without BurstLimit", + port.LogicalLabel) + return + } + } + } return nil } diff --git a/sdn/vm/pkg/configitems/ifHandle.go b/sdn/vm/pkg/configitems/ifHandle.go index 21032a06b..36acf9754 100644 --- a/sdn/vm/pkg/configitems/ifHandle.go +++ b/sdn/vm/pkg/configitems/ifHandle.go @@ -3,9 +3,9 @@ package configitems import ( "context" "fmt" - "github.com/lf-edge/eden/sdn/vm/pkg/maclookup" "net" + "github.com/lf-edge/eden/sdn/vm/pkg/maclookup" "github.com/lf-edge/eve/libs/depgraph" log "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" diff --git a/sdn/vm/pkg/configitems/registry.go b/sdn/vm/pkg/configitems/registry.go index a235c0136..afec7d023 100644 --- a/sdn/vm/pkg/configitems/registry.go +++ b/sdn/vm/pkg/configitems/registry.go @@ -28,6 +28,7 @@ func RegisterItems( {c: &IptablesChainConfigurator{}, t: IP6tablesChainTypename}, {c: &HttpProxyConfigurator{}, t: HTTPProxyTypename}, {c: &HttpServerConfigurator{}, t: HTTPServerTypename}, + {c: &TrafficControlConfigurator{MacLookup: macLookup}, t: TrafficControlTypename}, } for _, configurator := range configurators { err := registry.Register(configurator.c, configurator.t) diff --git a/sdn/vm/pkg/configitems/tc.go b/sdn/vm/pkg/configitems/tc.go new file mode 100644 index 000000000..32deb470f --- /dev/null +++ b/sdn/vm/pkg/configitems/tc.go @@ -0,0 +1,220 @@ +package configitems + +import ( + "context" + "errors" + "fmt" + "os/exec" + "strconv" + + "github.com/lf-edge/eden/sdn/vm/api" + "github.com/lf-edge/eden/sdn/vm/pkg/maclookup" + "github.com/lf-edge/eve/libs/depgraph" + log "github.com/sirupsen/logrus" +) + +// TrafficControl represents traffic control rules applied to a physical interface. +type TrafficControl struct { + api.TrafficControl + // PhysIf : target physical network interface for traffic control. + PhysIf PhysIf +} + +// Name returns MAC address of the physical interface as the unique identifier +// for the TrafficControl instance. +func (t TrafficControl) Name() string { + return t.PhysIf.MAC.String() +} + +// Label is used only for the visualization purposes of the config/state depgraph. +func (t TrafficControl) Label() string { + return t.PhysIf.LogicalLabel + " (traffic control)" +} + +// Type assigned to TrafficControl +func (t TrafficControl) Type() string { + return TrafficControlTypename +} + +// Equal is a comparison method for two equally-named TrafficControl instances. +func (t TrafficControl) Equal(other depgraph.Item) bool { + t2, isTrafficControl := other.(TrafficControl) + if !isTrafficControl { + return false + } + return t.TrafficControl == t2.TrafficControl +} + +// External returns false. +func (t TrafficControl) External() bool { + return false +} + +// String describes TrafficControl instance. +func (t TrafficControl) String() string { + return fmt.Sprintf("Traffic control: %#+v", t) +} + +// Dependencies lists the physical interface as the only dependency. +func (t TrafficControl) Dependencies() (deps []depgraph.Dependency) { + return []depgraph.Dependency{ + { + RequiredItem: depgraph.ItemRef{ + ItemType: PhysIfTypename, + ItemName: t.PhysIf.MAC.String(), + }, + Description: "Underlying physical network interface must exist", + }, + } +} + +// TrafficControlConfigurator implements Configurator interface for TrafficControl. +type TrafficControlConfigurator struct { + MacLookup *maclookup.MacLookup +} + +// Create applies traffic control rules for the physical interface. +func (c *TrafficControlConfigurator) Create(_ context.Context, item depgraph.Item) error { + tc, isTrafficControl := item.(TrafficControl) + if !isTrafficControl { + return fmt.Errorf("invalid item type %T, expected TrafficControl", item) + } + netIf, found := c.MacLookup.GetInterfaceByMAC(tc.PhysIf.MAC, false) + if !found { + err := fmt.Errorf("failed to get physical interface with MAC %v", tc.PhysIf.MAC) + log.Error(err) + return err + } + useTBF := tc.RateLimit != 0 + useNetem := tc.Delay != 0 || tc.LossProbability != 0 || tc.CorruptProbability != 0 || + tc.DuplicateProbability != 0 || tc.ReorderProbability != 0 + if useTBF && !useNetem { + // example: + // tc qdisc add dev eth2 root tbf rate 256kbit burst 16kb limit 30kb + var args []string + args = append(args, "qdisc", "add", "dev", netIf.IfName, "root", "tbf") + args = append(args, c.getTBFArgs(tc)...) + output, err := exec.Command("tc", args...).CombinedOutput() + if err != nil { + err = fmt.Errorf("failed to configure tc-tbf for interface %s: %s (%w)", + netIf.IfName, output, err) + log.Error(err) + return err + } + } + if !useTBF && useNetem { + // example: + // tc qdisc add dev eth2 root netem loss 5% + var args []string + args = append(args, "qdisc", "add", "dev", netIf.IfName, "root", "netem") + args = append(args, c.getNetemArgs(tc)...) + output, err := exec.Command("tc", args...).CombinedOutput() + if err != nil { + err = fmt.Errorf("failed to configure tc-netem for interface %s: %s (%w)", + netIf.IfName, output, err) + log.Error(err) + return err + } + } + if useTBF && useNetem { + // example: + // tc qdisc add dev eth2 root handle 1: tbf rate 256kbit buffer 16kb limit 30kb + // tc qdisc add dev eth2 parent 1:1 handle 10: netem delay 100ms + var args []string + args = append(args, "qdisc", "add", "dev", netIf.IfName, + "root", "handle", "1:", "tbf") + args = append(args, c.getTBFArgs(tc)...) + output, err := exec.Command("tc", args...).CombinedOutput() + if err != nil { + err = fmt.Errorf("failed to configure tc-tbf for interface %s: %s (%w)", + netIf.IfName, output, err) + log.Error(err) + return err + } + args = nil + args = append(args, "qdisc", "add", "dev", netIf.IfName, + "parent", "1:1", "handle", "2:", "netem") + args = append(args, c.getNetemArgs(tc)...) + output, err = exec.Command("tc", args...).CombinedOutput() + if err != nil { + err = fmt.Errorf("failed to configure tc-netem for interface %s: %s (%w)", + netIf.IfName, output, err) + log.Error(err) + return err + } + } + return nil +} + +func (c *TrafficControlConfigurator) getTBFArgs(tc TrafficControl) []string { + var args []string + if tc.RateLimit != 0 { + args = append(args, "rate", strconv.Itoa(int(tc.RateLimit))+"kbps") + } + if tc.BurstLimit != 0 { + args = append(args, "burst", strconv.Itoa(int(tc.BurstLimit))+"kb") + } + if tc.QueueLimit != 0 { + args = append(args, "limit", strconv.Itoa(int(tc.QueueLimit))+"kb") + } + return args +} + +func (c *TrafficControlConfigurator) getNetemArgs(tc TrafficControl) []string { + var args []string + if tc.Delay != 0 { + args = append(args, "delay", strconv.Itoa(int(tc.Delay))+"ms") + if tc.DelayJitter != 0 { + args = append(args, strconv.Itoa(int(tc.DelayJitter))+"ms") + } + } + if tc.LossProbability != 0 { + args = append(args, "loss", "random", strconv.Itoa(int(tc.LossProbability))+"%") + } + if tc.CorruptProbability != 0 { + args = append(args, "corrupt", strconv.Itoa(int(tc.CorruptProbability))+"%") + } + if tc.DuplicateProbability != 0 { + args = append(args, "duplicate", strconv.Itoa(int(tc.DuplicateProbability))+"%") + } + if tc.ReorderProbability != 0 { + args = append(args, "reorder", strconv.Itoa(int(tc.ReorderProbability))+"%") + } + return args +} + +// Modify is not implemented. +func (c *TrafficControlConfigurator) Modify(_ context.Context, _, _ depgraph.Item) (err error) { + return errors.New("not implemented") +} + +// Delete removes applied traffic control rules from the physical interface. +func (c *TrafficControlConfigurator) Delete(_ context.Context, item depgraph.Item) error { + tc, isTrafficControl := item.(TrafficControl) + if !isTrafficControl { + return fmt.Errorf("invalid item type %T, expected TrafficControl", item) + } + netIf, found := c.MacLookup.GetInterfaceByMAC(tc.PhysIf.MAC, false) + if !found { + err := fmt.Errorf("failed to get physical interface with MAC %v", tc.PhysIf.MAC) + log.Error(err) + return err + } + // example: + // tc qdisc del dev eth2 root + var args []string + args = append(args, "qdisc", "del", "dev", netIf.IfName, "root") + output, err := exec.Command("tc", args...).CombinedOutput() + if err != nil { + err = fmt.Errorf("failed to unconfigure tc from interface %s: %s (%w)", + netIf.IfName, output, err) + log.Error(err) + return err + } + return nil +} + +// NeedsRecreate returns true, Modify is not implemented. +func (c *TrafficControlConfigurator) NeedsRecreate(_, _ depgraph.Item) (recreate bool) { + return true +} diff --git a/sdn/vm/pkg/configitems/typenames.go b/sdn/vm/pkg/configitems/typenames.go index 40fdba651..6e509f25b 100644 --- a/sdn/vm/pkg/configitems/typenames.go +++ b/sdn/vm/pkg/configitems/typenames.go @@ -36,4 +36,6 @@ const ( HTTPProxyTypename = "HTTP-Proxy" // HTTPServerTypename : typename for HTTP server. HTTPServerTypename = "HTTP-Server" + // TrafficControlTypename : typename for TC rules applied to physical interface. + TrafficControlTypename = "Traffic-Control" )