Table of Contents

Class TunVpnAdapter

Namespace
VpnHood.Net.VpnAdapters.Abstractions
Assembly
VpnHood.Net.VpnAdapters.Abstractions.dll

Provides the start, stop, restart, packet I/O and network-change handling of a VPN adapter over a TUN-like interface; a derived class supplies the platform's steps.

public abstract class TunVpnAdapter : PacketTransport, IVpnAdapter, IPacketTransport, IDisposable
Inheritance
TunVpnAdapter
Implements
Derived
Inherited Members

Remarks

A start runs the derived class's steps in order: AdapterAdd(CancellationToken); SetSessionName(string, CancellationToken) when a name is given; AddAddress(IpNetwork, CancellationToken) per configured IP version; SetMetric(int, bool, bool, CancellationToken) and SetMtu(int, bool, bool, CancellationToken) when given; SetDnsServers(IReadOnlyList<IPAddress>, CancellationToken); AddRoute(IpNetwork, CancellationToken) per network and IP version; AddNat(IpNetwork, CancellationToken); the app filter; then AdapterOpen(CancellationToken). A stop runs AdapterClose() and AdapterRemove(). A stop or disposal during a start cancels the token the steps get; the base checks it before the routes and before opening, and each step should observe it, as a start may be torn down part-way. An owner's start runs its steps under no lock of the base, a restart under its restart lock.

Packets are read on a task of the adapter's own and handed to the transport's receivers; sent packets are written one by one. Ten exceptions in a row from reading, or ten from writing, stop the adapter: it then restarts itself with AutoRestart, or raises Failed.

When the host's network changes, the adapter restarts if RestartAfterNetworkAddressChanged says so, or takes the new primary addresses and raises PrimaryNetworkChanged; a new temporary IPv6 address on the same network only refreshes the primary addresses.

Constructors

TunVpnAdapter(VpnAdapterSettings)

Initializes a new instance of the TunVpnAdapter class, which starts watching the host's network changes until it is disposed.

protected TunVpnAdapter(VpnAdapterSettings adapterSettings)

Parameters

adapterSettings VpnAdapterSettings

The adapter's settings.

Exceptions

ArgumentOutOfRangeException

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

Properties

AdapterIpNetworkV4

Gets the interface's IPv4 address with its network in the current run, or null.

public IpNetwork? AdapterIpNetworkV4 { get; }

Property Value

IpNetwork

AdapterIpNetworkV6

Gets the interface's IPv6 address with its network in the current run, or null.

public IpNetwork? AdapterIpNetworkV6 { get; }

Property Value

IpNetwork

AdapterName

Gets the name of the adapter's interface.

public string AdapterName { get; }

Property Value

string

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 abstract string? AppPackageId { get; }

Property Value

string

AutoRestartDelay

Gets the wait before AutoRestart tries a failed restart again. The default is 10 seconds.

protected virtual TimeSpan AutoRestartDelay { get; }

Property Value

TimeSpan

CanProtectSocket

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

public virtual bool CanProtectSocket { get; }

Property Value

bool

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 abstract bool CheckAdapterInterfaceAfterNetworkChanged { get; }

Property Value

bool

GatewayIpV4

Gets the gateway of the interface's IPv4 network: its first address plus one, or null for a /31 or /32 network or when not started.

public IPAddress? GatewayIpV4 { get; }

Property Value

IPAddress

GatewayIpV6

Gets the gateway of the interface's IPv6 network: its first address plus one, or null for a /128 network, when not started, or in a run whose IPv6 address the interface refused.

public IPAddress? GatewayIpV6 { get; }

Property Value

IPAddress

IsAppFilterSupported

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

public abstract bool IsAppFilterSupported { get; }

Property Value

bool

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 abstract bool IsSocketProtectedByBind { get; }

Property Value

bool

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.

IsStarted

Gets a value indicating whether the adapter is started.

public bool IsStarted { get; }

Property Value

bool

PrimaryAdapterIpV4

Gets the host's primary IPv4 address, outside the tunnel, or null. A stop clears it; a start finds it again.

public IPAddress? PrimaryAdapterIpV4 { get; }

Property Value

IPAddress

PrimaryAdapterIpV6

Gets the host's primary IPv6 address, outside the tunnel, or null. A stop clears it; a start finds it again.

public IPAddress? PrimaryAdapterIpV6 { get; }

Property Value

IPAddress

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 abstract bool RestartAfterNetworkAddressChanged { get; }

Property Value

bool

ServerIp

