feat(tor): Add Tor Shield packages for OpenWrt

Add secubox-app-tor (backend) and luci-app-tor-shield (frontend) packages
for Tor anonymization on OpenWrt.

Backend features:
- UCI configuration with presets (anonymous, selective, censored)
- procd init script with iptables transparent proxy
- torctl CLI tool for status, enable/disable, circuits, leak-test
- DNS over Tor and kill switch support
- Hidden services and bridge management

Frontend features:
- Modern purple/onion themed dashboard
- One-click master toggle with visual status
- Real-time circuit visualization (Guard -> Middle -> Exit)
- Hidden services (.onion) management with copy/QR
- Bridge configuration (obfs4, snowflake, meek-azure)
- Leak detection tests
- Advanced settings for ports and exit node restrictions

Note: LuCI package renamed to luci-app-tor-shield to avoid conflict
with existing luci-app-tor package in OpenWrt LuCI feeds.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-23 17:15:39 +01:00
parent 4d08f99222
commit 23dac58741
15 changed files with 3975 additions and 0 deletions

View File

@ -0,0 +1,36 @@
# SPDX-License-Identifier: MIT
#
# Copyright (C) 2025 CyberMind.fr
#
# LuCI Tor Shield - Tor Anonymization Dashboard
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-tor-shield
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_ARCH:=all
PKG_LICENSE:=MIT
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
LUCI_TITLE:=LuCI Tor Shield
LUCI_DESCRIPTION:=Modern dashboard for Tor anonymization on OpenWrt
LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +rpcd +rpcd-mod-luci +secubox-app-tor
LUCI_PKGARCH:=all
# File permissions (CRITICAL: RPCD scripts MUST be executable 755)
# Format: path:owner:group:mode
# - RPCD scripts: 755 (executable by root, required for ubus calls)
PKG_FILE_MODES:=/usr/libexec/rpcd/luci.tor-shield:root:root:755
include $(TOPDIR)/feeds/luci/luci.mk
define Package/$(PKG_NAME)/conffiles
/etc/config/tor-shield
endef
# call BuildPackage - OpenWrt buildroot

View File

