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
adapterSettingsLinuxVpnAdapterSettingsThe 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
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
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
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
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
cancellationTokenCancellationTokenThe 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/tuncould 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
cancellationTokenwas 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
cancellationTokenCancellationTokenThe 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
ipNetworkIpNetworkThe interface's address with its network.
cancellationTokenCancellationTokenThe token to monitor for cancellation requests.
Returns
- Task
A task that represents the asynchronous operation.
Exceptions
- ExternalException
ip addr addfailed.- Win32Exception
bash could not be started.
- OperationCanceledException
cancellationTokenwas 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
ipNetworkIpNetworkThe virtual network.
cancellationTokenCancellationTokenThe 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
cancellationTokenwas 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
ipNetworkIpNetworkThe network.
cancellationTokenCancellationTokenThe 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 addfailed, as for a route the table has already.- Win32Exception
bash could not be started.
- OperationCanceledException
cancellationTokenwas 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
socketSocketThe socket, not bound yet.
ipAddressIPAddressThe 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
namestringThe 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, orvh-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
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
ipVersionIpVersionThe IP version.
Returns
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
namestringThe name.
Returns
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
bufferbyte[]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/nullin 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
appIdstringThe app's ID, as in AppId.
adapterNamestringThe 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
adapterNameis 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
packageIdsIEnumerable<string>The apps' IDs, this app's own included.
cancellationTokenCancellationTokenThe 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
packageIdsIEnumerable<string>The apps' IDs, never this app's own.
cancellationTokenCancellationTokenThe 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
dnsServersIReadOnlyList<IPAddress>The servers, only of the IP versions the interface carries; may be empty.
cancellationTokenCancellationTokenThe 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
resolvectlnorresolvconftook the servers, or/etc/resolv.confdoes not use them.- OperationCanceledException
cancellationTokenwas 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
metricintThe metric.
ipV4booltrue if the interface carries IPv4 in this run.
ipV6booltrue if the interface carries IPv6 in this run.
cancellationTokenCancellationTokenThe 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
mtuintThe MTU in bytes.
ipV4booltrue if the interface carries IPv4 in this run.
ipV6booltrue if the interface carries IPv6 in this run.
cancellationTokenCancellationTokenThe 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 setfailed.- Win32Exception
bash could not be started.
- OperationCanceledException
cancellationTokenwas 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
sessionNamestringThe name.
cancellationTokenCancellationTokenThe 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
ipPacketIpPacketThe 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.