blob: 61f28d929f8bf6da81c5311a165656cb9759a8e1 [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)),
276 far_id_up,
277 ctr_id_up, qfi)
278 return "PDR{{Match(Dst={}, TEID={}) -> LoadParams(SEID={}, FAR={}, CtrIdx={})}}".format(
279 self.s1u_address, hex(int(teid_up)), hex(int(pfcp_session_id)),
280 far_id_up, ctr_id_up)
281
282 def downPdrOnosString(self, pfcp_session_id, ue_address, down_id=None,
283 far_id_down=None, ctr_id_down=None, **kwargs):
284 # 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
288 return "PDR{{Match(Dst={}, !GTP) -> LoadParams(SEID={}, FAR={}, CtrIdx={})}}".format(
289 ue_address, hex(int(pfcp_session_id)), far_id_down, ctr_id_down)
290
291 def downFarOnosString(self, pfcp_session_id, teid=None, down_id=None,
292 teid_down=None, far_id_down=None, **kwargs):
293 if down_id is not None:
294 far_id_down = down_id
295 if teid is not None:
296 teid_down = teid
297 return "FAR{{Match(ID={}, SEID={}) -> Encap(Src={}, SPort={}, TEID={}, Dst={})}}".format(
298 far_id_down, hex(int(pfcp_session_id)), self.s1u_address, GPDU_PORT,
299 hex(int(teid_down)),
300 self.enb_address)
301
302 def upFarOnosString(self, pfcp_session_id, up_id=None, far_id_up=None,
303 **kwargs):
304 if up_id is not None:
305 far_id_up = up_id
306 return "FAR{{Match(ID={}, SEID={}) -> Forward()}}".format(
307 far_id_up, hex(int(pfcp_session_id)))
308
Daniele Moro80889562021-09-08 10:09:26 +0200309 @staticmethod
310 def __sanitizeUeData(ue):
Daniele Moro249d6e72021-09-20 10:32:54 +0200311 if "five_g" in ue and type(ue["five_g"]) != bool:
Daniele Moro80889562021-09-08 10:09:26 +0200312 ue["five_g"] = bool(strtobool(ue["five_g"]))
313 if "qfi" in ue and ue["qfi"] == "":
314 ue["qfi"] = None
315 return ue
316
317 def attachUe(self, pfcp_session_id, ue_address,
318 teid=None, up_id=None, down_id=None,
319 teid_up=None, teid_down=None,
320 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
321 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
322 qfi=None, five_g=False):
323 self.__programUp4Rules(pfcp_session_id,
324 ue_address,
325 teid, up_id, down_id,
326 teid_up, teid_down,
327 pdr_id_up, far_id_up, ctr_id_up,
328 pdr_id_down, far_id_down, ctr_id_down,
329 qfi, five_g, action="program")
330
331 def detachUe(self, pfcp_session_id, ue_address,
332 teid=None, up_id=None, down_id=None,
333 teid_up=None, teid_down=None,
334 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
335 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
336 qfi=None, five_g=False):
337 self.__programUp4Rules(pfcp_session_id,
338 ue_address,
339 teid, up_id, down_id,
340 teid_up, teid_down,
341 pdr_id_up, far_id_up, ctr_id_up,
342 pdr_id_down, far_id_down, ctr_id_down,
343 qfi, five_g, action="clear")
344
345 def __programUp4Rules(self, pfcp_session_id, ue_address,
346 teid=None, up_id=None, down_id=None,
347 teid_up=None, teid_down=None,
348 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
349 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
350 qfi=None, five_g=False, action="program"):
351 if up_id is not None:
352 pdr_id_up = up_id
353 far_id_up = up_id
354 ctr_id_up = up_id
355 if down_id is not None:
356 pdr_id_down = down_id
357 far_id_down = down_id
358 ctr_id_down = down_id
359 if teid is not None:
360 teid_up = teid
361 teid_down = teid
362
363 entries = []
364
365 # ========================#
366 # PDR Entries
367 # ========================#
368
369 # Uplink
370 tableName = 'PreQosPipe.pdrs'
371 actionName = ''
372 matchFields = {}
373 actionParams = {}
374 if qfi is None:
375 actionName = 'PreQosPipe.set_pdr_attributes'
376 else:
377 actionName = 'PreQosPipe.set_pdr_attributes_qos'
378 if five_g:
379 # TODO: currently QFI_MATCH is unsupported in TNA
380 matchFields['has_qfi'] = TRUE
381 matchFields["qfi"] = str(qfi)
382 actionParams['needs_qfi_push'] = FALSE
383 actionParams['qfi'] = str(qfi)
384 # Match fields
385 matchFields['src_iface'] = IFACE_ACCESS
386 matchFields['ue_addr'] = str(ue_address)
387 matchFields['teid'] = str(teid_up)
388 matchFields['tunnel_ipv4_dst'] = str(self.s1u_address)
389 # Action params
390 actionParams['id'] = str(pdr_id_up)
391 actionParams['fseid'] = str(pfcp_session_id)
392 actionParams['ctr_id'] = str(ctr_id_up)
393 actionParams['far_id'] = str(far_id_up)
394 actionParams['needs_gtpu_decap'] = TRUE
395 if not self.__add_entry(tableName, actionName, matchFields,
396 actionParams, entries, action):
397 return False
398
399 # Downlink
400 tableName = 'PreQosPipe.pdrs'
401 matchFields = {}
402 actionParams = {}
403 if qfi is None:
404 actionName = 'PreQosPipe.set_pdr_attributes'
405 else:
406 actionName = 'PreQosPipe.set_pdr_attributes_qos'
407 # TODO: currently QFI_PUSH is unsupported in TNA
408 actionParams['needs_qfi_push'] = TRUE if five_g else FALSE
409 actionParams['qfi'] = str(qfi)
410 # Match fields
411 matchFields['src_iface'] = IFACE_CORE
412 matchFields['ue_addr'] = str(ue_address)
413 # Action params
414 actionParams['id'] = str(pdr_id_down)
415 actionParams['fseid'] = str(pfcp_session_id)
416 actionParams['ctr_id'] = str(ctr_id_down)
417 actionParams['far_id'] = str(far_id_down)
418 actionParams['needs_gtpu_decap'] = FALSE
419 if not self.__add_entry(tableName, actionName, matchFields,
420 actionParams, entries, action):
421 return False
422
423 # ========================#
424 # FAR Entries
425 # ========================#
426
427 # Uplink
428 tableName = 'PreQosPipe.load_far_attributes'
429 actionName = 'PreQosPipe.load_normal_far_attributes'
430 matchFields = {}
431 actionParams = {}
432
433 # Match fields
434 matchFields['far_id'] = str(far_id_up)
435 matchFields['session_id'] = str(pfcp_session_id)
436 # Action params
437 actionParams['needs_dropping'] = FALSE
438 actionParams['notify_cp'] = FALSE
439 if not self.__add_entry(tableName, actionName, matchFields,
440 actionParams, entries, action):
441 return False
442
443 # Downlink
444 tableName = 'PreQosPipe.load_far_attributes'
445 actionName = 'PreQosPipe.load_tunnel_far_attributes'
446 matchFields = {}
447 actionParams = {}
448
449 # Match fields
450 matchFields['far_id'] = str(far_id_down)
451 matchFields['session_id'] = str(pfcp_session_id)
452 # Action params
453 actionParams['needs_dropping'] = FALSE
454 actionParams['notify_cp'] = FALSE
455 actionParams['needs_buffering'] = FALSE
456 actionParams['tunnel_type'] = TUNNEL_TYPE_GPDU
457 actionParams['src_addr'] = str(self.s1u_address)
458 actionParams['dst_addr'] = str(self.enb_address)
459 actionParams['teid'] = str(teid_down)
460 actionParams['sport'] = TUNNEL_SPORT
461 if not self.__add_entry(tableName, actionName, matchFields,
462 actionParams, entries, action):
463 return False
464 if action == "program":
465 main.log.info("All entries added successfully.")
466 elif action == "clear":
467 self.__clear_entries(entries)
468
469 def __add_entry(self, tableName, actionName, matchFields, actionParams,
470 entries, action):
471 if action == "program":
472 self.up4_client.buildP4RtTableEntry(
473 tableName=tableName, actionName=actionName,
474 actionParams=actionParams, matchFields=matchFields)
475 if self.up4_client.pushTableEntry(debug=True) == main.TRUE:
476 main.log.info("*** Entry added.")
477 else:
478 main.log.error("Error during table insertion")
479 self.__clear_entries(entries)
480 return False
481 entries.append({"tableName": tableName, "actionName": actionName,
482 "matchFields": matchFields,
483 "actionParams": actionParams})
484 return True
485
486 def __clear_entries(self, entries):
487 for i, entry in enumerate(entries):
488 self.up4_client.buildP4RtTableEntry(**entry)
489 if self.up4_client.deleteTableEntry(debug=True) == main.TRUE:
490 main.log.info(
491 "*** Entry %d of %d deleted." % (i + 1, len(entries)))
492 else:
493 main.log.error("Error during table delete")
Daniele Morobf53dec2021-09-13 18:11:56 +0200494
495 @staticmethod
496 def buildGtpPacket(host, src_ip_outer, dst_ip_outer, src_ip_inner,
497 dst_ip_inner, src_udp_inner, dst_udp_inner, teid):
498 host.buildEther()
499 host.buildIP(src=src_ip_outer, dst=dst_ip_outer)
500 host.buildUDP(ipVersion=4, dport=GPDU_PORT)
501 # FIXME: With newer scapy TEID becomes teid (required for Scapy 2.4.5)
502 host.buildGTP(gtp_type=0xFF, TEID=teid)
503 host.buildIP(overGtp=True, src=src_ip_inner, dst=dst_ip_inner)
504 host.buildUDP(ipVersion=4, overGtp=True, sport=src_udp_inner,
505 dport=dst_udp_inner)
506
507 @staticmethod
508 def buildUdpPacket(host, src_ip, dst_ip, src_udp, dst_udp, src_eth=None,
509 dst_eth=None):
510 host.buildEther(src=src_eth, dst=dst_eth)
511 host.buildIP(src=src_ip, dst=dst_ip)
512 host.buildUDP(ipVersion=4, sport=src_udp, dport=dst_udp)
513
514 @staticmethod
515 def checkFilterAndGetPackets(host):
516 finished = host.checkFilter()
517 if finished:
518 packets = host.readPackets(detailed=True)
519 for p in packets.splitlines():
520 main.log.debug(p)
521 # We care only of the last line from readPackets
522 return packets.splitlines()[-1]
523 else:
524 kill = host.killFilter()
525 main.log.debug(kill)
526 return ""