Remarks inline. Mostly ACK.

I’ll post an updated version soon. (I’ve also added a check for UDP in 
dco_check_option_conflict_ce().

On 10 Aug 2022, at 18:32, Gert Doering wrote:
> On Mon, Aug 08, 2022 at 04:34:23PM +0200, Kristof Provost via Openvpn-devel 
> wrote:
>> diff --git a/src/openvpn/dco_freebsd.c b/src/openvpn/dco_freebsd.c
>> new file mode 100644
>> index 00000000..a8a53fe3
>> --- /dev/null
>> +++ b/src/openvpn/dco_freebsd.c
>> @@ -0,0 +1,636 @@
> [..]
>> +static nvlist_t *
>> +sockaddr_to_nvlist(const struct sockaddr *sa)
>> +{
> [..]
>> +        default:
>> +            abort();
>
> please use "ASSERT(0);" here.  It will do the same thing, but if ever
> hit, it will print a __FILE__, __LINE__ message to the log, so it's
> easier for us to see *where* it triggered a "this must never happen"
> condition.
>
Fixed.

>> +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);
>
> We use C99 these days, so this could be written as
>
>> +    nvlist_t *nvl = nvlist_create(0);
>
> but this is a matter of personal preference.  Both are fine, just wanted
> to point out that the option exists.
>
That’s FreeBSD’s style(9) showing here. If it’s acceptable to OpenVPN I’m just 
going to keep it as-is.

>> +
>> +    nvlist_add_number(nvl, "fd", sd);
>> +    nvlist_add_number(nvl, "peerid", peerid);
>> +
>> +    bzero(&drv, sizeof(drv));
>> +    snprintf(drv.ifd_name, IFNAMSIZ, "%s", dco->ifname);
>> +    drv.ifd_cmd = OVPN_NEW_PEER;
>> +    drv.ifd_data = nvlist_pack(nvl, &drv.ifd_len);
>> +
>> +    ret = ioctl(dco->fd, SIOCSDRVSPEC, &drv);
>> +    free(drv.ifd_data);
>
> What happens should nvlist_pack() fail here?  Manpage says it returns
> NULL, will the ioctl() handle this gracefully, or will something crash?
>
That’d really only happen if a memory allocation fails, and given modern OS’s 
overcommit behaviour that’s also not going to happen. (If you manage to run the 
system out of memory malloc() will still give you virtual memory, but the OOM 
killer will come to visit when you get around to actually using it.)

> (If the ioctl() returns something like EINVAL in this case, perfectly fine)
>
The copyin() in the kernel will fail, and that’d return an error, without 
crashing things.
So, yes, that’s what’d happen.

>> +    nvlist_destroy(nvl);
>> +    if (ret)
>> +    {
>> +        msg(D_DCO, "Failed to create new peer %d", errno);
>> +        return ret;
>> +    }
>> +
>> +    return 0;
>> +}
>
> Most functions in OpenVPN that do use a "ret" variable would look
> more like this:
>
>> +    if (ret)
>> +    {
>> +        msg(D_DCO, "Failed to create new peer %d", errno);
>> +    }
>> +
>> +    return ret;
>> +}
>
> we do have all sorts of variants (*sigh*), but since you also used both
> versions, let's go for this one.
>
Fixed.

>> +        msg(D_DCO, "Failed to create new peer %d", errno);
>
> this can be written as
>
>> +        msg(D_DCO|M_ERRNO, "Failed to create new peer");
>
> M_ERRNO will automatically append the strerror(errno) stuff.
>
Oh neat. I’ve made that change.

