From: Ralf Lici <[email protected]>

Add a selftest to verify that when a socket is bound to a local address,
UDP traffic from ovpn is correctly routed through that address.

This test extends test-bind.sh by binding to the addresses on each veth
pair and uses tcpdump to confirm that traffic flows as expected.

Cc: Shuah Khan <[email protected]>
Signed-off-by: Ralf Lici <[email protected]>
Signed-off-by: Antonio Quartulli <[email protected]>
---
 tools/testing/selftests/net/ovpn/Makefile     |   1 +
 tools/testing/selftests/net/ovpn/common.sh    |   6 +-
 tools/testing/selftests/net/ovpn/ovpn-cli.c   | 202 +++++++++---------
 .../selftests/net/ovpn/test-bind-addr.sh      |  10 +
 tools/testing/selftests/net/ovpn/test-bind.sh |  52 +++--
 tools/testing/selftests/net/ovpn/test-mark.sh |   2 +-
 6 files changed, 152 insertions(+), 121 deletions(-)
 create mode 100755 tools/testing/selftests/net/ovpn/test-bind-addr.sh

diff --git a/tools/testing/selftests/net/ovpn/Makefile 
b/tools/testing/selftests/net/ovpn/Makefile
index f219d87e2c44..7a5ad7a19273 100644
--- a/tools/testing/selftests/net/ovpn/Makefile
+++ b/tools/testing/selftests/net/ovpn/Makefile
@@ -26,6 +26,7 @@ LDLIBS += $(NL_LDLIBS)
 TEST_FILES = common.sh
 
 TEST_PROGS := \
+       test-bind-addr.sh \
        test-bind.sh \
        test-chachapoly.sh \
        test-close-socket-tcp.sh \
