Expo module/wrapper for TunnelKit VPN client library for Apple platforms. Built on top of PIA's fork of TunnelKit. Expo config plugin sets up Xcode project settings for you so you can simply import the module and use it's API.
No. Use bare workflow.
npx expo prebuild
For bare React Native projects, you must ensure that you have installed and configured the expo
package before continuing.
❗️Make sure you have committed all changes before installing the package ❗️
npx expo install expo-tunnelkit
app.json
{
"expo": {
"plugins": ["expo-tunnelkit"]
}
}
npx expo prebuild
npx pod-install
The configuration plugin will add a new target named TunnelKitNetworkExtension
to your Xcode project and set it up. Check that:
TunnelKitNetworkExtension
target has the same bundle identifier as your main app target with .TunnelKitNetworkExtension
suffix.TunnelKitNetworkExtension
target has the same app group as your main app target.import ExpoTunnelkit from 'expo-tunnelkit';
// Setup app group and tunnel identifier
ExpoTunnelkit.setup();
// Set VPN credentials
ExpoTunnelkit.setCredentials('username', 'password');
// Add VPN status listener
const subscription = ExpoTunnelkit.addVpnStatusListener((status) =>
console.log('Current status', status.VPNStatus),
);
// Set VPN connection parameters
await ExpoTunnelkit.configFromString(ovpnConfigFileContent);
// Set additional parameters as needed
ExpoTunnelkit.setParam('Hostname', 'example.com');
// Connect to the VPN server
await ExpoTunnelkit.connect();
// Disconnect from the VPN server
await ExpoTunnelkit.disconnect();
// Remove VPN status listener
subscription.remove();
ExpoTunnelkit.setup
function setup(options?: SetupOptions): void;
type SetupOptions = {
appGroup?: string; // Group identifier shared between the app and the app extension.
tunnelIdentifier?: string; // Identifier of the network extension.
};
Setup VPN module with appGroup and tunnelIdentifier. This method should be called before any other VPN module methods. If options are not provided, the appGroup and tunnelIdentifier will be set to the default values based on the app's bundle identifier set in app.json (In most cases you don't need to provide any options).
Returns void.
ExpoTunnelkit.setCredentials
function setCredentials(username: string, password: string): void;
Set the VPN credentials.
Returns void.
ExpoTunnelkit.setParam
function setParam<T extends keyof SessionBuilder>(
key: T, // a parameter to be set
value: SessionBuilder[T], // a value to be set
): void;
Set VPN connection parameter. The parameters must be set before the VPN connection is established. You can set parameters manually or use configFromUrl
/configFromString
to set them from a configuration file. Parameters set manually after importing configuration from a file will override the parameters set from the file. See SessionBuilder
for the list of available parameters.
Returns void.
ExpoTunnelkit.configFromUrl
async function configFromUrl(
url: string, // URL of the configuration file
passphrase?: string, // The optional passphrase for encrypted data.
): Promise<void>;
Configure VPN connection from an .ovpn configuration file. You can modify set parameters using setParam
method after importing the configuration.
Returns Promise that resolves if the configuration was set successfully, rejects with an error otherwise.
ExpoTunnelkit.configFromString
async function configFromString(
config: string, // configuration string (.ovpn file content)
passphrase?: string, // The optional passphrase for encrypted data.
): Promise<void>;
Configure VPN connection from a configuration string (.ovpn file content). You can modify set parameters using setParam
method after importing the configuration.
Returns Promise that resolves if the configuration was set successfully, rejects with an error otherwise.
ExpoTunnelkit.getConnectionStatus
async function getConnectionStatus(): Promise<VpnStatus>;
type VpnStatus =
| 'Invalid'
| 'Disconnected'
| 'Connecting'
| 'Connected'
| 'Reasserting'
| 'Disconnecting'
| 'None'
| 'Unknown';
Get current VPN connection status.
Returns Promise that resolves to the current VpnStatus
.
ExpoTunnelkit.connect
async function connect(): Promise<void>;
Connect to the VPN server. Session parameters must be set before calling this method. Keep in mind that resolved promise does not always mean that the connection was successful because the connection status can change after the promise is resolved (e.g. connection was established and then immediately dropped by server or client). Use addVpnStatusListener
to get the current connection status.
Returns Promise that resolves if the connection was successful, rejects with an error otherwise.
ExpoTunnelkit.disconnect
async function disconnect(): Promise<void>;
Disconnect from the VPN server.
Returns Promise that resolves if the disconnection was successful, rejects with an error otherwise.
ExpoTunnelkit.addVpnStatusListener
function addVpnStatusListener(
listener: (state: {
VPNStatus: VpnStatus;
Error?: VpnError[keyof VpnError];
}) => void,
): Subscription;
type Subscription = {
remove: () => void;
};
type VpnStatus =
| 'Invalid'
| 'Disconnected'
| 'Connecting'
| 'Connected'
| 'Reasserting'
| 'Disconnecting'
| 'None'
| 'Unknown';
const ExpoTunnelkitError = {
dnsFailure: 'Socket endpoint could not be resolved.',
exhaustedProtocols: 'No more protocols available to try.',
socketActivity: 'Socket failed to reach active state.',
authentication: 'Credentials authentication failed.',
tlsInitialization:
'TLS could not be initialized (e.g. malformed CA or client PEMs).',
tlsServerVerification: 'TLS server verification failed.',
tlsHandshake: 'TLS handshake failed.',
encryptionInitialization:
'The encryption logic could not be initialized (e.g. PRNG, algorithms).',
encryptionData: 'Data encryption/decryption failed.',
lzo: 'The LZO engine failed.',
serverCompression: 'Server uses an unsupported compression algorithm.',
timeout: 'Tunnel timed out.',
linkError: 'An error occurred at the link level.',
routing: 'Network routing information is missing or incomplete.',
networkChanged:
'The current network changed (e.g. switched from WiFi to data connection).',
gatewayUnattainable: 'Default gateway could not be attained.',
unexpectedReply: 'The server replied in an unexpected way.',
} as const;
Add a listener to VPN status changes.
Returns Subscription
object that can be used to unsubscribe the listener.
ExpoTunnelkit.getCurrentConfig
function getCurrentConfig(): Promise<Record<keyof SessionBuilder, string>>;
Get the current VPN connection configuration. Useful for debugging.
Returns Promise that resolves to the current SessionBuilder
configuration.
ExpoTunnelkit.getDataCount
function getDataCount(): Promise<VpnDataCount>;
type VpnDataCount = {
dataIn: number;
dataOut: number;
interval: number;
};
Get the amount of data transferred over the VPN connection during current session (in bytes). dataIn
is the amount of data received, dataOut
is the amount of data sent, interval
is the time interval between internal data count updates (in milliseconds).
Data count interval is set to 1 second by default. You can change it by setting DataCountInterval
parameter (in milliseconds) using setParam
method. Setting it to 0 will disable data count checks.
ExpoTunnelkit.setParam('DataCountInterval', 1000);
Returns Promise that resolves to the VpnDataCount
object.
ExpoTunnelkit.getVpnLogs
async function getVpnLogs(): Promise<string>;
Get the last VPN logs. To start collecting logs, you need to set the Debug
parameter to true
.
setParam('Debug', true);
Returns Promise that resolves to the last VPN logs.
ExpoTunnelkit.addVpnThroughputListener
function addVpnThroughputListener(
listener: (throughput: VpnThroughput) => void,
): Subscription;
type VpnThroughput = {
throughputIn: number;
throughputOut: number;
interval: number;
};
type Subscription = {
remove: () => void;
};
Get the current VPN connection throughput that is being updated in sync with the data count.
Returns Subscription
object that can be used to unsubscribe the listener.
SessionBuilder
type SessionBuilder = {
Username: string;
Password: string;
AppGroup: string;
TunnelIdentifier: string;
Hostname: string;
CipherAlgorithm: Cipher;
DigestAlgorithm: Digest;
CompressionFraming: CompressionFraming;
CompressionAlgorithm: CompressionAlgorithm;
CA: string;
ClientCertificate: string;
ClientKey: string;
TLSWrapStrategy: TLSWrapStrategy;
TLSWrapKeyData: string;
TLSWrapKeyDirection: 0 | 1 | null;
TLSSecurityLevel: number;
KeepAliveInterval: number;
KeepAliveTimeout: number;
RenegotiatesAfter: number;
SocketType: SocketType;
Port: number;
ChecksEKU: boolean;
RandomizeEndpoint: boolean;
UsesPIAPatches: boolean;
AuthToken: string;
PeerID: number;
IPv4Settings: IPv4Settings;
IPv6Settings: IPv6Settings;
DNSServers: string[];
SearchDomains: string[];
HTTPProxy: Proxy;
HTTPSProxy: Proxy;
ProxyAutoConfigurationURL: string;
ProxyBypassDomains: string[];
RoutingPolicies: RoutingPolicy[];
PrefersResolvedAddresses: boolean;
ResolvedAddresses: string[];
MTU: number;
Debug: boolean;
DebugLogFormat: string;
MasksPrivateData: boolean;
DataCountInterval: number; // milliseconds between data count updates (0 to disable, default 1000)
};
VpnStatus
type VpnStatus =
| 'Invalid'
| 'Disconnected'
| 'Connecting'
| 'Connected'
| 'Reasserting'
| 'Disconnecting'
| 'None'
| 'Unknown';
VpnDataCount
type VpnDataCount = {
dataIn: number;
dataOut: number;
interval: number;
};
VpnThroughput
type VpnThroughput = {
throughputIn: number;
throughputOut: number;
interval: number;
};
Cipher
type Cipher =
| 'AES-128-CBC'
| 'AES-192-CBC'
| 'AES-256-CBC'
| 'AES-128-GCM'
| 'AES-192-GCM'
| 'AES-256-GCM';
Digest
type Digest = 'SHA1' | 'SHA256' | 'SHA384' | 'SHA512';
CompressionFraming
type CompressionFraming = 'disabled' | 'compLZO' | 'compress';
CompressionAlgorithm
type CompressionAlgorithm = 'disabled' | 'LZO' | 'other';
TLSWrapStrategy
type TLSWrapStrategy = 'auth' | 'crypt';
SocketType
type SocketType = 'TCP' | 'UDP';
RoutingPolicy
type RoutingPolicy = 'IPv4' | 'IPv6' | 'blockLocal';
Route
type Route = {
destination: string;
mask: string;
gateway: string;
};
IPv4Settings
type IPv4Settings = {
address: string;
addressMask: string;
defaultGateway: string;
routes: Route[];
};
IPv6Settings
type IPv6Settings = {
address: string;
addressPrefixLength: number;
defaultGateway: string;
routes: Route[];
};
Proxy
type Proxy = {
address: string;
port: number;
};
Clone the repository and install dependencies:
git clone https://github.com/sincerely-manny/expo-tunnelkit.git
cd expo-tunnelkit
npm install
At the root of the project run build scripts to compile the TypeScript code for the module and configuration plugin:
npm run build
npm run build plugin
Prebuild the example app:
cd example
npx expo prebuild --clean
Run the example app:
npm run ios
To debug VPN client you can set up a local VPN server using this guide. It's worth mentioning that by default the server uses tls-crypt-v2 which is not supported by the client. After starting up docker container go to admin panel and create a new profile without tls-crypt-v2.
Fix all the buggs, add new features, create a pull request. Drink water, listen to your mom, and don't forget to go out and touch some grass from time to time.