Compare commits

..
Author SHA1 Message Date
jackandGitHub 41486fd1c9 Merge branch 'main' into chore/perf-privacy-polish 2026-07-01 23:58:27 +02:00
68eeba97ff Use Xcode-bundled Swift in CI instead of a standalone toolchain (#1353)
The unpinned setup-swift action installs Swift 6.1, which refuses the
SDK on runner images that have rolled to Xcode 26.5 ("this SDK is not
supported by the compiler"). Jobs passed or failed depending on which
image they landed on. The Xcode-bundled toolchain always matches the
image's SDK, and matches local development. Cache keys now include the
toolchain version so artifacts from one compiler are never restored
into builds with another.

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:51:58 +02:00
jackandClaude Fable 5 d8527b98fd Add a pragmatic SwiftLint configuration
Baseline .swiftlint.yml for local use (not wired into CI yet):

- file_length: warning at 600, error at 4000 — above the current
  maximum (BLEService.swift, ~3,500 lines), so the codebase passes
  as-is and only future growth trips the error.
- function_body_length: warning 100, error 200.
- Disables the high-noise style rules (line_length, identifier_name,
  cyclomatic_complexity, force_cast/force_try for tests, etc.) so the
  remaining defaults stay actionable; SwiftLint is not installed in
  this environment, so the disabled list is a best-effort starting
  point to be tuned on first real run.
- Excludes vendored Arti and SwiftPM .build output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:32:19 +02:00
jackandClaude Fable 5 5be8b5b37f Track CLAUDE.md and update it to match the current architecture
The previous (untracked) CLAUDE.md described ChatViewModel as a ~170KB
class split into +Nostr/+PrivateChat/+Tor extension files. Reality:
ChatViewModel.swift is ~1,600 lines and delegates to ~25 coordinator/
pipeline types in bitchat/ViewModels/; the Extensions/ files are thin
delegation shims. Document the actual structure: AppRuntime as the
composition root, ConversationStore as the single-writer message
store, the BLE handler/policy collaborators around BLEService, and
BinaryProtocol's move into the BitFoundation local package.

Also start tracking CLAUDE.md (removing its .gitignore entry) so the
guidance stays versioned alongside the code it describes; build/test
command sections are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:31:59 +02:00
jackandClaude Fable 5 7200b3de2a Add NIP-44 style padding support to Nostr DM encryption
The bespoke "v2" DM envelope (XChaCha20-Poly1305 over the raw UTF-8
rumor JSON) leaks exact plaintext length to relays. Add NIP-44 v2
style payload framing: a 2-byte big-endian length prefix followed by
zero padding to NIP-44's calc_padded_len buckets (32-byte minimum,
power-of-two-derived chunks), carried in a new "v3:" envelope that
reuses the existing key derivation and AEAD.

Compatibility: deployed clients hard-reject any envelope that does not
start with "v2:", and the v2 payload is raw JSON, so padded payloads
cannot be introduced inside v2 either. This lands phase 1 of a
two-phase rollout: decrypt accepts both v2 (unpadded) and v3 (padded),
while encrypt keeps emitting v2, gated by
NostrProtocol.sendPaddedEnvelope (defaults to false with the rollout
plan documented inline). Flip the flag once v3-capable clients are
widely deployed.

Unpad validates that the claimed length's bucket exactly matches the
authenticated payload size, so a tampered or malformed length prefix
fails closed. Tests cover pad/unpad round-trips, the NIP-44 bucket
vectors, v3 round-trip, legacy v2 decrypt, default-envelope pinning,
and tampered length prefixes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:30:00 +02:00
jackandClaude Fable 5 3f6dc7dd36 Speed up hex encode/decode with lookup tables
Data.hexEncodedString() built each byte via String(format: "%02x", _),
paying Foundation format-string parsing per byte on the hot BLE receive
path (PeerID(hexData:) is called several times per received packet).
Replace it with an ASCII lookup table writing into a preallocated
buffer; output is byte-for-byte identical (lowercase hex).

Data(hexString:) similarly allocated a two-character substring and went
through integer radix parsing per byte; replace with direct nibble
lookups over the UTF-8 bytes. Semantics are unchanged except that
sign-prefixed pairs like "+f", which the old radix parser incorrectly
accepted, are now rejected.

Add round-trip, known-vector, and invalid-input tests to
BitFoundationTests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:26:38 +02:00
GitHub Action f688e529f6 Automated update of relay data - Sun Jun 21 07:37:42 UTC 2026 2026-06-21 07:37:42 +00:00
jackandGitHub 96e32ba990 Merge pull request #1345 from permissionlesstech/fix/security-audit-critical-high-batch
Fix security audit findings: 3 critical, 7 high
2026-06-17 09:44:26 +02:00
jack 0a2f4d9c9d Tighten panic wipe and NIP-17 regressions 2026-06-17 09:27:13 +02:00
jack 914135adb0 Fix panic wipe relay and geohash state 2026-06-16 13:56:15 +02:00
jackandGitHub cd7ffa0df9 Merge branch 'main' into fix/security-audit-critical-high-batch 2026-06-16 13:52:10 +02:00
GitHub Action bbe1ed0652 Automated update of relay data - Sun Jun 14 07:34:58 UTC 2026 2026-06-14 07:34:58 +00:00
jackandClaude Fable 5 f07b032b99 Fix CI exit hang: sign reassembled public packets in FragmentationTests
Bisecting (base was 4/4 clean, branch 3/3 hung, reliably reproducible)
pinned the parallel-suite exit hang to the public-message signature
requirement (security fix #2), via FragmentationTests:

reassemblyFromFragmentsDeliversPublicMessage and
duplicateFragmentDoesNotBreakReassembly send fragments of an UNSIGNED
public message and `await capture.waitForPublicMessages(...)`. With #2 the
reassembled unsigned message is now (correctly) dropped, so
didReceivePublicMessage never fires. The helper then trips a latent bug:
on timeout it cancels the waiter task but never resumes its
CheckedContinuation, so the throwing task group's teardown awaits a child
that never completes and the whole test process hangs at exit (SIGKILL'd
by CI). Base never hit it because the message always arrived in time.

Fix matches the security model — real public broadcasts are signed: sign
the reassembled packet with a NoiseEncryptionService and preseed the
sender's signing key (same pattern as duplicatePacket_isDeduped), so #2
verifies and delivers it. Full parallel suite now exits cleanly 5/5 locally
(branch was 3/3 hung before).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 23:44:12 +02:00
jackandClaude Fable 5 2cbcb290f7 Remove lingering save timer from SecureIdentityStateManager (CI exit hang)
The app test job hung at process exit (all tests pass, then SIGKILL at the
CI timeout). Root cause: fix #5 replaced the dead Timer.scheduledTimer with
a real DispatchSourceTimer, created per manager instance, resumed and never
cancelled. Those live timer sources kept the dispatch machinery alive so the
swift-testing process never exited. The earlier `isRunningTests` guard was
fragile (it does not reliably detect the swift-testing-only runner on CI).

Drop the debounce timer entirely. Mutations now persist via the same
serialized `queue` barrier their callers already run on (saveIdentityCache ->
performSave directly); forceSave is a direct, non-blocking call (no
queue.sync, which is unsafe on the cooperative pool). No timer is left
scheduled, so nothing keeps the process alive. The original bug is still
fixed — saves now actually happen, unlike the never-firing Timer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 22:55:51 +02:00
jackandClaude Fable 5 9cf7c80518 Reduce test-process churn to fix flaky CI exit hang
The app test job intermittently hung at process exit. The suite is
load-sensitive and historically prone to cooperative-pool/teardown
deadlocks; the security changes added background work to the test process
that pushed it over the edge. Make the unit-test BLEService/identity
manager quiescent and remove blocking sync:

- forceSave() no longer does queue.sync(.barrier). It is reachable from
  deinit and from async tests on the swift-concurrency cooperative pool,
  where a blocking barrier-sync can starve/deadlock the pool. It now
  cancels the debounce timer and persists directly. (Removed the
  now-unneeded queue-specific-key re-entrancy machinery.)
- SecureIdentityStateManager persists synchronously under tests instead of
  scheduling a DispatchSourceTimer that lingers past process exit.
- Gate gossip-sync start (in addition to the maintenance timer) behind
  real Bluetooth init, so the test BLEService runs no periodic
  sign/broadcast/sync churn.
- Skip the panic Nostr reconnect under tests (connecting the shared relay
  singleton starts network/reconnect work that never completes).

Production behavior is unchanged: real Bluetooth builds run all timers and
the debounced save as before; the debounce save now actually fires
(previously a Timer on a GCD queue that never ran).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 22:40:39 +02:00
jackandClaude Fable 5 f76fd8a538 Fix CI hang: gate maintenance timer to Bluetooth-enabled; sign dedup test packet
Root cause of the CI app-test hang was a pre-existing bleQueue<->collectionsQueue
lock inversion driven by the periodic maintenance timer (performMaintenance ->
drainAllPendingWrites takes collectionsQueue while another path holds it and
sync-waits on bleQueue via readLinkState). The timer is created unconditionally
in init, so it also ran in the unit-test process (initializeBluetoothManagers:
false), where it only churns BLE writes/notifications/announces that don't exist.
Recent timing changes made the latent deadlock surface reliably.

- Only start the maintenance timer when real CoreBluetooth managers were
  initialized (maintenanceTimerEnabled). Production behavior is unchanged; the
  unit-test process no longer runs the timer and cannot hit the inversion.

Also fix BLEServiceCoreTests.duplicatePacket_isDeduped, which sent an unsigned
public packet that the new signature requirement (security fix #2) correctly
drops. The test now signs the packet and preseeds the sender's signing key
(production sendMessage signs public broadcasts), exercising the dedup path
(security fix #7) end to end. _test_handlePacket gains an optional
signingPublicKey to seed the registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:09:06 +02:00
jackandClaude Fable 5 09c2c12838 Fix deadlock in identity-cache forceSave (CI hang)
The forceSave() rewrite used queue.sync(flags: .barrier), but forceSave
is also called from deinit. The debounce timer's barrier hop captured
self strongly, so when that block dropped the last reference the manager
deallocated *on* the identity queue — deinit -> forceSave -> queue.sync
then deadlocked synchronizing onto the queue it was already running on.
This hung the test process at exit (CI SIGKILL / exit 137).

- forceSave() now detects (via a queue-specific key) when it is already
  executing on the queue and runs the save directly instead of sync-ing
  onto itself.
- The timer's barrier hop now captures self weakly, so it can no longer
  trigger a deallocation on the queue in the first place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:46:24 +02:00
jackandClaude Fable 5 8378ff949a Address Codex review: verify public messages against registry signing key
The public-message signature check fell back to signedSenderDisplayName,
which only searches the asynchronously-persisted identity cache. Because
the peer registry is updated synchronously on a verified announce, a
message arriving immediately after that announce could have a valid
signature and a verified registry entry yet still be dropped (cache not
caught up).

Verify the packet signature against the signing key already present in
the synchronously-updated peer registry first; fall back to the
persisted-identity lookup only for peers not yet in the registry. The
security property is unchanged: a spoofed senderID claiming a registry
peer still fails registry verification and the persisted fallback, and
is dropped.

Adds tests for the race (delivered via registry key before cache
persists) and the spoof case (invalid signature falls back and drops).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:18:44 +02:00
jackandClaude Fable 5 ca63893197 Fix security audit findings: 3 critical, 7 high
A broad audit surfaced ten critical/high issues across the crypto,
transport, identity, and panic-wipe layers. This fixes all ten.

Critical:
- Nostr DMs were unauthenticated. The NIP-17 seal was signed with a
  throwaway ephemeral key and the receiver never verified it, so anyone
  who knows a recipient's npub could forge messages (and delivery/read
  receipts) into an existing trusted conversation. The seal is now
  signed with the sender's real identity key, and the receiver verifies
  the seal signature and that seal.pubkey == rumor.pubkey.
  NOTE: this is a breaking wire-protocol change (see PR).
- Public BLE messages trusted registry membership instead of the packet
  signature. Since senderID is attacker-controlled, any verified peer
  could be impersonated in public chat. A valid signature from the
  claimed sender is now required before any registry identity is used.
- Unverified announces still persisted the announced identity, letting a
  replayed noisePublicKey overwrite a victim's stored signing key and
  nickname. persistIdentity is now gated on verification.

High:
- Noise decrypt trapped on a 16-19 byte ciphertext (negative prefix
  length after nonce extraction) — a remote crash. Now validated.
- Identity-cache debounce save used Timer.scheduledTimer on a GCD queue
  with no run loop, so it never fired; block/verify/favorite changes
  only persisted on explicit forceSave. Replaced with a
  DispatchSourceTimer on the queue; forceSave is now serialized.
- Identity-cache key load couldn't tell "missing" from a transient
  keychain failure and would regenerate (deleting) the key, orphaning
  the cache. Now uses getIdentityKeyWithResult and falls back to a
  session-only ephemeral key without clobbering the persisted key/cache.
- BLE receive-dedup key lacked a payload digest, so post-handshake
  flushes (queued msgs + delivery/read acks in the same ms) were dropped
  as duplicates. Digest added, matching the ingress registry.
- Maintenance timer was created only in init and never recreated after a
  panic stop/start, silently degrading the mesh until app restart. Now
  recreated in startServices.
- Panic wipe left persisted location state (selected channel, teleport
  set, bookmarks) and cached per-geohash Nostr private keys behind. Both
  are now cleared.
- Panic spawned an orphan NostrRelayManager instead of reusing .shared,
  splitting relay state from every other component. Now reuses .shared.

Tests updated to assert the fixed behavior (announce no longer persists
unverified identities; public messages require a signature; receive
dedup ID includes the payload digest).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:07:28 +02:00
26 changed files with 1582 additions and 562 deletions
+10 -5
View File
@@ -29,17 +29,22 @@ jobs:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Swift
uses: swift-actions/setup-swift@v2
# Use the Xcode-bundled Swift toolchain: it always matches the SDK on
# the runner image. A standalone swift.org toolchain (setup-swift) broke
# whenever the image's Xcode moved ahead of it ("this SDK is not
# supported by the compiler").
- name: Note toolchain version (cache key)
id: swift-version
run: echo "version=$(swift --version 2>/dev/null | head -1 | shasum | cut -c1-12)" >> "$GITHUB_OUTPUT"
- name: Cache build artifacts
uses: actions/cache@v4
with:
path: ${{ matrix.path }}/.build
key: ${{ runner.os }}-${{ matrix.name }}-${{ hashFiles(format('{0}/**/*.swift', matrix.path), format('{0}/**/Package.resolved', matrix.path)) }}
key: ${{ runner.os }}-${{ steps.swift-version.outputs.version }}-${{ matrix.name }}-${{ hashFiles(format('{0}/**/*.swift', matrix.path), format('{0}/**/Package.resolved', matrix.path)) }}
restore-keys: |
${{ runner.os }}-${{ matrix.name }}-${{ hashFiles(format('{0}/**/Package.resolved', matrix.path)) }}
${{ runner.os }}-${{ matrix.name }}-
${{ runner.os }}-${{ steps.swift-version.outputs.version }}-${{ matrix.name }}-${{ hashFiles(format('{0}/**/Package.resolved', matrix.path)) }}
${{ runner.os }}-${{ steps.swift-version.outputs.version }}-${{ matrix.name }}-
- name: Build tests
# Built separately so the hang watchdog below times only test
-1
View File
@@ -6,7 +6,6 @@
plans/
## AI
CLAUDE.md
AGENTS.md
.claude/
+51
View File
@@ -0,0 +1,51 @@
# SwiftLint configuration for BitChat.
#
# Intentionally pragmatic: rules that would flood the existing codebase are
# disabled so the lint signal stays actionable; re-enable them individually as
# files get cleaned up. Not wired into CI yet — run locally with `swiftlint`
# from the repo root.
included:
- bitchat
- bitchatTests
- localPackages/BitFoundation/Sources
- localPackages/BitFoundation/Tests
- localPackages/BitLogger/Sources
excluded:
- .build
- localPackages/Arti
- localPackages/BitFoundation/.build
- localPackages/BitLogger/.build
disabled_rules:
# Style/volume rules that currently produce noise across the codebase.
- line_length
- identifier_name
- type_name
- type_body_length
- cyclomatic_complexity
- function_parameter_count
- large_tuple
- nesting
- todo
- trailing_comma
- opening_brace
- statement_position
- for_where
- redundant_string_enum_value
- non_optional_string_data_conversion
# Common in tests (force casts/tries on fixtures).
- force_cast
- force_try
file_length:
warning: 600
# BLEService.swift is currently ~3,500 lines; the error threshold sits above
# today's maximum so the codebase passes as-is and only future growth of the
# largest files trips it.
error: 4000
function_body_length:
warning: 100
error: 200
+106
View File
@@ -0,0 +1,106 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build & Test Commands
```bash
# Build macOS (no signing)
xcodebuild -project bitchat.xcodeproj -scheme "bitchat (macOS)" -configuration Debug CODE_SIGNING_ALLOWED=NO build
# Build iOS for simulator
xcodebuild -project bitchat.xcodeproj -scheme "bitchat (iOS)" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15' build
# Run all tests (iOS simulator)
xcodebuild -project bitchat.xcodeproj -scheme bitchat -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15' test
# Run tests via Swift Package Manager (used in CI)
swift build && swift test --parallel
# Clean build
xcodebuild -project bitchat.xcodeproj -scheme "bitchat (macOS)" clean
# Quick dev build and run (macOS only, requires `just`)
just run
```
### Running Specific Tests
```bash
# Run a single test class
xcodebuild test -project bitchat.xcodeproj -scheme bitchat -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15' -only-testing:bitchatTests/IntegrationTests
# Run a single test method
xcodebuild test -project bitchat.xcodeproj -scheme bitchat -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15' -only-testing:bitchatTests/NoiseProtocolTests/testHandshake
```
## Architecture Overview
BitChat is a **dual-transport P2P messaging app**: Bluetooth mesh for offline local communication, Nostr protocol for internet-based global messaging.
### Local Packages (`localPackages/`)
- **BitFoundation**: Shared foundation types — `BinaryProtocol` (compact binary packet format for BLE), `BitchatPacket`, `BitchatMessage`, `PeerID`, hex/SHA256/compression utilities. Has its own test suite under `localPackages/BitFoundation/Tests` (run `swift test` inside the package).
- **BitLogger**: `SecureLogger` logging.
- **Arti**: Tor integration (exposes the `Tor` product).
### Transport Layer
- **BLEService** (`bitchat/Services/BLE/BLEService.swift`): Core Bluetooth LE mesh networking - peer discovery, connection management, multi-hop relay (max 7 hops), packet fragmentation. The BLE directory contains ~40 focused collaborators (handlers, policies, buffers): `BLEReceivePipeline` dispatches inbound packets to `BLEAnnounceHandler` / `BLENoisePacketHandler` / `BLEPublicMessageHandler` / `BLEFragmentHandler` etc.; outbound planning lives in the `BLEOutbound*` types; peer state in `BLEPeerRegistry`.
- **NostrTransport** (`bitchat/Services/NostrTransport.swift`): Internet transport via Nostr relays with NIP-17 encryption
- **Transport protocol** (`bitchat/Services/Transport.swift`): Common interface both transports implement
### Encryption Layer
- **NoiseProtocol** (`bitchat/Noise/`): Noise XX pattern for end-to-end encryption with forward secrecy
- `NoiseEncryptionService`: Main encryption/decryption API
- `NoiseSessionManager`: Thread-safe per-peer session management
- `NoiseSession`: Individual peer session state (handshake, send/receive ciphers)
- **NostrProtocol** (`bitchat/Nostr/NostrProtocol.swift`): NIP-17 gift-wrapped encryption for Nostr messages
### Protocol Layer
- **BinaryProtocol** (`localPackages/BitFoundation/Sources/BitFoundation/BinaryProtocol.swift`): Compact binary packet format for BLE (lives in BitFoundation, not `bitchat/Protocols/`)
- **BitchatProtocol** (`bitchat/Protocols/BitchatProtocol.swift`): Message types and packet structures
- **LocationChannel/Geohash** (`bitchat/Protocols/`): Geographic channel routing
### Application Layer
- **AppRuntime** (`bitchat/App/AppRuntime.swift`): Composition root. Owns `ChatViewModel`, `ConversationStore`, the feature models (`PublicChatModel`, `PeerListModel`, `LocationPresenceStore`, `PeerIdentityStore`, ...), and the app event stream.
- **ConversationStore** (`bitchat/App/ConversationStore.swift`): Single-writer, single source of truth for conversation message state and selection (see `docs/CONVERSATION-STORE-DESIGN.md`). Feature models and `ChatViewModel` observe it and mutate it through its intent API.
- **ChatViewModel** (`bitchat/ViewModels/ChatViewModel.swift`, ~1,600 lines): Central coordinator that wires and delegates to ~25 focused coordinator/pipeline types in `bitchat/ViewModels/`, e.g.:
- `ChatPrivateConversationCoordinator` / `ChatPublicConversationCoordinator`: DM and public chat flows
- `ChatNostrCoordinator``GeohashSubscriptionManager`, `NostrInboundPipeline`, `GeoPresenceTracker`, `GeoChannelCoordinator`: Nostr/geohash channel logic
- `ChatOutgoingCoordinator`, `ChatDeliveryCoordinator`, `PublicMessagePipeline`: send paths and delivery tracking
- `ChatMediaTransferCoordinator`, `ChatMediaPreparation`: media transfers
- `ChatLifecycleCoordinator`, `ChatTransportEventCoordinator`, `ChatPeerListCoordinator`, `ChatPeerIdentityCoordinator`, `ChatVerificationCoordinator`: lifecycle, transport events, peer state
- `bitchat/ViewModels/Extensions/` (`ChatViewModel+Nostr/+PrivateChat/+Tor`): thin delegation shims kept for call-site stability; the real logic lives in the coordinators
- **MessageRouter** (`bitchat/Services/MessageRouter.swift`): Intelligent transport selection (BLE → Nostr fallback)
- **PrivateChatManager** (`bitchat/Services/PrivateChatManager.swift`): DM session management
## Test Infrastructure
Tests use an **in-memory networking harness** for deterministic, race-free testing:
- **MockBLEService** (`bitchatTests/Mocks/MockBLEService.swift`): Simulated BLE mesh with configurable topology
- `MockBLEService.resetTestBus()` - Clear state in setUp()
- `simulateConnectedPeer(_:)` / `simulateDisconnectedPeer(_:)` - Configure topology
- `autoFloodEnabled` - Enable broadcast flooding for Integration tests only
- **Test categories**:
- `bitchatTests/EndToEnd/`: Full message flow tests with explicit routing
- `bitchatTests/Integration/`: Multi-node topology tests with auto-flooding
- Unit tests: Individual component tests
## Key Patterns
- **Threading**: Use `@MainActor` for UI, `Task { @MainActor in ... }` for main thread dispatch
- **Delegation**: `BitchatDelegate`, `TransportPeerEventsDelegate` for event propagation
- **Session recovery**: On decrypt failure, clear local session and re-initiate Noise handshake (no NACK)
## Device Setup
To run on physical devices:
1. Copy `Configs/Local.xcconfig.example` to `Configs/Local.xcconfig`
2. Add your Developer Team ID to `Local.xcconfig`
3. Replace `group.chat.bitchat` with `group.<your_bundle_id>` in entitlements
@@ -151,38 +151,68 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
// Thread safety
private let queue = DispatchQueue(label: "bitchat.identity.state", attributes: .concurrent)
// Debouncing for keychain saves
private var saveTimer: Timer?
private let saveDebounceInterval: TimeInterval = 2.0 // Save at most once every 2 seconds
// Pending-save coalescing flag. Reads/writes are serialized on `queue`.
// Persistence is done with a fire-and-forget `queue.async(.barrier)` rather
// than a retained DispatchSourceTimer: a lingering, never-cancelled timer
// keeps the dispatch machinery alive and prevents the unit-test process from
// exiting. (The original code used Timer.scheduledTimer on a GCD queue with
// no run loop, so saves never actually fired.)
private var pendingSave = false
// Encryption key
private let encryptionKey: SymmetricKey
/// True when `encryptionKey` is a throwaway generated this session because the
/// persisted key could not be read (device locked / access denied). In that
/// state we must NOT persist (it would overwrite the real cache with data the
/// next launch can't decrypt) and must NOT delete the existing cache.
private let encryptionKeyIsEphemeral: Bool
init(_ keychain: KeychainManagerProtocol) {
self.keychain = keychain
// Generate or retrieve encryption key from keychain
// Retrieve (or, only on genuine first run, generate) the cache
// encryption key. We MUST distinguish "key doesn't exist yet" from a
// transient failure (device locked / access denied): the legacy
// getIdentityKey(forKey:) collapses both to nil, and generating+saving a
// new key deletes the existing one first permanently orphaning the
// encrypted cache on a launch that merely couldn't read the key.
let loadedKey: SymmetricKey
// Try to load from keychain
if let keyData = keychain.getIdentityKey(forKey: encryptionKeyName) {
let keyIsEphemeral: Bool
switch keychain.getIdentityKeyWithResult(forKey: encryptionKeyName) {
case .success(let keyData):
loadedKey = SymmetricKey(data: keyData)
keyIsEphemeral = false
SecureLogger.logKeyOperation(.load, keyType: "identity cache encryption key", success: true)
}
// Generate new key if needed
else {
loadedKey = SymmetricKey(size: .bits256)
let keyData = loadedKey.withUnsafeBytes { Data($0) }
// Save to keychain
case .itemNotFound:
// Genuine first run: generate and persist a new key.
let newKey = SymmetricKey(size: .bits256)
let keyData = newKey.withUnsafeBytes { Data($0) }
let saved = keychain.saveIdentityKey(keyData, forKey: encryptionKeyName)
loadedKey = newKey
// If even the save failed, treat the key as ephemeral so we don't
// later try to persist a cache the next launch can't read.
keyIsEphemeral = !saved
SecureLogger.logKeyOperation(.generate, keyType: "identity cache encryption key", success: saved)
case .deviceLocked, .authenticationFailed, .accessDenied, .otherError:
// Transient/critical read failure. Do NOT overwrite the persisted
// key. Use a session-only ephemeral key; the real key and cache are
// left intact for a healthy launch.
SecureLogger.warning("Identity cache key unavailable; using ephemeral key for this session (not persisting)", category: .security)
loadedKey = SymmetricKey(size: .bits256)
keyIsEphemeral = true
}
self.encryptionKey = loadedKey
// Load identity cache on init
loadIdentityCache()
self.encryptionKeyIsEphemeral = keyIsEphemeral
// Only read the persisted cache when we hold the real key; with an
// ephemeral key the decrypt would fail and discard the real cache.
if !keyIsEphemeral {
loadIdentityCache()
}
}
deinit {
@@ -211,23 +241,28 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
}
}
/// Persists the cache. Always invoked on `queue` under a barrier (its callers
/// run inside `queue.async(.barrier)`), so it simply marks the cache dirty
/// and persists it on the same serialized context no timer, nothing left
/// scheduled to keep the process alive.
private func saveIdentityCache() {
// Mark that we need to save
pendingSave = true
// Cancel any existing timer
saveTimer?.invalidate()
// Schedule a new save after the debounce interval
saveTimer = Timer.scheduledTimer(withTimeInterval: saveDebounceInterval, repeats: false) { [weak self] _ in
self?.performSave()
}
performSave()
}
/// Writes the cache to the keychain. Must run on `queue` with exclusive
/// (barrier) access.
private func performSave() {
guard pendingSave else { return }
pendingSave = false
// Never persist under an ephemeral key it would overwrite the real
// cache with data the next launch cannot decrypt.
guard !encryptionKeyIsEphemeral else {
SecureLogger.debug("Skipping identity cache save (ephemeral key this session)", category: .security)
return
}
do {
let data = try JSONEncoder().encode(cache)
let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
@@ -239,10 +274,14 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
SecureLogger.error(error, context: "Failed to save identity cache", category: .security)
}
}
// Force immediate save (for app termination)
// Force immediate save (for app termination / lifecycle events). Mutations
// already persist synchronously via saveIdentityCache, so this is normally a
// no-op (performSave early-returns when nothing is pending). Runs directly on
// the caller's thread deliberately NOT a `queue.sync(barrier)`, which is
// reachable from `deinit` and from async tests on the swift-concurrency
// cooperative pool where a blocking barrier-sync can starve/deadlock it.
func forceSave() {
saveTimer?.invalidate()
performSave()
}
+7
View File
@@ -322,6 +322,13 @@ final class NoiseCipherState {
throw NoiseError.replayDetected
}
// The 4-byte nonce prefix has been stripped, so the remaining bytes
// must still hold at least the 16-byte Poly1305 tag. The up-front
// `ciphertext.count >= 16` guard is not sufficient here (it counts
// the nonce), and `prefix(count - 16)` would trap on a short payload.
guard actualCiphertext.count >= 16 else {
throw NoiseError.invalidCiphertext
}
// Split ciphertext and tag
encryptedData = actualCiphertext.prefix(actualCiphertext.count - 16)
tag = actualCiphertext.suffix(16)
+7
View File
@@ -82,6 +82,13 @@ final class NostrIdentityBridge {
}
deviceSeedCache = nil
// Also drop the in-memory derived per-geohash identities. These hold the
// actual secp256k1 private keys; if left cached, post-panic geohash
// messages would still be signed with pre-panic keys (linkable across the
// wipe) until the app is force-quit.
cacheLock.lock()
derivedIdentityCache.removeAll()
cacheLock.unlock()
}
// MARK: - Per-Geohash Identities (Location Channels)
+194 -41
View File
@@ -39,22 +39,23 @@ struct NostrProtocol {
content: content
)
// 2. Create ephemeral key for this message
let ephemeralKey = try P256K.Schnorr.PrivateKey()
// Created ephemeral key for seal
// 3. Seal the rumor (encrypt to recipient)
// 2. Seal the rumor (encrypt to recipient) and sign it with the SENDER'S
// real identity key. NIP-17 requires the seal be signed by the sender
// so the recipient can authenticate who sent the message; signing with
// a throwaway key leaves DMs forgeable/impersonatable.
let senderKey = try senderIdentity.schnorrSigningKey()
let sealedEvent = try createSeal(
rumor: rumor,
recipientPubkey: recipientPubkey,
senderKey: ephemeralKey
senderKey: senderKey
)
// 4. Gift wrap the sealed event (encrypt to recipient again)
// 3. Gift wrap the sealed event with a throwaway ephemeral key (the wrap
// layer hides the sender's identity from relays; createGiftWrap mints
// its own ephemeral key internally).
let giftWrap = try createGiftWrap(
seal: sealedEvent,
recipientPubkey: recipientPubkey,
senderKey: ephemeralKey
recipientPubkey: recipientPubkey
)
// Created gift wrap
@@ -84,7 +85,15 @@ struct NostrProtocol {
throw error
}
// 2. Open the seal
// 2. Authenticate the seal. The seal MUST be signed by the sender's real
// identity key (NIP-17); without this check a DM is forgeable by anyone
// who knows the recipient's npub. Verify the seal's own signature.
guard seal.isValidSignature() else {
SecureLogger.error("❌ Rejecting DM: seal signature is missing or invalid", category: .session)
throw NostrError.invalidEvent
}
// 3. Open the seal
let rumor: NostrEvent
do {
rumor = try openSeal(
@@ -96,10 +105,63 @@ struct NostrProtocol {
SecureLogger.error("❌ Failed to open seal: \(error)", category: .session)
throw error
}
return (content: rumor.content, senderPubkey: rumor.pubkey, timestamp: rumor.created_at)
// 4. The sender claimed inside the rumor must match the key that actually
// signed the seal, otherwise the sender field is unauthenticated and
// spoofable.
guard seal.pubkey == rumor.pubkey else {
SecureLogger.error("❌ Rejecting DM: rumor pubkey does not match seal signer", category: .session)
throw NostrError.invalidEvent
}
// Return the seal signer's pubkey as the authenticated sender.
return (content: rumor.content, senderPubkey: seal.pubkey, timestamp: rumor.created_at)
}
#if DEBUG
static func createPrivateMessageWithInvalidSealSignatureForTesting(
content: String,
recipientPubkey: String,
senderIdentity: NostrIdentity
) throws -> NostrEvent {
let rumor = NostrEvent(
pubkey: senderIdentity.publicKeyHex,
createdAt: Date(),
kind: .dm,
tags: [],
content: content
)
var seal = try createSeal(
rumor: rumor,
recipientPubkey: recipientPubkey,
senderKey: senderIdentity.schnorrSigningKey()
)
seal.sig = String(repeating: "0", count: 128)
return try createGiftWrap(seal: seal, recipientPubkey: recipientPubkey)
}
static func createPrivateMessageWithMismatchedSealRumorPubkeyForTesting(
content: String,
recipientPubkey: String,
rumorIdentity: NostrIdentity,
sealSignerIdentity: NostrIdentity
) throws -> NostrEvent {
let rumor = NostrEvent(
pubkey: rumorIdentity.publicKeyHex,
createdAt: Date(),
kind: .dm,
tags: [],
content: content
)
let seal = try createSeal(
rumor: rumor,
recipientPubkey: recipientPubkey,
senderKey: sealSignerIdentity.schnorrSigningKey()
)
return try createGiftWrap(seal: seal, recipientPubkey: recipientPubkey)
}
#endif
/// Create a geohash-scoped ephemeral public message (kind 20000)
static func createEphemeralGeohashEvent(
content: String,
@@ -195,10 +257,9 @@ struct NostrProtocol {
private static func createGiftWrap(
seal: NostrEvent,
recipientPubkey: String,
senderKey: P256K.Schnorr.PrivateKey // This is the ephemeral key used for the seal
recipientPubkey: String
) throws -> NostrEvent {
let sealJSON = try seal.jsonString()
// Create new ephemeral key for gift wrap
@@ -267,52 +328,85 @@ struct NostrProtocol {
return try NostrEvent(from: rumorDict)
}
// MARK: - Encryption (NIP-44 v2)
private static func encrypt(
// MARK: - Encryption (NIP-44 v2/v3)
/// Whether outgoing DMs use the padded "v3:" envelope.
///
/// TWO-PHASE ROLLOUT DO NOT FLIP YET. Deployed clients hard-reject any
/// ciphertext that does not start with "v2:" (`decrypt` below, as shipped,
/// throws `invalidCiphertext` on unknown version prefixes), and the "v2:"
/// payload is the raw UTF-8 rumor JSON, so a padded payload cannot be
/// smuggled inside "v2:" without breaking old receivers either. Enabling
/// this today would strand every client in the field.
///
/// Phase 1 (this change): ship decrypt-side support for "v3:" everywhere.
/// Phase 2 (future release, once phase-1 clients are widely deployed):
/// set this to true so outgoing DMs stop leaking plaintext length to
/// relays.
static let sendPaddedEnvelope = false
/// Internal (rather than private) so tests can exercise both envelope
/// versions directly.
static func encrypt(
plaintext: String,
recipientPubkey: String,
senderKey: P256K.Schnorr.PrivateKey
senderKey: P256K.Schnorr.PrivateKey,
padded: Bool = NostrProtocol.sendPaddedEnvelope
) throws -> String {
guard let recipientPubkeyData = Data(hexString: recipientPubkey) else {
throw NostrError.invalidPublicKey
}
// Encrypting message (NIP-44 v2: XChaCha20-Poly1305, versioned)
// Encrypting message (XChaCha20-Poly1305, versioned envelope)
// Derive shared secret
let sharedSecret = try deriveSharedSecret(
privateKey: senderKey,
publicKey: recipientPubkeyData
)
// Derive NIP-44 v2 symmetric key (HKDF-SHA256 with label in info)
// Derive NIP-44 v2 symmetric key (HKDF-SHA256 with label in info).
// The v3 envelope deliberately reuses the same key derivation; it only
// changes the payload framing (length prefix + padding).
let key = try deriveNIP44V2Key(from: sharedSecret)
// 24-byte random nonce for XChaCha20-Poly1305
var nonce24 = Data(count: 24)
_ = nonce24.withUnsafeMutableBytes { ptr in
SecRandomCopyBytes(kSecRandomDefault, 24, ptr.baseAddress!)
}
// v2 payload: raw UTF-8 plaintext (length leaks to relays)
// v3 payload: NIP-44 style [2-byte BE length][plaintext][zero padding]
let pt = Data(plaintext.utf8)
let sealed = try XChaCha20Poly1305Compat.seal(plaintext: pt, key: key, nonce24: nonce24)
// v2: base64url(nonce24 || ciphertext || tag)
let payload = padded ? try NIP44Padding.pad(pt) : pt
let sealed = try XChaCha20Poly1305Compat.seal(plaintext: payload, key: key, nonce24: nonce24)
// version prefix + base64url(nonce24 || ciphertext || tag)
var combined = Data()
combined.append(nonce24)
combined.append(sealed.ciphertext)
combined.append(sealed.tag)
return "v2:" + Base64URLCoding.encode(combined)
return (padded ? "v3:" : "v2:") + Base64URLCoding.encode(combined)
}
private static func decrypt(
/// Internal (rather than private) so tests can exercise both envelope
/// versions directly.
static func decrypt(
ciphertext: String,
senderPubkey: String,
recipientKey: P256K.Schnorr.PrivateKey
) throws -> String {
// Expect NIP-44 v2 format
guard ciphertext.hasPrefix("v2:") else { throw NostrError.invalidCiphertext }
// Accept both the legacy unpadded "v2:" envelope and the padded "v3:"
// envelope (see `sendPaddedEnvelope` for the rollout plan).
let isPadded: Bool
if ciphertext.hasPrefix("v2:") {
isPadded = false
} else if ciphertext.hasPrefix("v3:") {
isPadded = true
} else {
throw NostrError.invalidCiphertext
}
let encoded = String(ciphertext.dropFirst(3))
guard let data = Base64URLCoding.decode(encoded),
data.count > (24 + 16),
@@ -338,18 +432,23 @@ struct NostrProtocol {
}
// If 32 bytes (x-only) try both parities, otherwise single try
let payload: Data
if senderPubkeyData.count == 32 {
let even = Data([0x02]) + senderPubkeyData
if let pt = try? attemptDecrypt(using: even) {
return String(data: pt, encoding: .utf8) ?? ""
payload = pt
} else {
let odd = Data([0x03]) + senderPubkeyData
payload = try attemptDecrypt(using: odd)
}
let odd = Data([0x03]) + senderPubkeyData
let pt = try attemptDecrypt(using: odd)
return String(data: pt, encoding: .utf8) ?? ""
} else {
let pt = try attemptDecrypt(using: senderPubkeyData)
return String(data: pt, encoding: .utf8) ?? ""
payload = try attemptDecrypt(using: senderPubkeyData)
}
// The AEAD tag has already authenticated the payload; unpadding
// failures here mean a malformed sender, not a wrong key.
let plaintextData = isPadded ? try NIP44Padding.unpad(payload) : payload
return String(data: plaintextData, encoding: .utf8) ?? ""
}
private static func deriveSharedSecret(
@@ -580,6 +679,60 @@ enum NostrError: Error {
case encryptionFailed
}
// MARK: - NIP-44 style padding (v3 envelope payload framing)
/// Payload framing for the padded "v3:" envelope, modeled on NIP-44 v2:
/// `[2-byte big-endian plaintext length][plaintext][zero padding]`, where the
/// total is padded to `paddedLength(for:)` power-of-two-derived buckets with
/// a 32-byte minimum so ciphertext length no longer reveals exact plaintext
/// length to relays.
enum NIP44Padding {
static let minPaddedLength = 32
static let maxPlaintextLength = 65535
/// NIP-44's calc_padded_len: pad to 32 bytes minimum, then to a chunk
/// granularity of max(32, nextPowerOfTwo/8).
static func paddedLength(for unpaddedLength: Int) -> Int {
guard unpaddedLength > minPaddedLength else { return minPaddedLength }
// Smallest power of two strictly greater than (unpaddedLength - 1).
let nextPower = 1 << (Int.bitWidth - (unpaddedLength - 1).leadingZeroBitCount)
let chunk = nextPower <= 256 ? 32 : nextPower / 8
return chunk * ((unpaddedLength - 1) / chunk + 1)
}
/// Prefix plaintext with its 2-byte big-endian length and zero-pad to the
/// bucketed length. Rejects empty plaintexts and plaintexts that do not
/// fit the 16-bit length prefix.
static func pad(_ plaintext: Data) throws -> Data {
let length = plaintext.count
guard length >= 1, length <= maxPlaintextLength else {
throw NostrError.encryptionFailed
}
let padded = paddedLength(for: length)
var result = Data(capacity: 2 + padded)
result.append(UInt8(length >> 8))
result.append(UInt8(length & 0xFF))
result.append(plaintext)
result.append(Data(count: padded - length))
return result
}
/// Read the 2-byte length prefix, validate the total padded size matches
/// it exactly, and return the plaintext. Throws on any inconsistency so a
/// malformed (already-authenticated) payload can never over- or
/// under-read.
static func unpad(_ padded: Data) throws -> Data {
guard padded.count >= 2 else { throw NostrError.invalidCiphertext }
let start = padded.startIndex
let length = Int(padded[start]) << 8 | Int(padded[start + 1])
guard length >= 1,
padded.count == 2 + paddedLength(for: length) else {
throw NostrError.invalidCiphertext
}
return padded.subdata(in: (start + 2)..<(start + 2 + length))
}
}
// MARK: - NIP-44 v2 helpers (XChaCha20-Poly1305)
private extension NostrProtocol {
+67
View File
@@ -281,6 +281,7 @@ final class NostrRelayManager: ObservableObject {
task.cancel(with: .goingAway, reason: nil)
}
connections.removeAll()
markRelaySocketsClosed(resetState: false)
// Sockets are gone, so per-relay subscription state is cleared but
// durable intent (subscriptionRequestState, messageHandlers, parked
// EOSE callbacks) is kept so REQs replay when relays reconnect
@@ -298,6 +299,60 @@ final class NostrRelayManager: ObservableObject {
torReadyWaitAttempts = 0
updateConnectionStatus()
}
/// Panic wipe reset: close sockets and drop every user/session-specific
/// relay intent without invoking old callbacks. Unlike `disconnect()`, this
/// must not preserve subscription replay state because geohash DM handlers
/// can capture pre-wipe Nostr private keys.
func resetForPanicWipe() {
connectionGeneration &+= 1
for (_, task) in connections {
task.cancel(with: .goingAway, reason: nil)
}
connections.removeAll()
markRelaySocketsClosed(resetState: true)
subscriptions.removeAll()
pendingSubscriptions.removeAll()
messageHandlers.removeAll()
subscriptionRequestState.removeAll()
subscribeCoalesce.removeAll()
eoseTrackers.removeAll()
pendingEOSECallbacks.removeAll()
pendingTorConnectionURLs.removeAll()
awaitingTorForConnections = false
torReadyWaitAttempts = 0
recentInboundEventKeys.removeAll()
recentInboundEventKeyOrder.removeAll()
duplicateInboundEventDropCount = 0
duplicateInboundEventDropCountBySubscription.removeAll()
inboundEventLogCount = 0
Self.pendingGiftWrapIDs.removeAll()
messageQueueLock.lock()
messageQueue.removeAll()
pendingSendDropCount = 0
messageQueueLock.unlock()
updateConnectionStatus()
}
private func markRelaySocketsClosed(resetState: Bool) {
let now = dependencies.now()
for index in relays.indices {
relays[index].isConnected = false
relays[index].nextReconnectTime = nil
if resetState {
relays[index].lastError = nil
relays[index].lastConnectedAt = nil
relays[index].lastDisconnectedAt = nil
relays[index].messagesSent = 0
relays[index].messagesReceived = 0
relays[index].reconnectAttempts = 0
} else {
relays[index].lastDisconnectedAt = now
}
}
}
/// Ensure connections exist to the given relay URLs (idempotent).
func ensureConnections(to relayUrls: [String]) {
@@ -1170,6 +1225,18 @@ final class NostrRelayManager: ObservableObject {
return Set(map.keys)
}
var debugMessageHandlerCount: Int {
messageHandlers.count
}
var debugSubscriptionRequestCount: Int {
subscriptionRequestState.count
}
var debugPendingEOSECallbackCount: Int {
pendingEOSECallbacks.count
}
var debugDuplicateInboundEventDropCount: Int {
duplicateInboundEventDropCount
}
@@ -168,8 +168,13 @@ final class BLEAnnounceHandler {
env.updateTopology(peerID, neighbors)
}
// Persist cryptographic identity and signing key for robust offline verification
env.persistIdentity(announcement)
// Persist cryptographic identity and signing key for robust offline
// verification only for verified announces. Persisting unverified
// announces would let an attacker who replays a victim's noisePublicKey
// overwrite the victim's stored signing key/nickname (identity poisoning).
if verifiedAnnounce {
env.persistIdentity(announcement)
}
let announceBackID = "announce-back-\(peerID)"
let shouldSendBack = !env.dedupContains(announceBackID)
@@ -16,6 +16,8 @@ struct BLEPublicMessageHandlerEnvironment {
let now: () -> Date
/// Snapshot of known peers keyed by ID (registry read).
let peersSnapshot: () -> [PeerID: BLEPeerInfo]
/// Verifies a packet's signature against a known signing public key.
let verifyPacketSignature: (_ packet: BitchatPacket, _ signingPublicKey: Data) -> Bool
/// Resolves a display name from a verified packet signature for peers missing from the registry.
let signedSenderDisplayName: (_ packet: BitchatPacket, _ peerID: PeerID) -> String?
/// Tracks the broadcast message packet for gossip sync.
@@ -68,14 +70,39 @@ final class BLEPublicMessageHandler {
// Snapshot peers to avoid concurrent mutation while iterating during nickname collision checks.
let peersSnapshot = env.peersSnapshot()
// Public messages are always signed by their sender. `senderID` is
// attacker-controlled, so registry membership alone is NOT proof of
// identity a peer in the registry as "verified" could be impersonated
// by anyone spoofing their senderID. Require a valid packet signature
// from the claimed sender (our own echoes are exempt; they are matched
// by self-broadcast tracking below).
//
// Verify against the signing key already in the (synchronously-updated)
// peer registry first: identity-cache persistence is asynchronous, so a
// message arriving right after a verified announce would otherwise be
// dropped because `signedSenderDisplayName` only searches the persisted
// cache. Fall back to that persisted-identity lookup for peers not (yet)
// in the registry.
let isSelf = peerID == env.localPeerID()
let registrySigningKey = peersSnapshot[peerID]?.signingPublicKey
let verifiedViaRegistry = !isSelf
&& (registrySigningKey.map { env.verifyPacketSignature(packet, $0) } ?? false)
let signedDisplayName = (isSelf || verifiedViaRegistry) ? nil : env.signedSenderDisplayName(packet, peerID)
guard isSelf || verifiedViaRegistry || signedDisplayName != nil else {
SecureLogger.warning("🚫 Dropping public message with missing/invalid signature for claimed sender \(peerID.id.prefix(8))", category: .security)
return
}
// Authenticity is established; prefer the registry's collision-resolved
// display name, then the signature-derived name.
guard let senderNickname = BLEPeerSenderDisplayName.resolveKnownPeer(
peerID: peerID,
localPeerID: env.localPeerID(),
localNickname: env.localNickname(),
peers: peersSnapshot,
allowConnectedUnverified: false
) ?? env.signedSenderDisplayName(packet, peerID) else {
SecureLogger.warning("🚫 Dropping public message from unverified or unknown peer \(peerID.id.prefix(8))", category: .security)
) ?? signedDisplayName else {
SecureLogger.warning("🚫 Dropping public message from unknown peer \(peerID.id.prefix(8))", category: .security)
return
}
@@ -12,7 +12,13 @@ struct BLEReceivedPacketContext: Equatable {
struct BLEReceivePipeline {
static func context(for packet: BitchatPacket, localPeerID: PeerID) -> BLEReceivedPacketContext {
let senderID = PeerID(hexData: packet.senderID)
let messageID = "\(senderID)-\(packet.timestamp)-\(packet.type)"
// Include a payload digest so that distinct packets sharing the same
// sender/timestamp(ms)/type are not collapsed as duplicates. The
// post-handshake flush sends queued messages, delivery and read receipts
// back-to-back within a single millisecond; without the digest every
// packet after the first would be silently dropped.
let digestPrefix = packet.payload.sha256Hash().prefix(4).hexEncodedString()
let messageID = "\(senderID)-\(packet.timestamp)-\(packet.type)-\(digestPrefix)"
let messageType = MessageType(rawValue: packet.type)
let allowSelfSyncReplay = packet.ttl == 0 && senderID == localPeerID
let shouldDeduplicate = messageType != .fragment && !allowSelfSyncReplay
+45 -13
View File
@@ -135,6 +135,13 @@ final class BLEService: NSObject {
private var maintenanceTimer: DispatchSourceTimer? // Single timer for all maintenance tasks
private var maintenanceCounter = 0 // Track maintenance cycles
/// Whether real CoreBluetooth managers were initialized. When false (unit
/// tests), periodic mesh background work is not started the maintenance
/// timer and the gossip-sync timers only drain BLE writes/notifications,
/// re-announce, and sign/broadcast sync packets, all meaningless without
/// Bluetooth. Leaving them running in the test process is pure background
/// churn that aggravates flaky exit hangs.
private var meshBackgroundEnabled = false
// MARK: - Connection budget & scheduling (central role)
private var connectionScheduler = BLEConnectionScheduler<CBPeripheral>()
@@ -233,16 +240,10 @@ final class BLEService: NSObject {
#endif
}
// Single maintenance timer for all periodic tasks (dispatch-based for determinism)
let timer = DispatchSource.makeTimerSource(queue: bleQueue)
timer.schedule(deadline: .now() + TransportConfig.bleMaintenanceInterval,
repeating: TransportConfig.bleMaintenanceInterval,
leeway: .seconds(TransportConfig.bleMaintenanceLeewaySeconds))
timer.setEventHandler { [weak self] in
self?.performMaintenance()
}
timer.resume()
maintenanceTimer = timer
// Single maintenance timer for all periodic tasks (dispatch-based for
// determinism). Only run it when real Bluetooth managers exist.
meshBackgroundEnabled = initializeBluetoothManagers
startMaintenanceTimer()
// Publish initial empty state
requestPeerDataPublish()
@@ -272,7 +273,12 @@ final class BLEService: NSObject {
let manager = GossipSyncManager(myPeerID: myPeerID, config: config, requestSyncManager: requestSyncManager)
manager.delegate = self
manager.start()
// Only start the periodic sync timers when real Bluetooth exists. In unit
// tests there is no mesh to sync with, and the periodic sign/broadcast
// churn just keeps the process busy and aggravates flaky exit hangs.
if meshBackgroundEnabled {
manager.start()
}
gossipSyncManager = manager
}
@@ -435,7 +441,29 @@ final class BLEService: NSObject {
// MARK: Lifecycle
/// Creates and starts the periodic maintenance timer if it is not already
/// running. Idempotent so it can be called from both `init` and
/// `startServices()` the latter matters after a panic reset, where
/// `stopServices()` cancels and nils the timer.
private func startMaintenanceTimer() {
guard meshBackgroundEnabled, maintenanceTimer == nil else { return }
let timer = DispatchSource.makeTimerSource(queue: bleQueue)
timer.schedule(deadline: .now() + TransportConfig.bleMaintenanceInterval,
repeating: TransportConfig.bleMaintenanceInterval,
leeway: .seconds(TransportConfig.bleMaintenanceLeewaySeconds))
timer.setEventHandler { [weak self] in
self?.performMaintenance()
}
timer.resume()
maintenanceTimer = timer
}
func startServices() {
// Restart the maintenance timer if a prior stopServices() cancelled it
// (e.g. the panic flow), otherwise periodic announces, peer reconciliation
// and cache cleanup would never resume until app restart.
startMaintenanceTimer()
// Start BLE services if not already running
if centralManager?.state == .poweredOn {
centralManager?.scanForPeripherals(
@@ -1602,7 +1630,7 @@ private extension BLEService {
#if DEBUG
// Test-only helper to inject packets into the receive pipeline
extension BLEService {
func _test_handlePacket(_ packet: BitchatPacket, fromPeerID: PeerID, preseedPeer: Bool = true) {
func _test_handlePacket(_ packet: BitchatPacket, fromPeerID: PeerID, preseedPeer: Bool = true, signingPublicKey: Data? = nil) {
if preseedPeer {
// Ensure the synthetic peer is known and marked verified for public-message tests
let normalizedID = PeerID(hexData: packet.senderID)
@@ -1610,6 +1638,7 @@ extension BLEService {
if var existing = peerRegistry.info(for: normalizedID) {
existing.isConnected = true
existing.isVerifiedNickname = true
if let signingPublicKey { existing.signingPublicKey = signingPublicKey }
existing.lastSeen = Date()
peerRegistry.upsert(existing)
} else {
@@ -1618,7 +1647,7 @@ extension BLEService {
nickname: "TestPeer_\(fromPeerID.id.prefix(4))",
isConnected: true,
noisePublicKey: packet.senderID,
signingPublicKey: nil,
signingPublicKey: signingPublicKey,
isVerifiedNickname: true,
lastSeen: Date()
))
@@ -3110,6 +3139,9 @@ extension BLEService {
guard let self = self else { return [:] }
return self.collectionsQueue.sync { self.peerRegistry.snapshotByID }
},
verifyPacketSignature: { [weak self] packet, signingPublicKey in
self?.noiseService.verifyPacketSignature(packet, publicKey: signingPublicKey) ?? false
},
signedSenderDisplayName: { [weak self] packet, peerID in
self?.signedSenderDisplayName(for: packet, from: peerID)
},
@@ -594,6 +594,22 @@ final class LocationStateManager: NSObject, CLLocationManagerDelegate, Observabl
}
}
/// Removes all persisted location state and resets the in-memory view.
/// Used by the panic wipe selected channel, teleport set and bookmarks
/// (which reveal where the user has been) must not survive on device.
func panicWipe() {
storage.removeObject(forKey: selectedChannelKey)
storage.removeObject(forKey: teleportedStoreKey)
storage.removeObject(forKey: bookmarksKey)
storage.removeObject(forKey: bookmarkNamesKey)
teleportedSet.removeAll()
bookmarkMembership.removeAll()
bookmarks = []
bookmarkNames = [:]
teleported = false
selectedChannel = .mesh
}
private static func normalizeGeohash(_ s: String) -> String {
let allowed = Set("0123456789bcdefghjkmnpqrstuvwxyz")
return s
+37 -10
View File
@@ -1129,6 +1129,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
userDefaults.removeObject(forKey: "bitchat.noiseIdentityKey")
userDefaults.removeObject(forKey: "bitchat.messageRetentionKey")
// Wipe persisted location state (selected channel, teleport set,
// bookmarks). For an activist-safety wipe, where the user has been is
// exactly the data an adversary inspecting the device wants.
LocationStateManager.shared.panicWipe()
// Reset nickname to anonymous
nickname = "anon\(Int.random(in: 1000...9999))"
saveNickname()
@@ -1153,13 +1158,25 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
// Clear selected private chat
selectedPrivateChatPeer = nil
// Clear live location/geohash session state. Persisted location state
// was wiped above, but the running view model can still be scoped to a
// geohash channel and hold subscriptions tied to the old Nostr identity.
activeChannel = .mesh
setGeoChatSubscriptionID(nil)
setGeoDmSubscriptionID(nil)
_ = clearGeoSamplingSubs()
cachedGeohashIdentity = nil
nostrKeyMapping.removeAll()
// Clear read receipt tracking
sentReadReceipts.removeAll()
deduplicationService.clearAll()
// IMPORTANT: Clear Nostr-related state
// Disconnect from Nostr relays and clear subscriptions
nostrRelayManager?.disconnect()
// Drop relay subscriptions, handlers, pending sends, and replay state.
// Geohash DM handlers can capture pre-wipe Nostr identities, so a plain
// disconnect is not enough here.
NostrRelayManager.shared.resetForPanicWipe()
nostrRelayManager = nil
// Clear Nostr identity associations
@@ -1175,15 +1192,25 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
// No need to force UserDefaults synchronization
// Reinitialize Nostr with new identity
// This will generate new Nostr keys derived from new Noise keys
Task { @MainActor in
// Small delay to ensure cleanup completes
try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncShortSleepNs) // 0.1 seconds
// This will generate new Nostr keys derived from new Noise keys.
// Skipped under tests: connecting the shared relay singleton starts
// real network/reconnect work that never completes and would keep the
// test process alive (the singleton, unlike a discardable instance, is
// never deallocated to cancel it).
if !TestEnvironment.isRunningTests {
Task { @MainActor in
// Small delay to ensure cleanup completes
try? await Task.sleep(nanoseconds: TransportConfig.uiAsyncShortSleepNs) // 0.1 seconds
// Reinitialize Nostr relay manager with new identity
nostrRelayManager = NostrRelayManager()
setupNostrMessageHandling()
nostrRelayManager?.connect()
// Reinitialize Nostr relay manager with new identity. Reuse the
// shared singleton every other component (NostrTransport, geohash
// subscriptions, AppRuntime observers) is bound to `.shared`, so
// creating a fresh instance here would split relay state and leave
// sends running against a disconnected manager.
nostrRelayManager = NostrRelayManager.shared
setupNostrMessageHandling()
nostrRelayManager?.connect()
}
}
// Delete ALL media files (incoming and outgoing) in background
+10 -4
View File
@@ -14,23 +14,29 @@ import BitFoundation
struct BLEServiceCoreTests {
@Test
func duplicatePacket_isDeduped() async {
func duplicatePacket_isDeduped() async throws {
let ble = makeService()
let delegate = PublicCaptureDelegate()
ble.delegate = delegate
// Public messages must carry a valid signature from the claimed sender;
// sign the packet and preseed the sender's signing key so the receiver
// can verify it (production `sendMessage` signs public broadcasts too).
let signer = NoiseEncryptionService(keychain: MockKeychain())
let sender = PeerID(str: "1122334455667788")
let timestamp = UInt64(Date().timeIntervalSince1970 * 1000)
let packet = makePublicPacket(content: "Hello", sender: sender, timestamp: timestamp)
let unsigned = makePublicPacket(content: "Hello", sender: sender, timestamp: timestamp)
let packet = try #require(signer.signPacket(unsigned), "Failed to sign public message")
let signingKey = signer.getSigningPublicKeyData()
ble._test_handlePacket(packet, fromPeerID: sender)
ble._test_handlePacket(packet, fromPeerID: sender, signingPublicKey: signingKey)
let receivedFirst = await TestHelpers.waitUntil(
{ delegate.publicMessagesSnapshot().count == 1 },
timeout: TestConstants.defaultTimeout
)
#expect(receivedFirst)
ble._test_handlePacket(packet, fromPeerID: sender)
ble._test_handlePacket(packet, fromPeerID: sender, signingPublicKey: signingKey)
let receivedDuplicate = await TestHelpers.waitUntil(
{ delegate.publicMessagesSnapshot().count > 1 },
timeout: TestConstants.shortTimeout
+32
View File
@@ -1042,6 +1042,38 @@ struct ChatViewModelPanicTests {
#expect(viewModel.unreadPrivateMessages.isEmpty)
#expect(viewModel.selectedPrivateChatPeer == nil)
}
@Test @MainActor
func panicClearAllData_resetsLiveGeohashAndNostrState() async throws {
let (viewModel, _) = makeTestableViewModel()
let geohash = "u4pruy"
let channel = GeohashChannel(level: .city, geohash: geohash)
let identity = try NostrIdentity.generate()
let pubkey = String(repeating: "ab", count: 32)
let peerID = PeerID(nostr: pubkey)
viewModel.activeChannel = .location(channel)
viewModel.setGeoChatSubscriptionID("geo-\(geohash)")
viewModel.setGeoDmSubscriptionID("geo-dm-\(geohash)")
viewModel.addGeoSamplingSub("geo-sample-\(geohash)", forGeohash: geohash)
viewModel.cachedGeohashIdentity = (geohash, identity)
viewModel.registerNostrKeyMapping(pubkey, for: peerID)
viewModel.currentGeohash = geohash
viewModel.geoNicknames = [pubkey: "alice"]
viewModel.teleportedGeo = [pubkey]
viewModel.panicClearAllData()
#expect(viewModel.activeChannel == .mesh)
#expect(viewModel.geoSubscriptionID == nil)
#expect(viewModel.geoDmSubscriptionID == nil)
#expect(viewModel.geoSamplingSubs.isEmpty)
#expect(viewModel.cachedGeohashIdentity == nil)
#expect(viewModel.nostrKeyMapping.isEmpty)
#expect(viewModel.currentGeohash == nil)
#expect(viewModel.geoNicknames.isEmpty)
#expect(viewModel.teleportedGeo.isEmpty)
}
}
// MARK: - Service Lifecycle Tests
@@ -21,9 +21,16 @@ struct FragmentationTests {
let capture = CaptureDelegate()
ble.delegate = capture
// Construct a big packet (3KB) from a remote sender (not our own ID)
// Construct a big SIGNED public packet (3KB) from a remote sender. Public
// messages must carry a valid signature, so the reassembled packet is
// signed and the sender's signing key is preseeded into the registry.
let signer = NoiseEncryptionService(keychain: MockKeychain())
let signingKey = signer.getSigningPublicKeyData()
let remoteShortID = PeerID(str: "1122334455667788")
let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 3_000)
let original = try #require(
signer.signPacket(makeLargePublicPacket(senderShortHex: remoteShortID, size: 3_000)),
"Failed to sign public packet"
)
// Use a small fragment size to ensure multiple pieces
let fragments = fragmentPacket(original, fragmentSize: 400)
@@ -36,7 +43,7 @@ struct FragmentationTests {
if i > 0 {
try await Task.sleep(for: .milliseconds(5))
}
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
ble._test_handlePacket(fragment, fromPeerID: remoteShortID, signingPublicKey: signingKey)
}
// Wait for delegate callback with proper timeout
@@ -52,8 +59,13 @@ struct FragmentationTests {
let capture = CaptureDelegate()
ble.delegate = capture
let signer = NoiseEncryptionService(keychain: MockKeychain())
let signingKey = signer.getSigningPublicKeyData()
let remoteShortID = PeerID(str: "A1B2C3D4E5F60708")
let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 2048)
let original = try #require(
signer.signPacket(makeLargePublicPacket(senderShortHex: remoteShortID, size: 2048)),
"Failed to sign public packet"
)
var frags = fragmentPacket(original, fragmentSize: 300)
// Duplicate one fragment
@@ -66,7 +78,7 @@ struct FragmentationTests {
if i > 0 {
try await Task.sleep(for: .milliseconds(5))
}
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
ble._test_handlePacket(fragment, fromPeerID: remoteShortID, signingPublicKey: signingKey)
}
// Wait for delegate callback with proper timeout
+200
View File
@@ -120,6 +120,42 @@ struct NostrProtocolTests {
}
}
@Test func decryptRejectsInvalidSealSignature() throws {
let sender = try NostrIdentity.generate()
let recipient = try NostrIdentity.generate()
let giftWrap = try NostrProtocol.createPrivateMessageWithInvalidSealSignatureForTesting(
content: "forged signature",
recipientPubkey: recipient.publicKeyHex,
senderIdentity: sender
)
expectInvalidEvent {
_ = try NostrProtocol.decryptPrivateMessage(
giftWrap: giftWrap,
recipientIdentity: recipient
)
}
}
@Test func decryptRejectsSealRumorPubkeyMismatch() throws {
let claimedSender = try NostrIdentity.generate()
let sealSigner = try NostrIdentity.generate()
let recipient = try NostrIdentity.generate()
let giftWrap = try NostrProtocol.createPrivateMessageWithMismatchedSealRumorPubkeyForTesting(
content: "spoofed sender",
recipientPubkey: recipient.publicKeyHex,
rumorIdentity: claimedSender,
sealSignerIdentity: sealSigner
)
expectInvalidEvent {
_ = try NostrProtocol.decryptPrivateMessage(
giftWrap: giftWrap,
recipientIdentity: recipient
)
}
}
func testAckRoundTripNIP44V2_Delivered() throws {
// Identities
let sender = try NostrIdentity.generate()
@@ -253,6 +289,159 @@ struct NostrProtocolTests {
#expect(object["limit"] as? Int == 42)
}
// MARK: - Padding (v3 envelope)
@Test func paddedLengthMatchesNIP44Buckets() {
// Vectors from the NIP-44 reference test suite (calc_padded_len).
let vectors: [(Int, Int)] = [
(1, 32), (16, 32), (32, 32), (33, 64), (37, 64), (45, 64), (49, 64),
(64, 64), (65, 96), (100, 128), (111, 128), (200, 224), (250, 256),
(320, 320), (383, 384), (384, 384), (400, 448), (500, 512),
(512, 512), (515, 640), (700, 768), (800, 896), (900, 1024),
(1020, 1024), (65535, 65536)
]
for (unpadded, expected) in vectors {
#expect(
NIP44Padding.paddedLength(for: unpadded) == expected,
"paddedLength(for: \(unpadded)) should be \(expected)"
)
}
}
@Test func padUnpadRoundTrip() throws {
for length in [1, 2, 31, 32, 33, 100, 320, 1020, 4096, 65535] {
let plaintext = Data((0..<length).map { _ in UInt8.random(in: .min ... .max) })
let padded = try NIP44Padding.pad(plaintext)
#expect(padded.count == 2 + NIP44Padding.paddedLength(for: length))
let unpadded = try NIP44Padding.unpad(padded)
#expect(unpadded == plaintext)
}
}
@Test func padHidesExactLengthWithinBucket() throws {
// Two plaintexts of different length in the same bucket must produce
// identically sized padded payloads (and thus ciphertexts).
let short = try NIP44Padding.pad(Data(repeating: 0x41, count: 65))
let long = try NIP44Padding.pad(Data(repeating: 0x42, count: 96))
#expect(short.count == long.count)
}
@Test func padRejectsOutOfRangePlaintexts() {
#expect(throws: (any Error).self) { try NIP44Padding.pad(Data()) }
#expect(throws: (any Error).self) { try NIP44Padding.pad(Data(count: 65536)) }
}
@Test func unpadRejectsTamperedLengthPrefix() throws {
var padded = try NIP44Padding.pad(Data(repeating: 0x41, count: 40))
// Claimed length larger than the actual payload
var tooLong = padded
tooLong[tooLong.startIndex] = 0xFF
tooLong[tooLong.startIndex + 1] = 0xFF
#expect(throws: NostrError.invalidCiphertext) { try NIP44Padding.unpad(tooLong) }
// Claimed length of zero
var zero = padded
zero[zero.startIndex] = 0x00
zero[zero.startIndex + 1] = 0x00
#expect(throws: NostrError.invalidCiphertext) { try NIP44Padding.unpad(zero) }
// Claimed length whose bucket does not match the payload size
// (payload is bucket 64; a claimed length of 20 expects bucket 32)
var wrongBucket = padded
wrongBucket[wrongBucket.startIndex] = 0x00
wrongBucket[wrongBucket.startIndex + 1] = 0x14
#expect(throws: NostrError.invalidCiphertext) { try NIP44Padding.unpad(wrongBucket) }
// Truncated payloads
#expect(throws: NostrError.invalidCiphertext) { try NIP44Padding.unpad(Data()) }
#expect(throws: NostrError.invalidCiphertext) { try NIP44Padding.unpad(Data([0x00])) }
padded.removeLast()
#expect(throws: NostrError.invalidCiphertext) { try NIP44Padding.unpad(padded) }
// Works on Data slices with non-zero startIndex
let sliced = try (Data([0xAB]) + NIP44Padding.pad(Data(repeating: 0x41, count: 40))).dropFirst()
#expect(try NIP44Padding.unpad(sliced) == Data(repeating: 0x41, count: 40))
}
@Test func paddedEnvelopeRoundTrip_v3() throws {
let sender = try NostrIdentity.generate()
let recipient = try NostrIdentity.generate()
let plaintext = "padded envelope test"
let ciphertext = try NostrProtocol.encrypt(
plaintext: plaintext,
recipientPubkey: recipient.publicKeyHex,
senderKey: sender.schnorrSigningKey(),
padded: true
)
#expect(ciphertext.hasPrefix("v3:"))
let decrypted = try NostrProtocol.decrypt(
ciphertext: ciphertext,
senderPubkey: sender.publicKeyHex,
recipientKey: recipient.schnorrSigningKey()
)
#expect(decrypted == plaintext)
}
@Test func legacyUnpaddedEnvelopeStillDecrypts_v2() throws {
let sender = try NostrIdentity.generate()
let recipient = try NostrIdentity.generate()
let plaintext = "legacy v2 envelope"
// What deployed clients send today.
let ciphertext = try NostrProtocol.encrypt(
plaintext: plaintext,
recipientPubkey: recipient.publicKeyHex,
senderKey: sender.schnorrSigningKey(),
padded: false
)
#expect(ciphertext.hasPrefix("v2:"))
let decrypted = try NostrProtocol.decrypt(
ciphertext: ciphertext,
senderPubkey: sender.publicKeyHex,
recipientKey: recipient.schnorrSigningKey()
)
#expect(decrypted == plaintext)
}
@Test func outgoingMessagesStillUseV2UntilRolloutFlagFlips() throws {
// Deployed clients reject anything that is not "v2:", so the padded
// envelope must stay off by default until decrypt-side support is
// widely shipped (see NostrProtocol.sendPaddedEnvelope).
#expect(NostrProtocol.sendPaddedEnvelope == false)
let sender = try NostrIdentity.generate()
let recipient = try NostrIdentity.generate()
let giftWrap = try NostrProtocol.createPrivateMessage(
content: "default envelope",
recipientPubkey: recipient.publicKeyHex,
senderIdentity: sender
)
#expect(giftWrap.content.hasPrefix("v2:"))
}
@Test func decryptRejectsUnknownEnvelopeVersion() throws {
let sender = try NostrIdentity.generate()
let recipient = try NostrIdentity.generate()
let ciphertext = try NostrProtocol.encrypt(
plaintext: "test",
recipientPubkey: recipient.publicKeyHex,
senderKey: sender.schnorrSigningKey(),
padded: false
)
let mutated = "v9:" + ciphertext.dropFirst(3)
#expect(throws: NostrError.invalidCiphertext) {
_ = try NostrProtocol.decrypt(
ciphertext: mutated,
senderPubkey: sender.publicKeyHex,
recipientKey: recipient.schnorrSigningKey()
)
}
}
// MARK: - Helpers
private static func base64URLDecode(_ s: String) -> Data? {
var str = s.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
@@ -260,4 +449,15 @@ struct NostrProtocolTests {
if rem > 0 { str.append(String(repeating: "=", count: 4 - rem)) }
return Data(base64Encoded: str)
}
private func expectInvalidEvent(_ operation: () throws -> Void) {
do {
try operation()
Issue.record("Expected NostrError.invalidEvent")
} catch NostrError.invalidEvent {
return
} catch {
Issue.record("Expected NostrError.invalidEvent, got \(error)")
}
}
}
@@ -173,7 +173,10 @@ struct BLEAnnounceHandlerTests {
#expect(recorder.uiEventDeliveries.count == 1)
#expect(recorder.uiEventDeliveries.first?.notifyPeerConnected == false)
#expect(recorder.uiEventDeliveries.first?.scheduleInitialSync == false)
#expect(recorder.persistedIdentities.count == 1)
// Identity persistence MUST NOT occur for unverified announces:
// persisting would let an attacker who replays a victim's noisePublicKey
// overwrite the victim's stored signing key/nickname (identity poisoning).
#expect(recorder.persistedIdentities.isEmpty)
#expect(recorder.trackedPackets.count == 1)
#expect(recorder.announceBacks == 1)
}
@@ -8,10 +8,12 @@ struct BLEPublicMessageHandlerTests {
var localNickname = "Me"
var peers: [PeerID: BLEPeerInfo] = [:]
var signedName: String?
var verifyPacketSignatureResult = false
var linkState: (hasPeripheral: Bool, hasCentral: Bool) = (false, false)
var selfBroadcastMessageID: String?
var peersSnapshotReads = 0
var verifyPacketSignatureQueries: [PeerID] = []
var signedNameQueries: [PeerID] = []
var trackedPackets: [BitchatPacket] = []
var selfBroadcastTakes: [BitchatPacket] = []
@@ -35,6 +37,10 @@ struct BLEPublicMessageHandlerTests {
recorder.peersSnapshotReads += 1
return recorder.peers
},
verifyPacketSignature: { packet, _ in
recorder.verifyPacketSignatureQueries.append(PeerID(hexData: packet.senderID))
return recorder.verifyPacketSignatureResult
},
signedSenderDisplayName: { _, peerID in
recorder.signedNameQueries.append(peerID)
return recorder.signedName
@@ -59,13 +65,17 @@ struct BLEPublicMessageHandlerTests {
let now = Date(timeIntervalSince1970: 1_000)
let recorder = Recorder()
recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true)]
// A valid packet signature is required even for a registry-verified peer:
// senderID is spoofable, so registry membership alone is not authentication.
recorder.signedName = "SignedAlice"
let handler = makeHandler(recorder: recorder, now: now)
let packet = makeMessagePacket(sender: remotePeerID, content: "hello mesh", timestamp: timestamp(now))
handler.handle(packet, from: remotePeerID)
#expect(recorder.peersSnapshotReads == 1)
#expect(recorder.signedNameQueries.isEmpty)
// Signature is verified, then the registry's collision-resolved name is preferred.
#expect(recorder.signedNameQueries == [remotePeerID])
#expect(recorder.trackedPackets.count == 1)
#expect(recorder.selfBroadcastTakes.isEmpty)
#expect(recorder.deliveries.count == 1)
@@ -133,6 +143,63 @@ struct BLEPublicMessageHandlerTests {
#expect(recorder.deliveries.isEmpty)
}
@Test
func registryVerifiedPeerDeliveredBeforeIdentityCachePersists() {
// A freshly verified announce updates the peer registry synchronously,
// but identity-cache persistence is async. A message arriving in that
// window has a valid signature and a registry signing key, yet the
// persisted-identity lookup (signedName) would still return nil. It must
// be verified against the registry key and delivered, not dropped.
let now = Date(timeIntervalSince1970: 1_000)
let recorder = Recorder()
recorder.peers = [remotePeerID: makePeerInfo(
remotePeerID,
nickname: "Alice",
isVerified: true,
signingPublicKey: Data(repeating: 0xAB, count: 32)
)]
recorder.verifyPacketSignatureResult = true
recorder.signedName = nil
let handler = makeHandler(recorder: recorder, now: now)
let packet = makeMessagePacket(sender: remotePeerID, content: "first msg", timestamp: timestamp(now))
handler.handle(packet, from: remotePeerID)
#expect(recorder.verifyPacketSignatureQueries == [remotePeerID])
// Verified via the registry key, so no fallback to the persisted lookup.
#expect(recorder.signedNameQueries.isEmpty)
#expect(recorder.trackedPackets.count == 1)
#expect(recorder.deliveries.count == 1)
#expect(recorder.deliveries.first?.nickname == "Alice")
#expect(recorder.deliveries.first?.content == "first msg")
}
@Test
func registryPeerWithInvalidSignatureFallsBackAndDrops() {
// Spoofed senderID: the peer is in the registry with a signing key, but
// the packet signature does not verify against it. The handler must fall
// back to the persisted lookup and, finding nothing, drop the message.
let now = Date(timeIntervalSince1970: 1_000)
let recorder = Recorder()
recorder.peers = [remotePeerID: makePeerInfo(
remotePeerID,
nickname: "Alice",
isVerified: true,
signingPublicKey: Data(repeating: 0xAB, count: 32)
)]
recorder.verifyPacketSignatureResult = false
recorder.signedName = nil
let handler = makeHandler(recorder: recorder, now: now)
let packet = makeMessagePacket(sender: remotePeerID, content: "spoofed", timestamp: timestamp(now))
handler.handle(packet, from: remotePeerID)
#expect(recorder.verifyPacketSignatureQueries == [remotePeerID])
#expect(recorder.signedNameQueries == [remotePeerID])
#expect(recorder.trackedPackets.isEmpty)
#expect(recorder.deliveries.isEmpty)
}
@Test
func signedSenderFallbackDeliversWithSignedName() {
let now = Date(timeIntervalSince1970: 1_000)
@@ -154,6 +221,7 @@ struct BLEPublicMessageHandlerTests {
let now = Date(timeIntervalSince1970: 1_000)
let recorder = Recorder()
recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true)]
recorder.signedName = "SignedAlice"
let handler = makeHandler(recorder: recorder, now: now)
let packet = makeMessagePacket(sender: remotePeerID, payload: Data([0xFF, 0xFE, 0xFD]), timestamp: timestamp(now))
@@ -187,6 +255,7 @@ struct BLEPublicMessageHandlerTests {
let now = Date(timeIntervalSince1970: 1_000)
let recorder = Recorder()
recorder.peers = [remotePeerID: makePeerInfo(remotePeerID, nickname: "Alice", isVerified: true)]
recorder.signedName = "SignedAlice"
let handler = makeHandler(recorder: recorder, now: now)
let packet = makeMessagePacket(
sender: remotePeerID,
@@ -213,14 +282,15 @@ struct BLEPublicMessageHandlerTests {
_ peerID: PeerID,
nickname: String,
isVerified: Bool,
isConnected: Bool = true
isConnected: Bool = true,
signingPublicKey: Data? = nil
) -> BLEPeerInfo {
BLEPeerInfo(
peerID: peerID,
nickname: nickname,
isConnected: isConnected,
noisePublicKey: nil,
signingPublicKey: nil,
signingPublicKey: signingPublicKey,
isVerifiedNickname: isVerified,
lastSeen: Date(timeIntervalSince1970: 999)
)
@@ -14,7 +14,10 @@ struct BLEReceivePipelineTests {
let context = BLEReceivePipeline.context(for: packet, localPeerID: local)
#expect(context.senderID == sender)
#expect(context.messageID == "\(sender)-1234-\(MessageType.message.rawValue)")
// The message ID includes a payload digest so distinct packets sharing a
// sender/timestamp(ms)/type are not collapsed as duplicates.
let digest = packet.payload.sha256Hash().prefix(4).hexEncodedString()
#expect(context.messageID == "\(sender)-1234-\(MessageType.message.rawValue)-\(digest)")
#expect(context.messageType == .message)
#expect(context.shouldDeduplicate)
#expect(context.logsHandlingDetails)
@@ -1328,6 +1328,68 @@ final class NostrRelayManagerTests: XCTestCase {
XCTAssertEqual(context.manager.debugPendingSubscriptionCount(for: relayURL), 0)
}
func test_resetForPanicWipe_dropsSessionRelayStateWithoutFiringCallbacks() async throws {
let relayURL = "wss://panic-reset.example"
let context = makeContext(permission: .denied, userTorEnabled: true, torEnforced: true, torIsReady: false)
let event = try makeSignedEvent(content: "queued before panic")
var handledEvents = 0
var eoseCount = 0
context.manager.subscribe(
filter: makeFilter(),
id: "panic-sub",
relayUrls: [relayURL],
handler: { _ in handledEvents += 1 },
onEOSE: { eoseCount += 1 }
)
context.manager.sendEvent(event, to: [relayURL])
XCTAssertEqual(context.manager.debugMessageHandlerCount, 1)
XCTAssertEqual(context.manager.debugSubscriptionRequestCount, 1)
XCTAssertEqual(context.manager.debugPendingSubscriptionCount(for: relayURL), 1)
XCTAssertEqual(context.manager.debugPendingEOSECallbackCount, 1)
XCTAssertEqual(context.manager.debugPendingMessageQueueCount, 1)
XCTAssertTrue(context.sessionFactory.requestedURLs.isEmpty)
context.manager.resetForPanicWipe()
XCTAssertEqual(context.manager.debugMessageHandlerCount, 0)
XCTAssertEqual(context.manager.debugSubscriptionRequestCount, 0)
XCTAssertEqual(context.manager.debugPendingSubscriptionCount(for: relayURL), 0)
XCTAssertEqual(context.manager.debugPendingEOSECallbackCount, 0)
XCTAssertEqual(context.manager.debugPendingMessageQueueCount, 0)
XCTAssertEqual(handledEvents, 0)
XCTAssertEqual(eoseCount, 0)
// Stale Tor wait and fallback callbacks from the pre-wipe generation
// must not resurrect connections or settle callbacks after reset.
context.torWaiter.resolve(true)
context.scheduler.runNext()
try? await Task.sleep(nanoseconds: 20_000_000)
XCTAssertTrue(context.sessionFactory.requestedURLs.isEmpty)
XCTAssertEqual(eoseCount, 0)
}
func test_resetForPanicWipe_marksConnectedRelaysDisconnected() async {
let relayURL = "wss://panic-connected.example"
let context = makeContext(permission: .denied)
context.manager.ensureConnections(to: [relayURL])
let connected = await waitUntil {
context.manager.isConnected &&
context.manager.relays.first(where: { $0.url == relayURL })?.isConnected == true
}
XCTAssertTrue(connected)
context.manager.resetForPanicWipe()
XCTAssertFalse(context.manager.isConnected)
XCTAssertEqual(context.manager.relays.first(where: { $0.url == relayURL })?.isConnected, false)
XCTAssertEqual(context.manager.relays.first(where: { $0.url == relayURL })?.reconnectAttempts, 0)
XCTAssertNil(context.manager.relays.first(where: { $0.url == relayURL })?.lastError)
}
func test_reconnectBackoff_appliesJitterWithinConfiguredBounds() async {
let relayURL = "wss://jitter-bounds.example"
// Pin the jitter source to the extremes and the midpoint of [0, 1).
@@ -8,12 +8,44 @@
import struct Foundation.Data
/// Lowercase hex digits used by `hexEncodedString()`.
private let hexDigits: [UInt8] = Array("0123456789abcdef".utf8)
/// Maps an ASCII byte to its hex nibble value, or nil for non-hex characters.
/// Accepts both lowercase and uppercase hex digits.
@inline(__always)
private func hexNibble(_ ascii: UInt8) -> UInt8? {
switch ascii {
case UInt8(ascii: "0")...UInt8(ascii: "9"):
return ascii - UInt8(ascii: "0")
case UInt8(ascii: "a")...UInt8(ascii: "f"):
return ascii - UInt8(ascii: "a") + 10
case UInt8(ascii: "A")...UInt8(ascii: "F"):
return ascii - UInt8(ascii: "A") + 10
default:
return nil
}
}
public extension Data {
/// Lowercase hex representation of the bytes.
///
/// Lookup-table based: this sits on the hot BLE receive path (it is called
/// several times per received packet via `PeerID(hexData:)`), where the
/// previous per-byte `String(format: "%02x", _)` implementation spent most
/// of its time re-parsing the format string through Foundation.
func hexEncodedString() -> String {
if self.isEmpty {
if isEmpty {
return ""
}
return self.map { String(format: "%02x", $0) }.joined()
var output = [UInt8](repeating: 0, count: count * 2)
var i = 0
for byte in self {
output[i] = hexDigits[Int(byte >> 4)]
output[i + 1] = hexDigits[Int(byte & 0x0F)]
i += 2
}
return String(decoding: output, as: UTF8.self)
}
/// Initialize Data from a hex string.
@@ -28,28 +60,28 @@ public extension Data {
hex = String(hex.dropFirst(2))
}
let ascii = Array(hex.utf8)
// Reject odd-length strings
guard hex.count % 2 == 0 else {
guard ascii.count % 2 == 0 else {
return nil
}
// Reject empty strings
guard !hex.isEmpty else {
// Accept empty strings
guard !ascii.isEmpty else {
self = Data()
return
}
let len = hex.count / 2
var data = Data(capacity: len)
var index = hex.startIndex
for _ in 0..<len {
let nextIndex = hex.index(index, offsetBy: 2)
guard let byte = UInt8(String(hex[index..<nextIndex]), radix: 16) else {
var data = Data(capacity: ascii.count / 2)
var index = 0
while index < ascii.count {
guard let high = hexNibble(ascii[index]),
let low = hexNibble(ascii[index + 1]) else {
return nil
}
data.append(byte)
index = nextIndex
data.append((high << 4) | low)
index += 2
}
self = data
@@ -0,0 +1,78 @@
//
// DataHexTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Testing
import Foundation
@testable import BitFoundation
struct DataHexTests {
// MARK: - Encoding
@Test func encode_knownVectors() {
#expect(Data().hexEncodedString() == "")
#expect(Data([0x00]).hexEncodedString() == "00")
#expect(Data([0x0f]).hexEncodedString() == "0f")
#expect(Data([0xf0]).hexEncodedString() == "f0")
#expect(Data([0xff]).hexEncodedString() == "ff")
#expect(Data([0xde, 0xad, 0xbe, 0xef]).hexEncodedString() == "deadbeef")
#expect(Data([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]).hexEncodedString() == "0123456789abcdef")
}
@Test func encode_allByteValues_matchesFormatReference() {
let all = Data((0...255).map { UInt8($0) })
let reference = (0...255).map { String(format: "%02x", $0) }.joined()
#expect(all.hexEncodedString() == reference)
}
@Test func encode_worksOnDataSlices() {
let data = Data([0xaa, 0xde, 0xad, 0xbe, 0xef, 0xbb])
let slice = data.dropFirst().dropLast()
#expect(slice.hexEncodedString() == "deadbeef")
}
// MARK: - Decoding
@Test func decode_knownVectors() {
#expect(Data(hexString: "deadbeef") == Data([0xde, 0xad, 0xbe, 0xef]))
#expect(Data(hexString: "DEADBEEF") == Data([0xde, 0xad, 0xbe, 0xef]))
#expect(Data(hexString: "DeAdBeEf") == Data([0xde, 0xad, 0xbe, 0xef]))
#expect(Data(hexString: "00") == Data([0x00]))
#expect(Data(hexString: "0123456789abcdef") == Data([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]))
}
@Test func decode_handlesPrefixAndWhitespace() {
#expect(Data(hexString: "0xdeadbeef") == Data([0xde, 0xad, 0xbe, 0xef]))
#expect(Data(hexString: "0XDEADBEEF") == Data([0xde, 0xad, 0xbe, 0xef]))
#expect(Data(hexString: " deadbeef\n") == Data([0xde, 0xad, 0xbe, 0xef]))
#expect(Data(hexString: "") == Data())
#expect(Data(hexString: "0x") == Data())
}
@Test func decode_rejectsInvalidInput() {
#expect(Data(hexString: "abc") == nil) // odd length
#expect(Data(hexString: "zz") == nil) // non-hex characters
#expect(Data(hexString: "0xg1") == nil) // non-hex after prefix
#expect(Data(hexString: "+f") == nil) // sign characters are not hex
#expect(Data(hexString: "-0") == nil)
#expect(Data(hexString: "a\u{00e9}") == nil) // non-ASCII
#expect(Data(hexString: "de ad") == nil) // interior whitespace
}
// MARK: - Round trip
@Test func roundTrip_randomLengths() {
for length in [0, 1, 2, 3, 8, 16, 31, 32, 33, 64, 255, 1024] {
let data = Data((0..<length).map { _ in UInt8.random(in: .min ... .max) })
let hex = data.hexEncodedString()
#expect(hex.count == length * 2)
#expect(Data(hexString: hex) == data)
#expect(Data(hexString: hex.uppercased()) == data)
}
}
}
+401 -426
View File
@@ -1,441 +1,416 @@
Relay URL,Latitude,Longitude
nostr.bitcoiner.social:443,47.6743,-117.112
relay-dev.satlantis.io:443,40.8302,-74.1299
relay.lightning.pub:443,39.0438,-77.4874
ribo.us.nostria.app:443,43.6532,-79.3832
relay.notoshi.win,13.3622,100.983
openrelay.ziomc.com,50.0755,14.4378
relay.agentry.com:443,42.8864,-78.8784
nostr-relay.xbytez.io,50.6924,3.20113
relay.gulugulu.moe,43.6532,-79.3832
nostr.thebiglake.org:443,32.71,-96.6745
relay.fountain.fm,43.6532,-79.3832
freelay.sovbit.host,60.1699,24.9384
nostr.girino.org:443,43.6532,-79.3832
relay.openresist.com,43.6532,-79.3832
nas01xanthosnet.synology.me:7778,47.1285,8.74735
relay.ohstr.com:443,43.6532,-79.3832
nostr-pub.wellorder.net,45.5201,-122.99
relay.nostrdice.com,-33.8688,151.209
bbw-nostr.xyz,41.5284,-87.4237
cs-relay.nostrdev.com:443,50.4754,12.3683
top.testrelay.top,43.6532,-79.3832
relay.trotters.cc,43.6532,-79.3832
blossom.gnostr.cloud,43.6532,-79.3832
ribo.eu.nostria.app:443,43.6532,-79.3832
public.crostr.com,43.6532,-79.3832
relay.nostar.org,43.6532,-79.3832
strfry.bonsai.com:443,39.0438,-77.4874
nostr.carroarmato0.be,50.914,3.21378
relay.endfiat.money,59.3327,18.0656
nostr.overmind.lol:443,43.6532,-79.3832
nostr.myshosholoza.co.za:443,52.3913,4.66545
relay.wellorder.net,45.5201,-122.99
nostr.tagomago.me,42.3601,-71.0589
relay.internationalright-wing.org,-22.5022,-48.7114
relay.fckstate.net,59.3293,18.0686
nostr.azzamo.net,52.2633,21.0283
relay.0xchat.com,43.6532,-79.3832
relayone.geektank.ai,39.1008,-94.5811
relay.homeinhk.xyz,35.694,139.754
nostr.nodesmap.com,59.3327,18.0656
relay.islandbitcoin.com:443,12.8498,77.6545
relay.mrmave.work,43.6532,-79.3832
relay.arx-ccn.com,50.4754,12.3683
fanfares.nostr1.com:443,40.7057,-74.0136
wot.shaving.kiwi,43.6532,-79.3832
relay.nearhood.co.uk,51.5072,-0.127586
relay.fundstr.me,42.3601,-71.0589
syb.lol:443,43.6532,-79.3832
dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832
relay.nostx.io,43.6532,-79.3832
no.str.cr,10.6352,-85.4378
relay.jbnco.co,43.6532,-79.3832
ribo.nostria.app:443,43.6532,-79.3832
us-east.nostr.pikachat.org,39.0438,-77.4874
nexus.libernet.app,43.6532,-79.3832
nostr.dlcdevkit.com,40.0992,-83.1141
nostr.debate.report,50.1109,8.68213
str-define-contributing-jackets.trycloudflare.com,43.6532,-79.3832
nostr2.girino.org,43.6532,-79.3832
nostr.unkn0wn.world,46.8499,9.53287
nostr.spicyz.io:443,43.6532,-79.3832
relay.layer.systems:443,49.0291,8.35695
nostr-relay.amethyst.name,39.0067,-77.4291
slick.mjex.me,39.0418,-77.4744
nostr.girino.org,43.6532,-79.3832
nostr.quali.chat:443,60.1699,24.9384
purplerelay.com:443,43.6532,-79.3832
nostr.notribe.net,40.8302,-74.1299
relay.plebeian.market:443,50.1109,8.68213
relay.samt.st,40.8302,-74.1299
relay.mitchelltribe.com:443,39.0438,-77.4874
nostr.0x7e.xyz,47.4949,8.71954
relayone.soundhsa.com:443,39.1008,-94.5811
nostr.thalheim.io:443,60.1699,24.9384
relay.getsafebox.app:443,43.6532,-79.3832
nostr-relay.psfoundation.info,39.0438,-77.4874
bucket.coracle.social,37.7775,-122.397
nostr.carroarmato0.be:443,50.914,3.21378
relay.staging.commonshub.brussels,49.4543,11.0746
relay-dev.satlantis.io,40.8302,-74.1299
relay.lacompagniemaximus.com:443,45.3147,-73.8785
relay-rpi.edufeed.org,49.4521,11.0767
nostr.computingcache.com:443,34.0356,-118.442
relay.lanavault.space:443,60.1699,24.9384
relay.getsafebox.app,43.6532,-79.3832
nrs-02.darkcloudarcade.com:443,39.9526,-75.1652
nostr.hekster.org,37.3986,-121.964
node.kommonzenze.de,49.4521,11.0767
0x-nostr-relay.fly.dev,37.7648,-122.432
speakeasy.cellar.social,49.4543,11.0746
nostr.0x7e.xyz:443,47.4949,8.71954
nostr.vulpem.com,49.4543,11.0746
relay5.bitransfer.org,43.6532,-79.3832
relay.inforsupports.com,43.6532,-79.3832
relay.plebeian.market,50.1109,8.68213
shu02.shugur.net,21.4902,39.2246
nostr.easycryptosend.it,43.6532,-79.3832
nostr.spaceshell.xyz,43.6532,-79.3832
relay.minibolt.info,43.6532,-79.3832
nostr.pbfs.io:443,50.4754,12.3683
nos.lol:443,50.4754,12.3683
nostr.thebiglake.org,32.71,-96.6745
relay.nostriot.com,41.5695,-83.9786
strfry.shock.network:443,39.0438,-77.4874
relay01.lnfi.network,35.6764,139.65
r.0kb.io:443,32.789,-96.7989
bcast.girino.org,43.6532,-79.3832
nostr-relay.psfoundation.info:443,39.0438,-77.4874
dm-test-strfry-discovery.samt.st,43.6532,-79.3832
testnet.samt.st,43.6532,-79.3832
relay.bornheimer.app,51.5072,-0.127586
nrs-02.darkcloudarcade.com,39.9526,-75.1652
relay.binaryrobot.com:443,43.6532,-79.3832
nostr.computingcache.com,34.0356,-118.442
nostr-rs-relay-qj1h.onrender.com,37.7775,-122.397
schnorr.me,43.6532,-79.3832
nostr.twinkle.lol,51.902,7.6657
nostr.overmind.lol,43.6532,-79.3832
relay.mmwaves.de:443,48.8575,2.35138
relay2.veganostr.com,60.1699,24.9384
nostr.mom,50.4754,12.3683
nostr2.girino.org:443,43.6532,-79.3832
wot.sudocarlos.com,43.6532,-79.3832
dev.relay.stream,43.6532,-79.3832
nostr.thalheim.io,60.1699,24.9384
relay-dev.gulugulu.moe:443,43.6532,-79.3832
conduitl2.fly.dev,37.7648,-122.432
relay.bullishbounty.com,43.6532,-79.3832
myvoiceourstory.org,37.3598,-121.981
relay.angor.io,48.1046,11.6002
r.0kb.io,32.789,-96.7989
nexus.libernet.app:443,43.6532,-79.3832
us-east.nostr.pikachat.org:443,39.0438,-77.4874
nostr.bitcoiner.social,47.6743,-117.112
nostrelay.circum.space:443,52.6907,4.8181
relayone.soundhsa.com,39.1008,-94.5811
nostr.snowbla.de,60.1699,24.9384
relay1.orangesync.tech,44.7839,-106.941
nostr-2.21crypto.ch:443,47.5356,8.73209
espelho.girino.org,43.6532,-79.3832
nostr.blankfors.se,60.1699,24.9384
relay.sigit.io:443,50.4754,12.3683
relay.vrtmrz.net:443,43.6532,-79.3832
nostr.wecsats.io:443,43.6532,-79.3832
nostrcity-club.fly.dev,37.7648,-122.432
nostrelay.circum.space,52.6907,4.8181
nostr-relay-1.trustlessenterprise.com,43.6532,-79.3832
relay.edufeed.org,49.4521,11.0767
nostr.dlcdevkit.com:443,40.0992,-83.1141
x.kojira.io,43.6532,-79.3832
relay.binaryrobot.com,43.6532,-79.3832
relay.nostrhub.fr,48.1045,11.6004
nostr.sathoarder.com,48.5734,7.75211
prl.plus,55.7628,37.5983
relay.cosmicbolt.net:443,37.3986,-121.964
relay.nostu.be:443,40.4167,-3.70329
cs-relay.nostrdev.com,50.4754,12.3683
satsage.xyz,37.3986,-121.964
relay.lab.rytswd.com:443,49.4543,11.0746
rilo.nostria.app:443,43.6532,-79.3832
portal-relay.pareto.space,49.0291,8.35696
relay.wavlake.com:443,41.2619,-95.8608
relay.beginningend.com,35.2227,-97.4786
relay.openresist.com:443,43.6532,-79.3832
bitcoiner.social:443,47.6743,-117.112
relay.notoshi.win:443,13.3622,100.983
dev.relay.edufeed.org:443,49.4521,11.0767
relay.cypherflow.ai:443,48.8575,2.35138
relay.ru.ac.th,13.7607,100.627
shu03.shugur.net,25.2048,55.2708
nostr.rtvslawenia.com:443,49.4543,11.0746
relay.agorist.space:443,52.3734,4.89406
relay02.lnfi.network,35.6764,139.65
relay-testnet.k8s.layer3.news:443,37.3387,-121.885
nostr.notribe.net:443,40.8302,-74.1299
relay.ditto.pub,43.6532,-79.3832
nostr.rikmeijer.nl,51.7111,5.36809
blossom.gnostr.cloud:443,43.6532,-79.3832
nostr.oxtr.dev,50.4754,12.3683
cache.trustr.ing,43.6548,-79.3885
nostr.bitczat.pl,60.1699,24.9384
relay.nostrverse.net,43.6532,-79.3832
shu04.shugur.net,25.2048,55.2708
eu.nostr.pikachat.org:443,49.4543,11.0746
ribo.us.nostria.app,43.6532,-79.3832
nostr-relay.cbrx.io,43.6532,-79.3832
nostr.bitczat.pl:443,60.1699,24.9384
nostr-relay.corb.net:443,38.8353,-104.822
relay.libernet.app,43.6532,-79.3832
nostr-verified.wellorder.net,45.5201,-122.99
relay.nostu.be,40.4167,-3.70329
rele.speyhard.fi,51.5072,-0.127586
relay.staging.plebeian.market,51.5072,-0.127586
nostr.spicyz.io,43.6532,-79.3832
nostrride.io,37.3986,-121.964
x.kojira.io:443,43.6532,-79.3832
relay.staging.plebeian.market:443,51.5072,-0.127586
relay.sigit.io,50.4754,12.3683
nostr.stakey.net:443,52.3676,4.90414
relay.nostr.blockhenge.com,39.0438,-77.4874
relay.comcomponent.com,43.6532,-79.3832
wot.nostr.place,43.6532,-79.3832
relay.mostr.pub:443,43.6532,-79.3832
nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832
no.str.cr:443,10.6352,-85.4378
relay.chorus.community:443,48.5333,10.7
relay.aarpia.com,37.3986,-121.964
relay.nostrmap.net,60.1699,24.9384
relay.snotr.nl:49999,52.0195,4.42946
relay.bebond.net,43.6532,-79.3832
relay.illuminodes.com,43.6532,-79.3832
chat-relay.zap-work.com:443,43.6532,-79.3832
testr.nymble.world,40.8054,-74.0241
relay.underorion.se,50.1109,8.68213
fanfares.nostr1.com,40.7057,-74.0136
relay.damus.io,43.6532,-79.3832
relay.nostriches.club,43.6532,-79.3832
nostr-dev.wellorder.net,45.5201,-122.99
relay2.angor.io,48.1046,11.6002
relay.openfarmtools.org,60.1699,24.9384
wot.makenomistakes.ca,43.7064,-79.3986
relay.thecryptosquid.com,50.4754,12.3683
test.thedude.cloud,50.1109,8.68213
nostr.rtvslawenia.com,49.4543,11.0746
nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874
relay.olas.app:443,60.1699,24.9384
strfry.bonsai.com,39.0438,-77.4874
relayrs.notoshi.win,43.6532,-79.3832
articles.layer3.news,37.3387,-121.885
wot.utxo.one,43.6532,-79.3832
relay.angor.io:443,48.1046,11.6002
relay.dwadziesciajeden.pl,52.2297,21.0122
soloco.nl,43.6532,-79.3832
armada.sharegap.net,43.6532,-79.3832
dm-test-strfry-generic.samt.st,43.6532,-79.3832
nostr.4rs.nl,49.0291,8.35696
nrs-01.darkcloudarcade.com,39.1008,-94.5811
relay.beginningend.com:443,35.2227,-97.4786
relay.nostr.place,43.6532,-79.3832
relay.layer.systems,49.0291,8.35695
adre.su,59.9311,30.3609
relay.0xchat.com:443,43.6532,-79.3832
nostr.infero.net,35.6764,139.65
relay.typedcypher.com:443,51.5072,-0.127586
relay.mostro.network:443,40.8302,-74.1299
relay-fra.zombi.cloudrodion.com,48.8566,2.35222
relay.mitchelltribe.com,39.0438,-77.4874
relay.mostr.pub,43.6532,-79.3832
bitcoiner.social,47.6743,-117.112
nostr.tac.lol:443,47.4748,-122.273
nostr.hekster.org:443,37.3986,-121.964
relay.satmaxt.xyz:443,43.6532,-79.3832
relay.cypherflow.ai,48.8575,2.35138
relay.zone667.com:443,60.1699,24.9384
relay.jeffg.fyi:443,43.6532,-79.3832
relay.agorist.space,52.3734,4.89406
nostr.spaceshell.xyz:443,43.6532,-79.3832
top.testrelay.top:443,43.6532,-79.3832
insta-relay.apps3.slidestr.net,40.4167,-3.70329
relay.nostrian-conquest.com:443,41.223,-111.974
relay.laantungir.net:443,-19.4692,-42.5315
relay.islandbitcoin.com,12.8498,77.6545
purplerelay.com,43.6532,-79.3832
nostr.tac.lol,47.4748,-122.273
nostr.wecsats.io,43.6532,-79.3832
nostr.2b9t.xyz,34.0549,-118.243
nostr.quali.chat,60.1699,24.9384
relay.dreamith.to,43.6532,-79.3832
nostr.islandarea.net:443,35.4669,-97.6473
relay.primal.net,43.6532,-79.3832
eu.nostr.pikachat.org,49.4543,11.0746
relay.nostr.net,43.6532,-79.3832
nostr.n7ekb.net,47.4941,-122.294
relay.mulatta.io,37.5665,126.978
nittom.nostr1.com,40.7057,-74.0136
relay2.orangesync.tech,40.7128,-74.006
relay.artx.market,43.6548,-79.3885
relay.wavefunc.live,41.8781,-87.6298
bitchat.nostr1.com,40.7057,-74.0136
relay.ditto.pub:443,43.6532,-79.3832
relay.wisp.talk,49.4543,11.0746
nostrelites.org,41.8781,-87.6298
relay.goodmorningbitcoin.com,43.6532,-79.3832
dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832
relay.chorus.community,48.5333,10.7
relay.plebchain.club,43.6532,-79.3832
nostr-relay.amethyst.name:443,39.0067,-77.4291
relay.lanacoin-eternity.com,40.8302,-74.1299
relay.edufeed.org:443,49.4521,11.0767
relay.lacompagniemaximus.com,45.3147,-73.8785
relay.dreamith.to:443,43.6532,-79.3832
nostr.stakey.net,52.3676,4.90414
nostr.n7ekb.net:443,47.4941,-122.294
relay.dyne.org,49.0291,8.35705
nostr.janx.com,43.6532,-79.3832
relay.trustr.ing,43.6548,-79.3885
relay.paulstephenborile.com:443,49.4543,11.0746
relay.wisp.talk:443,49.4543,11.0746
yabu.me,35.6092,139.73
bcast.seutoba.com.br,43.6532,-79.3832
nos.xmark.cc,50.6924,3.20113
nos.lol,50.4754,12.3683
relay.satmaxt.xyz,43.6532,-79.3832
relay.endfiat.money:443,59.3327,18.0656
relay.tapestry.ninja,40.8054,-74.0241
relay.lab.rytswd.com,49.4543,11.0746
nostr-kyomu-haskell.onrender.com,37.7775,-122.397
relay.damus.io:443,43.6532,-79.3832
treuzkas.branruz.com,48.8575,2.35138
relay.paulstephenborile.com:443,49.4543,11.0746
relay.binaryrobot.com,43.6532,-79.3832
nostr-2.21crypto.ch,47.5356,8.73209
spookstr2.nostr1.com:443,40.7057,-74.0136
fanfares.nostr1.com:443,40.7057,-74.0136
x.kojira.io,43.6532,-79.3832
freelay.sovbit.host,60.1699,24.9384
nostr-rs-relay-qj1h.onrender.com,37.7775,-122.397
testnet.samt.st,43.6532,-79.3832
relay.angor.io,48.1046,11.6002
relay-arg.zombi.cloudrodion.com,1.35208,103.82
nostr-01.yakihonne.com,1.32123,103.695
relay.nostriot.com:443,41.5695,-83.9786
nostr-relay.nextblockvending.com,47.2343,-119.853
nostr.aruku.ovh,1.27994,103.849
nostr.chaima.info,50.1109,8.68213
ynostr.yael.at,60.1699,24.9384
ynostr.yael.at:443,60.1699,24.9384
nostr-01.yakihonne.com:443,1.32123,103.695
spookstr2.nostr1.com,40.7057,-74.0136
relay.trustr.ing:443,43.6548,-79.3885
dev.relay.edufeed.org,49.4521,11.0767
relay2.angor.io:443,48.1046,11.6002
nostr.azzamo.net:443,52.2633,21.0283
offchain.bostr.online,43.6532,-79.3832
nostr-relay.cbrx.io,43.6532,-79.3832
relay.guggero.org,46.5971,9.59652
nostr.snowbla.de,60.1699,24.9384
relay.zone667.com,60.1699,24.9384
relay.veganostr.com,60.1699,24.9384
vault.iris.to:443,43.6532,-79.3832
relay.artx.market:443,43.6548,-79.3885
wot.rejecttheframe.xyz,43.6532,-79.3832
nostr.pbfs.io,50.4754,12.3683
relay.mostro.network,40.8302,-74.1299
strfry.shock.network,39.0438,-77.4874
relay.klabo.world,47.2343,-119.853
relay.fountain.fm:443,43.6532,-79.3832
nostrja-kari.heguro.com,43.6532,-79.3832
relaisnostr.trivaco.fr,48.5734,7.75211
offchain.pub,39.1585,-94.5728
relay.degmods.com,50.4754,12.3683
nostr-relay.xbytez.io:443,50.6924,3.20113
relay.paulstephenborile.com,49.4543,11.0746
nittom.nostr1.com:443,40.7057,-74.0136
dev-relay.nostreon.com,60.1699,24.9384
nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874
relay.jeffg.fyi,43.6532,-79.3832
nexus.libernet.app:443,43.6532,-79.3832
relay.islandbitcoin.com,12.8498,77.6545
relay-testnet.k8s.layer3.news,37.3387,-121.885
nostr-relay.xbytez.io,50.6924,3.20113
kasztanowa.bieda.it,43.6532,-79.3832
nostrcity-club.fly.dev,37.7648,-122.432
relay.typedcypher.com,51.5072,-0.127586
nostr.na.social:443,43.6532,-79.3832
relay.laantungir.net,-19.4692,-42.5315
nostr.data.haus:443,50.4754,12.3683
wot.codingarena.top,50.4754,12.3683
relay-dev.satlantis.io:443,40.8302,-74.1299
rilo.nostria.app,43.6532,-79.3832
nostr.hekster.org:443,37.3986,-121.964
nostr-relay.amethyst.name:443,39.0067,-77.4291
chat-relay.zap-work.com:443,43.6532,-79.3832
relay.edufeed.org,49.4521,11.0767
syb.lol:443,43.6532,-79.3832
relay.sigit.io,50.4754,12.3683
nostr-relay.xbytez.io:443,50.6924,3.20113
relay.wavefunc.live,41.8781,-87.6298
nostr.sathoarder.com,48.5734,7.75211
myvoiceourstory.org,37.3598,-121.981
relay.underorion.se,50.1109,8.68213
nostr.data.haus,50.4754,12.3683
relay.erybody.com,41.4513,-81.7021
espelho.girino.org,43.6532,-79.3832
nostr.pbfs.io:443,50.4754,12.3683
wot.dergigi.com,64.1476,-21.9392
nostr.bitcoiner.social:443,47.6743,-117.112
dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832
relay.gulugulu.moe,43.6532,-79.3832
nostr.spicyz.io,43.6532,-79.3832
relay.cypherflow.ai,48.8575,2.35138
treuzkas.branruz.com,48.8575,2.35138
relay1.nostrchat.io,60.1699,24.9384
kotukonostr.onrender.com,37.7775,-122.397
nostr.plantroon.com,50.1013,8.62643
nostr.davenov.com,50.1109,8.68213
node.kommonzenze.de,49.4521,11.0767
relay2.veganostr.com,60.1699,24.9384
armada.sharegap.net,43.6532,-79.3832
wot.makenomistakes.ca,43.7064,-79.3986
nostr.2b9t.xyz:443,34.0549,-118.243
relay.libernet.app:443,43.6532,-79.3832
nostr.ps1829.com,33.8851,130.883
wot.dergigi.com,64.1476,-21.9392
relay.olas.app,60.1699,24.9384
relay.lanacoin-eternity.com:443,40.8302,-74.1299
nostr.data.haus,50.4754,12.3683
relay-dev.gulugulu.moe,43.6532,-79.3832
relay.ohstr.com,43.6532,-79.3832
relay.lightning.pub,39.0438,-77.4874
relay.guggero.org,46.5971,9.59652
testnet-relay.samt.st:443,40.8302,-74.1299
thecitadel.nostr1.com,40.7057,-74.0136
nostr.ps1829.com:443,33.8851,130.883
nostr-relay.corb.net,38.8353,-104.822
relay.npubhaus.com,43.6532,-79.3832
relay.dreamith.to:443,43.6532,-79.3832
relay.lightning.pub:443,39.0438,-77.4874
nostr.rtvslawenia.com,49.4543,11.0746
nostr.21crypto.ch,47.5356,8.73209
nostr.tadryanom.me,43.6532,-79.3832
nostr.myshosholoza.co.za,52.3913,4.66545
relay.bitmacro.cloud,43.6532,-79.3832
ribo.nostria.app,43.6532,-79.3832
relay.vrtmrz.net,43.6532,-79.3832
syb.lol,43.6532,-79.3832
relay.bebond.net:443,43.6532,-79.3832
relay.agentry.com,42.8864,-78.8784
nostr-2.21crypto.ch,47.5356,8.73209
nostrcity-club.fly.dev:443,37.7648,-122.432
articles.layer3.news:443,37.3387,-121.885
relay.internationalright-wing.org:443,-22.5022,-48.7114
nostr-relay.zimage.com,34.0549,-118.243
chat-relay.zap-work.com,43.6532,-79.3832
relay.mwaters.net,50.9871,2.12554
nostr.liberty.fans,36.9104,-89.5875
spookstr2.nostr1.com:443,40.7057,-74.0136
relay.ditto.pub:443,43.6532,-79.3832
relay.plebchain.club,43.6532,-79.3832
memlay.v0l.io,53.3498,-6.26031
nostr.chaima.info:443,50.1109,8.68213
schnorr.me:443,43.6532,-79.3832
ribo.eu.nostria.app,43.6532,-79.3832
nostr.bond,50.1109,8.68213
wot.nostr.party,36.1659,-86.7844
temp.iris.to,43.6532,-79.3832
relay.lotek-distro.com,43.6532,-79.3832
relay.minibolt.info:443,43.6532,-79.3832
relay.lanavault.space,60.1699,24.9384
relay.mmwaves.de,48.8575,2.35138
social.amanah.eblessing.co,48.1046,11.6002
nostr2.thalheim.io,49.4543,11.0746
relay.typedcypher.com,51.5072,-0.127586
relay.cosmicbolt.net,37.3986,-121.964
relayrs.notoshi.win:443,43.6532,-79.3832
nostr-relay-1.trustlessenterprise.com:443,43.6532,-79.3832
nostr.islandarea.net,35.4669,-97.6473
relay.nostrmap.net:443,60.1699,24.9384
relay.bullishbounty.com:443,43.6532,-79.3832
nostr.oxtr.dev:443,50.4754,12.3683
srtrelay.c-stellar.net,43.6532,-79.3832
relay.mccormick.cx,52.3563,4.95714
vault.iris.to,43.6532,-79.3832
relay.mccormick.cx:443,52.3563,4.95714
relay.toastr.net,40.8054,-74.0241
nostr.hifish.org,47.4244,8.57658
speakeasy.cellar.social:443,49.4543,11.0746
relay.wavlake.com,41.2619,-95.8608
testnet-relay.samt.st,40.8302,-74.1299
aeon.libretechsystems.xyz,55.486,9.86577
mostro-p2p.tech,50.1109,8.68213
nostr.wild-vibes.ts.net,48.8566,2.35222
nostr.88mph.life,52.1941,-2.21905
relay.nostrcheck.me,43.6532,-79.3832
rilo.nostria.app,43.6532,-79.3832
relay.bowlafterbowl.com,32.9483,-96.7299
relay.nostr.place:443,43.6532,-79.3832
nostr.mom:443,50.4754,12.3683
relay.nostrian-conquest.com,41.223,-111.974
herbstmeister.com,34.0549,-118.243
nrs-01.darkcloudarcade.com:443,39.1008,-94.5811
nostr.red5d.dev,43.6532,-79.3832
nostrbtc.com,43.6532,-79.3832
strfry.apps3.slidestr.net,40.4167,-3.70329
relay.sharegap.net,43.6532,-79.3832
reraw.pbla2fish.cc,43.6532,-79.3832
relay-testnet.k8s.layer3.news,37.3387,-121.885
relay.wavlake.com:443,41.2619,-95.8608
nostr.thalheim.io:443,60.1699,24.9384
relay.lightning.pub,39.0438,-77.4874
dev.relay.edufeed.org:443,49.4521,11.0767
nostr.myshosholoza.co.za:443,52.3913,4.66545
relay.binaryrobot.com:443,43.6532,-79.3832
wot.nostr.place,43.6532,-79.3832
nostr.sathoarder.com:443,48.5734,7.75211
relay.veganostr.com:443,60.1699,24.9384
relay-fra.zombi.cloudrodion.com:443,48.8566,2.35222
thecitadel.nostr1.com,40.7057,-74.0136
relay.artx.market,43.6548,-79.3885
nos.lol,50.4754,12.3683
nostr.plantroon.com:443,50.1013,8.62643
premium.primal.net,43.6532,-79.3832
relay.wavefunc.live:443,41.8781,-87.6298
nas01xanthosnet.synology.me:7778,47.1285,8.74735
nostrja-kari.heguro.com,43.6532,-79.3832
relay.mrmave.work,43.6532,-79.3832
nostrelay.circum.space,52.6907,4.8181
mostro-p2p.tech,50.1109,8.68213
wot.shaving.kiwi,43.6532,-79.3832
relay.fundstr.me,42.3601,-71.0589
nostrelay.circum.space:443,52.6907,4.8181
relay.nostrdice.com,-33.8688,151.209
relay.getvia.xyz,60.1699,24.9384
strfry.shock.network:443,39.0438,-77.4874
relay.nostrmap.net:443,60.1699,24.9384
relay.nearhood.co.uk,51.5072,-0.127586
no.str.cr,10.6352,-85.4378
relay.getsafebox.app:443,43.6532,-79.3832
relay0.gfcom.info,13.6992,100.694
nostr.ps1829.com,33.8851,130.883
relay2.angor.io,48.1046,11.6002
relay.stickeroo.is-cool.dev,37.3387,-121.885
ricardo-oem.tailb5546.ts.net,40.7128,-74.006
relay.typedcypher.com:443,51.5072,-0.127586
relay.paulstephenborile.com,49.4543,11.0746
nittom.nostr1.com,40.7057,-74.0136
conduitl2.fly.dev,37.7648,-122.432
nostr.rikmeijer.nl,51.7111,5.36809
relay.thecryptosquid.com,50.4754,12.3683
spookstr2.nostr1.com,40.7057,-74.0136
offchain.bostr.online,43.6532,-79.3832
nostr.planix.org,43.6532,-79.3832
relay.mccormick.cx,52.3563,4.95714
0x-nostr-relay.fly.dev,37.7648,-122.432
nostr.wecsats.io,43.6532,-79.3832
schnorr.me,43.6532,-79.3832
relay.satmaxt.xyz,43.6532,-79.3832
relay.bornheimer.app,51.5072,-0.127586
relay.nostrhub.fr,48.1045,11.6004
blossom.gnostr.cloud:443,43.6532,-79.3832
nostr-02.yakihonne.com:443,1.32123,103.695
dev.relay.stream,43.6532,-79.3832
ithurtswhenip.ee,51.5072,-0.127586
nostr.myshosholoza.co.za,52.3913,4.66545
relayrs.notoshi.win:443,43.6532,-79.3832
relay-rpi.edufeed.org:443,49.4521,11.0767
kotukonostr.onrender.com,37.7775,-122.397
bridge.tagomago.me,42.3601,-71.0589
nostr.tadryanom.me:443,43.6532,-79.3832
relay.gulugulu.moe:443,43.6532,-79.3832
relay.olas.app:443,60.1699,24.9384
nostr.unkn0wn.world,46.8499,9.53287
relay.mitchelltribe.com,39.0438,-77.4874
yabu.me,35.6092,139.73
nostr.nodesmap.com,59.3327,18.0656
dm-test-strfry-generic.samt.st,43.6532,-79.3832
nostr2.girino.org:443,43.6532,-79.3832
wot.brightbolt.net,47.6735,-116.781
strfry.shock.network,39.0438,-77.4874
relay.kilombino.com,43.6532,-79.3832
relay.nostr.blockhenge.com,39.0438,-77.4874
shu04.shugur.net,25.2048,55.2708
relay-rpi.edufeed.org,49.4521,11.0767
relay.bullishbounty.com:443,43.6532,-79.3832
vault.iris.to:443,43.6532,-79.3832
relay.mostro.network:443,40.8302,-74.1299
offchain.pub:443,39.1585,-94.5728
soloco.nl,43.6532,-79.3832
relay.nostu.be,40.4167,-3.70329
nostr.pbfs.io,50.4754,12.3683
relay.directsponsor.net,42.8864,-78.8784
relay.decentralia.fr,49.4282,10.9796
relayrs.notoshi.win,43.6532,-79.3832
nostr-relay.amethyst.name,39.0067,-77.4291
relay.arx-ccn.com,50.4754,12.3683
nostr.spaceshell.xyz,43.6532,-79.3832
relay-fra.zombi.cloudrodion.com,48.8566,2.35222
rilo.nostria.app:443,43.6532,-79.3832
relay.trotters.cc:443,43.6532,-79.3832
nostr.overmind.lol:443,43.6532,-79.3832
nostr.girino.org:443,43.6532,-79.3832
bitsat.molonlabe.holdings,51.4012,-1.3147
nostr.azzamo.net,52.2633,21.0283
insta-relay.apps3.slidestr.net,40.4167,-3.70329
bridge.tagomago.me,42.3601,-71.0589
nostr.thalheim.io,60.1699,24.9384
relay.artx.market:443,43.6548,-79.3885
nostr.openhoofd.nl,51.5717,3.70417
nostr.bond,50.1109,8.68213
relay.earthly.city,34.1749,-118.54
nexus.libernet.app,43.6532,-79.3832
relay.plebeian.market,50.1109,8.68213
relay.nostr.net,43.6532,-79.3832
nostr.overmind.lol,43.6532,-79.3832
relay.ohstr.com,43.6532,-79.3832
testnet-relay.samt.st:443,40.8302,-74.1299
relay01.lnfi.network,35.6764,139.65
relay.mostr.pub:443,43.6532,-79.3832
wot.nostr.party,36.1659,-86.7844
relayone.soundhsa.com,39.1008,-94.5811
relay.mostro.network,40.8302,-74.1299
ribo.eu.nostria.app,43.6532,-79.3832
chat-relay.zap-work.com,43.6532,-79.3832
relay.nostreon.com,60.1699,24.9384
nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874
nostr.quali.chat:443,60.1699,24.9384
relay.internationalright-wing.org:443,-22.5022,-48.7114
relay.mitchelltribe.com:443,39.0438,-77.4874
relay.satlantis.io,40.8054,-74.0241
nittom.nostr1.com:443,40.7057,-74.0136
nostr.janx.com,43.6532,-79.3832
nostr.carroarmato0.be:443,50.914,3.21378
relay.mmwaves.de:443,48.8575,2.35138
relay.chorus.community:443,48.5333,10.7
wot.utxo.one,43.6532,-79.3832
relay.plebeian.market:443,50.1109,8.68213
relay.cosmicbolt.net,37.3986,-121.964
x.kojira.io:443,43.6532,-79.3832
top.testrelay.top,43.6532,-79.3832
nos.lol:443,50.4754,12.3683
dev.relay.edufeed.org,49.4521,11.0767
relayone.geektank.ai:443,39.1008,-94.5811
relay.nostar.org,43.6532,-79.3832
nostr.oxtr.dev:443,50.4754,12.3683
nostr.88mph.life,52.1941,-2.21905
relay.staging.commonshub.brussels,49.4543,11.0746
weboftrust.libretechsystems.xyz,55.4724,9.87335
relay.openfarmtools.org,60.1699,24.9384
cs-relay.nostrdev.com,50.4754,12.3683
relay.inforsupports.com,43.6532,-79.3832
nostr-verified.wellorder.net,45.5201,-122.99
nostr.hekster.org,37.3986,-121.964
relay.gulugulu.moe:443,43.6532,-79.3832
relay.mwaters.net,50.9871,2.12554
nostrcity-club.fly.dev:443,37.7648,-122.432
relay.vrtmrz.net:443,43.6532,-79.3832
relay.nostr.place,43.6532,-79.3832
relay.wavefunc.live:443,41.8781,-87.6298
nostr.islandarea.net,35.4669,-97.6473
purplerelay.com:443,43.6532,-79.3832
nostr-relay.psfoundation.info:443,39.0438,-77.4874
r.0kb.io,32.789,-96.7989
relay-us.zombi.cloudrodion.com,40.7862,-74.0743
relay.mulatta.io,37.5665,126.978
strfry.bonsai.com:443,39.0438,-77.4874
bendernostur.duckdns.org:8443,50.1109,8.68213
vault.iris.to,43.6532,-79.3832
ec2.f7z.io,60.1699,24.9384
nostr.debate.report,50.1109,8.68213
wot.codingarena.top,50.4754,12.3683
relay.layer.systems:443,49.0291,8.35695
relay.degmods.com,50.4754,12.3683
nostr.mom,50.4754,12.3683
ribo.us.nostria.app:443,43.6532,-79.3832
adre.su,59.9311,30.3609
wot.sudocarlos.com,43.6532,-79.3832
relay.nostrian-conquest.com,41.223,-111.974
nostr-relay.nextblockvending.com,47.2343,-119.853
relay.endfiat.money:443,59.3327,18.0656
nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874
nostr.carroarmato0.be,50.914,3.21378
relay.cypherflow.ai:443,48.8575,2.35138
nostr.girino.org,43.6532,-79.3832
nostr.thebiglake.org,32.71,-96.6745
strfry.ymir.cloud,43.6532,-79.3832
relay.mypathtofire.de,42.8864,-78.8784
relay.lanacoin-eternity.com,40.8302,-74.1299
nostr.snowbla.de:443,60.1699,24.9384
relay.ditto.pub,43.6532,-79.3832
relay.damus.io,43.6532,-79.3832
relay.ru.ac.th,13.7607,100.627
nrs-01.darkcloudarcade.com,39.1008,-94.5811
testnet-relay.samt.st,40.8302,-74.1299
antiprimal.net,43.6532,-79.3832
bitchat.nostr1.com,40.7057,-74.0136
relay.snort.social,53.3498,-6.26031
relay.mccormick.cx:443,52.3563,4.95714
relay02.lnfi.network,35.6764,139.65
srtrelay.c-stellar.net,43.6532,-79.3832
relay.minibolt.info,43.6532,-79.3832
nostrride.io,37.3986,-121.964
articles.layer3.news:443,37.3387,-121.885
rele.speyhard.fi,51.5072,-0.127586
relay.aarpia.com,37.3986,-121.964
nostr.chaima.info,50.1109,8.68213
relay.wisp.talk:443,49.4543,11.0746
relay.agorist.space:443,52.3734,4.89406
strfry.bonsai.com,39.0438,-77.4874
nostr.hifish.org,47.4244,8.57658
offchain.pub,39.1585,-94.5728
nostr.spicyz.io:443,43.6532,-79.3832
relay.beginningend.com,35.2227,-97.4786
relay.sharegap.net,43.6532,-79.3832
nostr.purpura.cloud,43.6532,-79.3832
nrs-01.darkcloudarcade.com:443,39.1008,-94.5811
relay.fountain.fm:443,43.6532,-79.3832
relay.olas.app,60.1699,24.9384
relay.mmwaves.de,48.8575,2.35138
relay.openresist.com:443,43.6532,-79.3832
relay.homeinhk.xyz,35.694,139.754
relay.libernet.app,43.6532,-79.3832
relay.comcomponent.com,43.6532,-79.3832
nostr.tac.lol,47.4748,-122.273
relay.goodmorningbitcoin.com,43.6532,-79.3832
relay.nostriot.com:443,41.5695,-83.9786
bcast.girino.org,43.6532,-79.3832
nostr.azzamo.net:443,52.2633,21.0283
relay.islandbitcoin.com:443,12.8498,77.6545
pool.libernet.app,43.6532,-79.3832
test.thedude.cloud,50.1109,8.68213
nostrelites.org,41.8781,-87.6298
nostr.infero.net,35.6764,139.65
relay.primal.net,43.6532,-79.3832
ribo.nostria.app,43.6532,-79.3832
relay.chorus.community,48.5333,10.7
bitcoiner.social:443,47.6743,-117.112
relay.wisp.talk,49.4543,11.0746
relay.layer.systems,49.0291,8.35695
relay-dev.satlantis.io,40.8302,-74.1299
nostr.bitcoiner.social,47.6743,-117.112
relay.lanavault.space:443,60.1699,24.9384
relay.staging.plebeian.market,51.5072,-0.127586
infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832
relay.fountain.fm,43.6532,-79.3832
nostr.middling.mydns.jp,35.8099,140.12
relay.dreamith.to,43.6532,-79.3832
relay.satmaxt.xyz:443,43.6532,-79.3832
shu03.shugur.net,25.2048,55.2708
zealand-charts-craig-thru.trycloudflare.com,43.6532,-79.3832
nostr.computingcache.com,34.0356,-118.442
ribo.us.nostria.app,43.6532,-79.3832
relay.agentry.com,42.8864,-78.8784
nostr.hifish.org:443,47.4244,8.57658
nostr.vulpem.com,49.4543,11.0746
relay.cosmicbolt.net:443,37.3986,-121.964
nostr-02.yakihonne.com,1.32123,103.695
r.0kb.io:443,32.789,-96.7989
nostr-relay.corb.net,38.8353,-104.822
ribo.eu.nostria.app:443,43.6532,-79.3832
nostr-relay.psfoundation.info,39.0438,-77.4874
relay.wellorder.net,45.5201,-122.99
relay.novospes.com,43.6532,-79.3832
nostr-dev.wellorder.net,45.5201,-122.99
relay.endfiat.money,59.3327,18.0656
relay.angor.io:443,48.1046,11.6002
relay-fra.zombi.cloudrodion.com:443,48.8566,2.35222
strfry.openhoofd.nl,51.5717,3.70417
relay.getsafebox.app,43.6532,-79.3832
relay.openresist.com,43.6532,-79.3832
relay5.bitransfer.org,43.6532,-79.3832
nostr.na.social,43.6532,-79.3832
portal-relay.pareto.space,49.0291,8.35696
nostr.notribe.net:443,40.8302,-74.1299
relay.bitmacro.cloud,43.6532,-79.3832
no.str.cr:443,10.6352,-85.4378
relay.klabo.world,47.2343,-119.853
nostr.notribe.net,40.8302,-74.1299
relay.staging.plebeian.market:443,51.5072,-0.127586
relay.nostrmap.net,60.1699,24.9384
temp.iris.to,43.6532,-79.3832
nostr.sovereignservices.xyz,43.6532,-79.3832
nostr.liberty.fans,36.9104,-89.5875
relay.nostrian-conquest.com:443,41.223,-111.974
relay.nostriot.com,41.5695,-83.9786
nostrbtc.com,43.6532,-79.3832
shu02.shugur.net,21.4902,39.2246
relay.kalcafe.xyz,37.3986,-121.964
relay.illuminodes.com,43.6532,-79.3832
relay.wavlake.com,41.2619,-95.8608
nostr.ps1829.com:443,33.8851,130.883
dm-test-strfry-discovery.samt.st,43.6532,-79.3832
nostr.wecsats.io:443,43.6532,-79.3832
nostr-pub.wellorder.net,45.5201,-122.99
nostr.dlcdevkit.com:443,40.0992,-83.1141
nostr.mom:443,50.4754,12.3683
ribo.nostria.app:443,43.6532,-79.3832
nostr.2b9t.xyz,34.0549,-118.243
nostr.data.haus:443,50.4754,12.3683
staging.yabu.me,35.6092,139.73
relay.sigit.io:443,50.4754,12.3683
relay.edufeed.org:443,49.4521,11.0767
nostr-01.yakihonne.com:443,1.32123,103.695
reraw.pbla2fish.cc,43.6532,-79.3832
cs-relay.nostrdev.com:443,50.4754,12.3683
herbstmeister.com,34.0549,-118.243
relay.minibolt.info:443,43.6532,-79.3832
relay2.angor.io:443,48.1046,11.6002
social.amanah.eblessing.co,48.1046,11.6002
nostr.stakey.net,52.3676,4.90414
nostr.computingcache.com:443,34.0356,-118.442
slick.mjex.me,39.0418,-77.4744
fanfares.nostr1.com,40.7057,-74.0136
bitcoinostr.duckdns.org,43.3434,-3.99532
nostr.oxtr.dev,50.4754,12.3683
cache.trustr.ing,43.6548,-79.3885
purplerelay.com,43.6532,-79.3832
nostr-kyomu-haskell.onrender.com,37.7775,-122.397
nostr-relay.corb.net:443,38.8353,-104.822
relay-dev.gulugulu.moe,43.6532,-79.3832
prl.plus,55.7628,37.5983
nostr.tac.lol:443,47.4748,-122.273
relay.mostr.pub,43.6532,-79.3832
schnorr.me:443,43.6532,-79.3832
dev-relay.nostreon.com,60.1699,24.9384
nostr.islandarea.net:443,35.4669,-97.6473
bucket.coracle.social,37.7775,-122.397
blossom.gnostr.cloud,43.6532,-79.3832
relay.solife.me,43.6532,-79.3832
nostr.quali.chat,60.1699,24.9384
relay.vrtmrz.net,43.6532,-79.3832
relay-dev.gulugulu.moe:443,43.6532,-79.3832
relay.bullishbounty.com,43.6532,-79.3832
relay.fckstate.net,59.3293,18.0686
nostr.rtvslawenia.com:443,49.4543,11.0746
relay.nostx.io,43.6532,-79.3832
relay.agorist.space,52.3734,4.89406
relay.notoshi.win,13.7829,100.546
dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832
relay.trotters.cc,43.6532,-79.3832
relay.lanavault.space,60.1699,24.9384
public.crostr.com:443,43.6532,-79.3832
nostr.stakey.net:443,52.3676,4.90414
relay.nostr.place:443,43.6532,-79.3832
nostr.dlcdevkit.com,40.0992,-83.1141
nostr.aruku.ovh,1.27994,103.849
satsage.xyz,37.3986,-121.964
strfry.apps3.slidestr.net,40.4167,-3.70329
nostr2.girino.org,43.6532,-79.3832
relay.samt.st,40.8302,-74.1299
articles.layer3.news,37.3387,-121.885
aeon.libretechsystems.xyz,55.486,9.86577
relay.routstr.com,59.4016,17.9455
relay.ohstr.com:443,43.6532,-79.3832
relay.lanacoin-eternity.com:443,40.8302,-74.1299
strfry.openhoofd.nl:443,51.5717,3.70417
nostr.blankfors.se,60.1699,24.9384
nostr-2.21crypto.ch:443,47.5356,8.73209
relayone.soundhsa.com:443,39.1008,-94.5811
relay.lab.rytswd.com:443,49.4543,11.0746
nostr.tagomago.me,42.3601,-71.0589
relay.0xchat.com:443,43.6532,-79.3832
1 Relay URL Latitude Longitude
nostr.bitcoiner.social:443 47.6743 -117.112
relay-dev.satlantis.io:443 40.8302 -74.1299
relay.lightning.pub:443 39.0438 -77.4874
ribo.us.nostria.app:443 43.6532 -79.3832
relay.notoshi.win 13.3622 100.983
openrelay.ziomc.com 50.0755 14.4378
relay.agentry.com:443 42.8864 -78.8784
nostr-relay.xbytez.io 50.6924 3.20113
relay.gulugulu.moe 43.6532 -79.3832
nostr.thebiglake.org:443 32.71 -96.6745
relay.fountain.fm 43.6532 -79.3832
freelay.sovbit.host 60.1699 24.9384
nostr.girino.org:443 43.6532 -79.3832
relay.openresist.com 43.6532 -79.3832
nas01xanthosnet.synology.me:7778 47.1285 8.74735
relay.ohstr.com:443 43.6532 -79.3832
nostr-pub.wellorder.net 45.5201 -122.99
relay.nostrdice.com -33.8688 151.209
bbw-nostr.xyz 41.5284 -87.4237
cs-relay.nostrdev.com:443 50.4754 12.3683
top.testrelay.top 43.6532 -79.3832
relay.trotters.cc 43.6532 -79.3832
blossom.gnostr.cloud 43.6532 -79.3832
ribo.eu.nostria.app:443 43.6532 -79.3832
public.crostr.com 43.6532 -79.3832
relay.nostar.org 43.6532 -79.3832
strfry.bonsai.com:443 39.0438 -77.4874
nostr.carroarmato0.be 50.914 3.21378
relay.endfiat.money 59.3327 18.0656
nostr.overmind.lol:443 43.6532 -79.3832
nostr.myshosholoza.co.za:443 52.3913 4.66545
relay.wellorder.net 45.5201 -122.99
nostr.tagomago.me 42.3601 -71.0589
relay.internationalright-wing.org -22.5022 -48.7114
relay.fckstate.net 59.3293 18.0686
nostr.azzamo.net 52.2633 21.0283
relay.0xchat.com 43.6532 -79.3832
relayone.geektank.ai 39.1008 -94.5811
relay.homeinhk.xyz 35.694 139.754
nostr.nodesmap.com 59.3327 18.0656
relay.islandbitcoin.com:443 12.8498 77.6545
relay.mrmave.work 43.6532 -79.3832
relay.arx-ccn.com 50.4754 12.3683
fanfares.nostr1.com:443 40.7057 -74.0136
wot.shaving.kiwi 43.6532 -79.3832
relay.nearhood.co.uk 51.5072 -0.127586
relay.fundstr.me 42.3601 -71.0589
syb.lol:443 43.6532 -79.3832
dm-test-strfry-discovery.samt.st:443 43.6532 -79.3832
relay.nostx.io 43.6532 -79.3832
no.str.cr 10.6352 -85.4378
relay.jbnco.co 43.6532 -79.3832
ribo.nostria.app:443 43.6532 -79.3832
us-east.nostr.pikachat.org 39.0438 -77.4874
nexus.libernet.app 43.6532 -79.3832
nostr.dlcdevkit.com 40.0992 -83.1141
nostr.debate.report 50.1109 8.68213
str-define-contributing-jackets.trycloudflare.com 43.6532 -79.3832
nostr2.girino.org 43.6532 -79.3832
nostr.unkn0wn.world 46.8499 9.53287
nostr.spicyz.io:443 43.6532 -79.3832
relay.layer.systems:443 49.0291 8.35695
nostr-relay.amethyst.name 39.0067 -77.4291
slick.mjex.me 39.0418 -77.4744
nostr.girino.org 43.6532 -79.3832
nostr.quali.chat:443 60.1699 24.9384
purplerelay.com:443 43.6532 -79.3832
nostr.notribe.net 40.8302 -74.1299
relay.plebeian.market:443 50.1109 8.68213
relay.samt.st 40.8302 -74.1299
relay.mitchelltribe.com:443 39.0438 -77.4874
nostr.0x7e.xyz 47.4949 8.71954
relayone.soundhsa.com:443 39.1008 -94.5811
nostr.thalheim.io:443 60.1699 24.9384
relay.getsafebox.app:443 43.6532 -79.3832
nostr-relay.psfoundation.info 39.0438 -77.4874
bucket.coracle.social 37.7775 -122.397
nostr.carroarmato0.be:443 50.914 3.21378
relay.staging.commonshub.brussels 49.4543 11.0746
relay-dev.satlantis.io 40.8302 -74.1299
relay.lacompagniemaximus.com:443 45.3147 -73.8785
relay-rpi.edufeed.org 49.4521 11.0767
nostr.computingcache.com:443 34.0356 -118.442
relay.lanavault.space:443 60.1699 24.9384
relay.getsafebox.app 43.6532 -79.3832
nrs-02.darkcloudarcade.com:443 39.9526 -75.1652
nostr.hekster.org 37.3986 -121.964
node.kommonzenze.de 49.4521 11.0767
0x-nostr-relay.fly.dev 37.7648 -122.432
speakeasy.cellar.social 49.4543 11.0746
nostr.0x7e.xyz:443 47.4949 8.71954
nostr.vulpem.com 49.4543 11.0746
relay5.bitransfer.org 43.6532 -79.3832
relay.inforsupports.com 43.6532 -79.3832
relay.plebeian.market 50.1109 8.68213
shu02.shugur.net 21.4902 39.2246
nostr.easycryptosend.it 43.6532 -79.3832
nostr.spaceshell.xyz 43.6532 -79.3832
relay.minibolt.info 43.6532 -79.3832
nostr.pbfs.io:443 50.4754 12.3683
nos.lol:443 50.4754 12.3683
nostr.thebiglake.org 32.71 -96.6745
relay.nostriot.com 41.5695 -83.9786
strfry.shock.network:443 39.0438 -77.4874
relay01.lnfi.network 35.6764 139.65
r.0kb.io:443 32.789 -96.7989
bcast.girino.org 43.6532 -79.3832
nostr-relay.psfoundation.info:443 39.0438 -77.4874
dm-test-strfry-discovery.samt.st 43.6532 -79.3832
testnet.samt.st 43.6532 -79.3832
relay.bornheimer.app 51.5072 -0.127586
nrs-02.darkcloudarcade.com 39.9526 -75.1652
relay.binaryrobot.com:443 43.6532 -79.3832
nostr.computingcache.com 34.0356 -118.442
nostr-rs-relay-qj1h.onrender.com 37.7775 -122.397
schnorr.me 43.6532 -79.3832
nostr.twinkle.lol 51.902 7.6657
nostr.overmind.lol 43.6532 -79.3832
relay.mmwaves.de:443 48.8575 2.35138
relay2.veganostr.com 60.1699 24.9384
nostr.mom 50.4754 12.3683
nostr2.girino.org:443 43.6532 -79.3832
wot.sudocarlos.com 43.6532 -79.3832
dev.relay.stream 43.6532 -79.3832
nostr.thalheim.io 60.1699 24.9384
relay-dev.gulugulu.moe:443 43.6532 -79.3832
conduitl2.fly.dev 37.7648 -122.432
relay.bullishbounty.com 43.6532 -79.3832
myvoiceourstory.org 37.3598 -121.981
relay.angor.io 48.1046 11.6002
r.0kb.io 32.789 -96.7989
nexus.libernet.app:443 43.6532 -79.3832
us-east.nostr.pikachat.org:443 39.0438 -77.4874
nostr.bitcoiner.social 47.6743 -117.112
nostrelay.circum.space:443 52.6907 4.8181
relayone.soundhsa.com 39.1008 -94.5811
nostr.snowbla.de 60.1699 24.9384
relay1.orangesync.tech 44.7839 -106.941
nostr-2.21crypto.ch:443 47.5356 8.73209
espelho.girino.org 43.6532 -79.3832
nostr.blankfors.se 60.1699 24.9384
relay.sigit.io:443 50.4754 12.3683
relay.vrtmrz.net:443 43.6532 -79.3832
nostr.wecsats.io:443 43.6532 -79.3832
nostrcity-club.fly.dev 37.7648 -122.432
nostrelay.circum.space 52.6907 4.8181
nostr-relay-1.trustlessenterprise.com 43.6532 -79.3832
relay.edufeed.org 49.4521 11.0767
nostr.dlcdevkit.com:443 40.0992 -83.1141
x.kojira.io 43.6532 -79.3832
relay.binaryrobot.com 43.6532 -79.3832
relay.nostrhub.fr 48.1045 11.6004
nostr.sathoarder.com 48.5734 7.75211
prl.plus 55.7628 37.5983
relay.cosmicbolt.net:443 37.3986 -121.964
relay.nostu.be:443 40.4167 -3.70329
cs-relay.nostrdev.com 50.4754 12.3683
satsage.xyz 37.3986 -121.964
relay.lab.rytswd.com:443 49.4543 11.0746
rilo.nostria.app:443 43.6532 -79.3832
portal-relay.pareto.space 49.0291 8.35696
relay.wavlake.com:443 41.2619 -95.8608
relay.beginningend.com 35.2227 -97.4786
relay.openresist.com:443 43.6532 -79.3832
bitcoiner.social:443 47.6743 -117.112
relay.notoshi.win:443 13.3622 100.983
dev.relay.edufeed.org:443 49.4521 11.0767
relay.cypherflow.ai:443 48.8575 2.35138
relay.ru.ac.th 13.7607 100.627
shu03.shugur.net 25.2048 55.2708
nostr.rtvslawenia.com:443 49.4543 11.0746
relay.agorist.space:443 52.3734 4.89406
relay02.lnfi.network 35.6764 139.65
relay-testnet.k8s.layer3.news:443 37.3387 -121.885
nostr.notribe.net:443 40.8302 -74.1299
relay.ditto.pub 43.6532 -79.3832
nostr.rikmeijer.nl 51.7111 5.36809
blossom.gnostr.cloud:443 43.6532 -79.3832
nostr.oxtr.dev 50.4754 12.3683
cache.trustr.ing 43.6548 -79.3885
nostr.bitczat.pl 60.1699 24.9384
relay.nostrverse.net 43.6532 -79.3832
shu04.shugur.net 25.2048 55.2708
eu.nostr.pikachat.org:443 49.4543 11.0746
ribo.us.nostria.app 43.6532 -79.3832
nostr-relay.cbrx.io 43.6532 -79.3832
nostr.bitczat.pl:443 60.1699 24.9384
nostr-relay.corb.net:443 38.8353 -104.822
relay.libernet.app 43.6532 -79.3832
nostr-verified.wellorder.net 45.5201 -122.99
relay.nostu.be 40.4167 -3.70329
rele.speyhard.fi 51.5072 -0.127586
relay.staging.plebeian.market 51.5072 -0.127586
nostr.spicyz.io 43.6532 -79.3832
nostrride.io 37.3986 -121.964
x.kojira.io:443 43.6532 -79.3832
relay.staging.plebeian.market:443 51.5072 -0.127586
relay.sigit.io 50.4754 12.3683
nostr.stakey.net:443 52.3676 4.90414
relay.nostr.blockhenge.com 39.0438 -77.4874
relay.comcomponent.com 43.6532 -79.3832
wot.nostr.place 43.6532 -79.3832
relay.mostr.pub:443 43.6532 -79.3832
nostr-rs-relay-ishosta.phamthanh.me 43.6532 -79.3832
no.str.cr:443 10.6352 -85.4378
relay.chorus.community:443 48.5333 10.7
relay.aarpia.com 37.3986 -121.964
relay.nostrmap.net 60.1699 24.9384
relay.snotr.nl:49999 52.0195 4.42946
relay.bebond.net 43.6532 -79.3832
relay.illuminodes.com 43.6532 -79.3832
chat-relay.zap-work.com:443 43.6532 -79.3832
testr.nymble.world 40.8054 -74.0241
relay.underorion.se 50.1109 8.68213
fanfares.nostr1.com 40.7057 -74.0136
relay.damus.io 43.6532 -79.3832
relay.nostriches.club 43.6532 -79.3832
nostr-dev.wellorder.net 45.5201 -122.99
relay2.angor.io 48.1046 11.6002
relay.openfarmtools.org 60.1699 24.9384
wot.makenomistakes.ca 43.7064 -79.3986
relay.thecryptosquid.com 50.4754 12.3683
test.thedude.cloud 50.1109 8.68213
nostr.rtvslawenia.com 49.4543 11.0746
nostr-rs-relay.dev.fedibtc.com 39.0438 -77.4874
relay.olas.app:443 60.1699 24.9384
strfry.bonsai.com 39.0438 -77.4874
relayrs.notoshi.win 43.6532 -79.3832
articles.layer3.news 37.3387 -121.885
wot.utxo.one 43.6532 -79.3832
relay.angor.io:443 48.1046 11.6002
relay.dwadziesciajeden.pl 52.2297 21.0122
soloco.nl 43.6532 -79.3832
armada.sharegap.net 43.6532 -79.3832
dm-test-strfry-generic.samt.st 43.6532 -79.3832
nostr.4rs.nl 49.0291 8.35696
nrs-01.darkcloudarcade.com 39.1008 -94.5811
relay.beginningend.com:443 35.2227 -97.4786
relay.nostr.place 43.6532 -79.3832
relay.layer.systems 49.0291 8.35695
adre.su 59.9311 30.3609
relay.0xchat.com:443 43.6532 -79.3832
nostr.infero.net 35.6764 139.65
relay.typedcypher.com:443 51.5072 -0.127586
relay.mostro.network:443 40.8302 -74.1299
relay-fra.zombi.cloudrodion.com 48.8566 2.35222
relay.mitchelltribe.com 39.0438 -77.4874
relay.mostr.pub 43.6532 -79.3832
bitcoiner.social 47.6743 -117.112
nostr.tac.lol:443 47.4748 -122.273
nostr.hekster.org:443 37.3986 -121.964
relay.satmaxt.xyz:443 43.6532 -79.3832
relay.cypherflow.ai 48.8575 2.35138
relay.zone667.com:443 60.1699 24.9384
relay.jeffg.fyi:443 43.6532 -79.3832
relay.agorist.space 52.3734 4.89406
nostr.spaceshell.xyz:443 43.6532 -79.3832
top.testrelay.top:443 43.6532 -79.3832
insta-relay.apps3.slidestr.net 40.4167 -3.70329
relay.nostrian-conquest.com:443 41.223 -111.974
relay.laantungir.net:443 -19.4692 -42.5315
relay.islandbitcoin.com 12.8498 77.6545
purplerelay.com 43.6532 -79.3832
nostr.tac.lol 47.4748 -122.273
nostr.wecsats.io 43.6532 -79.3832
nostr.2b9t.xyz 34.0549 -118.243
nostr.quali.chat 60.1699 24.9384
relay.dreamith.to 43.6532 -79.3832
nostr.islandarea.net:443 35.4669 -97.6473
relay.primal.net 43.6532 -79.3832
eu.nostr.pikachat.org 49.4543 11.0746
relay.nostr.net 43.6532 -79.3832
nostr.n7ekb.net 47.4941 -122.294
relay.mulatta.io 37.5665 126.978
nittom.nostr1.com 40.7057 -74.0136
relay2.orangesync.tech 40.7128 -74.006
relay.artx.market 43.6548 -79.3885
relay.wavefunc.live 41.8781 -87.6298
bitchat.nostr1.com 40.7057 -74.0136
relay.ditto.pub:443 43.6532 -79.3832
relay.wisp.talk 49.4543 11.0746
nostrelites.org 41.8781 -87.6298
relay.goodmorningbitcoin.com 43.6532 -79.3832
dm-test-nostr-rs-42-disabled.samt.st 43.6532 -79.3832
relay.chorus.community 48.5333 10.7
relay.plebchain.club 43.6532 -79.3832
nostr-relay.amethyst.name:443 39.0067 -77.4291
relay.lanacoin-eternity.com 40.8302 -74.1299
relay.edufeed.org:443 49.4521 11.0767
relay.lacompagniemaximus.com 45.3147 -73.8785
relay.dreamith.to:443 43.6532 -79.3832
nostr.stakey.net 52.3676 4.90414
nostr.n7ekb.net:443 47.4941 -122.294
relay.dyne.org 49.0291 8.35705
nostr.janx.com 43.6532 -79.3832
relay.trustr.ing 43.6548 -79.3885
relay.paulstephenborile.com:443 49.4543 11.0746
relay.wisp.talk:443 49.4543 11.0746
yabu.me 35.6092 139.73
bcast.seutoba.com.br 43.6532 -79.3832
nos.xmark.cc 50.6924 3.20113
nos.lol 50.4754 12.3683
relay.satmaxt.xyz 43.6532 -79.3832
relay.endfiat.money:443 59.3327 18.0656
relay.tapestry.ninja 40.8054 -74.0241
2 relay.lab.rytswd.com 49.4543 11.0746
3 nostr-kyomu-haskell.onrender.com relay.paulstephenborile.com:443 37.7775 49.4543 -122.397 11.0746
4 relay.damus.io:443 relay.binaryrobot.com 43.6532 -79.3832
5 treuzkas.branruz.com nostr-2.21crypto.ch 48.8575 47.5356 2.35138 8.73209
6 spookstr2.nostr1.com:443 40.7057 -74.0136
7 fanfares.nostr1.com:443 40.7057 -74.0136
8 x.kojira.io 43.6532 -79.3832
9 freelay.sovbit.host 60.1699 24.9384
10 nostr-rs-relay-qj1h.onrender.com 37.7775 -122.397
11 testnet.samt.st 43.6532 -79.3832
12 relay.angor.io 48.1046 11.6002
13 relay-arg.zombi.cloudrodion.com 1.35208 103.82
14 nostr-01.yakihonne.com 1.32123 103.695
15 relay.nostriot.com:443 nostr-relay.cbrx.io 41.5695 43.6532 -83.9786 -79.3832
16 nostr-relay.nextblockvending.com relay.guggero.org 47.2343 46.5971 -119.853 9.59652
17 nostr.aruku.ovh nostr.snowbla.de 1.27994 60.1699 103.849 24.9384
nostr.chaima.info 50.1109 8.68213
ynostr.yael.at 60.1699 24.9384
ynostr.yael.at:443 60.1699 24.9384
nostr-01.yakihonne.com:443 1.32123 103.695
spookstr2.nostr1.com 40.7057 -74.0136
relay.trustr.ing:443 43.6548 -79.3885
dev.relay.edufeed.org 49.4521 11.0767
relay2.angor.io:443 48.1046 11.6002
nostr.azzamo.net:443 52.2633 21.0283
offchain.bostr.online 43.6532 -79.3832
18 relay.zone667.com 60.1699 24.9384
19 relay.veganostr.com nexus.libernet.app:443 60.1699 43.6532 24.9384 -79.3832
20 vault.iris.to:443 relay.islandbitcoin.com 43.6532 12.8498 -79.3832 77.6545
21 relay.artx.market:443 relay-testnet.k8s.layer3.news 43.6548 37.3387 -79.3885 -121.885
22 wot.rejecttheframe.xyz nostr-relay.xbytez.io 43.6532 50.6924 -79.3832 3.20113
23 nostr.pbfs.io kasztanowa.bieda.it 50.4754 43.6532 12.3683 -79.3832
24 relay.mostro.network nostrcity-club.fly.dev 40.8302 37.7648 -74.1299 -122.432
25 strfry.shock.network relay.typedcypher.com 39.0438 51.5072 -77.4874 -0.127586
26 relay.klabo.world nostr.na.social:443 47.2343 43.6532 -119.853 -79.3832
relay.fountain.fm:443 43.6532 -79.3832
nostrja-kari.heguro.com 43.6532 -79.3832
relaisnostr.trivaco.fr 48.5734 7.75211
offchain.pub 39.1585 -94.5728
relay.degmods.com 50.4754 12.3683
nostr-relay.xbytez.io:443 50.6924 3.20113
relay.paulstephenborile.com 49.4543 11.0746
nittom.nostr1.com:443 40.7057 -74.0136
dev-relay.nostreon.com 60.1699 24.9384
nostr-rs-relay.dev.fedibtc.com:443 39.0438 -77.4874
relay.jeffg.fyi 43.6532 -79.3832
27 relay.laantungir.net -19.4692 -42.5315
28 nostr.data.haus:443 relay-dev.satlantis.io:443 50.4754 40.8302 12.3683 -74.1299
29 wot.codingarena.top rilo.nostria.app 50.4754 43.6532 12.3683 -79.3832
30 nostr.hekster.org:443 37.3986 -121.964
31 nostr-relay.amethyst.name:443 39.0067 -77.4291
32 chat-relay.zap-work.com:443 43.6532 -79.3832
33 relay.edufeed.org 49.4521 11.0767
34 syb.lol:443 43.6532 -79.3832
35 relay.sigit.io 50.4754 12.3683
36 nostr-relay.xbytez.io:443 50.6924 3.20113
37 relay.wavefunc.live 41.8781 -87.6298
38 nostr.sathoarder.com 48.5734 7.75211
39 myvoiceourstory.org 37.3598 -121.981
40 relay.underorion.se 50.1109 8.68213
41 nostr.data.haus 50.4754 12.3683
42 relay.erybody.com 41.4513 -81.7021
43 espelho.girino.org 43.6532 -79.3832
44 nostr.pbfs.io:443 50.4754 12.3683
45 wot.dergigi.com 64.1476 -21.9392
46 nostr.bitcoiner.social:443 47.6743 -117.112
47 dm-test-nostr-rs-42-disabled.samt.st 43.6532 -79.3832
48 relay.gulugulu.moe 43.6532 -79.3832
49 nostr.spicyz.io 43.6532 -79.3832
50 relay.cypherflow.ai 48.8575 2.35138
51 treuzkas.branruz.com 48.8575 2.35138
52 relay1.nostrchat.io 60.1699 24.9384
53 kotukonostr.onrender.com 37.7775 -122.397
54 nostr.plantroon.com 50.1013 8.62643
55 nostr.davenov.com 50.1109 8.68213
56 node.kommonzenze.de 49.4521 11.0767
57 relay2.veganostr.com 60.1699 24.9384
58 armada.sharegap.net 43.6532 -79.3832
59 wot.makenomistakes.ca 43.7064 -79.3986
60 nostr.2b9t.xyz:443 34.0549 -118.243
61 relay.libernet.app:443 43.6532 -79.3832
62 nostr.ps1829.com relay.dreamith.to:443 33.8851 43.6532 130.883 -79.3832
63 wot.dergigi.com relay.lightning.pub:443 64.1476 39.0438 -21.9392 -77.4874
64 relay.olas.app nostr.rtvslawenia.com 60.1699 49.4543 24.9384 11.0746
relay.lanacoin-eternity.com:443 40.8302 -74.1299
nostr.data.haus 50.4754 12.3683
relay-dev.gulugulu.moe 43.6532 -79.3832
relay.ohstr.com 43.6532 -79.3832
relay.lightning.pub 39.0438 -77.4874
relay.guggero.org 46.5971 9.59652
testnet-relay.samt.st:443 40.8302 -74.1299
thecitadel.nostr1.com 40.7057 -74.0136
nostr.ps1829.com:443 33.8851 130.883
nostr-relay.corb.net 38.8353 -104.822
relay.npubhaus.com 43.6532 -79.3832
65 nostr.21crypto.ch 47.5356 8.73209
66 nostr.tadryanom.me relay.ditto.pub:443 43.6532 -79.3832
67 nostr.myshosholoza.co.za relay.plebchain.club 52.3913 43.6532 4.66545 -79.3832
68 relay.bitmacro.cloud memlay.v0l.io 43.6532 53.3498 -79.3832 -6.26031
ribo.nostria.app 43.6532 -79.3832
relay.vrtmrz.net 43.6532 -79.3832
syb.lol 43.6532 -79.3832
relay.bebond.net:443 43.6532 -79.3832
relay.agentry.com 42.8864 -78.8784
nostr-2.21crypto.ch 47.5356 8.73209
nostrcity-club.fly.dev:443 37.7648 -122.432
articles.layer3.news:443 37.3387 -121.885
relay.internationalright-wing.org:443 -22.5022 -48.7114
nostr-relay.zimage.com 34.0549 -118.243
chat-relay.zap-work.com 43.6532 -79.3832
relay.mwaters.net 50.9871 2.12554
nostr.liberty.fans 36.9104 -89.5875
spookstr2.nostr1.com:443 40.7057 -74.0136
69 nostr.chaima.info:443 50.1109 8.68213
70 schnorr.me:443 relay.wavlake.com:443 43.6532 41.2619 -79.3832 -95.8608
71 ribo.eu.nostria.app nostr.thalheim.io:443 43.6532 60.1699 -79.3832 24.9384
72 nostr.bond relay.lightning.pub 50.1109 39.0438 8.68213 -77.4874
73 wot.nostr.party dev.relay.edufeed.org:443 36.1659 49.4521 -86.7844 11.0767
74 temp.iris.to nostr.myshosholoza.co.za:443 43.6532 52.3913 -79.3832 4.66545
75 relay.lotek-distro.com relay.binaryrobot.com:443 43.6532 -79.3832
76 relay.minibolt.info:443 wot.nostr.place 43.6532 -79.3832
relay.lanavault.space 60.1699 24.9384
relay.mmwaves.de 48.8575 2.35138
social.amanah.eblessing.co 48.1046 11.6002
nostr2.thalheim.io 49.4543 11.0746
relay.typedcypher.com 51.5072 -0.127586
relay.cosmicbolt.net 37.3986 -121.964
relayrs.notoshi.win:443 43.6532 -79.3832
nostr-relay-1.trustlessenterprise.com:443 43.6532 -79.3832
nostr.islandarea.net 35.4669 -97.6473
relay.nostrmap.net:443 60.1699 24.9384
relay.bullishbounty.com:443 43.6532 -79.3832
nostr.oxtr.dev:443 50.4754 12.3683
srtrelay.c-stellar.net 43.6532 -79.3832
relay.mccormick.cx 52.3563 4.95714
vault.iris.to 43.6532 -79.3832
relay.mccormick.cx:443 52.3563 4.95714
relay.toastr.net 40.8054 -74.0241
nostr.hifish.org 47.4244 8.57658
speakeasy.cellar.social:443 49.4543 11.0746
relay.wavlake.com 41.2619 -95.8608
testnet-relay.samt.st 40.8302 -74.1299
aeon.libretechsystems.xyz 55.486 9.86577
mostro-p2p.tech 50.1109 8.68213
nostr.wild-vibes.ts.net 48.8566 2.35222
nostr.88mph.life 52.1941 -2.21905
relay.nostrcheck.me 43.6532 -79.3832
rilo.nostria.app 43.6532 -79.3832
relay.bowlafterbowl.com 32.9483 -96.7299
relay.nostr.place:443 43.6532 -79.3832
nostr.mom:443 50.4754 12.3683
relay.nostrian-conquest.com 41.223 -111.974
herbstmeister.com 34.0549 -118.243
nrs-01.darkcloudarcade.com:443 39.1008 -94.5811
nostr.red5d.dev 43.6532 -79.3832
nostrbtc.com 43.6532 -79.3832
strfry.apps3.slidestr.net 40.4167 -3.70329
relay.sharegap.net 43.6532 -79.3832
reraw.pbla2fish.cc 43.6532 -79.3832
relay-testnet.k8s.layer3.news 37.3387 -121.885
77 nostr.sathoarder.com:443 48.5734 7.75211
78 relay.veganostr.com:443 thecitadel.nostr1.com 60.1699 40.7057 24.9384 -74.0136
79 relay-fra.zombi.cloudrodion.com:443 relay.artx.market 48.8566 43.6548 2.35222 -79.3885
80 nos.lol 50.4754 12.3683
81 nostr.plantroon.com:443 50.1013 8.62643
82 premium.primal.net 43.6532 -79.3832
83 relay.wavefunc.live:443 nas01xanthosnet.synology.me:7778 41.8781 47.1285 -87.6298 8.74735
84 nostrja-kari.heguro.com 43.6532 -79.3832
85 relay.mrmave.work 43.6532 -79.3832
86 nostrelay.circum.space 52.6907 4.8181
87 mostro-p2p.tech 50.1109 8.68213
88 wot.shaving.kiwi 43.6532 -79.3832
89 relay.fundstr.me 42.3601 -71.0589
90 nostrelay.circum.space:443 52.6907 4.8181
91 relay.nostrdice.com -33.8688 151.209
92 relay.getvia.xyz 60.1699 24.9384
93 strfry.shock.network:443 39.0438 -77.4874
94 relay.nostrmap.net:443 60.1699 24.9384
95 relay.nearhood.co.uk 51.5072 -0.127586
96 no.str.cr 10.6352 -85.4378
97 relay.getsafebox.app:443 43.6532 -79.3832
98 relay0.gfcom.info 13.6992 100.694
99 nostr.ps1829.com 33.8851 130.883
100 relay2.angor.io 48.1046 11.6002
101 relay.stickeroo.is-cool.dev 37.3387 -121.885
102 ricardo-oem.tailb5546.ts.net 40.7128 -74.006
103 relay.typedcypher.com:443 51.5072 -0.127586
104 relay.paulstephenborile.com 49.4543 11.0746
105 nittom.nostr1.com 40.7057 -74.0136
106 conduitl2.fly.dev 37.7648 -122.432
107 nostr.rikmeijer.nl 51.7111 5.36809
108 relay.thecryptosquid.com 50.4754 12.3683
109 spookstr2.nostr1.com 40.7057 -74.0136
110 offchain.bostr.online 43.6532 -79.3832
111 nostr.planix.org 43.6532 -79.3832
112 relay.mccormick.cx 52.3563 4.95714
113 0x-nostr-relay.fly.dev 37.7648 -122.432
114 nostr.wecsats.io 43.6532 -79.3832
115 schnorr.me 43.6532 -79.3832
116 relay.satmaxt.xyz 43.6532 -79.3832
117 relay.bornheimer.app 51.5072 -0.127586
118 relay.nostrhub.fr 48.1045 11.6004
119 blossom.gnostr.cloud:443 43.6532 -79.3832
120 nostr-02.yakihonne.com:443 1.32123 103.695
121 dev.relay.stream 43.6532 -79.3832
122 ithurtswhenip.ee 51.5072 -0.127586
123 nostr.myshosholoza.co.za 52.3913 4.66545
124 relayrs.notoshi.win:443 43.6532 -79.3832
125 relay-rpi.edufeed.org:443 49.4521 11.0767
126 kotukonostr.onrender.com relay.olas.app:443 37.7775 60.1699 -122.397 24.9384
127 bridge.tagomago.me nostr.unkn0wn.world 42.3601 46.8499 -71.0589 9.53287
128 nostr.tadryanom.me:443 relay.mitchelltribe.com 43.6532 39.0438 -79.3832 -77.4874
129 relay.gulugulu.moe:443 yabu.me 43.6532 35.6092 -79.3832 139.73
130 nostr.nodesmap.com 59.3327 18.0656
131 dm-test-strfry-generic.samt.st 43.6532 -79.3832
132 nostr2.girino.org:443 43.6532 -79.3832
133 wot.brightbolt.net 47.6735 -116.781
134 strfry.shock.network 39.0438 -77.4874
135 relay.kilombino.com 43.6532 -79.3832
136 relay.nostr.blockhenge.com 39.0438 -77.4874
137 shu04.shugur.net 25.2048 55.2708
138 relay-rpi.edufeed.org 49.4521 11.0767
139 relay.bullishbounty.com:443 43.6532 -79.3832
140 vault.iris.to:443 43.6532 -79.3832
141 relay.mostro.network:443 40.8302 -74.1299
142 offchain.pub:443 39.1585 -94.5728
143 soloco.nl 43.6532 -79.3832
144 relay.nostu.be 40.4167 -3.70329
145 nostr.pbfs.io 50.4754 12.3683
146 relay.directsponsor.net 42.8864 -78.8784
147 relay.decentralia.fr 49.4282 10.9796
148 relayrs.notoshi.win 43.6532 -79.3832
149 nostr-relay.amethyst.name 39.0067 -77.4291
150 relay.arx-ccn.com 50.4754 12.3683
151 nostr.spaceshell.xyz 43.6532 -79.3832
152 relay-fra.zombi.cloudrodion.com 48.8566 2.35222
153 rilo.nostria.app:443 43.6532 -79.3832
154 relay.trotters.cc:443 43.6532 -79.3832
155 nostr.overmind.lol:443 43.6532 -79.3832
156 nostr.girino.org:443 43.6532 -79.3832
157 bitsat.molonlabe.holdings 51.4012 -1.3147
158 nostr.azzamo.net 52.2633 21.0283
159 insta-relay.apps3.slidestr.net 40.4167 -3.70329
160 bridge.tagomago.me 42.3601 -71.0589
161 nostr.thalheim.io 60.1699 24.9384
162 relay.artx.market:443 43.6548 -79.3885
163 nostr.openhoofd.nl 51.5717 3.70417
164 nostr.bond 50.1109 8.68213
165 relay.earthly.city 34.1749 -118.54
166 nexus.libernet.app 43.6532 -79.3832
167 relay.plebeian.market 50.1109 8.68213
168 relay.nostr.net 43.6532 -79.3832
169 nostr.overmind.lol 43.6532 -79.3832
170 relay.ohstr.com 43.6532 -79.3832
171 testnet-relay.samt.st:443 40.8302 -74.1299
172 relay01.lnfi.network 35.6764 139.65
173 relay.mostr.pub:443 43.6532 -79.3832
174 wot.nostr.party 36.1659 -86.7844
175 relayone.soundhsa.com 39.1008 -94.5811
176 relay.mostro.network 40.8302 -74.1299
177 ribo.eu.nostria.app 43.6532 -79.3832
178 chat-relay.zap-work.com 43.6532 -79.3832
179 relay.nostreon.com 60.1699 24.9384
180 nostr-rs-relay.dev.fedibtc.com:443 39.0438 -77.4874
181 nostr.quali.chat:443 60.1699 24.9384
182 relay.internationalright-wing.org:443 -22.5022 -48.7114
183 relay.mitchelltribe.com:443 39.0438 -77.4874
184 relay.satlantis.io 40.8054 -74.0241
185 nittom.nostr1.com:443 40.7057 -74.0136
186 nostr.janx.com 43.6532 -79.3832
187 nostr.carroarmato0.be:443 50.914 3.21378
188 relay.mmwaves.de:443 48.8575 2.35138
189 relay.chorus.community:443 48.5333 10.7
190 wot.utxo.one 43.6532 -79.3832
191 relay.plebeian.market:443 50.1109 8.68213
192 relay.cosmicbolt.net 37.3986 -121.964
193 x.kojira.io:443 43.6532 -79.3832
194 top.testrelay.top 43.6532 -79.3832
195 nos.lol:443 50.4754 12.3683
196 dev.relay.edufeed.org 49.4521 11.0767
197 relayone.geektank.ai:443 39.1008 -94.5811
198 relay.nostar.org 43.6532 -79.3832
199 nostr.oxtr.dev:443 50.4754 12.3683
200 nostr.88mph.life 52.1941 -2.21905
201 relay.staging.commonshub.brussels 49.4543 11.0746
202 weboftrust.libretechsystems.xyz 55.4724 9.87335
203 relay.openfarmtools.org 60.1699 24.9384
204 cs-relay.nostrdev.com 50.4754 12.3683
205 relay.inforsupports.com 43.6532 -79.3832
206 nostr-verified.wellorder.net 45.5201 -122.99
207 nostr.hekster.org 37.3986 -121.964
208 relay.gulugulu.moe:443 43.6532 -79.3832
209 relay.mwaters.net 50.9871 2.12554
210 nostrcity-club.fly.dev:443 37.7648 -122.432
211 relay.vrtmrz.net:443 43.6532 -79.3832
212 relay.nostr.place 43.6532 -79.3832
213 relay.wavefunc.live:443 41.8781 -87.6298
214 nostr.islandarea.net 35.4669 -97.6473
215 purplerelay.com:443 43.6532 -79.3832
216 nostr-relay.psfoundation.info:443 39.0438 -77.4874
217 r.0kb.io 32.789 -96.7989
218 relay-us.zombi.cloudrodion.com 40.7862 -74.0743
219 relay.mulatta.io 37.5665 126.978
220 strfry.bonsai.com:443 39.0438 -77.4874
221 bendernostur.duckdns.org:8443 50.1109 8.68213
222 vault.iris.to 43.6532 -79.3832
223 ec2.f7z.io 60.1699 24.9384
224 nostr.debate.report 50.1109 8.68213
225 wot.codingarena.top 50.4754 12.3683
226 relay.layer.systems:443 49.0291 8.35695
227 relay.degmods.com 50.4754 12.3683
228 nostr.mom 50.4754 12.3683
229 ribo.us.nostria.app:443 43.6532 -79.3832
230 adre.su 59.9311 30.3609
231 wot.sudocarlos.com 43.6532 -79.3832
232 relay.nostrian-conquest.com 41.223 -111.974
233 nostr-relay.nextblockvending.com 47.2343 -119.853
234 relay.endfiat.money:443 59.3327 18.0656
235 nostr-rs-relay.dev.fedibtc.com 39.0438 -77.4874
236 nostr.carroarmato0.be 50.914 3.21378
237 relay.cypherflow.ai:443 48.8575 2.35138
238 nostr.girino.org 43.6532 -79.3832
239 nostr.thebiglake.org 32.71 -96.6745
240 strfry.ymir.cloud 43.6532 -79.3832
241 relay.mypathtofire.de 42.8864 -78.8784
242 relay.lanacoin-eternity.com 40.8302 -74.1299
243 nostr.snowbla.de:443 60.1699 24.9384
244 relay.ditto.pub 43.6532 -79.3832
245 relay.damus.io 43.6532 -79.3832
246 relay.ru.ac.th 13.7607 100.627
247 nrs-01.darkcloudarcade.com 39.1008 -94.5811
248 testnet-relay.samt.st 40.8302 -74.1299
249 antiprimal.net 43.6532 -79.3832
250 bitchat.nostr1.com 40.7057 -74.0136
251 relay.snort.social 53.3498 -6.26031
252 relay.mccormick.cx:443 52.3563 4.95714
253 relay02.lnfi.network 35.6764 139.65
254 srtrelay.c-stellar.net 43.6532 -79.3832
255 relay.minibolt.info 43.6532 -79.3832
256 nostrride.io 37.3986 -121.964
257 articles.layer3.news:443 37.3387 -121.885
258 rele.speyhard.fi 51.5072 -0.127586
259 relay.aarpia.com 37.3986 -121.964
260 nostr.chaima.info 50.1109 8.68213
261 relay.wisp.talk:443 49.4543 11.0746
262 relay.agorist.space:443 52.3734 4.89406
263 strfry.bonsai.com 39.0438 -77.4874
264 nostr.hifish.org 47.4244 8.57658
265 offchain.pub 39.1585 -94.5728
266 nostr.spicyz.io:443 43.6532 -79.3832
267 relay.beginningend.com 35.2227 -97.4786
268 relay.sharegap.net 43.6532 -79.3832
269 nostr.purpura.cloud 43.6532 -79.3832
270 nrs-01.darkcloudarcade.com:443 39.1008 -94.5811
271 relay.fountain.fm:443 43.6532 -79.3832
272 relay.olas.app 60.1699 24.9384
273 relay.mmwaves.de 48.8575 2.35138
274 relay.openresist.com:443 43.6532 -79.3832
275 relay.homeinhk.xyz 35.694 139.754
276 relay.libernet.app 43.6532 -79.3832
277 relay.comcomponent.com 43.6532 -79.3832
278 nostr.tac.lol 47.4748 -122.273
279 relay.goodmorningbitcoin.com 43.6532 -79.3832
280 relay.nostriot.com:443 41.5695 -83.9786
281 bcast.girino.org 43.6532 -79.3832
282 nostr.azzamo.net:443 52.2633 21.0283
283 relay.islandbitcoin.com:443 12.8498 77.6545
284 pool.libernet.app 43.6532 -79.3832
285 test.thedude.cloud 50.1109 8.68213
286 nostrelites.org 41.8781 -87.6298
287 nostr.infero.net 35.6764 139.65
288 relay.primal.net 43.6532 -79.3832
289 ribo.nostria.app 43.6532 -79.3832
290 relay.chorus.community 48.5333 10.7
291 bitcoiner.social:443 47.6743 -117.112
292 relay.wisp.talk 49.4543 11.0746
293 relay.layer.systems 49.0291 8.35695
294 relay-dev.satlantis.io 40.8302 -74.1299
295 nostr.bitcoiner.social 47.6743 -117.112
296 relay.lanavault.space:443 60.1699 24.9384
297 relay.staging.plebeian.market 51.5072 -0.127586
298 infinity-signal-relay.digitalforlifeagency.workers.dev 43.6532 -79.3832
299 relay.fountain.fm 43.6532 -79.3832
300 nostr.middling.mydns.jp 35.8099 140.12
301 relay.dreamith.to 43.6532 -79.3832
302 relay.satmaxt.xyz:443 43.6532 -79.3832
303 shu03.shugur.net 25.2048 55.2708
304 zealand-charts-craig-thru.trycloudflare.com 43.6532 -79.3832
305 nostr.computingcache.com 34.0356 -118.442
306 ribo.us.nostria.app 43.6532 -79.3832
307 relay.agentry.com 42.8864 -78.8784
308 nostr.hifish.org:443 47.4244 8.57658
309 nostr.vulpem.com 49.4543 11.0746
310 relay.cosmicbolt.net:443 37.3986 -121.964
311 nostr-02.yakihonne.com 1.32123 103.695
312 r.0kb.io:443 32.789 -96.7989
313 nostr-relay.corb.net 38.8353 -104.822
314 ribo.eu.nostria.app:443 43.6532 -79.3832
315 nostr-relay.psfoundation.info 39.0438 -77.4874
316 relay.wellorder.net 45.5201 -122.99
317 relay.novospes.com 43.6532 -79.3832
318 nostr-dev.wellorder.net 45.5201 -122.99
319 relay.endfiat.money 59.3327 18.0656
320 relay.angor.io:443 48.1046 11.6002
321 relay-fra.zombi.cloudrodion.com:443 48.8566 2.35222
322 strfry.openhoofd.nl 51.5717 3.70417
323 relay.getsafebox.app 43.6532 -79.3832
324 relay.openresist.com 43.6532 -79.3832
325 relay5.bitransfer.org 43.6532 -79.3832
326 nostr.na.social 43.6532 -79.3832
327 portal-relay.pareto.space 49.0291 8.35696
328 nostr.notribe.net:443 40.8302 -74.1299
329 relay.bitmacro.cloud 43.6532 -79.3832
330 no.str.cr:443 10.6352 -85.4378
331 relay.klabo.world 47.2343 -119.853
332 nostr.notribe.net 40.8302 -74.1299
333 relay.staging.plebeian.market:443 51.5072 -0.127586
334 relay.nostrmap.net 60.1699 24.9384
335 temp.iris.to 43.6532 -79.3832
336 nostr.sovereignservices.xyz 43.6532 -79.3832
337 nostr.liberty.fans 36.9104 -89.5875
338 relay.nostrian-conquest.com:443 41.223 -111.974
339 relay.nostriot.com 41.5695 -83.9786
340 nostrbtc.com 43.6532 -79.3832
341 shu02.shugur.net 21.4902 39.2246
342 relay.kalcafe.xyz 37.3986 -121.964
343 relay.illuminodes.com 43.6532 -79.3832
344 relay.wavlake.com 41.2619 -95.8608
345 nostr.ps1829.com:443 33.8851 130.883
346 dm-test-strfry-discovery.samt.st 43.6532 -79.3832
347 nostr.wecsats.io:443 43.6532 -79.3832
348 nostr-pub.wellorder.net 45.5201 -122.99
349 nostr.dlcdevkit.com:443 40.0992 -83.1141
350 nostr.mom:443 50.4754 12.3683
351 ribo.nostria.app:443 43.6532 -79.3832
352 nostr.2b9t.xyz 34.0549 -118.243
353 nostr.data.haus:443 50.4754 12.3683
354 staging.yabu.me 35.6092 139.73
355 relay.sigit.io:443 50.4754 12.3683
356 relay.edufeed.org:443 49.4521 11.0767
357 nostr-01.yakihonne.com:443 1.32123 103.695
358 reraw.pbla2fish.cc 43.6532 -79.3832
359 cs-relay.nostrdev.com:443 50.4754 12.3683
360 herbstmeister.com 34.0549 -118.243
361 relay.minibolt.info:443 43.6532 -79.3832
362 relay2.angor.io:443 48.1046 11.6002
363 social.amanah.eblessing.co 48.1046 11.6002
364 nostr.stakey.net 52.3676 4.90414
365 nostr.computingcache.com:443 34.0356 -118.442
366 slick.mjex.me 39.0418 -77.4744
367 fanfares.nostr1.com 40.7057 -74.0136
368 bitcoinostr.duckdns.org 43.3434 -3.99532
369 nostr.oxtr.dev 50.4754 12.3683
370 cache.trustr.ing 43.6548 -79.3885
371 purplerelay.com 43.6532 -79.3832
372 nostr-kyomu-haskell.onrender.com 37.7775 -122.397
373 nostr-relay.corb.net:443 38.8353 -104.822
374 relay-dev.gulugulu.moe 43.6532 -79.3832
375 prl.plus 55.7628 37.5983
376 nostr.tac.lol:443 47.4748 -122.273
377 relay.mostr.pub 43.6532 -79.3832
378 schnorr.me:443 43.6532 -79.3832
379 dev-relay.nostreon.com 60.1699 24.9384
380 nostr.islandarea.net:443 35.4669 -97.6473
381 bucket.coracle.social 37.7775 -122.397
382 blossom.gnostr.cloud 43.6532 -79.3832
383 relay.solife.me 43.6532 -79.3832
384 nostr.quali.chat 60.1699 24.9384
385 relay.vrtmrz.net 43.6532 -79.3832
386 relay-dev.gulugulu.moe:443 43.6532 -79.3832
387 relay.bullishbounty.com 43.6532 -79.3832
388 relay.fckstate.net 59.3293 18.0686
389 nostr.rtvslawenia.com:443 49.4543 11.0746
390 relay.nostx.io 43.6532 -79.3832
391 relay.agorist.space 52.3734 4.89406
392 relay.notoshi.win 13.7829 100.546
393 dm-test-strfry-discovery.samt.st:443 43.6532 -79.3832
394 relay.trotters.cc 43.6532 -79.3832
395 relay.lanavault.space 60.1699 24.9384
396 public.crostr.com:443 43.6532 -79.3832
397 nostr.stakey.net:443 52.3676 4.90414
398 relay.nostr.place:443 43.6532 -79.3832
399 nostr.dlcdevkit.com 40.0992 -83.1141
400 nostr.aruku.ovh 1.27994 103.849
401 satsage.xyz 37.3986 -121.964
402 strfry.apps3.slidestr.net 40.4167 -3.70329
403 nostr2.girino.org 43.6532 -79.3832
404 relay.samt.st 40.8302 -74.1299
405 articles.layer3.news 37.3387 -121.885
406 aeon.libretechsystems.xyz 55.486 9.86577
407 relay.routstr.com 59.4016 17.9455
408 relay.ohstr.com:443 43.6532 -79.3832
409 relay.lanacoin-eternity.com:443 40.8302 -74.1299
410 strfry.openhoofd.nl:443 51.5717 3.70417
411 nostr.blankfors.se 60.1699 24.9384
412 nostr-2.21crypto.ch:443 47.5356 8.73209
413 relayone.soundhsa.com:443 39.1008 -94.5811
414 relay.lab.rytswd.com:443 49.4543 11.0746
415 nostr.tagomago.me 42.3601 -71.0589
416 relay.0xchat.com:443 43.6532 -79.3832