@ -0,0 +1,213 @@
'use strict';
'require baseclass';
'require rpc';
/**
* Tor Shield API
* Package: luci-app-tor
* RPCD object: luci.tor-shield
*/
var callStatus = rpc.declare({
object: 'luci.tor-shield',
method: 'status',
expect: { }
});
var callEnable = rpc.declare({
object: 'luci.tor-shield',
method: 'enable',
params: ['preset'],
expect: { success: false }
});
var callDisable = rpc.declare({
object: 'luci.tor-shield',
method: 'disable',
expect: { success: false }
});
var callCircuits = rpc.declare({
object: 'luci.tor-shield',
method: 'circuits',
expect: { circuits: [] }
});
var callNewIdentity = rpc.declare({
object: 'luci.tor-shield',
method: 'new_identity',
expect: { success: false }
});
var callCheckLeaks = rpc.declare({
object: 'luci.tor-shield',
method: 'check_leaks',
expect: { }
});
var callHiddenServices = rpc.declare({
object: 'luci.tor-shield',
method: 'hidden_services',
expect: { services: [] }
});
var callAddHiddenService = rpc.declare({
object: 'luci.tor-shield',
method: 'add_hidden_service',
params: ['name', 'local_port', 'virtual_port'],
expect: { success: false }
});
var callRemoveHiddenService = rpc.declare({
object: 'luci.tor-shield',
method: 'remove_hidden_service',
params: ['name'],
expect: { success: false }
});
var callExitIp = rpc.declare({
object: 'luci.tor-shield',
method: 'exit_ip',
expect: { }
});
var callBandwidth = rpc.declare({
object: 'luci.tor-shield',
method: 'bandwidth',
expect: { }
});
var callPresets = rpc.declare({
object: 'luci.tor-shield',
method: 'presets',
expect: { presets: [] }
});
var callBridges = rpc.declare({
object: 'luci.tor-shield',
method: 'bridges',
expect: { }
});
var callSetBridges = rpc.declare({
object: 'luci.tor-shield',
method: 'set_bridges',
params: ['enabled', 'type'],
expect: { success: false }
});
var callSettings = rpc.declare({
object: 'luci.tor-shield',
method: 'settings',
expect: { }
});
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'],
expect: { success: false }
});
// Utility functions
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatRate(bytesPerSec) {
if (!bytesPerSec || bytesPerSec === 0) return '0 B/s';
var k = 1024;
var sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
var i = Math.floor(Math.log(bytesPerSec) / Math.log(k));
if (i >= sizes.length) i = sizes.length - 1;
return parseFloat((bytesPerSec / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function formatUptime(seconds) {
if (!seconds || seconds <= 0) return '0s';
var d = Math.floor(seconds / 86400);
var h = Math.floor((seconds % 86400) / 3600);
var m = Math.floor((seconds % 3600) / 60);
var s = seconds % 60;
if (d > 0) return d + 'd ' + h + 'h';
if (h > 0) return h + 'h ' + m + 'm';
if (m > 0) return m + 'm ' + s + 's';
return s + 's';
}
function getCountryFlag(code) {
if (!code || code.length !== 2) return '';
var offset = 127397;
var first = code.charCodeAt(0);
var second = code.charCodeAt(1);
return String.fromCodePoint(first + offset) + String.fromCodePoint(second + offset);
}
function getPresetIcon(icon) {
switch (icon) {
case 'shield': return '&#128737;';
case 'target': return '&#127919;';
case 'unlock': return '&#128275;';
default: return '&#128737;';
}
}
return baseclass.extend({
getStatus: callStatus,
enable: callEnable,
disable: callDisable,
getCircuits: callCircuits,
newIdentity: callNewIdentity,
checkLeaks: callCheckLeaks,
getHiddenServices: callHiddenServices,
addHiddenService: callAddHiddenService,
removeHiddenService: callRemoveHiddenService,
getExitIp: callExitIp,
getBandwidth: callBandwidth,
getPresets: callPresets,
getBridges: callBridges,
setBridges: callSetBridges,
getSettings: callSettings,
saveSettings: callSaveSettings,
formatBytes: formatBytes,
formatRate: formatRate,
formatUptime: formatUptime,
getCountryFlag: getCountryFlag,
getPresetIcon: getPresetIcon,
// Aggregate function for dashboard
getDashboardData: function() {
return Promise.all([
callStatus(),
callPresets(),
callBandwidth()
]).then(function(results) {
return {
status: results[0] || {},
presets: (results[1] || {}).presets || [],
bandwidth: results[2] || {}
};
});
},
// Get all data for monitoring
getMonitoringData: function() {
return Promise.all([
callStatus(),
callCircuits(),
callBandwidth()
]).then(function(results) {
return {
status: results[0] || {},
circuits: (results[1] || {}).circuits || [],
bandwidth: results[2] || {}
};
});
}
});

View File

@ -0,0 +1,625 @@
/* Tor Shield Dashboard - Purple/Onion Theme */
/* Copyright (C) 2025 CyberMind.fr */
:root {
--tor-bg-primary: #0a0015;
--tor-bg-secondary: #1a0a2e;
--tor-bg-card: #1e1233;
--tor-bg-card-hover: #2a1845;
--tor-accent-purple: #7d4e9f;
--tor-accent-light: #9f7aba;
--tor-accent-dark: #4a2c6a;
--tor-status-protected: #10b981;
--tor-status-exposed: #ef4444;
--tor-status-warning: #f59e0b;
--tor-status-disabled: #6b7280;
--tor-text-primary: #f3f4f6;
--tor-text-secondary: #9ca3af;
--tor-text-muted: #6b7280;
--tor-border-color: rgba(125, 78, 159, 0.3);
--tor-onion-gradient: linear-gradient(135deg, #7d4e9f, #4a2c6a);
--tor-success-gradient: linear-gradient(135deg, #10b981, #059669);
--tor-danger-gradient: linear-gradient(135deg, #ef4444, #dc2626);
}
.tor-dashboard {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(180deg, var(--tor-bg-primary) 0%, var(--tor-bg-secondary) 100%);
min-height: 100vh;
padding: 20px;
color: var(--tor-text-primary);
}
/* Header */
.tor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 20px 24px;
background: var(--tor-bg-card);
border-radius: 16px;
border: 1px solid var(--tor-border-color);
}
.tor-logo {
display: flex;
align-items: center;
gap: 12px;
}
.tor-logo-icon {
font-size: 40px;
filter: drop-shadow(0 0 20px rgba(125, 78, 159, 0.5));
}
.tor-logo-text {
font-size: 28px;
font-weight: 700;
color: var(--tor-text-primary);
}
.tor-logo-text span {
color: var(--tor-accent-purple);
}
.tor-status-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 20px;
font-weight: 500;
font-size: 14px;
}
.tor-status-badge.protected {
background: rgba(16, 185, 129, 0.15);
color: var(--tor-status-protected);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.tor-status-badge.exposed {
background: rgba(239, 68, 68, 0.15);
color: var(--tor-status-exposed);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.tor-status-badge.disabled {
background: rgba(107, 114, 128, 0.15);
color: var(--tor-text-secondary);
border: 1px solid rgba(107, 114, 128, 0.3);
}
.tor-status-badge.connecting {
background: rgba(245, 158, 11, 0.15);
color: var(--tor-status-warning);
border: 1px solid rgba(245, 158, 11, 0.3);
}
.tor-status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: currentColor;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Hero Section - Master Toggle */
.tor-hero {
display: flex;
gap: 32px;
margin-bottom: 24px;
padding: 32px;
background: var(--tor-bg-card);
border-radius: 16px;
border: 1px solid var(--tor-border-color);
}
.tor-toggle-section {
flex: 0 0 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.tor-master-toggle {
width: 140px;
height: 140px;
border-radius: 50%;
border: 4px solid var(--tor-border-color);
background: var(--tor-bg-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.tor-master-toggle:hover {
transform: scale(1.05);
border-color: var(--tor-accent-purple);
box-shadow: 0 0 40px rgba(125, 78, 159, 0.4);
}
.tor-master-toggle.active {
background: var(--tor-onion-gradient);
border-color: var(--tor-accent-light);
box-shadow: 0 0 50px rgba(125, 78, 159, 0.6);
}
.tor-master-toggle.active::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%) rotate(45deg); }
100% { transform: translateX(100%) rotate(45deg); }
}
.tor-toggle-label {
margin-top: 12px;
font-size: 16px;
font-weight: 600;
color: var(--tor-text-secondary);
}
.tor-toggle-label.active {
color: var(--tor-accent-light);
}
/* Protection Info */
.tor-protection-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.tor-protection-title {
font-size: 14px;
font-weight: 500;
color: var(--tor-text-muted);
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.tor-ip-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.tor-ip-item {
padding: 16px;
background: var(--tor-bg-secondary);
border-radius: 12px;
border: 1px solid var(--tor-border-color);
}
.tor-ip-label {
font-size: 12px;
color: var(--tor-text-muted);
margin-bottom: 4px;
text-transform: uppercase;
}
.tor-ip-value {
font-size: 18px;
font-weight: 600;
font-family: 'Monaco', 'Menlo', monospace;
}
.tor-ip-value.protected {
color: var(--tor-status-protected);
}
.tor-ip-value.exposed {
color: var(--tor-status-exposed);
}
.tor-exit-location {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
font-size: 14px;
color: var(--tor-text-secondary);
}
/* Presets */
.tor-presets {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.tor-preset {
padding: 20px;
background: var(--tor-bg-card);
border-radius: 12px;
border: 2px solid var(--tor-border-color);
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.tor-preset:hover {
border-color: var(--tor-accent-purple);
transform: translateY(-2px);
}
.tor-preset.active {
border-color: var(--tor-accent-light);
background: linear-gradient(180deg, var(--tor-bg-card-hover), var(--tor-bg-card));
}
.tor-preset-icon {
font-size: 32px;
margin-bottom: 8px;
}
.tor-preset-name {
font-size: 16px;
font-weight: 600;
color: var(--tor-text-primary);
margin-bottom: 4px;
}
.tor-preset-desc {
font-size: 12px;
color: var(--tor-text-muted);
}
/* Quick Stats */
.tor-quick-stats {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.tor-quick-stat {
padding: 16px 20px;
background: var(--tor-bg-card);
border-radius: 12px;
border: 1px solid var(--tor-border-color);
text-align: center;
}
.tor-quick-stat-icon {
font-size: 24px;
margin-bottom: 8px;
}
.tor-quick-stat-value {
font-size: 24px;
font-weight: 700;
color: var(--tor-accent-light);
margin-bottom: 4px;
}
.tor-quick-stat-label {
font-size: 12px;
color: var(--tor-text-muted);
text-transform: uppercase;
}
/* Cards */
.tor-card {
background: var(--tor-bg-card);
border-radius: 16px;
border: 1px solid var(--tor-border-color);
margin-bottom: 24px;
overflow: hidden;
}
.tor-card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--tor-border-color);
}
.tor-card-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 16px;
font-weight: 600;
}
.tor-card-title-icon {
font-size: 20px;
}
.tor-card-body {
padding: 20px;
}
.tor-card-badge {
padding: 4px 12px;
background: var(--tor-accent-dark);
border-radius: 12px;
font-size: 12px;
color: var(--tor-accent-light);
}
/* Circuit Visualization */
.tor-circuit {
display: flex;
align-items: center;
padding: 16px;
background: var(--tor-bg-secondary);
border-radius: 12px;
margin-bottom: 12px;
gap: 8px;
}
.tor-circuit-node {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 16px;
background: var(--tor-bg-card);
border-radius: 8px;
border: 1px solid var(--tor-border-color);
min-width: 80px;
}
.tor-circuit-node.you {
background: var(--tor-onion-gradient);
border-color: var(--tor-accent-light);
}
.tor-circuit-node.guard {
background: rgba(16, 185, 129, 0.1);
border-color: rgba(16, 185, 129, 0.3);
}
.tor-circuit-node.middle {
background: rgba(245, 158, 11, 0.1);
border-color: rgba(245, 158, 11, 0.3);
}
.tor-circuit-node.exit {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
}
.tor-circuit-node-flag {
font-size: 24px;
margin-bottom: 4px;
}
.tor-circuit-node-label {
font-size: 10px;
color: var(--tor-text-muted);
text-transform: uppercase;
}
.tor-circuit-arrow {
color: var(--tor-accent-purple);
font-size: 20px;
}
/* Hidden Services */
.tor-hidden-service {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: var(--tor-bg-secondary);
border-radius: 12px;
margin-bottom: 12px;
border: 1px solid var(--tor-border-color);
}
.tor-hidden-service-info {
flex: 1;
}
.tor-hidden-service-name {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.tor-hidden-service-address {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
color: var(--tor-accent-light);
word-break: break-all;
}
.tor-hidden-service-port {
font-size: 12px;
color: var(--tor-text-muted);
margin-top: 4px;
}
.tor-hidden-service-actions {
display: flex;
gap: 8px;
}
/* Buttons */
.tor-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
background: var(--tor-accent-dark);
color: var(--tor-text-primary);
}
.tor-btn:hover {
background: var(--tor-accent-purple);
}
.tor-btn-primary {
background: var(--tor-onion-gradient);
}
.tor-btn-primary:hover {
box-shadow: 0 0 20px rgba(125, 78, 159, 0.5);
}
.tor-btn-success {
background: var(--tor-success-gradient);
}
.tor-btn-danger {
background: var(--tor-danger-gradient);
}
.tor-btn-sm {
padding: 6px 12px;
font-size: 12px;
}
.tor-btn-icon {
padding: 8px;
border-radius: 6px;
}
/* Progress Bar */
.tor-progress {
height: 8px;
background: var(--tor-bg-secondary);
border-radius: 4px;
overflow: hidden;
}
.tor-progress-bar {
height: 100%;
background: var(--tor-onion-gradient);
border-radius: 4px;
transition: width 0.5s ease;
}
/* Bootstrap Progress */
.tor-bootstrap {
margin-top: 16px;
}
.tor-bootstrap-label {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 12px;
color: var(--tor-text-secondary);
}
/* Refresh Control */
.tor-refresh-control {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-bottom: 16px;
font-size: 13px;
color: var(--tor-text-muted);
}
.tor-refresh-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--tor-status-disabled);
}
.tor-refresh-indicator.active {
background: var(--tor-status-protected);
animation: pulse 2s infinite;
}
/* Empty State */
.tor-empty {
text-align: center;
padding: 40px;
color: var(--tor-text-muted);
}
.tor-empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.tor-empty-text {
font-size: 16px;
margin-bottom: 8px;
}
/* Animations */
.tor-fade-in {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.tor-value-updated {
animation: highlight 0.5s ease;
}
@keyframes highlight {
0%, 100% { background: transparent; }
50% { background: rgba(125, 78, 159, 0.3); }
}
/* Responsive */
@media (max-width: 1200px) {
.tor-quick-stats {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.tor-hero {
flex-direction: column;
align-items: center;
text-align: center;
}
.tor-presets {
grid-template-columns: 1fr;
}
.tor-quick-stats {
grid-template-columns: repeat(2, 1fr);
}
.tor-ip-info {
grid-template-columns: 1fr;
}
}

View File

@ -0,0 +1,210 @@
'use strict';
'require view';
'require ui';
'require tor-shield/api as api';
return view.extend({
title: _('Bridge Configuration'),
load: function() {
return api.getBridges();
},
handleToggleBridges: function(currentState) {
var self = this;
var newState = currentState ? '0' : '1';
ui.showModal(_('Updating Bridge Configuration'), [
E('p', { 'class': 'spinning' }, newState === '1' ? _('Enabling bridges...') : _('Disabling bridges...'))
]);
api.setBridges(newState, null).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Bridge configuration updated. Restart Tor Shield to apply changes.')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to update configuration')), 'error');
}
});
},
handleBridgeTypeChange: function(newType) {
var self = this;
ui.showModal(_('Updating Bridge Type'), [
E('p', { 'class': 'spinning' }, _('Changing bridge type to %s...').format(newType))
]);
api.setBridges(null, newType).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Bridge type updated to %s. Restart Tor Shield to apply changes.').format(newType)), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to update bridge type')), 'error');
}
});
},
render: function(data) {
var self = this;
var bridgesEnabled = data.enabled;
var bridgeType = data.type || 'obfs4';
var bridgeLines = data.bridge_lines || [];
var bridgeTypes = [
{ id: 'obfs4', name: 'obfs4', desc: _('Recommended - Most effective against censorship') },
{ id: 'snowflake', name: 'Snowflake', desc: _('Uses WebRTC - Good for restrictive networks') },
{ id: 'meek-azure', name: 'meek-azure', desc: _('Domain fronting via Microsoft Azure') },
{ id: 'vanilla', name: 'Vanilla', desc: _('Standard bridges - Less effective against DPI') }
];
var view = E('div', { 'class': 'tor-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('tor-shield/dashboard.css') }),
// Header
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\uD83C\uDF09'),
_('Bridge Configuration')
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 8px;' }, [
E('span', { 'style': 'font-size: 14px; color: var(--tor-text-secondary);' }, _('Bridges')),
E('button', {
'class': 'tor-btn tor-btn-sm ' + (bridgesEnabled ? 'tor-btn-success' : ''),
'click': L.bind(function() { this.handleToggleBridges(bridgesEnabled); }, self)
}, bridgesEnabled ? _('Enabled') : _('Disabled'))
])
]),
E('div', { 'class': 'tor-card-body' }, [
E('p', { 'style': 'color: var(--tor-text-secondary); margin-bottom: 20px;' },
_('Bridges help you connect to Tor in countries where Tor is blocked. They disguise your Tor traffic to look like normal internet traffic.')),
// When to use bridges
E('div', { 'style': 'background: rgba(125,78,159,0.1); border: 1px solid rgba(125,78,159,0.3); border-radius: 8px; padding: 16px; margin-bottom: 20px;' }, [
E('h4', { 'style': 'margin: 0 0 8px 0; color: var(--tor-accent-light);' }, _('When to use bridges?')),
E('ul', { 'style': 'margin: 0; padding-left: 20px; color: var(--tor-text-secondary);' }, [
E('li', {}, _('Your country blocks access to the Tor network')),
E('li', {}, _('Your ISP blocks or throttles Tor connections')),
E('li', {}, _('You want extra privacy by hiding that you use Tor')),
E('li', {}, _('Normal Tor connection attempts fail repeatedly'))
])
])
])
]),
// Bridge Type Selection
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\u2699'),
_('Bridge Type')
]),
E('div', { 'class': 'tor-card-badge' }, bridgeType)
]),
E('div', { 'class': 'tor-card-body' }, [
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px;' },
bridgeTypes.map(function(bt) {
return E('div', {
'style': 'padding: 16px; background: ' + (bridgeType === bt.id ? 'rgba(125,78,159,0.2)' : 'var(--tor-bg-secondary)') +
'; border: 2px solid ' + (bridgeType === bt.id ? 'var(--tor-accent-purple)' : 'var(--tor-border-color)') +
'; border-radius: 8px; cursor: pointer; transition: all 0.2s;',
'click': L.bind(function() { this.handleBridgeTypeChange(bt.id); }, self)
}, [
E('div', { 'style': 'display: flex; align-items: center; gap: 8px; margin-bottom: 4px;' }, [
E('span', { 'style': 'font-weight: 600;' }, bt.name),
bridgeType === bt.id ? E('span', { 'style': 'color: var(--tor-accent-light);' }, '\u2713') : ''
]),
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, bt.desc)
]);
})
)
])
]),
// Get Bridges
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\uD83D\uDD17'),
_('Get Bridges')
])
]),
E('div', { 'class': 'tor-card-body' }, [
E('p', { 'style': 'color: var(--tor-text-secondary); margin-bottom: 16px;' },
_('You can obtain bridges from the Tor Project:')),
E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' }, [
E('a', {
'href': 'https://bridges.torproject.org/',
'target': '_blank',
'class': 'tor-btn tor-btn-primary'
}, ['\uD83C\uDF10 ', _('Get Bridges Online')]),
E('span', { 'style': 'color: var(--tor-text-muted); display: flex; align-items: center;' }, _('or')),
E('div', { 'style': 'color: var(--tor-text-secondary);' }, [
_('Email: '),
E('code', {}, 'bridges@torproject.org'),
E('br', {}),
E('small', { 'style': 'color: var(--tor-text-muted);' }, _('(from Gmail or Riseup only)'))
])
]),
// Current bridge lines
bridgeLines.length > 0 ? E('div', { 'style': 'margin-top: 20px;' }, [
E('h4', { 'style': 'margin-bottom: 8px;' }, _('Configured Bridges')),
E('div', { 'style': 'background: var(--tor-bg-secondary); border-radius: 8px; padding: 12px; font-family: monospace; font-size: 12px; max-height: 150px; overflow-y: auto;' },
bridgeLines.map(function(line) {
return E('div', { 'style': 'margin-bottom: 4px; word-break: break-all;' }, line);
})
)
]) : ''
])
]),
// Help
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\u2139'),
_('Bridge Types Explained')
])
]),
E('div', { 'class': 'tor-card-body' }, [
E('dl', { 'style': 'margin: 0;' }, [
E('dt', { 'style': 'font-weight: 600; color: var(--tor-accent-light); margin-top: 8px;' }, 'obfs4'),
E('dd', { 'style': 'margin: 4px 0 12px 16px; color: var(--tor-text-secondary);' },
_('The most commonly used pluggable transport. Transforms Tor traffic to look random, making it hard to detect.')),
E('dt', { 'style': 'font-weight: 600; color: var(--tor-accent-light);' }, 'Snowflake'),
E('dd', { 'style': 'margin: 4px 0 12px 16px; color: var(--tor-text-secondary);' },
_('Uses WebRTC peer connections through volunteer browsers. Traffic looks like video calls.')),
E('dt', { 'style': 'font-weight: 600; color: var(--tor-accent-light);' }, 'meek-azure'),
E('dd', { 'style': 'margin: 4px 0 12px 16px; color: var(--tor-text-secondary);' },
_('Domain fronting through Microsoft Azure. Traffic appears as HTTPS to azure.com. Slower but harder to block.')),
E('dt', { 'style': 'font-weight: 600; color: var(--tor-accent-light);' }, 'Vanilla'),
E('dd', { 'style': 'margin: 4px 0 0 16px; color: var(--tor-text-secondary);' },
_('Standard bridge relays without obfuscation. Only useful when Tor IPs are blocked but protocol isn\'t inspected.'))
])
])
]),
// Back link
E('div', { 'style': 'margin-top: 16px;' }, [
E('a', {
'href': L.url('admin', 'services', 'tor-shield'),
'class': 'tor-btn'
}, ['\u2190 ', _('Back to Dashboard')])
])
]);
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,212 @@
'use strict';
'require view';
'require poll';
'require ui';
'require tor-shield/api as api';
return view.extend({
title: _('Tor Circuits'),
pollInterval: 10,
pollActive: true,
load: function() {
return api.getMonitoringData();
},
handleNewIdentity: function() {
var self = this;
ui.showModal(_('New Identity'), [
E('p', { 'class': 'spinning' }, _('Requesting new Tor identity...'))
]);
api.newIdentity().then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('New identity requested. Circuits will be renewed.')), 'info');
// Refresh circuits view
setTimeout(function() {
self.load().then(function(data) {
self.updateCircuits(data.circuits || []);
});
}, 3000);
} else {
ui.addNotification(null, E('p', result.error || _('Failed to request new identity')), 'error');
}
});
},
renderCircuitNode: function(type, label, flag) {
var nodeClass = 'tor-circuit-node';
switch (type) {
case 'you': nodeClass += ' you'; break;
case 'guard': nodeClass += ' guard'; break;
case 'middle': nodeClass += ' middle'; break;
case 'exit': nodeClass += ' exit'; break;
}
return E('div', { 'class': nodeClass }, [
E('div', { 'class': 'tor-circuit-node-flag' }, flag || '\uD83D\uDCBB'),
E('div', { 'class': 'tor-circuit-node-label' }, label)
]);
},
renderCircuit: function(circuit) {
var self = this;
var nodes = circuit.nodes || [];
var elements = [
this.renderCircuitNode('you', _('YOU'), '\uD83D\uDCBB'),
E('span', { 'class': 'tor-circuit-arrow' }, '\u2192')
];
nodes.forEach(function(node, idx) {
var type = 'middle';
if (idx === 0) type = 'guard';
else if (idx === nodes.length - 1) type = 'exit';
var label = node.name || node.fingerprint.substring(0, 8);
elements.push(self.renderCircuitNode(type, label, api.getCountryFlag(node.country) || '\uD83C\uDF10'));
if (idx < nodes.length - 1) {
elements.push(E('span', { 'class': 'tor-circuit-arrow' }, '\u2192'));
}
});
elements.push(E('span', { 'class': 'tor-circuit-arrow' }, '\u2192'));
elements.push(this.renderCircuitNode('web', _('WEB'), '\uD83C\uDF10'));
return E('div', { 'class': 'tor-circuit', 'data-circuit': circuit.id }, [
E('div', { 'style': 'display: flex; align-items: center; gap: 8px; flex-wrap: wrap;' }, elements),
E('div', { 'style': 'margin-left: auto; display: flex; align-items: center; gap: 8px;' }, [
E('span', { 'class': 'tor-card-badge' }, circuit.purpose || 'GENERAL'),
E('span', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, '#' + circuit.id)
])
]);
},
updateCircuits: function(circuits) {
var self = this;
var container = document.querySelector('.tor-circuits-container');
if (!container) return;
if (circuits.length === 0) {
container.innerHTML = '';
container.appendChild(E('div', { 'class': 'tor-empty' }, [
E('div', { 'class': 'tor-empty-icon' }, '\uD83D\uDD04'),
E('div', { 'class': 'tor-empty-text' }, _('No active circuits')),
E('p', {}, _('Tor is not running or not connected'))
]));
return;
}
container.innerHTML = '';
circuits.forEach(function(circuit) {
container.appendChild(self.renderCircuit(circuit));
});
// Update circuit count
var countEl = document.querySelector('.tor-circuit-count');
if (countEl) {
countEl.textContent = circuits.length + ' ' + (circuits.length === 1 ? _('circuit') : _('circuits'));
}
},
startPolling: function() {
var self = this;
this.pollActive = true;
poll.add(L.bind(function() {
if (!this.pollActive) return Promise.resolve();
return api.getMonitoringData().then(L.bind(function(data) {
this.updateCircuits(data.circuits || []);
}, this));
}, this), this.pollInterval);
},
stopPolling: function() {
this.pollActive = false;
poll.stop();
},
render: function(data) {
var self = this;
var status = data.status || {};
var circuits = data.circuits || [];
var view = E('div', { 'class': 'tor-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('tor-shield/dashboard.css') }),
// Header
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\uD83D\uDDFA'),
_('Tor Circuits')
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 12px;' }, [
E('span', { 'class': 'tor-card-badge tor-circuit-count' },
circuits.length + ' ' + (circuits.length === 1 ? _('circuit') : _('circuits'))),
E('button', {
'class': 'tor-btn tor-btn-sm tor-btn-primary',
'click': L.bind(this.handleNewIdentity, this),
'disabled': !status.running
}, ['\uD83D\uDD04 ', _('New Identity')])
])
]),
E('div', { 'class': 'tor-card-body' }, [
E('p', { 'style': 'color: var(--tor-text-secondary); margin-bottom: 16px;' },
_('Each circuit routes your traffic through three relays: Guard (entry), Middle, and Exit.')),
// Circuit legend
E('div', { 'style': 'display: flex; gap: 16px; margin-bottom: 20px; flex-wrap: wrap;' }, [
E('span', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [
E('span', { 'style': 'width: 12px; height: 12px; border-radius: 3px; background: var(--tor-onion-gradient);' }),
E('span', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('You'))
]),
E('span', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [
E('span', { 'style': 'width: 12px; height: 12px; border-radius: 3px; background: rgba(16,185,129,0.3);' }),
E('span', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('Guard'))
]),
E('span', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [
E('span', { 'style': 'width: 12px; height: 12px; border-radius: 3px; background: rgba(245,158,11,0.3);' }),
E('span', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('Middle'))
]),
E('span', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [
E('span', { 'style': 'width: 12px; height: 12px; border-radius: 3px; background: rgba(239,68,68,0.3);' }),
E('span', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('Exit'))
])
]),
// Circuits container
E('div', { 'class': 'tor-circuits-container' },
circuits.length > 0 ?
circuits.map(function(c) { return self.renderCircuit(c); }) :
E('div', { 'class': 'tor-empty' }, [
E('div', { 'class': 'tor-empty-icon' }, '\uD83D\uDD04'),
E('div', { 'class': 'tor-empty-text' }, _('No active circuits')),
E('p', {}, _('Tor is not running or not connected'))
])
)
])
]),
// Back link
E('div', { 'style': 'margin-top: 16px;' }, [
E('a', {
'href': L.url('admin', 'services', 'tor-shield'),
'class': 'tor-btn'
}, ['\u2190 ', _('Back to Dashboard')])
])
]);
this.startPolling();
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,246 @@
'use strict';
'require view';
'require ui';
'require tor-shield/api as api';
return view.extend({
title: _('Hidden Services'),
load: function() {
return api.getHiddenServices();
},
handleAddService: function() {
var self = this;
var nameInput, localPortInput, virtualPortInput;
ui.showModal(_('Add Hidden Service'), [
E('div', { 'style': 'margin-bottom: 16px;' }, [
E('label', { 'style': 'display: block; margin-bottom: 4px; font-weight: 500;' }, _('Service Name')),
nameInput = E('input', {
'type': 'text',
'class': 'cbi-input-text',
'placeholder': 'my-website',
'style': 'width: 100%;'
})
]),
E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;' }, [
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 4px; font-weight: 500;' }, _('Local Port')),
localPortInput = E('input', {
'type': 'number',
'class': 'cbi-input-text',
'value': '80',
'min': '1',
'max': '65535',
'style': 'width: 100%;'
}),
E('small', { 'style': 'color: var(--tor-text-muted);' }, _('Port on your router'))
]),
E('div', {}, [
E('label', { 'style': 'display: block; margin-bottom: 4px; font-weight: 500;' }, _('Virtual Port')),
virtualPortInput = E('input', {
'type': 'number',
'class': 'cbi-input-text',
'value': '80',
'min': '1',
'max': '65535',
'style': 'width: 100%;'
}),
E('small', { 'style': 'color: var(--tor-text-muted);' }, _('Port exposed on .onion'))
])
]),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'btn cbi-button-action',
'click': function() {
var name = nameInput.value.trim();
var localPort = parseInt(localPortInput.value) || 80;
var virtualPort = parseInt(virtualPortInput.value) || 80;
if (!name) {
ui.addNotification(null, E('p', _('Service name is required')), 'error');
return;
}
ui.showModal(_('Creating Service'), [
E('p', { 'class': 'spinning' }, _('Creating hidden service...'))
]);
api.addHiddenService(name, localPort, virtualPort).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Hidden service created. Restart Tor Shield to generate .onion address.')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to create service')), 'error');
}
});
}
}, _('Create'))
])
]);
},
handleRemoveService: function(name) {
var self = this;
ui.showModal(_('Remove Hidden Service'), [
E('p', {}, _('Are you sure you want to remove the hidden service "%s"?').format(name)),
E('p', { 'style': 'color: var(--tor-status-warning);' },
_('Warning: This will permanently delete the .onion address. You cannot recover it.')),
E('div', { 'class': 'right', 'style': 'margin-top: 16px;' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'btn cbi-button-negative',
'click': function() {
ui.showModal(_('Removing Service'), [
E('p', { 'class': 'spinning' }, _('Removing hidden service...'))
]);
api.removeHiddenService(name).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Hidden service removed')), 'info');
window.location.reload();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to remove service')), 'error');
}
});
}
}, _('Remove'))
])
]);
},
handleCopyAddress: function(address) {
if (navigator.clipboard) {
navigator.clipboard.writeText(address).then(function() {
ui.addNotification(null, E('p', _('Address copied to clipboard')), 'info');
});
} else {
// Fallback for older browsers
var textArea = document.createElement('textarea');
textArea.value = address;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
ui.addNotification(null, E('p', _('Address copied to clipboard')), 'info');
}
},
renderService: function(service) {
var self = this;
var hasAddress = service.onion_address && service.onion_address.length > 0;
return E('div', { 'class': 'tor-hidden-service' }, [
E('div', { 'class': 'tor-hidden-service-info' }, [
E('div', { 'class': 'tor-hidden-service-name' }, [
E('span', { 'style': 'margin-right: 8px;' }, '\uD83E\uDDC5'),
service.name,
service.enabled ?
E('span', { 'style': 'margin-left: 8px; padding: 2px 8px; background: rgba(16,185,129,0.2); color: #10b981; border-radius: 4px; font-size: 10px;' }, _('ACTIVE')) :
E('span', { 'style': 'margin-left: 8px; padding: 2px 8px; background: rgba(107,114,128,0.2); color: #9ca3af; border-radius: 4px; font-size: 10px;' }, _('DISABLED'))
]),
hasAddress ?
E('div', { 'class': 'tor-hidden-service-address' }, service.onion_address) :
E('div', { 'class': 'tor-hidden-service-address', 'style': 'color: var(--tor-text-muted);' }, _('Address will be generated after restart')),
E('div', { 'class': 'tor-hidden-service-port' },
_('Port %d -> 127.0.0.1:%d').format(service.virtual_port, service.local_port))
]),
E('div', { 'class': 'tor-hidden-service-actions' }, [
hasAddress ? E('button', {
'class': 'tor-btn tor-btn-sm',
'click': L.bind(function() { this.handleCopyAddress(service.onion_address); }, self),
'title': _('Copy address')
}, '\uD83D\uDCCB') : '',
E('button', {
'class': 'tor-btn tor-btn-sm tor-btn-danger',
'click': L.bind(function() { this.handleRemoveService(service.name); }, self),
'title': _('Remove service')
}, '\uD83D\uDDD1')
])
]);
},
render: function(data) {
var self = this;
var services = data.services || [];
var view = E('div', { 'class': 'tor-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('tor-shield/dashboard.css') }),
// Header
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\uD83E\uDDC5'),
_('Hidden Services')
]),
E('button', {
'class': 'tor-btn tor-btn-primary tor-btn-sm',
'click': L.bind(this.handleAddService, this)
}, ['+ ', _('Add Service')])
]),
E('div', { 'class': 'tor-card-body' }, [
E('p', { 'style': 'color: var(--tor-text-secondary); margin-bottom: 16px;' },
_('Hidden services allow you to host websites and services accessible only through the Tor network via .onion addresses.')),
services.length > 0 ?
E('div', { 'class': 'tor-services-list' },
services.map(function(s) { return self.renderService(s); })
) :
E('div', { 'class': 'tor-empty' }, [
E('div', { 'class': 'tor-empty-icon' }, '\uD83E\uDDC5'),
E('div', { 'class': 'tor-empty-text' }, _('No hidden services configured')),
E('p', {}, _('Click "Add Service" to create your first .onion service'))
])
])
]),
// Help Card
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\u2139'),
_('How Hidden Services Work')
])
]),
E('div', { 'class': 'tor-card-body' }, [
E('ul', { 'style': 'margin: 0; padding-left: 20px; color: var(--tor-text-secondary);' }, [
E('li', {}, _('Hidden services are accessible only through Tor Browser or Tor-enabled applications')),
E('li', {}, _('The .onion address is a public key that identifies your service')),
E('li', {}, _('Your real IP address remains hidden from visitors')),
E('li', {}, _('After creating a service, restart Tor Shield to generate the .onion address')),
E('li', {}, _('Make sure the local port has a service running (e.g., web server on port 80)'))
])
])
]),
// Back link
E('div', { 'style': 'margin-top: 16px;' }, [
E('a', {
'href': L.url('admin', 'services', 'tor-shield'),
'class': 'tor-btn'
}, ['\u2190 ', _('Back to Dashboard')])
])
]);
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,453 @@
'use strict';
'require view';
'require poll';
'require dom';
'require ui';
'require tor-shield/api as api';
return view.extend({
title: _('Tor Shield'),
pollInterval: 5,
pollActive: true,
currentPreset: 'anonymous',
load: function() {
return api.getDashboardData();
},
// Handle master toggle
handleToggle: function(status) {
var self = this;
if (status.enabled && status.running) {
// Disable Tor
ui.showModal(_('Disable Tor Shield'), [
E('p', { 'class': 'spinning' }, _('Stopping Tor Shield...'))
]);
api.disable().then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Tor Shield disabled. Your traffic is no longer anonymized.')), 'warning');
self.render();
} else {
ui.addNotification(null, E('p', result.error || _('Failed to disable')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
} else {
// Enable Tor with selected preset
ui.showModal(_('Enable Tor Shield'), [
E('p', { 'class': 'spinning' }, _('Starting Tor Shield with %s preset...').format(self.currentPreset))
]);
api.enable(self.currentPreset).then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Tor Shield is starting. Please wait for bootstrap to complete.')), 'info');
} else {
ui.addNotification(null, E('p', result.error || _('Failed to enable')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
}
},
// Handle preset selection
handlePresetSelect: function(presetId) {
this.currentPreset = presetId;
// Update UI
var presets = document.querySelectorAll('.tor-preset');
presets.forEach(function(p) {
p.classList.toggle('active', p.dataset.preset === presetId);
});
},
// Handle new identity request
handleNewIdentity: function() {
ui.showModal(_('New Identity'), [
E('p', { 'class': 'spinning' }, _('Requesting new Tor identity...'))
]);
api.newIdentity().then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('New identity requested. New circuits will be established shortly.')), 'info');
} else {
ui.addNotification(null, E('p', result.error || _('Failed to request new identity')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
},
// Handle leak test
handleLeakTest: function() {
var self = this;
ui.showModal(_('Leak Test'), [
E('p', { 'class': 'spinning' }, _('Running leak detection tests...'))
]);
api.checkLeaks().then(function(result) {
ui.hideModal();
var tests = result.tests || [];
var content = [
E('h4', {}, _('Leak Test Results'))
];
tests.forEach(function(test) {
content.push(E('div', { 'style': 'margin: 10px 0; padding: 10px; background: ' + (test.passed ? 'rgba(16,185,129,0.1)' : 'rgba(239,68,68,0.1)') + '; border-radius: 8px;' }, [
E('strong', {}, test.name + ': '),
E('span', { 'style': 'color: ' + (test.passed ? '#10b981' : '#ef4444') }, test.passed ? 'PASSED' : 'FAILED'),
E('p', { 'style': 'margin: 5px 0 0 0; font-size: 12px; opacity: 0.8;' }, test.message)
]));
});
content.push(E('div', { 'style': 'margin-top: 16px; text-align: center;' }, [
E('strong', { 'style': 'font-size: 18px; color: ' + (result.protected ? '#10b981' : '#ef4444') },
result.protected ? 'Your connection is PROTECTED' : 'WARNING: Potential leaks detected')
]));
content.push(E('div', { 'class': 'right', 'style': 'margin-top: 16px;' }, [
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
]));
ui.showModal(_('Leak Test Results'), content);
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Leak test failed: %s').format(err.message || err)), 'error');
});
},
// Update stats without full re-render
updateStats: function(status, bandwidth) {
// Update status badge
var badge = document.querySelector('.tor-status-badge');
if (badge) {
badge.className = 'tor-status-badge';
if (!status.enabled) {
badge.classList.add('disabled');
badge.innerHTML = '<span class="tor-status-dot"></span>' + _('Disabled');
} else if (status.bootstrap < 100) {
badge.classList.add('connecting');
badge.innerHTML = '<span class="tor-status-dot"></span>' + _('Connecting %d%%').format(status.bootstrap);
} else if (status.is_tor) {
badge.classList.add('protected');
badge.innerHTML = '<span class="tor-status-dot"></span>' + _('Protected');
} else {
badge.classList.add('exposed');
badge.innerHTML = '<span class="tor-status-dot"></span>' + _('Exposed');
}
}
// Update toggle state
var toggle = document.querySelector('.tor-master-toggle');
var toggleLabel = document.querySelector('.tor-toggle-label');
if (toggle) {
toggle.classList.toggle('active', status.enabled && status.running);
}
if (toggleLabel) {
toggleLabel.textContent = (status.enabled && status.running) ? _('Protected') : _('Go Anonymous');
toggleLabel.classList.toggle('active', status.enabled && status.running);
}
// Update IP info
var exitIp = document.querySelector('.tor-exit-ip');
var realIp = document.querySelector('.tor-real-ip');
if (exitIp) {
exitIp.textContent = status.exit_ip || _('Not connected');
exitIp.className = 'tor-ip-value ' + (status.is_tor ? 'protected' : 'exposed');
}
if (realIp) {
realIp.textContent = status.real_ip || _('Unknown');
}
// Update quick stats
var updates = [
{ selector: '.tor-stat-circuits', value: status.circuit_count || 0 },
{ selector: '.tor-stat-bandwidth', value: api.formatRate(bandwidth.read_rate || 0) },
{ selector: '.tor-stat-uptime', value: api.formatUptime(status.uptime || 0) },
{ selector: '.tor-stat-read', value: api.formatBytes(bandwidth.read || 0) },
{ selector: '.tor-stat-written', value: api.formatBytes(bandwidth.written || 0) }
];
updates.forEach(function(u) {
var el = document.querySelector(u.selector);
if (el && el.textContent !== String(u.value)) {
el.textContent = u.value;
el.classList.add('tor-value-updated');
setTimeout(function() { el.classList.remove('tor-value-updated'); }, 500);
}
});
// Update bootstrap progress if connecting
var progressBar = document.querySelector('.tor-progress-bar');
var progressLabel = document.querySelector('.tor-bootstrap-percent');
if (progressBar && status.enabled && status.bootstrap < 100) {
progressBar.style.width = status.bootstrap + '%';
}
if (progressLabel) {
progressLabel.textContent = status.bootstrap + '%';
}
},
startPolling: function() {
var self = this;
this.pollActive = true;
poll.add(L.bind(function() {
if (!this.pollActive) return Promise.resolve();
return api.getDashboardData().then(L.bind(function(data) {
this.updateStats(data.status || {}, data.bandwidth || {});
}, this));
}, this), this.pollInterval);
},
stopPolling: function() {
this.pollActive = false;
poll.stop();
},
render: function(data) {
var self = this;
var status = data.status || {};
var presets = data.presets || [];
var bandwidth = data.bandwidth || {};
var isActive = status.enabled && status.running;
var isProtected = isActive && status.is_tor;
var isConnecting = isActive && status.bootstrap < 100;
var statusClass = 'disabled';
var statusText = _('Disabled');
if (isConnecting) {
statusClass = 'connecting';
statusText = _('Connecting %d%%').format(status.bootstrap);
} else if (isProtected) {
statusClass = 'protected';
statusText = _('Protected');
} else if (isActive) {
statusClass = 'exposed';
statusText = _('Exposed');
}
var view = E('div', { 'class': 'tor-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('tor-shield/dashboard.css') }),
// Header
E('div', { 'class': 'tor-header' }, [
E('div', { 'class': 'tor-logo' }, [
E('div', { 'class': 'tor-logo-icon' }, '\uD83E\uDDC5'),
E('div', { 'class': 'tor-logo-text' }, ['Tor ', E('span', {}, 'Shield')])
]),
E('div', { 'class': 'tor-status-badge ' + statusClass }, [
E('span', { 'class': 'tor-status-dot' }),
statusText
])
]),
// Auto-refresh control
E('div', { 'class': 'tor-refresh-control' }, [
E('span', {}, [
E('span', { 'class': 'tor-refresh-indicator active' }),
' ' + _('Auto-refresh: '),
E('span', { 'class': 'tor-refresh-state' }, _('Active'))
]),
E('button', {
'class': 'tor-btn tor-btn-sm',
'click': L.bind(function(ev) {
var btn = ev.target;
var indicator = document.querySelector('.tor-refresh-indicator');
var state = document.querySelector('.tor-refresh-state');
if (this.pollActive) {
this.stopPolling();
btn.textContent = _('Resume');
indicator.classList.remove('active');
state.textContent = _('Paused');
} else {
this.startPolling();
btn.textContent = _('Pause');
indicator.classList.add('active');
state.textContent = _('Active');
}
}, this)
}, _('Pause'))
]),
// Hero Section
E('div', { 'class': 'tor-hero' }, [
// Master Toggle
E('div', { 'class': 'tor-toggle-section' }, [
E('button', {
'class': 'tor-master-toggle' + (isActive ? ' active' : ''),
'click': L.bind(function() { this.handleToggle(status); }, this),
'title': isActive ? _('Click to disable') : _('Click to enable')
}, '\uD83E\uDDC5'),
E('div', { 'class': 'tor-toggle-label' + (isActive ? ' active' : '') },
isActive ? _('Protected') : _('Go Anonymous'))
]),
// Protection Info
E('div', { 'class': 'tor-protection-info' }, [
E('div', { 'class': 'tor-protection-title' }, _('Your Protection Status')),
E('div', { 'class': 'tor-ip-info' }, [
E('div', { 'class': 'tor-ip-item' }, [
E('div', { 'class': 'tor-ip-label' }, _('Real IP')),
E('div', { 'class': 'tor-ip-value tor-real-ip' }, status.real_ip || _('Unknown'))
]),
E('div', { 'class': 'tor-ip-item' }, [
E('div', { 'class': 'tor-ip-label' }, _('Tor Exit IP')),
E('div', {
'class': 'tor-ip-value tor-exit-ip ' + (isProtected ? 'protected' : 'exposed')
}, status.exit_ip || _('Not connected')),
status.exit_ip ? E('div', { 'class': 'tor-exit-location' }, [
E('span', { 'class': 'tor-exit-country' }, api.getCountryFlag(status.exit_country) || ''),
status.exit_country || ''
]) : ''
])
]),
// Bootstrap progress (when connecting)
isConnecting ? E('div', { 'class': 'tor-bootstrap' }, [
E('div', { 'class': 'tor-bootstrap-label' }, [
E('span', {}, _('Bootstrapping...')),
E('span', { 'class': 'tor-bootstrap-percent' }, status.bootstrap + '%')
]),
E('div', { 'class': 'tor-progress' }, [
E('div', { 'class': 'tor-progress-bar', 'style': 'width: ' + status.bootstrap + '%' })
])
]) : ''
])
]),
// Presets
E('div', { 'class': 'tor-presets' },
presets.map(function(preset) {
return E('div', {
'class': 'tor-preset' + (self.currentPreset === preset.id ? ' active' : ''),
'data-preset': preset.id,
'click': L.bind(function() { this.handlePresetSelect(preset.id); }, self)
}, [
E('div', { 'class': 'tor-preset-icon' }, api.getPresetIcon(preset.icon)),
E('div', { 'class': 'tor-preset-name' }, preset.name),
E('div', { 'class': 'tor-preset-desc' }, preset.description)
]);
})
),
// Quick Stats
E('div', { 'class': 'tor-quick-stats' }, [
E('div', { 'class': 'tor-quick-stat' }, [
E('div', { 'class': 'tor-quick-stat-icon' }, '\uD83D\uDD04'),
E('div', { 'class': 'tor-quick-stat-value tor-stat-circuits' }, status.circuit_count || 0),
E('div', { 'class': 'tor-quick-stat-label' }, _('Circuits'))
]),
E('div', { 'class': 'tor-quick-stat' }, [
E('div', { 'class': 'tor-quick-stat-icon' }, '\uD83D\uDCCA'),
E('div', { 'class': 'tor-quick-stat-value tor-stat-bandwidth' }, api.formatRate(bandwidth.read_rate || 0)),
E('div', { 'class': 'tor-quick-stat-label' }, _('Bandwidth'))
]),
E('div', { 'class': 'tor-quick-stat' }, [
E('div', { 'class': 'tor-quick-stat-icon' }, '\u23F1'),
E('div', { 'class': 'tor-quick-stat-value tor-stat-uptime' }, api.formatUptime(status.uptime || 0)),
E('div', { 'class': 'tor-quick-stat-label' }, _('Uptime'))
]),
E('div', { 'class': 'tor-quick-stat' }, [
E('div', { 'class': 'tor-quick-stat-icon' }, '\uD83D\uDCE5'),
E('div', { 'class': 'tor-quick-stat-value tor-stat-read' }, api.formatBytes(bandwidth.read || 0)),
E('div', { 'class': 'tor-quick-stat-label' }, _('Downloaded'))
]),
E('div', { 'class': 'tor-quick-stat' }, [
E('div', { 'class': 'tor-quick-stat-icon' }, '\uD83D\uDCE4'),
E('div', { 'class': 'tor-quick-stat-value tor-stat-written' }, api.formatBytes(bandwidth.written || 0)),
E('div', { 'class': 'tor-quick-stat-label' }, _('Uploaded'))
])
]),
// Actions Card
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\u26A1'),
_('Quick Actions')
])
]),
E('div', { 'class': 'tor-card-body' }, [
E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' }, [
E('button', {
'class': 'tor-btn tor-btn-primary',
'click': L.bind(this.handleNewIdentity, this),
'disabled': !isActive
}, ['\uD83D\uDD04 ', _('New Identity')]),
E('button', {
'class': 'tor-btn',
'click': L.bind(this.handleLeakTest, this),
'disabled': !isActive
}, ['\uD83D\uDD0D ', _('Leak Test')]),
E('a', {
'class': 'tor-btn',
'href': L.url('admin', 'services', 'tor-shield', 'circuits')
}, ['\uD83D\uDDFA ', _('View Circuits')]),
E('a', {
'class': 'tor-btn',
'href': L.url('admin', 'services', 'tor-shield', 'hidden-services')
}, ['\uD83E\uDDC5 ', _('Hidden Services')]),
E('a', {
'class': 'tor-btn',
'href': L.url('admin', 'services', 'tor-shield', 'settings')
}, ['\u2699 ', _('Settings')])
])
])
]),
// Configuration Info
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\u2139'),
_('Current Configuration')
]),
E('div', { 'class': 'tor-card-badge' }, status.mode || 'transparent')
]),
E('div', { 'class': 'tor-card-body' }, [
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;' }, [
E('div', {}, [
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted); text-transform: uppercase;' }, _('Mode')),
E('div', { 'style': 'font-size: 16px; font-weight: 500;' }, status.mode === 'transparent' ? _('Transparent Proxy') : _('SOCKS Proxy'))
]),
E('div', {}, [
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted); text-transform: uppercase;' }, _('DNS over Tor')),
E('div', { 'style': 'font-size: 16px; font-weight: 500;' }, status.dns_over_tor ? _('Enabled') : _('Disabled'))
]),
E('div', {}, [
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted); text-transform: uppercase;' }, _('Kill Switch')),
E('div', { 'style': 'font-size: 16px; font-weight: 500;' }, status.kill_switch ? _('Enabled') : _('Disabled'))
]),
E('div', {}, [
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted); text-transform: uppercase;' }, _('Bridges')),
E('div', { 'style': 'font-size: 16px; font-weight: 500;' }, status.bridges_enabled ? status.bridge_type : _('Not used'))
])
])
])
])
]);
// Start auto-refresh
this.startPolling();
return view;
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,255 @@
'use strict';
'require view';
'require ui';
'require tor-shield/api as api';
return view.extend({
title: _('Tor Shield Settings'),
load: function() {
return api.getSettings();
},
handleSave: function(form) {
var self = this;
// Gather form values
var settings = {
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',
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,
exit_nodes: form.querySelector('[name="exit_nodes"]').value.trim(),
exclude_exit_nodes: form.querySelector('[name="exclude_exit_nodes"]').value.trim(),
strict_nodes: form.querySelector('[name="strict_nodes"]').checked ? '1' : '0'
};
ui.showModal(_('Saving Settings'), [
E('p', { 'class': 'spinning' }, _('Saving configuration...'))
]);
api.saveSettings(
settings.mode,
settings.dns_over_tor,
settings.kill_switch,
settings.socks_port,
settings.trans_port,
settings.dns_port,
settings.exit_nodes,
settings.exclude_exit_nodes,
settings.strict_nodes
).then(function(result) {
ui.hideModal();
if (result.success) {
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');
}
});
},
render: function(data) {
var self = this;
var view = E('div', { 'class': 'tor-dashboard' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('tor-shield/dashboard.css') }),
E('form', { 'id': 'tor-settings-form' }, [
// General Settings
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\u2699'),
_('General Settings')
])
]),
E('div', { 'class': 'tor-card-body' }, [
// Mode
E('div', { 'style': 'margin-bottom: 20px;' }, [
E('label', { 'style': 'display: block; font-weight: 600; margin-bottom: 8px;' }, _('Operating Mode')),
E('select', {
'name': 'mode',
'class': 'cbi-input-select',
'style': 'width: 100%; max-width: 300px;'
}, [
E('option', { 'value': 'transparent', 'selected': data.mode === 'transparent' }, _('Transparent Proxy')),
E('option', { 'value': 'socks', 'selected': data.mode === 'socks' }, _('SOCKS Proxy Only'))
]),
E('p', { 'style': 'font-size: 12px; color: var(--tor-text-muted); margin-top: 4px;' },
_('Transparent mode routes all traffic through Tor. SOCKS mode only provides a proxy.'))
]),
// DNS over Tor
E('div', { 'style': 'margin-bottom: 20px;' }, [
E('label', { 'style': 'display: flex; align-items: center; gap: 8px; cursor: pointer;' }, [
E('input', {
'type': 'checkbox',
'name': 'dns_over_tor',
'checked': data.dns_over_tor
}),
E('span', { 'style': 'font-weight: 600;' }, _('DNS over Tor'))
]),
E('p', { 'style': 'font-size: 12px; color: var(--tor-text-muted); margin-top: 4px; margin-left: 24px;' },
_('Route DNS queries through Tor to prevent DNS leaks. Recommended.'))
]),
// Kill Switch
E('div', { 'style': 'margin-bottom: 20px;' }, [
E('label', { 'style': 'display: flex; align-items: center; gap: 8px; cursor: pointer;' }, [
E('input', {
'type': 'checkbox',
'name': 'kill_switch',
'checked': data.kill_switch
}),
E('span', { 'style': 'font-weight: 600;' }, _('Kill Switch'))
]),
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.'))
])
])
]),
// Port Configuration
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\uD83D\uDD0C'),
_('Port Configuration')
])
]),
E('div', { 'class': 'tor-card-body' }, [
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;' }, [
// SOCKS Port
E('div', {}, [
E('label', { 'style': 'display: block; font-weight: 600; margin-bottom: 8px;' }, _('SOCKS Port')),
E('input', {
'type': 'number',
'name': 'socks_port',
'class': 'cbi-input-text',
'value': data.socks_port || 9050,
'min': '1024',
'max': '65535',
'style': 'width: 100%;'
}),
E('p', { 'style': 'font-size: 12px; color: var(--tor-text-muted); margin-top: 4px;' },
_('SOCKS5 proxy port for applications'))
]),
// Transparent Port
E('div', {}, [
E('label', { 'style': 'display: block; font-weight: 600; margin-bottom: 8px;' }, _('Transparent Port')),
E('input', {
'type': 'number',
'name': 'trans_port',
'class': 'cbi-input-text',
'value': data.trans_port || 9040,
'min': '1024',
'max': '65535',
'style': 'width: 100%;'
}),
E('p', { 'style': 'font-size: 12px; color: var(--tor-text-muted); margin-top: 4px;' },
_('Port for transparent proxying'))
]),
// DNS Port
E('div', {}, [
E('label', { 'style': 'display: block; font-weight: 600; margin-bottom: 8px;' }, _('DNS Port')),
E('input', {
'type': 'number',
'name': 'dns_port',
'class': 'cbi-input-text',
'value': data.dns_port || 9053,
'min': '1024',
'max': '65535',
'style': 'width: 100%;'
}),
E('p', { 'style': 'font-size: 12px; color: var(--tor-text-muted); margin-top: 4px;' },
_('Port for DNS over Tor'))
])
])
])
]),
// Exit Node Restrictions
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
E('div', { 'class': 'tor-card-title' }, [
E('span', { 'class': 'tor-card-title-icon' }, '\uD83C\uDF10'),
_('Exit Node Restrictions')
])
]),
E('div', { 'class': 'tor-card-body' }, [
E('p', { 'style': 'color: var(--tor-text-secondary); margin-bottom: 16px;' },
_('Control which countries can be used for exit nodes. Use ISO country codes (e.g., US, DE, NL) separated by commas.')),
// Exit Nodes
E('div', { 'style': 'margin-bottom: 20px;' }, [
E('label', { 'style': 'display: block; font-weight: 600; margin-bottom: 8px;' }, _('Preferred Exit Countries')),
E('input', {
'type': 'text',
'name': 'exit_nodes',
'class': 'cbi-input-text',
'value': data.exit_nodes || '',
'placeholder': 'e.g., {US},{DE},{NL}',
'style': 'width: 100%;'
}),
E('p', { 'style': 'font-size: 12px; color: var(--tor-text-muted); margin-top: 4px;' },
_('Only use exit nodes in these countries. Leave empty for any country.'))
]),
// Exclude Exit Nodes
E('div', { 'style': 'margin-bottom: 20px;' }, [
E('label', { 'style': 'display: block; font-weight: 600; margin-bottom: 8px;' }, _('Excluded Exit Countries')),
E('input', {
'type': 'text',
'name': 'exclude_exit_nodes',
'class': 'cbi-input-text',
'value': data.exclude_exit_nodes || '',
'placeholder': 'e.g., {RU},{CN},{IR}',
'style': 'width: 100%;'
}),
E('p', { 'style': 'font-size: 12px; color: var(--tor-text-muted); margin-top: 4px;' },
_('Never use exit nodes in these countries.'))
]),
// Strict Nodes
E('div', {}, [
E('label', { 'style': 'display: flex; align-items: center; gap: 8px; cursor: pointer;' }, [
E('input', {
'type': 'checkbox',
'name': 'strict_nodes',
'checked': data.strict_nodes
}),
E('span', { 'style': 'font-weight: 600;' }, _('Strict Nodes'))
]),
E('p', { 'style': 'font-size: 12px; color: var(--tor-text-muted); margin-top: 4px; margin-left: 24px;' },
_('Strictly enforce exit node restrictions. May reduce anonymity or cause connection failures.'))
])
])
]),
// Actions
E('div', { 'style': 'display: flex; gap: 12px; margin-top: 20px;' }, [
E('button', {
'type': 'button',
'class': 'tor-btn tor-btn-primary',
'click': function() {
self.handleSave(document.getElementById('tor-settings-form'));
}
}, ['\uD83D\uDCBE ', _('Save Settings')]),
E('a', {
'href': L.url('admin', 'services', 'tor-shield'),
'class': 'tor-btn'
}, ['\u2190 ', _('Back to Dashboard')])
])
])
]);
return view;
},
handleSaveApply: null,
handleReset: null
});