Gets the VPN server's address the current run's options name, or null.

protected IPAddress? ServerIp { get; }

Property Value

IPAddress

UseNat

Gets a value indicating whether the current run translates its virtual networks (NAT).

protected bool UseNat { get; }

Property Value

bool

Methods

AdapterAdd(CancellationToken)

Makes the interface: the first step of a start.

protected abstract Task AdapterAdd(CancellationToken cancellationToken)

Parameters

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

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 abstract void AdapterClose()

AdapterOpen(CancellationToken)

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

protected abstract Task AdapterOpen(CancellationToken cancellationToken)

Parameters

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

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 abstract void AdapterRemove()

AddAddress(IpNetwork, CancellationToken)

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

protected abstract 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.

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 abstract 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.

AddRoute(IpNetwork, CancellationToken)

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

protected abstract 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.

BindSocketToIp(Socket, IPAddress)

Binds a socket to an address, which is how ProtectSocket(Socket) keeps it out of the tunnel. The default binds it to the address with any port.

protected virtual void BindSocketToIp(Socket socket, IPAddress address)

Parameters

socket Socket

The socket, not bound yet.

address IPAddress

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

DisposeManaged()

Stops watching the host's network changes, raises Disposed, then disposes the transport.

protected override void DisposeManaged()

GetGatewayIp(IpVersion)

Gets the gateway of the interface's network of an IP version.

public IPAddress? GetGatewayIp(IpVersion ipVersion)

Parameters

ipVersion IpVersion

The IP version.

Returns

IPAddress

GatewayIpV4 or GatewayIpV6, which may be null.

GetIpNetwork(IpVersion)

Gets the interface's address with its network of an IP version.

public IpNetwork? GetIpNetwork(IpVersion ipVersion)

Parameters

ipVersion IpVersion

The IP version.

Returns

IpNetwork

AdapterIpNetworkV4 or AdapterIpNetworkV6, which may be null.

GetPrimaryAdapterAddress(IpVersion)

Gets the host's primary address of an IP version: the one it reaches the internet from, outside the tunnel.

public IPAddress? GetPrimaryAdapterAddress(IpVersion ipVersion)

Parameters

ipVersion IpVersion

The IP version.

Returns

IPAddress

The address, or null when the host has none of that version.

GetPrimaryAdapterDnsServers()

Gets the DNS servers of the network the tunnel runs over: the host's DNS servers as .NET lists them, less those the tunnel itself gives.

public virtual IReadOnlyList<IPAddress> GetPrimaryAdapterDnsServers()

Returns

IReadOnlyList<IPAddress>

The DNS servers; none where .NET lists none, which a platform's adapter may answer itself.

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 virtual bool IsAdapterInterfaceEnabled()

Returns

bool

true if the interface is there; otherwise, false.

IsIpVersionSupported(IpVersion)

Determines whether the host's network, outside the tunnel, supports an IP version.

public virtual bool IsIpVersionSupported(IpVersion ipVersion)

Parameters

ipVersion IpVersion

The IP version.

Returns

bool

true if the host's network supports it; otherwise, false.

Remarks

It tells whether the primary interface has an address of that version, for routing and split-tunneling decisions and to know whether the server can be reached with it. It says nothing of what the tunnel itself carries.

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 abstract bool IsNatSupported(IpVersion ipVersion)

Parameters

ipVersion IpVersion

The IP version.

Returns

bool

true if the adapter translates it; otherwise, false.

OnPrimaryNetworkChanged()

protected void OnPrimaryNetworkChanged()

PreDispose()

Stops the adapter, after any restart under way ends, before the transport is disposed.

protected override sealed void PreDispose()

ProtectSocket(Socket)

Keeps a socket's traffic out of the tunnel by binding it to the host's primary address of its IP version. Call it before the socket is bound or connected.

public virtual bool ProtectSocket(Socket socket)

Parameters

socket Socket

The socket, not bound yet.

Returns

bool

true if the socket is bound; false if the host has no primary address of the socket's IP version.

Exceptions

ObjectDisposedException

The adapter is disposed.

InvalidOperationException

The socket is already bound.

NotSupportedException

The socket is neither IPv4 nor IPv6.

ProtectSocket(Socket, IPAddress)

Keeps a socket's traffic to a given address out of the tunnel by binding it to the host's primary address of its IP version. Call it before the socket is bound or connected.

public virtual bool ProtectSocket(Socket socket, IPAddress remoteAddress)

Parameters

socket Socket

The socket, not bound yet.

