[Openvpn-devel,1/3] Use IPAPI for setting ipv6 routes when iservice not available

Message ID 20230105022718.1641751-1-selva.nair@gmail.com
State Accepted
Headers show
Series [Openvpn-devel,1/3] Use IPAPI for setting ipv6 routes when iservice not available | expand

Commit Message

Selva Nair Jan. 5, 2023, 2:27 a.m. UTC
From: Selva Nair <selva.nair@gmail.com>

Currently we use netsh for this. The new code closely follows
what interactive service does.

Signed-off-by: Selva Nair <selva.nair@gmail.com>
---
 src/openvpn/route.c | 175 ++++++++++++++++++--------------------------
 1 file changed, 71 insertions(+), 104 deletions(-)

Comments

Lev Stipakov Jan. 6, 2023, 1:44 p.m. UTC | #1
Hi,

Compiled and slightly tested - pinged server's tunnel IPv6 address.

Acked-by: Lev Stipakov <lstipakov@gmail.com>
Gert Doering Jan. 6, 2023, 8:19 p.m. UTC | #2
Hi,

On Fri, Jan 06, 2023 at 03:44:24PM +0200, Lev Stipakov wrote:
> Compiled and slightly tested - pinged server's tunnel IPv6 address.

Do we call "route" for connect networks on Windows?  We do on some
of the platforms, but for those where we do not, this is not a good
test for "route add" functionality - as tunnel IP + subnet gets 
setup via the "ifconfig-ipv6" part (implicit route for connected net).

gert
Selva Nair Jan. 6, 2023, 9:46 p.m. UTC | #3
On Fri, Jan 6, 2023 at 3:19 PM Gert Doering <gert@greenie.muc.de> wrote:

> Hi,
>
> On Fri, Jan 06, 2023 at 03:44:24PM +0200, Lev Stipakov wrote:
> > Compiled and slightly tested - pinged server's tunnel IPv6 address.
>
> Do we call "route" for connect networks on Windows?  We do on some
> of the platforms, but for those where we do not, this is not a good
> test for "route add" functionality - as tunnel IP + subnet gets
> setup via the "ifconfig-ipv6" part (implicit route for connected net).
>

Yes we do call add_route_connected_v6_net() on windows, so add_route_ipv6()
should always get exercised.

Though I vaguely understand why we set the address with /128 and then set
the route for the vpn subnet with fe80::8 as the gateway for tap-windows6,
wonder why we do this for dco as well.

Selva
Gert Doering Jan. 7, 2023, 4:12 p.m. UTC | #4
Hi,

On Fri, Jan 06, 2023 at 04:46:17PM -0500, Selva Nair wrote:
> > On Fri, Jan 06, 2023 at 03:44:24PM +0200, Lev Stipakov wrote:
> > > Compiled and slightly tested - pinged server's tunnel IPv6 address.
> >
> > Do we call "route" for connect networks on Windows?  We do on some
> > of the platforms, but for those where we do not, this is not a good
> > test for "route add" functionality - as tunnel IP + subnet gets
> > setup via the "ifconfig-ipv6" part (implicit route for connected net).
> 
> Yes we do call add_route_connected_v6_net() on windows, so add_route_ipv6()
> should always get exercised.

Thanks for reminding me :-)

> Though I vaguely understand why we set the address with /128 and then set
> the route for the vpn subnet with fe80::8 as the gateway for tap-windows6,
> wonder why we do this for dco as well.

Let me recap why we do this for tap-windows - for windows, this is always
an Ethernet adapter, so it wants to always do neighbour discovery (ND),
IPv6's equivalent of ARP for all "on this interface" addresses.

For "--dev tun" setups, OpenVPN does not want to deal with "Ethernet things",
so the TAP driver replies to ARP requests for "special addresses" - for
net30, OpenVPN ioctl()'s the "peer" address (which is also the gateway
address) into the TAP driver, and only this address is replied to - 
admittedly, I do not know how "topology subnet" works wrt virtual ARP,
need to read up on this again.

For IPv6, I decided that having to deal with ND is easier if I could just
look at a magic number, namely fe80::8, in the tap driver code - so, we
use /128 for the ifconfig bit ("no network here, everything is behind the
gateway") and point all routing to "on this interface, fe80::8" - so for
whatever IPv6 destination, windows will only do ND for fe80::8, and the
TAP driver will reply to it, without any further help (ioctl()) from 
OpenVPN.


This said, I have no idea why we do it that way for wintun and DCO - 
most likely because it avoids having to add even more windows-driver-
specific special case handling in places where we are nicely generic
today.

We are not really losing anything this way, though - even though we
pretend "the interface is a multiaccess interface", it is really only
point-to-point from the OS view, including win-dco.  So whatever
helps to convince windows "send the packet down this pipe" with the
least amount of code and debugging is good :-)

