From fa1f6ddbb82d8c73c7daededbc6577bc45cee36c Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 3 Feb 2026 13:46:26 +0100 Subject: [PATCH] feat(tor-shield): Add server mode for split-routing with public IP preservation Server mode routes all outbound traffic through Tor while preserving inbound connections (HAProxy, etc) on the public IP. Fixes kill switch blocking response packets by adding ESTABLISHED,RELATED conntrack rule, and adds PREROUTING chain for LAN client Tor routing. Co-Authored-By: Claude Opus 4.5 --- .../luci-static/resources/tor-shield/api.js | 6 +- .../resources/view/tor-shield/overview.js | 3 +- .../resources/view/tor-shield/settings.js | 16 +++++ .../root/usr/libexec/rpcd/luci.tor-shield | 22 +++++-- .../files/etc/config/tor-shield | 9 +++ .../files/etc/init.d/tor-shield | 58 ++++++++++++++++--- .../secubox-app-tor/files/usr/sbin/torctl | 7 ++- 7 files changed, 104 insertions(+), 17 deletions(-) diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js index 1525f289..4e6a77f9 100644 --- a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js @@ -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', 'apply_now'], + params: ['mode', 'dns_over_tor', 'kill_switch', 'socks_port', 'trans_port', 'dns_port', 'lan_proxy', '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, 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'); + saveSettings: function(mode, dns_over_tor, kill_switch, socks_port, trans_port, dns_port, lan_proxy, exit_nodes, exclude_exit_nodes, strict_nodes, apply_now) { + return callSaveSettings(mode, dns_over_tor, kill_switch, socks_port, trans_port, dns_port, lan_proxy, exit_nodes, exclude_exit_nodes, strict_nodes, apply_now !== false ? '1' : '0'); }, formatBytes: formatBytes, diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js index bb59bacb..5e095961 100644 --- a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js @@ -477,7 +477,8 @@ return view.extend({ [ { id: 'anonymous', name: _('Full Anonymity'), icon: '\u{1F6E1}', emoji: '\u{1F9D9}', desc: _('All traffic through Tor'), features: ['\u{2705} ' + _('Kill Switch'), '\u{2705} ' + _('DNS Protection'), '\u{2705} ' + _('Full Routing')] }, { id: 'selective', name: _('Selective Apps'), icon: '\u{1F3AF}', emoji: '\u{1F50D}', desc: _('SOCKS proxy mode'), features: ['\u{26AA} ' + _('No Kill Switch'), '\u{26AA} ' + _('Manual Config'), '\u{2705} ' + _('App Control')] }, - { id: 'censored', name: _('Bypass Censorship'), icon: '\u{1F513}', emoji: '\u{1F30D}', desc: _('Bridge connections'), features: ['\u{2705} ' + _('obfs4 Bridges'), '\u{2705} ' + _('Anti-Censorship'), '\u{2705} ' + _('Stealth Mode')] } + { id: 'censored', name: _('Bypass Censorship'), icon: '\u{1F513}', emoji: '\u{1F30D}', desc: _('Bridge connections'), features: ['\u{2705} ' + _('obfs4 Bridges'), '\u{2705} ' + _('Anti-Censorship'), '\u{2705} ' + _('Stealth Mode')] }, + { id: 'server', name: _('Server Mode'), icon: '\u{1F5A5}', emoji: '\u{1F5A5}\uFE0F', desc: _('Public IP + Tor outbound'), features: ['\u{2705} ' + _('Public IP Preserved'), '\u{2705} ' + _('Outbound via Tor'), '\u{2705} ' + _('LAN Clients Anonymized')] } ].map(function(preset) { var isSelected = self.currentPreset === preset.id; return E('div', { diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/settings.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/settings.js index cc96db84..025a87ab 100644 --- a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/settings.js +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/settings.js @@ -18,6 +18,7 @@ return view.extend({ mode: form.querySelector('[name="mode"]').value, dns_over_tor: form.querySelector('[name="dns_over_tor"]').checked ? '1' : '0', kill_switch: form.querySelector('[name="kill_switch"]').checked ? '1' : '0', + lan_proxy: form.querySelector('[name="lan_proxy"]').checked ? '1' : '0', socks_port: parseInt(form.querySelector('[name="socks_port"]').value) || 9050, trans_port: parseInt(form.querySelector('[name="trans_port"]').value) || 9040, dns_port: parseInt(form.querySelector('[name="dns_port"]').value) || 9053, @@ -37,6 +38,7 @@ return view.extend({ settings.socks_port, settings.trans_port, settings.dns_port, + settings.lan_proxy, settings.exit_nodes, settings.exclude_exit_nodes, settings.strict_nodes, @@ -112,6 +114,20 @@ return view.extend({ ]), E('p', { 'style': 'font-size: 12px; color: var(--tor-text-muted); margin-top: 4px; margin-left: 24px;' }, _('Block all non-Tor traffic if the connection drops. Prevents IP leaks.')) + ]), + + // LAN Proxy + E('div', { 'style': 'margin-bottom: 20px;' }, [ + E('label', { 'style': 'display: flex; align-items: center; gap: 8px; cursor: pointer;' }, [ + E('input', { + 'type': 'checkbox', + 'name': 'lan_proxy', + 'checked': data.lan_proxy + }), + E('span', { 'style': 'font-weight: 600;' }, _('LAN Client Proxy')) + ]), + E('p', { 'style': 'font-size: 12px; color: var(--tor-text-muted); margin-top: 4px; margin-left: 24px;' }, + _('Route LAN client traffic through Tor via PREROUTING. Used by Server Mode to anonymize outbound traffic while preserving inbound connections.')) ]) ]) ]), diff --git a/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield b/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield index dcb85ba8..905e0f48 100755 --- a/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield +++ b/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield @@ -155,11 +155,12 @@ do_enable() { # Load preset configuration config_load "$CONFIG" - local preset_mode preset_dns preset_kill preset_bridges + local preset_mode preset_dns preset_kill preset_bridges preset_lan_proxy config_get preset_mode "$preset" mode 'transparent' config_get preset_dns "$preset" dns_over_tor '1' config_get preset_kill "$preset" kill_switch '1' config_get preset_bridges "$preset" use_bridges '0' + config_get preset_lan_proxy "$preset" lan_proxy '0' # Apply preset settings uci set tor-shield.main.enabled='1' @@ -167,6 +168,7 @@ do_enable() { uci set tor-shield.main.dns_over_tor="$preset_dns" uci set tor-shield.main.kill_switch="$preset_kill" uci set tor-shield.main.current_preset="$preset" + uci set tor-shield.trans.lan_proxy="$preset_lan_proxy" if [ "$preset_bridges" = "1" ]; then uci set tor-shield.bridges.enabled='1' @@ -599,6 +601,14 @@ get_presets() { json_add_string "description" "Use bridges to bypass network restrictions" json_close_object + # Server preset + json_add_object + json_add_string "id" "server" + json_add_string "name" "Server Mode" + json_add_string "icon" "server" + json_add_string "description" "Serve websites publicly while routing outbound through Tor" + json_close_object + json_close_array json_dump } @@ -684,12 +694,14 @@ get_settings() { json_add_string "socks_address" "$socks_addr" # Transparent proxy settings - local trans_port dns_port + local trans_port dns_port lan_proxy config_get trans_port trans port '9040' config_get dns_port trans dns_port '9053' + config_get lan_proxy trans lan_proxy '0' json_add_int "trans_port" "$trans_port" json_add_int "dns_port" "$dns_port" + json_add_boolean "lan_proxy" "$lan_proxy" # Security settings local exit_nodes exclude_exit strict_nodes @@ -841,7 +853,7 @@ save_settings() { json_load "$input" # 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 mode dns_over_tor kill_switch socks_port trans_port dns_port lan_proxy local exit_nodes exclude_exit strict_nodes apply_now json_get_var mode mode json_get_var dns_over_tor dns_over_tor @@ -849,6 +861,7 @@ save_settings() { json_get_var socks_port socks_port json_get_var trans_port trans_port json_get_var dns_port dns_port + json_get_var lan_proxy lan_proxy json_get_var exit_nodes exit_nodes json_get_var exclude_exit exclude_exit_nodes json_get_var strict_nodes strict_nodes @@ -886,6 +899,7 @@ save_settings() { [ -n "$socks_port" ] && uci set tor-shield.socks.port="$socks_port" [ -n "$trans_port" ] && uci set tor-shield.trans.port="$trans_port" [ -n "$dns_port" ] && uci set tor-shield.trans.dns_port="$dns_port" + [ -n "$lan_proxy" ] && uci set tor-shield.trans.lan_proxy="$lan_proxy" [ -n "$exit_nodes" ] && uci set tor-shield.security.exit_nodes="$exit_nodes" [ -n "$exclude_exit" ] && uci set tor-shield.security.exclude_exit_nodes="$exclude_exit" [ -n "$strict_nodes" ] && uci set tor-shield.security.strict_nodes="$strict_nodes" @@ -930,7 +944,7 @@ do_restart() { case "$1" in list) - echo '{"status":{},"enable":{"preset":"str"},"disable":{},"restart":{},"circuits":{},"new_identity":{},"check_leaks":{},"hidden_services":{},"add_hidden_service":{"name":"str","local_port":"int","virtual_port":"int"},"remove_hidden_service":{"name":"str"},"exit_ip":{},"refresh_ips":{},"bandwidth":{},"presets":{},"bridges":{},"set_bridges":{"enabled":"bool","type":"str"},"settings":{},"save_settings":{"mode":"str","dns_over_tor":"bool","kill_switch":"bool","socks_port":"int","trans_port":"int","dns_port":"int","exit_nodes":"str","exclude_exit_nodes":"str","strict_nodes":"bool"},"excluded_destinations":{},"add_excluded_destination":{"destination":"str"},"remove_excluded_destination":{"destination":"str"},"apply_exclusions":{}}' + echo '{"status":{},"enable":{"preset":"str"},"disable":{},"restart":{},"circuits":{},"new_identity":{},"check_leaks":{},"hidden_services":{},"add_hidden_service":{"name":"str","local_port":"int","virtual_port":"int"},"remove_hidden_service":{"name":"str"},"exit_ip":{},"refresh_ips":{},"bandwidth":{},"presets":{},"bridges":{},"set_bridges":{"enabled":"bool","type":"str"},"settings":{},"save_settings":{"mode":"str","dns_over_tor":"bool","kill_switch":"bool","socks_port":"int","trans_port":"int","dns_port":"int","lan_proxy":"bool","exit_nodes":"str","exclude_exit_nodes":"str","strict_nodes":"bool"},"excluded_destinations":{},"add_excluded_destination":{"destination":"str"},"remove_excluded_destination":{"destination":"str"},"apply_exclusions":{}}' ;; call) case "$2" in diff --git a/package/secubox/secubox-app-tor/files/etc/config/tor-shield b/package/secubox/secubox-app-tor/files/etc/config/tor-shield index 5ab2cc99..0b724b7a 100644 --- a/package/secubox/secubox-app-tor/files/etc/config/tor-shield +++ b/package/secubox/secubox-app-tor/files/etc/config/tor-shield @@ -29,6 +29,14 @@ config preset 'censored' option use_bridges '1' option dns_over_tor '1' +config preset 'server' + option name 'Server Mode' + option icon 'server' + option mode 'transparent' + option dns_over_tor '1' + option kill_switch '1' + option lan_proxy '1' + config proxy 'socks' option port '9050' option address '127.0.0.1' @@ -36,6 +44,7 @@ config proxy 'socks' config transparent 'trans' option port '9040' option dns_port '9053' + option lan_proxy '0' list excluded_ips '192.168.0.0/16' list excluded_ips '10.0.0.0/8' list excluded_ips '172.16.0.0/12' diff --git a/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield b/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield index 3b3d4320..23698b79 100755 --- a/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield +++ b/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield @@ -173,13 +173,16 @@ setup_iptables() { # Get Tor user ID local tor_uid=$(id -u tor 2>/dev/null || echo "tor") - # Remove from OUTPUT chain first (to allow chain deletion) + # Remove from chains first (to allow chain deletion) iptables -t nat -D OUTPUT -j TOR_SHIELD 2>/dev/null iptables -t filter -D OUTPUT -j TOR_SHIELD 2>/dev/null + iptables -t nat -D PREROUTING -i br-lan -j TOR_SHIELD_LAN 2>/dev/null # Clear existing Tor rules - iptables -t nat -F TOR_SHIELD 2>/dev/null - iptables -t nat -X TOR_SHIELD 2>/dev/null + for chain in TOR_SHIELD TOR_SHIELD_LAN; do + iptables -t nat -F $chain 2>/dev/null + iptables -t nat -X $chain 2>/dev/null + done iptables -t filter -F TOR_SHIELD 2>/dev/null iptables -t filter -X TOR_SHIELD 2>/dev/null @@ -212,9 +215,14 @@ setup_iptables() { iptables -t filter -A TOR_SHIELD -m owner --uid-owner $tor_uid -j ACCEPT iptables -t filter -A TOR_SHIELD -d 127.0.0.0/8 -j ACCEPT config_list_foreach trans excluded_ips add_excluded_filter_ip + # Allow response packets for inbound connections (HAProxy, etc) + iptables -t filter -A TOR_SHIELD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -t filter -A TOR_SHIELD -j REJECT iptables -t filter -A OUTPUT -j TOR_SHIELD fi + + # LAN client Tor routing via PREROUTING + setup_lan_proxy } add_excluded_ip() { @@ -225,14 +233,50 @@ add_excluded_filter_ip() { iptables -t filter -A TOR_SHIELD -d "$1" -j ACCEPT } +add_excluded_lan_ip() { + iptables -t nat -A TOR_SHIELD_LAN -d "$1" -j RETURN +} + +setup_lan_proxy() { + local lan_proxy + config_get lan_proxy trans lan_proxy '0' + [ "$lan_proxy" = "1" ] || return 0 + + local trans_port dns_port dns_over_tor + config_get trans_port trans port '9040' + config_get dns_port trans dns_port '9053' + config_get dns_over_tor main dns_over_tor '1' + + # Create PREROUTING chain + iptables -t nat -N TOR_SHIELD_LAN 2>/dev/null || true + + # Exclude local destinations + config_list_foreach trans excluded_ips add_excluded_lan_ip + + # Redirect DNS + if [ "$dns_over_tor" = "1" ]; then + iptables -t nat -A TOR_SHIELD_LAN -p udp --dport 53 -j REDIRECT --to-ports $dns_port + iptables -t nat -A TOR_SHIELD_LAN -p tcp --dport 53 -j REDIRECT --to-ports $dns_port + fi + + # Redirect TCP to Tor + iptables -t nat -A TOR_SHIELD_LAN -p tcp -j REDIRECT --to-ports $trans_port + + # Apply to LAN interface + iptables -t nat -A PREROUTING -i br-lan -j TOR_SHIELD_LAN +} + remove_iptables() { - # Remove from OUTPUT chain + # Remove from chains iptables -t nat -D OUTPUT -j TOR_SHIELD 2>/dev/null iptables -t filter -D OUTPUT -j TOR_SHIELD 2>/dev/null + iptables -t nat -D PREROUTING -i br-lan -j TOR_SHIELD_LAN 2>/dev/null - # Flush and remove chains - iptables -t nat -F TOR_SHIELD 2>/dev/null - iptables -t nat -X TOR_SHIELD 2>/dev/null + # Flush and remove all chains + for chain in TOR_SHIELD TOR_SHIELD_LAN; do + iptables -t nat -F $chain 2>/dev/null + iptables -t nat -X $chain 2>/dev/null + done iptables -t filter -F TOR_SHIELD 2>/dev/null iptables -t filter -X TOR_SHIELD 2>/dev/null } diff --git a/package/secubox/secubox-app-tor/files/usr/sbin/torctl b/package/secubox/secubox-app-tor/files/usr/sbin/torctl index 603d701c..a8abb584 100644 --- a/package/secubox/secubox-app-tor/files/usr/sbin/torctl +++ b/package/secubox/secubox-app-tor/files/usr/sbin/torctl @@ -14,7 +14,7 @@ Usage: torctl [options] Commands: status Show Tor Shield status - enable [preset] Enable Tor Shield (presets: anonymous, selective, censored) + enable [preset] Enable Tor Shield (presets: anonymous, selective, censored, server) disable Disable Tor Shield restart Restart Tor Shield identity Get new Tor identity (new circuits) @@ -29,6 +29,7 @@ Options: Examples: torctl enable anonymous Enable with full anonymity preset + torctl enable server Enable server mode (public IP + Tor outbound) torctl status Show current status torctl identity Request new circuits torctl exit-ip Show Tor exit IP @@ -150,17 +151,19 @@ cmd_enable() { # Load preset configuration config_load "$CONFIG" - local preset_mode preset_dns preset_kill preset_bridges + local preset_mode preset_dns preset_kill preset_bridges preset_lan_proxy config_get preset_mode "$preset" mode 'transparent' config_get preset_dns "$preset" dns_over_tor '1' config_get preset_kill "$preset" kill_switch '1' config_get preset_bridges "$preset" use_bridges '0' + config_get preset_lan_proxy "$preset" lan_proxy '0' # Apply preset settings uci set tor-shield.main.enabled='1' uci set tor-shield.main.mode="$preset_mode" uci set tor-shield.main.dns_over_tor="$preset_dns" uci set tor-shield.main.kill_switch="$preset_kill" + uci set tor-shield.trans.lan_proxy="$preset_lan_proxy" if [ "$preset_bridges" = "1" ]; then uci set tor-shield.bridges.enabled='1'