fix(mitmproxy,tor-shield): Add transparent mode firewall support

- Add RPCD methods to mitmproxy: settings, save_settings, set_mode,
  setup_firewall, clear_firewall
- Add apply_now parameter to tor-shield save_settings to restart
  service and apply iptables rules immediately
- Update ACL files with new permissions
- Add Save & Apply button to tor-shield settings page
- Update api.js files to use correct RPCD method signatures

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-28 17:53:56 +01:00
parent 1621d51660
commit 329d5febb9
16 changed files with 1047 additions and 510 deletions

View File

@ -2,237 +2,176 @@
'require baseclass';
'require rpc';
var callMitmproxy = rpc.declare({
// Status and settings
var callStatus = rpc.declare({
object: 'luci.mitmproxy',
method: 'get_status'
method: 'status',
expect: {}
});
var callGetConfig = rpc.declare({
var callSettings = rpc.declare({
object: 'luci.mitmproxy',
method: 'get_config'
method: 'settings',
expect: {}
});
var callGetTransparentConfig = rpc.declare({
var callSaveSettings = rpc.declare({
object: 'luci.mitmproxy',
method: 'get_transparent_config'
method: 'save_settings',
params: ['mode', 'enabled', 'proxy_port', 'web_port', 'web_host', 'data_path',
'memory_limit', 'upstream_proxy', 'reverse_target', 'ssl_insecure',
'anticache', 'anticomp', 'transparent_enabled', 'transparent_interface',
'redirect_http', 'redirect_https', 'filtering_enabled', 'log_requests',
'filter_cdn', 'filter_media', 'block_ads', 'apply_now'],
expect: {}
});
var callGetWhitelistConfig = rpc.declare({
var callSetMode = rpc.declare({
object: 'luci.mitmproxy',
method: 'get_whitelist_config'
method: 'set_mode',
params: ['mode', 'apply_now'],
expect: {}
});
var callGetFilteringConfig = rpc.declare({
// Service control
var callInstall = rpc.declare({
object: 'luci.mitmproxy',
method: 'get_filtering_config'
method: 'install',
expect: {}
});
var callGetAllConfig = rpc.declare({
var callStart = rpc.declare({
object: 'luci.mitmproxy',
method: 'get_all_config'
method: 'start',
expect: {}
});
var callGetStats = rpc.declare({
var callStop = rpc.declare({
object: 'luci.mitmproxy',
method: 'get_stats'
method: 'stop',
expect: {}
});
var callGetRequests = rpc.declare({
var callRestart = rpc.declare({
object: 'luci.mitmproxy',
method: 'get_requests',
params: ['limit', 'category']
method: 'restart',
expect: {}
});
var callGetTopHosts = rpc.declare({
// Firewall control
var callSetupFirewall = rpc.declare({
object: 'luci.mitmproxy',
method: 'get_top_hosts',
params: ['limit']
method: 'setup_firewall',
expect: {}
});
var callGetCaInfo = rpc.declare({
var callClearFirewall = rpc.declare({
object: 'luci.mitmproxy',
method: 'get_ca_info'
});
var callGetWebToken = rpc.declare({
object: 'luci.mitmproxy',
method: 'get_web_token'
});
var callServiceStart = rpc.declare({
object: 'luci.mitmproxy',
method: 'service_start'
});
var callServiceStop = rpc.declare({
object: 'luci.mitmproxy',
method: 'service_stop'
});
var callServiceRestart = rpc.declare({
object: 'luci.mitmproxy',
method: 'service_restart'
});
var callFirewallSetup = rpc.declare({
object: 'luci.mitmproxy',
method: 'firewall_setup'
});
var callFirewallClear = rpc.declare({
object: 'luci.mitmproxy',
method: 'firewall_clear'
});
var callSetConfig = rpc.declare({
object: 'luci.mitmproxy',
method: 'set_config',
params: ['key', 'value']
});
var callAddToList = rpc.declare({
object: 'luci.mitmproxy',
method: 'add_to_list',
params: ['key', 'value']
});
var callRemoveFromList = rpc.declare({
object: 'luci.mitmproxy',
method: 'remove_from_list',
params: ['key', 'value']
});
var callClearData = rpc.declare({
object: 'luci.mitmproxy',
method: 'clear_data'
method: 'clear_firewall',
expect: {}
});
return baseclass.extend({
getStatus: function() {
return callMitmproxy().catch(function() {
return { running: false, enabled: false };
});
},
getConfig: function() {
return callGetConfig().catch(function() {
return {};
});
},
getTransparentConfig: function() {
return callGetTransparentConfig().catch(function() {
return { enabled: false };
});
},
getWhitelistConfig: function() {
return callGetWhitelistConfig().catch(function() {
return { enabled: true, bypass_ip: [], bypass_domain: [] };
});
},
getFilteringConfig: function() {
return callGetFilteringConfig().catch(function() {
return { enabled: false };
});
},
getAllConfig: function() {
return callGetAllConfig().catch(function() {
return { main: {}, transparent: {}, whitelist: {}, filtering: {} };
});
},
getStats: function() {
return callGetStats().catch(function() {
return callStatus().catch(function() {
return {
total_requests: 0,
unique_hosts: 0,
flow_file_size: 0,
cdn_requests: 0,
media_requests: 0,
blocked_ads: 0
running: false,
enabled: false,
installed: false,
lxc_available: false,
mode: 'regular',
nft_active: false
};
});
},
getRequests: function(limit, category) {
return callGetRequests(limit || 50, category || 'all').catch(function() {
return { requests: [] };
getSettings: function() {
return callSettings().catch(function() {
return {
enabled: false,
mode: 'regular',
proxy_port: 8888,
web_port: 8081,
web_host: '0.0.0.0',
data_path: '/srv/mitmproxy',
memory_limit: '256M',
transparent_enabled: false,
transparent_interface: 'br-lan',
redirect_http: true,
redirect_https: true,
filtering_enabled: false,
log_requests: true,
filter_cdn: false,
filter_media: false,
block_ads: false
};
});
},
getTopHosts: function(limit) {
return callGetTopHosts(limit || 20).catch(function() {
return { hosts: [] };
});
saveSettings: function(settings) {
return callSaveSettings(
settings.mode,
settings.enabled,
settings.proxy_port,
settings.web_port,
settings.web_host,
settings.data_path,
settings.memory_limit,
settings.upstream_proxy,
settings.reverse_target,
settings.ssl_insecure,
settings.anticache,
settings.anticomp,
settings.transparent_enabled,
settings.transparent_interface,
settings.redirect_http,
settings.redirect_https,
settings.filtering_enabled,
settings.log_requests,
settings.filter_cdn,
settings.filter_media,
settings.block_ads,
settings.apply_now !== false
);
},
getCaInfo: function() {
return callGetCaInfo().catch(function() {
return { installed: false };
});
setMode: function(mode, applyNow) {
return callSetMode(mode, applyNow !== false);
},
getWebToken: function() {
return callGetWebToken().catch(function() {
return { token: '', web_url: '', web_url_with_token: '' };
});
install: function() {
return callInstall();
},
serviceStart: function() {
return callServiceStart();
start: function() {
return callStart();
},
serviceStop: function() {
return callServiceStop();
stop: function() {
return callStop();
},
serviceRestart: function() {
return callServiceRestart();
restart: function() {
return callRestart();
},
firewallSetup: function() {
return callFirewallSetup();
setupFirewall: function() {
return callSetupFirewall();
},
firewallClear: function() {
return callFirewallClear();
},
setConfig: function(key, value) {
return callSetConfig(key, value);
},
addToList: function(key, value) {
return callAddToList(key, value);
},
removeFromList: function(key, value) {
return callRemoveFromList(key, value);
},
clearData: function() {
return callClearData();
clearFirewall: function() {
return callClearFirewall();
},
getAllData: function() {
var self = this;
return Promise.all([
self.getStatus(),
self.getAllConfig(),
self.getStats(),
self.getTopHosts(10),
self.getCaInfo()
self.getSettings()
]).then(function(results) {
return {
status: results[0],
config: results[1].main || results[1],
allConfig: results[1],
stats: results[2],
topHosts: results[3],
caInfo: results[4]
settings: results[1]
};
});
},

View File

@ -1,41 +1,325 @@
#!/bin/sh
# RPCD backend for mitmproxy LuCI app
CONFIG="mitmproxy"
CONTAINER="secbx-mitmproxy"
. /usr/share/libubox/jshn.sh
uci_get() { uci -q get ${CONFIG}.main.$1; }
CONFIG="mitmproxy"
LXC_NAME="mitmproxy"
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci set ${CONFIG}.$1="$2"; }
get_status() {
local enabled=$(uci_get enabled)
local web_port=$(uci_get web_port)
local proxy_port=$(uci_get proxy_port)
local data_path=$(uci_get data_path)
local enabled=$(uci_get main.enabled)
local web_port=$(uci_get main.web_port)
local proxy_port=$(uci_get main.proxy_port)
local data_path=$(uci_get main.data_path)
local mode=$(uci_get main.mode)
local docker_available=0
command -v docker >/dev/null 2>&1 && docker_available=1
# Check for LXC availability
local lxc_available=0
command -v lxc-start >/dev/null 2>&1 && lxc_available=1
# Check if container is running
local running=0
if [ "$docker_available" = "1" ]; then
docker ps --filter "name=$CONTAINER" --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER" && running=1
if [ "$lxc_available" = "1" ]; then
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING" && running=1
fi
# Check if installed (rootfs exists)
local installed=0
[ "$docker_available" = "1" ] && docker images --format "{{.Repository}}" 2>/dev/null | grep -q "mitmproxy" && installed=1
[ -d "$LXC_ROOTFS" ] && [ -x "$LXC_ROOTFS/usr/bin/mitmproxy" ] && installed=1
# Check nftables status for transparent mode
local nft_active=0
if [ "$mode" = "transparent" ] && command -v nft >/dev/null 2>&1; then
nft list table inet mitmproxy >/dev/null 2>&1 && nft_active=1
fi
cat <<EOFJ
{
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
"running": $([ "$running" = "1" ] && echo "true" || echo "false"),
"installed": $([ "$installed" = "1" ] && echo "true" || echo "false"),
"docker_available": $([ "$docker_available" = "1" ] && echo "true" || echo "false"),
"lxc_available": $([ "$lxc_available" = "1" ] && echo "true" || echo "false"),
"docker_available": $([ "$lxc_available" = "1" ] && echo "true" || echo "false"),
"web_port": ${web_port:-8081},
"proxy_port": ${proxy_port:-8080},
"data_path": "${data_path:-/srv/mitmproxy}"
"proxy_port": ${proxy_port:-8888},
"data_path": "${data_path:-/srv/mitmproxy}",
"mode": "${mode:-regular}",
"nft_active": $([ "$nft_active" = "1" ] && echo "true" || echo "false")
}
EOFJ
}
get_settings() {
json_init
# Main settings
local enabled=$(uci_get main.enabled)
local mode=$(uci_get main.mode)
local proxy_port=$(uci_get main.proxy_port)
local web_port=$(uci_get main.web_port)
local web_host=$(uci_get main.web_host)
local data_path=$(uci_get main.data_path)
local memory_limit=$(uci_get main.memory_limit)
local upstream_proxy=$(uci_get main.upstream_proxy)
local reverse_target=$(uci_get main.reverse_target)
local ssl_insecure=$(uci_get main.ssl_insecure)
local anticache=$(uci_get main.anticache)
local anticomp=$(uci_get main.anticomp)
json_add_boolean "enabled" "${enabled:-0}"
json_add_string "mode" "${mode:-regular}"
json_add_int "proxy_port" "${proxy_port:-8888}"
json_add_int "web_port" "${web_port:-8081}"
json_add_string "web_host" "${web_host:-0.0.0.0}"
json_add_string "data_path" "${data_path:-/srv/mitmproxy}"
json_add_string "memory_limit" "${memory_limit:-256M}"
json_add_string "upstream_proxy" "${upstream_proxy:-}"
json_add_string "reverse_target" "${reverse_target:-}"
json_add_boolean "ssl_insecure" "${ssl_insecure:-0}"
json_add_boolean "anticache" "${anticache:-0}"
json_add_boolean "anticomp" "${anticomp:-0}"
# Transparent settings
local transparent_enabled=$(uci_get transparent.enabled)
local transparent_iface=$(uci_get transparent.interface)
local redirect_http=$(uci_get transparent.redirect_http)
local redirect_https=$(uci_get transparent.redirect_https)
json_add_boolean "transparent_enabled" "${transparent_enabled:-0}"
json_add_string "transparent_interface" "${transparent_iface:-br-lan}"
json_add_boolean "redirect_http" "${redirect_http:-1}"
json_add_boolean "redirect_https" "${redirect_https:-1}"
# Filtering settings
local filtering_enabled=$(uci_get filtering.enabled)
local log_requests=$(uci_get filtering.log_requests)
local filter_cdn=$(uci_get filtering.filter_cdn)
local filter_media=$(uci_get filtering.filter_media)
local block_ads=$(uci_get filtering.block_ads)
json_add_boolean "filtering_enabled" "${filtering_enabled:-0}"
json_add_boolean "log_requests" "${log_requests:-1}"
json_add_boolean "filter_cdn" "${filter_cdn:-0}"
json_add_boolean "filter_media" "${filter_media:-0}"
json_add_boolean "block_ads" "${block_ads:-0}"
json_dump
}
save_settings() {
read input
json_load "$input"
# Get values from input
local mode enabled proxy_port web_port web_host data_path memory_limit
local upstream_proxy reverse_target ssl_insecure anticache anticomp
local transparent_enabled transparent_interface redirect_http redirect_https
local filtering_enabled log_requests filter_cdn filter_media block_ads
local apply_now
json_get_var mode mode
json_get_var enabled enabled
json_get_var proxy_port proxy_port
json_get_var web_port web_port
json_get_var web_host web_host
json_get_var data_path data_path
json_get_var memory_limit memory_limit
json_get_var upstream_proxy upstream_proxy
json_get_var reverse_target reverse_target
json_get_var ssl_insecure ssl_insecure
json_get_var anticache anticache
json_get_var anticomp anticomp
json_get_var transparent_enabled transparent_enabled
json_get_var transparent_interface transparent_interface
json_get_var redirect_http redirect_http
json_get_var redirect_https redirect_https
json_get_var filtering_enabled filtering_enabled
json_get_var log_requests log_requests
json_get_var filter_cdn filter_cdn
json_get_var filter_media filter_media
json_get_var block_ads block_ads
json_get_var apply_now apply_now
# Now initialize output JSON
json_init
# Ensure UCI sections exist
uci -q get mitmproxy.main >/dev/null 2>&1 || {
uci set mitmproxy.main=mitmproxy
uci set mitmproxy.main.enabled='0'
uci set mitmproxy.main.mode='regular'
}
uci -q get mitmproxy.transparent >/dev/null 2>&1 || {
uci set mitmproxy.transparent=transparent
uci set mitmproxy.transparent.enabled='0'
}
uci -q get mitmproxy.filtering >/dev/null 2>&1 || {
uci set mitmproxy.filtering=filtering
uci set mitmproxy.filtering.enabled='0'
}
# Apply main settings
[ -n "$mode" ] && uci_set main.mode "$mode"
[ -n "$enabled" ] && uci_set main.enabled "$enabled"
[ -n "$proxy_port" ] && uci_set main.proxy_port "$proxy_port"
[ -n "$web_port" ] && uci_set main.web_port "$web_port"
[ -n "$web_host" ] && uci_set main.web_host "$web_host"
[ -n "$data_path" ] && uci_set main.data_path "$data_path"
[ -n "$memory_limit" ] && uci_set main.memory_limit "$memory_limit"
[ -n "$upstream_proxy" ] && uci_set main.upstream_proxy "$upstream_proxy"
[ -n "$reverse_target" ] && uci_set main.reverse_target "$reverse_target"
[ -n "$ssl_insecure" ] && uci_set main.ssl_insecure "$ssl_insecure"
[ -n "$anticache" ] && uci_set main.anticache "$anticache"
[ -n "$anticomp" ] && uci_set main.anticomp "$anticomp"
# Apply transparent settings
[ -n "$transparent_enabled" ] && uci_set transparent.enabled "$transparent_enabled"
[ -n "$transparent_interface" ] && uci_set transparent.interface "$transparent_interface"
[ -n "$redirect_http" ] && uci_set transparent.redirect_http "$redirect_http"
[ -n "$redirect_https" ] && uci_set transparent.redirect_https "$redirect_https"
# Apply filtering settings
[ -n "$filtering_enabled" ] && uci_set filtering.enabled "$filtering_enabled"
[ -n "$log_requests" ] && uci_set filtering.log_requests "$log_requests"
[ -n "$filter_cdn" ] && uci_set filtering.filter_cdn "$filter_cdn"
[ -n "$filter_media" ] && uci_set filtering.filter_media "$filter_media"
[ -n "$block_ads" ] && uci_set filtering.block_ads "$block_ads"
uci commit mitmproxy
# Restart service to apply firewall rules if enabled and apply_now is set
local is_enabled=$(uci_get main.enabled)
local restarted=0
if [ "$is_enabled" = "1" ] && [ "$apply_now" = "1" ]; then
/etc/init.d/mitmproxy restart >/dev/null 2>&1 &
restarted=1
fi
json_add_boolean "success" 1
if [ "$restarted" = "1" ]; then
json_add_string "message" "Settings saved and applied"
json_add_boolean "restarted" 1
else
json_add_string "message" "Settings saved"
json_add_boolean "restarted" 0
fi
json_dump
}
set_mode() {
read input
json_load "$input"
local mode apply_now
json_get_var mode mode
json_get_var apply_now apply_now
json_init
if [ -z "$mode" ]; then
json_add_boolean "success" 0
json_add_string "error" "Mode is required"
json_dump
return
fi
# Validate mode
case "$mode" in
regular|transparent|upstream|reverse) ;;
*)
json_add_boolean "success" 0
json_add_string "error" "Invalid mode: $mode"
json_dump
return
;;
esac
# Ensure section exists
uci -q get mitmproxy.main >/dev/null 2>&1 || {
uci set mitmproxy.main=mitmproxy
uci set mitmproxy.main.enabled='0'
}
uci_set main.mode "$mode"
uci commit mitmproxy
# Restart to apply firewall rules if needed
local is_enabled=$(uci_get main.enabled)
local restarted=0
if [ "$is_enabled" = "1" ] && [ "$apply_now" = "1" ]; then
/etc/init.d/mitmproxy restart >/dev/null 2>&1 &
restarted=1
fi
json_add_boolean "success" 1
json_add_string "mode" "$mode"
if [ "$restarted" = "1" ]; then
json_add_string "message" "Mode set to $mode and applied"
json_add_boolean "restarted" 1
else
json_add_string "message" "Mode set to $mode"
json_add_boolean "restarted" 0
fi
json_dump
}
setup_firewall() {
json_init
if ! command -v mitmproxyctl >/dev/null 2>&1; then
json_add_boolean "success" 0
json_add_string "error" "mitmproxyctl not found"
json_dump
return
fi
mitmproxyctl firewall-setup >/tmp/mitmproxy-fw.log 2>&1
local result=$?
if [ $result -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Firewall rules applied"
else
json_add_boolean "success" 0
json_add_string "error" "Failed to setup firewall rules"
local log=$(cat /tmp/mitmproxy-fw.log 2>/dev/null)
[ -n "$log" ] && json_add_string "details" "$log"
fi
json_dump
}
clear_firewall() {
json_init
if ! command -v mitmproxyctl >/dev/null 2>&1; then
json_add_boolean "success" 0
json_add_string "error" "mitmproxyctl not found"
json_dump
return
fi
mitmproxyctl firewall-clear >/tmp/mitmproxy-fw.log 2>&1
local result=$?
if [ $result -eq 0 ]; then
json_add_boolean "success" 1
json_add_string "message" "Firewall rules cleared"
else
json_add_boolean "success" 0
json_add_string "error" "Failed to clear firewall rules"
fi
json_dump
}
do_install() {
command -v mitmproxyctl >/dev/null 2>&1 && { mitmproxyctl install >/tmp/mitmproxy-install.log 2>&1 & echo '{"success":true,"message":"Installing"}'; } || echo '{"success":false,"error":"mitmproxyctl not found"}'
}
@ -45,12 +329,26 @@ do_stop() { [ -x /etc/init.d/mitmproxy ] && /etc/init.d/mitmproxy stop >/dev/nul
do_restart() { [ -x /etc/init.d/mitmproxy ] && /etc/init.d/mitmproxy restart >/dev/null 2>&1; echo '{"success":true}'; }
list_methods() { cat <<'EOFM'
{"status":{},"install":{},"start":{},"stop":{},"restart":{}}
{"status":{},"settings":{},"save_settings":{"mode":"str","enabled":"bool","proxy_port":"int","web_port":"int","apply_now":"bool"},"set_mode":{"mode":"str","apply_now":"bool"},"setup_firewall":{},"clear_firewall":{},"install":{},"start":{},"stop":{},"restart":{}}
EOFM
}
case "$1" in
list) list_methods ;;
call) case "$2" in status) get_status ;; install) do_install ;; start) do_start ;; stop) do_stop ;; restart) do_restart ;; *) echo '{"error":"Unknown method"}' ;; esac ;;
call)
case "$2" in
status) get_status ;;
settings) get_settings ;;
save_settings) save_settings ;;
set_mode) set_mode ;;
setup_firewall) setup_firewall ;;
clear_firewall) clear_firewall ;;
install) do_install ;;
start) do_start ;;
stop) do_stop ;;
restart) do_restart ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
*) echo '{"error":"Unknown command"}' ;;
esac

View File

@ -1,7 +1,26 @@
{
"luci-app-mitmproxy": {
"description": "Grant access to mitmproxy",
"read": { "ubus": { "luci.mitmproxy": ["status"] }, "uci": ["mitmproxy"] },
"write": { "ubus": { "luci.mitmproxy": ["install", "start", "stop", "restart"] }, "uci": ["mitmproxy"] }
"read": {
"ubus": {
"luci.mitmproxy": ["status", "settings"]
},
"uci": ["mitmproxy"]
},
"write": {
"ubus": {
"luci.mitmproxy": [
"install",
"start",
"stop",
"restart",
"save_settings",
"set_mode",
"setup_firewall",
"clear_firewall"
]
},
"uci": ["mitmproxy"]
}
}
}

View File

@ -336,6 +336,15 @@ case "$1" in
json_add_object "setAdGuardEnabled"
json_add_string "enabled" "boolean"
json_close_object
json_add_object "getOpkgSettings"
json_close_object
json_add_object "setOpkgIpv4"
json_add_string "enabled" "boolean"
json_close_object
json_add_object "testUrl"
json_add_string "url" "string"
json_add_string "timeout" "integer"
json_close_object
json_dump
;;
@ -742,6 +751,98 @@ EOF
json_dump
;;
getOpkgSettings)
# Get opkg network settings
json_init
json_add_boolean "success" 1
# Check if IPv4 is forced (via profile alias)
ipv4_forced=0
if [ -f /etc/profile.d/opkg_ipv4.sh ]; then
grep -q "opkg.*-4\|force.*ipv4\|ipv4.*only" /etc/profile.d/opkg_ipv4.sh 2>/dev/null && ipv4_forced=1
fi
json_add_object "opkg"
json_add_boolean "ipv4_only" "$ipv4_forced"
json_close_object
json_dump
;;
setOpkgIpv4)
# Enable/disable IPv4-only mode for opkg
read input
json_load "$input"
json_get_var enabled enabled
if [ "$enabled" = "1" ] || [ "$enabled" = "true" ]; then
# Create profile script to force IPv4 for opkg
cat > /etc/profile.d/opkg_ipv4.sh << 'OPKG_EOF'
# Force opkg to use IPv4 only (uclient-fetch -4)
# Useful when IPv6 connectivity is broken
alias opkg='/bin/opkg'
export UCLIENT_FETCH_OPTS="-4"
OPKG_EOF
chmod +x /etc/profile.d/opkg_ipv4.sh
json_init
json_add_boolean "success" 1
json_add_string "message" "opkg IPv4-only mode enabled"
else
# Remove IPv4 forcing
rm -f /etc/profile.d/opkg_ipv4.sh
json_init
json_add_boolean "success" 1
json_add_string "message" "opkg IPv4-only mode disabled"
fi
json_dump
;;
testUrl)
# Test URL connectivity (for network diagnostics)
read input
json_load "$input"
json_get_var url url
json_get_var timeout timeout
[ -z "$timeout" ] && timeout=5
json_init
if [ -z "$url" ]; then
json_add_boolean "success" 0
json_add_string "error" "URL is required"
json_dump
exit 0
fi
# Test URL with wget --spider (more reliable than uclient-fetch)
status_msg=""
start_time=$(date +%s)
# Use wget with timeout and spider mode
if wget -4 --spider -q -T "$timeout" --no-check-certificate "$url" 2>/dev/null; then
status_msg="reachable"
else
status_msg="unreachable"
fi
end_time=$(date +%s)
response_time=$((end_time - start_time))
json_add_boolean "success" 1
json_add_object "result"
json_add_string "url" "$url"
json_add_string "status" "$status_msg"
json_add_int "response_time_s" "$response_time"
json_close_object
json_dump
;;
*)
json_init
json_add_boolean "success" 0

View File

@ -1089,6 +1089,7 @@ check_port_firewall_open() {
}
# Get network connectivity info (public IPs, port accessibility)
# NOTE: External port checks disabled - too slow (HTTP requests to external services)
method_get_network_info() {
json_init
json_add_boolean "success" 1
@ -1097,89 +1098,69 @@ method_get_network_info() {
lan_ip=$(get_lan_ip)
json_add_string "lan_ip" "$lan_ip"
# Get public IPv4
# Get public IPv4 (use uclient-fetch with IPv4 only for faster response)
json_add_object "ipv4"
local public_ipv4
public_ipv4=$(get_public_ipv4)
public_ipv4=$(uclient-fetch -4 -q -T 2 -O - "http://ipv4.icanhazip.com" 2>/dev/null | tr -d '\n')
if [ -n "$public_ipv4" ]; then
json_add_string "address" "$public_ipv4"
json_add_string "status" "ok"
# Reverse DNS
local rdns
rdns=$(get_reverse_dns "$public_ipv4")
[ -n "$rdns" ] && json_add_string "hostname" "$rdns"
else
json_add_string "status" "unavailable"
fi
json_close_object
# Get public IPv6
# IPv6 - skip network check (often broken/slow), just report firewall status
json_add_object "ipv6"
local public_ipv6
public_ipv6=$(get_public_ipv6)
if [ -n "$public_ipv6" ]; then
json_add_string "address" "$public_ipv6"
json_add_string "status" "ok"
# Reverse DNS
local rdns6
rdns6=$(get_reverse_dns "$public_ipv6")
[ -n "$rdns6" ] && json_add_string "hostname" "$rdns6"
# Check if IPv6 is enabled in network config
local ipv6_enabled=0
if uci -q get network.wan6 >/dev/null 2>&1 || \
uci -q get network.wan.ipv6 >/dev/null 2>&1; then
ipv6_enabled=1
fi
if [ "$ipv6_enabled" = "1" ]; then
json_add_string "status" "configured"
else
json_add_string "status" "unavailable"
json_add_string "status" "disabled"
fi
json_close_object
# External port accessibility (from internet perspective)
# External port accessibility - use firewall check only (fast)
# NOTE: Real external check would require slow HTTP requests to external services
json_add_object "external_ports"
if [ -n "$public_ipv4" ]; then
# Check port 80
json_add_object "http"
if check_external_port "$public_ipv4" 80; then
json_add_boolean "accessible" 1
json_add_string "status" "open"
else
json_add_boolean "accessible" 0
json_add_string "status" "blocked"
json_add_string "hint" "Check upstream router port forwarding"
fi
json_close_object
local http_fw=0
local https_fw=0
check_port_firewall_open 80 && http_fw=1
check_port_firewall_open 443 && https_fw=1
# Check port 443
json_add_object "https"
if check_external_port "$public_ipv4" 443; then
json_add_boolean "accessible" 1
json_add_string "status" "open"
else
json_add_boolean "accessible" 0
json_add_string "status" "blocked"
json_add_string "hint" "Check upstream router port forwarding"
fi
json_close_object
json_add_object "http"
if [ "$http_fw" = "1" ]; then
json_add_string "status" "firewall_open"
json_add_string "hint" "Firewall allows port 80"
else
json_add_object "http"
json_add_string "status" "unknown"
json_add_string "error" "No public IP"
json_close_object
json_add_object "https"
json_add_string "status" "unknown"
json_add_string "error" "No public IP"
json_close_object
json_add_string "status" "firewall_closed"
json_add_string "hint" "Add firewall rule for port 80"
fi
json_close_object
json_add_object "https"
if [ "$https_fw" = "1" ]; then
json_add_string "status" "firewall_open"
json_add_string "hint" "Firewall allows port 443"
else
json_add_string "status" "firewall_closed"
json_add_string "hint" "Add firewall rule for port 443"
fi
json_close_object
json_close_object
# Local firewall status
json_add_object "firewall"
local http_open=0
local https_open=0
check_port_firewall_open 80 && http_open=1
check_port_firewall_open 443 && https_open=1
json_add_boolean "http_open" "$http_open"
json_add_boolean "https_open" "$https_open"
if [ "$http_open" = "1" ] && [ "$https_open" = "1" ]; then
json_add_boolean "http_open" "$http_fw"
json_add_boolean "https_open" "$https_fw"
if [ "$http_fw" = "1" ] && [ "$https_fw" = "1" ]; then
json_add_string "status" "ok"
elif [ "$http_open" = "1" ] || [ "$https_open" = "1" ]; then
elif [ "$http_fw" = "1" ] || [ "$https_fw" = "1" ]; then
json_add_string "status" "partial"
else
json_add_string "status" "closed"
@ -1226,20 +1207,13 @@ method_check_service_health() {
json_add_string "service_id" "$service_id"
json_add_string "domain" "$domain"
# Get public IPs for comparison
local public_ipv4 public_ipv6
public_ipv4=$(get_public_ipv4)
public_ipv6=$(get_public_ipv6)
# Get public IPv4 (short timeout for responsiveness)
local public_ipv4
public_ipv4=$(wget -qO- -T 3 "http://ipv4.icanhazip.com" 2>/dev/null | tr -d '\n')
# Public IP info
json_add_object "public_ip"
json_add_string "ipv4" "$public_ipv4"
[ -n "$public_ipv6" ] && json_add_string "ipv6" "$public_ipv6"
if [ -n "$public_ipv4" ]; then
local rdns
rdns=$(get_reverse_dns "$public_ipv4")
[ -n "$rdns" ] && json_add_string "hostname" "$rdns"
fi
json_add_string "ipv4" "${public_ipv4:-unknown}"
json_close_object
# DNS check with public IP comparison
@ -1275,27 +1249,21 @@ method_check_service_health() {
fi
json_close_object
# External port accessibility check
# External port accessibility check (firewall-based, fast)
json_add_object "external_access"
if [ -n "$public_ipv4" ]; then
local http_ext=0
local https_ext=0
check_external_port "$public_ipv4" 80 && http_ext=1
check_external_port "$public_ipv4" 443 && https_ext=1
json_add_boolean "http_accessible" "$http_ext"
json_add_boolean "https_accessible" "$https_ext"
if [ "$http_ext" = "1" ] && [ "$https_ext" = "1" ]; then
json_add_string "status" "ok"
elif [ "$http_ext" = "1" ] || [ "$https_ext" = "1" ]; then
json_add_string "status" "partial"
json_add_string "hint" "Check upstream router/ISP port forwarding"
else
json_add_string "status" "blocked"
json_add_string "hint" "Ports not accessible from internet - check router/ISP"
fi
local http_fw=0
local https_fw=0
check_port_firewall_open 80 && http_fw=1
check_port_firewall_open 443 && https_fw=1
json_add_boolean "http_accessible" "$http_fw"
json_add_boolean "https_accessible" "$https_fw"
if [ "$http_fw" = "1" ] && [ "$https_fw" = "1" ]; then
json_add_string "status" "firewall_ok"
elif [ "$http_fw" = "1" ] || [ "$https_fw" = "1" ]; then
json_add_string "status" "partial"
else
json_add_string "status" "unknown"
json_add_string "error" "Could not determine public IP"
json_add_string "status" "closed"
json_add_string "hint" "Open firewall ports 80/443 for external access"
fi
json_close_object

View File

@ -105,7 +105,7 @@ var callSettings = rpc.declare({
var callSaveSettings = rpc.declare({
object: 'luci.tor-shield',
method: 'save_settings',
params: ['mode', 'dns_over_tor', 'kill_switch', 'socks_port', 'trans_port', 'dns_port', 'exit_nodes', 'exclude_exit_nodes', 'strict_nodes'],
params: ['mode', 'dns_over_tor', 'kill_switch', 'socks_port', 'trans_port', 'dns_port', 'exit_nodes', 'exclude_exit_nodes', 'strict_nodes', 'apply_now'],
expect: { }
});
@ -173,8 +173,8 @@ return baseclass.extend({
getBridges: function() { return callBridges(); },
setBridges: function(enabled, type) { return callSetBridges(enabled, type); },
getSettings: function() { return callSettings(); },
saveSettings: function(mode, dns_over_tor, kill_switch, socks_port, trans_port, dns_port, exit_nodes, exclude_exit_nodes, strict_nodes) {
return callSaveSettings(mode, dns_over_tor, kill_switch, socks_port, trans_port, dns_port, exit_nodes, exclude_exit_nodes, strict_nodes);
saveSettings: function(mode, dns_over_tor, kill_switch, socks_port, trans_port, dns_port, exit_nodes, exclude_exit_nodes, strict_nodes, apply_now) {
return callSaveSettings(mode, dns_over_tor, kill_switch, socks_port, trans_port, dns_port, exit_nodes, exclude_exit_nodes, strict_nodes, apply_now !== false ? '1' : '0');
},
formatBytes: formatBytes,

View File

@ -10,7 +10,7 @@ return view.extend({
return api.getSettings();
},
handleSave: function(form) {
handleSave: function(form, applyNow) {
var self = this;
// Gather form values
@ -27,7 +27,7 @@ return view.extend({
};
ui.showModal(_('Saving Settings'), [
E('p', { 'class': 'spinning' }, _('Saving configuration...'))
E('p', { 'class': 'spinning' }, applyNow ? _('Saving and applying configuration...') : _('Saving configuration...'))
]);
api.saveSettings(
@ -39,11 +39,16 @@ return view.extend({
settings.dns_port,
settings.exit_nodes,
settings.exclude_exit_nodes,
settings.strict_nodes
settings.strict_nodes,
applyNow
).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Settings saved. Restart Tor Shield to apply changes.')), 'info');
if (result.restarted) {
ui.addNotification(null, E('p', _('Settings saved and applied. Firewall rules updated.')), 'info');
} else {
ui.addNotification(null, E('p', _('Settings saved. Restart Tor Shield to apply changes.')), 'info');
}
} else {
ui.addNotification(null, E('p', result.error || _('Failed to save settings')), 'error');
}
@ -231,14 +236,22 @@ return view.extend({
]),
// Actions
E('div', { 'style': 'display: flex; gap: 12px; margin-top: 20px;' }, [
E('div', { 'style': 'display: flex; gap: 12px; margin-top: 20px; flex-wrap: wrap;' }, [
E('button', {
'type': 'button',
'class': 'tor-btn tor-btn-primary',
'style': 'background: linear-gradient(135deg, #059669 0%, #10b981 100%);',
'click': function() {
self.handleSave(document.getElementById('tor-settings-form'));
self.handleSave(document.getElementById('tor-settings-form'), true);
}
}, ['\uD83D\uDCBE ', _('Save Settings')]),
}, ['\u26A1 ', _('Save & Apply')]),
E('button', {
'type': 'button',
'class': 'tor-btn',
'click': function() {
self.handleSave(document.getElementById('tor-settings-form'), false);
}
}, ['\uD83D\uDCBE ', _('Save Only')]),
E('a', {
'href': L.url('admin', 'services', 'tor-shield'),
'class': 'tor-btn'

View File

@ -842,7 +842,7 @@ save_settings() {
# Get values from input BEFORE json_init (which wipes loaded JSON)
local mode dns_over_tor kill_switch socks_port trans_port dns_port
local exit_nodes exclude_exit strict_nodes
local exit_nodes exclude_exit strict_nodes apply_now
json_get_var mode mode
json_get_var dns_over_tor dns_over_tor
json_get_var kill_switch kill_switch
@ -852,6 +852,7 @@ save_settings() {
json_get_var exit_nodes exit_nodes
json_get_var exclude_exit exclude_exit_nodes
json_get_var strict_nodes strict_nodes
json_get_var apply_now apply_now
# Now initialize output JSON
json_init
@ -891,8 +892,25 @@ save_settings() {
uci commit tor-shield
# Restart service to apply firewall rules if Tor is enabled and apply_now is set
local enabled
config_load "$CONFIG"
config_get enabled main enabled '0'
local restarted=0
if [ "$enabled" = "1" ] && [ "$apply_now" = "1" ]; then
/etc/init.d/tor-shield restart >/dev/null 2>&1 &
restarted=1
fi
json_add_boolean "success" 1
json_add_string "message" "Settings saved"
if [ "$restarted" = "1" ]; then
json_add_string "message" "Settings saved and applied"
json_add_boolean "restarted" 1
else
json_add_string "message" "Settings saved"
json_add_boolean "restarted" 0
fi
json_dump
}

View File

@ -1,222 +1,170 @@
# LuCI WireGuard Dashboard
**Version:** 0.4.0
**Last Updated:** 2025-12-28
**Status:** Active
![Version](https://img.shields.io/badge/version-1.0.0-cyan)
![License](https://img.shields.io/badge/license-Apache--2.0-green)
![OpenWrt](https://img.shields.io/badge/OpenWrt-21.02+-orange)
Modern and intuitive dashboard for WireGuard VPN monitoring on OpenWrt. Visualize tunnels, peers, and traffic in real-time.
![Dashboard Preview](screenshots/dashboard-preview.png)
Modern WireGuard VPN management interface for OpenWrt with setup wizard, peer management, and real-time monitoring.
## Features
### 🔐 Tunnel Status
- Real-time interface monitoring
- Public key display
- Listen port and MTU info
- Interface state (up/down)
### 👥 Peer Management
- Active/idle/inactive status
- Endpoint tracking
- Last handshake time
- Allowed IPs display
- Preshared key indicator
### 📊 Traffic Statistics
- Per-peer RX/TX bytes
- Per-interface totals
- Combined traffic view
- Visual progress bars
### ⚙️ Configuration View
- WireGuard config syntax display
- Interface and peer sections
- Tunnel visualization
- UCI integration info
### 🎨 Modern Interface
- Cyan/blue VPN tunnel theme
- Animated status indicators
- Responsive grid layout
- Real-time updates
## Screenshots
### Status Overview
![Status](screenshots/status.png)
### Peers List
![Peers](screenshots/peers.png)
### Traffic Statistics
![Traffic](screenshots/traffic.png)
### Configuration
![Config](screenshots/config.png)
- **Setup Wizard**: Create tunnels and peers in minutes with presets for common use cases
- **Dashboard Overview**: Real-time status of all tunnels and peers
- **Peer Management**: Add, remove, and configure peers with QR code generation
- **Traffic Monitoring**: Live bandwidth statistics per interface and peer
- **Client Config Export**: Generate configuration files and QR codes for mobile apps
## Installation
### Prerequisites
- OpenWrt 21.02 or later
- WireGuard installed (`kmod-wireguard`, `wireguard-tools`)
- LuCI web interface
```bash
# Install WireGuard
opkg update
opkg install kmod-wireguard wireguard-tools luci-proto-wireguard
opkg install luci-app-wireguard-dashboard
```
### From Source
### Dependencies
- `wireguard-tools` - WireGuard userspace tools
- `luci-base` - LuCI web interface
- `qrencode` (optional) - For server-side QR code generation
## Setup Wizard
The wizard provides preset configurations for common VPN scenarios:
### Tunnel Presets
| Preset | Description | Default Port | Network |
|--------|-------------|--------------|---------|
| Road Warrior | Remote access for mobile users | 51820 | 10.10.0.0/24 |
| Site-to-Site | Connect two networks | 51821 | 10.20.0.0/24 |
| IoT Tunnel | Isolated tunnel for smart devices | 51822 | 10.30.0.0/24 |
### Peer Zone Presets
| Zone | Description | Tunnel Mode |
|------|-------------|-------------|
| Home User | Full network access | Full |
| Remote Worker | Office resources only | Split |
| Mobile Device | On-the-go access | Full |
| IoT Device | Limited VPN-only access | Split |
| Guest | Temporary visitor access | Full |
| Server/Site | Site-to-site connection | Split |
### Wizard Flow
1. **Select Tunnel Type** - Choose preset (Road Warrior, Site-to-Site, IoT)
2. **Configure Tunnel** - Set interface name, port, VPN network, public endpoint
3. **Select Peer Zones** - Choose which peer types to create
4. **Create** - Wizard generates keys, creates interface, adds peers, shows QR codes
## RPCD API
The dashboard communicates via `luci.wireguard-dashboard` RPCD object.
### Methods
| Method | Parameters | Description |
|--------|------------|-------------|
| `status` | - | Get overall WireGuard status |
| `interfaces` | - | List all WireGuard interfaces |
| `peers` | - | List all peers with status |
| `traffic` | - | Get traffic statistics |
| `generate_keys` | - | Generate new key pair + PSK |
| `create_interface` | name, private_key, listen_port, addresses, mtu | Create new WireGuard interface with firewall rules |
| `add_peer` | interface, name, allowed_ips, public_key, preshared_key, endpoint, persistent_keepalive | Add peer to interface |
| `remove_peer` | interface, public_key | Remove peer from interface |
| `interface_control` | interface, action (up/down/restart) | Control interface state |
| `generate_config` | interface, peer, private_key, endpoint | Generate client config file |
| `generate_qr` | interface, peer, private_key, endpoint | Generate QR code (requires qrencode) |
### Example: Create Interface via CLI
```bash
# Clone into OpenWrt build environment
cd ~/openwrt/feeds/luci/applications/
git clone https://github.com/gkerma/luci-app-wireguard-dashboard.git
# Generate keys
keys=$(ubus call luci.wireguard-dashboard generate_keys '{}')
privkey=$(echo "$keys" | jsonfilter -e '@.private_key')
# Update feeds and install
cd ~/openwrt
./scripts/feeds update -a
./scripts/feeds install -a
# Enable in menuconfig
make menuconfig
# Navigate to: LuCI > Applications > luci-app-wireguard-dashboard
# Build package
make package/luci-app-wireguard-dashboard/compile V=s
# Create interface
ubus call luci.wireguard-dashboard create_interface "{
\"name\": \"wg0\",
\"private_key\": \"$privkey\",
\"listen_port\": \"51820\",
\"addresses\": \"10.10.0.1/24\",
\"mtu\": \"1420\"
}"
```
### Manual Installation
### Example: Add Peer via CLI
```bash
# Transfer package to router
scp luci-app-wireguard-dashboard_1.0.0-1_all.ipk root@192.168.1.1:/tmp/
# Generate peer keys
peer_keys=$(ubus call luci.wireguard-dashboard generate_keys '{}')
peer_pubkey=$(echo "$peer_keys" | jsonfilter -e '@.public_key')
peer_psk=$(echo "$peer_keys" | jsonfilter -e '@.preshared_key')
# Install on router
ssh root@192.168.1.1
opkg install /tmp/luci-app-wireguard-dashboard_1.0.0-1_all.ipk
# Restart services
/etc/init.d/rpcd restart
# Add peer
ubus call luci.wireguard-dashboard add_peer "{
\"interface\": \"wg0\",
\"name\": \"Phone\",
\"allowed_ips\": \"10.10.0.2/32\",
\"public_key\": \"$peer_pubkey\",
\"preshared_key\": \"$peer_psk\",
\"persistent_keepalive\": \"25\"
}"
```
## Usage
## Firewall Integration
After installation, access the dashboard at:
When creating an interface via the wizard or `create_interface` API, the following firewall rules are automatically created:
**VPN → WireGuard Dashboard**
1. **Zone** (`wg_<interface>`): INPUT/OUTPUT/FORWARD = ACCEPT
2. **Forwarding**: Bidirectional forwarding to/from `lan` zone
3. **WAN Rule**: Allow UDP traffic on listen port from WAN
The dashboard has four tabs:
1. **Status**: Overview with interfaces and active peers
2. **Peers**: Detailed peer information and status
3. **Traffic**: Bandwidth statistics per peer/interface
4. **Configuration**: Config file view and tunnel visualization
## File Locations
## Architecture
| File | Purpose |
|------|---------|
| `/usr/libexec/rpcd/luci.wireguard-dashboard` | RPCD backend |
| `/www/luci-static/resources/wireguard-dashboard/api.js` | JavaScript API wrapper |
| `/www/luci-static/resources/view/wireguard-dashboard/*.js` | LuCI views |
| `/usr/share/luci/menu.d/luci-app-wireguard-dashboard.json` | Menu configuration |
| `/usr/share/rpcd/acl.d/luci-app-wireguard-dashboard.json` | ACL permissions |
```
┌─────────────────────────────────────────────────────────┐
│ LuCI JavaScript │
│ (status.js, peers.js, traffic.js) │
└───────────────────────────┬─────────────────────────────┘
│ ubus RPC
┌─────────────────────────────────────────────────────────┐
│ RPCD Backend │
│ /usr/libexec/rpcd/wireguard-dashboard │
└───────────────────────────┬─────────────────────────────┘
│ executes
┌─────────────────────────────────────────────────────────┐
│ wg show │
│ WireGuard CLI Tool │
└───────────────────────────┬─────────────────────────────┘
│ manages
┌─────────────────────────────────────────────────────────┐
│ WireGuard Kernel Module │
│ Encrypted Tunnels │
└─────────────────────────────────────────────────────────┘
## Troubleshooting
### Interface not coming up
```bash
# Check interface status
wg show wg0
# Check UCI configuration
uci show network.wg0
# Manually bring up
ifup wg0
# Check logs
logread | grep -i wireguard
```
## API Endpoints
### Peers not connecting
| Method | Description |
|--------|-------------|
| `status` | Overall VPN status, interface/peer counts, total traffic |
| `interfaces` | Detailed interface info (pubkey, port, IPs, state) |
| `peers` | All peers with endpoint, handshake, traffic, allowed IPs |
| `traffic` | Per-peer and per-interface RX/TX statistics |
| `config` | Configuration display (no private keys exposed) |
1. Verify firewall port is open: `iptables -L -n | grep 51820`
2. Check endpoint is reachable from client
3. Verify allowed_ips match on both ends
4. Check for NAT issues - enable PersistentKeepalive
## Peer Status Indicators
### QR codes not generating
| Status | Meaning | Handshake Age |
|--------|---------|---------------|
| 🟢 Active | Recent communication | < 3 minutes |
| 🟡 Idle | No recent traffic | 3-10 minutes |
| ⚪ Inactive | No handshake | > 10 minutes or never |
Install qrencode for server-side QR generation:
```bash
opkg install qrencode
```
## Requirements
- OpenWrt 21.02+
- `kmod-wireguard` (kernel module)
- `wireguard-tools` (wg command)
- `luci-proto-wireguard` (optional, for LuCI config)
- LuCI (luci-base)
- rpcd with luci module
## Dependencies
- `luci-base`
- `luci-lib-jsonc`
- `rpcd`
- `rpcd-mod-luci`
- `wireguard-tools`
## Security Notes
- Private keys are **never** exposed through the dashboard
- Only public keys and configuration are displayed
- All data is read-only (no config modifications)
- RPCD ACLs restrict access to authorized users
## Contributing
Contributions are welcome! Please feel free to submit issues and pull requests.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
The dashboard also supports client-side QR generation via JavaScript (no server dependency).
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
Apache-2.0
## Credits
## Author
- Powered by [WireGuard®](https://www.wireguard.com/)
- Built for [OpenWrt](https://openwrt.org/)
- Developed by [Gandalf @ CyberMind.fr](https://cybermind.fr)
## Related Projects
- [luci-proto-wireguard](https://github.com/openwrt/luci) - WireGuard protocol support
- [wg-easy](https://github.com/WeeJeWel/wg-easy) - Web UI for WireGuard
- [wireguard-ui](https://github.com/ngoduykhanh/wireguard-ui) - Another WireGuard UI
---
Made with 🔐 for secure networking
*WireGuard is a registered trademark of Jason A. Donenfeld.*
CyberMind.fr - SecuBox Project

View File

@ -43,8 +43,11 @@ return view.extend({
render: function(data) {
var self = this;
var peers = (data[0] || {}).peers || [];
var interfaces = (data[1] || {}).interfaces || [];
// Handle RPC expect unwrapping - results may be array or object with .peers/.interfaces
var peersData = data[0] || [];
var interfacesData = data[1] || [];
var peers = Array.isArray(peersData) ? peersData : (peersData.peers || []);
var interfaces = Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []);
var activePeers = peers.filter(function(p) { return p.status === 'active'; }).length;
var view = E('div', { 'class': 'cbi-map' }, [

View File

@ -14,7 +14,9 @@ return view.extend({
render: function(data) {
var status = data[0] || {};
var interfaces = (data[1] || {}).interfaces || [];
// Handle RPC expect unwrapping - results may be array or object
var interfacesData = data[1] || [];
var interfaces = Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []);
var view = E('div', { 'class': 'cbi-map' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),

View File

@ -137,7 +137,9 @@ return view.extend({
render: function(data) {
var self = this;
var interfaces = (data[0] || {}).interfaces || [];
// Handle RPC expect unwrapping - results may be array or object
var interfacesData = data[0] || [];
var interfaces = Array.isArray(interfacesData) ? interfacesData : (interfacesData.interfaces || []);
var status = data[1] || {};
var publicIP = data[2] || '';
@ -569,20 +571,33 @@ return view.extend({
createInterface: function() {
var self = this;
var data = this.wizardData;
var netmask = data.vpnNetwork.split('/')[1] || '24';
var addresses = data.serverIP + '/' + netmask;
// Call backend to create interface
return rpc.call('uci', 'add', {
config: 'network',
type: 'interface',
name: data.ifaceName,
values: {
proto: 'wireguard',
private_key: data.privateKey,
listen_port: data.listenPort,
addresses: [data.serverIP + '/' + data.vpnNetwork.split('/')[1]]
// Call backend to create interface using proper RPCD method
return api.createInterface(
data.ifaceName,
data.privateKey,
data.listenPort,
addresses,
data.mtu || '1420'
).then(function(result) {
// Handle various response formats from RPC
// Result could be: boolean, object with success field, or object with error
if (result === true) {
return { success: true };
}
}).then(function() {
return rpc.call('uci', 'commit', { config: 'network' });
if (result === false) {
throw new Error('Failed to create interface');
}
if (typeof result === 'object' && result !== null) {
if (result.success === false || result.error) {
throw new Error(result.error || 'Failed to create interface');
}
return result;
}
// If we got here with no error, assume success
return { success: true };
});
},

View File

@ -34,18 +34,25 @@ var callGenerateKeys = rpc.declare({
expect: { }
});
var callCreateInterface = rpc.declare({
object: 'luci.wireguard-dashboard',
method: 'create_interface',
params: ['name', 'private_key', 'listen_port', 'addresses', 'mtu'],
expect: { }
});
var callAddPeer = rpc.declare({
object: 'luci.wireguard-dashboard',
method: 'add_peer',
params: ['interface', 'name', 'allowed_ips', 'public_key', 'preshared_key', 'endpoint', 'persistent_keepalive'],
expect: { success: false }
expect: { }
});
var callRemovePeer = rpc.declare({
object: 'luci.wireguard-dashboard',
method: 'remove_peer',
params: ['interface', 'public_key'],
expect: { success: false }
expect: { }
});
var callGetConfig = rpc.declare({
@ -78,7 +85,7 @@ var callInterfaceControl = rpc.declare({
object: 'luci.wireguard-dashboard',
method: 'interface_control',
params: ['interface', 'action'],
expect: { success: false }
expect: { }
});
var callPeerDescriptions = rpc.declare({
@ -154,6 +161,7 @@ return baseclass.extend({
getConfig: callGetConfig,
getTraffic: callGetTraffic,
generateKeys: callGenerateKeys,
createInterface: callCreateInterface,
addPeer: callAddPeer,
removePeer: callRemovePeer,
generateConfig: callGenerateConfig,

View File

@ -387,6 +387,114 @@ generate_keys() {
json_dump
}
# Create a new WireGuard interface
create_interface() {
read input
json_load "$input"
json_get_var name name
json_get_var private_key private_key
json_get_var listen_port listen_port
json_get_var addresses addresses
json_get_var mtu mtu
json_init
# Validate required fields
if [ -z "$name" ] || [ -z "$private_key" ]; then
json_add_boolean "success" 0
json_add_string "error" "Missing required fields: name and private_key"
json_dump
return
fi
# Check if interface already exists
if uci -q get network.$name >/dev/null 2>&1; then
json_add_boolean "success" 0
json_add_string "error" "Interface $name already exists"
json_dump
return
fi
# Validate private key format (base64, 44 chars ending with =)
if ! echo "$private_key" | grep -qE '^[A-Za-z0-9+/]{43}=$'; then
json_add_boolean "success" 0
json_add_string "error" "Invalid private key format"
json_dump
return
fi
# Default values
[ -z "$listen_port" ] && listen_port="51820"
[ -z "$mtu" ] && mtu="1420"
# Create the interface in UCI
uci set network.$name=interface
uci set network.$name.proto='wireguard'
uci set network.$name.private_key="$private_key"
uci set network.$name.listen_port="$listen_port"
uci set network.$name.mtu="$mtu"
# Set addresses (can be multiple)
if [ -n "$addresses" ]; then
uci delete network.$name.addresses 2>/dev/null
for addr in $addresses; do
uci add_list network.$name.addresses="$addr"
done
fi
# Commit changes
if uci commit network; then
# Create firewall zone for the interface
local zone_name="wg_${name}"
local zone_exists=$(uci show firewall 2>/dev/null | grep "\.name='$zone_name'" | head -1)
if [ -z "$zone_exists" ]; then
# Add new firewall zone
local zone_section=$(uci add firewall zone)
uci set firewall.$zone_section.name="$zone_name"
uci set firewall.$zone_section.input='ACCEPT'
uci set firewall.$zone_section.output='ACCEPT'
uci set firewall.$zone_section.forward='ACCEPT'
uci add_list firewall.$zone_section.network="$name"
# Add forwarding to lan
local fwd_section=$(uci add firewall forwarding)
uci set firewall.$fwd_section.src="$zone_name"
uci set firewall.$fwd_section.dest='lan'
# Add forwarding from lan
local fwd2_section=$(uci add firewall forwarding)
uci set firewall.$fwd2_section.src='lan'
uci set firewall.$fwd2_section.dest="$zone_name"
# Add rule to allow WireGuard port from wan
local rule_section=$(uci add firewall rule)
uci set firewall.$rule_section.name="Allow-WireGuard-$name"
uci set firewall.$rule_section.src='wan'
uci set firewall.$rule_section.dest_port="$listen_port"
uci set firewall.$rule_section.proto='udp'
uci set firewall.$rule_section.target='ACCEPT'
uci commit firewall
fi
# Bring up the interface
ifup "$name" 2>/dev/null &
# Reload firewall
/etc/init.d/firewall reload 2>/dev/null &
json_add_boolean "success" 1
json_add_string "message" "Interface $name created successfully"
json_add_string "interface" "$name"
else
json_add_boolean "success" 0
json_add_string "error" "Failed to commit network configuration"
fi
json_dump
}
# Add a new peer to interface
add_peer() {
read input
@ -893,7 +1001,7 @@ get_bandwidth_rates() {
# Main dispatcher
case "$1" in
list)
echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"str"},"remove_peer":{"interface":"str","public_key":"str"},"generate_config":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"generate_qr":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"bandwidth_history":{},"endpoint_info":{"endpoint":"str"},"ping_peer":{"ip":"str"},"interface_control":{"interface":"str","action":"str"},"peer_descriptions":{},"bandwidth_rates":{}}'
echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"create_interface":{"name":"str","private_key":"str","listen_port":"str","addresses":"str","mtu":"str"},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"str"},"remove_peer":{"interface":"str","public_key":"str"},"generate_config":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"generate_qr":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"bandwidth_history":{},"endpoint_info":{"endpoint":"str"},"ping_peer":{"ip":"str"},"interface_control":{"interface":"str","action":"str"},"peer_descriptions":{},"bandwidth_rates":{}}'
;;
call)
case "$2" in
@ -915,6 +1023,9 @@ case "$1" in
generate_keys)
generate_keys
;;
create_interface)
create_interface
;;
add_peer)
add_peer
;;

View File

@ -27,6 +27,7 @@
"ubus": {
"luci.wireguard-dashboard": [
"generate_keys",
"create_interface",
"add_peer",
"remove_peer",
"generate_config",

View File

@ -5,7 +5,7 @@
# Main orchestration service for SecuBox framework
#
set -e
# Note: Not using set -e as many UCI/service checks legitimately return non-zero
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
@ -18,7 +18,57 @@ WATCHDOG_STATE="/var/run/secubox/watchdog.json"
# Services to monitor (init.d name:check_method:restart_delay)
# check_method: pid, docker, lxc, port:PORT
MONITORED_SERVICES="haproxy:pid:5 crowdsec:pid:10 tor:pid:10"
MONITORED_SERVICES=""
# Auto-discover SecuBox services from ctl scripts
discover_secubox_services() {
local services=""
# Discover LXC-based services from *ctl scripts
for ctl in /usr/sbin/*ctl; do
[ -x "$ctl" ] || continue
local basename=$(basename "$ctl")
# Extract service name from xxxctl pattern
local svc_name=""
case "$basename" in
haproxyctl)
svc_name="haproxy"
services="$services haproxy:lxc:5"
;;
lyrionctl)
svc_name="lyrion"
services="$services lyrion:lxc:10"
;;
mitmproxyctl)
svc_name="mitmproxy"
services="$services mitmproxy:lxc:10"
;;
metablogizerctl)
svc_name="metablogizer"
services="$services metablogizer:lxc:10"
;;
hexojsctl)
svc_name="hexojs"
services="$services hexojs:lxc:10"
;;
adguardhomectl)
svc_name="adguardhome"
services="$services adguardhome:docker:10"
;;
esac
done
# Add native services (PID-based)
if [ -x "/etc/init.d/crowdsec" ]; then
services="$services crowdsec:pid:10"
fi
if [ -x "/etc/init.d/tor" ]; then
services="$services tor:pid:10"
fi
echo "$services"
}
# Logging function
log() {
@ -172,10 +222,14 @@ run_watchdog() {
local watchdog_enabled=$(uci -q get secubox.main.watchdog_enabled || echo "1")
[ "$watchdog_enabled" != "1" ] && return 0
# Get monitored services from UCI or use defaults
local services=$(uci -q get secubox.main.watchdog_services || echo "$MONITORED_SERVICES")
# Auto-discover services if none configured
local services=$(uci -q get secubox.main.watchdog_services)
[ -z "$services" ] && services=$(discover_secubox_services)
local restart_count=0
local status_json=""
local checked_count=0
local running_count=0
local services_status=""
log debug "Watchdog: Checking services..."
@ -184,66 +238,105 @@ run_watchdog() {
local check_method=$(echo "$service_entry" | cut -d: -f2)
local restart_delay=$(echo "$service_entry" | cut -d: -f3)
[ -z "$restart_delay" ] && restart_delay=5
[ -z "$check_method" ] && check_method="pid"
# Check if service init script exists
[ ! -x "/etc/init.d/$service_name" ] && continue
# Determine if service is enabled (check ctl script or init.d)
local ctl_script="/usr/sbin/${service_name}ctl"
local init_script="/etc/init.d/$service_name"
local is_enabled=false
# Check if service is supposed to be enabled
/etc/init.d/$service_name enabled >/dev/null 2>&1 || continue
if [ -x "$ctl_script" ]; then
# Check via UCI for LXC/Docker services
local uci_enabled=$(uci -q get "$service_name.main.enabled" 2>/dev/null || echo "0")
[ "$uci_enabled" = "1" ] && is_enabled=true
elif [ -x "$init_script" ]; then
$init_script enabled >/dev/null 2>&1 && is_enabled=true
fi
# Skip disabled services
[ "$is_enabled" = "false" ] && continue
checked_count=$((checked_count + 1))
local is_running=false
local status_detail=""
case "$check_method" in
pid)
# Check via pidof or pgrep
# Check via pgrep
if pgrep "$service_name" >/dev/null 2>&1; then
is_running=true
status_detail="pid=$(pgrep -o "$service_name")"
fi
;;
docker)
# Check Docker container
if docker ps --filter "name=$service_name" --format "{{.Names}}" 2>/dev/null | grep -q "$service_name"; then
# Check Docker container (secbx- prefix)
local container_name="secbx-${service_name}"
if docker ps --filter "name=$container_name" --format "{{.Names}}" 2>/dev/null | grep -q "$container_name"; then
is_running=true
status_detail="container=$container_name"
fi
;;
lxc)
# Check LXC container
if lxc-info -n "$service_name" -s 2>/dev/null | grep -q "RUNNING"; then
is_running=true
# Get container IP if available
local lxc_ip=$(lxc-info -n "$service_name" -i 2>/dev/null | awk '{print $2}' | head -1)
[ -n "$lxc_ip" ] && status_detail="ip=$lxc_ip"
fi
;;
port:*)
# Check if port is listening
local port=$(echo "$check_method" | cut -d: -f2)
# Use /proc/net/tcp (ports in hex)
local port_hex=$(printf '%04X' "$port")
if grep -q ":$port_hex " /proc/net/tcp /proc/net/tcp6 2>/dev/null; then
is_running=true
status_detail="port=$port"
fi
;;
esac
if [ "$is_running" = "false" ]; then
if [ "$is_running" = "true" ]; then
running_count=$((running_count + 1))
services_status="$services_status ${service_name}:ok"
else
services_status="$services_status ${service_name}:down"
log warn "Watchdog: $service_name is down, restarting..."
sleep "$restart_delay"
# Double-check before restart (service might have recovered)
case "$check_method" in
pid) pgrep "$service_name" >/dev/null 2>&1 && continue ;;
pid) pgrep "$service_name" >/dev/null 2>&1 && { running_count=$((running_count + 1)); continue; } ;;
lxc) lxc-info -n "$service_name" -s 2>/dev/null | grep -q "RUNNING" && { running_count=$((running_count + 1)); continue; } ;;
esac
/etc/init.d/$service_name restart >/dev/null 2>&1
# Restart using ctl script if available, otherwise init.d
if [ -x "$ctl_script" ]; then
$ctl_script restart >/dev/null 2>&1 &
elif [ -x "$init_script" ]; then
$init_script restart >/dev/null 2>&1 &
fi
restart_count=$((restart_count + 1))
# Log restart event
log info "Watchdog: Restarted $service_name"
fi
done
# Save watchdog state
# Save detailed watchdog state
json_init
json_add_string "last_check" "$(date -Iseconds)"
json_add_int "restarts" "$restart_count"
json_add_int "checked" "$checked_count"
json_add_int "running" "$running_count"
json_add_object "services"
for svc_status in $services_status; do
local svc=$(echo "$svc_status" | cut -d: -f1)
local status=$(echo "$svc_status" | cut -d: -f2)
json_add_string "$svc" "$status"
done
json_close_object
json_dump > "$WATCHDOG_STATE" 2>/dev/null
return 0