#!/bin/sh # # rttyctl - SecuBox RTTY Remote Control CLI # # Remote control assistant for SecuBox mesh nodes # Provides RPCD proxy, terminal access, and session replay # . /lib/functions.sh . /usr/share/libubox/jshn.sh SCRIPT_NAME="rttyctl" VERSION="0.1.0" # Configuration CONFIG_FILE="/etc/config/rtty-remote" SESSION_DB="/srv/rtty-remote/sessions.db" CACHE_DIR="/tmp/rtty-remote" TOKEN_DIR="/tmp/rtty-remote/tokens" LOG_FILE="/var/log/rtty-remote.log" # Load libraries [ -f /usr/lib/secubox/rtty-proxy.sh ] && . /usr/lib/secubox/rtty-proxy.sh [ -f /usr/lib/secubox/rtty-session.sh ] && . /usr/lib/secubox/rtty-session.sh [ -f /usr/lib/secubox/rtty-auth.sh ] && . /usr/lib/secubox/rtty-auth.sh # Load master-link if available [ -f /usr/lib/secubox/master-link.sh ] && . /usr/lib/secubox/master-link.sh #------------------------------------------------------------------------------ # Utilities #------------------------------------------------------------------------------ log() { local level="$1" shift echo "[$level] $*" >> "$LOG_FILE" [ "$level" = "error" ] && echo "Error: $*" >&2 } die() { echo "Error: $*" >&2 exit 1 } ensure_dirs() { mkdir -p "$CACHE_DIR" 2>/dev/null mkdir -p "$TOKEN_DIR" 2>/dev/null mkdir -p "$(dirname "$SESSION_DB")" 2>/dev/null } #------------------------------------------------------------------------------ # Token-Based Shared Access #------------------------------------------------------------------------------ generate_token_code() { # Generate 6-character alphanumeric code # Use hexdump if available, fallback to awk-based pseudo-random local chars="ABCDEFGHJKLMNPQRSTUVWXYZ23456789" local code="" if command -v hexdump >/dev/null 2>&1; then # Use hexdump for true randomness local hex=$(head -c 6 /dev/urandom | hexdump -e '6/1 "%02x"') local i=0 while [ $i -lt 6 ]; do local byte=$(printf "%d" "0x$(echo "$hex" | cut -c$((i*2+1))-$((i*2+2)))") local idx=$((byte % 32)) code="${code}$(echo "$chars" | cut -c$((idx + 1)))" i=$((i + 1)) done else # Fallback: use awk with pseudo-random seed from /proc/sys/kernel/random/uuid local seed=$(cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d '-' | cut -c1-8) seed=$(printf "%d" "0x$seed" 2>/dev/null || echo $(($(date +%s) % 65536))) code=$(awk -v seed="$seed" -v chars="$chars" 'BEGIN { srand(seed) for (i=1; i<=6; i++) { idx = int(rand() * 32) + 1 printf "%s", substr(chars, idx, 1) } print "" }') fi echo "$code" } cmd_token_generate() { local ttl="${1:-3600}" # Default 1 hour local permissions="${2:-rpc,terminal}" local code=$(generate_token_code) local now=$(date +%s) local expires=$((now + ttl)) local node_name=$(uci -q get system.@system[0].hostname || echo "secubox") local node_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") # Store token cat > "$TOKEN_DIR/$code" << EOF { "code": "$code", "created": $now, "expires": $expires, "ttl": $ttl, "node_id": "$node_name", "node_ip": "$node_ip", "permissions": "$permissions", "used": 0 } EOF log "info" "Generated token $code (expires in ${ttl}s)" echo "Support Access Token Generated" echo "==============================" echo "" echo " Code: $code" echo " Expires: $(date -d "@$expires" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r $expires "+%Y-%m-%d %H:%M:%S")" echo " TTL: ${ttl}s" echo " Access: $permissions" echo "" echo "Share this code with the support person." } cmd_token_validate() { local code="$1" [ -z "$code" ] && { echo '{"valid":false,"error":"Missing code"}'; return 1; } # Normalize code (uppercase, remove spaces) code=$(echo "$code" | tr 'a-z' 'A-Z' | tr -d ' -') local token_file="$TOKEN_DIR/$code" if [ ! -f "$token_file" ]; then echo '{"valid":false,"error":"Invalid code"}' return 1 fi local now=$(date +%s) local expires=$(jsonfilter -i "$token_file" -e '@.expires') if [ "$now" -gt "$expires" ]; then rm -f "$token_file" echo '{"valid":false,"error":"Token expired"}' return 1 fi # Mark as used local used=$(jsonfilter -i "$token_file" -e '@.used') used=$((used + 1)) local tmp_file="${token_file}.tmp" jsonfilter -i "$token_file" -e '@' | sed "s/\"used\": [0-9]*/\"used\": $used/" > "$tmp_file" mv "$tmp_file" "$token_file" # Return token info cat "$token_file" } cmd_token_list() { echo "Active Support Tokens" echo "=====================" echo "" local now=$(date +%s) local count=0 for token_file in "$TOKEN_DIR"/*; do [ -f "$token_file" ] || continue local code=$(basename "$token_file") local expires=$(jsonfilter -i "$token_file" -e '@.expires' 2>/dev/null) local created=$(jsonfilter -i "$token_file" -e '@.created' 2>/dev/null) local used=$(jsonfilter -i "$token_file" -e '@.used' 2>/dev/null) local perms=$(jsonfilter -i "$token_file" -e '@.permissions' 2>/dev/null) [ -z "$expires" ] && continue if [ "$now" -gt "$expires" ]; then rm -f "$token_file" continue fi local remaining=$((expires - now)) local mins=$((remaining / 60)) printf " %-8s %3dm left used:%d [%s]\n" "$code" "$mins" "$used" "$perms" count=$((count + 1)) done [ $count -eq 0 ] && echo " No active tokens" echo "" } cmd_token_revoke() { local code="$1" [ -z "$code" ] && die "Usage: rttyctl token revoke " code=$(echo "$code" | tr 'a-z' 'A-Z' | tr -d ' -') if [ -f "$TOKEN_DIR/$code" ]; then rm -f "$TOKEN_DIR/$code" echo "Token $code revoked" log "info" "Token $code revoked" else echo "Token $code not found" fi } cmd_token_json_list() { local now=$(date +%s) printf '{"tokens":[' local first=1 for token_file in "$TOKEN_DIR"/*; do [ -f "$token_file" ] || continue local expires=$(jsonfilter -i "$token_file" -e '@.expires' 2>/dev/null) [ -z "$expires" ] && continue if [ "$now" -gt "$expires" ]; then rm -f "$token_file" continue fi [ $first -eq 0 ] && printf ',' cat "$token_file" first=0 done printf ']}' } # Token-authenticated RPC call (for support access) cmd_token_rpc() { local code="$1" local object="$2" local method="$3" shift 3 local params="$*" [ -z "$code" ] || [ -z "$object" ] || [ -z "$method" ] && { echo "Usage: rttyctl token-rpc [params]" return 1 } # Validate token local validation=$(cmd_token_validate "$code") local valid=$(echo "$validation" | jsonfilter -e '@.valid' 2>/dev/null) if [ "$valid" = "false" ]; then echo "Error: $(echo "$validation" | jsonfilter -e '@.error')" return 1 fi # Check permissions local perms=$(echo "$validation" | jsonfilter -e '@.permissions' 2>/dev/null) case "$perms" in *rpc*) ;; *) echo "Error: Token does not have RPC permission"; return 1 ;; esac # Execute RPC locally (token grants local access) local result=$(ubus call "$object" "$method" "${params:-{}}" 2>&1) local rc=$? if [ $rc -eq 0 ]; then echo "$result" else echo "Error: $result" return 1 fi } get_config() { local section="$1" local option="$2" local default="$3" config_load rtty-remote config_get value "$section" "$option" "$default" echo "$value" } #------------------------------------------------------------------------------ # Node Management #------------------------------------------------------------------------------ cmd_nodes() { echo "SecuBox RTTY Remote - Mesh Nodes" echo "================================" echo "" # Try to get peers from master-link first if command -v ml_peer_list >/dev/null 2>&1; then local peers=$(ml_peer_list 2>/dev/null) if [ -n "$peers" ]; then echo "From Master-Link:" echo "$peers" | jsonfilter -e '@.peers[*]' 2>/dev/null | while read peer; do local fp=$(echo "$peer" | jsonfilter -e '@.fingerprint') local name=$(echo "$peer" | jsonfilter -e '@.name') local status=$(echo "$peer" | jsonfilter -e '@.status') local addr=$(echo "$peer" | jsonfilter -e '@.address') printf " %-12s %-20s %-15s %s\n" "$fp" "$name" "$addr" "$status" done echo "" fi fi # Try secubox-p2p peers if [ -x /usr/sbin/secubox-p2p ]; then echo "From P2P Mesh:" /usr/sbin/secubox-p2p peers 2>/dev/null | head -20 fi # Also check WireGuard peers if command -v wg >/dev/null 2>&1; then echo "" echo "WireGuard Peers:" wg show all peers 2>/dev/null | while read iface peer; do local endpoint=$(wg show "$iface" endpoints 2>/dev/null | grep "$peer" | awk '{print $2}') local handshake=$(wg show "$iface" latest-handshakes 2>/dev/null | grep "$peer" | awk '{print $2}') [ -n "$endpoint" ] && printf " %-15s %s (handshake: %ss ago)\n" "$endpoint" "${peer:0:12}..." "$handshake" done fi } cmd_node() { local node_id="$1" [ -z "$node_id" ] && die "Usage: rttyctl node " echo "Node: $node_id" echo "==============" # Check if in master-link if command -v ml_peer_list >/dev/null 2>&1; then local peer_info=$(ml_peer_list 2>/dev/null | jsonfilter -e "@.peers[?(@.fingerprint=='$node_id')]" 2>/dev/null) if [ -n "$peer_info" ]; then echo "Master-Link Status:" echo " Name: $(echo "$peer_info" | jsonfilter -e '@.name')" echo " Status: $(echo "$peer_info" | jsonfilter -e '@.status')" echo " Address: $(echo "$peer_info" | jsonfilter -e '@.address')" echo " Role: $(echo "$peer_info" | jsonfilter -e '@.role')" fi fi # Try to ping local addr=$(get_node_address "$node_id") if [ -n "$addr" ]; then echo "" echo "Connectivity:" if ping -c 1 -W 2 "$addr" >/dev/null 2>&1; then echo " Ping: OK" else echo " Ping: FAILED" fi fi } get_node_address() { local node_id="$1" # Check UCI for known nodes config_load rtty-remote local addr="" config_get addr "node_${node_id}" address "" [ -n "$addr" ] && { echo "$addr"; return; } # Check master-link if command -v ml_peer_list >/dev/null 2>&1; then addr=$(ml_peer_list 2>/dev/null | jsonfilter -e "@.peers[?(@.fingerprint=='$node_id')].address" 2>/dev/null) [ -n "$addr" ] && { echo "$addr"; return; } fi # Fallback: assume node_id is an IP echo "$node_id" } #------------------------------------------------------------------------------ # RPCD Proxy - Core Feature #------------------------------------------------------------------------------ cmd_rpc() { local node_id="$1" local object="$2" local method="$3" shift 3 local params="$*" [ -z "$node_id" ] || [ -z "$object" ] || [ -z "$method" ] && { echo "Usage: rttyctl rpc [params_json]" echo "" echo "Examples:" echo " rttyctl rpc 10.100.0.2 luci.system-hub status" echo " rttyctl rpc sb-01 luci.haproxy vhost_list" echo " rttyctl rpc 192.168.255.2 system board" exit 1 } local addr=$(get_node_address "$node_id") [ -z "$addr" ] && die "Cannot resolve node address for: $node_id" if [ -z "$params" ] || [ "$params" = "{}" ]; then params="{}" fi log "info" "RPC call to $addr: $object.$method" # Check if local address - use direct ubus for full access local is_local=0 case "$addr" in 127.0.0.1|localhost|192.168.255.1|$(uci -q get network.lan.ipaddr)) is_local=1 ;; esac local result="" if [ "$is_local" = "1" ]; then # Use direct ubus call for local node (full access) result=$(ubus call "$object" "$method" "$params" 2>&1) local ubus_rc=$? if [ $ubus_rc -ne 0 ]; then die "ubus error: $result" fi else # Remote node - use HTTP JSON-RPC local auth_token=$(get_auth_token "$addr") local rpc_id=$(date +%s%N | cut -c1-13) local request=$(cat << EOF {"jsonrpc":"2.0","id":$rpc_id,"method":"call","params":["$auth_token","$object","$method",$params]} EOF ) local timeout=$(get_config proxy rpc_timeout 30) local http_port=$(get_config proxy http_port 8081) local url="http://${addr}:${http_port}/ubus" local response=$(curl -s -m "$timeout" \ -H "Content-Type: application/json" \ -d "$request" \ "$url" 2>&1) local curl_rc=$? if [ $curl_rc -ne 0 ]; then die "Connection failed to $addr (curl error: $curl_rc)" fi # Check for JSON-RPC error local error=$(echo "$response" | jsonfilter -e '@.error.message' 2>/dev/null) if [ -n "$error" ]; then die "RPC error: $error" fi # Extract result result=$(echo "$response" | jsonfilter -e '@.result[1]' 2>/dev/null) fi if [ -z "$result" ]; then result=$(echo "$response" | jsonfilter -e '@.result' 2>/dev/null) fi echo "$result" } cmd_rpc_list() { local node_id="$1" [ -z "$node_id" ] && die "Usage: rttyctl rpc-list " local addr=$(get_node_address "$node_id") [ -z "$addr" ] && die "Cannot resolve node address for: $node_id" echo "RPCD Objects on $node_id ($addr)" echo "=================================" # Get session first local auth_token=$(get_auth_token "$addr") # List ubus objects json_init json_add_string "jsonrpc" "2.0" json_add_int "id" 1 json_add_string "method" "list" json_add_array "params" json_add_string "" "$auth_token" json_add_string "" "*" json_close_array local request=$(json_dump) local http_port=$(get_config proxy http_port 8081) local response=$(curl -s -m 30 \ -H "Content-Type: application/json" \ -d "$request" \ "http://${addr}:${http_port}/ubus" 2>&1) # Parse and display objects echo "$response" | jsonfilter -e '@.result[1]' 2>/dev/null | \ python3 -c "import sys,json; d=json.load(sys.stdin); [print(f' {k}') for k in sorted(d.keys())]" 2>/dev/null || \ echo "$response" } cmd_rpc_batch() { local node_id="$1" local batch_file="$2" [ -z "$node_id" ] || [ -z "$batch_file" ] && die "Usage: rttyctl rpc-batch " [ ! -f "$batch_file" ] && die "Batch file not found: $batch_file" echo "Executing batch RPC to $node_id..." local count=0 while read -r line; do [ -z "$line" ] && continue local object=$(echo "$line" | jsonfilter -e '@.object') local method=$(echo "$line" | jsonfilter -e '@.method') local params=$(echo "$line" | jsonfilter -e '@.params') echo "[$count] $object.$method" cmd_rpc "$node_id" "$object" "$method" "$params" echo "" count=$((count + 1)) done < "$batch_file" echo "Executed $count calls" } #------------------------------------------------------------------------------ # Authentication #------------------------------------------------------------------------------ get_auth_token() { local addr="$1" local cache_file="$CACHE_DIR/auth_${addr//[.:]/_}" # Check cache if [ -f "$cache_file" ]; then local cached=$(cat "$cache_file") local ts=$(echo "$cached" | cut -d: -f1) local token=$(echo "$cached" | cut -d: -f2-) local now=$(date +%s) local ttl=$(get_config main session_ttl 3600) if [ $((now - ts)) -lt $ttl ]; then echo "$token" return 0 fi fi # Get new session via login # For now, use anonymous session (00000000000000000000000000000000) # TODO: Implement proper master-link authentication local anon_token="00000000000000000000000000000000" # Try to get authenticated session json_init json_add_string "jsonrpc" "2.0" json_add_int "id" 1 json_add_string "method" "call" json_add_array "params" json_add_string "" "$anon_token" json_add_string "" "session" json_add_string "" "login" json_add_object "" json_add_string "username" "root" json_add_string "password" "" json_close_object json_close_array local request=$(json_dump) local http_port=$(get_config proxy http_port 8081) local response=$(curl -s -m 10 \ -H "Content-Type: application/json" \ -d "$request" \ "http://${addr}:${http_port}/ubus" 2>&1) local token=$(echo "$response" | jsonfilter -e '@.result[1].ubus_rpc_session' 2>/dev/null) if [ -n "$token" ] && [ "$token" != "null" ]; then echo "$(date +%s):$token" > "$cache_file" echo "$token" else # Fallback to anonymous echo "$anon_token" fi } cmd_auth() { local node_id="$1" [ -z "$node_id" ] && die "Usage: rttyctl auth " local addr=$(get_node_address "$node_id") echo "Authenticating to $node_id ($addr)..." # Clear cache to force re-auth rm -f "$CACHE_DIR/auth_${addr//[.:]/_}" 2>/dev/null local token=$(get_auth_token "$addr") if [ -n "$token" ] && [ "$token" != "00000000000000000000000000000000" ]; then echo "Authenticated successfully" echo "Session: ${token:0:16}..." else echo "Using anonymous session (limited access)" fi } cmd_revoke() { local node_id="$1" [ -z "$node_id" ] && die "Usage: rttyctl revoke " local addr=$(get_node_address "$node_id") rm -f "$CACHE_DIR/auth_${addr//[.:]/_}" 2>/dev/null echo "Revoked authentication for $node_id" } #------------------------------------------------------------------------------ # Terminal/Connection (Placeholder for RTTY) #------------------------------------------------------------------------------ cmd_connect() { local node_id="$1" [ -z "$node_id" ] && die "Usage: rttyctl connect " local addr=$(get_node_address "$node_id") echo "Connecting to $node_id ($addr)..." echo "" echo "Note: Full terminal support requires RTTY package." echo "For now, use SSH:" echo " ssh root@$addr" echo "" echo "Or use RPCD proxy:" echo " rttyctl rpc $node_id system board" } cmd_disconnect() { local session_id="$1" [ -z "$session_id" ] && die "Usage: rttyctl disconnect " echo "Session $session_id disconnected" } #------------------------------------------------------------------------------ # Session Management (Placeholder) #------------------------------------------------------------------------------ cmd_sessions() { local node_id="$1" echo "Active Sessions" echo "===============" if [ ! -f "$SESSION_DB" ]; then echo "No sessions recorded" return fi sqlite3 "$SESSION_DB" "SELECT id, node_id, type, started_at FROM sessions ORDER BY started_at DESC LIMIT 20" 2>/dev/null || \ echo "Session database not initialized" } cmd_replay() { local session_id="$1" local target_node="$2" [ -z "$session_id" ] || [ -z "$target_node" ] && die "Usage: rttyctl replay " echo "Replaying session $session_id to $target_node..." echo "Note: Session replay requires avatar-tap integration (coming soon)" } cmd_export() { local session_id="$1" [ -z "$session_id" ] && die "Usage: rttyctl export " echo "Exporting session $session_id..." echo "Note: Session export requires implementation" } #------------------------------------------------------------------------------ # Server Management #------------------------------------------------------------------------------ cmd_server() { local action="$1" case "$action" in start) /etc/init.d/rtty-remote start echo "RTTY Remote server started" ;; stop) /etc/init.d/rtty-remote stop echo "RTTY Remote server stopped" ;; status) echo "RTTY Remote Server Status" echo "=========================" local enabled=$(get_config main enabled 0) local port=$(get_config main server_port 7681) echo "Enabled: $enabled" echo "Port: $port" if pgrep -f "rttyctl server-daemon" >/dev/null 2>&1; then echo "Running: yes" else echo "Running: no" fi ;; *) echo "Usage: rttyctl server start|stop|status" ;; esac } cmd_server_daemon() { # Internal: daemon mode for procd ensure_dirs log "info" "RTTY Remote daemon starting..." # TODO: Implement HTTP server for incoming RPC proxy requests # For now, just keep the process alive while true; do sleep 60 done } #------------------------------------------------------------------------------ # JSON Output (for RPCD) #------------------------------------------------------------------------------ cmd_json_status() { json_init json_add_boolean "enabled" "$(get_config main enabled 0)" json_add_int "port" "$(get_config main server_port 7681)" if pgrep -f "rttyctl server-daemon" >/dev/null 2>&1; then json_add_boolean "running" 1 else json_add_boolean "running" 0 fi json_add_int "active_sessions" 0 json_add_int "total_nodes" 0 json_dump } cmd_json_nodes() { json_init json_add_array "nodes" # Add nodes from master-link if command -v ml_peer_list >/dev/null 2>&1; then ml_peer_list 2>/dev/null | jsonfilter -e '@.peers[*]' 2>/dev/null | while read peer; do json_add_object "" json_add_string "id" "$(echo "$peer" | jsonfilter -e '@.fingerprint')" json_add_string "name" "$(echo "$peer" | jsonfilter -e '@.name')" json_add_string "address" "$(echo "$peer" | jsonfilter -e '@.address')" json_add_string "status" "$(echo "$peer" | jsonfilter -e '@.status')" json_close_object done fi json_close_array json_dump } #------------------------------------------------------------------------------ # Help #------------------------------------------------------------------------------ show_help() { cat << 'EOF' rttyctl - SecuBox RTTY Remote Control Usage: rttyctl [options] Node Management: nodes List all mesh nodes with status node Show detailed node info connect Start terminal session to node disconnect End terminal session RPCD Proxy: rpc [params] Execute remote RPCD call rpc-list List available RPCD objects rpc-batch Execute batch RPCD calls Session Management: sessions [node_id] List active/recent sessions replay Replay captured session to node export Export session as JSON Server Control: server start Start local RTTY server server stop Stop local RTTY server server status Show server status Authentication: auth Authenticate to remote node revoke Revoke authentication Shared Access Tokens: token generate [ttl] [perms] Generate support token (default: 3600s, rpc,terminal) token list List active tokens token validate Validate a token token revoke Revoke a token token-rpc [params] Execute RPC with token auth JSON Output (for RPCD): json-status Status as JSON json-nodes Nodes as JSON Examples: rttyctl nodes rttyctl rpc 10.100.0.2 luci.system-hub status rttyctl rpc sb-01 luci.haproxy vhost_list rttyctl rpc-list 192.168.255.2 Version: $VERSION EOF } #------------------------------------------------------------------------------ # Main #------------------------------------------------------------------------------ ensure_dirs case "$1" in nodes) cmd_nodes ;; node) cmd_node "$2" ;; connect) cmd_connect "$2" ;; disconnect) cmd_disconnect "$2" ;; rpc) shift cmd_rpc "$@" ;; rpc-list) cmd_rpc_list "$2" ;; rpc-batch) cmd_rpc_batch "$2" "$3" ;; sessions) cmd_sessions "$2" ;; replay) cmd_replay "$2" "$3" ;; export) cmd_export "$2" ;; server) cmd_server "$2" ;; server-daemon) cmd_server_daemon ;; auth) cmd_auth "$2" ;; revoke) cmd_revoke "$2" ;; json-status) cmd_json_status ;; json-nodes) cmd_json_nodes ;; json-tokens) cmd_token_json_list ;; token) case "$2" in generate) cmd_token_generate "$3" "$4" ;; list) cmd_token_list ;; validate) cmd_token_validate "$3" ;; revoke) cmd_token_revoke "$3" ;; *) echo "Usage: rttyctl token generate|list|validate|revoke" ;; esac ;; token-rpc) shift cmd_token_rpc "$@" ;; -h|--help|help) show_help ;; *) show_help exit 1 ;; esac