Skip to content

Commit

Permalink
[T2][macsec] Fixes to tests for running them with macsec enabled topo…
Browse files Browse the repository at this point in the history
…logy (#15905)

Run ptfhost and ptfrunner based tests with macsec enabled topology.
Enable send and receive macsec encrypted frames by overloading the testutils.send_packet and testutils.dp_poll APIs
  • Loading branch information
judyjoseph authored Feb 13, 2025
1 parent 2efa2f2 commit 052724b
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 54 deletions.
6 changes: 0 additions & 6 deletions ansible/roles/test/files/ptftests/fib_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import json
import itertools
import fib
import macsec

import ptf
import ptf.packet as scapy
Expand Down Expand Up @@ -215,11 +214,6 @@ def get_src_and_exp_ports(self, dst_ip):
if src_port in exp_port_list:
break
else:
# MACsec link only receive encrypted packets
# It's hard to simulate encrypted packets on the injected port
# Because the MACsec is session based channel but the injected ports are stateless ports
if src_port in macsec.MACSEC_INFOS.keys():
continue
if self.switch_type == "chassis-packet":
exp_port_lists = self.check_same_asic(src_port, exp_port_lists)
elif self.single_fib == "single-fib-single-hop" and exp_port_lists[0]:
Expand Down
45 changes: 44 additions & 1 deletion ansible/roles/test/files/ptftests/macsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,50 @@
import scapy.contrib.macsec as scapy_macsec

MACSEC_INFO_FILE = "macsec_info.pickle"
MACSEC_GLOBAL_PN_OFFSET = 1000
MACSEC_GLOBAL_PN_INCR = 100

MACSEC_INFOS = {}


def macsec_send(test, port_id, pkt, count=1):
# Check if the port is macsec enabled, if so send the macsec encap/encrypted frame
global MACSEC_GLOBAL_PN_OFFSET
global MACSEC_GLOBAL_PN_INCR

device, port_number = ptf.testutils.port_to_tuple(port_id)
if port_number in MACSEC_INFOS and MACSEC_INFOS[port_number]:
encrypt, send_sci, xpn_en, sci, an, sak, ssci, salt, peer_sci, peer_an, peer_ssci, pn = \
MACSEC_INFOS[port_number]

# Increment the PN by an offset so that the macsec frames are not late on DUT
pn += MACSEC_GLOBAL_PN_OFFSET
MACSEC_GLOBAL_PN_OFFSET += MACSEC_GLOBAL_PN_INCR

macsec_pkt = encap_macsec_pkt(pkt, peer_sci, peer_an, sak, encrypt, send_sci, pn, xpn_en, peer_ssci, salt)
# send the packet
__origin_send_packet(test, port_id, macsec_pkt, count)
else:
# send the packet
__origin_send_packet(test, port_id, pkt, count)


def encap_macsec_pkt(macsec_pkt, sci, an, sak, encrypt, send_sci, pn, xpn_en=False, ssci=None, salt=None):
sa = scapy_macsec.MACsecSA(sci=sci,
an=an,
pn=pn,
key=sak,
icvlen=16,
encrypt=encrypt,
send_sci=send_sci,
xpn_en=xpn_en,
ssci=ssci,
salt=salt)
macsec_pkt = sa.encap(macsec_pkt)
pkt = sa.encrypt(macsec_pkt)
return pkt


def __decap_macsec_pkt(macsec_pkt, sci, an, sak, encrypt, send_sci, pn, xpn_en=False, ssci=None, salt=None):
sa = scapy_macsec.MACsecSA(sci=sci,
an=an,
Expand Down Expand Up @@ -64,7 +104,8 @@ def __macsec_dp_poll(test, device_number=0, port_number=None, timeout=None, exp_
else:
continue
if ret.port in MACSEC_INFOS and MACSEC_INFOS[ret.port]:
encrypt, send_sci, xpn_en, sci, an, sak, ssci, salt = MACSEC_INFOS[ret.port]
encrypt, send_sci, xpn_en, sci, an, sak, ssci, salt, peer_sci, peer_an, peer_ssci, pn = \
MACSEC_INFOS[ret.port]
pkt, decap_success = __decap_macsec_pkt(
pkt, sci, an, sak, encrypt, send_sci, 0, xpn_en, ssci, salt)
if decap_success and ptf.dataplane.match_exp_pkt(exp_pkt, pkt):
Expand All @@ -82,3 +123,5 @@ def __macsec_dp_poll(test, device_number=0, port_number=None, timeout=None, exp_
if MACSEC_INFOS:
__origin_dp_poll = ptf.testutils.dp_poll
ptf.testutils.dp_poll = __macsec_dp_poll
__origin_send_packet = ptf.testutils.send_packet
ptf.testutils.send_packet = macsec_send
6 changes: 0 additions & 6 deletions ansible/roles/test/files/ptftests/py3/IP_decap_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
import itertools
import fib
import time
import macsec

import ptf
import ptf.packet as scapy
Expand Down Expand Up @@ -525,11 +524,6 @@ def get_src_and_exp_ports(self, dst_ip):
if src_port in exp_port_list:
break
else:
# MACsec link only receive encrypted packets
# It's hard to simulate encrypted packets on the injected port
# Because the MACsec is session based channel but the injected ports are stateless ports
if src_port in macsec.MACSEC_INFOS.keys():
continue
if self.single_fib == "single-fib-single-hop" and exp_port_lists[0]:
dest_port_dut_index = self.ptf_test_port_map[str(exp_port_lists[0][0])]['target_dut'][0]
src_port_dut_index = self.ptf_test_port_map[str(src_port)]['target_dut'][0]
Expand Down
6 changes: 0 additions & 6 deletions ansible/roles/test/files/ptftests/py3/hash_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@

import fib
import lpm
import macsec


class HashTest(BaseTest):
Expand Down Expand Up @@ -136,11 +135,6 @@ def get_src_and_exp_ports(self, dst_ip):
if src_port in exp_port_list:
break
else:
# MACsec link only receive encrypted packets
# It's hard to simulate encrypted packets on the injected port
# Because the MACsec is session based channel but the injected ports are stateless ports
if src_port in macsec.MACSEC_INFOS.keys():
continue
if self.single_fib == "single-fib-single-hop" and exp_port_lists[0]:
dest_port_dut_index = self.ptf_test_port_map[str(exp_port_lists[0][0])]['target_dut'][0]
src_port_dut_index = self.ptf_test_port_map[str(src_port)]['target_dut'][0]
Expand Down
2 changes: 1 addition & 1 deletion tests/common/macsec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def __startup_macsec():
setup_macsec_configuration(macsec_duthost, ctrl_links,
profile['name'], profile['priority'], profile['cipher_suite'],
profile['primary_cak'], profile['primary_ckn'], profile['policy'],
profile['send_sci'], profile['rekey_period'])
profile['send_sci'], profile['rekey_period'], tbinfo)
logger.info(
"Setup MACsec configuration with arguments:\n{}".format(locals()))
return __startup_macsec
Expand Down
24 changes: 21 additions & 3 deletions tests/common/macsec/macsec_config_helper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import logging
import time

from tests.common.macsec.macsec_helper import get_mka_session, getns_prefix, wait_all_complete, submit_async_task
from tests.common.macsec.macsec_helper import get_mka_session, getns_prefix, wait_all_complete, \
submit_async_task, load_all_macsec_info
from tests.common.macsec.macsec_platform_helper import global_cmd, find_portchannel_from_member, get_portchannel
from tests.common.devices.eos import EosHost
from tests.common.utilities import wait_until
Expand All @@ -14,12 +15,26 @@
'set_macsec_profile',
'delete_macsec_profile',
'enable_macsec_port',
'disable_macsec_port'
'disable_macsec_port',
'get_macsec_enable_status',
'get_macsec_profile'
]

logger = logging.getLogger(__name__)


def get_macsec_enable_status(host):
# Retrieve the enable_macsec flag passed by user for this testrun
request = host.duthosts.request
return request.config.getoption("--enable_macsec", default=False)


def get_macsec_profile(host):
# Retrieve the macsec_profile passed by user for this testrun
request = host.duthosts.request
return request.config.getoption("--macsec_profile", default=None)


def set_macsec_profile(host, port, profile_name, priority, cipher_suite,
primary_cak, primary_ckn, policy, send_sci, rekey_period=0):
if isinstance(host, EosHost):
Expand Down Expand Up @@ -184,7 +199,7 @@ def cleanup_macsec_configuration(duthost, ctrl_links, profile_name):


def setup_macsec_configuration(duthost, ctrl_links, profile_name, default_priority,
cipher_suite, primary_cak, primary_ckn, policy, send_sci, rekey_period):
cipher_suite, primary_cak, primary_ckn, policy, send_sci, rekey_period, tbinfo):
logger.info("Setup macsec configuration step1: set macsec profile")
# 1. Set macsec profile
i = 0
Expand Down Expand Up @@ -220,3 +235,6 @@ def setup_macsec_configuration(duthost, ctrl_links, profile_name, default_priori
# protocols. To hold some time for protocol recovery.
time.sleep(60)
logger.info("Setup macsec configuration finished")

# Load the MACSEC_INFO, to have data of all macsec sessions
load_all_macsec_info(duthost, ctrl_links, tbinfo)
103 changes: 86 additions & 17 deletions tests/common/macsec/macsec_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
import re
import json
import logging
import struct
import time
from collections import defaultdict, deque
from collections import defaultdict, deque, Counter
from multiprocessing import Process

import cryptography.exceptions
Expand All @@ -17,6 +16,7 @@

from tests.common.macsec.macsec_platform_helper import sonic_db_cli
from tests.common.devices.eos import EosHost
from tests.common.utilities import convert_scapy_packet_to_bytes

__all__ = [
'check_wpa_supplicant_process',
Expand Down Expand Up @@ -381,12 +381,48 @@ def get_macsec_attr(host, port):
sak = binascii.unhexlify(macsec_sa["sak"])
sci = int(get_sci(eth_src), 16)
if xpn_en:
ssci = struct.pack('!I', int(macsec_sa["ssci"]))
ssci = int(macsec_sa["ssci"])
salt = binascii.unhexlify(macsec_sa["salt"])
else:
ssci = None
salt = None
return encrypt, send_sci, xpn_en, sci, an, sak, ssci, salt

# Get the peer sci and an from the ingress macsec SA name
asic = host.get_port_asic_instance(port)
macsec_ingress_sa_name = get_macsec_sa_name(asic, port, False)
peer_sci = macsec_ingress_sa_name.split(':')[1]
peer_an = macsec_ingress_sa_name.split(':')[2]

# Get the ingress macsec sa
macsec_ingress_sa = sonic_db_cli(
host, QUERY_MACSEC_INGRESS_SA.format(getns_prefix(host, port), port, peer_sci, peer_an))
if xpn_en:
peer_ssci = int(macsec_ingress_sa["ssci"])
else:
peer_ssci = None

# Get the packet number
ns = host.get_namespace_from_asic_id(asic.asic_index) if host.is_multi_asic else ''
counters = Counter(get_macsec_counters(asic, ns, macsec_ingress_sa_name))
pn = counters['SAI_MACSEC_SA_ATTR_CURRENT_XPN']

return encrypt, send_sci, xpn_en, sci, an, sak, ssci, salt, int(peer_sci, 16), int(peer_an), peer_ssci, pn


def encap_macsec_pkt(macsec_pkt, sci, an, sak, encrypt, send_sci, pn, xpn_en=False, ssci=None, salt=None):
sa = scapy_macsec.MACsecSA(sci=sci,
an=an,
pn=pn,
key=sak,
icvlen=16,
encrypt=encrypt,
send_sci=send_sci,
xpn_en=xpn_en,
ssci=ssci,
salt=salt)
macsec_pkt = sa.encap(macsec_pkt)
pkt = sa.encrypt(macsec_pkt)
return pkt


def decap_macsec_pkt(macsec_pkt, sci, an, sak, encrypt, send_sci, pn, xpn_en=False, ssci=None, salt=None):
Expand All @@ -406,7 +442,7 @@ def decap_macsec_pkt(macsec_pkt, sci, an, sak, encrypt, send_sci, pn, xpn_en=Fal
# Invalid MACsec packets
return macsec_pkt, False
pkt = sa.decap(pkt)
return pkt, True
return convert_scapy_packet_to_bytes(pkt), True


def check_macsec_pkt(test, ptf_port_id, exp_pkt, timeout=3):
Expand All @@ -432,6 +468,35 @@ def load_macsec_info(duthost, port, force_reload=None):
return __macsec_infos[port]


# This API load the macsec session details from all ctrl links
def load_all_macsec_info(duthost, ctrl_links, tbinfo):
mg_facts = duthost.get_extended_minigraph_facts(tbinfo)
for port, nbr in ctrl_links.items():
ptf_id = mg_facts["minigraph_ptf_indices"][port]
MACSEC_INFO[ptf_id] = get_macsec_attr(duthost, port)


def macsec_send(test, port_id, pkt, count=1):
global MACSEC_GLOBAL_PN_OFFSET
global MACSEC_GLOBAL_PN_INCR

# Check if the port is macsec enabled, if so send the macsec encap/encrypted frame
device, port_number = testutils.port_to_tuple(port_id)
if port_number in MACSEC_INFO and MACSEC_INFO[port_number]:
encrypt, send_sci, xpn_en, sci, an, sak, ssci, salt, peer_sci, peer_an, peer_ssci, pn = MACSEC_INFO[port_number]

# Increment the PN in packet so that the packet s not marked as late in DUT
pn += MACSEC_GLOBAL_PN_OFFSET
MACSEC_GLOBAL_PN_OFFSET += MACSEC_GLOBAL_PN_INCR

macsec_pkt = encap_macsec_pkt(pkt, peer_sci, peer_an, sak, encrypt, send_sci, pn, xpn_en, peer_ssci, salt)
# send the packet
__origin_send_packet(test, port_id, macsec_pkt, count)
else:
# send the packet
__origin_send_packet(test, port_id, pkt, count)


def macsec_dp_poll(test, device_number=0, port_number=None, timeout=None, exp_pkt=None):
recent_packets = deque(maxlen=test.dataplane.POLL_MAX_RECENT_PACKETS)
packet_count = 0
Expand Down Expand Up @@ -463,21 +528,20 @@ def macsec_dp_poll(test, device_number=0, port_number=None, timeout=None, exp_pk
if ptf.dataplane.match_exp_pkt(exp_pkt, pkt):
return ret
else:
macsec_info = load_macsec_info(test.duthost, find_portname_from_ptf_id(test.mg_facts, ret.port),
force_reload[ret.port])
if macsec_info:
encrypt, send_sci, xpn_en, sci, an, sak, ssci, salt = macsec_info
if ret.port in MACSEC_INFO and MACSEC_INFO[ret.port]:
encrypt, send_sci, xpn_en, sci, an, sak, ssci, salt, peer_sci, peer_an, peer_ssci, pn = \
MACSEC_INFO[ret.port]
force_reload[ret.port] = False
pkt, decap_success = decap_macsec_pkt(pkt, sci, an, sak, encrypt, send_sci, 0, xpn_en, ssci, salt)
if decap_success and ptf.dataplane.match_exp_pkt(exp_pkt, pkt):
return ret
# Normally, if __origin_dp_poll returns a PollFailure,
# the PollFailure object will contain a list of recently received packets
# to help with debugging. However, since we call __origin_dp_poll multiple times,
# only the packets from the most recent call is retained.
# If we don't find a matching packet (either with or without MACsec decoding),
# we need to manually store the packet we received.
# Later if we return a PollFailure,
# Here we explicitly create the PollSuccess struct and send the pkt which us decoded
# and the caller test can validate the pkt fields. Without this fix in case of macsec
# the encrypted packet is being send back to caller which it will not be able to dissect
return test.dataplane.PollSuccess(ret.device, ret.port, pkt, exp_pkt, time.time())
# Normally, if __origin_dp_poll returns a PollFailure, the PollFailure object will contain a list of
# recently received packets to help with debugging. However, since we call __origin_dp_poll multiple times,
# only the packets from the most recent call is retained. If we don't find a matching packet (either with or
# without MACsec decoding), we need to manually store the packet we received. Later if we return a PollFailure,
# we can provide the received packets to emulate the behavior of __origin_dp_poll.
recent_packets.append(pkt)
packet_count += 1
Expand Down Expand Up @@ -587,5 +651,10 @@ def clear_macsec_counters(duthost):


__origin_dp_poll = testutils.dp_poll
__origin_send_packet = testutils.send_packet
__macsec_infos = defaultdict(lambda: None)
MACSEC_INFO = defaultdict(lambda: None)
MACSEC_GLOBAL_PN_OFFSET = 1000
MACSEC_GLOBAL_PN_INCR = 100
testutils.dp_poll = macsec_dp_poll
testutils.send_packet = macsec_send
3 changes: 3 additions & 0 deletions tests/common/plugins/conditional_mark/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,9 @@ def load_basic_facts(dut_name, session):

# Load possible other facts here

# Check if the testrun has enable_macsec parameter set
results['macsec_en'] = session.config.getoption("--enable_macsec", False)

return results


Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#######################################
##### cutsom_acl #####
#######################################
acl:
skip:
reason: "Acl testsuite is still not enabled for macsec enable sonic-mgmt run"
conditions:
- "macsec_en==True"

acl/custom_acl_table/test_custom_acl_table.py:
skip:
reason: "Custom ACL not supported on older releases or dualtor setup"
Expand Down Expand Up @@ -1643,6 +1649,7 @@ qos:
conditions_logical_operator: or
conditions:
- "topo_type in ['m0', 'mx']"
- "macsec_en==True"

qos/test_buffer.py:
skip:
Expand Down
4 changes: 3 additions & 1 deletion tests/common/plugins/ptfadapter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ def override_ptf_functions():
# code for updating the packet pattern before send it out. Generally we want to make the payload part of injected
# packet to have string of current test module and case name. While inspecting the captured packets, it is easier
# to fiture out which packets are injected by which test case.
origin_send_packet = ptf.testutils.send_packet

def _send(test, port_id, pkt, count=1):
update_payload = getattr(test, "update_payload", None)
if update_payload and callable(update_payload):
pkt = test.update_payload(pkt)

return ptf.testutils.send_packet(test, port_id, pkt, count=count)
return origin_send_packet(test, port_id, pkt, count=count)
setattr(ptf.testutils, "send", _send)

# Below code is to override the 'dp_poll' function in the ptf.testutils module. This function is called by all
Expand Down
Loading

0 comments on commit 052724b

Please sign in to comment.