@@ -3,13 +3,16 @@ package events
33import (
44 "context"
55 "fmt"
6+ "math"
67 "strconv"
78 "strings"
89 "sync"
910 "time"
1011
1112 "github.com/charmbracelet/log"
13+ gotc "github.com/florianl/go-tc"
1214 clabconstants "github.com/srl-labs/containerlab/constants"
15+ clabnetem "github.com/srl-labs/containerlab/netem"
1316 clabruntime "github.com/srl-labs/containerlab/runtime"
1417 clabutils "github.com/srl-labs/containerlab/utils"
1518 "github.com/vishvananda/netlink"
@@ -229,6 +232,7 @@ type netlinkWatcher struct {
229232 includeSnapshot bool
230233 includeStats bool
231234 statsInterval time.Duration
235+ nsHandle netns.NsHandle
232236}
233237
234238func (w * netlinkWatcher ) run (ctx context.Context , registry * netlinkRegistry ) {
@@ -261,6 +265,8 @@ func (w *netlinkWatcher) run(ctx context.Context, registry *netlinkRegistry) {
261265 }
262266 defer nsHandle .Close ()
263267
268+ w .nsHandle = nsHandle
269+
264270 netHandle , err := netlink .NewHandleAt (nsHandle )
265271 if err != nil {
266272 log .Debugf ("failed to create netlink handle for container %s: %v" , containerName , err )
@@ -275,6 +281,10 @@ func (w *netlinkWatcher) run(ctx context.Context, registry *netlinkRegistry) {
275281 states = make (map [int ]ifaceSnapshot )
276282 }
277283
284+ // Query and apply netem info to snapshots
285+ netemInfos := queryNetemInfo (nsHandle )
286+ applyNetemToSnapshots (states , netemInfos )
287+
278288 var statsSamples map [int ]ifaceStatsSample
279289 if w .includeStats {
280290 statsSamples = make (map [int ]ifaceStatsSample , len (states ))
@@ -306,6 +316,14 @@ func (w *netlinkWatcher) run(ctx context.Context, registry *netlinkRegistry) {
306316 return
307317 }
308318
319+ // Set up netem polling interval (reuse stats interval or default to 1s)
320+ netemPollInterval := w .statsInterval
321+ if netemPollInterval == 0 {
322+ netemPollInterval = time .Second
323+ }
324+ netemTicker := time .NewTicker (netemPollInterval )
325+ defer netemTicker .Stop ()
326+
309327 var (
310328 ticker * time.Ticker
311329 tickerC <- chan time.Time
@@ -320,6 +338,8 @@ func (w *netlinkWatcher) run(ctx context.Context, registry *netlinkRegistry) {
320338 select {
321339 case <- tickerC :
322340 w .collectAndEmitStats (netHandle , states , statsSamples , registry )
341+ case <- netemTicker .C :
342+ w .pollNetemChanges (states , registry )
323343 case <- ctx .Done ():
324344 close (done )
325345
@@ -362,6 +382,17 @@ func (w *netlinkWatcher) processUpdate(
362382 delete (statsSamples , snapshot .Index )
363383 registry .emitInterfaceEvent (w .container , "delete" , snapshot )
364384 case unix .RTM_NEWLINK :
385+ // Query netem info for this interface
386+ netemInfos := queryNetemInfo (w .nsHandle )
387+ if info , ok := netemInfos [snapshot .Index ]; ok {
388+ snapshot .HasNetem = true
389+ snapshot .Delay = info .Delay
390+ snapshot .Jitter = info .Jitter
391+ snapshot .PacketLoss = info .PacketLoss
392+ snapshot .Rate = info .Rate
393+ snapshot .Corruption = info .Corruption
394+ }
395+
365396 if exists && snapshot .equal (previous ) {
366397 return
367398 }
@@ -381,6 +412,41 @@ func (w *netlinkWatcher) processUpdate(
381412 }
382413}
383414
415+ // pollNetemChanges checks for netem changes and emits update events.
416+ func (w * netlinkWatcher ) pollNetemChanges (
417+ states map [int ]ifaceSnapshot ,
418+ registry * netlinkRegistry ,
419+ ) {
420+ netemInfos := queryNetemInfo (w .nsHandle )
421+
422+ for idx , snapshot := range states {
423+ previous := snapshot
424+ info , hasNetem := netemInfos [idx ]
425+
426+ if hasNetem {
427+ snapshot .HasNetem = true
428+ snapshot .Delay = info .Delay
429+ snapshot .Jitter = info .Jitter
430+ snapshot .PacketLoss = info .PacketLoss
431+ snapshot .Rate = info .Rate
432+ snapshot .Corruption = info .Corruption
433+ } else if snapshot .HasNetem {
434+ // Netem was removed
435+ snapshot .HasNetem = false
436+ snapshot .Delay = ""
437+ snapshot .Jitter = ""
438+ snapshot .PacketLoss = 0
439+ snapshot .Rate = 0
440+ snapshot .Corruption = 0
441+ }
442+
443+ if ! snapshot .equal (previous ) {
444+ states [idx ] = snapshot
445+ registry .emitInterfaceEvent (w .container , "update" , snapshot )
446+ }
447+ }
448+ }
449+
384450func firstContainerName (container * clabruntime.GenericContainer ) string {
385451 if container == nil || len (container .Names ) == 0 {
386452 return ""
@@ -426,6 +492,25 @@ func interfaceAttributes(
426492 attributes ["name" ] = name
427493 }
428494
495+ // Add netem attributes if present
496+ if snapshot .HasNetem {
497+ if snapshot .Delay != "" {
498+ attributes ["netem_delay" ] = snapshot .Delay
499+ }
500+ if snapshot .Jitter != "" {
501+ attributes ["netem_jitter" ] = snapshot .Jitter
502+ }
503+ if snapshot .PacketLoss != 0 {
504+ attributes ["netem_loss" ] = strconv .FormatFloat (snapshot .PacketLoss , 'f' , 2 , 64 ) + "%"
505+ }
506+ if snapshot .Rate != 0 {
507+ attributes ["netem_rate" ] = strconv .Itoa (snapshot .Rate ) + "kbit"
508+ }
509+ if snapshot .Corruption != 0 {
510+ attributes ["netem_corruption" ] = strconv .FormatFloat (snapshot .Corruption , 'f' , 2 , 64 ) + "%"
511+ }
512+ }
513+
429514 return attributes
430515}
431516
@@ -547,6 +632,21 @@ func snapshotInterfaces(netHandle *netlink.Handle) (map[int]ifaceSnapshot, error
547632 return states , nil
548633}
549634
635+ // applyNetemToSnapshots applies netem information to interface snapshots.
636+ func applyNetemToSnapshots (states map [int ]ifaceSnapshot , netemInfos map [int ]netemInfo ) {
637+ for idx , info := range netemInfos {
638+ if snapshot , ok := states [idx ]; ok {
639+ snapshot .HasNetem = true
640+ snapshot .Delay = info .Delay
641+ snapshot .Jitter = info .Jitter
642+ snapshot .PacketLoss = info .PacketLoss
643+ snapshot .Rate = info .Rate
644+ snapshot .Corruption = info .Corruption
645+ states [idx ] = snapshot
646+ }
647+ }
648+ }
649+
550650func snapshotFromLink (link netlink.Link ) ifaceSnapshot {
551651 attrs := link .Attrs ()
552652
@@ -588,6 +688,13 @@ type ifaceSnapshot struct {
588688 TxBytes uint64
589689 RxPackets uint64
590690 TxPackets uint64
691+ // Netem fields
692+ HasNetem bool
693+ Delay string
694+ Jitter string
695+ PacketLoss float64
696+ Rate int
697+ Corruption float64
591698}
592699
593700func (s ifaceSnapshot ) equal (other ifaceSnapshot ) bool {
@@ -597,7 +704,13 @@ func (s ifaceSnapshot) equal(other ifaceSnapshot) bool {
597704 s .MTU == other .MTU &&
598705 s .MAC == other .MAC &&
599706 s .OperState == other .OperState &&
600- s .Type == other .Type
707+ s .Type == other .Type &&
708+ s .HasNetem == other .HasNetem &&
709+ s .Delay == other .Delay &&
710+ s .Jitter == other .Jitter &&
711+ s .PacketLoss == other .PacketLoss &&
712+ s .Rate == other .Rate &&
713+ s .Corruption == other .Corruption
601714}
602715
603716type ifaceStatsSample struct {
@@ -749,3 +862,82 @@ func deltaCounter(previous, current uint64) uint64 {
749862
750863 return current
751864}
865+
866+ const msPerSec = 1000
867+
868+ // queryNetemInfo queries the TC qdiscs in the namespace and returns a map
869+ // of interface index to netem data.
870+ func queryNetemInfo (nsHandle netns.NsHandle ) map [int ]netemInfo {
871+ result := make (map [int ]netemInfo )
872+
873+ tcnl , err := clabnetem .NewTC (int (nsHandle ))
874+ if err != nil {
875+ log .Debugf ("failed to open tc socket for netem query: %v" , err )
876+ return result
877+ }
878+ defer tcnl .Close ()
879+
880+ qdiscs , err := clabnetem .Impairments (tcnl )
881+ if err != nil {
882+ log .Debugf ("failed to query tc qdiscs: %v" , err )
883+ return result
884+ }
885+
886+ for idx := range qdiscs {
887+ qdisc := & qdiscs [idx ]
888+ if qdisc .Attribute .Kind != "netem" || qdisc .Netem == nil {
889+ continue
890+ }
891+
892+ info := netemInfoFromQdisc (qdisc )
893+ if info .hasValues () {
894+ result [int (qdisc .Ifindex )] = info
895+ }
896+ }
897+
898+ return result
899+ }
900+
901+ type netemInfo struct {
902+ Delay string
903+ Jitter string
904+ PacketLoss float64
905+ Rate int
906+ Corruption float64
907+ }
908+
909+ func (n netemInfo ) hasValues () bool {
910+ return n .Delay != "" || n .Jitter != "" || n .PacketLoss != 0 || n .Rate != 0 || n .Corruption != 0
911+ }
912+
913+ func netemInfoFromQdisc (qdisc * gotc.Object ) netemInfo {
914+ var info netemInfo
915+
916+ if qdisc .Netem == nil {
917+ return info
918+ }
919+
920+ if qdisc .Netem .Latency64 != nil && * qdisc .Netem .Latency64 != 0 {
921+ info .Delay = (time .Duration (* qdisc .Netem .Latency64 ) * time .Nanosecond ).String ()
922+ }
923+
924+ if qdisc .Netem .Jitter64 != nil && * qdisc .Netem .Jitter64 != 0 {
925+ info .Jitter = (time .Duration (* qdisc .Netem .Jitter64 ) * time .Nanosecond ).String ()
926+ }
927+
928+ if qdisc .Netem .Rate != nil && qdisc .Netem .Rate .Rate != 0 {
929+ info .Rate = int (qdisc .Netem .Rate .Rate * 8 / msPerSec )
930+ }
931+
932+ if qdisc .Netem .Corrupt != nil && qdisc .Netem .Corrupt .Probability != 0 {
933+ info .Corruption = math .Round ((float64 (qdisc .Netem .Corrupt .Probability )/
934+ float64 (math .MaxUint32 )* 100 )* 100 ) / 100
935+ }
936+
937+ if qdisc .Netem .Qopt .Loss != 0 {
938+ info .PacketLoss = math .Round (
939+ (float64 (qdisc .Netem .Qopt .Loss )/ float64 (math .MaxUint32 )* 100 )* 100 ) / 100
940+ }
941+
942+ return info
943+ }
0 commit comments