[Openvpn-devel,2/2] ovpn-dco: introduce FreeBSD data-channel offload support

Message ID 20220224165557.22060-3-kprovost@netgate.com
State Changes Requested
Headers show
Series [Openvpn-devel,1/2] dco: process DCO control packets | expand

Commit Message

Kristof Provost via Openvpn-devel Feb. 24, 2022, 5:55 a.m. UTC
From: Kristof Provost <kp@FreeBSD.org>

Implement data-channel offload for FreeBSD. The implementation and flow
is very similar to that of the Linux DCO support.

Signed-off-by: Kristof Provost <kprovost@netgate.com>
---
 configure.ac               |  15 +-
 src/openvpn/Makefile.am    |   1 +
 src/openvpn/dco_freebsd.c  | 559 +++++++++++++++++++++++++++++++++++++
 src/openvpn/dco_freebsd.h  |  47 ++++
 src/openvpn/dco_internal.h |   1 +
 src/openvpn/forward.c      |   6 +-
 src/openvpn/mtcp.c         |   6 +-
 src/openvpn/multi.c        |  32 ++-
 src/openvpn/options.c      |   4 +-
 src/openvpn/tun.c          |   8 +-
 src/openvpn/tun.h          |   6 +
 11 files changed, 671 insertions(+), 14 deletions(-)
 create mode 100644 src/openvpn/dco_freebsd.c
 create mode 100644 src/openvpn/dco_freebsd.h

Comments

Antonio Quartulli March 8, 2022, 3:16 a.m. UTC | #1
Hi Kristof,

A quick question for you, see below

On 24/02/2022 17:55, Kristof Provost via Openvpn-devel wrote:
> --- a/configure.ac
> +++ b/configure.ac
> @@ -787,7 +787,20 @@ dnl
>   			AC_DEFINE(ENABLE_DCO, 1, [Enable data channel offload for Linux])
>   			AC_MSG_NOTICE([Enabled ovpn-dco support for Linux])
>   		;;
> -
> +		*-*-freebsd*)
> +			DCO_CFLAGS="-I${DCO_SOURCEDIR}"
> +			saved_CFLAGS="${CFLAGS}"
> +			CFLAGS="${CFLAGS} ${DCO_CFLAGS}"
> +			AC_CHECK_HEADERS(
> +					 [if_ovpn.h],
> +					 ,
> +					 [AC_MSG_ERROR([if_ovpn.h is missing (use DCO_SOURCEDIR to set path to it, CFLAGS=${CFLAGS})])]
> +					 )
> +			CFLAGS=${saved_CFLAGS}
> +			LIBS="${LIBS} -lnv"
> +			AC_DEFINE(ENABLE_DCO, 1, [Enable data channel offload for FreeBSD])
> +			AC_MSG_NOTICE([Enabled ovpn-dco support for FreeBSD])
> +		;;

If you double check the latest dco branch, you will see that I have 
dropped the DCO_SOURCEDIR variable and I have rather switched to "let's 
include the kernel API header in the openvpn repository all the time and 
be happy with it".

The idea is that the kernel API will always be backwards compatible, 
therefore having a stale header will never be a problem. It can be 
updated when a new feature is implemented in openvpn itself. (a similar 
approach is taken by hostapd or iw on Linux, where they ship a copy of 
nl80211.h)

On the other hand, we drastically simplify the configure logic and avoid 
having to deal with this variable which may have different pitfalls on 
different platforms.

Do you have any opinion about the above? or shipping a copy of the 
header with openvpn source code is fine with you?

Regards,


p.s. as a side note, the rest of the code contains some "code style" 
issues. Nothing major, but wanted to mention it (we can discuss on IRC 
if you want).
Kristof Provost via Openvpn-devel March 8, 2022, 3:29 a.m. UTC | #2
On 8 Mar 2022, at 15:16, Antonio Quartulli wrote:
> Hi Kristof,
>
> A quick question for you, see below
>
> On 24/02/2022 17:55, Kristof Provost via Openvpn-devel wrote:
>> --- a/configure.ac
>> +++ b/configure.ac
>> @@ -787,7 +787,20 @@ dnl
>>   			AC_DEFINE(ENABLE_DCO, 1, [Enable data channel offload for Linux])
>>   			AC_MSG_NOTICE([Enabled ovpn-dco support for Linux])
>>   		;;
>> -
>> +		*-*-freebsd*)
>> +			DCO_CFLAGS="-I${DCO_SOURCEDIR}"
>> +			saved_CFLAGS="${CFLAGS}"
>> +			CFLAGS="${CFLAGS} ${DCO_CFLAGS}"
>> +			AC_CHECK_HEADERS(
>> +					 [if_ovpn.h],
>> +					 ,
>> +					 [AC_MSG_ERROR([if_ovpn.h is missing (use DCO_SOURCEDIR to set path to it, CFLAGS=${CFLAGS})])]
>> +					 )
>> +			CFLAGS=${saved_CFLAGS}
>> +			LIBS="${LIBS} -lnv"
>> +			AC_DEFINE(ENABLE_DCO, 1, [Enable data channel offload for FreeBSD])
>> +			AC_MSG_NOTICE([Enabled ovpn-dco support for FreeBSD])
>> +		;;
>
> If you double check the latest dco branch, you will see that I have dropped the DCO_SOURCEDIR variable and I have rather switched to "let's include the kernel API header in the openvpn repository all the time and be happy with it".
>
> The idea is that the kernel API will always be backwards compatible, therefore having a stale header will never be a problem. It can be updated when a new feature is implemented in openvpn itself. (a similar approach is taken by hostapd or iw on Linux, where they ship a copy of nl80211.h)
>
> On the other hand, we drastically simplify the configure logic and avoid having to deal with this variable which may have different pitfalls on different platforms.
>
> Do you have any opinion about the above? or shipping a copy of the header with openvpn source code is fine with you?
>
It’s not really too relevant either way for FreeBSD, because the header contains very little information. It’s mostly only the ioctl numbers. There are no structures shared between the kernel and user land. Instead we use nvlists (name/value pair serialisation).