diff --git a/tools/testing/selftests/net/ovpn/common.sh 
b/tools/testing/selftests/net/ovpn/common.sh
index c802e4e50054..5de42948d189 100644
--- a/tools/testing/selftests/net/ovpn/common.sh
+++ b/tools/testing/selftests/net/ovpn/common.sh
@@ -67,10 +67,12 @@ setup_listener() {
 
 add_peer() {
        dev=${2:-"any"}
+       laddr=${3:-"any"}
 
        if [ "${PROTO}" == "UDP" ]; then
                if [ ${1} -eq 0 ]; then
-                       ip netns exec peer0 ${OVPN_CLI} new_multi_peer tun0 
${dev} 1 ${UDP_PEERS_FILE}
+                       ip netns exec peer0 ${OVPN_CLI} new_multi_peer tun0 
${dev} ${laddr} \
+                               1 ${UDP_PEERS_FILE}
 
                        for p in $(seq 1 ${NUM_PEERS}); do
                                ip netns exec peer0 ${OVPN_CLI} new_key tun0 
${p} 1 0 ${ALG} 0 \
@@ -82,7 +84,7 @@ add_peer() {
                        RPORT=$(awk "NR == ${1} {print \$4}" ${UDP_PEERS_FILE})
                        LPORT=$(awk "NR == ${1} {print \$6}" ${UDP_PEERS_FILE})
                        ip netns exec peer${1} ${OVPN_CLI} new_peer tun${1} 
${dev} ${TX_ID} ${1} \
-                               ${LPORT} ${RADDR} ${RPORT}
+                               ${laddr} ${LPORT} ${RADDR} ${RPORT}
                        ip netns exec peer${1} ${OVPN_CLI} new_key tun${1} 
${TX_ID} 1 0 \
                                ${ALG} 1 data64.key
                fi
diff --git a/tools/testing/selftests/net/ovpn/ovpn-cli.c 
b/tools/testing/selftests/net/ovpn/ovpn-cli.c
index 6d84380c76ad..7f7780a515d1 100644
--- a/tools/testing/selftests/net/ovpn/ovpn-cli.c
+++ b/tools/testing/selftests/net/ovpn/ovpn-cli.c
@@ -105,7 +105,7 @@ struct ovpn_ctx {
        sa_family_t sa_family;
 
        unsigned long peer_id, tx_id;
-       unsigned long lport;
+       const char *laddr, *lport;
 
        union {
                struct sockaddr_in in4;
@@ -470,59 +470,29 @@ static int ovpn_parse_key_direction(const char *dir, 
struct ovpn_ctx *ctx)
        return 0;
 }
 
-static int ovpn_socket(struct ovpn_ctx *ctx, sa_family_t family, int proto)
+static int ovpn_socket(struct ovpn_ctx *ctx, sa_family_t family, int type)
 {
-       struct sockaddr_storage local_sock = { 0 };
-       struct sockaddr_in6 *in6;
-       struct sockaddr_in *in;
-       int ret, s, sock_type;
-       size_t sock_len;
-
-       if (proto == IPPROTO_UDP)
-               sock_type = SOCK_DGRAM;
-       else if (proto == IPPROTO_TCP)
-               sock_type = SOCK_STREAM;
-       else
-               return -EINVAL;
+       int ret, s;
 
-       s = socket(family, sock_type, 0);
+       s = socket(family, type, 0);
        if (s < 0) {
                perror("cannot create socket");
                return -1;
        }
 
-       switch (family) {
-       case AF_INET:
-               in = (struct sockaddr_in *)&local_sock;
-               in->sin_family = family;
-               in->sin_port = htons(ctx->lport);
-               in->sin_addr.s_addr = htonl(INADDR_ANY);
-               sock_len = sizeof(*in);
-               break;
-       case AF_INET6:
-               in6 = (struct sockaddr_in6 *)&local_sock;
-               in6->sin6_family = family;
-               in6->sin6_port = htons(ctx->lport);
-               in6->sin6_addr = in6addr_any;
-               sock_len = sizeof(*in6);
-               break;
-       default:
-               return -1;
-       }
-
        int opt = 1;
 
        ret = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
 
        if (ret < 0) {
                perror("setsockopt for SO_REUSEADDR");
-               return ret;
+               goto close;
        }
 
        ret = setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
        if (ret < 0) {
                perror("setsockopt for SO_REUSEPORT");
-               return ret;
+               goto close;
        }
 
        if (ctx->mark != 0) {
@@ -530,16 +500,17 @@ static int ovpn_socket(struct ovpn_ctx *ctx, sa_family_t 
family, int proto)
                                 sizeof(ctx->mark));
                if (ret < 0) {
                        perror("setsockopt for SO_MARK");
-                       return ret;
+                       goto close;
                }
        }
 
        if (family == AF_INET6) {
                opt = 0;
-               if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &opt,
-                              sizeof(opt))) {
+               ret = setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &opt,
+                                sizeof(opt));
+               if (ret < 0) {
                        perror("failed to set IPV6_V6ONLY");
-                       return -1;
+                       goto close;
                }
        }
 
@@ -547,45 +518,83 @@ static int ovpn_socket(struct ovpn_ctx *ctx, sa_family_t 
family, int proto)
                if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, ctx->bind_dev,
                               strlen(ctx->bind_dev) + 1) != 0) {
                        perror("setsockopt for SO_BINDTODEVICE");
-                       return -1;
+                       goto close;
                }
        }
 
