Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

VPN

NuGet package Shiny.AppDeviceBridge.Vpn

The VPN bridge lets the page install the app’s profile in the operating system’s built-in IKEv2 VPN client, then connect, disconnect and watch the tunnel. It’s built on Shiny.Net.Vpn. The OS owns the tunnel: it keeps running after the app closes, shows in the system VPN settings, and the user can switch it off or delete it there.

This is IKEv2 only, through the OS’s own client. WireGuard, OpenVPN and custom protocols need a packet tunnel extension or a VpnService, which this bridge does not provide.

Platform Backed by
iOS, Mac Catalyst, macOS NEVPNManager (personal VPN)
Android 11+ Platform VPN profiles (VpnManager + Ikev2VpnProfile); Android 13 to report the tunnel’s state
Linux NetworkManager’s strongSwan plugin, over D-Bus
Android 10 and older, Windows nothing: every route answers 501
Terminal window
dotnet add package Shiny.AppDeviceBridge.Vpn
builder.UseAppDeviceBridge(
bridge => bridge.AddVpnBridge(),
webApp => { /* … */ }
);
  • Apple: enable the Personal VPN capability on the App ID and add the com.apple.developer.networking.vpn.api entitlement with the value allow-vpn.
  • Android: no manifest entries are needed.
  • Linux: install network-manager-strongswan (Debian, Ubuntu) or NetworkManager-strongswan (Fedora). Saving and connecting go through polkit: interactive on a desktop, and a polkit rule on a headless device.

An app that already calls services.AddVpn() keeps its registration. See Shiny.Net.Vpn.

import { VpnBridge } from "@shinyorg/appdevicebridge";
const vpn = new VpnBridge();
const { capabilities } = await vpn.getStatus();
if (!capabilities.usernamePassword)
return;
const { access } = await vpn.saveProfile({
name: "Acme",
serverAddress: "vpn.acme.com",
authentication: "UsernamePassword",
username,
password
});
if (access === "Denied")
return; // the user declined the OS consent prompt
await vpn.onStatusChanged(e => showState(e.state));
await vpn.connect({ timeoutSeconds: 30 });

In Blazor, register AddVpnBridgeClient() and inject IVpnBridge.

Platforms expose different parts of their VPN client, so check capabilities from GET vpn before offering an option. An option the platform lacks answers 501 not_supported.

Capability Missing on
status Android 11 and 12: the state is Unknown, connect returns once the OS accepts the request, and vpn.status never fires
certificate Linux, which reads certificates from files rather than taking them inline
serverRootCertificate Apple, which only trusts a private CA the user installed through a configuration profile
remoteIdentifier Android, which always uses the server address as the server’s identity

PUT vpn/profile installs the app’s one profile, or replaces it. It takes a name, a serverAddress and one of three kinds of authentication:

authentication Needs
SharedSecret secret, the pre-shared key. Servers usually look it up by localIdentifier, which Android requires
UsernamePassword username and password, for EAP-MSCHAPv2
Certificate pkcs12, the client certificate and its private key as a base64 .p12 bundle, with its password. Android requires localIdentifier

remoteIdentifier is the server’s IKE ID (the server address when left out), and serverRootCertificate is the CA certificate, DER or PEM, that issued the server’s, for a server with a private CA.

The first save shows the OS consent prompt. A user who declines gets { access: "Denied" } and nothing is saved. Saving does not connect, and a tunnel that is up keeps its old settings until the next connect. Secrets go to the keychain or keystore and are never read back. DELETE vpn/profile disconnects and deletes the profile and its secrets.

POST vpn/connect brings the tunnel up and waits for it, timeoutSeconds (1 to 300, 30 by default), then answers the new state. POST vpn/disconnect takes it down and keeps the profile. vpn.status fires on every change, including ones the user makes in system settings and the OS reconnecting on its own, and starts with the current state when the page subscribes.

state Meaning
NotConfigured No profile, or the user removed it in Settings
Disconnected, Connecting, Connected, Reconnecting, Disconnecting The tunnel’s state
Unknown The platform can’t report it (capabilities.status is false)
Status Code When
400 bad_request The profile is incomplete, or timeoutSeconds is out of range
403 vpn_permission No profile is saved, the user withdrew consent, or an entitlement is missing
501 not_supported The platform has no VPN client to drive, or lacks the option asked for
502 vpn_connection_failed The tunnel did not come up: rejected credentials, a server that does not answer, a rejected certificate, or the timeout. The message carries the OS’s reason
502 vpn_failed Anything else, such as NetworkManager not running
GET/_bridge/vpn#

The tunnel's state and what this platform can do.

C#IVpnBridge.GetStatusAsyncTSVpnBridge.getStatus
Response
200 VpnStatus
VpnStatus · 2 fields
stateVpnConnectionStateThe tunnel's state, or Unknown without status.
capabilitiesVpnCapabilityInfoWhat this platform can do.
PUT/_bridge/vpn/profile#

Installs or replaces the app's VPN profile. The first save shows the OS consent prompt, and a user who declines answers Denied with nothing saved. Saving does not connect. Fails with 400 when the profile is incomplete, and 501 when the platform cannot save this kind of profile.

