// SPDX-License-Identifier: AGPL-3.0-or-later /* PASST - Plug A Simple Socket Transport * for qemu/UNIX domain socket mode * * PASTA - Pack A Subtle Tap Abstraction * for network namespace/tap device mode * * conf.c - Configuration settings and option parsing * * Copyright (c) 2020-2021 Red Hat GmbH * Author: Stefano Brivio */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "util.h" #include "passt.h" #include "netlink.h" #include "udp.h" #include "tcp.h" #include "pasta.h" #include "lineread.h" #include "isolation.h" /** * get_bound_ports() - Get maps of ports with bound sockets * @c: Execution context * @ns: If set, set bitmaps for ports to tap/ns -- to init otherwise * @proto: Protocol number (IPPROTO_TCP or IPPROTO_UDP) */ void get_bound_ports(struct ctx *c, int ns, uint8_t proto) { uint8_t *udp_map, *udp_excl, *tcp_map, *tcp_excl; if (ns) { udp_map = c->udp.port_to_tap; udp_excl = c->udp.port_to_init; tcp_map = c->tcp.port_to_tap; tcp_excl = c->tcp.port_to_init; } else { udp_map = c->udp.port_to_init; udp_excl = c->udp.port_to_tap; tcp_map = c->tcp.port_to_init; tcp_excl = c->tcp.port_to_tap; } if (proto == IPPROTO_UDP) { memset(udp_map, 0, USHRT_MAX / 8); procfs_scan_listen(c, IPPROTO_UDP, V4, ns, udp_map, udp_excl); procfs_scan_listen(c, IPPROTO_UDP, V6, ns, udp_map, udp_excl); procfs_scan_listen(c, IPPROTO_TCP, V4, ns, udp_map, udp_excl); procfs_scan_listen(c, IPPROTO_TCP, V6, ns, udp_map, udp_excl); } else if (proto == IPPROTO_TCP) { memset(tcp_map, 0, USHRT_MAX / 8); procfs_scan_listen(c, IPPROTO_TCP, V4, ns, tcp_map, tcp_excl); procfs_scan_listen(c, IPPROTO_TCP, V6, ns, tcp_map, tcp_excl); } } /** * struct get_bound_ports_ns_arg - Arguments for get_bound_ports_ns() * @c: Execution context * @proto: Protocol number (IPPROTO_TCP or IPPROTO_UDP) */ struct get_bound_ports_ns_arg { struct ctx *c; uint8_t proto; }; /** * get_bound_ports_ns() - Get maps of ports in namespace with bound sockets * @arg: See struct get_bound_ports_ns_arg * * Return: 0 */ static int get_bound_ports_ns(void *arg) { struct get_bound_ports_ns_arg *a = (struct get_bound_ports_ns_arg *)arg; struct ctx *c = a->c; if (!c->pasta_netns_fd || ns_enter(c)) return 0; get_bound_ports(c, 1, a->proto); return 0; } enum conf_port_type { PORT_SPEC = 1, PORT_NONE, PORT_AUTO, PORT_ALL, }; /** * conf_ports() - Parse port configuration options, initialise UDP/TCP sockets * @c: Execution context * @optname: Short option name, t, T, u, or U * @optarg: Option argument (port specification) * @set: Pointer to @conf_port_type to be set (port binding type) * * Return: -EINVAL on parsing error, 0 otherwise */ static int conf_ports(struct ctx *c, char optname, const char *optarg, enum conf_port_type *set) { int start_src, end_src, start_dst, end_dst, exclude_only = 1, i, port; char addr_buf[sizeof(struct in6_addr)] = { 0 }, *addr = addr_buf; void (*remap)(in_port_t port, in_port_t delta); uint8_t *map, exclude[USHRT_MAX / 8] = { 0 }; char buf[BUFSIZ], *sep, *spec, *p; sa_family_t af = AF_UNSPEC; if (optname == 't') { map = c->tcp.port_to_tap; remap = tcp_remap_to_tap; } else if (optname == 'T') { map = c->tcp.port_to_init; remap = tcp_remap_to_init; } else if (optname == 'u') { map = c->udp.port_to_tap; remap = udp_remap_to_tap; } else if (optname == 'U') { map = c->udp.port_to_init; remap = udp_remap_to_init; } else { /* For gcc -O3 */ return 0; } if (!strcmp(optarg, "none")) { if (*set) return -EINVAL; *set = PORT_NONE; return 0; } if (!strcmp(optarg, "auto")) { if (*set || c->mode != MODE_PASTA) return -EINVAL; *set = PORT_AUTO; return 0; } if (!strcmp(optarg, "all")) { int i; if (*set || c->mode != MODE_PASST) return -EINVAL; *set = PORT_ALL; memset(map, 0xff, PORT_EPHEMERAL_MIN / 8); for (i = 0; i < PORT_EPHEMERAL_MIN; i++) { if (optname == 't') tcp_sock_init(c, 0, AF_UNSPEC, NULL, i); else if (optname == 'u') udp_sock_init(c, 0, AF_UNSPEC, NULL, i); } return 0; } if (*set > PORT_SPEC) return -EINVAL; *set = PORT_SPEC; strncpy(buf, optarg, sizeof(buf) - 1); if ((spec = strchr(buf, '/'))) { *spec = 0; spec++; if (optname != 't' && optname != 'u') goto bad; if (inet_pton(AF_INET, buf, addr)) af = AF_INET; else if (inet_pton(AF_INET6, buf, addr)) af = AF_INET6; else goto bad; } else { spec = buf; addr = NULL; } if (strspn(spec, "0123456789-,:~") != strlen(spec)) goto bad; /* Mark all exclusions first, they might be given after base ranges */ p = spec; start_src = end_src = -1; do { while (*p != '~' && start_src == -1) { exclude_only = 0; if (!(p = strchr(p, ','))) break; p++; } if (!p || !*p) break; if (*p == '~') p++; errno = 0; port = strtol(p, &sep, 10); if (sep == p) break; if (port < 0 || port > USHRT_MAX || errno) goto bad; switch (*sep) { case '-': if (start_src == -1) /* ~22-... */ start_src = port; break; case ',': case 0: if (start_src == -1) /* ~80 */ start_src = end_src = port; else if (end_src == -1) /* ~22-25 */ end_src = port; else goto bad; if (start_src > end_src) /* ~80-22 */ goto bad; for (i = start_src; i <= end_src; i++) { if (bitmap_isset(exclude, i)) goto overlap; bitmap_set(exclude, i); } start_src = end_src = -1; break; default: goto bad; } p = sep + 1; } while (*sep); if (exclude_only) { for (i = 0; i < PORT_EPHEMERAL_MIN; i++) { if (bitmap_isset(exclude, i)) continue; bitmap_set(map, i); if (optname == 't') tcp_sock_init(c, 0, af, addr, i); else if (optname == 'u') udp_sock_init(c, 0, af, addr, i); } return 0; } /* Now process base ranges, skipping exclusions */ start_src = end_src = start_dst = end_dst = -1; p = spec; do { while (*p == '~') { if (!(p = strchr(p, ','))) break; p++; } if (!p || !*p) break; errno = 0; port = strtol(p, &sep, 10); if (sep == p) break; if (port < 0 || port > USHRT_MAX || errno) goto bad; /* -p 22 * ^ start_src end_src == start_dst == end_dst == -1 * * -p 22-25 * | ^ end_src * ` start_src start_dst == end_dst == -1 * * -p 80:8080 * | ^ start_dst * ` start_src end_src == end_dst == -1 * * -p 22-80:8022-8080 * | | | ^ end_dst * | | ` start_dst * | ` end_dst * ` start_src */ switch (*sep) { case '-': if (start_src == -1) { /* 22-... */ start_src = port; } else { if (!end_src) /* 22:8022-8080 */ goto bad; start_dst = port; /* 22-80:8022-... */ } break; case ':': if (start_src == -1) /* 80:... */ start_src = end_src = port; else if (end_src == -1) /* 22-80:... */ end_src = port; else /* 22-80:8022:... */ goto bad; break; case ',': case 0: if (start_src == -1) /* 80 */ start_src = end_src = port; else if (end_src == -1) /* 22-25 */ end_src = port; else if (start_dst == -1) /* 80:8080 */ start_dst = end_dst = port; else if (end_dst == -1) /* 22-80:8022-8080 */ end_dst = port; else goto bad; if (start_src > end_src) /* 80-22 */ goto bad; if (start_dst > end_dst) /* 22-80:8080:8022 */ goto bad; if (end_dst != -1 && end_dst - start_dst != end_src - start_src) goto bad; /* 22-81:8022:8080 */ for (i = start_src; i <= end_src; i++) { if (bitmap_isset(map, i)) goto overlap; if (bitmap_isset(exclude, i)) continue; bitmap_set(map, i); if (start_dst != -1) { /* 80:8080 or 22-80:8080:8080 */ remap(i, (in_port_t)(start_dst - start_src)); } if (optname == 't') tcp_sock_init(c, 0, af, addr, i); else if (optname == 'u') udp_sock_init(c, 0, af, addr, i); } start_src = end_src = start_dst = end_dst = -1; break; } p = sep + 1; } while (*sep); return 0; bad: err("Invalid port specifier %s", optarg); return -EINVAL; overlap: err("Overlapping port specifier %s", optarg); return -EINVAL; } /** * get_dns() - Get nameserver addresses from local /etc/resolv.conf * @c: Execution context */ static void get_dns(struct ctx *c) { int dns4_set, dns6_set, dnss_set, dns_set, fd; struct in6_addr *dns6 = &c->ip6.dns[0]; struct fqdn *s = c->dns_search; uint32_t *dns4 = &c->ip4.dns[0]; struct lineread resolvconf; int line_len; char *line, *p, *end; dns4_set = !c->ifi4 || !!*dns4; dns6_set = !c->ifi6 || !IN6_IS_ADDR_UNSPECIFIED(dns6); dnss_set = !!*s->n || c->no_dns_search; dns_set = (dns4_set && dns6_set) || c->no_dns; if (dns_set && dnss_set) return; if ((fd = open("/etc/resolv.conf", O_RDONLY | O_CLOEXEC)) < 0) goto out; lineread_init(&resolvconf, fd); while ((line_len = lineread_get(&resolvconf, &line)) > 0) { if (!dns_set && strstr(line, "nameserver ") == line) { p = strrchr(line, ' '); if (!p) continue; end = strpbrk(line, "%\n"); if (end) *end = 0; if (!dns4_set && dns4 - &c->ip4.dns[0] < ARRAY_SIZE(c->ip4.dns) - 1 && inet_pton(AF_INET, p + 1, dns4)) { /* We can only access local addresses via the gw redirect */ if (ntohl(*dns4) >> IN_CLASSA_NSHIFT == IN_LOOPBACKNET) { if (c->no_map_gw) { *dns4 = 0; continue; } *dns4 = c->ip4.gw; } dns4++; *dns4 = 0; } if (!dns6_set && dns6 - &c->ip6.dns[0] < ARRAY_SIZE(c->ip6.dns) - 1 && inet_pton(AF_INET6, p + 1, dns6)) { /* We can only access local addresses via the gw redirect */ if (IN6_IS_ADDR_LOOPBACK(dns6)) { if (c->no_map_gw) { memset(dns6, 0, sizeof(*dns6)); continue; } memcpy(dns6, &c->ip6.gw, sizeof(*dns6)); } dns6++; memset(dns6, 0, sizeof(*dns6)); } } else if (!dnss_set && strstr(line, "search ") == line && s == c->dns_search) { end = strpbrk(line, "\n"); if (end) *end = 0; if (!strtok(line, " \t")) continue; while (s - c->dns_search < ARRAY_SIZE(c->dns_search) - 1 && (p = strtok(NULL, " \t"))) { strncpy(s->n, p, sizeof(c->dns_search[0])); s++; *s->n = 0; } } } if (line_len < 0) warn("Error reading /etc/resolv.conf: %s", strerror(errno)); close(fd); out: if (!dns_set && dns4 == c->ip4.dns && dns6 == c->ip6.dns) warn("Couldn't get any nameserver address"); } /** * conf_netns_opt() - Parse --netns option * @netns: buffer of size PATH_MAX, updated with netns path * @arg: --netns argument * * Return: 0 on success, negative error code otherwise */ static int conf_netns_opt(char *netns, const char *arg) { int ret; if (!strchr(arg, '/')) { /* looks like a netns name */ ret = snprintf(netns, PATH_MAX, "%s/%s", NETNS_RUN_DIR, arg); } else { /* otherwise assume it's a netns path */ ret = snprintf(netns, PATH_MAX, "%s", arg); } if (ret <= 0 || ret > PATH_MAX) { err("Network namespace name/path %s too long"); return -E2BIG; } return 0; } /** * conf_pasta_ns() - Validate all pasta namespace options * @netns_only: Don't use userns, may be updated * @userns: buffer of size PATH_MAX, initially contains --userns * argument (may be empty), updated with userns path * @netns: buffer of size PATH_MAX, initial contains --netns * argument (may be empty), updated with netns path * @optind: Index of first non-option argument * @argc: Number of arguments * @argv: Command line arguments * * Return: 0 on success, negative error code otherwise */ static int conf_pasta_ns(int *netns_only, char *userns, char *netns, int optind, int argc, char *argv[]) { if (*netns_only && *userns) { err("Both --userns and --netns-only given"); return -EINVAL; } if (*netns && optind != argc) { err("Both --netns and PID or command given"); return -EINVAL; } if (optind + 1 == argc) { char *endptr; long pidval; pidval = strtol(argv[optind], &endptr, 10); if (!*endptr) { /* Looks like a pid */ if (pidval < 0 || pidval > INT_MAX) { err("Invalid PID %s", argv[optind]); return -EINVAL; } snprintf(netns, PATH_MAX, "/proc/%ld/ns/net", pidval); if (!*userns) snprintf(userns, PATH_MAX, "/proc/%ld/ns/user", pidval); } } if (*userns && !*netns) { err("--userns requires --netns or PID"); return -EINVAL; } /* Attaching to a netns/PID, with no userns given */ if (*netns && !*userns) *netns_only = 1; return 0; } /** * conf_ip4() - Verify or detect IPv4 support, get relevant addresses * @ifi: Host interface to attempt (0 to determine one) * @ip4: IPv4 context (will be written) * @mac: MAC address to use (written if unset) * * Return: Interface index for IPv4, or 0 on failure. */ static unsigned int conf_ip4(unsigned int ifi, struct ip4_ctx *ip4, unsigned char *mac) { if (!ifi) ifi = nl_get_ext_if(AF_INET); if (!ifi) { warn("No external routable interface for IPv4"); return 0; } if (!ip4->gw) nl_route(0, ifi, AF_INET, &ip4->gw); if (!ip4->addr) { int mask_len = 0; nl_addr(0, ifi, AF_INET, &ip4->addr, &mask_len, NULL); ip4->mask = htonl(0xffffffff << (32 - mask_len)); } if (!ip4->mask) { if (IN_CLASSA(ntohl(ip4->addr))) ip4->mask = htonl(IN_CLASSA_NET); else if (IN_CLASSB(ntohl(ip4->addr))) ip4->mask = htonl(IN_CLASSB_NET); else if (IN_CLASSC(ntohl(ip4->addr))) ip4->mask = htonl(IN_CLASSC_NET); else ip4->mask = 0xffffffff; } memcpy(&ip4->addr_seen, &ip4->addr, sizeof(ip4->addr_seen)); if (MAC_IS_ZERO(mac)) nl_link(0, ifi, mac, 0, 0); if (!ip4->gw || !ip4->addr || MAC_IS_ZERO(mac)) return 0; return ifi; } /** * conf_ip6() - Verify or detect IPv6 support, get relevant addresses * @ifi: Host interface to attempt (0 to determine one) * @ip6: IPv6 context (will be written) * @mac: MAC address to use (written if unset) * * Return: Interface index for IPv6, or 0 on failure. */ static unsigned int conf_ip6(unsigned int ifi, struct ip6_ctx *ip6, unsigned char *mac) { int prefix_len = 0; if (!ifi) ifi = nl_get_ext_if(AF_INET6); if (!ifi) { warn("No external routable interface for IPv6"); return 0; } if (IN6_IS_ADDR_UNSPECIFIED(&ip6->gw)) nl_route(0, ifi, AF_INET6, &ip6->gw); nl_addr(0, ifi, AF_INET6, IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) ? &ip6->addr : NULL, &prefix_len, &ip6->addr_ll); memcpy(&ip6->addr_seen, &ip6->addr, sizeof(ip6->addr)); memcpy(&ip6->addr_ll_seen, &ip6->addr_ll, sizeof(ip6->addr_ll)); if (MAC_IS_ZERO(mac)) nl_link(0, ifi, mac, 0, 0); if (IN6_IS_ADDR_UNSPECIFIED(&ip6->gw) || IN6_IS_ADDR_UNSPECIFIED(&ip6->addr) || IN6_IS_ADDR_UNSPECIFIED(&ip6->addr_ll) || MAC_IS_ZERO(mac)) return 0; return ifi; } /** * usage() - Print usage and exit * @name: Executable name */ static void usage(const char *name) { if (strstr(name, "pasta")) { info("Usage: %s [OPTION]... [COMMAND] [ARGS]...", name); info(" %s [OPTION]... PID", name); info(" %s [OPTION]... --netns [PATH|NAME]", name); info(""); info("Without PID or --netns, run the given command or a"); info("default shell in a new network and user namespace, and"); info("connect it via pasta."); } else { info("Usage: %s [OPTION]...", name); } info(""); info( " -d, --debug Be verbose, don't run in background"); info( " --trace Be extra verbose, implies --debug"); info( " -q, --quiet Don't print informational messages"); info( " -f, --foreground Don't run in background"); info( " default: run in background if started from a TTY"); info( " -e, --stderr Log to stderr too"); info( " default: log to system logger only if started from a TTY"); info( " --runas UID|UID:GID Run as given UID, GID, which can be"); info( " numeric, or login and group names"); info( " default: drop to user \"nobody\""); info( " -h, --help Display this help message and exit"); if (strstr(name, "pasta")) { info( " -I, --ns-ifname NAME namespace interface name"); info( " default: same interface name as external one"); } else { info( " -s, --socket PATH UNIX domain socket path"); info( " default: probe free path starting from " UNIX_SOCK_PATH, 1); } info( " -p, --pcap FILE Log tap-facing traffic to pcap file"); info( " -P, --pid FILE Write own PID to the given file"); info( " -m, --mtu MTU Assign MTU via DHCP/NDP"); info( " a zero value disables assignment"); info( " default: 65520: maximum 802.3 MTU minus 802.3 header"); info( " length, rounded to 32 bits (IPv4 words)"); info( " -a, --address ADDR Assign IPv4 or IPv6 address ADDR"); info( " can be specified zero to two times (for IPv4 and IPv6)"); info( " default: use addresses from interface with default route"); info( " -n, --netmask MASK Assign IPv4 MASK, dot-decimal or bits"); info( " default: netmask from matching address on the host"); info( " -M, --mac-addr ADDR Use source MAC address ADDR"); info( " default: MAC address from interface with default route"); info( " -g, --gateway ADDR Pass IPv4 or IPv6 address as gateway"); info( " default: gateway from interface with default route"); info( " -i, --interface NAME Interface for addresses and routes"); info( " default: interface with first default route"); info( " -D, --dns ADDR Use IPv4 or IPv6 address as DNS"); info( " can be specified multiple times"); info( " a single, empty option disables DNS information"); if (strstr(name, "pasta")) info( " default: don't use any addresses"); else info( " default: use addresses from /etc/resolv.conf"); info( " -S, --search LIST Space-separated list, search domains"); info( " a single, empty option disables the DNS search list"); if (strstr(name, "pasta")) info( " default: don't use any search list"); else info( " default: use search list from /etc/resolv.conf"); if (strstr(name, "pasta")) info(" --dhcp-dns: \tPass DNS list via DHCP/DHCPv6/NDP"); else info(" --no-dhcp-dns: No DNS list in DHCP/DHCPv6/NDP"); if (strstr(name, "pasta")) info(" --dhcp-search: Pass list via DHCP/DHCPv6/NDP"); else info(" --no-dhcp-search: No list in DHCP/DHCPv6/NDP"); info( " --dns-forward ADDR Forward DNS queries sent to ADDR"); info( " can be specified zero to two times (for IPv4 and IPv6)"); info( " default: don't forward DNS queries"); info( " --no-tcp Disable TCP protocol handler"); info( " --no-udp Disable UDP protocol handler"); info( " --no-icmp Disable ICMP/ICMPv6 protocol handler"); info( " --no-dhcp Disable DHCP server"); info( " --no-ndp Disable NDP responses"); info( " --no-dhcpv6 Disable DHCPv6 server"); info( " --no-ra Disable router advertisements"); info( " --no-map-gw Don't map gateway address to host"); info( " -4, --ipv4-only Enable IPv4 operation only"); info( " -6, --ipv6-only Enable IPv6 operation only"); if (strstr(name, "pasta")) goto pasta_opts; info( " -t, --tcp-ports SPEC TCP port forwarding to guest"); info( " can be specified multiple times"); info( " SPEC can be:"); info( " 'none': don't forward any ports"); info( " 'all': forward all unbound, non-ephemeral ports"); info( " a comma-separated list, optionally ranged with '-'"); info( " and optional target ports after ':', with optional"); info( " address specification suffixed by '/'. Ranges can be"); info( " reduced by excluding ports or ranges prefixed by '~'"); info( " Examples:"); info( " -t 22 Forward local port 22 to 22 on guest"); info( " -t 22:23 Forward local port 22 to 23 on guest"); info( " -t 22,25 Forward ports 22, 25 to ports 22, 25"); info( " -t 22-80 Forward ports 22 to 80"); info( " -t 22-80:32-90 Forward ports 22 to 80 to"); info( " corresponding port numbers plus 10"); info( " -t 192.0.2.1/5 Bind port 5 of 192.0.2.1 to guest"); info( " -t 5-25,~10-20 Forward ports 5 to 9, and 21 to 25"); info( " -t ~25 Forward all ports except for 25"); info( " default: none"); info( " -u, --udp-ports SPEC UDP port forwarding to guest"); info( " SPEC is as described for TCP above"); info( " default: none"); exit(EXIT_FAILURE); pasta_opts: info( " -t, --tcp-ports SPEC TCP port forwarding to namespace"); info( " can be specified multiple times"); info( " SPEC can be:"); info( " 'none': don't forward any ports"); info( " 'auto': forward all ports currently bound in namespace"); info( " a comma-separated list, optionally ranged with '-'"); info( " and optional target ports after ':', with optional"); info( " address specification suffixed by '/'. Examples:"); info( " -t 22 Forward local port 22 to port 22 in netns"); info( " -t 22:23 Forward local port 22 to port 23"); info( " -t 22,25 Forward ports 22, 25 to ports 22, 25"); info( " -t 22-80 Forward ports 22 to 80"); info( " -t 22-80:32-90 Forward ports 22 to 80 to"); info( " corresponding port numbers plus 10"); info( " -t 192.0.2.1/5 Bind port 5 of 192.0.2.1 to namespace"); info( " -t 5-25,~10-20 Forward ports 5 to 9, and 21 to 25"); info( " -t ~25 Forward all bound ports except for 25"); info( " default: auto"); info( " IPv6 bound ports are also forwarded for IPv4"); info( " -u, --udp-ports SPEC UDP port forwarding to namespace"); info( " SPEC is as described for TCP above"); info( " default: auto"); info( " IPv6 bound ports are also forwarded for IPv4"); info( " unless specified, with '-t auto', UDP ports with numbers"); info( " corresponding to forwarded TCP port numbers are"); info( " forwarded too"); info( " -T, --tcp-ns SPEC TCP port forwarding to init namespace"); info( " SPEC is as described above"); info( " default: auto"); info( " -U, --udp-ns SPEC UDP port forwarding to init namespace"); info( " SPEC is as described above"); info( " default: auto"); info( " --userns NSPATH Target user namespace to join"); info( " --netns PATH|NAME Target network namespace to join"); info( " --netns-only Don't join existing user namespace"); info( " implied if PATH or NAME are given without --userns"); info( " --config-net Configure tap interface in namespace"); info( " --ns-mac-addr ADDR Set MAC address on tap interface"); exit(EXIT_FAILURE); } /** * conf_print() - Print fundamental configuration parameters * @c: Execution context */ static void conf_print(const struct ctx *c) { char buf4[INET_ADDRSTRLEN], ifn[IFNAMSIZ]; int i; if (c->ifi4) info("Outbound interface (IPv4): %s", if_indextoname(c->ifi4, ifn)); if (c->ifi6) info("Outbound interface (IPv6): %s", if_indextoname(c->ifi6, ifn)); if (c->mode == MODE_PASTA) info("Namespace interface: %s", c->pasta_ifn); info("MAC:"); info(" host: %02x:%02x:%02x:%02x:%02x:%02x", c->mac[0], c->mac[1], c->mac[2], c->mac[3], c->mac[4], c->mac[5]); if (c->ifi4) { if (!c->no_dhcp) { info("DHCP:"); info(" assign: %s", inet_ntop(AF_INET, &c->ip4.addr, buf4, sizeof(buf4))); info(" mask: %s", inet_ntop(AF_INET, &c->ip4.mask, buf4, sizeof(buf4))); info(" router: %s", inet_ntop(AF_INET, &c->ip4.gw, buf4, sizeof(buf4))); } for (i = 0; c->ip4.dns[i]; i++) { if (!i) info("DNS:"); inet_ntop(AF_INET, &c->ip4.dns[i], buf4, sizeof(buf4)); info(" %s", buf4); } for (i = 0; *c->dns_search[i].n; i++) { if (!i) info("DNS search list:"); info(" %s", c->dns_search[i].n); } } if (c->ifi6) { char buf6[INET6_ADDRSTRLEN]; if (!c->no_ndp && !c->no_dhcpv6) info("NDP/DHCPv6:"); else if (!c->no_ndp) info("DHCPv6:"); else if (!c->no_dhcpv6) info("NDP:"); else goto dns6; info(" assign: %s", inet_ntop(AF_INET6, &c->ip6.addr, buf6, sizeof(buf6))); info(" router: %s", inet_ntop(AF_INET6, &c->ip6.gw, buf6, sizeof(buf6))); info(" our link-local: %s", inet_ntop(AF_INET6, &c->ip6.addr_ll, buf6, sizeof(buf6))); dns6: for (i = 0; !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.dns[i]); i++) { if (!i) info("DNS:"); inet_ntop(AF_INET6, &c->ip6.dns[i], buf6, sizeof(buf6)); info(" %s", buf6); } for (i = 0; *c->dns_search[i].n; i++) { if (!i) info("DNS search list:"); info(" %s", c->dns_search[i].n); } } } /** * conf_runas() - Handle --runas: look up desired UID and GID * @opt: Passed option value * @uid: User ID, set on return if valid * @gid: Group ID, set on return if valid * * Return: 0 on success, negative error code on failure */ static int conf_runas(const char *opt, unsigned int *uid, unsigned int *gid) { char ubuf[LOGIN_NAME_MAX], gbuf[LOGIN_NAME_MAX], *endptr; struct passwd *pw; struct group *gr; /* NOLINTNEXTLINE(cert-err34-c): 2 if conversion succeeds */ if (sscanf(opt, "%u:%u", uid, gid) == 2 && *uid && *gid) return 0; *uid = strtol(opt, &endptr, 0); if (!*endptr && (*gid = *uid)) return 0; #ifdef GLIBC_NO_STATIC_NSS (void)ubuf; (void)gbuf; (void)pw; (void)gr; return -EINVAL; #else /* NOLINTNEXTLINE(cert-err34-c): 2 if conversion succeeds */ if (sscanf(opt, "%" STR(LOGIN_NAME_MAX) "[^:]:" "%" STR(LOGIN_NAME_MAX) "s", ubuf, gbuf) == 2) { if (!(pw = getpwnam(ubuf)) || !(*uid = pw->pw_uid)) return -ENOENT; if (!(gr = getgrnam(gbuf)) || !(*gid = gr->gr_gid)) return -ENOENT; return 0; } pw = getpwnam(ubuf); if (!pw || !(*uid = pw->pw_uid) || !(*gid = pw->pw_gid)) return -ENOENT; return 0; #endif /* !GLIBC_NO_STATIC_NSS */ } /** * conf_ugid() - Determine UID and GID to run as * @runas: --runas option, may be NULL * @uid: User ID, set on success * @gid: Group ID, set on success * * Return: 0 on success, negative error code on failure */ static int conf_ugid(const char *runas, uid_t *uid, gid_t *gid) { const char root_uid_map[] = " 0 0 4294967295"; struct passwd *pw; char buf[BUFSIZ]; int ret; int fd; /* If user has specified --runas, that takes precedence... */ if (runas) { ret = conf_runas(runas, uid, gid); if (ret) err("Invalid --runas option: %s", runas); return ret; } /* ...otherwise default to current user and group... */ *uid = geteuid(); *gid = getegid(); /* ...as long as it's not root... */ if (*uid) return 0; /* ...or at least not root in the init namespace... */ if ((fd = open("/proc/self/uid_map", O_RDONLY | O_CLOEXEC)) < 0) { ret = -errno; err("Can't determine if we're in init namespace: %s", strerror(-ret)); return ret; } if (read(fd, buf, BUFSIZ) != sizeof(root_uid_map) || strncmp(buf, root_uid_map, sizeof(root_uid_map) - 1)) { close(fd); return 0; } close(fd); /* ...otherwise use nobody:nobody */ warn("Don't run as root. Changing to nobody..."); #ifndef GLIBC_NO_STATIC_NSS pw = getpwnam("nobody"); if (!pw) { perror("getpwnam"); exit(EXIT_FAILURE); } *uid = pw->pw_uid; *gid = pw->pw_gid; #else (void)pw; /* Common value for 'nobody', not really specified */ *uid = *gid = 65534; #endif return 0; } /** * conf() - Process command-line arguments and set configuration * @c: Execution context * @argc: Argument count * @argv: Options, plus target PID for pasta mode */ void conf(struct ctx *c, int argc, char **argv) { struct option options[] = { {"debug", no_argument, NULL, 'd' }, {"quiet", no_argument, NULL, 'q' }, {"foreground", no_argument, NULL, 'f' }, {"stderr", no_argument, NULL, 'e' }, {"help", no_argument, NULL, 'h' }, {"socket", required_argument, NULL, 's' }, {"ns-ifname", required_argument, NULL, 'I' }, {"pcap", required_argument, NULL, 'p' }, {"pid", required_argument, NULL, 'P' }, {"mtu", required_argument, NULL, 'm' }, {"address", required_argument, NULL, 'a' }, {"netmask", required_argument, NULL, 'n' }, {"mac-addr", required_argument, NULL, 'M' }, {"gateway", required_argument, NULL, 'g' }, {"interface", required_argument, NULL, 'i' }, {"dns", required_argument, NULL, 'D' }, {"search", required_argument, NULL, 'S' }, {"no-tcp", no_argument, &c->no_tcp, 1 }, {"no-udp", no_argument, &c->no_udp, 1 }, {"no-icmp", no_argument, &c->no_icmp, 1 }, {"no-dhcp", no_argument, &c->no_dhcp, 1 }, {"no-dhcpv6", no_argument, &c->no_dhcpv6, 1 }, {"no-ndp", no_argument, &c->no_ndp, 1 }, {"no-ra", no_argument, &c->no_ra, 1 }, {"no-map-gw", no_argument, &c->no_map_gw, 1 }, {"ipv4-only", no_argument, NULL, '4' }, {"ipv6-only", no_argument, NULL, '6' }, {"tcp-ports", required_argument, NULL, 't' }, {"udp-ports", required_argument, NULL, 'u' }, {"tcp-ns", required_argument, NULL, 'T' }, {"udp-ns", required_argument, NULL, 'U' }, {"userns", required_argument, NULL, 2 }, {"netns", required_argument, NULL, 3 }, {"netns-only", no_argument, &c->netns_only, 1 }, {"config-net", no_argument, &c->pasta_conf_ns, 1 }, {"ns-mac-addr", required_argument, NULL, 4 }, {"dhcp-dns", no_argument, NULL, 5 }, {"no-dhcp-dns", no_argument, NULL, 6 }, {"dhcp-search", no_argument, NULL, 7 }, {"no-dhcp-search", no_argument, NULL, 8 }, {"dns-forward", required_argument, NULL, 9 }, {"no-netns-quit", no_argument, NULL, 10 }, {"trace", no_argument, NULL, 11 }, {"runas", required_argument, NULL, 12 }, { 0 }, }; struct get_bound_ports_ns_arg ns_ports_arg = { .c = c }; char userns[PATH_MAX] = { 0 }, netns[PATH_MAX] = { 0 }; enum conf_port_type tcp_tap = 0, tcp_init = 0; enum conf_port_type udp_tap = 0, udp_init = 0; bool v4_only = false, v6_only = false; struct in6_addr *dns6 = c->ip6.dns; struct fqdn *dnss = c->dns_search; uint32_t *dns4 = c->ip4.dns; int name, ret, mask, b, i; const char *runas = NULL; unsigned int ifi = 0; uid_t uid; gid_t gid; if (c->mode == MODE_PASTA) c->no_dhcp_dns = c->no_dhcp_dns_search = 1; do { const char *optstring; if (c->mode == MODE_PASST) optstring = "dqfehs:p:P:m:a:n:M:g:i:D:S:46t:u:"; else optstring = "dqfehI:p:P:m:a:n:M:g:i:D:S:46t:u:T:U:"; name = getopt_long(argc, argv, optstring, options, NULL); switch (name) { case -1: case 0: break; case 2: if (c->mode != MODE_PASTA) { err("--userns is for pasta mode only"); usage(argv[0]); } ret = snprintf(userns, sizeof(userns), "%s", optarg); if (ret <= 0 || ret >= (int)sizeof(userns)) { err("Invalid userns: %s", optarg); usage(argv[0]); } break; case 3: if (c->mode != MODE_PASTA) { err("--netns is for pasta mode only"); usage(argv[0]); } ret = conf_netns_opt(netns, optarg); if (ret < 0) usage(argv[0]); break; case 4: if (c->mode != MODE_PASTA) { err("--ns-mac-addr is for pasta mode only"); usage(argv[0]); } for (i = 0; i < ETH_ALEN; i++) { errno = 0; b = strtol(optarg + (intptr_t)i * 3, NULL, 16); if (b < 0 || b > UCHAR_MAX || errno) { err("Invalid MAC address: %s", optarg); usage(argv[0]); } c->mac_guest[i] = b; } break; case 5: if (c->mode != MODE_PASTA) { err("--dhcp-dns is for pasta mode only"); usage(argv[0]); } c->no_dhcp_dns = 0; break; case 6: if (c->mode != MODE_PASST) { err("--no-dhcp-dns is for passt mode only"); usage(argv[0]); } c->no_dhcp_dns = 1; break; case 7: if (c->mode != MODE_PASTA) { err("--dhcp-search is for pasta mode only"); usage(argv[0]); } c->no_dhcp_dns_search = 0; break; case 8: if (c->mode != MODE_PASST) { err("--no-dhcp-search is for passt mode only"); usage(argv[0]); } c->no_dhcp_dns_search = 1; break; case 9: if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.dns_fwd) && inet_pton(AF_INET6, optarg, &c->ip6.dns_fwd) && !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.dns_fwd) && !IN6_IS_ADDR_LOOPBACK(&c->ip6.dns_fwd)) break; if (c->ip4.dns_fwd == INADDR_ANY && inet_pton(AF_INET, optarg, &c->ip4.dns_fwd) && c->ip4.dns_fwd != INADDR_ANY && c->ip4.dns_fwd != INADDR_BROADCAST && c->ip4.dns_fwd != INADDR_LOOPBACK) break; err("Invalid DNS forwarding address: %s", optarg); usage(argv[0]); break; case 10: if (c->mode != MODE_PASTA) { err("--no-netns-quit is for pasta mode only"); usage(argv[0]); } c->no_netns_quit = 1; break; case 11: if (c->trace) { err("Multiple --trace options given"); usage(argv[0]); } if (c->quiet) { err("Either --trace or --quiet"); usage(argv[0]); } c->trace = c->debug = c->foreground = 1; break; case 12: if (runas) { err("Multiple --runas options given"); usage(argv[0]); } runas = optarg; break; case 'd': if (c->debug) { err("Multiple --debug options given"); usage(argv[0]); } if (c->quiet) { err("Either --debug or --quiet"); usage(argv[0]); } c->debug = 1; c->foreground = 1; break; case 'e': if (c->stderr) { err("Multiple --stderr options given"); usage(argv[0]); } c->stderr = 1; break; case 'q': if (c->quiet) { err("Multiple --quiet options given"); usage(argv[0]); } if (c->debug) { err("Either --debug or --quiet"); usage(argv[0]); } c->quiet = 1; break; case 'f': if (c->foreground && !c->debug) { err("Multiple --foreground options given"); usage(argv[0]); } c->foreground = 1; break; case 's': if (*c->sock_path) { err("Multiple --socket options given"); usage(argv[0]); } ret = snprintf(c->sock_path, UNIX_SOCK_MAX - 1, "%s", optarg); if (ret <= 0 || ret >= (int)sizeof(c->sock_path)) { err("Invalid socket path: %s", optarg); usage(argv[0]); } break; case 'I': if (*c->pasta_ifn) { err("Multiple --ns-ifname options given"); usage(argv[0]); } ret = snprintf(c->pasta_ifn, IFNAMSIZ - 1, "%s", optarg); if (ret <= 0 || ret >= IFNAMSIZ - 1) { err("Invalid interface name: %s", optarg); usage(argv[0]); } break; case 'p': if (*c->pcap) { err("Multiple --pcap options given"); usage(argv[0]); } ret = snprintf(c->pcap, sizeof(c->pcap), "%s", optarg); if (ret <= 0 || ret >= (int)sizeof(c->pcap)) { err("Invalid pcap path: %s", optarg); usage(argv[0]); } break; case 'P': if (*c->pid_file) { err("Multiple --pid options given"); usage(argv[0]); } ret = snprintf(c->pid_file, sizeof(c->pid_file), "%s", optarg); if (ret <= 0 || ret >= (int)sizeof(c->pid_file)) { err("Invalid PID file: %s", optarg); usage(argv[0]); } break; case 'm': if (c->mtu) { err("Multiple --mtu options given"); usage(argv[0]); } errno = 0; c->mtu = strtol(optarg, NULL, 0); if (!c->mtu) { c->mtu = -1; break; } if (c->mtu < ETH_MIN_MTU || c->mtu > (int)ETH_MAX_MTU || errno) { err("Invalid MTU: %s", optarg); usage(argv[0]); } break; case 'a': if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr) && inet_pton(AF_INET6, optarg, &c->ip6.addr) && !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.addr) && !IN6_IS_ADDR_LOOPBACK(&c->ip6.addr) && !IN6_IS_ADDR_V4MAPPED(&c->ip6.addr) && !IN6_IS_ADDR_V4COMPAT(&c->ip6.addr) && !IN6_IS_ADDR_MULTICAST(&c->ip6.addr)) break; if (c->ip4.addr == INADDR_ANY && inet_pton(AF_INET, optarg, &c->ip4.addr) && c->ip4.addr != INADDR_ANY && c->ip4.addr != INADDR_BROADCAST && c->ip4.addr != INADDR_LOOPBACK && !IN_MULTICAST(c->ip4.addr)) break; err("Invalid address: %s", optarg); usage(argv[0]); break; case 'n': if (inet_pton(AF_INET, optarg, &c->ip4.mask)) break; errno = 0; mask = strtol(optarg, NULL, 0); if (mask > 0 && mask <= 32 && !errno) { c->ip4.mask = htonl(0xffffffff << (32 - mask)); break; } err("Invalid netmask: %s", optarg); usage(argv[0]); break; case 'M': for (i = 0; i < ETH_ALEN; i++) { errno = 0; b = strtol(optarg + (intptr_t)i * 3, NULL, 16); if (b < 0 || b > UCHAR_MAX || errno) { err("Invalid MAC address: %s", optarg); usage(argv[0]); } c->mac[i] = b; } break; case 'g': if (IN6_IS_ADDR_UNSPECIFIED(&c->ip6.gw) && inet_pton(AF_INET6, optarg, &c->ip6.gw) && !IN6_IS_ADDR_UNSPECIFIED(&c->ip6.gw) && !IN6_IS_ADDR_LOOPBACK(&c->ip6.gw)) break; if (c->ip4.gw == INADDR_ANY && inet_pton(AF_INET, optarg, &c->ip4.gw) && c->ip4.gw != INADDR_ANY && c->ip4.gw != INADDR_BROADCAST && c->ip4.gw != INADDR_LOOPBACK) break; err("Invalid gateway address: %s", optarg); usage(argv[0]); break; case 'i': if (ifi) { err("Redundant interface: %s", optarg); usage(argv[0]); } if (!(ifi = if_nametoindex(optarg))) { err("Invalid interface name %s: %s", optarg, strerror(errno)); usage(argv[0]); } break; case 'D': if (!strcmp(optarg, "none")) { if (c->no_dns) { err("Redundant DNS options"); usage(argv[0]); } if (dns4 - c->ip4.dns || dns6 - c->ip6.dns) { err("Conflicting DNS options"); usage(argv[0]); } c->no_dns = 1; break; } if (c->no_dns) { err("Conflicting DNS options"); usage(argv[0]); } if (dns4 - &c->ip4.dns[0] < ARRAY_SIZE(c->ip4.dns) && inet_pton(AF_INET, optarg, dns4)) { dns4++; break; } if (dns6 - &c->ip6.dns[0] < ARRAY_SIZE(c->ip6.dns) && inet_pton(AF_INET6, optarg, dns6)) { dns6++; break; } err("Cannot use DNS address %s", optarg); usage(argv[0]); break; case 'S': if (!strcmp(optarg, "none")) { if (c->no_dns_search) { err("Redundant DNS search options"); usage(argv[0]); } if (dnss != c->dns_search) { err("Conflicting DNS search options"); usage(argv[0]); } c->no_dns_search = 1; break; } if (c->no_dns_search) { err("Conflicting DNS search options"); usage(argv[0]); } if (dnss - c->dns_search < ARRAY_SIZE(c->dns_search)) { ret = snprintf(dnss->n, sizeof(*c->dns_search), "%s", optarg); dnss++; if (ret > 0 && ret < (int)sizeof(*c->dns_search)) break; } err("Cannot use DNS search domain %s", optarg); usage(argv[0]); break; case '4': v4_only = true; break; case '6': v6_only = true; break; case 't': case 'u': case 'T': case 'U': /* Handle these later, once addresses are configured */ break; case '?': case 'h': default: usage(argv[0]); break; } } while (name != -1); ret = conf_ugid(runas, &uid, &gid); if (ret) usage(argv[0]); drop_root(uid, gid); if (c->mode == MODE_PASTA) { if (conf_pasta_ns(&c->netns_only, userns, netns, optind, argc, argv) < 0) usage(argv[0]); } else if (optind != argc) { usage(argv[0]); } if (c->pasta_conf_ns) c->no_ra = 1; if (c->mode == MODE_PASTA) { if (*netns) { pasta_open_ns(c, userns, netns); } else { pasta_start_ns(c, argc - optind, argv + optind); } } if (nl_sock_init(c)) { err("Failed to get netlink socket"); exit(EXIT_FAILURE); } if (v4_only && v6_only) { err("Options ipv4-only and ipv6-only are mutually exclusive"); usage(argv[0]); } if (!v6_only) c->ifi4 = conf_ip4(ifi, &c->ip4, c->mac); if (!v4_only) c->ifi6 = conf_ip6(ifi, &c->ip6, c->mac); if (!c->ifi4 && !c->ifi6) { err("External interface not usable"); exit(EXIT_FAILURE); } /* Now we can process port configuration options */ optind = 1; do { enum conf_port_type *set = NULL; const char *optstring; if (c->mode == MODE_PASST) optstring = "dqfehs:p::P:m:a:n:M:g:i:D::S::46t:u:"; else optstring = "dqfehI:p::P:m:a:n:M:g:i:D::S::46t:u:T:U:"; name = getopt_long(argc, argv, optstring, options, NULL); switch (name) { case 't': case 'u': case 'T': case 'U': if (name == 't') set = &tcp_tap; else if (name == 'T') set = &tcp_init; else if (name == 'u') set = &udp_tap; else if (name == 'U') set = &udp_init; if (!optarg || conf_ports(c, name, optarg, set)) usage(argv[0]); break; default: break; } } while (name != -1); if (!c->ifi4) c->no_dhcp = 1; if (!c->ifi6) { c->no_ndp = 1; c->no_dhcpv6 = 1; } if (!c->mtu) c->mtu = ROUND_DOWN(ETH_MAX_MTU - ETH_HLEN, sizeof(uint32_t)); get_dns(c); if (!*c->pasta_ifn) { if (c->ifi4) if_indextoname(c->ifi4, c->pasta_ifn); else if_indextoname(c->ifi6, c->pasta_ifn); } c->tcp.ns_detect_ports = c->udp.ns_detect_ports = 0; c->tcp.init_detect_ports = c->udp.init_detect_ports = 0; if (c->mode == MODE_PASTA) { c->proc_net_tcp[V4][0] = c->proc_net_tcp[V4][1] = -1; c->proc_net_tcp[V6][0] = c->proc_net_tcp[V6][1] = -1; c->proc_net_udp[V4][0] = c->proc_net_udp[V4][1] = -1; c->proc_net_udp[V6][0] = c->proc_net_udp[V6][1] = -1; if (!tcp_tap || tcp_tap == PORT_AUTO) { c->tcp.ns_detect_ports = 1; ns_ports_arg.proto = IPPROTO_TCP; NS_CALL(get_bound_ports_ns, &ns_ports_arg); } if (!udp_tap || udp_tap == PORT_AUTO) { c->udp.ns_detect_ports = 1; ns_ports_arg.proto = IPPROTO_UDP; NS_CALL(get_bound_ports_ns, &ns_ports_arg); } if (!tcp_init || tcp_init == PORT_AUTO) { c->tcp.init_detect_ports = 1; get_bound_ports(c, 0, IPPROTO_TCP); } if (!udp_init || udp_init == PORT_AUTO) { c->udp.init_detect_ports = 1; get_bound_ports(c, 0, IPPROTO_UDP); } } if (!c->quiet) conf_print(c); }