diff --git a/qdisc.go b/qdisc.go index 1cde43c94..5dfab91b5 100644 --- a/qdisc.go +++ b/qdisc.go @@ -12,6 +12,7 @@ const ( HANDLE_ROOT = 0xFFFFFFFF PRIORITY_MAP_LEN = 16 ) + const ( HANDLE_MIN_INGRESS = 0xFFFFFFF2 HANDLE_MIN_EGRESS = 0xFFFFFFF3 @@ -68,12 +69,27 @@ func HandleStr(handle uint32) string { } } +// Percentage2u32 converts a percentage (0-100) to a kernel format uint32 value. +// This is the inverse of u32ToPercentage. func Percentage2u32(percentage float32) uint32 { - // FIXME this is most likely not the best way to convert from % to uint32 - if percentage == 100 { + if percentage >= 100 { return math.MaxUint32 } - return uint32(math.MaxUint32 * (percentage / 100)) + if percentage <= 0 { + return 0 + } + + return uint32(float64(percentage) * math.MaxUint32 / 100) +} + +// u32ToPercentage converts a kernel format uint32 value back to a percentage. +// This is the inverse of Percentage2u32. +func u32ToPercentage(value uint32) float32 { + percentage := float32(float64(value) * 100 / float64(math.MaxUint32)) + if value < math.MaxUint32 && percentage == 100 { + return math.Nextafter32(100, 0) + } + return percentage } // PfifoFast is the default qdisc created by the kernel if one has not diff --git a/qdisc_linux.go b/qdisc_linux.go index 0a2a5891c..760df26bf 100644 --- a/qdisc_linux.go +++ b/qdisc_linux.go @@ -81,6 +81,28 @@ func NewNetem(attrs QdiscAttrs, nattrs NetemQdiscAttrs) *Netem { } } +// ToNetemQdiscAttrs converts the Netem struct back to human-readable NetemQdiscAttrs. +// This is useful when reading qdisc settings from the kernel, as the values stored +// in Netem are in kernel format (ticks for time, uint32 for percentages). +func (netem *Netem) ToNetemQdiscAttrs() NetemQdiscAttrs { + return NetemQdiscAttrs{ + Latency: tick2Time(netem.Latency), + DelayCorr: u32ToPercentage(netem.DelayCorr), + Limit: netem.Limit, + Loss: u32ToPercentage(netem.Loss), + LossCorr: u32ToPercentage(netem.LossCorr), + Gap: netem.Gap, + Duplicate: u32ToPercentage(netem.Duplicate), + DuplicateCorr: u32ToPercentage(netem.DuplicateCorr), + Jitter: tick2Time(netem.Jitter), + ReorderProb: u32ToPercentage(netem.ReorderProb), + ReorderCorr: u32ToPercentage(netem.ReorderCorr), + CorruptProb: u32ToPercentage(netem.CorruptProb), + CorruptCorr: u32ToPercentage(netem.CorruptCorr), + Rate64: netem.Rate64, + } +} + // QdiscDel will delete a qdisc from the system. // Equivalent to: `tc qdisc del $qdisc` func QdiscDel(qdisc Qdisc) error { diff --git a/qdisc_test.go b/qdisc_test.go index 9583c56d6..90d180b0f 100644 --- a/qdisc_test.go +++ b/qdisc_test.go @@ -4,6 +4,7 @@ package netlink import ( + "math" "testing" ) @@ -626,3 +627,130 @@ func TestIngressAddDel(t *testing.T) { t.Fatal("Failed to remove qdisc") } } + +// Tests the round-trip conversion of Netem attributes to ensure +// human-readable values are correctly preserved (fixes #480). +func TestNetemQdiscAttrsRoundTrip(t *testing.T) { + initClockMutex.Lock() + oldTickInUsec := tickInUsec + tickInUsec = 15.625 + initClockMutex.Unlock() + defer func() { + initClockMutex.Lock() + tickInUsec = oldTickInUsec + initClockMutex.Unlock() + }() + + nattrs := NetemQdiscAttrs{ + Latency: 5000, + DelayCorr: 12.5, + Limit: 2048, + Loss: 5.0, + LossCorr: 9.5, + Gap: 3, + Duplicate: 4.0, + DuplicateCorr: 7.0, + Jitter: 1300, + ReorderProb: 10.0, + ReorderCorr: 11.0, + CorruptProb: 3.0, + CorruptCorr: 2.0, + Rate64: 123456789, + } + netem := NewNetem(QdiscAttrs{}, nattrs) + + if netem.Latency != time2Tick(nattrs.Latency) { + t.Fatalf("kernel-format latency mismatch: got %d, want %d", netem.Latency, time2Tick(nattrs.Latency)) + } + if netem.DelayCorr != Percentage2u32(nattrs.DelayCorr) { + t.Fatalf("kernel-format delayCorr mismatch: got %d, want %d", netem.DelayCorr, Percentage2u32(nattrs.DelayCorr)) + } + if netem.Limit != nattrs.Limit { + t.Fatalf("kernel-format limit mismatch: got %d, want %d", netem.Limit, nattrs.Limit) + } + if netem.Loss != Percentage2u32(nattrs.Loss) { + t.Fatalf("kernel-format loss mismatch: got %d, want %d", netem.Loss, Percentage2u32(nattrs.Loss)) + } + if netem.LossCorr != Percentage2u32(nattrs.LossCorr) { + t.Fatalf("kernel-format lossCorr mismatch: got %d, want %d", netem.LossCorr, Percentage2u32(nattrs.LossCorr)) + } + if netem.Gap != nattrs.Gap { + t.Fatalf("kernel-format gap mismatch: got %d, want %d", netem.Gap, nattrs.Gap) + } + if netem.Duplicate != Percentage2u32(nattrs.Duplicate) { + t.Fatalf("kernel-format duplicate mismatch: got %d, want %d", netem.Duplicate, Percentage2u32(nattrs.Duplicate)) + } + if netem.DuplicateCorr != Percentage2u32(nattrs.DuplicateCorr) { + t.Fatalf("kernel-format duplicateCorr mismatch: got %d, want %d", netem.DuplicateCorr, Percentage2u32(nattrs.DuplicateCorr)) + } + if netem.Jitter != time2Tick(nattrs.Jitter) { + t.Fatalf("kernel-format jitter mismatch: got %d, want %d", netem.Jitter, time2Tick(nattrs.Jitter)) + } + if netem.ReorderProb != Percentage2u32(nattrs.ReorderProb) { + t.Fatalf("kernel-format reorderProb mismatch: got %d, want %d", netem.ReorderProb, Percentage2u32(nattrs.ReorderProb)) + } + if netem.ReorderCorr != Percentage2u32(nattrs.ReorderCorr) { + t.Fatalf("kernel-format reorderCorr mismatch: got %d, want %d", netem.ReorderCorr, Percentage2u32(nattrs.ReorderCorr)) + } + if netem.CorruptProb != Percentage2u32(nattrs.CorruptProb) { + t.Fatalf("kernel-format corruptProb mismatch: got %d, want %d", netem.CorruptProb, Percentage2u32(nattrs.CorruptProb)) + } + if netem.CorruptCorr != Percentage2u32(nattrs.CorruptCorr) { + t.Fatalf("kernel-format corruptCorr mismatch: got %d, want %d", netem.CorruptCorr, Percentage2u32(nattrs.CorruptCorr)) + } + if netem.Rate64 != nattrs.Rate64 { + t.Fatalf("kernel-format rate64 mismatch: got %d, want %d", netem.Rate64, nattrs.Rate64) + } + + human := netem.ToNetemQdiscAttrs() + if human.Latency != tick2Time(time2Tick(nattrs.Latency)) { + t.Fatalf("human-readable latency mismatch: got %d, want %d", human.Latency, tick2Time(time2Tick(nattrs.Latency))) + } + if math.Abs(float64(human.DelayCorr-nattrs.DelayCorr)) > 0.0001 { + t.Fatalf("human-readable delayCorr mismatch: got %f, want %f", human.DelayCorr, nattrs.DelayCorr) + } + if human.Limit != nattrs.Limit { + t.Fatalf("human-readable limit mismatch: got %d, want %d", human.Limit, nattrs.Limit) + } + if math.Abs(float64(human.Loss-nattrs.Loss)) > 0.0001 { + t.Fatalf("human-readable loss mismatch: got %f, want %f", human.Loss, nattrs.Loss) + } + if math.Abs(float64(human.LossCorr-nattrs.LossCorr)) > 0.0001 { + t.Fatalf("human-readable lossCorr mismatch: got %f, want %f", human.LossCorr, nattrs.LossCorr) + } + if human.Gap != nattrs.Gap { + t.Fatalf("human-readable gap mismatch: got %d, want %d", human.Gap, nattrs.Gap) + } + if math.Abs(float64(human.Duplicate-nattrs.Duplicate)) > 0.0001 { + t.Fatalf("human-readable duplicate mismatch: got %f, want %f", human.Duplicate, nattrs.Duplicate) + } + if math.Abs(float64(human.DuplicateCorr-nattrs.DuplicateCorr)) > 0.0001 { + t.Fatalf("human-readable duplicateCorr mismatch: got %f, want %f", human.DuplicateCorr, nattrs.DuplicateCorr) + } + if human.Jitter != tick2Time(time2Tick(nattrs.Jitter)) { + t.Fatalf("human-readable jitter mismatch: got %d, want %d", human.Jitter, tick2Time(time2Tick(nattrs.Jitter))) + } + if math.Abs(float64(human.ReorderProb-nattrs.ReorderProb)) > 0.0001 { + t.Fatalf("human-readable reorderProb mismatch: got %f, want %f", human.ReorderProb, nattrs.ReorderProb) + } + if math.Abs(float64(human.ReorderCorr-nattrs.ReorderCorr)) > 0.0001 { + t.Fatalf("human-readable reorderCorr mismatch: got %f, want %f", human.ReorderCorr, nattrs.ReorderCorr) + } + if math.Abs(float64(human.CorruptProb-nattrs.CorruptProb)) > 0.0001 { + t.Fatalf("human-readable corruptProb mismatch: got %f, want %f", human.CorruptProb, nattrs.CorruptProb) + } + if math.Abs(float64(human.CorruptCorr-nattrs.CorruptCorr)) > 0.0001 { + t.Fatalf("human-readable corruptCorr mismatch: got %f, want %f", human.CorruptCorr, nattrs.CorruptCorr) + } + if human.Rate64 != nattrs.Rate64 { + t.Fatalf("human-readable rate64 mismatch: got %d, want %d", human.Rate64, nattrs.Rate64) + } +} + +func TestU32ToPercentagePrecisionNearMax(t *testing.T) { + almostMax := uint32(math.MaxUint32 - 1) + got := u32ToPercentage(almostMax) + if got >= 100 { + t.Fatalf("u32ToPercentage(%d) must be < 100, got %f", almostMax, got) + } +}