-       ret = bind(s, (struct sockaddr *)&local_sock, sock_len);
-       if (ret < 0) {
-               perror("cannot bind socket");
-               goto err_socket;
+       return s;
+close:
+       close(s);
+       return ret;
+}
+
+static int ovpn_setup_socket(struct ovpn_ctx *ctx, sa_family_t family,
+                            int socktype)
+{
+       struct addrinfo *list_ai, *curr_ai;
+       struct addrinfo hints;
+       int ret, socket;
+
+       memset(&hints, 0, sizeof(hints));
+       hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV |
+                        (socktype == SOCK_STREAM ? 0: AI_V4MAPPED) |
+                        (ctx->laddr ? 0 : AI_PASSIVE);
+       hints.ai_family = family;
+       hints.ai_socktype = socktype;
+       ret = getaddrinfo(ctx->laddr, ctx->lport, &hints, &list_ai);
+       if (ret) {
+               fprintf(stderr, "laddr %s, lport %s, getaddrinfo on local 
address: %s\n", ctx->laddr, ctx->lport, gai_strerror(ret));
+               return ret;
        }
 
-       ctx->socket = s;
-       ctx->sa_family = family;
-       return 0;
+       for (curr_ai = list_ai; curr_ai != NULL; curr_ai = curr_ai->ai_next) {
+               socket = ovpn_socket(ctx, family, socktype);
+               if (socket < 0)
+                       continue;
 
-err_socket:
-       close(s);
-       return -1;
+               ret = bind(socket, curr_ai->ai_addr, curr_ai->ai_addrlen);
+               if (ret == 0)
+                       break;
+
+               close(socket);
+       }
+
+       freeaddrinfo(list_ai);
+
+       if (ret < 0) {
+               perror("cannot setup socket\n");
+               return ret;
+       }
+
+       return socket;
 }
 
 static int ovpn_udp_socket(struct ovpn_ctx *ctx, sa_family_t family)
 {
-       return ovpn_socket(ctx, family, IPPROTO_UDP);
+       int socket = ovpn_setup_socket(ctx, family, SOCK_DGRAM);
+       if (socket < 0)
+               return socket;
+
+       ctx->sa_family = family;
+       ctx->socket = socket;
+       return 0;
 }
 
 static int ovpn_listen(struct ovpn_ctx *ctx, sa_family_t family)
 {
-       int ret;
+       int ret, socket = ovpn_setup_socket(ctx, family, SOCK_STREAM);
+       if (socket < 0)
+               return socket;
 
-       ret = ovpn_socket(ctx, family, IPPROTO_TCP);
-       if (ret < 0)
-               return ret;
-
-       ret = listen(ctx->socket, 10);
+       ret = listen(socket, 10);
        if (ret < 0) {
                perror("listen");
-               close(ctx->socket);
+               close(socket);
                return -1;
        }
 
+       ctx->sa_family = family;
+       ctx->socket = socket;
        return 0;
 }
 
@@ -620,18 +629,13 @@ static int ovpn_accept(struct ovpn_ctx *ctx)
        return ret;
 }
 
