Table of Contents

Class LinuxTunVpnAdapter

Namespace
VpnHood.Net.VpnAdapters.LinuxTun
Assembly
VpnHood.Net.VpnAdapters.LinuxTun.dll

Provides the VPN adapter for Linux, on a TUN device of the process's own: it gives the interface its addresses, routes, MTU and DNS servers, and, for a server, NAT for IPv4 and IPv6 through iptables.

public class LinuxTunVpnAdapter : TunVpnAdapter, IVpnAdapter, IPacketTransport, IDisposable
Inheritance
LinuxTunVpnAdapter
Implements
Inherited Members

Remarks

It needs root, and runs ip, sysctl, resolvectl or resolvconf, iptables and ip6tables, nft and udevadm through bash; a canceled step stops waiting for its command, which runs on. The kernel removes the TUN device, with its routes and systemd-resolved's DNS servers for it, when the process ends, a crash included. A resolvconf entry and the DNS redirect stay, which RemoveLeftovers(string, string) clears, and so do NAT rules, until a start adds NAT for their network on the same primary interface again.

The interface's alias carries a tag made from the app's ID, when there is one (AppId), which tells a start whose an interface of its name is. A TUN device of the name that carries the tag, or, from versions before tags, none, is taken for one the app left and removed unless a process is known to hold it: a device that is down shows no holder, and the app's single-instance lock is relied on. Any other interface of the name refuses the start. A start also turns on IPv4 and IPv6 forwarding for the whole host, and leaves it on. There is no app filter.

Constructors

LinuxTunVpnAdapter(LinuxVpnAdapterSettings)

Initializes a new instance of the LinuxTunVpnAdapter class.

public LinuxTunVpnAdapter(LinuxVpnAdapterSettings adapterSettings)

Parameters

adapterSettings LinuxVpnAdapterSettings

The adapter's settings, whose AdapterName must be a valid interface name (IsValidAdapterName(string)), as GetValidAdapterName(string) derives from any name.

Exceptions

ArgumentOutOfRangeException

The settings' blocking timeout is negative or too long, or their queue capacity is not above zero.

ArgumentException

The adapter's name is not a valid interface name.

Properties

AppPackageId

Gets the ID the app filter knows this app by, which it always lets into the tunnel, or null where there is no app filter.

protected override string? AppPackageId { get; }

Property Value

string

Always null: there is no app filter.

CanProtectSocket

Gets a value indicating whether the adapter can keep a socket's traffic out of the tunnel. The default is true.

public override bool CanProtectSocket { get; }

Property Value

bool

false until a start has found the interface of the host's default route; true from then on.

CheckAdapterInterfaceAfterNetworkChanged

Gets a value indicating whether the adapter checks, at each change of the host's network, that its interface is still there, and stops when it is not. The check is IsAdapterInterfaceEnabled(), which by default looks for the interface by name or address only.

protected override bool CheckAdapterInterfaceAfterNetworkChanged { get; }

Property Value

bool

Always true.

IsAppFilterSupported

Gets a value indicating whether the adapter can include or exclude apps from the tunnel (IncludeApps, ExcludeApps).

public override bool IsAppFilterSupported { get; }

Property Value

bool

Always false.

IsSocketProtectedByBind

Gets a value indicating whether the adapter keeps a socket out of the tunnel by binding it to the primary address, rather than by an OS call of its own.

protected override bool IsSocketProtectedByBind { get; }

Property Value

bool

Always true: BindSocketToIp(Socket, IPAddress) binds a socket to an interface.

Remarks

When true, the routes leave out the probe networks the adapter aims at to find the primary addresses, and that search runs on an unprotected socket after a network change.

RestartAfterNetworkAddressChanged

Gets a value indicating whether the adapter restarts when the host moves to other networks. When false, it takes the new primary addresses and raises PrimaryNetworkChanged.

protected override bool RestartAfterNetworkAddressChanged { get; }

Property Value

bool

true when the adapter uses NAT, as a server's does, whose rules name the interface it started on; false for a client's, which takes a new network without a restart, whose teardown would let the host's traffic pass outside the tunnel for its seconds.

Methods

AdapterAdd(CancellationToken)

Makes the interface: the first step of a start.

protected override Task AdapterAdd(CancellationToken cancellationToken)

Parameters

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Remarks

It finds the interface of the host's first IPv4 default route, removes what this adapter's last run left, and tries to keep NetworkManager off the interface with a udev rule under /run, without which a desktop announces the VPN's coming and going as a network connection; a failure there is logged. Then it makes the TUN device, after removing a TUN device of the name that the app left (see the class's remarks), writes the app's tag as its alias, turns on IPv4 and IPv6 forwarding for the host, and sets the interface up.