View File

@ -0,0 +1,707 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Tor Shield RPCD backend
# Copyright (C) 2025 CyberMind.fr
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
CONFIG="tor-shield"
TOR_CONTROL="/var/run/tor/control"
TOR_DATA="/var/lib/tor"
# Send command to Tor control socket
tor_control() {
if [ ! -S "$TOR_CONTROL" ]; then
return 1
fi
echo -e "$1" | nc -U "$TOR_CONTROL" 2>/dev/null
}
# Check if Tor is running
is_running() {
pgrep tor >/dev/null 2>&1
}
# Get bootstrap percentage
get_bootstrap() {
local status=$(tor_control "GETINFO status/bootstrap-phase")
local progress=$(echo "$status" | grep "PROGRESS=" | sed 's/.*PROGRESS=\([0-9]*\).*/\1/')
echo "${progress:-0}"
}
# Get overall status
get_status() {
json_init
local enabled mode dns_over_tor kill_switch
config_load "$CONFIG"
config_get enabled main enabled '0'
config_get mode main mode 'transparent'
config_get dns_over_tor main dns_over_tor '1'
config_get kill_switch main kill_switch '1'
json_add_boolean "enabled" "$enabled"
json_add_string "mode" "$mode"
json_add_boolean "dns_over_tor" "$dns_over_tor"
json_add_boolean "kill_switch" "$kill_switch"
# Running state
if is_running; then
json_add_boolean "running" 1
# Bootstrap percentage
local bootstrap=$(get_bootstrap)
json_add_int "bootstrap" "${bootstrap:-0}"
# Get exit IP if bootstrapped
if [ "$bootstrap" -ge 100 ]; then
local socks_port
config_get socks_port socks port '9050'
local exit_ip=$(curl -s --max-time 10 --socks5-hostname 127.0.0.1:$socks_port https://check.torproject.org/api/ip 2>/dev/null | jsonfilter -e '@.IP' 2>/dev/null)
json_add_string "exit_ip" "${exit_ip:-unknown}"
# Check if using Tor
local is_tor=$(curl -s --max-time 10 --socks5-hostname 127.0.0.1:$socks_port https://check.torproject.org/api/ip 2>/dev/null | jsonfilter -e '@.IsTor' 2>/dev/null)
json_add_boolean "is_tor" "$([ "$is_tor" = "true" ] && echo 1 || echo 0)"
# Get circuit count
local circuits=$(tor_control "GETINFO circuit-status" | grep -c "BUILT" 2>/dev/null)
json_add_int "circuit_count" "${circuits:-0}"
# Get bandwidth
local bw_read=$(tor_control "GETINFO traffic/read" | grep "250" | awk '{print $2}')
local bw_written=$(tor_control "GETINFO traffic/written" | grep "250" | awk '{print $2}')
json_add_int "bytes_read" "${bw_read:-0}"
json_add_int "bytes_written" "${bw_written:-0}"
fi
# Uptime from pid file
local pidfile="/var/run/tor/tor.pid"
if [ -f "$pidfile" ]; then
local pid=$(cat "$pidfile")
if [ -d "/proc/$pid" ]; then
local start_time=$(stat -c %Y "/proc/$pid" 2>/dev/null)
local now=$(date +%s)
json_add_int "uptime" "$((now - start_time))"
fi
fi
else
json_add_boolean "running" 0
json_add_int "bootstrap" 0
fi
# Bridge status
local bridges_enabled bridge_type
config_get bridges_enabled bridges enabled '0'
config_get bridge_type bridges type 'obfs4'
json_add_boolean "bridges_enabled" "$bridges_enabled"
json_add_string "bridge_type" "$bridge_type"
# Get real IP
local real_ip=$(curl -s --max-time 5 https://ipinfo.io/ip 2>/dev/null)
json_add_string "real_ip" "${real_ip:-unknown}"
json_dump
}
# Enable Tor Shield
do_enable() {
read input
json_load "$input"
json_get_var preset preset
json_init
[ -z "$preset" ] && preset="anonymous"
# Load preset configuration
config_load "$CONFIG"
local preset_mode preset_dns preset_kill preset_bridges
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'
# 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"
if [ "$preset_bridges" = "1" ]; then
uci set tor-shield.bridges.enabled='1'
fi
uci commit tor-shield
# Restart service
/etc/init.d/tor-shield restart >/dev/null 2>&1 &
json_add_boolean "success" 1
json_add_string "message" "Tor Shield enabling with preset: $preset"
json_add_string "preset" "$preset"
json_dump
}
# Disable Tor Shield
do_disable() {
json_init
uci set tor-shield.main.enabled='0'
uci commit tor-shield
/etc/init.d/tor-shield stop >/dev/null 2>&1
json_add_boolean "success" 1
json_add_string "message" "Tor Shield disabled"
json_dump
}
# Get circuits
get_circuits() {
json_init
json_add_array "circuits"
if ! is_running; then
json_close_array
json_dump
return
fi
local circuits=$(tor_control "GETINFO circuit-status")
echo "$circuits" | grep "BUILT" | while read line; do
local id=$(echo "$line" | awk '{print $1}')
local status=$(echo "$line" | awk '{print $2}')
local path=$(echo "$line" | awk '{print $3}')
local purpose=$(echo "$line" | grep -o 'PURPOSE=[^ ]*' | cut -d= -f2)
if [ -n "$id" ] && [ "$id" != "250" ]; then
json_add_object
json_add_string "id" "$id"
json_add_string "status" "$status"
json_add_string "path" "$path"
json_add_string "purpose" "${purpose:-GENERAL}"
# Parse path into nodes
json_add_array "nodes"
local IFS=','
for node in $path; do
local fingerprint=$(echo "$node" | cut -d'~' -f1 | tr -d '$')
local name=$(echo "$node" | cut -d'~' -f2)
json_add_object
json_add_string "fingerprint" "$fingerprint"
json_add_string "name" "${name:-$fingerprint}"
json_close_object
done
json_close_array
json_close_object
fi
done
json_close_array
json_dump
}
# Request new identity
new_identity() {
json_init
if ! is_running; then
json_add_boolean "success" 0
json_add_string "error" "Tor is not running"
json_dump
return
fi
local result=$(tor_control "SIGNAL NEWNYM")
if echo "$result" | grep -q "250 OK"; then
json_add_boolean "success" 1
json_add_string "message" "New identity requested"
else
json_add_boolean "success" 0
json_add_string "error" "Failed to request new identity"
fi
json_dump
}
# Check for leaks
check_leaks() {
json_init
if ! is_running; then
json_add_boolean "success" 0
json_add_string "error" "Tor is not running"
json_dump
return
fi
local socks_port
config_load "$CONFIG"
config_get socks_port socks port '9050'
local leaks=0
json_add_array "tests"
# IP leak test
json_add_object
json_add_string "name" "IP Leak"
local tor_ip=$(curl -s --max-time 10 --socks5-hostname 127.0.0.1:$socks_port https://check.torproject.org/api/ip 2>/dev/null | jsonfilter -e '@.IP' 2>/dev/null)
local real_ip=$(curl -s --max-time 5 https://ipinfo.io/ip 2>/dev/null)
if [ -n "$tor_ip" ] && [ "$tor_ip" != "$real_ip" ]; then
json_add_boolean "passed" 1
json_add_string "message" "IP protected"
else
json_add_boolean "passed" 0
json_add_string "message" "Potential IP leak"
leaks=$((leaks + 1))
fi
json_close_object
# Tor detection test
json_add_object
json_add_string "name" "Tor Detection"
local is_tor=$(curl -s --max-time 10 --socks5-hostname 127.0.0.1:$socks_port https://check.torproject.org/api/ip 2>/dev/null | jsonfilter -e '@.IsTor' 2>/dev/null)
if [ "$is_tor" = "true" ]; then
json_add_boolean "passed" 1
json_add_string "message" "Traffic via Tor confirmed"
else
json_add_boolean "passed" 0
json_add_string "message" "Traffic may not be through Tor"
leaks=$((leaks + 1))
fi
json_close_object
json_close_array
json_add_int "leak_count" "$leaks"
json_add_boolean "protected" "$([ $leaks -eq 0 ] && echo 1 || echo 0)"
json_dump
}
# Get hidden services
get_hidden_services() {
json_init
json_add_array "services"
config_load "$CONFIG"
config_foreach add_hidden_service_json hidden_service
json_close_array
json_dump
}
add_hidden_service_json() {
local cfg="$1"
local enabled name local_port virtual_port
config_get enabled "$cfg" enabled '0'
config_get name "$cfg" name "$cfg"
config_get local_port "$cfg" local_port '80'
config_get virtual_port "$cfg" virtual_port '80'
local hostname_file="$TOR_DATA/hidden_service_$name/hostname"
local onion_addr=""
if [ -f "$hostname_file" ]; then
onion_addr=$(cat "$hostname_file")
fi
json_add_object
json_add_string "id" "$cfg"
json_add_string "name" "$name"
json_add_boolean "enabled" "$enabled"
json_add_int "local_port" "$local_port"
json_add_int "virtual_port" "$virtual_port"
json_add_string "onion_address" "$onion_addr"
json_close_object
}
# Add hidden service
add_hidden_service() {
read input
json_load "$input"
json_get_var name name
json_get_var local_port local_port
json_get_var virtual_port virtual_port
json_init
if [ -z "$name" ]; then
json_add_boolean "success" 0
json_add_string "error" "Name is required"
json_dump
return
fi
[ -z "$local_port" ] && local_port="80"
[ -z "$virtual_port" ] && virtual_port="80"
# Sanitize name
name=$(echo "$name" | tr -cd 'a-zA-Z0-9_-')
uci set tor-shield.hs_$name=hidden_service
uci set tor-shield.hs_$name.name="$name"
uci set tor-shield.hs_$name.enabled='1'
uci set tor-shield.hs_$name.local_port="$local_port"
uci set tor-shield.hs_$name.virtual_port="$virtual_port"
uci commit tor-shield
json_add_boolean "success" 1
json_add_string "message" "Hidden service created"
json_add_string "name" "$name"
json_dump
}
# Remove hidden service
remove_hidden_service() {
read input
json_load "$input"
json_get_var name name
json_init
if [ -z "$name" ]; then
json_add_boolean "success" 0
json_add_string "error" "Name is required"
json_dump
return
fi
uci delete tor-shield.hs_$name 2>/dev/null
uci commit tor-shield
# Remove data directory
rm -rf "$TOR_DATA/hidden_service_$name" 2>/dev/null
json_add_boolean "success" 1
json_add_string "message" "Hidden service removed"
json_dump
}
# Get exit IP
get_exit_ip() {
json_init
if ! is_running; then
json_add_string "error" "Tor is not running"
json_dump
return
fi
local socks_port
config_load "$CONFIG"
config_get socks_port socks port '9050'
local exit_info=$(curl -s --max-time 10 --socks5-hostname 127.0.0.1:$socks_port https://ipinfo.io 2>/dev/null)
if [ -n "$exit_info" ]; then
local ip=$(echo "$exit_info" | jsonfilter -e '@.ip' 2>/dev/null)
local city=$(echo "$exit_info" | jsonfilter -e '@.city' 2>/dev/null)
local region=$(echo "$exit_info" | jsonfilter -e '@.region' 2>/dev/null)
local country=$(echo "$exit_info" | jsonfilter -e '@.country' 2>/dev/null)
local org=$(echo "$exit_info" | jsonfilter -e '@.org' 2>/dev/null)
json_add_string "ip" "${ip:-unknown}"
json_add_string "city" "${city:-unknown}"
json_add_string "region" "${region:-unknown}"
json_add_string "country" "${country:-unknown}"
json_add_string "org" "${org:-unknown}"
else
json_add_string "ip" "unknown"
json_add_string "error" "Could not determine exit IP"
fi
json_dump
}
# Get bandwidth stats
get_bandwidth() {
json_init
if ! is_running; then
json_add_int "read" 0
json_add_int "written" 0
json_add_int "read_rate" 0
json_add_int "write_rate" 0
json_dump
return
fi
local bw_read=$(tor_control "GETINFO traffic/read" | grep "250" | awk '{print $2}')
local bw_written=$(tor_control "GETINFO traffic/written" | grep "250" | awk '{print $2}')
json_add_int "read" "${bw_read:-0}"
json_add_int "written" "${bw_written:-0}"
# Calculate rates from previous measurement
local prev_file="/tmp/tor_bandwidth"
local prev_read=0 prev_written=0 prev_time=0
local now=$(date +%s)
if [ -f "$prev_file" ]; then
read prev_read prev_written prev_time < "$prev_file"
fi
local read_rate=0 write_rate=0
local time_diff=$((now - prev_time))
if [ $time_diff -gt 0 ] && [ $prev_time -gt 0 ]; then
read_rate=$(( (${bw_read:-0} - prev_read) / time_diff ))
write_rate=$(( (${bw_written:-0} - prev_written) / time_diff ))
[ $read_rate -lt 0 ] && read_rate=0
[ $write_rate -lt 0 ] && write_rate=0
fi
echo "${bw_read:-0} ${bw_written:-0} $now" > "$prev_file"
json_add_int "read_rate" "$read_rate"
json_add_int "write_rate" "$write_rate"
json_dump
}
# Get presets
get_presets() {
json_init
json_add_array "presets"
config_load "$CONFIG"
# Anonymous preset
json_add_object
json_add_string "id" "anonymous"
json_add_string "name" "Full Anonymity"
json_add_string "icon" "shield"
json_add_string "description" "Route all traffic through Tor with kill switch"
json_close_object
# Selective preset
json_add_object
json_add_string "id" "selective"
json_add_string "name" "Selective Apps"
json_add_string "icon" "target"
json_add_string "description" "SOCKS proxy for specific applications"
json_close_object
# Censored preset
json_add_object
json_add_string "id" "censored"
json_add_string "name" "Bypass Censorship"
json_add_string "icon" "unlock"
json_add_string "description" "Use bridges to bypass network restrictions"
json_close_object
json_close_array
json_dump
}
# Get bridges config
get_bridges() {
json_init
local enabled type
config_load "$CONFIG"
config_get enabled bridges enabled '0'
config_get type bridges type 'obfs4'
json_add_boolean "enabled" "$enabled"
json_add_string "type" "$type"
json_add_array "bridge_lines"
config_list_foreach bridges bridge_lines add_bridge_json
json_close_array
json_dump
}
add_bridge_json() {
json_add_string "" "$1"
}
# Set bridges config
set_bridges() {
read input
json_load "$input"
json_get_var enabled enabled
json_get_var type type
json_init
[ -n "$enabled" ] && uci set tor-shield.bridges.enabled="$enabled"
[ -n "$type" ] && uci set tor-shield.bridges.type="$type"
uci commit tor-shield
json_add_boolean "success" 1
json_add_string "message" "Bridge configuration updated"
json_dump
}
# Get settings
get_settings() {
json_init
config_load "$CONFIG"
# Main settings
local mode dns_over_tor kill_switch auto_bridges
config_get mode main mode 'transparent'
config_get dns_over_tor main dns_over_tor '1'
config_get kill_switch main kill_switch '1'
config_get auto_bridges main auto_bridges '0'
json_add_string "mode" "$mode"
json_add_boolean "dns_over_tor" "$dns_over_tor"
json_add_boolean "kill_switch" "$kill_switch"
json_add_boolean "auto_bridges" "$auto_bridges"
# SOCKS settings
local socks_port socks_addr
config_get socks_port socks port '9050'
config_get socks_addr socks address '127.0.0.1'
json_add_int "socks_port" "$socks_port"
json_add_string "socks_address" "$socks_addr"
# Transparent proxy settings
local trans_port dns_port
config_get trans_port trans port '9040'
config_get dns_port trans dns_port '9053'
json_add_int "trans_port" "$trans_port"
json_add_int "dns_port" "$dns_port"
# Security settings
local exit_nodes exclude_exit strict_nodes
config_get exit_nodes security exit_nodes ''
config_get exclude_exit security exclude_exit_nodes ''
config_get strict_nodes security strict_nodes '0'
json_add_string "exit_nodes" "$exit_nodes"
json_add_string "exclude_exit_nodes" "$exclude_exit"
json_add_boolean "strict_nodes" "$strict_nodes"
# Excluded IPs
json_add_array "excluded_ips"
config_list_foreach trans excluded_ips add_excluded_ip_json
json_close_array
json_dump
}
add_excluded_ip_json() {
json_add_string "" "$1"
}
# Save settings
save_settings() {
read input
json_load "$input"
json_init
# Get values from input
json_get_var mode mode
json_get_var dns_over_tor dns_over_tor
json_get_var kill_switch kill_switch
json_get_var socks_port socks_port
json_get_var trans_port trans_port
json_get_var dns_port dns_port
json_get_var exit_nodes exit_nodes
json_get_var exclude_exit exclude_exit_nodes
json_get_var strict_nodes strict_nodes
# Apply settings
[ -n "$mode" ] && uci set tor-shield.main.mode="$mode"
[ -n "$dns_over_tor" ] && uci set tor-shield.main.dns_over_tor="$dns_over_tor"
[ -n "$kill_switch" ] && uci set tor-shield.main.kill_switch="$kill_switch"
[ -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 "$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"
uci commit tor-shield
json_add_boolean "success" 1
json_add_string "message" "Settings saved"
json_dump
}
# Main dispatcher
case "$1" in
list)
echo '{"status":{},"enable":{"preset":"str"},"disable":{},"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":{},"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"}}'
;;
call)
case "$2" in
status)
get_status
;;
enable)
do_enable
;;
disable)
do_disable
;;
circuits)
get_circuits
;;
new_identity)
new_identity
;;
check_leaks)
check_leaks
;;
hidden_services)
get_hidden_services
;;
add_hidden_service)
add_hidden_service
;;
remove_hidden_service)
remove_hidden_service
;;
exit_ip)
get_exit_ip
;;
bandwidth)
get_bandwidth
;;
presets)
get_presets
;;
bridges)
get_bridges
;;
set_bridges)
set_bridges
;;
settings)
get_settings
;;
save_settings)
save_settings
;;
*)
echo '{"error": "Unknown method"}'
;;
esac
;;
esac

