diff --git a/package/secubox/luci-app-masterlink/Makefile b/package/secubox/luci-app-masterlink/Makefile deleted file mode 100644 index 42c8ff03..00000000 --- a/package/secubox/luci-app-masterlink/Makefile +++ /dev/null @@ -1,60 +0,0 @@ -include $(TOPDIR)/rules.mk - -PKG_NAME:=luci-app-masterlink -PKG_VERSION:=1.0.0 -PKG_RELEASE:=1 - -PKG_MAINTAINER:=SecuBox -PKG_LICENSE:=MIT - -LUCI_TITLE:=SecuBox Master-Link Mesh Client -LUCI_DEPENDS:=+luci-base +wget +jsonfilter - -include $(INCLUDE_DIR)/package.mk - -define Package/luci-app-masterlink - SECTION:=luci - CATEGORY:=LuCI - SUBMENU:=3. Applications - TITLE:=$(LUCI_TITLE) - DEPENDS:=$(LUCI_DEPENDS) -endef - -define Package/luci-app-masterlink/description - SecuBox Master-Link client for joining SecuBox mesh networks. - Provides CLI tool and LuCI web interface for mesh enrollment. -endef - -define Package/luci-app-masterlink/install - $(INSTALL_DIR) $(1)/usr/libexec/rpcd - $(INSTALL_BIN) ./files/luci.masterlink $(1)/usr/libexec/rpcd/ - - $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d - $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-masterlink.json $(1)/usr/share/rpcd/acl.d/ - - $(INSTALL_DIR) $(1)/usr/share/luci/menu.d - $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-masterlink.json $(1)/usr/share/luci/menu.d/ - - $(INSTALL_DIR) $(1)/www/luci-static/resources/view/masterlink - $(INSTALL_DATA) ./htdocs/luci-static/resources/view/masterlink/*.js $(1)/www/luci-static/resources/view/masterlink/ - - $(INSTALL_DIR) $(1)/etc/config - $(INSTALL_CONF) ./files/masterlink.config $(1)/etc/config/masterlink - - $(INSTALL_DIR) $(1)/usr/bin - $(INSTALL_BIN) ./root/usr/bin/sbx-mesh-join $(1)/usr/bin/ -endef - -define Package/luci-app-masterlink/postinst -#!/bin/sh -[ -n "$${IPKG_INSTROOT}" ] || { - # Restart rpcd to load new handler - /etc/init.d/rpcd restart - - # Clear LuCI caches - rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null -} -exit 0 -endef - -$(eval $(call BuildPackage,luci-app-masterlink)) diff --git a/package/secubox/luci-app-masterlink/files/luci.masterlink b/package/secubox/luci-app-masterlink/files/luci.masterlink deleted file mode 100644 index 735dcb1e..00000000 --- a/package/secubox/luci-app-masterlink/files/luci.masterlink +++ /dev/null @@ -1,220 +0,0 @@ -#!/bin/sh -# SecuBox Master-Link RPCD handler -# Provides ubus interface for mesh enrollment - -. /usr/share/libubox/jshn.sh - -NODE_ID_FILE="/etc/secubox/node.id" -CONFIG_FILE="/etc/config/masterlink" - -# Generate or retrieve node fingerprint -get_fingerprint() { - if [ -f "$NODE_ID_FILE" ]; then - cat "$NODE_ID_FILE" - else - mkdir -p /etc/secubox - local mac="" - # Try br-lan first (OpenWRT), then eth0 - 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 primary LAN IP address -get_local_ip() { - local ip="" - # Try br-lan first (standard OpenWRT) - ip=$(ip -4 addr show br-lan 2>/dev/null | grep -oE 'inet [0-9.]+' | awk '{print $2}' | head -1) - if [ -z "$ip" ]; then - # Fallback to eth0 - ip=$(ip -4 addr show eth0 2>/dev/null | grep -oE 'inet [0-9.]+' | awk '{print $2}' | head -1) - fi - echo "$ip" -} - -# Get device model -get_model() { - if [ -f /tmp/sysinfo/model ]; then - cat /tmp/sysinfo/model - elif [ -f /etc/board.json ]; then - jsonfilter -i /etc/board.json -e '@.model.name' 2>/dev/null - else - echo "Unknown" - fi -} - -# Join a mesh network -do_join() { - local master_ip="$1" - local token="$2" - local fingerprint=$(get_fingerprint) - local hostname=$(uci -q get system.@system[0].hostname || echo "openwrt") - local address=$(get_local_ip) - local model=$(get_model) - - # Prepare JSON payload - local payload="{\"token\":\"${token}\",\"fingerprint\":\"${fingerprint}\",\"hostname\":\"${hostname}\",\"address\":\"${address}\",\"model\":\"${model}\"}" - - # Call master API - local response=$(wget -qO- --post-data="$payload" \ - --header="Content-Type: application/json" \ - --timeout=30 \ - "http://${master_ip}:7331/api/v1/p2p/master-link/join" 2>/dev/null) - - if [ -z "$response" ]; then - json_init - json_add_string "status" "error" - json_add_string "message" "Failed to connect to master" - json_dump - return - fi - - # Parse response status - local status=$(echo "$response" | jsonfilter -e '@.status' 2>/dev/null) - local master_fp=$(echo "$response" | jsonfilter -e '@.master_fingerprint' 2>/dev/null) - local depth=$(echo "$response" | jsonfilter -e '@.depth' 2>/dev/null) - - case "$status" in - approved|pending) - # Save configuration - uci set masterlink.settings.enabled='1' - uci set masterlink.settings.role='peer' - uci set masterlink.settings.master_ip="$master_ip" - uci set masterlink.settings.status="$status" - [ -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)" - uci commit masterlink - ;; - esac - - # Return original response - echo "$response" -} - -# Leave current mesh -do_leave() { - local master_ip=$(uci -q get masterlink.settings.master_ip) - local fingerprint=$(get_fingerprint) - - # Notify master if connected - if [ -n "$master_ip" ]; then - wget -qO- --post-data="{\"fingerprint\":\"${fingerprint}\"}" \ - --header="Content-Type: application/json" \ - --timeout=10 \ - "http://${master_ip}:7331/api/v1/p2p/master-link/leave" 2>/dev/null || true - fi - - # Clear local configuration - uci set masterlink.settings.enabled='0' - uci set masterlink.settings.role='standalone' - uci set masterlink.settings.status='disconnected' - uci delete masterlink.settings.master_ip 2>/dev/null - uci delete masterlink.settings.master_fingerprint 2>/dev/null - uci delete masterlink.settings.depth 2>/dev/null - uci delete masterlink.settings.joined_at 2>/dev/null - uci commit masterlink - - json_init - json_add_string "status" "ok" - json_add_string "message" "Left mesh network" - json_dump -} - -# Get current status -do_status() { - local enabled=$(uci -q get masterlink.settings.enabled || echo '0') - local role=$(uci -q get masterlink.settings.role || echo 'standalone') - local status=$(uci -q get masterlink.settings.status || echo 'disconnected') - local master_ip=$(uci -q get masterlink.settings.master_ip) - local master_fp=$(uci -q get masterlink.settings.master_fingerprint) - local depth=$(uci -q get masterlink.settings.depth || echo '0') - local joined_at=$(uci -q get masterlink.settings.joined_at) - - json_init - json_add_boolean "enabled" "$enabled" - json_add_string "role" "$role" - json_add_string "status" "$status" - json_add_string "master_ip" "$master_ip" - json_add_string "master_fingerprint" "$master_fp" - json_add_int "depth" "$depth" - json_add_string "joined_at" "$joined_at" - json_add_string "fingerprint" "$(get_fingerprint)" - json_dump -} - -# Get local node info -do_info() { - json_init - json_add_string "fingerprint" "$(get_fingerprint)" - json_add_string "hostname" "$(uci -q get system.@system[0].hostname || echo 'openwrt')" - json_add_string "address" "$(get_local_ip)" - json_add_string "model" "$(get_model)" - json_add_string "firmware" "$(cat /etc/openwrt_release 2>/dev/null | grep DISTRIB_RELEASE | cut -d= -f2 | tr -d \"\')" - json_dump -} - -# Verify master before joining (get master info without committing) -do_verify() { - local master_ip="$1" - local token="$2" - - # Request master info - local response=$(wget -qO- \ - --timeout=10 \ - "http://${master_ip}:7331/api/v1/p2p/master-link/info?token=${token}" 2>/dev/null) - - if [ -z "$response" ]; then - json_init - json_add_string "status" "error" - json_add_string "message" "Failed to connect to master" - json_dump - return - fi - - echo "$response" -} - -case "$1" in - list) - echo '{"status":{},"join":{"master_ip":"str","token":"str"},"leave":{},"info":{},"verify":{"master_ip":"str","token":"str"}}' - ;; - call) - case "$2" in - status) - do_status - ;; - join) - read -r input - json_load "$input" - json_get_var master_ip master_ip - json_get_var token token - do_join "$master_ip" "$token" - ;; - leave) - do_leave - ;; - info) - do_info - ;; - verify) - read -r input - json_load "$input" - json_get_var master_ip master_ip - json_get_var token token - do_verify "$master_ip" "$token" - ;; - *) - echo '{"error":"Unknown method"}' - ;; - esac - ;; -esac diff --git a/package/secubox/luci-app-masterlink/files/masterlink.config b/package/secubox/luci-app-masterlink/files/masterlink.config deleted file mode 100644 index 3aece4b6..00000000 --- a/package/secubox/luci-app-masterlink/files/masterlink.config +++ /dev/null @@ -1,8 +0,0 @@ -config mesh 'settings' - option enabled '0' - option role 'standalone' - option status 'disconnected' - option master_ip '' - option master_fingerprint '' - option depth '0' - option joined_at '' diff --git a/package/secubox/luci-app-masterlink/htdocs/luci-static/resources/view/masterlink/join.js b/package/secubox/luci-app-masterlink/htdocs/luci-static/resources/view/masterlink/join.js deleted file mode 100644 index 932ee9f1..00000000 --- a/package/secubox/luci-app-masterlink/htdocs/luci-static/resources/view/masterlink/join.js +++ /dev/null @@ -1,274 +0,0 @@ -'use strict'; -'require view'; -'require dom'; -'require poll'; -'require rpc'; -'require ui'; - -var callMasterLinkStatus = rpc.declare({ - object: 'luci.masterlink', - method: 'status', - expect: {} -}); - -var callMasterLinkInfo = rpc.declare({ - object: 'luci.masterlink', - method: 'info', - expect: {} -}); - -var callMasterLinkVerify = rpc.declare({ - object: 'luci.masterlink', - method: 'verify', - params: ['master_ip', 'token'], - expect: {} -}); - -var callMasterLinkJoin = rpc.declare({ - object: 'luci.masterlink', - method: 'join', - params: ['master_ip', 'token'], - expect: {} -}); - -var callMasterLinkLeave = rpc.declare({ - object: 'luci.masterlink', - method: 'leave', - expect: {} -}); - -// Parse invite URL to extract master IP and token -function parseInviteUrl(input) { - input = input.trim(); - - // Pattern 1: Full URL - http://IP:PORT/path?token=XXX or https://... - var urlMatch = input.match(/https?:\/\/([^:/]+)(?::\d+)?.*[?&]token=([^&\s]+)/i); - if (urlMatch) { - return { master_ip: urlMatch[1], token: urlMatch[2] }; - } - - // Pattern 2: IP and token separated by space or comma - var spaceMatch = input.match(/^([0-9.]+)[,\s]+([a-zA-Z0-9_-]+)$/); - if (spaceMatch) { - return { master_ip: spaceMatch[1], token: spaceMatch[2] }; - } - - // Pattern 3: Just a token (need to ask for IP separately or use default) - if (/^[a-zA-Z0-9_-]{16,}$/.test(input)) { - return { token: input, master_ip: null }; - } - - return null; -} - -function formatStatus(status) { - switch (status) { - case 'approved': - return E('span', { 'class': 'badge', 'style': 'background:#2e7d32;color:#fff' }, 'Connected'); - case 'pending': - return E('span', { 'class': 'badge', 'style': 'background:#f57c00;color:#fff' }, 'Pending Approval'); - case 'disconnected': - default: - return E('span', { 'class': 'badge', 'style': 'background:#616161;color:#fff' }, 'Not Connected'); - } -} - -return view.extend({ - load: function() { - return Promise.all([ - callMasterLinkStatus(), - callMasterLinkInfo() - ]); - }, - - render: function(data) { - var status = data[0] || {}; - var info = data[1] || {}; - var view = this; - - var statusSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', 'Mesh Status'), - E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td', 'style': 'width:200px' }, 'Status'), - E('td', { 'class': 'td' }, formatStatus(status.status)) - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Role'), - E('td', { 'class': 'td' }, status.role || 'standalone') - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Local Fingerprint'), - E('td', { 'class': 'td' }, E('code', {}, status.fingerprint || info.fingerprint || 'N/A')) - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Hostname'), - E('td', { 'class': 'td' }, info.hostname || 'N/A') - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'IP Address'), - E('td', { 'class': 'td' }, info.address || 'N/A') - ]) - ]) - ]); - - // If connected, show master info - if (status.enabled == 1 && status.master_ip) { - statusSection.appendChild(E('table', { 'class': 'table', 'style': 'margin-top:1em' }, [ - E('tr', { 'class': 'tr cbi-section-table-titles' }, [ - E('th', { 'class': 'th', 'colspan': 2 }, 'Connected Master') - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Master IP'), - E('td', { 'class': 'td' }, status.master_ip) - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Master Fingerprint'), - E('td', { 'class': 'td' }, E('code', {}, status.master_fingerprint || 'N/A')) - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Depth'), - E('td', { 'class': 'td' }, String(status.depth || 0)) - ]), - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, 'Joined At'), - E('td', { 'class': 'td' }, status.joined_at || 'N/A') - ]) - ])); - - // Leave button - var leaveBtn = E('button', { - 'class': 'btn cbi-button cbi-button-negative', - 'click': ui.createHandlerFn(this, function() { - return callMasterLinkLeave().then(function(res) { - if (res.status === 'ok') { - ui.addNotification(null, E('p', 'Left mesh network'), 'info'); - window.location.reload(); - } else { - ui.addNotification(null, E('p', 'Failed to leave: ' + (res.message || 'Unknown error')), 'error'); - } - }).catch(function(err) { - ui.addNotification(null, E('p', 'Error: ' + err.message), 'error'); - }); - }) - }, 'Leave Mesh'); - - statusSection.appendChild(E('div', { 'style': 'margin-top:1em' }, leaveBtn)); - } - - // Join section (only if not connected) - var joinSection = E('div', { 'class': 'cbi-section' }); - - if (status.enabled != 1) { - var inviteInput = E('input', { - 'type': 'text', - 'class': 'cbi-input-text', - 'id': 'invite-url', - 'style': 'width:100%;font-family:monospace', - 'placeholder': 'http://192.168.1.1:7331/master-link/?token=abc123... or: 192.168.1.1 abc123token' - }); - - var verifyBtn = E('button', { - 'class': 'btn cbi-button', - 'style': 'margin-right:0.5em', - 'click': ui.createHandlerFn(this, function() { - var input = document.getElementById('invite-url').value; - var parsed = parseInviteUrl(input); - - if (!parsed || !parsed.master_ip) { - ui.addNotification(null, E('p', 'Invalid invite URL or token'), 'error'); - return; - } - - return callMasterLinkVerify(parsed.master_ip, parsed.token).then(function(res) { - if (res.status === 'error') { - ui.addNotification(null, E('p', 'Verification failed: ' + (res.message || 'Unknown error')), 'error'); - } else { - var msg = E('div', {}, [ - E('p', { 'style': 'font-weight:bold' }, 'Master Node Information:'), - E('ul', {}, [ - E('li', {}, 'Hostname: ' + (res.hostname || 'N/A')), - E('li', {}, 'Fingerprint: ' + (res.fingerprint || 'N/A')), - E('li', {}, 'Network: ' + (res.network_name || 'N/A')) - ]), - E('p', { 'style': 'color:#f57c00' }, 'Verify this matches the expected master before joining!') - ]); - ui.addNotification(null, msg, 'info'); - } - }).catch(function(err) { - ui.addNotification(null, E('p', 'Verification error: ' + err.message), 'error'); - }); - }) - }, 'Verify Master'); - - var joinBtn = E('button', { - 'class': 'btn cbi-button cbi-button-positive', - 'click': ui.createHandlerFn(this, function() { - var input = document.getElementById('invite-url').value; - var parsed = parseInviteUrl(input); - - if (!parsed || !parsed.master_ip) { - ui.addNotification(null, E('p', 'Invalid invite URL or token'), 'error'); - return; - } - - return callMasterLinkJoin(parsed.master_ip, parsed.token).then(function(res) { - if (res.status === 'approved') { - ui.addNotification(null, E('p', 'Successfully joined mesh network!'), 'success'); - window.location.reload(); - } else if (res.status === 'pending') { - ui.addNotification(null, E('p', 'Join request submitted. Waiting for master approval.'), 'info'); - window.location.reload(); - } else if (res.status === 'error') { - ui.addNotification(null, E('p', 'Join failed: ' + (res.message || 'Unknown error')), 'error'); - } else { - ui.addNotification(null, E('p', 'Unexpected response: ' + JSON.stringify(res)), 'warning'); - } - }).catch(function(err) { - ui.addNotification(null, E('p', 'Join error: ' + err.message), 'error'); - }); - }) - }, 'Join Mesh'); - - joinSection.appendChild(E('h3', 'Join Mesh Network')); - joinSection.appendChild(E('p', { 'class': 'cbi-section-descr' }, - 'Enter the invite URL or token provided by the mesh master to join the network.')); - joinSection.appendChild(E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, 'Invite URL / Token'), - E('div', { 'class': 'cbi-value-field' }, inviteInput) - ])); - joinSection.appendChild(E('div', { 'class': 'cbi-page-actions' }, [ - verifyBtn, - joinBtn - ])); - } - - // CLI help section - var cliSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', 'Command Line'), - E('p', { 'class': 'cbi-section-descr' }, 'You can also join using the CLI tool:'), - E('pre', { 'style': 'background:#1a1a1a;padding:1em;border-radius:4px;overflow-x:auto' }, [ - E('code', {}, [ - '# Using IP and token\n', - 'sbx-mesh-join 192.168.1.1 abc123token\n\n', - '# Using full URL\n', - 'sbx-mesh-join \'http://192.168.1.1:7331/master-link/?token=abc123\'\n\n', - '# One-liner from master\n', - 'wget -qO- \'http://master-ip:7331/api/v1/p2p/master-link/join-script?token=xxx\' | sh' - ]) - ]) - ]); - - return E('div', { 'class': 'cbi-map' }, [ - E('h2', 'Master-Link'), - E('div', { 'class': 'cbi-map-descr' }, 'Join and manage SecuBox mesh network membership.'), - statusSection, - joinSection, - cliSection - ]); - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}); diff --git a/package/secubox/luci-app-masterlink/root/usr/bin/sbx-mesh-invite b/package/secubox/luci-app-masterlink/root/usr/bin/sbx-mesh-invite deleted file mode 100755 index 88e16f29..00000000 --- a/package/secubox/luci-app-masterlink/root/usr/bin/sbx-mesh-invite +++ /dev/null @@ -1,158 +0,0 @@ -#!/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 -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="" -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 "" diff --git a/package/secubox/luci-app-masterlink/root/usr/bin/sbx-mesh-join b/package/secubox/luci-app-masterlink/root/usr/bin/sbx-mesh-join deleted file mode 100644 index 2cf7fe54..00000000 --- a/package/secubox/luci-app-masterlink/root/usr/bin/sbx-mesh-join +++ /dev/null @@ -1,236 +0,0 @@ -#!/bin/sh -# SecuBox Mesh Join CLI Tool -# Usage: sbx-mesh-join -# or: sbx-mesh-join - -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 - sbx-mesh-join - -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 "$@" diff --git a/package/secubox/luci-app-masterlink/root/usr/share/luci/menu.d/luci-app-masterlink.json b/package/secubox/luci-app-masterlink/root/usr/share/luci/menu.d/luci-app-masterlink.json deleted file mode 100644 index a7d9317b..00000000 --- a/package/secubox/luci-app-masterlink/root/usr/share/luci/menu.d/luci-app-masterlink.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "admin/secubox/master-link": { - "title": "Master-Link", - "order": 90, - "action": { - "type": "view", - "path": "masterlink/join" - }, - "depends": { - "acl": ["luci-app-masterlink"] - } - } -} diff --git a/package/secubox/luci-app-masterlink/root/usr/share/rpcd/acl.d/luci-app-masterlink.json b/package/secubox/luci-app-masterlink/root/usr/share/rpcd/acl.d/luci-app-masterlink.json deleted file mode 100644 index 95528b96..00000000 --- a/package/secubox/luci-app-masterlink/root/usr/share/rpcd/acl.d/luci-app-masterlink.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "luci-app-masterlink": { - "description": "Grant access to Master-Link mesh enrollment", - "read": { - "ubus": { - "luci.masterlink": ["status", "info", "verify"] - }, - "uci": ["masterlink"] - }, - "write": { - "ubus": { - "luci.masterlink": ["join", "leave"] - }, - "uci": ["masterlink"] - } - } -}