diff --git a/Dockerfile.artifact b/Dockerfile.artifact index 49b8e9b..3c207ce 100644 --- a/Dockerfile.artifact +++ b/Dockerfile.artifact @@ -3,7 +3,7 @@ ARG DEBIAN_VERSION=10 FROM --platform=$TARGETPLATFORM debian:${DEBIAN_VERSION} AS build ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && apt install -y automake autotools-dev make gcc libglib2.0-dev libcap-dev libseccomp-dev git ninja-build python3-pip +RUN apt update && apt install -y automake autotools-dev make gcc libglib2.0-dev libcap-dev libseccomp-dev libssl-dev git ninja-build python3-pip RUN pip3 install meson RUN git clone https://gitlab.freedesktop.org/slirp/libslirp.git /libslirp WORKDIR /libslirp diff --git a/Dockerfile.buildtests b/Dockerfile.buildtests index 4873e7d..341fccf 100644 --- a/Dockerfile.buildtests +++ b/Dockerfile.buildtests @@ -2,7 +2,7 @@ ARG LIBSLIRP_COMMIT=v4.6.1 # Alpine FROM alpine:3 AS buildtest-alpine3-static -RUN apk add --no-cache git build-base autoconf automake libtool linux-headers glib-dev glib-static libcap-static libcap-dev libseccomp-dev libseccomp-static git meson +RUN apk add --no-cache git build-base autoconf automake libtool linux-headers glib-dev glib-static libcap-static libcap-dev libseccomp-dev openssl-dev openssl-libs-static libseccomp-static git meson RUN git clone https://gitlab.freedesktop.org/slirp/libslirp.git /libslirp WORKDIR /libslirp ARG LIBSLIRP_COMMIT @@ -14,7 +14,7 @@ RUN ./autogen.sh && ./configure LDFLAGS="-static" && make && cp -f slirp4netns / # Ubuntu FROM ubuntu:18.04 AS buildtest-ubuntu1804-common ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && apt install -y automake autotools-dev make gcc libglib2.0-dev libcap-dev libseccomp-dev git ninja-build python3-pip +RUN apt update && apt install -y automake autotools-dev make gcc libglib2.0-dev libcap-dev libseccomp-dev libssl-dev git ninja-build python3-pip RUN pip3 install meson RUN git clone https://gitlab.freedesktop.org/slirp/libslirp.git /libslirp WORKDIR /libslirp @@ -32,7 +32,7 @@ RUN ./configure && make && cp -f slirp4netns / # openSUSE (dynamic only) FROM opensuse/leap:15 AS buildtest-opensuse15-common -RUN zypper install -y --no-recommends autoconf automake gcc glib2-devel git make libcap-devel libseccomp-devel ninja python3-pip +RUN zypper install -y --no-recommends autoconf automake gcc glib2-devel git make libcap-devel libseccomp-devel libopenssl-devel ninja python3-pip RUN pip3 install meson RUN git clone https://gitlab.freedesktop.org/slirp/libslirp.git /libslirp WORKDIR /libslirp diff --git a/Dockerfile.tests b/Dockerfile.tests index 1b5ed2e..d570999 100644 --- a/Dockerfile.tests +++ b/Dockerfile.tests @@ -2,7 +2,7 @@ ARG LIBSLIRP_COMMIT=v4.6.1 FROM ubuntu:20.04 AS build ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && apt install -y automake autotools-dev make gcc libglib2.0-dev libcap-dev libseccomp-dev git ninja-build python3-pip +RUN apt update && apt install -y automake autotools-dev make gcc libglib2.0-dev libcap-dev libseccomp-dev libssl-dev git ninja-build python3-pip socat RUN pip3 install meson RUN git clone https://gitlab.freedesktop.org/slirp/libslirp.git /libslirp WORKDIR /libslirp diff --git a/Makefile.am b/Makefile.am index 00758ab..5c78129 100644 --- a/Makefile.am +++ b/Makefile.am @@ -7,11 +7,16 @@ noinst_LIBRARIES = libparson.a AM_TESTS_ENVIRONMENT = PATH="$(abs_top_builddir):$(PATH)" TESTS = tests/test-slirp4netns-api-socket.sh \ tests/test-slirp4netns-cidr.sh \ + tests/test-slirp4netns-cidr6.sh \ tests/test-slirp4netns-configure.sh \ tests/test-slirp4netns-dhcp.sh \ tests/test-slirp4netns-disable-dns.sh \ tests/test-slirp4netns-disable-host-loopback.sh \ tests/test-slirp4netns-exit-fd.sh \ + tests/test-slirp4netns-hostfwd.sh \ + tests/test-slirp4netns-hostfwd4.sh \ + tests/test-slirp4netns-hostfwd6.sh \ + tests/test-slirp4netns-ipv6.sh \ tests/test-slirp4netns-macaddress.sh \ tests/test-slirp4netns-nspath.sh \ tests/test-slirp4netns-outbound-addr.sh \ @@ -47,7 +52,7 @@ libparson_a_CFLAGS = $(AM_CFLAGS) -I$(abs_top_builddir)/vendor/parson libparson_a_SOURCES = vendor/parson/parson.c slirp4netns_SOURCES = main.c slirp4netns.c api.c sandbox.c seccompfilter.c -slirp4netns_LDADD = libparson.a @GLIB_LIBS@ @SLIRP_LIBS@ @LIBSECCOMP_LIBS@ -lpthread +slirp4netns_LDADD = libparson.a @GLIB_LIBS@ @SLIRP_LIBS@ @LIBSECCOMP_LIBS@ -lpthread -lcrypto man1_MANS = slirp4netns.1 generate-man: diff --git a/README.md b/README.md index 239b5df..8c0dc45 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# slirp4netns: User-mode networking for unprivileged network namespaces +# slirp4netns: User-mode networking for unprivileged network namespaces slirp4netns provides user-mode networking ("slirp") for unprivileged network namespaces. @@ -73,7 +73,7 @@ Also available as a package on almost all Linux distributions: * [Arch Linux](https://www.archlinux.org/packages/community/x86_64/slirp4netns/) * [openSUSE (since Leap 15.0)](https://build.opensuse.org/package/show/openSUSE%3AFactory/slirp4netns) * [SUSE Linux Enterprise (since 15)](https://build.opensuse.org/package/show/devel%3Akubic/slirp4netns) -* [Debian GNU/Linux (since 10.0)](https://packages.debian.org/buster/slirp4netns) +* [Debian GNU/Linux (since 10.0)](https://packages.debian.org/buster/slirp4netns) * [Ubuntu (since 19.04)](https://packages.ubuntu.com/search?keywords=slirp4netns) * [NixOS](https://github.com/NixOS/nixpkgs/tree/master/pkgs/tools/networking/slirp4netns) * [Gentoo Linux](https://packages.gentoo.org/packages/app-emulation/slirp4netns) @@ -126,7 +126,7 @@ starting slirp, MTU=65520 link/ether c2:28:0c:0e:29:06 brd ff:ff:ff:ff:ff:ff inet 10.0.2.100/24 brd 10.0.2.255 scope global tap0 valid_lft forever preferred_lft forever - inet6 fe80::c028:cff:fe0e:2906/64 scope link + inet6 fe80::c028:cff:fe0e:2906/64 scope link valid_lft forever preferred_lft forever (namespace)$ echo "nameserver 10.0.2.3" > /tmp/resolv.conf (namespace)$ mount --bind /tmp/resolv.conf /etc/resolv.conf @@ -169,13 +169,13 @@ The latest revision of slirp4netns is regularly benchmarked (`make benchmark`) o Build dependencies (`apt-get`): ```console -$ sudo apt-get install libglib2.0-dev libslirp-dev libcap-dev libseccomp-dev +$ sudo apt-get install libglib2.0-dev libslirp-dev libcap-dev libseccomp-dev libssl-dev ``` Build dependencies (`dnf`): ```console -$ sudo dnf install glib2-devel libslirp-devel libcap-devel libseccomp-devel +$ sudo dnf install glib2-devel libslirp-devel libcap-devel libseccomp-devel openssl-devel ``` Installation steps: @@ -187,7 +187,7 @@ $ make $ sudo make install ``` -* [libslirp](https://gitlab.freedesktop.org/slirp/libslirp) needs to be v4.1.0 or later. +* [libslirp](https://gitlab.freedesktop.org/slirp/libslirp) needs to be v4.1.0 or later. Using v4.5.0 or later is recommended. * To build `slirp4netns` as a static binary, run `./configure` with `LDFLAGS=-static`. * If you set `--prefix` to `$HOME`, you don't need to run `make install` with `sudo`. diff --git a/Vagrantfile b/Vagrantfile index 9a4c089..8a98657 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -20,8 +20,8 @@ Vagrant.configure("2") do |config| yum install -y \ autoconf automake make gcc gperf libtool \ git-core meson ninja-build \ - glib2-devel libcap-devel \ - git-core libtool iproute iputils iperf3 nmap jq + glib2-devel libcap-devel openssl-devel \ + git-core libtool iproute iputils iperf3 nmap jq socat # TODO: install udhcpc (required by test-slirp4netns-dhcp.sh) diff --git a/api.c b/api.c index c58e0c4..8d8fb19 100644 --- a/api.c +++ b/api.c @@ -41,9 +41,13 @@ int api_bindlisten(const char *api_socket) struct api_hostfwd { int id; int is_udp; - struct in_addr host_addr; + int is_ipv4; + int is_ipv6; + struct sockaddr_in host; + struct sockaddr_in guest; + struct sockaddr_in6 host6; + struct sockaddr_in6 guest6; int host_port; - struct in_addr guest_addr; int guest_port; }; @@ -107,9 +111,9 @@ static int api_handle_req_add_hostfwd(Slirp *slirp, int fd, struct api_ctx *ctx, exit(EXIT_FAILURE); } fwd->is_udp = -1; /* TODO: support SCTP */ - if (strcmp(proto_s, "udp") == 0) { + if (strncmp(proto_s, "udp", 3) == 0) { fwd->is_udp = 1; - } else if (strcmp(proto_s, "tcp") == 0) { + } else if (strncmp(proto_s, "tcp", 3) == 0) { fwd->is_udp = 0; } if (fwd->is_udp == -1) { @@ -119,16 +123,59 @@ static int api_handle_req_add_hostfwd(Slirp *slirp, int fd, struct api_ctx *ctx, free(fwd); goto finish; } +#if SLIRP_CONFIG_VERSION_MAX >= 3 + int flags = (fwd->is_udp ? SLIRP_HOSTFWD_UDP : 0); +#endif + if (host_addr_s == NULL || host_addr_s[0] == '\0') { host_addr_s = "0.0.0.0"; } - if (inet_pton(AF_INET, host_addr_s, &fwd->host_addr) != 1) { - const char *err = "{\"error\":{\"desc\":\"bad request: add_hostfwd: " - "bad arguments.host_addr\"}}"; - wrc = write(fd, err, strlen(err)); - free(fwd); - goto finish; + if (strcmp("0.0.0.0", host_addr_s) == 0 || strcmp("::", host_addr_s) == 0 || + strcmp("::0", host_addr_s) == 0) { + fwd->is_ipv4 = 1; + fwd->is_ipv6 = ctx->cfg->enable_ipv6; + host_addr_s = "0.0.0.0"; + } else { + if (strchr(host_addr_s, '.')) { + fwd->is_ipv4 = 1; + } else if (strchr(host_addr_s, ':')) { + if (!ctx->cfg->enable_ipv6) { + const char *err = + "{\"error\":{\"desc\":\"bad request: add_hostfwd: " + "bad arguments.host_addr\"}}"; + wrc = write(fd, err, strlen(err)); + free(fwd); + goto finish; + } + fwd->is_ipv6 = 1; + } } + + if (strlen(proto_s) == 4) { + if (proto_s[3] == '4') { + fwd->is_ipv4 = 1; + fwd->is_ipv6 = 0; + } else if (proto_s[3] == '6') { + if (!ctx->cfg->enable_ipv6) { + const char *err = + "{\"error\":{\"desc\":\"bad request: add_hostfwd: " + "bad arguments.proto\"}}"; + wrc = write(fd, err, strlen(err)); + free(fwd); + goto finish; + } + fwd->is_ipv4 = 0; + fwd->is_ipv6 = 1; + } else { + const char *err = + "{\"error\":{\"desc\":\"bad request: add_hostfwd: " + "bad arguments.proto\"}}"; + wrc = write(fd, err, strlen(err)); + free(fwd); + goto finish; + } + } + fwd->host_port = (int)json_object_dotget_number(jo, "arguments.host_port"); if (fwd->host_port == 0) { const char *err = "{\"error\":{\"desc\":\"bad request: add_hostfwd: " @@ -137,16 +184,6 @@ static int api_handle_req_add_hostfwd(Slirp *slirp, int fd, struct api_ctx *ctx, free(fwd); goto finish; } - - if (guest_addr_s == NULL || guest_addr_s[0] == '\0') { - fwd->guest_addr = ctx->cfg->recommended_vguest; - } else if (inet_pton(AF_INET, guest_addr_s, &fwd->guest_addr) != 1) { - const char *err = "{\"error\":{\"desc\":\"bad request: add_hostfwd: " - "bad arguments.guest_addr\"}}"; - wrc = write(fd, err, strlen(err)); - free(fwd); - goto finish; - } fwd->guest_port = (int)json_object_dotget_number(jo, "arguments.guest_port"); if (fwd->guest_port == 0) { @@ -156,14 +193,99 @@ static int api_handle_req_add_hostfwd(Slirp *slirp, int fd, struct api_ctx *ctx, free(fwd); goto finish; } - if (slirp_add_hostfwd(slirp, fwd->is_udp, fwd->host_addr, fwd->host_port, - fwd->guest_addr, fwd->guest_port) < 0) { - const char *err = "{\"error\":{\"desc\":\"bad request: add_hostfwd: " - "slirp_add_hostfwd failed\"}}"; - wrc = write(fd, err, strlen(err)); - free(fwd); - goto finish; + + if (fwd->is_ipv4) { + fwd->host.sin_family = AF_INET; + fwd->guest.sin_family = AF_INET; + fwd->host.sin_port = htons(fwd->host_port); + fwd->guest.sin_port = htons(fwd->guest_port); + if (guest_addr_s == NULL || guest_addr_s[0] == '\0') { + fwd->guest.sin_addr = ctx->cfg->recommended_vguest; + } else if (inet_pton(AF_INET, guest_addr_s, &fwd->guest.sin_addr) != + 1) { + const char *err = + "{\"error\":{\"desc\":\"bad request: add_hostfwd: " + "bad arguments.guest_addr\"}}"; + wrc = write(fd, err, strlen(err)); + free(fwd); + goto finish; + } + if (inet_pton(AF_INET, host_addr_s, &fwd->host.sin_addr) != 1) { + const char *err = + "{\"error\":{\"desc\":\"bad request: add_hostfwd: " + "bad arguments.host_addr\"}}"; + wrc = write(fd, err, strlen(err)); + free(fwd); + goto finish; + } +#if SLIRP_CONFIG_VERSION_MAX >= 3 + if (slirp_add_hostxfwd(slirp, (const struct sockaddr *)&fwd->host, + sizeof(fwd->host), + (const struct sockaddr *)&fwd->guest, + sizeof(fwd->guest), flags) < 0) { + const char *err = + "{\"error\":{\"desc\":\"bad request: add_hostfwd: " + "slirp_add_hostxfwd failed\"}}"; + wrc = write(fd, err, strlen(err)); + free(fwd); + goto finish; + } +#else + if (slirp_add_hostfwd(slirp, fwd->is_udp, fwd->host.sin_addr, + fwd->host_port, fwd->guest.sin_addr, + fwd->guest_port) < 0) { + const char *err = + "{\"error\":{\"desc\":\"bad request: add_hostfwd: " + "slirp_add_hostxfwd failed\"}}"; + wrc = write(fd, err, strlen(err)); + free(fwd); + goto finish; + } +#endif + } + +#if SLIRP_CONFIG_VERSION_MAX >= 3 + if (fwd->is_ipv6) { + fwd->host6.sin6_family = AF_INET6; + fwd->guest6.sin6_family = AF_INET6; + fwd->host6.sin6_port = htons(fwd->host_port); + fwd->guest6.sin6_port = htons(fwd->guest_port); + if (guest_addr_s == NULL || guest_addr_s[0] == '\0') { + fwd->guest6.sin6_addr = ctx->cfg->recommended_vguest6; + } else if (inet_pton(AF_INET6, guest_addr_s, &fwd->guest6.sin6_addr) != + 1) { + const char *err = + "{\"error\":{\"desc\":\"bad request: add_hostfwd: " + "bad arguments.guest_addr\"}}"; + wrc = write(fd, err, strlen(err)); + free(fwd); + goto finish; + } + if (strcmp(host_addr_s, "0.0.0.0") == 0) { + host_addr_s = "::"; + } + if (inet_pton(AF_INET6, host_addr_s, &fwd->host6.sin6_addr) != 1) { + const char *err = + "{\"error\":{\"desc\":\"bad request: add_hostfwd: " + "bad arguments.host_addr\"}}"; + wrc = write(fd, err, strlen(err)); + free(fwd); + goto finish; + } + flags |= SLIRP_HOSTFWD_V6ONLY; + if (slirp_add_hostxfwd(slirp, (const struct sockaddr *)&fwd->host6, + sizeof(fwd->host6), + (const struct sockaddr *)&fwd->guest6, + sizeof(fwd->guest6), flags) < 0) { + const char *err = + "{\"error\":{\"desc\":\"bad request: add_hostfwd: " + "slirp_add_hostxfwd failed\"}}"; + wrc = write(fd, err, strlen(err)); + free(fwd); + goto finish; + } } +#endif fwd->id = ctx->hostfwds_nextid; ctx->hostfwds_nextid++; ctx->hostfwds = g_list_append(ctx->hostfwds, fwd); @@ -185,21 +307,46 @@ static void api_handle_req_list_hostfwd_foreach(gpointer data, JSON_Value *entry_value = json_value_init_object(); JSON_Object *entry_object = json_value_get_object(entry_value); char host_addr[INET_ADDRSTRLEN], guest_addr[INET_ADDRSTRLEN]; - if (inet_ntop(AF_INET, &fwd->host_addr, host_addr, sizeof(host_addr)) == - NULL) { - perror("fatal: inet_ntop"); - exit(EXIT_FAILURE); + char host_addr6[INET6_ADDRSTRLEN], guest_addr6[INET6_ADDRSTRLEN]; + if (fwd->is_ipv4) { + if (inet_ntop(AF_INET, &fwd->host.sin_addr, host_addr, + sizeof(host_addr)) == NULL) { + perror("fatal: inet_ntop"); + exit(EXIT_FAILURE); + } + if (inet_ntop(AF_INET, &fwd->guest.sin_addr, guest_addr, + sizeof(guest_addr)) == NULL) { + perror("fatal: inet_ntop"); + exit(EXIT_FAILURE); + } } - if (inet_ntop(AF_INET, &fwd->guest_addr, guest_addr, sizeof(guest_addr)) == - NULL) { - perror("fatal: inet_ntop"); - exit(EXIT_FAILURE); + if (fwd->is_ipv6) { + if (inet_ntop(AF_INET6, &fwd->host6.sin6_addr, host_addr6, + sizeof(host_addr6)) == NULL) { + perror("fatal: inet_ntop"); + exit(EXIT_FAILURE); + } + if (inet_ntop(AF_INET6, &fwd->guest6.sin6_addr, guest_addr6, + sizeof(guest_addr6)) == NULL) { + perror("fatal: inet_ntop"); + exit(EXIT_FAILURE); + } } json_object_set_number(entry_object, "id", fwd->id); json_object_set_string(entry_object, "proto", fwd->is_udp ? "udp" : "tcp"); - json_object_set_string(entry_object, "host_addr", host_addr); + if (fwd->is_ipv4) { + json_object_set_string(entry_object, "host_addr", host_addr); + } + if (fwd->is_ipv6) { + json_object_set_string(entry_object, "host_addr6", host_addr6); + } json_object_set_number(entry_object, "host_port", fwd->host_port); - json_object_set_string(entry_object, "guest_addr", guest_addr); + if (fwd->is_ipv4) { + json_object_set_string(entry_object, "guest_addr", guest_addr); + } + if (fwd->is_ipv6) { + json_object_set_string(entry_object, "guest_addr6", guest_addr6); + } json_object_set_number(entry_object, "guest_port", fwd->guest_port); /* json_array_append_value does not copy passed value */ if (json_array_append_value(entries_array, entry_value) != JSONSuccess) { @@ -257,16 +404,41 @@ static int api_handle_req_remove_hostfwd(Slirp *slirp, int fd, } else { struct api_hostfwd *fwd = found->data; const char *api_ok = "{\"return\":{}}"; - if (slirp_remove_hostfwd(slirp, fwd->is_udp, fwd->host_addr, - fwd->host_port) < 0) { - const char *err = "{\"error\":{\"desc\":\"bad request: " - "remove_hostfwd: slirp_remove_hostfwd failed\"}}"; - wrc = write(fd, err, strlen(err)); - } else { - ctx->hostfwds = g_list_remove(ctx->hostfwds, fwd); - g_free(fwd); - wrc = write(fd, api_ok, strlen(api_ok)); + if (fwd->is_ipv4) { +#if SLIRP_CONFIG_VERSION_MAX >= 3 + if (slirp_remove_hostxfwd(slirp, + (const struct sockaddr *)&fwd->host, + sizeof(fwd->host), 0) < 0) { + const char *err = + "{\"error\":{\"desc\":\"bad request: " + "remove_hostfwd: slirp_remove_hostxfwd failed\"}}"; + return write(fd, err, strlen(err)); + } +#else + if (slirp_remove_hostfwd(slirp, fwd->is_udp, fwd->host.sin_addr, + fwd->host_port) < 0) { + const char *err = + "{\"error\":{\"desc\":\"bad request: " + "remove_hostfwd: slirp_remove_hostfwd failed\"}}"; + return write(fd, err, strlen(err)); + } +#endif + } +#if SLIRP_CONFIG_VERSION_MAX >= 3 + if (fwd->is_ipv6) { + if (slirp_remove_hostxfwd(slirp, + (const struct sockaddr *)&fwd->host6, + sizeof(fwd->host6), 0) < 0) { + const char *err = + "{\"error\":{\"desc\":\"bad request: " + "remove_hostfwd: slirp_remove_hostxfwd failed\"}}"; + return write(fd, err, strlen(err)); + } } +#endif + ctx->hostfwds = g_list_remove(ctx->hostfwds, fwd); + g_free(fwd); + wrc = write(fd, api_ok, strlen(api_ok)); } return wrc; } diff --git a/configure.ac b/configure.ac index 41935af..026f240 100644 --- a/configure.ac +++ b/configure.ac @@ -36,6 +36,7 @@ PKG_CHECK_MODULES(GLIB, glib-2.0) PKG_CHECK_MODULES(SLIRP, slirp >= 4.1.0) PKG_CHECK_MODULES(LIBCAP, libcap) PKG_CHECK_MODULES(LIBSECCOMP, libseccomp) +PKG_CHECK_MODULES(LIBCRYPTO, libcrypto) AC_CONFIG_FILES([Makefile]) AC_OUTPUT diff --git a/main.c b/main.c index f2dcb9c..6912757 100644 --- a/main.c +++ b/main.c @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -23,18 +25,26 @@ #include "slirp4netns.h" #include #include +#include #define DEFAULT_MTU (1500) #define DEFAULT_CIDR ("10.0.2.0/24") +#define DEFAULT_CIDR6 ("fd00::/64") #define DEFAULT_VHOST_OFFSET (2) // 10.0.2.2 +#define DEFAULT_VHOST_OFFSET6 (2) #define DEFAULT_VDHCPSTART_OFFSET (15) // 10.0.2.15 +#define DEFAULT_VDHCPSTART_OFFSET6 (15) #define DEFAULT_VNAMESERVER_OFFSET (3) // 10.0.2.3 +#define DEFAULT_VNAMESERVER_OFFSET6 (3) #define DEFAULT_RECOMMENDED_VGUEST_OFFSET (100) // 10.0.2.100 +#define DEFAULT_RECOMMENDED_VGUEST_OFFSET6 (100) #define DEFAULT_NETNS_TYPE ("pid") #define NETWORK_PREFIX_MIN (1) // >=26 is not supported because the recommended guest IP is set to network addr // + 100 . #define NETWORK_PREFIX_MAX (25) +#define NETWORK_PREFIX_MIN6 (8) +#define NETWORK_PREFIX_MAX6 (128) static int nsenter(pid_t target_pid, char *netns, char *userns, bool only_userns) @@ -205,6 +215,169 @@ static int configure_network(const char *tapname, return 0; } +struct in6_ifreq { + struct in6_addr ifr6_addr; + __u32 ifr6_prefixlen; + unsigned int ifr6_ifindex; +}; + +static const char *pseudo_random_global_id(const char *device) +{ + static char id[INET6_ADDRSTRLEN]; + char tmp[INET6_ADDRSTRLEN - 10]; + unsigned char eui64[16]; + unsigned char hash[SHA_DIGEST_LENGTH]; + struct ntptimeval ntv; + time_t tv; + struct ifreq ifr; + unsigned char mac[18]; + int sockfd; + + sockfd = socket(AF_INET, SOCK_DGRAM, 0); + if (sockfd < 0) { + perror("cannot create socket"); + return NULL; + } + + memset(&ifr, 0, sizeof(ifr)); + ifr.ifr_flags = IFF_UP | IFF_RUNNING; + + if (device == NULL) { + device = "lo"; + } + strncpy(ifr.ifr_name, device, sizeof(ifr.ifr_name) - 1); + + if (ioctl(sockfd, SIOCGIFHWADDR, &ifr) < 0) { + int rand = open("/dev/urandom", O_RDONLY); + if (rand == -1) { + rand = open("/dev/random", O_RDONLY); + } + if (rand == -1) { + perror("cannot get dev hwaddr and cannot open random"); + return NULL; + } + if (read(rand, &mac, sizeof(mac)) != sizeof(mac)) { + perror("cannot get dev hwaddr and cannot read random"); + return NULL; + } + close(rand); + } else { + strncpy((char *)mac, (const char *)ifr.ifr_ifru.ifru_addr.sa_data, + sizeof(mac)); + } + + /* https://tools.ietf.org/html/rfc4193 + * + * 3.2.2. Sample Code for Pseudo-Random Global ID Algorithm + */ + + /* + * 1) Obtain the current time of day in 64-bit NTP format [NTP]. + */ + time(&tv); + ntv.time.tv_sec = tv; + // TODO: check NTP format + ntv.time.tv_usec = 0; + + /* + * 2) Obtain an EUI-64 identifier from the system running this + * algorithm. If an EUI-64 does not exist, one can be created from + * a 48-bit MAC address as specified in [ADDARCH]. If an EUI-64 + * cannot be obtained or created, a suitably unique identifier, + * local to the node, should be used (e.g., system serial number). + */ + eui64[8] = mac[0]; + eui64[9] = mac[1]; + eui64[10] = mac[2]; + eui64[11] = 0xff; + eui64[12] = 0xfe; + eui64[13] = mac[3]; + eui64[14] = mac[4]; + eui64[15] = mac[5]; + + /* + * 3) Concatenate the time of day with the system-specific identifier + * in order to create a key. + */ + memcpy(&eui64[0], (void *)&ntv.time.tv_sec, 4); + memcpy(&eui64[4], (void *)&ntv.time.tv_usec, 4); + + /* + * 4) Compute an SHA-1 digest on the key as specified in [FIPS, SHA1]; + * the resulting value is 160 bits. + */ + SHA1(eui64, sizeof(eui64), hash); + + /* + * 5) Use the least significant 40 bits as the Global ID. + */ + int i = 0, j = 0; + for (; j < 5; i += 2, j++) { + sprintf(&tmp[i], "%02x", hash[j]); + if (j > 0 && (j % 2)) { + tmp[i + 2] = ':'; + i++; + } + } + + /* + * 6) Concatenate FC00::/7, the L bit set to 1, and the 40-bit Global + * ID to create a Local IPv6 address prefix. + */ + + snprintf(id, sizeof(id), "fd00:%s::/64", tmp); + + return id; +} + +static int configure_network6(const char *tapname, + struct slirp4netns_config *cfg) +{ + struct rtentry route; + struct ifreq ifr; + struct in6_ifreq ifr6; + struct sockaddr_in6 sai; + int sockfd; + const char *id; + + sockfd = socket(AF_INET6, SOCK_DGRAM, 0); + if (sockfd < 0) { + perror("cannot create socket"); + return -1; + } + + memset(&ifr, 0, sizeof(ifr)); + ifr.ifr_flags = IFF_UP | IFF_RUNNING; + strncpy(ifr.ifr_name, tapname, sizeof(ifr.ifr_name) - 1); + + if (ioctl(sockfd, SIOCGIFHWADDR, &ifr) < 0) { + perror("cannot get dev hwaddr"); + return -1; + } + + if (ioctl(sockfd, SIOGIFINDEX, &ifr) < 0) { + perror("cannot get dev index"); + return -1; + } + + memset(&sai, 0, sizeof(struct sockaddr)); + sai.sin6_family = AF_INET6; + sai.sin6_addr = cfg->recommended_vguest6; + sai.sin6_port = 0; + + memcpy((char *)&ifr6.ifr6_addr, &sai.sin6_addr, sizeof(struct in6_addr)); + + ifr6.ifr6_ifindex = ifr.ifr_ifindex; + ifr6.ifr6_prefixlen = 64; + + if (ioctl(sockfd, SIOCSIFADDR, &ifr6) < 0) { + perror("cannot set device address"); + return -1; + } + + return 0; +} + static int child(int sock, pid_t target_pid, bool do_config_network, const char *tapname, char *netns_path, char *userns_path, struct slirp4netns_config *cfg) @@ -219,6 +392,9 @@ static int child(int sock, pid_t target_pid, bool do_config_network, if (do_config_network && configure_network(tapname, cfg) < 0) { return -1; } + if (do_config_network && configure_network6(tapname, cfg) < 0) { + return -1; + } if (sendfd(sock, tapfd) < 0) { close(tapfd); close(sock); @@ -266,6 +442,7 @@ static int parent(int sock, int ready_fd, int exit_fd, const char *api_socket, struct slirp4netns_config *cfg, pid_t target_pid) { int rc, tapfd; + char ipv6[INET6_ADDRSTRLEN]; struct in_addr vdhcp_end = { #define NB_BOOTP_CLIENTS 16 /* NB_BOOTP_CLIENTS is hard-coded to 16 in libslirp: @@ -287,6 +464,18 @@ static int parent(int sock, int ready_fd, int exit_fd, const char *api_socket, printf("* DHCP begin: %s\n", inet_ntoa(cfg->vdhcp_start)); printf("* DHCP end: %s\n", inet_ntoa(vdhcp_end)); printf("* Recommended IP: %s\n", inet_ntoa(cfg->recommended_vguest)); + if (cfg->enable_ipv6) { + inet_ntop(AF_INET6, &cfg->vnetwork6, ipv6, INET6_ADDRSTRLEN); + printf("* IPv6 Network: %s\n", ipv6); + inet_ntop(AF_INET6, &cfg->vnetmask6, ipv6, INET6_ADDRSTRLEN); + printf("* IPv6 Netmask: %s\n", ipv6); + inet_ntop(AF_INET6, &cfg->vhost6, ipv6, INET6_ADDRSTRLEN); + printf("* IPv6 Gateway: %s\n", ipv6); + inet_ntop(AF_INET6, &cfg->vnameserver6, ipv6, INET6_ADDRSTRLEN); + printf("* IPv6 DNS: %s\n", ipv6); + inet_ntop(AF_INET6, &cfg->recommended_vguest6, ipv6, INET6_ADDRSTRLEN); + printf("* IPv6 Recommended IP: %s\n", ipv6); + } if (api_socket != NULL) { printf("* API Socket: %s\n", api_socket); } @@ -357,6 +546,9 @@ static void usage(const char *argv0) printf( "--cidr=CIDR specify network address CIDR (default=%s)\n", DEFAULT_CIDR); + printf( + "--cidr6=CIDR specify network address CIDR (default=%s)\n", + DEFAULT_CIDR6); printf("--disable-host-loopback prohibit connecting to 127.0.0.1:* on the " "host namespace\n"); /* v0.4.0 */ @@ -407,6 +599,7 @@ static void version() struct options { char *tapname; // argv[2] char *cidr; // --cidr + char *cidr6; // --cidr char *api_socket; // -a char *netns_type; // argv[1] char *netns_path; // --netns-path @@ -420,6 +613,7 @@ struct options { bool do_config_network; // -c bool disable_host_loopback; // --disable-host-loopback bool enable_ipv6; // -6 + bool ipv6_random; // --ipv6-random bool enable_sandbox; // --enable-sandbox bool enable_seccomp; // --enable-seccomp bool disable_dns; // --disable-dns @@ -443,6 +637,10 @@ static void options_destroy(struct options *options) free(options->cidr); options->cidr = NULL; } + if (options->cidr6 != NULL) { + free(options->cidr6); + options->cidr6 = NULL; + } if (options->api_socket != NULL) { free(options->api_socket); options->api_socket = NULL; @@ -481,13 +679,16 @@ static void parse_args(int argc, char *const argv[], struct options *options) int opt; char *strtol_e = NULL; char *optarg_cidr = NULL; + char *optarg_cidr6 = NULL; char *optarg_netns_type = NULL; char *optarg_userns_path = NULL; char *optarg_api_socket = NULL; char *optarg_outbound_addr = NULL; char *optarg_outbound_addr6 = NULL; char *optarg_macaddress = NULL; -#define CIDR -42 +#define CIDR -40 +#define CIDR6 -41 +#define IPV6_RANDOM -42 #define DISABLE_HOST_LOOPBACK -43 #define NETNS_TYPE -44 #define USERNS_PATH -45 @@ -507,12 +708,14 @@ static void parse_args(int argc, char *const argv[], struct options *options) { "ready-fd", required_argument, NULL, 'r' }, { "mtu", required_argument, NULL, 'm' }, { "cidr", required_argument, NULL, CIDR }, + { "cidr6", required_argument, NULL, CIDR6 }, { "disable-host-loopback", no_argument, NULL, DISABLE_HOST_LOOPBACK }, { "no-host-loopback", no_argument, NULL, _DEPRECATED_NO_HOST_LOOPBACK }, { "netns-type", required_argument, NULL, NETNS_TYPE }, { "userns-path", required_argument, NULL, USERNS_PATH }, { "api-socket", required_argument, NULL, 'a' }, { "enable-ipv6", no_argument, NULL, '6' }, + { "ipv6-random", no_argument, NULL, IPV6_RANDOM }, { "enable-sandbox", no_argument, NULL, ENABLE_SANDBOX }, { "create-sandbox", no_argument, NULL, _DEPRECATED_CREATE_SANDBOX }, { "enable-seccomp", no_argument, NULL, ENABLE_SECCOMP }, @@ -560,6 +763,9 @@ static void parse_args(int argc, char *const argv[], struct options *options) case CIDR: optarg_cidr = optarg; break; + case CIDR6: + optarg_cidr6 = optarg; + break; case _DEPRECATED_NO_HOST_LOOPBACK: // There was no tagged release with support for --no-host-loopback. // So no one will be affected by removal of --no-host-loopback. @@ -602,6 +808,9 @@ static void parse_args(int argc, char *const argv[], struct options *options) options->enable_ipv6 = true; printf("WARNING: Support for IPv6 is experimental\n"); break; + case IPV6_RANDOM: + options->ipv6_random = true; + break; case 'h': usage(argv[0]); exit(EXIT_SUCCESS); @@ -632,6 +841,9 @@ static void parse_args(int argc, char *const argv[], struct options *options) if (optarg_cidr != NULL) { options->cidr = strdup(optarg_cidr); } + if (optarg_cidr6 != NULL) { + options->cidr6 = strdup(optarg_cidr6); + } if (optarg_netns_type != NULL) { options->netns_type = strdup(optarg_netns_type); } @@ -657,6 +869,7 @@ static void parse_args(int argc, char *const argv[], struct options *options) } } #undef CIDR +#undef CIDR6 #undef DISABLE_HOST_LOOPBACK #undef NETNS_TYPE #undef USERNS_PATH @@ -767,6 +980,77 @@ static int parse_cidr(struct in_addr *network, struct in_addr *netmask, return rc; } +static int parse_cidr6(struct in6_addr *network, struct in6_addr *netmask, + const char *cidr) +{ + int rc = 0; + regex_t r; + regmatch_t matches[5]; + size_t nmatch = sizeof(matches) / sizeof(matches[0]); + const char *cidr_regex = "^((([a-fA-F0-9]{1,4}):){1,4}):/([0-9]{1,3})"; + char snetwork[INET6_ADDRSTRLEN], snetwork_end[INET6_ADDRSTRLEN], + sprefix[INET6_ADDRSTRLEN]; + int prefix; + const char *random; + int i; + + rc = regcomp(&r, cidr_regex, REG_EXTENDED); + if (rc != 0) { + fprintf(stderr, "internal regex error\n"); + rc = -1; + goto finish; + } + rc = regexec(&r, cidr, nmatch, matches, 0); + if (rc != 0) { + fprintf(stderr, "invalid CIDR: %s\n", cidr); + rc = -1; + goto finish; + } + rc = from_regmatch(snetwork, sizeof(snetwork), matches[1], cidr); + if (rc < 0) { + fprintf(stderr, "invalid CIDR: %s\n", cidr); + goto finish; + } + rc = from_regmatch(sprefix, sizeof(sprefix), matches[4], cidr); + if (rc < 0) { + fprintf(stderr, "invalid CIDR: %s\n", cidr); + goto finish; + } + strncat(snetwork, ":", 2); + if (inet_pton(AF_INET6, snetwork, network) != 1) { + fprintf(stderr, "invalid network address: %s\n", snetwork); + rc = -1; + goto finish; + } + errno = 0; + prefix = strtoul(sprefix, NULL, 10); + if (errno) { + fprintf(stderr, "invalid prefix length: %s\n", sprefix); + rc = -1; + goto finish; + } + if (prefix < NETWORK_PREFIX_MIN6 || prefix > NETWORK_PREFIX_MAX6) { + fprintf(stderr, "prefix length needs to be %d-%d\n", + NETWORK_PREFIX_MIN6, NETWORK_PREFIX_MAX6); + rc = -1; + goto finish; + } + + for (i = 0; i < 4; i++, prefix -= 32) { + if (prefix >= 32) { + netmask->s6_addr32[i] = 0xffffffff; + } else if (prefix > 0) { + netmask->s6_addr32[i] = htonl(~((1 << (32 - prefix)) - 1)); + } else { + netmask->s6_addr32[i] = 0; + } + } + +finish: + regfree(&r); + return rc; +} + static int slirp4netns_config_from_cidr(struct slirp4netns_config *cfg, const char *cidr) { @@ -787,6 +1071,62 @@ static int slirp4netns_config_from_cidr(struct slirp4netns_config *cfg, return rc; } +static int slirp4netns_config_from_cidr6(struct slirp4netns_config *cfg, + const char *cidr) +{ + int rc; + char net[INET6_ADDRSTRLEN]; + char tmp[INET6_ADDRSTRLEN]; + char portion[5]; + rc = parse_cidr6(&cfg->vnetwork6, &cfg->vnetmask6, cidr); + if (rc < 0) { + goto finish; + } + + if (inet_ntop(AF_INET6, &cfg->vnetwork6, net, INET6_ADDRSTRLEN) == NULL) { + return -1; + } + +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) + + snprintf(portion, sizeof(portion), "%u", DEFAULT_VHOST_OFFSET6); + strncpy(tmp, net, sizeof(tmp)); + strncat(tmp, portion, + MAX(0, INET6_ADDRSTRLEN - strlen(tmp) - strlen(portion))); + if (inet_pton(AF_INET6, tmp, &cfg->vhost6) != 1) { + return -1; + } + + snprintf(portion, sizeof(portion), "%u", DEFAULT_VDHCPSTART_OFFSET6); + strncpy(tmp, net, sizeof(tmp)); + strncat(tmp, portion, + MAX(0, INET6_ADDRSTRLEN - strlen(tmp) - strlen(portion))); + if (inet_pton(AF_INET6, tmp, &cfg->vdhcp_start6) != 1) { + return -1; + } + + snprintf(portion, sizeof(portion), "%u", DEFAULT_VNAMESERVER_OFFSET6); + strncpy(tmp, net, sizeof(tmp)); + strncat(tmp, portion, + MAX(0, INET6_ADDRSTRLEN - strlen(tmp) - strlen(portion))); + if (inet_pton(AF_INET6, tmp, &cfg->vnameserver6) != 1) { + return -1; + } + + snprintf(portion, sizeof(portion), "%u", + DEFAULT_RECOMMENDED_VGUEST_OFFSET6); + strncpy(tmp, net, sizeof(tmp)); + strncat(tmp, portion, + MAX(0, INET6_ADDRSTRLEN - strlen(tmp) - strlen(portion))); + if (inet_pton(AF_INET6, tmp, &cfg->recommended_vguest6) != 1) { + return -1; + } +#undef MAX + +finish: + return rc; +} + static int get_interface_addr(const char *interface, int af, void *addr) { struct ifaddrs *ifaddr, *ifa; @@ -863,6 +1203,25 @@ static int slirp4netns_config_from_options(struct slirp4netns_config *cfg, cfg->enable_sandbox = opt->enable_sandbox; cfg->enable_seccomp = opt->enable_seccomp; + if (cfg->enable_ipv6) { + const char *cidr = opt->cidr6; + if (cidr == NULL) { + cidr = DEFAULT_CIDR6; + if (opt->ipv6_random) { + cidr = pseudo_random_global_id("tap0"); + if (cidr == NULL) { + fprintf(stderr, "cannot create pseudo random global id\n"); + rc = -1; + goto finish; + } + } + } + rc = slirp4netns_config_from_cidr6(cfg, cidr); + if (rc < 0) { + goto finish; + } + } + #if SLIRP_CONFIG_VERSION_MAX >= 2 cfg->enable_outbound_addr = false; cfg->enable_outbound_addr6 = false; diff --git a/slirp4netns.1 b/slirp4netns.1 index b72bfe4..c3c392d 100644 --- a/slirp4netns.1 +++ b/slirp4netns.1 @@ -1,4 +1,3 @@ -.nh .TH SLIRP4NETNS 1 "June 2021" "Rootless Containers" "User Commands" .SH NAME @@ -78,6 +77,14 @@ API socket path \fB\fC\-\-cidr\fR (since v0.3.0) specify CIDR, e.g. 10.0.2.0/24 +.PP +\fB\fC\-\-cidr6\fR (since v1.1.12, EXPERIMENTAL) +specify IPv6 CIDR, e.g. fd00::/64 + +.PP +\fB\fC\-\-ipv6\-random\fR (since v1.1.12, EXPERIMENTAL) +generate a random local IPv6 range + .PP \fB\fC\-\-disable\-host\-loopback\fR (since v0.3.0) prohibit connecting to 127.0.0.1:* on the host namespace @@ -199,7 +206,7 @@ starting slirp, MTU=65520 link/ether c2:28:0c:0e:29:06 brd ff:ff:ff:ff:ff:ff inet 10.0.2.100/24 brd 10.0.2.255 scope global tap0 valid\_lft forever preferred\_lft forever - inet6 fe80::c028:cff:fe0e:2906/64 scope link + inet6 fe80::c028:cff:fe0e:2906/64 scope link valid\_lft forever preferred\_lft forever (namespace)$ echo "nameserver 10.0.2.3" > /tmp/resolv.conf (namespace)$ mount \-\-bind /tmp/resolv.conf /etc/resolv.conf @@ -311,7 +318,35 @@ slirp4netns can provide QMP\-like API server over an UNIX socket file: .RS .nf -(namespace)$ json='{"execute": "add\_hostfwd", "arguments": {"proto": "tcp", "host\_addr": "0.0.0.0", "host\_port": 8080, "guest\_addr": "10.0.2.100", "guest\_port": 80}}' +(namespace)$ json='{"execute": "add\_hostfwd", "arguments": {"proto": "tcp4", "host\_addr": "0.0.0.0", "host\_port": 8080, "guest\_addr": "10.0.2.100", "guest\_port": 80}}' +(namespace)$ echo \-n $json | nc \-U /tmp/slirp4netns.sock +{"return": {"id": 42}} + +.fi +.RE + +.PP +\fB\fCadd\_hostfwd\fR: Expose a port (IPv6 only) + +.PP +.RS + +.nf +(namespace)$ json='{"execute": "add\_hostfwd", "arguments": {"proto": "tcp6", "host\_addr": "::", "host\_port": 8080, "guest\_port": 80}}' +(namespace)$ echo \-n $json | nc \-U /tmp/slirp4netns.sock +{"return": {"id": 42}} + +.fi +.RE + +.PP +\fB\fCadd\_hostfwd\fR: Expose a port (IPv4 and IPv6) + +.PP +.RS + +.nf +(namespace)$ json='{"execute": "add\_hostfwd", "arguments": {"proto": "tcp", "host\_addr": "0.0.0.0", "host\_port": 8080, "guest\_port": 80}}' (namespace)$ echo \-n $json | nc \-U /tmp/slirp4netns.sock {"return": {"id": 42}} @@ -324,6 +359,9 @@ If \fB\fChost\_addr\fR is not specified, then it defaults to "0.0.0.0". .PP If \fB\fCguest\_addr\fR is not specified, then it will be set to the default address that corresponds to \fB\fC\-\-configure\fR\&. +.PP +To expose both IPv4 and IPv6 be sure to configure with \-\-enable\-ipv6 and set the \fB\fChost\_addr\fR to either "0.0.0.0" or "::". + .PP \fB\fClist\_hostfwd\fR: List exposed ports @@ -333,7 +371,7 @@ If \fB\fCguest\_addr\fR is not specified, then it will be set to the default add .nf (namespace)$ json='{"execute": "list\_hostfwd"}' (namespace)$ echo \-n $json | nc \-U /tmp/slirp4netns.sock -{"return": {"entries": [{"id": 42, "proto": "tcp", "host\_addr": "0.0.0.0", "host\_port": 8080, "guest\_addr": "10.0.2.100", "guest\_port": 80}]}} +{"return": {"entries": [{"id": 42, "proto": "tcp", "host\_addr": "0.0.0.0", "host\_addr6": "::", "host\_port": 8080, "guest\_addr": "10.0.2.100", "guest\_addr6": "fd00::100", "guest\_port": 80}]}} .fi .RE diff --git a/slirp4netns.1.md b/slirp4netns.1.md index f60b6b7..dbb211a 100644 --- a/slirp4netns.1.md +++ b/slirp4netns.1.md @@ -55,6 +55,12 @@ API socket path `--cidr` (since v0.3.0) specify CIDR, e.g. 10.0.2.0/24 +`--cidr6` (since v1.1.12, EXPERIMENTAL) +specify IPv6 CIDR, e.g. fd00::/64 + +`--ipv6-random` (since v1.1.12, EXPERIMENTAL) +generate a random local IPv6 range + `--disable-host-loopback` (since v0.3.0) prohibit connecting to 127.0.0.1:\* on the host namespace @@ -139,7 +145,7 @@ starting slirp, MTU=65520 link/ether c2:28:0c:0e:29:06 brd ff:ff:ff:ff:ff:ff inet 10.0.2.100/24 brd 10.0.2.255 scope global tap0 valid_lft forever preferred_lft forever - inet6 fe80::c028:cff:fe0e:2906/64 scope link + inet6 fe80::c028:cff:fe0e:2906/64 scope link valid_lft forever preferred_lft forever (namespace)$ echo "nameserver 10.0.2.3" > /tmp/resolv.conf (namespace)$ mount --bind /tmp/resolv.conf /etc/resolv.conf @@ -210,7 +216,23 @@ slirp4netns can provide QMP-like API server over an UNIX socket file: `add_hostfwd`: Expose a port (IPv4 only) ```console -(namespace)$ json='{"execute": "add_hostfwd", "arguments": {"proto": "tcp", "host_addr": "0.0.0.0", "host_port": 8080, "guest_addr": "10.0.2.100", "guest_port": 80}}' +(namespace)$ json='{"execute": "add_hostfwd", "arguments": {"proto": "tcp4", "host_addr": "0.0.0.0", "host_port": 8080, "guest_addr": "10.0.2.100", "guest_port": 80}}' +(namespace)$ echo -n $json | nc -U /tmp/slirp4netns.sock +{"return": {"id": 42}} +``` + +`add_hostfwd`: Expose a port (IPv6 only) + +```console +(namespace)$ json='{"execute": "add_hostfwd", "arguments": {"proto": "tcp6", "host_addr": "::", "host_port": 8080, "guest_port": 80}}' +(namespace)$ echo -n $json | nc -U /tmp/slirp4netns.sock +{"return": {"id": 42}} +``` + +`add_hostfwd`: Expose a port (IPv4 and IPv6) + +```console +(namespace)$ json='{"execute": "add_hostfwd", "arguments": {"proto": "tcp", "host_addr": "0.0.0.0", "host_port": 8080, "guest_port": 80}}' (namespace)$ echo -n $json | nc -U /tmp/slirp4netns.sock {"return": {"id": 42}} ``` @@ -219,12 +241,14 @@ If `host_addr` is not specified, then it defaults to "0.0.0.0". If `guest_addr` is not specified, then it will be set to the default address that corresponds to `--configure`. +To expose both IPv4 and IPv6 be sure to configure with --enable-ipv6 and set the `host_addr` to either "0.0.0.0" or "::". + `list_hostfwd`: List exposed ports ```console (namespace)$ json='{"execute": "list_hostfwd"}' (namespace)$ echo -n $json | nc -U /tmp/slirp4netns.sock -{"return": {"entries": [{"id": 42, "proto": "tcp", "host_addr": "0.0.0.0", "host_port": 8080, "guest_addr": "10.0.2.100", "guest_port": 80}]}} +{"return": {"entries": [{"id": 42, "proto": "tcp", "host_addr": "0.0.0.0", "host_addr6": "::", "host_port": 8080, "guest_addr": "10.0.2.100", "guest_addr6": "fd00::100", "guest_port": 80}]}} ``` `remove_hostfwd`: Remove an exposed port @@ -243,7 +267,7 @@ Remarks: * A request must be less than 4096 bytes. * JSON responses may contain `error` instead of `return`. -# DEFINED NAMESPACE PATHS +# DEFINED NAMESPACE PATHS A user can define a network namespace path as opposed to the default process ID: ```console @@ -256,14 +280,14 @@ Additionally, a `--userns-path=PATH` argument can be included to override any us (host)$ slirp4netns --netns-type=path --userns-path=/path/to/userns /path/to/netns tap0 ``` -# OUTBOUND ADDRESSES -A user can defined preferred outbound ipv4 and ipv6 address in multi IP scenarios. +# OUTBOUND ADDRESSES +A user can defined preferred outbound ipv4 and ipv6 address in multi IP scenarios. ```console (host)$ slirp4netns --outbound-addr=10.2.2.10 --outbound-addr6=fe80::10 ... ``` -Optionally you can use interface names instead of ip addresses. +Optionally you can use interface names instead of ip addresses. ```console (host)$ slirp4netns --outbound-addr=eth0 --outbound-addr6=eth0 ... diff --git a/slirp4netns.c b/slirp4netns.c index 8748808..426c3a7 100644 --- a/slirp4netns.c +++ b/slirp4netns.c @@ -260,16 +260,16 @@ Slirp *create_slirp(void *opaque, struct slirp4netns_config *s4nn) cfg.vnetmask = s4nn->vnetmask; cfg.vhost = s4nn->vhost; cfg.in6_enabled = (int)(s4nn->enable_ipv6); - inet_pton(AF_INET6, "fd00::", &cfg.vprefix_addr6); + cfg.vprefix_addr6 = s4nn->vnetwork6; cfg.vprefix_len = 64; - inet_pton(AF_INET6, "fd00::2", &cfg.vhost6); + cfg.vhost6 = s4nn->vhost6; cfg.vhostname = NULL; cfg.tftp_server_name = NULL; cfg.tftp_path = NULL; cfg.bootfile = NULL; cfg.vdhcp_start = s4nn->vdhcp_start; cfg.vnameserver = s4nn->vnameserver; - inet_pton(AF_INET6, "fd00::3", &cfg.vnameserver6); + cfg.vnameserver6 = s4nn->vnameserver6; cfg.vdnssearch = NULL; cfg.vdomainname = NULL; cfg.if_mtu = s4nn->mtu; diff --git a/slirp4netns.h b/slirp4netns.h index 7c70cbc..b9bdacc 100644 --- a/slirp4netns.h +++ b/slirp4netns.h @@ -12,6 +12,13 @@ struct slirp4netns_config { struct in_addr vnameserver; // 10.0.2.3 struct in_addr recommended_vguest; // 10.0.2.100 (slirp itself is unaware of vguest) + struct in6_addr vnetwork6; // fd00:RANDOM:0 + struct in6_addr vnetmask6; // /64 + struct in6_addr vhost6; // fd00:RANDOM:2 + struct in6_addr vdhcp_start6; // fd00:RANDOM:15 + struct in6_addr vnameserver6; // fd00:RANDOM:3 + struct in6_addr + recommended_vguest6; // fdd00::RANDOM:100 (slirp itself is unaware of vguest) bool enable_ipv6; bool disable_host_loopback; bool enable_sandbox; diff --git a/tests/test-slirp4netns-cidr6.sh b/tests/test-slirp4netns-cidr6.sh new file mode 100755 index 0000000..9f17197 --- /dev/null +++ b/tests/test-slirp4netns-cidr6.sh @@ -0,0 +1,71 @@ +#!/bin/bash +set -xeuo pipefail + +. $(dirname $0)/common.sh + +unshare -r -n sleep infinity & +child=$! + +wait_for_network_namespace $child + +set +e +result=$(slirp4netns -c --enable-ipv6 --cidr6 64 $child tun11 2>&1) +set -e +echo $result | grep "invalid CIDR" + +set +e +result=$(slirp4netns -c --enable-ipv6 --cidr6 foo $child tun11 2>&1) +set -e +echo $result | grep "invalid CIDR" + +set +e +result=$(slirp4netns -c --enable-ipv6 --cidr6 fd00::2 $child tun11 2>&1) +set -e +echo $result | grep "invalid CIDR" + +set +e +result=$(slirp4netns -c --enable-ipv6 --cidr6 fd00::/129 $child tun11 2>&1) +set -e +echo $result | grep "prefix length needs to be 8-128" + +cidr=fd00:a1e1:1724:1a +slirp4netns -c $child --enable-ipv6 --cidr6 $cidr::/64 tun11 & +slirp_pid=$! + +wait_for_network_device $child tun11 + +function cleanup { + kill -9 $child $slirp_pid +} +trap cleanup EXIT + +result="$(nsenter --preserve-credentials -U -n --target=$child ip a show dev tun11)" +echo "$result" | grep -o '^\s*inet6 .*/..' | grep -F $cidr::100/64 + +cleanup +sleep 1 + +unshare -r -n sleep infinity & +child=$! + +slirp4netns -c $child --enable-ipv6 tun11 & +slirp_pid=$! + +wait_for_network_device $child tun11 + +result="$(nsenter --preserve-credentials -U -n --target=$child ip a show dev tun11)" +echo "$result" | grep -o '^\s*inet6 .*/..' | grep -F fd00::100/64 + +cleanup +sleep 1 + +unshare -r -n sleep infinity & +child=$! + +slirp4netns -c $child --enable-ipv6 --ipv6-random tun11 & +slirp_pid=$! + +wait_for_network_device $child tun11 + +result="$(nsenter --preserve-credentials -U -n --target=$child ip a show dev tun11)" +echo "$result" | grep -o '^\s*inet6 .*/..' | grep -vF fd00::100/64 diff --git a/tests/test-slirp4netns-hostfwd.sh b/tests/test-slirp4netns-hostfwd.sh new file mode 100755 index 0000000..0171b35 --- /dev/null +++ b/tests/test-slirp4netns-hostfwd.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -xeuo pipefail + +. $(dirname $0)/common.sh + +SLIRP_CONFIG_VERSION_MAX=$(slirp4netns -v | grep "SLIRP_CONFIG_VERSION_MAX: " | sed 's#SLIRP_CONFIG_VERSION_MAX: \(\)##') + +if [ "${SLIRP_CONFIG_VERSION_MAX:-0}" -lt 3 ]; then + printf "forwarding test requires SLIRP_CONFIG_VERSION_MAX 3 or newer. Test skipped..." + exit "$TEST_EXIT_CODE_SKIP" +fi + +if ! ip a | grep inet6 > /dev/null; then + printf "forwarding test requires ipv6 enabled on host. Test skipped..." + exit "$TEST_EXIT_CODE_SKIP" +fi + +host_port=8080 +guest_port=1080 +cidr=fd00:a1e1:1724:1a + +unshare -r -n socat tcp6-listen:$guest_port,reuseaddr,fork exec:cat,nofork & +child=$! + +wait_for_network_namespace $child + +tmpdir=$(mktemp -d /tmp/slirp4netns-bench.XXXXXXXXXX) +apisocket=${tmpdir}/slirp4netns.sock + +slirp4netns -c $child --enable-ipv6 --cidr6=$cidr::/64 --api-socket $apisocket tun11 & +slirp_pid=$! + +wait_for_network_device $child tun11 + +function cleanup() { + kill -9 $child $slirp_pid + rm -rf $tmpdir +} +trap cleanup EXIT + +set +e +result=$(cat /dev/zero | ncat -U $apisocket || true) +set set -e +echo $result | jq .error.desc | grep "bad request: too large message" + +set -e +result=$(echo '{"execute": "add_hostfwd", "arguments":{"proto": "tcp","host_port":'$host_port',"guest_port":'$guest_port'}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +id=$(echo $result | jq .return.id) +[[ $id == 1 ]] + +result=$(echo '{"execute": "list_hostfwd"}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +[[ $(echo $result | jq .entries[0].id) == $id ]] +[[ $(echo $result | jq .entries[0].proto) == '"tcp"' ]] +[[ $(echo $result | jq .entries[0].host_addr) == '"0.0.0.0"' ]] +[[ $(echo $result | jq .entries[0].host_addr6) == '"::"' ]] +[[ $(echo $result | jq .entries[0].host_port) == $host_port ]] +[[ $(echo $result | jq .entries[0].guest_addr) == '"10.0.2.100"' ]] +[[ $(echo $result | jq .entries[0].guest_addr6) == '"'$cidr'::100"' ]] +[[ $(echo $result | jq .entries[0].guest_port) == $guest_port ]] + +result=$(echo works | ncat -w 10 -6 localhost $host_port) +[[ "$result" == "works" ]] + +result=$(echo works | ncat -w 10 -4 localhost $host_port) +[[ "$result" == "works" ]] + +result=$(echo '{"execute": "remove_hostfwd", "arguments":{"id": 1}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] + +# see also: benchmarks/benchmark-iperf3-reverse.sh diff --git a/tests/test-slirp4netns-hostfwd4.sh b/tests/test-slirp4netns-hostfwd4.sh new file mode 100755 index 0000000..792c9c3 --- /dev/null +++ b/tests/test-slirp4netns-hostfwd4.sh @@ -0,0 +1,102 @@ +#!/bin/bash +set -xeuo pipefail + +. $(dirname $0)/common.sh + +SLIRP_CONFIG_VERSION_MAX=$(slirp4netns -v | grep "SLIRP_CONFIG_VERSION_MAX: " | sed 's#SLIRP_CONFIG_VERSION_MAX: \(\)##') + +if [ "${SLIRP_CONFIG_VERSION_MAX:-0}" -lt 3 ]; then + printf "forwarding test requires SLIRP_CONFIG_VERSION_MAX 3 or newer. Test skipped..." + exit "$TEST_EXIT_CODE_SKIP" +fi + +if ! ip a | grep inet6 > /dev/null; then + printf "forwarding test requires ipv6 enabled on host. Test skipped..." + exit "$TEST_EXIT_CODE_SKIP" +fi + +host_port=8081 +guest_port=1080 + +unshare -r -n socat tcp-listen:$guest_port,reuseaddr,fork exec:cat,nofork & +child=$! + +wait_for_network_namespace $child + +tmpdir=$(mktemp -d /tmp/slirp4netns-bench.XXXXXXXXXX) +apisocket=${tmpdir}/slirp4netns.sock + +slirp4netns -c $child --api-socket $apisocket tun11 & +slirp_pid=$! + +wait_for_network_device $child tun11 + +function cleanup() { + kill -9 $child $slirp_pid + rm -rf $tmpdir +} +trap cleanup EXIT + +set +e +result=$(cat /dev/zero | ncat -U $apisocket || true) +set set -e +echo $result | jq .error.desc | grep "bad request: too large message" + +set -e +result=$(echo '{"execute": "add_hostfwd", "arguments":{"proto": "tcp","host_port":'$host_port',"guest_port":'$guest_port'}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +id=$(echo $result | jq .return.id) +[[ $id == 1 ]] + +result=$(echo '{"execute": "list_hostfwd"}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +[[ $(echo $result | jq .entries[0].id) == $id ]] +[[ $(echo $result | jq .entries[0].proto) == '"tcp"' ]] +[[ $(echo $result | jq .entries[0].host_addr) == '"0.0.0.0"' ]] +[[ $(echo $result | jq .entries[0].host_addr6) == null ]] +[[ $(echo $result | jq .entries[0].host_port) == $host_port ]] +[[ $(echo $result | jq .entries[0].guest_addr) == '"10.0.2.100"' ]] +[[ $(echo $result | jq .entries[0].guest_addr6) == null ]] +[[ $(echo $result | jq .entries[0].guest_port) == $guest_port ]] + +result=$(echo works | ncat -w 10 -4 localhost $host_port) +[[ "$result" == "works" ]] + +result=$(echo works | ncat -w 10 -6 localhost $host_port || true) +[[ "$result" != "works" ]] + +result=$(echo '{"execute": "remove_hostfwd", "arguments":{"id": 1}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] + +result=$(echo '{"execute": "add_hostfwd", "arguments":{"proto": "tcp","host_addr":"127.0.0.1","host_port":'$host_port',"guest_port":'$guest_port'}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +id=$(echo $result | jq .return.id) +[[ $id == 2 ]] + +result=$(echo '{"execute": "list_hostfwd"}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +[[ $(echo $result | jq .entries[0].id) == $id ]] +[[ $(echo $result | jq .entries[0].proto) == '"tcp"' ]] +[[ $(echo $result | jq .entries[0].host_addr) == '"127.0.0.1"' ]] +[[ $(echo $result | jq .entries[0].host_addr6) == null ]] +[[ $(echo $result | jq .entries[0].host_port) == $host_port ]] +[[ $(echo $result | jq .entries[0].guest_addr) == '"10.0.2.100"' ]] +[[ $(echo $result | jq .entries[0].guest_addr6) == null ]] +[[ $(echo $result | jq .entries[0].guest_port) == $guest_port ]] + +result=$(echo works | ncat -w 10 -4 localhost $host_port) +[[ "$result" == "works" ]] + +result=$(echo works | ncat -w 10 -6 localhost $host_port || true) +[[ "$result" != "works" ]] + +result=$(echo '{"execute": "remove_hostfwd", "arguments":{"id": 2}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] + +result=$(echo '{"execute": "add_hostfwd", "arguments":{"proto": "tcp6","host_port":'$host_port',"guest_port":'$guest_port'}}' | ncat -U $apisocket || true) +echo $result | jq .error.desc | grep "bad arguments.proto" + +result=$(echo '{"execute": "add_hostfwd", "arguments":{"proto": "tcp","host_addr":"::1","host_port":'$host_port',"guest_port":'$guest_port'}}' | ncat -U $apisocket || true) +echo $result | jq .error.desc | grep "bad arguments.host_addr" + +# see also: benchmarks/benchmark-iperf3-reverse.sh diff --git a/tests/test-slirp4netns-hostfwd6.sh b/tests/test-slirp4netns-hostfwd6.sh new file mode 100755 index 0000000..8f6d67e --- /dev/null +++ b/tests/test-slirp4netns-hostfwd6.sh @@ -0,0 +1,103 @@ +#!/bin/bash +set -xeuo pipefail + +. $(dirname $0)/common.sh + +SLIRP_CONFIG_VERSION_MAX=$(slirp4netns -v | grep "SLIRP_CONFIG_VERSION_MAX: " | sed 's#SLIRP_CONFIG_VERSION_MAX: \(\)##') + +if [ "${SLIRP_CONFIG_VERSION_MAX:-0}" -lt 3 ]; then + printf "forwarding test requires SLIRP_CONFIG_VERSION_MAX 3 or newer. Test skipped..." + exit "$TEST_EXIT_CODE_SKIP" +fi + +if ! ip a | grep inet6 > /dev/null; then + printf "forwarding test requires ipv6 enabled on host. Test skipped..." + exit "$TEST_EXIT_CODE_SKIP" +fi + +host_port=8082 +guest_port=1080 +cidr=fd00:a1e1:1724:1a + +unshare -r -n socat tcp6-listen:$guest_port,reuseaddr,fork exec:cat,nofork & +child=$! + +wait_for_network_namespace $child + +tmpdir=$(mktemp -d /tmp/slirp4netns-bench.XXXXXXXXXX) +apisocket=${tmpdir}/slirp4netns.sock + +slirp4netns -c $child --enable-ipv6 --cidr6=$cidr::/64 --api-socket $apisocket tun11 & +slirp_pid=$! + +wait_for_network_device $child tun11 + +function cleanup() { + kill -9 $child $slirp_pid + rm -rf $tmpdir +} +trap cleanup EXIT + +set +e +result=$(cat /dev/zero | ncat -U $apisocket || true) +set set -e +echo $result | jq .error.desc | grep "bad request: too large message" + +set -e +result=$(echo '{"execute": "add_hostfwd", "arguments":{"proto": "tcp6","host_port":'$host_port',"guest_port":'$guest_port'}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +id=$(echo $result | jq .return.id) +[[ $id == 1 ]] + +result=$(echo '{"execute": "list_hostfwd"}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +[[ $(echo $result | jq .entries[0].id) == $id ]] +[[ $(echo $result | jq .entries[0].proto) == '"tcp"' ]] +[[ $(echo $result | jq .entries[0].host_addr) == null ]] +[[ $(echo $result | jq .entries[0].host_addr6) == '"::"' ]] +[[ $(echo $result | jq .entries[0].host_port) == $host_port ]] +[[ $(echo $result | jq .entries[0].guest_addr) == null ]] +[[ $(echo $result | jq .entries[0].guest_addr6) == '"'$cidr'::100"' ]] +[[ $(echo $result | jq .entries[0].guest_port) == $guest_port ]] + +result=$(echo works | ncat -w 10 -6 localhost $host_port) +[[ "$result" == "works" ]] + +result=$(echo works | ncat -w 10 -4 localhost $host_port || true) +[[ "$result" != "works" ]] + +result=$(echo '{"execute": "remove_hostfwd", "arguments":{"id": 1}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] + +result=$(echo '{"execute": "add_hostfwd", "arguments":{"proto": "tcp6","host_addr":"::1","host_port":'$host_port',"guest_port":'$guest_port'}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +id=$(echo $result | jq .return.id) +[[ $id == 2 ]] + +result=$(echo '{"execute": "list_hostfwd"}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +[[ $(echo $result | jq .entries[0].id) == $id ]] +[[ $(echo $result | jq .entries[0].proto) == '"tcp"' ]] +[[ $(echo $result | jq .entries[0].host_addr) == null ]] +[[ $(echo $result | jq .entries[0].host_addr6) == '"::1"' ]] +[[ $(echo $result | jq .entries[0].host_port) == $host_port ]] +[[ $(echo $result | jq .entries[0].guest_addr) == null ]] +[[ $(echo $result | jq .entries[0].guest_addr6) == '"'$cidr'::100"' ]] +[[ $(echo $result | jq .entries[0].guest_port) == $guest_port ]] + +result=$(echo works | ncat -w 10 -6 localhost $host_port) +[[ "$result" == "works" ]] + +result=$(echo works | ncat -w 10 -4 localhost $host_port || true) +[[ "$result" != "works" ]] + +result=$(echo '{"execute": "remove_hostfwd", "arguments":{"id": 2}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] + +result=$(echo '{"execute": "add_hostfwd", "arguments":{"proto": "tcp4","host_addr":"::1","host_port":'$host_port',"guest_port":'$guest_port'}}' | ncat -U $apisocket || true) +echo $result | jq .error.desc | grep "bad arguments.host_addr" + +result=$(echo '{"execute": "add_hostfwd", "arguments":{"proto": "tcp6","host_addr":"127.0.0.1","host_port":'$host_port',"guest_port":'$guest_port'}}' | ncat -U $apisocket || true) +echo $result | jq .error.desc | grep "bad arguments.host_addr" + +# see also: benchmarks/benchmark-iperf3-reverse.sh diff --git a/tests/test-slirp4netns-ipv6.sh b/tests/test-slirp4netns-ipv6.sh new file mode 100755 index 0000000..12299cd --- /dev/null +++ b/tests/test-slirp4netns-ipv6.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -xeuo pipefail + +. $(dirname $0)/common.sh + +SLIRP_CONFIG_VERSION_MAX=$(slirp4netns -v | grep "SLIRP_CONFIG_VERSION_MAX: " | sed 's#SLIRP_CONFIG_VERSION_MAX: \(\)##') + +if [ "${SLIRP_CONFIG_VERSION_MAX:-0}" -lt 3 ]; then + printf "ipv6 test requires SLIRP_CONFIG_VERSION_MAX 3 or newer. Test skipped..." + exit "$TEST_EXIT_CODE_SKIP" +fi + +if ! ip a | grep inet6 > /dev/null; then + printf "ipv6 test requires ipv6 enabled on host. Test skipped..." + exit "$TEST_EXIT_CODE_SKIP" +fi + +host_port=8083 +guest_port=80 +cidr=fd00:a1e1:1724:1a + +unshare -r -n sleep infinity & +child=$! + +wait_for_network_namespace $child + +tmpdir=$(mktemp -d /tmp/slirp4netns-bench.XXXXXXXXXX) +apisocket=${tmpdir}/slirp4netns.sock + +slirp4netns -c $child --enable-ipv6 --cidr6=$cidr::/64 --api-socket $apisocket tun11 & +slirp_pid=$! + +wait_for_network_device $child tun11 + +function cleanup() { + kill -9 $child $slirp_pid + rm -rf $tmpdir +} +trap cleanup EXIT + +set +e +result=$(cat /dev/zero | ncat -U $apisocket || true) +set set -e +echo $result | jq .error.desc | grep "bad request: too large message" + +set -e +result=$(echo '{"execute": "add_hostfwd", "arguments":{"proto": "tcp","host_port":'$host_port',"guest_port":'$guest_port'}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +id=$(echo $result | jq .return.id) +[[ $id == 1 ]] + +result=$(echo '{"execute": "list_hostfwd"}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] +[[ $(echo $result | jq .entries[0].id) == $id ]] +[[ $(echo $result | jq .entries[0].proto) == '"tcp"' ]] +[[ $(echo $result | jq .entries[0].host_addr) == '"0.0.0.0"' ]] +[[ $(echo $result | jq .entries[0].host_addr6) == '"::"' ]] +[[ $(echo $result | jq .entries[0].host_port) == $host_port ]] +[[ $(echo $result | jq .entries[0].guest_addr) == '"10.0.2.100"' ]] +[[ $(echo $result | jq .entries[0].guest_addr6) == '"'$cidr'::100"' ]] +[[ $(echo $result | jq .entries[0].guest_port) == $guest_port ]] + +result=$(echo '{"execute": "remove_hostfwd", "arguments":{"id": 1}}' | ncat -U $apisocket) +[[ $(echo $result | jq .error) == null ]] + +# see also: benchmarks/benchmark-iperf3-reverse.sh