View File

@ -0,0 +1,52 @@
{
"admin/services/tor-shield": {
"title": "Tor Shield",
"order": 30,
"action": {
"type": "firstchild"
},
"depends": {
"acl": ["luci-app-tor-shield"]
}
},
"admin/services/tor-shield/overview": {
"title": "Overview",
"order": 10,
"action": {
"type": "view",
"path": "tor-shield/overview"
}
},
"admin/services/tor-shield/circuits": {
"title": "Circuits",
"order": 20,
"action": {
"type": "view",
"path": "tor-shield/circuits"
}
},
"admin/services/tor-shield/hidden-services": {
"title": "Hidden Services",
"order": 30,
"action": {
"type": "view",
"path": "tor-shield/hidden-services"
}
},
"admin/services/tor-shield/bridges": {
"title": "Bridges",
"order": 40,
"action": {
"type": "view",
"path": "tor-shield/bridges"
}
},
"admin/services/tor-shield/settings": {
"title": "Settings",
"order": 90,
"action": {
"type": "view",
"path": "tor-shield/settings"
}
}
}

View File

@ -0,0 +1,46 @@
{
"luci-app-tor-shield": {
"description": "Grant access to LuCI Tor Shield",
"read": {
"ubus": {
"luci.tor-shield": [
"status",
"circuits",
"hidden_services",
"exit_ip",
"bandwidth",
"presets",
"bridges",
"settings"
],
"system": [ "info", "board" ],
"file": [ "read", "stat", "exec" ]
},
"uci": [ "tor-shield" ],
"file": {
"/etc/config/tor-shield": [ "read" ],
"/var/lib/tor": [ "read" ],
"/var/run/tor": [ "read" ],
"/var/log/tor.log": [ "read" ]
}
},
"write": {
"ubus": {
"luci.tor-shield": [
"enable",
"disable",
"new_identity",
"check_leaks",
"add_hidden_service",
"remove_hidden_service",
"set_bridges",
"save_settings"
]
},
"uci": [ "tor-shield" ],
"file": {
"/etc/config/tor-shield": [ "write" ]
}
}
}
}

