Skip to content

Asymmetric link

Two hosts on a single switch, each link carrying delay in only one direction. The opposite direction is unshaped, so the resulting ping RTT is the sum of the two shaped one-ways.

What you'll see

h1 ping h2 reports an RTT of approximately 220 ms with sub-ms jitter — measured RTT in the integration test was min/avg/max/mdev = 220.981/221.288/222.048/0.396 ms.

Topology

examples/asymmetric_link/topology.py:

"""Two hosts plus one switch with asymmetric link delays.

The h1↔s1 link adds 200 ms egress delay at h1 (h1→s1 direction); the
h2↔s1 link adds 20 ms egress delay at s1 (s1→h2 direction). End-to-end
one-way h1→h2 delay is therefore ~220 ms; reverse h2→h1 is ~0 ms; ping
RTT from h1 to h2 is ~220 ms.

Run with:

    sudo p4net examples/asymmetric_link/topology.py

Then in the shell:

    h1 ping h2 count=10 timeout=2
"""

from __future__ import annotations

from pathlib import Path

from p4net import Network
from p4net.topo import Topology

HERE = Path(__file__).resolve().parent

topology = Topology()
h1 = topology.add_host("h1", ip="10.0.0.1/24", mac="00:00:00:00:00:01")
h2 = topology.add_host("h2", ip="10.0.0.2/24", mac="00:00:00:00:00:02")
s1 = topology.add_switch("s1", p4_src=HERE / "asymmetric.p4")
# h1↔s1: shape only the h1→s1 direction (egress at h1).
topology.add_link(h1, s1, port_b=1, delay_a_to_b="200ms")
# h2↔s1: shape only the s1→h2 direction. With a=h2 and b=s1, "b→a" =
# "s1→h2", so delay_b_to_a applies in that direction.
topology.add_link(h2, s1, port_b=2, delay_b_to_a="20ms")


def setup(net: Network) -> None:
    """Pre-seed static ARP."""
    h1 = net.host("h1")
    h2 = net.host("h2")
    h1.exec(
        [
            "ip",
            "neigh",
            "replace",
            "10.0.0.2",
            "lladdr",
            "00:00:00:00:00:02",
            "dev",
            "h1-eth0",
            "nud",
            "permanent",
        ]
    )
    h2.exec(
        [
            "ip",
            "neigh",
            "replace",
            "10.0.0.1",
            "lladdr",
            "00:00:00:00:00:01",
            "dev",
            "h2-eth0",
            "nud",
            "permanent",
        ]
    )


if __name__ == "__main__":
    from p4net.cli.main import main

    raise SystemExit(main([__file__]))

The h1↔s1 link has delay_a_to_b="200ms" — applied to the a side (h1's namespace). The h2↔s1 link has delay_b_to_a="20ms" — applied to the b side (s1's root-namespace veth toward h2).

Direction Path Shaped delay
h1 → s1 h1's egress veth 200 ms
s1 → h2 s1's egress veth 20 ms
h2 → s1 (none) 0 ms
s1 → h1 (none) 0 ms

End-to-end one-way h1→h2: 200 + 20 = 220 ms. Reverse h2→h1: 0 ms. Ping RTT (h1→h2 echo + h2→h1 reply): 220 ms.

P4 program

examples/asymmetric_link/asymmetric.p4:

#include <core.p4>
#include <v1model.p4>

header ethernet_t {
    bit<48> dstAddr;
    bit<48> srcAddr;
    bit<16> etherType;
}

struct headers {
    ethernet_t ethernet;
}

struct metadata {}

parser MyParser(packet_in pkt, out headers hdr, inout metadata meta,
                inout standard_metadata_t std) {
    state start {
        pkt.extract(hdr.ethernet);
        transition accept;
    }
}

control MyVerifyChecksum(inout headers hdr, inout metadata meta) { apply {} }

control MyIngress(inout headers hdr, inout metadata meta,
                  inout standard_metadata_t std) {
    apply {
        if (std.ingress_port == 1) {
            std.egress_spec = 2;
        } else if (std.ingress_port == 2) {
            std.egress_spec = 1;
        } else {
            mark_to_drop(std);
        }
    }
}

control MyEgress(inout headers hdr, inout metadata meta,
                 inout standard_metadata_t std) { apply {} }

control MyComputeChecksum(inout headers hdr, inout metadata meta) { apply {} }

control MyDeparser(packet_out pkt, in headers hdr) {
    apply {
        pkt.emit(hdr.ethernet);
    }
}

V1Switch(MyParser(), MyVerifyChecksum(), MyIngress(), MyEgress(),
         MyComputeChecksum(), MyDeparser()) main;

Identical port-swap pipeline as Quick start. The asymmetry lives entirely in the link impairment, not the dataplane.

Run it

sudo p4net examples/asymmetric_link/topology.py
p4net> h1 ping h2 5 3
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=222 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=221 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=221 ms
64 bytes from 10.0.0.2: icmp_seq=4 ttl=64 time=221 ms
64 bytes from 10.0.0.2: icmp_seq=5 ttl=64 time=221 ms

--- 10.0.0.2 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4126ms
rtt min/avg/max/mdev = 220.981/221.288/222.048/0.396 ms

(That's the literal output captured during phase-12 verification.)

tc qdisc show inside h1's namespace confirms which side carries the netem queue:

p4net> h1 cmd tc qdisc show dev h1-eth0
qdisc netem 8001: root ... delay 200ms

What's interesting

  • Direction semantics are fixed by (a, b) ordering. Link(h1, s1, delay_a_to_b="200ms") shapes h1→s1. If you swap the order to Link(s1, h1, ...), the same delay_a_to_b would shape s1→h1 instead. The add_link(a, b, ...) argument order is the source of truth.
  • Symmetric and asymmetric on the same parameter is rejected at construction. You can't say delay="50ms" plus delay_a_to_b="100ms". Pick one or the other for any given parameter, then mix freely across parameters (e.g. symmetric bandwidth with asymmetric delay).

Variations to try

  • Set loss_pct_a_to_b=50.0 instead of delay and observe a 50% packet-loss matrix in pingall.
  • Set delay_a_to_b="200ms" and delay_b_to_a="200ms" to recover symmetric 400 ms RTT.
  • Combine asymmetric delay with symmetric bandwidth shaping (bandwidth="1mbit") to model a typical residential link.