diff --git a/tools/testing/selftests/net/ovpn/Makefile b/tools/testing/selftests/net/ovpn/Makefile
index 5c70cac0a95b..453bee34ea08 100644
--- a/tools/testing/selftests/net/ovpn/Makefile
+++ b/tools/testing/selftests/net/ovpn/Makefile
@@ -33,6 +33,7 @@ TEST_FILES = \
 # end of TEST_FILES
 
 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 06ce298b6e0e..a51186cbb1dd 100644
--- a/tools/testing/selftests/net/ovpn/common.sh
+++ b/tools/testing/selftests/net/ovpn/common.sh
@@ -214,12 +214,13 @@ ovpn_add_peer() {
 	local server_ns="ovpn_peer0"
 	M_ID=${labels[OVPN_SYMMETRIC_ID]}
 	local dev=${2:-"any"}
+	local laddr=${3:-"any"}
 
 	if [ "${OVPN_PROTO}" == "UDP" ]; then
 		if [ ${1} -eq 0 ]; then
 			ip netns exec "${server_ns}" "${OVPN_CLI}" \
-				new_multi_peer tun0 "${dev}" 1 "${M_ID}" \
-				"${OVPN_UDP_PEERS_FILE}"
+				new_multi_peer tun0 "${dev}" "${laddr}" 1 \
+				"${M_ID}" "${OVPN_UDP_PEERS_FILE}"
 
 			for p in $(seq 1 ${OVPN_NUM_PEERS}); do
 				ip netns exec "${server_ns}" ${OVPN_CLI} \
@@ -244,7 +245,7 @@ ovpn_add_peer() {
 				${OVPN_UDP_PEERS_FILE})
 			ip netns exec "${peer_ns}" "${OVPN_CLI}" new_peer \
 				tun"${1}" "${dev}" "${PEER_ID}" "${TX_ID}" \
-				"${LPORT}" "${RADDR}" "${RPORT}"
+				"${laddr}" "${LPORT}" "${RADDR}" "${RPORT}"
 			ip netns exec "${peer_ns}" ${OVPN_CLI} new_key tun${1} \
 				${PEER_ID} 1 0 ${OVPN_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 312822c27909..0ae371bbaeb9 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;
@@ -471,59 +471,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) {
@@ -531,16 +501,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;
 		}
 	}
 
@@ -548,45 +519,87 @@ 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; 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);
 
-	ret = ovpn_socket(ctx, family, IPPROTO_TCP);
-	if (ret < 0)
-		return ret;
+	if (socket < 0)
+		return socket;
 
-	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;
 }
 
@@ -621,18 +634,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;
@@ -643,20 +651,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)
@@ -1712,7 +1722,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");
@@ -1720,16 +1730,20 @@ 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, 'none' for symmetric peer ID\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, "\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> <id_type> <peers_file> [mark]: add multiple peers as listed in the file\n");
+		"* new_multi_peer <iface> <dev> <laddr> <lport> <id_type> <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, "\tid_type:\n");
 	fprintf(stderr,
@@ -2224,11 +2238,8 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[])
 		if (argc < 6)
 			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];
 
 		if (strcmp(argv[4], "SYMM") == 0) {
 			ovpn->asymm_id = false;
@@ -2252,6 +2263,9 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[])
 		ovpn->sa_family = AF_INET;
 		ovpn->asymm_id = strcmp(argv[4], "none");
 
+		ovpn->laddr = NULL;
+		ovpn->lport = "1";
+
 		ret = ovpn_parse_new_peer(ovpn, argv[3], argv[4], argv[5],
 					  argv[6], NULL);
 		if (ret < 0) {
@@ -2271,52 +2285,46 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[])
 		}
 		break;
 	case CMD_NEW_PEER:
-		if (argc < 9)
+		if (argc < 10)
 			return -EINVAL;
 
 		ovpn->bind_dev = strcmp(argv[3], "any") == 0 ? NULL : argv[3];
 
 		ovpn->asymm_id = strcmp(argv[5], "none");
 
-		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], vpnip);
+		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 < 7)
+		if (argc < 8)
 			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];
 
