@kadi.build/tunnel-client
v0.4.1
Published
Supporting classes for KadiTunnelService — SSH mode, frpc client mode, reconnection, config generation, and output parsing.
Downloads
824
Readme
@kadi.build/tunnel-client
Supporting classes for KadiTunnelService — the self-hosted HTTPS tunnel provider for KĀDI agents.
This package provides the SSH mode, frpc client mode, config generation, output parsing, reconnection management, and utility functions that KadiTunnelService.js depends on.
KadiTunnelService.js itself is NOT part of this package. It is a standalone drop-in file — just like
NgrokTunnelService.jsandServeoTunnelService.js— that you copy into your project's services directory.
Integration Guide
Step 1: Install the npm package
npm install @kadi.build/tunnel-clientThis provides the supporting classes that KadiTunnelService.js imports.
Step 2: Copy KadiTunnelService.js into your project
Copy client/src/KadiTunnelService.js from the kadi-tunnel repo into your services directory:
src/providers/tunnel/
├── BaseTunnelService.js ← already in your project
├── TunnelService.js ← already in your project (service manager / auto-discovery)
├── errors.js ← already in your project
└── services/
├── NgrokTunnelService.js ← already in your project
├── ServeoTunnelService.js ← already in your project
└── KadiTunnelService.js ← COPY THIS FILE HERENo import changes needed. The file's imports already match the project structure:
// These resolve to your project's existing files (two levels up from services/)
import { BaseTunnelService } from '../BaseTunnelService.js';
import { TransientTunnelError, PermanentTunnelError, ... } from '../errors.js';
// This resolves to the npm package you installed in Step 1
import { KadiSSHMode, KadiClientMode, ... } from '@kadi.build/tunnel-client';Step 3: That's it for code
TunnelService.js uses automatic service discovery — it scans the services/ directory for files matching *Service.js and loads them. Since your file is named KadiTunnelService.js and exports a default class extending BaseTunnelService, it will be auto-discovered and registered under the name 'kadi'.
No manual registration or wiring needed.
Step 4: Configure environment variables
Add these KĀDI-specific variables to your .env file (or set them as environment variables):
# ── KĀDI Tunnel Configuration ──────────────────────────────
KADI_TUNNEL_SERVER=broker.kadi.build # Tunnel server hostname (REQUIRED)
KADI_TUNNEL_TOKEN=<your-token> # Auth token from server (REQUIRED)
KADI_TUNNEL_DOMAIN=tunnel.kadi.build # Wildcard tunnel domain (REQUIRED)
KADI_TUNNEL_PORT=7000 # frp protocol port (default: 7000)
KADI_TUNNEL_SSH_PORT=2200 # SSH gateway port (default: 2200)
KADI_TUNNEL_MODE=auto # 'auto' | 'ssh' | 'frpc' (default: auto)
KADI_TUNNEL_TRANSPORT=wss # 'wss' (default) | 'tcp' — WSS is firewall-friendly
KADI_TUNNEL_WSS_HOST=tunnel-control.kadi.build # WSS control channel hostname
KADI_AGENT_ID=agent # Agent ID for subdomain generation
# ── To make kadi the primary tunnel service ─────────────────
TUNNEL_SERVICE=kadi # was 'ngrok' or 'pinggy'
TUNNEL_FALLBACK_SERVICES=serveo,pinggy # fallback chain| Env Variable | Required | Default | Description |
|-------------|----------|---------|-------------|
| KADI_TUNNEL_SERVER | Yes | '' | Tunnel server hostname (e.g. broker.kadi.build) |
| KADI_TUNNEL_TOKEN | Yes | '' | Authentication token from the tunnel server |
| KADI_TUNNEL_DOMAIN | Yes | '' | Wildcard tunnel domain (e.g. tunnel.kadi.build) |
| KADI_TUNNEL_PORT | No | 7000 | frp protocol port |
| KADI_TUNNEL_SSH_PORT | No | 2200 | SSH gateway port |
| KADI_TUNNEL_MODE | No | auto | 'auto' | 'ssh' | 'frpc' |
| KADI_TUNNEL_TRANSPORT | No | wss | 'wss' | 'tcp' — WSS routes frpc through gateway on :443, bypasses firewalls |
| KADI_TUNNEL_WSS_HOST | When transport=wss | '' | WSS control channel hostname (e.g. tunnel-control.kadi.build) |
| KADI_AGENT_ID | No | agent | Agent identifier for proxy naming / subdomain |
| TUNNEL_SERVICE | No | pinggy | Set to kadi to make it the primary tunnel service |
Step 5: Where the token comes from
The KADI_TUNNEL_TOKEN is generated by the tunnel server during first-time setup. It lives in the server's .env file on the VPS.
To retrieve it:
# SSH into the VPS and read the token
ssh root@<your-vps-ip> 'grep KADI_TUNNEL_TOKEN /root/kadi-tunnel/server/.env'
# Or read it from the frps config directly
ssh root@<your-vps-ip> 'grep auth.token /root/kadi-tunnel/server/frps.toml'The same token is shared by all agents connecting to that server. It's generated once by server/scripts/setup.sh via openssl rand -base64 32.
How config flows through the system
.env / environment variables
↓
ConfigManager.load() ← reads KADI_TUNNEL_* env vars
↓
ConfigManager.getTunnelConfig() ← builds config object with frp.* nested keys
↓
LocalRemoteManager ← calls config.getTunnelConfig()
↓
TunnelProvider(config) ← receives the tunnel config object
↓
TunnelService(config) ← stores config, auto-discovers services
↓
new KadiTunnelService(config) ← reads config.frp.serverAddr, config.frp.token, etc.ConfigManager.getTunnelConfig() maps environment variables to the config object that KadiTunnelService expects:
// ConfigManager.getTunnelConfig() returns:
{
service: 'kadi', // from TUNNEL_SERVICE
fallbackServices: 'serveo,pinggy', // from TUNNEL_FALLBACK_SERVICES
agentId: 'agent', // from KADI_AGENT_ID
frp: {
serverAddr: 'broker.kadi.build', // from KADI_TUNNEL_SERVER
serverPort: 7000, // from KADI_TUNNEL_PORT
sshPort: 2200, // from KADI_TUNNEL_SSH_PORT
token: '<token>', // from KADI_TUNNEL_TOKEN
tunnelDomain: 'tunnel.kadi.build', // from KADI_TUNNEL_DOMAIN
mode: 'auto', // from KADI_TUNNEL_MODE
transport: 'wss', // from KADI_TUNNEL_TRANSPORT
wssControlHost: 'tunnel-control.kadi.build' // from KADI_TUNNEL_WSS_HOST
},
// plus existing ngrok/general keys...
}Step 6: Changes needed in configManager.js and tunnelProvider.js
Two files in the local-remote-file-manager-ability package need modifications (already done in the reference copy under docs/):
src/configManager.js — 3 changes:
- Add defaults for
KADI_TUNNEL_SERVER,KADI_TUNNEL_PORT,KADI_TUNNEL_SSH_PORT,KADI_TUNNEL_TOKEN,KADI_TUNNEL_DOMAIN,KADI_TUNNEL_MODE,KADI_AGENT_IDto thedefaultsobject - Update
getTunnelConfig()to returnagentIdand thefrp: { ... }nested object - Update
save()to write the KĀDI keys to.env
src/providers/tunnelProvider.js — 1 change:
- Add
kadito theserviceConfigsobject
Step 7: Use it
Once configured, kadi works like any other tunnel service:
// The system uses it automatically when TUNNEL_SERVICE=kadi
// No code changes needed — TunnelProvider handles it through the service manager
// Or via the service manager directly (e.g. in tests)
const tunnelService = new TunnelService(config);
await tunnelService.initialize();
const kadiService = tunnelService.getService('kadi');
const tunnel = await kadiService.connect({ port: 3000 });
console.log(tunnel.url); // https://<subdomain>.tunnel.kadi.build
console.log(tunnel.tunnelId); // tunnel_kadi_1234567890_abc123
console.log(tunnel.mode); // 'ssh' or 'frpc'How It Works
Connection Modes
| Mode | Binary Needed | How It Works | Reconnection |
|------|--------------|--------------|--------------|
| SSH (default) | ssh (system) | ssh -R :80:localhost:{port} v0@{serverAddr} -p {sshPort} | Exponential backoff (1s→30s, 5 retries) |
| frpc (enhanced) | frpc on $PATH | Spawns frpc -c /tmp/frpc-{tunnelId}.toml | Built-in (frpc handles it) |
| auto | either | Tries frpc first, falls back to SSH | Depends on selected mode |
Transport Protocol (frpc mode)
| Transport | Port | How It Works | When to use | |-----------|------|-------------|-------------| | wss (default) | 443 | frpc connects via WebSocket Secure through kadi-gateway (Caddy) | Campus/enterprise networks that block non-standard ports | | tcp | 7000 | frpc connects directly to frps bindPort | Open networks with no port restrictions |
WSS transport routes the frpc control channel through tunnel-control.{domain}:443 — Caddy terminates TLS and proxies to frps:7000 on the internal Docker network. SSH mode is unaffected (always uses port 2200).
Tunnel Lifecycle
connect({ port: 3000 })
→ validates config (serverAddr, token, tunnelDomain required)
→ selects mode (auto → detect frpc → fallback SSH)
→ generates subdomain (or uses provided one)
→ spawns SSH/frpc process
→ waits for connection confirmation (parses stdout)
→ returns { tunnelId, url, mode, ... }
→ sets up reconnection manager (SSH mode only)
disconnect(tunnelId)
→ stops reconnection manager
→ kills process (SIGTERM)
→ cleans up frpc config file (frpc mode)
→ removes from active tunnels
shutdown()
→ disconnects all active tunnels
→ stops all reconnection managersEvents
KadiTunnelService emits standard BaseTunnelService events:
| Event | Payload | When |
|-------|---------|------|
| tunnelProgress | { phase, message, tunnelId, timestamp } | connecting, connected, disconnecting, disconnected, reconnecting |
| tunnelCreated | { tunnelId, url, mode, port, ... } | Tunnel established |
| tunnelDestroyed | { tunnelId, timestamp } | Tunnel torn down |
| tunnelError | { error, tunnelId, timestamp } | Error occurred |
connect() Return Object
{
tunnelId: 'tunnel_kadi_1234567890_abc123', // unique ID for disconnect()
url: 'https://my-app.tunnel.kadi.build', // public HTTPS URL
subdomain: 'my-app', // subdomain portion
localPort: 3000, // port being tunneled
createdAt: Date, // when tunnel was created
status: 'active',
service: 'kadi', // service name
mode: 'ssh', // actual mode used
proxyName: 'my-agent-tunnel_kadi_...' // frps proxy name
}getStatus() Return Object
{
serviceName: 'kadi',
activeTunnels: 1,
available: true, // false if serverAddr/token/tunnelDomain missing
status: 'active', // 'active' or 'idle'
mode: 'auto', // configured preference
serverAddr: 'broker.kadi.build',
sshAvailable: true,
frpcAvailable: false, // null if not yet checked
tunnels: [{ tunnelId, url, localPort, mode, status, proxyName, createdAt }]
}What this package exports
These are the supporting classes used internally by KadiTunnelService.js. You typically don't import them directly — they're consumed by the drop-in file.
| Export | Description |
|--------|-------------|
| KadiSSHMode | SSH gateway tunnel mode (ssh -R) — zero binary dependencies |
| KadiClientMode | frpc binary tunnel mode — enhanced features, native reconnect |
| KadiConfigGenerator | Generates frpc TOML configuration files |
| KadiOutputParser | Parses frpc/ssh stdout for connection confirmation |
| ReconnectionManager | Exponential backoff reconnection with jitter |
| detectFrpcBinary | Detects whether frpc is available on $PATH |
| generateSubdomain | Generates an 8-char random subdomain |
Server Setup
The tunnel server must be deployed separately. See the server README for full VPS deployment instructions. The server consists of:
- frps (frp server) — handles tunnel protocol + SSH gateway
- Caddy — reverse proxy with automatic Let's Encrypt TLS for
*.tunnel.{domain}
Required DNS records: A record for server hostname + wildcard A record for *.tunnel.{domain}.
Development
# Run all tests (unit + integration)
npm test
# Unit tests only
npm run test:unit
# SSH mode tests
npm run test:ssh
# frpc client mode tests
npm run test:frpc
# Integration tests (requires running tunnel server)
npm run test:integrationLicense
UNLICENSED
