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" )