mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
fix(tor): seed hasEverBootstrapped from on-disk guard sample
A fresh process reset TorManager.hasEverBootstrapped to false, so the stuck-Connecting self-heal watchdog used the gentle reset() (drop client, keep state) instead of resetWithCleanState() (wipe state). When guards.json carried guards poisoned by TooManyIndeterminateFailures from a prior session, every retry reloaded the same poisoned guards and Tor stayed stuck in Connecting forever — never wiping the one thing blocking it. Seed hasEverBootstrapped at startup from durable on-disk evidence: Arti only writes confirmed_at on a guard after it has built real circuits, so a confirmed guard proves Tor bootstrapped successfully on this install before, even across the restarts that clear the in-memory flag. With it set, a stuck bootstrap correctly wipes the stale/poisoned state and rebuilds a fresh guard sample. - ArtiGuardState: pure, file/JNI-free parsers over guards.json (hasConfirmedGuard + hasNoUsableGuards extracted from TorService). - TorService.hasBootstrappedBefore() reads the file off-thread. - TorBackend gains the suspend method; TorManager seeds in init. - Tests cover the parser against a real captured poisoned-but-confirmed guards.json fixture, plus a watchdog test for the wipe-on-first-stuck path. Verified on an emulator stuck in Connecting from real poisoned guards: self-heal wiped state and Tor reached Active in ~5s (clean 0-disabled guard sample). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
24e9cf2aaa
commit
2f602a29bc
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.tor
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
|
||||
/**
|
||||
* Pure, file- and JNI-free parsers over Arti's persisted guard sample
|
||||
* (`<filesDir>/arti/state/state/guards.json`).
|
||||
*
|
||||
* Both recovery heuristics in the Tor stack read the same on-disk file, so the
|
||||
* decisions live here where they can be unit-tested against captured fixtures
|
||||
* (see `ArtiGuardStateTest`). [TorService] does the file IO and delegates the
|
||||
* parsing; [TorManager] consumes [hasConfirmedGuard] to seed its
|
||||
* `hasEverBootstrapped` flag across process restarts.
|
||||
*
|
||||
* The file shape is a map of guard-set selection name (e.g. `"default"`) to an
|
||||
* object with a `"guards"` array. Each guard entry carries:
|
||||
* - `disabled`: non-null once Arti permanently retires the guard (e.g.
|
||||
* `TooManyIndeterminateFailures` on a flaky network).
|
||||
* - `unlisted_since`: non-null once the guard drops out of the consensus.
|
||||
* - `confirmed_at`: non-null once the guard has actually been used to build a
|
||||
* circuit — i.e. a real bootstrap reached the guard-confirmation stage.
|
||||
*/
|
||||
object ArtiGuardState {
|
||||
private val mapper = jacksonObjectMapper()
|
||||
|
||||
/** Convenience for tests/callers holding the raw file text. */
|
||||
fun parse(json: String): JsonNode = mapper.readTree(json)
|
||||
|
||||
/**
|
||||
* True when at least one non-empty guard selection has *zero* usable guards —
|
||||
* every guard permanently `disabled` or dropped from the consensus
|
||||
* (`unlisted_since`). This is the AllGuardsDown wedge: Arti can neither build
|
||||
* circuits nor replenish the sample (it's full of unusable entries), so it
|
||||
* stays broken across restarts until the on-disk state is wiped. A single
|
||||
* usable guard is enough to recover, so this only trips on a total wipeout.
|
||||
*/
|
||||
fun hasNoUsableGuards(root: JsonNode): Boolean {
|
||||
var wedged = false
|
||||
root.forEach { selection ->
|
||||
val guards = selection.get("guards") ?: return@forEach
|
||||
if (guards.isArray && guards.size() > 0) {
|
||||
val usable = guards.count { !it.isDisabled() && !it.isUnlisted() }
|
||||
if (usable == 0) wedged = true
|
||||
}
|
||||
}
|
||||
return wedged
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the sample contains a guard Arti has *confirmed* (non-null
|
||||
* `confirmed_at`), even if that guard is now disabled or unlisted.
|
||||
*
|
||||
* Confirmation only happens after a guard has successfully built circuits, so
|
||||
* its presence on disk is durable proof that a real bootstrap completed at
|
||||
* least once on this install — surviving the process restarts that reset
|
||||
* [TorManager]'s in-memory `hasEverBootstrapped` flag to false. This lets the
|
||||
* stuck-Connecting watchdog treat persisted state as stale/poisoned (wipe it)
|
||||
* rather than as a pristine slow first bootstrap (leave it alone).
|
||||
*/
|
||||
fun hasConfirmedGuard(root: JsonNode): Boolean {
|
||||
var confirmed = false
|
||||
root.forEach { selection ->
|
||||
val guards = selection.get("guards") ?: return@forEach
|
||||
if (guards.isArray) {
|
||||
guards.forEach { guard ->
|
||||
val at = guard.get("confirmed_at")
|
||||
if (at != null && !at.isNull) confirmed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return confirmed
|
||||
}
|
||||
|
||||
private fun JsonNode.isDisabled(): Boolean {
|
||||
val d = get("disabled")
|
||||
return d != null && !d.isNull
|
||||
}
|
||||
|
||||
private fun JsonNode.isUnlisted(): Boolean {
|
||||
val u = get("unlisted_since")
|
||||
return u != null && !u.isNull
|
||||
}
|
||||
}
|
||||
@@ -37,4 +37,13 @@ interface TorBackend {
|
||||
suspend fun reset()
|
||||
|
||||
suspend fun resetWithCleanState()
|
||||
|
||||
/**
|
||||
* True when on-disk state proves Tor bootstrapped successfully on this
|
||||
* install before. Lets [TorManager] seed `hasEverBootstrapped` across process
|
||||
* restarts so the stuck-Connecting watchdog wipes stale/poisoned state rather
|
||||
* than waiting it out as a first bootstrap. Implementations do file IO, so
|
||||
* this is a `suspend` call.
|
||||
*/
|
||||
suspend fun hasBootstrappedBefore(): Boolean
|
||||
}
|
||||
|
||||
@@ -93,10 +93,27 @@ class TorManager(
|
||||
* bootstrap there is no stale state to wipe, and wiping just forces an unnecessary
|
||||
* re-bootstrap cycle. Once we've seen Tor work once, persisted `arti/state/` is fair game
|
||||
* for the recovery to wipe.
|
||||
*
|
||||
* Also seeded at startup from [TorBackend.hasBootstrappedBefore]: the in-memory flag resets
|
||||
* every process, but Arti's persisted guard sample does not. If it already holds a confirmed
|
||||
* guard, Tor bootstrapped here before, so a stuck Connecting span means the persisted state
|
||||
* is stale/poisoned and the watchdog should wipe it. Without this seed a fresh process would
|
||||
* mistake poisoned guards for a pristine first bootstrap and only ever gentle-reset (keeping
|
||||
* the poison), looping forever — the exact "can't connect to Tor across restarts" failure.
|
||||
*/
|
||||
@Volatile private var hasEverBootstrapped: Boolean = false
|
||||
|
||||
init {
|
||||
// Seed hasEverBootstrapped from persisted on-disk evidence before the watchdog can fire
|
||||
// (well under SELF_HEAL_AFTER_MS), so a stuck bootstrap on a previously-working install
|
||||
// wipes its stale guard state instead of nursing it.
|
||||
scope.launch(ioDispatcher) {
|
||||
if (service.hasBootstrappedBefore()) {
|
||||
hasEverBootstrapped = true
|
||||
Log.d("TorManager") { "Seeded hasEverBootstrapped from persisted confirmed guard" }
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(ioDispatcher) {
|
||||
lastBypassApprovalMs = torPrefs.loadLastBypassApprovalMs()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.tor
|
||||
|
||||
import android.content.Context
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -99,9 +100,21 @@ class TorService(
|
||||
*/
|
||||
private fun guardsFile() = File(File(File(artiDataDir(), "state"), "state"), "guards.json")
|
||||
|
||||
/** Reads and parses [guardsFile], or null if it is absent/unreadable. */
|
||||
private fun readGuardsTree(): JsonNode? {
|
||||
val file = guardsFile()
|
||||
if (!file.exists()) return null
|
||||
return try {
|
||||
jacksonObjectMapper().readTree(file)
|
||||
} catch (e: Exception) {
|
||||
Log.w("TorService") { "Could not inspect guards.json: ${e.message}" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the wedged-guard-sample state behind the long-standing "can't
|
||||
* connect to Tor" bug.
|
||||
* connect to Tor" bug (see [ArtiGuardState.hasNoUsableGuards]).
|
||||
*
|
||||
* On a flaky network, Arti records circuit failures past the first hop as
|
||||
* "indeterminate" (it can't tell whether the guard or a later hop was at
|
||||
@@ -116,43 +129,20 @@ class TorService(
|
||||
* `AllGuardsDown`. The state persists in `guards.json`, and bootstrap still
|
||||
* "succeeds" (it reads cached directory data), so none of the init-failure
|
||||
* self-heal paths ever fire and Tor is stuck across restarts.
|
||||
*
|
||||
* A single usable guard is enough to keep building circuits, so we only
|
||||
* recover at the last resort: when a non-empty guard set has *zero* usable
|
||||
* guards. A guard is unusable on disk if it has been permanently
|
||||
* `disabled` or dropped from the consensus (`unlisted_since` set);
|
||||
* reachability is in-memory only and not persisted, so it can't be checked
|
||||
* here. Returns true when at least one non-empty selection has no usable
|
||||
* guard left.
|
||||
*/
|
||||
private fun noUsableGuards(): Boolean {
|
||||
val file = guardsFile()
|
||||
if (!file.exists()) return false
|
||||
private fun noUsableGuards(): Boolean = readGuardsTree()?.let { ArtiGuardState.hasNoUsableGuards(it) } ?: false
|
||||
|
||||
return try {
|
||||
val root = jacksonObjectMapper().readTree(file)
|
||||
var wedged = false
|
||||
// Each top-level field is a guard-set selection (e.g. "default").
|
||||
root.forEach { selection ->
|
||||
val guards = selection.get("guards") ?: return@forEach
|
||||
if (guards.isArray && guards.size() > 0) {
|
||||
val usable =
|
||||
guards.count { guard ->
|
||||
val disabled = guard.get("disabled")
|
||||
val unlisted = guard.get("unlisted_since")
|
||||
val isDisabled = disabled != null && !disabled.isNull
|
||||
val isUnlisted = unlisted != null && !unlisted.isNull
|
||||
!isDisabled && !isUnlisted
|
||||
}
|
||||
if (usable == 0) wedged = true
|
||||
}
|
||||
}
|
||||
wedged
|
||||
} catch (e: Exception) {
|
||||
Log.w("TorService") { "Could not inspect guards.json: ${e.message}" }
|
||||
false
|
||||
/**
|
||||
* True when the persisted guard sample proves Tor bootstrapped successfully
|
||||
* on this install before (a confirmed guard on disk; see
|
||||
* [ArtiGuardState.hasConfirmedGuard]). [TorManager] seeds its in-memory
|
||||
* `hasEverBootstrapped` from this so a stuck bootstrap on a fresh process
|
||||
* wipes stale/poisoned state instead of nursing it as a first bootstrap.
|
||||
*/
|
||||
override suspend fun hasBootstrappedBefore(): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
readGuardsTree()?.let { ArtiGuardState.hasConfirmedGuard(it) } ?: false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all Arti persistent data (state + cache). Used as a last resort
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.tor
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ArtiGuardStateTest {
|
||||
private fun load(resource: String): String =
|
||||
javaClass.getResourceAsStream(resource)?.bufferedReader()?.use { it.readText() }
|
||||
?: error("Missing test resource: $resource")
|
||||
|
||||
/**
|
||||
* Real `guards.json` captured from an emulator stuck in "Connecting": two of the four
|
||||
* confirmed primary guards were poisoned (`TooManyIndeterminateFailures`), but usable
|
||||
* guards remained. This is the case that exposed the bug — full AllGuardsDown never trips,
|
||||
* so only the [ArtiGuardState.hasConfirmedGuard] seed recovers it.
|
||||
*/
|
||||
@Test
|
||||
fun `real device sample - confirmed guard present, not fully wedged`() {
|
||||
val root = ArtiGuardState.parse(load("/tor/guards-confirmed-with-poisoned.json"))
|
||||
|
||||
assertTrue(
|
||||
"device sample has confirmed guards → prior bootstrap proven",
|
||||
ArtiGuardState.hasConfirmedGuard(root),
|
||||
)
|
||||
assertFalse(
|
||||
"2 disabled of 22 guards is not a total wipeout → not wedged by the AllGuardsDown check",
|
||||
ArtiGuardState.hasNoUsableGuards(root),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirmed guard counts even when disabled or unlisted`() {
|
||||
val root =
|
||||
ArtiGuardState.parse(
|
||||
"""
|
||||
{ "default": { "guards": [
|
||||
{ "confirmed_at": "2026-06-17T01:36:40Z",
|
||||
"disabled": { "type": "TooManyIndeterminateFailures" } },
|
||||
{ "confirmed_at": null, "disabled": null, "unlisted_since": null }
|
||||
] } }
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
assertTrue(ArtiGuardState.hasConfirmedGuard(root))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fresh sample with no confirmed guard - first bootstrap, nothing to wipe`() {
|
||||
val root =
|
||||
ArtiGuardState.parse(
|
||||
"""
|
||||
{ "default": { "guards": [
|
||||
{ "confirmed_at": null, "disabled": null, "unlisted_since": null },
|
||||
{ "confirmed_at": null, "disabled": null, "unlisted_since": null }
|
||||
] } }
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
assertFalse(ArtiGuardState.hasConfirmedGuard(root))
|
||||
assertFalse(ArtiGuardState.hasNoUsableGuards(root))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all guards disabled or unlisted - AllGuardsDown wedge`() {
|
||||
val root =
|
||||
ArtiGuardState.parse(
|
||||
"""
|
||||
{ "default": { "guards": [
|
||||
{ "confirmed_at": "2026-06-10T15:52:00Z",
|
||||
"disabled": { "type": "TooManyIndeterminateFailures" } },
|
||||
{ "confirmed_at": "2026-06-11T15:52:00Z",
|
||||
"disabled": null, "unlisted_since": "2026-06-12T00:00:00Z" }
|
||||
] } }
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
assertTrue("every guard unusable → wedged", ArtiGuardState.hasNoUsableGuards(root))
|
||||
// Even when fully wedged, prior confirmation is still proven.
|
||||
assertTrue(ArtiGuardState.hasConfirmedGuard(root))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty selection is neither wedged nor confirmed`() {
|
||||
val root = ArtiGuardState.parse("""{ "default": { "guards": [] }, "restricted": { "guards": [] } }""")
|
||||
|
||||
assertFalse(ArtiGuardState.hasNoUsableGuards(root))
|
||||
assertFalse(ArtiGuardState.hasConfirmedGuard(root))
|
||||
}
|
||||
}
|
||||
@@ -272,6 +272,23 @@ class TorManagerTest {
|
||||
assertEquals(0, backend.resetWithCleanStateCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `watchdog wipes state on first stuck-Connecting when guards prove prior bootstrap`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
val backend = FakeTorBackend().apply { bootstrappedBefore = true }
|
||||
// Never reaches Active in this session, but on-disk state proves a prior bootstrap.
|
||||
val manager = buildManager(backend = backend, clock = { 1_000_000_000_000L })
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(TorServiceStatus.Connecting, manager.status.value)
|
||||
|
||||
advanceTimeBy(TorManager.SELF_HEAL_AFTER_MS + 1_000L)
|
||||
runCurrent()
|
||||
|
||||
assertEquals("seeded from disk: must wipe stale/poisoned state, not gentle-reset", 0, backend.resetCount)
|
||||
assertEquals(1, backend.resetWithCleanStateCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `watchdog uses full reset after first Active`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
@@ -476,6 +493,11 @@ private class FakeTorBackend : TorBackend {
|
||||
var resetWithCleanStateCount = 0
|
||||
private set
|
||||
|
||||
/** Simulates a persisted confirmed guard on disk (prior successful bootstrap). */
|
||||
var bootstrappedBefore = false
|
||||
|
||||
override suspend fun hasBootstrappedBefore(): Boolean = bootstrappedBefore
|
||||
|
||||
override suspend fun start() {
|
||||
startCount++
|
||||
_status.value = TorServiceStatus.Connecting
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
{
|
||||
"default": {
|
||||
"guards": [
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "rquRARGG0HF/y3ugtCi8HmhvZ6jIa1lSXQlFDUBrHBA",
|
||||
"rsa": "b8f0736b96819c88a1aed46d5965fa18610694b5"
|
||||
},
|
||||
"orports": [
|
||||
"142.132.205.43:9993",
|
||||
"[2a01:4f8:261:5145::2]:9993"
|
||||
],
|
||||
"added_at": "2026-06-14T13:48:30Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "TooManyIndeterminateFailures",
|
||||
"history": {
|
||||
"n_successes": 2,
|
||||
"n_failures": 0,
|
||||
"n_indeterminate": 14
|
||||
},
|
||||
"failure_ratio": 0.875,
|
||||
"threshold_ratio": 0.7
|
||||
},
|
||||
"confirmed_at": "2026-06-18T08:49:30Z",
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "1tmwk24X7PS9baMIflSwPnRxczUjs0AumO1YDHWzKpk",
|
||||
"rsa": "dfb5ce34fe6f5d9c56377f38e86b3e55fffa1830"
|
||||
},
|
||||
"orports": [
|
||||
"109.70.100.245:9010",
|
||||
"[2a03:e600:100:c3::11]:9010"
|
||||
],
|
||||
"added_at": "2026-06-16T08:21:30Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": {
|
||||
"type": "TooManyIndeterminateFailures",
|
||||
"history": {
|
||||
"n_successes": 2,
|
||||
"n_failures": 0,
|
||||
"n_indeterminate": 15
|
||||
},
|
||||
"failure_ratio": 0.8823529411764706,
|
||||
"threshold_ratio": 0.7
|
||||
},
|
||||
"confirmed_at": "2026-06-17T01:36:40Z",
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "gvuYmLGk50ed9efpKqMiEZWKM8SqkJW4bB2nwmIPKtM",
|
||||
"rsa": "c2f3631b1386fd7ba5227b28783315cd52eb1526"
|
||||
},
|
||||
"orports": [
|
||||
"152.53.18.121:9001",
|
||||
"[2a0a:4cc0:1:104e::1]:9001"
|
||||
],
|
||||
"added_at": "2026-06-14T20:06:10Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": "2026-06-16T22:54:00Z",
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "GX8TczQuVV+Wbk9Zeik5YA3LOPolZXO4wL3FYdGVcJ4",
|
||||
"rsa": "76d2eba82bbcca3df9f254a9a8372a10a26f3d14"
|
||||
},
|
||||
"orports": [
|
||||
"134.130.172.229:9001"
|
||||
],
|
||||
"added_at": "2026-06-10T15:52:00Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": "2026-06-10T15:52:00Z",
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "SHvdJYQ/zk1n24LrSDLjT0n1x/h/wn7UexhUnysm24Q",
|
||||
"rsa": "dc5f31743bb3074bdf6153a066e9bed01e03311d"
|
||||
},
|
||||
"orports": [
|
||||
"193.200.229.243:10443",
|
||||
"[2a03:94e0:ffff:193:200:229:0:243]:10443"
|
||||
],
|
||||
"added_at": "2026-06-09T03:23:00Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "zdp9Ni6/yN9xjXGwBkP8RgayDUl+gMzTkK1CvrqRGnQ",
|
||||
"rsa": "239ff93a066c1e70d95dbdb0a6819941caf73021"
|
||||
},
|
||||
"orports": [
|
||||
"51.83.41.117:9100",
|
||||
"[2001:41d0:305:2100::8448]:9100"
|
||||
],
|
||||
"added_at": "2026-06-16T19:32:40Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "//13KRovDTJtpueEaHyqB8zliL2sl1vNAriIvcUBWjs",
|
||||
"rsa": "99ec64fe9ef0e0ecec3e125a2327c2779c1e7947"
|
||||
},
|
||||
"orports": [
|
||||
"46.4.66.188:8000",
|
||||
"[2a01:4f8:140:244f::2]:8000"
|
||||
],
|
||||
"added_at": "2026-06-08T21:30:40Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "qA7+Jbr2gbkPMoDj13H5Bf02mmP+IdGdnTJfL2+DgAM",
|
||||
"rsa": "861bcfdd148973985e7fe97c7455c9e4ac4e13be"
|
||||
},
|
||||
"orports": [
|
||||
"157.90.212.53:443",
|
||||
"[2a01:4f8:252:194b::2]:443"
|
||||
],
|
||||
"added_at": "2026-06-16T22:43:00Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "y9oTlscEUikG1WYBbR7P1bEiV7h+Ne8RZvZoX/cPItM",
|
||||
"rsa": "e3040061dd578d614d83ac0ae3526abdfe9fc323"
|
||||
},
|
||||
"orports": [
|
||||
"87.106.54.7:9001"
|
||||
],
|
||||
"added_at": "2026-06-12T02:23:30Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "MnaJaeA1kVXRa4jYkVOI6ENFjFlZfRH5GP9kXlds+Wk",
|
||||
"rsa": "ef86ad86576f1a10f740ee26e8cb126e81dda5dd"
|
||||
},
|
||||
"orports": [
|
||||
"178.215.228.25:444",
|
||||
"[2a0d:5440::25]:9052"
|
||||
],
|
||||
"added_at": "2026-06-11T12:10:00Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "febl0xAVXMcOxOuEY3xbH1bDbgkxdNyCSAlgbuhqAms",
|
||||
"rsa": "f7b94b1a67b563459c6a7c6ad7d5b8031e127b26"
|
||||
},
|
||||
"orports": [
|
||||
"192.42.116.143:443",
|
||||
"[2001:67c:e60:c0c:192:42:116:143]:443"
|
||||
],
|
||||
"added_at": "2026-06-15T01:38:30Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "AV7CHhiBquq6Y21MEabK7hnAD4ZeNSmH8JDS1ppUXCM",
|
||||
"rsa": "04f4c22d98e5ba86a2ccf0bd6df42431c071f2aa"
|
||||
},
|
||||
"orports": [
|
||||
"87.236.199.223:9007",
|
||||
"[2a01:5f0:c001:108:29::8]:443"
|
||||
],
|
||||
"added_at": "2026-06-14T12:44:30Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "N/B+0QX4ggFsvw9+/bfnGCeeBm5ScqJQlkFaWYGEN50",
|
||||
"rsa": "c42c51efc275c45ef94cf810e02ba737fd99f7fa"
|
||||
},
|
||||
"orports": [
|
||||
"194.5.250.250:443"
|
||||
],
|
||||
"added_at": "2026-06-08T05:57:30Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "PtrkxtnBpL3NK7RQEw6TTdhy4YWT9EMpELF2DUh+vl8",
|
||||
"rsa": "0ea859733751f8e8cecd70e33c9994bb7a04ac10"
|
||||
},
|
||||
"orports": [
|
||||
"51.178.136.58:9000"
|
||||
],
|
||||
"added_at": "2026-06-07T08:24:50Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "F60AmQxOFjWujKzP9zPpezfV35XuqMNog9BvxjaY3mc",
|
||||
"rsa": "92fda7647b431e365b8a649851c3542e7a0ca280"
|
||||
},
|
||||
"orports": [
|
||||
"194.13.81.26:9001",
|
||||
"[2a03:4000:43:216:5443:2bff:fe16:c6b5]:9001"
|
||||
],
|
||||
"added_at": "2026-06-13T05:48:00Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "RiuLP1z65m0ghVEAqOEFD+a/96ta5b5F7Oz7mZLolBM",
|
||||
"rsa": "e08166ee25a85fd5c9433d133b39fc5ebde16d98"
|
||||
},
|
||||
"orports": [
|
||||
"217.12.206.128:9001"
|
||||
],
|
||||
"added_at": "2026-06-13T03:45:10Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "5fWkcDYvNYr0Gxwe2Ulu38f48m2FyDTBT6oziaTq8v8",
|
||||
"rsa": "724f4622ea76583af64614979db847dd39eea61b"
|
||||
},
|
||||
"orports": [
|
||||
"109.70.100.245:9030",
|
||||
"[2a03:e600:100:c3::11]:9030"
|
||||
],
|
||||
"added_at": "2026-06-08T00:07:00Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "xKIytpB9w1F0CNtCTkHT2sWBQ7nut7Ondyvim+NyJcg",
|
||||
"rsa": "a2a3b91e706d5fc1b90b0bf4eb6f8c5ec18ea181"
|
||||
},
|
||||
"orports": [
|
||||
"57.129.110.54:9100",
|
||||
"[2001:41d0:701:1100::9a4e]:9100"
|
||||
],
|
||||
"added_at": "2026-06-09T14:24:30Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "K8V12xahZ1pl0Sj2HN38VjL/TTq3DU+dcfn+qnm3KYs",
|
||||
"rsa": "91892720f9262cb37b91a7176d3d2280f7af14be"
|
||||
},
|
||||
"orports": [
|
||||
"46.4.66.178:9001",
|
||||
"[2a01:4f8:140:2459::2]:9001"
|
||||
],
|
||||
"added_at": "2026-06-11T12:53:40Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "MX9mVbBKjIQpNk9wDY5bsaYKcje0IeX73LxKVgapfp4",
|
||||
"rsa": "02bc27735d76b4b43fc789757900bbb38de0402c"
|
||||
},
|
||||
"orports": [
|
||||
"159.195.54.13:9001",
|
||||
"[2a0a:4cc0:c1:d5e3:54c9:6eff:fe0a:e8f7]:9001"
|
||||
],
|
||||
"added_at": "2026-06-10T06:03:10Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "wjv/3RiHzkbcjBQHBTFRxdds7YmsKReL2V9kMjSvuFc",
|
||||
"rsa": "a3af96c0600301138631d0a35a567cc748ce749d"
|
||||
},
|
||||
"orports": [
|
||||
"193.182.111.41:443",
|
||||
"[2a03:8600::a1]:443"
|
||||
],
|
||||
"added_at": "2026-06-10T07:03:00Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"ed25519": "HA/2D2GGWjO+pWKHccrTu10TLIaZW9h0JlGnCIU7GZE",
|
||||
"rsa": "beb887d44cb21ade582f2cb519c869ffdf520fdc"
|
||||
},
|
||||
"orports": [
|
||||
"57.128.170.236:9000",
|
||||
"[2001:41d0:801:2000::3129]:9000"
|
||||
],
|
||||
"added_at": "2026-06-15T05:16:50Z",
|
||||
"added_by": {
|
||||
"crate": "tor-guardmgr",
|
||||
"version": "0.42.0"
|
||||
},
|
||||
"disabled": null,
|
||||
"confirmed_at": null,
|
||||
"unlisted_since": null
|
||||
}
|
||||
],
|
||||
"confirmed": [
|
||||
{
|
||||
"ed25519": "1tmwk24X7PS9baMIflSwPnRxczUjs0AumO1YDHWzKpk",
|
||||
"rsa": "dfb5ce34fe6f5d9c56377f38e86b3e55fffa1830"
|
||||
},
|
||||
{
|
||||
"ed25519": "rquRARGG0HF/y3ugtCi8HmhvZ6jIa1lSXQlFDUBrHBA",
|
||||
"rsa": "b8f0736b96819c88a1aed46d5965fa18610694b5"
|
||||
},
|
||||
{
|
||||
"ed25519": "gvuYmLGk50ed9efpKqMiEZWKM8SqkJW4bB2nwmIPKtM",
|
||||
"rsa": "c2f3631b1386fd7ba5227b28783315cd52eb1526"
|
||||
},
|
||||
{
|
||||
"ed25519": "GX8TczQuVV+Wbk9Zeik5YA3LOPolZXO4wL3FYdGVcJ4",
|
||||
"rsa": "76d2eba82bbcca3df9f254a9a8372a10a26f3d14"
|
||||
}
|
||||
]
|
||||
},
|
||||
"restricted": {
|
||||
"guards": [],
|
||||
"confirmed": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user