Theoretically I’d prefer to use the OS header, but I can certainly see the upside of not having that dependency. We’ll always have to do the runtime check (dco_available()) anyway, so I can certainly live with that.

> p.s. as a side note, the rest of the code contains some "code style" issues. Nothing major, but wanted to mention it (we can discuss on IRC if you want).
>
I’m happy to fix that. Do you have a rough list of what’s wrong?
https://community.openvpn.net/openvpn/wiki/CodeStyle is the authoritative style guide, right?

Best regards,
Kristof
Antonio Quartulli March 8, 2022, 3:36 a.m. UTC | #3
Hi,

On 08/03/2022 15:29, Kristof Provost wrote:
> It’s not really too relevant either way for FreeBSD, because the header contains very little information. It’s mostly only the ioctl numbers. There are no structures shared between the kernel and user land. Instead we use nvlists (name/value pair serialisation).

Yeah, quite similar to the linux header actually - netlink is indeed a 
list of attribute/value airs. The Linux header contains also the 
attribute names (as they are encoded into an int and must not change 
once defined).

> 
> Theoretically I’d prefer to use the OS header, but I can certainly see the upside of not having that dependency. We’ll always have to do the runtime check (dco_available()) anyway, so I can certainly live with that.
> 
>> p.s. as a side note, the rest of the code contains some "code style" issues. Nothing major, but wanted to mention it (we can discuss on IRC if you want).
>>
> I’m happy to fix that. Do you have a rough list of what’s wrong?
> https://community.openvpn.net/openvpn/wiki/CodeStyle is the authoritative style guide, right?

Correct. Not everything might be there, so feel free to ask for any 
clarification on IRC. Your code is already pretty clean actually.
Some things I have seen:
- We tend to avoid "if (a == NULL)" (or "!= 0") in favor of just "if (a)";
- We avoid spaces after the cast operator;
- we always use brackets {} for one-line blocks;


Regards,
Kristof Provost via Openvpn-devel March 8, 2022, 3:49 a.m. UTC | #4
On 8 Mar 2022, at 15:36, Antonio Quartulli wrote:
> On 08/03/2022 15:29, Kristof Provost wrote:
>> Theoretically I’d prefer to use the OS header, but I can certainly see the upside of not having that dependency. We’ll always have to do the runtime check (dco_available()) anyway, so I can certainly live with that.
>>
>>> p.s. as a side note, the rest of the code contains some "code style" issues. Nothing major, but wanted to mention it (we can discuss on IRC if you want).
>>>
>> I’m happy to fix that. Do you have a rough list of what’s wrong?
>> https://community.openvpn.net/openvpn/wiki/CodeStyle is the authoritative style guide, right?
>
> Correct. Not everything might be there, so feel free to ask for any clarification on IRC. Your code is already pretty clean actually.
> Some things I have seen:
> - We tend to avoid "if (a == NULL)" (or "!= 0") in favor of just "if (a)";
> - We avoid spaces after the cast operator;
> - we always use brackets {} for one-line blocks;
>
Thanks!

