Introduce the equivalent of the --api-socket option from slirp4netns: spawn a subshell to handle requests, netcat binds to a UNIX domain socket and jq parses messages. Three minor differences compared to slirp4netns: - IPv6 ports are forwarded too - error messages are not as specific, for example we don't tell apart malformed JSON requests from invalid parameters - host addresses are always 0.0.0.0 and ::1, pasta doesn't bind on specific addresses for different ports Signed-off-by: Stefano Brivio --- slirp4netns.sh | 189 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 182 insertions(+), 7 deletions(-) diff --git a/slirp4netns.sh b/slirp4netns.sh index 7c2188d..1784926 100755 --- a/slirp4netns.sh +++ b/slirp4netns.sh @@ -12,13 +12,20 @@ # # WARNING: Draft quality, not really tested # -# Copyright (c) 2021 Red Hat GmbH +# Copyright (c) 2021-2022 Red Hat GmbH # Author: Stefano Brivio PASTA_PID="$(mktemp)" PASTA_OPTS="-q --ipv4-only -a 10.0.2.0 -n 24 -g 10.0.2.2 -m 1500 --no-ndp --no-dhcpv6 --no-dhcp -P ${PASTA_PID}" PASTA="$(command -v ./pasta || command -v pasta || :)" +API_SOCKET= +API_DIR="$(mktemp -d)" +PORTS_DIR="${API_DIR}/ports" +FIFO_REQ="${API_DIR}/req.fifo" +FIFO_RESP="${API_DIR}/resp.fifo" +PORT_ARGS= + USAGE_RET=1 NOTFOUND_RET=127 @@ -112,6 +119,172 @@ opt() { esac } +# start() - Start pasta +start() { + ${PASTA} ${PASTA_OPTS} ${PORT_ARGS} ${ns_spec} + [ ${RFD} -ne 0 ] && echo "1" >&${RFD} || : +} + +# start() - Terminate pasta process +stop() { + kill $(cat ${PASTA_PID}) +} + +# api_insert() - Handle add_hostfwd request, update PORT_ARGS +# $1: Protocol, "tcp" or "udp" +# $2: Host port +# $3: Guest port +api_insert() { + __id= + __next_id=1 # slirp4netns starts from ID 1 + PORT_ARGS= + + for __entry in $(ls ${PORTS_DIR}); do + PORT_ARGS="${PORT_ARGS} $(cat "${PORTS_DIR}/${__entry}")" + + if [ -z "${__id}" ] && [ ${__entry} -ne ${__next_id} ]; then + __id=${__next_id} + fi + + __next_id=$((__next_id + 1)) + done + [ -z "${__id}" ] && __id=${__next_id} + + # Invalid ports are accepted by slirp4netns, store them as empty files. + # Unknown protocols aren't. + + case ${1} in + "tcp") opt="-t" ;; + "udp") opt="-u" ;; + *) + echo '{"error":{"desc":"bad request: add_hostfwd: bad arguments.proto"}}' + return + ;; + esac + + if [ ${2} -ge 0 ] && [ ${2} -le 65535 ] && \ + [ ${3} -ge 0 ] && [ ${3} -le 65535 ]; then + echo "${opt} ${2}:${3}" > "${PORTS_DIR}/${__id}" + PORT_ARGS="${PORT_ARGS} ${opt} ${2}:${3}" + else + :> "${PORTS_DIR}/${__id}" + fi + + echo "{ \"return\": {\"id\": ${__id}}}" + + NEED_RESTART=1 +} + +# api_list_one() - Print a single port forwarding entry in JSON +# $1: ID +# $2: protocol option, -t or -u +# $3: host port +# $4: guest port +api_list_one() { + [ "${2}" = "-t" ] && __proto="tcp" || __proto="udp" + + printf '{"id": %i, "proto": "%s", "host_addr": "0.0.0.0", "host_port": %i, "guest_addr": "%s", "guest_port": %i}' \ + "${1}" "${__proto}" "${3}" "${A4}" "${4}" +} + +# api_list() - Handle list_hostfwd request: list port forwarding entries in JSON +api_list() { + printf '{ "return": {"entries": [' + + __first=1 + for __entry in $(ls "${PORTS_DIR}"); do + [ ${__first} -eq 0 ] && printf ", " || __first=0 + IFS=' :' + api_list_one ${__entry} $(cat ${PORTS_DIR}/${__entry}) + unset IFS + done + + printf ']}}' +} + +# api_delete() - Handle remove_hostfwd request: delete entry, update PORT_ARGS +# $1: Entry ID -- caller *must* ensure it's a number +api_delete() { + if [ ! -f "${PORTS_DIR}/${1}" ]; then + printf '{"error":{"desc":"bad request: remove_hostfwd: bad arguments.id"}}' + return + fi + + rm "${PORTS_DIR}/${1}" + + PORT_ARGS= + for __entry in $(ls ${PORTS_DIR}); do + PORT_ARGS="${PORT_ARGS} $(cat "${PORTS_DIR}/${__entry}")" + done + + printf '{"return":{}}' + + NEED_RESTART=1 +} + +# api_error() - Print generic error in JSON +api_error() { + printf '{"error":{"desc":"bad request"}}' +} + +# api_handler() - Entry point for slirp4netns-like API socket handler +api_handler() { + trap 'exit 0' INT QUIT TERM + mkdir "${PORTS_DIR}" + + while true; do + mkfifo "${FIFO_REQ}" "${FIFO_RESP}" + + cat "${FIFO_RESP}" | nc -l -U "${API_SOCKET}" | \ + tee /dev/null >"${FIFO_REQ}" & READER_PID=${!} + + __req="$(dd count=1 2>/dev/null <${FIFO_REQ})" + + >&2 echo "apifd event" + >&2 echo "api_handler: got request: ${__req}" + + eval $(echo "${__req}" | + (jq -r 'to_entries | .[0] | + .key + "=" + (.value | @sh)' || + printf 'execute=ERR')) + + if [ "${execute}" != "list_hostfwd" ]; then + eval $(echo "${__req}" | + (jq -r '.arguments | to_entries | .[] | + .key + "=" + (.value | @sh)' || + printf 'execute=ERR')) + fi + + NEED_RESTART=0 + case ${execute} in + "add_hostfwd") + api_insert "${proto}" "${host_port}" "${guest_port}" + __restart=1 + ;; + "list_hostfwd") + api_list + ;; + "remove_hostfwd") + case ${id} in + ''|*[!0-9]*) api_error ;; + *) api_delete "${id}"; __restart=1 ;; + esac + ;; + *) + api_error + ;; + esac >"${FIFO_RESP}" + + kill ${READER_PID} + + rm "${FIFO_REQ}" "${FIFO_RESP}" + + [ ${NEED_RESTART} -eq 1 ] && { stop; start; } + done + + exit 0 +} + # usage() - Print slirpnetns(1) usage and exit indicating failure # $1: Invalid option name, if any usage() { @@ -177,7 +350,7 @@ while getopts ce:r:m:6a:hv-: OPT 2>/dev/null; do r | ready-fd) opt u32 RFD ;; m | mtu) opt mtu MTU && sub -m ${MTU} ;; 6 | enable-ipv6) V6=1 ;; - a | api-socket) opt str API ;; + a | api-socket) opt str API_SOCKET ;; cidr) opt net4 A4 M4 && sub -a ${A4} -n ${M4} ;; disable-host-loopback) add "--no-map-gw" && no_map_gw=1 ;; netns-type) : Autodetected ;; @@ -203,14 +376,15 @@ if [ ${v6} -eq 1 ]; then add "-a $(gen_addr6) -g fd00::2 -D fd00::3" fi -${PASTA} ${PASTA_OPTS} ${ns_spec} && \ - [ ${RFD} -ne 0 ] && echo "1" >&${RFD} +start +[ -n "${API_SOCKET}" ] && api_handler &2 echo "sent tapfd=5 for ${ifname}" +>&2 echo "received tapfd=5" cat << EOF -sent tapfd=5 for ${ifname} -received tapfd=5 Starting slirp * MTU: ${MTU} * Network: ${A4} @@ -219,6 +393,7 @@ Starting slirp * DNS: 10.0.2.3 * Recommended IP: 10.0.2.100 EOF +[ -n "${API_SOCKET}" ] && echo "* API socket: ${API_SOCKET}" if [ ${no_map_gw} -eq 0 ]; then echo "WARNING: 127.0.0.1:* on the host is accessible as 10.0.2.2 (set --disable-host-loopback to prohibit connecting to 127.0.0.1:*)" -- 2.34.1