diff --git a/config/ns-support-tunnel.conf b/config/ns-support-tunnel.conf new file mode 100644 index 000000000..0a5e52f20 --- /dev/null +++ b/config/ns-support-tunnel.conf @@ -0,0 +1 @@ +CONFIG_PACKAGE_ns-support-tunnel=y diff --git a/packages/ns-support-tunnel/Makefile b/packages/ns-support-tunnel/Makefile new file mode 100644 index 000000000..9cd6ee9da --- /dev/null +++ b/packages/ns-support-tunnel/Makefile @@ -0,0 +1,51 @@ +# +# Copyright (C) 2026 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-only +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=ns-support-tunnel +PKG_VERSION:=0.0.1 +PKG_RELEASE:=1 + +PKG_BUILD_DIR:=$(BUILD_DIR)/ns-support-tunnel-$(PKG_VERSION) + +PKG_MAINTAINER:=Edoardo Spadoni +PKG_LICENSE:=GPL-3.0-only + +include $(INCLUDE_DIR)/package.mk + +define Package/ns-support-tunnel + SECTION:=base + CATEGORY:=NethSecurity + TITLE:=WebSocket-based remote support tunnel (coexists with don) + URL:=https://github.com/NethServer/nethsecurity/ + DEPENDS:=+jq +endef + +define Package/ns-support-tunnel/description + WebSocket-based remote support client using tunnel-client. + Coexists with the existing OpenVPN-based don support system. +endef + +# No compilation needed - binary is pre-built +define Build/Compile +endef + +define Package/ns-support-tunnel/install + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_DIR) $(1)/usr/share/my/users.d + $(INSTALL_DIR) $(1)/usr/share/my/diagnostics.d + $(INSTALL_BIN) ./files/support-tunnel $(1)/usr/sbin + $(INSTALL_BIN) ./files/tunnel-client $(1)/usr/sbin + $(INSTALL_BIN) ./files/ns.support-tunnel $(1)/usr/libexec/rpcd + $(INSTALL_DATA) ./files/ns.support-tunnel.json $(1)/usr/share/rpcd/acl.d + $(INSTALL_BIN) ./files/health $(1)/usr/share/my/diagnostics.d + $(INSTALL_CONF) ./files/20_ns_support_tunnel $(1)/etc/uci-defaults +endef + +$(eval $(call BuildPackage,ns-support-tunnel)) diff --git a/packages/ns-support-tunnel/files/20_ns_support_tunnel b/packages/ns-support-tunnel/files/20_ns_support_tunnel new file mode 100644 index 000000000..76d896977 --- /dev/null +++ b/packages/ns-support-tunnel/files/20_ns_support_tunnel @@ -0,0 +1,10 @@ +[ "$(uci -q get support-tunnel.config.url)" = "" ] || exit 0 + +uci -q import support-tunnel << EOI +config main 'config' + option url 'wss://support.nethesis.it/api/tunnel' + option system_key '' + option system_secret '' + option exclude_patterns '' + option tls_insecure '0' +EOI diff --git a/packages/ns-support-tunnel/files/health b/packages/ns-support-tunnel/files/health new file mode 100644 index 000000000..5fa9e1122 --- /dev/null +++ b/packages/ns-support-tunnel/files/health @@ -0,0 +1,152 @@ +#!/bin/sh + +# +# Copyright (C) 2026 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-only +# + +# +# diagnostics.d plugin for NethSecurity health +# +# Checks core services, WAN connectivity, firewall, disk/overlay usage, +# and DNS resolution. Outputs JSON PluginResult. +# +# Exit codes: 0=ok, 1=warning, 2=critical +# + +# Use /usr/bin/jsonfilter if jq is not available +JQ=$(command -v jq 2>/dev/null || echo "") + +overall="ok" + +worst() { + local a="$1" b="$2" + case "$a" in + critical) echo "critical" ;; + warning) [ "$b" = "critical" ] && echo "critical" || echo "warning" ;; + ok) echo "$b" ;; + *) echo "$b" ;; + esac +} + +checks="[" +first=true + +add_check() { + local name="$1" status="$2" value="$3" details="$4" + [ "$first" = true ] && first=false || checks="$checks," + if [ -n "$details" ]; then + checks="$checks{\"name\":\"$name\",\"status\":\"$status\",\"value\":\"$value\",\"details\":$details}" + else + checks="$checks{\"name\":\"$name\",\"status\":\"$status\",\"value\":\"$value\"}" + fi + overall=$(worst "$overall" "$status") +} + +# 1. Core services +services="firewall dnsmasq dropbear" +svc_details="[" +svc_first=true +svc_ok=0 +svc_total=0 +for svc in $services; do + svc_total=$((svc_total + 1)) + if /etc/init.d/$svc enabled 2>/dev/null; then + state=$(/etc/init.d/$svc running 2>/dev/null && echo "running" || echo "stopped") + else + state="disabled" + fi + s="ok" + [ "$state" != "running" ] && s="critical" + [ "$state" = "disabled" ] && s="ok" # disabled is fine + [ "$state" = "running" ] && svc_ok=$((svc_ok + 1)) + [ "$svc_first" = true ] && svc_first=false || svc_details="$svc_details," + svc_details="$svc_details{\"name\":\"$svc\",\"state\":\"$state\"}" +done +svc_details="$svc_details]" +svc_status="ok" +[ "$svc_ok" -lt "$svc_total" ] && svc_status="warning" +add_check "core_services" "$svc_status" "$svc_ok/$svc_total running" "$svc_details" + +# 2. WAN connectivity +wan_if=$(uci -q get network.wan.device || uci -q get network.wan.ifname) +wan_proto=$(uci -q get network.wan.proto) +wan_ip="" +if [ -n "$wan_if" ]; then + wan_ip=$(ip -4 addr show dev "$wan_if" 2>/dev/null | grep -oP 'inet \K[0-9.]+' | head -1) +fi + +if [ -n "$wan_ip" ]; then + wan_status="ok" + wan_value="$wan_ip" +else + wan_status="warning" + wan_value="no IP" +fi +add_check "wan" "$wan_status" "$wan_value" "{\"interface\":\"$wan_if\",\"proto\":\"$wan_proto\",\"ip\":\"$wan_ip\"}" + +# 3. DNS resolution +dns_result=$(nslookup nethesis.it 127.0.0.1 2>&1 | grep -c "Address.*[0-9]") +if [ "$dns_result" -gt 1 ]; then + add_check "dns" "ok" "resolving" +else + add_check "dns" "warning" "failing" + overall=$(worst "$overall" "warning") +fi + +# 4. Overlay disk usage +overlay_used=$(df /overlay 2>/dev/null | awk 'NR==2{print $5}' | tr -d '%') +if [ -n "$overlay_used" ]; then + overlay_total=$(df /overlay 2>/dev/null | awk 'NR==2{print $2}') + overlay_avail=$(df /overlay 2>/dev/null | awk 'NR==2{print $4}') + if [ "$overlay_used" -gt 95 ]; then + disk_status="critical" + elif [ "$overlay_used" -gt 85 ]; then + disk_status="warning" + else + disk_status="ok" + fi + add_check "overlay_usage" "$disk_status" "${overlay_used}%" "{\"total_kb\":$overlay_total,\"available_kb\":$overlay_avail,\"used_pct\":$overlay_used}" +fi + +# 5. Firewall status +fw_status=$(nft list tables 2>&1 | grep -c "table") +if [ "$fw_status" -gt 0 ]; then + # Count rules + rule_count=$(nft list ruleset 2>/dev/null | grep -c "^ ") + add_check "firewall" "ok" "$fw_status tables" "{\"tables\":$fw_status,\"rules\":$rule_count}" +else + add_check "firewall" "critical" "no tables loaded" +fi + +# 6. Connected clients (DHCP leases) +lease_count=0 +if [ -f /tmp/dhcp.leases ]; then + lease_count=$(wc -l < /tmp/dhcp.leases) +fi +add_check "dhcp_leases" "ok" "$lease_count" + +# 7. Uptime +uptime_sec=$(cat /proc/uptime | awk '{printf "%d", $1}') +days=$((uptime_sec / 86400)) +hours=$(( (uptime_sec % 86400) / 3600 )) +mins=$(( (uptime_sec % 3600) / 60 )) +add_check "uptime" "ok" "${days}d ${hours}h ${mins}m" "{\"days\":$days,\"hours\":$hours,\"minutes\":$mins,\"total_seconds\":$uptime_sec}" + +checks="$checks]" + +# Build summary +ok_count=$(echo "$checks" | grep -o '"status":"ok"' | wc -l) +warn_count=$(echo "$checks" | grep -o '"status":"warning"' | wc -l) +crit_count=$(echo "$checks" | grep -o '"status":"critical"' | wc -l) + +cat << EOF +{"id":"health","name":"NethSecurity Health","status":"$overall","summary":"$ok_count ok, $warn_count warning, $crit_count critical","checks":$checks} +EOF + +# Exit code +case "$overall" in + critical) exit 2 ;; + warning) exit 1 ;; + *) exit 0 ;; +esac diff --git a/packages/ns-support-tunnel/files/ns.support-tunnel b/packages/ns-support-tunnel/files/ns.support-tunnel new file mode 100755 index 000000000..25700a36b --- /dev/null +++ b/packages/ns-support-tunnel/files/ns.support-tunnel @@ -0,0 +1,49 @@ +#!/usr/bin/python3 + +# +# Copyright (C) 2026 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-only +# + +# Manage support-tunnel helpdesk session + +import sys +import json +import subprocess +from nethsec import utils + +def start(): + try: + p = subprocess.run(["/usr/sbin/support-tunnel", "start", "-j"], check=True, capture_output=True, text=True) + return json.loads(p.stdout) + except: + return utils.generic_error("support_tunnel_start_failed") + +def status(): + try: + p = subprocess.run(["/usr/sbin/support-tunnel", "status", "-j"], check=True, capture_output=True, text=True) + return json.loads(p.stdout) + except: + return {"active": False} + +def stop(): + try: + p = subprocess.run(["/usr/sbin/support-tunnel", "stop", "-j"], check=True, capture_output=True, text=True) + return json.loads(p.stdout) + except: + return utils.generic_error("support_tunnel_stop_failed") + +cmd = sys.argv[1] + +if cmd == 'list': + print(json.dumps({"start": {}, "stop": {}, "status":{}})) +else: + action = sys.argv[2] + if action == "start": + ret = start() + elif action == "status": + ret = status() + elif action == "stop": + ret = stop() + + print(json.dumps(ret)) diff --git a/packages/ns-support-tunnel/files/ns.support-tunnel.json b/packages/ns-support-tunnel/files/ns.support-tunnel.json new file mode 100644 index 000000000..3dc191878 --- /dev/null +++ b/packages/ns-support-tunnel/files/ns.support-tunnel.json @@ -0,0 +1,13 @@ +{ + "support-tunnel-manager": { + "description": "support-tunnel session manager", + "write": {}, + "read": { + "ubus": { + "ns.support-tunnel": [ + "*" + ] + } + } + } +} diff --git a/packages/ns-support-tunnel/files/support-tunnel b/packages/ns-support-tunnel/files/support-tunnel new file mode 100755 index 000000000..720886aee --- /dev/null +++ b/packages/ns-support-tunnel/files/support-tunnel @@ -0,0 +1,173 @@ +#!/bin/bash + +# +# Copyright (C) 2026 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-only +# + +# +# WebSocket-based remote support tunnel client. +# Coexists with the OpenVPN-based "don" support system. +# +# Usage: support-tunnel {start|stop|status} [-j] +# +# Configuration is read from UCI: +# uci set support-tunnel.config.url='wss://support.nethesis.it/api/tunnel' +# uci set support-tunnel.config.system_key='NETH-...' +# uci set support-tunnel.config.system_secret='...' +# uci commit support-tunnel +# + +base_dir=/var/run/support-tunnel +pid_file="$base_dir/tunnel-client.pid" +log_file=/var/log/support-tunnel.log +json_output=0 + +if [ "$2" = "-j" ]; then + json_output=1 +fi + +cleanup() { + if [ -f "$pid_file" ]; then + kill "$(cat $pid_file)" 2>/dev/null + # Wait for graceful shutdown (tunnel-client cleans up users on SIGTERM) + local i=0 + while [ -f "$pid_file" ] && [ $i -lt 15 ]; do + sleep 1 + i=$((i+1)) + done + # Force kill if still running + [ -f "$pid_file" ] && kill -9 "$(cat $pid_file)" 2>/dev/null + fi + rm -rf "$base_dir" +} + +start_tunnel() { + local url=$(uci -q get support-tunnel.config.url) + local system_key=$(uci -q get support-tunnel.config.system_key) + local system_secret=$(uci -q get support-tunnel.config.system_secret) + local exclude=$(uci -q get support-tunnel.config.exclude_patterns) + local tls_insecure=$(uci -q get support-tunnel.config.tls_insecure) + + # Fallback: read key from ns-plug if not set + if [ -z "$system_key" ]; then + system_key=$(uci -q get ns-plug.config.system_id) + fi + + if [ -z "$system_key" ] || [ -z "$system_secret" ]; then + if [ "$json_output" = 1 ]; then + echo '{"error": "missing credentials"}' + else + echo "ERROR: missing credentials" + echo " uci set support-tunnel.config.system_key='NETH-...'" + echo " uci set support-tunnel.config.system_secret='...'" + echo " uci commit support-tunnel" + fi + exit 2 + fi + + [ -z "$url" ] && url="wss://support.nethesis.it/api/tunnel" + + mkdir -p "$base_dir" + + # Start tunnel-client in background + /usr/sbin/tunnel-client \ + --url "$url" \ + --key "$system_key" \ + --secret "$system_secret" \ + --exclude "${exclude}" \ + --tls-insecure="${tls_insecure:-false}" \ + --users-dir /usr/share/my/users.d \ + --diagnostics-dir /usr/share/my/diagnostics.d \ + --users-state-file "$base_dir/users-state.json" \ + >> "$log_file" 2>&1 & + + local tunnel_pid=$! + echo $tunnel_pid > "$pid_file" + + # Wait for WebSocket connection (max 15 seconds) + local connected=false + local i=0 + while [ $i -lt 15 ]; do + if ! kill -0 $tunnel_pid 2>/dev/null; then + if [ "$json_output" = 1 ]; then + echo '{"error": "tunnel-client exited unexpectedly"}' + else + echo "ERROR: tunnel-client exited unexpectedly" + fi + rm -f "$pid_file" + exit 4 + fi + if grep -q "WebSocket connected" "$log_file" 2>/dev/null; then + connected=true + break + fi + sleep 1 + i=$((i+1)) + done + + if [ "$json_output" = 1 ]; then + jq -c -n \ + --arg url "$url" \ + --arg system_key "$system_key" \ + --argjson connected "$connected" \ + --argjson pid "$tunnel_pid" \ + '{url: $url, system_key: $system_key, connected: $connected, pid: $pid}' + else + echo "Tunnel started (PID: $tunnel_pid)" + echo "URL: $url" + echo "System key: $system_key" + echo "Connected: $connected" + fi +} + +show_status() { + if [ -f "$pid_file" ] && kill -0 "$(cat $pid_file)" 2>/dev/null; then + local pid=$(cat $pid_file) + local url=$(uci -q get support-tunnel.config.url) + local system_key=$(uci -q get support-tunnel.config.system_key) + [ -z "$system_key" ] && system_key=$(uci -q get ns-plug.config.system_id) + [ -z "$url" ] && url="wss://support.nethesis.it/api/tunnel" + + if [ "$json_output" = 1 ]; then + jq -c -n \ + --arg url "$url" \ + --arg system_key "$system_key" \ + --argjson pid "$pid" \ + '{active: true, url: $url, system_key: $system_key, pid: $pid}' + else + echo "Tunnel is running (PID: $pid)" + echo "URL: $url" + echo "System key: $system_key" + fi + exit 0 + else + if [ "$json_output" = 1 ]; then + echo '{"active": false}' + else + echo "Tunnel is not running" + fi + exit 1 + fi +} + +case "$1" in +start) + cleanup 2>/dev/null + > "$log_file" + start_tunnel + ;; +stop) + cleanup + if [ "$json_output" = 1 ]; then + echo '{"result": "success"}' + fi + ;; +status) + show_status + ;; +*) + echo "Usage: $0 {start|stop|status} [-j]" + exit 1 + ;; +esac diff --git a/packages/ns-support-tunnel/files/tunnel-client b/packages/ns-support-tunnel/files/tunnel-client new file mode 100755 index 000000000..51d009c8d Binary files /dev/null and b/packages/ns-support-tunnel/files/tunnel-client differ