Skip to content

Commit

Permalink
Merge pull request #4453 from gizmoguy/ttl-expired
Browse files Browse the repository at this point in the history
Add support for sending ICMP v4 and v6 time exceeded messages.
  • Loading branch information
anarkiwi committed Jan 26, 2024
2 parents e986dc7 + 8d24afc commit d7acc37
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 13 deletions.
87 changes: 87 additions & 0 deletions clib/mininet_test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
IPV4_ETH = 0x0800
IPV6_ETH = 0x86DD
FPING_ARGS = "-s -T 1 -A"
TRACEROUTE_ARGS = "--max-hops=1"


class FaucetTestBase(unittest.TestCase):
Expand Down Expand Up @@ -101,6 +102,8 @@ class FaucetTestBase(unittest.TestCase):
FPING_ARGS_SHORT = " ".join((FPING_ARGS, "-i10 -p100 -t100"))
FPINGS_ARGS_ONE = " ".join(("fping", FPING_ARGS, "-t100 -c 1"))

TRACEROUTE_ARGS = TRACEROUTE_ARGS

REQUIRES_METERS = False
REQUIRES_METADATA = False

Expand Down Expand Up @@ -3143,6 +3146,90 @@ def retry_net_ping(self, hosts=None, required_loss=0, retries=3, timeout=2):
time.sleep(1)
self.fail("ping %f loss > required loss %f" % (loss, required_loss))

def _ip_traceroute(
self,
host,
vip,
dst,
retries,
timeout=500,
traceroute_bin="traceroute",
protocol="udp",
intf=None,
expected_result=True,
):
"""Traceroute to a destination from a host"""
if intf is None:
intf = host.defaultIntf()
good_traceroute = r"1 %s" % (vip)
traceroute_cmd = "%s %s --interface=%s --%s %s" % (
traceroute_bin,
self.TRACEROUTE_ARGS,
intf,
protocol,
dst,
)
pause = timeout / 1e3
for _ in range(retries):
traceroute_out = host.cmd(traceroute_cmd)
traceroute_result = bool(re.search(good_traceroute, traceroute_out))
if traceroute_result:
break
time.sleep(pause)
pause *= 2
self.assertEqual(
traceroute_result,
expected_result,
msg="%s %s: %s" % (traceroute_cmd, traceroute_result, traceroute_out),
)

def ipv4_traceroute(
self,
host,
vip,
dst,
retries=3,
timeout=1000,
protocol="udp",
intf=None,
expected_result=True,
):
"""Traceroute to an IPv4 destination from a host"""
return self._ip_traceroute(
host,
vip,
dst,
retries,
timeout=timeout,
protocol=protocol,
intf=intf,
expected_result=expected_result,
)

def ipv6_traceroute(
self,
host,
vip,
dst,
retries=3,
timeout=1000,
protocol="udp",
intf=None,
expected_result=True,
):
"""Traceroute to an IPv6 destination from a host"""
return self._ip_traceroute(
host,
vip,
dst,
retries,
timeout=timeout,
traceroute_bin="traceroute6",
protocol=protocol,
intf=intf,
expected_result=expected_result,
)

@staticmethod
def tcp_port_free(host, port, ipv=4):
listen_out = host.cmd(mininet_test_util.tcp_listening_cmd(port, ipv))
Expand Down
28 changes: 24 additions & 4 deletions faucet/valve.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,7 @@ def _control_plane_handler(now, pkt_meta, route_manager):
if (
pkt_meta.eth_dst == pkt_meta.vlan.faucet_mac
or not valve_packet.mac_addr_is_unicast(pkt_meta.eth_dst)
or pkt_meta.reason == valve_of.ofp.OFPR_INVALID_TTL
):
return route_manager.control_plane_handler(now, pkt_meta)
return []
Expand Down Expand Up @@ -1177,11 +1178,21 @@ def learn_host(self, now, pkt_meta, other_valves):
return []

