feat: Implement core features for network-modes and system-hub

Network Modes (+536 lines RPCD):
- Enhanced router.js with improved proxy configuration
- Expanded RPCD backend with new methods
- Updated README with feature documentation

System Hub (+511 lines RPCD, +127 lines API):
- Implemented diagnostics collection features
- Enhanced remote management interface
- Expanded RPCD backend with new RPC methods
- Added new API methods for diagnostics and remote access

Total changes: +1,392 additions, -126 deletions

Preparation for v0.3.6 release with real feature implementations
from CODEX roadmaps.
This commit is contained in:
CyberMind-FR 2025-12-28 15:34:23 +01:00
parent 78d84ec74f
commit 4406825611
8 changed files with 1389 additions and 123 deletions

View File

@ -144,7 +144,9 @@
"Bash(for module in luci-app-ksm-manager luci-app-media-flow luci-app-netdata-dashboard luci-app-netifyd-dashboard luci-app-network-modes luci-app-secubox luci-app-system-hub luci-app-traffic-shaper luci-app-vhost-manager luci-app-wireguard-dashboard)",
"Bash(do echo \"=== $module ===\" find \"$module/htdocs/luci-static/resources/view\" -name \"*.js\")",
"Bash(gh run view:*)",
"Bash(/tmp/deploy-system-hub-overview-fix.sh)"
"Bash(/tmp/deploy-system-hub-overview-fix.sh)",
"Bash(./secubox-tools/deploy-network-modes.sh:*)",
"Bash(/tmp/generalize-makefile-filemodes.sh)"
]
}
}

View File

@ -19,6 +19,7 @@ Configure your OpenWrt router for different network operation modes with a moder
- **WireGuard automation:** generate key pairs, deploy `wg0` interfaces, and push MTU/MSS/BBR optimizations directly from the Relay panel.
- **Optimization RPCs:** new backend methods expose MTU clamping, TCP BBR, and WireGuard deployment to both UI and automation agents.
- **UI action buttons:** Relay mode now includes one-click buttons for key generation, interface deployment, and optimization runs.
- **Integrated proxies:** router mode now auto-configures Squid/TinyProxy/Privoxy, transparent HTTP redirection, DNS-over-HTTPS, and nginx reverse proxy vhosts with optional Lets Encrypt certificates.
### 🔍 Sniffer Bridge Mode (Inline / Passthrough)
Transparent Ethernet bridge without IP address for in-line traffic analysis. All traffic passes through the device.
@ -60,6 +61,18 @@ Internet Router (Gateway)
- ✅ Can perform traffic shaping
- ⚠️ Single point of failure (if device fails, network is down)
**Proxy / DoH Requirements (Router Mode):**
| Capability | Packages |
|------------|----------|
| Caching proxy | `squid` or `tinyproxy` or `privoxy` |
| Transparent proxy redirect | (iptables built-in) |
| DNS over HTTPS | `https-dns-proxy`, `ca-certificates` |
| HTTPS reverse proxy | `nginx` (HAProxy/Caddy support planned) |
| Lets Encrypt automation | `acme`, `acme-dnsapi`, `openssl-util` |
Install these packages before enabling the associated toggles in the Router panel so the automation can write configs and restart services successfully.
---
### 👁️ Sniffer Passive Mode (Out-of-band / Monitor Only)

View File