> D_DCO is "--verb 3", so that message won't be visible in normal operation
> - if we consider this an error, then it should be
>
>> +        msg(M_WARN|M_ERRNO, "Failed to create new peer");
>
> instead (or if it's "fatal error, give up" -> M_ERR).
>
These are generally fatal errors, yes.
There are follow-up error messages from the calling code that do get logged, 
but they’re usually less clear about what the actual problem is. I’ll see about 
changing D_DCO -> M_ERR or M_WARN.

> Now, I'm not sure if accessing errno after the free() call is guaranteed
> to be save - so maybe move the msg() call up?
>
I’m pretty sure free() doesn’t mess with errno (as in, I’ve looked at the 
jemalloc code), but it’s better to not have to check.
Happily, thanks to your earlier suggestion it’s much easier to just move the 
free after the log, so I’ll do that.

>> +bool
>> +ovpn_dco_init(int mode, dco_context_t *dco)
>> +{
>> +    if (open_fd(dco) < 0)
>> +    {
>> +        msg(D_DCO, "Failed to open socket");
>
> Same here - if you want this to be heard, D_DCO should be M_WARN|M_ERRNO.
>
> If this is just informational, because the caller will make sufficient
> noise, D_DCO|M_ERRNO is good (but you might want to point out that this
> is the "DCO socket" that couldn't be opened).
>
Yeah, Iv’e gone through and made them all a bit louder. It’s all failure 
conditions, so being overly verbose there isn’t really an issue.

>> +static int
>> +create_interface(struct tuntap *tt, const char *dev)
>> +{
>> +    int ret;
>> +    struct ifreq ifr;
>> +
>> +    bzero(&ifr, sizeof(ifr));
>
> There is a convenience macro here, CLEAR(ifr)  (which does exactly
> this - #define CLEAR(x) memset(&(x), 0, sizeof(x)).
>
Neat. Changed.

>> +    /* 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;
>> +    }
>
> -> M_ERRNO
Done.
>
>> +    /* Rename */
>> +    if (!strcmp(dev, "tun"))
>> +    {
>> +        ifr.ifr_data = "ovpn";
>> +    }
>> +    else
>> +    {
>> +        ifr.ifr_data = (char *)dev;
>> +    }
>
> I'm not sure I understand this code part.  When would dev be "tun"
> here, triggering a rename to "ovpn"?
>
> The call chain leading to this is (now)
>
>  - tun.c:open_tun_dco_generic()
>    - open_tun_dco()
>      - create_interface(tt,dev)
>
> and tun_dco_generic() should never pass in a bare "tun" name (because
> in that case, it would iterate "tun0, tun1, tun2").
>
> Is this a safeguard against "bad things will happen in kernel land if
> we use the unqualified name of another driver"?
>
No, it’s that OpenVPN lets you pick the interface name, and FreeBSD figures out 
which driver it needs to call based on the interface name.
So as long as users decide they want the interface to be ovpnX it’ll be fine, 
but if they want ‘vpn0’ or even something silly like ‘vlan1’ the kernel isn’t 
going to create an if_ovpn interface.

So we create opvnX first, and then rename it to whatever the user wants it to 
be.

>> +    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);
>
> -> M_ERRNO
>
> (there's more, but no need to point them out individually :-) )
>
Fixed.

>> +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;
>> +}
>
> Maybe always clear the ifname, and do "return ret;" here too?
>
Good idea.

>> +static int
>> +start_tun(dco_context_t *dco)
>> +{
>> +    struct ifdrv drv;
>> +    int ret;
>> +
>> +    bzero(&drv, sizeof(drv));
>
> CLEAR(drv);
>
Fixed.

>> +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;
>> +
>> +    /* Flush any pending data from the pipe. */
>> +    (void)read(dco->pipefd[1], buf, sizeof(buf));
>> +
>> +    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;
>
> Documentation (dco.h) says "0 on success or a negative error code otherwise",
> so this needs to be "return -errno;" to behave the same as dco_linux
>
Fixed.
>> +    }
>> +
>> +    nvl = nvlist_unpack(buf, drv.ifd_len, 0);
>> +    if (!nvl)
>> +    {
>> +        msg(D_DCO, "Failed to unpack nvlist");
>> +        return EINVAL;
>
> -EINVAL
>
Fixed.

>> +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);
>> +}
>
> The return code of dco_do_write() is not clearly defined in dco.h
> (Antonio, looking at you...!) but the linux version seems to do
> "negative is error, positive is buffer length written", so this should
> be fine.  Might need a change to "return -errno".
>
Makes sense. Done.

>> +bool
>> +dco_available(int msglevel)
>> +{
>> +    struct if_clonereq ifcr;
>> +    char *buf = NULL;
>> +    int fd;
>> +    int ret;
>> +    bool available = false;
>> +
>> +    /* Attempt to load the module. Ignore errors, because it might already 
>> be
>> +     * loaded, or built into the kernel. */
>> +    (void)kldload("if_ovpn");
>> +
>> +    fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
>> +    if (fd < 0)
>> +    {
>> +        return false;
>> +    }
>> +
>> +    bzero(&ifcr, sizeof(ifcr));
>
> CLEAR(ifcr)
>
Fixed.

>> +    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), "openvpn") == 0)
>
> This looks fairly magic to me - am I reading this right that the first
> call returns a number of "somethings", then we malloc(something * IFNAMSIZ),
> and the second call returns a list of strings that describe "something",
> and if there is one of them named "openvpn", we know that DCO is
> available?
>
Correct. The first call is to figure out how much space we need, we then 
allocate that and get the list.

> Why is it called "openvpn", not "ovpn" (if_ovpn, man ovpn)?
>
That’s what the driver chose to do. It’s a bit cheap of me to blame the driver 
author, but he’s an ass anyway ;)
More seriously, that’s mostly because that also ensures that the interfaces are 
automatically in the ‘openvpn’ netif group, which is something that makes 
pfsense’s life easier.

> (A few lines of comment on what SIOCIFGCLONERS does - or which manpage
> to look at - would be appreciated here :-) )
>
It’s a BSD-ism. Some types of interfaces (essentially any interface that’s not 
actual hardware) is created through ‘cloning’. The relevant code is in 
sys/net/if_clone.c in the kernel.

