feat(secubox-master-link): Add CLI tools and secubox-deb API prompt
Add sbx-mesh-invite and sbx-mesh-join CLI tools to secubox-master-link: - sbx-mesh-invite: Generate invite tokens with URL output (for masters) - sbx-mesh-join: Join mesh with token (for peers), uses HTTPS Add .claude/prompts/secubox-deb-masterlink.md: - API specification for implementing master-link on secubox-deb (VM) - Endpoints: status, invite, join, peers, approve, cleanup - Data structures for tokens.json and peers.json - Integration notes for existing LuCI UI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6f9dd3aa17
commit
39fe2aced4
240
.claude/prompts/secubox-deb-masterlink.md
Normal file
240
.claude/prompts/secubox-deb-masterlink.md
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
# SecuBox-Deb Master-Link API Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Implement the Master-Link mesh enrollment API for SecuBox-Deb (Debian/Ubuntu VM version). This allows the VM to act as a **master node** that can onboard OpenWrt peer nodes into the mesh network.
|
||||||
|
|
||||||
|
The API should be added to the existing P2P FastAPI service running at `/run/secubox/p2p.sock` and exposed via nginx at `https://<host>/api/v1/p2p/master-link/*`.
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
- P2P service: `/usr/bin/uvicorn api.main:app --uds /run/secubox/p2p.sock`
|
||||||
|
- Token storage: `/var/lib/secubox/p2p/master-link/tokens.json`
|
||||||
|
- Peer storage: `/var/lib/secubox/p2p/master-link/peers.json`
|
||||||
|
- The existing VM already has partial master-link support
|
||||||
|
|
||||||
|
## Required API Endpoints
|
||||||
|
|
||||||
|
### 1. Status Endpoint
|
||||||
|
```
|
||||||
|
GET /master-link/status
|
||||||
|
```
|
||||||
|
Returns mesh status:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"role": "master",
|
||||||
|
"depth": 0,
|
||||||
|
"max_depth": 3,
|
||||||
|
"upstream": null,
|
||||||
|
"fingerprint": "sb-<unique-id>",
|
||||||
|
"hostname": "secubox-vm-x64",
|
||||||
|
"auto_approve": false,
|
||||||
|
"peers": {
|
||||||
|
"pending": 0,
|
||||||
|
"approved": 3,
|
||||||
|
"rejected": 0,
|
||||||
|
"total": 3
|
||||||
|
},
|
||||||
|
"active_tokens": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Generate Invite Token
|
||||||
|
```
|
||||||
|
POST /master-link/invite
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"auto_approve": true,
|
||||||
|
"ttl": 3600
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Returns:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "abc123def456...",
|
||||||
|
"hash": "sha256-hash-of-token",
|
||||||
|
"expires": "2026-03-26T16:00:00Z",
|
||||||
|
"expires_ts": 1774540800,
|
||||||
|
"ttl": 3600,
|
||||||
|
"auto_approve": true,
|
||||||
|
"url": "https://192.168.255.200/master-link/?token=abc123def456..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Token Generation Logic:**
|
||||||
|
```python
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
token = secrets.token_hex(16) # 32 char hex string
|
||||||
|
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
expires = datetime.now() + timedelta(seconds=ttl)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Join Endpoint (for peers)
|
||||||
|
```
|
||||||
|
POST /master-link/join
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"token": "abc123def456...",
|
||||||
|
"fingerprint": "owrt-0050430d1918",
|
||||||
|
"hostname": "C3BOX",
|
||||||
|
"address": "192.168.255.1",
|
||||||
|
"model": "Globalscale MOCHAbin"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation Flow:**
|
||||||
|
1. Hash incoming token: `sha256(token)`
|
||||||
|
2. Find matching token in `tokens.json` by hash
|
||||||
|
3. Check token status is "active" and not expired
|
||||||
|
4. If `auto_approve` is true, immediately approve
|
||||||
|
5. Otherwise, queue for manual approval
|
||||||
|
|
||||||
|
**Success Response (auto-approved):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "approved",
|
||||||
|
"fingerprint": "owrt-0050430d1918",
|
||||||
|
"message": "Welcome to the mesh",
|
||||||
|
"master_fingerprint": "sb-test123456",
|
||||||
|
"depth": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success Response (pending):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "pending",
|
||||||
|
"fingerprint": "owrt-0050430d1918",
|
||||||
|
"message": "Awaiting master approval"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
```json
|
||||||
|
{"status": "error", "message": "Invalid or expired token"}
|
||||||
|
{"status": "error", "message": "Token already used"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. List Peers
|
||||||
|
```
|
||||||
|
GET /master-link/peers
|
||||||
|
```
|
||||||
|
Returns:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"peers": [
|
||||||
|
{
|
||||||
|
"fingerprint": "owrt-0050430d1918",
|
||||||
|
"hostname": "C3BOX",
|
||||||
|
"address": "192.168.255.1",
|
||||||
|
"model": "Globalscale MOCHAbin",
|
||||||
|
"status": "approved",
|
||||||
|
"joined_at": "2026-03-26T14:00:32.721532",
|
||||||
|
"depth": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Approve/Reject Peer
|
||||||
|
```
|
||||||
|
POST /master-link/approve
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"fingerprint": "owrt-0050430d1918",
|
||||||
|
"action": "approve" // or "reject"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Cleanup Tokens
|
||||||
|
```
|
||||||
|
POST /master-link/cleanup
|
||||||
|
```
|
||||||
|
Removes expired and used tokens.
|
||||||
|
|
||||||
|
## Data Structures
|
||||||
|
|
||||||
|
### tokens.json
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"hash": "sha256-of-token",
|
||||||
|
"type": "join",
|
||||||
|
"created": "2026-03-26T11:44:54.033842",
|
||||||
|
"expires": "2026-03-26T12:44:54.033842",
|
||||||
|
"expires_ts": 1774529094,
|
||||||
|
"ttl": 3600,
|
||||||
|
"status": "active", // active, used, expired
|
||||||
|
"auto_approve": true,
|
||||||
|
"peer_fp": null, // filled when used
|
||||||
|
"used_by": null,
|
||||||
|
"used_at": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### peers.json
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"fingerprint": "owrt-0050430d1918",
|
||||||
|
"hostname": "C3BOX",
|
||||||
|
"address": "192.168.255.1",
|
||||||
|
"model": "Globalscale MOCHAbin",
|
||||||
|
"status": "approved",
|
||||||
|
"token_hash": "abc123...",
|
||||||
|
"joined_at": "2026-03-26T14:00:32.721532",
|
||||||
|
"depth": 1,
|
||||||
|
"last_seen": "2026-03-26T15:30:00.000000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Tools (for reference)
|
||||||
|
|
||||||
|
The OpenWrt side has these CLI tools that interact with this API:
|
||||||
|
|
||||||
|
### sbx-mesh-invite (on master)
|
||||||
|
Generates invite token and outputs join URL/command.
|
||||||
|
|
||||||
|
### sbx-mesh-join (on peer)
|
||||||
|
Joins a mesh by sending join request with token.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On master (VM)
|
||||||
|
sbx-mesh-invite --ip 192.168.255.200
|
||||||
|
# Output: Token and join URL
|
||||||
|
|
||||||
|
# On peer (OpenWrt)
|
||||||
|
sbx-mesh-join 192.168.255.200 <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
1. **HTTPS Required**: The join endpoint uses HTTPS (port 443), not HTTP port 7331
|
||||||
|
2. **Self-signed Certs**: Peers use `--no-check-certificate` (wget) or `-k` (curl)
|
||||||
|
3. **Token Security**: Tokens are one-time use; mark as "used" immediately upon successful join
|
||||||
|
4. **Auto-approve**: When `auto_approve=true`, skip manual approval step
|
||||||
|
5. **Fingerprint**: Use unique device identifier (MAC-based for OpenWrt, random for VMs)
|
||||||
|
|
||||||
|
## Integration with Existing UI
|
||||||
|
|
||||||
|
The existing LuCI UI at `/admin/services/secubox-mesh` shows:
|
||||||
|
- Node status (Role, Fingerprint, Peers, Chain)
|
||||||
|
- ZKP Authentication section
|
||||||
|
- Generate Token / Cleanup Tokens buttons
|
||||||
|
|
||||||
|
These buttons should call the API endpoints above.
|
||||||
|
|
||||||
|
## File Locations (secubox-deb)
|
||||||
|
|
||||||
|
- API source: `/srv/secubox/api/routers/master_link.py` (to create)
|
||||||
|
- Data dir: `/var/lib/secubox/p2p/master-link/`
|
||||||
|
- Config: `/etc/secubox/master-link.yaml`
|
||||||
158
package/secubox/secubox-master-link/files/usr/bin/sbx-mesh-invite
Executable file
158
package/secubox/secubox-master-link/files/usr/bin/sbx-mesh-invite
Executable file
@ -0,0 +1,158 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SecuBox Mesh Invite Generator
|
||||||
|
# Generates a join URL that can be shared with peer nodes
|
||||||
|
# Usage: sbx-mesh-invite [--auto-approve] [--ttl SECONDS]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TOKENS_FILE="/var/lib/secubox/p2p/master-link/tokens.json"
|
||||||
|
AUTO_APPROVE="true"
|
||||||
|
TTL=3600
|
||||||
|
MASTER_IP=""
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--auto-approve|-a)
|
||||||
|
AUTO_APPROVE="true"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--manual-approve|-m)
|
||||||
|
AUTO_APPROVE="false"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--ttl|-t)
|
||||||
|
TTL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ip|-i)
|
||||||
|
MASTER_IP="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
cat << 'EOF'
|
||||||
|
SecuBox Mesh Invite Generator
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
sbx-mesh-invite [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-a, --auto-approve Auto-approve joining nodes (default)
|
||||||
|
-m, --manual-approve Require manual approval
|
||||||
|
-t, --ttl SECONDS Token validity in seconds (default: 3600)
|
||||||
|
-i, --ip ADDRESS Master IP address for the invite URL
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Output:
|
||||||
|
Prints a join URL that can be copy-pasted to peer nodes.
|
||||||
|
On the peer, run: sbx-mesh-join <URL>
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Ensure tokens directory exists
|
||||||
|
mkdir -p "$(dirname "$TOKENS_FILE")"
|
||||||
|
|
||||||
|
# Generate random token (32 hex chars)
|
||||||
|
if command -v openssl >/dev/null; then
|
||||||
|
TOKEN=$(openssl rand -hex 16)
|
||||||
|
elif [ -r /dev/urandom ]; then
|
||||||
|
TOKEN=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
||||||
|
else
|
||||||
|
# Fallback: use date + random
|
||||||
|
TOKEN=$(date +%s%N | sha256sum | head -c 32)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Calculate hash
|
||||||
|
if command -v sha256sum >/dev/null; then
|
||||||
|
HASH=$(printf '%s' "$TOKEN" | sha256sum | cut -d' ' -f1)
|
||||||
|
elif command -v openssl >/dev/null; then
|
||||||
|
HASH=$(printf '%s' "$TOKEN" | openssl dgst -sha256 | awk '{print $2}')
|
||||||
|
else
|
||||||
|
echo "Error: No sha256 tool available" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get timestamps
|
||||||
|
NOW=$(date -Iseconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S)
|
||||||
|
NOW_TS=$(date +%s)
|
||||||
|
EXPIRES_TS=$((NOW_TS + TTL))
|
||||||
|
EXPIRES=$(date -Iseconds -d "@$EXPIRES_TS" 2>/dev/null || date -r "$EXPIRES_TS" -Iseconds 2>/dev/null || echo "$EXPIRES_TS")
|
||||||
|
|
||||||
|
# Get local IP for URL
|
||||||
|
LOCAL_IP="$MASTER_IP"
|
||||||
|
if [ -z "$LOCAL_IP" ]; then
|
||||||
|
# Try common LAN interfaces first, prefer 192.168.x.x addresses
|
||||||
|
for iface in br-lan eth0 enp0s8 enp0s3 ens3; do
|
||||||
|
candidate=$(ip -4 addr show "$iface" 2>/dev/null | grep -oE 'inet 192\.168\.[0-9.]+' | awk '{print $2}' | head -1)
|
||||||
|
if [ -n "$candidate" ]; then
|
||||||
|
LOCAL_IP="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Fallback to any non-loopback, non-10.x address
|
||||||
|
if [ -z "$LOCAL_IP" ]; then
|
||||||
|
LOCAL_IP=$(ip -4 addr show 2>/dev/null | grep -oE 'inet [0-9.]+' | grep -v '127.0.0' | grep -v 'inet 10\.' | awk '{print $2}' | head -1)
|
||||||
|
fi
|
||||||
|
[ -z "$LOCAL_IP" ] && LOCAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
|
[ -z "$LOCAL_IP" ] && LOCAL_IP="<MASTER_IP>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create new token entry
|
||||||
|
NEW_TOKEN="{
|
||||||
|
\"hash\": \"$HASH\",
|
||||||
|
\"type\": \"join\",
|
||||||
|
\"created\": \"$NOW\",
|
||||||
|
\"expires\": \"$EXPIRES\",
|
||||||
|
\"expires_ts\": $EXPIRES_TS,
|
||||||
|
\"ttl\": $TTL,
|
||||||
|
\"status\": \"active\",
|
||||||
|
\"auto_approve\": $AUTO_APPROVE
|
||||||
|
}"
|
||||||
|
|
||||||
|
# Add to tokens file
|
||||||
|
if [ -f "$TOKENS_FILE" ]; then
|
||||||
|
# Append to existing array using Python or jq
|
||||||
|
if command -v python3 >/dev/null; then
|
||||||
|
python3 << PYEOF
|
||||||
|
import json
|
||||||
|
with open("$TOKENS_FILE", "r") as f:
|
||||||
|
tokens = json.load(f)
|
||||||
|
tokens.append(json.loads('''$NEW_TOKEN'''))
|
||||||
|
with open("$TOKENS_FILE", "w") as f:
|
||||||
|
json.dump(tokens, f, indent=2)
|
||||||
|
PYEOF
|
||||||
|
elif command -v jq >/dev/null; then
|
||||||
|
tmp=$(mktemp)
|
||||||
|
jq ". + [$NEW_TOKEN]" "$TOKENS_FILE" > "$tmp" && mv "$tmp" "$TOKENS_FILE"
|
||||||
|
else
|
||||||
|
# Manual append (risky but works for simple cases)
|
||||||
|
sed -i 's/]$/,'"$(echo "$NEW_TOKEN" | tr -d '\n')"']/' "$TOKENS_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[$NEW_TOKEN]" > "$TOKENS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Output the invite URL
|
||||||
|
echo ""
|
||||||
|
echo "Mesh Invite Generated"
|
||||||
|
echo "====================="
|
||||||
|
echo ""
|
||||||
|
echo "Token: $TOKEN"
|
||||||
|
echo "Expires: $EXPIRES"
|
||||||
|
echo "Auto-approve: $AUTO_APPROVE"
|
||||||
|
echo ""
|
||||||
|
echo "Join URL (copy this to the peer node):"
|
||||||
|
echo ""
|
||||||
|
echo " https://${LOCAL_IP}/master-link/?token=${TOKEN}"
|
||||||
|
echo ""
|
||||||
|
echo "Or run on the peer:"
|
||||||
|
echo ""
|
||||||
|
echo " sbx-mesh-join ${LOCAL_IP} ${TOKEN}"
|
||||||
|
echo ""
|
||||||
236
package/secubox/secubox-master-link/files/usr/bin/sbx-mesh-join
Executable file
236
package/secubox/secubox-master-link/files/usr/bin/sbx-mesh-join
Executable file
@ -0,0 +1,236 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SecuBox Mesh Join CLI Tool
|
||||||
|
# Usage: sbx-mesh-join <master-ip> <token>
|
||||||
|
# or: sbx-mesh-join <invite-url>
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
NODE_ID_FILE="/etc/secubox/node.id"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
log_info() { printf "${GREEN}[+]${NC} %s\n" "$1"; }
|
||||||
|
log_warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; }
|
||||||
|
log_error() { printf "${RED}[-]${NC} %s\n" "$1"; }
|
||||||
|
log_step() { printf "${CYAN}[*]${NC} %s\n" "$1"; }
|
||||||
|
|
||||||
|
# Get or generate node fingerprint
|
||||||
|
get_fingerprint() {
|
||||||
|
if [ -f "$NODE_ID_FILE" ]; then
|
||||||
|
cat "$NODE_ID_FILE"
|
||||||
|
else
|
||||||
|
mkdir -p /etc/secubox
|
||||||
|
local mac=""
|
||||||
|
if [ -f /sys/class/net/br-lan/address ]; then
|
||||||
|
mac=$(cat /sys/class/net/br-lan/address | tr -d ':')
|
||||||
|
elif [ -f /sys/class/net/eth0/address ]; then
|
||||||
|
mac=$(cat /sys/class/net/eth0/address | tr -d ':')
|
||||||
|
else
|
||||||
|
mac=$(cat /sys/class/net/*/address 2>/dev/null | head -1 | tr -d ':')
|
||||||
|
fi
|
||||||
|
local fp="owrt-${mac}"
|
||||||
|
echo "$fp" > "$NODE_ID_FILE"
|
||||||
|
echo "$fp"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get local IP
|
||||||
|
get_local_ip() {
|
||||||
|
ip -4 addr show br-lan 2>/dev/null | grep -oE 'inet [0-9.]+' | awk '{print $2}' | head -1 || \
|
||||||
|
ip -4 addr show eth0 2>/dev/null | grep -oE 'inet [0-9.]+' | awk '{print $2}' | head -1 || \
|
||||||
|
echo "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse URL to extract IP and token
|
||||||
|
parse_url() {
|
||||||
|
local url="$1"
|
||||||
|
# Match: http(s)://IP(:PORT)/...?token=XXX
|
||||||
|
echo "$url" | sed -n 's|.*://\([^:/]*\).*[?&]token=\([^&]*\).*|\1 \2|p'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
usage() {
|
||||||
|
cat << 'EOF'
|
||||||
|
SecuBox Mesh Join Tool
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
sbx-mesh-join <master-ip> <token>
|
||||||
|
sbx-mesh-join <invite-url>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
sbx-mesh-join 192.168.1.1 abc123def456
|
||||||
|
sbx-mesh-join 'https://192.168.1.1/master-link/?token=abc123'
|
||||||
|
sbx-mesh-join 'https://master.local/master-link/?token=abc123'
|
||||||
|
|
||||||
|
The tool will:
|
||||||
|
1. Generate a unique node fingerprint (if not exists)
|
||||||
|
2. Collect local device info (hostname, IP, model)
|
||||||
|
3. Send join request to the master
|
||||||
|
4. Save mesh configuration on success
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
main() {
|
||||||
|
local master_ip=""
|
||||||
|
local token=""
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
case "$#" in
|
||||||
|
1)
|
||||||
|
# Single argument - could be URL
|
||||||
|
local parsed=$(parse_url "$1")
|
||||||
|
if [ -n "$parsed" ]; then
|
||||||
|
master_ip=$(echo "$parsed" | awk '{print $1}')
|
||||||
|
token=$(echo "$parsed" | awk '{print $2}')
|
||||||
|
else
|
||||||
|
log_error "Invalid URL format"
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
# Two arguments: IP and token
|
||||||
|
master_ip="$1"
|
||||||
|
token="$2"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "$master_ip" ] || [ -z "$token" ]; then
|
||||||
|
log_error "Missing master IP or token"
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "SecuBox Mesh Join"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Gather local info
|
||||||
|
log_step "Collecting node information..."
|
||||||
|
local fingerprint=$(get_fingerprint)
|
||||||
|
local hostname=$(uci -q get system.@system[0].hostname 2>/dev/null || hostname || echo "openwrt")
|
||||||
|
local address=$(get_local_ip)
|
||||||
|
local model=""
|
||||||
|
if [ -f /tmp/sysinfo/model ]; then
|
||||||
|
model=$(cat /tmp/sysinfo/model)
|
||||||
|
elif command -v jsonfilter >/dev/null && [ -f /etc/board.json ]; then
|
||||||
|
model=$(jsonfilter -i /etc/board.json -e '@.model.name' 2>/dev/null || echo "Unknown")
|
||||||
|
else
|
||||||
|
model="Unknown"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Fingerprint: $fingerprint"
|
||||||
|
log_info "Hostname: $hostname"
|
||||||
|
log_info "Address: $address"
|
||||||
|
log_info "Model: $model"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prepare JSON payload
|
||||||
|
local payload="{\"token\":\"${token}\",\"fingerprint\":\"${fingerprint}\",\"hostname\":\"${hostname}\",\"address\":\"${address}\",\"model\":\"${model}\"}"
|
||||||
|
|
||||||
|
log_step "Connecting to master at ${master_ip}..."
|
||||||
|
|
||||||
|
# Send join request (HTTPS with self-signed cert support)
|
||||||
|
local response=""
|
||||||
|
local api_url="https://${master_ip}/api/v1/p2p/master-link/join"
|
||||||
|
|
||||||
|
if command -v curl >/dev/null; then
|
||||||
|
response=$(curl -sf -k --connect-timeout 30 -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
"$api_url" 2>/dev/null)
|
||||||
|
else
|
||||||
|
response=$(wget -qO- --no-check-certificate --timeout=30 \
|
||||||
|
--header="Content-Type: application/json" \
|
||||||
|
--post-data="$payload" \
|
||||||
|
"$api_url" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$response" ]; then
|
||||||
|
log_error "Failed to connect to master"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse response
|
||||||
|
local status=""
|
||||||
|
local message=""
|
||||||
|
local master_fp=""
|
||||||
|
local depth=""
|
||||||
|
|
||||||
|
if command -v jsonfilter >/dev/null; then
|
||||||
|
status=$(echo "$response" | jsonfilter -e '@.status' 2>/dev/null || true)
|
||||||
|
message=$(echo "$response" | jsonfilter -e '@.message' 2>/dev/null || true)
|
||||||
|
master_fp=$(echo "$response" | jsonfilter -e '@.master_fingerprint' 2>/dev/null || true)
|
||||||
|
depth=$(echo "$response" | jsonfilter -e '@.depth' 2>/dev/null || true)
|
||||||
|
elif command -v jq >/dev/null; then
|
||||||
|
status=$(echo "$response" | jq -r '.status // empty')
|
||||||
|
message=$(echo "$response" | jq -r '.message // empty')
|
||||||
|
master_fp=$(echo "$response" | jq -r '.master_fingerprint // empty')
|
||||||
|
depth=$(echo "$response" | jq -r '.depth // empty')
|
||||||
|
else
|
||||||
|
# Fallback: grep for status
|
||||||
|
status=$(echo "$response" | grep -oE '"status"\s*:\s*"[^"]*"' | cut -d'"' -f4)
|
||||||
|
message=$(echo "$response" | grep -oE '"message"\s*:\s*"[^"]*"' | cut -d'"' -f4)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
case "$status" in
|
||||||
|
approved)
|
||||||
|
log_info "Successfully joined mesh network!"
|
||||||
|
[ -n "$master_fp" ] && log_info "Master fingerprint: $master_fp"
|
||||||
|
[ -n "$depth" ] && log_info "Network depth: $depth"
|
||||||
|
|
||||||
|
# Save to UCI if available
|
||||||
|
if command -v uci >/dev/null; then
|
||||||
|
log_step "Saving configuration..."
|
||||||
|
# Ensure config file and section exist
|
||||||
|
[ -f /etc/config/masterlink ] || touch /etc/config/masterlink
|
||||||
|
uci -q get masterlink.settings >/dev/null || uci set masterlink.settings='mesh'
|
||||||
|
uci set masterlink.settings.enabled='1'
|
||||||
|
uci set masterlink.settings.role='peer'
|
||||||
|
uci set masterlink.settings.status='approved'
|
||||||
|
uci set masterlink.settings.master_ip="$master_ip"
|
||||||
|
[ -n "$master_fp" ] && uci set masterlink.settings.master_fingerprint="$master_fp"
|
||||||
|
[ -n "$depth" ] && uci set masterlink.settings.depth="$depth"
|
||||||
|
uci set masterlink.settings.joined_at="$(date -Iseconds 2>/dev/null || date)"
|
||||||
|
uci commit masterlink
|
||||||
|
log_info "Configuration saved"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
pending)
|
||||||
|
log_warn "Join request submitted - waiting for master approval"
|
||||||
|
log_info "Check back later or ask the master admin to approve your node"
|
||||||
|
|
||||||
|
# Save pending state
|
||||||
|
if command -v uci >/dev/null; then
|
||||||
|
[ -f /etc/config/masterlink ] || touch /etc/config/masterlink
|
||||||
|
uci -q get masterlink.settings >/dev/null || uci set masterlink.settings='mesh'
|
||||||
|
uci set masterlink.settings.enabled='1'
|
||||||
|
uci set masterlink.settings.role='peer'
|
||||||
|
uci set masterlink.settings.status='pending'
|
||||||
|
uci set masterlink.settings.master_ip="$master_ip"
|
||||||
|
uci commit masterlink
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
error|rejected)
|
||||||
|
log_error "Join failed: ${message:-Unknown error}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "Unexpected response: $response"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "Done"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Loading…
Reference in New Issue
Block a user