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 <noreply@anthropic.com>
This commit is contained in:
parent
ea18674638
commit
fa1f6ddbb8
@ -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,
|
||||
|
||||
@ -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', {
|
||||
|
||||
@ -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.'))
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ Usage: torctl <command> [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'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user