X-Git-Url: https://gerrit.fd.io/r/gitweb?a=blobdiff_plain;f=test%2Ftest_wireguard.py;h=e809daec1636cc89d15cf5b764e3acc15b96b5e5;hb=4941afb4f96a20df7dc8b6688f7921a3d713b77d;hp=7395402e27f7170e7da2ea348a3821f1fa9902a2;hpb=44ec846f4ad1c11cc596c9fa6b73284511131ed4;p=vpp.git diff --git a/test/test_wireguard.py b/test/test_wireguard.py index 7395402e27f..e809daec163 100644 --- a/test/test_wireguard.py +++ b/test/test_wireguard.py @@ -4,13 +4,15 @@ import datetime import base64 import os +import struct from hashlib import blake2s -from scapy.packet import Packet +from config import config from scapy.packet import Raw -from scapy.layers.l2 import Ether, ARP +from scapy.layers.l2 import Ether from scapy.layers.inet import IP, UDP from scapy.layers.inet6 import IPv6 +from scapy.layers.vxlan import VXLAN from scapy.contrib.wireguard import ( Wireguard, WireguardResponse, @@ -28,19 +30,19 @@ from cryptography.hazmat.primitives.serialization import ( PublicFormat, NoEncryption, ) -from cryptography.hazmat.primitives.hashes import BLAKE2s, Hash -from cryptography.hazmat.primitives.hmac import HMAC -from cryptography.hazmat.backends import default_backend from noise.connection import NoiseConnection, Keypair from Crypto.Cipher import ChaCha20_Poly1305 from Crypto.Random import get_random_bytes -from vpp_ipip_tun_interface import VppIpIpTunInterface from vpp_interface import VppInterface +from vpp_pg_interface import is_ipv6_misc from vpp_ip_route import VppIpRoute, VppRoutePath +from vpp_l2 import VppBridgeDomain, VppBridgeDomainPort +from vpp_vxlan_tunnel import VppVxlanTunnel from vpp_object import VppObject from vpp_papi import VppEnum +from asfframework import tag_run_solo, tag_fixme_vpp_debug from framework import VppTestCase from re import compile import unittest @@ -78,6 +80,9 @@ class VppWgInterface(VppInterface): self.private_key = X25519PrivateKey.generate() self.public_key = self.private_key.public_key() + # cookie related params + self.cookie_key = blake2s(b"cookie--" + self.public_key_bytes()).digest() + def public_key_bytes(self): return public_key_bytes(self.public_key) @@ -134,18 +139,16 @@ class VppWgInterface(VppInterface): return "wireguard-%d" % self._sw_if_index -def find_route(test, prefix, is_ip6, table_id=0): - routes = test.vapi.ip_route_dump(table_id, is_ip6) - - for e in routes: - if table_id == e.route.table_id and str(e.route.prefix) == str(prefix): - return True - return False - - NOISE_HANDSHAKE_NAME = b"Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s" NOISE_IDENTIFIER_NAME = b"WireGuard v1 zx2c4 Jason@zx2c4.com" +HANDSHAKE_COUNTING_INTERVAL = 0.5 +UNDER_LOAD_INTERVAL = 1.0 +HANDSHAKE_NUM_PER_PEER_UNTIL_UNDER_LOAD = 40 +HANDSHAKE_NUM_BEFORE_RATELIMITING = 5 + +HANDSHAKE_JITTER = 0.5 + class VppWgPeer(VppObject): def __init__(self, test, itf, endpoint, port, allowed_ips, persistent_keepalive=15): @@ -163,10 +166,16 @@ class VppWgPeer(VppObject): # cookie related params self.cookie_key = blake2s(b"cookie--" + self.public_key_bytes()).digest() self.last_sent_cookie = None + self.last_mac1 = None + self.last_received_cookie = None self.noise = NoiseConnection.from_name(NOISE_HANDSHAKE_NAME) - def add_vpp_config(self, is_ip6=False): + def change_endpoint(self, endpoint, port): + self.endpoint = endpoint + self.port = port + + def add_vpp_config(self): rv = self._test.vapi.wireguard_peer_add( peer={ "public_key": self.public_key_bytes(), @@ -196,17 +205,19 @@ class VppWgPeer(VppObject): peers = self._test.vapi.wireguard_peers_dump() for p in peers: + # "::" endpoint will be returned as "0.0.0.0" in peer's details + endpoint = "0.0.0.0" if self.endpoint == "::" else self.endpoint if ( p.peer.public_key == self.public_key_bytes() and p.peer.port == self.port - and str(p.peer.endpoint) == self.endpoint + and str(p.peer.endpoint) == endpoint and p.peer.sw_if_index == self.itf.sw_if_index and len(self.allowed_ips) == p.peer.n_allowed_ips ): self.allowed_ips.sort() p.peer.allowed_ips.sort() - for (a1, a2) in zip(self.allowed_ips, p.peer.allowed_ips): + for a1, a2 in zip(self.allowed_ips, p.peer.allowed_ips): if str(a1) != str(a2): return False return True @@ -226,6 +237,9 @@ class VppWgPeer(VppObject): / UDP(sport=self.port, dport=self.itf.port) ) + def noise_reset(self): + self.noise = NoiseConnection.from_name(NOISE_HANDSHAKE_NAME) + def noise_init(self, public_key=None): self.noise.set_prologue(NOISE_IDENTIFIER_NAME) self.noise.set_psks(psk=bytes(bytearray(32))) @@ -293,6 +307,27 @@ class VppWgPeer(VppObject): return cookie_reply + def consume_cookie(self, p, is_ip6=False): + self.verify_header(p, is_ip6) + + cookie_reply = Wireguard(p[Raw]) + + self._test.assertEqual(cookie_reply[Wireguard].message_type, 3) + self._test.assertEqual(cookie_reply[Wireguard].reserved_zero, 0) + self._test.assertEqual( + cookie_reply[WireguardCookieReply].receiver_index, self.receiver_index + ) + + # collect info from cookie reply + nonce = cookie_reply[WireguardCookieReply].nonce + encrypted_cookie = cookie_reply[WireguardCookieReply].encrypted_cookie + ciphertext, tag = encrypted_cookie[:16], encrypted_cookie[16:] + + # decrypt cookie data + cipher = ChaCha20_Poly1305.new(key=self.itf.cookie_key, nonce=nonce) + cipher.update(self.last_mac1) + self.last_received_cookie = cipher.decrypt_and_verify(ciphertext, tag) + def mk_handshake(self, tx_itf, is_ip6=False, public_key=None): self.noise.set_as_initiator() self.noise_init(public_key) @@ -320,10 +355,19 @@ class VppWgPeer(VppObject): # generate the mac1 hash mac_key = blake2s(b"mac1----" + self.itf.public_key_bytes()).digest() - p[WireguardInitiation].mac1 = blake2s( - bytes(p)[0:116], digest_size=16, key=mac_key - ).digest() - p[WireguardInitiation].mac2 = bytearray(16) + mac1 = blake2s(bytes(p)[0:116], digest_size=16, key=mac_key).digest() + p[WireguardInitiation].mac1 = mac1 + self.last_mac1 = mac1 + + # generate the mac2 hash + if self.last_received_cookie: + mac2 = blake2s( + bytes(p)[0:132], digest_size=16, key=self.last_received_cookie + ).digest() + p[WireguardInitiation].mac2 = mac2 + self.last_received_cookie = None + else: + p[WireguardInitiation].mac2 = bytearray(16) p = self.mk_tunnel_header(tx_itf, is_ip6) / p @@ -333,12 +377,13 @@ class VppWgPeer(VppObject): if is_ip6 is False: self._test.assertEqual(p[IP].src, self.itf.src) self._test.assertEqual(p[IP].dst, self.endpoint) + self._test.assert_packet_checksums_valid(p) else: self._test.assertEqual(p[IPv6].src, self.itf.src) self._test.assertEqual(p[IPv6].dst, self.endpoint) + self._test.assert_packet_checksums_valid(p, False) self._test.assertEqual(p[UDP].sport, self.itf.port) self._test.assertEqual(p[UDP].dport, self.port) - self._test.assert_packet_checksums_valid(p) def consume_init(self, p, tx_itf, is_ip6=False, is_mac2=False): self.noise.set_as_responder() @@ -384,6 +429,7 @@ class VppWgPeer(VppObject): ) mac1 = blake2s(bytes(resp)[:-32], digest_size=16, key=mac_key).digest() resp[WireguardResponse].mac1 = mac1 + self.last_mac1 = mac1 resp = self.mk_tunnel_header(tx_itf, is_ip6) / resp self._test.assertTrue(self.noise.handshake_finished) @@ -423,20 +469,22 @@ class VppWgPeer(VppObject): def encrypt_transport(self, p): return self.noise.encrypt(bytes(p)) - def validate_encapped(self, rxs, tx, is_ip6=False): + def validate_encapped(self, rxs, tx, is_tunnel_ip6=False, is_transport_ip6=False): + ret_rxs = [] for rx in rxs: - if is_ip6 is False: - rx = IP(self.decrypt_transport(rx)) - - # chech the oringial packet is present + rx = self.decrypt_transport(rx, is_tunnel_ip6) + if is_transport_ip6 is False: + rx = IP(rx) + # check the original packet is present self._test.assertEqual(rx[IP].dst, tx[IP].dst) self._test.assertEqual(rx[IP].ttl, tx[IP].ttl - 1) else: - rx = IPv6(self.decrypt_transport(rx)) - - # chech the oringial packet is present + rx = IPv6(rx) + # check the original packet is present self._test.assertEqual(rx[IPv6].dst, tx[IPv6].dst) - self._test.assertEqual(rx[IPv6].ttl, tx[IPv6].ttl - 1) + self._test.assertEqual(rx[IPv6].hlim, tx[IPv6].hlim - 1) + ret_rxs.append(rx) + return ret_rxs def want_events(self): self._test.vapi.want_wireguard_peer_events( @@ -452,6 +500,16 @@ class VppWgPeer(VppObject): self._test.assertEqual(rv.peer_index, self.index) +def is_handshake_init(p): + wg_p = Wireguard(p[Raw]) + + return wg_p[Wireguard].message_type == 1 + + +@unittest.skipIf( + "wireguard" in config.excluded_plugins, "Exclude Wireguard plugin tests" +) +@tag_run_solo class TestWg(VppTestCase): """Wireguard Test Case""" @@ -471,6 +529,8 @@ class TestWg(VppTestCase): peer6_out_err = wg6_output_node_name + "Peer error" cookie_dec4_err = wg4_input_node_name + "Failed during Cookie decryption" cookie_dec6_err = wg6_input_node_name + "Failed during Cookie decryption" + ratelimited4_err = wg4_input_node_name + "Handshake ratelimited" + ratelimited6_err = wg6_input_node_name + "Handshake ratelimited" @classmethod def setUpClass(cls): @@ -508,6 +568,31 @@ class TestWg(VppTestCase): self.base_cookie_dec6_err = self.statistics.get_err_counter( self.cookie_dec6_err ) + self.base_ratelimited4_err = self.statistics.get_err_counter( + self.ratelimited4_err + ) + self.base_ratelimited6_err = self.statistics.get_err_counter( + self.ratelimited6_err + ) + + def send_and_assert_no_replies_ignoring_init( + self, intf, pkts, remark="", timeout=None + ): + self.pg_send(intf, pkts) + + def _filter_out_fn(p): + return is_ipv6_misc(p) or is_handshake_init(p) + + try: + if not timeout: + timeout = 1 + for i in self.pg_interfaces: + i.assert_nothing_captured( + timeout=timeout, remark=remark, filter_out_fn=_filter_out_fn + ) + timeout = 0.1 + finally: + pass def test_wg_interface(self): """Simple interface creation""" @@ -592,6 +677,9 @@ class TestWg(VppTestCase): self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) if is_resp: + # skip the first automatic handshake + self.pg1.get_capture(1, timeout=HANDSHAKE_JITTER) + # prepare and send a handshake initiation # expect the peer to send a handshake response init = peer_1.mk_handshake(self.pg1, is_ip6=is_ip6) @@ -646,11 +734,99 @@ class TestWg(VppTestCase): """Send cookie on handshake response (v6)""" self._test_wg_send_cookie_tmpl(is_resp=True, is_ip6=True) - def test_wg_peer_resp(self): - """Send handshake response""" + def _test_wg_receive_cookie_tmpl(self, is_resp, is_ip6): port = 12323 - # Create interfaces + # create wg interface + if is_ip6: + wg0 = VppWgInterface(self, self.pg1.local_ip6, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip6() + else: + wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip4() + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # create a peer + if is_ip6: + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip6, port + 1, ["1::3:0/112"] + ).add_vpp_config() + else: + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip4, port + 1, ["10.11.3.0/24"] + ).add_vpp_config() + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) + + if is_resp: + # wait for the peer to send a handshake initiation + rxs = self.pg1.get_capture(1, timeout=2) + # prepare and send a bunch of handshake responses + # expect to switch to under load state + resp = peer_1.consume_init(rxs[0], self.pg1, is_ip6=is_ip6) + txs = [resp] * HANDSHAKE_NUM_PER_PEER_UNTIL_UNDER_LOAD + rxs = self.send_and_expect_some(self.pg1, txs, self.pg1) + # reset noise to be able to turn into initiator later + peer_1.noise_reset() + else: + # skip the first automatic handshake + self.pg1.get_capture(1, timeout=HANDSHAKE_JITTER) + + # prepare and send a bunch of handshake initiations + # expect to switch to under load state + init = peer_1.mk_handshake(self.pg1, is_ip6=is_ip6) + txs = [init] * HANDSHAKE_NUM_PER_PEER_UNTIL_UNDER_LOAD + rxs = self.send_and_expect_some(self.pg1, txs, self.pg1) + + # expect the peer to send a cookie reply + peer_1.consume_cookie(rxs[-1], is_ip6=is_ip6) + + # prepare and send a handshake initiation with wrong mac2 + # expect a cookie reply + init = peer_1.mk_handshake(self.pg1, is_ip6=is_ip6) + init.mac2 = b"1234567890" + rxs = self.send_and_expect(self.pg1, [init], self.pg1) + peer_1.consume_cookie(rxs[0], is_ip6=is_ip6) + + # prepare and send a handshake initiation with correct mac2 + # expect a handshake response + init = peer_1.mk_handshake(self.pg1, is_ip6=is_ip6) + rxs = self.send_and_expect(self.pg1, [init], self.pg1) + + # verify the response + peer_1.consume_response(rxs[0], is_ip6=is_ip6) + + # clear up under load state + self.sleep(UNDER_LOAD_INTERVAL) + + # remove configs + peer_1.remove_vpp_config() + wg0.remove_vpp_config() + + def test_wg_receive_cookie_on_init_v4(self): + """Receive cookie on handshake initiation (v4)""" + self._test_wg_receive_cookie_tmpl(is_resp=False, is_ip6=False) + + def test_wg_receive_cookie_on_init_v6(self): + """Receive cookie on handshake initiation (v6)""" + self._test_wg_receive_cookie_tmpl(is_resp=False, is_ip6=True) + + def test_wg_receive_cookie_on_resp_v4(self): + """Receive cookie on handshake response (v4)""" + self._test_wg_receive_cookie_tmpl(is_resp=True, is_ip6=False) + + def test_wg_receive_cookie_on_resp_v6(self): + """Receive cookie on handshake response (v6)""" + self._test_wg_receive_cookie_tmpl(is_resp=True, is_ip6=True) + + def test_wg_under_load_interval(self): + """Under load interval""" + port = 12323 + + # create wg interface wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() wg0.admin_up() wg0.config_ip4() @@ -658,180 +834,487 @@ class TestWg(VppTestCase): self.pg_enable_capture(self.pg_interfaces) self.pg_start() + # create a peer peer_1 = VppWgPeer( self, wg0, self.pg1.remote_ip4, port + 1, ["10.11.3.0/24"] ).add_vpp_config() self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) - r1 = VppIpRoute( - self, "10.11.3.0", 24, [VppRoutePath("10.11.3.1", wg0.sw_if_index)] - ).add_vpp_config() + # skip the first automatic handshake + self.pg1.get_capture(1, timeout=HANDSHAKE_JITTER) - # wait for the peer to send a handshake - rx = self.pg1.get_capture(1, timeout=2) + # prepare and send a bunch of handshake initiations + # expect to switch to under load state + init = peer_1.mk_handshake(self.pg1) + txs = [init] * HANDSHAKE_NUM_PER_PEER_UNTIL_UNDER_LOAD + rxs = self.send_and_expect_some(self.pg1, txs, self.pg1) - # consume the handshake in the noise protocol and - # generate the response - resp = peer_1.consume_init(rx[0], self.pg1) + # expect the peer to send a cookie reply + peer_1.consume_cookie(rxs[-1]) - # send the response, get keepalive - rxs = self.send_and_expect(self.pg1, [resp], self.pg1) + # sleep till the next counting interval + # expect under load state is still active + self.sleep(HANDSHAKE_COUNTING_INTERVAL) - for rx in rxs: - b = peer_1.decrypt_transport(rx) - self.assertEqual(0, len(b)) + # prepare and send a handshake initiation with wrong mac2 + # expect a cookie reply + init = peer_1.mk_handshake(self.pg1) + init.mac2 = b"1234567890" + rxs = self.send_and_expect(self.pg1, [init], self.pg1) + peer_1.consume_cookie(rxs[0]) - # send a packets that are routed into the tunnel - p = ( - Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) - / IP(src=self.pg0.remote_ip4, dst="10.11.3.2") - / UDP(sport=555, dport=556) - / Raw(b"\x00" * 80) - ) + # sleep till the end of being under load + # expect under load state is over + self.sleep(UNDER_LOAD_INTERVAL - HANDSHAKE_COUNTING_INTERVAL) - rxs = self.send_and_expect(self.pg0, p * 255, self.pg1) + # prepare and send a handshake initiation with wrong mac2 + # expect a handshake response + init = peer_1.mk_handshake(self.pg1) + init.mac2 = b"1234567890" + rxs = self.send_and_expect(self.pg1, [init], self.pg1) - peer_1.validate_encapped(rxs, p) + # verify the response + peer_1.consume_response(rxs[0]) - # send packets into the tunnel, expect to receive them on - # the other side - p = [ - ( - peer_1.mk_tunnel_header(self.pg1) - / Wireguard(message_type=4, reserved_zero=0) - / WireguardTransport( - receiver_index=peer_1.sender, - counter=ii, - encrypted_encapsulated_packet=peer_1.encrypt_transport( - ( - IP(src="10.11.3.1", dst=self.pg0.remote_ip4, ttl=20) - / UDP(sport=222, dport=223) - / Raw() - ) - ), - ) + # remove configs + peer_1.remove_vpp_config() + wg0.remove_vpp_config() + + def _test_wg_handshake_ratelimiting_tmpl(self, is_ip6): + port = 12323 + + # create wg interface + if is_ip6: + wg0 = VppWgInterface(self, self.pg1.local_ip6, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip6() + else: + wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip4() + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # create a peer + if is_ip6: + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip6, port + 1, ["1::3:0/112"] + ).add_vpp_config() + else: + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip4, port + 1, ["10.11.3.0/24"] + ).add_vpp_config() + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) + + # skip the first automatic handshake + self.pg1.get_capture(1, timeout=HANDSHAKE_JITTER) + + # prepare and send a bunch of handshake initiations + # expect to switch to under load state + init = peer_1.mk_handshake(self.pg1, is_ip6=is_ip6) + txs = [init] * HANDSHAKE_NUM_PER_PEER_UNTIL_UNDER_LOAD + rxs = self.send_and_expect_some(self.pg1, txs, self.pg1) + + # expect the peer to send a cookie reply + peer_1.consume_cookie(rxs[-1], is_ip6=is_ip6) + + # prepare and send a bunch of handshake initiations with correct mac2 + # expect a handshake response and then ratelimiting + NUM_TO_REJECT = 10 + init = peer_1.mk_handshake(self.pg1, is_ip6=is_ip6) + txs = [init] * (HANDSHAKE_NUM_BEFORE_RATELIMITING + NUM_TO_REJECT) + + # TODO: Deterimine why no handshake response is sent back if test is + # not run in as part of the test suite. It fails only very occasionally + # when run solo. + # + # Until then, if no response, don't fail trying to verify it. + # The error counter test still verifies that the correct number of + # handshake initiaions are ratelimited. + try: + rxs = self.send_and_expect_some(self.pg1, txs, self.pg1) + except: + self.logger.debug( + f"{self._testMethodDoc}: send_and_expect_some() failed to get any response packets." ) - for ii in range(255) - ] + rxs = None + pass - rxs = self.send_and_expect(self.pg1, p, self.pg0) + if is_ip6: + self.assertEqual( + self.base_ratelimited6_err + NUM_TO_REJECT, + self.statistics.get_err_counter(self.ratelimited6_err), + ) + else: + self.assertEqual( + self.base_ratelimited4_err + NUM_TO_REJECT, + self.statistics.get_err_counter(self.ratelimited4_err), + ) - for rx in rxs: - self.assertEqual(rx[IP].dst, self.pg0.remote_ip4) - self.assertEqual(rx[IP].ttl, 19) + # verify the response + if rxs is not None: + peer_1.consume_response(rxs[0], is_ip6=is_ip6) - r1.remove_vpp_config() + # clear up under load state + self.sleep(UNDER_LOAD_INTERVAL) + + # remove configs peer_1.remove_vpp_config() wg0.remove_vpp_config() - def test_wg_peer_v4o4(self): - """Test v4o4""" + def test_wg_handshake_ratelimiting_v4(self): + """Handshake ratelimiting (v4)""" + self._test_wg_handshake_ratelimiting_tmpl(is_ip6=False) - port = 12333 + def test_wg_handshake_ratelimiting_v6(self): + """Handshake ratelimiting (v6)""" + self._test_wg_handshake_ratelimiting_tmpl(is_ip6=True) - # Create interfaces + def test_wg_handshake_ratelimiting_multi_peer(self): + """Handshake ratelimiting (multiple peer)""" + port = 12323 + + # create wg interface wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() wg0.admin_up() wg0.config_ip4() - peer_1 = VppWgPeer( - self, wg0, self.pg1.remote_ip4, port + 1, ["10.11.3.0/24"] - ).add_vpp_config() - self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() - r1 = VppIpRoute( - self, "10.11.3.0", 24, [VppRoutePath("10.11.3.1", wg0.sw_if_index)] + # create two peers + NUM_PEERS = 2 + self.pg1.generate_remote_hosts(NUM_PEERS) + self.pg1.configure_ipv4_neighbors() + + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_hosts[0].ip4, port + 1, ["10.11.3.0/24"] ).add_vpp_config() - r2 = VppIpRoute( - self, "20.22.3.0", 24, [VppRoutePath("20.22.3.1", wg0.sw_if_index)] + peer_2 = VppWgPeer( + self, wg0, self.pg1.remote_hosts[1].ip4, port + 1, ["10.11.4.0/24"] ).add_vpp_config() - - # route a packet into the wg interface - # use the allowed-ip prefix - # this is dropped because the peer is not initiated - p = ( - Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) - / IP(src=self.pg0.remote_ip4, dst="10.11.3.2") - / UDP(sport=555, dport=556) - / Raw() - ) - self.send_and_assert_no_replies(self.pg0, [p]) - self.assertEqual( - self.base_kp4_err + 1, self.statistics.get_err_counter(self.kp4_error) + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 2) + + # skip the first automatic handshake + self.pg1.get_capture(NUM_PEERS, timeout=HANDSHAKE_JITTER) + + # (peer_1) prepare and send a bunch of handshake initiations + # expect not to switch to under load state + init_1 = peer_1.mk_handshake(self.pg1) + txs = [init_1] * HANDSHAKE_NUM_PER_PEER_UNTIL_UNDER_LOAD + rxs = self.send_and_expect_some(self.pg1, txs, self.pg1) + + # (peer_1) expect the peer to send a handshake response + peer_1.consume_response(rxs[0]) + peer_1.noise_reset() + + # (peer_1) send another bunch of handshake initiations + # expect to switch to under load state + rxs = self.send_and_expect_some(self.pg1, txs, self.pg1) + + # (peer_1) expect the peer to send a cookie reply + peer_1.consume_cookie(rxs[-1]) + + # (peer_2) prepare and send a handshake initiation + # expect a cookie reply + init_2 = peer_2.mk_handshake(self.pg1) + rxs = self.send_and_expect(self.pg1, [init_2], self.pg1) + peer_2.consume_cookie(rxs[0]) + + # (peer_1) (peer_2) prepare and send a bunch of handshake initiations with correct mac2 + # expect a handshake response and then ratelimiting + PEER_1_NUM_TO_REJECT = 2 + PEER_2_NUM_TO_REJECT = 5 + init_1 = peer_1.mk_handshake(self.pg1) + txs = [init_1] * (HANDSHAKE_NUM_BEFORE_RATELIMITING + PEER_1_NUM_TO_REJECT) + init_2 = peer_2.mk_handshake(self.pg1) + txs += [init_2] * (HANDSHAKE_NUM_BEFORE_RATELIMITING + PEER_2_NUM_TO_REJECT) + rxs = self.send_and_expect_some(self.pg1, txs, self.pg1) + + self.assertTrue( + self.base_ratelimited4_err + PEER_1_NUM_TO_REJECT + < self.statistics.get_err_counter(self.ratelimited4_err) + <= self.base_ratelimited4_err + PEER_1_NUM_TO_REJECT + PEER_2_NUM_TO_REJECT ) - # route a packet into the wg interface - # use a not allowed-ip prefix - # this is dropped because there is no matching peer - p = ( - Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) - / IP(src=self.pg0.remote_ip4, dst="20.22.3.2") - / UDP(sport=555, dport=556) - / Raw() - ) - self.send_and_assert_no_replies(self.pg0, [p]) - self.assertEqual( - self.base_peer4_out_err + 1, - self.statistics.get_err_counter(self.peer4_out_err), - ) + # (peer_1) (peer_2) verify the response + peer_1.consume_response(rxs[0]) + peer_2.consume_response(rxs[1]) - # send a handsake from the peer with an invalid MAC - p = peer_1.mk_handshake(self.pg1) - p[WireguardInitiation].mac1 = b"foobar" - self.send_and_assert_no_replies(self.pg1, [p]) - self.assertEqual( - self.base_mac4_err + 1, self.statistics.get_err_counter(self.mac4_error) - ) + # clear up under load state + self.sleep(UNDER_LOAD_INTERVAL) - # send a handsake from the peer but signed by the wrong key. - p = peer_1.mk_handshake( - self.pg1, False, X25519PrivateKey.generate().public_key() - ) - self.send_and_assert_no_replies(self.pg1, [p]) - self.assertEqual( - self.base_peer4_in_err + 1, - self.statistics.get_err_counter(self.peer4_in_err), - ) + # remove configs + peer_1.remove_vpp_config() + peer_2.remove_vpp_config() + wg0.remove_vpp_config() - # send a valid handsake init for which we expect a response - p = peer_1.mk_handshake(self.pg1) + def _test_wg_peer_roaming_on_handshake_tmpl(self, is_endpoint_set, is_resp, is_ip6): + port = 12323 - rx = self.send_and_expect(self.pg1, [p], self.pg1) + # create wg interface + if is_ip6: + wg0 = VppWgInterface(self, self.pg1.local_ip6, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip6() + else: + wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip4() - peer_1.consume_response(rx[0]) + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() - # route a packet into the wg interface - # this is dropped because the peer is still not initiated - p = ( - Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) - / IP(src=self.pg0.remote_ip4, dst="10.11.3.2") - / UDP(sport=555, dport=556) - / Raw() + # create more remote hosts + NUM_REMOTE_HOSTS = 2 + self.pg1.generate_remote_hosts(NUM_REMOTE_HOSTS) + if is_ip6: + self.pg1.configure_ipv6_neighbors() + else: + self.pg1.configure_ipv4_neighbors() + + # create a peer + if is_ip6: + peer_1 = VppWgPeer( + test=self, + itf=wg0, + endpoint=self.pg1.remote_hosts[0].ip6 if is_endpoint_set else "::", + port=port + 1 if is_endpoint_set else 0, + allowed_ips=["1::3:0/112"], + ).add_vpp_config() + else: + peer_1 = VppWgPeer( + test=self, + itf=wg0, + endpoint=self.pg1.remote_hosts[0].ip4 if is_endpoint_set else "0.0.0.0", + port=port + 1 if is_endpoint_set else 0, + allowed_ips=["10.11.3.0/24"], + ).add_vpp_config() + self.assertTrue(peer_1.query_vpp_config()) + + if is_resp: + # wait for the peer to send a handshake initiation + rxs = self.pg1.get_capture(1, timeout=2) + # prepare a handshake response + resp = peer_1.consume_init(rxs[0], self.pg1, is_ip6=is_ip6) + # change endpoint + if is_ip6: + peer_1.change_endpoint(self.pg1.remote_hosts[1].ip6, port + 100) + resp[IPv6].src, resp[UDP].sport = peer_1.endpoint, peer_1.port + else: + peer_1.change_endpoint(self.pg1.remote_hosts[1].ip4, port + 100) + resp[IP].src, resp[UDP].sport = peer_1.endpoint, peer_1.port + # send the handshake response + # expect a keepalive message sent to the new endpoint + rxs = self.send_and_expect(self.pg1, [resp], self.pg1) + # verify the keepalive message + b = peer_1.decrypt_transport(rxs[0], is_ip6=is_ip6) + self.assertEqual(0, len(b)) + else: + # change endpoint + if is_ip6: + peer_1.change_endpoint(self.pg1.remote_hosts[1].ip6, port + 100) + else: + peer_1.change_endpoint(self.pg1.remote_hosts[1].ip4, port + 100) + # prepare and send a handshake initiation + # expect a handshake response sent to the new endpoint + init = peer_1.mk_handshake(self.pg1, is_ip6=is_ip6) + rxs = self.send_and_expect(self.pg1, [init], self.pg1) + # verify the response + peer_1.consume_response(rxs[0], is_ip6=is_ip6) + self.assertTrue(peer_1.query_vpp_config()) + + # remove configs + peer_1.remove_vpp_config() + wg0.remove_vpp_config() + + def test_wg_peer_roaming_on_init_v4(self): + """Peer roaming on handshake initiation (v4)""" + self._test_wg_peer_roaming_on_handshake_tmpl( + is_endpoint_set=False, is_resp=False, is_ip6=False ) - self.send_and_assert_no_replies(self.pg0, [p]) - self.assertEqual( - self.base_kp4_err + 2, self.statistics.get_err_counter(self.kp4_error) + + def test_wg_peer_roaming_on_init_v6(self): + """Peer roaming on handshake initiation (v6)""" + self._test_wg_peer_roaming_on_handshake_tmpl( + is_endpoint_set=False, is_resp=False, is_ip6=True ) - # send a data packet from the peer through the tunnel - # this completes the handshake - p = ( - IP(src="10.11.3.1", dst=self.pg0.remote_ip4, ttl=20) - / UDP(sport=222, dport=223) - / Raw() + def test_wg_peer_roaming_on_resp_v4(self): + """Peer roaming on handshake response (v4)""" + self._test_wg_peer_roaming_on_handshake_tmpl( + is_endpoint_set=True, is_resp=True, is_ip6=False ) - d = peer_1.encrypt_transport(p) - p = peer_1.mk_tunnel_header(self.pg1) / ( - Wireguard(message_type=4, reserved_zero=0) + + def test_wg_peer_roaming_on_resp_v6(self): + """Peer roaming on handshake response (v6)""" + self._test_wg_peer_roaming_on_handshake_tmpl( + is_endpoint_set=True, is_resp=True, is_ip6=True + ) + + def _test_wg_peer_roaming_on_data_tmpl(self, is_async, is_ip6): + self.vapi.wg_set_async_mode(is_async) + port = 12323 + + # create wg interface + if is_ip6: + wg0 = VppWgInterface(self, self.pg1.local_ip6, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip6() + else: + wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip4() + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # create more remote hosts + NUM_REMOTE_HOSTS = 2 + self.pg1.generate_remote_hosts(NUM_REMOTE_HOSTS) + if is_ip6: + self.pg1.configure_ipv6_neighbors() + else: + self.pg1.configure_ipv4_neighbors() + + # create a peer + if is_ip6: + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_hosts[0].ip6, port + 1, ["1::3:0/112"] + ).add_vpp_config() + else: + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_hosts[0].ip4, port + 1, ["10.11.3.0/24"] + ).add_vpp_config() + self.assertTrue(peer_1.query_vpp_config()) + + # create a route to rewrite traffic into the wg interface + if is_ip6: + r1 = VppIpRoute( + self, "1::3:0", 112, [VppRoutePath("1::3:1", wg0.sw_if_index)] + ).add_vpp_config() + else: + r1 = VppIpRoute( + self, "10.11.3.0", 24, [VppRoutePath("10.11.3.1", wg0.sw_if_index)] + ).add_vpp_config() + + # wait for the peer to send a handshake initiation + rxs = self.pg1.get_capture(1, timeout=2) + + # prepare and send a handshake response + # expect a keepalive message + resp = peer_1.consume_init(rxs[0], self.pg1, is_ip6=is_ip6) + rxs = self.send_and_expect(self.pg1, [resp], self.pg1) + + # verify the keepalive message + b = peer_1.decrypt_transport(rxs[0], is_ip6=is_ip6) + self.assertEqual(0, len(b)) + + # change endpoint + if is_ip6: + peer_1.change_endpoint(self.pg1.remote_hosts[1].ip6, port + 100) + else: + peer_1.change_endpoint(self.pg1.remote_hosts[1].ip4, port + 100) + + # prepare and send a data packet + # expect endpoint change + if is_ip6: + ip_header = IPv6(src="1::3:1", dst=self.pg0.remote_ip6, hlim=20) + else: + ip_header = IP(src="10.11.3.1", dst=self.pg0.remote_ip4, ttl=20) + data = ( + peer_1.mk_tunnel_header(self.pg1, is_ip6=is_ip6) + / Wireguard(message_type=4, reserved_zero=0) / WireguardTransport( - receiver_index=peer_1.sender, counter=0, encrypted_encapsulated_packet=d + receiver_index=peer_1.sender, + counter=0, + encrypted_encapsulated_packet=peer_1.encrypt_transport( + ip_header / UDP(sport=222, dport=223) / Raw() + ), ) ) - rxs = self.send_and_expect(self.pg1, [p], self.pg0) + rxs = self.send_and_expect(self.pg1, [data], self.pg0) + if is_ip6: + self.assertEqual(rxs[0][IPv6].dst, self.pg0.remote_ip6) + self.assertEqual(rxs[0][IPv6].hlim, 19) + else: + self.assertEqual(rxs[0][IP].dst, self.pg0.remote_ip4) + self.assertEqual(rxs[0][IP].ttl, 19) + self.assertTrue(peer_1.query_vpp_config()) + + # prepare and send a packet that will be rewritten into the wg interface + # expect a data packet sent to the new endpoint + if is_ip6: + ip_header = IPv6(src=self.pg0.remote_ip6, dst="1::3:2") + else: + ip_header = IP(src=self.pg0.remote_ip4, dst="10.11.3.2") + p = ( + Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) + / ip_header + / UDP(sport=555, dport=556) + / Raw() + ) + rxs = self.send_and_expect(self.pg0, [p], self.pg1) + + # verify the data packet + peer_1.validate_encapped(rxs, p, is_tunnel_ip6=is_ip6, is_transport_ip6=is_ip6) + + # remove configs + r1.remove_vpp_config() + peer_1.remove_vpp_config() + wg0.remove_vpp_config() + + def test_wg_peer_roaming_on_data_v4_sync(self): + """Peer roaming on data packet (v4, sync)""" + self._test_wg_peer_roaming_on_data_tmpl(is_async=False, is_ip6=False) + + def test_wg_peer_roaming_on_data_v6_sync(self): + """Peer roaming on data packet (v6, sync)""" + self._test_wg_peer_roaming_on_data_tmpl(is_async=False, is_ip6=True) + + def test_wg_peer_roaming_on_data_v4_async(self): + """Peer roaming on data packet (v4, async)""" + self._test_wg_peer_roaming_on_data_tmpl(is_async=True, is_ip6=False) + + def test_wg_peer_roaming_on_data_v6_async(self): + """Peer roaming on data packet (v6, async)""" + self._test_wg_peer_roaming_on_data_tmpl(is_async=True, is_ip6=True) + + def test_wg_peer_resp(self): + """Send handshake response IPv4 tunnel""" + port = 12323 + + # Create interfaces + wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip4() + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip4, port + 1, ["10.11.3.0/24"] + ).add_vpp_config() + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) + + r1 = VppIpRoute( + self, "10.11.3.0", 24, [VppRoutePath("10.11.3.1", wg0.sw_if_index)] + ).add_vpp_config() + + # wait for the peer to send a handshake + rx = self.pg1.get_capture(1, timeout=2) + + # consume the handshake in the noise protocol and + # generate the response + resp = peer_1.consume_init(rx[0], self.pg1) + + # send the response, get keepalive + rxs = self.send_and_expect(self.pg1, [resp], self.pg1) for rx in rxs: - self.assertEqual(rx[IP].dst, self.pg0.remote_ip4) - self.assertEqual(rx[IP].ttl, 19) + b = peer_1.decrypt_transport(rx) + self.assertEqual(0, len(b)) # send a packets that are routed into the tunnel p = ( @@ -843,22 +1326,94 @@ class TestWg(VppTestCase): rxs = self.send_and_expect(self.pg0, p * 255, self.pg1) + peer_1.validate_encapped(rxs, p) + + # send packets into the tunnel, expect to receive them on + # the other side + p = [ + ( + peer_1.mk_tunnel_header(self.pg1) + / Wireguard(message_type=4, reserved_zero=0) + / WireguardTransport( + receiver_index=peer_1.sender, + counter=ii, + encrypted_encapsulated_packet=peer_1.encrypt_transport( + ( + IP(src="10.11.3.1", dst=self.pg0.remote_ip4, ttl=20) + / UDP(sport=222, dport=223) + / Raw() + ) + ), + ) + ) + for ii in range(255) + ] + + rxs = self.send_and_expect(self.pg1, p, self.pg0) + for rx in rxs: - rx = IP(peer_1.decrypt_transport(rx)) + self.assertEqual(rx[IP].dst, self.pg0.remote_ip4) + self.assertEqual(rx[IP].ttl, 19) - # chech the oringial packet is present - self.assertEqual(rx[IP].dst, p[IP].dst) - self.assertEqual(rx[IP].ttl, p[IP].ttl - 1) + r1.remove_vpp_config() + peer_1.remove_vpp_config() + wg0.remove_vpp_config() + + def test_wg_peer_resp_ipv6(self): + """Send handshake response IPv6 tunnel""" + port = 12323 + + # Create interfaces + wg0 = VppWgInterface(self, self.pg1.local_ip6, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip4() + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip6, port + 1, ["10.11.3.0/24"] + ).add_vpp_config() + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) + + r1 = VppIpRoute( + self, "10.11.3.0", 24, [VppRoutePath("10.11.3.1", wg0.sw_if_index)] + ).add_vpp_config() + + # wait for the peer to send a handshake + rx = self.pg1.get_capture(1, timeout=2) + + # consume the handshake in the noise protocol and + # generate the response + resp = peer_1.consume_init(rx[0], self.pg1, is_ip6=True) + + # send the response, get keepalive + rxs = self.send_and_expect(self.pg1, [resp], self.pg1) + + for rx in rxs: + b = peer_1.decrypt_transport(rx, True) + self.assertEqual(0, len(b)) + + # send a packets that are routed into the tunnel + p = ( + Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) + / IP(src=self.pg0.remote_ip4, dst="10.11.3.2") + / UDP(sport=555, dport=556) + / Raw(b"\x00" * 80) + ) + + rxs = self.send_and_expect(self.pg0, p * 2, self.pg1) + peer_1.validate_encapped(rxs, p, True) # send packets into the tunnel, expect to receive them on # the other side p = [ ( - peer_1.mk_tunnel_header(self.pg1) + peer_1.mk_tunnel_header(self.pg1, True) / Wireguard(message_type=4, reserved_zero=0) / WireguardTransport( receiver_index=peer_1.sender, - counter=ii + 1, + counter=ii, encrypted_encapsulated_packet=peer_1.encrypt_transport( ( IP(src="10.11.3.1", dst=self.pg0.remote_ip4, ttl=20) @@ -878,46 +1433,43 @@ class TestWg(VppTestCase): self.assertEqual(rx[IP].ttl, 19) r1.remove_vpp_config() - r2.remove_vpp_config() peer_1.remove_vpp_config() wg0.remove_vpp_config() - def test_wg_peer_v6o6(self): - """Test v6o6""" + def test_wg_peer_v4o4(self): + """Test v4o4""" - port = 12343 + port = 12333 # Create interfaces - wg0 = VppWgInterface(self, self.pg1.local_ip6, port).add_vpp_config() + wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() wg0.admin_up() - wg0.config_ip6() + wg0.config_ip4() peer_1 = VppWgPeer( - self, wg0, self.pg1.remote_ip6, port + 1, ["1::3:0/112"] - ).add_vpp_config(True) + self, wg0, self.pg1.remote_ip4, port + 1, ["10.11.3.0/24"] + ).add_vpp_config() self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) r1 = VppIpRoute( - self, "1::3:0", 112, [VppRoutePath("1::3:1", wg0.sw_if_index)] + self, "10.11.3.0", 24, [VppRoutePath("10.11.3.1", wg0.sw_if_index)] ).add_vpp_config() r2 = VppIpRoute( - self, "22::3:0", 112, [VppRoutePath("22::3:1", wg0.sw_if_index)] + self, "20.22.3.0", 24, [VppRoutePath("20.22.3.1", wg0.sw_if_index)] ).add_vpp_config() # route a packet into the wg interface # use the allowed-ip prefix # this is dropped because the peer is not initiated - p = ( Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) - / IPv6(src=self.pg0.remote_ip6, dst="1::3:2") + / IP(src=self.pg0.remote_ip4, dst="10.11.3.2") / UDP(sport=555, dport=556) / Raw() ) - self.send_and_assert_no_replies(self.pg0, [p]) - + self.send_and_assert_no_replies_ignoring_init(self.pg0, [p]) self.assertEqual( - self.base_kp6_err + 1, self.statistics.get_err_counter(self.kp6_error) + self.base_kp4_err + 1, self.statistics.get_err_counter(self.kp4_error) ) # route a packet into the wg interface @@ -925,20 +1477,180 @@ class TestWg(VppTestCase): # this is dropped because there is no matching peer p = ( Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) - / IPv6(src=self.pg0.remote_ip6, dst="22::3:2") + / IP(src=self.pg0.remote_ip4, dst="20.22.3.2") / UDP(sport=555, dport=556) / Raw() ) - self.send_and_assert_no_replies(self.pg0, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg0, [p]) self.assertEqual( - self.base_peer6_out_err + 1, - self.statistics.get_err_counter(self.peer6_out_err), + self.base_peer4_out_err + 1, + self.statistics.get_err_counter(self.peer4_out_err), + ) + + # send a handsake from the peer with an invalid MAC + p = peer_1.mk_handshake(self.pg1) + p[WireguardInitiation].mac1 = b"foobar" + self.send_and_assert_no_replies_ignoring_init(self.pg1, [p]) + self.assertEqual( + self.base_mac4_err + 1, self.statistics.get_err_counter(self.mac4_error) + ) + + # send a handsake from the peer but signed by the wrong key. + p = peer_1.mk_handshake( + self.pg1, False, X25519PrivateKey.generate().public_key() + ) + self.send_and_assert_no_replies_ignoring_init(self.pg1, [p]) + self.assertEqual( + self.base_peer4_in_err + 1, + self.statistics.get_err_counter(self.peer4_in_err), + ) + + # send a valid handsake init for which we expect a response + p = peer_1.mk_handshake(self.pg1) + + rx = self.send_and_expect(self.pg1, [p], self.pg1) + + peer_1.consume_response(rx[0]) + + # route a packet into the wg interface + # this is dropped because the peer is still not initiated + p = ( + Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) + / IP(src=self.pg0.remote_ip4, dst="10.11.3.2") + / UDP(sport=555, dport=556) + / Raw() + ) + self.send_and_assert_no_replies_ignoring_init(self.pg0, [p]) + self.assertEqual( + self.base_kp4_err + 2, self.statistics.get_err_counter(self.kp4_error) + ) + + # send a data packet from the peer through the tunnel + # this completes the handshake + p = ( + IP(src="10.11.3.1", dst=self.pg0.remote_ip4, ttl=20) + / UDP(sport=222, dport=223) + / Raw() + ) + d = peer_1.encrypt_transport(p) + p = peer_1.mk_tunnel_header(self.pg1) / ( + Wireguard(message_type=4, reserved_zero=0) + / WireguardTransport( + receiver_index=peer_1.sender, counter=0, encrypted_encapsulated_packet=d + ) + ) + rxs = self.send_and_expect(self.pg1, [p], self.pg0) + + for rx in rxs: + self.assertEqual(rx[IP].dst, self.pg0.remote_ip4) + self.assertEqual(rx[IP].ttl, 19) + + # send a packets that are routed into the tunnel + p = ( + Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) + / IP(src=self.pg0.remote_ip4, dst="10.11.3.2") + / UDP(sport=555, dport=556) + / Raw(b"\x00" * 80) + ) + + rxs = self.send_and_expect(self.pg0, p * 255, self.pg1) + + for rx in rxs: + rx = IP(peer_1.decrypt_transport(rx)) + + # check the original packet is present + self.assertEqual(rx[IP].dst, p[IP].dst) + self.assertEqual(rx[IP].ttl, p[IP].ttl - 1) + + # send packets into the tunnel, expect to receive them on + # the other side + p = [ + ( + peer_1.mk_tunnel_header(self.pg1) + / Wireguard(message_type=4, reserved_zero=0) + / WireguardTransport( + receiver_index=peer_1.sender, + counter=ii + 1, + encrypted_encapsulated_packet=peer_1.encrypt_transport( + ( + IP(src="10.11.3.1", dst=self.pg0.remote_ip4, ttl=20) + / UDP(sport=222, dport=223) + / Raw() + ) + ), + ) + ) + for ii in range(255) + ] + + rxs = self.send_and_expect(self.pg1, p, self.pg0) + + for rx in rxs: + self.assertEqual(rx[IP].dst, self.pg0.remote_ip4) + self.assertEqual(rx[IP].ttl, 19) + + r1.remove_vpp_config() + r2.remove_vpp_config() + peer_1.remove_vpp_config() + wg0.remove_vpp_config() + + def test_wg_peer_v6o6(self): + """Test v6o6""" + + port = 12343 + + # Create interfaces + wg0 = VppWgInterface(self, self.pg1.local_ip6, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip6() + + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip6, port + 1, ["1::3:0/112"] + ).add_vpp_config() + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) + + r1 = VppIpRoute( + self, "1::3:0", 112, [VppRoutePath("1::3:1", wg0.sw_if_index)] + ).add_vpp_config() + r2 = VppIpRoute( + self, "22::3:0", 112, [VppRoutePath("22::3:1", wg0.sw_if_index)] + ).add_vpp_config() + + # route a packet into the wg interface + # use the allowed-ip prefix + # this is dropped because the peer is not initiated + + p = ( + Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) + / IPv6(src=self.pg0.remote_ip6, dst="1::3:2") + / UDP(sport=555, dport=556) + / Raw() + ) + self.send_and_assert_no_replies_ignoring_init(self.pg0, [p]) + + self.assertEqual( + self.base_kp6_err + 1, self.statistics.get_err_counter(self.kp6_error) + ) + + # route a packet into the wg interface + # use a not allowed-ip prefix + # this is dropped because there is no matching peer + p = ( + Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) + / IPv6(src=self.pg0.remote_ip6, dst="22::3:2") + / UDP(sport=555, dport=556) + / Raw() + ) + self.send_and_assert_no_replies_ignoring_init(self.pg0, [p]) + self.assertEqual( + self.base_peer6_out_err + 1, + self.statistics.get_err_counter(self.peer6_out_err), ) # send a handsake from the peer with an invalid MAC p = peer_1.mk_handshake(self.pg1, True) p[WireguardInitiation].mac1 = b"foobar" - self.send_and_assert_no_replies(self.pg1, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg1, [p]) self.assertEqual( self.base_mac6_err + 1, self.statistics.get_err_counter(self.mac6_error) @@ -948,7 +1660,7 @@ class TestWg(VppTestCase): p = peer_1.mk_handshake( self.pg1, True, X25519PrivateKey.generate().public_key() ) - self.send_and_assert_no_replies(self.pg1, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg1, [p]) self.assertEqual( self.base_peer6_in_err + 1, self.statistics.get_err_counter(self.peer6_in_err), @@ -969,7 +1681,7 @@ class TestWg(VppTestCase): / UDP(sport=555, dport=556) / Raw() ) - self.send_and_assert_no_replies(self.pg0, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg0, [p]) self.assertEqual( self.base_kp6_err + 2, self.statistics.get_err_counter(self.kp6_error) ) @@ -1007,7 +1719,7 @@ class TestWg(VppTestCase): for rx in rxs: rx = IPv6(peer_1.decrypt_transport(rx, True)) - # chech the oringial packet is present + # check the original packet is present self.assertEqual(rx[IPv6].dst, p[IPv6].dst) self.assertEqual(rx[IPv6].hlim, p[IPv6].hlim - 1) @@ -1055,7 +1767,7 @@ class TestWg(VppTestCase): peer_1 = VppWgPeer( self, wg0, self.pg1.remote_ip4, port + 1, ["1::3:0/112"] - ).add_vpp_config(True) + ).add_vpp_config() self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) r1 = VppIpRoute( @@ -1071,7 +1783,7 @@ class TestWg(VppTestCase): / UDP(sport=555, dport=556) / Raw() ) - self.send_and_assert_no_replies(self.pg0, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg0, [p]) self.assertEqual( self.base_kp6_err + 1, self.statistics.get_err_counter(self.kp6_error) ) @@ -1079,7 +1791,7 @@ class TestWg(VppTestCase): # send a handsake from the peer with an invalid MAC p = peer_1.mk_handshake(self.pg1) p[WireguardInitiation].mac1 = b"foobar" - self.send_and_assert_no_replies(self.pg1, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg1, [p]) self.assertEqual( self.base_mac4_err + 1, self.statistics.get_err_counter(self.mac4_error) @@ -1089,7 +1801,7 @@ class TestWg(VppTestCase): p = peer_1.mk_handshake( self.pg1, False, X25519PrivateKey.generate().public_key() ) - self.send_and_assert_no_replies(self.pg1, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg1, [p]) self.assertEqual( self.base_peer4_in_err + 1, self.statistics.get_err_counter(self.peer4_in_err), @@ -1110,7 +1822,7 @@ class TestWg(VppTestCase): / UDP(sport=555, dport=556) / Raw() ) - self.send_and_assert_no_replies(self.pg0, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg0, [p]) self.assertEqual( self.base_kp6_err + 2, self.statistics.get_err_counter(self.kp6_error) ) @@ -1148,7 +1860,7 @@ class TestWg(VppTestCase): for rx in rxs: rx = IPv6(peer_1.decrypt_transport(rx)) - # chech the oringial packet is present + # check the original packet is present self.assertEqual(rx[IPv6].dst, p[IPv6].dst) self.assertEqual(rx[IPv6].hlim, p[IPv6].hlim - 1) @@ -1211,7 +1923,7 @@ class TestWg(VppTestCase): / UDP(sport=555, dport=556) / Raw() ) - self.send_and_assert_no_replies(self.pg0, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg0, [p]) self.assertEqual( self.base_kp4_err + 1, self.statistics.get_err_counter(self.kp4_error) ) @@ -1219,7 +1931,7 @@ class TestWg(VppTestCase): # send a handsake from the peer with an invalid MAC p = peer_1.mk_handshake(self.pg1, True) p[WireguardInitiation].mac1 = b"foobar" - self.send_and_assert_no_replies(self.pg1, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg1, [p]) self.assertEqual( self.base_mac6_err + 1, self.statistics.get_err_counter(self.mac6_error) ) @@ -1228,7 +1940,7 @@ class TestWg(VppTestCase): p = peer_1.mk_handshake( self.pg1, True, X25519PrivateKey.generate().public_key() ) - self.send_and_assert_no_replies(self.pg1, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg1, [p]) self.assertEqual( self.base_peer6_in_err + 1, self.statistics.get_err_counter(self.peer6_in_err), @@ -1249,7 +1961,7 @@ class TestWg(VppTestCase): / UDP(sport=555, dport=556) / Raw() ) - self.send_and_assert_no_replies(self.pg0, [p]) + self.send_and_assert_no_replies_ignoring_init(self.pg0, [p]) self.assertEqual( self.base_kp4_err + 2, self.statistics.get_err_counter(self.kp4_error) ) @@ -1287,7 +1999,7 @@ class TestWg(VppTestCase): for rx in rxs: rx = IP(peer_1.decrypt_transport(rx, True)) - # chech the oringial packet is present + # check the original packet is present self.assertEqual(rx[IP].dst, p[IP].dst) self.assertEqual(rx[IP].ttl, p[IP].ttl - 1) @@ -1422,6 +2134,9 @@ class TestWg(VppTestCase): self.pg0.generate_remote_hosts(NUM_IFS) self.pg0.configure_ipv4_neighbors() + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + # Create interfaces with a peer on each peers = [] routes = [] @@ -1451,203 +2166,594 @@ class TestWg(VppTestCase): ).add_vpp_config() ) - self.assertEqual(len(self.vapi.wireguard_peers_dump()), NUM_IFS) + self.assertEqual(len(self.vapi.wireguard_peers_dump()), NUM_IFS) + + # skip the first automatic handshake + self.pg1.get_capture(NUM_IFS, timeout=HANDSHAKE_JITTER) + + for i in range(NUM_IFS): + # send a valid handsake init for which we expect a response + p = peers[i].mk_handshake(self.pg1) + rx = self.send_and_expect(self.pg1, [p], self.pg1) + peers[i].consume_response(rx[0]) + + # send a data packet from the peer through the tunnel + # this completes the handshake + p = ( + IP(src="10.0.%d.4" % i, dst=self.pg0.remote_hosts[i].ip4, ttl=20) + / UDP(sport=222, dport=223) + / Raw() + ) + d = peers[i].encrypt_transport(p) + p = peers[i].mk_tunnel_header(self.pg1) / ( + Wireguard(message_type=4, reserved_zero=0) + / WireguardTransport( + receiver_index=peers[i].sender, + counter=0, + encrypted_encapsulated_packet=d, + ) + ) + rxs = self.send_and_expect(self.pg1, [p], self.pg0) + for rx in rxs: + self.assertEqual(rx[IP].dst, self.pg0.remote_hosts[i].ip4) + self.assertEqual(rx[IP].ttl, 19) + + # send a packets that are routed into the tunnel + for i in range(NUM_IFS): + p = ( + Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) + / IP(src=self.pg0.remote_hosts[i].ip4, dst="10.0.%d.4" % i) + / UDP(sport=555, dport=556) + / Raw(b"\x00" * 80) + ) + + rxs = self.send_and_expect(self.pg0, p * 64, self.pg1) + + for rx in rxs: + rx = IP(peers[i].decrypt_transport(rx)) + + # check the oringial packet is present + self.assertEqual(rx[IP].dst, p[IP].dst) + self.assertEqual(rx[IP].ttl, p[IP].ttl - 1) + + # send packets into the tunnel + for i in range(NUM_IFS): + p = [ + ( + peers[i].mk_tunnel_header(self.pg1) + / Wireguard(message_type=4, reserved_zero=0) + / WireguardTransport( + receiver_index=peers[i].sender, + counter=ii + 1, + encrypted_encapsulated_packet=peers[i].encrypt_transport( + ( + IP( + src="10.0.%d.4" % i, + dst=self.pg0.remote_hosts[i].ip4, + ttl=20, + ) + / UDP(sport=222, dport=223) + / Raw() + ) + ), + ) + ) + for ii in range(64) + ] + + rxs = self.send_and_expect(self.pg1, p, self.pg0) + + for rx in rxs: + self.assertEqual(rx[IP].dst, self.pg0.remote_hosts[i].ip4) + self.assertEqual(rx[IP].ttl, 19) + + for r in routes: + r.remove_vpp_config() + for p in peers: + p.remove_vpp_config() + for i in wg_ifs: + i.remove_vpp_config() + + def test_wg_event(self): + """Test events""" + port = 12600 + ESTABLISHED_FLAG = ( + VppEnum.vl_api_wireguard_peer_flags_t.WIREGUARD_PEER_ESTABLISHED + ) + DEAD_FLAG = VppEnum.vl_api_wireguard_peer_flags_t.WIREGUARD_PEER_STATUS_DEAD + + # Create interfaces + wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() + wg1 = VppWgInterface(self, self.pg2.local_ip4, port + 1).add_vpp_config() + wg0.admin_up() + wg1.admin_up() + + # Check peer counter + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 0) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # Create peers + NUM_PEERS = 2 + self.pg2.generate_remote_hosts(NUM_PEERS) + self.pg2.configure_ipv4_neighbors() + self.pg1.generate_remote_hosts(NUM_PEERS) + self.pg1.configure_ipv4_neighbors() + + peers_0 = [] + peers_1 = [] + routes_0 = [] + routes_1 = [] + for i in range(NUM_PEERS): + peers_0.append( + VppWgPeer( + self, + wg0, + self.pg1.remote_hosts[i].ip4, + port + 1 + i, + ["10.0.%d.4/32" % i], + ).add_vpp_config() + ) + routes_0.append( + VppIpRoute( + self, + "10.0.%d.4" % i, + 32, + [VppRoutePath(self.pg1.remote_hosts[i].ip4, wg0.sw_if_index)], + ).add_vpp_config() + ) + + peers_1.append( + VppWgPeer( + self, + wg1, + self.pg2.remote_hosts[i].ip4, + port + 100 + i, + ["10.100.%d.4/32" % i], + ).add_vpp_config() + ) + routes_1.append( + VppIpRoute( + self, + "10.100.%d.4" % i, + 32, + [VppRoutePath(self.pg2.remote_hosts[i].ip4, wg1.sw_if_index)], + ).add_vpp_config() + ) + + self.assertEqual(len(self.vapi.wireguard_peers_dump()), NUM_PEERS * 2) + + # skip the first automatic handshake + self.pg1.get_capture(NUM_PEERS, timeout=HANDSHAKE_JITTER) + self.pg2.get_capture(NUM_PEERS, timeout=HANDSHAKE_JITTER) + + # Want events from the first perr of wg0 + # and from all wg1 peers + peers_0[0].want_events() + wg1.want_events() + + for i in range(NUM_PEERS): + # wg0 peers: send a valid handsake init for which we expect a response + p = peers_0[i].mk_handshake(self.pg1) + rx = self.send_and_expect(self.pg1, [p], self.pg1) + peers_0[i].consume_response(rx[0]) + + # wg0 peers: send empty packet, it means successful connection (WIREGUARD_PEER_ESTABLISHED) + keepalive = peers_0[i].encrypt_transport(0) + p = peers_0[i].mk_tunnel_header(self.pg1) / ( + Wireguard(message_type=4, reserved_zero=0) + / WireguardTransport( + receiver_index=peers_0[i].sender, + counter=0, + encrypted_encapsulated_packet=keepalive, + ) + ) + # TODO: Figure out wny there are sometimes wg packets received here + # self.send_and_assert_no_replies(self.pg1, [p]) + self.pg_send(self.pg1, [p]) + + # wg0 peers: wait for established flag + if i == 0: + peers_0[0].wait_event(ESTABLISHED_FLAG) + + # wg1 peers: send a valid handsake init for which we expect a response + p = peers_1[i].mk_handshake(self.pg2) + rx = self.send_and_expect(self.pg2, [p], self.pg2) + peers_1[i].consume_response(rx[0]) + + # wg1 peers: send empty packet, it means successful connection (WIREGUARD_PEER_ESTABLISHED) + keepalive = peers_1[i].encrypt_transport(0) + p = peers_1[i].mk_tunnel_header(self.pg2) / ( + Wireguard(message_type=4, reserved_zero=0) + / WireguardTransport( + receiver_index=peers_1[i].sender, + counter=0, + encrypted_encapsulated_packet=keepalive, + ) + ) + self.send_and_assert_no_replies(self.pg2, [p]) + + # wg1 peers: wait for established flag + wg1.wait_events(ESTABLISHED_FLAG, [peers_1[0].index, peers_1[1].index]) + + # remove routes + for r in routes_0: + r.remove_vpp_config() + for r in routes_1: + r.remove_vpp_config() + + # remove peers + for i in range(NUM_PEERS): + self.assertTrue(peers_0[i].query_vpp_config()) + peers_0[i].remove_vpp_config() + if i == 0: + peers_0[i].wait_event(0) + peers_0[i].wait_event(DEAD_FLAG) + for p in peers_1: + self.assertTrue(p.query_vpp_config()) + p.remove_vpp_config() + p.wait_event(0) + p.wait_event(DEAD_FLAG) + + wg0.remove_vpp_config() + wg1.remove_vpp_config() + + def test_wg_sending_handshake_when_admin_down(self): + """Sending handshake when admin down""" + port = 12323 + + # create wg interface + wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() + wg0.config_ip4() + + # create a peer + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip4, port + 1, ["10.11.3.0/24"] + ).add_vpp_config() + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # wait for the peer to send a handshake initiation + # expect no handshakes + for i in range(2): + self.pg1.assert_nothing_captured(remark="handshake packet(s) sent") + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # administratively enable the wg interface + # expect the peer to send a handshake initiation + wg0.admin_up() + rxs = self.pg1.get_capture(1, timeout=2) + peer_1.consume_init(rxs[0], self.pg1) + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # administratively disable the wg interface + # expect no handshakes + wg0.admin_down() + for i in range(6): + self.pg1.assert_nothing_captured(remark="handshake packet(s) sent") + + # remove configs + peer_1.remove_vpp_config() + wg0.remove_vpp_config() + + def test_wg_sending_data_when_admin_down(self): + """Sending data when admin down""" + port = 12323 + + # create wg interface + wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip4() + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # create a peer + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip4, port + 1, ["10.11.3.0/24"] + ).add_vpp_config() + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) + + # create a route to rewrite traffic into the wg interface + r1 = VppIpRoute( + self, "10.11.3.0", 24, [VppRoutePath("10.11.3.1", wg0.sw_if_index)] + ).add_vpp_config() + + # wait for the peer to send a handshake initiation + rxs = self.pg1.get_capture(1, timeout=2) + + # prepare and send a handshake response + # expect a keepalive message + resp = peer_1.consume_init(rxs[0], self.pg1) + rxs = self.send_and_expect(self.pg1, [resp], self.pg1) + + # verify the keepalive message + b = peer_1.decrypt_transport(rxs[0]) + self.assertEqual(0, len(b)) + + # prepare and send a packet that will be rewritten into the wg interface + # expect a data packet sent + p = ( + Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) + / IP(src=self.pg0.remote_ip4, dst="10.11.3.2") + / UDP(sport=555, dport=556) + / Raw() + ) + rxs = self.send_and_expect(self.pg0, [p], self.pg1) + + # verify the data packet + peer_1.validate_encapped(rxs, p) + + # administratively disable the wg interface + wg0.admin_down() + + # send a packet that will be rewritten into the wg interface + # expect no data packets sent + self.send_and_assert_no_replies(self.pg0, [p]) + + # administratively enable the wg interface + # expect the peer to send a handshake initiation + wg0.admin_up() + peer_1.noise_reset() + rxs = self.pg1.get_capture(1, timeout=2) + resp = peer_1.consume_init(rxs[0], self.pg1) + + # send a packet that will be rewritten into the wg interface + # expect no data packets sent because the peer is not initiated + self.send_and_assert_no_replies(self.pg0, [p]) + self.assertEqual( + self.base_kp4_err + 1, self.statistics.get_err_counter(self.kp4_error) + ) + + # send a handshake response and expect a keepalive message + rxs = self.send_and_expect(self.pg1, [resp], self.pg1) + + # verify the keepalive message + b = peer_1.decrypt_transport(rxs[0]) + self.assertEqual(0, len(b)) + + # send a packet that will be rewritten into the wg interface + # expect a data packet sent + rxs = self.send_and_expect(self.pg0, [p], self.pg1) + + # verify the data packet + peer_1.validate_encapped(rxs, p) + + # remove configs + r1.remove_vpp_config() + peer_1.remove_vpp_config() + wg0.remove_vpp_config() + + def _test_wg_large_packet_tmpl(self, is_async, is_ip6): + self.vapi.wg_set_async_mode(is_async) + port = 12323 + + # create wg interface + if is_ip6: + wg0 = VppWgInterface(self, self.pg1.local_ip6, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip6() + else: + wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip4() + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # create a peer + if is_ip6: + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip6, port + 1, ["1::3:0/112"] + ).add_vpp_config() + else: + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip4, port + 1, ["10.11.3.0/24"] + ).add_vpp_config() + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) + + # create a route to rewrite traffic into the wg interface + if is_ip6: + r1 = VppIpRoute( + self, "1::3:0", 112, [VppRoutePath("1::3:1", wg0.sw_if_index)] + ).add_vpp_config() + else: + r1 = VppIpRoute( + self, "10.11.3.0", 24, [VppRoutePath("10.11.3.1", wg0.sw_if_index)] + ).add_vpp_config() + + # wait for the peer to send a handshake initiation + rxs = self.pg1.get_capture(1, timeout=2) - for i in range(NUM_IFS): - # send a valid handsake init for which we expect a response - p = peers[i].mk_handshake(self.pg1) - rx = self.send_and_expect(self.pg1, [p], self.pg1) - peers[i].consume_response(rx[0]) + # prepare and send a handshake response + # expect a keepalive message + resp = peer_1.consume_init(rxs[0], self.pg1, is_ip6=is_ip6) + rxs = self.send_and_expect(self.pg1, [resp], self.pg1) - # send a data packet from the peer through the tunnel - # this completes the handshake - p = ( - IP(src="10.0.%d.4" % i, dst=self.pg0.remote_hosts[i].ip4, ttl=20) - / UDP(sport=222, dport=223) - / Raw() - ) - d = peers[i].encrypt_transport(p) - p = peers[i].mk_tunnel_header(self.pg1) / ( - Wireguard(message_type=4, reserved_zero=0) + # verify the keepalive message + b = peer_1.decrypt_transport(rxs[0], is_ip6=is_ip6) + self.assertEqual(0, len(b)) + + # prepare and send data packets + # expect to receive them decrypted + if is_ip6: + ip_header = IPv6(src="1::3:1", dst=self.pg0.remote_ip6, hlim=20) + else: + ip_header = IP(src="10.11.3.1", dst=self.pg0.remote_ip4, ttl=20) + packet_len_opts = ( + 2500, # two buffers + 1500, # one buffer + 4500, # three buffers + 1910 if is_ip6 else 1950, # auth tag is not contiguous + ) + txs = [] + for l in packet_len_opts: + txs.append( + peer_1.mk_tunnel_header(self.pg1, is_ip6=is_ip6) + / Wireguard(message_type=4, reserved_zero=0) / WireguardTransport( - receiver_index=peers[i].sender, - counter=0, - encrypted_encapsulated_packet=d, + receiver_index=peer_1.sender, + counter=len(txs), + encrypted_encapsulated_packet=peer_1.encrypt_transport( + ip_header / UDP(sport=222, dport=223) / Raw(b"\xfe" * l) + ), ) ) - rxs = self.send_and_expect(self.pg1, [p], self.pg0) - for rx in rxs: - self.assertEqual(rx[IP].dst, self.pg0.remote_hosts[i].ip4) - self.assertEqual(rx[IP].ttl, 19) + rxs = self.send_and_expect(self.pg1, txs, self.pg0) - # send a packets that are routed into the tunnel - for i in range(NUM_IFS): - p = ( + # verify decrypted packets + for i, l in enumerate(packet_len_opts): + if is_ip6: + self.assertEqual(rxs[i][IPv6].dst, self.pg0.remote_ip6) + self.assertEqual(rxs[i][IPv6].hlim, ip_header.hlim - 1) + else: + self.assertEqual(rxs[i][IP].dst, self.pg0.remote_ip4) + self.assertEqual(rxs[i][IP].ttl, ip_header.ttl - 1) + self.assertEqual(len(rxs[i][Raw]), l) + self.assertEqual(bytes(rxs[i][Raw]), b"\xfe" * l) + + # prepare and send packets that will be rewritten into the wg interface + # expect data packets sent + if is_ip6: + ip_header = IPv6(src=self.pg0.remote_ip6, dst="1::3:2") + else: + ip_header = IP(src=self.pg0.remote_ip4, dst="10.11.3.2") + packet_len_opts = ( + 2500, # two buffers + 1500, # one buffer + 4500, # three buffers + 1980 if is_ip6 else 2000, # no free space to write auth tag + ) + txs = [] + for l in packet_len_opts: + txs.append( Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) - / IP(src=self.pg0.remote_hosts[i].ip4, dst="10.0.%d.4" % i) + / ip_header / UDP(sport=555, dport=556) - / Raw(b"\x00" * 80) + / Raw(b"\xfe" * l) ) + rxs = self.send_and_expect(self.pg0, txs, self.pg1) - rxs = self.send_and_expect(self.pg0, p * 64, self.pg1) + # verify the data packets + rxs_decrypted = peer_1.validate_encapped( + rxs, ip_header, is_tunnel_ip6=is_ip6, is_transport_ip6=is_ip6 + ) - for rx in rxs: - rx = IP(peers[i].decrypt_transport(rx)) + for i, l in enumerate(packet_len_opts): + self.assertEqual(len(rxs_decrypted[i][Raw]), l) + self.assertEqual(bytes(rxs_decrypted[i][Raw]), b"\xfe" * l) - # check the oringial packet is present - self.assertEqual(rx[IP].dst, p[IP].dst) - self.assertEqual(rx[IP].ttl, p[IP].ttl - 1) + # remove configs + r1.remove_vpp_config() + peer_1.remove_vpp_config() + wg0.remove_vpp_config() - # send packets into the tunnel - for i in range(NUM_IFS): - p = [ - ( - peers[i].mk_tunnel_header(self.pg1) - / Wireguard(message_type=4, reserved_zero=0) - / WireguardTransport( - receiver_index=peers[i].sender, - counter=ii + 1, - encrypted_encapsulated_packet=peers[i].encrypt_transport( - ( - IP( - src="10.0.%d.4" % i, - dst=self.pg0.remote_hosts[i].ip4, - ttl=20, - ) - / UDP(sport=222, dport=223) - / Raw() - ) - ), - ) - ) - for ii in range(64) - ] + def test_wg_large_packet_v4_sync(self): + """Large packet (v4, sync)""" + self._test_wg_large_packet_tmpl(is_async=False, is_ip6=False) - rxs = self.send_and_expect(self.pg1, p, self.pg0) + def test_wg_large_packet_v6_sync(self): + """Large packet (v6, sync)""" + self._test_wg_large_packet_tmpl(is_async=False, is_ip6=True) - for rx in rxs: - self.assertEqual(rx[IP].dst, self.pg0.remote_hosts[i].ip4) - self.assertEqual(rx[IP].ttl, 19) + def test_wg_large_packet_v4_async(self): + """Large packet (v4, async)""" + self._test_wg_large_packet_tmpl(is_async=True, is_ip6=False) - for r in routes: - r.remove_vpp_config() - for p in peers: - p.remove_vpp_config() - for i in wg_ifs: - i.remove_vpp_config() + def test_wg_large_packet_v6_async(self): + """Large packet (v6, async)""" + self._test_wg_large_packet_tmpl(is_async=True, is_ip6=True) - def test_wg_event(self): - """Test events""" - port = 12600 - ESTABLISHED_FLAG = ( - VppEnum.vl_api_wireguard_peer_flags_t.WIREGUARD_PEER_ESTABLISHED - ) - DEAD_FLAG = VppEnum.vl_api_wireguard_peer_flags_t.WIREGUARD_PEER_STATUS_DEAD + def test_wg_lack_of_buf_headroom(self): + """Lack of buffer's headroom (v6 vxlan over v6 wg)""" + port = 12323 - # Create interfaces - wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() - wg1 = VppWgInterface(self, self.pg2.local_ip4, port + 1).add_vpp_config() + # create wg interface + wg0 = VppWgInterface(self, self.pg1.local_ip6, port).add_vpp_config() wg0.admin_up() - wg1.admin_up() - - # Check peer counter - self.assertEqual(len(self.vapi.wireguard_peers_dump()), 0) + wg0.config_ip6() self.pg_enable_capture(self.pg_interfaces) self.pg_start() - # Create peers - NUM_PEERS = 2 - self.pg2.generate_remote_hosts(NUM_PEERS) - self.pg2.configure_ipv4_neighbors() - self.pg1.generate_remote_hosts(NUM_PEERS) - self.pg1.configure_ipv4_neighbors() + # create a peer + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip6, port + 1, ["::/0"] + ).add_vpp_config() + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) - peers_0 = [] - peers_1 = [] - routes_0 = [] - routes_1 = [] - for i in range(NUM_PEERS): - peers_0.append( - VppWgPeer( - self, - wg0, - self.pg1.remote_hosts[i].ip4, - port + 1 + i, - ["10.0.%d.4/32" % i], - ).add_vpp_config() - ) - routes_0.append( - VppIpRoute( - self, - "10.0.%d.4" % i, - 32, - [VppRoutePath(self.pg1.remote_hosts[i].ip4, wg0.sw_if_index)], - ).add_vpp_config() - ) + # create a route to enable communication between wg interface addresses + r1 = VppIpRoute( + self, wg0.remote_ip6, 128, [VppRoutePath("0.0.0.0", wg0.sw_if_index)] + ).add_vpp_config() - peers_1.append( - VppWgPeer( - self, - wg1, - self.pg2.remote_hosts[i].ip4, - port + 100 + i, - ["10.100.%d.4/32" % i], - ).add_vpp_config() - ) - routes_1.append( - VppIpRoute( - self, - "10.100.%d.4" % i, - 32, - [VppRoutePath(self.pg2.remote_hosts[i].ip4, wg1.sw_if_index)], - ).add_vpp_config() - ) + # wait for the peer to send a handshake initiation + rxs = self.pg1.get_capture(1, timeout=2) - self.assertEqual(len(self.vapi.wireguard_peers_dump()), NUM_PEERS * 2) + # prepare and send a handshake response + # expect a keepalive message + resp = peer_1.consume_init(rxs[0], self.pg1, is_ip6=True) + rxs = self.send_and_expect(self.pg1, [resp], self.pg1) - # Want events from the first perr of wg0 - # and from all wg1 peers - peers_0[0].want_events() - wg1.want_events() + # verify the keepalive message + b = peer_1.decrypt_transport(rxs[0], is_ip6=True) + self.assertEqual(0, len(b)) - for i in range(NUM_PEERS): - # send a valid handsake init for which we expect a response - p = peers_0[i].mk_handshake(self.pg1) - rx = self.send_and_expect(self.pg1, [p], self.pg1) - peers_0[i].consume_response(rx[0]) - if i == 0: - peers_0[0].wait_event(ESTABLISHED_FLAG) + # create vxlan interface over the wg interface + vxlan0 = VppVxlanTunnel(self, src=wg0.local_ip6, dst=wg0.remote_ip6, vni=1111) + vxlan0.add_vpp_config() - p = peers_1[i].mk_handshake(self.pg2) - rx = self.send_and_expect(self.pg2, [p], self.pg2) - peers_1[i].consume_response(rx[0]) + # create bridge domain + bd1 = VppBridgeDomain(self, bd_id=1) + bd1.add_vpp_config() - wg1.wait_events(ESTABLISHED_FLAG, [peers_1[0].index, peers_1[1].index]) + # add the vxlan interface and pg0 to the bridge domain + bd1_ports = ( + VppBridgeDomainPort(self, bd1, vxlan0).add_vpp_config(), + VppBridgeDomainPort(self, bd1, self.pg0).add_vpp_config(), + ) - # remove routes - for r in routes_0: - r.remove_vpp_config() - for r in routes_1: - r.remove_vpp_config() + # prepare and send packets that will be rewritten into the vxlan interface + # expect they to be rewritten into the wg interface then and data packets sent + tx = ( + Ether(dst="00:00:00:00:00:01", src="00:00:00:00:00:02") + / IPv6(src="::1", dst="::2", hlim=20) + / UDP(sport=1111, dport=1112) + / Raw(b"\xfe" * 1900) + ) + rxs = self.send_and_expect(self.pg0, [tx] * 5, self.pg1) - # remove peers - for i in range(NUM_PEERS): - self.assertTrue(peers_0[i].query_vpp_config()) - peers_0[i].remove_vpp_config() - if i == 0: - peers_0[i].wait_event(0) - peers_0[i].wait_event(DEAD_FLAG) - for p in peers_1: - self.assertTrue(p.query_vpp_config()) - p.remove_vpp_config() - p.wait_event(0) - p.wait_event(DEAD_FLAG) + # verify the data packet + for rx in rxs: + rx_decrypted = IPv6(peer_1.decrypt_transport(rx, is_ip6=True)) + + self.assertEqual(rx_decrypted[VXLAN].vni, vxlan0.vni) + inner = rx_decrypted[VXLAN].payload + # check the original packet is present + self.assertEqual(inner[IPv6].dst, tx[IPv6].dst) + self.assertEqual(inner[IPv6].hlim, tx[IPv6].hlim) + self.assertEqual(len(inner[Raw]), len(tx[Raw])) + self.assertEqual(bytes(inner[Raw]), bytes(tx[Raw])) + + # remove configs + for bdp in bd1_ports: + bdp.remove_vpp_config() + bd1.remove_vpp_config() + vxlan0.remove_vpp_config() + r1.remove_vpp_config() + peer_1.remove_vpp_config() wg0.remove_vpp_config() - wg1.remove_vpp_config() +@tag_fixme_vpp_debug class WireguardHandoffTests(TestWg): """Wireguard Tests in multi worker setup""" @@ -1663,6 +2769,9 @@ class WireguardHandoffTests(TestWg): wg0.admin_up() wg0.config_ip4() + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + peer_1 = VppWgPeer( self, wg0, self.pg1.remote_ip4, port + 1, ["10.11.2.0/24", "10.11.3.0/24"] ).add_vpp_config() @@ -1672,6 +2781,9 @@ class WireguardHandoffTests(TestWg): self, "10.11.3.0", 24, [VppRoutePath("10.11.3.1", wg0.sw_if_index)] ).add_vpp_config() + # skip the first automatic handshake + self.pg1.get_capture(1, timeout=HANDSHAKE_JITTER) + # send a valid handsake init for which we expect a response p = peer_1.mk_handshake(self.pg1) @@ -1737,7 +2849,7 @@ class WireguardHandoffTests(TestWg): self.assertEqual(rx[IP].ttl, 19) # send a packets that are routed into the tunnel - # from owrker 0 + # from worker 0 rxs = self.send_and_expect(self.pg0, pe * 255, self.pg1, worker=0) peer_1.validate_encapped(rxs, pe) @@ -1749,3 +2861,90 @@ class WireguardHandoffTests(TestWg): @unittest.skip("test disabled") def test_wg_multi_interface(self): """Multi-tunnel on the same port""" + + +@unittest.skipIf( + "wireguard" in config.excluded_plugins, "Exclude Wireguard plugin tests" +) +@tag_run_solo +class TestWgFIB(VppTestCase): + """Wireguard FIB Test Case""" + + @classmethod + def setUpClass(cls): + super(TestWgFIB, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + super(TestWgFIB, cls).tearDownClass() + + def setUp(self): + super(TestWgFIB, self).setUp() + + self.create_pg_interfaces(range(2)) + + for i in self.pg_interfaces: + i.admin_up() + i.config_ip4() + + def tearDown(self): + for i in self.pg_interfaces: + i.unconfig_ip4() + i.admin_down() + super(TestWgFIB, self).tearDown() + + def test_wg_fib_tracking(self): + """FIB tracking""" + port = 12323 + + # create wg interface + wg0 = VppWgInterface(self, self.pg1.local_ip4, port).add_vpp_config() + wg0.admin_up() + wg0.config_ip4() + + self.pg_enable_capture(self.pg_interfaces) + self.pg_start() + + # create a peer + peer_1 = VppWgPeer( + self, wg0, self.pg1.remote_ip4, port + 1, ["10.11.3.0/24"] + ).add_vpp_config() + self.assertEqual(len(self.vapi.wireguard_peers_dump()), 1) + + # create a route to rewrite traffic into the wg interface + r1 = VppIpRoute( + self, "10.11.3.0", 24, [VppRoutePath("10.11.3.1", wg0.sw_if_index)] + ).add_vpp_config() + + # resolve ARP and expect the adjacency to update + self.pg1.resolve_arp() + + # wait for the peer to send a handshake initiation + rxs = self.pg1.get_capture(2, timeout=6) + + # prepare and send a handshake response + # expect a keepalive message + resp = peer_1.consume_init(rxs[1], self.pg1) + rxs = self.send_and_expect(self.pg1, [resp], self.pg1) + + # verify the keepalive message + b = peer_1.decrypt_transport(rxs[0]) + self.assertEqual(0, len(b)) + + # prepare and send a packet that will be rewritten into the wg interface + # expect a data packet sent + p = ( + Ether(dst=self.pg0.local_mac, src=self.pg0.remote_mac) + / IP(src=self.pg0.remote_ip4, dst="10.11.3.2") + / UDP(sport=555, dport=556) + / Raw() + ) + rxs = self.send_and_expect(self.pg0, [p], self.pg1) + + # verify the data packet + peer_1.validate_encapped(rxs, p) + + # remove configs + r1.remove_vpp_config() + peer_1.remove_vpp_config() + wg0.remove_vpp_config()