remoteAddress IPAddress

The remote address the socket will reach.

Returns

bool

true if the socket is bound; false if the host has no primary address of the socket's IP version, or exactly one of the two addresses is a loopback address, which no bind can route.

Exceptions

ObjectDisposedException

The adapter is disposed.

InvalidOperationException

The socket is already bound.

NotSupportedException

The socket is neither IPv4 nor IPv6.

ReadPacket(byte[])

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

protected abstract 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.

ReadPacket(int)

Reads one packet into an array rented from Shared, with ReadPacket(byte[]).

protected virtual IpPacket? ReadPacket(int mtu)

Parameters

mtu int

The size of the array to rent: the run's MTU.

Returns

IpPacket

The packet, which owns the array, or null if none is ready.

Exceptions

ArgumentException

The bytes read are no valid IP packet.

RefreshPrimaryAdapterIps()

Looks at the host's network again for its primary addresses, as a change of the host's addresses makes the adapter do, and raises PrimaryAdapterIpChanged when they differ from the last ones.

public void RefreshPrimaryAdapterIps()

Remarks

For a caller that needs GetPrimaryAdapterAddress(IpVersion) and IsIpVersionSupported(IpVersion) to tell the network as it is now: the OS may move the host's traffic to another network without an address change. An adapter that is stopped, starting or restarting is left alone: its start finds the addresses.

Restart(CancellationToken)

Stops the adapter and starts it again with its last start's options, after two seconds.

public Task Restart(CancellationToken cancellationToken)

Parameters

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation.

Remarks

A restart already under way, an owner's start under way, the owner's stop or a disposal makes it return without restarting; the stop or disposal is checked again after the two seconds. A failed start leaves the adapter stopped, and the adapter answers that itself: it retries with AutoRestart, else raises Failed.

Exceptions

InvalidOperationException

The adapter has never been started.

OperationCanceledException

cancellationToken was canceled, or the owner stopped the adapter meanwhile.

SendPacket(IpPacket)

Writes one packet to the interface, retrying with doubling waits while it cannot take it (see MaxPacketSendDelay), after which the packet is dropped with a warning.

protected void SendPacket(IpPacket ipPacket)

Parameters

ipPacket IpPacket

The packet.

Remarks

A packet the interface refuses during a stop or a start is dropped. A dropped packet is counted as dropped, not sent, so call it only from SendPacketsAsync(IReadOnlyList<IpPacket>). Ten exceptions in a row from writing stop the adapter, which then restarts itself or raises Failed; a send that completes without an exception, a drop included, resets the count.

Exceptions

InvalidOperationException

The adapter is not started, or is stopping or disposed.

SendPacketsAsync(IReadOnlyList<IpPacket>)

Writes a batch of packets to the interface, one after another, with SendPacket(IpPacket).

protected override ValueTask SendPacketsAsync(IReadOnlyList<IpPacket> ipPackets)

Parameters

ipPackets IReadOnlyList<IpPacket>

The packets.

Returns

ValueTask

A completed task: the writes are synchronous.

Exceptions

InvalidOperationException

The adapter is not started, or is stopping or disposed.

SetAllowedApps(IEnumerable<string>, CancellationToken)

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

protected abstract 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.

SetDisallowedApps(IEnumerable<string>, CancellationToken)

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

protected abstract 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.

SetDnsServers(IReadOnlyList<IPAddress>, CancellationToken)

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

protected abstract 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.

SetMetric(int, bool, bool, CancellationToken)

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

protected abstract 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.

SetMtu(int, bool, bool, CancellationToken)

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

protected abstract 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.

SetSessionName(string, CancellationToken)

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

protected abstract 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.

Start(VpnAdapterOptions, CancellationToken)

Starts the adapter: makes its interface with its addresses, DNS servers, routes, NAT and app filter, opens it and starts reading packets.

public Task Start(VpnAdapterOptions options, CancellationToken cancellationToken)

Parameters

options VpnAdapterOptions

The options this run uses, which each restart uses again.

cancellationToken CancellationToken

The token to monitor for cancellation requests.

Returns

Task

A task that represents the asynchronous operation. It completes when the adapter is up.

Remarks

Await one start before the next. From here the adapter keeps itself started until Stop(), disposal, or a failure it does not recover from (without AutoRestart). IsStarted is true from the start's beginning, before the interface exists. A failed start tries to remove what it added and leaves the adapter stopped, with none of the run's addresses and gateways; it raises no Failed, as the caller has the exception. An IPv6 address the interface refuses is logged, and the run goes on without IPv6. Once the adapter is up and before the start completes, it raises Started on its own thread.

