#!/usr/bin/env bash # VPN status for waybar - outputs JSON get_status() { local wg_status="" local nb_status="" local ts_status="" local text="VPN off" local tooltip="No VPN active" local class="disconnected" local has_tunnel=false # true if WG, NetBird, or TS with exit node # Check NetBird if command -v netbird &>/dev/null; then nb_json=$(netbird status --json 2>/dev/null) if [[ -n "$nb_json" ]]; then nb_mgmt=$(echo "$nb_json" | jq -r '.management.connected // false') if [[ "$nb_mgmt" == "true" ]]; then nb_ip=$(echo "$nb_json" | jq -r '.netbirdIp // empty' | cut -d'/' -f1) nb_fqdn=$(echo "$nb_json" | jq -r '.fqdn // empty') nb_connected=$(echo "$nb_json" | jq -r '.peers.connected // 0') nb_total=$(echo "$nb_json" | jq -r '.peers.total // 0') # Extract network name from fqdn (e.g., box-128-45.lumia.overlay -> lumia) nb_net=$(echo "$nb_fqdn" | awk -F'.' '{print $(NF-1)}') nb_status="NB:${nb_net}(${nb_connected}/${nb_total})" tooltip="NetBird: ${nb_fqdn}\nIP: ${nb_ip}\nPeers: ${nb_connected}/${nb_total}" # NetBird mesh connection alone doesn't route all traffic like a VPN fi fi fi # Check WireGuard interfaces (excluding NetBird's wt0) wg_ifaces=$(ip link show type wireguard 2>/dev/null | grep -oP '^\d+: \K[^:]+' | grep -v '^wt') if [[ -n "$wg_ifaces" ]]; then wg_name=$(echo "$wg_ifaces" | head -1) wg_status="WG:$wg_name" [[ -z "$tooltip" || "$tooltip" == "No VPN active" ]] && tooltip="" [[ -n "$tooltip" ]] && tooltip+="\n" tooltip+="WireGuard: $wg_name" has_tunnel=true fi # Check Tailscale if command -v tailscale &>/dev/null && tailscale status &>/dev/null; then ts_json=$(tailscale status --json 2>/dev/null) if [[ -n "$ts_json" ]]; then ts_state=$(echo "$ts_json" | jq -r '.BackendState // empty') if [[ "$ts_state" == "Running" ]]; then # Get tailnet name (domain) tailnet=$(echo "$ts_json" | jq -r '.CurrentTailnet.Name // empty') tailnet_short="${tailnet%.ts.net}" tailnet_short="${tailnet_short%.tail*}" # Check for exit node exit_node_id=$(echo "$ts_json" | jq -r '.ExitNodeStatus.ID // empty') exit_online=$(echo "$ts_json" | jq -r '.ExitNodeStatus.Online // false') if [[ -n "$exit_node_id" && "$exit_online" == "true" ]]; then exit_host=$(echo "$ts_json" | jq -r ' .Peer | to_entries[] | select(.value.ExitNode == true) | .value.HostName // "exit" ' 2>/dev/null) [[ -z "$exit_host" || "$exit_host" == "null" ]] && exit_host="exit" ts_status="TS:${tailnet_short}→${exit_host}" [[ -n "$tooltip" ]] && tooltip+="\n" tooltip+="Tailscale: ${tailnet}\nExit: ${exit_host}" has_tunnel=true else ts_status="TS:${tailnet_short}" [[ -n "$tooltip" ]] && tooltip+="\n" tooltip+="Tailscale: ${tailnet} (no exit)" # No exit node = no tunnel, don't set has_tunnel fi fi fi fi # Build final output local parts=() [[ -n "$wg_status" ]] && parts+=("$wg_status") [[ -n "$nb_status" ]] && parts+=("$nb_status") [[ -n "$ts_status" ]] && parts+=("$ts_status") if [[ ${#parts[@]} -gt 0 ]]; then text=$(IFS=' | '; echo "${parts[*]}") fi # Only green if actual tunnel (WG, NetBird, or TS+exit) if [[ "$has_tunnel" == true ]]; then class="connected" elif [[ ${#parts[@]} -gt 0 ]]; then class="connected-no-tunnel" fi printf '{"text": "%s", "tooltip": "%s", "class": "%s"}\n' "$text" "$tooltip" "$class" } get_status