Exceptions

InvalidOperationException

The host has no IPv4 default route, or, with NAT, a multipath one or one whose interface has a name not valid here; an interface of the adapter's name is not a TUN device, or is another app's or held by a process; or /dev/net/tun could not be opened.

PInvokeException

The kernel refused the TUN device, as without CAP_NET_ADMIN, or its non-blocking mode.

ExternalException

A command failed.

Win32Exception

bash could not be started.

OperationCanceledException

cancellationToken was canceled.

AdapterClose()

Closes the interface for reading and writing: the first step of a stop, under the base's stop lock. It may run after a partial start.

protected override void AdapterClose()

Remarks

It puts /dev/null in the place of the reader's and the sender's descriptor, so a late read meets the end of a file and a late write a sink, never a file that took the number; AdapterRemove() frees it. When /dev/null cannot be put there, which is logged, the descriptor stays the TUN device's until then.

AdapterOpen(CancellationToken)

Opens the interface for reading and writing packets: the last step of a start, before the reader starts.

protected override Task AdapterOpen(CancellationToken cancellationToken)

Parameters

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Remarks

The reader and the sender use a copy of the TUN device's descriptor, which AdapterClose() retires.

Exceptions

InvalidOperationException

The TUN device has not been made, or a stop has removed it.

PInvokeException

The descriptor could not be copied.

AdapterRemove()

Removes the interface and what the start added with it: the last step of a stop, after AdapterClose(). When the base calls it, it runs under the base's stop lock and must not wait for the reader or a sender to take that lock. It may run after a partial start, and after a close that failed.

protected override void AdapterRemove()

Remarks

It removes only what this adapter added: its resolvconf entry and DNS redirect, the TUN device it made, and its NAT rules, each deleted by its spec, every copy of it, rather than by restoring a table, so other rules stay. The specs name the current primary interface, so rules an earlier run added for another one stay too. A failure is logged, not thrown.

Before the device's descriptor is freed, it waits up to a second for the reader to leave, which the device's removal wakes, and up to a second plus twice MaxPacketSendDelay for a batch being sent, then 100 ms more for each that is late; a reader or sender that stops the adapter itself is not waited for. When one does not leave in time, the descriptor stays taken until the process exits, which is logged as an error.

AddAddress(IpNetwork, CancellationToken)

Gives the interface its address in a virtual network. A start calls it once per configured IP version.

protected override Task AddAddress(IpNetwork ipNetwork, CancellationToken cancellationToken)

Parameters

ipNetwork IpNetwork

The interface's address with its network.

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Exceptions

ExternalException

ip addr add failed.

Win32Exception

bash could not be started.

OperationCanceledException

cancellationToken was canceled.

AddNat(IpNetwork, CancellationToken)

Translates a virtual network to the host's primary address (NAT). A start calls it with UseNat, for each IP version the adapter's NAT translates and the host has a primary address of.

protected override Task AddNat(IpNetwork ipNetwork, CancellationToken cancellationToken)

Parameters

ipNetwork IpNetwork

The virtual network.

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Remarks

After removing the rules it added before for the network, it adds three with iptables, or ip6tables for IPv6: a masquerade of the network's traffic out of the primary interface, and two forwarding rules between that interface and the tunnel's, the way back for related and established connections only. Each command waits up to 5 seconds for the xtables lock another tool may hold. A canceled start's command may still add its rule after the cleanup; the next start that adds NAT for the network on the same primary interface removes it.

Exceptions

ExternalException

A command failed.

Win32Exception

bash could not be started.

OperationCanceledException

cancellationToken was canceled.

AddRoute(IpNetwork, CancellationToken)

Routes a network through the interface. A start calls it once per network, for each IP version the interface carries.

protected override Task AddRoute(IpNetwork ipNetwork, CancellationToken cancellationToken)

Parameters

ipNetwork IpNetwork

The network.

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Remarks

The route takes the metric SetMetric(int, bool, bool, CancellationToken) kept in this run, if any.

Exceptions

ExternalException

ip route add failed, as for a route the table has already.

Win32Exception

bash could not be started.

OperationCanceledException

cancellationToken was canceled.

BindSocketToIp(Socket, IPAddress)

Binds a socket to the interface that holds an address (SO_BINDTODEVICE), not to the address, so the socket's traffic leaves by that interface rather than the tunnel. It is how ProtectSocket(Socket) keeps a socket out of the tunnel here.