-static int ovpn_connect(struct ovpn_ctx *ovpn)
+static int ovpn_connect(struct ovpn_ctx *ctx)
 {
+       const sa_family_t family = ctx->remote.in4.sin_family;
        socklen_t socklen;
-       int s, ret;
+       int ret, socket;
 
-       s = socket(ovpn->remote.in4.sin_family, SOCK_STREAM, 0);
-       if (s < 0) {
-               perror("cannot create socket");
-               return -1;
-       }
-
-       switch (ovpn->remote.in4.sin_family) {
+       switch (family) {
        case AF_INET:
                socklen = sizeof(struct sockaddr_in);
                break;
@@ -642,20 +646,22 @@ static int ovpn_connect(struct ovpn_ctx *ovpn)
                return -EOPNOTSUPP;
        }
 
-       ret = connect(s, (struct sockaddr *)&ovpn->remote, socklen);
+       socket = ovpn_setup_socket(ctx, family, SOCK_STREAM);
+       if (socket < 0)
+               return socket;
+
+       ret = connect(socket, (struct sockaddr *)&ctx->remote, socklen);
        if (ret < 0) {
                perror("connect");
-               goto err;
+               close(socket);
+               return ret;
        }
 
        fprintf(stderr, "connected\n");
 
-       ovpn->socket = s;
-
+       ctx->sa_family = family;
+       ctx->socket = socket;
        return 0;
-err:
-       close(s);
-       return ret;
 }
 
 static int ovpn_new_peer(struct ovpn_ctx *ovpn, bool is_tcp)
@@ -1702,7 +1708,7 @@ static void usage(const char *cmd)
                "\tkey_file: file containing the symmetric key for 
encryption\n");
 
        fprintf(stderr,
-               "* new_peer <iface> <dev> <peer_id> <tx_id> <lport> <raddr> 
<rport> [vpnaddr]: add new peer\n");
+               "* new_peer <iface> <dev> <peer_id> <tx_id> <laddr> <lport> 
<raddr> <rport> [vpnaddr]: add new peer\n");
        fprintf(stderr, "\tiface: ovpn interface name\n");
        fprintf(stderr,
                "\tdev: transport interface name to bind to, supports 'any'\n");
@@ -1711,15 +1717,19 @@ static void usage(const char *cmd)
                "\tpeer_id: peer ID found in data packets received from this 
peer\n");
        fprintf(stderr,
                "\ttx_id: peer ID to be used when sending to this peer\n");
+       fprintf(stderr,
+               "\tladdr: local UDP address to bind to, supports 'any'\n");
        fprintf(stderr, "\traddr: peer IP address\n");
        fprintf(stderr, "\trport: peer UDP port\n");
        fprintf(stderr, "\tvpnaddr: peer VPN IP\n");
 
        fprintf(stderr,
-               "* new_multi_peer <iface> <dev> <lport> <peers_file> [mark]: 
add multiple peers as listed in the file\n");
+               "* new_multi_peer <iface> <dev> <laddr> <lport> <peers_file> 
[mark]: add multiple peers as listed in the file\n");
        fprintf(stderr, "\tiface: ovpn interface name\n");
        fprintf(stderr,
                "\tdev: transport interface name to bind to, supports 'any'\n");
+       fprintf(stderr,
+               "\tladdr: local UDP address to bind to, supports 'any'\n");
        fprintf(stderr, "\tlport: local UDP port to bind to\n");
        fprintf(stderr,
                "\tpeers_file: text file containing one peer per line. Line 
format:\n");
@@ -2203,11 +2213,8 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, 
int argc, char *argv[])
                if (argc < 5)
                        return -EINVAL;
 
-               ovpn->lport = strtoul(argv[3], NULL, 10);
-               if (errno == ERANGE || ovpn->lport > 65535) {
-                       fprintf(stderr, "lport value out of range\n");
-                       return -1;
-               }
+               ovpn->laddr = NULL;
+               ovpn->lport = argv[3];
 
                ovpn->peers_file = argv[4];
 
@@ -2221,6 +2228,9 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int 
argc, char *argv[])
 
                ovpn->sa_family = AF_INET;
 
+               ovpn->laddr = NULL;
+               ovpn->lport = "1";
+
                ret = ovpn_parse_new_peer(ovpn, argv[3], argv[4], argv[5], 
argv[6],
                                          NULL);
                if (ret < 0) {
@@ -2240,41 +2250,35 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, 
int argc, char *argv[])
                }
                break;
        case CMD_NEW_PEER:
-               if (argc < 8)
+               if (argc < 9)
                        return -EINVAL;
 
                ovpn->bind_dev = strcmp(argv[3], "any") == 0 ? NULL : argv[3];
 
-               ovpn->lport = strtoul(argv[6], NULL, 10);
-               if (errno == ERANGE || ovpn->lport > 65535) {
-                       fprintf(stderr, "lport value out of range\n");
-                       return -1;
-               }
+               ovpn->laddr = strcmp(argv[6], "any") == 0 ? NULL : argv[6];
+               ovpn->lport = argv[7];
 
-               const char *vpnip = (argc > 9) ? argv[9] : NULL;
+               const char *vpnip = (argc > 10) ? argv[10] : NULL;
 
-               ret = ovpn_parse_new_peer(ovpn, argv[4], argv[5], argv[7], 
argv[8],
+               ret = ovpn_parse_new_peer(ovpn, argv[4], argv[5], argv[8], 
argv[9],
                                          vpnip);
                if (ret < 0)
                        return -1;
                break;
        case CMD_NEW_MULTI_PEER:
-               if (argc < 6)
+               if (argc < 7)
                        return -EINVAL;
 
                ovpn->bind_dev = strcmp(argv[3], "any") == 0 ? NULL : argv[3];
 
-               ovpn->lport = strtoul(argv[4], NULL, 10);
-               if (errno == ERANGE || ovpn->lport > 65535) {
-                       fprintf(stderr, "lport value out of range\n");
-                       return -1;
-               }
+               ovpn->laddr = strcmp(argv[4], "any") == 0 ? NULL : argv[4];
+               ovpn->lport = argv[5];
 
-               ovpn->peers_file = argv[5];
+               ovpn->peers_file = argv[6];
 
                ovpn->mark = 0;
-               if (argc > 6) {
-                       ovpn->mark = strtoul(argv[6], NULL, 10);
+               if (argc > 7) {
+                       ovpn->mark = strtoul(argv[7], NULL, 10);
                        if (errno == ERANGE || ovpn->mark > UINT32_MAX) {
                                fprintf(stderr, "mark value out of range\n");
                                return -1;
diff --git a/tools/testing/selftests/net/ovpn/test-bind-addr.sh 
b/tools/testing/selftests/net/ovpn/test-bind-addr.sh
new file mode 100755
index 000000000000..e33a433ceb4b
--- /dev/null
+++ b/tools/testing/selftests/net/ovpn/test-bind-addr.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2020-2025 OpenVPN, Inc.
+#
+#      Author: Ralf Lici <[email protected]>
+#              Antonio Quartulli <[email protected]>
+
+BIND_TYPE="ADDR"
+
+source test-bind.sh
diff --git a/tools/testing/selftests/net/ovpn/test-bind.sh 
b/tools/testing/selftests/net/ovpn/test-bind.sh
index fd7c3c8fdf63..b642f396c2a4 100755
--- a/tools/testing/selftests/net/ovpn/test-bind.sh
+++ b/tools/testing/selftests/net/ovpn/test-bind.sh
@@ -11,6 +11,8 @@ set -e
 PROTO=UDP
 source ./common.sh
 
+BIND_TYPE=${BIND_TYPE:-"DEV"}
+
 cleanup
 
 modprobe -q ovpn || true
@@ -61,17 +63,19 @@ run_bind_test() {
        # close any active socket
        killall $(basename ${OVPN_CLI}) 2>/dev/null || true
 
-       ip netns exec peer1 ${OVPN_CLI} new_peer tun1 ${dev1} 1 10 1 
${raddr4_peer1} 1
+       ip netns exec peer1 ${OVPN_CLI} new_peer tun1 ${dev1} 1 10 
${raddr4_peer2} 1 ${raddr4_peer1} 1
        ip netns exec peer1 ${OVPN_CLI} new_key tun1 1 1 0 ${ALG} 0 data64.key
-       ip netns exec peer2 ${OVPN_CLI} new_peer tun2 ${dev2} 10 1 1 
${raddr4_peer2} 1
+       ip netns exec peer2 ${OVPN_CLI} new_peer tun2 ${dev2} 10 1 
${raddr4_peer1} 1 ${raddr4_peer2} 1
        ip netns exec peer2 ${OVPN_CLI} new_key tun2 10 1 0 ${ALG} 1 data64.key
 
        ip netns exec peer1 ${OVPN_CLI} set_peer tun1 1 60 120
        ip netns exec peer2 ${OVPN_CLI} set_peer tun2 10 60 120
 
-       timeout 2 ip netns exec peer1 tcpdump -i veth1 "${PROTO,,}" port 1 -n 
-q > /tmp/ovpn-bind1.log &
+       timeout 2 ip netns exec peer1 tcpdump -i veth1 "${PROTO,,}" and host 
${raddr4_peer2} \
+               and port 1 -n -q > /tmp/ovpn-bind1.log &
        tcpdump1_pid=$!
-       timeout 2 ip netns exec peer1 tcpdump -i veth2 "${PROTO,,}" port 1 -n 
-q > /tmp/ovpn-bind2.log &
+       timeout 2 ip netns exec peer1 tcpdump -i veth2 "${PROTO,,}" and host 
${raddr4_peer2} \
+               and port 1 -n -q > /tmp/ovpn-bind2.log &
        tcpdump2_pid=$!
        sleep 0.5
 
@@ -81,21 +85,31 @@ run_bind_test() {
        wait ${tcpdump2_pid} || true
 }
 
-run_bind_test veth1 any 10.10.10.2 10.10.10.1
-[ "$(grep -c -i udp /tmp/ovpn-bind1.log)" -ge 100 ]
-[ "$(grep -c -i udp /tmp/ovpn-bind2.log)" -eq 0 ]
-
-run_bind_test veth2 any 20.20.20.2 20.20.20.1
-[ "$(grep -c -i udp /tmp/ovpn-bind2.log)" -ge 100 ]
-[ "$(grep -c -i udp /tmp/ovpn-bind1.log)" -eq 0 ]
-
-run_bind_test any veth1 10.10.10.2 10.10.10.1
-[ "$(grep -c -i udp /tmp/ovpn-bind1.log)" -ge 100 ]
-[ "$(grep -c -i udp /tmp/ovpn-bind2.log)" -eq 0 ]
-
-run_bind_test any veth2 20.20.20.2 20.20.20.1
-[ "$(grep -c -i udp /tmp/ovpn-bind2.log)" -ge 100 ]
-[ "$(grep -c -i udp /tmp/ovpn-bind1.log)" -eq 0 ]
+if [ "${BIND_TYPE}" == "DEV" ]; then
+       run_bind_test veth1 any 10.10.10.2 10.10.10.1
+       [ "$(grep -c -i udp /tmp/ovpn-bind1.log)" -ge 100 ]
+       [ "$(grep -c -i udp /tmp/ovpn-bind2.log)" -eq 0 ]
+
+       run_bind_test veth2 any 20.20.20.2 20.20.20.1
+       [ "$(grep -c -i udp /tmp/ovpn-bind2.log)" -ge 100 ]
+       [ "$(grep -c -i udp /tmp/ovpn-bind1.log)" -eq 0 ]
+
+       run_bind_test any veth1 10.10.10.2 10.10.10.1
+       [ "$(grep -c -i udp /tmp/ovpn-bind1.log)" -ge 100 ]
+       [ "$(grep -c -i udp /tmp/ovpn-bind2.log)" -eq 0 ]
+
+       run_bind_test any veth2 20.20.20.2 20.20.20.1
+       [ "$(grep -c -i udp /tmp/ovpn-bind2.log)" -ge 100 ]
+       [ "$(grep -c -i udp /tmp/ovpn-bind1.log)" -eq 0 ]
+else
+       run_bind_test any any 10.10.10.2 10.10.10.1
+       [ "$(grep -c -i udp /tmp/ovpn-bind1.log)" -ge 100 ]
+       [ "$(grep -c -i udp /tmp/ovpn-bind2.log)" -eq 0 ]
+
+       run_bind_test any any 20.20.20.2 20.20.20.1
+       [ "$(grep -c -i udp /tmp/ovpn-bind2.log)" -ge 100 ]
+       [ "$(grep -c -i udp /tmp/ovpn-bind1.log)" -eq 0 ]
+fi
 
 cleanup
 
diff --git a/tools/testing/selftests/net/ovpn/test-mark.sh 
b/tools/testing/selftests/net/ovpn/test-mark.sh
index c2600bb22e2c..6f575f7b0635 100755
--- a/tools/testing/selftests/net/ovpn/test-mark.sh
+++ b/tools/testing/selftests/net/ovpn/test-mark.sh
@@ -26,7 +26,7 @@ for p in $(seq 0 3); do
 done
 
 # add peer0 with mark
-ip netns exec peer0 ${OVPN_CLI} new_multi_peer tun0 any 1 ${UDP_PEERS_FILE} 
${MARK}
+ip netns exec peer0 ${OVPN_CLI} new_multi_peer tun0 any any 1 
${UDP_PEERS_FILE} ${MARK}
 for p in $(seq 1 3); do
        ip netns exec peer0 ${OVPN_CLI} new_key tun0 ${p} 1 0 ${ALG} 0 
data64.key
 done
-- 
2.51.2


Reply via email to