mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 11:12:29 +00:00
docs/OPENWRT-MASTERLINK.md: - RPCD backend script (luci.masterlink) - LuCI JavaScript view for join/leave UI - UCI config template - OpenWRT package Makefile - ACL permissions file - CLI and web UI testing commands - Complete directory structure for luci-app-masterlink Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
15 KiB
15 KiB
OpenWRT Master-Link Client Implementation
Goal
Add Master-Link client support to SecuBox OpenWRT to join a SecuBox Debian mesh network.
Files to Create/Modify
1. /usr/bin/sbx-mesh-join - Shell script
Already compatible with OpenWRT - uses wget, uci, br-lan detection.
Copy from secubox-deb: packages/secubox-p2p/scripts/sbx-mesh-join
2. /usr/libexec/rpcd/luci.masterlink - RPCD backend
-- Methods needed:
-- status: Get current mesh membership status
-- join: Join a mesh using token
-- leave: Leave current mesh
-- info: Get local node info (fingerprint, hostname, IP)
3. /usr/share/luci/menu.d/luci-app-masterlink.json - Menu entry
{
"admin/services/masterlink": {
"title": "Master-Link",
"order": 90,
"action": { "type": "view", "path": "masterlink/join" }
}
}
4. /www/luci-static/resources/view/masterlink/join.js - LuCI JS view
// UI Features:
// - Input field for invite URL or token
// - Parse URL to extract master IP + token
// - Show master info before joining (fingerprint verification)
// - Join button → calls luci.masterlink/join
// - Status display: Not joined / Pending / Approved
5. /etc/config/masterlink - UCI config
config mesh 'settings'
option enabled '0'
option role 'peer'
option master_ip ''
option master_fingerprint ''
option local_fingerprint ''
option depth '0'
option joined_at ''
6. /etc/secubox/node.id - Persistent node fingerprint
# Generate on first boot:
owrt-$(cat /sys/class/net/br-lan/address | tr -d ':')
RPCD Script (/usr/libexec/rpcd/luci.masterlink)
#!/bin/sh
. /usr/share/libubox/jshn.sh
NODE_ID_FILE="/etc/secubox/node.id"
CONFIG_FILE="/etc/config/masterlink"
get_fingerprint() {
if [ -f "$NODE_ID_FILE" ]; then
cat "$NODE_ID_FILE"
else
mkdir -p /etc/secubox
local fp="owrt-$(cat /sys/class/net/br-lan/address 2>/dev/null | tr -d ':')"
echo "$fp" > "$NODE_ID_FILE"
echo "$fp"
fi
}
get_local_ip() {
ip -4 addr show br-lan 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1
}
case "$1" in
list)
echo '{"status":{},"join":{"master_ip":"str","token":"str"},"leave":{},"info":{}}'
;;
call)
case "$2" in
status)
json_init
json_add_string "role" "$(uci -q get masterlink.settings.role || echo 'standalone')"
json_add_string "master_ip" "$(uci -q get masterlink.settings.master_ip)"
json_add_string "fingerprint" "$(get_fingerprint)"
json_add_boolean "enabled" "$(uci -q get masterlink.settings.enabled || echo 0)"
json_dump
;;
join)
read -r input
json_load "$input"
json_get_var master_ip master_ip
json_get_var token token
local fp=$(get_fingerprint)
local hostname=$(uci get system.@system[0].hostname)
local address=$(get_local_ip)
# Call master API
local response=$(wget -qO- --post-data="{\"token\":\"$token\",\"fingerprint\":\"$fp\",\"hostname\":\"$hostname\",\"address\":\"$address\"}" \
--header="Content-Type: application/json" \
"http://$master_ip:7331/api/v1/p2p/master-link/join" 2>/dev/null)
# Parse response
local status=$(echo "$response" | jsonfilter -e '@.status')
if [ "$status" = "approved" ]; then
uci set masterlink.settings.enabled='1'
uci set masterlink.settings.role='peer'
uci set masterlink.settings.master_ip="$master_ip"
uci commit masterlink
fi
echo "$response"
;;
leave)
uci set masterlink.settings.enabled='0'
uci set masterlink.settings.role='standalone'
uci delete masterlink.settings.master_ip
uci commit masterlink
json_init
json_add_string "status" "ok"
json_dump
;;
info)
json_init
json_add_string "fingerprint" "$(get_fingerprint)"
json_add_string "hostname" "$(uci get system.@system[0].hostname)"
json_add_string "address" "$(get_local_ip)"
json_add_string "model" "$(cat /tmp/sysinfo/model 2>/dev/null)"
json_dump
;;
esac
;;
esac
LuCI View (/www/luci-static/resources/view/masterlink/join.js)
'use strict';
'require view';
'require form';
'require rpc';
'require ui';
var callMasterLinkStatus = rpc.declare({
object: 'luci.masterlink',
method: 'status'
});
var callMasterLinkJoin = rpc.declare({
object: 'luci.masterlink',
method: 'join',
params: ['master_ip', 'token']
});
var callMasterLinkLeave = rpc.declare({
object: 'luci.masterlink',
method: 'leave'
});
var callMasterLinkInfo = rpc.declare({
object: 'luci.masterlink',
method: 'info'
});
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 container = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('Master-Link')),
E('div', { 'class': 'cbi-map-descr' },
_('Join a SecuBox mesh network using an invite token.'))
]);
// Status section
var statusSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Mesh Status')),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Status')),
E('div', { 'class': 'cbi-value-field' },
status.enabled == 1
? E('span', { 'style': 'color:green' }, '✓ Connected to ' + status.master_ip)
: E('span', { 'style': 'color:gray' }, 'Not connected'))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Fingerprint')),
E('div', { 'class': 'cbi-value-field' },
E('code', {}, info.fingerprint || 'Unknown'))
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Hostname')),
E('div', { 'class': 'cbi-value-field' }, info.hostname || 'Unknown')
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Local IP')),
E('div', { 'class': 'cbi-value-field' }, info.address || 'Unknown')
])
]);
container.appendChild(statusSection);
// Join section (only if not connected)
if (status.enabled != 1) {
var joinSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Join Mesh')),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Invite URL or Token')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'invite_input',
'class': 'cbi-input-text',
'style': 'width: 100%; max-width: 500px;',
'placeholder': 'http://192.168.1.1:7331/master-link/?token=abc123...'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, ''),
E('div', { 'class': 'cbi-value-field' }, [
E('button', {
'class': 'cbi-button cbi-button-apply',
'click': function() {
var input = document.getElementById('invite_input').value;
var match = input.match(/(\d+\.\d+\.\d+\.\d+).*token=([^&\s]+)/);
var master_ip, token;
if (match) {
master_ip = match[1];
token = match[2];
} else if (input.includes(' ')) {
// Format: "IP TOKEN"
var parts = input.trim().split(/\s+/);
master_ip = parts[0];
token = parts[1];
} else {
ui.addNotification(null, E('p', _('Invalid invite format')), 'error');
return;
}
ui.showModal(_('Joining...'), [
E('p', { 'class': 'spinning' }, _('Connecting to master...'))
]);
callMasterLinkJoin(master_ip, token).then(function(res) {
ui.hideModal();
if (res.status == 'approved') {
ui.addNotification(null, E('p', _('Successfully joined mesh!')), 'success');
window.location.reload();
} else if (res.status == 'pending') {
ui.addNotification(null, E('p', _('Waiting for master approval. Your fingerprint: ') + info.fingerprint), 'warning');
} else {
ui.addNotification(null, E('p', _('Failed: ') + JSON.stringify(res)), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: ') + err), 'error');
});
}
}, _('Join Mesh'))
])
])
]);
container.appendChild(joinSection);
} else {
// Leave section
var leaveSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('Leave Mesh')),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, ''),
E('div', { 'class': 'cbi-value-field' }, [
E('button', {
'class': 'cbi-button cbi-button-remove',
'click': function() {
if (confirm(_('Are you sure you want to leave the mesh?'))) {
callMasterLinkLeave().then(function() {
ui.addNotification(null, E('p', _('Left mesh network')), 'success');
window.location.reload();
});
}
}
}, _('Leave Mesh'))
])
])
]);
container.appendChild(leaveSection);
}
return container;
}
});
UCI Default Config (/etc/config/masterlink)
config mesh 'settings'
option enabled '0'
option role 'standalone'
option master_ip ''
option master_fingerprint ''
option depth '0'
Package Makefile
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-masterlink
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=Gerald KERMA <devel@cybermind.fr>
PKG_LICENSE:=GPL-3.0
include $(INCLUDE_DIR)/package.mk
define Package/luci-app-masterlink
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=SecuBox Master-Link mesh client
DEPENDS:=+luci-base +wget +jsonfilter
PKGARCH:=all
endef
define Package/luci-app-masterlink/description
LuCI application for joining SecuBox mesh networks via Master-Link.
Allows OpenWRT devices to join a SecuBox Debian master node.
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/luci/menu.d
$(INSTALL_DATA) ./files/luci-app-masterlink.json $(1)/usr/share/luci/menu.d/
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./files/luci-app-masterlink.acl $(1)/usr/share/rpcd/acl.d/
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/masterlink
$(INSTALL_DATA) ./files/join.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) ./files/sbx-mesh-join $(1)/usr/bin/
endef
define Package/luci-app-masterlink/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || /etc/init.d/rpcd restart
exit 0
endef
$(eval $(call BuildPackage,luci-app-masterlink))
ACL File (/usr/share/rpcd/acl.d/luci-app-masterlink.acl)
{
"luci-app-masterlink": {
"description": "Grant access to Master-Link mesh functions",
"read": {
"ubus": {
"luci.masterlink": ["status", "info"]
},
"uci": ["masterlink"]
},
"write": {
"ubus": {
"luci.masterlink": ["join", "leave"]
},
"uci": ["masterlink"]
}
}
}
Directory Structure
luci-app-masterlink/
├── Makefile
└── files/
├── luci.masterlink # RPCD backend script
├── luci-app-masterlink.json # Menu entry
├── luci-app-masterlink.acl # ACL permissions
├── masterlink.config # UCI default config
├── join.js # LuCI JS view
└── sbx-mesh-join # CLI tool (copy from Debian)
Testing Commands
# On OpenWRT device:
# 1. Test CLI join
sbx-mesh-join 192.168.1.1 abc123token456
# Or with URL
sbx-mesh-join 'http://192.168.1.1:7331/master-link/?token=abc123'
# 2. Test RPCD methods
ubus call luci.masterlink status
ubus call luci.masterlink info
ubus call luci.masterlink join '{"master_ip":"192.168.1.1","token":"abc123"}'
ubus call luci.masterlink leave
# 3. Check UCI config
uci show masterlink
# 4. Restart RPCD after changes
/etc/init.d/rpcd restart
Workflow Summary
On SecuBox Debian (Master)
- Generate invite:
POST /api/v1/p2p/master-link/invite - Copy the invite URL or CLI command
On OpenWRT (Client)
Option A - Web UI:
- Go to Services → Master-Link
- Paste invite URL
- Click "Join Mesh"
Option B - CLI:
sbx-mesh-join 'http://master-ip:7331/master-link/?token=xxx'
Option C - One-liner:
wget -qO- 'http://master-ip:7331/api/v1/p2p/master-link/join-script?token=xxx' | sh
See Also
- SecuBox Debian P2P API:
packages/secubox-p2p/api/main.py - Join script:
packages/secubox-p2p/scripts/sbx-mesh-join - Master-Link web UI:
packages/secubox-p2p/www/master-link/