-		if (!strcmp(argv[5], "SYMM")) {
+		if (!strcmp(argv[6], "SYMM")) {
 			ovpn->asymm_id = false;
-		} else if (!strcmp(argv[5], "ASYMM")) {
+		} else if (!strcmp(argv[6], "ASYMM")) {
 			ovpn->asymm_id = true;
 		} else {
-			fprintf(stderr, "Cannot parse id type: %s\n", argv[5]);
+			fprintf(stderr, "Cannot parse id type: %s\n", argv[6]);
 			return -1;
 		}
 
-		ovpn->peers_file = argv[6];
+		ovpn->peers_file = argv[7];
 
 		ovpn->mark = 0;
-		if (argc > 7) {
-			ovpn->mark = strtoul(argv[7], NULL, 10);
+		if (argc > 8) {
+			ovpn->mark = strtoul(argv[8], 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 <ralf@mandelbit.com>
+#		Antonio Quartulli <antonio@openvpn.net>
+
+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 bc0b8a0b4373..8d83bfef0917 100755
--- a/tools/testing/selftests/net/ovpn/test-bind.sh
+++ b/tools/testing/selftests/net/ovpn/test-bind.sh
@@ -10,6 +10,7 @@
 set -eE
 
 OVPN_PROTO=UDP
+BIND_TYPE=${BIND_TYPE:-"DEV"}
 
 source ./common.sh
 
@@ -62,8 +63,9 @@ ovpn_bind_prepare_network() {
 	ovpn_cmd_ok "bring up peer2 second underlay link" \
 		ip -n ovpn_peer2 link set veth2 up
 
-	# Some test cases intentionally bind peer1 to a device that does not
-	# match the route-selected underlay, so allow asymmetric underlay paths.
+	# Some test cases intentionally bind peer1 to a device or address that
+	# does not match the route-selected underlay, so allow asymmetric
+	# underlay paths.
 	ovpn_cmd_ok "disable peer1 global rp_filter" \
 		ip netns exec ovpn_peer1 sysctl -w \
 			net.ipv4.conf.all.rp_filter=0
@@ -110,8 +112,10 @@ ovpn_bind_prepare_network() {
 ovpn_bind_configure_peers() {
 	local dev1="$1"
 	local dev2="$2"
-	local raddr4_peer1="$3"
-	local raddr4_peer2="$4"
+	local laddr4_peer1="$3"
+	local laddr4_peer2="$4"
+	local raddr4_peer1="$5"
+	local raddr4_peer2="$6"
 
 	ip netns exec ovpn_peer1 "${OVPN_CLI}" del_peer tun1 1 \
 		>/dev/null 2>&1 || true
@@ -121,15 +125,16 @@ ovpn_bind_configure_peers() {
 	# Close any active userspace socket before installing a new peer pair.
 	killall "$(basename "${OVPN_CLI}")" 2>/dev/null || true
 
-	ovpn_cmd_ok "create peer1 bound peer on ${dev1}" \
+	ovpn_cmd_ok "create peer1 bound peer" \
 		ip netns exec ovpn_peer1 "${OVPN_CLI}" new_peer tun1 \
-			"${dev1}" 1 10 1 "${raddr4_peer1}" 1
+			"${dev1}" 1 10 "${laddr4_peer1}" 1 "${raddr4_peer1}" 1
 	ovpn_cmd_ok "install peer1 key" \
 		ip netns exec ovpn_peer1 "${OVPN_CLI}" new_key tun1 1 1 0 \
 			"${OVPN_ALG}" 0 data64.key
-	ovpn_cmd_ok "create peer2 bound peer on ${dev2}" \
+	ovpn_cmd_ok "create peer2 bound peer" \
 		ip netns exec ovpn_peer2 "${OVPN_CLI}" new_peer tun2 \
-			"${dev2}" 10 1 1 "${raddr4_peer2}" 1
+			"${dev2}" 10 1 "${laddr4_peer2}" 1 \
+			"${raddr4_peer2}" 1
 	ovpn_cmd_ok "install peer2 key" \
 		ip netns exec ovpn_peer2 "${OVPN_CLI}" new_key tun2 10 1 0 \
 			"${OVPN_ALG}" 1 data64.key
@@ -153,7 +158,7 @@ ovpn_bind_start_capture() {
 	OVPN_BIND_TCPDUMP_PIDS+=("${pid}")
 }
 
-ovpn_bind_run_positive_case() {
+ovpn_bind_run_dev_positive_case() {
 	local dev1="$1"
 	local dev2="$2"
 	local raddr4_peer1="$3"
@@ -165,8 +170,8 @@ ovpn_bind_run_positive_case() {
 	local header2="0x4800000a"
 	local ping_start_delay="0.3"
 
-	ovpn_bind_configure_peers "${dev1}" "${dev2}" "${raddr4_peer1}" \
-		"${raddr4_peer2}"
+	ovpn_bind_configure_peers "${dev1}" "${dev2}" any any \
+		"${raddr4_peer1}" "${raddr4_peer2}"
 	filter="$(printf '(%s) or (%s)' \
 		"$(ovpn_build_capture_filter "${header1}" "${raddr4_peer1}")" \
 		"$(ovpn_build_capture_filter "${header2}" "${raddr4_peer2}")")"
@@ -192,7 +197,7 @@ ovpn_bind_run_positive_case() {
 	OVPN_BIND_TCPDUMP_PIDS=("${OVPN_BIND_TCPDUMP_PIDS[@]:1}")
 }
 
-ovpn_bind_run_sender_negative_case() {
+ovpn_bind_run_dev_sender_negative_case() {
 	local dev1="$1"
 	local raddr4_peer1="$2"
 	local raddr4_peer2="$3"
@@ -201,7 +206,7 @@ ovpn_bind_run_sender_negative_case() {
 	local header="0x4800000a"
 	local ping_start_delay="0.3"
 
-	ovpn_bind_configure_peers "${dev1}" any "${raddr4_peer1}" \
+	ovpn_bind_configure_peers "${dev1}" any any any "${raddr4_peer1}" \
 		"${raddr4_peer2}"
 	filter="$(ovpn_build_capture_filter "${header}" "${raddr4_peer1}")"
 
@@ -217,7 +222,7 @@ ovpn_bind_run_sender_negative_case() {
 	OVPN_BIND_TCPDUMP_PIDS=("${OVPN_BIND_TCPDUMP_PIDS[@]:1}")
 }
 
-ovpn_bind_run_receiver_negative_case() {
+ovpn_bind_run_dev_receiver_negative_case() {
 	local dev2="$1"
 	local raddr4_peer1="$2"
 	local raddr4_peer2="$3"
@@ -226,7 +231,7 @@ ovpn_bind_run_receiver_negative_case() {
 	local header="0x4800000a"
 	local ping_start_delay="0.3"
 
-	ovpn_bind_configure_peers any "${dev2}" "${raddr4_peer1}" \
+	ovpn_bind_configure_peers any "${dev2}" any any "${raddr4_peer1}" \
 		"${raddr4_peer2}"
 	filter="$(ovpn_build_capture_filter "${header}" "${raddr4_peer1}")"
 
@@ -242,40 +247,138 @@ ovpn_bind_run_receiver_negative_case() {
 	OVPN_BIND_TCPDUMP_PIDS=("${OVPN_BIND_TCPDUMP_PIDS[@]:1}")
 }
 
+ovpn_bind_run_addr_positive_case() {
+	local laddr4_peer1="$1"
+	local raddr4_peer1="$2"
+	local raddr4_peer2="$3"
+	local expected_dev="$4"
+	local unexpected_dev="$5"
+	local filter
+	local header="0x4800000a"
+	local ping_start_delay="0.3"
+
+	ovpn_bind_configure_peers any any "${laddr4_peer1}" any \
+		"${raddr4_peer1}" "${raddr4_peer2}"
+	filter="$(printf '(%s) and src host %s' \
+		"$(ovpn_build_capture_filter "${header}" "${raddr4_peer1}")" \
+		"${laddr4_peer1}")"
+
+	# The route-selected device must carry peer1 data packets with the
+	# explicitly bound local address as outer source address.
+	ovpn_bind_start_capture "${expected_dev}" 1 "${filter}"
+
+	# The other underlay device must not carry peer1 data packets with the
+	# explicitly bound local address.
+	ovpn_bind_start_capture "${unexpected_dev}" 1 "${filter}"
+
+	sleep "${ping_start_delay}"
+	ovpn_cmd_ok "send tunnel traffic from peer1 to peer2" \
+		ip netns exec ovpn_peer1 ping -qfc 10 -w 3 5.5.5.2
+	ovpn_cmd_ok "capture bound source on ${expected_dev}" \
+		wait "${OVPN_BIND_TCPDUMP_PIDS[0]}"
+	OVPN_BIND_TCPDUMP_PIDS=("${OVPN_BIND_TCPDUMP_PIDS[@]:1}")
+
+	ovpn_cmd_fail "capture bound source on ${unexpected_dev}" \
+		wait "${OVPN_BIND_TCPDUMP_PIDS[0]}"
+	OVPN_BIND_TCPDUMP_PIDS=("${OVPN_BIND_TCPDUMP_PIDS[@]:1}")
+}
+
+ovpn_bind_run_addr_receiver_positive_case() {
+	local laddr4_peer2="$1"
+	local raddr4_peer1="$2"
+	local raddr4_peer2="$3"
+	local expected_dev="$4"
+	local unexpected_dev="$5"
+	local filter
+	local header="0x4800000a"
+	local ping_start_delay="0.3"
+
+	ovpn_bind_configure_peers any any any "${laddr4_peer2}" \
+		"${raddr4_peer1}" "${raddr4_peer2}"
+	filter="$(printf '(%s) and dst host %s' \
+		"$(ovpn_build_capture_filter "${header}" "${raddr4_peer1}")" \
+		"${raddr4_peer1}")"
+
+	# The destination-matching underlay device must carry peer1 data
+	# packets to the address peer2 is bound to.
+	ovpn_bind_start_capture "${expected_dev}" 1 "${filter}"
+
+	# The other underlay device must not carry those packets.
+	ovpn_bind_start_capture "${unexpected_dev}" 1 "${filter}"
+
+	sleep "${ping_start_delay}"
+	ovpn_cmd_ok "send tunnel traffic from peer1 to peer2" \
+		ip netns exec ovpn_peer1 ping -qfc 10 -w 3 5.5.5.2
+	ovpn_cmd_ok "capture bound destination on ${expected_dev}" \
+		wait "${OVPN_BIND_TCPDUMP_PIDS[0]}"
+	OVPN_BIND_TCPDUMP_PIDS=("${OVPN_BIND_TCPDUMP_PIDS[@]:1}")
+
+	ovpn_cmd_fail "capture bound destination on ${unexpected_dev}" \
+		wait "${OVPN_BIND_TCPDUMP_PIDS[0]}"
+	OVPN_BIND_TCPDUMP_PIDS=("${OVPN_BIND_TCPDUMP_PIDS[@]:1}")
+}
+
+ovpn_bind_run_dev_tests() {
+	ktap_set_plan 9
+
+	ovpn_run_stage "setup network topology" ovpn_bind_prepare_network
+	ovpn_run_stage "peer1 bind_dev=veth1 routes over veth1" \
+		ovpn_bind_run_dev_positive_case \
+		veth1 any 10.10.10.2 10.10.10.1 veth1 veth2
+	ovpn_run_stage "peer1 bind_dev=veth2 routes over veth2" \
+		ovpn_bind_run_dev_positive_case \
+		veth2 any 20.20.20.2 20.20.20.1 veth2 veth1
+	ovpn_run_stage "peer2 bind_dev=veth1 replies over veth1" \
+		ovpn_bind_run_dev_positive_case \
+		any veth1 10.10.10.2 10.10.10.1 veth1 veth2
+	ovpn_run_stage "peer2 bind_dev=veth2 replies over veth2" \
+		ovpn_bind_run_dev_positive_case \
+		any veth2 20.20.20.2 20.20.20.1 veth2 veth1
+	ovpn_run_stage "peer1 bind_dev=veth1 blocks veth2 egress" \
+		ovpn_bind_run_dev_sender_negative_case \
+		veth1 20.20.20.2 20.20.20.1 veth2
+	ovpn_run_stage "peer1 bind_dev=veth2 blocks veth1 egress" \
+		ovpn_bind_run_dev_sender_negative_case \
+		veth2 10.10.10.2 10.10.10.1 veth1
+	ovpn_run_stage "peer2 bind_dev=veth1 rejects veth2 ingress" \
+		ovpn_bind_run_dev_receiver_negative_case \
+		veth1 20.20.20.2 20.20.20.1 veth2
+	ovpn_run_stage "peer2 bind_dev=veth2 rejects veth1 ingress" \
+		ovpn_bind_run_dev_receiver_negative_case \
+		veth2 10.10.10.2 10.10.10.1 veth1
+}
+
+ovpn_bind_run_addr_tests() {
+	ktap_set_plan 5
+
+	ovpn_run_stage "setup network topology" ovpn_bind_prepare_network
+	ovpn_run_stage "peer1 bind_addr=10.10.10.1 sends from 10.10.10.1 \
+		via veth2" ovpn_bind_run_addr_positive_case \
+		10.10.10.1 20.20.20.2 10.10.10.1 veth2 veth1
+	ovpn_run_stage "peer1 bind_addr=20.20.20.1 sends from 20.20.20.1 \
+		via veth1" ovpn_bind_run_addr_positive_case \
+		20.20.20.1 10.10.10.2 20.20.20.1 veth1 veth2
+	ovpn_run_stage "peer2 bind_addr=10.10.10.2 accepts dst 10.10.10.2 \
+		via veth1" ovpn_bind_run_addr_receiver_positive_case \
+		10.10.10.2 10.10.10.2 10.10.10.1 veth1 veth2
+	ovpn_run_stage "peer2 bind_addr=20.20.20.2 accepts dst 20.20.20.2 \
+		via veth2" ovpn_bind_run_addr_receiver_positive_case \
+		20.20.20.2 20.20.20.2 20.20.20.1 veth2 veth1
+}
+
 trap ovpn_test_exit EXIT
 trap ovpn_stage_err ERR
 
 ktap_print_header
-ktap_set_plan 9
 
 ovpn_cleanup
 modprobe -q ovpn || true
 
-ovpn_run_stage "setup network topology" ovpn_bind_prepare_network
-ovpn_run_stage "peer1 bind_dev=veth1 routes over veth1" \
-	ovpn_bind_run_positive_case \
-	veth1 any 10.10.10.2 10.10.10.1 veth1 veth2
-ovpn_run_stage "peer1 bind_dev=veth2 routes over veth2" \
-	ovpn_bind_run_positive_case \
-	veth2 any 20.20.20.2 20.20.20.1 veth2 veth1
-ovpn_run_stage "peer2 bind_dev=veth1 replies over veth1" \
-	ovpn_bind_run_positive_case \
-	any veth1 10.10.10.2 10.10.10.1 veth1 veth2
-ovpn_run_stage "peer2 bind_dev=veth2 replies over veth2" \
-	ovpn_bind_run_positive_case \
-	any veth2 20.20.20.2 20.20.20.1 veth2 veth1
-ovpn_run_stage "peer1 bind_dev=veth1 blocks veth2 egress" \
-	ovpn_bind_run_sender_negative_case \
-	veth1 20.20.20.2 20.20.20.1 veth2
-ovpn_run_stage "peer1 bind_dev=veth2 blocks veth1 egress" \
-	ovpn_bind_run_sender_negative_case \
-	veth2 10.10.10.2 10.10.10.1 veth1
-ovpn_run_stage "peer2 bind_dev=veth1 rejects veth2 ingress" \
-	ovpn_bind_run_receiver_negative_case \
-	veth1 20.20.20.2 20.20.20.1 veth2
-ovpn_run_stage "peer2 bind_dev=veth2 rejects veth1 ingress" \
-	ovpn_bind_run_receiver_negative_case \
-	veth2 10.10.10.2 10.10.10.1 veth1
+if [ "${BIND_TYPE}" = "ADDR" ]; then
+	ovpn_bind_run_addr_tests
+else
+	ovpn_bind_run_dev_tests
+fi
 
 ovpn_test_finished=1
 ktap_finished
diff --git a/tools/testing/selftests/net/ovpn/test-mark.sh b/tools/testing/selftests/net/ovpn/test-mark.sh
index 010f5b44dbf4..581d208c18ac 100755
--- a/tools/testing/selftests/net/ovpn/test-mark.sh
+++ b/tools/testing/selftests/net/ovpn/test-mark.sh
@@ -39,7 +39,7 @@ ovpn_mark_prepare_network() {
 
 	ovpn_cmd_ok "create server-side multi-peer with fwmark" \
 		ip netns exec ovpn_peer0 "${OVPN_CLI}" new_multi_peer tun0 \
-			any 1 ASYMM "${OVPN_UDP_PEERS_FILE}" "${MARK}"
+			any any 1 ASYMM "${OVPN_UDP_PEERS_FILE}" "${MARK}"
 	for p in $(seq 1 3); do
 		ovpn_cmd_ok "install server key for peer ${p}" \
 			ip netns exec ovpn_peer0 "${OVPN_CLI}" new_key tun0 \