protected override void BindSocketToIp(Socket socket, IPAddress ipAddress)

Parameters

socket Socket

The socket, not bound yet.

ipAddress IPAddress

The host's primary address of the socket's IP version.

Exceptions

InvalidOperationException

No interface holds the address.

PInvokeException

The kernel refused the option, as without CAP_NET_RAW.

DisposeUnmanaged()

Releases unmanaged resources, such as native handles. A derived class must catch its own errors.

protected override void DisposeUnmanaged()

Remarks

While the adapter still holds its TUN device, it removes it as AdapterRemove() does, with its NAT rules, also when the finalizer calls it, which no stop came before: a masquerade rule left for a deleted interface can break the host's network.

~LinuxTunVpnAdapter()

Removes the TUN device of an adapter that was not disposed, as DisposeUnmanaged() does.

protected ~LinuxTunVpnAdapter()

Remarks

The adapter's handlers on the host's network-change events keep it reachable until it is disposed, so dispose it rather than count on this.

GetValidAdapterName(string)

Derives a valid interface name (IsValidAdapterName(string)) from any name.

public static string GetValidAdapterName(string name)

Parameters

name string

The name, such as the app's.

Returns

string

The name itself when it is valid; otherwise its first ten valid characters less any leading - or ., a dash and the first four hex digits of the SHA-256 hash of the whole name, or vh- and those digits when no character is left. The same name always gives the same result.

IsAdapterInterfaceEnabled()

Determines whether the adapter's interface is still on the host's IP stack, found by its name or its address. Its operational status is not consulted: a fresh interface may report down for a while.

protected override bool IsAdapterInterfaceEnabled()

Returns

bool

true if the interface is there; otherwise, false.

Remarks

It reads the interface's flags from sysfs: a link set down keeps its address, which the base's look finds, while the kernel drops its routes. Its carrier is not consulted, as a new TUN device may report it down for up to a second.

IsNatSupported(IpVersion)

Determines whether the adapter's NAT translates an IP version. A server forwards a version the adapter does not translate by its own forwarders.

public override bool IsNatSupported(IpVersion ipVersion)

Parameters

ipVersion IpVersion

The IP version.

Returns

bool

true if the adapter translates it; otherwise, false.

Remarks

Always true: iptables and ip6tables translate both IP versions.

IsValidAdapterName(string)

Determines whether a name is one the adapter gives its interface: 1 to 15 characters, each an ASCII letter or digit, _, - or ., not starting with -, and neither . nor ...

public static bool IsValidAdapterName(string name)

Parameters

name string

The name.

Returns

bool

true if the name is valid; otherwise, false.

Remarks

The kernel refuses a longer name, and some characters; a name starting with - would read as an option on a command line.

ReadPacket(byte[])

Reads one packet from the interface into a buffer, without waiting for one.

protected override int ReadPacket(byte[] buffer)

Parameters

buffer byte[]

The buffer, at least the MTU long.

Returns

int

The number of bytes read, from the buffer's start; 0 if no packet is ready.

Remarks

Only the bytes read are parsed, and a packet whose header claims more is refused: the buffer is rented, so past them it holds an earlier read's bytes.

Exceptions

IOException

The adapter is closed.

PInvokeException

The read failed, or read no bytes, as it does once a close has put /dev/null in the device's place.

RemoveLeftovers(string, string)

Removes what an earlier run of the app left on the host: its resolvconf DNS entry and DNS redirect, when no interface has the adapter's name, and every TUN device with a valid name that carries the app's tag as its alias and that no process is known to hold.

public static void RemoveLeftovers(string appId, string adapterName)

Parameters

appId string

The app's ID, as in AppId.

adapterName string

The adapter's name, as in AdapterName.

Remarks

Call it when the app starts, once no other instance of it can run, rather than at the next connect: a crashed run's TUN device went with its process, but its resolvconf entry would keep /etc/resolv.conf on the VPN's servers, and the kernel keeps the redirect's tables. The TUN devices go for versions that made them with ip tuntap, which outlive their process. It needs root; a failure is logged, not thrown.

Exceptions

ArgumentException

adapterName is not a valid interface name.

SetAllowedApps(IEnumerable<string>, CancellationToken)

Lets only these apps' traffic into the tunnel. A start calls it with IncludeApps, where IsAppFilterSupported is true.

protected override Task SetAllowedApps(IEnumerable<string> packageIds, CancellationToken cancellationToken)

Parameters

packageIds IEnumerable<string>