On a "real" multiaccess network, different neighbours would have different
MAC addresses, and more ND activity would happen - but "--dev tun" mode
never is (--dev tap on a --server would be).

gert
Selva Nair Jan. 7, 2023, 5:13 p.m. UTC | #5
On Sat, Jan 7, 2023 at 11:12 AM Gert Doering <gert@greenie.muc.de> wrote:

>
>
> > Though I vaguely understand why we set the address with /128 and then set
> > the route for the vpn subnet with fe80::8 as the gateway for
> tap-windows6,
> > wonder why we do this for dco as well.
>
> Let me recap why we do this for tap-windows - for windows, this is always
> an Ethernet adapter, so it wants to always do neighbour discovery (ND),
> IPv6's equivalent of ARP for all "on this interface" addresses.
>
> For "--dev tun" setups, OpenVPN does not want to deal with "Ethernet
> things",
> so the TAP driver replies to ARP requests for "special addresses" - for
> net30, OpenVPN ioctl()'s the "peer" address (which is also the gateway
> address) into the TAP driver, and only this address is replied to -
> admittedly, I do not know how "topology subnet" works wrt virtual ARP,
> need to read up on this again.
>
> For IPv6, I decided that having to deal with ND is easier if I could just
> look at a magic number, namely fe80::8, in the tap driver code - so, we
> use /128 for the ifconfig bit ("no network here, everything is behind the
> gateway") and point all routing to "on this interface, fe80::8" - so for
> whatever IPv6 destination, windows will only do ND for fe80::8, and the
> TAP driver will reply to it, without any further help (ioctl()) from
> OpenVPN.
>

Thanks for the explanation. I had a vague idea that this is done for ND.


>
>
> This said, I have no idea why we do it that way for wintun and DCO -
> most likely because it avoids having to add even more windows-driver-
> specific special case handling in places where we are nicely generic
> today.
>

I guess it works because nexthop is ignored[*] for tun adapters and we
could have just set an onlink route omitting the gateway parameter.

Selva

[*] Any bogus gateway seems to work, but still, "netsh int ipv6 show
neighbours"
lists any such gateway set for dco as initially "Probe" and then
"Unreachable",
so not totally ignored?
Gert Doering Jan. 8, 2023, 4:11 p.m. UTC | #6
Stared-at-code ("looks reasonable").  Getting rid of netsh.exe calls is
a good thing.

Actually tested!  MinGW builds, on a Win10 system, running openvpn.exe
from a "cmd.exe run as administrator" window, so no iservice involved -
OpenVPN tells me "IPv6 route added using ipapi", and "route print -6"
looks good.

There is something incorrect in the log, though - I connect via IPv6,
and OpenVPN needs to install a redirect host route due to "gateway and
pushed route overlaps".  It is installed correctly, that is, the /128
route points to the LAN interface and that IPv6 default route (fe80::1),
but the log says

add_route_ipv6(2001:608:8003::200/128 -> fe80::1 metric 1) dev LAN-Verbindung 2
IPv6 route added using ipapi
add_route_ipv6(2001:608:8003::/48 -> 2001:608:8003:f:98::1 metric -1) dev LAN-Verbindung 2
IPv6 route added using ipapi

In "route show -6", the first route ends up on "If 8 ... Intel Pro/100" and
the second route on "If 2 ... TAP-Windows Adapter V9 #2" - which is both 
correct, but only the second is "dev LAN-Verbindung 2".  So it seems to
always print the name of the TAP adapter here - which is misleading at
best.


I have also tested "not run as administrator", and it will correctly
error out with
 
  "ROUTE: route addition failed using ipapi: Zugriff verweigert  [status=5 if_index=2]"

(if it gets there at all :-) - without --ifconfig-noexec, it fails
IPv6 ifconfig/netsh already, and never proceeds to route addition - 
which actually brings up the observation that there's netsh.exe calls
left, "netsh.exe interface ipv6 set address 2 2001:608:..." :-) )


Your patch has been applied to the master and release/2.6 branch.

commit dd66958198f7c4dcf7fca0db82ca72996100b3bd (master)
commit 66a3dc3a007c13e7a8d48ef793e046b09d8e6d30 (release/2.6)
Author: Selva Nair
Date:   Wed Jan 4 21:27:16 2023 -0500

     Use IPAPI for setting ipv6 routes when iservice not available

     Signed-off-by: Selva Nair <selva.nair@gmail.com>
     Acked-by: Lev Stipakov <lstipakov@gmail.com>
     Message-Id: <20230105022718.1641751-1-selva.nair@gmail.com>
     URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg25886.html
     Signed-off-by: Gert Doering <gert@greenie.muc.de>


