feat(vortex-dns): Add LuCI dashboard for mesh DNS management
New package: luci-app-vortex-dns - Dashboard showing mode, status, sync info - Master section with delegated zones table - Slave section with parent master info - Mesh peers section with online status - Actions: Initialize master, Join slave, Delegate zone, Mesh sync - RPCD handler with 8 methods Also fixes: - Mail port hijacking: WAN-only DNAT rules - Threat-analyst LocalAI port: 8081 → 8091 - Domoticz password reset Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ffc3138d2b
commit
7f4f34b930
@ -279,7 +279,12 @@
|
||||
"WebFetch(domain:zigbeefordomoticz.github.io)",
|
||||
"WebFetch(domain:rustdesk.com)",
|
||||
"WebFetch(domain:deepwiki.com)",
|
||||
"Bash(traceroute:*)"
|
||||
"Bash(traceroute:*)",
|
||||
"Bash(git -C /home/reepost/CyberMindStudio/secubox-openwrt add package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl package/secubox/secubox-app-mailserver/files/usr/lib/mailserver/container.sh .claude/WIP.md)",
|
||||
"Bash(git -C /home/reepost/CyberMindStudio/secubox-openwrt commit -m \"$\\(cat <<''EOF''\nfix\\(mailserver\\): Use LMDB maps instead of hash for Alpine Postfix\n\nAlpine Linux''s Postfix is compiled with LMDB support, not BerkeleyDB\nhash support. This caused \"Temporary lookup failure\" errors on send.\n\nChanges:\n- Changed virtual_alias_maps and virtual_mailbox_maps to lmdb: prefix\n- Copy resolv.conf to Postfix chroot for DNS resolution\n- Added `mailctl fix-postfix` command to repair existing installations\n\nRoot cause: virtual_alias_maps was configured as hash:/etc/postfix/virtual\nbut the hash map type is not supported on Alpine, only lmdb.\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(git -C /home/reepost/CyberMindStudio/secubox-openwrt add:*)",
|
||||
"Bash(recipient table\" errors because Postfix treated the domain as local\ninstead of virtual.\n\nChanges:\n- Remove $mydomain from mydestination in setup.sh\n- Update fix-postfix command to also fix this issue\n- Ensure vdomains file is properly created\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(git -C /home/reepost/CyberMindStudio/secubox-openwrt commit -m \"$\\(cat <<''EOF''\ndocs: Document mail port hijacking fix\n\nFirewall DNAT rules were redirecting ALL port 993/587/465 traffic\nto local mailserver, blocking external mail server connections.\n\nFix: Add -i $WAN_IF to only redirect inbound WAN traffic.\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
30
package/secubox/luci-app-vortex-dns/Makefile
Normal file
30
package/secubox/luci-app-vortex-dns/Makefile
Normal file
@ -0,0 +1,30 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-vortex-dns
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
|
||||
LUCI_TITLE:=LuCI Vortex DNS Dashboard
|
||||
LUCI_DEPENDS:=+secubox-vortex-dns +luci-base
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/luci-app-vortex-dns/install
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-vortex-dns.json $(1)/usr/share/luci/menu.d/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-vortex-dns.json $(1)/usr/share/rpcd/acl.d/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.vortex-dns $(1)/usr/libexec/rpcd/
|
||||
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/vortex-dns
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/vortex-dns/*.js $(1)/www/luci-static/resources/view/vortex-dns/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-vortex-dns))
|
||||
@ -0,0 +1,433 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require form';
|
||||
'require uci';
|
||||
|
||||
var callStatus = rpc.declare({
|
||||
object: 'luci.vortex-dns',
|
||||
method: 'status',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callGetSlaves = rpc.declare({
|
||||
object: 'luci.vortex-dns',
|
||||
method: 'get_slaves',
|
||||
expect: { slaves: [] }
|
||||
});
|
||||
|
||||
var callGetPeers = rpc.declare({
|
||||
object: 'luci.vortex-dns',
|
||||
method: 'get_peers',
|
||||
expect: { peers: [] }
|
||||
});
|
||||
|
||||
var callMasterInit = rpc.declare({
|
||||
object: 'luci.vortex-dns',
|
||||
method: 'master_init',
|
||||
params: ['domain'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callDelegate = rpc.declare({
|
||||
object: 'luci.vortex-dns',
|
||||
method: 'delegate',
|
||||
params: ['node', 'zone'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callSlaveJoin = rpc.declare({
|
||||
object: 'luci.vortex-dns',
|
||||
method: 'slave_join',
|
||||
params: ['master', 'token'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callMeshSync = rpc.declare({
|
||||
object: 'luci.vortex-dns',
|
||||
method: 'mesh_sync',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
callStatus(),
|
||||
callGetSlaves(),
|
||||
callGetPeers()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var status = data[0] || {};
|
||||
var slaves = data[1] || [];
|
||||
var peers = data[2] || [];
|
||||
|
||||
var view = E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, 'Vortex DNS'),
|
||||
E('div', { 'class': 'cbi-map-descr' },
|
||||
'Meshed multi-dynamic subdomain delegation system'),
|
||||
|
||||
// Status Card
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, 'Status'),
|
||||
E('div', { 'class': 'table' }, [
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'Mode'),
|
||||
E('div', { 'class': 'td' }, this.renderModeBadge(status.mode))
|
||||
]),
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'Enabled'),
|
||||
E('div', { 'class': 'td' }, status.enabled ?
|
||||
E('span', { 'class': 'badge success' }, 'Yes') :
|
||||
E('span', { 'class': 'badge warning' }, 'No'))
|
||||
]),
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'Sync Interval'),
|
||||
E('div', { 'class': 'td' }, (status.sync_interval || 300) + 's')
|
||||
]),
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'Last Sync'),
|
||||
E('div', { 'class': 'td' }, status.last_sync || 'Never')
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Master Section (if master mode)
|
||||
status.master ? this.renderMasterSection(status.master, slaves) : null,
|
||||
|
||||
// Slave Section (if slave mode)
|
||||
status.slave ? this.renderSlaveSection(status.slave) : null,
|
||||
|
||||
// Mesh Peers Section
|
||||
this.renderPeersSection(status.mesh, peers),
|
||||
|
||||
// Actions Section
|
||||
this.renderActionsSection(status)
|
||||
]);
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
renderModeBadge: function(mode) {
|
||||
var colors = {
|
||||
'master': 'primary',
|
||||
'slave': 'info',
|
||||
'submaster': 'warning',
|
||||
'standalone': 'secondary'
|
||||
};
|
||||
return E('span', {
|
||||
'class': 'badge ' + (colors[mode] || 'secondary'),
|
||||
'style': 'padding: 4px 8px; border-radius: 4px; font-weight: bold;'
|
||||
}, (mode || 'standalone').toUpperCase());
|
||||
},
|
||||
|
||||
renderMasterSection: function(master, slaves) {
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, 'Master Node'),
|
||||
E('div', { 'class': 'table' }, [
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'Wildcard Domain'),
|
||||
E('div', { 'class': 'td' }, E('strong', {}, '*.' + (master.wildcard_domain || 'not set')))
|
||||
]),
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'DNS Provider'),
|
||||
E('div', { 'class': 'td' }, master.dns_provider || 'not set')
|
||||
]),
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'Delegated Slaves'),
|
||||
E('div', { 'class': 'td' }, master.slave_count || 0)
|
||||
])
|
||||
]),
|
||||
|
||||
// Slaves Table
|
||||
slaves.length > 0 ? E('div', { 'style': 'margin-top: 16px;' }, [
|
||||
E('h4', {}, 'Delegated Zones'),
|
||||
E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, 'Zone'),
|
||||
E('th', { 'class': 'th' }, 'FQDN'),
|
||||
E('th', { 'class': 'th' }, 'Node IP'),
|
||||
E('th', { 'class': 'th' }, 'Created')
|
||||
])
|
||||
].concat(slaves.map(function(s) {
|
||||
return E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, s.zone),
|
||||
E('td', { 'class': 'td' }, s.fqdn),
|
||||
E('td', { 'class': 'td' }, s.node),
|
||||
E('td', { 'class': 'td' }, s.created)
|
||||
]);
|
||||
})))
|
||||
]) : null
|
||||
]);
|
||||
},
|
||||
|
||||
renderSlaveSection: function(slave) {
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, 'Slave Node'),
|
||||
E('div', { 'class': 'table' }, [
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'Parent Master'),
|
||||
E('div', { 'class': 'td' }, slave.parent_master || 'not set')
|
||||
]),
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'Delegated Zone'),
|
||||
E('div', { 'class': 'td' }, E('strong', {}, slave.delegated_zone || 'pending'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
renderPeersSection: function(mesh, peers) {
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, 'Mesh Network'),
|
||||
E('div', { 'class': 'table' }, [
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'Gossip'),
|
||||
E('div', { 'class': 'td' }, mesh && mesh.gossip_enabled ?
|
||||
E('span', { 'class': 'badge success' }, 'Enabled') :
|
||||
E('span', { 'class': 'badge warning' }, 'Disabled'))
|
||||
]),
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'First Peek'),
|
||||
E('div', { 'class': 'td' }, mesh && mesh.first_peek ? 'Enabled' : 'Disabled')
|
||||
]),
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'Connected Peers'),
|
||||
E('div', { 'class': 'td' }, (mesh && mesh.peer_count) || 0)
|
||||
]),
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, 'Published Services'),
|
||||
E('div', { 'class': 'td' }, (mesh && mesh.published_count) || 0)
|
||||
])
|
||||
]),
|
||||
|
||||
// Peers Table
|
||||
peers.length > 0 ? E('div', { 'style': 'margin-top: 16px;' }, [
|
||||
E('h4', {}, 'Connected Peers'),
|
||||
E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, 'Name'),
|
||||
E('th', { 'class': 'th' }, 'IP'),
|
||||
E('th', { 'class': 'th' }, 'Status')
|
||||
])
|
||||
].concat(peers.map(function(p) {
|
||||
return E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td' }, p.name || p.id),
|
||||
E('td', { 'class': 'td' }, p.ip),
|
||||
E('td', { 'class': 'td' }, p.online ?
|
||||
E('span', { 'class': 'badge success' }, 'Online') :
|
||||
E('span', { 'class': 'badge danger' }, 'Offline'))
|
||||
]);
|
||||
})))
|
||||
]) : E('p', { 'style': 'color: #888; margin-top: 8px;' }, 'No peers connected')
|
||||
]);
|
||||
},
|
||||
|
||||
renderActionsSection: function(status) {
|
||||
var self = this;
|
||||
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', {}, 'Actions'),
|
||||
E('div', { 'style': 'display: flex; gap: 8px; flex-wrap: wrap;' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-action',
|
||||
'click': function() { self.doMeshSync(); }
|
||||
}, 'Sync Mesh'),
|
||||
|
||||
status.mode === 'standalone' ? E('button', {
|
||||
'class': 'btn cbi-button-positive',
|
||||
'click': function() { self.showMasterInitDialog(); }
|
||||
}, 'Initialize as Master') : null,
|
||||
|
||||
status.mode === 'standalone' ? E('button', {
|
||||
'class': 'btn cbi-button-neutral',
|
||||
'click': function() { self.showSlaveJoinDialog(); }
|
||||
}, 'Join as Slave') : null,
|
||||
|
||||
status.mode === 'master' ? E('button', {
|
||||
'class': 'btn cbi-button-positive',
|
||||
'click': function() { self.showDelegateDialog(); }
|
||||
}, 'Delegate Zone') : null
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
doMeshSync: function() {
|
||||
ui.showModal('Syncing...', [
|
||||
E('p', { 'class': 'spinning' }, 'Syncing with mesh peers...')
|
||||
]);
|
||||
|
||||
callMeshSync().then(function(res) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, 'Mesh sync completed at ' + res.synced_at), 'success');
|
||||
}).catch(function(e) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, 'Sync failed: ' + e.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
showMasterInitDialog: function() {
|
||||
var self = this;
|
||||
ui.showModal('Initialize as Master', [
|
||||
E('p', {}, 'Enter the wildcard domain to manage:'),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'id': 'master-domain',
|
||||
'placeholder': 'secubox.io',
|
||||
'style': 'width: 100%; padding: 8px; margin: 8px 0;'
|
||||
}),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.hideModal
|
||||
}, 'Cancel'),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-positive',
|
||||
'click': function() {
|
||||
var domain = document.getElementById('master-domain').value;
|
||||
if (domain) {
|
||||
self.doMasterInit(domain);
|
||||
}
|
||||
}
|
||||
}, 'Initialize')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
doMasterInit: function(domain) {
|
||||
ui.showModal('Initializing...', [
|
||||
E('p', { 'class': 'spinning' }, 'Initializing master for *.' + domain + '...')
|
||||
]);
|
||||
|
||||
callMasterInit(domain).then(function(res) {
|
||||
ui.hideModal();
|
||||
ui.showModal('Master Initialized', [
|
||||
E('p', {}, 'Successfully initialized as master for *.' + domain),
|
||||
E('p', {}, [
|
||||
E('strong', {}, 'Enrollment Token: '),
|
||||
E('code', {}, res.token)
|
||||
]),
|
||||
E('p', {}, 'Slaves can join with: vortexctl slave join <this_ip> ' + res.token),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-positive',
|
||||
'click': function() { location.reload(); }
|
||||
}, 'OK')
|
||||
])
|
||||
]);
|
||||
}).catch(function(e) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, 'Failed: ' + e.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
showSlaveJoinDialog: function() {
|
||||
var self = this;
|
||||
ui.showModal('Join as Slave', [
|
||||
E('p', {}, 'Enter master connection details:'),
|
||||
E('label', {}, 'Master IP:'),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'id': 'master-ip',
|
||||
'placeholder': '192.168.1.1',
|
||||
'style': 'width: 100%; padding: 8px; margin: 8px 0;'
|
||||
}),
|
||||
E('label', {}, 'Enrollment Token:'),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'id': 'master-token',
|
||||
'placeholder': 'token from master',
|
||||
'style': 'width: 100%; padding: 8px; margin: 8px 0;'
|
||||
}),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.hideModal
|
||||
}, 'Cancel'),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-positive',
|
||||
'click': function() {
|
||||
var ip = document.getElementById('master-ip').value;
|
||||
var token = document.getElementById('master-token').value;
|
||||
if (ip && token) {
|
||||
self.doSlaveJoin(ip, token);
|
||||
}
|
||||
}
|
||||
}, 'Join')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
doSlaveJoin: function(master, token) {
|
||||
ui.showModal('Joining...', [
|
||||
E('p', { 'class': 'spinning' }, 'Joining master at ' + master + '...')
|
||||
]);
|
||||
|
||||
callSlaveJoin(master, token).then(function(res) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, 'Successfully joined master'), 'success');
|
||||
location.reload();
|
||||
}).catch(function(e) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, 'Failed: ' + e.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
showDelegateDialog: function() {
|
||||
var self = this;
|
||||
ui.showModal('Delegate Zone', [
|
||||
E('p', {}, 'Delegate a subzone to a slave node:'),
|
||||
E('label', {}, 'Slave Node IP:'),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'id': 'delegate-node',
|
||||
'placeholder': '192.168.1.100',
|
||||
'style': 'width: 100%; padding: 8px; margin: 8px 0;'
|
||||
}),
|
||||
E('label', {}, 'Zone Name:'),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'id': 'delegate-zone',
|
||||
'placeholder': 'node1',
|
||||
'style': 'width: 100%; padding: 8px; margin: 8px 0;'
|
||||
}),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.hideModal
|
||||
}, 'Cancel'),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-positive',
|
||||
'click': function() {
|
||||
var node = document.getElementById('delegate-node').value;
|
||||
var zone = document.getElementById('delegate-zone').value;
|
||||
if (node && zone) {
|
||||
self.doDelegate(node, zone);
|
||||
}
|
||||
}
|
||||
}, 'Delegate')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
doDelegate: function(node, zone) {
|
||||
ui.showModal('Delegating...', [
|
||||
E('p', { 'class': 'spinning' }, 'Delegating ' + zone + ' to ' + node + '...')
|
||||
]);
|
||||
|
||||
callDelegate(node, zone).then(function(res) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, 'Zone ' + zone + ' delegated to ' + node), 'success');
|
||||
location.reload();
|
||||
}).catch(function(e) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, 'Failed: ' + e.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -0,0 +1,215 @@
|
||||
#!/bin/sh
|
||||
# RPCD handler for Vortex DNS
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
CONFIG="vortex-dns"
|
||||
STATE_DIR="/var/lib/vortex-dns"
|
||||
|
||||
uci_get() { uci -q get "${CONFIG}.$1"; }
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{"status":{},"get_slaves":{},"get_peers":{},"get_published":{},"master_init":{"domain":"str"},"delegate":{"node":"str","zone":"str"},"revoke":{"zone":"str"},"slave_join":{"master":"str","token":"str"},"mesh_sync":{},"mesh_publish":{"service":"str","domain":"str"}}'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status)
|
||||
json_init
|
||||
|
||||
enabled=$(uci_get main.enabled)
|
||||
mode=$(uci_get main.mode)
|
||||
sync_interval=$(uci_get main.sync_interval)
|
||||
|
||||
json_add_boolean "enabled" "${enabled:-0}"
|
||||
json_add_string "mode" "${mode:-standalone}"
|
||||
json_add_int "sync_interval" "${sync_interval:-300}"
|
||||
|
||||
# Master info
|
||||
if [ "$(uci_get master.enabled)" = "1" ]; then
|
||||
json_add_object "master"
|
||||
json_add_string "wildcard_domain" "$(uci_get master.wildcard_domain)"
|
||||
json_add_string "dns_provider" "$(uci_get master.dns_provider)"
|
||||
|
||||
# Count slaves
|
||||
slaves=$(uci show "$CONFIG" 2>/dev/null | grep -c "=delegation")
|
||||
json_add_int "slave_count" "$slaves"
|
||||
json_close_object
|
||||
fi
|
||||
|
||||
# Slave info
|
||||
if [ "$(uci_get slave.enabled)" = "1" ]; then
|
||||
json_add_object "slave"
|
||||
json_add_string "parent_master" "$(uci_get slave.parent_master)"
|
||||
json_add_string "delegated_zone" "$(uci_get slave.delegated_zone)"
|
||||
json_close_object
|
||||
fi
|
||||
|
||||
# Mesh info
|
||||
json_add_object "mesh"
|
||||
json_add_boolean "gossip_enabled" "$(uci_get mesh.gossip_enabled)"
|
||||
json_add_boolean "first_peek" "$(uci_get mesh.first_peek)"
|
||||
json_add_boolean "auto_register" "$(uci_get mesh.auto_register)"
|
||||
|
||||
# Count peers
|
||||
if command -v secubox-p2p >/dev/null 2>&1; then
|
||||
peers=$(secubox-p2p peers 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l)
|
||||
json_add_int "peer_count" "$peers"
|
||||
else
|
||||
json_add_int "peer_count" 0
|
||||
fi
|
||||
|
||||
# Count published
|
||||
if [ -f "$STATE_DIR/published.json" ]; then
|
||||
published=$(jsonfilter -i "$STATE_DIR/published.json" -e '@[*]' 2>/dev/null | wc -l)
|
||||
json_add_int "published_count" "$published"
|
||||
else
|
||||
json_add_int "published_count" 0
|
||||
fi
|
||||
json_close_object
|
||||
|
||||
# Last sync
|
||||
if [ -f "$STATE_DIR/last_sync" ]; then
|
||||
json_add_string "last_sync" "$(cat "$STATE_DIR/last_sync")"
|
||||
fi
|
||||
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_slaves)
|
||||
json_init
|
||||
json_add_array "slaves"
|
||||
|
||||
uci show "$CONFIG" 2>/dev/null | grep "=delegation" | while read -r line; do
|
||||
section=$(echo "$line" | cut -d= -f1 | cut -d. -f2)
|
||||
json_add_object
|
||||
json_add_string "zone" "$(uci_get "${section}.zone")"
|
||||
json_add_string "node" "$(uci_get "${section}.node")"
|
||||
json_add_string "fqdn" "$(uci_get "${section}.fqdn")"
|
||||
json_add_string "created" "$(uci_get "${section}.created")"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_peers)
|
||||
json_init
|
||||
json_add_array "peers"
|
||||
|
||||
if command -v secubox-p2p >/dev/null 2>&1; then
|
||||
secubox-p2p peers 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | while read -r peer; do
|
||||
json_add_object
|
||||
json_add_string "id" "$(echo "$peer" | jsonfilter -e '@.id' 2>/dev/null)"
|
||||
json_add_string "name" "$(echo "$peer" | jsonfilter -e '@.name' 2>/dev/null)"
|
||||
json_add_string "ip" "$(echo "$peer" | jsonfilter -e '@.ip' 2>/dev/null)"
|
||||
json_add_boolean "online" "$(echo "$peer" | jsonfilter -e '@.online' 2>/dev/null)"
|
||||
json_close_object
|
||||
done
|
||||
fi
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
;;
|
||||
|
||||
get_published)
|
||||
json_init
|
||||
json_add_array "services"
|
||||
|
||||
if [ -f "$STATE_DIR/published.json" ]; then
|
||||
cat "$STATE_DIR/published.json"
|
||||
else
|
||||
echo "[]"
|
||||
fi
|
||||
;;
|
||||
|
||||
master_init)
|
||||
read -r input
|
||||
domain=$(echo "$input" | jsonfilter -e '@.domain')
|
||||
|
||||
if [ -z "$domain" ]; then
|
||||
echo '{"error":"Domain required"}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
output=$(vortexctl master init "$domain" 2>&1)
|
||||
token=$(echo "$output" | grep "enrollment token:" | awk '{print $NF}')
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_string "token" "$token"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
delegate)
|
||||
read -r input
|
||||
node=$(echo "$input" | jsonfilter -e '@.node')
|
||||
zone=$(echo "$input" | jsonfilter -e '@.zone')
|
||||
|
||||
if [ -z "$node" ] || [ -z "$zone" ]; then
|
||||
echo '{"error":"Node and zone required"}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vortexctl master delegate "$node" "$zone" >/dev/null 2>&1
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "zone" "$zone"
|
||||
json_add_string "node" "$node"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
slave_join)
|
||||
read -r input
|
||||
master=$(echo "$input" | jsonfilter -e '@.master')
|
||||
token=$(echo "$input" | jsonfilter -e '@.token')
|
||||
|
||||
if [ -z "$master" ] || [ -z "$token" ]; then
|
||||
echo '{"error":"Master IP and token required"}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vortexctl slave join "$master" "$token" >/dev/null 2>&1
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_dump
|
||||
;;
|
||||
|
||||
mesh_sync)
|
||||
vortexctl mesh sync >/dev/null 2>&1
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "synced_at" "$(date -Iseconds)"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
mesh_publish)
|
||||
read -r input
|
||||
service=$(echo "$input" | jsonfilter -e '@.service')
|
||||
domain=$(echo "$input" | jsonfilter -e '@.domain')
|
||||
|
||||
if [ -z "$service" ] || [ -z "$domain" ]; then
|
||||
echo '{"error":"Service and domain required"}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vortexctl mesh publish "$service" "$domain" >/dev/null 2>&1
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "service" "$service"
|
||||
json_add_string "domain" "$domain"
|
||||
json_dump
|
||||
;;
|
||||
|
||||
*)
|
||||
echo '{"error":"Unknown method"}'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,14 @@
|
||||
{
|
||||
"admin/services/vortex-dns": {
|
||||
"title": "Vortex DNS",
|
||||
"order": 85,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "vortex-dns/dashboard"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-vortex-dns"],
|
||||
"uci": {"vortex-dns": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-vortex-dns": {
|
||||
"description": "Grant access to Vortex DNS",
|
||||
"read": {
|
||||
"uci": ["vortex-dns"],
|
||||
"ubus": {
|
||||
"luci.vortex-dns": ["status", "get_slaves", "get_peers", "get_published"]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": ["vortex-dns"],
|
||||
"ubus": {
|
||||
"luci.vortex-dns": ["master_init", "delegate", "revoke", "slave_join", "mesh_sync", "mesh_publish"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user