From 23dac587414a3bf78aee510837ff89f3102f7ee5 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 23 Jan 2026 17:15:39 +0100 Subject: [PATCH] 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 --- package/secubox/luci-app-tor-shield/Makefile | 36 + .../luci-static/resources/tor-shield/api.js | 213 ++++++ .../resources/tor-shield/dashboard.css | 625 ++++++++++++++++ .../resources/view/tor-shield/bridges.js | 210 ++++++ .../resources/view/tor-shield/circuits.js | 212 ++++++ .../view/tor-shield/hidden-services.js | 246 ++++++ .../resources/view/tor-shield/overview.js | 453 +++++++++++ .../resources/view/tor-shield/settings.js | 255 +++++++ .../root/usr/libexec/rpcd/luci.tor-shield | 707 ++++++++++++++++++ .../luci/menu.d/luci-app-tor-shield.json | 52 ++ .../share/rpcd/acl.d/luci-app-tor-shield.json | 46 ++ package/secubox/secubox-app-tor/Makefile | 78 ++ .../files/etc/config/tor-shield | 51 ++ .../files/etc/init.d/tor-shield | 265 +++++++ .../secubox-app-tor/files/usr/sbin/torctl | 526 +++++++++++++ 15 files changed, 3975 insertions(+) create mode 100644 package/secubox/luci-app-tor-shield/Makefile create mode 100644 package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js create mode 100644 package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/dashboard.css create mode 100644 package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/bridges.js create mode 100644 package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/circuits.js create mode 100644 package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/hidden-services.js create mode 100644 package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js create mode 100644 package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/settings.js create mode 100644 package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield create mode 100644 package/secubox/luci-app-tor-shield/root/usr/share/luci/menu.d/luci-app-tor-shield.json create mode 100644 package/secubox/luci-app-tor-shield/root/usr/share/rpcd/acl.d/luci-app-tor-shield.json create mode 100644 package/secubox/secubox-app-tor/Makefile create mode 100644 package/secubox/secubox-app-tor/files/etc/config/tor-shield create mode 100644 package/secubox/secubox-app-tor/files/etc/init.d/tor-shield create mode 100644 package/secubox/secubox-app-tor/files/usr/sbin/torctl diff --git a/package/secubox/luci-app-tor-shield/Makefile b/package/secubox/luci-app-tor-shield/Makefile new file mode 100644 index 00000000..e3a99212 --- /dev/null +++ b/package/secubox/luci-app-tor-shield/Makefile @@ -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 + +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 diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js new file mode 100644 index 00000000..be2fe776 --- /dev/null +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/api.js @@ -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] || {} + }; + }); + } +}); diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/dashboard.css b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/dashboard.css new file mode 100644 index 00000000..c933c460 --- /dev/null +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/tor-shield/dashboard.css @@ -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; + } +} diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/bridges.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/bridges.js new file mode 100644 index 00000000..c231ba48 --- /dev/null +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/bridges.js @@ -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 +}); diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/circuits.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/circuits.js new file mode 100644 index 00000000..f23ad3e2 --- /dev/null +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/circuits.js @@ -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 +}); diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/hidden-services.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/hidden-services.js new file mode 100644 index 00000000..27dcb654 --- /dev/null +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/hidden-services.js @@ -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 +}); diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js new file mode 100644 index 00000000..b4a2937e --- /dev/null +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js @@ -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 = '' + _('Disabled'); + } else if (status.bootstrap < 100) { + badge.classList.add('connecting'); + badge.innerHTML = '' + _('Connecting %d%%').format(status.bootstrap); + } else if (status.is_tor) { + badge.classList.add('protected'); + badge.innerHTML = '' + _('Protected'); + } else { + badge.classList.add('exposed'); + badge.innerHTML = '' + _('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 +}); diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/settings.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/settings.js new file mode 100644 index 00000000..a0d1970e --- /dev/null +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/settings.js @@ -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 +}); diff --git a/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield b/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield new file mode 100644 index 00000000..2960ae7d --- /dev/null +++ b/package/secubox/luci-app-tor-shield/root/usr/libexec/rpcd/luci.tor-shield @@ -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 diff --git a/package/secubox/luci-app-tor-shield/root/usr/share/luci/menu.d/luci-app-tor-shield.json b/package/secubox/luci-app-tor-shield/root/usr/share/luci/menu.d/luci-app-tor-shield.json new file mode 100644 index 00000000..b8722430 --- /dev/null +++ b/package/secubox/luci-app-tor-shield/root/usr/share/luci/menu.d/luci-app-tor-shield.json @@ -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" + } + } +} diff --git a/package/secubox/luci-app-tor-shield/root/usr/share/rpcd/acl.d/luci-app-tor-shield.json b/package/secubox/luci-app-tor-shield/root/usr/share/rpcd/acl.d/luci-app-tor-shield.json new file mode 100644 index 00000000..e166b030 --- /dev/null +++ b/package/secubox/luci-app-tor-shield/root/usr/share/rpcd/acl.d/luci-app-tor-shield.json @@ -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" ] + } + } + } +} diff --git a/package/secubox/secubox-app-tor/Makefile b/package/secubox/secubox-app-tor/Makefile new file mode 100644 index 00000000..96e437ad --- /dev/null +++ b/package/secubox/secubox-app-tor/Makefile @@ -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 +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)) diff --git a/package/secubox/secubox-app-tor/files/etc/config/tor-shield b/package/secubox/secubox-app-tor/files/etc/config/tor-shield new file mode 100644 index 00000000..5ab2cc99 --- /dev/null +++ b/package/secubox/secubox-app-tor/files/etc/config/tor-shield @@ -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' diff --git a/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield b/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield new file mode 100644 index 00000000..48b1a99c --- /dev/null +++ b/package/secubox/secubox-app-tor/files/etc/init.d/tor-shield @@ -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 +} diff --git a/package/secubox/secubox-app-tor/files/usr/sbin/torctl b/package/secubox/secubox-app-tor/files/usr/sbin/torctl new file mode 100644 index 00000000..603d701c --- /dev/null +++ b/package/secubox/secubox-app-tor/files/usr/sbin/torctl @@ -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 [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 " + fi + ;; + *) + echo "Usage: torctl bridges [status|enable|disable|add ]" + ;; + 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 [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 " + 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