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:
parent
4d08f99222
commit
23dac58741
36
package/secubox/luci-app-tor-shield/Makefile
Normal file
36
package/secubox/luci-app-tor-shield/Makefile
Normal 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
|
||||
@ -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 '🛡';
|
||||
case 'target': return '🎯';
|
||||
case 'unlock': return '🔓';
|
||||
default: return '🛡';
|
||||
}
|
||||
}
|
||||
|
||||
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] || {}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
});
|
||||
@ -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
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
package/secubox/secubox-app-tor/Makefile
Normal file
78
package/secubox/secubox-app-tor/Makefile
Normal 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))
|
||||
51
package/secubox/secubox-app-tor/files/etc/config/tor-shield
Normal file
51
package/secubox/secubox-app-tor/files/etc/config/tor-shield
Normal 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'
|
||||
265
package/secubox/secubox-app-tor/files/etc/init.d/tor-shield
Normal file
265
package/secubox/secubox-app-tor/files/etc/init.d/tor-shield
Normal 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
|
||||
}
|
||||
526
package/secubox/secubox-app-tor/files/usr/sbin/torctl
Normal file
526
package/secubox/secubox-app-tor/files/usr/sbin/torctl
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user