Most of those are probably because I’m used to FreeBSD’s style (https://www.freebsd.org/cgi/man.cgi?query=style&apropos=0&sektion=0&manpath=FreeBSD+13.0-RELEASE+and+Ports&arch=default&format=html), but consistency is important.

I’ll update the patch (and also address the SET_TIMEOUT thing) soon. I’ve got a bit of travel coming up, so I’m not quite sure when it’ll be though.

Best regards,
Kristof

Patch

diff --git a/configure.ac b/configure.ac
index cdfb198e..bf1013cc 100644
--- a/configure.ac
+++ b/configure.ac
@@ -787,7 +787,20 @@  dnl
 			AC_DEFINE(ENABLE_DCO, 1, [Enable data channel offload for Linux])
 			AC_MSG_NOTICE([Enabled ovpn-dco support for Linux])
 		;;
-
+		*-*-freebsd*)
+			DCO_CFLAGS="-I${DCO_SOURCEDIR}"
+			saved_CFLAGS="${CFLAGS}"
+			CFLAGS="${CFLAGS} ${DCO_CFLAGS}"
+			AC_CHECK_HEADERS(
+					 [if_ovpn.h],
+					 ,
+					 [AC_MSG_ERROR([if_ovpn.h is missing (use DCO_SOURCEDIR to set path to it, CFLAGS=${CFLAGS})])]
+					 )
+			CFLAGS=${saved_CFLAGS}
+			LIBS="${LIBS} -lnv"
+			AC_DEFINE(ENABLE_DCO, 1, [Enable data channel offload for FreeBSD])
+			AC_MSG_NOTICE([Enabled ovpn-dco support for FreeBSD])
+		;;
 		*-mingw*)
 			AC_MSG_NOTICE([NOTE: --enable-dco ignored on Windows because it's always enabled])
 		;;
diff --git a/src/openvpn/Makefile.am b/src/openvpn/Makefile.am
index b11c3f9e..c4dbf706 100644
--- a/src/openvpn/Makefile.am
+++ b/src/openvpn/Makefile.am
@@ -54,6 +54,7 @@  openvpn_SOURCES = \
 	crypto_openssl.c crypto_openssl.h \
 	crypto_mbedtls.c crypto_mbedtls.h \
 	dco.c dco.h dco_internal.h \
+	dco_freebsd.c dco_freebsd.h \
 	dco_linux.c dco_linux.h \
 	dco_win.c dco_win.h \
 	dhcp.c dhcp.h \
diff --git a/src/openvpn/dco_freebsd.c b/src/openvpn/dco_freebsd.c
new file mode 100644
index 00000000..8e048d74
--- /dev/null
+++ b/src/openvpn/dco_freebsd.c
@@ -0,0 +1,559 @@ 
+/*
+ *  Interface to FreeBSD dco networking code
+ *
+ *  Copyright (C) 2022 Rubicon Communications, LLC (Netgate). All Rights Reserved.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 2
+ *  as published by the Free Software Foundation.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program (see the file COPYING included with this
+ *  distribution); if not, write to the Free Software Foundation, Inc.,
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#elif defined(_MSC_VER)
+#include "config-msvc.h"
+#endif
+
+#if defined(ENABLE_DCO) && defined(TARGET_FREEBSD)
+
+#include "syshead.h"
+
+#include <sys/param.h>
+#include <sys/linker.h>
+#include <sys/nv.h>
+#include <netinet/in.h>
+
+#include "dco_freebsd.h"
+#include "dco.h"
+#include "tun.h"
+#include "crypto.h"
+#include "ssl_common.h"
+
+static nvlist_t *
+sockaddr_to_nvlist(const struct sockaddr *sa)
+{
+    nvlist_t *nvl = nvlist_create(0);
+
+    nvlist_add_number(nvl, "af", sa->sa_family);
+
+    switch (sa->sa_family) {
+    case AF_INET:
+    {
+        const struct sockaddr_in *in = (const struct sockaddr_in *)sa;
+        nvlist_add_binary(nvl, "address", &in->sin_addr, sizeof(in->sin_addr));
+        nvlist_add_number(nvl, "port", in->sin_port);
+        break;
+    }
+    case AF_INET6:
+    {
+        const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)sa;
+        nvlist_add_binary(nvl, "address", &in6->sin6_addr, sizeof(in6->sin6_addr));
+        nvlist_add_number(nvl, "port", in6->sin6_port);
+        break;
+    }
+    default:
+        abort();
+    }
+
+    return (nvl);
+}
+
+int
+dco_new_peer(dco_context_t *dco, unsigned int peerid, int sd,
+    struct sockaddr *localaddr, struct sockaddr *remoteaddr,
+    struct in_addr *remote_in4, struct in6_addr *remote_in6)
+{
+    struct ifdrv drv;
+    nvlist_t *nvl;
+    int ret;
+
+    nvl = nvlist_create(0);
+
+    msg(D_DCO_DEBUG, "%s: peer-id %d, fd %d", __func__, peerid, sd);
+
+    if (localaddr)
+        nvlist_add_nvlist(nvl, "local", sockaddr_to_nvlist(localaddr));
+
+    if (remoteaddr)
+        nvlist_add_nvlist(nvl, "remote", sockaddr_to_nvlist(remoteaddr));
+
+    nvlist_add_number(nvl, "fd", sd);
+
+    bzero(&drv, sizeof(drv));
+    snprintf(drv.ifd_name, IFNAMSIZ, "%s", dco->ifname);
+    drv.ifd_cmd = OVPN_SET_PEER;
+    drv.ifd_data = nvlist_pack(nvl, &drv.ifd_len);
+
+    ret = ioctl(dco->fd, SIOCSDRVSPEC, &drv);
+    free(drv.ifd_data);
+    nvlist_destroy(nvl);
+    if (ret)
+    {
+        msg(D_DCO, "Failed to create new peer %d", errno);
+        return ret;
+    }
+
+    return 0;
+}
+
+static int
+open_fd(dco_context_t *dco)
+{
+    int ret;
+
+    ret = pipe2(dco->pipefd, O_CLOEXEC);
+    if (ret != 0)
+        return -1;
+
+    dco->fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    dco->dco_packet_in = alloc_buf(PAGE_SIZE);
+
+    return dco->fd;
+}
+
+static void
+close_fd(dco_context_t *dco)
+{
+    close(dco->pipefd[0]);
+    close(dco->pipefd[1]);
+    close(dco->fd);
+}
+
+bool
+ovpn_dco_init(dco_context_t *dco)
+{
+    if (open_fd(dco) < 0)
+    {
+        msg(D_DCO, "Failed to open socket");
+        return false;
+    }
+    return true;
+}
+
+static int
+create_interface(struct tuntap *tt, const char *dev)
+{
+    int ret;
+    struct ifreq ifr;
+
+    bzero(&ifr, sizeof(ifr));
+
+    /* Attempt to load the module. Ignore errors, because it might already be
+     * loaded, or built into the kernel. */
+    (void)kldload("if_ovpn");
+
+    /* Create ovpnx first, then rename it. */
+    snprintf(ifr.ifr_name, IFNAMSIZ, "ovpn");
+    ret = ioctl(tt->dco.fd, SIOCIFCREATE2, &ifr);
+    if (ret)
+    {
+        msg(D_DCO, "Failed to create interface %s: %d", ifr.ifr_name, errno);
+        return ret;
+    }
+
+    /* Rename */
+    if (! strcmp(dev, "tun"))
+        ifr.ifr_data = "ovpn";
+    else
+        ifr.ifr_data = dev;
+    ret = ioctl(tt->dco.fd, SIOCSIFNAME, &ifr);
+    if (ret)
+    {
+        /* Delete the created interface again. */
+        (void)ioctl(tt->dco.fd, SIOCIFDESTROY, &ifr);
+        msg(D_DCO, "Failed to create interface %s: %d", ifr.ifr_data, errno);
+        return ret;
+    }
+
+    snprintf(tt->dco.ifname, IFNAMSIZ, "%s", ifr.ifr_data);
+    tt->actual_name = string_alloc(tt->dco.ifname, NULL);
+
+    return 0;
+}
+
+    static int
+remove_interface(struct tuntap *tt)
+{
+    int ret;
+    struct ifreq ifr;
+
+    bzero(&ifr, sizeof(ifr));
+    snprintf(ifr.ifr_name, IFNAMSIZ, "%s", tt->dco.ifname);
+
+    ret = ioctl(tt->dco.fd, SIOCIFDESTROY, &ifr);
+    if (ret)
+    {
+        msg(D_DCO, "Failed to remove interface %s: %d", ifr.ifr_name, errno);
+        return ret;
+    }
+
+    tt->dco.ifname[0] = 0;
+
+    return 0;
+}
+
+int
+open_tun_dco(struct tuntap *tt, openvpn_net_ctx_t *ctx, const char *dev)
+{
+    int ret;
+
+    ret = create_interface(tt, dev);
+
+    if (ret < 0)
+        msg(D_DCO, "Failed to create interface");
+
+    return ret;
+}
+
+void
+close_tun_dco(struct tuntap *tt, openvpn_net_ctx_t *ctx)
+{
+    remove_interface(tt);
+    close_fd(&tt->dco);
+}
+
+int
+dco_swap_keys(dco_context_t *dco, unsigned int peerid)
+{
+    struct ifdrv drv;
+    int ret;
+
+    msg(D_DCO_DEBUG, "%s: peer-id %d", __func__, peerid);
+
+    bzero(&drv, sizeof(drv));
+    snprintf(drv.ifd_name, IFNAMSIZ, "%s", dco->ifname);
+    drv.ifd_cmd = OVPN_SWAP_KEYS;
+
+    ret = ioctl(dco->fd, SIOCSDRVSPEC, &drv);
+    if (ret)
+    {
+        msg(D_DCO, "Failed to swap keys %d", errno);
+        return ret;
+    }
+
+    return 0;
+}
+
+int
+dco_del_peer(dco_context_t *dco, unsigned int peerid)
+{
+    struct ifdrv drv;
+    int ret;
+
+    bzero(&drv, sizeof(drv));
+    snprintf(drv.ifd_name, IFNAMSIZ, "%s", dco->ifname);
+    drv.ifd_cmd = OVPN_DEL_PEER;
+
+    ret = ioctl(dco->fd, SIOCSDRVSPEC, &drv);
+    if (ret)
+    {
+        msg(D_DCO, "Failed to delete peer %d", errno);
+        return ret;
+    }
+
+    return 0;
+}
+
+int
+dco_del_key(dco_context_t *dco, unsigned int peerid,
+    dco_key_slot_t slot)
+{
+    struct ifdrv drv;
+    nvlist_t *nvl;
+    int ret;
+
+    msg(D_DCO, "%s: peer-id %d, slot %d", __func__, peerid, slot);
+
+    nvl = nvlist_create(0);
+    nvlist_add_number(nvl, "slot", slot);
+    nvlist_add_number(nvl, "peerid", peerid);
+
+    bzero(&drv, sizeof(drv));
+    snprintf(drv.ifd_name, IFNAMSIZ, "%s", dco->ifname);
+    drv.ifd_cmd = OVPN_DEL_KEY;
+    drv.ifd_data = nvlist_pack(nvl, &drv.ifd_len);
+
+    ret = ioctl(dco->fd, SIOCSDRVSPEC, &drv);
+    free(drv.ifd_data);
+    nvlist_destroy(nvl);
+    if (ret)
+    {
+        msg(D_DCO, "Failed to delete key %d", errno);
+        return ret;
+    }
+
+    return 0;
+}
+
+static nvlist_t *
+key_to_nvlist(const uint8_t *key, const uint8_t *implicit_iv, const char *ciphername)
+{
+    nvlist_t *nvl;
+    size_t key_len;
+
+    nvl = nvlist_create(0);
+
+    nvlist_add_string(nvl, "cipher", ciphername);
+
+    if (strcmp(ciphername, "none") != 0)
+    {
+        key_len = cipher_kt_key_size(ciphername);
+
+        nvlist_add_binary(nvl, "key", key, key_len);
+        nvlist_add_binary(nvl, "iv", implicit_iv, 8);
+    }
+
+    return (nvl);
+}
+
+static int
+start_tun(dco_context_t *dco)
+{
+    struct ifdrv drv;
+    int ret;
+
+    bzero(&drv, sizeof(drv));
+    snprintf(drv.ifd_name, IFNAMSIZ, "%s", dco->ifname);
+    drv.ifd_cmd = OVPN_START_VPN;
+
+    ret = ioctl(dco->fd, SIOCSDRVSPEC, &drv);
+    if (ret)
+    {
+        msg(D_DCO, "Failed to start vpn %d", errno);
+        return ret;
+    }
+
+    return 0;
+}
+
+int
+dco_new_key(dco_context_t *dco, unsigned int peerid, int keyid,
+    dco_key_slot_t slot,
+    const uint8_t *encrypt_key, const uint8_t *encrypt_iv,
+    const uint8_t *decrypt_key, const uint8_t *decrypt_iv,
+    const char *ciphername)
+{
+    struct ifdrv drv;
+    nvlist_t *nvl;
+    int ret;
+
+    msg(D_DCO_DEBUG, "%s: slot %d, key-id %d, peer-id %d, cipher %s",
+        __func__, slot, keyid, peerid, ciphername);
+
+    nvl = nvlist_create(0);
+
+    nvlist_add_number(nvl, "slot", slot);
+    nvlist_add_number(nvl, "keyid", keyid);
+    nvlist_add_number(nvl, "peerid", peerid);
+
+    nvlist_add_nvlist(nvl, "encrypt",
+        key_to_nvlist(encrypt_key, encrypt_iv, ciphername));
+    nvlist_add_nvlist(nvl, "decrypt",
+        key_to_nvlist(decrypt_key, decrypt_iv, ciphername));
+
+    bzero(&drv, sizeof(drv));
+    snprintf(drv.ifd_name, IFNAMSIZ, "%s", dco->ifname);
+    drv.ifd_cmd = OVPN_NEW_KEY;
+    drv.ifd_data = nvlist_pack(nvl, &drv.ifd_len);
+
+    ret = ioctl(dco->fd, SIOCSDRVSPEC, &drv);
+    free(drv.ifd_data);
+    nvlist_destroy(nvl);
+    if (ret) {
+        msg(D_DCO, "Failed to set key %d", errno);
+        return ret;
+    }
+
+    return start_tun(dco);
+}
+
+int
+ovpn_set_peer(dco_context_t *dco, unsigned int peerid,
+    unsigned int keepalive_interval, unsigned int keepalive_timeout)
+{
+    struct ifdrv drv;
+    nvlist_t *nvl;
+    int ret;
+
+    nvl = nvlist_create(0);
+    nvlist_add_number(nvl, "interval", keepalive_interval);
+    nvlist_add_number(nvl, "timeout", keepalive_timeout);
+
+    bzero(&drv, sizeof(drv));
+    snprintf(drv.ifd_name, IFNAMSIZ, "%s", dco->ifname);
+    drv.ifd_cmd = OVPN_SET_TIMEOUT;
+    drv.ifd_data = nvlist_pack(nvl, &drv.ifd_len);
+
+    ret = ioctl(dco->fd, SIOCSDRVSPEC, &drv);
+    free(drv.ifd_data);
+    nvlist_destroy(nvl);
+    if (ret)
+    {
+        msg(D_DCO, "Failed to set keepalive %d", errno);
+        return ret;
+    }
+
+    return 0;
+}
+
+int
+dco_do_read(dco_context_t *dco)
+{
+    struct ifdrv drv;
+    uint8_t buf[4096];
+    nvlist_t *nvl;
+    const uint8_t *pkt;
+    size_t pktlen;
+    int ret;
+
+    bzero(&drv, sizeof(drv));
+    snprintf(drv.ifd_name, IFNAMSIZ, "%s", dco->ifname);
+    drv.ifd_cmd = OVPN_GET_PKT;
+    drv.ifd_data = buf;
+    drv.ifd_len = sizeof(buf);
+
+    ret = ioctl(dco->fd, SIOCGDRVSPEC, &drv);
+    if (ret)
+    {
+        msg(D_DCO, "Failed to read control packet %d", errno);
+        return errno;
+    }
+
+    nvl = nvlist_unpack(buf, drv.ifd_len, 0);
+    if (nvl == NULL)
+    {
+        msg(D_DCO, "Failed to unpack nvlist");
+        return EINVAL;
+    }
+
+    dco->dco_message_peer_id = nvlist_get_number(nvl, "peerid");
+
+    pkt = nvlist_get_binary(nvl, "packet", &pktlen);
+    memcpy(BPTR(&dco->dco_packet_in), pkt, pktlen);
+    dco->dco_packet_in.len = pktlen;
+
+    nvlist_destroy(nvl);
+
+    return 0;
+}
+
+int
+dco_do_write(dco_context_t *dco, int peer_id, struct buffer *buf)
+{
+    struct ifdrv drv;
+    nvlist_t *nvl;
+    int ret;
+
+    nvl = nvlist_create(0);
+
+    nvlist_add_binary(nvl, "packet", BSTR(buf), BLEN(buf));
+    nvlist_add_number(nvl, "peerid", peer_id);
+
+    bzero(&drv, sizeof(drv));
+    snprintf(drv.ifd_name, IFNAMSIZ, "%s", dco->ifname);
+    drv.ifd_cmd = OVPN_SEND_PKT;
+    drv.ifd_data = nvlist_pack(nvl, &drv.ifd_len);
+
+    ret = ioctl(dco->fd, SIOCSDRVSPEC, &drv);
+    free(drv.ifd_data);
+    nvlist_destroy(nvl);
+    if (ret)
+    {
+        msg(D_DCO, "Failed to send control packet %d", errno);
+        return ret;
+    }
+
+    return BLEN(buf);
+}
+
+bool
+dco_available(int msglevel)
+{
+    struct if_clonereq ifcr;
+    char *buf = NULL;
+    int fd;
+    int ret;
+    bool available = false;
+
+    fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+    if (fd < 0)
+        return false;
+
+    bzero(&ifcr, sizeof(ifcr));
+
+    ret = ioctl(fd, SIOCIFGCLONERS, &ifcr);
+    if (ret != 0)
+        goto out;
+
+    buf = malloc(ifcr.ifcr_total * IFNAMSIZ);
+
+    ifcr.ifcr_count = ifcr.ifcr_total;
+    ifcr.ifcr_buffer = buf;
+    ret = ioctl(fd, SIOCIFGCLONERS, &ifcr);
+    if (ret != 0)
+        goto out;
+
+    for (int i = 0; i < ifcr.ifcr_total; i++)
+    {
+        if (strcmp(buf + (i * IFNAMSIZ), "ovpn") == 0)
+        {
+            available = true;
+            goto out;
+        }
+    }
+
+out:
+    free(buf);
+    close(fd);
+
+    return available;
+}
+
+void
+dco_event_set(dco_context_t *dco, struct event_set *es, void *arg)
+{
+    struct ifdrv drv;
+    nvlist_t *nvl;
+    uint8_t buf[128];
+    int ret;
+
+    bzero(&drv, sizeof(drv));
+    snprintf(drv.ifd_name, IFNAMSIZ, "%s", dco->ifname);
+    drv.ifd_cmd = OVPN_POLL_PKT;
+    drv.ifd_len = sizeof(buf);
+    drv.ifd_data = buf;
+
+    ret = ioctl(dco->fd, SIOCGDRVSPEC, &drv);
+    if (ret)
+    {
+        msg(D_DCO, "Failed to poll for packets %d", errno);
+        return;
+    }
+
+    nvl = nvlist_unpack(buf, drv.ifd_len, 0);
+    if (nvl == NULL)
+    {
+        msg(D_DCO, "Failed to unpack nvlist");
+        return;
+    }
+
+    if (nvlist_get_number(nvl, "pending") > 0)
+    {
+        (void)write(dco->pipefd[0], " ", 1);
+        event_ctl(es, dco->pipefd[1], EVENT_READ, arg);
+    }
+
+    nvlist_destroy(nvl);
+}
+
+#endif /* defined(ENABLE_DCO) && defined(TARGET_FREEBSD) */
diff --git a/src/openvpn/dco_freebsd.h b/src/openvpn/dco_freebsd.h
new file mode 100644
index 00000000..e7aee274
--- /dev/null
+++ b/src/openvpn/dco_freebsd.h
@@ -0,0 +1,47 @@ 
+/*
+ *  Interface to FreeBSD dco networking code
+ *
+ *  Copyright (C) 2022 Rubicon Communications, LLC (Netgate). All Rights Reserved.
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License version 2
+ *  as published by the Free Software Foundation.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program (see the file COPYING included with this
+ *  distribution); if not, write to the Free Software Foundation, Inc.,
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+#ifndef DCO_FREEBSD_H
+#define DCO_FREEBSD_H
+
+#if defined(ENABLE_DCO) && defined(TARGET_FREEBSD)
+
+#include <if_ovpn.h>
+#include <buffer.h>
+#include "event.h"
+
+#define DCO_SUPPORTED_CIPHERS "none:AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305"
+
+typedef enum ovpn_key_slot dco_key_slot_t;
+typedef enum ovpn_key_cipher dco_cipher_t;
+
+typedef struct dco_context {
+	int fd;
+	int pipefd[2];
+
+	char ifname[IFNAMSIZ];
+
+	struct buffer dco_packet_in;
+
+	int dco_message_peer_id;
+	int dco_del_peer_reason;
+} dco_context_t;
+
+#endif /* defined(ENABLE_DCO) && defined(TARGET_FREEBSD) */
+#endif /* ifndef DCO_FREEBSD_H */
diff --git a/src/openvpn/dco_internal.h b/src/openvpn/dco_internal.h
index ed1a1cff..80d6821a 100644
--- a/src/openvpn/dco_internal.h
+++ b/src/openvpn/dco_internal.h
@@ -27,6 +27,7 @@ 
 
 #if defined(ENABLE_DCO)
 
+#include "dco_freebsd.h"
 #include "dco_linux.h"
 #include "dco_win.h"
 
diff --git a/src/openvpn/forward.c b/src/openvpn/forward.c
index c16f32fc..b26c8e60 100644
--- a/src/openvpn/forward.c
+++ b/src/openvpn/forward.c
@@ -1639,7 +1639,7 @@  process_outgoing_link(struct context *c)
                 socks_preprocess_outgoing_link(c, &to_addr, &size_delta);
 
                 /* Send packet */
-#ifdef TARGET_LINUX
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
                 if (c->c2.link_socket->info.dco_installed)
                 {
                     size = dco_do_write(&c->c1.tuntap->dco,
@@ -1914,7 +1914,7 @@  io_wait_dowork(struct context *c, const unsigned int flags)
 #ifdef ENABLE_ASYNC_PUSH
     static int file_shift = FILE_SHIFT;
 #endif
-#ifdef TARGET_LINUX
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
     static int dco_shift = DCO_SHIFT;    /* Event from DCO linux kernel module */
 #endif
 
@@ -2024,7 +2024,7 @@  io_wait_dowork(struct context *c, const unsigned int flags)
      */
     socket_set(c->c2.link_socket, c->c2.event_set, socket, (void *)&socket_shift, NULL);
     tun_set(c->c1.tuntap, c->c2.event_set, tuntap, (void *)&tun_shift, NULL);
-#if defined(TARGET_LINUX)
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
     if (socket & EVENT_READ && c->c2.did_open_tun)
     {
         dco_event_set(&c->c1.tuntap->dco, c->c2.event_set, (void *)&dco_shift);
diff --git a/src/openvpn/mtcp.c b/src/openvpn/mtcp.c
index b4445dbe..43573e5b 100644
--- a/src/openvpn/mtcp.c
+++ b/src/openvpn/mtcp.c
@@ -281,7 +281,7 @@  multi_tcp_wait(const struct context *c,
     }
 #endif
     tun_set(c->c1.tuntap, mtcp->es, EVENT_READ, MTCP_TUN, persistent);
-#if defined(TARGET_LINUX)
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
     dco_event_set(&c->c1.tuntap->dco, mtcp->es, MTCP_DCO);
 #endif
 
@@ -400,7 +400,7 @@  multi_tcp_wait_lite(struct multi_context *m, struct multi_instance *mi, const in
 
     tv_clear(&c->c2.timeval); /* ZERO-TIMEOUT */
 
-#if defined(TARGET_LINUX)
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
     if (mi && mi->context.c2.link_socket->info.dco_installed)
     {
         /* If we got a socket that has been handed over to the kernel
@@ -763,7 +763,7 @@  multi_tcp_process_io(struct multi_context *m)
                     multi_tcp_action(m, mi, TA_INITIAL, false);
                 }
             }
-#if defined(ENABLE_DCO) && defined(TARGET_LINUX)
+#if defined(ENABLE_DCO) && (defined(TARGET_LINUX) || defined(TARGET_FREEBSD))
             /* incoming data on DCO? */
             else if (e->arg == MTCP_DCO)
             {
diff --git a/src/openvpn/multi.c b/src/openvpn/multi.c
index 5054c4dd..908e5d35 100644
--- a/src/openvpn/multi.c
+++ b/src/openvpn/multi.c
@@ -3138,7 +3138,8 @@  multi_signal_instance(struct multi_context *m, struct multi_instance *mi, const
 }
 #endif
 
-#if defined(ENABLE_DCO) && defined(TARGET_LINUX)
+#if defined(ENABLE_DCO)
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
 static void
 process_incoming_dco_packet(struct multi_context *m, struct multi_instance *mi,  dco_context_t *dco)
 {
@@ -3166,7 +3167,9 @@  process_incoming_dco_packet(struct multi_context *m, struct multi_instance *mi,
     done:
     buf_init(&dco->dco_packet_in, 0);
 }
+#endif
 
+#if defined(TARGET_LINUX)
 static void
 process_incoming_del_peer(struct multi_context *m, struct multi_instance *mi, dco_context_t *dco)
 {
@@ -3225,6 +3228,33 @@  multi_process_incoming_dco(struct multi_context *m)
     dco->dco_message_peer_id = -1;
     return ret > 0;
 }
+
+#elif defined(TARGET_FREEBSD)
+
+bool
+multi_process_incoming_dco(struct multi_context *m)
+{
+    dco_context_t *dco = &m->top.c1.tuntap->dco;
+
+    struct multi_instance *mi = NULL;
+
+    int ret = dco_do_read(&m->top.c1.tuntap->dco);
+    int peer_id = dco->dco_message_peer_id;
+
+    if ((peer_id >= 0) && (peer_id < m->max_clients) && (m->instances[peer_id]))
+    {
+        mi = m->instances[peer_id];
+        process_incoming_dco_packet(m, mi, dco);
+    }
+    else
+    {
+        msg(D_DCO, "Received packet for peer-id unknown to OpenVPN: %d" , peer_id);
+    }
+
+    dco->dco_message_peer_id = -1;
+    return ret > 0;
+}
+#endif
 #endif
 
 /*
diff --git a/src/openvpn/options.c b/src/openvpn/options.c
index 816d8db8..ab0147b0 100644
--- a/src/openvpn/options.c
+++ b/src/openvpn/options.c
@@ -3343,7 +3343,7 @@  options_postprocess_mutate(struct options *o)
     }
 
     /* check if any option should force disabling DCO */
-#if defined(TARGET_LINUX)
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
     o->tuntap_options.disable_dco = dco_check_option_conflict(D_DCO, o);
 #endif
 
@@ -5668,7 +5668,7 @@  add_option(struct options *options,
 #endif
     else if (streq(p[0], "disable-dco") || streq(p[0], "dco-disable"))
     {
-#if defined(TARGET_LINUX)
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
         options->tuntap_options.disable_dco = true;
 #endif
     }
diff --git a/src/openvpn/tun.c b/src/openvpn/tun.c
index 9d0be713..c89e635c 100644
--- a/src/openvpn/tun.c
+++ b/src/openvpn/tun.c
@@ -1796,7 +1796,7 @@  open_tun_generic(const char *dev, const char *dev_type, const char *dev_node,
                                      "/dev/%s%d", dev, i);
                     openvpn_snprintf(dynamic_name, sizeof(dynamic_name),
                                      "%s%d", dev, i);
-#ifdef TARGET_LINUX
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
                     if (!tt->options.disable_dco)
                     {
                         if (open_tun_dco(tt, ctx, dynamic_name) == 0)
@@ -1829,7 +1829,7 @@  open_tun_generic(const char *dev, const char *dev_type, const char *dev_node,
             }
         }
 
-#ifdef TARGET_LINUX
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
         if (!tt->options.disable_dco)
         {
             if (!dynamic_opened)
@@ -2009,7 +2009,7 @@  open_tun(const char *dev, const char *dev_type, const char *dev_node, struct tun
     {
         open_null(tt);
     }
-#if defined(TARGET_LINUX)
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
     else if (!tt->options.disable_dco)
     {
         open_tun_generic(dev, dev_type, NULL, true, tt, ctx);
@@ -2265,7 +2265,7 @@  close_tun(struct tuntap *tt, openvpn_net_ctx_t *ctx)
         net_ctx_reset(ctx);
     }
 
-#ifdef TARGET_LINUX
+#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
     if (!tt->options.disable_dco)
     {
         close_tun_dco(tt, ctx);
diff --git a/src/openvpn/tun.h b/src/openvpn/tun.h
index fee2c61c..4490ae9a 100644
--- a/src/openvpn/tun.h
+++ b/src/openvpn/tun.h
@@ -145,6 +145,12 @@  struct tuntap_options {
     bool disable_dco;
 };
 
+#elif defined(TARGET_FREEBSD)
+
+struct tuntap_options {
+    bool disable_dco;
+};
+
 #else  /* if defined(_WIN32) || defined(TARGET_ANDROID) */
 
 struct tuntap_options {