Skip to content

Fix KeepCarrier tun/tap device option#30504

Merged
yuwata merged 1 commit intosystemd:mainfrom
KonishchevDmitry:fix-tun-keep-carrier
Jan 3, 2024
Merged

Fix KeepCarrier tun/tap device option#30504
yuwata merged 1 commit intosystemd:mainfrom
KonishchevDmitry:fix-tun-keep-carrier

Conversation

@KonishchevDmitry
Copy link
Copy Markdown
Contributor

When KeepCarrier is set, networkd doesn't close tun/tap file descriptor preserving the active interface state, but doesn't disable its queue which makes kernel to think that it's still active and send packets to it.

This patch disables the created queue right after tun/tap interface creation.

Here is the steps to reproduce the bug:

Having:

systemd/network/10-tun-test.netdev:

[NetDev]
Name=tun-test
Kind=tun

[Tun]
MultiQueue=yes
KeepCarrier=yes

systemd/network/10-tun-test.network:

[Match]
Name=tun-test

[Network]
DHCP=no
IPv6AcceptRA=false

LLMNR=false
MulticastDNS=false

Address=172.31.0.1/24

app.c:

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <linux/if.h>
#include <sys/ioctl.h>
#include <linux/if_tun.h>

int main() {
    int fd;
    struct ifreq ifr;

    memset(&ifr, 0, sizeof ifr);
    strcpy(ifr.ifr_name, "tun-test");
    ifr.ifr_flags = IFF_TUN | IFF_NO_PI | IFF_MULTI_QUEUE;

    if((fd = open("/dev/net/tun", O_RDWR)) < 0) {
        perror("Open error");
        return 1;
    }

    if(ioctl(fd, TUNSETIFF, &ifr)) {
        perror("Configure error");
        return 1;
    }

    puts("Ready.");

    char buf[1500];

    while(1) {
        int size = read(fd, buf, sizeof buf);
        if(size < 0) {
            perror("Read error");
            return 1;
        }
        printf("Read %d bytes.\n", size);
    }

    return 0;
}

Run:

  • gcc -o app app.c && ./app
  • ping -I tun-test 172.31.0.2

Before the patch the app shows no pings, but after it works properly.

@github-actions github-actions bot added network please-review PR is ready for (re-)review by a maintainer labels Dec 16, 2023
@bluca bluca requested a review from yuwata December 22, 2023 09:45
if (ioctl(fd, TUNSETIFF, &ifr) < 0)
return log_netdev_error_errno(netdev, errno, "TUNSETIFF failed: %m");

if (t->multi_queue) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here you check for multi_queue, not for keep_fd?

Copy link
Copy Markdown
Contributor Author

@KonishchevDmitry KonishchevDmitry Jan 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's intentional: networkd never reads packets from its devices, so it would be more correct to always disable the queue right after device creation / attaching to existing device to prevent packet dropping even for the short time of device configuration. So I would even remove if (t->multi_queue) and do it unconditionally, but kernel doesn't allow to disable queue on non-multi queue devices. It would be best to attach in disabled queue mode, but kernel doesn't have one, so the best we can do is disable the queue right after attaching.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, so this requires an in-code comment.

But what I am not grokking: this detaches the queue, but if a carrier comes, then something needs to attach the queue again? what am i missing here?

(my insight into tun/tap behaviour is a bit superficial, in case you didn't notice)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we open tun/tap device, kernel creates a queue for each open FD.

Link is considered to have carrier if its open FD count is not zero.

When packet arrives, kernel selects a queue to send the packet to. From a quick look at the kernel sources the logic is something like "take current CPU ID and map it to index in active queue list". So if some packets are handled by CPU #N and N maps to our queue, all these packets will be sent to our queue - even if no one reads it.

So from my view the whole picture should be the following:

  • I create a tun/tap device using networkd, which properly configures it and leaves FD open, but disables its queue. So kernel considering the device as connected.
  • Then I start some rootless service which opens the device and kernels starts send all packets to its queue(s).
  • When the service stops, we have one open FD (by networkd) and 0 active and 1 disabled queue (networkd's). Kernel remains to consider link as active, and continues to route packets to it, but they are effectively dropped since we don't have active queues now. And this is desired behaviour for us - for example in case of VPN. In other case we might need to create some blackhole routing table with lower priority to make kernel send packets to it when the main interface lose its carrier.
  • When the service starts again, we have one active queue and packets start flow to it.

@poettering poettering added reviewed/needs-rework 🔨 PR has been reviewed and needs another round of reworks and removed please-review PR is ready for (re-)review by a maintainer labels Jan 3, 2024
When KeepCarrier is set, networkd doesn't close tun/tap file descriptor
preserving the active interface state, but doesn't disable its queue
which makes kernel to think that it's still active and send packets to
it.

This patch disables the created queue right after tun/tap interface
creation.

Here is the steps to reproduce the bug:

Having:

systemd/network/10-tun-test.netdev:

    [NetDev]
    Name=tun-test
    Kind=tun

    [Tun]
    MultiQueue=yes
    KeepCarrier=yes

systemd/network/10-tun-test.network:

    [Match]
    Name=tun-test

    [Network]
    DHCP=no
    IPv6AcceptRA=false

    LLMNR=false
    MulticastDNS=false

    Address=172.31.0.1/24

app.c:

    #include <fcntl.h>
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <linux/if.h>
    #include <sys/ioctl.h>
    #include <linux/if_tun.h>

    int main() {
        int fd;
        struct ifreq ifr;

        memset(&ifr, 0, sizeof ifr);
        strcpy(ifr.ifr_name, "tun-test");
        ifr.ifr_flags = IFF_TUN | IFF_NO_PI | IFF_MULTI_QUEUE;

        if((fd = open("/dev/net/tun", O_RDWR)) < 0) {
            perror("Open error");
            return 1;
        }

        if(ioctl(fd, TUNSETIFF, &ifr)) {
            perror("Configure error");
            return 1;
        }

        puts("Ready.");

        char buf[1500];

        while(1) {
            int size = read(fd, buf, sizeof buf);
            if(size < 0) {
                perror("Read error");
                return 1;
            }
            printf("Read %d bytes.\n", size);
        }

        return 0;
    }

Run:
* gcc -o app app.c && ./app
* ping -I tun-test 172.31.0.2

Before the patch the app shows no pings, but after it works properly.
@github-actions github-actions bot added please-review PR is ready for (re-)review by a maintainer and removed reviewed/needs-rework 🔨 PR has been reviewed and needs another round of reworks labels Jan 3, 2024
@poettering
Copy link
Copy Markdown
Member

ok, this makes more sense to me now.

should we default to MultiQueue=true btw?

will leave final review to @yuwata

@KonishchevDmitry
Copy link
Copy Markdown
Contributor Author

should we default to MultiQueue=true btw?

I would say no, because it will break backward compatibility: when you attach to existing tun/tap device, you must specify the same flags it was created with.

Copy link
Copy Markdown
Member

@yuwata yuwata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Sorry for late review.

@yuwata yuwata merged commit 0e1ab22 into systemd:main Jan 3, 2024
@github-actions github-actions bot removed the please-review PR is ready for (re-)review by a maintainer label Jan 3, 2024
@KonishchevDmitry KonishchevDmitry deleted the fix-tun-keep-carrier branch January 4, 2024 06:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

3 participants