You can list them with “ifconfig -C”.

We use it as a way to figure out if DCO is supported or not. We could 
potentially also enumerate the loaded kernel modules and check for if_ovpn.ko, 
but that’d break if someone built that into the kernel.

>> +const char *
>> +dco_get_supported_ciphers()
>> +{
>> +    return "none:AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305";
>> +}
>
> Is "none" indeed supported?  I find that useful to test, but Arne and
> Antonio refuse(d) to support it in linux DCO :-)
>
It is, yes, although I have to admit that it’s certainly the least tested mode. 
(Because if you really want to tunnel without crypto you can just use if_gif, 
without needing the OpenVPN userspace code.)

>> diff --git a/src/openvpn/forward.c b/src/openvpn/forward.c
>> index 55c939c4..14ad24fa 100644
>> --- a/src/openvpn/forward.c
>> +++ b/src/openvpn/forward.c
>
> ... these are all very straightforward "LINUX" -> "LINUX || FREEBSD" :-)
>
If at some point someone adds OpenBSD or NetBSD or Illumos or .. it’s going to 
be worth thinking about a ‘PLATFORM_HAS_DCO’, or ‘UNIX_Y_HAS_DCO’ or similar 
define for these.

>> diff --git a/src/openvpn/tun.c b/src/openvpn/tun.c
>> index 54f7d72c..e84d5a27 100644
>> --- a/src/openvpn/tun.c
>> +++ b/src/openvpn/tun.c
> [..]
>> @@ -2917,20 +2917,24 @@ void
>>  open_tun(const char *dev, const char *dev_type, const char *dev_node, 
>> struct tuntap *tt,
>>           openvpn_net_ctx_t *ctx)
>>  {
>> -    open_tun_generic(dev, dev_type, dev_node, tt);
>> -
>> -    if (tt->fd >= 0 && tt->type == DEV_TYPE_TUN)
>> -    {
>> -        int i = IFF_POINTOPOINT | IFF_MULTICAST;
>> +    if (tun_dco_enabled(tt)) {
>> +        open_tun_dco_generic(dev, dev_type, tt, ctx);
>> +    } else {
>> +        open_tun_generic(dev, dev_type, dev_node, tt);
>
> This one will upset the whitespace dragon and it will eat all of
> the patch :-)
>
> OpenVPN bracket convention requires this to be:
>
> +    if (tun_dco_enabled(tt))
> +    {
> +        open_tun_dco_generic(dev, dev_type, tt, ctx);
> +    }
> +    else
> +    {
> +        open_tun_generic(dev, dev_type, dev_node, tt);
>
> (I run uncrustify on merge, so this would get fixed - but it would
> be good to have it properly formatted right away)
>
Whoops. I thought I’d been good about following local style, but I seem to have 
missed one. Fixed.

Best regards,
Kristof


_______________________________________________
Openvpn-devel mailing list
Openvpn-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/openvpn-devel

Reply via email to