feat(rtty): Add remote package installation for mesh nodes
Add rttyctl commands for remote package deployment: - rttyctl install <node|all> <app_id> - Install package on node(s) - rttyctl install-status <node> [app] - Check package status - rttyctl deploy-ttyd <node|all> - Deploy ttyd web terminal RPCD methods added: - install_remote, install_mesh, deploy_ttyd, install_status Features: - Node discovery from master-link, WireGuard, P2P mesh - Auto-enables and starts ttyd after installation - Batch install with summary stats (installed/skipped/failed) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e2723946d4
commit
ac7912e0a1
@ -5232,3 +5232,15 @@ git checkout HEAD -- index.html
|
||||
- Fixed protocol display bug ("TCPnull" → "TCP")
|
||||
- Removed mitmproxy-out container (not needed for WAF)
|
||||
- Updated MITM detection to check mitmproxy-in specifically
|
||||
|
||||
### 2026-03-16
|
||||
|
||||
- **Remote ttyd Deployment for Mesh Nodes (Complete)**
|
||||
- CLI commands: `rttyctl install <node|all> <app>`, `rttyctl install-status`, `rttyctl deploy-ttyd`
|
||||
- Installs packages on remote mesh nodes via RPC proxy to AppStore
|
||||
- Auto-enables and starts ttyd service after installation
|
||||
- `rttyctl install all <app>` - batch install across all mesh nodes
|
||||
- Node discovery from: master-link peers, WireGuard endpoints, P2P mesh
|
||||
- 4 new RPCD methods: install_remote, install_mesh, deploy_ttyd, install_status
|
||||
- ACL permissions updated for remote installation write actions
|
||||
- Use case: Deploy ttyd web terminal to all SecuBox nodes for browser-based SSH
|
||||
|
||||
@ -10,6 +10,15 @@ _Last updated: 2026-03-16 (DPI LAN Passive Analysis)_
|
||||
|
||||
### 2026-03-16
|
||||
|
||||
- **Remote ttyd Deployment for Mesh Nodes (Complete)**
|
||||
- CLI commands: `rttyctl install`, `rttyctl install-status`, `rttyctl deploy-ttyd`
|
||||
- Installs packages on remote mesh nodes via RPC proxy
|
||||
- Auto-enables and starts ttyd service after installation
|
||||
- `rttyctl install all <app>` - batch install across all mesh nodes
|
||||
- Node discovery from: master-link, WireGuard peers, P2P mesh
|
||||
- 4 new RPCD methods: install_remote, install_mesh, deploy_ttyd, install_status
|
||||
- ACL permissions updated for remote installation actions
|
||||
|
||||
- **Dual-Stream DPI Phase 4 - LAN Passive Flow Analysis (Complete)**
|
||||
- New `dpi-lan-collector` daemon for passive br-lan monitoring
|
||||
- Zero MITM, zero caching - pure nDPI/conntrack flow observation
|
||||
@ -626,8 +635,8 @@ _Last updated: 2026-03-16 (DPI LAN Passive Analysis)_
|
||||
|
||||
### v1.0 Release Prep
|
||||
|
||||
1. **Remote ttyd Deployment** - Auto-install ttyd on mesh nodes
|
||||
2. **Device Provisioning** - Use Config Vault export-clone for SecuBox replication
|
||||
1. **Device Provisioning** - Use Config Vault export-clone for SecuBox replication
|
||||
2. **LuCI Remote Install Button** - Add "Deploy ttyd" action to Remote Control dashboard (optional)
|
||||
|
||||
### v1.1+ Extended Mesh
|
||||
|
||||
|
||||
@ -438,6 +438,126 @@ method_token_rpc() {
|
||||
fi
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Remote Package Installation
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# Install package on remote node
|
||||
method_install_remote() {
|
||||
local node_id app_id
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var node_id node_id
|
||||
json_get_var app_id app_id
|
||||
|
||||
[ -z "$node_id" ] || [ -z "$app_id" ] && {
|
||||
echo '{"success":false,"error":"Missing node_id or app_id"}'
|
||||
return
|
||||
}
|
||||
|
||||
# Execute via rttyctl
|
||||
local result=$($RTTYCTL install "$node_id" "$app_id" 2>&1)
|
||||
local rc=$?
|
||||
|
||||
json_init
|
||||
if [ $rc -eq 0 ] && echo "$result" | grep -q "✓"; then
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Package $app_id installed on $node_id"
|
||||
json_add_string "output" "$result"
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "$result"
|
||||
fi
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Install package on all mesh nodes
|
||||
method_install_mesh() {
|
||||
local app_id
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var app_id app_id
|
||||
|
||||
[ -z "$app_id" ] && {
|
||||
echo '{"success":false,"error":"Missing app_id"}'
|
||||
return
|
||||
}
|
||||
|
||||
# Execute via rttyctl with 'all' target
|
||||
local result=$($RTTYCTL install all "$app_id" 2>&1)
|
||||
local rc=$?
|
||||
|
||||
# Parse summary line
|
||||
local installed=$(echo "$result" | grep "Summary:" | sed 's/.*: //' | cut -d, -f1 | awk '{print $1}')
|
||||
local skipped=$(echo "$result" | grep "Summary:" | sed 's/.*: //' | cut -d, -f2 | awk '{print $1}')
|
||||
local failed=$(echo "$result" | grep "Summary:" | sed 's/.*: //' | cut -d, -f3 | awk '{print $1}')
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "app_id" "$app_id"
|
||||
json_add_int "installed" "${installed:-0}"
|
||||
json_add_int "skipped" "${skipped:-0}"
|
||||
json_add_int "failed" "${failed:-0}"
|
||||
json_add_string "output" "$result"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Deploy ttyd to node(s) - shortcut
|
||||
method_deploy_ttyd() {
|
||||
local target
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var target target
|
||||
|
||||
[ -z "$target" ] && {
|
||||
echo '{"success":false,"error":"Missing target (node_id or \"all\")"}'
|
||||
return
|
||||
}
|
||||
|
||||
# Execute via rttyctl
|
||||
local result=$($RTTYCTL deploy-ttyd "$target" 2>&1)
|
||||
local rc=$?
|
||||
|
||||
json_init
|
||||
if [ $rc -eq 0 ] && echo "$result" | grep -q "✓"; then
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "ttyd deployed to $target"
|
||||
|
||||
# If single node, include ttyd URL
|
||||
if [ "$target" != "all" ]; then
|
||||
local addr=$($RTTYCTL node "$target" 2>&1 | grep "Address:" | awk '{print $2}')
|
||||
[ -z "$addr" ] && addr="$target"
|
||||
json_add_string "terminal_url" "http://${addr}:7681/"
|
||||
fi
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "$result"
|
||||
fi
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Check package status on remote node
|
||||
method_install_status() {
|
||||
local node_id app_id
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var node_id node_id
|
||||
json_get_var app_id app_id
|
||||
|
||||
[ -z "$node_id" ] && {
|
||||
echo '{"success":false,"error":"Missing node_id"}'
|
||||
return
|
||||
}
|
||||
|
||||
local result=$($RTTYCTL install-status "$node_id" "$app_id" 2>&1)
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "node_id" "$node_id"
|
||||
json_add_string "output" "$result"
|
||||
json_dump
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Avatar-Tap Session Integration
|
||||
#------------------------------------------------------------------------------
|
||||
@ -621,7 +741,11 @@ case "$1" in
|
||||
"replay_to_node": {"session_id": "integer", "target_node": "string"},
|
||||
"export_session": {"session_id": "integer"},
|
||||
"import_session": {"content": "string"},
|
||||
"get_tap_status": {}
|
||||
"get_tap_status": {},
|
||||
"install_remote": {"node_id": "string", "app_id": "string"},
|
||||
"install_mesh": {"app_id": "string"},
|
||||
"deploy_ttyd": {"target": "string"},
|
||||
"install_status": {"node_id": "string", "app_id": "string"}
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
@ -651,6 +775,10 @@ EOF
|
||||
export_session) method_export_session ;;
|
||||
import_session) method_import_session ;;
|
||||
get_tap_status) method_get_tap_status ;;
|
||||
install_remote) method_install_remote ;;
|
||||
install_mesh) method_install_mesh ;;
|
||||
deploy_ttyd) method_deploy_ttyd ;;
|
||||
install_status) method_install_status ;;
|
||||
*)
|
||||
echo '{"error":"Unknown method"}'
|
||||
;;
|
||||
|
||||
@ -14,7 +14,8 @@
|
||||
"token_validate",
|
||||
"get_tap_sessions",
|
||||
"get_tap_session",
|
||||
"get_tap_status"
|
||||
"get_tap_status",
|
||||
"install_status"
|
||||
]
|
||||
},
|
||||
"uci": ["rtty-remote", "avatar-tap"]
|
||||
@ -34,7 +35,10 @@
|
||||
"token_rpc",
|
||||
"replay_to_node",
|
||||
"export_session",
|
||||
"import_session"
|
||||
"import_session",
|
||||
"install_remote",
|
||||
"install_mesh",
|
||||
"deploy_ttyd"
|
||||
]
|
||||
},
|
||||
"uci": ["rtty-remote"]
|
||||
|
||||
@ -936,6 +936,235 @@ cmd_tap_json_session() {
|
||||
"SELECT id, domain, path, method, captured_at, last_used, use_count, label FROM sessions WHERE id=$session_id" 2>/dev/null || echo '{"error":"Session not found"}'
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Remote Package Installation
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
cmd_install() {
|
||||
local target="$1"
|
||||
local app_id="$2"
|
||||
|
||||
[ -z "$target" ] || [ -z "$app_id" ] && {
|
||||
echo "Usage: rttyctl install <node|all> <app_id>"
|
||||
echo ""
|
||||
echo "Install a package on remote mesh node(s)."
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " rttyctl install 192.168.255.2 ttyd"
|
||||
echo " rttyctl install sb-office ttyd"
|
||||
echo " rttyctl install all ttyd # Install on ALL mesh nodes"
|
||||
echo ""
|
||||
echo "Common apps: ttyd, crowdsec, netifyd, haproxy"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ "$target" = "all" ]; then
|
||||
cmd_install_all "$app_id"
|
||||
return $?
|
||||
fi
|
||||
|
||||
local addr=$(get_node_address "$target")
|
||||
[ -z "$addr" ] && die "Cannot resolve node address for: $target"
|
||||
|
||||
echo "Installing '$app_id' on $target ($addr)..."
|
||||
echo ""
|
||||
|
||||
# Check if node is reachable
|
||||
if ! ping -c 1 -W 2 "$addr" >/dev/null 2>&1; then
|
||||
die "Node $target ($addr) is not reachable"
|
||||
fi
|
||||
|
||||
# Check if already installed
|
||||
echo "Checking if already installed..."
|
||||
local check_result=$(cmd_rpc "$target" "luci.secubox" "check_package" "{\"package\":\"$app_id\"}" 2>/dev/null)
|
||||
local installed=$(echo "$check_result" | jsonfilter -e '@.installed' 2>/dev/null)
|
||||
|
||||
if [ "$installed" = "true" ] || [ "$installed" = "1" ]; then
|
||||
echo "Package '$app_id' is already installed on $target"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Install via remote RPC
|
||||
echo "Installing package..."
|
||||
local install_result=$(cmd_rpc "$target" "luci.secubox" "install_appstore_app" "{\"app_id\":\"$app_id\"}" 2>&1)
|
||||
local rc=$?
|
||||
|
||||
if [ $rc -eq 0 ]; then
|
||||
local success=$(echo "$install_result" | jsonfilter -e '@.success' 2>/dev/null)
|
||||
if [ "$success" = "true" ] || [ "$success" = "1" ]; then
|
||||
echo "✓ Successfully installed '$app_id' on $target"
|
||||
log "info" "Installed $app_id on $target ($addr)"
|
||||
|
||||
# If ttyd, enable and start the service
|
||||
if [ "$app_id" = "ttyd" ]; then
|
||||
echo ""
|
||||
echo "Enabling and starting ttyd service..."
|
||||
cmd_rpc "$target" "luci" "setInitAction" '{"name":"ttyd","action":"enable"}' >/dev/null 2>&1
|
||||
cmd_rpc "$target" "luci" "setInitAction" '{"name":"ttyd","action":"start"}' >/dev/null 2>&1
|
||||
echo "✓ ttyd service enabled and started"
|
||||
echo ""
|
||||
echo "Web terminal available at: http://$addr:7681/"
|
||||
fi
|
||||
else
|
||||
local error=$(echo "$install_result" | jsonfilter -e '@.error' 2>/dev/null)
|
||||
echo "✗ Installation failed: ${error:-Unknown error}"
|
||||
echo "Raw response: $install_result"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "✗ RPC call failed"
|
||||
echo "Response: $install_result"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_install_all() {
|
||||
local app_id="$1"
|
||||
[ -z "$app_id" ] && die "Usage: rttyctl install all <app_id>"
|
||||
|
||||
echo "Installing '$app_id' on ALL mesh nodes..."
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
local success_count=0
|
||||
local fail_count=0
|
||||
local skip_count=0
|
||||
|
||||
# Get all nodes from various sources
|
||||
local nodes=""
|
||||
|
||||
# From master-link
|
||||
if command -v ml_peer_list >/dev/null 2>&1; then
|
||||
local ml_nodes=$(ml_peer_list 2>/dev/null | jsonfilter -e '@.peers[*].address' 2>/dev/null)
|
||||
nodes="$nodes $ml_nodes"
|
||||
fi
|
||||
|
||||
# From WireGuard peers
|
||||
if command -v wg >/dev/null 2>&1; then
|
||||
local wg_nodes=$(wg show all endpoints 2>/dev/null | awk '{print $2}' | cut -d: -f1)
|
||||
nodes="$nodes $wg_nodes"
|
||||
fi
|
||||
|
||||
# From P2P mesh
|
||||
if [ -x /usr/sbin/secubox-p2p ]; then
|
||||
local p2p_nodes=$(/usr/sbin/secubox-p2p peers --json 2>/dev/null | jsonfilter -e '@[*].address' 2>/dev/null)
|
||||
nodes="$nodes $p2p_nodes"
|
||||
fi
|
||||
|
||||
# Deduplicate and process
|
||||
local unique_nodes=$(echo "$nodes" | tr ' ' '\n' | sort -u | grep -v '^$')
|
||||
local total=$(echo "$unique_nodes" | wc -l)
|
||||
|
||||
echo "Found $total mesh nodes"
|
||||
echo ""
|
||||
|
||||
for node in $unique_nodes; do
|
||||
[ -z "$node" ] && continue
|
||||
echo "[$node]"
|
||||
|
||||
# Skip localhost
|
||||
case "$node" in
|
||||
127.0.0.1|localhost|$(uci -q get network.lan.ipaddr))
|
||||
echo " Skipping (local node)"
|
||||
skip_count=$((skip_count + 1))
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check reachability
|
||||
if ! ping -c 1 -W 2 "$node" >/dev/null 2>&1; then
|
||||
echo " ✗ Not reachable"
|
||||
fail_count=$((fail_count + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Try to install
|
||||
local result=$(cmd_rpc "$node" "luci.secubox" "install_appstore_app" "{\"app_id\":\"$app_id\"}" 2>&1)
|
||||
local success=$(echo "$result" | jsonfilter -e '@.success' 2>/dev/null)
|
||||
|
||||
if [ "$success" = "true" ] || [ "$success" = "1" ]; then
|
||||
echo " ✓ Installed"
|
||||
success_count=$((success_count + 1))
|
||||
|
||||
# Enable ttyd if that's what we installed
|
||||
if [ "$app_id" = "ttyd" ]; then
|
||||
cmd_rpc "$node" "luci" "setInitAction" '{"name":"ttyd","action":"enable"}' >/dev/null 2>&1
|
||||
cmd_rpc "$node" "luci" "setInitAction" '{"name":"ttyd","action":"start"}' >/dev/null 2>&1
|
||||
echo " ✓ ttyd enabled and started"
|
||||
fi
|
||||
else
|
||||
local error=$(echo "$result" | jsonfilter -e '@.error' 2>/dev/null)
|
||||
if echo "$error" | grep -qi "already installed"; then
|
||||
echo " ⊘ Already installed"
|
||||
skip_count=$((skip_count + 1))
|
||||
else
|
||||
echo " ✗ Failed: ${error:-Unknown error}"
|
||||
fail_count=$((fail_count + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Summary: $success_count installed, $skip_count skipped, $fail_count failed"
|
||||
|
||||
log "info" "Mesh install of $app_id: $success_count ok, $skip_count skip, $fail_count fail"
|
||||
}
|
||||
|
||||
cmd_install_status() {
|
||||
local target="$1"
|
||||
local app_id="$2"
|
||||
|
||||
[ -z "$target" ] && {
|
||||
echo "Usage: rttyctl install-status <node> [app_id]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
local addr=$(get_node_address "$target")
|
||||
[ -z "$addr" ] && die "Cannot resolve node address for: $target"
|
||||
|
||||
echo "Package Status on $target ($addr)"
|
||||
echo "=================================="
|
||||
|
||||
if [ -n "$app_id" ]; then
|
||||
# Check specific package
|
||||
local result=$(cmd_rpc "$target" "luci.secubox" "check_package" "{\"package\":\"$app_id\"}" 2>/dev/null)
|
||||
local installed=$(echo "$result" | jsonfilter -e '@.installed' 2>/dev/null)
|
||||
local version=$(echo "$result" | jsonfilter -e '@.version' 2>/dev/null)
|
||||
|
||||
if [ "$installed" = "true" ] || [ "$installed" = "1" ]; then
|
||||
echo " $app_id: installed (v${version:-?})"
|
||||
else
|
||||
echo " $app_id: not installed"
|
||||
fi
|
||||
else
|
||||
# List all SecuBox packages
|
||||
local result=$(cmd_rpc "$target" "luci.secubox" "list_installed" '{}' 2>/dev/null)
|
||||
echo "$result" | jsonfilter -e '@.packages[*]' 2>/dev/null | while read pkg; do
|
||||
local name=$(echo "$pkg" | jsonfilter -e '@.name')
|
||||
local ver=$(echo "$pkg" | jsonfilter -e '@.version')
|
||||
echo " $name: $ver"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_deploy_ttyd() {
|
||||
local target="$1"
|
||||
|
||||
[ -z "$target" ] && {
|
||||
echo "Usage: rttyctl deploy-ttyd <node|all>"
|
||||
echo ""
|
||||
echo "Deploy ttyd web terminal to mesh node(s)."
|
||||
echo "This is a shortcut for: rttyctl install <target> ttyd"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "Deploying ttyd Web Terminal"
|
||||
echo "==========================="
|
||||
echo ""
|
||||
|
||||
cmd_install "$target" "ttyd"
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Server Management
|
||||
#------------------------------------------------------------------------------
|
||||
@ -1048,6 +1277,11 @@ RPCD Proxy:
|
||||
rpc-list <node> List available RPCD objects
|
||||
rpc-batch <node> <file.json> Execute batch RPCD calls
|
||||
|
||||
Remote Package Installation:
|
||||
install <node|all> <app_id> Install package on node(s)
|
||||
install-status <node> [app] Check package installation status
|
||||
deploy-ttyd <node|all> Deploy ttyd web terminal (shortcut)
|
||||
|
||||
Avatar-Tap Session Replay:
|
||||
tap-sessions [domain] List captured sessions (filter by domain)
|
||||
tap-show <id> Show session details (headers, cookies)
|
||||
@ -1116,6 +1350,15 @@ case "$1" in
|
||||
rpc-batch)
|
||||
cmd_rpc_batch "$2" "$3"
|
||||
;;
|
||||
install)
|
||||
cmd_install "$2" "$3"
|
||||
;;
|
||||
install-status)
|
||||
cmd_install_status "$2" "$3"
|
||||
;;
|
||||
deploy-ttyd)
|
||||
cmd_deploy_ttyd "$2"
|
||||
;;
|
||||
sessions)
|
||||
cmd_sessions "$2"
|
||||
;;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user