{
  "report_info": {
    "version": "9.0.1",
    "generated_at": "2026-07-21T01:54:43Z",
    "binary_name": "routing-switch"
  },
  "binary": {
    "name": "routing-switch",
    "path": "/opt/kodachi/dashboard/hooks/routing-switch",
    "timestamp": "2026-07-21T01:54:43Z",
    "file_info": {
      "size": 10082048,
      "sha256": "875b6e38ba047ab609e3213ab4c2e10490749f8368d1f5d797444d631ef2e4fc"
    },
    "flag_n": {
      "command": "info",
      "data": {
        "author": "Warith Al Maawali",
        "configuration": "Uses online-auth get-card for the Kodachi VPN credentials. VPNGate servers cost nothing, and browsing them (vpngate-fetch/list/export) needs no authentication, but connecting to one (vpngate-connect) still goes through the online-auth gate because it rewrites your routing. Every `providers` verb requires authentication.",
        "connection_limit": "One active connection at a time",
        "copyright": "© 2026 Kodachi OS",
        "description": "System-wide traffic routing through various proxy protocols",
        "features": [
          "System-wide traffic routing through proxy protocols",
          "Support for OpenVPN, WireGuard, and Dante SOCKS5",
          "Support for Tor, Shadowsocks, V2Ray, and Xray protocols",
          "Support for Mieru/Mita and Hysteria2 protocols",
          "Automatic protocol scoring and selection",
          "DNS leak prevention with multiple modes",
          "Metric-based and direct routing options",
          "External configuration file support",
          "IPv4/IPv6 dual-stack support",
          "QR code generation for mobile clients",
          "VPNGate free VPN integration (100+ public servers; browsing is auth-free, vpngate-connect requires authentication)",
          "External VPN Providers sub-tree (routing-switch providers): 20 verbs over a user-editable catalog of commercial and free providers (VPN Gate, Riseup, NordVPN, IVPN, PIA, Surfshark, AirVPN, Mullvad-WG), with credential storage, custom config import, latency benchmarking, and random/fastest pick",
          "Microsocks SOCKS5 server mode: share this machine's active tunnel with other devices on the LAN",
          "Comprehensive logging and monitoring"
        ],
        "license": "Proprietary",
        "name": "routing-switch",
        "security_features": [
          "Input validation: All inputs are validated and sanitized",
          "Rate limiting: Built-in rate limiting for network operations",
          "Authentication: Secure authentication with certificate pinning",
          "Encryption: TLS 1.3 for all network communications",
          "Kill switch controls: health-control handles leak response; Direct routing prefers the TUN path",
          "DNS protection: Multiple DNS modes to prevent leaks"
        ],
        "state_file": "./results/routing-switch-state.json",
        "supported_protocols": [
          "OpenVPN",
          "WireGuard",
          "Dante",
          "Tor",
          "Shadowsocks",
          "V2Ray",
          "Xray-VLESS",
          "Xray-VLESS-Reality",
          "Xray-Trojan",
          "Xray-VMess",
          "Mita/Mieru",
          "Hysteria2",
          "VPNGate (free public servers)"
        ],
        "system_requirements": {
          "dependencies": "OpenSSL, libcurl, tun2socks (for most protocols), redsocks (for Tor)",
          "os": "Linux (Debian-based)",
          "privileges": "root/sudo for system operations"
        },
        "version": "9.8.4 (build 320)",
        "website": "https://digi77.com"
      },
      "errors": [],
      "metadata": {
        "executionTime": 2,
        "hostname": "REDACTED-BUILD-HOST",
        "user": "REDACTED-BUILD-USER"
      },
      "status": "success",
      "timestamp": "2026-07-21T01:54:43.277795595Z",
      "version": "9.8.4 (build 320)",
      "warnings": []
    },
    "flag_v": {
      "command": "version",
      "data": {
        "buildDate": "REDACTED-BUILD-TIME",
        "gitCommit": "unknown",
        "name": "routing-switch",
        "rustVersion": "1.70.0",
        "version": "9.8.4 (build 320)"
      },
      "errors": [],
      "metadata": {
        "executionTime": 2,
        "hostname": "REDACTED-BUILD-HOST",
        "user": "REDACTED-BUILD-USER"
      },
      "status": "success",
      "timestamp": "2026-07-21T01:54:43.372883596Z",
      "version": "9.8.4 (build 320)",
      "warnings": []
    },
    "flag_h": {
      "command": "help",
      "data": {
        "commandCategories": [
          {
            "category": "CONNECTION COMMANDS",
            "commands": [
              {
                "description": "Connect to a proxy protocol (openvpn, wireguard use native routing; tor uses redsocks and must be layered on an active tunnel; dante, shadowsocks, v2ray, xray-*, mita, hysteria2 use tun2socks). Requires root and a prior online-auth login.",
                "examples": [
                  "sudo routing-switch connect wireguard",
                  "sudo routing-switch connect openvpn --config /path/to/vpn.ovpn --force",
                  "sudo routing-switch connect wireguard && sudo routing-switch connect tor",
                  "sudo routing-switch connect shadowsocks --no-tun"
                ],
                "name": "connect",
                "options": [
                  {
                    "description": "Use an external config file instead of the auth card (.ovpn / .conf for OpenVPN and WireGuard, card-native services.<proto> JSON for the rest)",
                    "flag": "--config <FILE>"
                  },
                  {
                    "description": "Force the IPv4 endpoint of a dual-stack protocol (conflicts with --ipv6)",
                    "flag": "--ipv4"
                  },
                  {
                    "description": "Force the IPv6 endpoint of a dual-stack protocol (conflicts with --ipv4)",
                    "flag": "--ipv6"
                  },
                  {
                    "description": "Disconnect the currently active protocol first, then connect this one. It does NOT stack two VPNs. Not needed for `connect tor`, which layers on the active tunnel by design",
                    "flag": "--force"
                  },
                  {
                    "default": "auto",
                    "description": "DNS routing: auto (default, DNS flows through the tunnel), hybrid (VPN server DNS bypasses the tunnel), strict (ALL DNS through the tunnel), system (ALL DNS bypasses the tunnel: LEAKS, debugging only)",
                    "flag": "--dns-mode <MODE>"
                  },
                  {
                    "description": "Keep private/LAN networks off the VPN so local devices stay reachable",
                    "flag": "--exclude-private"
                  },
                  {
                    "description": "Run the proxy client without a TUN device: system routing is untouched and you point apps at the local SOCKS5 port by hand. Supported by the tun2socks protocols only, `connect tor --no-tun` is rejected",
                    "flag": "--no-tun"
                  },
                  {
                    "description": "Metric-based routing: keeps the original route as a fallback. LESS SECURE, traffic can leak to the clear net if the TUN drops. Default is direct routing",
                    "flag": "--metric"
                  },
                  {
                    "description": "Skip the IPv4-forwarding / NAT / MASQUERADE checks before connecting",
                    "flag": "--skip-prerequisites"
                  },
                  {
                    "description": "DANGER: keep the connection alive even when an IPv6 leak is detected. The default is fail-closed (the connection is torn down). With this flag your real IPv6 address stays exposed",
                    "flag": "--allow-ipv6-leak"
                  }
                ],
                "requires_sudo": true,
                "usage": "sudo routing-switch connect <PROTOCOL> [OPTIONS]"
              },
              {
                "description": "Disconnect the current proxy connection and restore normal routing",
                "examples": [
                  "sudo routing-switch disconnect",
                  "sudo routing-switch disconnect --force --clean-firewall"
                ],
                "name": "disconnect",
                "options": [
                  {
                    "description": "Kill the protocol processes outright and skip the graceful cleanup checks",
                    "flag": "--force"
                  },
                  {
                    "description": "Also flush the iptables rules (Docker/KVM chains are preserved). Use when the network stays stuck after a normal disconnect. REQUIRES AUTHENTICATION: it tears down the kill-switch rules, so unlike a plain disconnect it is not auth-exempt",
                    "flag": "--clean-firewall"
                  }
                ],
                "requires_sudo": true,
                "usage": "sudo routing-switch disconnect [OPTIONS]"
              },
              {
                "description": "Show the current connection status (auth-exempt, but needs sudo to read privileged system state)",
                "examples": [
                  "sudo routing-switch status",
                  "sudo routing-switch status --detailed --json"
                ],
                "name": "status",
                "options": [
                  {
                    "description": "Add the exact Connected Since timestamp to the normal status block",
                    "flag": "--detailed"
                  }
                ],
                "requires_sudo": true,
                "usage": "sudo routing-switch status [OPTIONS]"
              }
            ]
          },
          {
            "category": "INFORMATION COMMANDS",
            "commands": [
              {
                "description": "Display current DNS server information (auth-exempt)",
                "examples": [
                  "routing-switch dns-info",
                  "routing-switch dns-info --json"
                ],
                "name": "dns-info",
                "options": [],
                "requires_sudo": false,
                "usage": "routing-switch dns-info [OPTIONS]"
              },
              {
                "description": "Display the Tor DNS and SOCKS endpoints. Kodachi's Tor is VPN-side: its SOCKS5 lives on the worker (10.0.0.1:9050 over WireGuard, 172.16.0.1:9050 over OpenVPN), never on 127.0.0.1",
                "examples": [
                  "routing-switch tor-dns-info",
                  "routing-switch tor-dns-info --json"
                ],
                "name": "tor-dns-info",
                "options": [],
                "requires_sudo": false,
                "usage": "routing-switch tor-dns-info [OPTIONS]"
              },
              {
                "description": "Display VPS server information (hostname, IP, stats)",
                "examples": [
                  "routing-switch vps-info",
                  "routing-switch vps-info --detailed"
                ],
                "name": "vps-info",
                "options": [
                  {
                    "description": "Include CPU load, memory usage, uptime and connection counts",
                    "flag": "--detailed"
                  }
                ],
                "requires_sudo": false,
                "usage": "routing-switch vps-info [OPTIONS]"
              },
              {
                "description": "List available protocols with their security/speed scores (auth-exempt)",
                "examples": [
                  "routing-switch list-protocols",
                  "routing-switch list-protocols --detailed --sort-by-security"
                ],
                "name": "list-protocols",
                "options": [
                  {
                    "description": "Show the full scoring breakdown per protocol, not just the overall score",
                    "flag": "--detailed"
                  },
                  {
                    "description": "Probe each protocol's endpoint for real instead of scoring from the auth card alone (slower, more accurate, needs network)",
                    "flag": "--test"
                  },
                  {
                    "description": "Hide protocols the auth card does not provide",
                    "flag": "--available-only"
                  },
                  {
                    "description": "Sort by security score (conflicts with --sort-by-speed)",
                    "flag": "--sort-by-security"
                  },
                  {
                    "description": "Sort by speed score (conflicts with --sort-by-security)",
                    "flag": "--sort-by-speed"
                  }
                ],
                "requires_sudo": false,
                "usage": "routing-switch list-protocols [OPTIONS]"
              }
            ]
          },
          {
            "category": "TESTING COMMANDS",
            "commands": [
              {
                "description": "Test one protocol's endpoint (or `all`) without touching routing. Reads the auth card, so it needs authentication",
                "examples": [
                  "routing-switch test-protocol wireguard",
                  "routing-switch test-protocol all --json"
                ],
                "name": "test-protocol",
                "options": [
                  {
                    "description": "Run the longer test set (latency and throughput checks) instead of a plain reachability probe",
                    "flag": "--extended"
                  }
                ],
                "requires_sudo": false,
                "usage": "routing-switch test-protocol <PROTOCOL|all> [OPTIONS]"
              },
              {
                "description": "Measure TCP connection latency for all available protocols. Requires authentication because it reads auth-card endpoints",
                "examples": [
                  "routing-switch benchmark",
                  "routing-switch benchmark --iterations 5 --json"
                ],
                "name": "benchmark",
                "options": [
                  {
                    "description": "Number of test iterations per protocol (default: 3). More iterations means slower but steadier numbers",
                    "flag": "--iterations <N>"
                  }
                ],
                "requires_sudo": false,
                "usage": "routing-switch benchmark [OPTIONS]"
              }
            ]
          },
          {
            "category": "CONFIGURATION COMMANDS",
            "commands": [
              {
                "description": "Export protocol configurations to files. Requires authentication",
                "examples": [
                  "routing-switch export-config wireguard",
                  "routing-switch export-config all --path /tmp/vpn-configs/"
                ],
                "name": "export-config",
                "options": [
                  {
                    "description": "Directory to write the configs to (default: ./results/configs/). Created if it does not exist",
                    "flag": "-p, --path <PATH>"
                  },
                  {
                    "description": "WARNING: writes the real passwords and keys into the exported files",
                    "flag": "--include-credentials"
                  }
                ],
                "requires_sudo": false,
                "usage": "routing-switch export-config [PROTOCOL|all] [OPTIONS]"
              },
              {
                "description": "Show a protocol's configuration. Requires authentication",
                "examples": [
                  "routing-switch showconfig wireguard",
                  "routing-switch showconfig dante --mask-sensitive"
                ],
                "name": "showconfig",
                "options": [
                  {
                    "description": "Redact passwords, keys and credential-bearing URLs so the output is safe to share or log",
                    "flag": "--mask-sensitive"
                  }
                ],
                "requires_sudo": false,
                "usage": "routing-switch showconfig [PROTOCOL|all] [OPTIONS]"
              },
              {
                "description": "Show a protocol's configuration as a connection URL (ss://, socks5://, ...). Requires authentication",
                "examples": [
                  "routing-switch showconfigurl shadowsocks",
                  "routing-switch showconfigurl all --json"
                ],
                "name": "showconfigurl",
                "options": [],
                "requires_sudo": false,
                "usage": "routing-switch showconfigurl [PROTOCOL|all] [OPTIONS]"
              },
              {
                "description": "Show a protocol's configuration as a QR code for mobile clients. Requires authentication",
                "examples": [
                  "routing-switch showconfigqr wireguard",
                  "routing-switch showconfigqr shadowsocks --ipv4 --ipv6 --save-files"
                ],
                "name": "showconfigqr",
                "options": [
                  {
                    "description": "Generate the IPv4 QR code (this is the default for dual-stack protocols)",
                    "flag": "--ipv4"
                  },
                  {
                    "description": "Generate the IPv6 QR code. Can be combined with --ipv4 to emit both",
                    "flag": "--ipv6"
                  },
                  {
                    "description": "Do not re-read the generated QR to verify it (faster, for debugging)",
                    "flag": "--skip-validation"
                  },
                  {
                    "description": "Fail the whole operation if any generated QR code fails validation",
                    "flag": "--strict-validation"
                  },
                  {
                    "description": "Also write the QR codes as PNG files into ./results/qr-codes/",
                    "flag": "--save-files"
                  }
                ],
                "requires_sudo": false,
                "usage": "routing-switch showconfigqr [PROTOCOL|all] [OPTIONS]"
              },
              {
                "description": "Validate QR code configurations against the auth card (by file, by stdin, or by protocol). Requires authentication",
                "examples": [
                  "routing-switch validate-qr shadowsocks_ipv4.png",
                  "echo 'ss://...' | routing-switch validate-qr --stdin"
                ],
                "name": "validate-qr",
                "options": [
                  {
                    "description": "Path to a QR image to decode. Equivalent to passing the filename positionally. If the file is not in the current directory, ./results/qr-codes/ is searched",
                    "flag": "-f, --file <FILE>"
                  },
                  {
                    "description": "Read the QR payload (the URL) from stdin instead of an image",
                    "flag": "--stdin"
                  },
                  {
                    "description": "Round-trip check: generate a QR for the protocol and confirm it decodes back to the same config",
                    "flag": "--test-readability"
                  }
                ],
                "requires_sudo": false,
                "usage": "routing-switch validate-qr [PROTOCOL|<FILE>.png|all] [OPTIONS]"
              }
            ]
          },
          {
            "category": "MANAGEMENT COMMANDS",
            "commands": [
              {
                "description": "Score the available protocols, pick the best one, and connect to it. Requires authentication",
                "examples": [
                  "sudo routing-switch auto-select",
                  "sudo routing-switch auto-select --min-security 90"
                ],
                "name": "auto-select",
                "options": [
                  {
                    "default": "80",
                    "description": "Reject protocols scoring below this security threshold, 0 to 100 (default: 80)",
                    "flag": "--min-security <SCORE>"
                  },
                  {
                    "description": "Weight speed above the other factors when ranking",
                    "flag": "--prefer-speed"
                  }
                ],
                "requires_sudo": true,
                "usage": "sudo routing-switch auto-select [OPTIONS]"
              },
              {
                "description": "Reset all routing to the system default state. DESTRUCTIVE: drops the active tunnel, so you are back on the clear net afterwards. Auth-exempt on purpose (it must work when the network is down)",
                "examples": [
                  "sudo routing-switch reset",
                  "sudo routing-switch reset --force"
                ],
                "name": "reset",
                "options": [
                  {
                    "description": "Reset even when the stored state says a connection is still active or the teardown reports errors",
                    "flag": "--force"
                  }
                ],
                "requires_sudo": true,
                "usage": "sudo routing-switch reset [OPTIONS]"
              },
              {
                "description": "Clean up orphaned protocol processes and leftover resources. Auth-exempt",
                "examples": [
                  "sudo routing-switch cleanup",
                  "sudo routing-switch cleanup --thorough"
                ],
                "name": "cleanup",
                "options": [
                  {
                    "description": "Deeper sweep: also removes stale TUN devices, routes and iptables rules, not just stray processes. Can disturb an active connection, so run it after disconnecting",
                    "flag": "--thorough"
                  }
                ],
                "requires_sudo": true,
                "usage": "sudo routing-switch cleanup [OPTIONS]"
              },
              {
                "description": "Diagnose and repair a half-broken routing state (cleanup, route restore, DNS reset). The 'my internet is stuck' command. Auth-exempt",
                "examples": [
                  "sudo routing-switch recover",
                  "sudo routing-switch recover --force"
                ],
                "name": "recover",
                "options": [
                  {
                    "description": "Run the recovery even when routing-switch believes the state is healthy. DESTRUCTIVE: drops any active tunnel and restores the clear-net default route",
                    "flag": "--force"
                  }
                ],
                "requires_sudo": true,
                "usage": "sudo routing-switch recover [OPTIONS]"
              },
              {
                "description": "Check and fix the network prerequisites for VPN connections (IPv4 forwarding, NAT MASQUERADE). Fixes only what is wrong. Auth-exempt",
                "examples": [
                  "sudo routing-switch check-prerequisites",
                  "sudo routing-switch check-prerequisites --ipv6 --json"
                ],
                "name": "check-prerequisites",
                "options": [
                  {
                    "description": "Also check (and enable, if the system supports it) the IPv6 forwarding prerequisites",
                    "flag": "--ipv6"
                  }
                ],
                "requires_sudo": true,
                "usage": "sudo routing-switch check-prerequisites [OPTIONS]"
              }
            ]
          },
          {
            "category": "MICROSOCKS SERVER MANAGEMENT",
            "commands": [
              {
                "description": "Run a SOCKS5 server on this machine so other devices on the LAN can ride its active tunnel. Connect a protocol FIRST, then enable this. Requires authentication",
                "examples": [
                  "sudo routing-switch microsocks-enable -u microkodachi-8273 -p 'S@Cur9P@s@Wo-Ds'",
                  "sudo routing-switch microsocks-enable -u microkodachi-8273 -p 'S@Cur9P@s@Wo-Ds' --port 30051"
                ],
                "name": "microsocks-enable",
                "options": [
                  {
                    "description": "SOCKS5 username the client devices must present (required)",
                    "flag": "-u, --username <USERNAME>",
                    "required": true
                  },
                  {
                    "description": "SOCKS5 password (required). The listener binds 0.0.0.0, so use a strong one",
                    "flag": "-p, --password <PASSWORD>",
                    "required": true
                  },
                  {
                    "description": "Listening port. Omit to auto-pick the first free port in the 30050-30054 range",
                    "flag": "--port <PORT>"
                  }
                ],
                "requires_sudo": true,
                "usage": "sudo routing-switch microsocks-enable -u <USERNAME> -p <PASSWORD> [--port PORT]"
              },
              {
                "description": "Stop the microsocks SOCKS5 server. Requires authentication",
                "examples": [
                  "sudo routing-switch microsocks-disable",
                  "sudo routing-switch microsocks-disable --json"
                ],
                "name": "microsocks-disable",
                "options": [],
                "requires_sudo": true,
                "usage": "sudo routing-switch microsocks-disable"
              },
              {
                "description": "Show the microsocks server status (running, port, PID, uptime). Auth-exempt",
                "examples": [
                  "sudo routing-switch microsocks-status",
                  "sudo routing-switch microsocks-status --json"
                ],
                "name": "microsocks-status",
                "options": [],
                "requires_sudo": false,
                "usage": "sudo routing-switch microsocks-status [OPTIONS]"
              }
            ]
          },
          {
            "category": "VPNGATE FREE VPN",
            "commands": [
              {
                "description": "Fetch the VPNGate public server list into the local cache (auth-exempt, refreshes hourly)",
                "examples": [
                  "routing-switch vpngate-fetch",
                  "routing-switch vpngate-fetch --json"
                ],
                "name": "vpngate-fetch",
                "options": [],
                "requires_sudo": false,
                "usage": "routing-switch vpngate-fetch [OPTIONS]"
              },
              {
                "description": "List the cached VPNGate servers with filtering and sorting (auth-exempt). The index it prints is what vpngate-connect and vpngate-export take",
                "examples": [
                  "routing-switch vpngate-list",
                  "routing-switch vpngate-list --country JP --sort speed -l 10"
                ],
                "name": "vpngate-list",
                "options": [
                  {
                    "description": "Filter by country name or code (for example JP, Japan, US)",
                    "flag": "-c, --country <COUNTRY>"
                  },
                  {
                    "default": "score",
                    "description": "score (default, highest first), speed (highest first), ping (lowest first), sessions (lowest first, i.e. least crowded)",
                    "flag": "-s, --sort <CRITERIA>"
                  },
                  {
                    "description": "Show only the first N results after filtering and sorting",
                    "flag": "-l, --limit <N>"
                  }
                ],
                "requires_sudo": false,
                "usage": "routing-switch vpngate-list [OPTIONS]"
              },
              {
                "description": "Connect to a VPNGate server by index (OpenVPN, public vpn/vpn credentials). REQUIRES AUTHENTICATION: it rewrites routes, DNS and firewall like any other connect. Only VPNGate BROWSING (fetch/list/export) is auth-free",
                "examples": [
                  "sudo routing-switch vpngate-connect 5",
                  "sudo routing-switch vpngate-connect 12 --force"
                ],
                "name": "vpngate-connect",
                "options": [
                  {
                    "description": "Disconnect the currently active connection first",
                    "flag": "--force"
                  },
                  {
                    "description": "Skip the IPv4-forwarding / NAT / MASQUERADE checks before connecting",
                    "flag": "--skip-prerequisites"
                  }
                ],
                "requires_sudo": true,
                "usage": "sudo routing-switch vpngate-connect <INDEX> [OPTIONS]"
              },
              {
                "description": "Export one cached VPNGate server's OpenVPN config as a standalone .ovpn file (auth-exempt)",
                "examples": [
                  "routing-switch vpngate-export 1",
                  "routing-switch vpngate-export 5 --json"
                ],
                "name": "vpngate-export",
                "options": [],
                "requires_sudo": false,
                "usage": "routing-switch vpngate-export <INDEX> [OPTIONS]"
              },
              {
                "description": "Export every cached VPNGate server as a .ovpn file into results/vpngate-configs (auth-exempt)",
                "examples": [
                  "routing-switch vpngate-export-all",
                  "routing-switch vpngate-export-all --json"
                ],
                "name": "vpngate-export-all",
                "options": [],
                "requires_sudo": false,
                "usage": "routing-switch vpngate-export-all [OPTIONS]"
              },
              {
                "description": "External VPN Providers sub-tree (20 verbs) over a user-editable catalog of commercial and free providers (VPN Gate, Riseup, NordVPN, IVPN, PIA, Surfshark, AirVPN, Mullvad-WG). EVERY providers verb requires authentication, and every cache-writing verb requires root. Run `routing-switch providers --help` for the per-verb flags",
                "examples": [
                  "routing-switch providers list",
                  "sudo routing-switch providers fetch vpngate --force",
                  "routing-switch providers list-profiles vpngate --country jp --limit 5",
                  "sudo routing-switch providers connect-fastest vpngate --country jp --force"
                ],
                "name": "providers",
                "options": [
                  {
                    "description": "Show the catalog with cache freshness; re-read the catalog JSON after editing dashboard/hooks/config/vpn-providers-public-api.json",
                    "flag": "list | refresh-catalog"
                  },
                  {
                    "description": "Fetch a provider's profile list into the cache. --force bypasses the cache TTL. Needs root",
                    "flag": "fetch <ID> [--force]"
                  },
                  {
                    "description": "Browse the cached profiles, filtered by 2-letter country code and/or transport protocol substring (udp, tcp, openvpn, wireguard)",
                    "flag": "list-profiles <ID> [-c|--country CC] [-p|--protocol PROTO] [-l|--limit N]"
                  },
                  {
                    "description": "Print one profile's raw config body. Lazily fetches on a cache miss, so it needs root",
                    "flag": "get-profile <ID> <PROFILE_ID>"
                  },
                  {
                    "description": "Connect through a provider profile, auto-injecting any stored credentials. Needs root",
                    "flag": "connect <ID> <PROFILE_ID> [--force] [--skip-prerequisites]"
                  },
                  {
                    "description": "Pick a random profile from the provider's cache (optionally country-filtered) and connect. Needs root",
                    "flag": "connect-random <ID> [--country CC] [--force]"
                  },
                  {
                    "description": "Pick the fastest profile (benchmarked latency first, then provider-reported speed, then load) and connect. Needs root",
                    "flag": "connect-fastest <ID> [--country CC] [--force]"
                  },
                  {
                    "description": "Latency-probe the cached profiles and store the result so connect-fastest can use it. Manual only, never auto-runs. Needs root",
                    "flag": "benchmark <ID> [--country CC] [--top N] [--concurrency N] [--timeout S]"
                  },
                  {
                    "description": "Manage per-provider VPN account credentials (never the dashboard login). Values are sealed with AES-256-GCM. Set and delete need root",
                    "flag": "credentials-list | credentials-set <ID> --field k=v | credentials-delete <ID>"
                  },
                  {
                    "description": "Import your own OpenVPN / WireGuard / Shadowsocks / V2Ray / Hysteria2 config as a custom profile (import needs root); validate-config previews the detection without writing",
                    "flag": "import-config --name N (--file F | --text T) [--protocol-hint P] | validate-config"
                  },
                  {
                    "description": "Remove imported custom profiles and their stored credentials. Irreversible, no confirmation prompt. Needs root",
                    "flag": "delete-custom-profile <PROFILE_ID> | delete-custom-profiles-bulk (--id ID... | --all)"
                  },
                  {
                    "description": "Fill in a cached profile's missing country (via ip-fetch geo) or its missing IPv4 (via DNS), writing back to the cache. Needs root",
                    "flag": "resolve-country <ID> <PROFILE_ID> [--force] | resolve-ip <ID> <PROFILE_ID> | resolve-ips <ID> [--concurrency N]"
                  },
                  {
                    "description": "Drop a provider's cached index and configs, or every provider's with --all. Credentials are NEVER touched. Needs root",
                    "flag": "clear-cache [<ID>] [--all]"
                  }
                ],
                "requires_sudo": false,
                "usage": "routing-switch providers <SUBCOMMAND> [OPTIONS]"
              }
            ]
          }
        ],
        "description": "System-wide traffic routing through various proxy protocols",
        "environmentVariables": [
          {
            "default": "info",
            "description": "Set logging level (default: info)",
            "name": "RUST_LOG",
            "values": "error, warn, info, debug, trace"
          }
        ],
        "exitCodes": {
          "0": "Success",
          "1": "General error",
          "2": "Invalid arguments",
          "3": "Permission denied"
        },
        "globalOptions": [
          {
            "description": "Print help information",
            "flag": "-h, --help"
          },
          {
            "description": "Print version information",
            "flag": "-v, --version"
          },
          {
            "description": "Display detailed information",
            "flag": "-n, --info"
          },
          {
            "description": "Show usage examples",
            "flag": "-e, --examples"
          },
          {
            "description": "Output in JSON format",
            "flag": "--json"
          },
          {
            "description": "Pretty-print JSON output with indentation",
            "flag": "--json-pretty"
          },
          {
            "description": "Enhanced JSON output with improved formatting (like jq)",
            "flag": "--json-human"
          },
          {
            "description": "Enable verbose output",
            "flag": "--verbose"
          },
          {
            "description": "Force output format: text (default) or json",
            "flag": "-o, --output-format <FORMAT>"
          }
        ],
        "name": "routing-switch",
        "usage": "routing-switch [OPTIONS] <COMMAND>"
      },
      "errors": [],
      "metadata": {
        "executionTime": 0,
        "hostname": "REDACTED-BUILD-HOST",
        "user": "REDACTED-BUILD-USER"
      },
      "status": "success",
      "timestamp": "2026-07-21T01:54:43.467750957Z",
      "version": "9.8.4 (build 320)",
      "warnings": []
    },
    "flag_e": {
      "command": "examples",
      "data": {
        "categories": [
          {
            "description": "Connect to a proxy protocol. Every connect verb needs root (it rewrites routes, DNS and iptables) and needs a prior online-auth login: without one you get 'Authentication required'. Only one connection is active at a time; add --force to replace an existing one. The one exception is `connect tor`, which LAYERS on an active tunnel and therefore does not take --force.",
            "examples": [
              {
                "command": "sudo routing-switch connect openvpn",
                "description": "Connect via OpenVPN",
                "expectedOutput": "Successfully connected to OpenVPN server",
                "notes": "Requires OpenVPN configuration in auth card"
              },
              {
                "command": "sudo routing-switch connect wireguard",
                "description": "Connect via WireGuard",
                "expectedOutput": "Successfully connected to WireGuard server",
                "notes": "Ultra-fast, modern VPN protocol"
              },
              {
                "command": "sudo routing-switch connect dante",
                "description": "Connect via Dante SOCKS5 proxy",
                "expectedOutput": "Successfully connected to Dante server",
                "notes": "Dante rides the shared tun_routing device, so it clashes with any other tun2socks protocol (shadowsocks, v2ray, xray-*, mita, hysteria2). If one of those is already up, disconnect first, or pass --force (which drops the existing connection before dialing)."
              },
              {
                "command": "sudo routing-switch connect wireguard && sudo routing-switch connect tor",
                "description": "Route traffic through Tor - VPN-only, layered on an active Kodachi tunnel",
                "expectedOutput": "Layering Tor on the active wireguard tunnel ... Successfully routing traffic through Tor",
                "notes": "Kodachi's Tor SOCKS proxy is bound to the VPN network only - never exposed on a public IP, because a public Tor SOCKS port gets abused as an open proxy. So `connect tor` is NOT a standalone command: connect ANY Kodachi protocol FIRST (WireGuard, OpenVPN, or a proxy protocol - Shadowsocks/V2Ray/Xray/Hysteria2/Mita), then `connect tor` layers Tor on that tunnel (the underlay is kept, not disconnected). Running `connect tor` with no tunnel up is refused with on-screen guidance. Easier alternative for WireGuard/OpenVPN: set your browser's SOCKS5 proxy to 172.16.0.1:9050 (over OpenVPN) or 10.0.0.1:9050 (over WireGuard)."
              },
              {
                "command": "sudo routing-switch connect shadowsocks",
                "description": "Connect via Shadowsocks",
                "expectedOutput": "Successfully connected to Shadowsocks server",
                "notes": "Optimized for bypassing censorship"
              },
              {
                "command": "sudo routing-switch connect v2ray",
                "description": "Connect via V2Ray VMess",
                "expectedOutput": "Successfully connected to V2Ray server",
                "notes": "tun2socks protocol: local SOCKS5 is allocated from 30005-30009. Conflicts with any other tun2socks protocol on tun_routing."
              },
              {
                "command": "sudo routing-switch connect xray-vless",
                "description": "Connect via Xray VLESS TLS",
                "expectedOutput": "Successfully connected to Xray VLESS server",
                "notes": "Lightweight, fast protocol with TLS"
              },
              {
                "command": "sudo routing-switch connect xray-vless-reality",
                "description": "Connect via Xray VLESS Reality",
                "expectedOutput": "Successfully connected to Xray VLESS Reality server",
                "notes": "Most advanced censorship resistance"
              },
              {
                "command": "sudo routing-switch connect xray-trojan",
                "description": "Connect via Xray Trojan",
                "expectedOutput": "Successfully connected to Xray Trojan server",
                "notes": "Mimics HTTPS traffic"
              },
              {
                "command": "sudo routing-switch connect xray-vmess",
                "description": "Connect via Xray VMess",
                "expectedOutput": "Successfully connected to Xray VMess server",
                "notes": "VMess over the Xray core: local SOCKS5 is allocated from 30010-30014. Conflicts with any other tun2socks protocol on tun_routing."
              },
              {
                "command": "sudo routing-switch connect mita",
                "description": "Connect via Mieru/Mita protocol",
                "expectedOutput": "Successfully connected to Mita server",
                "notes": "Anti-censorship protocol"
              },
              {
                "command": "sudo routing-switch connect hysteria2",
                "description": "Connect via Hysteria2 protocol",
                "expectedOutput": "Successfully connected to Hysteria2 server",
                "notes": "UDP-based, high-performance anti-censorship protocol"
              }
            ],
            "id": "basic_connections",
            "title": "BASIC CONNECTIONS"
          },
          {
            "description": "Connect a custom profile with --config instead of the auth card. OpenVPN/WireGuard take standard text bodies; v2ray/xray/shadowsocks/hysteria2/mita take the card-native services.<proto> JSON object (a custom profile is just a card service slice).",
            "examples": [
              {
                "command": "sudo routing-switch connect openvpn --config /home/user/vpn.ovpn",
                "description": "Connect using external OpenVPN config file",
                "expectedOutput": "Successfully connected to OpenVPN server",
                "notes": "Supports standard .ovpn configuration files"
              },
              {
                "command": "sudo routing-switch connect wireguard --config /path/to/wg0.conf",
                "description": "Connect using external WireGuard config",
                "expectedOutput": "Successfully connected to WireGuard server",
                "notes": "Uses standard WireGuard .conf format"
              },
              {
                "command": "sudo routing-switch connect shadowsocks --config /path/to/shadowsocks-profile.json",
                "description": "Connect a custom Shadowsocks profile",
                "expectedOutput": "Successfully connected to Shadowsocks server",
                "notes": "File is the card-native services.shadowsocks JSON object (method, password, port, ipv4{host,url})"
              },
              {
                "command": "sudo routing-switch connect v2ray --config /path/to/v2ray-profile.json",
                "description": "Connect a custom V2Ray (VMess) profile",
                "expectedOutput": "Successfully connected to V2Ray server",
                "notes": "File is the card-native services.v2ray JSON object (uuid, port, network, path, security, alterId, ipv4{host,url})"
              },
              {
                "command": "sudo routing-switch connect xray-vless-reality --config /path/to/xray-profile.json",
                "description": "Connect a custom Xray profile (xray-vless / xray-vless-reality / xray-trojan / xray-vmess)",
                "expectedOutput": "Successfully connected to Xray VLESS Reality server",
                "notes": "File is the card-native services.xray JSON object (domain, protocols{vless|trojan|vmess}, reality_public_key)"
              },
              {
                "command": "sudo routing-switch connect hysteria2 --config /path/to/hysteria2-profile.json",
                "description": "Connect a custom Hysteria2 profile",
                "expectedOutput": "Successfully connected to Hysteria2 server",
                "notes": "File is the card-native services.hysteria2 JSON object (password, port, protocol, obfs_type, obfs_password, ipv4{host,url})"
              },
              {
                "command": "sudo routing-switch connect mita --config /path/to/mita-profile.json",
                "description": "Connect a custom Mita/Mieru profile",
                "expectedOutput": "Successfully connected to Mita server",
                "notes": "File is the card-native services.mita JSON object (username, password, port, protocol, mtu, multiplexing, ipv4{host,url})"
              },
              {
                "command": "sudo routing-switch connect openvpn --config ./vpn.ovpn --force",
                "description": "Force connection with external config",
                "expectedOutput": "Disconnecting existing... Connected via external config",
                "notes": "Combines --config with other flags like --force"
              },
              {
                "command": "sudo routing-switch connect shadowsocks --config ~/configs/ss.json --no-tun",
                "description": "External config in no-TUN mode",
                "expectedOutput": "Shadowsocks SOCKS5 proxy ready at localhost:30000",
                "notes": "Use external config for manual proxy setup"
              }
            ],
            "id": "external_config",
            "title": "EXTERNAL CONFIGURATION FILES"
          },
          {
            "description": "Explicitly choose the IPv4 or the IPv6 endpoint of a protocol. --ipv4 and --ipv6 are mutually exclusive (clap rejects both together). If an IPv6 leak is detected while connecting, routing-switch fails closed and tears the connection down; --allow-ipv6-leak is the only way to override that, and it is documented in DNS MODES AND SECURITY below.",
            "examples": [
              {
                "command": "sudo routing-switch connect dante --ipv4",
                "description": "Force IPv4 connection to Dante",
                "expectedOutput": "Connected via IPv4"
              },
              {
                "command": "sudo routing-switch connect dante --ipv6",
                "description": "Force IPv6 connection to Dante",
                "expectedOutput": "Connected via IPv6",
                "notes": "Requires IPv6 support"
              },
              {
                "command": "sudo routing-switch connect shadowsocks --ipv4",
                "description": "Connect Shadowsocks over IPv4",
                "expectedOutput": "Shadowsocks connected via IPv4"
              },
              {
                "command": "sudo routing-switch connect wireguard && sudo routing-switch connect tor --ipv6",
                "description": "Layer Tor on an active WireGuard tunnel and use the worker's IPv6 Tor SOCKS endpoint",
                "expectedOutput": "Layering Tor on the active wireguard tunnel ... Tor SOCKS reached over the IPv6 endpoint",
                "notes": "UNDERLAY REQUIRED: `connect tor` on its own is refused. Kodachi's Tor SOCKS proxy is bound to the VPN network only (never exposed on a public IP), so a Kodachi tunnel (wg0, tun0 or tun_routing) must already be up. --ipv6 picks the card's IPv6 Tor endpoint, so both the underlay and the worker need working IPv6."
              },
              {
                "command": "sudo routing-switch connect xray-vless --ipv6",
                "description": "Use IPv6 for Xray VLESS",
                "expectedOutput": "Xray VLESS connected via IPv6"
              },
              {
                "command": "sudo routing-switch connect hysteria2 --ipv4",
                "description": "Connect Hysteria2 over IPv4",
                "expectedOutput": "Hysteria2 connected via IPv4"
              },
              {
                "command": "sudo routing-switch connect hysteria2 --ipv6",
                "description": "Connect Hysteria2 over IPv6",
                "expectedOutput": "Hysteria2 connected via IPv6",
                "notes": "Requires IPv6 support"
              }
            ],
            "id": "ip_version",
            "title": "IPV4/IPV6 SELECTION"
          },
          {
            "description": "Check and manage network prerequisites for VPN connections",
            "examples": [
              {
                "command": "sudo routing-switch check-prerequisites",
                "description": "Check and fix network prerequisites",
                "expectedOutput": "✓ IPv4 forwarding: Already enabled (correct)\n✗→✓ NAT MASQUERADE: Fixed",
                "notes": "Fixes only what's wrong, preserves correct settings"
              },
              {
                "command": "sudo routing-switch check-prerequisites --ipv6",
                "description": "Check prerequisites for IPv6 connections",
                "expectedOutput": "✓ IPv6 forwarding: Enabled",
                "notes": "Only enables IPv6 if system supports it"
              },
              {
                "command": "sudo routing-switch check-prerequisites --json",
                "description": "Get prerequisites status in JSON format",
                "expectedOutput": "{\"command\":\"check-prerequisites\",\"data\":{\"prerequisites\":{\"checks\":[...],\"ready\":true}},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              },
              {
                "command": "sudo routing-switch connect shadowsocks",
                "description": "Connect with automatic prerequisites check",
                "expectedOutput": "Checking network prerequisites...\n✓ All checks passed\n\nConnecting to shadowsocks...",
                "notes": "Prerequisites are checked by default"
              },
              {
                "command": "sudo routing-switch connect shadowsocks --skip-prerequisites",
                "description": "Connect without prerequisites check",
                "expectedOutput": "Connecting to shadowsocks...",
                "notes": "Skips prerequisites for backward compatibility"
              },
              {
                "command": "sudo routing-switch connect wireguard --force",
                "description": "Force reconnection with prerequisites check",
                "expectedOutput": "Checking network prerequisites...\nForcing new connection...",
                "notes": "Prerequisites persist after disconnect for other VPN tools"
              }
            ],
            "id": "prerequisites",
            "title": "NETWORK PREREQUISITES"
          },
          {
            "description": "Check and monitor connection status",
            "examples": [
              {
                "command": "sudo routing-switch status",
                "description": "Show current connection status",
                "expectedOutput": "Connection Status: CONNECTED\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nSource:          Kodachi VPS card\nProtocol:        wireguard\nServer:          ...\nUptime:          0h 42m 11s\nTUN Device:      wg0",
                "notes": "Requires sudo for accurate results (reads privileged system state). No online-auth login needed: status is auth-exempt."
              },
              {
                "command": "sudo routing-switch status --detailed",
                "description": "Status plus the exact timestamp the connection was established",
                "expectedOutput": "Connection Status: CONNECTED\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nSource:          Kodachi VPS card\nProtocol:        wireguard\nServer:          ...\nUptime:          0h 42m 11s\nTUN Device:      wg0\nConnected Since: 2026-07-13T09:12:41Z",
                "notes": "--detailed adds the Connected Since line on top of the normal status block. Everything else is the same."
              },
              {
                "command": "sudo routing-switch status --json",
                "description": "Get status in JSON format",
                "expectedOutput": "{\"command\":\"status\",\"data\":{\"connected\":true,\"protocol\":\"wireguard\",\"connected_since\":\"...\",\"server\":\"...\",\"tun_device\":\"wg0\",\"uptime\":\"...\"},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              },
              {
                "command": "sudo routing-switch status --json-pretty",
                "description": "Pretty-printed JSON status",
                "expectedOutput": "Formatted JSON with connection details"
              },
              {
                "command": "sudo routing-switch status --json-human",
                "description": "Human-readable JSON status",
                "expectedOutput": "Enhanced JSON output with descriptions"
              }
            ],
            "id": "status",
            "title": "CONNECTION STATUS"
          },
          {
            "description": "Control DNS routing for security and compatibility (DEFAULT: auto mode)",
            "examples": [
              {
                "command": "sudo routing-switch connect wireguard",
                "description": "DEFAULT: Auto mode (no special DNS routing)",
                "expectedOutput": "DNS handled naturally through tunnel",
                "notes": "DEFAULT safe mode - DNS flows through tunnel like all traffic. Note: May not work with all protocols"
              },
              {
                "command": "sudo routing-switch connect wireguard --dns-mode auto",
                "description": "Auto mode: DNS flows naturally through tunnel (explicit)",
                "expectedOutput": "DNS handled naturally through tunnel",
                "notes": "Same as omitting --dns-mode; explicit form for scripting"
              },
              {
                "command": "sudo routing-switch connect wireguard --dns-mode hybrid",
                "description": "Hybrid mode: VPN server DNS bypasses tunnel",
                "expectedOutput": "Using hybrid DNS mode: VPN server DNS bypasses, others through tunnel",
                "notes": "⚠️ WARNING: May cause internet loss! Use 'sudo routing-switch recover' to restore connectivity. Good balance of security and reliability for home/office use when you need LAN access"
              },
              {
                "command": "sudo routing-switch connect wireguard --dns-mode strict",
                "description": "Strict mode: ALL DNS through tunnel",
                "expectedOutput": "Using strict DNS mode: Maximum security",
                "notes": "Most secure, may fail if VPN server uses hostname"
              },
              {
                "command": "sudo routing-switch connect wireguard --dns-mode system",
                "description": "System mode: DNS bypasses tunnel",
                "expectedOutput": "WARNING: System DNS mode - DNS queries will leak!",
                "notes": "⚠️ NOT SECURE - DNS leaks! Use only for debugging"
              },
              {
                "command": "sudo routing-switch connect wireguard --allow-ipv6-leak",
                "description": "Keep the connection up even when an IPv6 leak is detected (default is fail-closed)",
                "expectedOutput": "IPv6 leak detected ... continuing anyway because --allow-ipv6-leak was passed",
                "notes": "DANGER: this is the one flag that deliberately weakens leak protection. By default routing-switch terminates the connection the moment it sees an IPv6 leak, so your real IPv6 address cannot escape. With --allow-ipv6-leak the connection is kept and your real IPv6 address stays reachable to every site you visit. Use it only on a host with no usable IPv6, and only when you accept the deanonymisation risk."
              },
              {
                "command": "sudo routing-switch connect wireguard --exclude-private",
                "description": "Exclude private networks from VPN",
                "expectedOutput": "Private networks will bypass VPN tunnel",
                "notes": "Keep LAN access while using VPN"
              },
              {
                "command": "sudo routing-switch connect wireguard --dns-mode hybrid --exclude-private",
                "description": "Hybrid DNS with private network exclusion",
                "expectedOutput": "Using hybrid DNS mode with local network access",
                "notes": "Good for home/office use when you need LAN access"
              }
            ],
            "id": "dns_modes",
            "title": "DNS MODES AND SECURITY"
          },
          {
            "description": "Choose between Direct (preferred tunnel route) or Metric (with fallback) routing",
            "examples": [
              {
                "command": "sudo routing-switch connect wireguard && sudo routing-switch connect tor",
                "description": "DEFAULT: Direct routing mode (MOST SECURE), Tor layered on WireGuard",
                "expectedOutput": "Using DIRECT routing: TUN route preferred; original route kept as high-metric recovery",
                "notes": "✅ RECOMMENDED, strongest default, paired with leak checks and kill-switch controls. UNDERLAY REQUIRED: `connect tor` on its own is refused, Kodachi's Tor SOCKS proxy is VPN-side only. Connect WireGuard, OpenVPN or a proxy protocol first."
              },
              {
                "command": "sudo routing-switch connect wireguard && sudo routing-switch connect tor --metric",
                "description": "Metric routing mode (LESS SECURE, has fallback), Tor layered on WireGuard",
                "expectedOutput": "Using METRIC routing: Original route kept as backup",
                "notes": "⚠️ WARNING: can leak traffic if the TUN fails, use only if you need failover. UNDERLAY REQUIRED: `connect tor` on its own is refused, a Kodachi tunnel (wg0, tun0 or tun_routing) must already be up."
              },
              {
                "command": "sudo routing-switch connect shadowsocks",
                "description": "Direct mode for Shadowsocks (default)",
                "expectedOutput": "Direct routing: Shadowsocks TUN route is preferred",
                "notes": "Strongest default; leak checks and health controls decide failure handling"
              },
              {
                "command": "sudo routing-switch connect dante --metric",
                "description": "Metric mode for corporate VPN scenarios",
                "expectedOutput": "Metric routing: Fallback to original connection if VPN drops",
                "notes": "Use for work VPNs where you need automatic failover"
              },
              {
                "command": "sudo routing-switch connect xray-vless --metric --exclude-private",
                "description": "Metric mode with LAN access preserved",
                "expectedOutput": "Failover routing with local network bypass",
                "notes": "Corporate setup: VPN with failover + LAN access"
              }
            ],
            "id": "routing_modes",
            "title": "ROUTING MODES - SECURITY vs FAILOVER"
          },
          {
            "description": "Disconnect and restore normal routing",
            "examples": [
              {
                "command": "sudo routing-switch disconnect",
                "description": "Disconnect current proxy",
                "expectedOutput": "Successfully disconnected"
              },
              {
                "command": "sudo routing-switch disconnect --force",
                "description": "Force disconnect even if issues",
                "expectedOutput": "Force disconnected",
                "notes": "Kills all processes forcefully"
              },
              {
                "command": "sudo routing-switch disconnect --clean-firewall",
                "description": "Disconnect and clean all firewall rules",
                "expectedOutput": "Disconnected and firewall rules cleaned",
                "notes": "Use when network is stuck after disconnect due to lingering iptables rules"
              },
              {
                "command": "sudo routing-switch disconnect --force --clean-firewall",
                "description": "Force disconnect with complete firewall cleanup",
                "expectedOutput": "Force disconnected, all firewall rules flushed",
                "notes": "Emergency recovery - resets all iptables rules (preserves Docker/KVM chains)"
              },
              {
                "command": "sudo routing-switch disconnect --json",
                "description": "Disconnect with JSON output",
                "expectedOutput": "{\"command\":\"disconnect\",\"data\":{\"status\":\"disconnected\",\"message\":\"...\"},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              }
            ],
            "id": "disconnect",
            "title": "DISCONNECTION"
          },
          {
            "description": "Understanding --force behavior and which protocols can layer together.\nPROTOCOL TYPES: native VPNs own their interface (openvpn -> tun0, wireguard -> wg0). Tor is redsocks based (iptables NAT, no interface of its own). Everything else is tun2socks based and shares ONE device, tun_routing: shadowsocks, dante, v2ray, xray-vless, xray-vless-reality, xray-trojan, xray-vmess, mita, hysteria2.\nCOMPATIBILITY: native + Tor layers cleanly, and a proxy protocol + Tor layers cleanly. Two tun2socks protocols never do, they fight over tun_routing.\nRECOMMENDED LAYERING: connect WireGuard or OpenVPN (or one proxy protocol) FIRST, then `connect tor` on top. --force is not needed for that, Tor-over-VPN is allowed through the single-connection guard on purpose. Use --force only when you want to REPLACE the active connection with a different one. Always re-test connectivity after layering.",
            "examples": [
              {
                "command": "sudo routing-switch connect wireguard && sudo routing-switch connect tor",
                "description": "Creates LAYERED connection: Tor-over-WireGuard ✓ (no --force needed)",
                "expectedOutput": "→ connect tor detects the active WireGuard tunnel and layers on it\n    → WireGuard's wg0 stays up; Tor SOCKS reached at 10.0.0.1:9050 via wg0\n    → Tor's iptables rules redirect traffic on top",
                "notes": "LAYERED: connect tor keeps the VPN as its underlay (does NOT disconnect it). Traffic: App → Tor (redsocks) → WireGuard → Internet. Required: the worker Tor is VPN-only."
              },
              {
                "command": "sudo routing-switch connect openvpn && sudo routing-switch connect tor",
                "description": "Creates LAYERED connection: Tor-over-OpenVPN ✓ (no --force needed)",
                "expectedOutput": "→ connect tor detects the active OpenVPN tunnel and layers on it\n    → Traffic: Your App → Tor (redsocks) → OpenVPN (tun0) → Internet\n    → Tor SOCKS reached at 172.16.0.1:9050 via tun0",
                "notes": "connect tor keeps OpenVPN as its underlay. Tor uses iptables NAT, no interface conflict with tun0. The worker Tor is VPN-only - it cannot be connected standalone."
              },
              {
                "command": "sudo routing-switch connect shadowsocks && sudo routing-switch connect tor",
                "description": "Creates LAYERED connection: Tor-over-proxy ✓ (works for shadowsocks/v2ray/xray/hysteria2/mita)",
                "expectedOutput": "→ connect tor detects the active proxy tunnel (tun_routing) and layers on it\n    → Traffic: Your App → Tor (redsocks) → Shadowsocks → Internet\n    → Tor SOCKS reached at 172.16.0.1:9050 via tun_routing",
                "notes": "connect tor keeps the proxy protocol as its underlay. Works the same for all proxy protocols (shadowsocks/v2ray/xray/hysteria2/mita) - they all ride the tun2socks tun_routing device. The underlay's own uplink to its worker is auto-excluded from the Tor redirect so the tunnel is not broken."
              },
              {
                "command": "sudo routing-switch connect shadowsocks && sudo routing-switch connect dante",
                "description": "REFUSED: two tun2socks protocols cannot both be up ✗",
                "expectedOutput": "→ Error: Already connected via shadowsocks. Use 'routing-switch disconnect' first or use --force flag",
                "notes": "All tun2socks protocols (shadowsocks, dante, v2ray, xray-*, mita, hysteria2) share the same tun_routing device, so they cannot coexist. Adding --force does NOT stack them: it disconnects shadowsocks first and then connects dante."
              },
              {
                "command": "sudo routing-switch connect wireguard && sudo routing-switch connect openvpn --force",
                "description": "REPLACE the active connection: --force disconnects WireGuard, then dials OpenVPN",
                "expectedOutput": "→ Existing wireguard connection detected\n    → Disconnecting it (--force)\n    → Successfully connected to OpenVPN server",
                "notes": "This is what --force is actually for: swapping one connection for another. It does NOT stack two VPNs. Without --force the second connect is refused with 'another connection is active'."
              }
            ],
            "id": "force_reconnect",
            "title": "FORCE FLAG & PROTOCOL COMPATIBILITY"
          },
          {
            "description": "Check DNS configuration",
            "examples": [
              {
                "command": "routing-switch dns-info",
                "description": "Show current DNS servers",
                "expectedOutput": "IPv4: <VPN_IP>:53, IPv6: [<VPN_IPv6>]:53"
              },
              {
                "command": "routing-switch dns-info --json",
                "description": "DNS info in JSON format",
                "expectedOutput": "{\"command\":\"dns-info\",\"data\":{\"ipv4\":{\"host\":\"<VPN_IP>\",\"port\":53},\"ipv6\":{\"host\":\"<VPN_IPv6>\",\"port\":53}},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              }
            ],
            "id": "dns_info",
            "title": "DNS INFORMATION"
          },
          {
            "description": "Check Tor DNS and SOCKS configuration",
            "examples": [
              {
                "command": "routing-switch tor-dns-info",
                "description": "Show Tor DNS and SOCKS endpoints",
                "expectedOutput": "DNS: dns://<VPS_IP>:9054#Tor-DNS",
                "notes": "Resolves both regular and .onion domains"
              },
              {
                "command": "routing-switch tor-dns-info --json",
                "description": "Tor DNS info in JSON format",
                "expectedOutput": "{\"command\":\"tor-dns-info\",\"data\":{\"ipv4\":{\"dns_url\":\"dns://...\",\"socks_url\":\"socks5://...\"}},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              }
            ],
            "id": "tor_dns_info",
            "title": "TOR DNS INFORMATION"
          },
          {
            "description": "Display VPS server details and statistics",
            "examples": [
              {
                "command": "routing-switch vps-info",
                "description": "Show basic VPS information",
                "expectedOutput": "Hostname: <type>-<random>, IPv4: <VPS_IP>"
              },
              {
                "command": "routing-switch vps-info --detailed",
                "description": "Show detailed VPS stats",
                "expectedOutput": "CPU Load, Memory Usage, Uptime, Network Connections",
                "notes": "Includes CPU load, memory, uptime, and connection stats"
              },
              {
                "command": "routing-switch vps-info --json",
                "description": "VPS info in JSON format",
                "expectedOutput": "{\"command\":\"vps-info\",\"data\":{\"hostname\":\"...\",\"cpu_load\":{...},\"memory\":{...},\"card_type\":\"...\",\"generation_t\":\"...\"},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              }
            ],
            "id": "vps_info",
            "title": "VPS SERVER INFORMATION"
          },
          {
            "description": "List available protocols with scores",
            "examples": [
              {
                "command": "routing-switch list-protocols",
                "description": "List all available protocols",
                "expectedOutput": "WireGuard (Score: 96.5), OpenVPN (Score: 94.0)..."
              },
              {
                "command": "routing-switch list-protocols --json",
                "description": "Protocol list in JSON",
                "expectedOutput": "{\"command\":\"list-protocols\",\"data\":{\"protocols\":[{\"name\":\"wireguard\",\"overall_score\":96.5,\"available\":true,...}],\"scoring\":{...}},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              },
              {
                "command": "routing-switch list-protocols --detailed",
                "description": "Show the full scoring breakdown per protocol, not just the overall score",
                "expectedOutput": "wireguard  overall 96.5  security 98.0  speed 99.0  stability 95.0  available yes"
              },
              {
                "command": "routing-switch list-protocols --test",
                "description": "Probe each protocol's endpoint for real instead of scoring from the card alone",
                "expectedOutput": "Testing connectivity ... wireguard [OK] 12ms, shadowsocks [OK] 45ms, v2ray [FAIL]",
                "notes": "Slower (it opens a connection per protocol) but far more accurate than the static scores. Needs network access."
              },
              {
                "command": "routing-switch list-protocols --available-only",
                "description": "Show only available protocols",
                "expectedOutput": "Available: WireGuard, Tor, Shadowsocks",
                "notes": "Based on auth card"
              },
              {
                "command": "routing-switch list-protocols --sort-by-security",
                "description": "Sort by security score",
                "expectedOutput": "1. WireGuard (98.0), 2. OpenVPN (95.0)..."
              },
              {
                "command": "routing-switch list-protocols --sort-by-speed",
                "description": "Sort by speed score",
                "expectedOutput": "1. WireGuard (99.0), 2. Shadowsocks (85.0)..."
              }
            ],
            "id": "list_protocols",
            "title": "PROTOCOL LISTING"
          },
          {
            "description": "Automatically select best protocol",
            "examples": [
              {
                "command": "sudo routing-switch auto-select",
                "description": "Auto-select best protocol",
                "expectedOutput": "Selected WireGuard (best overall score)"
              },
              {
                "command": "sudo routing-switch auto-select --min-security 90",
                "description": "Auto-select with minimum security",
                "expectedOutput": "Selected OpenVPN (security: 95.0)"
              },
              {
                "command": "sudo routing-switch auto-select --prefer-speed",
                "description": "Prioritize speed in selection",
                "expectedOutput": "Selected WireGuard (fastest protocol)"
              },
              {
                "command": "sudo routing-switch auto-select --json",
                "description": "Auto-select with JSON output",
                "expectedOutput": "{\"command\":\"auto-select\",\"data\":{\"selected\":\"wireguard\",\"reason\":\"best overall score (95.5)\",\"status\":\"connected\"},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              }
            ],
            "id": "auto_select",
            "title": "AUTO-SELECTION"
          },
          {
            "description": "Local SOCKS5 port allocation reference. When a tun2socks protocol connects it picks the first free port in its own reserved range on 127.0.0.1:\nShadowsocks 30000-30004, V2Ray 30005-30009, Xray VMess 30010-30014, Xray VLESS 30015-30019, VLESS Reality 30020-30024, Xray Trojan 30025-30029, Mieru/Mita 30030-30034, Tor 30035-30039, Dante 30040-30044, Hysteria2 30045-30049, microsocks 30050-30054.\nThe ranges are high (30000+) so they never collide with system services, and each protocol gets its own so two of them can never fight over a port.\nTRAFFIC FLOW (tun2socks protocols): application -> tun_routing interface -> tun2socks -> 127.0.0.1:<PORT> -> protocol client -> remote server.\nNATIVE VPNs are different: WireGuard uses the UDP port from its config (typically 51820) and OpenVPN the TCP/UDP port from its config (typically 1194). Neither uses tun2socks or a local SOCKS5 port.\nKODACHI TOR is VPN-side, not local: its SOCKS5 lives on the worker at 172.16.0.1:9050 over OpenVPN and 10.0.0.1:9050 over WireGuard.",
            "examples": [
              {
                "command": "sudo routing-switch connect shadowsocks --no-tun",
                "description": "See the local SOCKS5 port a tun2socks protocol actually took",
                "expectedOutput": "Shadowsocks SOCKS5 proxy ready at localhost:30000",
                "notes": "No-TUN mode is the mode that prints the allocated port, because that port is the whole point of it. In normal (TUN) mode the port is internal plumbing between tun2socks and the protocol client, and you never need it."
              },
              {
                "command": "sudo routing-switch connect v2ray --no-tun --json",
                "description": "Get the allocated local SOCKS5 port back as JSON (no-TUN mode reports it explicitly)",
                "expectedOutput": "{\"command\":\"connect\",\"data\":{\"local_proxy_address\":\"127.0.0.1\",\"local_proxy_port\":30005,\"protocol\":\"v2ray\",\"mode\":\"no-tun\"},...}"
              }
            ],
            "id": "port_usage",
            "title": "PORT USAGE INFORMATION"
          },
          {
            "description": "Different JSON output options",
            "examples": [
              {
                "command": "sudo routing-switch status --json",
                "description": "Compact JSON output",
                "expectedOutput": "{\"command\":\"connect\",\"data\":{\"status\":\"connected\",\"protocol\":\"tor\",\"tun_device\":\"...\",\"server\":\"...\"},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}",
                "notes": "Requires sudo for accurate results"
              },
              {
                "command": "sudo routing-switch status --json-pretty",
                "description": "Pretty-printed JSON",
                "expectedOutput": "{\n  \"command\": \"status\",\n  \"data\": {\n    \"connected\": true,\n    \"protocol\": \"tor\",\n    ...\n  },\n  \"errors\": [],\n  \"metadata\": {...},\n  \"status\": \"success\",\n  \"timestamp\": \"...\",\n  \"version\": \"<VERSION>\",\n  \"warnings\": []\n}"
              },
              {
                "command": "sudo routing-switch status --json-human",
                "description": "Human-enhanced JSON",
                "expectedOutput": "JSON with descriptions and metadata"
              },
              {
                "command": "routing-switch list-protocols --json --json-pretty",
                "description": "Pretty JSON protocol list",
                "expectedOutput": "Formatted JSON array of protocols"
              }
            ],
            "id": "json_formats",
            "title": "JSON OUTPUT FORMATS"
          },
          {
            "description": "Test protocol performance",
            "examples": [
              {
                "command": "routing-switch benchmark",
                "description": "Benchmark all protocols (3 iterations default)",
                "expectedOutput": "Measured TCP connection latency for each available protocol",
                "notes": "This command measures reachability latency only; it does not claim download or upload throughput"
              },
              {
                "command": "routing-switch benchmark --iterations 5",
                "description": "Benchmark with custom iteration count",
                "expectedOutput": "Five measured TCP connection latency probes per protocol",
                "notes": "More iterations = more accurate results"
              },
              {
                "command": "routing-switch benchmark --json",
                "description": "Benchmark results in JSON",
                "expectedOutput": "{\"command\":\"benchmark\",\"data\":{\"results\":[{\"protocol\":\"wireguard\",\"avg_latency_ms\":42,\"successful_probes\":3}],\"iterations\":3},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              }
            ],
            "id": "benchmark",
            "title": "BENCHMARKING"
          },
          {
            "description": "Probe a protocol's endpoint without touching routing. Reads the endpoints from the auth card, so it needs a prior online-auth login.",
            "examples": [
              {
                "command": "routing-switch test-protocol tor",
                "description": "Test whether the worker's Tor SOCKS endpoint is reachable",
                "expectedOutput": "Tor: SOCKS5 endpoint <worker_ip>:9050 reachable",
                "notes": "Does not change routing. The endpoint comes from the auth card's Tor socks_url, so it is the worker address (VPN-side), never 127.0.0.1. Without a tunnel up it will simply report unreachable, which is expected."
              },
              {
                "command": "routing-switch test-protocol shadowsocks --extended",
                "description": "Extended protocol test",
                "expectedOutput": "Shadowsocks: Connected, Latency: 45ms, Throughput: OK"
              },
              {
                "command": "routing-switch test-protocol all",
                "description": "Test all available protocols",
                "expectedOutput": "Testing: Tor [OK], Shadowsocks [OK], V2Ray [FAIL]..."
              },
              {
                "command": "routing-switch test-protocol wireguard --json",
                "description": "Test with JSON output",
                "expectedOutput": "{\"command\":\"test-protocol\",\"data\":{\"protocol\":\"wireguard\",\"status\":\"ok\",\"available\":true,\"connectivity\":true,\"latency_ms\":12},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              }
            ],
            "id": "test_protocol",
            "title": "TESTING PROTOCOLS"
          },
          {
            "description": "Run a proxy client without creating a TUN device, so system traffic is untouched and only the apps you point at the local SOCKS5 port go through it.\nConfigure the client by hand: Firefox -> Settings -> Network Settings -> Manual proxy -> SOCKS5 host 127.0.0.1, port <the allocated port>. Chrome/Chromium: start it with --proxy-server=\"socks5://127.0.0.1:30000\".\n--no-tun works for the tun2socks protocols (shadowsocks, dante, v2ray, xray-*, mita, hysteria2). It does NOT work for `connect tor`: Kodachi's Tor is redsocks/iptables based and rejects --no-tun outright (see the tor entry below).",
            "examples": [
              {
                "command": "sudo routing-switch connect shadowsocks --no-tun",
                "description": "Start Shadowsocks SOCKS5 proxy without TUN",
                "expectedOutput": "Shadowsocks SOCKS5 proxy ready at localhost:30000",
                "notes": "Configure browser/app manually with localhost:30000"
              },
              {
                "command": "sudo routing-switch connect dante --no-tun",
                "description": "Connect to Dante SOCKS5 without routing traffic",
                "expectedOutput": "Dante proxy: host:port, username, password",
                "notes": "Direct connection to remote Dante server"
              },
              {
                "command": "sudo routing-switch connect wireguard && routing-switch tor-dns-info",
                "description": "Use Kodachi's Tor as a manual proxy: connect a VPN, then read the Tor SOCKS endpoint and point your app at it",
                "expectedOutput": "SOCKS: socks5://10.0.0.1:9050 (over WireGuard) or socks5://172.16.0.1:9050 (over OpenVPN)",
                "notes": "`connect tor --no-tun` is NOT supported and fails with 'Tor via redsocks does not support no-TUN mode'. Kodachi's Tor is redsocks/iptables based, and its SOCKS5 lives on the worker inside the VPN network (10.0.0.1:9050 over WireGuard, 172.16.0.1:9050 over OpenVPN), never on 127.0.0.1. So the manual-proxy path for Tor is: connect a Kodachi VPN, then set your browser's SOCKS5 proxy to that worker address."
              },
              {
                "command": "sudo routing-switch connect v2ray --no-tun --json",
                "description": "V2Ray proxy in manual mode with JSON output",
                "expectedOutput": "{\"command\":\"connect\",\"data\":{\"local_proxy_address\":\"127.0.0.1\",\"local_proxy_port\":30005,\"protocol\":\"v2ray\",\"mode\":\"no-tun\"},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}",
                "notes": "V2Ray client runs, no system routing"
              },
              {
                "command": "sudo routing-switch connect xray-vless --no-tun",
                "description": "Xray VLESS proxy for manual configuration",
                "expectedOutput": "Xray SOCKS5 proxy ready at localhost:30015"
              },
              {
                "command": "sudo routing-switch disconnect",
                "description": "Stop the proxy (works for both TUN and no-TUN modes)",
                "expectedOutput": "Proxy stopped"
              }
            ],
            "id": "no_tun_mode",
            "title": "NO-TUN MODE (MANUAL PROXY)"
          },
          {
            "description": "Export and display protocol configurations",
            "examples": [
              {
                "command": "routing-switch export-config",
                "description": "Export all configurations to default location",
                "expectedOutput": "Exported 9 configuration(s) to ./results/configs/",
                "notes": "Creates timestamped config files"
              },
              {
                "command": "routing-switch export-config wireguard",
                "description": "Export specific protocol configuration",
                "expectedOutput": "Exported 1 configuration(s) to ./results/configs/\n  - wireguard: wireguard_20250817_143022.conf"
              },
              {
                "command": "routing-switch export-config all --path /tmp/vpn-configs/",
                "description": "Export all configs to custom path",
                "expectedOutput": "Exported 9 configuration(s) to /tmp/vpn-configs/",
                "notes": "Creates directory if it doesn't exist"
              },
              {
                "command": "routing-switch export-config shadowsocks --include-credentials",
                "description": "Export with credentials included",
                "expectedOutput": "Exported 1 configuration(s) to ./results/configs/",
                "notes": "⚠️ WARNING: Contains sensitive passwords"
              },
              {
                "command": "routing-switch export-config --json",
                "description": "Export with JSON output showing results",
                "expectedOutput": "{\"command\":\"export-config\",\"data\":{\"exported_count\":9,\"export_directory\":\"./results/configs/\",\"files\":[...]},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              },
              {
                "command": "routing-switch showconfig wireguard",
                "description": "Display WireGuard configuration",
                "expectedOutput": "{\"command\":\"showconfig\",\"data\":{\"protocol\":\"wireguard\",\"config\":{\"available\":true,\"config_content\":\"[Interface]\\nPrivateKey = ...\",\"masked\":false,\"name\":\"wireguard\",\"status\":\"available\"},\"masked\":false},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}",
                "notes": "Masks sensitive data by default"
              },
              {
                "command": "routing-switch showconfig all --json-pretty",
                "description": "Show all protocol configs in pretty JSON",
                "expectedOutput": "Formatted JSON with all available protocol configurations"
              },
              {
                "command": "routing-switch showconfig dante --mask-sensitive",
                "description": "Show config with passwords hidden",
                "expectedOutput": "{\"command\":\"showconfig\",\"data\":{\"protocol\":\"dante\",\"config\":{\"username\":\"dante_user\",\"password\":\"***REDACTED***\",\"ipv4_url\":\"socks5://dante_user:***REDACTED***@...\",...},\"masked\":true},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}",
                "notes": "Safe for sharing/logging"
              },
              {
                "command": "routing-switch showconfigurl shadowsocks",
                "description": "Get Shadowsocks connection URL",
                "expectedOutput": "ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTp6TDQ4cVYx...@<VPN_IP>:8388#Shadowsocks-IPv4",
                "notes": "URL can be imported into mobile apps"
              },
              {
                "command": "routing-switch showconfigurl dante --json",
                "description": "Get SOCKS5 URLs in JSON format",
                "expectedOutput": "{\"ipv4\": \"socks5://user:pass@<VPN_IP>:1080\", \"ipv6\": \"socks5://user:pass@[<VPN_IPv6>]:1080\"}"
              },
              {
                "command": "routing-switch showconfigurl all",
                "description": "Get URLs for all available protocols",
                "expectedOutput": "Protocol Connection URLs:\n\nshadowsocks:\n  ipv4: ss://...\n\ndante:\n  ipv4: socks5://..."
              },
              {
                "command": "routing-switch showconfigqr wireguard",
                "description": "Generate QR code for WireGuard config",
                "expectedOutput": "[QR Code ASCII Art]\n\nURL: wireguard://base64_encoded_config",
                "notes": "Scan with WireGuard mobile app"
              },
              {
                "command": "routing-switch showconfigqr shadowsocks",
                "description": "Generate IPv4 QR code for Shadowsocks (default)",
                "expectedOutput": "[QR Code ASCII Art]\n\nURL: ss://...",
                "notes": "Shows IPv4 QR code by default for dual-stack protocols"
              },
              {
                "command": "routing-switch showconfigqr shadowsocks --ipv6",
                "description": "Generate IPv6 QR code for Shadowsocks",
                "expectedOutput": "[QR Code ASCII Art]\n\nURL: ss://...",
                "notes": "Use --ipv6 flag to show IPv6 QR for dual-stack protocols"
              },
              {
                "command": "routing-switch showconfigqr shadowsocks --skip-validation",
                "description": "Generate QR without validation (faster)",
                "expectedOutput": "[QR Code ASCII Art]\n\n⚠ Validation skipped",
                "notes": "Skips automatic QR validation for debugging"
              },
              {
                "command": "routing-switch showconfigqr all --strict-validation",
                "description": "Generate all QR codes with strict validation",
                "expectedOutput": "✓ Shadowsocks QR validated\n✓ WireGuard QR validated...",
                "notes": "Fails if any QR code validation fails"
              },
              {
                "command": "routing-switch showconfigqr dante --json",
                "description": "Get QR data in JSON format",
                "expectedOutput": "{\"command\":\"showconfigqr\",\"data\":{\"protocol\":\"dante\",\"qr\":{\"content_type\":\"image/png\",\"format\":\"ipv4\",\"protocol\":\"dante\",\"qr_code\":\"<base64-png>\",\"url\":\"socks5://...\",\"validated\":true}},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}",
                "notes": "JSON includes the URL for QR generation"
              },
              {
                "command": "routing-switch showconfigqr shadowsocks --save-files",
                "description": "Generate QR code and save as PNG file",
                "expectedOutput": "[QR Code ASCII Art]\n\n✓ IPv4 QR code files saved to ./results/qr-codes/",
                "notes": "Saves clean PNG file: shadowsocks_ipv4.png"
              },
              {
                "command": "routing-switch showconfigqr shadowsocks --ipv6 --save-files",
                "description": "Generate IPv6 QR code and save as PNG file",
                "expectedOutput": "[QR Code ASCII Art]\n\n✓ IPv6 QR code files saved to ./results/qr-codes/",
                "notes": "Saves clean PNG file: shadowsocks_ipv6.png"
              },
              {
                "command": "routing-switch showconfigqr shadowsocks --ipv4 --ipv6 --save-files",
                "description": "Generate both IPv4 and IPv6 QR codes",
                "expectedOutput": "[QR Code ASCII Art]\n\n✓ IPv4 and IPv6 QR code files saved to ./results/qr-codes/",
                "notes": "Saves both shadowsocks_ipv4.png and shadowsocks_ipv6.png"
              },
              {
                "command": "routing-switch showconfigqr all --save-files --json-pretty",
                "description": "Generate all QR codes with pretty JSON output",
                "expectedOutput": "Pretty-formatted JSON with QR data and file paths",
                "notes": "Creates clean PNG files for all available protocols, supports all JSON formats"
              },
              {
                "command": "routing-switch showconfigqr wireguard --save-files --json-human",
                "description": "Save QR file with human-enhanced JSON output",
                "expectedOutput": "{\"command\":\"showconfigqr\",\"data\":{\"protocol\":\"wireguard\",\"qr\":{\"qr_code\":\"<base64-png>\",\"saved_file_path\":\"./results/qr-codes/wireguard.png\",...}},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}",
                "notes": "VPN protocols use single filename (no IP version suffix)"
              }
            ],
            "id": "config_export",
            "title": "CONFIGURATION EXPORT AND DISPLAY"
          },
          {
            "description": "Validate QR codes against auth card",
            "examples": [
              {
                "command": "routing-switch validate-qr shadowsocks_ipv4.png",
                "description": "Validate QR code from image file (NEW: direct filename)",
                "expectedOutput": "✓ QR valid: Shadowsocks (confidence: 98.5%)",
                "notes": "Smart detection: automatically finds file in current dir or results/qr-codes/"
              },
              {
                "command": "routing-switch validate-qr dante_ipv4.png",
                "description": "Validate QR code from results/qr-codes directory",
                "expectedOutput": "✓ QR valid: Dante (confidence: 96.2%)",
                "notes": "Automatically looks in ./results/qr-codes/ if not found in current directory"
              },
              {
                "command": "routing-switch validate-qr --file shadowsocks_qr.png",
                "description": "Validate QR code using explicit --file flag (legacy syntax)",
                "expectedOutput": "✓ QR valid: Shadowsocks (confidence: 98.5%)",
                "notes": "Both direct filename and --file flag work identically"
              },
              {
                "command": "echo 'ss://...' | routing-switch validate-qr --stdin",
                "description": "Validate QR URL from stdin",
                "expectedOutput": "✓ Protocol: shadowsocks\n✓ Config matches auth card",
                "notes": "Useful for clipboard validation"
              },
              {
                "command": "routing-switch validate-qr all",
                "description": "Validate all generated QR codes",
                "expectedOutput": "Validating all QR codes...\n✓ shadowsocks_ipv4.png: valid\n✓ wireguard.png: valid",
                "notes": "Checks all QR codes in results/qr-codes/"
              },
              {
                "command": "routing-switch validate-qr --test-readability shadowsocks",
                "description": "Test QR code readability",
                "expectedOutput": "Testing QR readability...\n✓ QR code is readable",
                "notes": "Round-trip test of QR generation"
              },
              {
                "command": "routing-switch validate-qr qr_code.png --json",
                "description": "JSON validation output",
                "expectedOutput": "{\"command\":\"validate-qr\",\"data\":{\"is_valid\":true,\"protocol\":\"shadowsocks\",\"confidence_score\":0.985,\"config_data\":{...}},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}",
                "notes": "Machine-readable validation results"
              }
            ],
            "id": "qr_validation",
            "title": "QR CODE VALIDATION"
          },
          {
            "description": "Recover from a broken or half-torn-down connection. reset, cleanup, recover and a plain disconnect are all auth-exempt on purpose: they have to work when the network (and therefore online-auth) is down.",
            "examples": [
              {
                "command": "sudo routing-switch disconnect --force",
                "description": "Force disconnect stuck connection",
                "expectedOutput": "Force killed all processes, routes restored",
                "notes": "Use when normal disconnect fails"
              },
              {
                "command": "sudo routing-switch reset",
                "description": "Reset all routing to default",
                "expectedOutput": "Routing reset to system defaults",
                "notes": "Emergency recovery command"
              },
              {
                "command": "sudo routing-switch reset --force",
                "description": "Reset routing even when the state file says something is still connected",
                "expectedOutput": "Forcing reset ... Routing reset to system defaults",
                "notes": "DESTRUCTIVE: tears down the active tunnel, its routes and its DNS overrides without asking. Your traffic goes back to the clear-net default route, so you are NOT anonymised afterwards. Use only when a normal disconnect will not complete."
              },
              {
                "command": "sudo routing-switch cleanup",
                "description": "Clean up orphaned processes",
                "expectedOutput": "Cleaned up 2 orphaned tun2socks processes"
              },
              {
                "command": "sudo routing-switch cleanup --thorough",
                "description": "Deeper sweep: also removes leftover TUN devices, routes and iptables rules, not just stray processes",
                "expectedOutput": "Thorough cleanup: killed 2 orphaned processes, removed tun_routing, dropped 6 stale iptables rules",
                "notes": "Heavier than plain cleanup and can disturb an active connection, so run it after disconnecting."
              },
              {
                "command": "sudo routing-switch recover",
                "description": "Automatically diagnose and fix routing issues, restore network settings",
                "expectedOutput": "System recovered, ready for new connection",
                "notes": "Performs comprehensive recovery including cleanup, route restoration, and DNS reset"
              },
              {
                "command": "sudo routing-switch recover --force",
                "description": "Run the recovery even when routing-switch thinks nothing is wrong",
                "expectedOutput": "Forced recovery: cleanup, routes restored, DNS reset, ready for new connection",
                "notes": "DESTRUCTIVE: drops any active tunnel and restores the clear-net default route, so you lose VPN/Tor protection. This is the last-resort 'my internet is stuck' command."
              }
            ],
            "id": "error_recovery",
            "title": "ERROR RECOVERY"
          },
          {
            "description": "Turn this Kodachi box into a SOCKS5 proxy server so other devices on your LAN can ride its active tunnel.\nWORKFLOW: 1) connect routing-switch to any protocol (WireGuard, OpenVPN, V2Ray, Shadowsocks, Hysteria2, ...). 2) `microsocks-enable` with a username and password. 3) On the other device, set SOCKS5 host = this machine's LAN IP, port = the listening port (30050-30054), plus those credentials. All of that device's traffic then leaves through this machine's tunnel.\nmicrosocks-enable and microsocks-disable need root AND a prior online-auth login. microsocks-status is auth-exempt.\nSECURITY: the listener binds 0.0.0.0, so anything that can reach this host on that port can try to use it. Pick a strong password, and disable the server when you are done.",
            "examples": [
              {
                "command": "sudo routing-switch microsocks-enable -u microkodachi-8273 -p 'S@Cur9P@s@Wo-Ds'",
                "description": "Enable microsocks server with auto port detection",
                "expectedOutput": "Microsocks server started on port 30050, listening on all interfaces",
                "notes": "Automatically selects available port from 30050-30054 range. Used when Kodachi acts as a server. After connecting routing-switch to any service (WireGuard, V2Ray, etc.), enable microsocks so other PCs on your network can connect through this machine using the listening microsocks port. Strong credentials recommended for security."
              },
              {
                "command": "sudo routing-switch microsocks-enable -u microkodachi-8273 -p 'S@Cur9P@s@Wo-Ds' --port 30051",
                "description": "Enable with specific port",
                "expectedOutput": "Microsocks server started on port 30051",
                "notes": "Use when you need a specific port number within the 30050-30054 range"
              },
              {
                "command": "sudo routing-switch microsocks-status",
                "description": "Check microsocks server status",
                "expectedOutput": "Status: Running, Port: 30050, PID: 12345",
                "notes": "Requires sudo for accurate results (reads privileged state)"
              },
              {
                "command": "sudo routing-switch microsocks-status --json",
                "description": "Check status in JSON format",
                "expectedOutput": "{\"command\":\"microsocks-status\",\"data\":{\"status\":\"running\",\"running\":true,\"port\":30050,\"pid\":12345,\"listening_address\":\"0.0.0.0\",\"username\":\"...\",\"started_at\":\"...\",\"uptime_seconds\":...},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              },
              {
                "command": "sudo routing-switch microsocks-disable",
                "description": "Stop microsocks server",
                "expectedOutput": "Microsocks server stopped successfully"
              },
              {
                "command": "sudo routing-switch connect wireguard && sudo routing-switch microsocks-enable -u microkodachi-8273 -p 'S@Cur9P@s@Wo-Ds'",
                "description": "Complete server workflow: bring up a tunnel, then share it over SOCKS5",
                "expectedOutput": "Successfully connected to WireGuard server\nMicrosocks server started on port 30050, listening on all interfaces",
                "notes": "The other device then points its SOCKS5 client at socks5://microkodachi-8273:<password>@<this-machine-LAN-IP>:30050 (for example 192.168.1.100:30050). Its traffic leaves through this machine's WireGuard tunnel. Order matters: enable microsocks AFTER the tunnel is up."
              }
            ],
            "id": "microsocks_server",
            "title": "MICROSOCKS SERVER MODE"
          },
          {
            "description": "Use the free VPNGate public server pool.\nAUTH: browsing is auth-free (vpngate-fetch, vpngate-list, vpngate-export, vpngate-export-all all work on a fresh shell). `vpngate-connect` is NOT auth-free: it mutates routes, DNS and firewall exactly like `connect`, so it goes through the same online-auth gate and needs root. The servers themselves cost nothing, the auth gate is Kodachi's, not VPNGate's.",
            "examples": [
              {
                "command": "routing-switch vpngate-fetch",
                "description": "Fetch VPNGate server list from public API",
                "expectedOutput": "Fetched 150 VPNGate servers",
                "notes": "Downloads and caches server list (refreshes every hour)"
              },
              {
                "command": "routing-switch vpngate-list",
                "description": "List all cached VPNGate servers",
                "expectedOutput": "Index Flag Country IP Speed Ping Score Sessions",
                "notes": "Shows servers sorted by score (default)"
              },
              {
                "command": "routing-switch vpngate-list --country JP --sort speed -l 10",
                "description": "List top 10 Japanese servers by speed",
                "expectedOutput": "10 servers shown, filtered by country: JP",
                "notes": "Filter by country name or code, sort by speed/ping/score/sessions"
              },
              {
                "command": "routing-switch vpngate-list --country US --sort ping",
                "description": "List US servers sorted by lowest ping",
                "expectedOutput": "Servers sorted by ping latency"
              },
              {
                "command": "routing-switch vpngate-list --sort sessions -l 10",
                "description": "List the 10 least congested servers (fewest active VPN sessions)",
                "expectedOutput": "Servers sorted by session count, lowest first",
                "notes": "--sort accepts score (default, highest first), speed (highest first), ping (lowest first) and sessions (LOWEST first, i.e. least crowded)."
              },
              {
                "command": "sudo routing-switch vpngate-connect 5",
                "description": "Connect to VPNGate server at index 5",
                "expectedOutput": "Successfully connected to <server> via openvpn",
                "notes": "Uses OpenVPN with the public vpn/vpn credentials. REQUIRES AUTHENTICATION: unlike vpngate-fetch/list/export, this one establishes a connection, so it goes through the online-auth gate and needs root. Index comes from vpngate-list, so fetch and list first."
              },
              {
                "command": "sudo routing-switch vpngate-connect 12 --force",
                "description": "Force connect to server 12 (disconnect existing first)",
                "expectedOutput": "Force connected to VPNGate server"
              },
              {
                "command": "sudo routing-switch vpngate-connect 3 --skip-prerequisites",
                "description": "Connect without running the network prerequisite checks first",
                "expectedOutput": "Connecting to VPNGate server 3 (prerequisite checks skipped)",
                "notes": "Skips the IPv4-forwarding / NAT / MASQUERADE checks. Only use it if you know those are already correct: on a box that is not set up, the connection can come up half-broken."
              },
              {
                "command": "routing-switch vpngate-fetch --json",
                "description": "Fetch servers with JSON output",
                "expectedOutput": "{\"command\":\"vpngate-fetch\",\"data\":{\"status\":\"success\",\"server_count\":150,\"cache_file\":\"...\",\"fetched_at\":\"...\"},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              },
              {
                "command": "routing-switch vpngate-list --json-pretty",
                "description": "List servers in pretty JSON format",
                "expectedOutput": "Pretty-printed JSON array of servers"
              },
              {
                "command": "routing-switch vpngate-export 1",
                "description": "Export top server's OpenVPN config as .ovpn file",
                "expectedOutput": "Exported: JP Japan (1.2.3.4) -> results/vpngate-configs/1_jp_1-2-3-4.ovpn",
                "notes": "Includes embedded vpn/vpn credentials for standalone use"
              },
              {
                "command": "routing-switch vpngate-export-all",
                "description": "Export all cached servers as .ovpn files",
                "expectedOutput": "Exported 99 VPNGate configs to results/vpngate-configs",
                "notes": "Bulk export for use in external OpenVPN clients"
              },
              {
                "command": "routing-switch vpngate-export 5 --json",
                "description": "Export server #5 config with JSON output",
                "expectedOutput": "{\"command\":\"vpngate-export\",\"data\":{\"action\":\"vpngate-export\",\"file\":\"...\",\"server\":{\"index\":5,\"country\":\"...\",\"country_code\":\"...\",\"flag\":\"...\",\"ip\":\"...\",\"speed\":\"...\",\"ping\":...,\"score\":...}},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              }
            ],
            "id": "vpngate_free_vpn",
            "title": "VPNGATE FREE VPN"
          },
          {
            "description": "Browse, fetch, and connect through commercial and free third-party VPN providers (VPN Gate, Riseup, NordVPN, IVPN, PIA, Surfshark, AirVPN, Mullvad-WG).\nThis is the largest surface in routing-switch: 20 verbs under `routing-switch providers`. Run `routing-switch providers --help` for the full list.\nAUTH: EVERY `providers` verb requires a prior online-auth login, including the read-only ones (list, list-profiles, get-profile). The catalog and the fetched configs are curated content, not open scrape targets.\nSUDO: any verb that writes the provider cache needs root, because the cache lives under dashboard/hooks/cache/vpn-providers/<id>/ which is root-owned in /opt. That is fetch, get-profile (it lazily fetches on a cache miss), import-config, delete-custom-profile, delete-custom-profiles-bulk, resolve-country, resolve-ip, resolve-ips, benchmark, clear-cache, credentials-set, credentials-delete, and all three connect verbs. Only `list`, `list-profiles`, `credentials-list`, `refresh-catalog` and `validate-config` are pure reads.",
            "examples": [
              {
                "command": "routing-switch providers list",
                "description": "Show every provider in the catalog with cache freshness",
                "expectedOutput": "VPN Provider Catalog ... vpngate ... riseup ... nordvpn",
                "notes": "Reads dashboard/hooks/config/vpn-providers-public-api.json (user-editable)"
              },
              {
                "command": "routing-switch providers list --json",
                "description": "Same list in machine-readable JSON for the GUI / scripts",
                "expectedOutput": "{\"command\":\"providers-list\",\"data\":{\"schema_version\":\"1.1.0\",\"catalog_path\":\"...\",\"provider_count\":13,\"providers\":[{\"id\":\"vpngate\",\"tier\":\"tier_a\",...}]},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              },
              {
                "command": "sudo routing-switch providers fetch vpngate",
                "description": "Fetch the live profile list for a provider into the cache",
                "expectedOutput": "✓ Fetched N profiles for 'vpngate' (cached at ...)",
                "notes": "Hits the provider's public endpoint. Cache lives at /opt/kodachi/dashboard/hooks/cache/vpn-providers/<id>/"
              },
              {
                "command": "sudo routing-switch providers fetch vpngate --force",
                "description": "Bypass the per-provider cache TTL and always re-fetch",
                "expectedOutput": "Refetches and overwrites the cache",
                "notes": "Use this when you specifically want fresh data (e.g. after edits to the catalog endpoints)."
              },
              {
                "command": "routing-switch providers list-profiles vpngate",
                "description": "Browse cached profiles for a provider (table view)",
                "expectedOutput": "# Display name | Flag | CC | Remote | Protocol | Port | Load"
              },
              {
                "command": "routing-switch providers list-profiles vpngate --country jp --limit 5",
                "description": "Filter to top 5 Japanese profiles",
                "expectedOutput": "5 profiles, all CC=JP",
                "notes": "Country code is 2-letter ISO, lowercase."
              },
              {
                "command": "routing-switch providers list-profiles nordvpn --protocol wireguard --limit 20",
                "description": "Filter a provider's profiles by transport protocol",
                "expectedOutput": "20 profiles, all Protocol=wireguard",
                "notes": "--protocol (-p) is a substring match against the profile's transport: udp, tcp, openvpn, wireguard. Combine it with --country (-c) and --limit (-l)."
              },
              {
                "command": "routing-switch providers list-profiles vpngate --json-pretty",
                "description": "JSON form (the GUI's primary surface)",
                "expectedOutput": "{\"command\":\"providers-list-profiles\",\"data\":{\"provider_id\":\"vpngate\",\"profile_count\":99,\"country_filter\":null,\"protocol_filter\":null,\"profiles\":[...]},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}"
              },
              {
                "command": "sudo routing-switch providers get-profile vpngate 1_219-100-37-203 > /tmp/jp.ovpn",
                "description": "Dump a single profile's .ovpn content to stdout (or pipe to a file)",
                "expectedOutput": "(raw .ovpn body)",
                "notes": "Needs root: on a cache miss it lazily fetches the profile and writes it into the root-owned provider cache. profile_id format is provider-specific, see `providers list-profiles --json`. The body is the actual OpenVPN config text, so you can feed it to `routing-switch connect openvpn --config /tmp/jp.ovpn`."
              },
              {
                "command": "routing-switch providers refresh-catalog",
                "description": "Re-read the catalog JSON after editing it on disk",
                "expectedOutput": "✓ Catalog reloaded ...",
                "notes": "Use this after manually editing vpn-providers-public-api.json (add custom providers, change endpoints) without restarting anything."
              },
              {
                "command": "sudo routing-switch connect openvpn --config /tmp/jp.ovpn --force",
                "description": "Connect using a profile previously fetched via `providers get-profile`",
                "expectedOutput": "Successfully connected to OpenVPN server",
                "notes": "The catalog provides the profile metadata; the connect command does the actual handshake. For paid-tier providers, use `providers credentials-set <id>` first, then `providers connect <id> <profile_id>` which auto-injects auth-user-pass."
              },
              {
                "command": "sudo routing-switch providers credentials-set nordvpn --field username=foo --field password=bar",
                "description": "Save NordVPN service credentials (separate from the dashboard login)",
                "expectedOutput": "✓ Saved 2 credential field(s) for 'nordvpn'",
                "notes": "Stored at ~/.config/kodachi/vpn-credentials.json with mode 0600, values sealed with AES-256-GCM (machine-bound key). credentials-list shows which providers are configured."
              },
              {
                "command": "routing-switch providers credentials-list",
                "description": "Show which providers have stored credentials (values are never printed)",
                "expectedOutput": "VPN Provider Credentials ... nordvpn ... yes (username,password)"
              },
              {
                "command": "sudo routing-switch providers connect nordvpn pl150-nordvpn-com_udp",
                "description": "Connect via a provider profile, auto-injecting saved credentials into auth-user-pass",
                "expectedOutput": "Successfully connected to OpenVPN server",
                "notes": "Equivalent to: get-profile -> patch auth-user-pass -> connect openvpn --config <tempfile>. Re-execs in the same routing-switch process so all the connect prerequisites still run."
              },
              {
                "command": "sudo routing-switch providers import-config --name 'My VPN' --file /tmp/my.ovpn",
                "description": "Phase 5: import your own .ovpn / .conf / JSON / YAML config as a custom profile",
                "expectedOutput": "✓ Imported custom profile ... Protocol: openvpn ... Connect with: routing-switch providers connect custom <id>",
                "notes": "Auto-detects OpenVPN / WireGuard / Shadowsocks / V2Ray / Hysteria2. Validation produces warnings, never blockers."
              },
              {
                "command": "sudo routing-switch providers credentials-set custom/my-vpn-20260522 --field username=foo --field password=bar",
                "description": "Save the VPN account username/password for an imported OpenVPN profile (Proton VPN and most commercial providers)",
                "expectedOutput": "✓ Saved 2 credential field(s) for custom profile 'my-vpn-20260522'",
                "notes": "Needed when the imported .ovpn has a bare `auth-user-pass` line. `connect custom <id>` then builds the auth-user-pass file automatically. The dashboard's Import panel does this for you."
              },
              {
                "command": "sudo routing-switch providers import-config --name 'WG Peer' --text \"$(cat ~/wg.conf)\" --protocol-hint wireguard",
                "description": "Import inline text with an explicit protocol hint (overrides auto-detect)",
                "expectedOutput": "✓ Imported custom profile ... Protocol: wireguard",
                "notes": "Needs root: the imported profile is written into the root-owned custom-provider cache. --file and --text are mutually exclusive."
              },
              {
                "command": "routing-switch providers list-profiles custom",
                "description": "Browse the configs you've imported",
                "expectedOutput": "Provider: custom ... Profiles: N"
              },
              {
                "command": "sudo routing-switch providers delete-custom-profile my-vpn-20260520143000",
                "description": "Remove a previously-imported custom profile",
                "expectedOutput": "✓ Deleted custom profile '...'",
                "notes": "Needs root (it removes files from the root-owned cache). Irreversible: the imported config body is gone, re-import it if you still need it."
              },
              {
                "command": "sudo routing-switch providers delete-custom-profiles-bulk --id my-vpn-20260520143000 --id work-vpn-20260601090000 --json",
                "description": "Delete several imported custom profiles in one root invocation",
                "expectedOutput": "{\"command\":\"providers-delete-custom-profiles-bulk\",\"data\":{\"deleted\":[\"my-vpn-20260520143000\",\"work-vpn-20260601090000\"],\"failed\":[]},...}",
                "notes": "--id is repeatable. Any credentials stored under custom/<id> are wiped with the profile. Irreversible. --id and --all are mutually exclusive."
              },
              {
                "command": "sudo routing-switch providers delete-custom-profiles-bulk --all --json",
                "description": "Delete EVERY imported custom profile",
                "expectedOutput": "{\"command\":\"providers-delete-custom-profiles-bulk\",\"data\":{\"deleted\":[\"my-vpn-...\",\"work-vpn-...\"],\"failed\":[]},...}",
                "notes": "DESTRUCTIVE and irreversible: wipes every config you ever imported, plus their stored credentials. There is no confirmation prompt. This is what the dashboard's 'Delete all' bulk action calls."
              },
              {
                "command": "sudo routing-switch providers credentials-delete nordvpn",
                "description": "Delete the stored credentials for one provider",
                "expectedOutput": "✓ Deleted stored credentials for 'nordvpn'",
                "notes": "Only removes the credentials, the cached profiles stay (use `providers clear-cache <id>` for those). After this, `providers connect nordvpn <profile>` fails until you run credentials-set again. For an imported profile the id is the custom/<profile_id> form."
              },
              {
                "command": "routing-switch providers validate-config --text \"vless://uuid@example.com:8443?type=tcp#NY\" --json",
                "description": "CV-3 dry-run: preview detected protocol + endpoint + warnings without writing to cache",
                "expectedOutput": "{\"command\":\"providers-validate-config\",\"data\":{\"detected_protocol\":\"v2ray\",\"remote_host\":\"example.com\",\"remote_port\":8443,\"transport\":\"tcp\",\"warnings\":[],\"hint_used\":false},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}",
                "notes": "Validates URI schemes (vmess/vless/trojan/ss/hysteria2/tuic), Clash YAML, sing-box JSON, and plain OpenVPN/WireGuard bodies. Always warnings, never blockers."
              },
              {
                "command": "sudo routing-switch providers resolve-country riseup vpn12-nyc_udp_1194",
                "description": "Look up the country for a cached profile via ip-fetch (skips if already tagged)",
                "expectedOutput": "✓ Resolved country for riseup/vpn12-nyc_udp_1194: us (United States)",
                "notes": "Pass --force to re-verify even if a country is already set (Verify mode)."
              },
              {
                "command": "sudo routing-switch providers resolve-ip riseup vpn12-nyc_udp_1194",
                "description": "DNS-resolve the profile's hostname to an IPv4 and store in remote_ip",
                "expectedOutput": "✓ riseup/vpn12-nyc_udp_1194 resolved vpn12-nyc.riseup.net → 198.252.153.x",
                "notes": "Uses `getent hosts` for resolution so it honors /etc/hosts overrides."
              },
              {
                "command": "sudo routing-switch providers resolve-ips riseup --concurrency 8",
                "description": "Bulk DNS-resolve every cached profile in a provider that's missing an IP",
                "expectedOutput": "✓ N resolved out of M (failed: K)",
                "notes": "Bounded parallelism via tokio::sync::Semaphore so we don't hammer the resolver."
              },
              {
                "command": "sudo routing-switch providers connect-random vpngate --country jp --force",
                "description": "Pick a random profile (optionally filtered by country) and connect",
                "expectedOutput": "Successfully connected to OpenVPN server",
                "notes": "Useful for VPN Gate where load is largely uniform and randomness gives crowd-cover."
              },
              {
                "command": "sudo routing-switch providers connect-fastest vpngate --country jp --force",
                "description": "Pick the fastest profile and connect (lowest measured latency if you've benchmarked; otherwise highest speed_bps)",
                "expectedOutput": "Successfully connected to OpenVPN server",
                "notes": "Prefers latency_ms from `providers benchmark`. Without a benchmark, falls back to provider-reported speed_bps then lowest load."
              },
              {
                "command": "sudo routing-switch providers benchmark vpngate --country jp --top 20 --concurrency 8 --timeout 3 --json",
                "description": "User-triggered latency probe: pings each cached server (ICMP, falling back to TCP-connect) and stores latency_ms. Manual only, same UX as ip-fetch's cache refresh.",
                "expectedOutput": "{\"command\":\"providers-benchmark\",\"data\":{\"provider_id\":\"vpngate\",\"tested\":20,\"succeeded\":18,\"failed\":2,\"elapsed_ms\":5200,\"timeout_secs\":3,\"concurrency\":8,\"fastest\":{\"profile_id\":\"1_219-100-37-203\",\"latency_ms\":42}},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}",
                "notes": "Never auto-runs. `connect-fastest` will prefer benchmarked profiles once latency is stored."
              },
              {
                "command": "sudo routing-switch providers clear-cache riseup --json",
                "description": "Drop the on-disk cache for one provider (index + configs/). Credentials are NOT touched.",
                "expectedOutput": "{\"command\":\"providers-clear-cache\",\"data\":{\"provider_id\":\"riseup\",\"removed\":true},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}",
                "notes": "Symmetric with `providers fetch`. Use --all to wipe every cached provider."
              },
              {
                "command": "sudo routing-switch providers clear-cache --all --json",
                "description": "Bulk wipe: remove every cached provider's index + configs in one shot",
                "expectedOutput": "{\"command\":\"providers-clear-cache\",\"data\":{\"scope\":\"all\",\"providers_cleared\":9},\"errors\":[],\"metadata\":{...},\"status\":\"success\",\"timestamp\":\"...\",\"version\":\"<VERSION>\",\"warnings\":[]}",
                "notes": "Used by the dashboard \"Clear all\" bulk action. Idempotent."
              }
            ],
            "id": "external_vpn_providers",
            "title": "EXTERNAL VPN PROVIDERS"
          }
        ],
        "description": "System-wide traffic routing through various proxy protocols",
        "name": "routing-switch",
        "quickReference": [
          "sudo routing-switch connect <PROTOCOL> - Connect (openvpn, wireguard, dante, tor, shadowsocks, v2ray, xray-vless, xray-vless-reality, xray-trojan, xray-vmess, mita, hysteria2)",
          "sudo routing-switch disconnect - Disconnect and restore normal routing",
          "sudo routing-switch status --detailed - Check connection status (sudo needed for accurate results)",
          "routing-switch list-protocols - Show available protocols with scores",
          "sudo routing-switch recover - Emergency: diagnose and restore routing when the network is stuck",
          "routing-switch providers <VERB> - External VPN providers, the largest surface here (20 verbs: list, fetch, list-profiles, get-profile, connect, connect-random, connect-fastest, benchmark, import-config, validate-config, credentials-set/list/delete, delete-custom-profile(s), resolve-country/ip/ips, refresh-catalog, clear-cache). Run `routing-switch providers --help`.",
          "connect tor is NOT standalone: a Kodachi tunnel must be up first, e.g. `sudo routing-switch connect wireguard && sudo routing-switch connect tor`",
          "Use --json / --json-pretty / --json-human for machine-readable output, --verbose for details",
          "AUTH (online-auth login) REQUIRED for: connect, auto-select, vpngate-connect, disconnect --clean-firewall, microsocks-enable/disable, showconfig/showconfigurl/showconfigqr, export-config, validate-qr, test-protocol, benchmark, and EVERY providers verb",
          "AUTH-FREE: status, dns-info, tor-dns-info, vps-info, check-prerequisites, list-protocols, plain disconnect, reset, cleanup, recover, microsocks-status, and VPNGate BROWSING (vpngate-fetch / vpngate-list / vpngate-export / vpngate-export-all). VPNGate servers are free, but `vpngate-connect` still needs authentication because it changes your routing."
        ]
      },
      "errors": [],
      "metadata": {
        "executionTime": 2,
        "hostname": "REDACTED-BUILD-HOST",
        "user": "REDACTED-BUILD-USER"
      },
      "status": "success",
      "timestamp": "2026-07-21T01:54:43.668827232Z",
      "version": "9.8.4 (build 320)",
      "warnings": []
    }
  }
}