def parse_rcv_packet(
self, in_port, vlan_vid, eth_type, data, orig_len, pkt, eth_pkt, vlan_pkt
self,
reason,
in_port,
vlan_vid,
eth_type,
data,
orig_len,
pkt,
eth_pkt,
vlan_pkt,
):
"""Parse a received packet into a PacketMeta instance.
Args:
reason (int): reason for packet in message.
in_port (int): port packet was received on.
vlan_vid (int): VLAN VID of port packet was received on.
eth_type (int): Ethernet type of packet.
Expand All @@ -1200,6 +1211,7 @@ def parse_rcv_packet(
vlan = self.dp.vlans[vlan_vid]
port = self.dp.ports[in_port]
pkt_meta = valve_packet.PacketMeta(
reason,
data,
orig_len,
pkt,
Expand Down Expand Up @@ -1227,7 +1239,7 @@ def parse_pkt_meta(self, msg):
self.logger.info("got packet in with unknown cookie %s" % msg.cookie)
return None
# Drop any packet we didn't specifically ask for
if msg.reason != valve_of.ofp.OFPR_ACTION:
if msg.reason not in (valve_of.ofp.OFPR_ACTION, valve_of.ofp.OFPR_INVALID_TTL):
return None
if not msg.match:
return None
Expand Down Expand Up @@ -1255,7 +1267,15 @@ def parse_pkt_meta(self, msg):
self.logger.info("packet for unknown VLAN %u" % vlan_vid)
return None
pkt_meta = self.parse_rcv_packet(
in_port, vlan_vid, eth_type, data, msg.total_len, pkt, eth_pkt, vlan_pkt
msg.reason,
in_port,
vlan_vid,
eth_type,
data,
msg.total_len,
pkt,
eth_pkt,
vlan_pkt,
)
if not valve_packet.mac_addr_is_unicast(pkt_meta.eth_src):
self.logger.info(
Expand Down Expand Up @@ -1411,7 +1431,7 @@ def router_rcv_packet(self, now, pkt_meta):
ofmsgs = []
if control_plane_ofmsgs:
ofmsgs.extend(control_plane_ofmsgs)
else:
elif pkt_meta.reason == valve_of.ofp.OFPR_ACTION:
ofmsgs.extend(route_manager.add_host_fib_route_from_pkt(now, pkt_meta))
# No CPN activity, run resolver.
ofmsgs.extend(
Expand Down
2 changes: 1 addition & 1 deletion faucet/valve_of.py
Original file line number Diff line number Diff line change
Expand Up @@ -1332,7 +1332,7 @@ def faucet_async(
"""Return async message config for FAUCET/Gauge"""
packet_in_mask = 0
if packet_in:
packet_in_mask = 1 << ofp.OFPR_ACTION
packet_in_mask = 1 << ofp.OFPR_ACTION | 1 << ofp.OFPR_INVALID_TTL
port_status_mask = 0
if port_status:
port_status_mask = (
Expand Down
59 changes: 59 additions & 0 deletions faucet/valve_packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,32 @@ def echo_reply(vid, eth_src, eth_dst, src_ip, dst_ip, data):
return pkt


def time_exceeded(vid, eth_src, eth_dst, src_ip, dst_ip, data):
"""Return an ICMP time-to-live exceeded packet.
Args:
vid (int or None): VLAN VID to use (or None).
eth_src (str): Ethernet source address.
eth_dst (str): destination Ethernet MAC address.
src_ip (ipaddress.IPv4Address): source IPv4 address.
dst_ip (ipaddress.IPv4Address): destination IPv4 address.
data (bytes): payload for reply.
Returns:
ryu.lib.packet.icmp: serialized ICMP time-to-live exceeded packet.
"""
pkt = build_pkt_header(vid, eth_src, eth_dst, valve_of.ether.ETH_TYPE_IP)
ipv4_pkt = ipv4.ipv4(dst=dst_ip, src=src_ip, proto=valve_of.inet.IPPROTO_ICMP)
pkt.add_protocol(ipv4_pkt)
icmp_pkt = icmp.icmp(
type_=icmp.ICMP_TIME_EXCEEDED,
code=icmp.ICMP_TTL_EXPIRED_CODE,
data=icmp.TimeExceeded(data=data),
)
pkt.add_protocol(icmp_pkt)
pkt.serialize()
return pkt


@functools.lru_cache(maxsize=1024)
def ipv6_link_eth_mcast(dst_ip):
"""Return an Ethernet multicast address from an IPv6 address.
Expand Down Expand Up @@ -695,6 +721,28 @@ def icmpv6_echo_reply(vid, eth_src, eth_dst, src_ip, dst_ip, hop_limit, id_, seq
return pkt


def icmpv6_time_exceeded(vid, eth_src, eth_dst, src_ip, dst_ip, data):
r"""Return IPv6 ICMP hop limit exceeded packet.
Args:
vid (int or None): VLAN VID to use (or None).
eth_src (str): source Ethernet MAC address.
eth_dst (str): destination Ethernet MAC address.
src_ip (ipaddress.IPv6Address): source IPv6 address.
dst_ip (ipaddress.IPv6Address): destination IPv6 address.
data (bytes): payload for reply.
Returns:
ryu.lib.packet.ethernet: Serialized IPv6 ICMP time-to-live exceeded packet.
"""
pkt = build_pkt_header(vid, eth_src, eth_dst, valve_of.ether.ETH_TYPE_IPV6)
ipv6_reply = ipv6.ipv6(src=src_ip, dst=dst_ip, nxt=valve_of.inet.IPPROTO_ICMPV6)
pkt.add_protocol(ipv6_reply)
icmpv6_pkt = icmpv6.icmpv6(type_=icmpv6.ICMPV6_TIME_EXCEEDED, data=data)
pkt.add_protocol(icmpv6_pkt)
pkt.serialize()
return pkt


def router_advert(vid, eth_src, eth_dst, src_ip, dst_ip, vips, pi_flags=0x6):
"""Return IPv6 ICMP Router Advert.
Expand Down Expand Up @@ -744,6 +792,7 @@ class PacketMeta:
__slots__ = [
"data",
"orig_len",
"reason",
"pkt",
"eth_pkt",
"vlan_pkt",
Expand Down Expand Up @@ -776,6 +825,7 @@ class PacketMeta:

def __init__(
self,
reason,
data,
orig_len,
pkt,
Expand All @@ -787,6 +837,7 @@ def __init__(
eth_dst,
eth_type,
):
self.reason = reason
self.data = data
self.orig_len = orig_len
self.pkt = pkt
Expand Down Expand Up @@ -870,3 +921,11 @@ def reparse_ip(self, payload=0):
def packet_complete(self):
"""True if we have the complete packet."""
return len(self.data) == self.orig_len

def l3_offset(self):
"""Return offset to where layer 3 header (IPv4/IPv6) starts"""
if self.vlan_pkt is not None:
return ETH_VLAN_HEADER_SIZE
if self.eth_pkt is not None:
return ETH_HEADER_SIZE
return None

0 comments on commit d7acc37

Please sign in to comment.