View File

@ -0,0 +1,78 @@
# SPDX-License-Identifier: MIT
#
# Copyright (C) 2025 CyberMind.fr
#
# SecuBox Tor Shield - Tor anonymization for OpenWrt
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-tor
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_ARCH:=all
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
PKG_LICENSE:=MIT
include $(INCLUDE_DIR)/package.mk
define Package/secubox-app-tor
SECTION:=utils
CATEGORY:=Utilities
PKGARCH:=all
SUBMENU:=SecuBox Apps
TITLE:=SecuBox Tor Shield
DEPENDS:=+tor +tor-geoip +iptables +curl +jsonfilter
endef
define Package/secubox-app-tor/description
SecuBox Tor Shield - One-click Tor anonymization for OpenWrt
Features:
- Transparent proxy mode (route all traffic through Tor)
- SOCKS proxy mode (selective app routing)
- DNS over Tor (prevent DNS leaks)
- Kill switch (block non-Tor traffic)
- Hidden services (.onion) management
- Bridge support (obfs4, snowflake) for censored networks
- Circuit visualization and identity management
Configure in /etc/config/tor-shield.
endef
define Package/secubox-app-tor/conffiles
/etc/config/tor-shield
endef
define Build/Compile
endef
define Package/secubox-app-tor/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/tor-shield $(1)/etc/config/tor-shield
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/etc/init.d/tor-shield $(1)/etc/init.d/tor-shield
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/torctl $(1)/usr/sbin/torctl
endef
define Package/secubox-app-tor/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
echo ""
echo "SecuBox Tor Shield installed."
echo ""
echo "Enable Tor Shield:"
echo " torctl enable"
echo " /etc/init.d/tor-shield start"
echo ""
echo "Check status:"
echo " torctl status"
echo ""
}
exit 0
endef
$(eval $(call BuildPackage,secubox-app-tor))