@ -19,6 +19,8 @@ return view.extend({
var fwConfig = config.firewall || {};
var proxyConfig = config.proxy || {};
var frontendConfig = config.https_frontend || {};
var proxyStatus = proxyConfig.enabled ? (proxyConfig.type || 'squid') : 'disabled';
var dohStatus = proxyConfig.dns_over_https ? _('Enabled') : _('Disabled');
var vhosts = config.virtual_hosts || [];
var view = E('div', { 'class': 'network-modes-dashboard' }, [
@ -48,8 +50,15 @@ return view.extend({
E('span', { 'class': 'nm-card-title-icon' }, '🌍'),
'WAN Configuration'
])
]),
E('div', { 'class': 'nm-card-body' }, [
]),
E('div', { 'class': 'nm-card-body' }, [
E('div', { 'class': 'nm-alert nm-alert-info', 'style': 'margin-bottom: 12px;' }, [
E('span', { 'class': 'nm-alert-icon' }, ''),
E('div', {}, [
E('div', { 'class': 'nm-alert-title' }, _('Proxy status')),
E('div', { 'class': 'nm-alert-text' }, _('Type: ') + proxyStatus + ' • DoH: ' + dohStatus)
])
]),
E('div', { 'class': 'nm-form-group' }, [
E('label', { 'class': 'nm-form-label' }, 'WAN Interface'),
E('input', {
@ -226,6 +235,13 @@ return view.extend({
E('div', { 'class': 'nm-card-badge' }, vhosts.length + ' virtual hosts')
]),
E('div', { 'class': 'nm-card-body' }, [
E('div', { 'class': 'nm-alert nm-alert-info', 'style': 'margin-bottom: 12px;' }, [
E('span', { 'class': 'nm-alert-icon' }, frontendConfig.enabled ? '🟢' : '⚪'),
E('div', {}, [
E('div', { 'class': 'nm-alert-title' }, _('Frontend status: ') + (frontendConfig.enabled ? _('Enabled') : _('Disabled'))),
E('div', { 'class': 'nm-alert-text' }, _('Type: ') + (frontendConfig.type || 'nginx') + ' • ' + _('Let\'s Encrypt: ') + (frontendConfig.letsencrypt ? _('Enabled') : _('Disabled')))
])
]),
E('div', { 'class': 'nm-toggle' }, [
E('div', { 'class': 'nm-toggle-info' }, [
E('span', { 'class': 'nm-toggle-icon' }, '🌐'),

View File

@ -8,6 +8,7 @@
CONFIG_FILE="/etc/config/network-modes"
BACKUP_DIR="/etc/network-modes-backup"
PCAP_DIR="/var/log/pcap"
# Get current status
get_status() {
@ -382,42 +383,49 @@ apply_mode() {
# Apply network configuration based on mode
case "$pending_mode" in
router)
# Router mode: NAT, DHCP server, firewall
# WAN interface
uci delete network.wan 2>/dev/null
uci set network.wan=interface
uci set network.wan.proto='dhcp'
uci set network.wan.device='eth1'
# Router mode: NAT, DHCP server, firewall
# WAN interface
uci delete network.wan 2>/dev/null
uci set network.wan=interface
uci set network.wan.proto='dhcp'
uci set network.wan.device='eth1'
# LAN interface
uci set network.lan=interface
uci set network.lan.proto='static'
uci set network.lan.device='eth0'
uci set network.lan.ipaddr='192.168.1.1'
uci set network.lan.netmask='255.255.255.0'
# LAN interface
uci set network.lan=interface
uci set network.lan.proto='static'
uci set network.lan.device='eth0'
uci set network.lan.ipaddr='192.168.1.1'
uci set network.lan.netmask='255.255.255.0'
# DHCP server
uci set dhcp.lan=dhcp
uci set dhcp.lan.interface='lan'
uci set dhcp.lan.start='100'
uci set dhcp.lan.limit='150'
uci set dhcp.lan.leasetime='12h'
# DHCP server
uci set dhcp.lan=dhcp
uci set dhcp.lan.interface='lan'
uci set dhcp.lan.start='100'
uci set dhcp.lan.limit='150'
uci set dhcp.lan.leasetime='12h'
# Firewall zones
uci set firewall.@zone[0]=zone
uci set firewall.@zone[0].name='lan'
uci set firewall.@zone[0].input='ACCEPT'
uci set firewall.@zone[0].output='ACCEPT'
uci set firewall.@zone[0].forward='ACCEPT'
# Firewall zones
uci set firewall.@zone[0]=zone
uci set firewall.@zone[0].name='lan'
uci set firewall.@zone[0].input='ACCEPT'
uci set firewall.@zone[0].output='ACCEPT'
uci set firewall.@zone[0].forward='ACCEPT'
uci set firewall.@zone[1]=zone
uci set firewall.@zone[1].name='wan'
uci set firewall.@zone[1].input='REJECT'
uci set firewall.@zone[1].output='ACCEPT'
uci set firewall.@zone[1].forward='REJECT'
uci set firewall.@zone[1].masq='1'
uci set firewall.@zone[1].mtu_fix='1'
;;
uci set firewall.@zone[1]=zone
uci set firewall.@zone[1].name='wan'
uci set firewall.@zone[1].input='REJECT'
uci set firewall.@zone[1].output='ACCEPT'
uci set firewall.@zone[1].forward='REJECT'
uci set firewall.@zone[1].masq='1'
uci set firewall.@zone[1].mtu_fix='1'
generate_squid_config
generate_tinyproxy_config
generate_privoxy_config
configure_doh_proxy
apply_transparent_proxy_rules
deploy_nginx_vhosts
;;
accesspoint)
# Access Point mode: Bridge, no NAT, DHCP client
@ -437,6 +445,7 @@ apply_mode() {
uci set firewall.@zone[0].input='ACCEPT'
uci set firewall.@zone[0].forward='ACCEPT'
uci delete firewall.@zone[1] 2>/dev/null
apply_accesspoint_features
;;
relay)
@ -479,6 +488,12 @@ apply_mode() {
;;
esac
if [ "$pending_mode" = "sniffer" ]; then
start_packet_capture
else
stop_packet_capture
fi
# Commit all changes
uci commit network
uci commit dhcp
@ -633,12 +648,453 @@ update_settings() {
esac
uci commit network-modes
[ "$mode" = "accesspoint" ] && apply_accesspoint_features
if [ "$mode" = "sniffer" ]; then
local current_mode=$(uci -q get network-modes.config.current_mode || echo "")
[ "$current_mode" = "sniffer" ] && start_packet_capture
fi
json_add_boolean "success" 1
json_add_string "message" "Settings updated for $mode mode"
json_dump
}
# Squid proxy configuration
generate_squid_config() {
local proxy_type=$(uci -q get network-modes.router.proxy_type || echo "none")
local enabled=$(uci -q get network-modes.router.proxy_enabled || echo 0)
[ "$proxy_type" = "squid" ] || return 0
[ "$enabled" = "1" ] || return 0
if ! command -v squid >/dev/null 2>&1; then
logger -t network-modes "squid binary not found"
return 1
fi
local proxy_port=$(uci -q get network-modes.router.proxy_port || echo 3128)
local cache_size=$(uci -q get network-modes.router.proxy_cache_size || echo 256)
mkdir -p /etc/squid /var/spool/squid /var/log/squid
cat > /etc/squid/squid.conf <<EOF
# SecuBox Network Modes - Squid Configuration
# Generated: $(date)
http_port $proxy_port transparent
cache_dir ufs /var/spool/squid $cache_size 16 256
cache_mem 64 MB
maximum_object_size 4096 KB
minimum_object_size 0 KB
acl localnet src 192.168.0.0/16
acl localnet src 10.0.0.0/8
acl localnet src fc00::/7
acl SSL_ports port 443
acl Safe_ports port 80
acl Safe_ports port 443
acl CONNECT method CONNECT
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow localhost manager
http_access deny manager
http_access allow localnet
http_access allow localhost
http_access deny all
access_log /var/log/squid/access.log squid
cache_log /var/log/squid/cache.log
dns_nameservers 1.1.1.1 8.8.8.8
forwarded_for on
via on
EOF
squid -z >/dev/null 2>&1 || true
/etc/init.d/squid restart >/dev/null 2>&1 || true
}
generate_tinyproxy_config() {
local proxy_type=$(uci -q get network-modes.router.proxy_type || echo "none")
local enabled=$(uci -q get network-modes.router.proxy_enabled || echo 0)
[ "$proxy_type" = "tinyproxy" ] || return 0
[ "$enabled" = "1" ] || return 0
if ! command -v tinyproxy >/dev/null 2>&1; then
logger -t network-modes "tinyproxy binary not found"
return 1
fi
local proxy_port=$(uci -q get network-modes.router.proxy_port || echo 8888)
mkdir -p /etc/tinyproxy
cat > /etc/tinyproxy/tinyproxy.conf <<EOF
User nobody
Group nogroup
Port $proxy_port
Listen 0.0.0.0
Timeout 600
DefaultErrorFile "/usr/share/tinyproxy/default.html"
Logfile "/var/log/tinyproxy/tinyproxy.log"
StatFile "/usr/share/tinyproxy/stats.html"
MaxClients 100
MinSpareServers 5
MaxSpareServers 20
StartServers 10
Allow 192.168.0.0/16
Allow 10.0.0.0/8
Allow 172.16.0.0/12
ViaProxyName "SecuBox"
EOF
/etc/init.d/tinyproxy restart >/dev/null 2>&1 || true
}
generate_privoxy_config() {
local proxy_type=$(uci -q get network-modes.router.proxy_type || echo "none")
local enabled=$(uci -q get network-modes.router.proxy_enabled || echo 0)
[ "$proxy_type" = "privoxy" ] || return 0
[ "$enabled" = "1" ] || return 0
if ! command -v privoxy >/dev/null 2>&1; then
logger -t network-modes "privoxy binary not found"
return 1
fi
local proxy_port=$(uci -q get network-modes.router.proxy_port || echo 8118)
mkdir -p /etc/privoxy
cat > /etc/privoxy/config <<EOF
listen-address 0.0.0.0:$proxy_port
toggle 1
enable-remote-toggle 0
enable-remote-http-toggle 0
accept-intercepted-requests 1
forward-socks5 / 127.0.0.1:9050 .
logfile /var/log/privoxy/logfile
EOF
/etc/init.d/privoxy restart >/dev/null 2>&1 || true
}
apply_transparent_proxy_rules() {
local proxy_enabled=$(uci -q get network-modes.router.proxy_enabled || echo 0)
local proxy_port=$(uci -q get network-modes.router.proxy_port || echo 3128)
local transparent=$(uci -q get network-modes.router.proxy_transparent || echo 0)
uci -q delete firewall.proxy_redirect
if [ "$proxy_enabled" = "1" ] && [ "$transparent" = "1" ]; then
uci set firewall.proxy_redirect=redirect
uci set firewall.proxy_redirect.name='Transparent Web Proxy'
uci set firewall.proxy_redirect.src='lan'
uci set firewall.proxy_redirect.proto='tcp'
uci set firewall.proxy_redirect.src_dport='80'
uci set firewall.proxy_redirect.dest_port="$proxy_port"
uci set firewall.proxy_redirect.target='DNAT'
fi
uci commit firewall
/etc/init.d/firewall reload >/dev/null 2>&1 || true
}
configure_doh_proxy() {
local doh_enabled=$(uci -q get network-modes.router.dns_over_https || echo 0)
[ "$doh_enabled" = "1" ] || return 0
if ! command -v https_dns_proxy >/dev/null 2>&1; then
logger -t network-modes "https_dns_proxy not installed"
return 1
fi
local provider=$(uci -q get network-modes.router.doh_provider || echo "cloudflare")
local doh_url
case "$provider" in
cloudflare) doh_url="https://1.1.1.1/dns-query" ;;
google) doh_url="https://dns.google/dns-query" ;;
quad9) doh_url="https://dns.quad9.net/dns-query" ;;
*) doh_url="https://1.1.1.1/dns-query" ;;
esac
uci set https-dns-proxy.@https-dns-proxy[0]=https-dns-proxy
uci set https-dns-proxy.@https-dns-proxy[0].resolver_url="$doh_url"
uci set https-dns-proxy.@https-dns-proxy[0].listen_addr='127.0.0.1'
uci set https-dns-proxy.@https-dns-proxy[0].listen_port='5053'
uci commit https-dns-proxy
uci set dhcp.@dnsmasq[0].noresolv='1'
uci -q delete dhcp.@dnsmasq[0].server
uci add_list dhcp.@dnsmasq[0].server="127.0.0.1#5053"
uci commit dhcp
/etc/init.d/https-dns-proxy restart >/dev/null 2>&1 || true
/etc/init.d/dnsmasq restart >/dev/null 2>&1 || true
}
apply_accesspoint_features() {
local iface="wireless.@wifi-iface[0]"
uci -q show "$iface" >/dev/null 2>&1 || return 0
local changed=0
local ft_enabled=$(uci -q get network-modes.accesspoint.roaming_enabled || echo 0)
if [ "$ft_enabled" = "1" ]; then
local ssid=$(uci -q get $iface.ssid || echo "SecuBox")
local mobility_domain=$(echo -n "$ssid" | md5sum 2>/dev/null | cut -c1-4)
[ -z "$mobility_domain" ] && mobility_domain="a1b2"
uci set $iface.ieee80211r='1'
uci set $iface.mobility_domain="$mobility_domain"
uci set $iface.ft_over_ds='1'
uci set $iface.ft_psk_generate_local='1'
uci set $iface.reassociation_deadline='1000'
changed=1
else
uci set $iface.ieee80211r='0'
uci -q delete $iface.mobility_domain
fi
local rrm_enabled=$(uci -q get network-modes.accesspoint.rrm_enabled || echo 0)
if [ "$rrm_enabled" = "1" ]; then
uci set $iface.ieee80211k='1'
uci set $iface.rrm_neighbor_report='1'
uci set $iface.rrm_beacon_report='1'
changed=1
else
uci set $iface.ieee80211k='0'
uci -q delete $iface.rrm_neighbor_report
uci -q delete $iface.rrm_beacon_report
fi
local v_enabled=$(uci -q get network-modes.accesspoint.wnm_enabled || echo 0)
if [ "$v_enabled" = "1" ]; then
uci set $iface.ieee80211v='1'
uci set $iface.bss_transition='1'
uci set $iface.wnm_sleep_mode='1'
changed=1
else
uci set $iface.ieee80211v='0'
uci -q delete $iface.bss_transition
uci -q delete $iface.wnm_sleep_mode
fi
local band=$(uci -q get network-modes.accesspoint.band_steering || echo 0)
if [ "$band" = "1" ]; then
local radio_2g=$(uci show wireless 2>/dev/null | grep "band='2g'" | head -n1)
local radio_5g=$(uci show wireless 2>/dev/null | grep "band='5g'" | head -n1)
if [ -n "$radio_2g" ] && [ -n "$radio_5g" ]; then
uci set $iface.bss_load_update_period='60'
uci set $iface.chan_util_avg_period='600'
uci set $iface.disassoc_low_ack='1'
changed=1
else
logger -t network-modes "band steering requested but dual-band radios missing"
fi
else
uci -q delete $iface.bss_load_update_period
uci -q delete $iface.chan_util_avg_period
uci -q delete $iface.disassoc_low_ack
fi
if [ "$changed" = "1" ]; then
uci commit wireless
wifi reload >/dev/null 2>&1 || true
fi
}
start_packet_capture() {
local capture_enabled=$(uci -q get network-modes.sniffer.pcap_capture || echo 0)
if [ "$capture_enabled" != "1" ]; then
stop_packet_capture
return 0
fi
if ! command -v tcpdump >/dev/null 2>&1; then
logger -t network-modes "tcpdump not installed"
return 1
fi
local capture_interface=$(uci -q get network-modes.sniffer.capture_interface || echo "br-lan")
local capture_filter=$(uci -q get network-modes.sniffer.capture_filter || echo "")
local max_size=$(uci -q get network-modes.sniffer.pcap_max_size || echo "100")
local rotate_count=$(uci -q get network-modes.sniffer.pcap_rotate || echo "10")
mkdir -p "$PCAP_DIR"
stop_packet_capture
if [ -n "$capture_filter" ]; then
tcpdump -i "$capture_interface" -w "$PCAP_DIR/capture.pcap" -C "$max_size" -W "$rotate_count" -s 0 $capture_filter >/dev/null 2>&1 &
else
tcpdump -i "$capture_interface" -w "$PCAP_DIR/capture.pcap" -C "$max_size" -W "$rotate_count" -s 0 >/dev/null 2>&1 &
fi
local pid=$!
echo "$pid" > /var/run/tcpdump-sniffer.pid
}
stop_packet_capture() {
if [ -f /var/run/tcpdump-sniffer.pid ]; then
local pid=$(cat /var/run/tcpdump-sniffer.pid 2>/dev/null)
[ -n "$pid" ] && kill "$pid" 2>/dev/null || true
rm -f /var/run/tcpdump-sniffer.pid
fi
killall tcpdump >/dev/null 2>&1 || true
}
validate_pcap_filter() {
read input
json_load "$input"
json_get_var filter filter
if [ -z "$filter" ]; then
json_init
json_add_boolean "valid" 1
json_add_string "filter" "all"
json_dump
return
fi
if command -v tcpdump >/dev/null 2>&1 && tcpdump -i any -d "$filter" >/dev/null 2>&1; then
json_init
json_add_boolean "valid" 1
json_add_string "filter" "$filter"
json_dump
else
json_init
json_add_boolean "valid" 0
json_add_string "error" "Invalid BPF syntax"
json_dump
fi
}
cleanup_old_pcaps() {
local max_age=$(uci -q get network-modes.sniffer.pcap_retention_days || echo "7")
mkdir -p "$PCAP_DIR"
local deleted=0
find "$PCAP_DIR" -name "*.pcap*" -mtime +$max_age -type f 2>/dev/null | while read -r file; do
rm -f "$file"
deleted=$((deleted + 1))
done
local total_size=$(du -sm "$PCAP_DIR" 2>/dev/null | cut -f1 || echo "0")
json_init
json_add_boolean "success" 1
json_add_int "deleted" "$deleted"
json_add_int "total_size_mb" "$total_size"
json_dump
}
deploy_nginx_vhosts() {
local frontend_enabled=$(uci -q get network-modes.router.https_frontend || echo 0)
local frontend_type=$(uci -q get network-modes.router.frontend_type || echo nginx)
[ "$frontend_enabled" = "1" ] || return 0
[ "$frontend_type" = "nginx" ] || return 0
if ! command -v nginx >/dev/null 2>&1; then
logger -t network-modes "nginx not installed"
return 1
fi
rm -f /etc/nginx/conf.d/vhost-*.conf 2>/dev/null || true
mkdir -p /etc/nginx/conf.d /etc/acme
local idx=0
local deployed=0
while true; do
local domain=$(uci -q get network-modes.@vhost[$idx].domain)
[ -n "$domain" ] || break
local backend=$(uci -q get network-modes.@vhost[$idx].backend || echo "127.0.0.1")
local port=$(uci -q get network-modes.@vhost[$idx].port || echo 80)
local ssl=$(uci -q get network-modes.@vhost[$idx].ssl || echo 0)
cat > "/etc/nginx/conf.d/vhost-$idx.conf" <<EOF
server {
listen 80;
server_name $domain;
EOF
if [ "$ssl" = "1" ]; then
cat >> "/etc/nginx/conf.d/vhost-$idx.conf" <<EOF
return 301 https://\$server_name\$request_uri;
}
server {
listen 443 ssl http2;
server_name $domain;
ssl_certificate /etc/acme/$domain/fullchain.cer;
ssl_certificate_key /etc/acme/$domain/$domain.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://$backend:$port;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
EOF
else
cat >> "/etc/nginx/conf.d/vhost-$idx.conf" <<EOF
location / {
proxy_pass http://$backend:$port;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
EOF
fi
deployed=$((deployed + 1))
idx=$((idx + 1))
done
/etc/init.d/nginx reload >/dev/null 2>&1 || true
logger -t network-modes "Deployed $deployed nginx vhosts"
}
issue_letsencrypt_cert() {
local domain="$1"
[ -n "$domain" ] || return 1
local acme_email=$(uci -q get network-modes.router.acme_email)
[ -n "$acme_email" ] || acme_email="admin@$domain"
if ! command -v /usr/lib/acme/acme.sh >/dev/null 2>&1; then
opkg update >/dev/null 2>&1 || true
opkg install acme acme-dnsapi >/dev/null 2>&1 || true
fi
/usr/lib/acme/acme.sh --issue -d "$domain" --webroot /www --accountemail "$acme_email" --force >/tmp/acme-issue.log 2>&1
if [ $? -eq 0 ]; then
mkdir -p /etc/acme/$domain
/usr/lib/acme/acme.sh --install-cert \
-d "$domain" \
--cert-file /etc/acme/$domain/cert.cer \
--key-file /etc/acme/$domain/$domain.key \
--fullchain-file /etc/acme/$domain/fullchain.cer \
--reloadcmd "/etc/init.d/nginx reload" >/dev/null 2>&1
return 0
else
logger -t network-modes "ACME issue failed for $domain"
return 1
fi
}
validate_ssl_cert() {
local domain="$1"
local cert_file="/etc/acme/$domain/fullchain.cer"
[ -f "$cert_file" ] || return 1
command -v openssl >/dev/null 2>&1 || return 1
local expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" | cut -d= -f2)
local expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null || echo 0)
local now_epoch=$(date +%s)
[ "$expiry_epoch" -gt 0 ] || return 1
local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
logger -t network-modes "Cert $domain valid $days_left days"
[ "$days_left" -gt 0 ]
}
# Generate WireGuard key pair and store in UCI
generate_wireguard_keys() {
json_init
@ -1250,7 +1706,7 @@ rollback() {
# Main dispatcher
case "$1" in
list)
echo '{"status":{},"modes":{},"get_current_mode":{},"get_available_modes":{},"set_mode":{"mode":"str"},"preview_changes":{},"apply_mode":{},"confirm_mode":{},"rollback":{},"sniffer_config":{},"ap_config":{},"relay_config":{},"router_config":{},"update_settings":{"mode":"str"},"generate_wireguard_keys":{},"apply_wireguard_config":{},"apply_mtu_clamping":{},"enable_tcp_bbr":{},"add_vhost":{"domain":"str","backend":"str","port":"int","ssl":"bool"},"generate_config":{"mode":"str"}}'
echo '{"status":{},"modes":{},"get_current_mode":{},"get_available_modes":{},"set_mode":{"mode":"str"},"preview_changes":{},"apply_mode":{},"confirm_mode":{},"rollback":{},"sniffer_config":{},"ap_config":{},"relay_config":{},"router_config":{},"update_settings":{"mode":"str"},"generate_wireguard_keys":{},"apply_wireguard_config":{},"apply_mtu_clamping":{},"enable_tcp_bbr":{},"add_vhost":{"domain":"str","backend":"str","port":"int","ssl":"bool"},"generate_config":{"mode":"str"},"validate_pcap_filter":{"filter":"str"},"cleanup_old_pcaps":{}}'
;;
call)
case "$2" in
@ -1326,6 +1782,12 @@ case "$1" in
generate_config)
generate_config
;;
validate_pcap_filter)
validate_pcap_filter
;;
cleanup_old_pcaps)
cleanup_old_pcaps
;;
*)
echo '{"error": "Unknown method"}'
;;

View File

@ -101,6 +101,86 @@ var callGetComponentsByCategory = rpc.declare({
expect: {}
});
var callCollectDiagnostics = rpc.declare({
object: 'luci.system-hub',
method: 'collect_diagnostics',
params: ['include_logs', 'include_config', 'include_network', 'anonymize'],
expect: {}
});
var callListDiagnostics = rpc.declare({
object: 'luci.system-hub',
method: 'list_diagnostics',
expect: {}
});
var callDownloadDiagnostic = rpc.declare({
object: 'luci.system-hub',
method: 'download_diagnostic',
params: ['name'],
expect: {}
});
var callDeleteDiagnostic = rpc.declare({
object: 'luci.system-hub',
method: 'delete_diagnostic',
params: ['name'],
expect: {}
});
var callRunDiagnosticTest = rpc.declare({
object: 'luci.system-hub',
method: 'run_diagnostic_test',
params: ['test'],
expect: {}
});
var callUploadDiagnostics = rpc.declare({
object: 'luci.system-hub',
method: 'upload_diagnostics',
params: ['name'],
expect: {}
});
var callRemoteStatus = rpc.declare({
object: 'luci.system-hub',
method: 'remote_status',
expect: {}
});
var callRemoteInstall = rpc.declare({
object: 'luci.system-hub',
method: 'remote_install',
expect: {}
});
var callRemoteConfigure = rpc.declare({
object: 'luci.system-hub',
method: 'remote_configure',
params: ['relay_server', 'relay_key', 'rustdesk_enabled'],
expect: {}
});
var callRemoteGetCredentials = rpc.declare({
object: 'luci.system-hub',
method: 'remote_get_credentials',
expect: {}
});
var callRemoteServiceAction = rpc.declare({
object: 'luci.system-hub',
method: 'remote_service_action',
params: ['action'],
expect: {}
});
var callRemoteSaveSettings = rpc.declare({
object: 'luci.system-hub',
method: 'remote_save_settings',
params: ['allow_unattended', 'require_approval', 'notify_on_connect'],
expect: {}
});
return baseclass.extend({
// RPC methods - exposed via ubus
getStatus: callStatus,
@ -116,5 +196,50 @@ return baseclass.extend({
reboot: callReboot,
getStorage: callGetStorage,
getSettings: callGetSettings,
saveSettings: callSaveSettings
saveSettings: callSaveSettings,
collectDiagnostics: function(includeLogs, includeConfig, includeNetwork, anonymize) {
return callCollectDiagnostics({
include_logs: includeLogs ? 1 : 0,
include_config: includeConfig ? 1 : 0,
include_network: includeNetwork ? 1 : 0,
anonymize: anonymize ? 1 : 0
});
},
listDiagnostics: callListDiagnostics,
downloadDiagnostic: function(name) {
return callDownloadDiagnostic({ name: name });
},
deleteDiagnostic: function(name) {
return callDeleteDiagnostic({ name: name });
},
runDiagnosticTest: function(test) {
return callRunDiagnosticTest({ test: test });
},
uploadDiagnostics: function(name) {
return callUploadDiagnostics({ name: name });
},
formatBytes: function(bytes) {
if (!bytes || bytes <= 0)
return '0 B';
var units = ['B', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + units[i];
},
remoteStatus: callRemoteStatus,
remoteInstall: callRemoteInstall,
remoteConfigure: function(data) {
return callRemoteConfigure(data);
},
remoteCredentials: callRemoteGetCredentials,
remoteServiceAction: function(action) {
return callRemoteServiceAction({ action: action });
},
remoteSaveSettings: function(data) {
return callRemoteSaveSettings(data);
}
});

View File

@ -7,8 +7,13 @@
var api = L.require('system-hub.api');
return view.extend({
render: function() {
var self = this;
load: function() {
return api.listDiagnostics();
},
render: function(data) {
this.currentArchives = (data && data.archives) || [];
var archives = this.currentArchives;
var view = E('div', { 'class': 'system-hub-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
@ -59,11 +64,7 @@ return view.extend({
E('div', { 'class': 'sh-card-header' }, [
E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '📁'), 'Archives Récentes' ])
]),
E('div', { 'class': 'sh-card-body', 'id': 'archives-list' }, [
this.renderArchiveItem('diagnostic_20241220_154500.tar.gz', '256 KB', '2024-12-20 15:45:00'),
this.renderArchiveItem('diagnostic_20241219_093000.tar.gz', '312 KB', '2024-12-19 09:30:00'),
this.renderArchiveItem('diagnostic_20241218_110000.tar.gz', '298 KB', '2024-12-18 11:00:00')
])
E('div', { 'class': 'sh-card-body', 'id': 'archives-list' }, this.renderArchiveList(archives))
]),
// Test Results
@ -113,7 +114,19 @@ return view.extend({
]);
},
renderArchiveItem: function(name, size, date) {
renderArchiveList: function(archives) {
if (!archives.length) {
return [
E('div', { 'style': 'text-align:center; color:#707080; padding:24px;' }, 'Aucune archive disponible')
];
}
return archives.map(this.renderArchiveItem, this);
},
renderArchiveItem: function(archive) {
var name = archive.name || '';
var size = api.formatBytes(archive.size || 0);
var date = archive.created_at || '';
return E('div', { 'style': 'display: flex; align-items: center; justify-content: space-between; padding: 12px; background: #1a1a24; border-radius: 8px; margin-bottom: 10px;' }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 12px;' }, [
E('span', { 'style': 'font-size: 20px;' }, '📦'),
@ -123,8 +136,16 @@ return view.extend({
])
]),
E('div', { 'style': 'display: flex; gap: 8px;' }, [
E('button', { 'class': 'sh-btn', 'style': 'padding: 6px 10px; font-size: 10px;' }, '📥 Télécharger'),
E('button', { 'class': 'sh-btn', 'style': 'padding: 6px 10px; font-size: 10px;' }, '☁️ Envoyer')
E('button', {
'class': 'sh-btn',
'style': 'padding: 6px 10px; font-size: 10px;',
'click': L.bind(this.downloadArchive, this, name)
}, '📥 Télécharger'),
E('button', {
'class': 'sh-btn',
'style': 'padding: 6px 10px; font-size: 10px; background:#321616;',
'click': L.bind(this.deleteArchive, this, name)
}, '🗑️ Supprimer')
])
]);
},
@ -140,49 +161,116 @@ return view.extend({
E('div', { 'class': 'spinning' })
]);
api.callCollectDiagnostics(includeLogs, includeConfig, includeNetwork, anonymize).then(function(result) {
api.collectDiagnostics(includeLogs, includeConfig, includeNetwork, anonymize).then(L.bind(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', {}, '✅ Archive créée: ' + result.file + ' (' + api.formatBytes(result.size) + ')'), 'success');
this.refreshArchives();
} else {
ui.addNotification(null, E('p', {}, '❌ Erreur lors de la collecte'), 'error');
}
}, this)).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
},
uploadDiagnostics: function() {
ui.addNotification(null, E('p', {}, '⚠️ Fonctionnalité non configurée. Configurez l\'URL d\'upload dans les paramètres.'), 'warning');
var archives = this.currentArchives || [];
if (!archives.length) {
ui.addNotification(null, E('p', {}, 'Aucune archive à envoyer'), 'warning');
return;
}
var latest = archives[0];
ui.showModal(_('Upload Support'), [
E('p', {}, 'Envoi de ' + latest.name + '…'),
E('div', { 'class': 'spinning' })
]);
api.uploadDiagnostics(latest.name).then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, '☁️ Archive envoyée au support (' + (result.status || 'OK') + ')'), 'info');
} else {
ui.addNotification(null, E('p', {}, '❌ Upload impossible: ' + ((result && result.error) || 'Erreur inconnue')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
},
runTest: function(type) {
var resultsDiv = document.getElementById('test-results');
resultsDiv.innerHTML = '<div style="text-align: center; padding: 20px;"><div class="spinning"></div><div style="margin-top: 12px;">Test en cours...</div></div>';
// Simulate test
setTimeout(function() {
var results = {
'connectivity': { status: 'ok', message: 'WAN connecté, DNS fonctionnel', details: 'Ping: 8.8.8.8 - 12ms' },
'dns': { status: 'ok', message: 'Résolution DNS OK', details: 'google.com → 142.250.185.78' },
'latency': { status: 'warning', message: 'Latence élevée', details: 'Google: 45ms (seuil: 30ms)' },
'disk': { status: 'ok', message: 'Disque OK', details: 'Lecture: 25 MB/s, Écriture: 18 MB/s' },
'firewall': { status: 'ok', message: '127 règles actives', details: 'INPUT: 23, FORWARD: 89, OUTPUT: 15' },
'wifi': { status: 'ok', message: '2 radios actives', details: '2.4GHz: 8 clients, 5GHz: 4 clients' }
};
var r = results[type] || { status: 'ok', message: 'Test complété', details: '' };
var color = r.status === 'ok' ? '#22c55e' : (r.status === 'warning' ? '#f59e0b' : '#ef4444');
var icon = r.status === 'ok' ? '✅' : (r.status === 'warning' ? '⚠️' : '❌');
api.runDiagnosticTest(type).then(function(result) {
var color = result.success ? '#22c55e' : '#ef4444';
var bg = result.success ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)';
var icon = result.success ? '✅' : '❌';
resultsDiv.innerHTML = '';
resultsDiv.appendChild(E('div', { 'style': 'padding: 20px; background: rgba(' + (r.status === 'ok' ? '34,197,94' : '245,158,11') + ', 0.1); border-radius: 10px; border-left: 3px solid ' + color }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 12px; margin-bottom: 8px;' }, [
E('span', { 'style': 'font-size: 24px;' }, icon),
E('span', { 'style': 'font-size: 16px; font-weight: 600;' }, r.message)
resultsDiv.appendChild(E('div', { 'style': 'padding: 18px; border-radius: 10px; border-left: 3px solid ' + color + '; background: ' + bg }, [
E('div', { 'style': 'display:flex; align-items:center; gap:10px;' }, [
E('span', { 'style': 'font-size:24px;' }, icon),
E('div', { 'style': 'font-weight:600;' }, (result.test || type) + ' - ' + (result.success ? 'Réussi' : 'Échec'))
]),
E('div', { 'style': 'font-size: 12px; color: #a0a0b0; margin-left: 36px;' }, r.details)
E('pre', { 'style': 'margin-top:12px; font-size:12px; white-space:pre-wrap;' }, result.output || '')
]));
}, 1500);
}).catch(function(err) {
resultsDiv.innerHTML = '';
resultsDiv.appendChild(E('div', { 'class': 'sh-alert error' }, [
E('div', { 'class': 'sh-alert-title' }, 'Erreur'),
E('div', {}, err.message || err)
]));
});
},
downloadArchive: function(name) {
ui.showModal(_('Téléchargement…'), [
E('p', {}, 'Préparation de ' + name)
]);
api.downloadDiagnostic(name).then(function(result) {
ui.hideModal();
if (!result.success || !result.data) {
ui.addNotification(null, E('p', {}, '❌ Téléchargement impossible'), 'error');
return;
}
var link = document.createElement('a');
link.href = 'data:application/gzip;base64,' + result.data;
link.download = result.name || name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
},
deleteArchive: function(name) {
if (!confirm(_('Supprimer ') + name + ' ?')) return;
api.deleteDiagnostic(name).then(L.bind(function(result) {
if (result.success) {
ui.addNotification(null, E('p', {}, '🗑️ Archive supprimée'), 'info');
this.refreshArchives();
} else {
ui.addNotification(null, E('p', {}, '❌ Suppression impossible'), 'error');
}
}, this));
},
refreshArchives: function() {
api.listDiagnostics().then(L.bind(function(data) {
this.currentArchives = data.archives || [];
var list = document.getElementById('archives-list');
if (!list) return;
list.innerHTML = '';
(this.renderArchiveList(this.currentArchives)).forEach(function(node) {
list.appendChild(node);
});
}, this));
},
handleSaveApply: null,

View File

@ -5,32 +5,13 @@
var api = L.require('system-hub.api');
// Stub: Get remote access config (planned feature)
function getRemoteConfig() {
return Promise.resolve({
rustdesk_enabled: false,
rustdesk_installed: false,
rustdesk_id: null,
allow_unattended: false,
require_approval: true,
notify_on_connect: true,
support: {
provider: 'CyberMind.fr',
email: 'support@cybermind.fr',
phone: '+33 1 23 45 67 89',
website: 'https://cybermind.fr'
}
});
}
return view.extend({
load: function() {
return getRemoteConfig();
return api.remoteStatus();
},
render: function(data) {
var remote = data;
var self = this;
render: function(remote) {
this.remote = remote || {};
var view = E('div', { 'class': 'system-hub-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
@ -39,37 +20,43 @@ return view.extend({
E('div', { 'class': 'sh-card sh-remote-card' }, [
E('div', { 'class': 'sh-card-header' }, [
E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '🖥️'), 'RustDesk - Assistance à Distance' ]),
E('div', { 'class': 'sh-card-badge' }, remote.rustdesk_enabled ? 'Actif' : 'Inactif')
E('div', { 'class': 'sh-card-badge' }, remote.enabled ? 'Actif' : 'Inactif')
]),
E('div', { 'class': 'sh-card-body' }, [
// RustDesk ID
E('div', { 'class': 'sh-remote-id' }, [
E('div', { 'class': 'sh-remote-id-icon' }, '🖥️'),
E('div', {}, [
E('div', { 'class': 'sh-remote-id-value' }, remote.rustdesk_id || '--- --- ---'),
E('div', { 'class': 'sh-remote-id-value', 'id': 'remote-id-value' }, remote.id || '--- --- ---'),
E('div', { 'class': 'sh-remote-id-label' }, 'ID RustDesk - Communiquez ce code au support')
])
]),
// Settings
this.renderToggle('🔒', 'Accès sans surveillance', 'Permettre la connexion sans approbation', remote.allow_unattended),
this.renderToggle('✅', 'Approbation requise', 'Confirmer chaque connexion entrante', remote.require_approval),
this.renderToggle('🔔', 'Notification de connexion', 'Recevoir une alerte à chaque session', remote.notify_on_connect),
this.renderToggle('🔒', 'Accès sans surveillance', 'Permettre la connexion sans approbation', remote.allow_unattended, 'allow_unattended'),
this.renderToggle('✅', 'Approbation requise', 'Confirmer chaque connexion entrante', remote.require_approval, 'require_approval'),
this.renderToggle('🔔', 'Notification de connexion', 'Recevoir une alerte à chaque session', remote.notify_on_connect, 'notify_on_connect'),
// Status
!remote.rustdesk_installed ? E('div', { 'style': 'padding: 16px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border-left: 3px solid #f59e0b; margin-top: 16px;' }, [
!remote.installed ? E('div', { 'style': 'padding: 16px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border-left: 3px solid #f59e0b; margin-top: 16px;' }, [
E('span', { 'style': 'font-size: 20px; margin-right: 12px;' }, '⚠️'),
E('span', {}, 'RustDesk n\'est pas installé. '),
E('a', { 'href': '#', 'style': 'color: #6366f1;' }, 'Installer maintenant')
]) : E('span'),
E('a', { 'href': '#', 'style': 'color: #6366f1;', 'click': L.bind(this.installRustdesk, this) }, 'Installer maintenant')
]) : E('div', { 'style': 'padding: 10px; background: rgba(34,197,94,0.12); border-radius: 10px; margin-top: 16px;' }, [
E('span', { 'style': 'font-size: 20px; margin-right: 12px;' }, remote.running ? '🟢' : '🟠'),
E('span', {}, remote.running ? 'Service RustDesk en cours d\'exécution' : 'Service installé mais arrêté')
]),
// Actions
E('div', { 'class': 'sh-btn-group' }, [
E('button', {
'class': 'sh-btn sh-btn-primary',
'click': L.bind(this.startSession, this, 'rustdesk')
}, [ '🚀 Démarrer Session' ]),
E('button', { 'class': 'sh-btn' }, [ '⚙️ Configurer RustDesk' ])
'click': L.bind(this.showCredentials, this)
}, [ '🔑 Identifiants' ]),
E('button', {
'class': 'sh-btn',
'click': L.bind(this.toggleService, this)
}, [ remote.running ? '⏹️ Arrêter' : '▶️ Démarrer' ])
])
])
]),
@ -96,7 +83,7 @@ return view.extend({
])
]),
// Support Contact
// Support Contact (static)
E('div', { 'class': 'sh-card' }, [
E('div', { 'class': 'sh-card-header' }, [
E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '📞'), 'Contact Support' ])
@ -105,24 +92,20 @@ return view.extend({
E('div', { 'class': 'sh-sysinfo-grid' }, [
E('div', { 'class': 'sh-sysinfo-item' }, [
E('span', { 'class': 'sh-sysinfo-label' }, 'Fournisseur'),
E('span', { 'class': 'sh-sysinfo-value' }, remote.support?.provider || 'N/A')
E('span', { 'class': 'sh-sysinfo-value' }, 'CyberMind.fr')
]),
E('div', { 'class': 'sh-sysinfo-item' }, [
E('span', { 'class': 'sh-sysinfo-label' }, 'Email'),
E('span', { 'class': 'sh-sysinfo-value' }, remote.support?.email || 'N/A')
E('span', { 'class': 'sh-sysinfo-value' }, 'support@cybermind.fr')
]),
E('div', { 'class': 'sh-sysinfo-item' }, [
E('span', { 'class': 'sh-sysinfo-label' }, 'Téléphone'),
E('span', { 'class': 'sh-sysinfo-value' }, remote.support?.phone || 'N/A')
E('span', { 'class': 'sh-sysinfo-value' }, '+33 1 23 45 67 89')
]),
E('div', { 'class': 'sh-sysinfo-item' }, [
E('span', { 'class': 'sh-sysinfo-label' }, 'Website'),
E('span', { 'class': 'sh-sysinfo-value' }, remote.support?.website || 'N/A')
E('span', { 'class': 'sh-sysinfo-value' }, 'https://cybermind.fr')
])
]),
E('div', { 'class': 'sh-btn-group' }, [
E('button', { 'class': 'sh-btn sh-btn-primary' }, [ '🎫 Ouvrir un Ticket' ]),
E('button', { 'class': 'sh-btn' }, [ '📚 Documentation' ])
])
])
])
@ -131,7 +114,7 @@ return view.extend({
return view;
},
renderToggle: function(icon, label, desc, enabled) {
renderToggle: function(icon, label, desc, enabled, field) {
return E('div', { 'class': 'sh-toggle' }, [
E('div', { 'class': 'sh-toggle-info' }, [
E('span', { 'class': 'sh-toggle-icon' }, icon),
@ -142,22 +125,88 @@ return view.extend({
]),
E('div', {
'class': 'sh-toggle-switch' + (enabled ? ' active' : ''),
'click': function(ev) { ev.target.classList.toggle('active'); }
'data-field': field,
'click': L.bind(function(ev) {
ev.target.classList.toggle('active');
this.saveSettings();
}, this)
})
]);
},
startSession: function(type) {
ui.showModal(_('Démarrage Session'), [
E('p', {}, 'Démarrage de la session ' + type + '...'),
showCredentials: function() {
ui.showModal(_('Identifiants RustDesk'), [
E('p', {}, 'Récupération en cours…'),
E('div', { 'class': 'spinning' })
]);
// Stub: Remote session not yet implemented
setTimeout(function() {
api.remoteCredentials().then(function(result) {
ui.hideModal();
ui.addNotification(null, E('p', {}, '⚠️ Remote session feature coming soon'), 'info');
}, 1000);
ui.showModal(_('Identifiants RustDesk'), [
E('div', { 'style': 'font-size:18px; margin-bottom:8px;' }, 'ID: ' + (result.id || '---')),
E('div', { 'style': 'font-size:18px;' }, 'Mot de passe: ' + (result.password || '---')),
E('div', { 'class': 'sh-btn-group', 'style': 'margin-top:16px;' }, [
E('button', { 'class': 'sh-btn sh-btn-primary', 'click': ui.hideModal }, 'Fermer')
])
]);
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
},
toggleService: function() {
if (!this.remote || !this.remote.installed) return;
var action = this.remote.running ? 'stop' : 'start';
api.remoteServiceAction(action).then(L.bind(function(res) {
if (res.success) {
this.reload();
ui.addNotification(null, E('p', {}, '✅ ' + action), 'info');
} else {
ui.addNotification(null, E('p', {}, res.error || 'Action impossible'), 'error');
}
}, this));
},
installRustdesk: function(ev) {
ev.preventDefault();
ui.showModal(_('Installation'), [
E('p', {}, 'Installation de RustDesk…'),
E('div', { 'class': 'spinning' })
]);
api.remoteInstall().then(L.bind(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', {}, result.message || 'Installé'), 'info');
this.reload();
} else {
ui.addNotification(null, E('p', {}, result.error || 'Installation impossible'), 'error');
}
}, this)).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err.message || err), 'error');
});
},
saveSettings: function() {
var allow = document.querySelector('[data-field="allow_unattended"]').classList.contains('active') ? 1 : 0;
var require = document.querySelector('[data-field="require_approval"]').classList.contains('active') ? 1 : 0;
var notify = document.querySelector('[data-field="notify_on_connect"]').classList.contains('active') ? 1 : 0;
api.remoteSaveSettings({
allow_unattended: allow,
require_approval: require,
notify_on_connect: notify
});
},
reload: function() {
this.load().then(L.bind(function(data) {
var node = this.render(data);
var root = document.querySelector('.system-hub-dashboard');
if (root && root.parentNode) {
root.parentNode.replaceChild(node, root);
}
}, this));
},
handleSaveApply: null,

View File

@ -6,6 +6,16 @@
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
DIAG_DIR="/tmp/system-hub/diagnostics"
mkdir -p "$DIAG_DIR"
safe_filename() {
local name="$1"
name="${name##*/}"
name="${name//../}"
echo "$name"
}
# Get comprehensive system status
status() {
json_init
@ -560,6 +570,470 @@ get_storage() {
json_dump
}
# Collect diagnostic data into archive
collect_diagnostics() {
read input
[ -n "$input" ] && json_load "$input"
local include_logs include_config include_network anonymize
json_get_var include_logs include_logs 2>/dev/null
json_get_var include_config include_config 2>/dev/null
json_get_var include_network include_network 2>/dev/null
json_get_var anonymize anonymize 2>/dev/null
[ -z "$include_logs" ] && include_logs=1
[ -z "$include_config" ] && include_config=1
[ -z "$include_network" ] && include_network=1
[ -z "$anonymize" ] && anonymize=0
local timestamp="$(date +%Y%m%d-%H%M%S)"
local workdir="$DIAG_DIR/work-$timestamp"
mkdir -p "$workdir"
# System info
{
echo "=== System Information ==="
uname -a
echo
echo "--- CPU ---"
cat /proc/cpuinfo
echo
echo "--- Memory ---"
cat /proc/meminfo
echo
echo "--- Disk ---"
df -h
echo
echo "--- Uptime ---"
uptime
} >"$workdir/sysinfo.txt"
# Logs
if [ "$include_logs" = "1" ]; then
logread 2>/dev/null >"$workdir/system.log" || true
dmesg 2>/dev/null >"$workdir/kernel.log" || true
fi
# Configs
if [ "$include_config" = "1" ]; then
mkdir -p "$workdir/config"
sysupgrade -b "$workdir/config/backup.tar.gz" >/dev/null 2>&1 || true
for conf in /etc/config/*; do
[ -f "$conf" ] || continue
local dest="$workdir/config/$(basename "$conf")"
if [ "$anonymize" = "1" ]; then
grep -viE "(password|passwd|secret|key|token|psk|ipaddr|macaddr)" "$conf" >"$dest" 2>/dev/null || cp "$conf" "$dest"
else
cp "$conf" "$dest"
fi
done
fi
# Network info
if [ "$include_network" = "1" ]; then
{
echo "=== Interfaces ==="
ip addr
echo
echo "=== Routes ==="
ip route
echo
echo "=== Firewall ==="
iptables -L -n -v 2>/dev/null || true
echo
echo "=== DNS ==="
cat /etc/resolv.conf 2>/dev/null
echo
echo "=== Connectivity (8.8.8.8) ==="
ping -c 3 -W 2 8.8.8.8 2>&1
echo
echo "=== Connectivity (1.1.1.1) ==="
ping -c 3 -W 2 1.1.1.1 2>&1
} >"$workdir/network.txt"
fi
local archive_name="diagnostics-$(hostname)-$timestamp.tar.gz"
local archive_path="$DIAG_DIR/$archive_name"
tar -czf "$archive_path" -C "$workdir" . >/dev/null 2>&1 || {
rm -rf "$workdir"
json_init
json_add_boolean "success" 0
json_add_string "error" "archive_failed"
json_dump
return
}
rm -rf "$workdir"
local size=$(stat -c%s "$archive_path" 2>/dev/null || stat -f%z "$archive_path")
local created=$(date -r "$archive_path" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S')
json_init
json_add_boolean "success" 1
json_add_string "file" "$archive_name"
json_add_int "size" "$size"
json_add_string "created_at" "$created"
json_dump
}
list_diagnostics() {
json_init
json_add_array "archives"
for file in $(ls -t "$DIAG_DIR"/diagnostics-*.tar.gz 2>/dev/null | head -n 50); do
[ -f "$file" ] || continue
local size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file")
local created=$(date -r "$file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "")
json_add_object
json_add_string "name" "$(basename "$file")"
json_add_int "size" "$size"
json_add_string "created_at" "$created"
json_close_object
done
json_close_array
json_add_boolean "success" 1
json_dump
}
download_diagnostic() {
read input
json_load "$input"
json_get_var name name
local safe="$(safe_filename "$name")"
local file="$DIAG_DIR/$safe"
json_init
if [ -z "$safe" ] || [ ! -f "$file" ]; then
json_add_boolean "success" 0
json_add_string "error" "not_found"
json_dump
return
}
local data=$(base64 "$file" 2>/dev/null)
json_add_boolean "success" 1
json_add_string "name" "$safe"
json_add_string "data" "$data"
json_dump
}
delete_diagnostic() {
read input
json_load "$input"
json_get_var name name
local safe="$(safe_filename "$name")"
local file="$DIAG_DIR/$safe"
json_init
if [ -n "$safe" ] && [ -f "$file" ]; then
rm -f "$file"
json_add_boolean "success" 1
else
json_add_boolean "success" 0
json_add_string "error" "not_found"
fi
json_dump
}
run_diagnostic_test() {
read input
json_load "$input"
json_get_var test test
local output=""
local success=1
case "$test" in
connectivity)
output="$(ping -c 3 -W 2 8.8.8.8 2>&1; echo; ping -c 3 -W 2 1.1.1.1 2>&1)"
;;
dns)
if command -v nslookup >/dev/null 2>&1; then
output="$(nslookup openwrt.org 2>&1)"
else
output="nslookup unavailable"
success=0
fi
;;
latency)
output="$(ping -c 5 -W 2 google.com 2>&1)"
;;
disk)
local testfile="/tmp/system-hub-disk-test"
if dd if=/dev/zero of="$testfile" bs=1M count=4 conv=fsync 2>&1; then
sync
rm -f "$testfile"
output="Disk write test completed (4MB)."
else
output="Disk test failed."
success=0
fi
;;
firewall)
output="$(iptables -L -n -v 2>&1)"
;;
wifi)
if command -v iwinfo >/dev/null 2>&1; then
output="$(iwinfo 2>&1)"
else
output="iwinfo not available"
success=0
fi
;;
*)
output="unknown test"
success=0
;;
esac
json_init
json_add_boolean "success" "$success"
json_add_string "test" "${test:-unknown}"
json_add_string "output" "$output"
json_dump
}
remote_status() {
local section="system-hub.remote"
local enabled=$(uci -q get $section.rustdesk_enabled || echo 0)
local relay_server=$(uci -q get $section.rustdesk_server || echo "")
local relay_key=$(uci -q get $section.rustdesk_key || echo "")
local stored_id=$(uci -q get $section.rustdesk_id || echo "")
local stored_password=$(uci -q get $section.rustdesk_password || echo "")
local allow_unattended=$(uci -q get $section.allow_unattended || echo 0)
local require_approval=$(uci -q get $section.require_approval || echo 1)
local notify_on_connect=$(uci -q get $section.notify_on_connect || echo 1)
local installed=0
command -v rustdesk >/dev/null 2>&1 && installed=1
local running=0
if [ -x /etc/init.d/rustdesk ]; then
/etc/init.d/rustdesk status >/dev/null 2>&1 && running=1
fi
json_init
json_add_boolean "installed" "$installed"
json_add_boolean "running" "$running"
json_add_boolean "enabled" "$enabled"
json_add_string "server" "$relay_server"
json_add_string "key" "$relay_key"
json_add_string "id" "$stored_id"
json_add_string "password" "$stored_password"
json_add_boolean "allow_unattended" "$allow_unattended"
json_add_boolean "require_approval" "$require_approval"
json_add_boolean "notify_on_connect" "$notify_on_connect"
json_dump
}
remote_install() {
json_init
if command -v rustdesk >/dev/null 2>&1; then
json_add_boolean "success" 0
json_add_string "error" "already_installed"
json_dump
return
fi
if ! command -v opkg >/dev/null 2>&1; then
json_add_boolean "success" 0
json_add_string "error" "opkg_missing"
json_dump
return
fi
opkg update >/tmp/rustdesk-install.log 2>&1
if opkg install rustdesk >>/tmp/rustdesk-install.log 2>&1; then
json_add_boolean "success" 1
json_add_string "message" "RustDesk installed"
else
local err="$(tail -n 20 /tmp/rustdesk-install.log 2>/dev/null)"
json_add_boolean "success" 0
json_add_string "error" "${err:-install_failed}"
fi
json_dump
}
remote_configure() {
read input
json_load "$input"
local section="system-hub.remote"
local server key enabled
json_get_var server relay_server
json_get_var key relay_key
json_get_var enabled rustdesk_enabled
[ -n "$server" ] && uci set $section.rustdesk_server="$server"
[ -n "$key" ] && uci set $section.rustdesk_key="$key"
[ -n "$enabled" ] && uci set $section.rustdesk_enabled="$enabled"
uci commit system-hub
if [ -n "$server" ] || [ -n "$key" ]; then
mkdir -p /etc/rustdesk
cat > /etc/rustdesk/config.toml <<EOF
[relay]
server = "${server:-$(uci -q get $section.rustdesk_server)}"
[options]
key = "${key:-$(uci -q get $section.rustdesk_key)}"
auto_start = true
EOF
fi
if [ -x /etc/init.d/rustdesk ] && [ "${enabled:-0}" = "1" ]; then
/etc/init.d/rustdesk enable >/dev/null 2>&1 || true
/etc/init.d/rustdesk restart >/dev/null 2>&1 || true
elif [ -x /etc/init.d/rustdesk ]; then
/etc/init.d/rustdesk stop >/dev/null 2>&1 || true
/etc/init.d/rustdesk disable >/dev/null 2>&1 || true
fi
json_init
json_add_boolean "success" 1
json_dump
}
remote_get_credentials() {
local section="system-hub.remote"
local rid="" rpass=""
if command -v rustdesk >/dev/null 2>&1; then
rid=$(rustdesk --get-id 2>/dev/null || echo "")
rpass=$(rustdesk --password 2>/dev/null || echo "")
fi
[ -z "$rid" ] && rid=$(uci -q get $section.rustdesk_id || echo "")
[ -z "$rpass" ] && rpass=$(uci -q get $section.rustdesk_password || echo "")
json_init
json_add_boolean "success" 1
json_add_string "id" "$rid"
json_add_string "password" "$rpass"
json_dump
}
remote_service_action() {
read input
json_load "$input"
json_get_var action action
json_init
if [ ! -x /etc/init.d/rustdesk ]; then
json_add_boolean "success" 0
json_add_string "error" "service_missing"
json_dump
return
fi
case "$action" in
start|stop|restart|enable|disable)
if /etc/init.d/rustdesk "$action" >/dev/null 2>&1; then
json_add_boolean "success" 1
json_add_string "message" "$action"
else
json_add_boolean "success" 0
json_add_string "error" "action_failed"
fi
;;
*)
json_add_boolean "success" 0
json_add_string "error" "invalid_action"
;;
esac
json_dump
}
remote_save_settings() {
read input
json_load "$input"
local section="system-hub.remote"
local allow require notify
json_get_var allow allow_unattended
json_get_var require require_approval
json_get_var notify notify_on_connect
[ -n "$allow" ] && uci set $section.allow_unattended="$allow"
[ -n "$require" ] && uci set $section.require_approval="$require"
[ -n "$notify" ] && uci set $section.notify_on_connect="$notify"
uci commit system-hub
json_init
json_add_boolean "success" 1
json_dump
}
upload_diagnostics() {
read input
json_load "$input"
json_get_var name name
local upload_url=$(uci -q get system-hub.diagnostics.upload_url)
local upload_token=$(uci -q get system-hub.diagnostics.upload_token)
json_init
if [ -z "$upload_url" ]; then
json_add_boolean "success" 0
json_add_string "error" "upload_url_missing"
json_dump
return
fi
if ! command -v curl >/dev/null 2>&1; then
json_add_boolean "success" 0
json_add_string "error" "curl_missing"
json_dump
return
fi
local safe="$(safe_filename "$name")"
if [ -z "$safe" ]; then
# Use latest archive if not specified
safe="$(ls -t "$DIAG_DIR"/diagnostics-*.tar.gz 2>/dev/null | head -n1)"
[ -n "$safe" ] && safe="$(basename "$safe")"
fi
local file="$DIAG_DIR/$safe"
if [ -z "$safe" ] || [ ! -f "$file" ]; then
json_add_boolean "success" 0
json_add_string "error" "archive_not_found"
json_dump
return
fi
local response
if [ -n "$upload_token" ]; then
response=$(curl -s -S -o /tmp/system-hub-upload.log -w "%{http_code}" \
-H "Authorization: Bearer $upload_token" \
-F "file=@$file" \
-F "hostname=$(hostname)" \
-F "timestamp=$(date +%s)" \
"$upload_url" 2>&1)
else
response=$(curl -s -S -o /tmp/system-hub-upload.log -w "%{http_code}" \
-F "file=@$file" \
-F "hostname=$(hostname)" \
-F "timestamp=$(date +%s)" \
"$upload_url" 2>&1)
fi
local curl_exit=$?
local body="$(cat /tmp/system-hub-upload.log 2>/dev/null || true)"
rm -f /tmp/system-hub-upload.log
if [ $curl_exit -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "status" "$response"
json_add_string "body" "$body"
else
json_add_boolean "success" 0
json_add_string "error" "upload_failed"
json_add_string "details" "$body"
fi
json_dump
}
# Get settings
get_settings() {
json_init
@ -770,6 +1244,31 @@ case "$1" in
"disk_critical": 95,
"temp_warning": 70,
"temp_critical": 85
},
"collect_diagnostics": {
"include_logs": 1,
"include_config": 1,
"include_network": 1,
"anonymize": 1
},
"list_diagnostics": {},
"download_diagnostic": { "name": "string" },
"delete_diagnostic": { "name": "string" },
"run_diagnostic_test": { "test": "string" },
"upload_diagnostics": { "name": "string" },
"remote_status": {},
"remote_install": {},
"remote_configure": {
"relay_server": "string",
"relay_key": "string",
"rustdesk_enabled": 1
},
"remote_get_credentials": {},
"remote_service_action": { "action": "start|stop|restart|enable|disable" },
"remote_save_settings": {
"allow_unattended": 0,
"require_approval": 1,
"notify_on_connect": 1
}
}
EOF
@ -790,6 +1289,18 @@ EOF
get_storage) get_storage ;;
get_settings) get_settings ;;
save_settings) save_settings ;;
collect_diagnostics) collect_diagnostics ;;
list_diagnostics) list_diagnostics ;;
download_diagnostic) download_diagnostic ;;
delete_diagnostic) delete_diagnostic ;;
run_diagnostic_test) run_diagnostic_test ;;
upload_diagnostics) upload_diagnostics ;;
remote_status) remote_status ;;
remote_install) remote_install ;;
remote_configure) remote_configure ;;
remote_get_credentials) remote_get_credentials ;;
remote_service_action) remote_service_action ;;
remote_save_settings) remote_save_settings ;;
*)
json_init
json_add_boolean "success" 0