/src/open5gs/lib/ipfw/ogs-ipfw.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) 2019-2024 by Sukchan Lee <acetcom@gmail.com> |
3 | | * |
4 | | * This file is part of Open5GS. |
5 | | * |
6 | | * This program is free software: you can redistribute it and/or modify |
7 | | * it under the terms of the GNU Affero General Public License as published by |
8 | | * the Free Software Foundation, either version 3 of the License, or |
9 | | * (at your option) any later version. |
10 | | * |
11 | | * This program is distributed in the hope that it will be useful, |
12 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | | * GNU General Public License for more details. |
15 | | * |
16 | | * You should have received a copy of the GNU General Public License |
17 | | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
18 | | */ |
19 | | |
20 | | #ifndef IF_NAMESIZE |
21 | | #define IF_NAMESIZE 16 |
22 | | #ifndef IFNAMSIZ |
23 | | #define IFNAMSIZ IF_NAMESIZE |
24 | | #endif |
25 | | #endif |
26 | | |
27 | | #include "ipfw2.h" |
28 | | #include "objs/include_e/netinet/ip_fw.h" |
29 | | |
30 | 3.63k | #define MAX_NUM_OF_TOKEN 32 |
31 | | #define MAX_NUM_OF_RULE_BUFFER 1024 |
32 | | |
33 | | void compile_rule(char *av[], uint32_t *rbuf, int *rbufsize, void *tstate); |
34 | | |
35 | | /* Set by the errx() override in ipfw2.c when a rule fails to parse */ |
36 | | extern int ogs_ipfw_parse_error; |
37 | | |
38 | | /* Protocol name table shared with add_proto0() in ipfw2.c */ |
39 | | extern int ipfw_proto_by_name(const char *name); |
40 | | |
41 | | /* |
42 | | * 3GPP TS 29.212 limits the IPFilterRule carried in a Flow-Description to |
43 | | * |
44 | | * permit out <proto> from <src> [<ports>] to <dst> [<ports>] |
45 | | * |
46 | | * compile_rule() below is the full ipfw(8) command line parser. It also |
47 | | * accepts hostnames, address sets, lookup tables and the entire option |
48 | | * keyword space, and -- because errx() no longer exits -- it keeps parsing |
49 | | * after it has already rejected a token, dereferencing arguments that are |
50 | | * not there. A Flow-Description from a PCF, PCRF or AF can therefore crash |
51 | | * an SMF, UPF or SGW-U: |
52 | | * |
53 | | * permit out ip -> SEGV in add_src() |
54 | | * permit out ip from 1.2.3.4 -> SEGV in add_dst() |
55 | | * permit out ip from 1.2.3.4 to 1.2.3.4 uid -> SEGV in compile_rule() |
56 | | * permit out ip from table(1) to assigned -> SEGV in pack_table() |
57 | | * permit out 58 from ff02::2/129 to assigned -> exit() from ipv6.c |
58 | | * |
59 | | * Check the token stream against the grammar we accept before handing it |
60 | | * over. The structure, the addresses and the ports are settled here; an |
61 | | * unrecognised protocol name is the one thing still left to the parser, |
62 | | * which rejects it through ogs_ipfw_parse_error below. |
63 | | */ |
64 | | static bool ipfw_parse_number(const char *s, int min, int max, int *value) |
65 | 1.30k | { |
66 | 1.30k | int v = 0; |
67 | | |
68 | 1.30k | if (!s || !*s) |
69 | 43 | return false; |
70 | | |
71 | 3.54k | for (; *s; s++) { |
72 | 2.41k | if (*s < '0' || *s > '9') |
73 | 103 | return false; |
74 | 2.31k | v = v * 10 + (*s - '0'); |
75 | 2.31k | if (v > max) |
76 | 30 | return false; |
77 | 2.31k | } |
78 | | |
79 | 1.12k | if (v < min) |
80 | 6 | return false; |
81 | | |
82 | 1.12k | if (value) |
83 | 905 | *value = v; |
84 | | |
85 | 1.12k | return true; |
86 | 1.12k | } |
87 | | |
88 | | static bool ipfw_is_number(const char *s, int min, int max) |
89 | 141 | { |
90 | 141 | return ipfw_parse_number(s, min, max, NULL); |
91 | 141 | } |
92 | | |
93 | | /* |
94 | | * "ip", a protocol number, or a name that resolves to one. |
95 | | * |
96 | | * The token is rewritten in place as a number so that compile_rule() only |
97 | | * ever sees "ip" or a decimal, because every other spelling it understands |
98 | | * ends up meaning "no protocol restriction" once the rule reaches |
99 | | * ogs_ipfw_rule_t: |
100 | | * |
101 | | * - add_proto0() stores the number in a u_char without a range check, so |
102 | | * "256" is truncated to 0 and a UDP filter turns into "ip"; "999" |
103 | | * becomes protocol 231. |
104 | | * - add_proto() matches "all" with _substrcmp(), which succeeds on a |
105 | | * prefix, so "a" and "al" are accepted as "all" -> protocol 0. |
106 | | * - "ip4", "ipv4", "ip6" and "ipv6" are family selectors emitted as O_IP4 |
107 | | * or O_IP6, opcodes the loop below does not read, so the restriction is |
108 | | * dropped and protocol 0 is what remains. |
109 | | * - a name whose number is 0 -- "hopopt" -- passes add_proto0() unchanged |
110 | | * and lands as protocol 0 as well. |
111 | | * |
112 | | * Resolve the name through ipfw_proto_by_name(), the table add_proto0() |
113 | | * itself uses, rather than keeping a second one here. |
114 | | */ |
115 | | static int ipfw_check_proto(char **token, char *buf, int size, int *protop) |
116 | 1.13k | { |
117 | 1.13k | const char *s = *token; |
118 | 1.13k | const char *p; |
119 | 1.13k | int proto; |
120 | | |
121 | 1.13k | if (!s || !*s) |
122 | 0 | return OGS_ERROR; |
123 | | |
124 | | /* the only name carried through: no protocol restriction */ |
125 | 1.13k | if (strcmp(s, "ip") == 0) { |
126 | 8 | *protop = IPPROTO_IP; |
127 | 8 | return OGS_OK; |
128 | 8 | } |
129 | | |
130 | 1.12k | if (*s >= '0' && *s <= '9') { |
131 | 731 | if (!ipfw_parse_number(s, 1, 255, &proto)) |
132 | 24 | return OGS_ERROR; |
133 | 731 | } else { |
134 | 3.24k | for (p = s; *p; p++) { |
135 | 2.88k | if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || |
136 | 712 | (*p >= '0' && *p <= '9') || *p == '-') |
137 | 2.84k | continue; |
138 | 39 | return OGS_ERROR; |
139 | 2.88k | } |
140 | | |
141 | | /* |
142 | | * "all" and the family selectors would resolve to something, or to |
143 | | * nothing, that no longer means what was written. Refuse them by |
144 | | * name so that the operator is told to use "ip" instead. |
145 | | */ |
146 | 358 | if (strcmp(s, "all") == 0 || strcmp(s, "ip4") == 0 || |
147 | 356 | strcmp(s, "ipv4") == 0 || strcmp(s, "ip6") == 0 || |
148 | 354 | strcmp(s, "ipv6") == 0) |
149 | 5 | return OGS_ERROR; |
150 | | |
151 | 353 | proto = ipfw_proto_by_name(s); |
152 | 353 | if (proto < 1 || proto > 255) |
153 | 351 | return OGS_ERROR; |
154 | 353 | } |
155 | | |
156 | 709 | ogs_snprintf(buf, size, "%d", proto); |
157 | 709 | *token = buf; |
158 | 709 | *protop = proto; |
159 | | |
160 | 709 | return OGS_OK; |
161 | 1.12k | } |
162 | | |
163 | | /* |
164 | | * "any", "assigned", or a literal IPv4/IPv6 address with optional prefix. |
165 | | * |
166 | | * Returns the address family, AF_UNSPEC for the two keywords -- which carry |
167 | | * no family of their own -- or -1 when the token is not an address at all. |
168 | | */ |
169 | | static int ipfw_addr_family(const char *s) |
170 | 867 | { |
171 | 867 | char buf[OGS_ADDRSTRLEN]; |
172 | 867 | struct in_addr addr4; |
173 | 867 | struct in6_addr addr6; |
174 | 867 | const char *slash; |
175 | 867 | size_t len; |
176 | | |
177 | 867 | if (!s || !*s) |
178 | 0 | return -1; |
179 | | |
180 | 867 | if (strcmp(s, "any") == 0 || strcmp(s, "assigned") == 0) |
181 | 69 | return AF_UNSPEC; |
182 | | |
183 | 798 | slash = strchr(s, '/'); |
184 | 798 | len = slash ? (size_t)(slash - s) : strlen(s); |
185 | 798 | if (len == 0 || len >= sizeof(buf)) |
186 | 10 | return -1; |
187 | 788 | memcpy(buf, s, len); |
188 | 788 | buf[len] = '\0'; |
189 | | |
190 | 788 | if (inet_pton(AF_INET, buf, &addr4) == 1) |
191 | 63 | return (!slash || ipfw_is_number(slash + 1, 0, 32)) ? AF_INET : -1; |
192 | 725 | if (inet_pton(AF_INET6, buf, &addr6) == 1) |
193 | 623 | return (!slash || ipfw_is_number(slash + 1, 0, 128)) ? AF_INET6 : -1; |
194 | | |
195 | 102 | return -1; |
196 | 725 | } |
197 | | |
198 | | /* |
199 | | * A port number, written in decimal. |
200 | | * |
201 | | * fill_newports() reads it with strtol(..., 0), where a leading zero means |
202 | | * octal, so the two readings have to be kept from diverging: "010" would be |
203 | | * accepted here as 10 and compiled as 8, and "08" is not a number at all to |
204 | | * the parser, which then leaves the rule's remaining tokens shifted by one |
205 | | * and sends "to" through lookup_host() as if it were an address. |
206 | | */ |
207 | | static bool ipfw_parse_port(const char *s, int *value) |
208 | 455 | { |
209 | 455 | if (s && s[0] == '0' && s[1] != '\0') |
210 | 25 | return false; |
211 | | |
212 | 430 | return ipfw_parse_number(s, 1, 65535, value); |
213 | 455 | } |
214 | | |
215 | | /* |
216 | | * A single port or a "low-high" range, 1..65535. |
217 | | * |
218 | | * Port 0 must not be accepted: ogs_ipfw_rule_t uses 0 to mean "no port |
219 | | * condition", so "from 1.2.3.4 0" would silently become "from 1.2.3.4" and |
220 | | * widen the filter to every port. fill_newports() stores the two ends of a |
221 | | * range without comparing them, so a reversed range has to be caught here |
222 | | * as well. |
223 | | */ |
224 | | static bool ipfw_is_ports(const char *s) |
225 | 325 | { |
226 | 325 | char buf[8]; |
227 | 325 | const char *dash; |
228 | 325 | size_t len; |
229 | 325 | int low, high; |
230 | | |
231 | 325 | if (!s || !*s) |
232 | 0 | return false; |
233 | | |
234 | 325 | dash = strchr(s, '-'); |
235 | 325 | if (!dash) |
236 | 157 | return ipfw_parse_port(s, NULL); |
237 | | |
238 | 168 | len = dash - s; |
239 | 168 | if (len == 0 || len >= sizeof(buf)) |
240 | 3 | return false; |
241 | 165 | memcpy(buf, s, len); |
242 | 165 | buf[len] = '\0'; |
243 | | |
244 | 165 | if (!ipfw_parse_port(buf, &low)) |
245 | 32 | return false; |
246 | 133 | if (!ipfw_parse_port(dash + 1, &high)) |
247 | 68 | return false; |
248 | | |
249 | 65 | return low <= high; |
250 | 133 | } |
251 | | |
252 | | /* |
253 | | * av[0] is unused and av[1] is "permit", so the tokens to check are |
254 | | * av[2] .. av[last-1]. The direction, which the caller has moved to the |
255 | | * end of the array, is not part of this grammar. |
256 | | */ |
257 | | static int ipfw_check_tokens( |
258 | | char *av[], int last, char *proto_buf, int proto_buf_size, |
259 | | char *flow_description) |
260 | 1.13k | { |
261 | 1.13k | int x = 2; |
262 | 1.13k | int proto = IPPROTO_IP; |
263 | 1.13k | int src_family, dst_family; |
264 | | |
265 | 1.13k | #define REJECT(reason) \ |
266 | 1.13k | do { \ |
267 | 929 | ogs_error("Invalid Flow-Description [%s] : %s", \ |
268 | 929 | flow_description, reason); \ |
269 | 929 | return OGS_ERROR; \ |
270 | 929 | } while (0) |
271 | 1.13k | #define REQUIRE(cond, reason) \ |
272 | 5.82k | do { if (!(cond)) REJECT(reason); } while (0) |
273 | | |
274 | 1.13k | REQUIRE(x < last, "no protocol"); |
275 | 1.13k | REQUIRE(ipfw_check_proto( |
276 | 1.13k | &av[x], proto_buf, proto_buf_size, &proto) == OGS_OK, |
277 | 1.13k | "bad protocol"); |
278 | 717 | x++; |
279 | | |
280 | 717 | REQUIRE(x < last && strcmp(av[x], "from") == 0, "missing 'from'"); |
281 | 640 | x++; |
282 | | |
283 | 640 | REQUIRE(x < last, "no source address"); |
284 | | /* |
285 | | * Refer to lib/ipfw/ogs-ipfw.h |
286 | | * Issue #338 |
287 | | * |
288 | | * A Flow-Description is always written in the downlink orientation, so the |
289 | | * UE address -- "assigned" -- can only appear after "to". An uplink flow |
290 | | * arrives as RX "permit in from <UE> to <REMOTE>", which flow_rx_to_gx() |
291 | | * has already rewritten into that form before it reaches here. |
292 | | */ |
293 | 639 | if (strcmp(av[x], "assigned") == 0) |
294 | 1 | REJECT("'assigned' is the UE address and is only valid after 'to'"); |
295 | 638 | src_family = ipfw_addr_family(av[x]); |
296 | 638 | REQUIRE(src_family >= 0, "bad source address"); |
297 | 499 | x++; |
298 | | |
299 | 499 | if (x < last && strcmp(av[x], "to") != 0) { |
300 | 288 | REQUIRE(ipfw_is_ports(av[x]), "bad source port"); |
301 | 127 | x++; |
302 | 127 | } |
303 | | |
304 | 338 | REQUIRE(x < last && strcmp(av[x], "to") == 0, "missing 'to'"); |
305 | 230 | x++; |
306 | | |
307 | 230 | REQUIRE(x < last, "no destination address"); |
308 | 229 | dst_family = ipfw_addr_family(av[x]); |
309 | 229 | REQUIRE(dst_family >= 0, "bad destination address"); |
310 | 220 | x++; |
311 | | |
312 | 220 | if (x < last) { |
313 | 37 | REQUIRE(ipfw_is_ports(av[x]), "bad destination port"); |
314 | 30 | x++; |
315 | 30 | } |
316 | | |
317 | 213 | REQUIRE(x == last, "trailing token"); |
318 | | |
319 | | /* one packet cannot carry both families */ |
320 | 212 | REQUIRE(src_family == AF_UNSPEC || dst_family == AF_UNSPEC || |
321 | 212 | src_family == dst_family, |
322 | 212 | "source and destination address family differ"); |
323 | | |
324 | | /* |
325 | | * Protocol 41 is IPv6 encapsulation, and the outer addresses of a |
326 | | * 6in4 tunnel are legitimately IPv4. add_src() and add_dst(), however, |
327 | | * treat the number as if it selected the address family: once the |
328 | | * protocol is IPPROTO_IPV6 they send every address through |
329 | | * add_srcip6() and add_dstip6(), whatever it looks like. ipv6.c calls |
330 | | * the real errx() from err.h -- it never sees the override in |
331 | | * ipfw2.c -- so an IPv4 literal exits the daemon rather than failing |
332 | | * the rule. Refuse the combination until the parser can carry it. |
333 | | */ |
334 | 210 | if (proto == IPPROTO_IPV6) |
335 | 6 | REQUIRE(src_family != AF_INET && dst_family != AF_INET, |
336 | 210 | "IPv4 literal with protocol 41 is unsupported"); |
337 | | |
338 | 208 | #undef REQUIRE |
339 | 208 | #undef REJECT |
340 | | |
341 | 208 | return OGS_OK; |
342 | 210 | } |
343 | | |
344 | | |
345 | | int ogs_ipfw_compile_rule(ogs_ipfw_rule_t *ipfw_rule, char *flow_description) |
346 | 1.23k | { |
347 | 1.23k | char *token, *dir; |
348 | 1.23k | char *saveptr; |
349 | 1.23k | int i; |
350 | | |
351 | 1.23k | char *av[MAX_NUM_OF_TOKEN]; |
352 | 1.23k | char proto_buf[4]; |
353 | 1.23k | uint32_t rulebuf[MAX_NUM_OF_RULE_BUFFER]; |
354 | 1.23k | int rbufsize; |
355 | 1.23k | struct ip_fw_rule *rule = (struct ip_fw_rule *)rulebuf; |
356 | | |
357 | 1.23k | int x, l; |
358 | 1.23k | ipfw_insn *cmd; |
359 | | |
360 | 1.23k | char *description = NULL; |
361 | | |
362 | 1.23k | ogs_assert(ipfw_rule); |
363 | 1.23k | ogs_assert(flow_description); |
364 | | |
365 | 1.23k | rbufsize = sizeof(rulebuf); |
366 | 1.23k | memset(rulebuf, 0, rbufsize); |
367 | | |
368 | 1.23k | av[0] = NULL; |
369 | | |
370 | | /* ACTION */ |
371 | 1.23k | description = ogs_strdup(flow_description); |
372 | 1.23k | ogs_assert(description); |
373 | | |
374 | 1.23k | token = ogs_strtok_r(description, " ", &saveptr); |
375 | 1.23k | if (!token || strcmp(token, "permit") != 0) { |
376 | 61 | ogs_error("Not begins with reserved keyword : 'permit'"); |
377 | 61 | ogs_free(description); |
378 | 61 | return OGS_ERROR; |
379 | 61 | } |
380 | 1.17k | av[1] = token; |
381 | | |
382 | | /* Save DIRECTION */ |
383 | 1.17k | dir = token = ogs_strtok_r(NULL, " ", &saveptr); |
384 | 1.17k | if (!token || strcmp(token, "out") != 0) { |
385 | 33 | ogs_error("Not begins with reserved keyword : 'permit out'"); |
386 | 33 | ogs_free(description); |
387 | 33 | return OGS_ERROR; |
388 | 33 | } |
389 | | |
390 | | /* ADDR */ |
391 | 1.13k | i = 2; |
392 | 1.13k | token = ogs_strtok_r(NULL, " ", &saveptr); |
393 | 4.77k | while ((token != NULL) && (i < (MAX_NUM_OF_TOKEN-2))) { |
394 | 3.63k | av[i++] = token; |
395 | 3.63k | token = ogs_strtok_r(NULL, " ", &saveptr); |
396 | 3.63k | } |
397 | | |
398 | | /* Add DIRECTION */ |
399 | 1.13k | av[i++] = dir; |
400 | | |
401 | 1.13k | av[i] = NULL; |
402 | | |
403 | 1.13k | if (ipfw_check_tokens(av, i-1, proto_buf, sizeof(proto_buf), |
404 | 1.13k | flow_description) != OGS_OK) { |
405 | 929 | ogs_free(description); |
406 | 929 | return OGS_ERROR; |
407 | 929 | } |
408 | | |
409 | | /* "to assigned" --> "to any" */ |
410 | 1.51k | for (x = 2; av[x] != NULL; x++) { |
411 | 1.30k | if (strcmp(av[x], "assigned") == 0 && strcmp(av[x-1], "to") == 0) { |
412 | 1 | av[x] = "any"; |
413 | 1 | break; |
414 | 1 | } |
415 | 1.30k | } |
416 | | |
417 | 208 | ogs_ipfw_parse_error = 0; |
418 | 208 | compile_rule(av, (uint32_t *)rule, &rbufsize, NULL); |
419 | | |
420 | 208 | memset(ipfw_rule, 0, sizeof(ogs_ipfw_rule_t)); |
421 | | |
422 | 208 | if (ogs_ipfw_parse_error) { |
423 | 0 | ogs_error("Cannot compile Flow-Description [%s]", flow_description); |
424 | 0 | ogs_free(description); |
425 | 0 | return OGS_ERROR; |
426 | 0 | } |
427 | 208 | for (l = rule->act_ofs, cmd = rule->cmd; |
428 | 1.02k | l > 0 ; l -= F_LEN(cmd) , cmd += F_LEN(cmd)) { |
429 | 814 | uint32_t *a = NULL; |
430 | 814 | uint16_t *p = NULL; |
431 | 814 | switch (cmd->opcode) { |
432 | 201 | case O_PROTO: |
433 | 201 | ipfw_rule->proto = cmd->arg1; |
434 | 201 | break; |
435 | 7 | case O_IP_SRC: |
436 | 11 | case O_IP_SRC_MASK: |
437 | 11 | a = ((ipfw_insn_u32 *)cmd)->d; |
438 | 11 | ipfw_rule->ipv4_src = 1; |
439 | 11 | ipfw_rule->ip.src.addr[0] = a[0]; |
440 | 11 | if (cmd->opcode == O_IP_SRC_MASK) |
441 | 4 | ipfw_rule->ip.src.mask[0] = a[1]; |
442 | 7 | else |
443 | 7 | ipfw_rule->ip.src.mask[0] = 0xffffffff; |
444 | 11 | break; |
445 | 9 | case O_IP_DST: |
446 | 41 | case O_IP_DST_MASK: |
447 | 41 | a = ((ipfw_insn_u32 *)cmd)->d; |
448 | 41 | ipfw_rule->ipv4_dst = 1; |
449 | 41 | ipfw_rule->ip.dst.addr[0] = a[0]; |
450 | 41 | if (cmd->opcode == O_IP_DST_MASK) |
451 | 32 | ipfw_rule->ip.dst.mask[0] = a[1]; |
452 | 9 | else |
453 | 9 | ipfw_rule->ip.dst.mask[0] = 0xffffffff; |
454 | 41 | break; |
455 | 127 | case O_IP6_SRC: |
456 | 143 | case O_IP6_SRC_MASK: |
457 | 143 | a = ((ipfw_insn_u32 *)cmd)->d; |
458 | 143 | ipfw_rule->ipv6_src = 1; |
459 | 143 | memcpy(ipfw_rule->ip.src.addr, a, OGS_IPV6_LEN); |
460 | 143 | if (cmd->opcode == O_IP6_SRC_MASK) |
461 | 16 | memcpy(ipfw_rule->ip.src.mask, a+4, OGS_IPV6_LEN); |
462 | 127 | else |
463 | 127 | n2mask((struct in6_addr *)ipfw_rule->ip.src.mask, 128); |
464 | 143 | break; |
465 | 120 | case O_IP6_DST: |
466 | 145 | case O_IP6_DST_MASK: |
467 | 145 | a = ((ipfw_insn_u32 *)cmd)->d; |
468 | 145 | ipfw_rule->ipv6_dst = 1; |
469 | 145 | memcpy(ipfw_rule->ip.dst.addr, a, OGS_IPV6_LEN); |
470 | 145 | if (cmd->opcode == O_IP6_DST_MASK) |
471 | 25 | memcpy(ipfw_rule->ip.dst.mask, a+4, OGS_IPV6_LEN); |
472 | 120 | else |
473 | 120 | n2mask((struct in6_addr *)ipfw_rule->ip.dst.mask, 128); |
474 | 145 | break; |
475 | 29 | case O_IP_SRCPORT: |
476 | 29 | p = ((ipfw_insn_u16 *)cmd)->ports; |
477 | 29 | ipfw_rule->port.src.low = p[0]; |
478 | 29 | ipfw_rule->port.src.high = p[1]; |
479 | 29 | break; |
480 | 29 | case O_IP_DSTPORT: |
481 | 29 | p = ((ipfw_insn_u16 *)cmd)->ports; |
482 | 29 | ipfw_rule->port.dst.low = p[0]; |
483 | 29 | ipfw_rule->port.dst.high = p[1]; |
484 | 29 | break; |
485 | 814 | } |
486 | 814 | } |
487 | | |
488 | 208 | ogs_free(description); |
489 | 208 | return OGS_OK; |
490 | 208 | } |
491 | | |
492 | | char *ogs_ipfw_encode_flow_description(ogs_ipfw_rule_t *ipfw_rule) |
493 | 0 | { |
494 | 0 | char flow_description[OGS_HUGE_LEN]; |
495 | 0 | char *p, *last; |
496 | 0 | char buf[OGS_ADDRSTRLEN]; |
497 | 0 | ogs_sockaddr_t sa; |
498 | 0 | int prefixlen = 0; |
499 | |
|
500 | 0 | p = flow_description; |
501 | 0 | last = flow_description + OGS_HUGE_LEN; |
502 | |
|
503 | 0 | ogs_assert(ipfw_rule); |
504 | | |
505 | 0 | p = ogs_slprintf(p, last, "permit out"); |
506 | |
|
507 | 0 | if (ipfw_rule->proto) { |
508 | 0 | p = ogs_slprintf(p, last, " %d", ipfw_rule->proto); |
509 | 0 | } else { |
510 | 0 | p = ogs_slprintf(p, last, " ip"); |
511 | 0 | } |
512 | |
|
513 | 0 | #define IPV4_BITLEN (OGS_IPV4_LEN * 8) |
514 | 0 | #define IPV6_BITLEN (OGS_IPV6_LEN * 8) |
515 | |
|
516 | 0 | p = ogs_slprintf(p, last, " from"); |
517 | 0 | memset(&sa, 0, sizeof(sa)); |
518 | |
|
519 | 0 | if (ipfw_rule->ipv4_src) { |
520 | 0 | sa.ogs_sa_family = AF_INET; |
521 | 0 | memcpy(&sa.sin.sin_addr, |
522 | 0 | ipfw_rule->ip.src.addr, sizeof(struct in_addr)); |
523 | |
|
524 | 0 | OGS_ADDR(&sa, buf); |
525 | 0 | prefixlen = contigmask( |
526 | 0 | (uint8_t *)ipfw_rule->ip.src.mask, IPV4_BITLEN); |
527 | |
|
528 | 0 | if (prefixlen < 0) { |
529 | 0 | ogs_error("Invalid mask[%x:%x:%x:%x]", |
530 | 0 | ipfw_rule->ip.src.mask[0], |
531 | 0 | ipfw_rule->ip.src.mask[1], |
532 | 0 | ipfw_rule->ip.src.mask[2], |
533 | 0 | ipfw_rule->ip.src.mask[3]); |
534 | 0 | return NULL; |
535 | 0 | } else if (prefixlen == 0) { |
536 | 0 | p = ogs_slprintf(p, last, " any"); |
537 | 0 | } else if (prefixlen > 0 && prefixlen < IPV4_BITLEN) { |
538 | 0 | p = ogs_slprintf(p, last, " %s/%d", buf, prefixlen); |
539 | 0 | } else if (prefixlen == IPV4_BITLEN) { |
540 | 0 | p = ogs_slprintf(p, last, " %s", buf); |
541 | 0 | } else { |
542 | 0 | ogs_fatal("Invalid prefixlen[%d]", prefixlen); |
543 | 0 | ogs_assert_if_reached(); |
544 | 0 | } |
545 | |
|
546 | 0 | } else if (ipfw_rule->ipv6_src) { |
547 | 0 | sa.ogs_sa_family = AF_INET6; |
548 | 0 | memcpy(&sa.sin6.sin6_addr, |
549 | 0 | ipfw_rule->ip.src.addr, sizeof(struct in6_addr)); |
550 | |
|
551 | 0 | OGS_ADDR(&sa, buf); |
552 | 0 | prefixlen = contigmask( |
553 | 0 | (uint8_t *)ipfw_rule->ip.src.mask, IPV6_BITLEN); |
554 | |
|
555 | 0 | if (prefixlen < 0) { |
556 | 0 | ogs_error("Invalid mask[%x:%x:%x:%x]", |
557 | 0 | ipfw_rule->ip.src.mask[0], |
558 | 0 | ipfw_rule->ip.src.mask[1], |
559 | 0 | ipfw_rule->ip.src.mask[2], |
560 | 0 | ipfw_rule->ip.src.mask[3]); |
561 | 0 | return NULL; |
562 | 0 | } else if (prefixlen == 0) { |
563 | 0 | p = ogs_slprintf(p, last, " any"); |
564 | 0 | } else if (prefixlen > 0 && prefixlen < IPV6_BITLEN) { |
565 | 0 | p = ogs_slprintf(p, last, " %s/%d", buf, prefixlen); |
566 | 0 | } else if (prefixlen == IPV6_BITLEN) { |
567 | 0 | p = ogs_slprintf(p, last, " %s", buf); |
568 | 0 | } else { |
569 | 0 | ogs_fatal("Invalid prefixlen[%d]", prefixlen); |
570 | 0 | ogs_assert_if_reached(); |
571 | 0 | } |
572 | 0 | } else |
573 | 0 | p = ogs_slprintf(p, last, " any"); |
574 | | |
575 | 0 | if (ipfw_rule->port.src.low == ipfw_rule->port.src.high) { |
576 | 0 | if (ipfw_rule->port.src.low == 0) { |
577 | | /* Nothing */ |
578 | 0 | } else { |
579 | 0 | p = ogs_slprintf(p, last, " %d", ipfw_rule->port.src.low); |
580 | 0 | } |
581 | 0 | } else { |
582 | 0 | p = ogs_slprintf(p, last, " %d-%d", |
583 | 0 | ipfw_rule->port.src.low, ipfw_rule->port.src.high); |
584 | 0 | } |
585 | |
|
586 | 0 | p = ogs_slprintf(p, last, " to"); |
587 | 0 | memset(&sa, 0, sizeof(sa)); |
588 | |
|
589 | 0 | if (ipfw_rule->ipv4_dst) { |
590 | 0 | sa.ogs_sa_family = AF_INET; |
591 | 0 | memcpy(&sa.sin.sin_addr, |
592 | 0 | ipfw_rule->ip.dst.addr, sizeof(struct in_addr)); |
593 | |
|
594 | 0 | OGS_ADDR(&sa, buf); |
595 | 0 | prefixlen = contigmask( |
596 | 0 | (uint8_t *)ipfw_rule->ip.dst.mask, IPV4_BITLEN); |
597 | |
|
598 | 0 | if (prefixlen < 0) { |
599 | 0 | ogs_error("Invalid mask[%x:%x:%x:%x]", |
600 | 0 | ipfw_rule->ip.dst.mask[0], |
601 | 0 | ipfw_rule->ip.dst.mask[1], |
602 | 0 | ipfw_rule->ip.dst.mask[2], |
603 | 0 | ipfw_rule->ip.dst.mask[3]); |
604 | 0 | return NULL; |
605 | 0 | } else if (prefixlen == 0) { |
606 | 0 | p = ogs_slprintf(p, last, " assigned"); |
607 | 0 | } else if (prefixlen > 0 && prefixlen < IPV4_BITLEN) { |
608 | 0 | p = ogs_slprintf(p, last, " %s/%d", buf, prefixlen); |
609 | 0 | } else if (prefixlen == IPV4_BITLEN) { |
610 | 0 | p = ogs_slprintf(p, last, " %s", buf); |
611 | 0 | } else { |
612 | 0 | ogs_fatal("Invalid prefixlen[%d]", prefixlen); |
613 | 0 | ogs_assert_if_reached(); |
614 | 0 | } |
615 | |
|
616 | 0 | } else if (ipfw_rule->ipv6_dst) { |
617 | 0 | sa.ogs_sa_family = AF_INET6; |
618 | 0 | memcpy(&sa.sin6.sin6_addr, |
619 | 0 | ipfw_rule->ip.dst.addr, sizeof(struct in6_addr)); |
620 | |
|
621 | 0 | OGS_ADDR(&sa, buf); |
622 | 0 | prefixlen = contigmask( |
623 | 0 | (uint8_t *)ipfw_rule->ip.dst.mask, IPV6_BITLEN); |
624 | |
|
625 | 0 | if (prefixlen < 0) { |
626 | 0 | ogs_error("Invalid mask[%x:%x:%x:%x]", |
627 | 0 | ipfw_rule->ip.dst.mask[0], |
628 | 0 | ipfw_rule->ip.dst.mask[1], |
629 | 0 | ipfw_rule->ip.dst.mask[2], |
630 | 0 | ipfw_rule->ip.dst.mask[3]); |
631 | 0 | return NULL; |
632 | 0 | } else if (prefixlen == 0) { |
633 | 0 | p = ogs_slprintf(p, last, " assigned"); |
634 | 0 | } else if (prefixlen > 0 && prefixlen < IPV6_BITLEN) { |
635 | 0 | p = ogs_slprintf(p, last, " %s/%d", buf, prefixlen); |
636 | 0 | } else if (prefixlen == IPV6_BITLEN) { |
637 | 0 | p = ogs_slprintf(p, last, " %s", buf); |
638 | 0 | } else { |
639 | 0 | ogs_fatal("Invalid prefixlen[%d]", prefixlen); |
640 | 0 | ogs_assert_if_reached(); |
641 | 0 | } |
642 | 0 | } else |
643 | 0 | p = ogs_slprintf(p, last, " assigned"); |
644 | | |
645 | 0 | if (ipfw_rule->port.dst.low == ipfw_rule->port.dst.high) { |
646 | 0 | if (ipfw_rule->port.dst.low == 0) { |
647 | | /* Nothing */ |
648 | 0 | } else { |
649 | 0 | p = ogs_slprintf(p, last, " %d", ipfw_rule->port.dst.low); |
650 | 0 | } |
651 | 0 | } else { |
652 | 0 | p = ogs_slprintf(p, last, " %d-%d", |
653 | 0 | ipfw_rule->port.dst.low, ipfw_rule->port.dst.high); |
654 | 0 | } |
655 | |
|
656 | 0 | return ogs_strdup(flow_description); |
657 | 0 | } |
658 | | |
659 | | ogs_ipfw_rule_t *ogs_ipfw_copy_and_swap( |
660 | | ogs_ipfw_rule_t *dst, ogs_ipfw_rule_t *src) |
661 | 0 | { |
662 | 0 | ogs_assert(src); |
663 | 0 | ogs_assert(dst); |
664 | 0 | ogs_assert(src != dst); |
665 | | |
666 | 0 | memcpy(dst, src, sizeof(ogs_ipfw_rule_t)); |
667 | |
|
668 | 0 | dst->ipv4_src = src->ipv4_dst; |
669 | 0 | dst->ipv4_dst = src->ipv4_src; |
670 | 0 | dst->ipv6_src = src->ipv6_dst; |
671 | 0 | dst->ipv6_dst = src->ipv6_src; |
672 | |
|
673 | 0 | memcpy(&dst->ip.src, &src->ip.dst, sizeof(dst->ip.src)); |
674 | 0 | memcpy(&dst->ip.dst, &src->ip.src, sizeof(dst->ip.dst)); |
675 | 0 | memcpy(&dst->port.src, &src->port.dst, sizeof(dst->port.src)); |
676 | 0 | memcpy(&dst->port.dst, &src->port.src, sizeof(dst->port.dst)); |
677 | |
|
678 | 0 | return dst; |
679 | 0 | } |
680 | | |
681 | | void ogs_ipfw_rule_swap(ogs_ipfw_rule_t *ipfw_rule) |
682 | 0 | { |
683 | 0 | ogs_ipfw_rule_t dst; |
684 | |
|
685 | 0 | ogs_assert(ipfw_rule); |
686 | | |
687 | 0 | ogs_ipfw_copy_and_swap(&dst, ipfw_rule); |
688 | 0 | memcpy(ipfw_rule, &dst, sizeof(ogs_ipfw_rule_t)); |
689 | 0 | } |
690 | | |
691 | | void ogs_pf_content_from_ipfw_rule( |
692 | | uint8_t direction, ogs_pf_content_t *content, ogs_ipfw_rule_t *rule, |
693 | | bool no_ipv4v6_local_addr_in_packet_filter) |
694 | 0 | { |
695 | 0 | int j, len; |
696 | |
|
697 | 0 | ogs_assert(content); |
698 | 0 | ogs_assert(rule); |
699 | | |
700 | 0 | j = 0, len = 0; |
701 | 0 | if (rule->proto) { |
702 | 0 | content->component[j].type = |
703 | 0 | OGS_PACKET_FILTER_PROTOCOL_IDENTIFIER_NEXT_HEADER_TYPE; |
704 | 0 | content->component[j].proto = rule->proto; |
705 | 0 | j++; len += 2; |
706 | 0 | } |
707 | | |
708 | | /* |
709 | | * As per 3GPP TS 24.008, following Packet filter component type identifier |
710 | | * are not supported on the LTE pre release-11 UEs: |
711 | | * |
712 | | * IPv4 local address type |
713 | | * IPv6 remote address/prefix length type |
714 | | * IPv6 local address/prefix length type |
715 | | * |
716 | | * And, |
717 | | * IPv6 remote address/prefix length type and |
718 | | * IPv6 local address/prefix length type shall be used when both MS and |
719 | | * Network support Local Address in TFTs. |
720 | | */ |
721 | |
|
722 | 0 | if (rule->ipv4_src) { |
723 | 0 | switch (direction) { |
724 | 0 | case OGS_FLOW_DOWNLINK_ONLY: |
725 | 0 | case OGS_FLOW_BIDIRECTIONAL: |
726 | 0 | content->component[j].type = |
727 | 0 | OGS_PACKET_FILTER_IPV4_REMOTE_ADDRESS_TYPE; |
728 | 0 | content->component[j].ipv4.addr = rule->ip.src.addr[0]; |
729 | 0 | content->component[j].ipv4.mask = rule->ip.src.mask[0]; |
730 | 0 | j++; len += 9; |
731 | 0 | break; |
732 | 0 | case OGS_FLOW_UPLINK_ONLY: |
733 | 0 | if (!no_ipv4v6_local_addr_in_packet_filter) { |
734 | 0 | content->component[j].type = |
735 | 0 | OGS_PACKET_FILTER_IPV4_LOCAL_ADDRESS_TYPE; |
736 | 0 | content->component[j].ipv4.addr = rule->ip.src.addr[0]; |
737 | 0 | content->component[j].ipv4.mask = rule->ip.src.mask[0]; |
738 | 0 | j++; len += 9; |
739 | 0 | } |
740 | 0 | break; |
741 | 0 | default: |
742 | 0 | ogs_fatal("Unsupported direction [%d]", direction); |
743 | 0 | ogs_assert_if_reached(); |
744 | 0 | } |
745 | 0 | } |
746 | | |
747 | 0 | if (rule->ipv4_dst) { |
748 | 0 | switch (direction) { |
749 | 0 | case OGS_FLOW_DOWNLINK_ONLY: |
750 | 0 | case OGS_FLOW_BIDIRECTIONAL: |
751 | 0 | if (!no_ipv4v6_local_addr_in_packet_filter) { |
752 | 0 | content->component[j].type = |
753 | 0 | OGS_PACKET_FILTER_IPV4_LOCAL_ADDRESS_TYPE; |
754 | 0 | content->component[j].ipv4.addr = rule->ip.dst.addr[0]; |
755 | 0 | content->component[j].ipv4.mask = rule->ip.dst.mask[0]; |
756 | 0 | j++; len += 9; |
757 | 0 | } |
758 | 0 | break; |
759 | 0 | case OGS_FLOW_UPLINK_ONLY: |
760 | 0 | content->component[j].type = |
761 | 0 | OGS_PACKET_FILTER_IPV4_REMOTE_ADDRESS_TYPE; |
762 | 0 | content->component[j].ipv4.addr = rule->ip.dst.addr[0]; |
763 | 0 | content->component[j].ipv4.mask = rule->ip.dst.mask[0]; |
764 | 0 | j++; len += 9; |
765 | 0 | break; |
766 | 0 | default: |
767 | 0 | ogs_fatal("Unsupported direction [%d]", direction); |
768 | 0 | ogs_assert_if_reached(); |
769 | 0 | } |
770 | 0 | } |
771 | | |
772 | 0 | if (rule->ipv6_src) { |
773 | 0 | switch (direction) { |
774 | 0 | case OGS_FLOW_DOWNLINK_ONLY: |
775 | 0 | case OGS_FLOW_BIDIRECTIONAL: |
776 | 0 | if (no_ipv4v6_local_addr_in_packet_filter) { |
777 | 0 | content->component[j].type = |
778 | 0 | OGS_PACKET_FILTER_IPV6_REMOTE_ADDRESS_TYPE; |
779 | 0 | memcpy(content->component[j].ipv6_mask.addr, |
780 | 0 | rule->ip.src.addr, sizeof rule->ip.src.addr); |
781 | 0 | memcpy(content->component[j].ipv6_mask.mask, |
782 | 0 | rule->ip.src.mask, sizeof rule->ip.src.mask); |
783 | 0 | j++; len += 33; |
784 | 0 | } else { |
785 | 0 | content->component[j].type = |
786 | 0 | OGS_PACKET_FILTER_IPV6_REMOTE_ADDRESS_PREFIX_LENGTH_TYPE; |
787 | 0 | memcpy(content->component[j].ipv6.addr, |
788 | 0 | rule->ip.src.addr, sizeof rule->ip.src.addr); |
789 | 0 | content->component[j].ipv6.prefixlen = |
790 | 0 | contigmask((uint8_t *)rule->ip.src.mask, 128); |
791 | 0 | j++; len += 18; |
792 | 0 | } |
793 | 0 | break; |
794 | 0 | case OGS_FLOW_UPLINK_ONLY: |
795 | 0 | if (!no_ipv4v6_local_addr_in_packet_filter) { |
796 | 0 | content->component[j].type = |
797 | 0 | OGS_PACKET_FILTER_IPV6_LOCAL_ADDRESS_PREFIX_LENGTH_TYPE; |
798 | 0 | memcpy(content->component[j].ipv6.addr, |
799 | 0 | rule->ip.src.addr, sizeof rule->ip.src.addr); |
800 | 0 | content->component[j].ipv6.prefixlen = |
801 | 0 | contigmask((uint8_t *)rule->ip.src.mask, 128); |
802 | 0 | j++; len += 18; |
803 | 0 | } |
804 | 0 | break; |
805 | 0 | default: |
806 | 0 | ogs_fatal("Unsupported direction [%d]", direction); |
807 | 0 | ogs_assert_if_reached(); |
808 | 0 | } |
809 | 0 | } |
810 | | |
811 | 0 | if (rule->ipv6_dst) { |
812 | 0 | switch (direction) { |
813 | 0 | case OGS_FLOW_DOWNLINK_ONLY: |
814 | 0 | case OGS_FLOW_BIDIRECTIONAL: |
815 | 0 | if (!no_ipv4v6_local_addr_in_packet_filter) { |
816 | 0 | content->component[j].type = |
817 | 0 | OGS_PACKET_FILTER_IPV6_LOCAL_ADDRESS_PREFIX_LENGTH_TYPE; |
818 | 0 | memcpy(content->component[j].ipv6.addr, |
819 | 0 | rule->ip.dst.addr, sizeof rule->ip.dst.addr); |
820 | 0 | content->component[j].ipv6.prefixlen = |
821 | 0 | contigmask((uint8_t *)rule->ip.dst.mask, 128); |
822 | 0 | j++; len += 18; |
823 | 0 | } |
824 | 0 | break; |
825 | 0 | case OGS_FLOW_UPLINK_ONLY: |
826 | 0 | if (no_ipv4v6_local_addr_in_packet_filter) { |
827 | 0 | content->component[j].type = |
828 | 0 | OGS_PACKET_FILTER_IPV6_REMOTE_ADDRESS_TYPE; |
829 | 0 | memcpy(content->component[j].ipv6_mask.addr, |
830 | 0 | rule->ip.dst.addr, sizeof rule->ip.dst.addr); |
831 | 0 | memcpy(content->component[j].ipv6_mask.mask, |
832 | 0 | rule->ip.dst.mask, sizeof rule->ip.dst.mask); |
833 | 0 | j++; len += 33; |
834 | 0 | } else { |
835 | 0 | content->component[j].type = |
836 | 0 | OGS_PACKET_FILTER_IPV6_REMOTE_ADDRESS_PREFIX_LENGTH_TYPE; |
837 | 0 | memcpy(content->component[j].ipv6.addr, |
838 | 0 | rule->ip.dst.addr, sizeof rule->ip.dst.addr); |
839 | 0 | content->component[j].ipv6.prefixlen = |
840 | 0 | contigmask((uint8_t *)rule->ip.dst.mask, 128); |
841 | 0 | j++; len += 18; |
842 | 0 | } |
843 | 0 | break; |
844 | 0 | default: |
845 | 0 | ogs_fatal("Unsupported direction [%d]", direction); |
846 | 0 | ogs_assert_if_reached(); |
847 | 0 | } |
848 | 0 | } |
849 | | |
850 | 0 | if (rule->port.src.low) { |
851 | 0 | if (rule->port.src.low == rule->port.src.high) { |
852 | 0 | switch (direction) { |
853 | 0 | case OGS_FLOW_DOWNLINK_ONLY: |
854 | 0 | case OGS_FLOW_BIDIRECTIONAL: |
855 | 0 | content->component[j].type = |
856 | 0 | OGS_PACKET_FILTER_SINGLE_REMOTE_PORT_TYPE; |
857 | 0 | break; |
858 | 0 | case OGS_FLOW_UPLINK_ONLY: |
859 | 0 | content->component[j].type = |
860 | 0 | OGS_PACKET_FILTER_SINGLE_LOCAL_PORT_TYPE; |
861 | 0 | break; |
862 | 0 | default: |
863 | 0 | ogs_fatal("Unsupported direction [%d]", direction); |
864 | 0 | ogs_assert_if_reached(); |
865 | 0 | } |
866 | 0 | content->component[j].port.low = rule->port.src.low; |
867 | 0 | j++; len += 3; |
868 | 0 | } else { |
869 | 0 | switch (direction) { |
870 | 0 | case OGS_FLOW_DOWNLINK_ONLY: |
871 | 0 | case OGS_FLOW_BIDIRECTIONAL: |
872 | 0 | content->component[j].type = |
873 | 0 | OGS_PACKET_FILTER_REMOTE_PORT_RANGE_TYPE; |
874 | 0 | break; |
875 | 0 | case OGS_FLOW_UPLINK_ONLY: |
876 | 0 | content->component[j].type = |
877 | 0 | OGS_PACKET_FILTER_LOCAL_PORT_RANGE_TYPE; |
878 | 0 | break; |
879 | 0 | default: |
880 | 0 | ogs_fatal("Unsupported direction [%d]", direction); |
881 | 0 | ogs_assert_if_reached(); |
882 | 0 | } |
883 | 0 | content->component[j].port.low = rule->port.src.low; |
884 | 0 | content->component[j].port.high = rule->port.src.high; |
885 | 0 | j++; len += 5; |
886 | 0 | } |
887 | 0 | } |
888 | | |
889 | 0 | if (rule->port.dst.low) { |
890 | 0 | if (rule->port.dst.low == rule->port.dst.high) { |
891 | 0 | switch (direction) { |
892 | 0 | case OGS_FLOW_DOWNLINK_ONLY: |
893 | 0 | case OGS_FLOW_BIDIRECTIONAL: |
894 | 0 | content->component[j].type = |
895 | 0 | OGS_PACKET_FILTER_SINGLE_LOCAL_PORT_TYPE; |
896 | 0 | break; |
897 | 0 | case OGS_FLOW_UPLINK_ONLY: |
898 | 0 | content->component[j].type = |
899 | 0 | OGS_PACKET_FILTER_SINGLE_REMOTE_PORT_TYPE; |
900 | 0 | break; |
901 | 0 | default: |
902 | 0 | ogs_fatal("Unsupported direction [%d]", direction); |
903 | 0 | ogs_assert_if_reached(); |
904 | 0 | } |
905 | 0 | content->component[j].port.low = rule->port.dst.low; |
906 | 0 | j++; len += 3; |
907 | 0 | } else { |
908 | 0 | switch (direction) { |
909 | 0 | case OGS_FLOW_DOWNLINK_ONLY: |
910 | 0 | case OGS_FLOW_BIDIRECTIONAL: |
911 | 0 | content->component[j].type = |
912 | 0 | OGS_PACKET_FILTER_LOCAL_PORT_RANGE_TYPE; |
913 | 0 | break; |
914 | 0 | case OGS_FLOW_UPLINK_ONLY: |
915 | 0 | content->component[j].type = |
916 | 0 | OGS_PACKET_FILTER_REMOTE_PORT_RANGE_TYPE; |
917 | 0 | break; |
918 | 0 | default: |
919 | 0 | ogs_fatal("Unsupported direction [%d]", direction); |
920 | 0 | ogs_assert_if_reached(); |
921 | 0 | } |
922 | 0 | content->component[j].port.low = rule->port.dst.low; |
923 | 0 | content->component[j].port.high = rule->port.dst.high; |
924 | 0 | j++; len += 5; |
925 | 0 | } |
926 | 0 | } |
927 | | |
928 | 0 | content->num_of_component = j; |
929 | 0 | content->length = len; |
930 | 0 | } |