View File

@ -0,0 +1,51 @@
# SecuBox Tor Shield configuration
# /etc/config/tor-shield
config tor-shield 'main'
option enabled '0'
option mode 'transparent'
option dns_over_tor '1'
option kill_switch '1'
option auto_bridges '0'
config preset 'anonymous'
option name 'Full Anonymity'
option icon 'shield'
option mode 'transparent'
option dns_over_tor '1'
option kill_switch '1'
config preset 'selective'
option name 'Selective Apps'
option icon 'target'
option mode 'socks'
option dns_over_tor '0'
option kill_switch '0'
config preset 'censored'
option name 'Bypass Censorship'
option icon 'unlock'
option mode 'transparent'
option use_bridges '1'
option dns_over_tor '1'
config proxy 'socks'
option port '9050'
option address '127.0.0.1'
config transparent 'trans'
option port '9040'
option dns_port '9053'
list excluded_ips '192.168.0.0/16'
list excluded_ips '10.0.0.0/8'
list excluded_ips '172.16.0.0/12'
list excluded_ips '127.0.0.0/8'
config bridges 'bridges'
option enabled '0'
option type 'obfs4'
config security 'security'
option exit_nodes ''
option exclude_exit_nodes ''
option strict_nodes '0'

View File

@ -0,0 +1,265 @@
#!/bin/sh /etc/rc.common
# SecuBox Tor Shield - Tor anonymization service
# Copyright (C) 2025 CyberMind.fr
START=95
STOP=10
USE_PROCD=1
PROG=/usr/sbin/torctl
CONFIG=tor-shield
TORRC=/var/run/tor/torrc
TOR_DATA=/var/lib/tor
TOR_RUN=/var/run/tor
. /lib/functions.sh
generate_torrc() {
local enabled mode dns_over_tor socks_port socks_addr trans_port dns_port
local bridges_enabled bridge_type exit_nodes exclude_exit_nodes strict_nodes
config_load "$CONFIG"
config_get enabled main enabled '0'
config_get mode main mode 'transparent'
config_get dns_over_tor main dns_over_tor '1'
config_get socks_port socks port '9050'
config_get socks_addr socks address '127.0.0.1'
config_get trans_port trans port '9040'
config_get dns_port trans dns_port '9053'
config_get bridges_enabled bridges enabled '0'
config_get bridge_type bridges type 'obfs4'
config_get exit_nodes security exit_nodes ''
config_get exclude_exit_nodes security exclude_exit_nodes ''
config_get strict_nodes security strict_nodes '0'
mkdir -p "$TOR_RUN" "$TOR_DATA"
chmod 700 "$TOR_DATA"
cat > "$TORRC" << EOF
# SecuBox Tor Shield - Auto-generated config
# Do not edit - managed by tor-shield
User tor
DataDirectory $TOR_DATA
PidFile $TOR_RUN/tor.pid
Log notice file /var/log/tor.log
ControlSocket $TOR_RUN/control
ControlSocketsGroupWritable 1
# SOCKS proxy
SocksPort $socks_addr:$socks_port
SocksPolicy accept 127.0.0.1
SocksPolicy accept 192.168.0.0/16
SocksPolicy accept 10.0.0.0/8
SocksPolicy reject *
EOF
# Transparent proxy mode
if [ "$mode" = "transparent" ]; then
cat >> "$TORRC" << EOF
# Transparent proxy
TransPort 0.0.0.0:$trans_port
EOF
fi
# DNS over Tor
if [ "$dns_over_tor" = "1" ]; then
cat >> "$TORRC" << EOF
# DNS over Tor
DNSPort 0.0.0.0:$dns_port
AutomapHostsOnResolve 1
AutomapHostsSuffixes .onion,.exit
VirtualAddrNetworkIPv4 10.192.0.0/10
EOF
fi
# Bridge configuration
if [ "$bridges_enabled" = "1" ]; then
cat >> "$TORRC" << EOF
# Bridge mode
UseBridges 1
EOF
# Add bridge lines from config
config_list_foreach bridges bridge_lines add_bridge_line
fi
# Exit node restrictions
if [ -n "$exit_nodes" ]; then
echo "ExitNodes $exit_nodes" >> "$TORRC"
fi
if [ -n "$exclude_exit_nodes" ]; then
echo "ExcludeExitNodes $exclude_exit_nodes" >> "$TORRC"
fi
if [ "$strict_nodes" = "1" ]; then
echo "StrictNodes 1" >> "$TORRC"
fi
# Hidden services
config_foreach add_hidden_service hidden_service
# GeoIP files
if [ -f /usr/share/tor/geoip ]; then
echo "GeoIPFile /usr/share/tor/geoip" >> "$TORRC"
fi
if [ -f /usr/share/tor/geoip6 ]; then
echo "GeoIPv6File /usr/share/tor/geoip6" >> "$TORRC"
fi
}
add_bridge_line() {
echo "Bridge $1" >> "$TORRC"
}
add_hidden_service() {
local cfg="$1"
local enabled name local_port virtual_port
config_get enabled "$cfg" enabled '0'
[ "$enabled" = "1" ] || return
config_get name "$cfg" name "hidden_$cfg"
config_get local_port "$cfg" local_port '80'
config_get virtual_port "$cfg" virtual_port '80'
local hs_dir="$TOR_DATA/hidden_service_$name"
mkdir -p "$hs_dir"
chmod 700 "$hs_dir"
cat >> "$TORRC" << EOF
# Hidden Service: $name
HiddenServiceDir $hs_dir
HiddenServicePort $virtual_port 127.0.0.1:$local_port
EOF
}
setup_iptables() {
local mode trans_port dns_port dns_over_tor kill_switch
config_load "$CONFIG"
config_get mode main mode 'transparent'
config_get kill_switch main kill_switch '1'
config_get dns_over_tor main dns_over_tor '1'
config_get trans_port trans port '9040'
config_get dns_port trans dns_port '9053'
# Get Tor user ID
local tor_uid=$(id -u tor 2>/dev/null || echo "tor")
# Clear existing Tor rules
iptables -t nat -F TOR_SHIELD 2>/dev/null
iptables -t nat -X TOR_SHIELD 2>/dev/null
iptables -t filter -F TOR_SHIELD 2>/dev/null
iptables -t filter -X TOR_SHIELD 2>/dev/null
[ "$mode" = "transparent" ] || return 0
# Create chains
iptables -t nat -N TOR_SHIELD
iptables -t filter -N TOR_SHIELD
# Exclude Tor traffic
iptables -t nat -A TOR_SHIELD -m owner --uid-owner $tor_uid -j RETURN
# Exclude local networks
config_list_foreach trans excluded_ips add_excluded_ip
# Redirect DNS if enabled
if [ "$dns_over_tor" = "1" ]; then
iptables -t nat -A TOR_SHIELD -p udp --dport 53 -j REDIRECT --to-ports $dns_port
iptables -t nat -A TOR_SHIELD -p tcp --dport 53 -j REDIRECT --to-ports $dns_port
fi
# Redirect TCP to transparent proxy
iptables -t nat -A TOR_SHIELD -p tcp -j REDIRECT --to-ports $trans_port
# Add to OUTPUT chain
iptables -t nat -A OUTPUT -j TOR_SHIELD
# Kill switch - block non-Tor traffic
if [ "$kill_switch" = "1" ]; then
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
iptables -t filter -A TOR_SHIELD -j REJECT
iptables -t filter -A OUTPUT -j TOR_SHIELD
fi
}
add_excluded_ip() {
iptables -t nat -A TOR_SHIELD -d "$1" -j RETURN
}
add_excluded_filter_ip() {
iptables -t filter -A TOR_SHIELD -d "$1" -j ACCEPT
}
remove_iptables() {
# Remove from OUTPUT chain
iptables -t nat -D OUTPUT -j TOR_SHIELD 2>/dev/null
iptables -t filter -D OUTPUT -j TOR_SHIELD 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
iptables -t filter -F TOR_SHIELD 2>/dev/null
iptables -t filter -X TOR_SHIELD 2>/dev/null
}
start_service() {
local enabled
config_load "$CONFIG"
config_get enabled main enabled '0'
[ "$enabled" = "1" ] || {
echo "Tor Shield is disabled. Enable with: uci set tor-shield.main.enabled=1"
return 0
}
# Generate torrc
generate_torrc
# Setup iptables rules
setup_iptables
# Start Tor via procd
procd_open_instance tor
procd_set_param command /usr/sbin/tor -f "$TORRC"
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param pidfile "$TOR_RUN/tor.pid"
procd_close_instance
}
stop_service() {
# Remove iptables rules
remove_iptables
# Kill tor process
if [ -f "$TOR_RUN/tor.pid" ]; then
kill $(cat "$TOR_RUN/tor.pid") 2>/dev/null
rm -f "$TOR_RUN/tor.pid"
fi
}
service_triggers() {
procd_add_reload_trigger "$CONFIG"
}
reload_service() {
stop
start
}
status() {
"$PROG" status
}

