blob: 344d0cf16ec0b02945278abc8f316bd38ff92416 [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():
88 # Sanitize values coming from the params file
89 ue = UP4.__sanitizeUeData(ue)
90 self.attachUe(**ue)
91
92 def detachUes(self):
93 for ue in self.emulated_ues.values():
94 # No need to sanitize, has already been done in attach
95 self.detachUe(**ue)
96
97 def testUpstreamTraffic(self):
98 if self.enodeb_host is None or self.pdn_host is None:
99 main.log.error(
100 "Need eNodeB and PDN host params to generate scapy traffic")
101 return
102 # Scapy filter needs to start before sending traffic
103 pkt_filter_upstream = ""
104 for ue in self.emulated_ues.values():
105 if "ue_address" in ue:
106 if len(pkt_filter_upstream) != 0:
107 pkt_filter_upstream += " or "
108 pkt_filter_upstream += "src host " + ue["ue_address"]
109 pkt_filter_upstream = "ip and udp dst port %s and (%s) and dst host %s" % \
110 (PDN_PORT, pkt_filter_upstream,
111 self.pdn_interface["ips"][0])
112 main.log.info("Start listening on %s intf %s" %
113 (self.pdn_host.name, self.pdn_interface["name"]))
114 main.log.debug("BPF Filter Upstream: \n %s" % pkt_filter_upstream)
115 self.pdn_host.startFilter(ifaceName=self.pdn_interface["name"],
116 sniffCount=len(self.emulated_ues),
117 pktFilter=pkt_filter_upstream)
118
119 main.log.info(
120 "Sending %d packets from eNodeB host" % len(self.emulated_ues))
121 for ue in self.emulated_ues.values():
Daniele Morobf53dec2021-09-13 18:11:56 +0200122 UP4.buildGtpPacket(self.enodeb_host,
123 src_ip_outer=self.enb_address,
124 dst_ip_outer=self.s1u_address,
125 src_ip_inner=ue["ue_address"],
126 dst_ip_inner=self.pdn_interface["ips"][0],
127 src_udp_inner=UE_PORT,
128 dst_udp_inner=PDN_PORT,
129 teid=int(ue["teid"]))
Daniele Moro80889562021-09-08 10:09:26 +0200130
131 self.enodeb_host.sendPacket(iface=self.enodeb_interface["name"])
132
Daniele Morobf53dec2021-09-13 18:11:56 +0200133 packets = UP4.checkFilterAndGetPackets(self.pdn_host)
Daniele Moro80889562021-09-08 10:09:26 +0200134 fail = False
135 if len(self.emulated_ues) != packets.count('Ether'):
136 fail = True
137 msg = "Failed to capture packets in PDN. "
138 else:
139 msg = "Correctly captured packet in PDN. "
140 # We expect exactly 1 packet per UE
141 pktsFiltered = [packets.count("src=" + ue["ue_address"])
142 for ue in self.emulated_ues.values()]
143 if pktsFiltered.count(1) != len(pktsFiltered):
144 fail = True
145 msg += "More than one packet per UE in downstream. "
146 else:
147 msg += "One packet per UE in upstream. "
148
149 utilities.assert_equal(
150 expect=False, actual=fail, onpass=msg, onfail=msg)
151
152 def testDownstreamTraffic(self):
153 if self.enodeb_host is None or self.pdn_host is None:
154 main.log.error(
155 "Need eNodeB and PDN host params to generate scapy traffic")
156 return
157 pkt_filter_downstream = "ip and udp src port %d and udp dst port %d and dst host %s and src host %s" % (
158 GPDU_PORT, GPDU_PORT, self.enb_address, self.s1u_address)
159 main.log.info("Start listening on %s intf %s" % (
160 self.enodeb_host.name, self.enodeb_interface["name"]))
161 main.log.debug("BPF Filter Downstream: \n %s" % pkt_filter_downstream)
162 self.enodeb_host.startFilter(ifaceName=self.enodeb_interface["name"],
163 sniffCount=len(self.emulated_ues),
164 pktFilter=pkt_filter_downstream)
165
166 main.log.info(
167 "Sending %d packets from PDN host" % len(self.emulated_ues))
168 for ue in self.emulated_ues.values():
169 # From PDN we have to set dest MAC, otherwise scapy will do ARP
170 # request for the UE IP address.
Daniele Morobf53dec2021-09-13 18:11:56 +0200171 UP4.buildUdpPacket(self.pdn_host,
172 dst_eth=self.router_mac,
173 src_ip=self.pdn_interface["ips"][0],
174 dst_ip=ue["ue_address"],
175 src_udp=PDN_PORT,
176 dst_udp=UE_PORT)
Daniele Moro80889562021-09-08 10:09:26 +0200177 self.pdn_host.sendPacket(iface=self.pdn_interface["name"])
178
Daniele Morobf53dec2021-09-13 18:11:56 +0200179 packets = UP4.checkFilterAndGetPackets(self.enodeb_host)
Daniele Moro80889562021-09-08 10:09:26 +0200180
181 # The BPF filter might capture non-GTP packets because we can't filter
182 # GTP header in BPF. For this reason, check that the captured packets
183 # are from the expected tunnels.
184 # TODO: check inner UDP and IP fields as well
185 # FIXME: with newer scapy TEID becomes teid (required for Scapy 2.4.5)
186 pktsFiltered = [packets.count("TEID=" + hex(int(ue["teid"])) + "L ")
187 for ue in self.emulated_ues.values()]
188
189 fail = False
190 if len(self.emulated_ues) != sum(pktsFiltered):
191 fail = True
192 msg = "Failed to capture packets in eNodeB. "
193 else:
194 msg = "Correctly captured packets in eNodeB. "
195 # We expect exactly 1 packet per UE
196 if pktsFiltered.count(1) != len(pktsFiltered):
197 fail = True
198 msg += "More than one packet per GTP TEID in downstream. "
199 else:
200 msg += "One packet per GTP TEID in downstream. "
201
202 utilities.assert_equal(
203 expect=False, actual=fail, onpass=msg, onfail=msg)
204
Daniele Morobf53dec2021-09-13 18:11:56 +0200205 def verifyNoUesFlow(self, onosCli, retries=3):
206 """
207 Verify that no PDRs and FARs are installed in ONOS.
208
209 :param onosCli: An instance of a OnosCliDriver
210 :param retries: number of retries
211 :return:
212 """
213 retValue = utilities.retry(f=UP4.__verifyNoPdrsFarsOnos,
214 retValue=False,
215 args=[onosCli],
216 sleep=1,
217 attempts=retries)
218 utilities.assert_equal(expect=True,
219 actual=retValue,
220 onpass="No PDRs and FARs in ONOS",
221 onfail="Stale PDRs or FARs")
222
223 @staticmethod
224 def __verifyNoPdrsFarsOnos(onosCli):
225 """
226 Verify that no PDRs and FARs are installed in ONOS
227
228 :param onosCli: An instance of a OnosCliDriver
229 """
230 pdrs = onosCli.sendline(cmdStr="up4:read-pdrs", showResponse=True,
231 noExit=True, expectJson=False)
232 fars = onosCli.sendline(cmdStr="up4:read-fars", showResponse=True,
233 noExit=True, expectJson=False)
234 return pdrs == "" and fars == ""
235
236 def verifyUp4Flow(self, onosCli):
237 """
238 Verify PDRs and FARs installed via UP4 using the ONOS CLI.
239
240 :param onosCli: An instance of a OnosCliDriver
241 """
242 pdrs = onosCli.sendline(cmdStr="up4:read-pdrs", showResponse=True,
243 noExit=True, expectJson=False)
244 fars = onosCli.sendline(cmdStr="up4:read-fars", showResponse=True,
245 noExit=True, expectJson=False)
246 fail = False
247 failMsg = ""
248 for ue in self.emulated_ues.values():
249 if pdrs.count(self.upPdrOnosString(**ue)) != 1:
250 failMsg += self.upPdrOnosString(**ue) + "\n"
251 fail = True
252 if pdrs.count(self.downPdrOnosString(**ue)) != 1:
253 failMsg += self.downPdrOnosString(**ue) + "\n"
254 fail = True
255 if fars.count(self.upFarOnosString(**ue)) != 1:
256 failMsg += self.upFarOnosString(**ue) + "\n"
257 fail = True
258 if fars.count(self.downFarOnosString(**ue)) != 1:
259 failMsg += self.downFarOnosString(**ue) + "\n"
260 fail = True
261 utilities.assert_equal(expect=False, actual=fail,
262 onpass="Correct PDRs and FARs in ONOS",
263 onfail="Wrong PDRs and FARs in ONOS. Missing PDR/FAR:\n" + failMsg)
264
265 def upPdrOnosString(self, pfcp_session_id, teid=None, up_id=None,
266 teid_up=None, far_id_up=None, ctr_id_up=None, qfi=None,
267 **kwargs):
268 # TODO: consider that with five_g the output might be different
269 if up_id is not None:
270 far_id_up = up_id
271 ctr_id_up = up_id
272 if teid is not None:
273 teid_up = teid
274 if qfi is not None:
275 return "PDR{{Match(Dst={}, TEID={}) -> LoadParams(SEID={}, FAR={}, CtrIdx={}, QFI={})}}".format(
276 self.s1u_address, hex(int(teid_up)), hex(int(pfcp_session_id)),
277 far_id_up,
278 ctr_id_up, qfi)
279 return "PDR{{Match(Dst={}, TEID={}) -> LoadParams(SEID={}, FAR={}, CtrIdx={})}}".format(
280 self.s1u_address, hex(int(teid_up)), hex(int(pfcp_session_id)),
281 far_id_up, ctr_id_up)
282
283 def downPdrOnosString(self, pfcp_session_id, ue_address, down_id=None,
284 far_id_down=None, ctr_id_down=None, **kwargs):
285 # TODO: consider that with five_g the output might be different
286 if down_id is not None:
287 far_id_down = down_id
288 ctr_id_down = down_id
289 return "PDR{{Match(Dst={}, !GTP) -> LoadParams(SEID={}, FAR={}, CtrIdx={})}}".format(
290 ue_address, hex(int(pfcp_session_id)), far_id_down, ctr_id_down)
291
292 def downFarOnosString(self, pfcp_session_id, teid=None, down_id=None,
293 teid_down=None, far_id_down=None, **kwargs):
294 if down_id is not None:
295 far_id_down = down_id
296 if teid is not None:
297 teid_down = teid
298 return "FAR{{Match(ID={}, SEID={}) -> Encap(Src={}, SPort={}, TEID={}, Dst={})}}".format(
299 far_id_down, hex(int(pfcp_session_id)), self.s1u_address, GPDU_PORT,
300 hex(int(teid_down)),
301 self.enb_address)
302
303 def upFarOnosString(self, pfcp_session_id, up_id=None, far_id_up=None,
304 **kwargs):
305 if up_id is not None:
306 far_id_up = up_id
307 return "FAR{{Match(ID={}, SEID={}) -> Forward()}}".format(
308 far_id_up, hex(int(pfcp_session_id)))
309
Daniele Moro80889562021-09-08 10:09:26 +0200310 @staticmethod
311 def __sanitizeUeData(ue):
312 if "five_g" in ue:
313 ue["five_g"] = bool(strtobool(ue["five_g"]))
314 if "qfi" in ue and ue["qfi"] == "":
315 ue["qfi"] = None
316 return ue
317
318 def attachUe(self, pfcp_session_id, ue_address,
319 teid=None, up_id=None, down_id=None,
320 teid_up=None, teid_down=None,
321 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
322 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
323 qfi=None, five_g=False):
324 self.__programUp4Rules(pfcp_session_id,
325 ue_address,
326 teid, up_id, down_id,
327 teid_up, teid_down,
328 pdr_id_up, far_id_up, ctr_id_up,
329 pdr_id_down, far_id_down, ctr_id_down,
330 qfi, five_g, action="program")
331
332 def detachUe(self, pfcp_session_id, ue_address,
333 teid=None, up_id=None, down_id=None,
334 teid_up=None, teid_down=None,
335 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
336 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
337 qfi=None, five_g=False):
338 self.__programUp4Rules(pfcp_session_id,
339 ue_address,
340 teid, up_id, down_id,
341 teid_up, teid_down,
342 pdr_id_up, far_id_up, ctr_id_up,
343 pdr_id_down, far_id_down, ctr_id_down,
344 qfi, five_g, action="clear")
345
346 def __programUp4Rules(self, pfcp_session_id, ue_address,
347 teid=None, up_id=None, down_id=None,
348 teid_up=None, teid_down=None,
349 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
350 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
351 qfi=None, five_g=False, action="program"):
352 if up_id is not None:
353 pdr_id_up = up_id
354 far_id_up = up_id
355 ctr_id_up = up_id
356 if down_id is not None:
357 pdr_id_down = down_id
358 far_id_down = down_id
359 ctr_id_down = down_id
360 if teid is not None:
361 teid_up = teid
362 teid_down = teid
363
364 entries = []
365
366 # ========================#
367 # PDR Entries
368 # ========================#
369
370 # Uplink
371 tableName = 'PreQosPipe.pdrs'
372 actionName = ''
373 matchFields = {}
374 actionParams = {}
375 if qfi is None:
376 actionName = 'PreQosPipe.set_pdr_attributes'
377 else:
378 actionName = 'PreQosPipe.set_pdr_attributes_qos'
379 if five_g:
380 # TODO: currently QFI_MATCH is unsupported in TNA
381 matchFields['has_qfi'] = TRUE
382 matchFields["qfi"] = str(qfi)
383 actionParams['needs_qfi_push'] = FALSE
384 actionParams['qfi'] = str(qfi)
385 # Match fields
386 matchFields['src_iface'] = IFACE_ACCESS
387 matchFields['ue_addr'] = str(ue_address)
388 matchFields['teid'] = str(teid_up)
389 matchFields['tunnel_ipv4_dst'] = str(self.s1u_address)
390 # Action params
391 actionParams['id'] = str(pdr_id_up)
392 actionParams['fseid'] = str(pfcp_session_id)
393 actionParams['ctr_id'] = str(ctr_id_up)
394 actionParams['far_id'] = str(far_id_up)
395 actionParams['needs_gtpu_decap'] = TRUE
396 if not self.__add_entry(tableName, actionName, matchFields,
397 actionParams, entries, action):
398 return False
399
400 # Downlink
401 tableName = 'PreQosPipe.pdrs'
402 matchFields = {}
403 actionParams = {}
404 if qfi is None:
405 actionName = 'PreQosPipe.set_pdr_attributes'
406 else:
407 actionName = 'PreQosPipe.set_pdr_attributes_qos'
408 # TODO: currently QFI_PUSH is unsupported in TNA
409 actionParams['needs_qfi_push'] = TRUE if five_g else FALSE
410 actionParams['qfi'] = str(qfi)
411 # Match fields
412 matchFields['src_iface'] = IFACE_CORE
413 matchFields['ue_addr'] = str(ue_address)
414 # Action params
415 actionParams['id'] = str(pdr_id_down)
416 actionParams['fseid'] = str(pfcp_session_id)
417 actionParams['ctr_id'] = str(ctr_id_down)
418 actionParams['far_id'] = str(far_id_down)
419 actionParams['needs_gtpu_decap'] = FALSE
420 if not self.__add_entry(tableName, actionName, matchFields,
421 actionParams, entries, action):
422 return False
423
424 # ========================#
425 # FAR Entries
426 # ========================#
427
428 # Uplink
429 tableName = 'PreQosPipe.load_far_attributes'
430 actionName = 'PreQosPipe.load_normal_far_attributes'
431 matchFields = {}
432 actionParams = {}
433
434 # Match fields
435 matchFields['far_id'] = str(far_id_up)
436 matchFields['session_id'] = str(pfcp_session_id)
437 # Action params
438 actionParams['needs_dropping'] = FALSE
439 actionParams['notify_cp'] = FALSE
440 if not self.__add_entry(tableName, actionName, matchFields,
441 actionParams, entries, action):
442 return False
443
444 # Downlink
445 tableName = 'PreQosPipe.load_far_attributes'
446 actionName = 'PreQosPipe.load_tunnel_far_attributes'
447 matchFields = {}
448 actionParams = {}
449
450 # Match fields
451 matchFields['far_id'] = str(far_id_down)
452 matchFields['session_id'] = str(pfcp_session_id)
453 # Action params
454 actionParams['needs_dropping'] = FALSE
455 actionParams['notify_cp'] = FALSE
456 actionParams['needs_buffering'] = FALSE
457 actionParams['tunnel_type'] = TUNNEL_TYPE_GPDU
458 actionParams['src_addr'] = str(self.s1u_address)
459 actionParams['dst_addr'] = str(self.enb_address)
460 actionParams['teid'] = str(teid_down)
461 actionParams['sport'] = TUNNEL_SPORT
462 if not self.__add_entry(tableName, actionName, matchFields,
463 actionParams, entries, action):
464 return False
465 if action == "program":
466 main.log.info("All entries added successfully.")
467 elif action == "clear":
468 self.__clear_entries(entries)
469
470 def __add_entry(self, tableName, actionName, matchFields, actionParams,
471 entries, action):
472 if action == "program":
473 self.up4_client.buildP4RtTableEntry(
474 tableName=tableName, actionName=actionName,
475 actionParams=actionParams, matchFields=matchFields)
476 if self.up4_client.pushTableEntry(debug=True) == main.TRUE:
477 main.log.info("*** Entry added.")
478 else:
479 main.log.error("Error during table insertion")
480 self.__clear_entries(entries)
481 return False
482 entries.append({"tableName": tableName, "actionName": actionName,
483 "matchFields": matchFields,
484 "actionParams": actionParams})
485 return True
486
487 def __clear_entries(self, entries):
488 for i, entry in enumerate(entries):
489 self.up4_client.buildP4RtTableEntry(**entry)
490 if self.up4_client.deleteTableEntry(debug=True) == main.TRUE:
491 main.log.info(
492 "*** Entry %d of %d deleted." % (i + 1, len(entries)))
493 else:
494 main.log.error("Error during table delete")
Daniele Morobf53dec2021-09-13 18:11:56 +0200495
496 @staticmethod
497 def buildGtpPacket(host, src_ip_outer, dst_ip_outer, src_ip_inner,
498 dst_ip_inner, src_udp_inner, dst_udp_inner, teid):
499 host.buildEther()
500 host.buildIP(src=src_ip_outer, dst=dst_ip_outer)
501 host.buildUDP(ipVersion=4, dport=GPDU_PORT)
502 # FIXME: With newer scapy TEID becomes teid (required for Scapy 2.4.5)
503 host.buildGTP(gtp_type=0xFF, TEID=teid)
504 host.buildIP(overGtp=True, src=src_ip_inner, dst=dst_ip_inner)
505 host.buildUDP(ipVersion=4, overGtp=True, sport=src_udp_inner,
506 dport=dst_udp_inner)
507
508 @staticmethod
509 def buildUdpPacket(host, src_ip, dst_ip, src_udp, dst_udp, src_eth=None,
510 dst_eth=None):
511 host.buildEther(src=src_eth, dst=dst_eth)
512 host.buildIP(src=src_ip, dst=dst_ip)
513 host.buildUDP(ipVersion=4, sport=src_udp, dport=dst_udp)
514
515 @staticmethod
516 def checkFilterAndGetPackets(host):
517 finished = host.checkFilter()
518 if finished:
519 packets = host.readPackets(detailed=True)
520 for p in packets.splitlines():
521 main.log.debug(p)
522 # We care only of the last line from readPackets
523 return packets.splitlines()[-1]
524 else:
525 kill = host.killFilter()
526 main.log.debug(kill)
527 return ""