blob: cd883e481c6a836e3da0275fedcb6bdb0075a2f3 [file] [log] [blame]
Daniele Moro80889562021-09-08 10:09:26 +02001from distutils.util import strtobool
2
3FALSE = '0'
4TRUE = '1'
5DIR_UPLINK = '1'
6DIR_DOWNLINK = '2'
7IFACE_ACCESS = '1'
8IFACE_CORE = '2'
9TUNNEL_SPORT = '2152'
10TUNNEL_TYPE_GPDU = '3'
11
12UE_PORT = 400
13PDN_PORT = 800
14GPDU_PORT = 2152
15
16
17class UP4:
18 """
19 Utility that manages interaction with UP4 via a P4RuntimeCliDriver available
20 in the cluster. Additionally, can verify connectivity by crafting GTP packets
21 via Scapy with an HostDriver component, specified via <enodeb_host>, <pdn_host>,
22 and <router_mac> parameters.
23
24 Example params file:
25 <UP4>
26 <pdn_host>Compute1</pdn_host> # Needed to verify connectivity with scapy
27 <enodeb_host>Compute3</enodeb_host> # Needed to verify connectivity with scapy
28 <router_mac>00:00:0A:4C:1C:46</router_mac> # Needed to verify connectivity with scapy
29 <s1u_address>10.32.11.126</s1u_address>
30 <enb_address>10.32.11.100</enb_address>
31 <ues>
32 <ue2>
33 <pfcp_session_id>100</pfcp_session_id>
34 <ue_address>10.240.0.2</ue_address>
35 <teid>200</teid>
36 <up_id>20</up_id>
37 <down_id>21</down_id>
38 <qfi>2</qfi>
39 <five_g>False</five_g>
40 </ue2>
41 </ues>
42 </UP4>
43 """
44
45 def __init__(self):
46 self.s1u_address = None
47 self.enb_address = None
48 self.enodeb_host = None
49 self.enodeb_interface = None
50 self.pdn_host = None
51 self.pdn_interface = None
52 self.router_mac = None
53 self.emulated_ues = []
54 self.up4_client = None
55
56 def setup(self, p4rt_client):
57 self.s1u_address = main.params["UP4"]["s1u_address"]
58 self.enb_address = main.params["UP4"]["enb_address"]
59 self.emulated_ues = main.params["UP4"]['ues']
60 self.up4_client = p4rt_client
61
62 # Optional Parameters
63 if "enodeb_host" in main.params["UP4"]:
64 self.enodeb_host = getattr(main, main.params["UP4"]["enodeb_host"])
65 self.enodeb_interface = self.enodeb_host.interfaces[0]
66 if "pdn_host" in main.params["UP4"]:
67 self.pdn_host = getattr(main, main.params["UP4"]["pdn_host"])
68 self.pdn_interface = self.pdn_host.interfaces[0]
69 self.router_mac = main.params["UP4"].get("router_mac", None)
70
71 # Start components
72 self.up4_client.startP4RtClient()
73 if self.enodeb_host is not None:
74 self.enodeb_host.startScapy(ifaceName=self.enodeb_interface["name"],
75 enableGtp=True)
76 if self.pdn_host is not None:
77 self.pdn_host.startScapy(ifaceName=self.pdn_interface["name"])
78
79 def teardown(self):
80 self.up4_client.stopP4RtClient()
81 if self.enodeb_host is not None:
82 self.enodeb_host.stopScapy()
83 if self.pdn_host is not None:
84 self.pdn_host.stopScapy()
85
86 def attachUes(self):
87 for ue in self.emulated_ues.values():
Daniele Moro80889562021-09-08 10:09:26 +020088 ue = UP4.__sanitizeUeData(ue)
89 self.attachUe(**ue)
90
91 def detachUes(self):
92 for ue in self.emulated_ues.values():
Daniele Moro249d6e72021-09-20 10:32:54 +020093 ue = UP4.__sanitizeUeData(ue)
Daniele Moro80889562021-09-08 10:09:26 +020094 self.detachUe(**ue)
95
96 def testUpstreamTraffic(self):
97 if self.enodeb_host is None or self.pdn_host is None:
98 main.log.error(
99 "Need eNodeB and PDN host params to generate scapy traffic")
100 return
101 # Scapy filter needs to start before sending traffic
102 pkt_filter_upstream = ""
103 for ue in self.emulated_ues.values():
104 if "ue_address" in ue:
105 if len(pkt_filter_upstream) != 0:
106 pkt_filter_upstream += " or "
107 pkt_filter_upstream += "src host " + ue["ue_address"]
108 pkt_filter_upstream = "ip and udp dst port %s and (%s) and dst host %s" % \
109 (PDN_PORT, pkt_filter_upstream,
110 self.pdn_interface["ips"][0])
111 main.log.info("Start listening on %s intf %s" %
112 (self.pdn_host.name, self.pdn_interface["name"]))
113 main.log.debug("BPF Filter Upstream: \n %s" % pkt_filter_upstream)
114 self.pdn_host.startFilter(ifaceName=self.pdn_interface["name"],
115 sniffCount=len(self.emulated_ues),
116 pktFilter=pkt_filter_upstream)
117
118 main.log.info(
119 "Sending %d packets from eNodeB host" % len(self.emulated_ues))
120 for ue in self.emulated_ues.values():
Daniele Morobf53dec2021-09-13 18:11:56 +0200121 UP4.buildGtpPacket(self.enodeb_host,
122 src_ip_outer=self.enb_address,
123 dst_ip_outer=self.s1u_address,
124 src_ip_inner=ue["ue_address"],
125 dst_ip_inner=self.pdn_interface["ips"][0],
126 src_udp_inner=UE_PORT,
127 dst_udp_inner=PDN_PORT,
128 teid=int(ue["teid"]))
Daniele Moro80889562021-09-08 10:09:26 +0200129
130 self.enodeb_host.sendPacket(iface=self.enodeb_interface["name"])
131
Daniele Morobf53dec2021-09-13 18:11:56 +0200132 packets = UP4.checkFilterAndGetPackets(self.pdn_host)
Daniele Moro80889562021-09-08 10:09:26 +0200133 fail = False
134 if len(self.emulated_ues) != packets.count('Ether'):
135 fail = True
136 msg = "Failed to capture packets in PDN. "
137 else:
138 msg = "Correctly captured packet in PDN. "
139 # We expect exactly 1 packet per UE
140 pktsFiltered = [packets.count("src=" + ue["ue_address"])
141 for ue in self.emulated_ues.values()]
142 if pktsFiltered.count(1) != len(pktsFiltered):
143 fail = True
144 msg += "More than one packet per UE in downstream. "
145 else:
146 msg += "One packet per UE in upstream. "
147
148 utilities.assert_equal(
149 expect=False, actual=fail, onpass=msg, onfail=msg)
150
151 def testDownstreamTraffic(self):
152 if self.enodeb_host is None or self.pdn_host is None:
153 main.log.error(
154 "Need eNodeB and PDN host params to generate scapy traffic")
155 return
156 pkt_filter_downstream = "ip and udp src port %d and udp dst port %d and dst host %s and src host %s" % (
157 GPDU_PORT, GPDU_PORT, self.enb_address, self.s1u_address)
158 main.log.info("Start listening on %s intf %s" % (
159 self.enodeb_host.name, self.enodeb_interface["name"]))
160 main.log.debug("BPF Filter Downstream: \n %s" % pkt_filter_downstream)
161 self.enodeb_host.startFilter(ifaceName=self.enodeb_interface["name"],
162 sniffCount=len(self.emulated_ues),
163 pktFilter=pkt_filter_downstream)
164
165 main.log.info(
166 "Sending %d packets from PDN host" % len(self.emulated_ues))
167 for ue in self.emulated_ues.values():
168 # From PDN we have to set dest MAC, otherwise scapy will do ARP
169 # request for the UE IP address.
Daniele Morobf53dec2021-09-13 18:11:56 +0200170 UP4.buildUdpPacket(self.pdn_host,
171 dst_eth=self.router_mac,
172 src_ip=self.pdn_interface["ips"][0],
173 dst_ip=ue["ue_address"],
174 src_udp=PDN_PORT,
175 dst_udp=UE_PORT)
Daniele Moro80889562021-09-08 10:09:26 +0200176 self.pdn_host.sendPacket(iface=self.pdn_interface["name"])
177
Daniele Morobf53dec2021-09-13 18:11:56 +0200178 packets = UP4.checkFilterAndGetPackets(self.enodeb_host)
Daniele Moro80889562021-09-08 10:09:26 +0200179
180 # The BPF filter might capture non-GTP packets because we can't filter
181 # GTP header in BPF. For this reason, check that the captured packets
182 # are from the expected tunnels.
183 # TODO: check inner UDP and IP fields as well
184 # FIXME: with newer scapy TEID becomes teid (required for Scapy 2.4.5)
185 pktsFiltered = [packets.count("TEID=" + hex(int(ue["teid"])) + "L ")
186 for ue in self.emulated_ues.values()]
187
188 fail = False
189 if len(self.emulated_ues) != sum(pktsFiltered):
190 fail = True
191 msg = "Failed to capture packets in eNodeB. "
192 else:
193 msg = "Correctly captured packets in eNodeB. "
194 # We expect exactly 1 packet per UE
195 if pktsFiltered.count(1) != len(pktsFiltered):
196 fail = True
197 msg += "More than one packet per GTP TEID in downstream. "
198 else:
199 msg += "One packet per GTP TEID in downstream. "
200
201 utilities.assert_equal(
202 expect=False, actual=fail, onpass=msg, onfail=msg)
203
Daniele Morobf53dec2021-09-13 18:11:56 +0200204 def verifyNoUesFlow(self, onosCli, retries=3):
205 """
206 Verify that no PDRs and FARs are installed in ONOS.
207
208 :param onosCli: An instance of a OnosCliDriver
209 :param retries: number of retries
210 :return:
211 """
212 retValue = utilities.retry(f=UP4.__verifyNoPdrsFarsOnos,
213 retValue=False,
214 args=[onosCli],
215 sleep=1,
216 attempts=retries)
217 utilities.assert_equal(expect=True,
218 actual=retValue,
219 onpass="No PDRs and FARs in ONOS",
220 onfail="Stale PDRs or FARs")
221
222 @staticmethod
223 def __verifyNoPdrsFarsOnos(onosCli):
224 """
225 Verify that no PDRs and FARs are installed in ONOS
226
227 :param onosCli: An instance of a OnosCliDriver
228 """
229 pdrs = onosCli.sendline(cmdStr="up4:read-pdrs", showResponse=True,
230 noExit=True, expectJson=False)
231 fars = onosCli.sendline(cmdStr="up4:read-fars", showResponse=True,
232 noExit=True, expectJson=False)
233 return pdrs == "" and fars == ""
234
235 def verifyUp4Flow(self, onosCli):
236 """
237 Verify PDRs and FARs installed via UP4 using the ONOS CLI.
238
239 :param onosCli: An instance of a OnosCliDriver
240 """
241 pdrs = onosCli.sendline(cmdStr="up4:read-pdrs", showResponse=True,
242 noExit=True, expectJson=False)
243 fars = onosCli.sendline(cmdStr="up4:read-fars", showResponse=True,
244 noExit=True, expectJson=False)
245 fail = False
246 failMsg = ""
247 for ue in self.emulated_ues.values():
248 if pdrs.count(self.upPdrOnosString(**ue)) != 1:
249 failMsg += self.upPdrOnosString(**ue) + "\n"
250 fail = True
251 if pdrs.count(self.downPdrOnosString(**ue)) != 1:
252 failMsg += self.downPdrOnosString(**ue) + "\n"
253 fail = True
254 if fars.count(self.upFarOnosString(**ue)) != 1:
255 failMsg += self.upFarOnosString(**ue) + "\n"
256 fail = True
257 if fars.count(self.downFarOnosString(**ue)) != 1:
258 failMsg += self.downFarOnosString(**ue) + "\n"
259 fail = True
260 utilities.assert_equal(expect=False, actual=fail,
261 onpass="Correct PDRs and FARs in ONOS",
262 onfail="Wrong PDRs and FARs in ONOS. Missing PDR/FAR:\n" + failMsg)
263
264 def upPdrOnosString(self, pfcp_session_id, teid=None, up_id=None,
265 teid_up=None, far_id_up=None, ctr_id_up=None, qfi=None,
266 **kwargs):
267 # TODO: consider that with five_g the output might be different
268 if up_id is not None:
269 far_id_up = up_id
270 ctr_id_up = up_id
271 if teid is not None:
272 teid_up = teid
273 if qfi is not None:
274 return "PDR{{Match(Dst={}, TEID={}) -> LoadParams(SEID={}, FAR={}, CtrIdx={}, QFI={})}}".format(
275 self.s1u_address, hex(int(teid_up)), hex(int(pfcp_session_id)),
Daniele Moro3e4cf3b2021-09-21 18:53:22 +0200276 far_id_up, ctr_id_up, qfi)
Daniele Morobf53dec2021-09-13 18:11:56 +0200277 return "PDR{{Match(Dst={}, TEID={}) -> LoadParams(SEID={}, FAR={}, CtrIdx={})}}".format(
278 self.s1u_address, hex(int(teid_up)), hex(int(pfcp_session_id)),
279 far_id_up, ctr_id_up)
280
281 def downPdrOnosString(self, pfcp_session_id, ue_address, down_id=None,
Daniele Moro3e4cf3b2021-09-21 18:53:22 +0200282 far_id_down=None, ctr_id_down=None, qfi=None,
283 **kwargs):
Daniele Morobf53dec2021-09-13 18:11:56 +0200284 # TODO: consider that with five_g the output might be different
285 if down_id is not None:
286 far_id_down = down_id
287 ctr_id_down = down_id
Daniele Moro3e4cf3b2021-09-21 18:53:22 +0200288 if qfi is not None:
289 return "PDR{{Match(Dst={}, !GTP) -> LoadParams(SEID={}, FAR={}, CtrIdx={}, QFI={})}}".format(
290 ue_address, hex(int(pfcp_session_id)), far_id_down, ctr_id_down,
291 qfi)
Daniele Morobf53dec2021-09-13 18:11:56 +0200292 return "PDR{{Match(Dst={}, !GTP) -> LoadParams(SEID={}, FAR={}, CtrIdx={})}}".format(
293 ue_address, hex(int(pfcp_session_id)), far_id_down, ctr_id_down)
294
295 def downFarOnosString(self, pfcp_session_id, teid=None, down_id=None,
296 teid_down=None, far_id_down=None, **kwargs):
297 if down_id is not None:
298 far_id_down = down_id
299 if teid is not None:
300 teid_down = teid
301 return "FAR{{Match(ID={}, SEID={}) -> Encap(Src={}, SPort={}, TEID={}, Dst={})}}".format(
302 far_id_down, hex(int(pfcp_session_id)), self.s1u_address, GPDU_PORT,
303 hex(int(teid_down)),
304 self.enb_address)
305
306 def upFarOnosString(self, pfcp_session_id, up_id=None, far_id_up=None,
307 **kwargs):
308 if up_id is not None:
309 far_id_up = up_id
310 return "FAR{{Match(ID={}, SEID={}) -> Forward()}}".format(
311 far_id_up, hex(int(pfcp_session_id)))
312
Daniele Moro80889562021-09-08 10:09:26 +0200313 @staticmethod
314 def __sanitizeUeData(ue):
Daniele Moro249d6e72021-09-20 10:32:54 +0200315 if "five_g" in ue and type(ue["five_g"]) != bool:
Daniele Moro80889562021-09-08 10:09:26 +0200316 ue["five_g"] = bool(strtobool(ue["five_g"]))
317 if "qfi" in ue and ue["qfi"] == "":
318 ue["qfi"] = None
319 return ue
320
321 def attachUe(self, pfcp_session_id, ue_address,
322 teid=None, up_id=None, down_id=None,
323 teid_up=None, teid_down=None,
324 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
325 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
326 qfi=None, five_g=False):
327 self.__programUp4Rules(pfcp_session_id,
328 ue_address,
329 teid, up_id, down_id,
330 teid_up, teid_down,
331 pdr_id_up, far_id_up, ctr_id_up,
332 pdr_id_down, far_id_down, ctr_id_down,
333 qfi, five_g, action="program")
334
335 def detachUe(self, pfcp_session_id, ue_address,
336 teid=None, up_id=None, down_id=None,
337 teid_up=None, teid_down=None,
338 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
339 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
340 qfi=None, five_g=False):
341 self.__programUp4Rules(pfcp_session_id,
342 ue_address,
343 teid, up_id, down_id,
344 teid_up, teid_down,
345 pdr_id_up, far_id_up, ctr_id_up,
346 pdr_id_down, far_id_down, ctr_id_down,
347 qfi, five_g, action="clear")
348
349 def __programUp4Rules(self, pfcp_session_id, ue_address,
350 teid=None, up_id=None, down_id=None,
351 teid_up=None, teid_down=None,
352 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
353 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
354 qfi=None, five_g=False, action="program"):
355 if up_id is not None:
356 pdr_id_up = up_id
357 far_id_up = up_id
358 ctr_id_up = up_id
359 if down_id is not None:
360 pdr_id_down = down_id
361 far_id_down = down_id
362 ctr_id_down = down_id
363 if teid is not None:
364 teid_up = teid
365 teid_down = teid
366
367 entries = []
368
369 # ========================#
370 # PDR Entries
371 # ========================#
372
373 # Uplink
374 tableName = 'PreQosPipe.pdrs'
375 actionName = ''
376 matchFields = {}
377 actionParams = {}
378 if qfi is None:
379 actionName = 'PreQosPipe.set_pdr_attributes'
380 else:
381 actionName = 'PreQosPipe.set_pdr_attributes_qos'
382 if five_g:
383 # TODO: currently QFI_MATCH is unsupported in TNA
384 matchFields['has_qfi'] = TRUE
385 matchFields["qfi"] = str(qfi)
386 actionParams['needs_qfi_push'] = FALSE
387 actionParams['qfi'] = str(qfi)
388 # Match fields
389 matchFields['src_iface'] = IFACE_ACCESS
390 matchFields['ue_addr'] = str(ue_address)
391 matchFields['teid'] = str(teid_up)
392 matchFields['tunnel_ipv4_dst'] = str(self.s1u_address)
393 # Action params
394 actionParams['id'] = str(pdr_id_up)
395 actionParams['fseid'] = str(pfcp_session_id)
396 actionParams['ctr_id'] = str(ctr_id_up)
397 actionParams['far_id'] = str(far_id_up)
398 actionParams['needs_gtpu_decap'] = TRUE
399 if not self.__add_entry(tableName, actionName, matchFields,
400 actionParams, entries, action):
401 return False
402
403 # Downlink
404 tableName = 'PreQosPipe.pdrs'
405 matchFields = {}
406 actionParams = {}
407 if qfi is None:
408 actionName = 'PreQosPipe.set_pdr_attributes'
409 else:
410 actionName = 'PreQosPipe.set_pdr_attributes_qos'
411 # TODO: currently QFI_PUSH is unsupported in TNA
412 actionParams['needs_qfi_push'] = TRUE if five_g else FALSE
413 actionParams['qfi'] = str(qfi)
414 # Match fields
415 matchFields['src_iface'] = IFACE_CORE
416 matchFields['ue_addr'] = str(ue_address)
417 # Action params
418 actionParams['id'] = str(pdr_id_down)
419 actionParams['fseid'] = str(pfcp_session_id)
420 actionParams['ctr_id'] = str(ctr_id_down)
421 actionParams['far_id'] = str(far_id_down)
422 actionParams['needs_gtpu_decap'] = FALSE
423 if not self.__add_entry(tableName, actionName, matchFields,
424 actionParams, entries, action):
425 return False
426
427 # ========================#
428 # FAR Entries
429 # ========================#
430
431 # Uplink
432 tableName = 'PreQosPipe.load_far_attributes'
433 actionName = 'PreQosPipe.load_normal_far_attributes'
434 matchFields = {}
435 actionParams = {}
436
437 # Match fields
438 matchFields['far_id'] = str(far_id_up)
439 matchFields['session_id'] = str(pfcp_session_id)
440 # Action params
441 actionParams['needs_dropping'] = FALSE
442 actionParams['notify_cp'] = FALSE
443 if not self.__add_entry(tableName, actionName, matchFields,
444 actionParams, entries, action):
445 return False
446
447 # Downlink
448 tableName = 'PreQosPipe.load_far_attributes'
449 actionName = 'PreQosPipe.load_tunnel_far_attributes'
450 matchFields = {}
451 actionParams = {}
452
453 # Match fields
454 matchFields['far_id'] = str(far_id_down)
455 matchFields['session_id'] = str(pfcp_session_id)
456 # Action params
457 actionParams['needs_dropping'] = FALSE
458 actionParams['notify_cp'] = FALSE
459 actionParams['needs_buffering'] = FALSE
460 actionParams['tunnel_type'] = TUNNEL_TYPE_GPDU
461 actionParams['src_addr'] = str(self.s1u_address)
462 actionParams['dst_addr'] = str(self.enb_address)
463 actionParams['teid'] = str(teid_down)
464 actionParams['sport'] = TUNNEL_SPORT
465 if not self.__add_entry(tableName, actionName, matchFields,
466 actionParams, entries, action):
467 return False
468 if action == "program":
469 main.log.info("All entries added successfully.")
470 elif action == "clear":
471 self.__clear_entries(entries)
472
473 def __add_entry(self, tableName, actionName, matchFields, actionParams,
474 entries, action):
475 if action == "program":
476 self.up4_client.buildP4RtTableEntry(
477 tableName=tableName, actionName=actionName,
478 actionParams=actionParams, matchFields=matchFields)
479 if self.up4_client.pushTableEntry(debug=True) == main.TRUE:
480 main.log.info("*** Entry added.")
481 else:
482 main.log.error("Error during table insertion")
483 self.__clear_entries(entries)
484 return False
485 entries.append({"tableName": tableName, "actionName": actionName,
486 "matchFields": matchFields,
487 "actionParams": actionParams})
488 return True
489
490 def __clear_entries(self, entries):
491 for i, entry in enumerate(entries):
492 self.up4_client.buildP4RtTableEntry(**entry)
493 if self.up4_client.deleteTableEntry(debug=True) == main.TRUE:
494 main.log.info(
495 "*** Entry %d of %d deleted." % (i + 1, len(entries)))
496 else:
497 main.log.error("Error during table delete")
Daniele Morobf53dec2021-09-13 18:11:56 +0200498
499 @staticmethod
500 def buildGtpPacket(host, src_ip_outer, dst_ip_outer, src_ip_inner,
501 dst_ip_inner, src_udp_inner, dst_udp_inner, teid):
502 host.buildEther()
503 host.buildIP(src=src_ip_outer, dst=dst_ip_outer)
504 host.buildUDP(ipVersion=4, dport=GPDU_PORT)
505 # FIXME: With newer scapy TEID becomes teid (required for Scapy 2.4.5)
506 host.buildGTP(gtp_type=0xFF, TEID=teid)
507 host.buildIP(overGtp=True, src=src_ip_inner, dst=dst_ip_inner)
508 host.buildUDP(ipVersion=4, overGtp=True, sport=src_udp_inner,
509 dport=dst_udp_inner)
510
511 @staticmethod
512 def buildUdpPacket(host, src_ip, dst_ip, src_udp, dst_udp, src_eth=None,
513 dst_eth=None):
514 host.buildEther(src=src_eth, dst=dst_eth)
515 host.buildIP(src=src_ip, dst=dst_ip)
516 host.buildUDP(ipVersion=4, sport=src_udp, dport=dst_udp)
517
518 @staticmethod
519 def checkFilterAndGetPackets(host):
520 finished = host.checkFilter()
521 if finished:
522 packets = host.readPackets(detailed=True)
523 for p in packets.splitlines():
524 main.log.debug(p)
525 # We care only of the last line from readPackets
526 return packets.splitlines()[-1]
527 else:
528 kill = host.killFilter()
529 main.log.debug(kill)
530 return ""