The apps' IDs, this app's own included.

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Exceptions

NotSupportedException

Always: LinuxTun has no app filter.

SetDisallowedApps(IEnumerable<string>, CancellationToken)

Keeps these apps' traffic out of the tunnel. A start calls it with ExcludeApps, where IsAppFilterSupported is true.

protected override Task SetDisallowedApps(IEnumerable<string> packageIds, CancellationToken cancellationToken)

Parameters

packageIds IEnumerable<string>

The apps' IDs, never this app's own.

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Exceptions

NotSupportedException

Always: LinuxTun has no app filter.

SetDnsServers(IReadOnlyList<IPAddress>, CancellationToken)

Sets the DNS servers the interface gives the host. A start calls it every time.

[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
protected override Task SetDnsServers(IReadOnlyList<IPAddress> dnsServers, CancellationToken cancellationToken)

Parameters

dnsServers IReadOnlyList<IPAddress>

The servers, only of the IP versions the interface carries; may be empty.

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Remarks

With no servers, it does nothing. Otherwise it gives them to systemd-resolved, through resolvectl, as the interface's servers with the routing domain ~., which sends them every name no other link has a more specific domain for; when that fails, it gives them to resolvconf as its exclusive entry, a command a cancellation does not cut off, which needs a resolvconf with an exclusive mode, as openresolv's. Then, rather than let DNS go around the tunnel, it fails when /etc/resolv.conf names no server, or any server but these and systemd-resolved's stub, 127.0.0.53 or 127.0.0.54.

While the servers are set, an nftables rule sends DNS over TCP or UDP port 53 that the host forwards, a container's, to the VPN's first server of its IP version when it is for a network of the host's primary interfaces or a server systemd-resolved knows, or refuses it where the VPN has no server of that version. DNS that comes in by a primary interface, or is for one of its addresses, is left alone, and the host's own lookups never meet the rule. A network change rewrites it; a failure is logged, as only forwarded DNS depends on it.

Exceptions

Exception

Neither resolvectl nor resolvconf took the servers, or /etc/resolv.conf does not use them.

OperationCanceledException

cancellationToken was canceled.

SetMetric(int, bool, bool, CancellationToken)

Sets the interface's route metric. A start calls it when Metric is set.

protected override Task SetMetric(int metric, bool ipV4, bool ipV6, CancellationToken cancellationToken)

Parameters

metric int

The metric.

ipV4 bool

true if the interface carries IPv4 in this run.

ipV6 bool

true if the interface carries IPv6 in this run.

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Remarks

It keeps the metric for the routes the start adds next, of both IP versions, and changes no route already there. The next start forgets it.

SetMtu(int, bool, bool, CancellationToken)

Sets the interface's MTU. A start calls it when Mtu is set.

protected override Task SetMtu(int mtu, bool ipV4, bool ipV6, CancellationToken cancellationToken)

Parameters

mtu int

The MTU in bytes.

ipV4 bool

true if the interface carries IPv4 in this run.

ipV6 bool

true if the interface carries IPv6 in this run.

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Remarks

It sets the interface's MTU, for both IP versions.

Exceptions

ExternalException

ip link set failed.

Win32Exception

bash could not be started.

OperationCanceledException

cancellationToken was canceled.

SetSessionName(string, CancellationToken)

Names the session where the OS shows it. A start calls it when SessionName is not empty.

protected override Task SetSessionName(string sessionName, CancellationToken cancellationToken)

Parameters

sessionName string

The name.

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Remarks

It does nothing.

WaitForTunRead()

Blocks until a packet may be ready, after a read found none. It runs on the reader's task.

protected override void WaitForTunRead()

Remarks

It polls the device's descriptor with no time limit, and polls again when a signal interrupts the poll; a stop's removal of the device ends it. Before the first open, it returns at once.

Exceptions

PInvokeException

The poll failed.

WaitForTunWrite()

Blocks until the interface may take a packet again, between the retries of a write it refused.

protected override void WaitForTunWrite()

Remarks

It polls the device's descriptor with no time limit, and polls again when a signal interrupts the poll. Before the first open, it returns at once.

Exceptions

PInvokeException

The poll failed.

WritePacket(IpPacket)

Writes one packet to the interface, without waiting for room.

protected override bool WritePacket(IpPacket ipPacket)

Parameters

ipPacket IpPacket

The packet.

Returns

bool

true if the interface took it; false if it cannot take it now, which the adapter retries with doubling waits (see MaxPacketSendDelay).

Exceptions

PInvokeException

The write failed with another error.