]>
Commit | Line | Data |
---|---|---|
abeab069 MT |
1 | /*############################################################################# |
2 | # # | |
3 | # IPFire.org - A linux based firewall # | |
4 | # Copyright (C) 2023 IPFire Network Development Team # | |
5 | # # | |
6 | # This program is free software: you can redistribute it and/or modify # | |
7 | # it under the terms of the GNU 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 <http://www.gnu.org/licenses/>. # | |
18 | # # | |
19 | #############################################################################*/ | |
20 | ||
827435c8 | 21 | #include <limits.h> |
abeab069 | 22 | #include <net/if.h> |
2e7f4705 | 23 | #include <stdint.h> |
abeab069 MT |
24 | #include <stdlib.h> |
25 | ||
7297ba7f MT |
26 | #include <systemd/sd-bus.h> |
27 | ||
2e7f4705 | 28 | #include "address.h" |
abeab069 | 29 | #include "config.h" |
30d4ab67 | 30 | #include "link.h" |
827435c8 | 31 | #include "logging.h" |
abeab069 | 32 | #include "port.h" |
9e8af30e | 33 | #include "port-dummy.h" |
ff886975 | 34 | #include "port-vlan.h" |
15240e08 MT |
35 | #include "stats-collector.h" |
36 | #include "string.h" | |
abeab069 | 37 | |
827435c8 MT |
38 | static const struct nw_port_type_map { |
39 | nw_port_type_t type; | |
40 | const char* name; | |
41 | } nw_port_type_map[] = { | |
c7761af8 MT |
42 | { NW_PORT_DUMMY, "dummy" }, |
43 | { NW_PORT_VLAN, "vlan" }, | |
827435c8 MT |
44 | { NW_PORT_UNKNOWN, NULL }, |
45 | }; | |
46 | ||
47 | static nw_port_type_t nw_port_type_from_string(const char* s) { | |
48 | const struct nw_port_type_map* map = NULL; | |
49 | ||
50 | for (map = nw_port_type_map; *map->name; map++) { | |
51 | if (strcmp(map->name, s) == 0) | |
52 | return map->type; | |
53 | } | |
54 | ||
55 | return NW_PORT_UNKNOWN; | |
56 | } | |
57 | ||
2361667e | 58 | static void nw_port_free(nw_port* port) { |
02801f0d MT |
59 | if (port->link) |
60 | nw_link_unref(port->link); | |
abeab069 MT |
61 | if (port->config) |
62 | nw_config_unref(port->config); | |
30d4ab67 MT |
63 | if (port->daemon) |
64 | nw_daemon_unref(port->daemon); | |
abeab069 MT |
65 | |
66 | free(port); | |
67 | } | |
68 | ||
2361667e | 69 | static int nw_port_setup_address(nw_port* port) { |
d667fff4 | 70 | char* __address = NULL; |
2e7f4705 MT |
71 | int r; |
72 | ||
73 | // Read ADDRESS from configuration | |
74 | const char* s = nw_config_get(port->config, "ADDRESS"); | |
75 | if (!s) { | |
240e331b | 76 | ERROR("Port %s: Address is not set\n", port->name); |
9a93da54 | 77 | goto ERROR; |
2e7f4705 MT |
78 | } |
79 | ||
80 | // Parse the address | |
81 | r = nw_address_from_string(&port->address, s); | |
82 | if (r) { | |
83 | ERROR("Port %s: Could not parse address: %m\n", port->name); | |
9a93da54 | 84 | goto ERROR; |
2e7f4705 MT |
85 | } |
86 | ||
d81d9cf5 MT |
87 | // Check if this address is usable |
88 | r = nw_address_is_multicast(&port->address); | |
89 | if (r) { | |
90 | DEBUG("Port %s: Multicast bit is set on Ethernet address\n", port->name); | |
91 | goto ERROR; | |
92 | } | |
2e7f4705 | 93 | |
9a93da54 MT |
94 | return 0; |
95 | ||
96 | ERROR: | |
97 | // Generate a random Ethernet address | |
98 | r = nw_address_generate(&port->address); | |
99 | if (r) { | |
100 | ERROR("Could not generate a random Ethernet address: %m\n"); | |
101 | return r; | |
102 | } | |
103 | ||
d667fff4 MT |
104 | // Format the generated address |
105 | __address = nw_address_to_string(&port->address); | |
106 | if (__address) { | |
107 | ERROR("Generated a random Ethernet address for %s: %s\n", port->name, __address); | |
108 | ||
109 | // Free the address | |
110 | free(__address); | |
111 | } | |
112 | ||
2e7f4705 MT |
113 | return 0; |
114 | } | |
115 | ||
2361667e | 116 | static int nw_port_setup_common(nw_port* port) { |
2e7f4705 MT |
117 | int r; |
118 | ||
119 | // Address | |
120 | r = nw_port_setup_address(port); | |
121 | if (r) | |
122 | return r; | |
123 | ||
124 | return 0; | |
827435c8 MT |
125 | } |
126 | ||
611d4aca MT |
127 | static int nw_port_set_link(nw_port* port, nw_link* link) { |
128 | // Do nothing if the same link is being re-assigned | |
129 | if (port->link == link) | |
130 | return 0; | |
131 | ||
132 | // Dereference the former link if set | |
133 | if (port->link) | |
134 | nw_link_unref(port->link); | |
135 | ||
136 | // Store the new link | |
137 | if (link) { | |
138 | port->link = nw_link_ref(link); | |
139 | ||
140 | DEBUG("Port %s: Assigned link %d\n", port->name, nw_link_ifindex(port->link)); | |
141 | ||
142 | // Or clear the pointer if no link has been provided | |
143 | } else { | |
144 | port->link = NULL; | |
145 | ||
146 | DEBUG("Port %s: Removed link\n", port->name); | |
147 | } | |
148 | ||
149 | return 0; | |
150 | } | |
151 | ||
2361667e | 152 | static int nw_port_setup(nw_port* port) { |
611d4aca | 153 | nw_link* link = NULL; |
827435c8 MT |
154 | char path[PATH_MAX]; |
155 | int r; | |
156 | ||
02801f0d | 157 | // Find the link |
611d4aca MT |
158 | link = nw_daemon_get_link_by_name(port->daemon, port->name); |
159 | if (link) { | |
160 | r = nw_port_set_link(port, link); | |
161 | if (r) | |
162 | goto ERROR; | |
02801f0d MT |
163 | } |
164 | ||
827435c8 MT |
165 | // Compose the path to the main configuration file |
166 | r = nw_path_join(path, PORT_CONFIG_DIR, port->name); | |
167 | if (r) | |
611d4aca | 168 | goto ERROR; |
827435c8 MT |
169 | |
170 | // Initialize the configuration | |
171 | r = nw_config_create(&port->config, path); | |
172 | if (r) | |
611d4aca | 173 | goto ERROR; |
827435c8 | 174 | |
827435c8 MT |
175 | // Perform some common initialization |
176 | r = nw_port_setup_common(port); | |
177 | if (r) | |
611d4aca | 178 | goto ERROR; |
827435c8 MT |
179 | |
180 | // Call any custom initialization | |
89f8f6af MT |
181 | if (NW_PORT_OPS(port)->config_read) { |
182 | r = NW_PORT_OPS(port)->config_read(port); | |
ff886975 MT |
183 | if (r) |
184 | goto ERROR; | |
827435c8 MT |
185 | } |
186 | ||
611d4aca MT |
187 | ERROR: |
188 | if (link) | |
189 | nw_link_unref(link); | |
190 | ||
191 | return r; | |
827435c8 MT |
192 | } |
193 | ||
06bc93d3 | 194 | int nw_port_create(nw_port** port, nw_daemon* daemon, nw_port_type_t type, const char* name) { |
abeab069 MT |
195 | int r; |
196 | ||
197 | // Allocate a new object | |
2361667e | 198 | nw_port* p = calloc(1, sizeof(*p)); |
abeab069 MT |
199 | if (!p) |
200 | return 1; | |
201 | ||
30d4ab67 MT |
202 | // Store a reference to the daemon |
203 | p->daemon = nw_daemon_ref(daemon); | |
204 | ||
abeab069 MT |
205 | // Initialize reference counter |
206 | p->nrefs = 1; | |
207 | ||
06bc93d3 MT |
208 | // Store the type |
209 | p->type = type; | |
210 | ||
9e8af30e MT |
211 | // Set operations |
212 | switch (p->type) { | |
213 | case NW_PORT_DUMMY: | |
89f8f6af | 214 | p->info = &nw_port_info_dummy; |
9e8af30e | 215 | break; |
ff886975 MT |
216 | |
217 | case NW_PORT_VLAN: | |
89f8f6af | 218 | p->info = &nw_port_info_vlan; |
ff886975 | 219 | break; |
9e8af30e MT |
220 | } |
221 | ||
abeab069 MT |
222 | // Store the name |
223 | r = nw_string_set(p->name, name); | |
224 | if (r) | |
225 | goto ERROR; | |
226 | ||
827435c8 MT |
227 | // Setup the port |
228 | r = nw_port_setup(p); | |
229 | if (r) | |
230 | goto ERROR; | |
231 | ||
abeab069 MT |
232 | *port = p; |
233 | return 0; | |
234 | ||
235 | ERROR: | |
236 | nw_port_free(p); | |
237 | return r; | |
238 | } | |
239 | ||
06bc93d3 MT |
240 | int nw_port_create_from_config(nw_port** port, nw_daemon* daemon, |
241 | const char* name, const char* path) { | |
242 | nw_config* config = NULL; | |
243 | int r; | |
244 | ||
245 | // Initialize the configuration | |
246 | r = nw_config_create(&config, path); | |
247 | if (r) | |
248 | goto ERROR; | |
249 | ||
250 | // Fetch the type | |
251 | const char* type = nw_config_get(config, "TYPE"); | |
252 | if (!type) { | |
253 | ERROR("Port configuration %s has no TYPE\n", path); | |
254 | r = 1; | |
255 | goto ERROR; | |
256 | } | |
257 | ||
258 | // Create a new port | |
259 | r = nw_port_create(port, daemon, nw_port_type_from_string(type), name); | |
260 | if (r) | |
261 | goto ERROR; | |
262 | ||
263 | ERROR: | |
264 | if (config) | |
265 | nw_config_unref(config); | |
266 | ||
267 | return r; | |
268 | } | |
269 | ||
2361667e | 270 | nw_port* nw_port_ref(nw_port* port) { |
abeab069 MT |
271 | port->nrefs++; |
272 | ||
273 | return port; | |
274 | } | |
275 | ||
2361667e | 276 | nw_port* nw_port_unref(nw_port* port) { |
abeab069 MT |
277 | if (--port->nrefs > 0) |
278 | return port; | |
279 | ||
280 | nw_port_free(port); | |
281 | return NULL; | |
282 | } | |
283 | ||
240e331b MT |
284 | /* |
285 | This is a helper function for when we pass a reference to the event loop | |
286 | it will have to dereference the port instance later. | |
287 | */ | |
288 | static void __nw_port_unref(void* data) { | |
289 | nw_port* port = (nw_port*)data; | |
290 | ||
291 | nw_port_unref(port); | |
292 | } | |
293 | ||
c403eb4c MT |
294 | int nw_port_destroy(nw_port* port) { |
295 | int r; | |
296 | ||
297 | DEBUG("Destroying port %s\n", port->name); | |
298 | ||
299 | // Destroy the physical link (if exists) | |
300 | if (port->link) { | |
301 | r = nw_link_destroy(port->link); | |
302 | if (r) | |
303 | return r; | |
304 | } | |
305 | ||
306 | // Dereference the port from other ports | |
307 | r = nw_daemon_ports_walk(port->daemon, __nw_port_drop_port, port); | |
308 | if (r) | |
309 | return r; | |
310 | ||
311 | // Dereference the port from other zones | |
312 | r = nw_daemon_zones_walk(port->daemon, __nw_zone_drop_port, port); | |
313 | if (r) | |
314 | return r; | |
315 | ||
316 | // Destroy the configuration | |
317 | r = nw_config_destroy(port->config); | |
318 | if (r) | |
319 | return r; | |
320 | ||
321 | // Reset type | |
322 | port->type = NW_PORT_UNKNOWN; | |
323 | ||
324 | return 0; | |
325 | } | |
326 | ||
327 | int __nw_port_drop_port(nw_daemon* daemon, nw_port* port, void* data) { | |
328 | nw_port* dropped_port = (nw_port*)data; | |
329 | int r; | |
330 | ||
331 | switch (port->type) { | |
332 | case NW_PORT_VLAN: | |
333 | if (port->vlan.parent == dropped_port) { | |
334 | r = nw_port_set_vlan_parent(port, NULL); | |
335 | if (r) | |
336 | return r; | |
337 | } | |
338 | break; | |
339 | ||
340 | case NW_PORT_DUMMY: | |
341 | case NW_PORT_UNKNOWN: | |
342 | break; | |
343 | } | |
344 | ||
345 | return 0; | |
346 | } | |
347 | ||
605e975f MT |
348 | int nw_port_save(nw_port* port) { |
349 | int r; | |
350 | ||
ff886975 | 351 | // Call the custom handler |
89f8f6af MT |
352 | if (NW_PORT_OPS(port)->config_write) { |
353 | r = NW_PORT_OPS(port)->config_write(port); | |
ff886975 MT |
354 | if (r) |
355 | goto ERROR; | |
c7761af8 MT |
356 | } |
357 | ||
ff886975 | 358 | // Write the configuration |
605e975f MT |
359 | r = nw_config_write(port->config); |
360 | if (r) | |
361 | return r; | |
362 | ||
363 | return 0; | |
c7761af8 MT |
364 | |
365 | ERROR: | |
366 | ERROR("Could not save configuration for port %s: %m\n", port->name); | |
367 | ||
368 | return 1; | |
605e975f MT |
369 | } |
370 | ||
2361667e | 371 | const char* nw_port_name(nw_port* port) { |
abeab069 MT |
372 | return port->name; |
373 | } | |
7297ba7f | 374 | |
2361667e | 375 | char* nw_port_bus_path(nw_port* port) { |
7297ba7f MT |
376 | char* p = NULL; |
377 | int r; | |
378 | ||
379 | // Encode the bus path | |
380 | r = sd_bus_path_encode("/org/ipfire/network1/port", port->name, &p); | |
381 | if (r < 0) | |
382 | return NULL; | |
383 | ||
384 | return p; | |
385 | } | |
4a383286 | 386 | |
611d4aca MT |
387 | int __nw_port_set_link(nw_daemon* daemon, nw_port* port, void* data) { |
388 | nw_link* link = (nw_link*)data; | |
389 | ||
390 | // Fetch the link name | |
391 | const char* ifname = nw_link_ifname(link); | |
392 | if (!ifname) { | |
393 | ERROR("Link does not have a name set\n"); | |
394 | return 1; | |
395 | } | |
396 | ||
397 | // Set link if the name matches | |
398 | if (strcmp(port->name, ifname) == 0) | |
399 | return nw_port_set_link(port, link); | |
400 | ||
401 | // If we have the link set but the name did not match, we will remove it | |
402 | else if (port->link == link) | |
403 | return nw_port_set_link(port, NULL); | |
404 | ||
405 | return 0; | |
406 | } | |
407 | ||
408 | int __nw_port_drop_link(nw_daemon* daemon, nw_port* port, void* data) { | |
409 | nw_link* link = (nw_link*)data; | |
02801f0d | 410 | |
611d4aca MT |
411 | // Drop the link if it matches |
412 | if (port->link == link) | |
413 | return nw_port_set_link(port, NULL); | |
414 | ||
415 | return 0; | |
30d4ab67 MT |
416 | } |
417 | ||
240e331b MT |
418 | static nw_link* nw_port_get_link(nw_port* port) { |
419 | // Fetch the link if not set | |
420 | if (!port->link) | |
421 | port->link = nw_daemon_get_link_by_name(port->daemon, port->name); | |
422 | ||
423 | if (!port->link) | |
424 | return NULL; | |
425 | ||
426 | return nw_link_ref(port->link); | |
427 | } | |
428 | ||
2361667e | 429 | const nw_address_t* nw_port_get_address(nw_port* port) { |
4a383286 MT |
430 | return &port->address; |
431 | } | |
20375a08 | 432 | |
371b836a MT |
433 | static int nw_port_is_disabled(nw_port* port) { |
434 | return nw_config_get_bool(port->config, "DISABLED"); | |
435 | } | |
436 | ||
240e331b MT |
437 | static nw_link* nw_port_get_parent_link(nw_port* port) { |
438 | nw_port* parent = NULL; | |
439 | nw_link* link = NULL; | |
440 | ||
441 | // Do nothing if not implemented | |
89f8f6af | 442 | if (!NW_PORT_OPS(port)->get_parent_port) |
240e331b MT |
443 | goto ERROR; |
444 | ||
445 | // Fetch the parent | |
89f8f6af | 446 | parent = NW_PORT_OPS(port)->get_parent_port(port); |
240e331b MT |
447 | if (!parent) |
448 | goto ERROR; | |
449 | ||
450 | // Fetch the link | |
451 | link = nw_port_get_link(parent); | |
452 | ||
453 | ERROR: | |
454 | if (parent) | |
455 | nw_port_unref(parent); | |
456 | ||
457 | return link; | |
458 | } | |
459 | ||
460 | static int __nw_port_create_link(sd_netlink* rtnl, sd_netlink_message* m, void* data) { | |
461 | nw_port* port = (nw_port*)data; | |
462 | int r; | |
463 | ||
464 | // Check if the operation was successful | |
465 | r = sd_netlink_message_get_errno(m); | |
466 | if (r < 0) { | |
467 | ERROR("Could not create port %s: %s\n", port->name, strerror(-r)); | |
468 | // XXX We should extract the error message | |
469 | ||
470 | return 0; | |
471 | } | |
472 | ||
473 | DEBUG("Successfully created %s\n", port->name); | |
474 | ||
475 | return 0; | |
476 | } | |
477 | ||
96b1b84d | 478 | static int nw_port_create_link(nw_port* port) { |
240e331b MT |
479 | sd_netlink_message* m = NULL; |
480 | nw_link* link = NULL; | |
ff886975 MT |
481 | int r; |
482 | ||
240e331b MT |
483 | sd_netlink* rtnl = nw_daemon_get_rtnl(port->daemon); |
484 | ||
575d1a3f MT |
485 | DEBUG("Creating port %s...\n", port->name); |
486 | ||
240e331b MT |
487 | // Create a new link |
488 | r = sd_rtnl_message_new_link(rtnl, &m, RTM_NEWLINK, 0); | |
489 | if (r < 0) { | |
490 | ERROR("Could not create netlink message: %m\n"); | |
491 | goto ERROR; | |
492 | } | |
493 | ||
494 | // Set the name | |
495 | r = sd_netlink_message_append_string(m, IFLA_IFNAME, port->name); | |
496 | if (r < 0) { | |
497 | ERROR("Could not set port name: %s\n", strerror(-r)); | |
498 | goto ERROR; | |
ff886975 MT |
499 | } |
500 | ||
1574bc00 MT |
501 | // XXX Set common things like MTU, etc. |
502 | ||
d667fff4 | 503 | // Set Ethernet address |
1574bc00 MT |
504 | r = sd_netlink_message_append_ether_addr(m, IFLA_ADDRESS, &port->address); |
505 | if (r < 0) { | |
506 | ERROR("Could not set MAC address: %s\n", strerror(-r)); | |
507 | goto ERROR; | |
508 | } | |
240e331b MT |
509 | |
510 | // Fetch the parent link | |
511 | link = nw_port_get_parent_link(port); | |
512 | if (link) { | |
513 | r = sd_netlink_message_append_u32(m, IFLA_LINK, nw_link_ifindex(link)); | |
514 | if (r < 0) | |
515 | goto ERROR; | |
516 | } | |
517 | ||
518 | // Open an IFLA_LINKINFO container | |
519 | r = sd_netlink_message_open_container(m, IFLA_LINKINFO); | |
520 | if (r < 0) | |
521 | goto ERROR; | |
522 | ||
523 | // Run the custom setup | |
89f8f6af MT |
524 | if (NW_PORT_OPS(port)->create_link) { |
525 | r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, NW_PORT_INFO(port)->kind); | |
240e331b MT |
526 | if (r < 0) { |
527 | ERROR("Could not open IFLA_INFO_DATA container: %s\n", strerror(-r)); | |
528 | goto ERROR; | |
529 | } | |
530 | ||
89f8f6af | 531 | r = NW_PORT_OPS(port)->create_link(port, m); |
240e331b MT |
532 | if (r) { |
533 | ERROR("Could not create port %s: %m\n", port->name); | |
534 | goto ERROR; | |
535 | } | |
536 | ||
537 | // Close the container | |
538 | r = sd_netlink_message_close_container(m); | |
539 | if (r < 0) | |
540 | goto ERROR; | |
541 | ||
542 | // Just set IFLA_INFO_KIND if there is no custom function | |
543 | } else { | |
89f8f6af | 544 | r = sd_netlink_message_append_string(m, IFLA_INFO_KIND, NW_PORT_INFO(port)->kind); |
240e331b MT |
545 | if (r < 0) |
546 | goto ERROR; | |
547 | } | |
548 | ||
549 | // Close the container | |
550 | r = sd_netlink_message_close_container(m); | |
551 | if (r < 0) | |
552 | goto ERROR; | |
553 | ||
554 | // Send the message | |
555 | r = sd_netlink_call_async(rtnl, NULL, m, __nw_port_create_link, | |
556 | __nw_port_unref, nw_port_ref(port), -1, NULL); | |
557 | if (r < 0) { | |
558 | ERROR("Could not send netlink message: %s\n", strerror(-r)); | |
559 | goto ERROR; | |
560 | } | |
561 | ||
562 | r = 0; | |
563 | ||
564 | ERROR: | |
565 | if (m) | |
566 | sd_netlink_message_unref(m); | |
567 | if (link) | |
568 | nw_link_unref(link); | |
ff886975 MT |
569 | |
570 | return r; | |
96b1b84d MT |
571 | } |
572 | ||
b9769b09 | 573 | int nw_port_reconfigure(nw_port* port) { |
371b836a MT |
574 | int r; |
575 | ||
576 | // If the port is disabled, we will try to destroy it | |
577 | if (nw_port_is_disabled(port)) { | |
578 | if (port->link) { | |
579 | r = nw_link_destroy(port->link); | |
580 | if (r) | |
581 | return r; | |
582 | } | |
583 | ||
584 | return 0; | |
585 | } | |
586 | ||
96b1b84d | 587 | // If there is no link, we will try to create it |
895f7134 MT |
588 | if (!port->link) |
589 | return nw_port_create_link(port); | |
96b1b84d | 590 | |
371b836a | 591 | // XXX TODO |
96b1b84d MT |
592 | |
593 | return 0; | |
b9769b09 MT |
594 | } |
595 | ||
20375a08 | 596 | int nw_port_has_carrier(nw_port* port) { |
02801f0d MT |
597 | if (!port->link) |
598 | return 0; | |
20375a08 | 599 | |
02801f0d | 600 | return nw_link_has_carrier(port->link); |
20375a08 | 601 | } |
15240e08 MT |
602 | |
603 | /* | |
604 | Stats | |
605 | */ | |
606 | ||
607 | const struct rtnl_link_stats64* nw_port_get_stats64(nw_port* port) { | |
608 | if (!port->link) | |
609 | return NULL; | |
610 | ||
611 | return nw_link_get_stats64(port->link); | |
612 | } | |
613 | ||
614 | int __nw_port_update_stats(nw_daemon* daemon, nw_port* port, void* data) { | |
615 | nw_link* link = (nw_link*)data; | |
616 | ||
617 | // Emit stats if link matches | |
618 | if (port->link == link) | |
619 | return nw_stats_collector_emit_port_stats(daemon, port); | |
620 | ||
621 | return 0; | |
622 | } | |
623 | ||
624 | int nw_port_update_stats(nw_port* port) { | |
625 | if (port->link) | |
626 | return nw_link_update_stats(port->link); | |
627 | ||
628 | return 0; | |
629 | } | |
c7761af8 | 630 | |
ff886975 | 631 | int nw_port_check_type(nw_port* port, const nw_port_type_t type) { |
c7761af8 MT |
632 | if (port->type == type) |
633 | return 0; | |
634 | ||
635 | errno = ENOTSUP; | |
636 | return -errno; | |
637 | } |