Exceptions

ObjectDisposedException

The adapter is disposed.

OperationCanceledException

cancellationToken was canceled, or a stop or disposal came during the start.

NotSupportedException

UseNat is set and the adapter's NAT does not translate IPv4.

InvalidOperationException

Both IncludeApps and ExcludeApps are non-empty on an adapter with an app filter, or the filter has no AppPackageId.

Exception

A route could not be added; the message names it.

StartReadingPackets()

Reads packets from the interface and hands each to the transport's receivers, until a stop retires this run. It runs on the reader's task; ten read errors in a row stop the adapter.

protected virtual void StartReadingPackets()

Stop()

Stops the adapter and removes its interface, with its routes and DNS servers. A restart under way ends first, and a pending retry is dropped, even when the adapter is already stopped.

public void Stop()

Remarks

The adapter counts as stopped even when closing or removing the interface fails: the error reaches the caller, and the next start removes a leftover. A close that fails does not skip the removal.

Exceptions

ObjectDisposedException

The adapter is disposed.

WaitForReader(TimeSpan)

Waits for this run's reader to end, for an adapter about to free what the reader uses. A stop made on the reader's own thread, by its read errors or by a subscriber, does not wait for itself.

protected bool WaitForReader(TimeSpan timeout)

Parameters

timeout TimeSpan

The longest wait.

Returns

bool

true if the reader has ended, or the call is on its thread; otherwise, false.

WaitForSender(TimeSpan)

Waits for the batch being sent to end, for an adapter that has cleared the handle its WritePacket uses: that batch may still hold the handle, a later one finds it cleared. The sender writes its flag with no fence, so a process-wide barrier before the wait makes a flag set before the clearing seen here, and one after it makes all the batch did before clearing its flag seen too (ARM64 may show the clearing first). A stop made by this adapter's sender, on its own errors, does not wait for itself.

protected bool WaitForSender(TimeSpan timeout)

Parameters

timeout TimeSpan

The longest wait.

Returns

bool

true if no batch is being sent, or the call is the sender's own; otherwise, false.

WaitForTunRead()

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

protected abstract void WaitForTunRead()

WaitForTunWrite()

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

protected abstract void WaitForTunWrite()

WritePacket(IpPacket)

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

protected abstract 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).

Events

Disposed

Occurs when the adapter is disposed.

public event EventHandler? Disposed

Event Type

EventHandler

Failed

Occurs when the adapter stopped on its own and stays down: by repeated I/O errors, by losing its interface, or by a restart that could not start it again. Its owner must act, a client by ending its session.

public event EventHandler<Exception>? Failed

Event Type

EventHandler<Exception>

Remarks

It is raised on a thread-pool task, after the failure. A stop, a disposal or a new start by the owner that begins before that task runs cancels it; one that begins while a handler runs does not interrupt the handler. It is not raised with AutoRestart, where the adapter retries instead. The owner's stop or disposal does not raise it, nor does a failed owner's start, whose caller gets the exception.

PrimaryAdapterIpChanged

Occurs when the adapter finds new primary addresses without a restart, as when the OS rotates a temporary IPv6 address within its network.

public event EventHandler? PrimaryAdapterIpChanged

Event Type

EventHandler

Remarks

It is raised on the thread that saw the network change, only when the addresses found differ from the last ones; not for the start's own discovery, nor when a stop clears them.

PrimaryNetworkChanged

Occurs when the host moved to other networks and the adapter took their primary addresses without a restart: what was connected from the old networks may be dead, though an OS may keep it open, as Linux does.

public event EventHandler? PrimaryNetworkChanged

Event Type

EventHandler

Remarks

It is raised on the thread that saw the network change. The primary addresses may stay the same.

Started

Occurs when the adapter is up, with the options it runs with: after the owner's start and after each restart, which may make the interface anew. What points at the interface from outside, such as a kill switch's block, must follow it.

public event EventHandler<VpnAdapterOptions>? Started

Event Type

EventHandler<VpnAdapterOptions>

Remarks

It is raised on the starting thread, once the adapter is up and before the start or the restart completes. A handler may stop or dispose the adapter, and start it again once it has stopped it; it must not wait for another start or restart of it. A run that is over by a handler's turn is not announced to that handler, and one run's handlers return before the next run's are called, except those of a run a handler itself starts. A stop or a disposal on another thread does not wait for the handlers: one may still run, or be called, as that stop returns.