From 806e8639c6cff29857d18ff313e03081e3fac562 Mon Sep 17 00:00:00 2001 From: Aaron Goodman Date: Wed, 12 Aug 2020 23:58:25 -0400 Subject: [PATCH] mwan3: use helper library for mwan3track Rather than using a special mwan3 user to manage mwan3track's tracking packets, this commit implements a small helper library to bind to device and to set a fwmark so that the tracking packets can be routed out of the correct interface. This provides a consistent method for binding to a device rather than relying on various packages potentially buggy implementations. For example: #8139 and #12836 This helper issue also allows for more tracking methods to be added even if they do not have a command line option to bind to device, such as iperf3 (eg #13050). Signed-off-by: Aaron Goodman --- net/mwan3/Makefile | 10 +- net/mwan3/files/lib/mwan3/common.sh | 97 +++++++++++ net/mwan3/files/lib/mwan3/mwan3.sh | 93 +--------- net/mwan3/files/usr/sbin/mwan3track | 51 +++--- net/mwan3/src/sockopt_wrap.c | 255 ++++++++++++++++++++++++++++ 5 files changed, 386 insertions(+), 120 deletions(-) create mode 100644 net/mwan3/src/sockopt_wrap.c diff --git a/net/mwan3/Makefile b/net/mwan3/Makefile index 65e2925c3b6da3..4850bfae438695 100644 --- a/net/mwan3/Makefile +++ b/net/mwan3/Makefile @@ -8,10 +8,11 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mwan3 -PKG_VERSION:=2.9.0 +PKG_VERSION:=2.10.0 PKG_RELEASE:=1 PKG_MAINTAINER:=Florian Eckert PKG_LICENSE:=GPL-2.0 +PKG_CONFIG_DEPENDS:=CONFIG_IPV6 include $(INCLUDE_DIR)/package.mk @@ -61,8 +62,13 @@ fi exit 0 endef +define Build/Compile + $(TARGET_CC) $(CFLAGS) $(LDFLAGS) $(FPIC) -shared -o $(PKG_BUILD_DIR)/libwrap_mwan3_sockopt.so.1.0 $(if $(CONFIG_IPV6),-DCONFIG_IPV6) $(PKG_BUILD_DIR)/sockopt_wrap.c -ldl +endef + define Package/mwan3/install -$(CP) ./files/* $(1) + $(CP) ./files/* $(1) + $(CP) $(PKG_BUILD_DIR)/libwrap_mwan3_sockopt.so.1.0 $(1)/lib/mwan3/ endef $(eval $(call BuildPackage,mwan3)) diff --git a/net/mwan3/files/lib/mwan3/common.sh b/net/mwan3/files/lib/mwan3/common.sh index 5766572bbe1cd6..536b89d09c9b59 100644 --- a/net/mwan3/files/lib/mwan3/common.sh +++ b/net/mwan3/files/lib/mwan3/common.sh @@ -8,8 +8,19 @@ get_uptime() { IP4="ip -4" IP6="ip -6" SCRIPTNAME="$(basename "$0")" + +MWAN3_STATUS_DIR="/var/run/mwan3" MWAN3TRACK_STATUS_DIR="/var/run/mwan3track" +MWAN3_INTERFACE_MAX="" + +MMX_MASK="" +MMX_DEFAULT="" +MMX_BLACKHOLE="" +MM_BLACKHOLE="" + +MMX_UNREACHABLE="" +MM_UNREACHABLE="" MAX_SLEEP=$(((1<<31)-1)) LOG() @@ -21,6 +32,21 @@ LOG() [ "$facility" = "debug" ] && return logger -t "$SCRIPTNAME[$$]" -p $facility "$*" } + +mwan3_get_true_iface() +{ + local family V + _true_iface=$2 + config_get family "$2" family ipv4 + if [ "$family" = "ipv4" ]; then + V=4 + elif [ "$family" = "ipv6" ]; then + V=6 + fi + ubus call "network.interface.${2}_${V}" status &>/dev/null && _true_iface="${2}_${V}" + export "$1=$_true_iface" +} + mwan3_get_src_ip() { local family _src_ip true_iface device addr_cmd default_ip IP sed_str @@ -52,3 +78,74 @@ mwan3_get_src_ip() fi export "$1=$_src_ip" } + +mwan3_init() +{ + local bitcnt + local mmdefault + + [ -d $MWAN3_STATUS_DIR ] || mkdir -p $MWAN3_STATUS_DIR/iface_state + + # mwan3's MARKing mask (at least 3 bits should be set) + if [ -e "${MWAN3_STATUS_DIR}/mmx_mask" ]; then + MMX_MASK=$(cat "${MWAN3_STATUS_DIR}/mmx_mask") + MWAN3_INTERFACE_MAX=$(uci_get_state mwan3 globals iface_max) + else + config_load mwan3 + config_get MMX_MASK globals mmx_mask '0x3F00' + echo "$MMX_MASK"| tr 'A-F' 'a-f' > "${MWAN3_STATUS_DIR}/mmx_mask" + LOG debug "Using firewall mask ${MMX_MASK}" + + bitcnt=$(mwan3_count_one_bits MMX_MASK) + mmdefault=$(((1<>bit_msk)&1)) = "1" ]; then + if [ $((($1>>bit_val)&1)) = "1" ]; then + result=$((result|(1< /dev/null NO_IPV6=$? @@ -78,20 +69,6 @@ mwan3_update_iface_to_table() config_foreach update_table interface } -mwan3_get_true_iface() -{ - local family V - _true_iface=$2 - config_get family "$iface" family ipv4 - if [ "$family" = "ipv4" ]; then - V=4 - elif [ "$family" = "ipv6" ]; then - V=6 - fi - ubus call "network.interface.${iface}_${V}" status &>/dev/null && _true_iface="${iface}_${V}" - export "$1=$_true_iface" -} - mwan3_route_line_dev() { # must have mwan3 config already loaded @@ -127,63 +104,6 @@ mwan3_count_one_bits() echo $count } -# maps the 1st parameter so it only uses the bits allowed by the bitmask (2nd parameter) -# which means spreading the bits of the 1st parameter to only use the bits that are set to 1 in the 2nd parameter -# 0 0 0 0 0 1 0 1 (0x05) 1st parameter -# 1 0 1 0 1 0 1 0 (0xAA) 2nd parameter -# 1 0 1 result -mwan3_id2mask() -{ - local bit_msk bit_val result - bit_val=0 - result=0 - for bit_msk in $(seq 0 31); do - if [ $((($2>>bit_msk)&1)) = "1" ]; then - if [ $((($1>>bit_val)&1)) = "1" ]; then - result=$((result|(1< "${MWAN3_STATUS_DIR}/mmx_mask" - LOG debug "Using firewall mask ${MMX_MASK}" - - bitcnt=$(mwan3_count_one_bits MMX_MASK) - mmdefault=$(((1<&1) || LOG error "set_connected_ipv6: $error" } @@ -382,15 +298,10 @@ mwan3_set_general_iptables() -p ipv6-icmp \ -m icmp6 --icmpv6-type 137 \ -j RETURN - # do not mangle outgoing echo request - mwan3_push_update -A mwan3_hook \ - -m set --match-set mwan3_source_v6 src \ - -p ipv6-icmp \ - -m icmp6 --icmpv6-type 128 \ - -j RETURN fi mwan3_push_update -A mwan3_hook \ + -m mark --mark 0x0/$MMX_MASK \ -j CONNMARK --restore-mark --nfmask "$MMX_MASK" --ctmask "$MMX_MASK" mwan3_push_update -A mwan3_hook \ -m mark --mark 0x0/$MMX_MASK \ diff --git a/net/mwan3/files/usr/sbin/mwan3track b/net/mwan3/files/usr/sbin/mwan3track index 833a3916d23fa3..09321f05446ec5 100755 --- a/net/mwan3/files/usr/sbin/mwan3track +++ b/net/mwan3/files/usr/sbin/mwan3track @@ -11,6 +11,12 @@ PING="/bin/ping" IFDOWN_EVENT=0 IFUP_EVENT=0 +mwan3_init + +WRAP(){ + FAMILY=$FAMILY DEVICE=$DEVICE SRCIP=$SRC_IP FWMARK=$MMX_DEFAULT LD_PRELOAD=/lib/mwan3/libwrap_mwan3_sockopt.so.1.0 $* +} + clean_up() { LOG notice "Stopping mwan3track for interface \"${INTERFACE}\"" [ -n "$SLEEP_PID" ] && kill "$SLEEP_PID" @@ -68,6 +74,12 @@ validate_track_method() { esac } +validate_wrap() { + [ -x /lib/mwan3/libwrap_mwan3_sockopt.so.1.0 ] && return + LOG error "Missing libwrap_mwan3_sockopt. Please reinstall mwan3." && + exit 1 +} + disconnected() { STATUS='offline' echo "offline" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/STATUS @@ -116,17 +128,6 @@ firstconnect() { mwan3_get_src_ip SRC_IP $true_iface - # pinging IPv6 hosts with an interface is troublesome - # https://bugs.openwrt.org/index.php?do=details&task_id=2897 - # https://bugs.openwrt.org/index.php?do=details&task_id=2167 - # https://forum.openwrt.org/t/ping-and-traceroute-failing-for-eth0-3-on-ipv6/44680/11 - # so use the IP address of the interface - if [ "$family" = "ipv6" ]; then - SOURCE="$SRC_IP" - else - SOURCE="$DEVICE" - fi - LOG debug "firstconnect: called on $INTERFACE/$true_iface ($DEVICE). Status is $STATUS. SRC_IP is $SRC_IP" STARTED=1 @@ -152,7 +153,8 @@ main() { local recovery_interval down up size local keep_failure_interval check_quality failure_latency local recovery_latency failure_loss recovery_loss - local max_ttl httping_ssl family track_ips + + local max_ttl httping_ssl track_ips INTERFACE=$1 STATUS="" @@ -164,6 +166,7 @@ main() { trap if_up USR2 config_load mwan3 + config_get FAMILY $INTERFACE family ipv4 config_get track_method $INTERFACE track_method ping config_get_bool httping_ssl $INTERFACE httping_ssl 0 validate_track_method $track_method $SRC_IP || { @@ -213,18 +216,12 @@ main() { if [ $host_up_count -lt $reliability ]; then case "$track_method" in ping) - # pinging IPv6 hosts with an interface is troublesome - # https://bugs.openwrt.org/index.php?do=details&task_id=2897 - # so get the IP address of the interface and use that instead - if [ $check_quality -eq 0 ]; then - $PING -${family#ipv} -I ${SOURCE} -c $count -W $timeout -s $size -t $max_ttl -q $track_ip &> /dev/null + WRAP $PING -${FAMILY#ipv} -c $count -W $timeout -s $size -t $max_ttl -q $track_ip &> /dev/null result=$? else - ping_result_raw="$($PING -${family#ipv} -I ${SOURCE} -c $count -W $timeout -s $size -t $max_ttl -q $track_ip 2>/dev/null)" - + ping_result_raw="$(WRAP $PING -${FAMILY#ipv} -c $count -W $timeout -s $size -t $max_ttl -q $track_ip 2>/dev/null)" ping_status=$? - ping_result=$(echo "$ping_result_raw" | tail -n2) loss="$(echo "$ping_result" | grep "packet loss" | cut -d "," -f3 | awk '{print $1}' | sed -e 's/%//')" if [ "$ping_status" -ne 0 ] || [ "$loss" -eq 100 ]; then @@ -236,28 +233,28 @@ main() { fi ;; arping) - arping -I $DEVICE -c $count -w $timeout -q $track_ip &> /dev/null + WRAP arping -I $DEVICE -c $count -w $timeout -q $track_ip &> /dev/null result=$? ;; httping) if [ "$httping_ssl" -eq 1 ]; then - httping -y $SRC_IP -c $count -t $timeout -q "https://$track_ip" &> /dev/null + WRAP httping -c $count -t $timeout -q "https://$track_ip" &> /dev/null else - httping -y $SRC_IP -c $count -t $timeout -q "http://$track_ip" &> /dev/null + WRAP httping -c $count -t $timeout -q "http://$track_ip" &> /dev/null fi result=$? ;; nping-tcp) - result=$(nping -e $DEVICE -c $count $track_ip --tcp | grep Lost | awk '{print $12}') + result=$(WRAP nping -c $count $track_ip --tcp | grep Lost | awk '{print $12}') ;; nping-udp) - result=$(nping -e $DEVICE -c $count $track_ip --udp | grep Lost | awk '{print $12}') + result=$(WRAP nping -c $count $track_ip --udp | grep Lost | awk '{print $12}') ;; nping-icmp) - result=$(nping -e $DEVICE -c $count $track_ip --icmp | grep Lost | awk '{print $12}') + result=$(WRAP nping -c $count $track_ip --icmp | grep Lost | awk '{print $12}') ;; nping-arp) - result=$(nping -e $DEVICE -c $count $track_ip --arp | grep Lost | awk '{print $12}') + result=$(WRAP nping -c $count $track_ip --arp | grep Lost | awk '{print $12}') ;; esac if [ $check_quality -eq 0 ]; then diff --git a/net/mwan3/src/sockopt_wrap.c b/net/mwan3/src/sockopt_wrap.c new file mode 100644 index 00000000000000..c0097eed0538d0 --- /dev/null +++ b/net/mwan3/src/sockopt_wrap.c @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2020 Aaron Goodman . All Rights Reserved. + */ + +/* + * sockopt_wrap.c provides a shared library that intercepts syscalls to various + * networking functions to bind the sockets a source IP address and network device + * and to set the firewall mark on otugoing packets. Parameters are set using the + * DEVICE, SRCIP, FWMARK environment variables. + * + * Additionally the FAMILY environment variable can be set to either 'ipv4' or + * 'ipv6' to cause sockets opened with ipv6 or ipv4 to fail, respectively. + * + * Each environment variable is optional, and if not set, the library will not + * enforce the particular parameter. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +static int (*next_socket)(int domain, int type, int protocol); +static int (*next_setsockopt)(int sockfd, int level, int optname, + const void *optval, socklen_t optlen); +static int (*next_bind)(int sockfd, const struct sockaddr *addr, socklen_t addrlen); +static int (*next_close)(int fd); +static ssize_t (*next_send)(int sockfd, const void *buf, size_t len, int flags); +static ssize_t (*next_sendto)(int sockfd, const void *buf, size_t len, int flags, + const struct sockaddr *dest_addr, socklen_t addrlen); +static ssize_t (*next_sendmsg)(int sockfd, const struct msghdr *msg, int flags); +static int (*next_connect)(int sockfd, const struct sockaddr *addr, + socklen_t addrlen); +static int device=0; +static struct sockaddr_in source4 = {0}; +#ifdef CONFIG_IPV6 +static struct sockaddr_in6 source6 = {0}; +#endif +static struct sockaddr * source = 0; +static int sockaddr_size = 0; +static int is_bound [1024] = {0}; + +#define next_func(x)\ +void set_next_##x(){\ + if (next_##x) return;\ + next_##x = dlsym(RTLD_NEXT, #x);\ + dlerror_handle();\ + return;\ +} + +void dlerror_handle() +{ + char *msg; + if ((msg = dlerror()) != NULL) { + fprintf(stderr, "socket: dlopen failed : %s\n", msg); + fflush(stderr); + exit(EXIT_FAILURE); + } +} + +next_func(bind); +next_func(close); +next_func(setsockopt); +next_func(socket); +next_func(send); +next_func(sendto); +next_func(sendmsg); +next_func(connect); + +void dobind(int sockfd) +{ + if (source && sockfd < 1024 && !is_bound[sockfd]) { + set_next_bind(); + if (next_bind(sockfd, source, sockaddr_size)) { + perror("failed to bind to ip address"); + next_close(sockfd); + exit(EXIT_FAILURE); + } + is_bound[sockfd] = 1; + } +} + +int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) +{ + set_next_connect(); + dobind(sockfd); + return next_connect(sockfd, addr, addrlen); +} + +ssize_t send(int sockfd, const void *buf, size_t len, int flags) +{ + set_next_send(); + dobind(sockfd); + return next_send(sockfd, buf, len, flags); +} + +ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, + const struct sockaddr *dest_addr, socklen_t addrlen) +{ + set_next_sendto(); + dobind(sockfd); + return next_sendto(sockfd, buf, len, flags, dest_addr, addrlen); +} + +ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags) +{ + set_next_sendmsg(); + dobind(sockfd); + return next_sendmsg(sockfd, msg, flags); +} + +int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) +{ + set_next_bind(); + if (device && addr->sa_family == AF_PACKET) { + ((struct sockaddr_ll*)addr)->sll_ifindex=device; + } + else if (source && addr->sa_family == AF_INET) { + ((struct sockaddr_in*)addr)->sin_addr = source4.sin_addr; + } +#ifdef CONFIG_IPV6 + else if (source && addr->sa_family == AF_INET6) { + ((struct sockaddr_in6*)addr)->sin6_addr = source6.sin6_addr; + } +#endif + if (sockfd < 1024) + is_bound[sockfd] = 1; + return next_bind(sockfd, addr, addrlen); +} + +int close (int sockfd) +{ + set_next_close(); + if (sockfd < 1024) + is_bound[sockfd]=0; + return next_close(sockfd); +} + +int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen) +{ + set_next_setsockopt(); + if (level == SOL_SOCKET && (optname == SO_MARK || optname == SO_BINDTODEVICE)) + return 0; + return next_setsockopt(sockfd, level, optname, optval, optlen); +} + +int socket(int domain, int type, int protocol) +{ + int handle; + + const char *socket_str = getenv("DEVICE"); + const char *srcip_str = getenv("SRCIP"); + const char *fwmark_str = getenv("FWMARK"); + const char *family_str = getenv("FAMILY"); + const int iface_len = socket_str ? strnlen(socket_str, IFNAMSIZ) : 0; + int has_family = family_str && *family_str != 0; + int has_srcip = srcip_str && *srcip_str != 0; + const int fwmark = fwmark_str ? (int)strtol(fwmark_str, NULL, 0) : 0; + + set_next_close(); + set_next_socket(); + set_next_send(); + set_next_setsockopt(); + set_next_sendmsg(); + set_next_sendto(); + set_next_connect(); + if(has_family) { +#ifdef CONFIG_IPV6 + if(domain == AF_INET && strncmp(family_str,"ipv6",4) == 0) + return -1; +#endif + if(domain == AF_INET6 && strncmp(family_str,"ipv4",4) == 0) + return -1; + } + + if (domain != AF_INET +#ifdef CONFIG_IPV6 + && domain != AF_INET6 +#endif + ) { + return next_socket(domain, type, protocol); + } + + + if (iface_len > 0) { + if (iface_len == IFNAMSIZ) { + fprintf(stderr,"socket: Too long iface name\n"); + fflush(stderr); + exit(EXIT_FAILURE); + } + } + + if (has_srcip) { + int s; + void * addr_buf; + if (domain == AF_INET) { + addr_buf = &source4.sin_addr; + sockaddr_size=sizeof source4; + memset(&source4, 0, sockaddr_size); + source4.sin_family = domain; + source = (struct sockaddr*)&source4; + } +#ifdef CONFIG_IPV6 + else { + addr_buf = &source6.sin6_addr; + sockaddr_size=sizeof source6; + memset(&source6, 0, sockaddr_size); + source6.sin6_family=domain; + source = (struct sockaddr*)&source6; + } +#endif + s = inet_pton(domain, srcip_str, addr_buf); + if (s == 0) { + fprintf(stderr, "socket: ip address invalid format for family %s\n", + domain == AF_INET ? "AF_INET" : domain == AF_INET6 ? + "AF_INET6" : "unknown"); + return -1; + } + if (s < 0) { + perror("inet_pton"); + exit(EXIT_FAILURE); + } + } + + handle = next_socket(domain, type, protocol); + if (handle == -1 ) { + return handle; + } + + if (iface_len > 0) { + device=if_nametoindex(socket_str); + if (next_setsockopt(handle, SOL_SOCKET, SO_BINDTODEVICE, + socket_str, iface_len + 1)) { + perror("socket: setting interface name failed with error"); + next_close(handle); + exit(EXIT_FAILURE); + } + } + + if (fwmark > 0) { + if (next_setsockopt(handle, SOL_SOCKET, SO_MARK, + &fwmark, sizeof fwmark)) { + perror("failed setting mark for socket"); + next_close(handle); + exit(EXIT_FAILURE); + } + } + return handle; +}