multi-tun currently hosts local CLIs for:
vless-tunfor DenseVPN / DanceVPN oversing-box, with optional Xray-core sidecar support for VLESS profiles that need Xray-only fieldsopenconnect-tunfor Cisco AnyConnect / ASA profile inspection and future OpenConnect runtime workdumpfor manual VPN capture sessions and local dump artifacts
The vless-tun flow replaces the v2RayTun client path with a controllable desktop workflow for DenseVPN / DanceVPN subscriptions. sing-box remains the default runtime and TUN frontend; selected profiles can opt into an Xray-core sidecar when the provider requires VLESS features that sing-box does not parse.
Current scope:
- fetch a live subscription URL such as
https://key.vpn.dance/connect?... - decode plaintext or base64 subscription payloads
- parse
vless://profiles, including Reality + gRPC variants - cache the latest subscription snapshot locally
- render a
sing-boxTUN config with optional suffix-based direct bypasses, or an Xray sidecar config plussing-boxTUN frontend for Xray-selected profiles
This repo also keeps live notes from previous VPN investigations:
v2raytun-dancevpn-routing.mdcorp-vpn-wifi-bypass.md
Release notes and migration guidance live in RELEASE_NOTES.md.
Operational instructions live under instructions/:
desktop/: current shipped codebasedesktop/internal/core/: shared desktop infrastructuredesktop/internal/vless/: VLESS-specific desktop logicdesktop/internal/anyconnect/: Cisco/OpenConnect-specific desktop logic
android/: Android client/runtime workspace with a modular app shell, persisted generic tunnel config, realVpnService+libboxTUN runtime, and a separate observer app for cross-UID egress checksios/: future iOS client/runtime workspace
Android real-device smoke currently lives behind the dedicated runner:
./scripts/android/run-device-smoke.sh
./scripts/android/run-device-smoke.sh --serial 535a1632
./scripts/android/run-device-smoke.sh \
--serial 535a1632 \
--test-class works.relux.vless_tun_app.TunnelHomeEditorStateTest
./scripts/android/run-device-smoke.sh \
--serial 535a1632 \
--test-class works.relux.vless_tun_app.TunnelEgressSmokeTest \
--source-inline-vless-from-desktop-config
./scripts/android/run-device-suite.sh \
--serial 535a1632 \
--source-inline-vless-from-desktop-configThat path uses preinstall + direct adb shell am instrument instead of connectedDebugAndroidTest, which is more reliable on MIUI/Xiaomi devices.
The stable split is:
TunnelHomeSmokeTest#tunnelHomeLoadsfor cold app launch viaUiAutomatorTunnelHomeEditorStateTestfor source-url editor/save regression via Compose instrumentationTunnelConnectSmokeTestforVpnService/TUN bring-upTunnelEgressSmokeTestfor real cross-UID egress change through the observer app
The live egress loop is verified on a real Xiaomi device with a separate observer app: direct observer egress was 91.77.167.22 · Russia (RU), and after connect it switched to 144.31.90.46 · Finland (FI).
./scripts/setup.sh
./scripts/setup.sh --mac-arch amd64
./scripts/deinit.sh --dry-run
# edit ~/.config/vless-tun/config.json and set servers.<name>.source.url
vless-tun refresh dance
vless-tun list dance
vless-tun setup --source-url "vless://..."
vless-tun set-current fortinetz de
vless-tun start dance
vless-tun start fortinetz nl
vless-tun reconnect dance
vless-tun status dance
vless-tun diagnose
vless-tun stop
vless-tun render dance
vpn-core inspect-vless-url --insecure "https://example.com/subscription"
openconnect-tun status
openconnect-tun setup --vpn-name "Corp VPN"
openconnect-tun profiles
openconnect-tun inspect-profiles
openconnect-tun set-current ural ural-outside
openconnect-tun start msk msk-outside --mode full --dry-run
openconnect-tun start ural ural-outside --mode split-include --dry-run
dump status./scripts/setup.sh installs the shipped desktop toolchain end-to-end: it ensures the default runtime prerequisites such as sing-box, builds the bundled desktop/cmd/vpn-auth Swift helper, and links the resulting binaries into ~/.local/bin. xray is an optional runtime prerequisite only for configs that set engine.type=xray.
That installed toolchain now also includes android-release, the local Go helper for:
- seeding Android release-signing metadata
- generating the Play upload keystore
- building a signed
app-release.aabfor Play test tracks with required release notes sidecar output - emitting
native-debug-symbols.zipand ProGuard mapping artifacts for Play symbolication - manually publishing the signed bundle to a chosen Google Play track via Gradle Play Publisher
On macOS the default ./scripts/setup.sh path is host-native: on Apple Silicon it builds/install arm64 binaries, and on Intel Macs it builds/install amd64 binaries with the normal toolchain prerequisites for that machine. If you explicitly pass --mac-arch arm64|amd64 for the non-host architecture, setup switches into artifact-only cross-build mode and writes desktop binaries into artifacts/releases/ without touching ~/.local/bin, configs, or skill wiring.
./scripts/deinit.sh removes the managed multi-tun global/local skill links and ~/.local/bin symlinks. Config, cache, keychain secrets, and repo build artifacts stay intact unless you pass the explicit --purge-* flags.
Generated artifacts:
- cache snapshot:
servers.<name>.cache_dir/snapshot.json - rendered sing-box config:
servers.<name>.artifacts.singbox_config_path - rendered Xray sidecar config when
engine.type=xray:servers.<name>.artifacts.xray_config_path, or a derivedxray_*.jsonnext to the sing-box config
| Tool | Used for | How to run | Outputs |
|---|---|---|---|
task-board |
File-based task tracking for repo work | task-board q --format compact 'summary()', task-board m 'set_notes(TASK-ID, text="...")' |
.task-board/ |
vless-tun |
VLESS subscription refresh, profile rendering, and local TUN sessions | vless-tun refresh dance, vless-tun render fortinetz nl |
~/.config/vless-tun/, ~/.cache/vless-tun/, rendered runtime JSON |
openconnect-tun |
Cisco/OpenConnect profile inspection and runtime experiments | openconnect-tun status, openconnect-tun inspect-profiles |
~/.config/openconnect-tun/, ~/.cache/openconnect-tun/ |
vpn-core |
Privileged helper backend for tunnel startup and packet capture support; also includes a user-space VLESS subscription metadata probe | vpn-core install, vpn-core status, vpn-core inspect-vless-url --insecure <url> |
LaunchDaemon state, helper logs under runtime cache paths |
sing-box |
VLESS/Reality client runtime and config validation | sing-box check -c <config.json> |
Runtime logs in session cache or .temp/ |
xray |
Optional VLESS engine sidecar for profiles that require Xray-only VLESS fields such as encryption=mlkem... |
xray run -c <xray-config.json> through vless-tun start when engine.type=xray |
Sidecar config and sidecar logs under the selected vless-tun cache/artifact paths |
Docker / Colima |
Linux container labs and controlled SNI-gate experiments | artifacts/fortinetz-cascade-whitelist/lab/run-macmini-sni-lab.sh nl |
.temp/fortinetz-cascade/sni-lab-remote/; remote scratch under /Users/administrator/.cache/multi-tun/fortinetz-sni-lab |
ssh / rsync |
Remote Mac mini lab orchestration without disturbing the local tunnel | ssh relux-works-dedicated-macmini ..., used by the lab runner |
.temp/fortinetz-cascade/remote-*.log |
go |
Desktop CLI builds and tests | go test ./..., go build -o vless-tun ./desktop/cmd/vless-tun |
local binaries, test logs in .temp/ when captured |
xcodebuild / swift |
Apple helper builds and tests when needed | run through ./scripts/setup.sh or targeted Swift build commands |
build products under project and toolchain output paths |
adb / Gradle |
Android smoke and instrumentation tests | ./scripts/android/run-device-smoke.sh --serial <serial> |
Android build outputs and smoke logs under script-defined paths |
vless-tun setup
vless-tun init
vless-tun refresh dance
vless-tun list dance
vless-tun set-current fortinetz de
vless-tun start dance
vless-tun start fortinetz nl
vless-tun reconnect dance
vless-tun status dance
vless-tun stop
vless-tun render danceUse this companion CLI when you need to inspect Cisco AnyConnect / ASA state, resolve real ASA profile targets, and stage OpenConnect routing experiments next to vless-tun.
The corporate hostnames, domains, profile labels, and IPs shown in the examples below are anonymized placeholders.
vpn-core status
vpn-core install
openconnect-tun status
openconnect-tun helper status
openconnect-tun helper install
openconnect-tun profiles
openconnect-tun inspect-profiles
openconnect-tun inspect-profiles --dir ~/Downloads/cisco-anyconnect-profiles/profiles
openconnect-tun setup --vpn-name 'Corp VPN'
openconnect-tun set-current ural ural-outside
openconnect-tun start msk msk-outside --mode full --dry-run
openconnect-tun start ural ural-outside --mode split-include --dry-run
openconnect-tun start --profile 'Ural Outside extended' --mode full --dry-run
openconnect-tun start --profile 'Ural Outside extended' --mode split-include \
--second-factor-mode manual_otp \
--route 198.51.100.0/24 \
--route 203.0.113.0/24 \
--vpn-domains corp.example,digital.example,services.corp.example \
--bypass-suffixes bypass.corp.example \
--dry-run
openconnect-tun reconnect --profile 'Ural Outside extended' --mode full
openconnect-tun routes
openconnect-tun stop
dump start
dump start --probe-host gitlab.services.corp.example --probe-host portal.corp.example
dump status
dump stop
dump inspect --session-id 20260326T144914ZOperational notes:
openconnect-tunkeeps its own runtime state under~/.cache/openconnect-tun, with session logs in~/.cache/openconnect-tun/sessionsand the current session pointer in~/.cache/openconnect-tun/runtime/current-session.json. This is intentionally separate from~/.cache/vless-tun.vpn-core installperforms the one-time privileged setup for autonomous runs. It installs a shared root LaunchDaemon that exposes a user-owned unix socket, so lateropenconnect-tunandvless-tuncommands can reuse the same trusted backend without repeatedsudo.- on macOS
vless-tun startinnetwork.mode=tunnow refuses to start if the upstream VLESS server itself already routes through another VPN interface such asutun*,tun*,ppp*, oripsec*. That avoids accidental nested-tunnel startup wherevless-tunsilently builds on top ofv2RayTun, AnyConnect, or another active VPN and drags the whole network into an undefined state. - Existing installs of the legacy
works.relux.openconnect-tun-helperdaemon are auto-detected for compatibility. A freshvpn-core installreplaces that legacy helper with the shared core service. openconnect-tun helper install|status|uninstallremain as compatibility wrappers around the sharedvpn-coreservice.--profileresolves against local AnyConnect XMLHostEntryvalues and automatically deduplicates the same server repeated across/opt/cisco/...and~/Downloads/....- the canonical lifecycle commands are now
start,reconnect,status, andstop;run,connect, anddisconnectremain as compatibility aliases. openconnect-tun set-current [server [profile]]updatesdefault.server_urlanddefault.profilein~/.config/openconnect-tun/config.json. If the selected configured server has the current default profile or exactly one configured profile, the profile argument may be omitted; otherwise pass it explicitly.openconnect-tun start [server [profile]]andopenconnect-tun reconnect [server [profile]]can override the default configured server/profile for one run. Existing--server/--profileflags remain supported.- configured OpenConnect server keys can be friendly aliases. Put the real ASA endpoint in
servers.<alias>.server_url; configured profile keys can also be aliases, with the real AnyConnect profile label inservers.<alias>.profiles.<profile_alias>.name. openconnect-tuncan read auth defaults from~/.config/openconnect-tun/config.json. The preferred shape isservers.<url>.auth, so credentials are selected from the server being used. The legacy root-levelauthblock still works as a compatibility fallback. The shipped bootstrap convention is fully keychain-backed:servers.<url>.auth.username_keychain_account=corp-vpn/usernameandservers.<url>.auth.password_keychain_account=corp-vpn/password. Plainusernameremains as a compatibility fallback.servers.<url>.auth.second_factor.modecontrols whether the OTP step waits for manual SMS/manual entry or auto-submits TOTP.- live auth now defaults to
--auth aggregate, which is the only path that currently completes the example SSO+CSD flow on this machine;--auth openconnectremains available as the directopenconnect --authenticatepath for debugging.vpn-authis used for the browser-assisted SAML steps in aggregate mode, with preset-cookie support for follow-up pages. ./scripts/setup.shnow treats the full live runtime as part of the shipped toolchain: it ensuressing-boxforvless-tun,totp-cliforvpn-auth, buildsdesktop/cmd/vpn-auth, installs the resulting binary into~/.local/bin, and./scripts/deinit.shremoves that managed binary link again.- the CSD helper is resolved from the active OpenConnect install, preferring native Cisco
libcsd.dylibwhen it is available under~/.cisco/...; otherwise it falls back to Homebrew's stableopt/openconnect/libexec/openconnect/csd-post.shpath instead of versionedCellar/...paths. The fallbackcsd-post.shpath is still wrapped with tiny macOS shims forpidofand GNU-stylestat -c %Y. dumpis now the canonical activity-oriented name for packet diagnostics.cisco-dumpremains installed as a compatibility alias, while runtime state stays under~/.cache/cisco-dumpfor continuity.dumpkeeps its own runtime state under~/.cache/cisco-dump, with session logs in~/.cache/cisco-dump/sessions, the current session pointer in~/.cache/cisco-dump/runtime/current-session.json, mirrored Cisco logs and cache artifacts under each session directory, tracked Cisco per-pidlsofsnapshots, all-loopback TCP snapshots fromlsof/netstat, a default tunnel-aware traffic capture pcap, a separate loopback OCSC pcap, and host-level DNS/route/TCP/HTTPS probe snapshots for the anonymized example targets.dump startnow defaults topktap,allwith a broadtcp or udpfilter so tunnel traffic onutun*, loopback IPC, and ordinary uplink traffic are captured in one session. A separatelocalhost-loopback.pcapsidecar is still recorded so OCSC decoding remains stable.- when a
dumpsession stops with an OCSC loopback pcap present, it now also derivesocsc-timeline.txtandocsc-summary.txtfrom AnyConnect OCSC loopback traffic.dump inspect --session-id ...can re-run that decoder on an older captured session. dump startnow defaults to probinggitlab.services.corp.exampleandportal.corp.example; pass--probe-host,--probe-ns, or--no-host-probeswhen you need a different target set.- live
dumptcpdumpcapture now prefers the sharedvpn-corebackend when it is installed, and falls back tosudootherwise. The rest of the diagnostic session stays in user space. - live
startnow emits compactauth_stage:updates in the terminal during authentication instead of forcing you to tail the session log for every auth transition. The full detailed auth log still stays in the session log file. - live
startnow resolves the privileged backend automatically: sharedvpn-corefirst when it is installed, otherwise the oldsudo -v+sudo -npath so the privileged password prompt still does not compete with the cookie being piped into--cookie-on-stdin. --mode fulluses the stockvpnc-script, so OpenConnect will own default route and global DNS. Treat it as a smoke-test path, not a coexistence-safe mode.--mode split-includeusesvpn-slice. On macOS that means scoped/etc/resolver/<domain>files for VPN DNS instead of replacing the global resolver stack, which is the direction we want for split-include coexistence next tovless-tun.- host trust and guest trust are separate concerns. A macOS host may trust a corporate CA in Keychain while a Colima or Docker VM still rejects the same internal registry with
x509: certificate signed by unknown authority. If an internal registry is reachable from the host butdocker pullfails inside the guest, install the corporate CA into the guest trust store or into/etc/docker/certs.d/<registry>/ca.crton the daemon host.
Scaffolds a default openconnect-tun config and the matching keychain account names for one VPN profile.
Pass --vpn-name with the user-facing AnyConnect profile name. setup resolves the matching server_url from local AnyConnect XML, writes a default full-mode config with no bypasses, seeds placeholder keychain entries for username/password/TOTP, and prints the resulting config path so the caller can review it.
If TOTP starts from a Google Authenticator export QR, use ./scripts/google-auth-export-secret.sh. The export URL contains a URL-encoded base64 protobuf payload, but the final Keychain value for totp_secret must be the derived base32 secret:
./scripts/google-auth-export-secret.sh 'otpauth-migration://offline?...'
./scripts/google-auth-export-secret.sh --list 'otpauth-migration://offline?...'Default live config path:
~/.config/openconnect-tun/config.json
Example config:
{
"cache_dir": "~/.cache/openconnect-tun",
"default": {
"server_url": "vpn-gw2.corp.example/outside",
"profile": "ural-outside"
},
"servers": {
"ural": {
"server_url": "vpn-gw2.corp.example/outside",
"auth": {
"username_keychain_account": "corp-vpn/username",
"password_keychain_account": "corp-vpn/password",
"second_factor": {
"mode": "manual_otp",
"totp_secret_keychain_account": "corp-vpn/totp_secret"
},
"fallback_servers": [
"vpn-gw2-backup.corp.example/outside"
]
},
"client_mimicry": {
"user_agent": "AnyConnect",
"version": "4.10.08029",
"os": "mac-intel",
"local_hostname": "corp-macbook",
"auth_methods": [
"single-sign-on-v2",
"single-sign-on-external-browser"
],
"http_headers": {
"X-Support-HTTP-Auth": "true"
}
},
"profiles": {
"ural-outside": {
"name": "Ural Outside extended",
"mode": "split-include",
"split_include": {
"routes": [
"10.0.0.0/8",
"11.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"
],
"nameservers": [
"10.23.16.4",
"10.23.0.23"
],
"vpn_domains": [
"corp.example",
"corp-it.example",
"corp-it.internal",
"erp.example",
"short.example",
"corp-sec.example",
"branch.example"
],
"bypass_suffixes": [
"vpn-gw2.corp.example",
"bypass.corp.example"
]
}
},
"public": {
"name": "Public Corp",
"mode": "full"
}
}
}
}
}Field reference:
cache_dir: local runtime/state directory foropenconnect-tun. This is where the CLI keeps its own ephemeral artifacts, not user-authored config.cache_dirsession logs:~/.cache/openconnect-tun/sessions/openconnect-session-<UTC timestamp>.logcache_dirruntime metadata:~/.cache/openconnect-tun/runtime/current-session.jsoncache_dirhelper/runtime logs: files under~/.cache/openconnect-tun/runtime/, such as orphan-cleanup logscache_dirseparation: intentionally separate from~/.cache/vless-tun, so the two tunnel stacks do not overwrite each other's runtime statedefault.server_url: the default OpenConnect target whenstart|reconnectruns without an explicit--serverdefault.profile: the default configured profile key whenstart|reconnectruns without an explicit--profile; it may be a friendly alias such asural-outsidedefaultpairing: these two fields are meant to point at the same configured VPN choice, so the config reads as one default selection instead of separate root-level knobsopenconnect-tun set-current: validates the selectedservers.<url>entry and profile, then rewrites the paireddefaultblock for later no-argument startsservers.<alias>: configuration bucket for one ASA endpoint. The key may be a friendly alias such asmskoruralservers.<alias>.server_url: the real ASA endpoint such asvpn-gw2.corp.example/outside; legacy configs may still use the URL itself as theserverskeyservers.<alias>.auth: preferred auth override for that concrete server. Use this when different ASA endpoints require different keychain entries, usernames, or aggregate-auth fallback behaviorservers.<url>.auth.second_factor.mode: second-factor behavior for browser-assisted SAML auth. Valid values aremanual_otpandtotp_auto.servers.<url>.auth.second_factor.mode=manual_otp: auto-fill username/password, then wait for an SMS or other manual one-time code on terminal stdin.vpn-authinjects that code into the OTP WebView and submits the form; if the page returns to OTP, enter the next code in the terminal.servers.<url>.auth.second_factor.mode=totp_auto: readsecond_factor.totp_secret_keychain_account, generate TOTP, and auto-submit the OTP form. The legacy top-leveltotp_secret_keychain_accountis still accepted as a compatibility shorthand fortotp_auto.servers.<url>.auth.second_factor.totp_secret_keychain_account: Keychain account containing the base32 TOTP secret. It can remain present whilemode=manual_otp, so switching back tototp_autodoes not require rediscovering the account name.--second-factor-mode manual_otp|totp_auto: per-run override forstartandreconnect, useful when the same ASA/SAML flow oscillates between SMS and authenticator TOTP.servers.<url>.auth.fallback_servers: aggregate-auth fallback targets for that concrete server. If the initial endpoint or its redirect target returns an auth-request withoutsso-v2-login, the session log records the original request URL, final redirected URL, response summary, and configured fallback selected before retrying. Other request failures are not hidden by this fallback.servers.<url>.client_mimicry: optional AnyConnect client identity for that concrete server. It is applied to aggregate-auth init/reply requests and to the finalopenconnectcommand, so endpoint-specific ASA/LB behavior can be reproduced without provider-specific code.servers.<url>.client_mimicry.user_agent: User-Agent for aggregate-auth HTTP requests andopenconnect --useragent. Defaults toAnyConnectwhen omitted.servers.<url>.client_mimicry.version: client version for aggregate-auth XML<version>andopenconnect --version-string. Defaults to the locally detected Cisco Secure Client version, then4.10.07061.servers.<url>.client_mimicry.os: OS value foropenconnect --os; it is also used as aggregate-auth<device-id>whendevice_idis omitted.servers.<url>.client_mimicry.device_id: explicit aggregate-auth XML<device-id>override when it should differ fromos.servers.<url>.client_mimicry.local_hostname: local hostname passed toopenconnect --local-hostname; omitted values still use the host-detected name.servers.<url>.client_mimicry.auth_methods: aggregate-auth<capabilities>auth methods. Omit the field to keep the Cisco-like default method list.servers.<url>.client_mimicry.http_headers: extra or overriding aggregate-auth HTTP headers by name. Values are logged only by header name, not by value.servers.<alias>.profiles.<profile_alias>: one profile variant under that server; this is wherename,mode, andsplit_includeliveservers.<alias>.profiles.<profile_alias>.name: the real AnyConnect profile label passed to OpenConnect, such asUral Outside extended- legacy auth fallback: root-level
authis still accepted for older configs, but new configs should preferservers.<url>.auth servers.<url>.profiles.<profile>.mode: default connect mode for that profile. Usesplit-includefor coexistence-safe split routing orfullfor stock full-tunnel behaviorservers.<url>.profiles.<profile>.split_include.routes: included CIDRs, hosts, or aliases passed tovpn-slicein split-include modeservers.<url>.profiles.<profile>.split_include.nameservers: scoped VPN DNS servers to use for that profileservers.<url>.profiles.<profile>.split_include.vpn_domains: suffix masks, not exact-only hostnames.corp.examplealready covers*.corp.example, so do not also list covered entries likeinside.corp.exampleorregion.corp.exampleservers.<url>.profiles.<profile>.split_include.vpn_domainsnormalization: the CLI now collapses covered suffixes automatically during option resolution, but the config should still stay clean and minimalservers.<url>.profiles.<profile>.split_include.bypass_suffixes: suffixes that must stay on the public resolver path even when a broader VPN suffix also matchessplit_include.bypass_suffixessemantics: on macOSopenconnect-tunimplements bypasses by writing a more specific public/etc/resolver/<suffix>entry over the broader VPN-scoped resolver, sobypass.corp.examplecan stay public whilecorp.examplestill uses VPN DNS- split-DNS cleanup: when
search.tailscalealready contains search domains covered bysplit_include.vpn_domains,openconnect-tunremoves both exact and covered subdomain entries from that search resolver while preserving anything covered bysplit_include.bypass_suffixes, so macOS does not bounce between public and VPN search paths - built-in example augmentation: for the anonymized
/outsideexample targetsopenconnect-tunstill augments split DNS with the extra official Cisco suffix set (inside.corp.example,region.corp.example,branch.example,workspace.example, and related domains), so the config can stay minimal without manually restating every covered subdomain
Creates ~/.config/vless-tun/config.json by default using the preferred schema.
Use --source-url for either an HTTP subscription endpoint or a literal vless://... URI. setup prints the resulting config path so the caller can review it immediately.
With no config argument, vless-tun uses ~/.config/vless-tun/config.json.
Lifecycle commands use positional arguments for selection overrides against that config:
vless-tun start [server [profile]]
vless-tun render [server [profile]]
vless-tun status [server [profile]]
vless-tun reconnect [server [profile]]
vless-tun set-current [server [profile]]
vless-tun list [server]
vless-tun refresh [server]
vless-tun stop
vless-tun diagnose
vless-tun diagnose config [server [profile]]stop and diagnose do not require a provider/profile; they scan all configured session cache directories for recorded tunnel state. reconnect uses the same stop-all scan before starting the selected provider/profile, so switching current.server from one provider to another does not leave the old tunnel blocking the new one. Use diagnose config when you want to validate provider/profile selection.
Use --config when you intentionally want a non-default config file. setup and init still accept one positional config path for bootstrapping.
Updates current.server and current.profile in ~/.config/vless-tun/config.json, so later no-argument commands use that selection:
vless-tun set-current dance
vless-tun set-current freedom
vless-tun set-current fortinetz deIf the selected server has a default profile or exactly one profile, the profile argument may be omitted. If the server has multiple profiles and no default, pass the profile explicitly.
Creates ~/.config/vless-tun/config.json by default.
init remains as the compatibility entrypoint. --subscription-url remains the compatibility flag name, but the preferred config shape now writes it into source.url.
Refreshes the configured VLESS source, parses all available vless:// profiles, and writes a local cache snapshot.
source.mode=proxy means source.url is fetched over HTTP and expected to resolve to one or more vless:// entries. source.mode=direct means source.url already contains a literal vless://... URI and no extra fetch indirection is used.
Shows the cached profiles in a compact form. Use --refresh if you want it to pull the subscription first.
Refreshes the selected provider subscription cache before rendering, including explicit profile aliases such as fortinetz nl, so provider-side endpoint, SNI, fingerprint, and profile changes are picked up at start time. Use --refresh=false only when the subscription endpoint is unavailable and the last cached snapshot is good enough to start from.
If the configured profile alias no longer matches any profile in the refreshed subscription, start fails before rendering or starting the tunnel, prints the available profiles, and points at the servers.<name>.profiles.<alias>.selector value to update.
Short provider starts work through positional selection overrides:
vless-tun start dance
vless-tun start fortinetz nl
vless-tun start fortinetz de
vless-tun start freedomWhen vless-tun is running in network.mode=tun above an active openconnect-tun split-include session, start now waits for overlay DNS convergence before returning. In that overlay mode, a live sing-box PID alone is not treated as ready; the CLI also waits for the system public resolver path to settle so follow-up terminal clients do not race the DNS handoff. The public resolver handoff is applied through a scoped scutil resolver without copying MTS/corporate split domains into search suffixes; the active network service DNS is only a fallback if scoped handoff fails.
Each start creates a new timestamped session log and metadata pair under:
~/.cache/vless-tun/sessions/sing-box-session-<UTC timestamp>.log~/.cache/vless-tun/sessions/session-<UTC timestamp>.json
With the default logging.max_lines=1000, helper-backed sessions retain only the last 1000 lines in the current sing-box log. This keeps accidental logging.level=info runs from creating multi-million-line files, but verbose engine logging can still cost CPU; use logging.level=warn for normal TUN runtime.
When engine.type=xray, vless-tun starts Xray as a local SOCKS sidecar first, then starts the sing-box TUN frontend. The sidecar gets its own log:
~/.cache/vless-tun/sessions/xray-session-<UTC timestamp>.log
Before starting a sidecar, start scans for stale sidecar processes that match the configured sidecar executable/name and the same generated sidecar config path. Matching stale processes receive SIGTERM, with SIGKILL escalation if they do not exit. This cleanup is intentionally scoped to the generated config path; it does not broadly kill every xray process on the host.
The currently active session pointer is stored at:
~/.cache/vless-tun/runtime/current-session.json
start is the command that should actually bring the tunnel up. status does not connect anything by itself.
In network.mode=tun, start resolves the launch backend automatically. If the shared vpn-core daemon is installed, that is the preferred happy-path backend. An explicit launch block is only needed as an override for fallback or debugging:
auto: resolve to sharedvpn-corewhen it is installed, otherwisesudofor TUN as a regular user anddirectwhen already rootsudo: runsudo sing-box run -c ...after caching credentials withsudo -vdirect: runsing-boxdirectly; intended for already-root environmentshelper: spawn and stop rootsing-boxthrough the sharedvpn-coredaemonlaunchd: compatibility alias forhelper; the long-lived LaunchDaemon now belongs tovpn-core, not to eachsing-boxsession
Reloads the local config, stops any currently recorded session across all configured VLESS server cache directories, refreshes the subscription cache by default, rerenders the selected profile, and starts a fresh vless-tun session with the selected engine.
This is the command to use after changing:
current.servercurrent.profileservers.<name>.profiles.<profile>.selectorrouting.bypass_suffixesrouting.routesdns.proxy_resolverservers.<name>.artifacts.singbox_config_pathservers.<name>.artifacts.xray_config_pathengine- any other render-time setting in
~/.config/vless-tun/config.json
Shows the current local view of the tunnel state:
- whether a recorded
vless-tunsession is active, stale, or absent - the selected engine and any recorded sidecar processes
- the current session ID, PID, launch mode, start timestamp, and log file path
- whether the configured TUN interface exists
- whether the rendered config file exists
- which profile is selected from cache
- which bypass suffixes are configured
- which cached profiles are available
This is a heuristic runtime status, not a control plane.
vless-tun diagnose is a tunnel/runtime diagnostic. It scans configured session cache directories and reports recorded session state plus launch backend readiness. When the effective launch backend resolves to helper or launchd, it checks the shared vpn-core daemon and reports the current socket and daemon PID.
vless-tun diagnose config [server [profile]] is a config diagnostic. It validates selection, resolved cache/render paths, source mode, bypasses, and launch settings without starting or stopping a tunnel.
Stops the currently recorded vless-tun session using SIGTERM by default. For engine.type=xray, this also stops the Xray sidecar. Use --force if you want it to escalate to SIGKILL after the timeout.
Selects a cached profile and writes runtime JSON config with:
- a TUN inbound
- proxy detour for the rest of the traffic
- optional direct DNS and direct outbound for configured suffix bypasses
- automatic direct routing and TUN route exclusion for IP-literal upstream VLESS endpoints, so the proxy server itself is not captured by broad full-TUN routes
- default route sniffing, unless disabled through
singbox.sniff - optional outbound TLS fragmentation, TLS record fragmentation, fallback delay, and TLS curve preferences from
singbox.tls
If routing.bypass_suffixes is empty, the renderer produces a simple full-tunnel config with no suffix-based bypasses.
With the default engine.type=sing-box, the generated sing-box config contains the VLESS outbound directly. With engine.type=xray, render writes an Xray VLESS sidecar config and a sing-box TUN frontend config whose proxy outbound points at the local Xray SOCKS inbound. The frontend adds a direct process_name rule for Xray so the sidecar's upstream connection is not captured by the TUN.
Inspects a VLESS subscription URL or literal vless:// URI and prints safe profile metadata: payload format, profile count, display name, endpoint, protocol, security, network, SNI, fingerprint, and whether sensitive fields are present. It intentionally does not print the raw URI, UUID, public key, or short ID.
For HTTP subscriptions the probe runs an analyzer chain and prints every attempt. The chain covers the default request, explicit Host/SNI options, insecure TLS, browser/v2ray user agents, and certificate-derived Host/SNI hints. That keeps one-off provider URLs debuggable without editing scripts.
vpn-core inspect-vless-url "vless://..."
vpn-core inspect-vless-url --insecure "https://example.com/subscription"
vpn-core inspect-vless-url --host subrouter.fyi --tls-server-name subrouter.fyi "https://203.0.113.10:7654/freedom/token"Default live config path:
~/.config/vless-tun/config.json
Repo-local example:
configs/local.example.json
If you want to keep using a repo-local config, pass --config.
Example config:
{
"current": {
"server": "dance",
"profile": "default"
},
"servers": {
"dance": {
"source": {
"mode": "proxy",
"url": "https://key.vpn.dance/connect?key=REPLACE_ME"
},
"cache_dir": "~/.cache/vless-tun/dance",
"artifacts": {
"singbox_config_path": "~/.config/vless-tun/generated/sing-box_dance.json"
},
"engine": {
"type": "sing-box"
},
"routing": {
"bypass_suffixes": [
".ru",
".рф"
],
"bypass_exclude_suffixes": [
".telegram.org",
"t.me"
],
"routes": []
},
"profiles": {
"default": {
"transport": "grpc"
}
}
},
"fortinetz": {
"source": {
"mode": "proxy",
"url": "https://sub.fortinetz.com/sub/t/REPLACE_ME"
},
"cache_dir": "~/.cache/vless-tun/fortinetz",
"artifacts": {
"singbox_config_path": "~/.config/vless-tun/generated/sing-box_fortinetz.json"
},
"engine": {
"type": "sing-box"
},
"routing": {
"bypass_suffixes": [
".ru",
".рф"
],
"bypass_exclude_suffixes": [
".telegram.org",
"t.me"
],
"routes": []
},
"profiles": {
"nl": {
"selector": "Netherlands"
},
"de": {
"selector": "Germany"
}
}
},
"freedom": {
"source": {
"mode": "proxy",
"url": "https://example.com/freedom/REPLACE_ME"
},
"cache_dir": "~/.cache/vless-tun/freedom",
"artifacts": {
"singbox_config_path": "~/.config/vless-tun/generated/sing-box_freedom.json",
"xray_config_path": "~/.config/vless-tun/generated/xray_freedom.json"
},
"engine": {
"type": "xray",
"xray": {
"executable": "xray",
"socks_listen": "127.0.0.1",
"socks_port": 20808,
"process_names": ["xray"]
}
},
"routing": {
"bypass_suffixes": [
".ru",
".рф"
],
"bypass_exclude_suffixes": [
".telegram.org",
"t.me"
],
"routes": []
},
"profiles": {
"default": {
"selector": ""
}
}
}
},
"network": {
"mode": "tun",
"tun": {
"interface_name": "utun233",
"addresses": [
"172.19.0.1/30",
"fdfe:dcba:9876::1/126"
]
}
},
"dns": {
"proxy_resolver": {
"address": "1.1.1.1",
"port": 853,
"tls_server_name": "cloudflare-dns.com"
}
},
"logging": {
"level": "warn",
"max_lines": 1000
}
}Field reference:
current.server: selected configured server/source, such asdanceorfortinetzcurrent.profile: selected local profile alias under the selected serverservers.<name>.source.mode:proxyordirectservers.<name>.source.url: the actual source address; inproxymode this is an HTTP endpoint that resolves to one or morevless://entries, and indirectmode this is a literalvless://...URIservers.<name>.cache_dir: local runtime/cache directory for that server's refresh snapshots, session logs, and runtime metadataservers.<name>.artifacts.singbox_config_path: generated sing-box config path for that serverservers.<name>.artifacts.xray_config_path: generated Xray sidecar config path forengine.type=xray; if omitted, the path is derived from the sing-box config nameservers.<name>.profiles.<profile>.transport: optional explicit VLESS transport selector, currentlytcporgrpc. Set this when a provider returns one TCP and one gRPC profile for the same logical server, such as Danceservers.<name>.profiles.<profile>.selector: optional selector by exact id, exact name, endpoint, transport name, or substring. If a source resolves to multiple remote profiles andtransportis unset, startup fails instead of silently following provider orderservers.<name>.engine.type: required runtime engine for each configured server in multi-server configs; valid values aresing-boxandxrayenginemay also exist globally or per profile for shared settings and overrides, but everyservers.<name>block still needs its own explicitengine.typeengine.xray.executable: Xray binary name or absolute path. The binary must be installed and runnable beforestartorreconnectengine.xray.socks_listen: local Xray SOCKS inbound address, default127.0.0.1engine.xray.socks_port: local Xray SOCKS inbound port, default20808engine.xray.process_names: process names routeddirectby the generated sing-box TUN frontend, defaulting to the Xray executable basenamenetwork.mode: currentlytunnetwork.tun.interface_name: TUN interface name fortunmodenetwork.tun.addresses: TUN addresses fortunmoderoutingmay exist globally, per server, or per profile; more specific values override broader valuesrouting.bypass_suffixes: domains that should godirect; set[]for full-tunnel bring-uprouting.bypass_exclude_suffixes: optional suffixes that must stay on proxy even when a broader bypass list existsrouting.routes: CIDRs/IPs that should routedirectsingboxmay exist globally, per server, or per profile; more specific values override broader valuessingbox.sniff.enabled: controls the generated sing-box route sniff action. Default istrue, preserving the previous renderer behaviorsingbox.sniff.sniffers: optional sing-box sniffers list such as["tls", "http"]singbox.sniff.timeout: optional sniff timeout string such as"1s"singbox.tls.fragment: optional boolean for sing-box outbound TLS fragmentationsingbox.tls.record_fragment: optional boolean for sing-box outbound TLS record fragmentationsingbox.tls.fragment_fallback_delay: optional delay string for fragmentation fallback, such as"500ms"singbox.tls.curve_preferences: optional TLS curve preference list such as["X25519MLKEM768", "X25519"]dns.proxy_resolver: upstream DNS endpoint for proxied trafficlogging.level: runtime log level written into generated configs; valid values aretrace,debug,info,warn,error,fatal, andpanic. New configs default towarnso normal full-TUN sessions do not write every connection to disk. Xray sidecar rendering mapswarnto Xray'swarninglogging.max_lines: maximum number of tail lines retained in bounded session logs, default1000. Set0to disable bounding. The sharedvpn-corehelper applies this to long-runningsing-boxstdout/stderr;vless-tuninternal runtime events also use the same bounded append pathlaunch.mode: optional override for the runtime backend. Omitlaunchin the happy path andvless-tunwill resolve to the sharedvpn-corebackend automatically when it is availablelaunch.labelandlaunch.plist_path: legacy compatibility overrides only; the shared daemon now belongs tovpn-core, not to eachsing-boxsession- legacy flat configs with root-level
source,cache_dir,artifacts, anddefault.profile_selectorstill load; new multi-server configs are preferred for more than one provider/source
Example per-server sing-box tuning:
{
"servers": {
"freedom": {
"singbox": {
"sniff": {
"enabled": true,
"sniffers": ["tls", "http"],
"timeout": "1s"
},
"tls": {
"fragment": true,
"record_fragment": true,
"fragment_fallback_delay": "500ms",
"curve_preferences": ["X25519MLKEM768", "X25519"]
}
}
}
}
}Example per-server Xray engine override:
{
"servers": {
"freedom": {
"artifacts": {
"singbox_config_path": "~/.config/vless-tun/generated/sing-box_freedom.json",
"xray_config_path": "~/.config/vless-tun/generated/xray_freedom.json"
},
"engine": {
"type": "xray",
"xray": {
"executable": "xray",
"socks_listen": "127.0.0.1",
"socks_port": 20808,
"process_names": ["xray"]
}
}
}
}
}VLESS URI parameters such as encryption=mlkem... and spx=... are parsed into the profile cache. With engine.type=xray, they are written into the Xray sidecar config. singbox.tls.curve_preferences is a sing-box TLS setting; it is not a replacement for unsupported VLESS outbound encryption fields in sing-box versions that reject them.
Example config fragment for a real TUN session managed by the shared vpn-core daemon without any explicit launch override:
{
"network": {
"mode": "tun",
"tun": {
"interface_name": "utun233",
"addresses": [
"172.19.0.1/30",
"fdfe:dcba:9876::1/126"
]
}
}
}Then run:
vpn-core install
vless-tun reconnect --refresh
vless-tun statusgo fmt ./...
go test ./...
go build -o vless-tun ./desktop/cmd/vless-tun
go build -o openconnect-tun ./desktop/cmd/openconnect-tun
go build -o dump ./desktop/cmd/dump
go build -o cisco-dump ./desktop/cmd/cisco-dump- This version manages the local
sing-boxsession lifecycle withstart,status,stop, and a configurable privileged TUN backend for macOS. startrefreshes the selected provider subscription before rendering by default, so configured profile aliases follow provider-side endpoint changes. When a provider exposes multiple remote profiles, configuretransportorselector; vless-tun no longer silently follows provider order.--refresh=falseis the explicit cached fallback for offline starts.reconnectis the "restart with latest config" path: it rereads local config, stops any recorded VLESS session across configured server cache directories, refreshes the subscription by default, rerenders, and starts the selected session.statusis an introspection view over recorded session state, launch backend, process liveness, interface presence, and cached profile data; it is not a deep traffic verifier.- If your public IP does not change, check the latest session log first. The expected control flow is
start->status-> inspect the session log, notstatusalone. system_proxyrender mode has been removed; legacy configs should usenetwork.mode=tunand drop any oldnetwork.system_proxyblock.- Design note: Why
vless-tunstays TUN-only - Generated config now includes
route.default_domain_resolver, whichsing-box 1.13.xexpects as part of the DNS resolver migration path. - Every
startgets its own timestamped log file so later debugging has a stable artifact even if the next session behaves differently. - The bypass rule is intentionally domain-suffix based because the original user requirement was
*.ru. If later you want IP or community rulesets, extend the renderer rather than hardcoding provider-specific blobs.
This project is part of the open-source ecosystem of Relux Works, an AI-native software development studio. We build fixed-price MVPs, rescue vibe-coded apps, run local AI inference, and train teams to work with coding agents. Much of the infrastructure behind that work is open source.
- Full catalog: relux.works/en/open-source
- Agentic enablement: agent harnesses & team training
- Hire us the agent-native way: point your assistant at
https://api.relux.works/mcp - Contact: ivan@relux.works