fix: Tor management system, bug fixes, federation name sync
Major changes: - Full Tor hidden service management via systemd path unit pattern (tor-helper.sh + archipelago-tor-helper.path/service) — respects NoNewPrivileges=yes, no sudo needed from backend - Container doctor: prefer system Tor over container, remove archy-tor - Deploy script: fix torrc generation (read correct services.json path), web apps map port 80→local port, enable both tor and tor@default - Federation: server rename pushes name to peers via background sync - Server name: fix root-owned file, optimistic store update - Mesh: local echo for sent messages, sendingArch loading state - Web5: Message button → Mesh redirect, node name lookup in messages - PeerFiles: show DID not onion in header - Connected Nodes: flex-1 instead of fixed max-h - Toast notifications route to Mesh - Deploy script: fix single-quote syntax in SSH block Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f7872e2914
commit
cffcc9f665
+220
-25
@@ -275,7 +275,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6 mb-6">
|
||||
<div class="grid grid-cols-1 2xl:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Network Interfaces -->
|
||||
<div class="glass-card p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@@ -326,8 +326,8 @@
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="relative shrink-0">
|
||||
<div class="w-3 h-3 rounded-full" :class="torRunning ? 'bg-green-400' : 'bg-red-400'"></div>
|
||||
<div v-if="torRunning" class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
|
||||
<div class="w-3 h-3 rounded-full" :class="torDaemonRunning ? 'bg-green-400' : 'bg-red-400'"></div>
|
||||
<div v-if="torDaemonRunning" class="absolute inset-0 w-3 h-3 rounded-full bg-green-400 animate-ping opacity-75"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-white/96">Tor Services</h2>
|
||||
@@ -335,17 +335,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="cleanupRotatedServices" :disabled="torCleaning" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
{{ torCleaning ? 'Cleaning...' : 'Cleanup Old' }}
|
||||
</button>
|
||||
<button @click="loadTorServices" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<button @click="restartTor" :disabled="torRestarting" class="glass-button px-3 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Refresh
|
||||
{{ torRestarting ? 'Restarting...' : 'Restart Tor' }}
|
||||
</button>
|
||||
<button @click="showAddServiceModal = true" class="glass-button px-3 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add Service
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -354,9 +354,16 @@
|
||||
<div v-else class="space-y-2">
|
||||
<div v-for="svc in torServices" :key="svc.name" class="bg-black/20 rounded-xl border border-white/10 p-3 flex items-center justify-between gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-white text-sm font-medium">{{ svc.name }}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-white text-sm font-medium">{{ svc.name }}</p>
|
||||
<span class="text-white/30 text-xs">:{{ svc.local_port }}</span>
|
||||
<span v-if="svc.protocol" class="text-xs px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400">protocol</span>
|
||||
<span v-else-if="!svc.unauthenticated" class="text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-400">auth</span>
|
||||
<span v-else class="text-xs px-1.5 py-0.5 rounded bg-amber-500/20 text-amber-400">open</span>
|
||||
</div>
|
||||
<p v-if="svc.onion_address" class="text-amber-300/80 text-xs font-mono truncate cursor-pointer" :title="svc.onion_address" @click="copyTorAddress(svc.onion_address)">{{ svc.onion_address }}</p>
|
||||
<p v-else class="text-white/30 text-xs">No .onion address</p>
|
||||
<p v-else-if="svc.enabled" class="text-white/30 text-xs">Waiting for .onion address...</p>
|
||||
<p v-else class="text-white/30 text-xs">Disabled</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
@@ -367,11 +374,116 @@
|
||||
>
|
||||
{{ torRotating === svc.name ? 'Rotating...' : 'Rotate' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="svc.name !== 'archipelago'"
|
||||
@click="deleteService(svc.name)"
|
||||
:disabled="torDeleting === svc.name"
|
||||
class="glass-button px-2 py-1.5 rounded-lg text-xs text-red-400 hover:text-red-300"
|
||||
:title="'Delete ' + svc.name + ' hidden service'"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<ToggleSwitch :model-value="svc.enabled" @update:model-value="toggleTorApp(svc.name, $event)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Tor Service Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showAddServiceModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-md" @click.self="showAddServiceModal = false" @keydown.escape="showAddServiceModal = false">
|
||||
<div class="glass-card p-6 max-w-md w-full">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Add Tor Hidden Service</h3>
|
||||
|
||||
<!-- Tabs: Installed Apps | Manual -->
|
||||
<div class="flex gap-1 mb-4 border-b border-white/10">
|
||||
<button
|
||||
@click="addServiceTab = 'apps'"
|
||||
class="px-4 py-2 text-sm font-medium rounded-t-lg transition-colors"
|
||||
:class="addServiceTab === 'apps' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white/80 hover:bg-white/5'"
|
||||
>
|
||||
Installed Apps
|
||||
</button>
|
||||
<button
|
||||
@click="addServiceTab = 'manual'"
|
||||
class="px-4 py-2 text-sm font-medium rounded-t-lg transition-colors"
|
||||
:class="addServiceTab === 'manual' ? 'bg-white/10 text-white' : 'text-white/60 hover:text-white/80 hover:bg-white/5'"
|
||||
>
|
||||
Manual
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Installed Apps tab -->
|
||||
<div v-if="addServiceTab === 'apps'">
|
||||
<p class="text-white/60 text-sm mb-3">Select an installed app to create a .onion address for it.</p>
|
||||
<div v-if="availableAppsForTor.length === 0" class="p-4 text-center text-white/40 text-sm">
|
||||
All installed apps already have Tor services.
|
||||
</div>
|
||||
<div v-else class="space-y-2 max-h-64 overflow-y-auto">
|
||||
<button
|
||||
v-for="app in availableAppsForTor"
|
||||
:key="app.id"
|
||||
@click="createServiceForApp(app.id)"
|
||||
:disabled="addingService"
|
||||
class="w-full flex items-center justify-between p-3 bg-white/5 rounded-lg hover:bg-white/10 transition-colors text-left"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium text-white">{{ app.title }}</p>
|
||||
<p class="text-xs text-white/40">{{ app.id }}</p>
|
||||
</div>
|
||||
<span class="text-xs text-orange-400 shrink-0">+ Add</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manual tab -->
|
||||
<div v-if="addServiceTab === 'manual'">
|
||||
<p class="text-white/60 text-sm mb-3">Create a .onion address for any local service.</p>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/80 mb-1">Service Name</label>
|
||||
<input
|
||||
v-model="newServiceName"
|
||||
class="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-white/20 focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
|
||||
placeholder="my-app"
|
||||
maxlength="64"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-white/80 mb-1">Local Port</label>
|
||||
<input
|
||||
v-model.number="newServicePort"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
class="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-white/20 focus:border-orange-500 focus:ring-1 focus:ring-orange-500"
|
||||
placeholder="8080"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button
|
||||
@click="createService"
|
||||
:disabled="!newServiceName.trim() || !newServicePort || addingService"
|
||||
class="flex-1 px-4 py-2 rounded-lg bg-orange-500 text-white font-medium hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{{ addingService ? 'Creating...' : 'Create Service' }}
|
||||
</button>
|
||||
<button
|
||||
@click="showAddServiceModal = false"
|
||||
class="px-4 py-2 rounded-lg bg-white/10 text-white font-medium hover:bg-white/20 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="addServiceError" class="mt-3 text-sm text-red-400">{{ addServiceError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
|
||||
<!-- WiFi Scan Modal -->
|
||||
@@ -511,8 +623,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import ToggleSwitch from '@/components/ToggleSwitch.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// Connected nodes
|
||||
const connectedNodes = ref(0)
|
||||
|
||||
@@ -797,28 +912,52 @@ interface TorServiceInfo {
|
||||
local_port: number
|
||||
onion_address: string | null
|
||||
enabled: boolean
|
||||
unauthenticated: boolean
|
||||
protocol: boolean
|
||||
}
|
||||
|
||||
const torServices = ref<TorServiceInfo[]>([])
|
||||
const torServicesLoading = ref(false)
|
||||
const torCleaning = ref(false)
|
||||
const torDaemonRunning = ref(false)
|
||||
const torRestarting = ref(false)
|
||||
const torRotating = ref<string | false>(false)
|
||||
const torDeleting = ref<string | false>(false)
|
||||
|
||||
const torRunning = computed(() => torServices.value.length > 0 && torServices.value.some(s => s.onion_address))
|
||||
// Add service modal
|
||||
const showAddServiceModal = ref(false)
|
||||
const addServiceTab = ref<'apps' | 'manual'>('apps')
|
||||
const newServiceName = ref('')
|
||||
const newServicePort = ref<number | null>(null)
|
||||
const addingService = ref(false)
|
||||
const addServiceError = ref('')
|
||||
|
||||
// Installed apps that don't already have a Tor service
|
||||
const availableAppsForTor = computed(() => {
|
||||
const existingNames = new Set(torServices.value.map(s => s.name))
|
||||
const pkgs = appStore.packages
|
||||
return Object.entries(pkgs)
|
||||
.filter(([id]) => !existingNames.has(id))
|
||||
.map(([id, pkg]) => ({
|
||||
id,
|
||||
title: (pkg as { manifest?: { title?: string } })?.manifest?.title || id,
|
||||
}))
|
||||
.sort((a, b) => a.title.localeCompare(b.title))
|
||||
})
|
||||
|
||||
async function loadTorServices() {
|
||||
torServicesLoading.value = true
|
||||
try {
|
||||
const res = await rpcClient.call<{ services: TorServiceInfo[] }>({ method: 'tor.list-services' })
|
||||
const res = await rpcClient.call<{ services: TorServiceInfo[]; tor_running: boolean }>({ method: 'tor.list-services' })
|
||||
torServices.value = res.services || []
|
||||
torDaemonRunning.value = res.tor_running ?? false
|
||||
} catch {
|
||||
torServices.value = []
|
||||
torDaemonRunning.value = false
|
||||
} finally {
|
||||
torServicesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const torRotating = ref<string | false>(false)
|
||||
|
||||
function copyTorAddress(address: string) {
|
||||
navigator.clipboard.writeText(address)
|
||||
logsToast.value = 'Onion address copied to clipboard'
|
||||
@@ -827,7 +966,7 @@ function copyTorAddress(address: string) {
|
||||
|
||||
async function toggleTorApp(appId: string, enabled: boolean) {
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.toggle-app', params: { app_id: appId, enabled } })
|
||||
await rpcClient.call({ method: 'tor.toggle-app', params: { app_id: appId, enabled }, timeout: 90000 })
|
||||
await loadTorServices()
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to toggle Tor app:', e)
|
||||
@@ -837,7 +976,7 @@ async function toggleTorApp(appId: string, enabled: boolean) {
|
||||
async function rotateService(name: string) {
|
||||
torRotating.value = name
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.rotate-service', params: { name } })
|
||||
await rpcClient.call({ method: 'tor.rotate-service', params: { name }, timeout: 90000 })
|
||||
await loadTorServices()
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to rotate Tor address:', e)
|
||||
@@ -846,15 +985,71 @@ async function rotateService(name: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupRotatedServices() {
|
||||
torCleaning.value = true
|
||||
async function restartTor() {
|
||||
torRestarting.value = true
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.cleanup-rotated' })
|
||||
await rpcClient.call({ method: 'tor.restart', timeout: 90000 })
|
||||
await loadTorServices()
|
||||
logsToast.value = 'Tor restarted successfully'
|
||||
setTimeout(() => { logsToast.value = '' }, 3000)
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to cleanup rotated Tor services:', e)
|
||||
logsToast.value = 'Failed to restart Tor'
|
||||
setTimeout(() => { logsToast.value = '' }, 5000)
|
||||
if (import.meta.env.DEV) console.warn('Failed to restart Tor:', e)
|
||||
} finally {
|
||||
torCleaning.value = false
|
||||
torRestarting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createServiceForApp(appId: string) {
|
||||
addServiceError.value = ''
|
||||
addingService.value = true
|
||||
try {
|
||||
// Backend knows the port from known_service_port() — pass 0 to let it auto-detect
|
||||
await rpcClient.call({ method: 'tor.create-service', params: { name: appId, local_port: 0 }, timeout: 90000 })
|
||||
showAddServiceModal.value = false
|
||||
await loadTorServices()
|
||||
logsToast.value = `Tor service for "${appId}" created`
|
||||
setTimeout(() => { logsToast.value = '' }, 3000)
|
||||
} catch (e) {
|
||||
addServiceError.value = e instanceof Error ? e.message : 'Failed to create service'
|
||||
} finally {
|
||||
addingService.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createService() {
|
||||
const name = newServiceName.value.trim()
|
||||
const port = newServicePort.value
|
||||
if (!name || !port) return
|
||||
addServiceError.value = ''
|
||||
addingService.value = true
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.create-service', params: { name, local_port: port }, timeout: 90000 })
|
||||
showAddServiceModal.value = false
|
||||
newServiceName.value = ''
|
||||
newServicePort.value = null
|
||||
await loadTorServices()
|
||||
logsToast.value = `Tor service "${name}" created`
|
||||
setTimeout(() => { logsToast.value = '' }, 3000)
|
||||
} catch (e) {
|
||||
addServiceError.value = e instanceof Error ? e.message : 'Failed to create service'
|
||||
} finally {
|
||||
addingService.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteService(name: string) {
|
||||
torDeleting.value = name
|
||||
try {
|
||||
await rpcClient.call({ method: 'tor.delete-service', params: { name }, timeout: 90000 })
|
||||
await loadTorServices()
|
||||
logsToast.value = `Tor service "${name}" deleted`
|
||||
setTimeout(() => { logsToast.value = '' }, 3000)
|
||||
} catch (e) {
|
||||
if (import.meta.env.DEV) console.warn('Failed to delete Tor service:', e)
|
||||
} finally {
|
||||
torDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user