C#IVpnBridge.SaveProfileAsyncTSVpnBridge.saveProfile
Body
application/json · VpnProfileRequest
VpnProfileRequest · 10 fields
namestringThe name shown in the system VPN settings (Android lists the profile under the app's name).
serverAddressstringThe VPN server's host name or IP address.
authenticationVpnAuthenticationTypeHow the device proves who it is.
secretstring | nullThe pre-shared key, for SharedSecret.
usernamestring | nullThe EAP username, for UsernamePassword.
passwordstring | nullThe EAP password, or the PKCS#12 bundle's password for Certificate.
pkcs12string | nullThe client certificate and private key (.p12 / .pfx), base64 in JSON, for Certificate.
remoteIdentifierstring | nullThe server's IKE ID; the server address when null.
localIdentifierstring | nullThe device's IKE ID. Shared secret servers usually look the secret up by it, and Android needs it for shared secret and certificate profiles.
serverRootCertificatestring | nullThe CA certificate that issued the server's, DER or PEM; the system CA store when null.
Response
200 VpnProfileResult
VpnProfileResult · 1 field
accessAccessStateAvailable once saved, Denied when the user declined.
Errors
400Bad request501Not on this platform
DELETE/_bridge/vpn/profile#

Disconnects and deletes the app's VPN profile and its saved secrets. Does nothing when there is none.

C#IVpnBridge.RemoveProfileAsyncTSVpnBridge.removeProfile
Response
204 No content
POST/_bridge/vpn/connect#

Brings the tunnel up and waits until it is connected. Fails with 403 vpn_permission when no profile is saved or the user withdrew consent, and 502 vpn_connection_failed with the OS's reason when the tunnel does not come up in time.

C#IVpnBridge.ConnectAsyncTSVpnBridge.connect
Body
application/json · VpnConnectRequest
VpnConnectRequest · 1 field
timeoutSecondsnumber | nullHow long to wait for the tunnel, 1 to 300 seconds; 30 when null.
Response
200 VpnStatus
VpnStatus · 2 fields
stateVpnConnectionStateThe tunnel's state, or Unknown without status.
capabilitiesVpnCapabilityInfoWhat this platform can do.
Errors
403Forbidden502Upstream failed
POST/_bridge/vpn/disconnect#

Takes the tunnel down. The profile stays saved.

C#IVpnBridge.DisconnectAsyncTSVpnBridge.disconnect
Response
200 VpnStatus
VpnStatus · 2 fields
stateVpnConnectionStateThe tunnel's state, or Unknown without status.
capabilitiesVpnCapabilityInfoWhat this platform can do.
EVENTvpn.status#

The tunnel's state changed, including changes the user makes in system settings and the OS reconnecting on its own. Never fires without status.

C#IVpnBridge.OnStatusChangedAsyncTSVpnBridge.onStatusChanged
Payload
VpnStatusChanged
stateVpnConnectionStateThe tunnel's new state.

JSON property names are camelCase and enums travel as their names. A ? marks a property the payload may omit;| null one that may be null.

VpnConnectionStateenum#

The state of the app's VPN tunnel.

"Unknown""NotConfigured""Disconnected""Connecting""Connected""Reconnecting""Disconnecting"
VpnCapabilityInfoobject#

What this platform's VPN client can do.

profilebooleanA profile can be saved and removed.
connectbooleanThe tunnel can be brought up.
disconnectbooleanThe tunnel can be taken down.
statusbooleanThe tunnel's state is reported. Without it (Android 11 and 12) connect returns once the OS accepts the request, and vpn.status never fires.
sharedSecretbooleanA SharedSecret profile can be saved.
usernamePasswordbooleanA UsernamePassword profile can be saved.
certificatebooleanA Certificate profile can be saved (not on Linux).
serverRootCertificatebooleanserverRootCertificate is honoured (not on Apple).
remoteIdentifierbooleanremoteIdentifier can differ from the server address (not on Android).
VpnProfileRequestobject#

The IKEv2 profile to install. The app owns exactly one, so saving again replaces it. Secrets go to the OS's keychain or keystore and are never read back.

namestringThe name shown in the system VPN settings (Android lists the profile under the app's name).
serverAddressstringThe VPN server's host name or IP address.
authenticationVpnAuthenticationTypeHow the device proves who it is.
secret?string | nullThe pre-shared key, for SharedSecret.
username?string | nullThe EAP username, for UsernamePassword.
password?string | nullThe EAP password, or the PKCS#12 bundle's password for Certificate.
pkcs12?string | nullThe client certificate and private key (.p12 / .pfx), base64 in JSON, for Certificate.
remoteIdentifier?string | nullThe server's IKE ID; the server address when null.
localIdentifier?string | nullThe device's IKE ID. Shared secret servers usually look the secret up by it, and Android needs it for shared secret and certificate profiles.
serverRootCertificate?string | nullThe CA certificate that issued the server's, DER or PEM; the system CA store when null.
VpnAuthenticationTypeenum#

How a profile authenticates with the server.

"SharedSecret""UsernamePassword""Certificate"
VpnProfileResultobject#
accessAccessStateAvailable once saved, Denied when the user declined.
AccessStateenum#

Where a permission stands. Reported by every bridge that needs one; the names match Shiny's own.

"Unknown""NotSupported""NotSetup""Disabled""Restricted""Denied""Available"
VpnConnectRequestobject#
timeoutSeconds?number | nullHow long to wait for the tunnel, 1 to 300 seconds; 30 when null.

The VPN decides where the device’s traffic goes, so a page that can save a profile can send it to a server of its choosing. The bridge sits behind the bridge policy like every other (callers on this device only, plus the launch session), but register it only for a web app you ship. Don’t put credentials in the web app’s bundle: fetch them from your server after the user signs in. The C# VpnProfileRequest leaves its secrets out of ToString(), so a logged request doesn’t leak them. See Security.