blob: dd1c72a10678afb8c9ccb12cf0d6f156ec3b8bca [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():
122 self.enodeb_host.buildEther()
123 self.enodeb_host.buildIP(src=self.enb_address, dst=self.s1u_address)
124 self.enodeb_host.buildUDP(ipVersion=4, dport=GPDU_PORT)
125 # FIXME: With newer scapy TEID becomes teid (required for Scapy 2.4.5)
126 self.enodeb_host.buildGTP(gtp_type=0xFF, TEID=int(ue["teid"]))
127 self.enodeb_host.buildIP(overGtp=True, src=ue["ue_address"],
128 dst=self.pdn_interface["ips"][0])
129 self.enodeb_host.buildUDP(ipVersion=4, overGtp=True, sport=UE_PORT,
130 dport=PDN_PORT)
131
132 self.enodeb_host.sendPacket(iface=self.enodeb_interface["name"])
133
134 finished = self.pdn_host.checkFilter()
135 packets = ""
136 if finished:
137 packets = self.pdn_host.readPackets(detailed=True)
138 for p in packets.splitlines():
139 main.log.debug(p)
140 # We care only of the last line from readPackets
141 packets = packets.splitlines()[-1]
142 else:
143 kill = self.pdn_host.killFilter()
144 main.log.debug(kill)
145 fail = False
146 if len(self.emulated_ues) != packets.count('Ether'):
147 fail = True
148 msg = "Failed to capture packets in PDN. "
149 else:
150 msg = "Correctly captured packet in PDN. "
151 # We expect exactly 1 packet per UE
152 pktsFiltered = [packets.count("src=" + ue["ue_address"])
153 for ue in self.emulated_ues.values()]
154 if pktsFiltered.count(1) != len(pktsFiltered):
155 fail = True
156 msg += "More than one packet per UE in downstream. "
157 else:
158 msg += "One packet per UE in upstream. "
159
160 utilities.assert_equal(
161 expect=False, actual=fail, onpass=msg, onfail=msg)
162
163 def testDownstreamTraffic(self):
164 if self.enodeb_host is None or self.pdn_host is None:
165 main.log.error(
166 "Need eNodeB and PDN host params to generate scapy traffic")
167 return
168 pkt_filter_downstream = "ip and udp src port %d and udp dst port %d and dst host %s and src host %s" % (
169 GPDU_PORT, GPDU_PORT, self.enb_address, self.s1u_address)
170 main.log.info("Start listening on %s intf %s" % (
171 self.enodeb_host.name, self.enodeb_interface["name"]))
172 main.log.debug("BPF Filter Downstream: \n %s" % pkt_filter_downstream)
173 self.enodeb_host.startFilter(ifaceName=self.enodeb_interface["name"],
174 sniffCount=len(self.emulated_ues),
175 pktFilter=pkt_filter_downstream)
176
177 main.log.info(
178 "Sending %d packets from PDN host" % len(self.emulated_ues))
179 for ue in self.emulated_ues.values():
180 # From PDN we have to set dest MAC, otherwise scapy will do ARP
181 # request for the UE IP address.
182 self.pdn_host.buildEther(dst=self.router_mac)
183 self.pdn_host.buildIP(src=self.pdn_interface["ips"][0],
184 dst=ue["ue_address"])
185 self.pdn_host.buildUDP(ipVersion=4, sport=PDN_PORT, dport=UE_PORT)
186 self.pdn_host.sendPacket(iface=self.pdn_interface["name"])
187
188 finished = self.enodeb_host.checkFilter()
189 packets = ""
190 if finished:
191 packets = self.enodeb_host.readPackets(detailed=True)
192 for p in packets.splitlines():
193 main.log.debug(p)
194 # We care only of the last line from readPackets
195 packets = packets.splitlines()[-1]
196 else:
197 kill = self.enodeb_host.killFilter()
198 main.log.debug(kill)
199
200 # The BPF filter might capture non-GTP packets because we can't filter
201 # GTP header in BPF. For this reason, check that the captured packets
202 # are from the expected tunnels.
203 # TODO: check inner UDP and IP fields as well
204 # FIXME: with newer scapy TEID becomes teid (required for Scapy 2.4.5)
205 pktsFiltered = [packets.count("TEID=" + hex(int(ue["teid"])) + "L ")
206 for ue in self.emulated_ues.values()]
207
208 fail = False
209 if len(self.emulated_ues) != sum(pktsFiltered):
210 fail = True
211 msg = "Failed to capture packets in eNodeB. "
212 else:
213 msg = "Correctly captured packets in eNodeB. "
214 # We expect exactly 1 packet per UE
215 if pktsFiltered.count(1) != len(pktsFiltered):
216 fail = True
217 msg += "More than one packet per GTP TEID in downstream. "
218 else:
219 msg += "One packet per GTP TEID in downstream. "
220
221 utilities.assert_equal(
222 expect=False, actual=fail, onpass=msg, onfail=msg)
223
224 @staticmethod
225 def __sanitizeUeData(ue):
226 if "five_g" in ue:
227 ue["five_g"] = bool(strtobool(ue["five_g"]))
228 if "qfi" in ue and ue["qfi"] == "":
229 ue["qfi"] = None
230 return ue
231
232 def attachUe(self, pfcp_session_id, ue_address,
233 teid=None, up_id=None, down_id=None,
234 teid_up=None, teid_down=None,
235 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
236 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
237 qfi=None, five_g=False):
238 self.__programUp4Rules(pfcp_session_id,
239 ue_address,
240 teid, up_id, down_id,
241 teid_up, teid_down,
242 pdr_id_up, far_id_up, ctr_id_up,
243 pdr_id_down, far_id_down, ctr_id_down,
244 qfi, five_g, action="program")
245
246 def detachUe(self, pfcp_session_id, ue_address,
247 teid=None, up_id=None, down_id=None,
248 teid_up=None, teid_down=None,
249 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
250 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
251 qfi=None, five_g=False):
252 self.__programUp4Rules(pfcp_session_id,
253 ue_address,
254 teid, up_id, down_id,
255 teid_up, teid_down,
256 pdr_id_up, far_id_up, ctr_id_up,
257 pdr_id_down, far_id_down, ctr_id_down,
258 qfi, five_g, action="clear")
259
260 def __programUp4Rules(self, pfcp_session_id, ue_address,
261 teid=None, up_id=None, down_id=None,
262 teid_up=None, teid_down=None,
263 pdr_id_up=None, far_id_up=None, ctr_id_up=None,
264 pdr_id_down=None, far_id_down=None, ctr_id_down=None,
265 qfi=None, five_g=False, action="program"):
266 if up_id is not None:
267 pdr_id_up = up_id
268 far_id_up = up_id
269 ctr_id_up = up_id
270 if down_id is not None:
271 pdr_id_down = down_id
272 far_id_down = down_id
273 ctr_id_down = down_id
274 if teid is not None:
275 teid_up = teid
276 teid_down = teid
277
278 entries = []
279
280 # ========================#
281 # PDR Entries
282 # ========================#
283
284 # Uplink
285 tableName = 'PreQosPipe.pdrs'
286 actionName = ''
287 matchFields = {}
288 actionParams = {}
289 if qfi is None:
290 actionName = 'PreQosPipe.set_pdr_attributes'
291 else:
292 actionName = 'PreQosPipe.set_pdr_attributes_qos'
293 if five_g:
294 # TODO: currently QFI_MATCH is unsupported in TNA
295 matchFields['has_qfi'] = TRUE
296 matchFields["qfi"] = str(qfi)
297 actionParams['needs_qfi_push'] = FALSE
298 actionParams['qfi'] = str(qfi)
299 # Match fields
300 matchFields['src_iface'] = IFACE_ACCESS
301 matchFields['ue_addr'] = str(ue_address)
302 matchFields['teid'] = str(teid_up)
303 matchFields['tunnel_ipv4_dst'] = str(self.s1u_address)
304 # Action params
305 actionParams['id'] = str(pdr_id_up)
306 actionParams['fseid'] = str(pfcp_session_id)
307 actionParams['ctr_id'] = str(ctr_id_up)
308 actionParams['far_id'] = str(far_id_up)
309 actionParams['needs_gtpu_decap'] = TRUE
310 if not self.__add_entry(tableName, actionName, matchFields,
311 actionParams, entries, action):
312 return False
313
314 # Downlink
315 tableName = 'PreQosPipe.pdrs'
316 matchFields = {}
317 actionParams = {}
318 if qfi is None:
319 actionName = 'PreQosPipe.set_pdr_attributes'
320 else:
321 actionName = 'PreQosPipe.set_pdr_attributes_qos'
322 # TODO: currently QFI_PUSH is unsupported in TNA
323 actionParams['needs_qfi_push'] = TRUE if five_g else FALSE
324 actionParams['qfi'] = str(qfi)
325 # Match fields
326 matchFields['src_iface'] = IFACE_CORE
327 matchFields['ue_addr'] = str(ue_address)
328 # Action params
329 actionParams['id'] = str(pdr_id_down)
330 actionParams['fseid'] = str(pfcp_session_id)
331 actionParams['ctr_id'] = str(ctr_id_down)
332 actionParams['far_id'] = str(far_id_down)
333 actionParams['needs_gtpu_decap'] = FALSE
334 if not self.__add_entry(tableName, actionName, matchFields,
335 actionParams, entries, action):
336 return False
337
338 # ========================#
339 # FAR Entries
340 # ========================#
341
342 # Uplink
343 tableName = 'PreQosPipe.load_far_attributes'
344 actionName = 'PreQosPipe.load_normal_far_attributes'
345 matchFields = {}
346 actionParams = {}
347
348 # Match fields
349 matchFields['far_id'] = str(far_id_up)
350 matchFields['session_id'] = str(pfcp_session_id)
351 # Action params
352 actionParams['needs_dropping'] = FALSE
353 actionParams['notify_cp'] = FALSE
354 if not self.__add_entry(tableName, actionName, matchFields,
355 actionParams, entries, action):
356 return False
357
358 # Downlink
359 tableName = 'PreQosPipe.load_far_attributes'
360 actionName = 'PreQosPipe.load_tunnel_far_attributes'
361 matchFields = {}
362 actionParams = {}
363
364 # Match fields
365 matchFields['far_id'] = str(far_id_down)
366 matchFields['session_id'] = str(pfcp_session_id)
367 # Action params
368 actionParams['needs_dropping'] = FALSE
369 actionParams['notify_cp'] = FALSE
370 actionParams['needs_buffering'] = FALSE
371 actionParams['tunnel_type'] = TUNNEL_TYPE_GPDU
372 actionParams['src_addr'] = str(self.s1u_address)
373 actionParams['dst_addr'] = str(self.enb_address)
374 actionParams['teid'] = str(teid_down)
375 actionParams['sport'] = TUNNEL_SPORT
376 if not self.__add_entry(tableName, actionName, matchFields,
377 actionParams, entries, action):
378 return False
379 if action == "program":
380 main.log.info("All entries added successfully.")
381 elif action == "clear":
382 self.__clear_entries(entries)
383
384 def __add_entry(self, tableName, actionName, matchFields, actionParams,
385 entries, action):
386 if action == "program":
387 self.up4_client.buildP4RtTableEntry(
388 tableName=tableName, actionName=actionName,
389 actionParams=actionParams, matchFields=matchFields)
390 if self.up4_client.pushTableEntry(debug=True) == main.TRUE:
391 main.log.info("*** Entry added.")
392 else:
393 main.log.error("Error during table insertion")
394 self.__clear_entries(entries)
395 return False
396 entries.append({"tableName": tableName, "actionName": actionName,
397 "matchFields": matchFields,
398 "actionParams": actionParams})
399 return True
400
401 def __clear_entries(self, entries):
402 for i, entry in enumerate(entries):
403 self.up4_client.buildP4RtTableEntry(**entry)
404 if self.up4_client.deleteTableEntry(debug=True) == main.TRUE:
405 main.log.info(
406 "*** Entry %d of %d deleted." % (i + 1, len(entries)))
407 else:
408 main.log.error("Error during table delete")