Skip to content

Commit cf45c59

Browse files
authored
[dhcp_relay] DHCPv6 automatic test (sonic-net#3565)
What is the motivation for this PR? Add DHCPv6 automatic test infrastructure and PTF test. How did you do it? Enhance the minigraph_facts.py and minigraph_dpg.j2 to support DHCPv6 instances. Add DHCPv6 servers to lab.yml file. This will deploy on a DUT DHCPv6 servers on regression run. Develop a test with 3 test cases: test_dhcp_relay_default test_dhcp_relay_after_link_flap test_dhcp_relay_start_with_uplinks_down Develop a PTF runner test to simulate the traffic according to the test case from sonic-mgmt. How did you verify/test it? Use testbed-cli.sh to generate and deploy a minigraph with DHCPv6 instances on a switch. Run the test. This test depends on PR: [dhcp_relay] DHCP relay support for IPv6 sonic-buildimage#7772 Supported testbed topology if it's a new test case? T0 Signed-off-by: Shlomi Bitton <[email protected]>
1 parent 10f0fc8 commit cf45c59

File tree

5 files changed

+537
-3
lines changed

5 files changed

+537
-3
lines changed

ansible/group_vars/lab/lab.yml

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ snmp_servers: ['10.0.0.9']
3939
# dhcp relay servers
4040
dhcp_servers: ['192.0.0.1', '192.0.0.2', '192.0.0.3', '192.0.0.4']
4141

42+
# dhcpv6 relay servers
43+
dhcpv6_servers: ['fc02:2000::1', 'fc02:2000::2', 'fc02:2000::3', 'fc02:2000::4']
44+
4245
# snmp variables
4346
snmp_rocommunity: public
4447
snmp_location: testlab

ansible/library/minigraph_facts.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ def parse_dpg(dpg, hname):
363363

364364
vlanintfs = child.find(str(QName(ns, "VlanInterfaces")))
365365
dhcp_servers = []
366+
dhcpv6_servers = []
366367
vlans = {}
367368
for vintf in vlanintfs.findall(str(QName(ns, "VlanInterface"))):
368369
vintfname = vintf.find(str(QName(ns, "Name"))).text
@@ -375,6 +376,12 @@ def parse_dpg(dpg, hname):
375376
else:
376377
vlandhcpservers = ""
377378
dhcp_servers = vlandhcpservers.split(";")
379+
vintf_node = vintf.find(str(QName(ns, "Dhcpv6Relays")))
380+
if vintf_node is not None and vintf_node.text is not None:
381+
vlandhcpservers = vintf_node.text
382+
else:
383+
vlandhcpservers = ""
384+
dhcpv6_servers = vlandhcpservers.split(";")
378385
for i, member in enumerate(vmbr_list):
379386
# Skip PortChannel inside Vlan
380387
if member in pcs:
@@ -402,7 +409,7 @@ def parse_dpg(dpg, hname):
402409
if acl_intfs:
403410
acls[aclname] = acl_intfs
404411

405-
return intfs, lo_intfs, mgmt_intf, vlans, pcs, acls, dhcp_servers
412+
return intfs, lo_intfs, mgmt_intf, vlans, pcs, acls, dhcp_servers, dhcpv6_servers
406413
return None, None, None, None, None, None, None
407414

408415
def parse_cpg(cpg, hname):
@@ -578,6 +585,7 @@ def parse_xml(filename, hostname, asic_name=None):
578585
hostname = None
579586
syslog_servers = []
580587
dhcp_servers = []
588+
dhcpv6_servers = []
581589
ntp_servers = []
582590
mgmt_routes = []
583591
bgp_peers_with_range = []
@@ -608,7 +616,7 @@ def parse_xml(filename, hostname, asic_name=None):
608616
for child in root:
609617
if asic_name is None:
610618
if child.tag == str(QName(ns, "DpgDec")):
611-
(intfs, lo_intfs, mgmt_intf, vlans, pcs, acls, dhcp_servers) = parse_dpg(child, hostname)
619+
(intfs, lo_intfs, mgmt_intf, vlans, pcs, acls, dhcp_servers, dhcpv6_servers) = parse_dpg(child, hostname)
612620
elif child.tag == str(QName(ns, "CpgDec")):
613621
(bgp_sessions, bgp_asn, bgp_peers_with_range) = parse_cpg(child, hostname)
614622
elif child.tag == str(QName(ns, "PngDec")):
@@ -619,7 +627,7 @@ def parse_xml(filename, hostname, asic_name=None):
619627
(syslog_servers, ntp_servers, mgmt_routes, deployment_id) = parse_meta(child, hostname)
620628
else:
621629
if child.tag == str(QName(ns, "DpgDec")):
622-
(intfs, lo_intfs, mgmt_intf, vlans, pcs, acls, dhcp_servers) = parse_dpg(child, asic_name)
630+
(intfs, lo_intfs, mgmt_intf, vlans, pcs, acls, dhcp_servers, dhcpv6_servers) = parse_dpg(child, asic_name)
623631
host_lo_intfs = parse_host_loopback(child, hostname)
624632
elif child.tag == str(QName(ns, "CpgDec")):
625633
(bgp_sessions, bgp_asn, bgp_peers_with_range) = parse_cpg(child, asic_name)
@@ -700,6 +708,7 @@ def parse_xml(filename, hostname, asic_name=None):
700708
results['minigraph_mgmt'] = get_mgmt_info(devices, mgmt_dev, mgmt_port)
701709
results['syslog_servers'] = syslog_servers
702710
results['dhcp_servers'] = dhcp_servers
711+
results['dhcpv6_servers'] = dhcpv6_servers
703712
results['ntp_servers'] = ntp_servers
704713
results['forced_mgmt_routes'] = mgmt_routes
705714
results['deployment_id'] = deployment_id
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import ast
2+
import subprocess
3+
4+
# Packet Test Framework imports
5+
import ptf
6+
import ptf.packet as packet
7+
import ptf.testutils as testutils
8+
from ptf import config
9+
from ptf.base_tests import BaseTest
10+
from ptf.mask import Mask
11+
12+
IPv6 = scapy.layers.inet6.IPv6
13+
14+
class DataplaneBaseTest(BaseTest):
15+
def __init__(self):
16+
BaseTest.__init__(self)
17+
18+
def setUp(self):
19+
self.dataplane = ptf.dataplane_instance
20+
self.dataplane.flush()
21+
if config["log_dir"] is not None:
22+
filename = os.path.join(config["log_dir"], str(self)) + ".pcap"
23+
self.dataplane.start_pcap(filename)
24+
25+
def tearDown(self):
26+
if config["log_dir"] is not None:
27+
self.dataplane.stop_pcap()
28+
29+
"""
30+
This test simulates a new host booting up on the VLAN network of a ToR and
31+
requesting an IPv6 address via DHCPv6. Setup is as follows:
32+
- DHCP client is simulated by listening/sending on an interface connected to VLAN of ToR.
33+
- DHCP server is simulated by listening/sending on injected PTF interfaces which link
34+
ToR to leaves. This way we can listen for traffic sent from DHCP relay out to would-be DHCPv6 servers
35+
36+
This test performs the following functionality:
37+
1.) Simulated client broadcasts a DHCPv6 SOLICIT message.
38+
2.) Verify DHCP relay running on ToR receives the DHCPv6 SOLICIT message and send a DHCPv6 RELAY-FORWARD
39+
message encapsulating the client DHCPv6 SOLICIT message and relays it to all of its known DHCP servers.
40+
3.) Simulate DHCPv6 RELAY-REPLY message send from a DHCP server to the ToR encapsulating DHCPv6 ADVERTISE message.
41+
4.) Verify DHCP relay receives the DHCPv6 RELAY-REPLY message decapsulate it and forwards DHCPv6 ADVERTISE
42+
message to our simulated client.
43+
5.) Simulated client broadcasts a DHCPv6 REQUEST message.
44+
6.) Verify DHCP relay running on ToR receives the DHCPv6 REQUEST message and send a DHCPv6 RELAY-FORWARD
45+
message encapsulating the client DHCPv6 REQUEST message and relays it to all of its known DHCP servers.
46+
7.) Simulate DHCPv6 RELAY-REPLY message send from a DHCP server to the ToR encapsulating DHCPv6 REPLY message.
47+
8.) Verify DHCP relay receives the DHCPv6 RELAY-REPLY message decapsulate it and forwards DHCPv6 REPLY
48+
message to our simulated client.
49+
50+
"""
51+
52+
class DHCPTest(DataplaneBaseTest):
53+
54+
BROADCAST_MAC = '33:33:00:01:00:02'
55+
BROADCAST_IP = 'ff02::1:2'
56+
DHCP_CLIENT_PORT = 546
57+
DHCP_SERVER_PORT = 547
58+
59+
def __init__(self):
60+
self.test_params = testutils.test_params_get()
61+
self.client_port_index = int(self.test_params['client_port_index'])
62+
self.client_link_local = self.generate_client_interace_ipv6_link_local_address(self.client_port_index)
63+
64+
DataplaneBaseTest.__init__(self)
65+
66+
def setUp(self):
67+
DataplaneBaseTest.setUp(self)
68+
self.hostname = self.test_params['hostname']
69+
70+
# These are the interfaces we are injected into that link to out leaf switches
71+
self.server_port_indices = ast.literal_eval(self.test_params['leaf_port_indices'])
72+
self.num_dhcp_servers = int(self.test_params['num_dhcp_servers'])
73+
self.assertTrue(self.num_dhcp_servers > 0,
74+
"Error: This test requires at least one DHCP server to be specified!")
75+
76+
# We will simulate a responding DHCP server on the first interface in the provided set
77+
self.server_ip = self.test_params['server_ip']
78+
79+
self.relay_iface_ip = self.test_params['relay_iface_ip']
80+
self.relay_iface_mac = self.test_params['relay_iface_mac']
81+
self.relay_link_local = self.test_params['relay_link_local']
82+
83+
self.vlan_ip = self.test_params['vlan_ip']
84+
85+
self.client_mac = self.dataplane.get_mac(0, self.client_port_index)
86+
87+
def generate_client_interace_ipv6_link_local_address(self, client_port_index):
88+
# Shutdown and startup the client interface to generate a proper IPv6 link-local address
89+
command = "ifconfig eth{} down".format(client_port_index)
90+
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
91+
proc.communicate()
92+
93+
command = "ifconfig eth{} up".format(client_port_index)
94+
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
95+
proc.communicate()
96+
97+
command = "ip addr show eth{} | grep inet6 | grep 'scope link' | awk '{{print $2}}' | cut -d '/' -f1".format(client_port_index)
98+
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
99+
stdout, stderr = proc.communicate()
100+
101+
return stdout.strip()
102+
103+
def tearDown(self):
104+
DataplaneBaseTest.tearDown(self)
105+
106+
107+
"""
108+
Packet generation functions/wrappers
109+
110+
"""
111+
112+
def create_dhcp_solicit_packet(self):
113+
114+
solicit_packet = Ether(src=self.client_mac, dst=self.BROADCAST_MAC)
115+
solicit_packet /= IPv6(src=self.client_link_local, dst=self.BROADCAST_IP)
116+
solicit_packet /= UDP(sport=self.DHCP_CLIENT_PORT, dport=self.DHCP_SERVER_PORT)
117+
solicit_packet /= DHCP6_Solicit(trid=12345)
118+
119+
return solicit_packet
120+
121+
def create_dhcp_solicit_relay_forward_packet(self):
122+
123+
solicit_relay_forward_packet = Ether(src=self.relay_iface_mac)
124+
solicit_relay_forward_packet /= IPv6()
125+
solicit_relay_forward_packet /= UDP(sport=self.DHCP_SERVER_PORT, dport=self.DHCP_SERVER_PORT)
126+
solicit_relay_forward_packet /= DHCP6_RelayForward(msgtype=12, linkaddr=self.vlan_ip, peeraddr=self.client_link_local)
127+
solicit_relay_forward_packet /= DHCP6OptRelayMsg()
128+
solicit_relay_forward_packet /= DHCP6_Solicit(trid=12345)
129+
130+
return solicit_relay_forward_packet
131+
132+
def create_dhcp_advertise_packet(self):
133+
134+
advertise_packet = Ether(src=self.relay_iface_mac, dst=self.client_mac)
135+
advertise_packet /= IPv6(src=self.relay_link_local, dst=self.client_link_local)
136+
advertise_packet /= UDP(sport=self.DHCP_SERVER_PORT, dport=self.DHCP_CLIENT_PORT)
137+
advertise_packet /= DHCP6_Advertise(trid=12345)
138+
139+
return advertise_packet
140+
141+
def create_dhcp_advertise_relay_reply_packet(self):
142+
143+
advertise_relay_reply_packet = Ether(dst=self.relay_iface_mac)
144+
advertise_relay_reply_packet /= IPv6(src=self.server_ip, dst=self.relay_iface_ip)
145+
advertise_relay_reply_packet /= UDP(sport=self.DHCP_SERVER_PORT, dport=self.DHCP_SERVER_PORT)
146+
advertise_relay_reply_packet /= DHCP6_RelayReply(msgtype=13, linkaddr=self.vlan_ip, peeraddr=self.client_link_local)
147+
advertise_relay_reply_packet /= DHCP6OptRelayMsg()
148+
advertise_relay_reply_packet /= DHCP6_Advertise(trid=12345)
149+
150+
return advertise_relay_reply_packet
151+
152+
def create_dhcp_request_packet(self):
153+
154+
request_packet = Ether(src=self.client_mac, dst=self.BROADCAST_MAC)
155+
request_packet /= IPv6(src=self.client_link_local, dst=self.BROADCAST_IP)
156+
request_packet /= UDP(sport=self.DHCP_CLIENT_PORT, dport=self.DHCP_SERVER_PORT)
157+
request_packet /= DHCP6_Request(trid=12345)
158+
159+
return request_packet
160+
161+
def create_dhcp_request_relay_forward_packet(self):
162+
163+
request_relay_forward_packet = Ether(src=self.relay_iface_mac)
164+
request_relay_forward_packet /= IPv6()
165+
request_relay_forward_packet /= UDP(sport=self.DHCP_SERVER_PORT, dport=self.DHCP_SERVER_PORT)
166+
request_relay_forward_packet /= DHCP6_RelayForward(msgtype=12, linkaddr=self.vlan_ip, peeraddr=self.client_link_local)
167+
request_relay_forward_packet /= DHCP6OptRelayMsg()
168+
request_relay_forward_packet /= DHCP6_Request(trid=12345)
169+
170+
return request_relay_forward_packet
171+
172+
def create_dhcp_reply_packet(self):
173+
174+
reply_packet = Ether(src=self.relay_iface_mac, dst=self.client_mac)
175+
reply_packet /= IPv6(src=self.relay_link_local, dst=self.client_link_local)
176+
reply_packet /= UDP(sport=self.DHCP_SERVER_PORT, dport=self.DHCP_CLIENT_PORT)
177+
reply_packet /= DHCP6_Reply(trid=12345)
178+
179+
return reply_packet
180+
181+
def create_dhcp_reply_relay_reply_packet(self):
182+
183+
reply_relay_reply_packet = Ether(dst=self.relay_iface_mac)
184+
reply_relay_reply_packet /= IPv6(src=self.server_ip, dst=self.relay_iface_ip)
185+
reply_relay_reply_packet /= UDP(sport=self.DHCP_SERVER_PORT, dport=self.DHCP_SERVER_PORT)
186+
reply_relay_reply_packet /= DHCP6_RelayReply(msgtype=13, linkaddr=self.vlan_ip, peeraddr=self.client_link_local)
187+
reply_relay_reply_packet /= DHCP6OptRelayMsg()
188+
reply_relay_reply_packet /= DHCP6_Reply(trid=12345)
189+
190+
return reply_relay_reply_packet
191+
192+
193+
"""
194+
Send/receive functions
195+
196+
"""
197+
198+
# Simulate client connecting on VLAN and broadcasting a DHCPv6 SOLICIT message
199+
def client_send_solicit(self):
200+
# Form and send DHCPv6 SOLICIT packet
201+
solicit_packet = self.create_dhcp_solicit_packet()
202+
testutils.send_packet(self, self.client_port_index, solicit_packet)
203+
204+
# Verify that the DHCP relay actually received and relayed the DHCPv6 SOLICIT message to all of
205+
# its known DHCP servers.
206+
def verify_relayed_solicit_relay_forward(self):
207+
# Create a packet resembling a DHCPv6 RELAY-FORWARD encapsulating SOLICIT packet
208+
solicit_relay_forward_packet = self.create_dhcp_solicit_relay_forward_packet()
209+
210+
# Mask off fields we don't care about matching
211+
masked_packet = Mask(solicit_relay_forward_packet)
212+
masked_packet.set_do_not_care_scapy(packet.Ether, "dst")
213+
masked_packet.set_do_not_care_scapy(IPv6, "src")
214+
masked_packet.set_do_not_care_scapy(IPv6, "dst")
215+
masked_packet.set_do_not_care_scapy(IPv6, "fl")
216+
masked_packet.set_do_not_care_scapy(IPv6, "tc")
217+
masked_packet.set_do_not_care_scapy(IPv6, "plen")
218+
masked_packet.set_do_not_care_scapy(IPv6, "nh")
219+
masked_packet.set_do_not_care_scapy(packet.UDP, "chksum")
220+
masked_packet.set_do_not_care_scapy(packet.UDP, "len")
221+
222+
# Count the number of these packets received on the ports connected to our leaves
223+
solicit_count = testutils.count_matched_packets_all_ports(self, masked_packet, self.server_port_indices)
224+
self.assertTrue(solicit_count >= 1,
225+
"Failed: Solicit count of %d" % (solicit_count))
226+
227+
# Simulate a DHCP server sending a DHCPv6 RELAY-REPLY encapsulating ADVERTISE packet message to client.
228+
# We do this by injecting a RELAY-REPLY encapsulating ADVERTISE message on the link connected to one
229+
# of our leaf switches.
230+
def server_send_advertise_relay_reply(self):
231+
# Form and send DHCPv6 RELAY-REPLY encapsulating ADVERTISE packet
232+
advertise_relay_reply_packet = self.create_dhcp_advertise_relay_reply_packet()
233+
testutils.send_packet(self, self.server_port_indices[0], advertise_relay_reply_packet)
234+
235+
# Verify that the DHCPv6 ADVERTISE would be received by our simulated client
236+
def verify_relayed_advertise(self):
237+
# Create a packet resembling a DHCPv6 ADVERTISE packet
238+
advertise_packet = self.create_dhcp_advertise_packet()
239+
240+
# Mask off fields we don't care about matching
241+
masked_packet = Mask(advertise_packet)
242+
masked_packet.set_do_not_care_scapy(IPv6, "fl")
243+
masked_packet.set_do_not_care_scapy(packet.UDP, "chksum")
244+
masked_packet.set_do_not_care_scapy(packet.UDP, "len")
245+
246+
# NOTE: verify_packet() will fail for us via an assert, so no need to check a return value here
247+
testutils.verify_packet(self, masked_packet, self.client_port_index)
248+
249+
# Simulate our client sending a DHCPv6 REQUEST message
250+
def client_send_request(self):
251+
# Form and send DHCPv6 REQUEST packet
252+
request_packet = self.create_dhcp_request_packet()
253+
testutils.send_packet(self, self.client_port_index, request_packet)
254+
255+
# Verify that the DHCP relay actually received and relayed the DHCPv6 REQUEST message to all of
256+
# its known DHCP servers.
257+
def verify_relayed_request_relay_forward(self):
258+
# Create a packet resembling a DHCPv6 RELAY-FORWARD encapsulating REQUEST packet
259+
request_relay_forward_packet = self.create_dhcp_request_relay_forward_packet()
260+
261+
# Mask off fields we don't care about matching
262+
masked_packet = Mask(request_relay_forward_packet)
263+
masked_packet.set_do_not_care_scapy(packet.Ether, "dst")
264+
masked_packet.set_do_not_care_scapy(IPv6, "src")
265+
masked_packet.set_do_not_care_scapy(IPv6, "dst")
266+
masked_packet.set_do_not_care_scapy(IPv6, "fl")
267+
masked_packet.set_do_not_care_scapy(IPv6, "tc")
268+
masked_packet.set_do_not_care_scapy(IPv6, "plen")
269+
masked_packet.set_do_not_care_scapy(IPv6, "nh")
270+
masked_packet.set_do_not_care_scapy(packet.UDP, "chksum")
271+
masked_packet.set_do_not_care_scapy(packet.UDP, "len")
272+
273+
# Count the number of these packets received on the ports connected to our leaves
274+
request_count = testutils.count_matched_packets_all_ports(self, masked_packet, self.server_port_indices)
275+
self.assertTrue(request_count >= 1,
276+
"Failed: Request count of %d" % (request_count))
277+
278+
# Simulate a DHCP server sending a DHCPv6 RELAY-REPLY encapsulating REPLY packet message to client.
279+
def server_send_reply_relay_reply(self):
280+
# Form and send DHCPv6 RELAY-REPLY encapsulating REPLY packet
281+
reply_relay_reply_packet = self.create_dhcp_reply_relay_reply_packet()
282+
testutils.send_packet(self, self.server_port_indices[0], reply_relay_reply_packet)
283+
284+
# Verify that the DHCPv6 REPLY would be received by our simulated client
285+
def verify_relayed_reply(self):
286+
# Create a packet resembling a DHCPv6 REPLY packet
287+
reply_packet = self.create_dhcp_reply_packet()
288+
289+
# Mask off fields we don't care about matching
290+
masked_packet = Mask(reply_packet)
291+
masked_packet.set_do_not_care_scapy(IPv6, "fl")
292+
masked_packet.set_do_not_care_scapy(packet.UDP, "chksum")
293+
masked_packet.set_do_not_care_scapy(packet.UDP, "len")
294+
295+
# NOTE: verify_packet() will fail for us via an assert, so no need to check a return value here
296+
testutils.verify_packet(self, masked_packet, self.client_port_index)
297+
298+
def runTest(self):
299+
self.client_send_solicit()
300+
self.verify_relayed_solicit_relay_forward()
301+
self.server_send_advertise_relay_reply()
302+
self.verify_relayed_advertise()
303+
self.client_send_request()
304+
self.verify_relayed_request_relay_forward()
305+
self.server_send_reply_relay_reply()
306+
self.verify_relayed_reply()

ansible/templates/minigraph_dpg.j2

+2
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@
130130
{% endif %}
131131
{% set dhcp_servers_str=';'.join(dhcp_servers) %}
132132
<DhcpRelays>{{ dhcp_servers_str }}</DhcpRelays>
133+
{% set dhcpv6_servers_str=';'.join(dhcpv6_servers) %}
134+
<Dhcpv6Relays>{{ dhcpv6_servers_str }}</Dhcpv6Relays>
133135
<VlanID>{{ vlan_param['id'] }}</VlanID>
134136
<Tag>{{ vlan_param['tag'] }}</Tag>
135137
<Subnets>{{ vlan_param['prefix'] | ipaddr('network') }}/{{ vlan_param['prefix'] | ipaddr('prefix') }}</Subnets>

0 commit comments

Comments
 (0)