View File

@ -0,0 +1,526 @@
#!/bin/sh
# SecuBox Tor Shield - CLI management tool
# Copyright (C) 2025 CyberMind.fr
CONFIG="tor-shield"
TOR_CONTROL="/var/run/tor/control"
TOR_DATA="/var/lib/tor"
. /lib/functions.sh
usage() {
cat << EOF
Usage: torctl <command> [options]
Commands:
status Show Tor Shield status
enable [preset] Enable Tor Shield (presets: anonymous, selective, censored)
disable Disable Tor Shield
restart Restart Tor Shield
identity Get new Tor identity (new circuits)
circuits Show active circuits
exit-ip Show current exit IP address
leak-test Test for DNS/IP leaks
bridges Manage bridge configuration
hidden Manage hidden services
Options:
-h, --help Show this help
Examples:
torctl enable anonymous Enable with full anonymity preset
torctl status Show current status
torctl identity Request new circuits
torctl exit-ip Show Tor exit IP
EOF
}
# Send command to Tor control socket
tor_control() {
if [ ! -S "$TOR_CONTROL" ]; then
echo "Error: Tor control socket not available"
return 1
fi
echo -e "$1" | nc -U "$TOR_CONTROL" 2>/dev/null
}
# Get bootstrap percentage
get_bootstrap() {
local status=$(tor_control "GETINFO status/bootstrap-phase")
echo "$status" | grep "PROGRESS=" | sed 's/.*PROGRESS=\([0-9]*\).*/\1/'
}
# Check if Tor is running
is_running() {
pgrep tor >/dev/null 2>&1
}
# Get current exit IP
get_exit_ip() {
# Try multiple services to get external IP through Tor
local socks_port=$(uci -q get tor-shield.socks.port || echo "9050")
local ip=""
# Try check.torproject.org first
ip=$(curl -s --socks5-hostname 127.0.0.1:$socks_port https://check.torproject.org/api/ip 2>/dev/null | jsonfilter -e '@.IP' 2>/dev/null)
if [ -z "$ip" ]; then
# Fallback to ipinfo.io
ip=$(curl -s --socks5-hostname 127.0.0.1:$socks_port https://ipinfo.io/ip 2>/dev/null)
fi
echo "${ip:-unknown}"
}
# Get real IP (without Tor)
get_real_ip() {
local ip=$(curl -s --max-time 5 https://ipinfo.io/ip 2>/dev/null)
echo "${ip:-unknown}"
}
# Status command
cmd_status() {
local enabled mode bootstrap exit_ip circuit_count
config_load "$CONFIG"
config_get enabled main enabled '0'
config_get mode main mode 'transparent'
echo "Tor Shield Status"
echo "================="
if [ "$enabled" != "1" ]; then
echo "Status: DISABLED"
echo ""
echo "Enable with: torctl enable"
return 0
fi
if ! is_running; then
echo "Status: ENABLED but NOT RUNNING"
echo ""
echo "Start with: /etc/init.d/tor-shield start"
return 1
fi
bootstrap=$(get_bootstrap)
bootstrap=${bootstrap:-0}
echo "Status: ACTIVE"
echo "Mode: $mode"
echo "Bootstrap: ${bootstrap}%"
if [ "$bootstrap" -ge 100 ]; then
exit_ip=$(get_exit_ip)
echo "Exit IP: $exit_ip"
# Get circuit count
circuit_count=$(tor_control "GETINFO circuit-status" | grep -c "BUILT")
echo "Circuits: ${circuit_count:-0}"
else
echo "Exit IP: (bootstrapping...)"
fi
# Show config details
local dns_over_tor kill_switch
config_get dns_over_tor main dns_over_tor '1'
config_get kill_switch main kill_switch '1'
echo ""
echo "Configuration:"
echo " DNS over Tor: $([ "$dns_over_tor" = "1" ] && echo "Yes" || echo "No")"
echo " Kill Switch: $([ "$kill_switch" = "1" ] && echo "Yes" || echo "No")"
# Bridge status
local bridges_enabled
config_get bridges_enabled bridges enabled '0'
if [ "$bridges_enabled" = "1" ]; then
local bridge_type
config_get bridge_type bridges type 'obfs4'
echo " Bridges: $bridge_type"
fi
}
# Enable command
cmd_enable() {
local preset="${1:-anonymous}"
echo "Enabling Tor Shield with preset: $preset"
# Load preset configuration
config_load "$CONFIG"
local preset_mode preset_dns preset_kill preset_bridges
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'
# 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"
if [ "$preset_bridges" = "1" ]; then
uci set tor-shield.bridges.enabled='1'
fi
uci commit tor-shield
# Start service
/etc/init.d/tor-shield restart
echo "Tor Shield enabled. Waiting for bootstrap..."
# Wait for bootstrap
local count=0
while [ $count -lt 60 ]; do
if is_running; then
local progress=$(get_bootstrap)
if [ -n "$progress" ] && [ "$progress" -ge 100 ]; then
echo "Bootstrap complete!"
cmd_status
return 0
fi
echo "Bootstrap: ${progress:-0}%"
fi
sleep 2
count=$((count + 1))
done
echo "Warning: Bootstrap taking longer than expected"
cmd_status
}
# Disable command
cmd_disable() {
echo "Disabling Tor Shield..."
uci set tor-shield.main.enabled='0'
uci commit tor-shield
/etc/init.d/tor-shield stop
echo "Tor Shield disabled."
}
# Restart command
cmd_restart() {
echo "Restarting Tor Shield..."
/etc/init.d/tor-shield restart
sleep 2
cmd_status
}
# New identity command
cmd_identity() {
if ! is_running; then
echo "Error: Tor is not running"
return 1
fi
echo "Requesting new identity..."
# Send NEWNYM signal
local result=$(tor_control "SIGNAL NEWNYM")
if echo "$result" | grep -q "250 OK"; then
echo "New identity requested successfully."
echo "New circuits will be established shortly."
sleep 3
echo ""
echo "New exit IP: $(get_exit_ip)"
else
echo "Failed to request new identity"
return 1
fi
}
# Circuits command
cmd_circuits() {
if ! is_running; then
echo "Error: Tor is not running"
return 1
fi
echo "Active Circuits"
echo "==============="
local circuits=$(tor_control "GETINFO circuit-status")
echo "$circuits" | grep "BUILT" | while read line; do
# Parse circuit info
local id=$(echo "$line" | awk '{print $1}')
local status=$(echo "$line" | awk '{print $2}')
local path=$(echo "$line" | awk '{print $3}')
if [ -n "$path" ]; then
# Extract node names/fingerprints
echo "Circuit $id: $path"
fi
done
# Get circuit count
local count=$(echo "$circuits" | grep -c "BUILT")
echo ""
echo "Total built circuits: ${count:-0}"
}
# Exit IP command
cmd_exit_ip() {
if ! is_running; then
echo "Error: Tor is not running"
return 1
fi
local bootstrap=$(get_bootstrap)
if [ -z "$bootstrap" ] || [ "$bootstrap" -lt 100 ]; then
echo "Tor is still bootstrapping (${bootstrap:-0}%)"
return 1
fi
local exit_ip=$(get_exit_ip)
local real_ip=$(get_real_ip)
echo "Real IP: $real_ip"
echo "Tor Exit IP: $exit_ip"
if [ "$exit_ip" != "$real_ip" ] && [ "$exit_ip" != "unknown" ]; then
echo "Status: PROTECTED"
else
echo "Status: WARNING - IPs match or unknown"
fi
}
# Leak test command
cmd_leak_test() {
if ! is_running; then
echo "Error: Tor is not running"
return 1
fi
echo "Running leak test..."
echo ""
local socks_port=$(uci -q get tor-shield.socks.port || echo "9050")
local leaks=0
# Test 1: IP leak
echo "1. IP Leak Test"
local tor_ip=$(get_exit_ip)
local real_ip=$(get_real_ip)
if [ "$tor_ip" = "$real_ip" ] || [ "$tor_ip" = "unknown" ]; then
echo " FAIL: IP may be leaking"
leaks=$((leaks + 1))
else
echo " PASS: Tor IP ($tor_ip) differs from real IP ($real_ip)"
fi
# Test 2: DNS leak (via Tor check)
echo ""
echo "2. Tor Detection Test"
local tor_check=$(curl -s --socks5-hostname 127.0.0.1:$socks_port https://check.torproject.org/api/ip 2>/dev/null)
local is_tor=$(echo "$tor_check" | jsonfilter -e '@.IsTor' 2>/dev/null)
if [ "$is_tor" = "true" ]; then
echo " PASS: Traffic confirmed going through Tor"
else
echo " FAIL: Traffic may not be going through Tor"
leaks=$((leaks + 1))
fi
# Test 3: DNS resolution through Tor
echo ""
echo "3. DNS Over Tor Test"
local dns_over_tor=$(uci -q get tor-shield.main.dns_over_tor)
if [ "$dns_over_tor" = "1" ]; then
# Try resolving a .onion address (only works through Tor)
local dns_test=$(curl -s --socks5-hostname 127.0.0.1:$socks_port http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/ 2>/dev/null | head -c 100)
if [ -n "$dns_test" ]; then
echo " PASS: DNS resolution working through Tor"
else
echo " WARNING: Could not verify DNS over Tor"
fi
else
echo " SKIP: DNS over Tor is disabled"
fi
echo ""
echo "===================="
if [ $leaks -eq 0 ]; then
echo "Result: ALL TESTS PASSED"
else
echo "Result: $leaks POTENTIAL LEAK(S) DETECTED"
fi
}
# Bridges command
cmd_bridges() {
local action="${1:-status}"
case "$action" in
status)
local enabled type
config_load "$CONFIG"
config_get enabled bridges enabled '0'
config_get type bridges type 'obfs4'
echo "Bridge Configuration"
echo "===================="
echo "Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")"
echo "Type: $type"
echo ""
echo "Bridge lines:"
config_list_foreach bridges bridge_lines echo_bridge
;;
enable)
uci set tor-shield.bridges.enabled='1'
uci commit tor-shield
echo "Bridges enabled. Restart Tor Shield to apply."
;;
disable)
uci set tor-shield.bridges.enabled='0'
uci commit tor-shield
echo "Bridges disabled. Restart Tor Shield to apply."
;;
add)
shift
local bridge_line="$*"
if [ -n "$bridge_line" ]; then
uci add_list tor-shield.bridges.bridge_lines="$bridge_line"
uci commit tor-shield
echo "Bridge added. Restart Tor Shield to apply."
else
echo "Usage: torctl bridges add <bridge_line>"
fi
;;
*)
echo "Usage: torctl bridges [status|enable|disable|add <line>]"
;;
esac
}
echo_bridge() {
echo " $1"
}
# Hidden services command
cmd_hidden() {
local action="${1:-list}"
case "$action" in
list)
echo "Hidden Services"
echo "==============="
config_load "$CONFIG"
config_foreach list_hidden_service hidden_service
;;
add)
shift
local name="$1"
local local_port="${2:-80}"
local virtual_port="${3:-80}"
if [ -z "$name" ]; then
echo "Usage: torctl hidden add <name> [local_port] [virtual_port]"
return 1
fi
# Create new hidden service config
uci set tor-shield.hs_$name=hidden_service
uci set tor-shield.hs_$name.name="$name"
uci set tor-shield.hs_$name.enabled='1'
uci set tor-shield.hs_$name.local_port="$local_port"
uci set tor-shield.hs_$name.virtual_port="$virtual_port"
uci commit tor-shield
echo "Hidden service '$name' created."
echo "Restart Tor Shield to generate .onion address."
;;
remove)
local name="$2"
if [ -z "$name" ]; then
echo "Usage: torctl hidden remove <name>"
return 1
fi
uci delete tor-shield.hs_$name 2>/dev/null
uci commit tor-shield
echo "Hidden service '$name' removed."
;;
*)
echo "Usage: torctl hidden [list|add|remove]"
;;
esac
}
list_hidden_service() {
local cfg="$1"
local enabled name local_port virtual_port
config_get enabled "$cfg" enabled '0'
config_get name "$cfg" name "$cfg"
config_get local_port "$cfg" local_port '80'
config_get virtual_port "$cfg" virtual_port '80'
local status="disabled"
[ "$enabled" = "1" ] && status="enabled"
local hostname_file="$TOR_DATA/hidden_service_$name/hostname"
local onion_addr="(not generated)"
if [ -f "$hostname_file" ]; then
onion_addr=$(cat "$hostname_file")
fi
echo ""
echo "Service: $name ($status)"
echo " Address: $onion_addr"
echo " Port: $virtual_port -> 127.0.0.1:$local_port"
}
# Main dispatcher
case "$1" in
status)
cmd_status
;;
enable)
shift
cmd_enable "$@"
;;
disable)
cmd_disable
;;
restart)
cmd_restart
;;
identity|new-identity|newnym)
cmd_identity
;;
circuits|circuit)
cmd_circuits
;;
exit-ip|ip)
cmd_exit_ip
;;
leak-test|leaks|test)
cmd_leak_test
;;
bridges|bridge)
shift
cmd_bridges "$@"
;;
hidden|hs|onion)
shift
cmd_hidden "$@"
;;
-h|--help|help)
usage
;;
*)
usage
exit 1
;;
esac