--
kind regards,

Gert Doering
Selva Nair Jan. 8, 2023, 4:46 p.m. UTC | #7
Hi

which actually brings up the observation that there's netsh.exe calls
> left, "netsh.exe interface ipv6 set address 2 2001:608:..." :-) )
>

Yes, it's on my radar :)

Selva

Patch

diff --git a/src/openvpn/route.c b/src/openvpn/route.c
index ded8fec8..eabfe0a5 100644
--- a/src/openvpn/route.c
+++ b/src/openvpn/route.c
@@ -65,6 +65,8 @@  static bool add_route_ipv6_service(const struct route_ipv6 *, const struct tunta
 
 static bool del_route_ipv6_service(const struct route_ipv6 *, const struct tuntap *);
 
+static bool route_ipv6_ipapi(bool add, const struct route_ipv6 *, const struct tuntap *);
+
 #endif
 
 static void delete_route(struct route_ipv4 *r, const struct tuntap *tt, unsigned int flags, const struct route_gateway_info *rgi, const struct env_set *es, openvpn_net_ctx_t *ctx);
@@ -1975,58 +1977,8 @@  add_route_ipv6(struct route_ipv6 *r6, const struct tuntap *tt,
     }
     else
     {
-        DWORD adapter_index;
-        if (r6->adapter_index)          /* vpn server special route */
-        {
-            adapter_index = r6->adapter_index;
-            gateway_needed = true;
-        }
-        else
-        {
-            adapter_index = tt->adapter_index;
-        }
-
-        /* netsh interface ipv6 add route 2001:db8::/32 42 */
-        argv_printf(&argv, "%s%s interface ipv6 add route %s/%d %lu",
-                    get_win_sys_path(),
-                    NETSH_PATH_SUFFIX,
-                    network,
-                    r6->netbits,
-                    adapter_index);
-
-        /* next-hop depends on TUN or TAP mode:
-         * - in TAP mode, we use the "real" next-hop
-         * - in TUN mode we use a special-case link-local address that the tapdrvr
-         *   knows about and will answer ND (neighbor discovery) packets for
-         */
-        if (tt->type == DEV_TYPE_TUN && !gateway_needed)
-        {
-            argv_printf_cat( &argv, " %s", "fe80::8" );
-        }
-        else if (!IN6_IS_ADDR_UNSPECIFIED(&r6->gateway) )
-        {
-            argv_printf_cat( &argv, " %s", gateway );
-        }
-
-#if 0
-        if (r6->flags & RT_METRIC_DEFINED)
-        {
-            argv_printf_cat(&argv, " METRIC %d", r->metric);
-        }
-#endif
-
-        /* in some versions of Windows, routes are persistent across reboots by
-         * default, unless "store=active" is set (pointed out by Tony Lim, thanks)
-         */
-        argv_printf_cat( &argv, " store=active" );
-
-        argv_msg(D_ROUTE, &argv);
-
-        netcmd_semaphore_lock();
-        status = openvpn_execve_check(&argv, es, 0, "ERROR: Windows route add ipv6 command failed");
-        netcmd_semaphore_release();
+        status = route_ipv6_ipapi(true, r6, tt);
     }
-
 #elif defined (TARGET_SOLARIS)
 
     /* example: route add -inet6 2001:db8::/32 somegateway 0 */
@@ -2416,60 +2368,8 @@  delete_route_ipv6(const struct route_ipv6 *r6, const struct tuntap *tt,
     }
     else
     {
-        DWORD adapter_index;
-        if (r6->adapter_index)          /* vpn server special route */
-        {
-            adapter_index = r6->adapter_index;
-            gateway_needed = true;
-        }
-        else
-        {
-            adapter_index = tt->adapter_index;
-        }
-
-        /* netsh interface ipv6 delete route 2001:db8::/32 42 */
-        argv_printf(&argv, "%s%s interface ipv6 delete route %s/%d %lu",
-                    get_win_sys_path(),
-                    NETSH_PATH_SUFFIX,
-                    network,
-                    r6->netbits,
-                    adapter_index);
-
-        /* next-hop depends on TUN or TAP mode:
-         * - in TAP mode, we use the "real" next-hop
-         * - in TUN mode we use a special-case link-local address that the tapdrvr
-         *   knows about and will answer ND (neighbor discovery) packets for
-         * (and "route deletion without specifying next-hop" does not work...)
-         */
-        if (tt->type == DEV_TYPE_TUN && !gateway_needed)
-        {
-            argv_printf_cat( &argv, " %s", "fe80::8" );
-        }
-        else if (!IN6_IS_ADDR_UNSPECIFIED(&r6->gateway) )
-        {
-            argv_printf_cat( &argv, " %s", gateway );
-        }
-
-#if 0
-        if (r6->flags & RT_METRIC_DEFINED)
-        {
-            argv_printf_cat(&argv, "METRIC %d", r->metric);
-        }
-#endif
-
-        /* Windows XP to 7 "just delete" routes, wherever they came from, but
-         * in Windows 8(.1?), if you create them with "store=active", this is
-         * how you should delete them as well (pointed out by Cedric Tabary)
-         */
-        argv_printf_cat( &argv, " store=active" );
-
-        argv_msg(D_ROUTE, &argv);
-
-        netcmd_semaphore_lock();
-        openvpn_execve_check(&argv, es, 0, "ERROR: Windows route delete ipv6 command failed");
-        netcmd_semaphore_release();
+        route_ipv6_ipapi(false, r6, tt);
     }
-
 #elif defined (TARGET_SOLARIS)
 
     /* example: route delete -inet6 2001:db8::/32 somegateway */
@@ -3049,6 +2949,73 @@  do_route_ipv4_service(const bool add, const struct route_ipv4 *r, const struct t
     return do_route_service(add, &msg, sizeof(msg), tt->options.msg_channel);
 }
 
+/* Add or delete an ipv6 route */
+static bool
+route_ipv6_ipapi(const bool add, const struct route_ipv6 *r, const struct tuntap *tt)
+{
+    DWORD err;
+    PMIB_IPFORWARD_ROW2 fwd_row;
+    struct gc_arena gc = gc_new();
+
+    fwd_row = gc_malloc(sizeof(*fwd_row), true, &gc);
+
+    fwd_row->ValidLifetime = 0xffffffff;
+    fwd_row->PreferredLifetime = 0xffffffff;
+    fwd_row->Protocol = MIB_IPPROTO_NETMGMT;
+    fwd_row->Metric = ((r->flags & RT_METRIC_DEFINED) ? r->metric : -1);
+    fwd_row->DestinationPrefix.Prefix.si_family = AF_INET6;
+    fwd_row->DestinationPrefix.Prefix.Ipv6.sin6_addr = r->network;
+    fwd_row->DestinationPrefix.PrefixLength = (UINT8) r->netbits;
+    fwd_row->NextHop.si_family = AF_INET6;
+    fwd_row->NextHop.Ipv6.sin6_addr = r->gateway;
+    fwd_row->InterfaceIndex = r->adapter_index ? r->adapter_index : tt->adapter_index;
+
+    /* In TUN mode we use a special link-local address as the next hop.
+     * The tapdrvr knows about it and will answer neighbor discovery packets.
+     * (only do this for routes actually using the tun/tap device)
+     */
+    if (tt->type == DEV_TYPE_TUN && !r->adapter_index)
+    {
+        inet_pton(AF_INET6, "fe80::8", &fwd_row->NextHop.Ipv6.sin6_addr);
+    }
+
+    /* Use LUID if interface index not available */
+    if (fwd_row->InterfaceIndex == TUN_ADAPTER_INDEX_INVALID && strlen(tt->actual_name))
+    {
+        NET_LUID luid;
+        err = ConvertInterfaceAliasToLuid(wide_string(tt->actual_name, &gc), &luid);
+        if (err != NO_ERROR)
+        {
+            goto out;
+        }
+        fwd_row->InterfaceLuid = luid;
+        fwd_row->InterfaceIndex = 0;
+    }
+
+    if (add)
+    {
+        err = CreateIpForwardEntry2(fwd_row);
+    }
+    else
+    {
+        err = DeleteIpForwardEntry2(fwd_row);
+    }
+
+out:
+    if (err != NO_ERROR)
+    {
+        msg(M_WARN, "ROUTE: route %s failed using ipapi: %s [status=%lu if_index=%lu]",
+            (add ? "addition" : "deletion"), strerror_win32(err, &gc), err, fwd_row->InterfaceIndex);
+    }
+    else
+    {
+        msg(D_ROUTE, "IPv6 route %s using ipapi", add ? "added" : "deleted");
+    }
+    gc_free(&gc);
+
+    return (err == NO_ERROR);
+}
+
 static bool
 do_route_ipv6_service(const bool add, const struct route_ipv6 *r, const struct tuntap *tt)
 {