mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 22:25:20 +00:00
Compare commits
70
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fbbd24021 | ||
|
|
ec54877140 | ||
|
|
293d627c28 | ||
|
|
0c3f84224c | ||
|
|
7323c0b96c | ||
|
|
7bb835ffc9 | ||
|
|
23d63ab4df | ||
|
|
fa3c74f941 | ||
|
|
9bac52051a | ||
|
|
7b9ffe464a | ||
|
|
7b940485d9 | ||
|
|
5a87ee3e62 | ||
|
|
342eabbc00 | ||
|
|
241ce2d52c | ||
|
|
18b56e7393 | ||
|
|
beb04fc887 | ||
|
|
5fcffefa28 | ||
|
|
46ae039587 | ||
|
|
917f7ebe5f | ||
|
|
b47fb736f4 | ||
|
|
ebb5bb6558 | ||
|
|
7cfdcfe174 | ||
|
|
21e0cbc607 | ||
|
|
1b439a543e | ||
|
|
d30a3b14cf | ||
|
|
d03128612d | ||
|
|
ae294aab6d | ||
|
|
07b0e146f2 | ||
|
|
cc5939cb13 | ||
|
|
cada784844 | ||
|
|
b1aeb931bc | ||
|
|
b675738664 | ||
|
|
4091a30f10 | ||
|
|
238311aefb | ||
|
|
6defae71c6 | ||
|
|
b533d9560d | ||
|
|
f41a390a94 | ||
|
|
f83316bd1f | ||
|
|
09818a02ed | ||
|
|
9cd955ae2f | ||
|
|
49b1413d85 | ||
|
|
31be6b83a7 | ||
|
|
1563209797 | ||
|
|
99896bcded | ||
|
|
4b3077169a | ||
|
|
b84c36c6fa | ||
|
|
3eac5858e4 | ||
|
|
10b7c1fd80 | ||
|
|
bf3249aef7 | ||
|
|
95a6ec7315 | ||
|
|
74864472c7 | ||
|
|
9404c03477 | ||
|
|
6faa46a22f | ||
|
|
e02f4327c0 | ||
|
|
ce31d85323 | ||
|
|
b6d8a5b758 | ||
|
|
6630f5a792 | ||
|
|
0f5299a0f5 | ||
|
|
eb3bbfd861 | ||
|
|
a37243e780 | ||
|
|
90b134186b | ||
|
|
3c7e14f49d | ||
|
|
31275856dd | ||
|
|
b6cf44a824 | ||
|
|
b6ae08be60 | ||
|
|
d469704c34 | ||
|
|
c975abf2ff | ||
|
|
aff700a15e | ||
|
|
869d766f8d | ||
|
|
1b4f120014 |
@@ -25,47 +25,18 @@ jobs:
|
||||
wget -q https://raw.githubusercontent.com/permissionlesstech/georelays/refs/heads/main/nostr_relays.csv
|
||||
mv nostr_relays.csv ./relays/online_relays_gps.csv
|
||||
|
||||
- name: Configure git
|
||||
- name: Check for changes
|
||||
id: git-check
|
||||
run: |
|
||||
git config user.email "action@github.com"
|
||||
git config user.name "GitHub Action"
|
||||
git diff --exit-code || echo "changes=true" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create update branch if changes
|
||||
id: create_branch
|
||||
- name: Commit and push changes
|
||||
if: steps.git-check.outputs.changes == 'true'
|
||||
run: |
|
||||
# exit early if no changes
|
||||
if git diff --quiet --relays/online_relays_gps.csv; then
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# branch name with timestamp
|
||||
BRANCH="update-georelays-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
git checkout -b "$BRANCH"
|
||||
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add relays/online_relays_gps.csv
|
||||
git commit -m "Automated update of relay data - $(date -u --rfc-3339=seconds)"
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push branch
|
||||
if: steps.create_branch.outputs.changed == 'true'
|
||||
run: |
|
||||
git push --set-upstream origin "${{ steps.create_branch.outputs.branch }}"
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.create_branch.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: Automated update of relay data
|
||||
branch: ${{ steps.create_branch.outputs.branch }}
|
||||
base: main
|
||||
title: Automated update of relay data
|
||||
body: |
|
||||
This PR was created automatically by the scheduled workflow. It updates relays/online_relays_gps.csv from the GeoRelays source.
|
||||
labels: automated, georelays
|
||||
|
||||
- name: No changes
|
||||
if: steps.create_branch.outputs.changed != 'true'
|
||||
run: echo "No changes to relays/online_relays_gps.csv"
|
||||
git commit -m "Automated update of relay data - $(date -u)"
|
||||
git push
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+2
-2
@@ -16,7 +16,7 @@ let package = Package(
|
||||
),
|
||||
],
|
||||
dependencies:[
|
||||
.package(path: "localPackages/Tor"),
|
||||
.package(path: "localPackages/Arti"),
|
||||
.package(path: "localPackages/BitLogger"),
|
||||
.package(url: "https://github.com/21-DOT-DEV/swift-secp256k1", exact: "0.21.1")
|
||||
],
|
||||
@@ -26,7 +26,7 @@ let package = Package(
|
||||
dependencies: [
|
||||
.product(name: "P256K", package: "swift-secp256k1"),
|
||||
.product(name: "BitLogger", package: "BitLogger"),
|
||||
.product(name: "Tor", package: "Tor")
|
||||
.product(name: "Tor", package: "Arti")
|
||||
],
|
||||
path: "bitchat",
|
||||
exclude: [
|
||||
|
||||
Generated
+5
-7
@@ -14,7 +14,6 @@
|
||||
A6E3E5722E7703760032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E5712E7703760032EA8A /* BitLogger */; };
|
||||
A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3EA7E2E7706720032EA8A /* Tor */; };
|
||||
A6E3EA812E7706A80032EA8A /* Tor in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3EA802E7706A80032EA8A /* Tor */; };
|
||||
A6F183FD2E948783006A9046 /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A6F183FC2E948783006A9046 /* tor-nolzma.xcframework */; };
|
||||
E0A1B2C3D4E5F6012345678D /* relays/online_relays_gps.csv in Resources */ = {isa = PBXBuildFile; fileRef = E0A1B2C3D4E5F6012345678A /* relays/online_relays_gps.csv */; };
|
||||
E0A1B2C3D4E5F6012345678E /* relays/online_relays_gps.csv in Resources */ = {isa = PBXBuildFile; fileRef = E0A1B2C3D4E5F6012345678A /* relays/online_relays_gps.csv */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -162,7 +161,6 @@
|
||||
B5A5CC493FFB3D8966548140 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
files = (
|
||||
A6F183FD2E948783006A9046 /* tor-nolzma.xcframework in Frameworks */,
|
||||
A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */,
|
||||
885BBED78092484A5B069461 /* P256K in Frameworks */,
|
||||
A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */,
|
||||
@@ -345,7 +343,7 @@
|
||||
packageReferences = (
|
||||
B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */,
|
||||
A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */,
|
||||
A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Tor" */,
|
||||
A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Arti" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 90;
|
||||
projectDirPath = "";
|
||||
@@ -913,9 +911,9 @@
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = localPackages/BitLogger;
|
||||
};
|
||||
A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Tor" */ = {
|
||||
A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Arti" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = localPackages/Tor;
|
||||
relativePath = localPackages/Arti;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
@@ -924,8 +922,8 @@
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/21-DOT-DEV/swift-secp256k1";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.21.1;
|
||||
kind = exactVersion;
|
||||
version = 0.21.1;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
@@ -63,6 +63,10 @@ struct BitchatApp: App {
|
||||
|
||||
// Initialize network activation policy; will start Tor/Nostr only when allowed
|
||||
NetworkActivationService.shared.start()
|
||||
|
||||
// Start presence service (will wait for Tor readiness)
|
||||
GeohashPresenceService.shared.start()
|
||||
|
||||
// Check for shared content
|
||||
checkForSharedContent()
|
||||
}
|
||||
|
||||
@@ -165,8 +165,12 @@ final class NoiseCipherState {
|
||||
// MARK: - Sliding Window Replay Protection
|
||||
|
||||
/// Check if nonce is valid for replay protection
|
||||
/// BCH-01-010: Use safe arithmetic to prevent integer overflow
|
||||
private func isValidNonce(_ receivedNonce: UInt64) -> Bool {
|
||||
if receivedNonce + UInt64(Self.REPLAY_WINDOW_SIZE) <= highestReceivedNonce {
|
||||
// Safe overflow check: instead of (receivedNonce + WINDOW_SIZE <= highest)
|
||||
// use (highest >= WINDOW_SIZE && receivedNonce <= highest - WINDOW_SIZE)
|
||||
let windowSize = UInt64(Self.REPLAY_WINDOW_SIZE)
|
||||
if highestReceivedNonce >= windowSize && receivedNonce <= highestReceivedNonce - windowSize {
|
||||
return false // Too old, outside window
|
||||
}
|
||||
|
||||
@@ -348,15 +352,19 @@ final class NoiseCipherState {
|
||||
do {
|
||||
let plaintext = try ChaChaPoly.open(sealedBox, using: key, authenticating: associatedData)
|
||||
|
||||
// BCH-01-010: Atomic nonce state update
|
||||
// Both replay window marking and nonce increment must complete together
|
||||
// to prevent state desynchronization. We perform both after successful
|
||||
// decryption only, ensuring state consistency on any failure path.
|
||||
if useExtractedNonce {
|
||||
// Mark nonce as seen after successful decryption
|
||||
markNonceAsSeen(decryptionNonce)
|
||||
}
|
||||
nonce += 1
|
||||
|
||||
return plaintext
|
||||
} catch {
|
||||
// Decryption failed - nonce state remains unchanged (atomic rollback)
|
||||
SecureLogger.debug("Decrypt failed: \(error) for nonce \(decryptionNonce)")
|
||||
// Log authentication failures with nonce info
|
||||
SecureLogger.error("Decryption failed at nonce \(decryptionNonce)", category: .encryption)
|
||||
throw error
|
||||
}
|
||||
@@ -459,9 +467,32 @@ final class NoiseSymmetricState {
|
||||
let c1 = NoiseCipherState(key: tempKey1, useExtractedNonce: useExtractedNonce)
|
||||
let c2 = NoiseCipherState(key: tempKey2, useExtractedNonce: useExtractedNonce)
|
||||
|
||||
// BCH-01-010: Clear symmetric state after split per Noise spec
|
||||
// The chaining key and hash should not be retained after handshake completes
|
||||
clearSensitiveData()
|
||||
|
||||
return (c1, c2)
|
||||
}
|
||||
|
||||
/// BCH-01-010: Securely clear sensitive cryptographic state
|
||||
/// Called after split() to clear chaining key and hash per Noise spec
|
||||
func clearSensitiveData() {
|
||||
// Clear chaining key by overwriting with zeros
|
||||
let chainingKeyCount = chainingKey.count
|
||||
chainingKey = Data(repeating: 0, count: chainingKeyCount)
|
||||
|
||||
// Clear hash by overwriting with zeros
|
||||
let hashCount = hash.count
|
||||
hash = Data(repeating: 0, count: hashCount)
|
||||
|
||||
// Clear the internal cipher state
|
||||
cipherState.clearSensitiveData()
|
||||
}
|
||||
|
||||
deinit {
|
||||
clearSensitiveData()
|
||||
}
|
||||
|
||||
// HKDF implementation
|
||||
private func hkdf(chainingKey: Data, inputKeyMaterial: Data, numOutputs: Int) -> [Data] {
|
||||
let tempKey = HMAC<SHA256>.authenticationCode(for: inputKeyMaterial, using: SymmetricKey(data: chainingKey))
|
||||
@@ -807,16 +838,20 @@ final class NoiseHandshakeState {
|
||||
return currentPattern >= messagePatterns.count
|
||||
}
|
||||
|
||||
func getTransportCiphers(useExtractedNonce: Bool) throws -> (send: NoiseCipherState, receive: NoiseCipherState) {
|
||||
func getTransportCiphers(useExtractedNonce: Bool) throws -> (send: NoiseCipherState, receive: NoiseCipherState, handshakeHash: Data) {
|
||||
guard isHandshakeComplete() else {
|
||||
throw NoiseError.handshakeNotComplete
|
||||
}
|
||||
|
||||
// BCH-01-010: Capture handshake hash BEFORE split() clears symmetric state
|
||||
let finalHandshakeHash = symmetricState.getHandshakeHash()
|
||||
|
||||
let (c1, c2) = symmetricState.split(useExtractedNonce: useExtractedNonce)
|
||||
|
||||
// Initiator uses c1 for sending, c2 for receiving
|
||||
// Responder uses c2 for sending, c1 for receiving
|
||||
return role == .initiator ? (c1, c2) : (c2, c1)
|
||||
let ciphers = role == .initiator ? (c1, c2) : (c2, c1)
|
||||
return (send: ciphers.0, receive: ciphers.1, handshakeHash: finalHandshakeHash)
|
||||
}
|
||||
|
||||
func getRemoteStaticPublicKey() -> Curve25519.KeyAgreement.PublicKey? {
|
||||
@@ -877,19 +912,44 @@ enum NoiseError: Error {
|
||||
case nonceExceeded
|
||||
}
|
||||
|
||||
// MARK: - Constant-Time Operations
|
||||
|
||||
/// BCH-01-010: Constant-time comparison to prevent timing side-channel attacks
|
||||
/// This function compares two Data objects in constant time, preventing
|
||||
/// information leakage via timing analysis.
|
||||
private func constantTimeCompare(_ a: Data, _ b: Data) -> Bool {
|
||||
guard a.count == b.count else { return false }
|
||||
|
||||
var result: UInt8 = 0
|
||||
for i in 0..<a.count {
|
||||
result |= a[a.startIndex.advanced(by: i)] ^ b[b.startIndex.advanced(by: i)]
|
||||
}
|
||||
return result == 0
|
||||
}
|
||||
|
||||
/// BCH-01-010: Constant-time check if all bytes are zero
|
||||
private func constantTimeIsZero(_ data: Data) -> Bool {
|
||||
var result: UInt8 = 0
|
||||
for byte in data {
|
||||
result |= byte
|
||||
}
|
||||
return result == 0
|
||||
}
|
||||
|
||||
// MARK: - Key Validation
|
||||
|
||||
extension NoiseHandshakeState {
|
||||
/// Validate a Curve25519 public key
|
||||
/// Checks for weak/invalid keys that could compromise security
|
||||
/// BCH-01-010: Uses constant-time operations to prevent timing side-channels
|
||||
static func validatePublicKey(_ keyData: Data) throws -> Curve25519.KeyAgreement.PublicKey {
|
||||
// Check key length
|
||||
guard keyData.count == 32 else {
|
||||
throw NoiseError.invalidPublicKey
|
||||
}
|
||||
|
||||
// Check for all-zero key (point at infinity)
|
||||
if keyData.allSatisfy({ $0 == 0 }) {
|
||||
// BCH-01-010: Constant-time check for all-zero key (point at infinity)
|
||||
if constantTimeIsZero(keyData) {
|
||||
throw NoiseError.invalidPublicKey
|
||||
}
|
||||
|
||||
@@ -914,8 +974,16 @@ extension NoiseHandshakeState {
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) // Another bad point
|
||||
]
|
||||
|
||||
// Check against known bad points
|
||||
if lowOrderPoints.contains(keyData) {
|
||||
// BCH-01-010: Constant-time check against known bad points
|
||||
// We check all points and accumulate matches to avoid early exit timing leaks
|
||||
var foundBadPoint = false
|
||||
for badPoint in lowOrderPoints {
|
||||
if constantTimeCompare(keyData, badPoint) {
|
||||
foundBadPoint = true
|
||||
}
|
||||
}
|
||||
|
||||
if foundBadPoint {
|
||||
SecureLogger.warning("Low-order point detected", category: .security)
|
||||
throw NoiseError.invalidPublicKey
|
||||
}
|
||||
|
||||
@@ -102,8 +102,8 @@ class NoiseSession {
|
||||
|
||||
// Check if handshake is complete
|
||||
if handshake.isHandshakeComplete() {
|
||||
// Get transport ciphers
|
||||
let (send, receive) = try handshake.getTransportCiphers(useExtractedNonce: true)
|
||||
// Get transport ciphers and handshake hash (hash captured before split clears state)
|
||||
let (send, receive, hash) = try handshake.getTransportCiphers(useExtractedNonce: true)
|
||||
sendCipher = send
|
||||
receiveCipher = receive
|
||||
|
||||
@@ -111,7 +111,7 @@ class NoiseSession {
|
||||
remoteStaticPublicKey = handshake.getRemoteStaticPublicKey()
|
||||
|
||||
// Store handshake hash for channel binding
|
||||
handshakeHash = handshake.getHandshakeHash()
|
||||
handshakeHash = hash
|
||||
|
||||
state = .established
|
||||
handshakeState = nil // Clear handshake state
|
||||
@@ -128,8 +128,8 @@ class NoiseSession {
|
||||
|
||||
// Check if handshake is complete after writing
|
||||
if handshake.isHandshakeComplete() {
|
||||
// Get transport ciphers
|
||||
let (send, receive) = try handshake.getTransportCiphers(useExtractedNonce: true)
|
||||
// Get transport ciphers and handshake hash (hash captured before split clears state)
|
||||
let (send, receive, hash) = try handshake.getTransportCiphers(useExtractedNonce: true)
|
||||
sendCipher = send
|
||||
receiveCipher = receive
|
||||
|
||||
@@ -137,7 +137,7 @@ class NoiseSession {
|
||||
remoteStaticPublicKey = handshake.getRemoteStaticPublicKey()
|
||||
|
||||
// Store handshake hash for channel binding
|
||||
handshakeHash = handshake.getHandshakeHash()
|
||||
handshakeHash = hash
|
||||
|
||||
state = .established
|
||||
handshakeState = nil // Clear handshake state
|
||||
|
||||
@@ -18,6 +18,7 @@ struct NostrProtocol {
|
||||
case seal = 13 // NIP-17 sealed event
|
||||
case giftWrap = 1059 // NIP-59 gift wrap
|
||||
case ephemeralEvent = 20000
|
||||
case geohashPresence = 20001
|
||||
}
|
||||
|
||||
/// Create a NIP-17 private message
|
||||
@@ -125,6 +126,24 @@ struct NostrProtocol {
|
||||
return try event.sign(with: schnorrKey)
|
||||
}
|
||||
|
||||
/// Create a geohash presence heartbeat (kind 20001)
|
||||
/// Must contain empty content and NO nickname tag
|
||||
static func createGeohashPresenceEvent(
|
||||
geohash: String,
|
||||
senderIdentity: NostrIdentity
|
||||
) throws -> NostrEvent {
|
||||
let tags = [["g", geohash]]
|
||||
let event = NostrEvent(
|
||||
pubkey: senderIdentity.publicKeyHex,
|
||||
createdAt: Date(),
|
||||
kind: .geohashPresence,
|
||||
tags: tags,
|
||||
content: ""
|
||||
)
|
||||
let schnorrKey = try senderIdentity.schnorrSigningKey()
|
||||
return try event.sign(with: schnorrKey)
|
||||
}
|
||||
|
||||
/// Create a persistent location note (kind 1: text note) tagged to a street-level geohash.
|
||||
static func createGeohashTextNote(
|
||||
content: String,
|
||||
|
||||
@@ -887,10 +887,10 @@ struct NostrFilter: Encodable {
|
||||
return filter
|
||||
}
|
||||
|
||||
// For location channels: geohash-scoped ephemeral events (kind 20000)
|
||||
static func geohashEphemeral(_ geohash: String, since: Date? = nil, limit: Int = 200) -> NostrFilter {
|
||||
// For location channels: geohash-scoped ephemeral events (kind 20000) and presence (kind 20001)
|
||||
static func geohashEphemeral(_ geohash: String, since: Date? = nil, limit: Int = 1000) -> NostrFilter {
|
||||
var filter = NostrFilter()
|
||||
filter.kinds = [20000]
|
||||
filter.kinds = [20000, 20001]
|
||||
filter.since = since?.timeIntervalSince1970.toInt()
|
||||
filter.tagFilters = ["g": [geohash]]
|
||||
filter.limit = limit
|
||||
|
||||
@@ -161,7 +161,9 @@ struct BinaryProtocol {
|
||||
}
|
||||
|
||||
let lengthFieldBytes = lengthFieldSize(for: version)
|
||||
let originalRoute = packet.route ?? []
|
||||
|
||||
// Route is only supported for v2+ packets (per SOURCE_ROUTING.md spec)
|
||||
let originalRoute = (version >= 2) ? (packet.route ?? []) : []
|
||||
if originalRoute.contains(where: { $0.isEmpty }) { return nil }
|
||||
let sanitizedRoute: [Data] = originalRoute.map { hop in
|
||||
if hop.count == senderIDSize { return hop }
|
||||
@@ -175,13 +177,14 @@ struct BinaryProtocol {
|
||||
let hasRoute = !sanitizedRoute.isEmpty
|
||||
let routeLength = hasRoute ? 1 + sanitizedRoute.count * senderIDSize : 0
|
||||
let originalSizeFieldBytes = isCompressed ? lengthFieldBytes : 0
|
||||
let payloadDataSize = routeLength + payload.count + originalSizeFieldBytes
|
||||
// payloadLength in header is payload-only (does NOT include route bytes)
|
||||
let payloadDataSize = payload.count + originalSizeFieldBytes
|
||||
|
||||
if version == 1 && payloadDataSize > Int(UInt16.max) { return nil }
|
||||
if version == 2 && payloadDataSize > Int(UInt32.max) { return nil }
|
||||
|
||||
guard let headerSize = headerSize(for: version) else { return nil }
|
||||
let estimatedHeader = headerSize + senderIDSize + (packet.recipientID == nil ? 0 : recipientIDSize)
|
||||
let estimatedHeader = headerSize + senderIDSize + (packet.recipientID == nil ? 0 : recipientIDSize) + routeLength
|
||||
let estimatedPayload = payloadDataSize
|
||||
let estimatedSignature = (packet.signature == nil ? 0 : signatureSize)
|
||||
var data = Data()
|
||||
@@ -199,7 +202,8 @@ struct BinaryProtocol {
|
||||
if packet.recipientID != nil { flags |= Flags.hasRecipient }
|
||||
if packet.signature != nil { flags |= Flags.hasSignature }
|
||||
if isCompressed { flags |= Flags.isCompressed }
|
||||
if hasRoute { flags |= Flags.hasRoute }
|
||||
// HAS_ROUTE is only valid for v2+ packets
|
||||
if hasRoute && version >= 2 { flags |= Flags.hasRoute }
|
||||
data.append(flags)
|
||||
|
||||
if version == 2 {
|
||||
@@ -323,6 +327,8 @@ struct BinaryProtocol {
|
||||
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
||||
let hasSignature = (flags & Flags.hasSignature) != 0
|
||||
let isCompressed = (flags & Flags.isCompressed) != 0
|
||||
// HAS_ROUTE is only valid for v2+ packets; ignore the flag for v1
|
||||
let hasRoute = (version >= 2) && (flags & Flags.hasRoute) != 0
|
||||
|
||||
let payloadLength: Int
|
||||
if version == 2 {
|
||||
@@ -343,27 +349,24 @@ struct BinaryProtocol {
|
||||
if recipientID == nil { return nil }
|
||||
}
|
||||
|
||||
// Route (optional, v2+ only): route bytes are NOT included in payloadLength
|
||||
var route: [Data]? = nil
|
||||
var remainingPayloadBytes = payloadLength
|
||||
|
||||
if (flags & Flags.hasRoute) != 0 {
|
||||
guard remainingPayloadBytes >= 1, let routeCount = read8() else { return nil }
|
||||
remainingPayloadBytes -= 1
|
||||
if hasRoute {
|
||||
guard let routeCount = read8() else { return nil }
|
||||
if routeCount > 0 {
|
||||
var hops: [Data] = []
|
||||
for _ in 0..<Int(routeCount) {
|
||||
guard remainingPayloadBytes >= senderIDSize,
|
||||
let hop = readData(senderIDSize) else { return nil }
|
||||
remainingPayloadBytes -= senderIDSize
|
||||
guard let hop = readData(senderIDSize) else { return nil }
|
||||
hops.append(hop)
|
||||
}
|
||||
route = hops
|
||||
}
|
||||
}
|
||||
|
||||
// Payload: payloadLength is exactly the payload size (+ compression preamble if compressed)
|
||||
let payload: Data
|
||||
if isCompressed {
|
||||
guard remainingPayloadBytes >= lengthFieldBytes else { return nil }
|
||||
guard payloadLength >= lengthFieldBytes else { return nil }
|
||||
let originalSize: Int
|
||||
if version == 2 {
|
||||
guard let rawSize = read32() else { return nil }
|
||||
@@ -372,11 +375,9 @@ struct BinaryProtocol {
|
||||
guard let rawSize = read16() else { return nil }
|
||||
originalSize = Int(rawSize)
|
||||
}
|
||||
remainingPayloadBytes -= lengthFieldBytes
|
||||
guard originalSize >= 0 && originalSize <= FileTransferLimits.maxFramedFileBytes else { return nil }
|
||||
let compressedSize = remainingPayloadBytes
|
||||
let compressedSize = payloadLength - lengthFieldBytes
|
||||
guard compressedSize > 0, let compressed = readData(compressedSize) else { return nil }
|
||||
remainingPayloadBytes = 0
|
||||
|
||||
let compressionRatio = Double(originalSize) / Double(compressedSize)
|
||||
guard compressionRatio <= 50_000.0 else {
|
||||
@@ -388,9 +389,7 @@ struct BinaryProtocol {
|
||||
decompressed.count == originalSize else { return nil }
|
||||
payload = decompressed
|
||||
} else {
|
||||
guard remainingPayloadBytes >= 0,
|
||||
let rawPayload = readData(remainingPayloadBytes) else { return nil }
|
||||
remainingPayloadBytes = 0
|
||||
guard let rawPayload = readData(payloadLength) else { return nil }
|
||||
payload = rawPayload
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ final class BLEService: NSObject {
|
||||
|
||||
// Default per-fragment chunk size when link limits are unknown
|
||||
private let defaultFragmentSize = TransportConfig.bleDefaultFragmentSize
|
||||
private let bleMaxMTU = 512
|
||||
private let maxMessageLength = InputValidator.Limits.maxMessageLength
|
||||
private let messageTTL: UInt8 = TransportConfig.messageTTLDefault
|
||||
// Flood/battery controls
|
||||
@@ -52,6 +53,15 @@ final class BLEService: NSObject {
|
||||
private var subscribedCentrals: [CBCentral] = []
|
||||
private var centralToPeerID: [String: PeerID] = [:] // Central UUID -> Peer ID mapping
|
||||
|
||||
// BCH-01-004: Rate-limiting for subscription-triggered announces
|
||||
// Tracks subscription attempts per central to prevent enumeration attacks
|
||||
private struct SubscriptionRateLimitState {
|
||||
var lastAnnounceTime: Date
|
||||
var attemptCount: Int
|
||||
var currentBackoffSeconds: TimeInterval
|
||||
}
|
||||
private var centralSubscriptionRateLimits: [String: SubscriptionRateLimitState] = [:] // Central UUID -> rate limit state
|
||||
|
||||
// 3. Peer Information (single source of truth)
|
||||
private struct PeerInfo {
|
||||
let peerID: PeerID
|
||||
@@ -574,6 +584,9 @@ final class BLEService: NSObject {
|
||||
subscribedCentrals.removeAll()
|
||||
centralToPeerID.removeAll()
|
||||
meshTopology.reset()
|
||||
|
||||
// BCH-01-004: Clear rate-limit state
|
||||
centralSubscriptionRateLimits.removeAll()
|
||||
}
|
||||
|
||||
// MARK: Connectivity and peers
|
||||
@@ -715,7 +728,10 @@ final class BLEService: NSObject {
|
||||
SecureLogger.error("❌ Failed to encode file packet for private send", category: .session)
|
||||
return
|
||||
}
|
||||
guard let recipientData = Data(hexString: peerID.id) else {
|
||||
// Normalize to short form (SHA256-derived 16-hex) for wire protocol compatibility
|
||||
// This ensures 64-hex Noise keys are converted to the canonical routing format
|
||||
let targetID = peerID.toShort()
|
||||
guard let recipientData = Data(hexString: targetID.id) else {
|
||||
SecureLogger.error("❌ Invalid recipient peer ID for file transfer: \(peerID)", category: .session)
|
||||
return
|
||||
}
|
||||
@@ -731,8 +747,6 @@ final class BLEService: NSObject {
|
||||
version: 2
|
||||
)
|
||||
|
||||
self.applyRouteIfAvailable(&packet, to: peerID)
|
||||
|
||||
if let signed = self.noiseService.signPacket(packet) {
|
||||
packet = signed
|
||||
}
|
||||
@@ -752,7 +766,7 @@ final class BLEService: NSObject {
|
||||
SecureLogger.debug("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)", category: .session)
|
||||
do {
|
||||
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
||||
var packet = BitchatPacket(
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.noiseEncrypted.rawValue,
|
||||
senderID: myPeerIDData,
|
||||
recipientID: Data(hexString: peerID.id),
|
||||
@@ -761,7 +775,6 @@ final class BLEService: NSObject {
|
||||
signature: nil,
|
||||
ttl: messageTTL
|
||||
)
|
||||
applyRouteIfAvailable(&packet, to: peerID)
|
||||
broadcastPacket(packet)
|
||||
} catch {
|
||||
SecureLogger.error("Failed to send read receipt: \(error)")
|
||||
@@ -780,21 +793,29 @@ final class BLEService: NSObject {
|
||||
// MARK: - Packet Broadcasting
|
||||
|
||||
private func broadcastPacket(_ packet: BitchatPacket, transferId: String? = nil) {
|
||||
// Apply route if recipient exists (centralized route application)
|
||||
let packetToSend: BitchatPacket
|
||||
if let recipientPeerID = PeerID(hexData: packet.recipientID) {
|
||||
packetToSend = applyRouteIfAvailable(packet, to: recipientPeerID)
|
||||
} else {
|
||||
packetToSend = packet
|
||||
}
|
||||
|
||||
// Encode once using a small per-type padding policy, then delegate by type
|
||||
let padForBLE = padPolicy(for: packet.type)
|
||||
if packet.type == MessageType.fileTransfer.rawValue {
|
||||
sendFragmentedPacket(packet, pad: padForBLE, maxChunk: nil, directedOnlyPeer: nil, transferId: transferId)
|
||||
let padForBLE = padPolicy(for: packetToSend.type)
|
||||
if packetToSend.type == MessageType.fileTransfer.rawValue {
|
||||
sendFragmentedPacket(packetToSend, pad: padForBLE, maxChunk: nil, directedOnlyPeer: nil, transferId: transferId)
|
||||
return
|
||||
}
|
||||
guard let data = packet.toBinaryData(padding: padForBLE) else {
|
||||
guard let data = packetToSend.toBinaryData(padding: padForBLE) else {
|
||||
SecureLogger.error("❌ Failed to convert packet to binary data", category: .session)
|
||||
return
|
||||
}
|
||||
if packet.type == MessageType.noiseEncrypted.rawValue {
|
||||
sendEncrypted(packet, data: data, pad: padForBLE)
|
||||
if packetToSend.type == MessageType.noiseEncrypted.rawValue {
|
||||
sendEncrypted(packetToSend, data: data, pad: padForBLE)
|
||||
return
|
||||
}
|
||||
sendGenericBroadcast(packet, data: data, pad: padForBLE)
|
||||
sendGenericBroadcast(packetToSend, data: data, pad: padForBLE)
|
||||
}
|
||||
|
||||
// MARK: - Broadcast helpers (single responsibility)
|
||||
@@ -1149,6 +1170,9 @@ final class BLEService: NSObject {
|
||||
return
|
||||
}
|
||||
|
||||
// BCH-01-002: Enforce storage quota before saving
|
||||
enforceIncomingFilesQuota(reservingBytes: filePacket.content.count)
|
||||
|
||||
let fallbackExt = mime.defaultExtension
|
||||
let subdirectory: String
|
||||
switch mime.category {
|
||||
@@ -1234,7 +1258,7 @@ final class BLEService: NSObject {
|
||||
if noiseService.hasEstablishedSession(with: peerID) {
|
||||
do {
|
||||
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
||||
var packet = BitchatPacket(
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.noiseEncrypted.rawValue,
|
||||
senderID: myPeerIDData,
|
||||
recipientID: Data(hexString: peerID.id),
|
||||
@@ -1243,7 +1267,6 @@ final class BLEService: NSObject {
|
||||
signature: nil,
|
||||
ttl: messageTTL
|
||||
)
|
||||
applyRouteIfAvailable(&packet, to: peerID)
|
||||
broadcastPacket(packet)
|
||||
} catch {
|
||||
SecureLogger.error("Failed to send delivery ACK: \(error)")
|
||||
@@ -1385,6 +1408,72 @@ final class BLEService: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Storage Quota Management (BCH-01-002)
|
||||
|
||||
/// Maximum total storage for incoming files (100 MB)
|
||||
private static let incomingFilesQuota: Int64 = 100 * 1024 * 1024
|
||||
|
||||
/// Enforces storage quota for incoming files by deleting oldest files when quota is exceeded.
|
||||
/// Call before saving a new incoming file.
|
||||
private func enforceIncomingFilesQuota(reservingBytes: Int) {
|
||||
do {
|
||||
let base = try applicationFilesDirectory()
|
||||
let incomingDirs = [
|
||||
base.appendingPathComponent("voicenotes/incoming", isDirectory: true),
|
||||
base.appendingPathComponent("images/incoming", isDirectory: true),
|
||||
base.appendingPathComponent("files/incoming", isDirectory: true)
|
||||
]
|
||||
|
||||
// Gather all incoming files with their sizes and modification dates
|
||||
var allFiles: [(url: URL, size: Int64, modified: Date)] = []
|
||||
let fileManager = FileManager.default
|
||||
|
||||
for dir in incomingDirs {
|
||||
guard fileManager.fileExists(atPath: dir.path) else { continue }
|
||||
guard let contents = try? fileManager.contentsOfDirectory(
|
||||
at: dir,
|
||||
includingPropertiesForKeys: [.fileSizeKey, .contentModificationDateKey],
|
||||
options: [.skipsHiddenFiles]
|
||||
) else { continue }
|
||||
|
||||
for fileURL in contents {
|
||||
guard let attrs = try? fileURL.resourceValues(forKeys: [.fileSizeKey, .contentModificationDateKey]),
|
||||
let size = attrs.fileSize,
|
||||
let modified = attrs.contentModificationDate else { continue }
|
||||
allFiles.append((url: fileURL, size: Int64(size), modified: modified))
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate current usage
|
||||
let currentUsage = allFiles.reduce(0) { $0 + $1.size }
|
||||
let targetUsage = Self.incomingFilesQuota - Int64(reservingBytes)
|
||||
|
||||
guard currentUsage > targetUsage else { return }
|
||||
|
||||
// Sort by modification date (oldest first) and delete until under quota
|
||||
let sortedFiles = allFiles.sorted { $0.modified < $1.modified }
|
||||
var freedSpace: Int64 = 0
|
||||
let needToFree = currentUsage - targetUsage
|
||||
|
||||
for file in sortedFiles {
|
||||
guard freedSpace < needToFree else { break }
|
||||
do {
|
||||
try fileManager.removeItem(at: file.url)
|
||||
freedSpace += file.size
|
||||
SecureLogger.debug("🗑️ BCH-01-002: Deleted old incoming file to free space: \(file.url.lastPathComponent)", category: .security)
|
||||
} catch {
|
||||
SecureLogger.warning("⚠️ Failed to delete old file for quota: \(error)", category: .security)
|
||||
}
|
||||
}
|
||||
|
||||
if freedSpace > 0 {
|
||||
SecureLogger.info("📊 BCH-01-002: Freed \(ByteCountFormatter.string(fromByteCount: freedSpace, countStyle: .file)) to stay within incoming files quota", category: .security)
|
||||
}
|
||||
} catch {
|
||||
SecureLogger.warning("⚠️ Could not enforce storage quota: \(error)", category: .security)
|
||||
}
|
||||
}
|
||||
|
||||
private func sendLeave() {
|
||||
SecureLogger.debug("👋 Sending leave announcement", category: .session)
|
||||
let packet = BitchatPacket(
|
||||
@@ -1753,9 +1842,10 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
||||
peers[peerID] = info
|
||||
}
|
||||
}
|
||||
clearDirectLink(with: peerID)
|
||||
refreshLocalTopology()
|
||||
}
|
||||
|
||||
|
||||
// Restart scanning with allow duplicates for faster rediscovery
|
||||
if centralManager?.state == .poweredOn {
|
||||
// Stop and restart scanning to ensure we get fresh discovery events
|
||||
@@ -2041,7 +2131,7 @@ extension BLEService: CBPeripheralDelegate {
|
||||
peripherals[peripheralUUID] = state
|
||||
}
|
||||
peerToPeripheralUUID[senderID] = peripheralUUID
|
||||
registerDirectLink(with: senderID)
|
||||
refreshLocalTopology()
|
||||
}
|
||||
|
||||
let msgID = makeMessageID(for: packet)
|
||||
@@ -2173,9 +2263,70 @@ extension BLEService: CBPeripheralManagerDelegate {
|
||||
}
|
||||
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
|
||||
SecureLogger.debug("📥 Central subscribed: \(central.identifier.uuidString)", category: .session)
|
||||
let centralUUID = central.identifier.uuidString
|
||||
SecureLogger.debug("📥 Central subscribed: \(centralUUID)", category: .session)
|
||||
subscribedCentrals.append(central)
|
||||
// Send announce to the newly subscribed central after a small delay to avoid overwhelming
|
||||
|
||||
// BCH-01-004: Rate-limit subscription-triggered announces to prevent enumeration attacks
|
||||
let now = Date()
|
||||
var state = centralSubscriptionRateLimits[centralUUID]
|
||||
|
||||
// Clean up stale entries periodically
|
||||
cleanupStaleSubscriptionRateLimits()
|
||||
|
||||
// Check if this central is rate-limited
|
||||
if let existingState = state {
|
||||
let timeSinceLastAnnounce = now.timeIntervalSince(existingState.lastAnnounceTime)
|
||||
|
||||
// If within backoff period, skip the announce
|
||||
if timeSinceLastAnnounce < existingState.currentBackoffSeconds {
|
||||
SecureLogger.warning("🛡️ BCH-01-004: Rate-limited announce for central \(centralUUID.prefix(8))... (backoff: \(Int(existingState.currentBackoffSeconds))s, attempts: \(existingState.attemptCount))", category: .security)
|
||||
|
||||
// Increment attempt count and increase backoff
|
||||
// Update lastAnnounceTime to 'now' so each blocked attempt extends the suppression window
|
||||
// This prevents attackers from waiting out the backoff while spamming attempts
|
||||
let newAttemptCount = existingState.attemptCount + 1
|
||||
let newBackoff = min(
|
||||
existingState.currentBackoffSeconds * TransportConfig.bleSubscriptionRateLimitBackoffFactor,
|
||||
TransportConfig.bleSubscriptionRateLimitMaxBackoffSeconds
|
||||
)
|
||||
centralSubscriptionRateLimits[centralUUID] = SubscriptionRateLimitState(
|
||||
lastAnnounceTime: now, // Reset timer on each blocked attempt
|
||||
attemptCount: newAttemptCount,
|
||||
currentBackoffSeconds: newBackoff
|
||||
)
|
||||
|
||||
// If too many rapid attempts, this is likely an enumeration attack - don't respond
|
||||
if newAttemptCount >= TransportConfig.bleSubscriptionRateLimitMaxAttempts {
|
||||
SecureLogger.warning("🚨 BCH-01-004: Possible enumeration attack from central \(centralUUID.prefix(8))... - suppressing announce", category: .security)
|
||||
return
|
||||
}
|
||||
|
||||
// Still flush directed packets and rebroadcast for legitimate mesh operation
|
||||
messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostAnnounceDelaySeconds) { [weak self] in
|
||||
self?.flushDirectedSpool()
|
||||
self?.rebroadcastRecentAnnounces()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Outside backoff period - allow announce but track it
|
||||
state = SubscriptionRateLimitState(
|
||||
lastAnnounceTime: now,
|
||||
attemptCount: 1,
|
||||
currentBackoffSeconds: TransportConfig.bleSubscriptionRateLimitMinSeconds
|
||||
)
|
||||
} else {
|
||||
// First subscription from this central - track it
|
||||
state = SubscriptionRateLimitState(
|
||||
lastAnnounceTime: now,
|
||||
attemptCount: 1,
|
||||
currentBackoffSeconds: TransportConfig.bleSubscriptionRateLimitMinSeconds
|
||||
)
|
||||
}
|
||||
centralSubscriptionRateLimits[centralUUID] = state
|
||||
|
||||
// Send announce to the newly subscribed central after a small delay
|
||||
messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostAnnounceDelaySeconds) { [weak self] in
|
||||
self?.sendAnnounce(forceSend: true)
|
||||
// Flush any spooled directed packets now that we have a central subscribed
|
||||
@@ -2185,6 +2336,15 @@ extension BLEService: CBPeripheralManagerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
/// BCH-01-004: Clean up stale rate-limit entries to prevent memory growth
|
||||
private func cleanupStaleSubscriptionRateLimits() {
|
||||
let now = Date()
|
||||
let windowSeconds = TransportConfig.bleSubscriptionRateLimitWindowSeconds
|
||||
centralSubscriptionRateLimits = centralSubscriptionRateLimits.filter { _, state in
|
||||
now.timeIntervalSince(state.lastAnnounceTime) < windowSeconds
|
||||
}
|
||||
}
|
||||
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
|
||||
SecureLogger.debug("📤 Central unsubscribed: \(central.identifier.uuidString)", category: .session)
|
||||
subscribedCentrals.removeAll { $0.identifier == central.identifier }
|
||||
@@ -2208,7 +2368,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
||||
|
||||
// Clean up mappings
|
||||
centralToPeerID.removeValue(forKey: centralUUID)
|
||||
clearDirectLink(with: peerID)
|
||||
refreshLocalTopology()
|
||||
|
||||
// Update UI immediately
|
||||
notifyUI { [weak self] in
|
||||
@@ -2327,7 +2487,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
||||
if packet.type == MessageType.announce.rawValue {
|
||||
if packet.ttl == messageTTL {
|
||||
centralToPeerID[centralUUID] = senderID
|
||||
registerDirectLink(with: senderID)
|
||||
refreshLocalTopology()
|
||||
}
|
||||
// Record ingress link for last-hop suppression then process
|
||||
let msgID = makeMessageID(for: packet)
|
||||
@@ -2442,27 +2602,39 @@ extension BLEService {
|
||||
peerID.toShort().routingData
|
||||
}
|
||||
|
||||
private func registerDirectLink(with peerID: PeerID) {
|
||||
meshTopology.recordDirectLink(between: myPeerIDData, and: routingData(for: peerID))
|
||||
}
|
||||
|
||||
private func clearDirectLink(with peerID: PeerID) {
|
||||
meshTopology.removeDirectLink(between: myPeerIDData, and: routingData(for: peerID))
|
||||
}
|
||||
|
||||
private func registerRoute(_ route: [Data]?) {
|
||||
guard let hops = route, !hops.isEmpty else { return }
|
||||
meshTopology.recordRoute(hops)
|
||||
private func refreshLocalTopology() {
|
||||
let neighbors: [Data] = collectionsQueue.sync {
|
||||
peers.values.filter { $0.isConnected }.compactMap { $0.peerID.routingData }
|
||||
}
|
||||
meshTopology.updateNeighbors(for: myPeerIDData, neighbors: neighbors)
|
||||
}
|
||||
|
||||
private func computeRoute(to peerID: PeerID) -> [Data]? {
|
||||
meshTopology.computeRoute(from: myPeerIDData, to: routingData(for: peerID))
|
||||
}
|
||||
|
||||
private func applyRouteIfAvailable(_ packet: inout BitchatPacket, to recipient: PeerID) {
|
||||
guard let route = computeRoute(to: recipient), route.count >= 2 else { return }
|
||||
packet.route = route
|
||||
meshTopology.recordRoute(route)
|
||||
private func applyRouteIfAvailable(_ packet: BitchatPacket, to recipient: PeerID) -> BitchatPacket {
|
||||
guard let route = computeRoute(to: recipient), route.count >= 1 else {
|
||||
return packet
|
||||
}
|
||||
// Create new packet with route applied and version upgraded to 2
|
||||
let routedPacket = BitchatPacket(
|
||||
type: packet.type,
|
||||
senderID: packet.senderID,
|
||||
recipientID: packet.recipientID,
|
||||
timestamp: packet.timestamp,
|
||||
payload: packet.payload,
|
||||
signature: nil, // Will be re-signed below
|
||||
ttl: packet.ttl,
|
||||
version: 2,
|
||||
route: route
|
||||
)
|
||||
// Re-sign the packet since route and version changed
|
||||
guard let signedPacket = noiseService.signPacket(routedPacket) else {
|
||||
SecureLogger.error("❌ Failed to re-sign packet with route", category: .security)
|
||||
return packet // Return original packet if signing fails
|
||||
}
|
||||
return signedPacket
|
||||
}
|
||||
|
||||
private func routingPeer(from data: Data) -> PeerID? {
|
||||
@@ -2472,23 +2644,46 @@ extension BLEService {
|
||||
private func forwardAlongRouteIfNeeded(_ packet: BitchatPacket) -> Bool {
|
||||
guard let route = packet.route, !route.isEmpty else { return false }
|
||||
let myRoutingData = routingData(for: myPeerID) ?? (myPeerIDData.isEmpty ? nil : myPeerIDData)
|
||||
guard let selfData = myRoutingData,
|
||||
let index = route.firstIndex(of: selfData) else { return false }
|
||||
guard let selfData = myRoutingData else { return false }
|
||||
|
||||
// No further hops: respect explicit route termination
|
||||
if index == route.count - 1 {
|
||||
// Route contains only intermediate hops (start and end excluded)
|
||||
// If we're not in the route, we're the sender - forward to first hop
|
||||
guard let index = route.firstIndex(of: selfData) else {
|
||||
// We're the sender, forward to first intermediate hop
|
||||
guard packet.ttl > 1 else { return true }
|
||||
let firstHopData = route[0]
|
||||
guard let nextPeer = routingPeer(from: firstHopData),
|
||||
isPeerConnected(nextPeer) else {
|
||||
return false
|
||||
}
|
||||
var relayPacket = packet
|
||||
relayPacket.ttl = packet.ttl - 1
|
||||
sendPacketDirected(relayPacket, to: nextPeer)
|
||||
return true
|
||||
}
|
||||
|
||||
guard packet.ttl > 1 else { return true }
|
||||
// We're an intermediate node in the route
|
||||
// If we're the last intermediate hop, forward to destination
|
||||
if index == route.count - 1 {
|
||||
guard packet.ttl > 1 else { return true }
|
||||
guard let destinationPeer = PeerID(hexData: packet.recipientID),
|
||||
isPeerConnected(destinationPeer) else {
|
||||
return false
|
||||
}
|
||||
var relayPacket = packet
|
||||
relayPacket.ttl = packet.ttl - 1
|
||||
sendPacketDirected(relayPacket, to: destinationPeer)
|
||||
return true
|
||||
}
|
||||
|
||||
// Forward to next intermediate hop
|
||||
guard packet.ttl > 1 else { return true }
|
||||
let nextHopData = route[index + 1]
|
||||
guard let nextPeer = routingPeer(from: nextHopData),
|
||||
isPeerConnected(nextPeer) else {
|
||||
return false
|
||||
}
|
||||
|
||||
registerDirectLink(with: nextPeer)
|
||||
var relayPacket = packet
|
||||
relayPacket.ttl = packet.ttl - 1
|
||||
sendPacketDirected(relayPacket, to: nextPeer)
|
||||
@@ -2547,7 +2742,7 @@ extension BLEService {
|
||||
}
|
||||
do {
|
||||
let encrypted = try noiseService.encrypt(typedPayload, for: peerID)
|
||||
var packet = BitchatPacket(
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.noiseEncrypted.rawValue,
|
||||
senderID: myPeerIDData,
|
||||
recipientID: Data(hexString: peerID.id),
|
||||
@@ -2556,7 +2751,6 @@ extension BLEService {
|
||||
signature: nil,
|
||||
ttl: messageTTL
|
||||
)
|
||||
applyRouteIfAvailable(&packet, to: peerID)
|
||||
broadcastPacket(packet)
|
||||
} catch {
|
||||
SecureLogger.error("Failed to send verification payload: \(error)")
|
||||
@@ -2775,7 +2969,7 @@ extension BLEService {
|
||||
}
|
||||
}
|
||||
|
||||
var packet = BitchatPacket(
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.noiseEncrypted.rawValue,
|
||||
senderID: myPeerIDData,
|
||||
recipientID: recipientData,
|
||||
@@ -2784,7 +2978,6 @@ extension BLEService {
|
||||
signature: nil,
|
||||
ttl: messageTTL
|
||||
)
|
||||
applyRouteIfAvailable(&packet, to: recipientID)
|
||||
|
||||
broadcastPacket(packet)
|
||||
|
||||
@@ -2824,7 +3017,7 @@ extension BLEService {
|
||||
let handshakeData = try noiseService.initiateHandshake(with: peerID)
|
||||
|
||||
// Send handshake init
|
||||
var packet = BitchatPacket(
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.noiseHandshake.rawValue,
|
||||
senderID: myPeerIDData,
|
||||
recipientID: Data(hexString: peerID.id),
|
||||
@@ -2833,7 +3026,6 @@ extension BLEService {
|
||||
signature: nil,
|
||||
ttl: messageTTL
|
||||
)
|
||||
applyRouteIfAvailable(&packet, to: peerID)
|
||||
broadcastPacket(packet)
|
||||
} catch {
|
||||
SecureLogger.error("Failed to initiate handshake: \(error)")
|
||||
@@ -2867,7 +3059,7 @@ extension BLEService {
|
||||
|
||||
let encrypted = try noiseService.encrypt(messagePayload, for: peerID)
|
||||
|
||||
var packet = BitchatPacket(
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.noiseEncrypted.rawValue,
|
||||
senderID: myPeerIDData,
|
||||
recipientID: Data(hexString: peerID.id),
|
||||
@@ -2876,7 +3068,6 @@ extension BLEService {
|
||||
signature: nil,
|
||||
ttl: messageTTL
|
||||
)
|
||||
applyRouteIfAvailable(&packet, to: peerID)
|
||||
|
||||
// We're already on messageQueue from the callback
|
||||
broadcastPacket(packet)
|
||||
@@ -2970,7 +3161,21 @@ extension BLEService {
|
||||
}
|
||||
// Fragment the unpadded frame; each fragment will be encoded independently
|
||||
let fragmentID = Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
||||
let chunk = context.maxChunk ?? defaultFragmentSize
|
||||
// Dynamic Fragment Sizing (Source Routing v2)
|
||||
// See docs/SOURCE_ROUTING.md Section 5.1
|
||||
var fragmentVersion: UInt8 = 1
|
||||
var calculatedChunk = defaultFragmentSize
|
||||
|
||||
if let route = packet.route, !route.isEmpty {
|
||||
fragmentVersion = 2
|
||||
// RouteSize = 1 + (Hops * 8)
|
||||
let routeSize = 1 + (route.count * 8)
|
||||
// Overhead = HeaderV2(16) + SenderID(8) + RecipientID(8) + RouteSize + FragmentHeader(13) + PaddingBuffer(16)
|
||||
let overhead = 16 + 8 + 8 + routeSize + 13 + 16
|
||||
calculatedChunk = max(64, bleMaxMTU - overhead)
|
||||
}
|
||||
|
||||
let chunk = context.maxChunk ?? calculatedChunk
|
||||
let safeChunk = max(64, chunk)
|
||||
let fragments = stride(from: 0, to: fullData.count, by: safeChunk).map { offset in
|
||||
Data(fullData[offset..<min(offset + safeChunk, fullData.count)])
|
||||
@@ -3029,6 +3234,7 @@ extension BLEService {
|
||||
payload: payload,
|
||||
signature: nil,
|
||||
ttl: packet.ttl,
|
||||
version: fragmentVersion,
|
||||
route: packet.route
|
||||
)
|
||||
|
||||
@@ -3245,10 +3451,6 @@ extension BLEService {
|
||||
return
|
||||
}
|
||||
|
||||
registerRoute(packet.route)
|
||||
if peerID != myPeerID && packet.ttl == messageTTL {
|
||||
registerDirectLink(with: peerID)
|
||||
}
|
||||
|
||||
// Deduplication (thread-safe)
|
||||
let senderID = PeerID(hexData: packet.senderID)
|
||||
@@ -3450,6 +3652,9 @@ extension BLEService {
|
||||
// Require verified announce; ignore otherwise (no backward compatibility)
|
||||
if !verified {
|
||||
SecureLogger.warning("❌ Ignoring unverified announce from \(peerID.id.prefix(8))…", category: .security)
|
||||
// Reset flags to prevent post-barrier code from acting on unverified announces
|
||||
isNewPeer = false
|
||||
isReconnectedPeer = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3497,6 +3702,11 @@ extension BLEService {
|
||||
}
|
||||
}
|
||||
|
||||
// Update topology with verified neighbor claims (only for authenticated announces)
|
||||
if verifiedAnnounce, let neighbors = announcement.directNeighbors {
|
||||
meshTopology.updateNeighbors(for: peerID.routingData, neighbors: neighbors)
|
||||
}
|
||||
|
||||
// Persist cryptographic identity and signing key for robust offline verification
|
||||
identityManager.upsertCryptographicIdentity(
|
||||
fingerprint: announcement.noisePublicKey.sha256Fingerprint(),
|
||||
@@ -3683,7 +3893,7 @@ extension BLEService {
|
||||
do {
|
||||
if let response = try noiseService.processHandshakeMessage(from: peerID, message: packet.payload) {
|
||||
// Send response
|
||||
var responsePacket = BitchatPacket(
|
||||
let responsePacket = BitchatPacket(
|
||||
type: MessageType.noiseHandshake.rawValue,
|
||||
senderID: myPeerIDData,
|
||||
recipientID: Data(hexString: peerID.id),
|
||||
@@ -3692,7 +3902,6 @@ extension BLEService {
|
||||
signature: nil,
|
||||
ttl: messageTTL
|
||||
)
|
||||
applyRouteIfAvailable(&responsePacket, to: peerID)
|
||||
// We're on messageQueue from delegate callback
|
||||
broadcastPacket(responsePacket)
|
||||
}
|
||||
@@ -3977,6 +4186,11 @@ extension BLEService {
|
||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh local topology to keep our own entry fresh and sync any changes
|
||||
refreshLocalTopology()
|
||||
// Prune stale topology nodes (using safe retention window)
|
||||
meshTopology.prune(olderThan: 60.0)
|
||||
}
|
||||
|
||||
private func performCleanup() {
|
||||
|
||||
@@ -84,6 +84,9 @@ public final class GeohashParticipantTracker: ObservableObject {
|
||||
map[key] = Date()
|
||||
participants[geohash] = map
|
||||
|
||||
// Always notify observers that state has changed so counts in UI update
|
||||
objectWillChange.send()
|
||||
|
||||
// Only refresh visible list if this geohash is currently active
|
||||
if activeGeohash == geohash {
|
||||
refresh()
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// GeohashPresenceService.swift
|
||||
// bitchat
|
||||
//
|
||||
// Manages the broadcasting of ephemeral presence heartbeats (Kind 20001)
|
||||
// to geohash location channels.
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import BitLogger
|
||||
import Tor
|
||||
|
||||
/// Service that coordinates the broadcasting of presence heartbeats.
|
||||
///
|
||||
/// Behavior:
|
||||
/// - Monitors location changes via LocationStateManager
|
||||
/// - Broadcasts Kind 20001 events to low-precision geohash channels
|
||||
/// - Uses randomized timing (40-80s loop) and decorrelated bursts
|
||||
/// - Respects privacy by NOT broadcasting to Neighborhood/Block/Building levels
|
||||
@MainActor
|
||||
final class GeohashPresenceService: ObservableObject {
|
||||
static let shared = GeohashPresenceService()
|
||||
|
||||
private var subscriptions = Set<AnyCancellable>()
|
||||
private var heartbeatTimer: Timer?
|
||||
private let idBridge = NostrIdentityBridge()
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
// Loop interval range in seconds
|
||||
private let loopMinInterval: TimeInterval = 40.0
|
||||
private let loopMaxInterval: TimeInterval = 80.0
|
||||
|
||||
// Per-broadcast decorrelation delay range in seconds
|
||||
private let burstMinDelay: TimeInterval = 2.0
|
||||
private let burstMaxDelay: TimeInterval = 5.0
|
||||
|
||||
// Privacy: Only broadcast to these levels
|
||||
private let allowedPrecisions: Set<Int> = [
|
||||
GeohashChannelLevel.region.precision, // 2
|
||||
GeohashChannelLevel.province.precision, // 4
|
||||
GeohashChannelLevel.city.precision // 5
|
||||
]
|
||||
|
||||
private init() {
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
/// Start the service (safe to call multiple times)
|
||||
func start() {
|
||||
SecureLogger.info("Presence: service starting...", category: .session)
|
||||
scheduleNextHeartbeat()
|
||||
}
|
||||
|
||||
private func setupObservers() {
|
||||
// Monitor location channel changes
|
||||
LocationStateManager.shared.$availableChannels
|
||||
.dropFirst()
|
||||
.sink { [weak self] _ in
|
||||
self?.handleLocationChange()
|
||||
}
|
||||
.store(in: &subscriptions)
|
||||
|
||||
// Monitor Tor readiness to kick off heartbeat if it was stalled
|
||||
NotificationCenter.default.publisher(for: .TorDidBecomeReady)
|
||||
.sink { [weak self] _ in
|
||||
self?.handleConnectivityChange()
|
||||
}
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
|
||||
private func handleLocationChange() {
|
||||
// When location changes, we trigger an immediate (but slightly delayed) heartbeat
|
||||
// to announce presence in the new zone, then reset the loop.
|
||||
SecureLogger.debug("Presence: location changed, scheduling update", category: .session)
|
||||
heartbeatTimer?.invalidate()
|
||||
|
||||
// Small delay to allow location state to settle
|
||||
heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { [weak self] _ in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.performHeartbeat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleConnectivityChange() {
|
||||
SecureLogger.debug("Presence: connectivity restored, triggering heartbeat", category: .session)
|
||||
// If we were waiting for network, do it now
|
||||
if heartbeatTimer == nil || !heartbeatTimer!.isValid {
|
||||
scheduleNextHeartbeat()
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleNextHeartbeat() {
|
||||
heartbeatTimer?.invalidate()
|
||||
let interval = TimeInterval.random(in: loopMinInterval...loopMaxInterval)
|
||||
heartbeatTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.performHeartbeat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func performHeartbeat() {
|
||||
// Always schedule next loop first ensures continuity even if this one fails/skips
|
||||
defer { scheduleNextHeartbeat() }
|
||||
|
||||
// 1. Check preconditions
|
||||
guard TorManager.shared.isReady else {
|
||||
SecureLogger.debug("Presence: skipping heartbeat (Tor not ready)", category: .session)
|
||||
return
|
||||
}
|
||||
|
||||
// App must be active (or at least we shouldn't broadcast if in background, usually)
|
||||
if !TorManager.shared.isForeground() {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Get channels
|
||||
let channels = LocationStateManager.shared.availableChannels
|
||||
guard !channels.isEmpty else { return }
|
||||
|
||||
// 3. Filter and broadcast
|
||||
// We use Task + sleep for decorrelation to allow the main runloop to proceed
|
||||
for channel in channels {
|
||||
// Check privacy restriction
|
||||
if !self.allowedPrecisions.contains(channel.geohash.count) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Launch independent task for each channel's delay
|
||||
Task { @MainActor in
|
||||
// Random delay for decorrelation
|
||||
let delay = TimeInterval.random(in: self.burstMinDelay...self.burstMaxDelay)
|
||||
let nanoseconds = UInt64(delay * 1_000_000_000)
|
||||
try? await Task.sleep(nanoseconds: nanoseconds)
|
||||
|
||||
self.broadcastPresence(for: channel.geohash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func broadcastPresence(for geohash: String) {
|
||||
do {
|
||||
guard let identity = try? idBridge.deriveIdentity(forGeohash: geohash) else {
|
||||
return
|
||||
}
|
||||
|
||||
let event = try NostrProtocol.createGeohashPresenceEvent(
|
||||
geohash: geohash,
|
||||
senderIdentity: identity
|
||||
)
|
||||
|
||||
// Send via RelayManager
|
||||
let targetRelays = GeoRelayDirectory.shared.closestRelays(
|
||||
toGeohash: geohash,
|
||||
count: TransportConfig.nostrGeoRelayCount
|
||||
)
|
||||
|
||||
if !targetRelays.isEmpty {
|
||||
NostrRelayManager.shared.sendEvent(event, to: targetRelays)
|
||||
SecureLogger.debug("Presence: sent heartbeat for \(geohash) (pub=\(identity.publicKeyHex.prefix(6))...)", category: .session)
|
||||
}
|
||||
} catch {
|
||||
SecureLogger.error("Presence: failed to create event for \(geohash): \(error)", category: .session)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,47 @@ import BitLogger
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
// MARK: - Keychain Error Types
|
||||
// BCH-01-009: Proper error classification to distinguish expected states from critical failures
|
||||
|
||||
/// Result of a keychain read operation with proper error classification
|
||||
enum KeychainReadResult {
|
||||
case success(Data)
|
||||
case itemNotFound // Expected: key doesn't exist yet
|
||||
case accessDenied // Critical: app lacks keychain access
|
||||
case deviceLocked // Recoverable: device is locked
|
||||
case authenticationFailed // Recoverable: biometric/passcode failed
|
||||
case otherError(OSStatus) // Unexpected error
|
||||
|
||||
var isRecoverableError: Bool {
|
||||
switch self {
|
||||
case .deviceLocked, .authenticationFailed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a keychain save operation with proper error classification
|
||||
enum KeychainSaveResult {
|
||||
case success
|
||||
case duplicateItem // Can retry with update
|
||||
case accessDenied // Critical: app lacks keychain access
|
||||
case deviceLocked // Recoverable: device is locked
|
||||
case storageFull // Critical: no space available
|
||||
case otherError(OSStatus)
|
||||
|
||||
var isRecoverableError: Bool {
|
||||
switch self {
|
||||
case .duplicateItem, .deviceLocked:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol KeychainManagerProtocol {
|
||||
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool
|
||||
func getIdentityKey(forKey key: String) -> Data?
|
||||
@@ -21,6 +62,12 @@ protocol KeychainManagerProtocol {
|
||||
|
||||
func verifyIdentityKeyExists() -> Bool
|
||||
|
||||
// BCH-01-009: Methods with proper error classification
|
||||
/// Get identity key with detailed result for error handling
|
||||
func getIdentityKeyWithResult(forKey key: String) -> KeychainReadResult
|
||||
/// Save identity key with detailed result for error handling
|
||||
func saveIdentityKeyWithResult(_ keyData: Data, forKey key: String) -> KeychainSaveResult
|
||||
|
||||
// MARK: - Generic Data Storage (consolidated from KeychainHelper)
|
||||
/// Save data with a custom service name
|
||||
func save(key: String, data: Data, service: String, accessible: CFString?)
|
||||
@@ -55,6 +102,180 @@ final class KeychainManager: KeychainManagerProtocol {
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - BCH-01-009: Methods with Proper Error Classification
|
||||
|
||||
/// Get identity key with detailed result for proper error handling
|
||||
/// Distinguishes between missing keys (expected) and critical failures
|
||||
func getIdentityKeyWithResult(forKey key: String) -> KeychainReadResult {
|
||||
let fullKey = "identity_\(key)"
|
||||
return retrieveDataWithResult(forKey: fullKey)
|
||||
}
|
||||
|
||||
/// Save identity key with detailed result and retry logic for transient errors
|
||||
func saveIdentityKeyWithResult(_ keyData: Data, forKey key: String) -> KeychainSaveResult {
|
||||
let fullKey = "identity_\(key)"
|
||||
return saveDataWithResult(keyData, forKey: fullKey)
|
||||
}
|
||||
|
||||
/// Internal method to save data with detailed result and retry for transient errors
|
||||
private func saveDataWithResult(_ data: Data, forKey key: String, retryCount: Int = 2) -> KeychainSaveResult {
|
||||
// Delete any existing item first to ensure clean state
|
||||
_ = delete(forKey: key)
|
||||
|
||||
// Build base query
|
||||
var base: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecValueData as String: data,
|
||||
kSecAttrService as String: service,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked,
|
||||
kSecAttrLabel as String: "bitchat-\(key)"
|
||||
]
|
||||
#if os(macOS)
|
||||
base[kSecAttrSynchronizable as String] = false
|
||||
#endif
|
||||
|
||||
func attempt(addAccessGroup: Bool) -> OSStatus {
|
||||
var query = base
|
||||
if addAccessGroup { query[kSecAttrAccessGroup as String] = appGroup }
|
||||
return SecItemAdd(query as CFDictionary, nil)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
var status = attempt(addAccessGroup: true)
|
||||
if status == -34018 { // Missing entitlement, retry without access group
|
||||
status = attempt(addAccessGroup: false)
|
||||
}
|
||||
#else
|
||||
let status = attempt(addAccessGroup: false)
|
||||
#endif
|
||||
|
||||
// Classify the result
|
||||
let result = classifySaveStatus(status)
|
||||
|
||||
// Log all outcomes consistently
|
||||
switch result {
|
||||
case .success:
|
||||
SecureLogger.debug("Keychain save succeeded for key: \(key)", category: .keychain)
|
||||
case .duplicateItem:
|
||||
SecureLogger.warning("Keychain save found duplicate for key: \(key)", category: .keychain)
|
||||
case .accessDenied:
|
||||
SecureLogger.error(NSError(domain: "Keychain", code: Int(status)),
|
||||
context: "Keychain access denied for key: \(key)", category: .keychain)
|
||||
case .deviceLocked:
|
||||
SecureLogger.warning("Device locked during keychain save for key: \(key)", category: .keychain)
|
||||
case .storageFull:
|
||||
SecureLogger.error(NSError(domain: "Keychain", code: Int(status)),
|
||||
context: "Keychain storage full for key: \(key)", category: .keychain)
|
||||
case .otherError(let code):
|
||||
SecureLogger.error(NSError(domain: "Keychain", code: Int(code)),
|
||||
context: "Keychain save failed for key: \(key)", category: .keychain)
|
||||
}
|
||||
|
||||
// Retry transient errors with exponential backoff
|
||||
if result.isRecoverableError && retryCount > 0 {
|
||||
let delayMs = UInt32((3 - retryCount) * 100) // 100ms, 200ms backoff
|
||||
usleep(delayMs * 1000)
|
||||
SecureLogger.debug("Retrying keychain save for key: \(key), attempts remaining: \(retryCount)", category: .keychain)
|
||||
return saveDataWithResult(data, forKey: key, retryCount: retryCount - 1)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Internal method to retrieve data with detailed result
|
||||
private func retrieveDataWithResult(forKey key: String) -> KeychainReadResult {
|
||||
let base: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecAttrService as String: service,
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
func attempt(withAccessGroup: Bool) -> OSStatus {
|
||||
var q = base
|
||||
if withAccessGroup { q[kSecAttrAccessGroup as String] = appGroup }
|
||||
return SecItemCopyMatching(q as CFDictionary, &result)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
var status = attempt(withAccessGroup: true)
|
||||
if status == -34018 { status = attempt(withAccessGroup: false) }
|
||||
#else
|
||||
let status = attempt(withAccessGroup: false)
|
||||
#endif
|
||||
|
||||
// Classify the result
|
||||
let readResult = classifyReadStatus(status, data: result as? Data)
|
||||
|
||||
// Log all outcomes consistently
|
||||
switch readResult {
|
||||
case .success:
|
||||
SecureLogger.debug("Keychain read succeeded for key: \(key)", category: .keychain)
|
||||
case .itemNotFound:
|
||||
// Expected case - no logging needed for missing keys
|
||||
break
|
||||
case .accessDenied:
|
||||
SecureLogger.error(NSError(domain: "Keychain", code: Int(status)),
|
||||
context: "Keychain access denied for key: \(key)", category: .keychain)
|
||||
case .deviceLocked:
|
||||
SecureLogger.warning("Device locked during keychain read for key: \(key)", category: .keychain)
|
||||
case .authenticationFailed:
|
||||
SecureLogger.warning("Authentication failed for keychain read of key: \(key)", category: .keychain)
|
||||
case .otherError(let code):
|
||||
SecureLogger.error(NSError(domain: "Keychain", code: Int(code)),
|
||||
context: "Keychain read failed for key: \(key)", category: .keychain)
|
||||
}
|
||||
|
||||
return readResult
|
||||
}
|
||||
|
||||
/// Classify keychain read status into meaningful categories
|
||||
private func classifyReadStatus(_ status: OSStatus, data: Data?) -> KeychainReadResult {
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
if let data = data {
|
||||
return .success(data)
|
||||
}
|
||||
return .otherError(status)
|
||||
case errSecItemNotFound:
|
||||
return .itemNotFound
|
||||
case errSecInteractionNotAllowed:
|
||||
// Device is locked or in a state that doesn't allow keychain access
|
||||
return .deviceLocked
|
||||
case errSecAuthFailed:
|
||||
return .authenticationFailed
|
||||
case -34018: // errSecMissingEntitlement
|
||||
return .accessDenied
|
||||
case errSecNotAvailable:
|
||||
return .accessDenied
|
||||
default:
|
||||
return .otherError(status)
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify keychain save status into meaningful categories
|
||||
private func classifySaveStatus(_ status: OSStatus) -> KeychainSaveResult {
|
||||
switch status {
|
||||
case errSecSuccess:
|
||||
return .success
|
||||
case errSecDuplicateItem:
|
||||
return .duplicateItem
|
||||
case errSecInteractionNotAllowed:
|
||||
return .deviceLocked
|
||||
case -34018: // errSecMissingEntitlement
|
||||
return .accessDenied
|
||||
case errSecNotAvailable:
|
||||
return .accessDenied
|
||||
case errSecDiskFull:
|
||||
return .storageFull
|
||||
default:
|
||||
return .otherError(status)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Generic Operations
|
||||
|
||||
private func save(_ value: String, forKey key: String) -> Bool {
|
||||
|
||||
@@ -6,104 +6,97 @@ final class MeshTopologyTracker {
|
||||
|
||||
private let queue = DispatchQueue(label: "mesh.topology", attributes: .concurrent)
|
||||
private let hopSize = 8
|
||||
private var adjacency: [RoutingID: Set<RoutingID>] = [:]
|
||||
// Directed claims: Key claims to see Value (neighbors)
|
||||
private var claims: [RoutingID: Set<RoutingID>] = [:]
|
||||
// Last time we received an update from a node
|
||||
private var lastSeen: [RoutingID: Date] = [:]
|
||||
|
||||
func reset() {
|
||||
queue.sync(flags: .barrier) {
|
||||
self.adjacency.removeAll()
|
||||
self.claims.removeAll()
|
||||
self.lastSeen.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
func recordDirectLink(between a: Data?, and b: Data?) {
|
||||
guard let left = sanitize(a), let right = sanitize(b), left != right else { return }
|
||||
queue.sync(flags: .barrier) {
|
||||
var setA = self.adjacency[left] ?? []
|
||||
setA.insert(right)
|
||||
self.adjacency[left] = setA
|
||||
/// Update the topology with a node's self-reported neighbor list
|
||||
func updateNeighbors(for sourceData: Data?, neighbors: [Data]) {
|
||||
guard let source = sanitize(sourceData) else { return }
|
||||
// Sanitize neighbors and exclude self-loops
|
||||
let validNeighbors = Set(neighbors.compactMap { sanitize($0) }).subtracting([source])
|
||||
|
||||
var setB = self.adjacency[right] ?? []
|
||||
setB.insert(left)
|
||||
self.adjacency[right] = setB
|
||||
}
|
||||
}
|
||||
|
||||
func removeDirectLink(between a: Data?, and b: Data?) {
|
||||
guard let left = sanitize(a), let right = sanitize(b), left != right else { return }
|
||||
queue.sync(flags: .barrier) {
|
||||
if var setA = self.adjacency[left] {
|
||||
setA.remove(right)
|
||||
self.adjacency[left] = setA.isEmpty ? nil : setA
|
||||
}
|
||||
if var setB = self.adjacency[right] {
|
||||
setB.remove(left)
|
||||
self.adjacency[right] = setB.isEmpty ? nil : setB
|
||||
}
|
||||
self.claims[source] = validNeighbors
|
||||
self.lastSeen[source] = Date()
|
||||
}
|
||||
}
|
||||
|
||||
func removePeer(_ data: Data?) {
|
||||
guard let peer = sanitize(data) else { return }
|
||||
queue.sync(flags: .barrier) {
|
||||
guard let neighbors = self.adjacency.removeValue(forKey: peer) else { return }
|
||||
for neighbor in neighbors {
|
||||
if var set = self.adjacency[neighbor] {
|
||||
set.remove(peer)
|
||||
self.adjacency[neighbor] = set.isEmpty ? nil : set
|
||||
}
|
||||
}
|
||||
self.claims.removeValue(forKey: peer)
|
||||
self.lastSeen.removeValue(forKey: peer)
|
||||
}
|
||||
}
|
||||
|
||||
func recordRoute(_ hops: [Data]) {
|
||||
let sanitized = hops.compactMap { sanitize($0) }
|
||||
guard sanitized.count >= 2 else { return }
|
||||
/// Prune nodes that haven't updated their topology in `age` seconds
|
||||
func prune(olderThan age: TimeInterval) {
|
||||
let deadline = Date().addingTimeInterval(-age)
|
||||
queue.sync(flags: .barrier) {
|
||||
for idx in 0..<(sanitized.count - 1) {
|
||||
let left = sanitized[idx]
|
||||
let right = sanitized[idx + 1]
|
||||
guard left != right else { continue }
|
||||
|
||||
var setA = self.adjacency[left] ?? []
|
||||
setA.insert(right)
|
||||
self.adjacency[left] = setA
|
||||
|
||||
var setB = self.adjacency[right] ?? []
|
||||
setB.insert(left)
|
||||
self.adjacency[right] = setB
|
||||
let stale = self.lastSeen.filter { $0.value < deadline }
|
||||
for (peer, _) in stale {
|
||||
self.claims.removeValue(forKey: peer)
|
||||
self.lastSeen.removeValue(forKey: peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func computeRoute(from start: Data?, to goal: Data?, maxHops: Int = 255) -> [Data]? {
|
||||
func computeRoute(from start: Data?, to goal: Data?, maxHops: Int = 10) -> [Data]? {
|
||||
guard let source = sanitize(start), let target = sanitize(goal) else { return nil }
|
||||
if source == target { return [source] }
|
||||
if source == target { return [] } // Direct connection, no intermediate hops
|
||||
|
||||
let graph = queue.sync { adjacency }
|
||||
guard graph[source] != nil, graph[target] != nil else { return nil }
|
||||
return queue.sync {
|
||||
// BFS
|
||||
var visited: Set<RoutingID> = [source]
|
||||
// Queue stores paths: [Start, Hop1, Hop2, ..., Current]
|
||||
var queuePaths: [[RoutingID]] = [[source]]
|
||||
|
||||
var visited: Set<RoutingID> = [source]
|
||||
var queuePaths: [[RoutingID]] = [[source]]
|
||||
var index = 0
|
||||
while !queuePaths.isEmpty {
|
||||
let path = queuePaths.removeFirst()
|
||||
// Limit path length (path contains source + maxHops + target) -> maxHops intermediate
|
||||
// If maxHops = 10, max edges = 11, max nodes = 12.
|
||||
if path.count > maxHops + 1 { continue }
|
||||
|
||||
while index < queuePaths.count {
|
||||
let path = queuePaths[index]
|
||||
index += 1
|
||||
guard path.count <= maxHops else { continue }
|
||||
guard let last = path.last, let neighbors = graph[last] else { continue }
|
||||
guard let last = path.last else { continue }
|
||||
|
||||
for neighbor in neighbors {
|
||||
if visited.contains(neighbor) { continue }
|
||||
var nextPath = path
|
||||
nextPath.append(neighbor)
|
||||
if neighbor == target { return nextPath }
|
||||
if nextPath.count <= maxHops {
|
||||
// Get neighbors that 'last' claims to see
|
||||
guard let neighbors = claims[last] else { continue }
|
||||
|
||||
for neighbor in neighbors {
|
||||
if visited.contains(neighbor) { continue }
|
||||
|
||||
// CONFIRMED EDGE CHECK:
|
||||
// 'last' claims 'neighbor' (checked above)
|
||||
// Does 'neighbor' claim 'last'?
|
||||
guard let neighborClaims = claims[neighbor],
|
||||
neighborClaims.contains(last) else {
|
||||
continue
|
||||
}
|
||||
|
||||
var nextPath = path
|
||||
nextPath.append(neighbor)
|
||||
|
||||
if neighbor == target {
|
||||
// Return only intermediate hops
|
||||
// Path: [Source, I1, I2, Target] -> [I1, I2]
|
||||
return Array(nextPath.dropFirst().dropLast())
|
||||
}
|
||||
|
||||
visited.insert(neighbor)
|
||||
queuePaths.append(nextPath)
|
||||
}
|
||||
visited.insert(neighbor)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@@ -200,46 +200,85 @@ final class NoiseEncryptionService {
|
||||
init(keychain: KeychainManagerProtocol) {
|
||||
self.keychain = keychain
|
||||
|
||||
// Load or create static identity key (ONLY from keychain)
|
||||
// BCH-01-009: Load or create static identity key with proper error handling
|
||||
let loadedKey: Curve25519.KeyAgreement.PrivateKey
|
||||
|
||||
// Try to load from keychain
|
||||
if let identityData = keychain.getIdentityKey(forKey: "noiseStaticKey"),
|
||||
let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) {
|
||||
loadedKey = key
|
||||
SecureLogger.logKeyOperation(.load, keyType: "noiseStaticKey", success: true)
|
||||
}
|
||||
// If no identity exists, create new one
|
||||
else {
|
||||
loadedKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
let keyData = loadedKey.rawRepresentation
|
||||
// Try to load from keychain with proper error classification
|
||||
let noiseKeyResult = keychain.getIdentityKeyWithResult(forKey: "noiseStaticKey")
|
||||
|
||||
// Save to keychain
|
||||
let saved = keychain.saveIdentityKey(keyData, forKey: "noiseStaticKey")
|
||||
SecureLogger.logKeyOperation(.create, keyType: "noiseStaticKey", success: saved)
|
||||
switch noiseKeyResult {
|
||||
case .success(let identityData):
|
||||
if let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) {
|
||||
loadedKey = key
|
||||
SecureLogger.logKeyOperation(.load, keyType: "noiseStaticKey", success: true)
|
||||
} else {
|
||||
// Data corrupted, regenerate
|
||||
SecureLogger.warning("Noise static key data corrupted, regenerating", category: .keychain)
|
||||
loadedKey = Self.generateAndSaveNoiseKey(keychain: keychain)
|
||||
}
|
||||
|
||||
case .itemNotFound:
|
||||
// Expected case: no key exists yet, create new one
|
||||
loadedKey = Self.generateAndSaveNoiseKey(keychain: keychain)
|
||||
|
||||
case .accessDenied:
|
||||
// Critical error - log but proceed with ephemeral key (will be lost on restart)
|
||||
SecureLogger.error(NSError(domain: "Keychain", code: -1),
|
||||
context: "Keychain access denied - using ephemeral identity", category: .keychain)
|
||||
loadedKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
|
||||
case .deviceLocked, .authenticationFailed:
|
||||
// Recoverable error - use ephemeral key and warn
|
||||
SecureLogger.warning("Device locked or auth failed - using ephemeral identity until unlocked", category: .keychain)
|
||||
loadedKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
|
||||
case .otherError(let status):
|
||||
// Unexpected error - log and use ephemeral key
|
||||
SecureLogger.error(NSError(domain: "Keychain", code: Int(status)),
|
||||
context: "Unexpected keychain error - using ephemeral identity", category: .keychain)
|
||||
loadedKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
}
|
||||
|
||||
// Now assign the final value
|
||||
self.staticIdentityKey = loadedKey
|
||||
self.staticIdentityPublicKey = staticIdentityKey.publicKey
|
||||
|
||||
// Load or create signing key pair
|
||||
// BCH-01-009: Load or create signing key pair with proper error handling
|
||||
let loadedSigningKey: Curve25519.Signing.PrivateKey
|
||||
|
||||
// Try to load from keychain
|
||||
if let signingData = keychain.getIdentityKey(forKey: "ed25519SigningKey"),
|
||||
let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
|
||||
loadedSigningKey = key
|
||||
SecureLogger.logKeyOperation(.load, keyType: "ed25519SigningKey", success: true)
|
||||
}
|
||||
// If no signing key exists, create new one
|
||||
else {
|
||||
loadedSigningKey = Curve25519.Signing.PrivateKey()
|
||||
let keyData = loadedSigningKey.rawRepresentation
|
||||
let signingKeyResult = keychain.getIdentityKeyWithResult(forKey: "ed25519SigningKey")
|
||||
|
||||
// Save to keychain
|
||||
let saved = keychain.saveIdentityKey(keyData, forKey: "ed25519SigningKey")
|
||||
SecureLogger.logKeyOperation(.create, keyType: "ed25519SigningKey", success: saved)
|
||||
switch signingKeyResult {
|
||||
case .success(let signingData):
|
||||
if let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
|
||||
loadedSigningKey = key
|
||||
SecureLogger.logKeyOperation(.load, keyType: "ed25519SigningKey", success: true)
|
||||
} else {
|
||||
// Data corrupted, regenerate
|
||||
SecureLogger.warning("Ed25519 signing key data corrupted, regenerating", category: .keychain)
|
||||
loadedSigningKey = Self.generateAndSaveSigningKey(keychain: keychain)
|
||||
}
|
||||
|
||||
case .itemNotFound:
|
||||
// Expected case: no key exists yet, create new one
|
||||
loadedSigningKey = Self.generateAndSaveSigningKey(keychain: keychain)
|
||||
|
||||
case .accessDenied:
|
||||
// Critical error - log but proceed with ephemeral key
|
||||
SecureLogger.error(NSError(domain: "Keychain", code: -1),
|
||||
context: "Keychain access denied - using ephemeral signing key", category: .keychain)
|
||||
loadedSigningKey = Curve25519.Signing.PrivateKey()
|
||||
|
||||
case .deviceLocked, .authenticationFailed:
|
||||
// Recoverable error - use ephemeral key and warn
|
||||
SecureLogger.warning("Device locked or auth failed - using ephemeral signing key until unlocked", category: .keychain)
|
||||
loadedSigningKey = Curve25519.Signing.PrivateKey()
|
||||
|
||||
case .otherError(let status):
|
||||
// Unexpected error - log and use ephemeral key
|
||||
SecureLogger.error(NSError(domain: "Keychain", code: Int(status)),
|
||||
context: "Unexpected keychain error - using ephemeral signing key", category: .keychain)
|
||||
loadedSigningKey = Curve25519.Signing.PrivateKey()
|
||||
}
|
||||
|
||||
// Now assign the signing keys
|
||||
@@ -258,6 +297,56 @@ final class NoiseEncryptionService {
|
||||
startRekeyTimer()
|
||||
}
|
||||
|
||||
// MARK: - BCH-01-009: Key Generation Helpers with Save Verification
|
||||
|
||||
/// Generate and save a new Noise static key, verifying the save succeeds
|
||||
private static func generateAndSaveNoiseKey(keychain: KeychainManagerProtocol) -> Curve25519.KeyAgreement.PrivateKey {
|
||||
let newKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
let keyData = newKey.rawRepresentation
|
||||
|
||||
// Save to keychain and verify success
|
||||
let saveResult = keychain.saveIdentityKeyWithResult(keyData, forKey: "noiseStaticKey")
|
||||
|
||||
switch saveResult {
|
||||
case .success:
|
||||
SecureLogger.logKeyOperation(.create, keyType: "noiseStaticKey", success: true)
|
||||
case .duplicateItem:
|
||||
// This shouldn't happen since we just tried to load, but handle it
|
||||
SecureLogger.warning("Noise key already exists (race condition?)", category: .keychain)
|
||||
default:
|
||||
// Save failed - log but continue with the key (it will be ephemeral)
|
||||
SecureLogger.error(NSError(domain: "Keychain", code: -1),
|
||||
context: "Failed to persist noise static key - identity will be lost on restart",
|
||||
category: .keychain)
|
||||
}
|
||||
|
||||
return newKey
|
||||
}
|
||||
|
||||
/// Generate and save a new Ed25519 signing key, verifying the save succeeds
|
||||
private static func generateAndSaveSigningKey(keychain: KeychainManagerProtocol) -> Curve25519.Signing.PrivateKey {
|
||||
let newKey = Curve25519.Signing.PrivateKey()
|
||||
let keyData = newKey.rawRepresentation
|
||||
|
||||
// Save to keychain and verify success
|
||||
let saveResult = keychain.saveIdentityKeyWithResult(keyData, forKey: "ed25519SigningKey")
|
||||
|
||||
switch saveResult {
|
||||
case .success:
|
||||
SecureLogger.logKeyOperation(.create, keyType: "ed25519SigningKey", success: true)
|
||||
case .duplicateItem:
|
||||
// This shouldn't happen since we just tried to load, but handle it
|
||||
SecureLogger.warning("Signing key already exists (race condition?)", category: .keychain)
|
||||
default:
|
||||
// Save failed - log but continue with the key (it will be ephemeral)
|
||||
SecureLogger.error(NSError(domain: "Keychain", code: -1),
|
||||
context: "Failed to persist signing key - identity will be lost on restart",
|
||||
category: .keychain)
|
||||
}
|
||||
|
||||
return newKey
|
||||
}
|
||||
|
||||
// MARK: - Public Interface
|
||||
|
||||
/// Get our static public key for sharing
|
||||
|
||||
@@ -62,6 +62,7 @@ struct NotificationStreamAssembler {
|
||||
let hasRecipient = (flags & BinaryProtocol.Flags.hasRecipient) != 0
|
||||
let hasSignature = (flags & BinaryProtocol.Flags.hasSignature) != 0
|
||||
let isCompressed = (flags & BinaryProtocol.Flags.isCompressed) != 0
|
||||
let hasRoute = (version >= 2) && (flags & BinaryProtocol.Flags.hasRoute) != 0
|
||||
|
||||
let lengthOffset = 12
|
||||
let payloadLength: Int
|
||||
@@ -80,6 +81,15 @@ struct NotificationStreamAssembler {
|
||||
var frameLength = framePrefix + payloadLength
|
||||
if hasRecipient { frameLength += BinaryProtocol.recipientIDSize }
|
||||
if hasSignature { frameLength += BinaryProtocol.signatureSize }
|
||||
|
||||
if hasRoute {
|
||||
let routeCountOffset = framePrefix + (hasRecipient ? BinaryProtocol.recipientIDSize : 0)
|
||||
let routeCountIndex = buffer.startIndex + routeCountOffset
|
||||
guard buffer.count > routeCountOffset else { break }
|
||||
let routeCount = Int(buffer[routeCountIndex])
|
||||
frameLength += 1 + (routeCount * BinaryProtocol.senderIDSize)
|
||||
}
|
||||
|
||||
if isCompressed {
|
||||
let rawLengthFieldBytes = (version == 2) ? 4 : 2
|
||||
if payloadLength < rawLengthFieldBytes {
|
||||
|
||||
@@ -58,6 +58,10 @@ protocol Transport: AnyObject {
|
||||
// QR verification (optional for transports)
|
||||
func sendVerifyChallenge(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
|
||||
func sendVerifyResponse(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
|
||||
|
||||
// Pending file management (BCH-01-002: files held in memory until user accepts)
|
||||
func acceptPendingFile(id: String) -> URL?
|
||||
func declinePendingFile(id: String)
|
||||
}
|
||||
|
||||
extension Transport {
|
||||
@@ -70,6 +74,9 @@ extension Transport {
|
||||
func sendMessage(_ content: String, mentions: [String], messageID: String, timestamp: Date) {
|
||||
sendMessage(content, mentions: mentions)
|
||||
}
|
||||
|
||||
func acceptPendingFile(id: String) -> URL? { nil }
|
||||
func declinePendingFile(id: String) {}
|
||||
}
|
||||
|
||||
protocol TransportPeerEventsDelegate: AnyObject {
|
||||
|
||||
@@ -162,6 +162,14 @@ enum TransportConfig {
|
||||
static let blePostAnnounceDelaySeconds: TimeInterval = 0.4
|
||||
static let bleForceAnnounceMinIntervalSeconds: TimeInterval = 0.15
|
||||
|
||||
// BCH-01-004: Rate-limiting for subscription-triggered announces
|
||||
// Prevents rapid enumeration attacks by rate-limiting announce responses
|
||||
static let bleSubscriptionRateLimitMinSeconds: TimeInterval = 2.0 // Minimum interval between announces per central
|
||||
static let bleSubscriptionRateLimitBackoffFactor: Double = 2.0 // Exponential backoff multiplier
|
||||
static let bleSubscriptionRateLimitMaxBackoffSeconds: TimeInterval = 30.0 // Maximum backoff period
|
||||
static let bleSubscriptionRateLimitWindowSeconds: TimeInterval = 60.0 // Window for tracking subscription attempts
|
||||
static let bleSubscriptionRateLimitMaxAttempts: Int = 5 // Max attempts before extended cooldown
|
||||
|
||||
// Store-and-forward for directed packets at relays
|
||||
static let bleDirectedSpoolWindowSeconds: TimeInterval = 15.0
|
||||
|
||||
|
||||
@@ -52,11 +52,13 @@ struct InputValidator {
|
||||
// MessageType/NoisePayloadType enums; keeping validator free of stale lists.
|
||||
|
||||
/// Validates timestamp is reasonable (not too far in past or future)
|
||||
/// BCH-01-011: Reduced from ±1 hour to ±5 minutes to limit replay attack window
|
||||
static func validateTimestamp(_ timestamp: Date) -> Bool {
|
||||
let now = Date()
|
||||
let oneHourAgo = now.addingTimeInterval(-3600)
|
||||
let oneHourFromNow = now.addingTimeInterval(3600)
|
||||
return timestamp >= oneHourAgo && timestamp <= oneHourFromNow
|
||||
// 5 minutes = 300 seconds (industry standard for replay protection)
|
||||
let fiveMinutesAgo = now.addingTimeInterval(-300)
|
||||
let fiveMinutesFromNow = now.addingTimeInterval(300)
|
||||
return timestamp >= fiveMinutesAgo && timestamp <= fiveMinutesFromNow
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2055,6 +2055,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv
|
||||
} catch {
|
||||
SecureLogger.error("Failed to clear media files during panic: \(error)", category: .session)
|
||||
}
|
||||
|
||||
// BCH-01-013: Clear iOS app switcher snapshots
|
||||
// These are stored in Library/Caches/Snapshots/<bundle_id>/
|
||||
#if os(iOS)
|
||||
Self.clearAppSwitcherSnapshots()
|
||||
#endif
|
||||
}
|
||||
|
||||
// Force immediate UI update for panic mode
|
||||
@@ -2062,6 +2068,29 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv
|
||||
|
||||
}
|
||||
|
||||
/// BCH-01-013: Clear iOS app switcher snapshots during panic mode
|
||||
/// iOS stores preview screenshots in Library/Caches/Snapshots/<bundle_id>/
|
||||
/// These could reveal sensitive information visible in the app at the time
|
||||
#if os(iOS)
|
||||
private nonisolated static func clearAppSwitcherSnapshots() {
|
||||
do {
|
||||
let cacheDir = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
||||
let snapshotsDir = cacheDir.appendingPathComponent("Snapshots", isDirectory: true)
|
||||
|
||||
// Clear all snapshots (iOS stores them in subdirectories by bundle ID and scene)
|
||||
if FileManager.default.fileExists(atPath: snapshotsDir.path) {
|
||||
let contents = try FileManager.default.contentsOfDirectory(at: snapshotsDir, includingPropertiesForKeys: nil)
|
||||
for item in contents {
|
||||
try FileManager.default.removeItem(at: item)
|
||||
}
|
||||
SecureLogger.info("🗑️ Cleared app switcher snapshots during panic clear", category: .session)
|
||||
}
|
||||
} catch {
|
||||
SecureLogger.error("Failed to clear app switcher snapshots: \(error)", category: .session)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Autocomplete
|
||||
|
||||
func updateAutocomplete(for text: String, cursorPosition: Int) {
|
||||
@@ -3046,46 +3075,91 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv
|
||||
}
|
||||
}
|
||||
|
||||
/// Find message index trying both short (16-hex) and long (64-hex) peer ID formats.
|
||||
/// Returns the peer ID where the message was found and its index, or nil if not found.
|
||||
private func findMessageIndex(messageID: String, peerID: PeerID) -> (peerID: PeerID, index: Int)? {
|
||||
// Try direct lookup first
|
||||
if let messages = privateChats[peerID],
|
||||
let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||
return (peerID, idx)
|
||||
}
|
||||
|
||||
// Try with full noise key if peerID is short (16 hex chars)
|
||||
if peerID.bare.count == 16,
|
||||
let peer = unifiedPeerService.getPeer(by: peerID),
|
||||
!peer.noisePublicKey.isEmpty {
|
||||
let longID = PeerID(hexData: peer.noisePublicKey)
|
||||
if let messages = privateChats[longID],
|
||||
let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||
return (longID, idx)
|
||||
}
|
||||
}
|
||||
|
||||
// Try with short form if peerID is long (64 hex = noise key)
|
||||
if peerID.bare.count == 64 {
|
||||
let shortID = peerID.toShort()
|
||||
if let messages = privateChats[shortID],
|
||||
let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||
return (shortID, idx)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Low-level BLE events
|
||||
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
||||
Task { @MainActor in
|
||||
switch type {
|
||||
case .privateMessage:
|
||||
guard let pm = PrivateMessagePacket.decode(from: payload) else { return }
|
||||
|
||||
// BCH-01-012: Check blocking before processing private message to prevent notification bypass
|
||||
if isPeerBlocked(peerID) {
|
||||
SecureLogger.debug("🚫 Ignoring Noise payload from blocked peer: \(peerID)", category: .security)
|
||||
return
|
||||
}
|
||||
|
||||
let senderName = unifiedPeerService.getPeer(by: peerID)?.nickname ?? "Unknown"
|
||||
let pmMentions = parseMentions(from: pm.content)
|
||||
let msg = BitchatMessage(
|
||||
id: pm.messageID,
|
||||
sender: senderName,
|
||||
content: pm.content,
|
||||
timestamp: timestamp,
|
||||
isRelay: false,
|
||||
originalSender: nil,
|
||||
isPrivate: true,
|
||||
recipientNickname: nickname,
|
||||
senderPeerID: peerID,
|
||||
mentions: pmMentions.isEmpty ? nil : pmMentions
|
||||
)
|
||||
let pmMentions = parseMentions(from: pm.content)
|
||||
let msg = BitchatMessage(
|
||||
id: pm.messageID,
|
||||
sender: senderName,
|
||||
content: pm.content,
|
||||
timestamp: timestamp,
|
||||
isRelay: false,
|
||||
originalSender: nil,
|
||||
isPrivate: true,
|
||||
recipientNickname: nickname,
|
||||
senderPeerID: peerID,
|
||||
mentions: pmMentions.isEmpty ? nil : pmMentions
|
||||
)
|
||||
handlePrivateMessage(msg)
|
||||
// Send delivery ACK back over BLE
|
||||
meshService.sendDeliveryAck(for: pm.messageID, to: peerID)
|
||||
|
||||
case .delivered:
|
||||
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
||||
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
|
||||
if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||
privateChats[peerID]?[idx].deliveryStatus = .delivered(to: name, at: Date())
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
guard let name = unifiedPeerService.getPeer(by: peerID)?.nickname,
|
||||
let (foundPeerID, idx) = findMessageIndex(messageID: messageID, peerID: peerID) else { return }
|
||||
|
||||
// Don't downgrade from .read to .delivered
|
||||
if case .read = privateChats[foundPeerID]?[idx].deliveryStatus { return }
|
||||
|
||||
privateChats[foundPeerID]?[idx].deliveryStatus = .delivered(to: name, at: Date())
|
||||
objectWillChange.send()
|
||||
|
||||
case .readReceipt:
|
||||
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
||||
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
|
||||
if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
||||
privateChats[peerID]?[idx].deliveryStatus = .read(by: name, at: Date())
|
||||
objectWillChange.send()
|
||||
}
|
||||
guard let name = unifiedPeerService.getPeer(by: peerID)?.nickname,
|
||||
let (foundPeerID, idx) = findMessageIndex(messageID: messageID, peerID: peerID) else { return }
|
||||
|
||||
// Explicitly unwrap and re-assign to ensure the @Published setter is called
|
||||
if let messages = privateChats[foundPeerID], idx < messages.count {
|
||||
messages[idx].deliveryStatus = .read(by: name, at: Date())
|
||||
privateChats[foundPeerID] = messages
|
||||
privateChatManager.objectWillChange.send()
|
||||
objectWillChange.send()
|
||||
}
|
||||
case .verifyChallenge:
|
||||
// Parse and respond
|
||||
|
||||
@@ -56,7 +56,8 @@ extension ChatViewModel {
|
||||
}
|
||||
|
||||
func subscribeNostrEvent(_ event: NostrEvent) {
|
||||
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue,
|
||||
guard (event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue ||
|
||||
event.kind == NostrProtocol.EventKind.geohashPresence.rawValue),
|
||||
!deduplicationService.hasProcessedNostrEvent(event.id)
|
||||
else {
|
||||
return
|
||||
@@ -86,6 +87,11 @@ extension ChatViewModel {
|
||||
// Update participants last-seen for this pubkey
|
||||
participantTracker.recordParticipant(pubkeyHex: event.pubkey)
|
||||
|
||||
// If presence heartbeat (Kind 20001), stop here - no content to display
|
||||
if event.kind == NostrProtocol.EventKind.geohashPresence.rawValue {
|
||||
return
|
||||
}
|
||||
|
||||
// Track teleported tag (only our format ["t","teleport"]) for icon state
|
||||
let hasTeleportTag = event.tags.contains(where: { tag in
|
||||
tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport"
|
||||
@@ -124,9 +130,18 @@ extension ChatViewModel {
|
||||
mentions: mentions.isEmpty ? nil : mentions
|
||||
)
|
||||
Task { @MainActor in
|
||||
// BCH-01-012: Check blocking before any notifications
|
||||
// handlePublicMessage has its own blocking check but returns silently,
|
||||
// so we must also guard checkForMentions to prevent notification bypass
|
||||
let isBlocked = identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey.lowercased())
|
||||
|
||||
handlePublicMessage(msg)
|
||||
checkForMentions(msg)
|
||||
sendHapticFeedback(for: msg)
|
||||
|
||||
// Only check mentions and send haptic if sender is not blocked
|
||||
if !isBlocked {
|
||||
checkForMentions(msg)
|
||||
sendHapticFeedback(for: msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,8 +254,9 @@ extension ChatViewModel {
|
||||
}
|
||||
|
||||
func handleNostrEvent(_ event: NostrEvent) {
|
||||
// Only handle ephemeral kind 20000 with matching tag
|
||||
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
||||
// Only handle ephemeral kind 20000 or presence kind 20001 with matching tag
|
||||
guard (event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue ||
|
||||
event.kind == NostrProtocol.EventKind.geohashPresence.rawValue) else { return }
|
||||
|
||||
// Deduplicate
|
||||
if deduplicationService.hasProcessedNostrEvent(event.id) { return }
|
||||
@@ -250,6 +266,11 @@ extension ChatViewModel {
|
||||
let tagSummary = event.tags.map { "[" + $0.joined(separator: ",") + "]" }.joined(separator: ",")
|
||||
SecureLogger.debug("GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tags=\(tagSummary)", category: .session)
|
||||
|
||||
// If this pubkey is blocked, skip mapping, participants, and timeline
|
||||
if identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey) {
|
||||
return
|
||||
}
|
||||
|
||||
// Track teleport tag for participants – only our format ["t", "teleport"]
|
||||
let hasTeleportTag: Bool = event.tags.contains { tag in
|
||||
tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport"
|
||||
@@ -273,6 +294,9 @@ extension ChatViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
// Update participants last-seen for this pubkey
|
||||
participantTracker.recordParticipant(pubkeyHex: event.pubkey)
|
||||
|
||||
// Skip only very recent self-echo from relay; include older self events for hydration
|
||||
if isSelf {
|
||||
let eventTime = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||
@@ -287,17 +311,14 @@ extension ChatViewModel {
|
||||
geoNicknames[event.pubkey.lowercased()] = nick
|
||||
}
|
||||
|
||||
// If this pubkey is blocked, skip mapping, participants, and timeline
|
||||
if identityManager.isNostrBlocked(pubkeyHexLowercased: event.pubkey) {
|
||||
return
|
||||
}
|
||||
|
||||
// Store mapping for geohash DM initiation
|
||||
nostrKeyMapping[PeerID(nostr_: event.pubkey)] = event.pubkey
|
||||
nostrKeyMapping[PeerID(nostr: event.pubkey)] = event.pubkey
|
||||
|
||||
// Update participants last-seen for this pubkey
|
||||
participantTracker.recordParticipant(pubkeyHex: event.pubkey)
|
||||
// If presence heartbeat (Kind 20001), stop here - no content to display
|
||||
if event.kind == NostrProtocol.EventKind.geohashPresence.rawValue {
|
||||
return
|
||||
}
|
||||
|
||||
let senderName = displayNameForNostrPubkey(event.pubkey)
|
||||
let content = event.content
|
||||
@@ -464,7 +485,8 @@ extension ChatViewModel {
|
||||
}
|
||||
|
||||
func subscribeNostrEvent(_ event: NostrEvent, gh: String) {
|
||||
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
||||
guard (event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue ||
|
||||
event.kind == NostrProtocol.EventKind.geohashPresence.rawValue) else { return }
|
||||
|
||||
// Compute current participant count (5-minute window) BEFORE updating with this event
|
||||
let existingCount = participantTracker.participantCount(for: gh)
|
||||
|
||||
@@ -740,15 +740,11 @@ extension ChatViewModel {
|
||||
|
||||
if isViewing {
|
||||
// Mark read immediately if viewing
|
||||
// Use router to send read receipt
|
||||
// Use the incoming peerID directly - it has the established Noise session.
|
||||
// Don't use PeerID(hexData: noiseKey) as that creates a 64-hex ID without a session.
|
||||
// Use meshService directly (not messageRouter) so it queues if peer disconnects.
|
||||
let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID, readerNickname: nickname)
|
||||
if let key = noiseKey {
|
||||
// Send via router to stable key if available (preferred for persistence/Nostr fallback)
|
||||
messageRouter.sendReadReceipt(receipt, to: PeerID(hexData: key))
|
||||
} else {
|
||||
// Fallback to mesh direct
|
||||
meshService.sendReadReceipt(receipt, to: peerID)
|
||||
}
|
||||
meshService.sendReadReceipt(receipt, to: peerID)
|
||||
sentReadReceipts.insert(message.id)
|
||||
} else {
|
||||
// Notify
|
||||
|
||||
@@ -86,10 +86,6 @@ struct AppInfoView: View {
|
||||
]
|
||||
}
|
||||
|
||||
enum Warning {
|
||||
static let title: LocalizedStringKey = "app_info.warning.title"
|
||||
static let message: LocalizedStringKey = "app_info.warning.message"
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -192,24 +188,6 @@ struct AppInfoView: View {
|
||||
|
||||
FeatureRow(info: Strings.Privacy.panic)
|
||||
}
|
||||
|
||||
// Warning
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
SectionHeader(Strings.Warning.title)
|
||||
.foregroundColor(Color.red)
|
||||
|
||||
Text(Strings.Warning.message)
|
||||
.font(.bitchatSystem(size: 14, design: .monospaced))
|
||||
.foregroundColor(Color.red)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.top, 6)
|
||||
.padding(.bottom, 16)
|
||||
.padding(.horizontal)
|
||||
.background(Color.red.opacity(0.1))
|
||||
.cornerRadius(8)
|
||||
|
||||
.padding(.top)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
@@ -40,10 +40,31 @@ struct LocationChannelsSheet: View {
|
||||
}
|
||||
|
||||
static func levelTitle(for level: GeohashChannelLevel, count: Int) -> String {
|
||||
// High-precision uncertainty: if count is 0 for high-precision levels,
|
||||
// show "?" because presence broadcasting is disabled for privacy.
|
||||
let isHighPrecision = (level == .neighborhood || level == .block || level == .building)
|
||||
if isHighPrecision && count == 0 {
|
||||
return String(
|
||||
format: String(localized: "location_channels.row_title_unknown", defaultValue: "%@ [? people]"),
|
||||
locale: .current,
|
||||
level.displayName
|
||||
)
|
||||
}
|
||||
return rowTitle(label: level.displayName, count: count)
|
||||
}
|
||||
|
||||
static func bookmarkTitle(geohash: String, count: Int) -> String {
|
||||
// Check precision for bookmarks too
|
||||
let len = geohash.count
|
||||
// Neighborhood=6, Block=7, Building=8+
|
||||
let isHighPrecision = (len >= 6)
|
||||
if isHighPrecision && count == 0 {
|
||||
return String(
|
||||
format: String(localized: "location_channels.row_title_unknown", defaultValue: "%@ [? people]"),
|
||||
locale: .current,
|
||||
"#\(geohash)"
|
||||
)
|
||||
}
|
||||
return rowTitle(label: "#\(geohash)", count: count)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,19 @@ final class PreviewKeychainManager: KeychainManagerProtocol {
|
||||
storage["identity_noiseStaticKey"] != nil
|
||||
}
|
||||
|
||||
// BCH-01-009: New methods with proper error classification
|
||||
func getIdentityKeyWithResult(forKey key: String) -> KeychainReadResult {
|
||||
if let data = storage[key] {
|
||||
return .success(data)
|
||||
}
|
||||
return .itemNotFound
|
||||
}
|
||||
|
||||
func saveIdentityKeyWithResult(_ keyData: Data, forKey key: String) -> KeychainSaveResult {
|
||||
storage[key] = keyData
|
||||
return .success
|
||||
}
|
||||
|
||||
// MARK: - Generic Data Storage (consolidated from KeychainHelper)
|
||||
|
||||
func save(key: String, data: Data, service: String, accessible: CFString?) {
|
||||
|
||||
@@ -43,17 +43,16 @@ struct FragmentationTests {
|
||||
// Shuffle fragments to simulate out-of-order arrival
|
||||
let shuffled = fragments.shuffled()
|
||||
|
||||
// Inject fragments spaced out to avoid concurrent mutation inside BLEService
|
||||
// Send fragments sequentially with small delays (no fire-and-forget Tasks)
|
||||
for (i, fragment) in shuffled.enumerated() {
|
||||
let delay = 5 * Double(i) * 0.001
|
||||
Task {
|
||||
try await sleep(delay)
|
||||
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
|
||||
if i > 0 {
|
||||
try await Task.sleep(for: .milliseconds(5))
|
||||
}
|
||||
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
|
||||
}
|
||||
|
||||
// Allow async processing
|
||||
try await sleep(0.5)
|
||||
// Wait for delegate callback with proper timeout
|
||||
try await capture.waitForPublicMessages(count: 1, timeout: .seconds(2))
|
||||
|
||||
#expect(capture.publicMessages.count == 1)
|
||||
#expect(capture.publicMessages.first?.content.count == 3_000)
|
||||
@@ -78,16 +77,16 @@ struct FragmentationTests {
|
||||
frags.insert(dup, at: 1)
|
||||
}
|
||||
|
||||
// Send fragments sequentially with small delays (no fire-and-forget Tasks)
|
||||
for (i, fragment) in frags.enumerated() {
|
||||
let delay = 5 * Double(i) * 0.001
|
||||
Task {
|
||||
try await sleep(delay)
|
||||
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
|
||||
if i > 0 {
|
||||
try await Task.sleep(for: .milliseconds(5))
|
||||
}
|
||||
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
|
||||
}
|
||||
|
||||
// Allow async processing
|
||||
try await sleep(0.5)
|
||||
// Wait for delegate callback with proper timeout
|
||||
try await capture.waitForPublicMessages(count: 1, timeout: .seconds(2))
|
||||
|
||||
#expect(capture.publicMessages.count == 1)
|
||||
#expect(capture.publicMessages.first?.content.count == 2048)
|
||||
@@ -196,12 +195,128 @@ struct FragmentationTests {
|
||||
}
|
||||
|
||||
extension FragmentationTests {
|
||||
private final class CaptureDelegate: BitchatDelegate {
|
||||
var publicMessages: [(peerID: PeerID, nickname: String, content: String)] = []
|
||||
var receivedMessages: [BitchatMessage] = []
|
||||
func didReceiveMessage(_ message: BitchatMessage) {
|
||||
receivedMessages.append(message)
|
||||
/// Thread-safe delegate that supports awaiting message delivery
|
||||
private final class CaptureDelegate: BitchatDelegate, @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var _publicMessages: [(peerID: PeerID, nickname: String, content: String)] = []
|
||||
private var _receivedMessages: [BitchatMessage] = []
|
||||
private var publicMessageContinuation: CheckedContinuation<Void, Never>?
|
||||
private var receivedMessageContinuation: CheckedContinuation<Void, Never>?
|
||||
private var expectedPublicMessageCount: Int = 0
|
||||
private var expectedReceivedMessageCount: Int = 0
|
||||
|
||||
var publicMessages: [(peerID: PeerID, nickname: String, content: String)] {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return _publicMessages
|
||||
}
|
||||
|
||||
var receivedMessages: [BitchatMessage] {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return _receivedMessages
|
||||
}
|
||||
|
||||
func didReceiveMessage(_ message: BitchatMessage) {
|
||||
lock.lock()
|
||||
_receivedMessages.append(message)
|
||||
let count = _receivedMessages.count
|
||||
let expected = expectedReceivedMessageCount
|
||||
let continuation = receivedMessageContinuation
|
||||
lock.unlock()
|
||||
|
||||
if count >= expected, let cont = continuation {
|
||||
lock.lock()
|
||||
receivedMessageContinuation = nil
|
||||
lock.unlock()
|
||||
cont.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?) {
|
||||
lock.lock()
|
||||
_publicMessages.append((peerID, nickname, content))
|
||||
let count = _publicMessages.count
|
||||
let expected = expectedPublicMessageCount
|
||||
let continuation = publicMessageContinuation
|
||||
lock.unlock()
|
||||
|
||||
if count >= expected, let cont = continuation {
|
||||
lock.lock()
|
||||
publicMessageContinuation = nil
|
||||
lock.unlock()
|
||||
cont.resume()
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for the specified number of public messages to be received
|
||||
func waitForPublicMessages(count: Int, timeout: Duration = .seconds(2)) async throws {
|
||||
lock.lock()
|
||||
if _publicMessages.count >= count {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
expectedPublicMessageCount = count
|
||||
lock.unlock()
|
||||
|
||||
try await withThrowingTaskGroup(of: Void.self) { group in
|
||||
group.addTask {
|
||||
await withCheckedContinuation { continuation in
|
||||
self.lock.lock()
|
||||
// Recheck count after acquiring lock to avoid race condition
|
||||
// where message arrives between initial check and continuation install
|
||||
if self._publicMessages.count >= count {
|
||||
self.lock.unlock()
|
||||
continuation.resume()
|
||||
return
|
||||
}
|
||||
self.publicMessageContinuation = continuation
|
||||
self.lock.unlock()
|
||||
}
|
||||
}
|
||||
group.addTask {
|
||||
try await Task.sleep(for: timeout)
|
||||
throw CancellationError()
|
||||
}
|
||||
try await group.next()
|
||||
group.cancelAll()
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for the specified number of received messages
|
||||
func waitForReceivedMessages(count: Int, timeout: Duration = .seconds(2)) async throws {
|
||||
lock.lock()
|
||||
if _receivedMessages.count >= count {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
expectedReceivedMessageCount = count
|
||||
lock.unlock()
|
||||
|
||||
try await withThrowingTaskGroup(of: Void.self) { group in
|
||||
group.addTask {
|
||||
await withCheckedContinuation { continuation in
|
||||
self.lock.lock()
|
||||
// Recheck count after acquiring lock to avoid race condition
|
||||
// where message arrives between initial check and continuation install
|
||||
if self._receivedMessages.count >= count {
|
||||
self.lock.unlock()
|
||||
continuation.resume()
|
||||
return
|
||||
}
|
||||
self.receivedMessageContinuation = continuation
|
||||
self.lock.unlock()
|
||||
}
|
||||
}
|
||||
group.addTask {
|
||||
try await Task.sleep(for: timeout)
|
||||
throw CancellationError()
|
||||
}
|
||||
try await group.next()
|
||||
group.cancelAll()
|
||||
}
|
||||
}
|
||||
|
||||
func didConnectToPeer(_ peerID: PeerID) {}
|
||||
func didDisconnectFromPeer(_ peerID: PeerID) {}
|
||||
func didUpdatePeerList(_ peers: [PeerID]) {}
|
||||
@@ -209,9 +324,6 @@ extension FragmentationTests {
|
||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
|
||||
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {}
|
||||
func didUpdateBluetoothState(_ state: CBManagerState) {}
|
||||
func didReceivePublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date, messageID: String?) {
|
||||
publicMessages.append((peerID, nickname, content))
|
||||
}
|
||||
func didReceiveRegionalPublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,567 @@
|
||||
//
|
||||
// GeohashPresenceTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// Tests for the Geohash Presence (Kind 20001) feature.
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
import Combine
|
||||
@testable import bitchat
|
||||
|
||||
// MARK: - NostrProtocol Presence Event Tests
|
||||
|
||||
struct NostrProtocolPresenceTests {
|
||||
|
||||
@Test func createGeohashPresenceEvent_hasCorrectKind() throws {
|
||||
let identity = try makeTestIdentity()
|
||||
let event = try NostrProtocol.createGeohashPresenceEvent(
|
||||
geohash: "u4pruydq",
|
||||
senderIdentity: identity
|
||||
)
|
||||
|
||||
#expect(event.kind == NostrProtocol.EventKind.geohashPresence.rawValue)
|
||||
#expect(event.kind == 20001)
|
||||
}
|
||||
|
||||
@Test func createGeohashPresenceEvent_hasEmptyContent() throws {
|
||||
let identity = try makeTestIdentity()
|
||||
let event = try NostrProtocol.createGeohashPresenceEvent(
|
||||
geohash: "u4pruydq",
|
||||
senderIdentity: identity
|
||||
)
|
||||
|
||||
#expect(event.content == "")
|
||||
}
|
||||
|
||||
@Test func createGeohashPresenceEvent_hasOnlyGeohashTag() throws {
|
||||
let identity = try makeTestIdentity()
|
||||
let event = try NostrProtocol.createGeohashPresenceEvent(
|
||||
geohash: "u4pruydq",
|
||||
senderIdentity: identity
|
||||
)
|
||||
|
||||
// Should have exactly one tag: ["g", geohash]
|
||||
#expect(event.tags.count == 1)
|
||||
#expect(event.tags[0] == ["g", "u4pruydq"])
|
||||
}
|
||||
|
||||
@Test func createGeohashPresenceEvent_noNicknameTag() throws {
|
||||
let identity = try makeTestIdentity()
|
||||
let event = try NostrProtocol.createGeohashPresenceEvent(
|
||||
geohash: "u4pruydq",
|
||||
senderIdentity: identity
|
||||
)
|
||||
|
||||
// Should NOT contain nickname tag
|
||||
let hasNicknameTag = event.tags.contains { $0.first == "n" }
|
||||
#expect(!hasNicknameTag)
|
||||
}
|
||||
|
||||
@Test func createGeohashPresenceEvent_usesSenderPubkey() throws {
|
||||
let identity = try makeTestIdentity()
|
||||
let event = try NostrProtocol.createGeohashPresenceEvent(
|
||||
geohash: "u4pruydq",
|
||||
senderIdentity: identity
|
||||
)
|
||||
|
||||
#expect(event.pubkey == identity.publicKeyHex)
|
||||
}
|
||||
|
||||
@Test func createGeohashPresenceEvent_isSigned() throws {
|
||||
let identity = try makeTestIdentity()
|
||||
let event = try NostrProtocol.createGeohashPresenceEvent(
|
||||
geohash: "u4pruydq",
|
||||
senderIdentity: identity
|
||||
)
|
||||
|
||||
#expect(event.sig != nil && !event.sig!.isEmpty)
|
||||
#expect(!event.id.isEmpty)
|
||||
}
|
||||
|
||||
@Test func createGeohashPresenceEvent_differentGeohashes() throws {
|
||||
let identity = try makeTestIdentity()
|
||||
|
||||
let event1 = try NostrProtocol.createGeohashPresenceEvent(geohash: "87", senderIdentity: identity)
|
||||
let event2 = try NostrProtocol.createGeohashPresenceEvent(geohash: "87yw", senderIdentity: identity)
|
||||
let event3 = try NostrProtocol.createGeohashPresenceEvent(geohash: "87yw7", senderIdentity: identity)
|
||||
|
||||
#expect(event1.tags[0][1] == "87")
|
||||
#expect(event2.tags[0][1] == "87yw")
|
||||
#expect(event3.tags[0][1] == "87yw7")
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
private func makeTestIdentity() throws -> NostrIdentity {
|
||||
// Generate a fresh test identity
|
||||
return try NostrIdentity.generate()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NostrFilter Presence Tests
|
||||
|
||||
struct NostrFilterPresenceTests {
|
||||
|
||||
@Test func geohashEphemeral_includesBothKinds() {
|
||||
let filter = NostrFilter.geohashEphemeral("u4pruydq")
|
||||
|
||||
#expect(filter.kinds?.contains(20000) == true)
|
||||
#expect(filter.kinds?.contains(20001) == true)
|
||||
}
|
||||
|
||||
@Test func geohashEphemeral_hasLimit1000() {
|
||||
let filter = NostrFilter.geohashEphemeral("u4pruydq")
|
||||
|
||||
#expect(filter.limit == 1000)
|
||||
}
|
||||
|
||||
@Test func geohashEphemeral_respectsSinceParameter() {
|
||||
let since = Date(timeIntervalSince1970: 1700000000)
|
||||
let filter = NostrFilter.geohashEphemeral("u4pruydq", since: since)
|
||||
|
||||
#expect(filter.since == 1700000000)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ChatViewModel Presence Handling Tests
|
||||
|
||||
@MainActor
|
||||
struct ChatViewModelPresenceHandlingTests {
|
||||
|
||||
@Test func handleNostrEvent_presenceUpdatesParticipantTracker() async {
|
||||
let (viewModel, _) = makeTestableViewModel()
|
||||
let geohash = "u4pruydq"
|
||||
|
||||
// Set up the channel
|
||||
viewModel.switchLocationChannel(to: .location(GeohashChannel(level: .city, geohash: geohash)))
|
||||
|
||||
// Create a presence event (kind 20001)
|
||||
var event = NostrEvent(
|
||||
pubkey: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||
createdAt: Date(),
|
||||
kind: .geohashPresence,
|
||||
tags: [["g", geohash]],
|
||||
content: ""
|
||||
)
|
||||
event.id = "presence_evt_1"
|
||||
event.sig = "sig"
|
||||
|
||||
// Handle the event
|
||||
viewModel.handleNostrEvent(event)
|
||||
|
||||
// Allow async processing
|
||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||
|
||||
// Participant should be recorded
|
||||
let count = viewModel.geohashParticipantCount(for: geohash)
|
||||
#expect(count >= 1)
|
||||
}
|
||||
|
||||
@Test func handleNostrEvent_presenceDoesNotAddToTimeline() async {
|
||||
let (viewModel, _) = makeTestableViewModel()
|
||||
let geohash = "u4pruydq"
|
||||
|
||||
viewModel.switchLocationChannel(to: .location(GeohashChannel(level: .city, geohash: geohash)))
|
||||
|
||||
let initialMessageCount = viewModel.messages.count
|
||||
|
||||
// Create a presence event (kind 20001)
|
||||
var event = NostrEvent(
|
||||
pubkey: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||
createdAt: Date(),
|
||||
kind: .geohashPresence,
|
||||
tags: [["g", geohash]],
|
||||
content: ""
|
||||
)
|
||||
event.id = "presence_evt_2"
|
||||
event.sig = "sig"
|
||||
|
||||
viewModel.handleNostrEvent(event)
|
||||
|
||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||
|
||||
// Message count should NOT increase
|
||||
#expect(viewModel.messages.count == initialMessageCount)
|
||||
}
|
||||
|
||||
@Test func handleNostrEvent_chatMessageUpdatesParticipant() async {
|
||||
let (viewModel, _) = makeTestableViewModel()
|
||||
let geohash = "u4pruydq"
|
||||
|
||||
viewModel.switchLocationChannel(to: .location(GeohashChannel(level: .city, geohash: geohash)))
|
||||
|
||||
// Create a chat event (kind 20000) - NOT presence
|
||||
var event = NostrEvent(
|
||||
pubkey: "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
||||
createdAt: Date(),
|
||||
kind: .ephemeralEvent,
|
||||
tags: [["g", geohash]],
|
||||
content: "Hello world"
|
||||
)
|
||||
event.id = "chat_evt_1"
|
||||
event.sig = "sig"
|
||||
|
||||
viewModel.handleNostrEvent(event)
|
||||
|
||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||
|
||||
// Chat messages should also update participant count (not just presence)
|
||||
let count = viewModel.geohashParticipantCount(for: geohash)
|
||||
#expect(count >= 1)
|
||||
}
|
||||
|
||||
@Test func presenceEvent_hasDifferentKindThanChat() {
|
||||
// Verify the two event kinds are distinct
|
||||
let presenceKind = NostrProtocol.EventKind.geohashPresence.rawValue
|
||||
let chatKind = NostrProtocol.EventKind.ephemeralEvent.rawValue
|
||||
|
||||
#expect(presenceKind != chatKind)
|
||||
#expect(presenceKind == 20001)
|
||||
#expect(chatKind == 20000)
|
||||
}
|
||||
|
||||
@Test func subscribeNostrEvent_acceptsPresenceKind() async {
|
||||
let (viewModel, _) = makeTestableViewModel()
|
||||
let geohash = "u4pruydq"
|
||||
|
||||
viewModel.switchLocationChannel(to: .location(GeohashChannel(level: .city, geohash: geohash)))
|
||||
|
||||
// Create presence event
|
||||
var event = NostrEvent(
|
||||
pubkey: "test1234test1234test1234test1234test1234test1234test1234test1234",
|
||||
createdAt: Date(),
|
||||
kind: .geohashPresence,
|
||||
tags: [["g", geohash]],
|
||||
content: ""
|
||||
)
|
||||
event.id = "subscribe_presence_evt"
|
||||
event.sig = "sig"
|
||||
|
||||
// subscribeNostrEvent should accept kind 20001
|
||||
viewModel.subscribeNostrEvent(event)
|
||||
|
||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||
|
||||
// Should record participant
|
||||
let count = viewModel.geohashParticipantCount(for: geohash)
|
||||
#expect(count >= 1)
|
||||
}
|
||||
|
||||
@Test func subscribeNostrEvent_presenceForNonActiveGeohash() async {
|
||||
let (viewModel, _) = makeTestableViewModel()
|
||||
let activeGeohash = "u4pruydq"
|
||||
let otherGeohash = "87yw7"
|
||||
|
||||
viewModel.switchLocationChannel(to: .location(GeohashChannel(level: .city, geohash: activeGeohash)))
|
||||
|
||||
// Create presence event for a DIFFERENT geohash
|
||||
var event = NostrEvent(
|
||||
pubkey: "other1234other1234other1234other1234other1234other1234other1234",
|
||||
createdAt: Date(),
|
||||
kind: .geohashPresence,
|
||||
tags: [["g", otherGeohash]],
|
||||
content: ""
|
||||
)
|
||||
event.id = "other_geohash_presence"
|
||||
event.sig = "sig"
|
||||
|
||||
// Use subscribeNostrEvent with geohash parameter
|
||||
viewModel.subscribeNostrEvent(event, gh: otherGeohash)
|
||||
|
||||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||||
|
||||
// Should record for the other geohash
|
||||
let count = viewModel.geohashParticipantCount(for: otherGeohash)
|
||||
#expect(count >= 1)
|
||||
}
|
||||
|
||||
// MARK: - Test Helper
|
||||
|
||||
private func makeTestableViewModel() -> (viewModel: ChatViewModel, transport: MockTransport) {
|
||||
let keychain = MockKeychain()
|
||||
let keychainHelper = MockKeychainHelper()
|
||||
let idBridge = NostrIdentityBridge(keychain: keychainHelper)
|
||||
let identityManager = MockIdentityManager(keychain)
|
||||
let transport = MockTransport()
|
||||
|
||||
let viewModel = ChatViewModel(
|
||||
keychain: keychain,
|
||||
idBridge: idBridge,
|
||||
identityManager: identityManager,
|
||||
transport: transport
|
||||
)
|
||||
|
||||
return (viewModel, transport)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Presence Privacy Tests
|
||||
|
||||
struct GeohashPresencePrivacyTests {
|
||||
|
||||
@Test func allowedPrecisions_onlyLowPrecision() {
|
||||
// The allowed precisions for presence broadcasting should be:
|
||||
// Region (2), Province (4), City (5)
|
||||
// NOT Neighborhood (6), Block (7), Building (8+)
|
||||
|
||||
let regionPrecision = GeohashChannelLevel.region.precision
|
||||
let provincePrecision = GeohashChannelLevel.province.precision
|
||||
let cityPrecision = GeohashChannelLevel.city.precision
|
||||
let neighborhoodPrecision = GeohashChannelLevel.neighborhood.precision
|
||||
let blockPrecision = GeohashChannelLevel.block.precision
|
||||
let buildingPrecision = GeohashChannelLevel.building.precision
|
||||
|
||||
#expect(regionPrecision == 2)
|
||||
#expect(provincePrecision == 4)
|
||||
#expect(cityPrecision == 5)
|
||||
#expect(neighborhoodPrecision == 6)
|
||||
#expect(blockPrecision == 7)
|
||||
#expect(buildingPrecision == 8)
|
||||
|
||||
// High precision channels should NOT receive presence broadcasts
|
||||
#expect(neighborhoodPrecision > 5)
|
||||
#expect(blockPrecision > 5)
|
||||
#expect(buildingPrecision > 5)
|
||||
}
|
||||
|
||||
@Test func geohashLengthDeterminesPrecision() {
|
||||
// Verify geohash length maps to expected precision
|
||||
#expect("87".count == GeohashChannelLevel.region.precision)
|
||||
#expect("87yw".count == GeohashChannelLevel.province.precision)
|
||||
#expect("87yw7".count == GeohashChannelLevel.city.precision)
|
||||
#expect("87yw7t".count == GeohashChannelLevel.neighborhood.precision)
|
||||
#expect("87yw7tc".count == GeohashChannelLevel.block.precision)
|
||||
#expect("87yw7tcx".count == GeohashChannelLevel.building.precision)
|
||||
}
|
||||
|
||||
@Test func highPrecisionGeohash_isPrivacySensitive() {
|
||||
// Helper to check if a geohash is "high precision" (privacy sensitive)
|
||||
func isHighPrecision(_ geohash: String) -> Bool {
|
||||
geohash.count >= 6
|
||||
}
|
||||
|
||||
// Low precision - OK to broadcast presence
|
||||
#expect(!isHighPrecision("87")) // region
|
||||
#expect(!isHighPrecision("87yw")) // province
|
||||
#expect(!isHighPrecision("87yw7")) // city
|
||||
|
||||
// High precision - should NOT broadcast presence
|
||||
#expect(isHighPrecision("87yw7t")) // neighborhood
|
||||
#expect(isHighPrecision("87yw7tc")) // block
|
||||
#expect(isHighPrecision("87yw7tcx")) // building
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Display Logic Tests
|
||||
|
||||
struct LocationChannelsDisplayLogicTests {
|
||||
|
||||
@Test func displayLogic_highPrecisionZeroCount_showsUnknown() {
|
||||
// Test the logic that determines "?" vs actual count
|
||||
// High precision + count 0 = "?"
|
||||
|
||||
let shouldShowUnknown = shouldShowUnknownCount(
|
||||
level: .neighborhood,
|
||||
count: 0
|
||||
)
|
||||
#expect(shouldShowUnknown)
|
||||
}
|
||||
|
||||
@Test func displayLogic_highPrecisionNonZeroCount_showsActual() {
|
||||
// High precision + count > 0 = show actual
|
||||
let shouldShowUnknown = shouldShowUnknownCount(
|
||||
level: .neighborhood,
|
||||
count: 5
|
||||
)
|
||||
#expect(!shouldShowUnknown)
|
||||
}
|
||||
|
||||
@Test func displayLogic_lowPrecisionZeroCount_showsActual() {
|
||||
// Low precision + count 0 = show "0" (not "?")
|
||||
let shouldShowUnknown = shouldShowUnknownCount(
|
||||
level: .city,
|
||||
count: 0
|
||||
)
|
||||
#expect(!shouldShowUnknown)
|
||||
}
|
||||
|
||||
@Test func displayLogic_lowPrecisionNonZeroCount_showsActual() {
|
||||
// Low precision + count > 0 = show actual
|
||||
let shouldShowUnknown = shouldShowUnknownCount(
|
||||
level: .region,
|
||||
count: 10
|
||||
)
|
||||
#expect(!shouldShowUnknown)
|
||||
}
|
||||
|
||||
@Test func displayLogic_allHighPrecisionLevels() {
|
||||
// All high precision levels with 0 should show "?"
|
||||
let highPrecisionLevels: [GeohashChannelLevel] = [.neighborhood, .block, .building]
|
||||
|
||||
for level in highPrecisionLevels {
|
||||
let shouldShowUnknown = shouldShowUnknownCount(level: level, count: 0)
|
||||
#expect(shouldShowUnknown, "Level \(level) with count 0 should show unknown")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func displayLogic_allLowPrecisionLevels() {
|
||||
// All low precision levels with 0 should show actual count
|
||||
let lowPrecisionLevels: [GeohashChannelLevel] = [.region, .province, .city]
|
||||
|
||||
for level in lowPrecisionLevels {
|
||||
let shouldShowUnknown = shouldShowUnknownCount(level: level, count: 0)
|
||||
#expect(!shouldShowUnknown, "Level \(level) with count 0 should show actual count")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func displayLogic_bookmarkHighPrecision() {
|
||||
// Bookmarks use geohash length to determine precision
|
||||
#expect(shouldShowUnknownForBookmark(geohash: "87yw7t", count: 0)) // len 6
|
||||
#expect(shouldShowUnknownForBookmark(geohash: "87yw7tc", count: 0)) // len 7
|
||||
#expect(shouldShowUnknownForBookmark(geohash: "87yw7tcx", count: 0)) // len 8
|
||||
}
|
||||
|
||||
@Test func displayLogic_bookmarkLowPrecision() {
|
||||
#expect(!shouldShowUnknownForBookmark(geohash: "87", count: 0)) // len 2
|
||||
#expect(!shouldShowUnknownForBookmark(geohash: "87yw", count: 0)) // len 4
|
||||
#expect(!shouldShowUnknownForBookmark(geohash: "87yw7", count: 0)) // len 5
|
||||
}
|
||||
|
||||
// MARK: - Helpers (mirror the logic from LocationChannelsSheet)
|
||||
|
||||
private func shouldShowUnknownCount(level: GeohashChannelLevel, count: Int) -> Bool {
|
||||
let isHighPrecision = (level == .neighborhood || level == .block || level == .building)
|
||||
return isHighPrecision && count == 0
|
||||
}
|
||||
|
||||
private func shouldShowUnknownForBookmark(geohash: String, count: Int) -> Bool {
|
||||
let isHighPrecision = (geohash.count >= 6)
|
||||
return isHighPrecision && count == 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Event Kind Tests
|
||||
|
||||
struct NostrEventKindTests {
|
||||
|
||||
@Test func eventKind_geohashPresence_is20001() {
|
||||
#expect(NostrProtocol.EventKind.geohashPresence.rawValue == 20001)
|
||||
}
|
||||
|
||||
@Test func eventKind_ephemeralEvent_is20000() {
|
||||
#expect(NostrProtocol.EventKind.ephemeralEvent.rawValue == 20000)
|
||||
}
|
||||
|
||||
@Test func eventKind_presenceIsEphemeral() {
|
||||
// Both 20000 and 20001 are in the ephemeral range (20000-29999)
|
||||
let presenceKind = NostrProtocol.EventKind.geohashPresence.rawValue
|
||||
let chatKind = NostrProtocol.EventKind.ephemeralEvent.rawValue
|
||||
|
||||
#expect(presenceKind >= 20000 && presenceKind < 30000)
|
||||
#expect(chatKind >= 20000 && chatKind < 30000)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Participant Tracker Presence Integration Tests
|
||||
|
||||
@MainActor
|
||||
struct ParticipantTrackerPresenceTests {
|
||||
|
||||
@Test func recordParticipant_fromPresenceEvent_countsParticipant() async {
|
||||
let tracker = GeohashParticipantTracker()
|
||||
let context = PresenceTestParticipantContext()
|
||||
tracker.configure(context: context)
|
||||
|
||||
let geohash = "87yw7"
|
||||
tracker.setActiveGeohash(geohash)
|
||||
|
||||
// Simulate recording from a presence event
|
||||
tracker.recordParticipant(pubkeyHex: "presence_user_1")
|
||||
|
||||
#expect(tracker.participantCount(for: geohash) == 1)
|
||||
}
|
||||
|
||||
@Test func recordParticipant_multiplePresenceEvents_countsUnique() async {
|
||||
let tracker = GeohashParticipantTracker()
|
||||
let context = PresenceTestParticipantContext()
|
||||
tracker.configure(context: context)
|
||||
|
||||
let geohash = "87yw7"
|
||||
tracker.setActiveGeohash(geohash)
|
||||
|
||||
// Multiple presence events from same user = 1 participant
|
||||
tracker.recordParticipant(pubkeyHex: "user_a")
|
||||
tracker.recordParticipant(pubkeyHex: "user_a")
|
||||
tracker.recordParticipant(pubkeyHex: "user_a")
|
||||
|
||||
#expect(tracker.participantCount(for: geohash) == 1)
|
||||
|
||||
// Different user = 2 participants
|
||||
tracker.recordParticipant(pubkeyHex: "user_b")
|
||||
|
||||
#expect(tracker.participantCount(for: geohash) == 2)
|
||||
}
|
||||
|
||||
@Test func recordParticipant_nonActiveGeohash_stillCounts() async {
|
||||
let tracker = GeohashParticipantTracker()
|
||||
let context = PresenceTestParticipantContext()
|
||||
tracker.configure(context: context)
|
||||
|
||||
// Active geohash is different from where we're recording
|
||||
tracker.setActiveGeohash("active_gh")
|
||||
|
||||
// Record to a non-active geohash (like when sampling nearby channels)
|
||||
tracker.recordParticipant(pubkeyHex: "nearby_user", geohash: "other_gh")
|
||||
|
||||
#expect(tracker.participantCount(for: "other_gh") == 1)
|
||||
#expect(tracker.participantCount(for: "active_gh") == 0)
|
||||
}
|
||||
|
||||
@Test func objectWillChange_firesOnNonActiveGeohashUpdate() async {
|
||||
let tracker = GeohashParticipantTracker()
|
||||
let context = PresenceTestParticipantContext()
|
||||
tracker.configure(context: context)
|
||||
|
||||
tracker.setActiveGeohash("active_gh")
|
||||
|
||||
var changeCount = 0
|
||||
let cancellable = tracker.objectWillChange.sink { _ in
|
||||
changeCount += 1
|
||||
}
|
||||
|
||||
// Record to non-active geohash
|
||||
tracker.recordParticipant(pubkeyHex: "user1", geohash: "other_gh")
|
||||
|
||||
// Should fire objectWillChange even for non-active geohash
|
||||
#expect(changeCount >= 1)
|
||||
|
||||
_ = cancellable // Keep alive
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mock for Participant Context (Presence Tests)
|
||||
|
||||
@MainActor
|
||||
private final class PresenceTestParticipantContext: GeohashParticipantContext {
|
||||
var blockedPubkeys: Set<String> = []
|
||||
var nicknameMap: [String: String] = [:]
|
||||
var selfPubkey: String?
|
||||
|
||||
func displayNameForPubkey(_ pubkeyHex: String) -> String {
|
||||
let suffix = String(pubkeyHex.suffix(4))
|
||||
if let s = selfPubkey, pubkeyHex.lowercased() == s.lowercased() {
|
||||
return "me#\(suffix)"
|
||||
}
|
||||
if let nick = nicknameMap[pubkeyHex.lowercased()] {
|
||||
return "\(nick)#\(suffix)"
|
||||
}
|
||||
return "anon#\(suffix)"
|
||||
}
|
||||
|
||||
func isBlocked(_ pubkeyHexLowercased: String) -> Bool {
|
||||
blockedPubkeys.contains(pubkeyHexLowercased.lowercased())
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,7 @@ struct InputValidatorTests {
|
||||
}
|
||||
|
||||
// MARK: - Timestamp Validation Tests
|
||||
// BCH-01-011: Window reduced from ±1 hour to ±5 minutes
|
||||
|
||||
@Test func currentTimestampIsValid() throws {
|
||||
let now = Date()
|
||||
@@ -138,31 +139,48 @@ struct InputValidatorTests {
|
||||
#expect(result == true)
|
||||
}
|
||||
|
||||
@Test func timestampWithinOneHourIsValid() throws {
|
||||
@Test func timestampWithinFiveMinutesIsValid() throws {
|
||||
// 2 minutes ago should be valid (within 5-minute window)
|
||||
let twoMinutesAgo = Date().addingTimeInterval(-2 * 60)
|
||||
let result = InputValidator.validateTimestamp(twoMinutesAgo)
|
||||
#expect(result == true)
|
||||
}
|
||||
|
||||
@Test func timestampThirtyMinutesAgoIsInvalid() throws {
|
||||
// BCH-01-011: 30 minutes is now outside the 5-minute window
|
||||
let thirtyMinutesAgo = Date().addingTimeInterval(-30 * 60)
|
||||
let result = InputValidator.validateTimestamp(thirtyMinutesAgo)
|
||||
#expect(result == true)
|
||||
}
|
||||
|
||||
@Test func timestampTwoHoursAgoIsInvalid() throws {
|
||||
let twoHoursAgo = Date().addingTimeInterval(-2 * 3600)
|
||||
let result = InputValidator.validateTimestamp(twoHoursAgo)
|
||||
#expect(result == false)
|
||||
}
|
||||
|
||||
@Test func timestampTwoHoursInFutureIsInvalid() throws {
|
||||
let twoHoursFromNow = Date().addingTimeInterval(2 * 3600)
|
||||
let result = InputValidator.validateTimestamp(twoHoursFromNow)
|
||||
@Test func timestampTenMinutesAgoIsInvalid() throws {
|
||||
// 10 minutes is outside the 5-minute window
|
||||
let tenMinutesAgo = Date().addingTimeInterval(-10 * 60)
|
||||
let result = InputValidator.validateTimestamp(tenMinutesAgo)
|
||||
#expect(result == false)
|
||||
}
|
||||
|
||||
@Test func timestampAtOneHourBoundaryIsValid() throws {
|
||||
// Just slightly within the one-hour window
|
||||
let almostOneHourAgo = Date().addingTimeInterval(-3599)
|
||||
let result = InputValidator.validateTimestamp(almostOneHourAgo)
|
||||
@Test func timestampTenMinutesInFutureIsInvalid() throws {
|
||||
// 10 minutes in future is outside the 5-minute window
|
||||
let tenMinutesFromNow = Date().addingTimeInterval(10 * 60)
|
||||
let result = InputValidator.validateTimestamp(tenMinutesFromNow)
|
||||
#expect(result == false)
|
||||
}
|
||||
|
||||
@Test func timestampAtFiveMinuteBoundaryIsValid() throws {
|
||||
// Just slightly within the five-minute window (299 seconds)
|
||||
let almostFiveMinutesAgo = Date().addingTimeInterval(-299)
|
||||
let result = InputValidator.validateTimestamp(almostFiveMinutesAgo)
|
||||
#expect(result == true)
|
||||
}
|
||||
|
||||
@Test func timestampJustOutsideFiveMinuteWindowIsInvalid() throws {
|
||||
// Just outside the five-minute window (301 seconds)
|
||||
let justOverFiveMinutesAgo = Date().addingTimeInterval(-301)
|
||||
let result = InputValidator.validateTimestamp(justOverFiveMinutesAgo)
|
||||
#expect(result == false)
|
||||
}
|
||||
|
||||
// MARK: - Edge Cases
|
||||
|
||||
@Test func singleCharacterStringIsAccepted() throws {
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
//
|
||||
// KeychainErrorHandlingTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
// BCH-01-009: Tests for proper keychain error classification and handling
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import bitchat
|
||||
|
||||
struct KeychainErrorHandlingTests {
|
||||
|
||||
// MARK: - Error Classification Tests
|
||||
|
||||
@Test func keychainReadResult_successIsNotRecoverable() throws {
|
||||
let result = KeychainReadResult.success(Data([1, 2, 3]))
|
||||
#expect(result.isRecoverableError == false)
|
||||
}
|
||||
|
||||
@Test func keychainReadResult_itemNotFoundIsNotRecoverable() throws {
|
||||
let result = KeychainReadResult.itemNotFound
|
||||
#expect(result.isRecoverableError == false)
|
||||
}
|
||||
|
||||
@Test func keychainReadResult_deviceLockedIsRecoverable() throws {
|
||||
let result = KeychainReadResult.deviceLocked
|
||||
#expect(result.isRecoverableError == true)
|
||||
}
|
||||
|
||||
@Test func keychainReadResult_authenticationFailedIsRecoverable() throws {
|
||||
let result = KeychainReadResult.authenticationFailed
|
||||
#expect(result.isRecoverableError == true)
|
||||
}
|
||||
|
||||
@Test func keychainReadResult_accessDeniedIsNotRecoverable() throws {
|
||||
let result = KeychainReadResult.accessDenied
|
||||
#expect(result.isRecoverableError == false)
|
||||
}
|
||||
|
||||
@Test func keychainSaveResult_successIsNotRecoverable() throws {
|
||||
let result = KeychainSaveResult.success
|
||||
#expect(result.isRecoverableError == false)
|
||||
}
|
||||
|
||||
@Test func keychainSaveResult_duplicateItemIsRecoverable() throws {
|
||||
let result = KeychainSaveResult.duplicateItem
|
||||
#expect(result.isRecoverableError == true)
|
||||
}
|
||||
|
||||
@Test func keychainSaveResult_deviceLockedIsRecoverable() throws {
|
||||
let result = KeychainSaveResult.deviceLocked
|
||||
#expect(result.isRecoverableError == true)
|
||||
}
|
||||
|
||||
@Test func keychainSaveResult_storageFullIsNotRecoverable() throws {
|
||||
let result = KeychainSaveResult.storageFull
|
||||
#expect(result.isRecoverableError == false)
|
||||
}
|
||||
|
||||
// MARK: - Mock Keychain Error Simulation Tests
|
||||
|
||||
@Test func mockKeychain_canSimulateReadErrors() throws {
|
||||
let keychain = MockKeychain()
|
||||
|
||||
// Simulate access denied error
|
||||
keychain.simulatedReadError = .accessDenied
|
||||
let result = keychain.getIdentityKeyWithResult(forKey: "testKey")
|
||||
|
||||
switch result {
|
||||
case .accessDenied:
|
||||
// Expected
|
||||
break
|
||||
default:
|
||||
throw KeychainTestError("Expected accessDenied, got \(result)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func mockKeychain_canSimulateSaveErrors() throws {
|
||||
let keychain = MockKeychain()
|
||||
|
||||
// Simulate storage full error
|
||||
keychain.simulatedSaveError = .storageFull
|
||||
let result = keychain.saveIdentityKeyWithResult(Data([1, 2, 3]), forKey: "testKey")
|
||||
|
||||
switch result {
|
||||
case .storageFull:
|
||||
// Expected
|
||||
break
|
||||
default:
|
||||
throw KeychainTestError("Expected storageFull, got \(result)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func mockKeychain_returnsItemNotFoundForMissingKey() throws {
|
||||
let keychain = MockKeychain()
|
||||
let result = keychain.getIdentityKeyWithResult(forKey: "nonExistentKey")
|
||||
|
||||
switch result {
|
||||
case .itemNotFound:
|
||||
// Expected
|
||||
break
|
||||
default:
|
||||
throw KeychainTestError("Expected itemNotFound, got \(result)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func mockKeychain_returnsSuccessForExistingKey() throws {
|
||||
let keychain = MockKeychain()
|
||||
let testData = Data([1, 2, 3, 4, 5])
|
||||
|
||||
// First save the key
|
||||
_ = keychain.saveIdentityKey(testData, forKey: "existingKey")
|
||||
|
||||
// Now read it back
|
||||
let result = keychain.getIdentityKeyWithResult(forKey: "existingKey")
|
||||
|
||||
switch result {
|
||||
case .success(let data):
|
||||
#expect(data == testData)
|
||||
default:
|
||||
throw KeychainTestError("Expected success, got \(result)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func mockKeychain_saveWithResultStoresData() throws {
|
||||
let keychain = MockKeychain()
|
||||
let testData = Data([10, 20, 30])
|
||||
|
||||
let saveResult = keychain.saveIdentityKeyWithResult(testData, forKey: "newKey")
|
||||
|
||||
switch saveResult {
|
||||
case .success:
|
||||
// Verify data was stored
|
||||
let readResult = keychain.getIdentityKeyWithResult(forKey: "newKey")
|
||||
switch readResult {
|
||||
case .success(let data):
|
||||
#expect(data == testData)
|
||||
default:
|
||||
throw KeychainTestError("Expected to read back saved data")
|
||||
}
|
||||
default:
|
||||
throw KeychainTestError("Expected save success, got \(saveResult)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NoiseEncryptionService Integration Tests
|
||||
|
||||
@Test func noiseEncryptionService_generatesNewIdentityWhenMissing() throws {
|
||||
let keychain = MockKeychain()
|
||||
|
||||
// Create service with empty keychain - should generate new identity
|
||||
let service = NoiseEncryptionService(keychain: keychain)
|
||||
|
||||
// Should have generated and saved keys
|
||||
#expect(service.getStaticPublicKeyData().count == 32)
|
||||
#expect(service.getSigningPublicKeyData().count == 32)
|
||||
|
||||
// Keys should be persisted
|
||||
let noiseKeyResult = keychain.getIdentityKeyWithResult(forKey: "noiseStaticKey")
|
||||
switch noiseKeyResult {
|
||||
case .success:
|
||||
// Expected - key was saved
|
||||
break
|
||||
default:
|
||||
throw KeychainTestError("Expected noise key to be saved")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func noiseEncryptionService_loadsExistingIdentity() throws {
|
||||
let keychain = MockKeychain()
|
||||
|
||||
// Create first service to generate identity
|
||||
let service1 = NoiseEncryptionService(keychain: keychain)
|
||||
let originalPublicKey = service1.getStaticPublicKeyData()
|
||||
let originalSigningKey = service1.getSigningPublicKeyData()
|
||||
|
||||
// Create second service - should load same identity
|
||||
let service2 = NoiseEncryptionService(keychain: keychain)
|
||||
|
||||
#expect(service2.getStaticPublicKeyData() == originalPublicKey)
|
||||
#expect(service2.getSigningPublicKeyData() == originalSigningKey)
|
||||
}
|
||||
|
||||
@Test func noiseEncryptionService_handlesAccessDeniedGracefully() throws {
|
||||
let keychain = MockKeychain()
|
||||
keychain.simulatedReadError = .accessDenied
|
||||
|
||||
// Service should still initialize with ephemeral key
|
||||
let service = NoiseEncryptionService(keychain: keychain)
|
||||
|
||||
// Should have an identity (ephemeral)
|
||||
#expect(service.getStaticPublicKeyData().count == 32)
|
||||
#expect(service.getSigningPublicKeyData().count == 32)
|
||||
}
|
||||
|
||||
@Test func noiseEncryptionService_handlesDeviceLockedGracefully() throws {
|
||||
let keychain = MockKeychain()
|
||||
keychain.simulatedReadError = .deviceLocked
|
||||
|
||||
// Service should still initialize with ephemeral key
|
||||
let service = NoiseEncryptionService(keychain: keychain)
|
||||
|
||||
// Should have an identity (ephemeral)
|
||||
#expect(service.getStaticPublicKeyData().count == 32)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper error type for tests
|
||||
private struct KeychainTestError: Error, CustomStringConvertible {
|
||||
let message: String
|
||||
init(_ message: String) { self.message = message }
|
||||
var description: String { message }
|
||||
}
|
||||
@@ -13,6 +13,10 @@ final class MockKeychain: KeychainManagerProtocol {
|
||||
private var storage: [String: Data] = [:]
|
||||
private var serviceStorage: [String: [String: Data]] = [:]
|
||||
|
||||
// BCH-01-009: Configurable error simulation for testing
|
||||
var simulatedReadError: KeychainReadResult?
|
||||
var simulatedSaveError: KeychainSaveResult?
|
||||
|
||||
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
|
||||
storage[key] = keyData
|
||||
return true
|
||||
@@ -45,6 +49,25 @@ final class MockKeychain: KeychainManagerProtocol {
|
||||
storage["identity_noiseStaticKey"] != nil
|
||||
}
|
||||
|
||||
// BCH-01-009: New methods with proper error classification
|
||||
func getIdentityKeyWithResult(forKey key: String) -> KeychainReadResult {
|
||||
if let simulated = simulatedReadError {
|
||||
return simulated
|
||||
}
|
||||
if let data = storage[key] {
|
||||
return .success(data)
|
||||
}
|
||||
return .itemNotFound
|
||||
}
|
||||
|
||||
func saveIdentityKeyWithResult(_ keyData: Data, forKey key: String) -> KeychainSaveResult {
|
||||
if let simulated = simulatedSaveError {
|
||||
return simulated
|
||||
}
|
||||
storage[key] = keyData
|
||||
return .success
|
||||
}
|
||||
|
||||
// MARK: - Generic Data Storage (consolidated from KeychainHelper)
|
||||
|
||||
func save(key: String, data: Data, service: String, accessible: CFString?) {
|
||||
@@ -76,6 +99,10 @@ final class TrackingMockKeychain: KeychainManagerProtocol {
|
||||
private var _secureClearDataCallCount = 0
|
||||
private var _secureClearStringCallCount = 0
|
||||
|
||||
// BCH-01-009: Configurable error simulation for testing
|
||||
var simulatedReadError: KeychainReadResult?
|
||||
var simulatedSaveError: KeychainSaveResult?
|
||||
|
||||
var secureClearDataCallCount: Int {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
@@ -137,6 +164,25 @@ final class TrackingMockKeychain: KeychainManagerProtocol {
|
||||
storage["identity_noiseStaticKey"] != nil
|
||||
}
|
||||
|
||||
// BCH-01-009: New methods with proper error classification
|
||||
func getIdentityKeyWithResult(forKey key: String) -> KeychainReadResult {
|
||||
if let simulated = simulatedReadError {
|
||||
return simulated
|
||||
}
|
||||
if let data = storage[key] {
|
||||
return .success(data)
|
||||
}
|
||||
return .itemNotFound
|
||||
}
|
||||
|
||||
func saveIdentityKeyWithResult(_ keyData: Data, forKey key: String) -> KeychainSaveResult {
|
||||
if let simulated = simulatedSaveError {
|
||||
return simulated
|
||||
}
|
||||
storage[key] = keyData
|
||||
return .success
|
||||
}
|
||||
|
||||
func save(key: String, data: Data, service: String, accessible: CFString?) {
|
||||
if serviceStorage[service] == nil {
|
||||
serviceStorage[service] = [:]
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// NotificationBlockingTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
// BCH-01-012: Tests for notification blocking feature
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import bitchat
|
||||
|
||||
struct NotificationBlockingTests {
|
||||
|
||||
// MARK: - Nostr Blocking Tests
|
||||
|
||||
@Test("isNostrBlocked returns true for blocked pubkeys")
|
||||
func isNostrBlocked_returnsTrueForBlockedPubkey() {
|
||||
let keychain = MockKeychain()
|
||||
let manager = MockIdentityManager(keychain)
|
||||
|
||||
let testPubkey = "abc123def456".lowercased()
|
||||
|
||||
// Initially not blocked
|
||||
#expect(manager.isNostrBlocked(pubkeyHexLowercased: testPubkey) == false)
|
||||
|
||||
// Block the pubkey
|
||||
manager.setNostrBlocked(testPubkey, isBlocked: true)
|
||||
|
||||
// Now should be blocked
|
||||
#expect(manager.isNostrBlocked(pubkeyHexLowercased: testPubkey) == true)
|
||||
|
||||
// Unblock
|
||||
manager.setNostrBlocked(testPubkey, isBlocked: false)
|
||||
#expect(manager.isNostrBlocked(pubkeyHexLowercased: testPubkey) == false)
|
||||
}
|
||||
|
||||
@Test("isBlocked returns true for blocked fingerprints")
|
||||
func isBlocked_returnsTrueForBlockedFingerprint() {
|
||||
let keychain = MockKeychain()
|
||||
let manager = MockIdentityManager(keychain)
|
||||
|
||||
let testFingerprint = "fingerprint123"
|
||||
|
||||
// Initially not blocked
|
||||
#expect(manager.isBlocked(fingerprint: testFingerprint) == false)
|
||||
|
||||
// Block the fingerprint
|
||||
manager.setBlocked(testFingerprint, isBlocked: true)
|
||||
|
||||
// Now should be blocked
|
||||
#expect(manager.isBlocked(fingerprint: testFingerprint) == true)
|
||||
|
||||
// Unblock
|
||||
manager.setBlocked(testFingerprint, isBlocked: false)
|
||||
#expect(manager.isBlocked(fingerprint: testFingerprint) == false)
|
||||
}
|
||||
|
||||
@Test("getBlockedNostrPubkeys returns all blocked pubkeys")
|
||||
func getBlockedNostrPubkeys_returnsAllBlocked() {
|
||||
let keychain = MockKeychain()
|
||||
let manager = MockIdentityManager(keychain)
|
||||
|
||||
let pubkey1 = "pubkey1".lowercased()
|
||||
let pubkey2 = "pubkey2".lowercased()
|
||||
let pubkey3 = "pubkey3".lowercased()
|
||||
|
||||
manager.setNostrBlocked(pubkey1, isBlocked: true)
|
||||
manager.setNostrBlocked(pubkey2, isBlocked: true)
|
||||
manager.setNostrBlocked(pubkey3, isBlocked: true)
|
||||
|
||||
let blocked = manager.getBlockedNostrPubkeys()
|
||||
|
||||
#expect(blocked.count == 3)
|
||||
#expect(blocked.contains(pubkey1))
|
||||
#expect(blocked.contains(pubkey2))
|
||||
#expect(blocked.contains(pubkey3))
|
||||
}
|
||||
|
||||
// MARK: - Message Blocking Tests
|
||||
|
||||
@Test("BitchatMessage with blocked sender is identified")
|
||||
func bitchatMessage_blockedSenderIdentified() {
|
||||
let keychain = MockKeychain()
|
||||
let manager = MockIdentityManager(keychain)
|
||||
|
||||
let blockedFingerprint = "blocked_fingerprint_123"
|
||||
manager.setBlocked(blockedFingerprint, isBlocked: true)
|
||||
|
||||
#expect(manager.isBlocked(fingerprint: blockedFingerprint) == true)
|
||||
}
|
||||
|
||||
@Test("Case insensitive blocking for Nostr pubkeys")
|
||||
func nostrBlocking_caseInsensitive() {
|
||||
let keychain = MockKeychain()
|
||||
let manager = MockIdentityManager(keychain)
|
||||
|
||||
let pubkeyLower = "abc123def456"
|
||||
|
||||
// Block lowercase
|
||||
manager.setNostrBlocked(pubkeyLower, isBlocked: true)
|
||||
|
||||
// Check lowercase is blocked
|
||||
#expect(manager.isNostrBlocked(pubkeyHexLowercased: pubkeyLower) == true)
|
||||
|
||||
// Note: The API expects lowercased input, so callers must normalize
|
||||
// This test verifies the contract that pubkeys should be lowercased before checking
|
||||
// The fix in ChatViewModel+Nostr.swift normalizes via event.pubkey.lowercased()
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,8 @@ struct BinaryProtocolTests {
|
||||
#expect(decodedPacket.signature == TestConstants.testSignature)
|
||||
}
|
||||
|
||||
// MARK: - Source-Based Routing Tests (v2 only)
|
||||
|
||||
@Test func packetWithRouteRoundTrip() throws {
|
||||
let route: [Data] = [
|
||||
try #require(Data(hexString: "0102030405060708")),
|
||||
@@ -62,6 +64,7 @@ struct BinaryProtocolTests {
|
||||
try #require(Data(hexString: "2122232425262728"))
|
||||
]
|
||||
|
||||
// Route is only supported for v2+ packets
|
||||
var packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: route[0],
|
||||
@@ -69,7 +72,8 @@ struct BinaryProtocolTests {
|
||||
timestamp: 1_720_000_000_000,
|
||||
payload: Data("route-test".utf8),
|
||||
signature: nil,
|
||||
ttl: 6
|
||||
ttl: 6,
|
||||
version: 2
|
||||
)
|
||||
packet.route = route
|
||||
|
||||
@@ -78,6 +82,7 @@ struct BinaryProtocolTests {
|
||||
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) != 0)
|
||||
|
||||
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with route")
|
||||
#expect(decoded.version == 2)
|
||||
let decodedRoute = try #require(decoded.route)
|
||||
#expect(decodedRoute.count == route.count)
|
||||
for (expected, actual) in zip(route, decodedRoute) {
|
||||
@@ -90,6 +95,7 @@ struct BinaryProtocolTests {
|
||||
let destination = try #require(Data(hexString: "8899aabbccddeeff"))
|
||||
let shortHop = Data([0xAA, 0xBB, 0xCC])
|
||||
|
||||
// Route is only supported for v2+ packets
|
||||
var packet = BitchatPacket(
|
||||
type: 0x02,
|
||||
senderID: sender,
|
||||
@@ -97,7 +103,8 @@ struct BinaryProtocolTests {
|
||||
timestamp: 1_730_000_000_000,
|
||||
payload: Data("pad-test".utf8),
|
||||
signature: nil,
|
||||
ttl: 5
|
||||
ttl: 5,
|
||||
version: 2
|
||||
)
|
||||
packet.route = [shortHop, destination]
|
||||
|
||||
@@ -117,6 +124,7 @@ struct BinaryProtocolTests {
|
||||
try #require(Data(hexString: "0202020202020202"))
|
||||
]
|
||||
let repeatedString = String(repeating: "compress-me", count: 150)
|
||||
// Route is only supported for v2+ packets
|
||||
var packet = BitchatPacket(
|
||||
type: 0x03,
|
||||
senderID: route[0],
|
||||
@@ -124,7 +132,8 @@ struct BinaryProtocolTests {
|
||||
timestamp: 1_740_000_000_000,
|
||||
payload: Data(repeatedString.utf8),
|
||||
signature: nil,
|
||||
ttl: 7
|
||||
ttl: 7,
|
||||
version: 2
|
||||
)
|
||||
packet.route = route
|
||||
|
||||
@@ -135,6 +144,146 @@ struct BinaryProtocolTests {
|
||||
#expect(decodedRoute == route)
|
||||
}
|
||||
|
||||
@Test func v1PacketIgnoresRouteOnEncode() throws {
|
||||
// v1 packets should NOT include route even if route is set on the packet object
|
||||
let route: [Data] = [
|
||||
try #require(Data(hexString: "0102030405060708")),
|
||||
try #require(Data(hexString: "1112131415161718"))
|
||||
]
|
||||
|
||||
var packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: route[0],
|
||||
recipientID: route.last,
|
||||
timestamp: 1_720_000_000_000,
|
||||
payload: Data("v1-no-route".utf8),
|
||||
signature: nil,
|
||||
ttl: 6
|
||||
// version defaults to 1 (v1 packet)
|
||||
)
|
||||
packet.route = route // route is set but should be ignored for v1
|
||||
|
||||
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode v1 packet")
|
||||
|
||||
// HAS_ROUTE flag should NOT be set for v1 packets
|
||||
let flagsByte = encoded[BinaryProtocol.Offsets.flags]
|
||||
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) == 0, "v1 packet should not have HAS_ROUTE flag set")
|
||||
|
||||
// Decoded packet should have no route
|
||||
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode v1 packet")
|
||||
#expect(decoded.version == 1)
|
||||
#expect(decoded.route == nil, "v1 packet should decode with nil route")
|
||||
#expect(decoded.payload == Data("v1-no-route".utf8))
|
||||
}
|
||||
|
||||
@Test func v2PacketIncludesRouteOnEncode() throws {
|
||||
// v2 packets SHOULD include route when route is set
|
||||
let route: [Data] = [
|
||||
try #require(Data(hexString: "0102030405060708")),
|
||||
try #require(Data(hexString: "1112131415161718"))
|
||||
]
|
||||
|
||||
var packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: route[0],
|
||||
recipientID: route.last,
|
||||
timestamp: 1_720_000_000_000,
|
||||
payload: Data("v2-with-route".utf8),
|
||||
signature: nil,
|
||||
ttl: 6,
|
||||
version: 2
|
||||
)
|
||||
packet.route = route
|
||||
|
||||
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode v2 packet")
|
||||
|
||||
// HAS_ROUTE flag SHOULD be set for v2 packets with route
|
||||
let flagsByte = encoded[BinaryProtocol.Offsets.flags]
|
||||
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) != 0, "v2 packet should have HAS_ROUTE flag set")
|
||||
|
||||
// Decoded packet should have route
|
||||
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode v2 packet")
|
||||
#expect(decoded.version == 2)
|
||||
let decodedRoute = try #require(decoded.route, "v2 packet should decode with route")
|
||||
#expect(decodedRoute.count == route.count)
|
||||
#expect(decoded.payload == Data("v2-with-route".utf8))
|
||||
}
|
||||
|
||||
@Test func v2PacketWithoutRouteDecodesCorrectly() throws {
|
||||
// v2 packet without route should still work
|
||||
let sender = try #require(Data(hexString: "0011223344556677"))
|
||||
let recipient = try #require(Data(hexString: "8899aabbccddeeff"))
|
||||
|
||||
let packet = BitchatPacket(
|
||||
type: 0x02,
|
||||
senderID: sender,
|
||||
recipientID: recipient,
|
||||
timestamp: 1_750_000_000_000,
|
||||
payload: Data("v2-no-route".utf8),
|
||||
signature: nil,
|
||||
ttl: 5,
|
||||
version: 2
|
||||
)
|
||||
// route is nil by default
|
||||
|
||||
let encoded = try #require(BinaryProtocol.encode(packet), "Failed to encode v2 packet without route")
|
||||
|
||||
// HAS_ROUTE flag should NOT be set when no route
|
||||
let flagsByte = encoded[BinaryProtocol.Offsets.flags]
|
||||
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) == 0, "v2 packet without route should not have HAS_ROUTE flag")
|
||||
|
||||
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode v2 packet without route")
|
||||
#expect(decoded.version == 2)
|
||||
#expect(decoded.route == nil)
|
||||
#expect(decoded.payload == Data("v2-no-route".utf8))
|
||||
}
|
||||
|
||||
@Test func v1AndV2PayloadLengthDifference() throws {
|
||||
// Verify that payloadLength does NOT include route bytes
|
||||
// by comparing encoded sizes
|
||||
let route: [Data] = [
|
||||
try #require(Data(hexString: "0102030405060708"))
|
||||
]
|
||||
let payloadData = Data("test-payload".utf8)
|
||||
|
||||
// v1 packet (route ignored)
|
||||
var v1Packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: route[0],
|
||||
recipientID: nil,
|
||||
timestamp: 1_720_000_000_000,
|
||||
payload: payloadData,
|
||||
signature: nil,
|
||||
ttl: 6
|
||||
// version defaults to 1
|
||||
)
|
||||
v1Packet.route = route // will be ignored for v1
|
||||
|
||||
// v2 packet with same payload but route included
|
||||
var v2Packet = BitchatPacket(
|
||||
type: 0x01,
|
||||
senderID: route[0],
|
||||
recipientID: nil,
|
||||
timestamp: 1_720_000_000_000,
|
||||
payload: payloadData,
|
||||
signature: nil,
|
||||
ttl: 6,
|
||||
version: 2
|
||||
)
|
||||
v2Packet.route = route
|
||||
|
||||
let v1Encoded = try #require(BinaryProtocol.encode(v1Packet, padding: false))
|
||||
let v2Encoded = try #require(BinaryProtocol.encode(v2Packet, padding: false))
|
||||
|
||||
// v2 should be larger by: 2 bytes (header length field difference) + 1 byte (route count) + 8 bytes (one hop)
|
||||
// Header: v1=14, v2=16 -> +2 bytes
|
||||
// Route: 1 + 8 = 9 bytes
|
||||
// Total expected difference: 11 bytes
|
||||
let expectedDiff = 2 + 1 + 8 // header diff + route count + one hop
|
||||
#expect(v2Encoded.count - v1Encoded.count == expectedDiff,
|
||||
"v2 packet should be \(expectedDiff) bytes larger than v1 (actual diff: \(v2Encoded.count - v1Encoded.count))")
|
||||
}
|
||||
|
||||
// MARK: - Compression Tests
|
||||
|
||||
@Test("Create a large, compressible payload above current threshold (2048B)")
|
||||
|
||||
@@ -20,9 +20,13 @@ struct MeshTopologyTrackerTests {
|
||||
let a = try hex("0102030405060708")
|
||||
let b = try hex("1112131415161718")
|
||||
|
||||
tracker.recordDirectLink(between: a, and: b)
|
||||
// Bidirectional announcement
|
||||
tracker.updateNeighbors(for: a, neighbors: [b])
|
||||
tracker.updateNeighbors(for: b, neighbors: [a])
|
||||
|
||||
let route = try #require(tracker.computeRoute(from: a, to: b))
|
||||
#expect(route == [a, b])
|
||||
// Direct connection returns empty route (no intermediate hops)
|
||||
#expect(route == [])
|
||||
}
|
||||
|
||||
@Test func multiHopRouteComputation() throws {
|
||||
@@ -32,41 +36,38 @@ struct MeshTopologyTrackerTests {
|
||||
let c = try hex("2021222324252627")
|
||||
let d = try hex("3031323334353637")
|
||||
|
||||
tracker.recordDirectLink(between: a, and: b)
|
||||
tracker.recordDirectLink(between: b, and: c)
|
||||
tracker.recordDirectLink(between: c, and: d)
|
||||
// Bidirectional announcements for A-B, B-C, C-D
|
||||
tracker.updateNeighbors(for: a, neighbors: [b])
|
||||
tracker.updateNeighbors(for: b, neighbors: [a, c])
|
||||
tracker.updateNeighbors(for: c, neighbors: [b, d])
|
||||
tracker.updateNeighbors(for: d, neighbors: [c])
|
||||
|
||||
let route = try #require(tracker.computeRoute(from: a, to: d))
|
||||
#expect(route == [a, b, c, d])
|
||||
// Route should only contain intermediate hops (b, c), excluding start (a) and end (d)
|
||||
#expect(route == [b, c])
|
||||
}
|
||||
|
||||
@Test func recordRouteAddsEdges() throws {
|
||||
let tracker = MeshTopologyTracker()
|
||||
var a = Data([0xAA, 0xBB, 0xCC])
|
||||
let b = try hex("4445464748494A4B")
|
||||
let c = try hex("5455565758595A5B")
|
||||
|
||||
tracker.recordRoute([a, b, c])
|
||||
|
||||
a.append(Data(repeating: 0, count: BinaryProtocol.senderIDSize - a.count))
|
||||
let route = try #require(tracker.computeRoute(from: a, to: c))
|
||||
#expect(route.first == a)
|
||||
#expect(route.last == c)
|
||||
}
|
||||
|
||||
@Test func removingDirectLinkBreaksRoute() throws {
|
||||
@Test func unconfirmedEdgeDoesNotRoute() throws {
|
||||
let tracker = MeshTopologyTracker()
|
||||
let a = try hex("0101010101010101")
|
||||
let b = try hex("0202020202020202")
|
||||
let c = try hex("0303030303030303")
|
||||
|
||||
tracker.recordDirectLink(between: a, and: b)
|
||||
tracker.recordDirectLink(between: b, and: c)
|
||||
let initialRoute = try #require(tracker.computeRoute(from: a, to: c))
|
||||
#expect(initialRoute == [a, b, c])
|
||||
// A announces B (confirmed)
|
||||
// B announces A, C (confirmed A-B, unconfirmed B-C)
|
||||
// C does NOT announce B
|
||||
tracker.updateNeighbors(for: a, neighbors: [b])
|
||||
tracker.updateNeighbors(for: b, neighbors: [a, c])
|
||||
// C is silent or announces empty
|
||||
|
||||
tracker.removeDirectLink(between: b, and: c)
|
||||
// Should NOT find route A->C because B->C is unconfirmed (C didn't announce B)
|
||||
#expect(tracker.computeRoute(from: a, to: c) == nil)
|
||||
|
||||
// Now C announces B
|
||||
tracker.updateNeighbors(for: c, neighbors: [b])
|
||||
// Should find route
|
||||
let route = try #require(tracker.computeRoute(from: a, to: c))
|
||||
#expect(route == [b])
|
||||
}
|
||||
|
||||
@Test func removingPeerClearsEdges() throws {
|
||||
@@ -75,12 +76,28 @@ struct MeshTopologyTrackerTests {
|
||||
let b = try hex("0A0B0C0D0E0F0001")
|
||||
let c = try hex("0011223344556677")
|
||||
|
||||
tracker.recordRoute([a, b, c])
|
||||
tracker.updateNeighbors(for: a, neighbors: [b])
|
||||
tracker.updateNeighbors(for: b, neighbors: [a, c])
|
||||
tracker.updateNeighbors(for: c, neighbors: [b])
|
||||
|
||||
let initialRoute = try #require(tracker.computeRoute(from: a, to: c))
|
||||
#expect(initialRoute == [a, b, c])
|
||||
#expect(initialRoute == [b])
|
||||
|
||||
tracker.removePeer(b)
|
||||
#expect(tracker.computeRoute(from: a, to: c) == nil)
|
||||
}
|
||||
|
||||
@Test func sameStartAndEndReturnsEmptyRoute() throws {
|
||||
let tracker = MeshTopologyTracker()
|
||||
let a = try hex("0102030405060708")
|
||||
let b = try hex("1112131415161718")
|
||||
|
||||
tracker.updateNeighbors(for: a, neighbors: [b])
|
||||
tracker.updateNeighbors(for: b, neighbors: [a])
|
||||
|
||||
// When start == end, route should be empty (no intermediate hops needed)
|
||||
let route = try #require(tracker.computeRoute(from: a, to: a))
|
||||
#expect(route == [])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// SubscriptionRateLimitTests.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 bitchat
|
||||
|
||||
/// Tests for BCH-01-004 fix: Rate-limiting subscription-triggered announces
|
||||
/// to prevent device enumeration attacks
|
||||
struct SubscriptionRateLimitTests {
|
||||
|
||||
@Test("Rate limit configuration values are sensible")
|
||||
func rateLimitConfigurationValues() {
|
||||
// Minimum interval should be at least 1 second to slow enumeration
|
||||
#expect(TransportConfig.bleSubscriptionRateLimitMinSeconds >= 1.0)
|
||||
|
||||
// Backoff factor should be > 1 for exponential backoff
|
||||
#expect(TransportConfig.bleSubscriptionRateLimitBackoffFactor > 1.0)
|
||||
|
||||
// Max backoff should be reasonable (not hours)
|
||||
#expect(TransportConfig.bleSubscriptionRateLimitMaxBackoffSeconds <= 60.0)
|
||||
#expect(TransportConfig.bleSubscriptionRateLimitMaxBackoffSeconds >= TransportConfig.bleSubscriptionRateLimitMinSeconds)
|
||||
|
||||
// Window should be long enough to track repeated attempts
|
||||
#expect(TransportConfig.bleSubscriptionRateLimitWindowSeconds >= 30.0)
|
||||
|
||||
// Max attempts before suppression should be > 1 to allow legitimate reconnects
|
||||
#expect(TransportConfig.bleSubscriptionRateLimitMaxAttempts >= 2)
|
||||
}
|
||||
|
||||
@Test("Exponential backoff calculation is correct")
|
||||
func exponentialBackoffCalculation() {
|
||||
let minInterval = TransportConfig.bleSubscriptionRateLimitMinSeconds
|
||||
let factor = TransportConfig.bleSubscriptionRateLimitBackoffFactor
|
||||
let maxBackoff = TransportConfig.bleSubscriptionRateLimitMaxBackoffSeconds
|
||||
|
||||
// Simulate backoff progression
|
||||
var currentBackoff = minInterval
|
||||
var iterations = 0
|
||||
let maxIterations = 10
|
||||
|
||||
while currentBackoff < maxBackoff && iterations < maxIterations {
|
||||
let nextBackoff = min(currentBackoff * factor, maxBackoff)
|
||||
#expect(nextBackoff >= currentBackoff, "Backoff should increase or stay at max")
|
||||
currentBackoff = nextBackoff
|
||||
iterations += 1
|
||||
}
|
||||
|
||||
// Should reach max within reasonable iterations
|
||||
#expect(iterations <= maxIterations, "Backoff should reach max within \(maxIterations) iterations")
|
||||
#expect(currentBackoff == maxBackoff, "Final backoff should equal max")
|
||||
}
|
||||
|
||||
@Test("Rate limiting would significantly slow enumeration attacks")
|
||||
func rateLimitingSlowsEnumeration() {
|
||||
// Without rate limiting: ~120 devices/minute (0.5 seconds per device)
|
||||
// With rate limiting: minimum interval enforced
|
||||
|
||||
let minInterval = TransportConfig.bleSubscriptionRateLimitMinSeconds
|
||||
let devicesPerMinuteWithRateLimit = 60.0 / minInterval
|
||||
|
||||
// Should be significantly slower than 120 devices/minute
|
||||
#expect(devicesPerMinuteWithRateLimit < 60, "Rate limiting should significantly slow enumeration")
|
||||
|
||||
// With 2-second minimum interval, max ~30 devices/minute per connection
|
||||
// And with backoff, repeated attempts are even slower
|
||||
#expect(devicesPerMinuteWithRateLimit <= 30, "With 2s minimum, should be <=30/min")
|
||||
}
|
||||
|
||||
@Test("Max attempts threshold prevents complete enumeration")
|
||||
func maxAttemptsThresholdPreventsEnumeration() {
|
||||
let maxAttempts = TransportConfig.bleSubscriptionRateLimitMaxAttempts
|
||||
let minInterval = TransportConfig.bleSubscriptionRateLimitMinSeconds
|
||||
|
||||
// After max attempts within window, announces are suppressed entirely
|
||||
// This means an attacker gets at most maxAttempts announces per window
|
||||
#expect(maxAttempts >= 2, "Should allow at least 2 attempts for legitimate reconnects")
|
||||
#expect(maxAttempts <= 10, "Should cap attempts to prevent enumeration")
|
||||
|
||||
// With 5 attempts max and 2s minimum interval, attacker gets limited info
|
||||
let maxAnnounces = maxAttempts
|
||||
#expect(maxAnnounces <= 10, "Max announces per window should be limited")
|
||||
}
|
||||
}
|
||||
@@ -426,4 +426,70 @@ struct PeerIDTests {
|
||||
#expect(!PeerID(str: "nostr:\(hex65)").isValid)
|
||||
#expect(!PeerID(str: "nostr_\(hex65)").isValid)
|
||||
}
|
||||
|
||||
// MARK: - File Transfer PeerID Normalization
|
||||
// These tests verify the fix for asymmetric voice/media delivery (BCH-01-XXX)
|
||||
// The bug occurred when selectedPrivateChatPeer was migrated to 64-hex stable key
|
||||
// but the receiver expected SHA256-derived 16-hex format
|
||||
|
||||
@Test func fileTransfer_toShortNormalizesNoiseKeyToFingerprint() {
|
||||
// Given: A 64-hex Noise public key (what selectedPrivateChatPeer becomes after session)
|
||||
let noiseKey = Data(repeating: 0xAB, count: 32)
|
||||
let stableKeyPeerID = PeerID(hexData: noiseKey) // 64-hex
|
||||
|
||||
// When: Convert to short form (what sendFilePrivate should do)
|
||||
let shortID = stableKeyPeerID.toShort()
|
||||
|
||||
// Then: Should be 16-hex SHA256 fingerprint (matching myPeerID format)
|
||||
let expected = noiseKey.sha256Fingerprint().prefix(16)
|
||||
#expect(shortID.id == String(expected))
|
||||
#expect(shortID.id.count == 16)
|
||||
}
|
||||
|
||||
@Test func fileTransfer_shortIDMatchesMyPeerIDFormat() {
|
||||
// Given: A receiver's myPeerID is SHA256-derived (from refreshPeerIdentity)
|
||||
let noiseKey = Data(repeating: 0xCD, count: 32)
|
||||
let myPeerID = PeerID(publicKey: noiseKey) // SHA256-derived 16-hex
|
||||
|
||||
// When: Sender uses toShort() on 64-hex stable key
|
||||
let senderStableKey = PeerID(hexData: noiseKey) // 64-hex
|
||||
let recipientData = Data(hexString: senderStableKey.toShort().id)!
|
||||
let receivedRecipientID = PeerID(hexData: recipientData)
|
||||
|
||||
// Then: Should match receiver's myPeerID (file transfer accepted)
|
||||
#expect(receivedRecipientID == myPeerID)
|
||||
}
|
||||
|
||||
@Test func fileTransfer_truncatedRawKeyDoesNotMatchMyPeerID() {
|
||||
// This test demonstrates the bug we fixed
|
||||
// When 64-hex was truncated to first 8 bytes instead of using SHA256 fingerprint
|
||||
|
||||
// Given: Receiver's myPeerID is SHA256-derived
|
||||
let noiseKey = Data(repeating: 0xEF, count: 32)
|
||||
let myPeerID = PeerID(publicKey: noiseKey) // SHA256-derived 16-hex
|
||||
|
||||
// When: Truncate raw key (the OLD buggy behavior)
|
||||
let truncatedRaw = noiseKey.prefix(8) // First 8 bytes of raw key
|
||||
let wrongRecipientID = PeerID(hexData: truncatedRaw)
|
||||
|
||||
// Then: Should NOT match (demonstrates why fix was needed)
|
||||
#expect(wrongRecipientID != myPeerID)
|
||||
}
|
||||
|
||||
@Test func fileTransfer_shortIDProducesCorrect8ByteRoutingData() {
|
||||
// Verify the wire format is correct (8 bytes for BinaryProtocol)
|
||||
let noiseKey = Data(repeating: 0x12, count: 32)
|
||||
let stableKeyPeerID = PeerID(hexData: noiseKey)
|
||||
let shortID = stableKeyPeerID.toShort()
|
||||
|
||||
// routingData should be 8 bytes (16 hex chars -> 8 bytes)
|
||||
let routingData = shortID.routingData
|
||||
#expect(routingData != nil)
|
||||
#expect(routingData?.count == 8)
|
||||
|
||||
// And it should match SHA256 fingerprint first 8 bytes
|
||||
let expectedFingerprint = noiseKey.sha256Fingerprint()
|
||||
let expectedFirst8 = Data(hexString: String(expectedFingerprint.prefix(16)))
|
||||
#expect(routingData == expectedFirst8)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Geohash Presence Specification
|
||||
|
||||
## Overview
|
||||
|
||||
The Geohash Presence feature provides a mechanism to track online participants in geohash-based location channels. It uses a dedicated ephemeral Nostr event kind to broadcast "heartbeats," ensuring accurate and privacy-preserving online counts.
|
||||
|
||||
## Nostr Protocol
|
||||
|
||||
### Event Kind
|
||||
A new ephemeral event kind is defined for presence heartbeats:
|
||||
- **Kind:** `20001` (`GEOHASH_PRESENCE`)
|
||||
- **Type:** Ephemeral (not stored by relays long-term)
|
||||
|
||||
### Event Structure
|
||||
The presence event mimics the structure of a geohash chat message (Kind 20000) but without content or nickname metadata, to minimize overhead and focus purely on "liveness".
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 20001,
|
||||
"created_at": <timestamp>,
|
||||
"tags": [
|
||||
["g", "<geohash>"]
|
||||
],
|
||||
"content": "",
|
||||
"pubkey": "<geohash_derived_pubkey>",
|
||||
"id": "<event_id>",
|
||||
"sig": "<signature>"
|
||||
}
|
||||
```
|
||||
|
||||
* **`content`**: Must be empty string.
|
||||
* **`tags`**: Must include `["g", "<geohash>"]`. Should NOT include `["n", "<nickname>"]`.
|
||||
* **`pubkey`**: The ephemeral identity derived specifically for this geohash (same as used for chat messages).
|
||||
|
||||
## Client Behavior
|
||||
|
||||
### 1. Broadcasting Presence
|
||||
|
||||
Clients MUST broadcast a Kind 20001 presence event globally when the app is open, regardless of which screen the user is viewing.
|
||||
|
||||
* **Global Heartbeat:**
|
||||
* **Trigger:** Application start / initialization, or whenever location (available geohashes) changes.
|
||||
* **Frequency:** Randomized loop interval between **40s and 80s** (average 60s).
|
||||
* **Scope:** Sent to *all* geohash channels corresponding to the device's *current physical location*.
|
||||
* **Privacy Restriction:** Presence MUST ONLY be broadcast to low-precision geohash levels to protect user privacy. Specifically:
|
||||
* **Allowed:** `REGION` (precision 2), `PROVINCE` (precision 4), `CITY` (precision 5).
|
||||
* **Denied:** `NEIGHBORHOOD` (precision 6), `BLOCK` (precision 7), `BUILDING` (precision 8+).
|
||||
* **Decorrelation:** Individual broadcasts within a heartbeat loop must be separated by random delays (e.g., 2-5 seconds) to prevent temporal correlation of public keys across different geohash levels. The main loop delay is adjusted to maintain the target average cadence.
|
||||
|
||||
### 2. Subscribing to Presence
|
||||
|
||||
Clients must update their Nostr filters to listen for both chat and presence events on geohash channels.
|
||||
|
||||
* **Filter:**
|
||||
* `kinds`: `[20000, 20001]`
|
||||
* `#g`: `["<geohash>"]`
|
||||
|
||||
### 3. Participant Counting
|
||||
|
||||
The "online participants" count shown in the UI aggregates unique public keys from both presence heartbeats and active chat messages.
|
||||
|
||||
* **Logic:**
|
||||
* Maintain a map of `pubkey -> last_seen_timestamp` for each geohash.
|
||||
* Update `last_seen_timestamp` upon receiving a valid **Kind 20001 (Presence)** OR **Kind 20000 (Chat)** event.
|
||||
* A participant is considered "online" if their `last_seen_timestamp` is within the last **5 minutes**.
|
||||
|
||||
### 4. UI Presentation
|
||||
|
||||
The presentation of the participant count depends on the geohash precision level and data availability.
|
||||
|
||||
* **Standard Display:** For channels where presence is broadcast (Region, Province, City) OR any channel where at least one participant has been detected, show the exact count: `[N people]`.
|
||||
* **High-Precision Uncertainty:** For high-precision channels (Neighborhood, Block, Building) where:
|
||||
* Presence broadcasting is disabled (privacy restriction).
|
||||
* **AND** the detected participant count is `0`.
|
||||
* **Display:** `[? people]`
|
||||
* **Reasoning:** Since clients don't announce themselves in these channels, a count of "0" is misleading (people could be lurking).
|
||||
|
||||
### 5. Implementation Details (Android Reference)
|
||||
|
||||
* **`NostrKind.GEOHASH_PRESENCE`**: Added constant `20001`.
|
||||
* **`NostrProtocol.createGeohashPresenceEvent`**: Helper to generate the event.
|
||||
* **`GeohashViewModel`**:
|
||||
* `startGlobalPresenceHeartbeat()`: Coroutine that `collectLatest` on `LocationChannelManager.availableChannels`.
|
||||
* Implements randomized loop logic (40-80s) and per-broadcast random delays (2-5s).
|
||||
* Filters channels by `precision <= 5` before broadcasting.
|
||||
* **`GeohashMessageHandler`**:
|
||||
* Refactored `onEvent` to update participant counts for both Kind 20000 and 20001.
|
||||
* **`LocationChannelsSheet`**:
|
||||
* Implements the `[? people]` display logic for high-precision, zero-count channels.
|
||||
|
||||
## Benefits
|
||||
|
||||
* **Accuracy:** Counts reflect both active listeners (via heartbeats) and active speakers (via messages).
|
||||
* **Privacy:** High-precision location presence is NOT broadcast. Temporal correlation between different levels is obfuscated via random delays.
|
||||
* **Consistency:** "Online" status is maintained globally while the app is open.
|
||||
* **Transparency:** The UI correctly reflects uncertainty (`?`) when privacy rules prevent accurate passive counting.
|
||||
@@ -0,0 +1,146 @@
|
||||
# Source-Based Routing for BitChat Packets (v2)
|
||||
|
||||
This document specifies the Source-Based Routing extension (v2) for the BitChat protocol. This upgrade enables efficient unicast routing across the mesh by allowing senders to specify an explicit path of intermediate relays.
|
||||
|
||||
**Status:** Implemented in Android and iOS. Backward compatible (v1 clients ignore routing data).
|
||||
|
||||
---
|
||||
|
||||
## 1. Protocol Versioning & Layering
|
||||
|
||||
To support source routing and larger payloads, the packet format has been upgraded to **Version 2**.
|
||||
|
||||
* **Version 1 (Legacy):** 2-byte payload length limit. Ignores routing flags.
|
||||
* **Version 2 (Current):** 4-byte payload length limit. Supports Source Routing.
|
||||
|
||||
**Key Rule:** The `HAS_ROUTE (0x08)` flag is **only valid** if the packet `version >= 2`. Relays receiving a v1 packet must ignore this flag even if set.
|
||||
|
||||
---
|
||||
|
||||
## 2. Packet Structure Comparison
|
||||
|
||||
The following diagram illustrates the structural differences between a standard v1 packet and a source-routed v2 packet.
|
||||
|
||||
### V1 Packet (Legacy)
|
||||
```text
|
||||
+-------------------+---------------------------------------------------------+
|
||||
| Fixed Header (14) | Variable Sections |
|
||||
+-------------------+----------+-------------+------------------+-------------+
|
||||
| Ver: 1 (1B) | SenderID | RecipientID | Payload | Signature |
|
||||
| Type, TTL, etc. | (8B) | (8B) | (Length in Head) | (64B) |
|
||||
| Len: 2 Bytes | | (Optional) | | (Optional) |
|
||||
+-------------------+----------+-------------+------------------+-------------+
|
||||
```
|
||||
|
||||
### V2 Packet (Source Routed)
|
||||
```text
|
||||
+-------------------+-----------------------------------------------------------------------------+
|
||||
| Fixed Header (16) | Variable Sections |
|
||||
+-------------------+----------+-------------+-----------------------+------------------+-------------+
|
||||
| Ver: 2 (1B) | SenderID | RecipientID | SOURCE ROUTE | Payload | Signature |
|
||||
| Type, TTL, etc. | (8B) | (8B) | (Variable) | (Length in Head) | (64B) |
|
||||
| Len: 4 Bytes | | (Required*) | Only if HAS_ROUTE=1 | | (Optional) |
|
||||
+-------------------+----------+-------------+-----------------------+------------------+-------------+
|
||||
```
|
||||
|
||||
**(*) Note:** A `Route` can be attached to **any** packet type that has a `RecipientID` (flag `HAS_RECIPIENT` set).
|
||||
|
||||
### Fixed Header Differences
|
||||
|
||||
| Field | Size (v1) | Size (v2) | Description |
|
||||
|---|---|---|---|
|
||||
| **Version** | 1 byte | 1 byte | `0x01` vs `0x02` |
|
||||
| **Payload Length** | **2 bytes** | **4 bytes** | `UInt32` in v2 to support large files. **Excludes** route/IDs/sig. |
|
||||
| **Total Size** | **14 bytes** | **16 bytes** | V2 header is 2 bytes larger. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Source Route Specification
|
||||
|
||||
The `Source Route` field is a variable-length list of **intermediate hops** that the packet must traverse.
|
||||
|
||||
* **Location:** Immediately follows `RecipientID`.
|
||||
* **Structure:**
|
||||
* `Count` (1 byte): Number of intermediate hops (`N`).
|
||||
* `Hops` (`N * 8` bytes): Sequence of Peer IDs.
|
||||
|
||||
### Intermediate Hops Only
|
||||
The route list MUST contain **only** the intermediate relays between the sender and the recipient.
|
||||
* **DO NOT** include the `SenderID` (it is already in the packet).
|
||||
* **DO NOT** include the `RecipientID` (it is already in the packet).
|
||||
|
||||
**Example:**
|
||||
Topology: `Alice (Sender) -> Bob -> Charlie -> Dave (Recipient)`
|
||||
* Packet `SenderID`: Alice
|
||||
* Packet `RecipientID`: Dave
|
||||
* Packet `Route`: `[Bob, Charlie]` (Count = 2)
|
||||
|
||||
---
|
||||
|
||||
## 4. Topology Discovery (Gossip)
|
||||
|
||||
To calculate routes, nodes need a view of the network topology. This is achieved via a **Neighbor List** extension to the `IdentityAnnouncement` packet.
|
||||
|
||||
The `ANNOUNCE` packet payload now consists of a sequence of TLVs. The standard identity information is followed by an optional Gossip TLV.
|
||||
|
||||
* **Mechanism:** Appended to the `IdentityAnnouncement` payload.
|
||||
* **New TLV Type:** `0x04` (Direct Neighbors).
|
||||
* **Content:** A list of Peer IDs that the announcing node is directly connected to.
|
||||
|
||||
**TLV Structure (Type 0x04):**
|
||||
```text
|
||||
[Type: 0x04] [Length: 1B] [NeighborID1 (8B)] [NeighborID2 (8B)] ...
|
||||
```
|
||||
The `Length` field indicates the total size of the neighbor IDs in bytes (N * 8). There is no explicit count field.
|
||||
|
||||
Nodes receiving this TLV update their local mesh graph, linking the sender to the listed neighbors.
|
||||
|
||||
### Edge Verification (Two-Way Handshake)
|
||||
|
||||
To prevent spoofing and routing through stale connections, the Mesh Graph service implements a strict two-way handshake verification:
|
||||
|
||||
* **Unconfirmed Edge:** If Peer A announces Peer B, but Peer B does *not* announce Peer A, the connection is treated as **unconfirmed**. Unconfirmed edges are visualized as dotted lines in debug tools but are **excluded** from route calculations.
|
||||
* **Confirmed Edge:** An edge is only valid for routing when **both** peers explicitly announce each other in their neighbor lists. This ensures that the connection is bidirectional and currently active from both perspectives.
|
||||
|
||||
---
|
||||
|
||||
## 5. Fragmentation & Source Routing
|
||||
|
||||
When a large source-routed packet (e.g., File Transfer) exceeds the MTU and requires fragmentation:
|
||||
|
||||
1. **Version Inheritance:** All fragments MUST be marked as **Version 2**.
|
||||
2. **Route Inheritance:** All fragments MUST contain the **exact same Route field** as the parent packet.
|
||||
|
||||
**Why?** If fragments were sent as v1 packets or without routes, they would fall back to flooding, negating the bandwidth benefits of source routing for large data transfers.
|
||||
|
||||
---
|
||||
|
||||
## 6. Security & Signing
|
||||
|
||||
Source routing is fully secured by the existing Ed25519 signature scheme.
|
||||
|
||||
* **Scope:** The signature covers the **entire packet structure** (Header + Sender + Recipient + Route + Payload).
|
||||
* **Verification:** The receiver verifies the signature against the `SenderID`'s public key.
|
||||
* **Integrity:** Any tampering with the route list by malicious relays will invalidate the signature, causing the packet to be dropped by the destination.
|
||||
|
||||
**Signature Input Construction:**
|
||||
Serialize the packet exactly as transmitted, but temporarily set `TTL = 0` and remove the `Signature` bytes.
|
||||
|
||||
---
|
||||
|
||||
## 7. Relay Logic
|
||||
|
||||
When a node receives a packet **not** addressed to itself:
|
||||
|
||||
1. **Check Route:**
|
||||
* Is `Version >= 2`?
|
||||
* Is `HAS_ROUTE` flag set?
|
||||
* Is the route list non-empty?
|
||||
2. **If YES (Source Routed):**
|
||||
* Find local Peer ID in the route list at index `i`.
|
||||
* **Next Hop:** The peer at `i + 1`.
|
||||
* **Last Hop:** If `i` is the last index, the Next Hop is the `RecipientID`.
|
||||
* **Action:** Attempt to unicast (`sendToPeer`) to the Next Hop.
|
||||
* **Fallback:** If the Next Hop is unreachable, **fall back to broadcast/flood** to ensure delivery.
|
||||
3. **If NO (Standard):**
|
||||
* Flood the packet to all connected neighbors (subject to TTL and probability rules).
|
||||
@@ -0,0 +1,3 @@
|
||||
/target/
|
||||
/.build/
|
||||
/.swiftpm/
|
||||
Generated
+5046
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["arti-bitchat"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = "symbols"
|
||||
+24
-18
@@ -6,11 +6,13 @@
|
||||
<array>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>tor-nolzma.framework/tor-nolzma</string>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>HeadersPath</key>
|
||||
<string>Headers</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>macos-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>tor-nolzma.framework</string>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
@@ -20,11 +22,29 @@
|
||||
</dict>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>tor-nolzma.framework/tor-nolzma</string>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>HeadersPath</key>
|
||||
<string>Headers</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>HeadersPath</key>
|
||||
<string>Headers</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64-simulator</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>tor-nolzma.framework</string>
|
||||
<string>libarti_bitchat.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
@@ -34,20 +54,6 @@
|
||||
<key>SupportedPlatformVariant</key>
|
||||
<string>simulator</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>BinaryPath</key>
|
||||
<string>tor-nolzma.framework/tor-nolzma</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>tor-nolzma.framework</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XFWK</string>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* # Arguments
|
||||
* * `data_dir` - Path to data directory for Tor state (C string)
|
||||
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if already running
|
||||
* * -2 if data_dir is invalid
|
||||
* * -3 if runtime initialization failed
|
||||
* * -4 if bootstrap failed
|
||||
*/
|
||||
int arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* # Returns
|
||||
* * 1 if running
|
||||
* * 0 if not running
|
||||
*/
|
||||
int arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*/
|
||||
int arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* # Arguments
|
||||
* * `buf` - Buffer to write the summary into
|
||||
* * `len` - Length of the buffer
|
||||
*
|
||||
* # Returns
|
||||
* * Number of bytes written (not including null terminator)
|
||||
* * -1 if buffer is null or too small
|
||||
*/
|
||||
int arti_bootstrap_summary(char *buf, int len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
* This is a hint; Arti may not fully support dormant mode yet.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_wake(void);
|
||||
|
||||
#endif /* ARTI_H */
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* # Arguments
|
||||
* * `data_dir` - Path to data directory for Tor state (C string)
|
||||
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if already running
|
||||
* * -2 if data_dir is invalid
|
||||
* * -3 if runtime initialization failed
|
||||
* * -4 if bootstrap failed
|
||||
*/
|
||||
int arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* # Returns
|
||||
* * 1 if running
|
||||
* * 0 if not running
|
||||
*/
|
||||
int arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*/
|
||||
int arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* # Arguments
|
||||
* * `buf` - Buffer to write the summary into
|
||||
* * `len` - Length of the buffer
|
||||
*
|
||||
* # Returns
|
||||
* * Number of bytes written (not including null terminator)
|
||||
* * -1 if buffer is null or too small
|
||||
*/
|
||||
int arti_bootstrap_summary(char *buf, int len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
* This is a hint; Arti may not fully support dormant mode yet.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_wake(void);
|
||||
|
||||
#endif /* ARTI_H */
|
||||
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* # Arguments
|
||||
* * `data_dir` - Path to data directory for Tor state (C string)
|
||||
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if already running
|
||||
* * -2 if data_dir is invalid
|
||||
* * -3 if runtime initialization failed
|
||||
* * -4 if bootstrap failed
|
||||
*/
|
||||
int arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* # Returns
|
||||
* * 1 if running
|
||||
* * 0 if not running
|
||||
*/
|
||||
int arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*/
|
||||
int arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* # Arguments
|
||||
* * `buf` - Buffer to write the summary into
|
||||
* * `len` - Length of the buffer
|
||||
*
|
||||
* # Returns
|
||||
* * Number of bytes written (not including null terminator)
|
||||
* * -1 if buffer is null or too small
|
||||
*/
|
||||
int arti_bootstrap_summary(char *buf, int len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
* This is a hint; Arti may not fully support dormant mode yet.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_wake(void);
|
||||
|
||||
#endif /* ARTI_H */
|
||||
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* # Arguments
|
||||
* * `data_dir` - Path to data directory for Tor state (C string)
|
||||
* * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if already running
|
||||
* * -2 if data_dir is invalid
|
||||
* * -3 if runtime initialization failed
|
||||
* * -4 if bootstrap failed
|
||||
*/
|
||||
int arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* # Returns
|
||||
* * 1 if running
|
||||
* * 0 if not running
|
||||
*/
|
||||
int arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*/
|
||||
int arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* # Arguments
|
||||
* * `buf` - Buffer to write the summary into
|
||||
* * `len` - Length of the buffer
|
||||
*
|
||||
* # Returns
|
||||
* * Number of bytes written (not including null terminator)
|
||||
* * -1 if buffer is null or too small
|
||||
*/
|
||||
int arti_bootstrap_summary(char *buf, int len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
* This is a hint; Arti may not fully support dormant mode yet.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* # Returns
|
||||
* * 0 on success
|
||||
* * -1 if not running
|
||||
*/
|
||||
int arti_wake(void);
|
||||
|
||||
#endif /* ARTI_H */
|
||||
@@ -0,0 +1,46 @@
|
||||
// swift-tools-version:5.9
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Tor", // Keep name "Tor" for drop-in compatibility
|
||||
platforms: [
|
||||
.iOS(.v16),
|
||||
.macOS(.v13),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "Tor",
|
||||
targets: ["Tor"]
|
||||
),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../BitLogger"),
|
||||
],
|
||||
targets: [
|
||||
// Main Swift target
|
||||
.target(
|
||||
name: "Tor",
|
||||
dependencies: [
|
||||
"arti",
|
||||
.product(name: "BitLogger", package: "BitLogger"),
|
||||
],
|
||||
path: "Sources",
|
||||
exclude: ["C"],
|
||||
sources: [
|
||||
"TorManager.swift",
|
||||
"TorURLSession.swift",
|
||||
"TorNotifications.swift",
|
||||
],
|
||||
linkerSettings: [
|
||||
.linkedLibrary("resolv"),
|
||||
.linkedLibrary("z"),
|
||||
.linkedLibrary("sqlite3"),
|
||||
]
|
||||
),
|
||||
// Binary framework containing the Rust static library
|
||||
.binaryTarget(
|
||||
name: "arti",
|
||||
path: "Frameworks/arti.xcframework"
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
// Empty shim file to satisfy SPM target requirements.
|
||||
// The actual implementation is in the Rust static library (arti.xcframework).
|
||||
// This file exists only to make SPM happy with a C target.
|
||||
@@ -0,0 +1,71 @@
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* @param data_dir Path to data directory for Tor state (C string)
|
||||
* @param socks_port Port for SOCKS5 proxy (e.g., 39050)
|
||||
* @return 0 on success, negative on error:
|
||||
* -1: already running
|
||||
* -2: invalid data_dir
|
||||
* -3: runtime initialization failed
|
||||
* -4: bootstrap failed
|
||||
*/
|
||||
int32_t arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* @return 1 if running, 0 if not running
|
||||
*/
|
||||
int32_t arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*
|
||||
* @return Progress percentage
|
||||
*/
|
||||
int32_t arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* @param buf Buffer to write the summary into
|
||||
* @param len Length of the buffer
|
||||
* @return Number of bytes written, -1 on error
|
||||
*/
|
||||
int32_t arti_bootstrap_summary(char *buf, int32_t len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_wake(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ARTI_H */
|
||||
@@ -0,0 +1,4 @@
|
||||
module ArtiC {
|
||||
header "arti.h"
|
||||
export *
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
import BitLogger
|
||||
import Foundation
|
||||
#if canImport(Network)
|
||||
import Network
|
||||
#endif
|
||||
|
||||
#if !canImport(Network)
|
||||
private final class NWPathMonitor {
|
||||
var pathUpdateHandler: ((Any) -> Void)?
|
||||
|
||||
func start(queue: DispatchQueue) {
|
||||
// Path monitoring is unavailable on this platform; nothing to do.
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// FFI declarations for Arti (Rust)
|
||||
@_silgen_name("arti_start")
|
||||
private func arti_start(_ dataDir: UnsafePointer<CChar>, _ socksPort: UInt16) -> Int32
|
||||
|
||||
@_silgen_name("arti_stop")
|
||||
private func arti_stop() -> Int32
|
||||
|
||||
@_silgen_name("arti_is_running")
|
||||
private func arti_is_running() -> Int32
|
||||
|
||||
@_silgen_name("arti_bootstrap_progress")
|
||||
private func arti_bootstrap_progress() -> Int32
|
||||
|
||||
@_silgen_name("arti_bootstrap_summary")
|
||||
private func arti_bootstrap_summary(_ buf: UnsafeMutablePointer<CChar>, _ len: Int32) -> Int32
|
||||
|
||||
@_silgen_name("arti_go_dormant")
|
||||
private func arti_go_dormant() -> Int32
|
||||
|
||||
@_silgen_name("arti_wake")
|
||||
private func arti_wake() -> Int32
|
||||
|
||||
/// Arti-based Tor integration for BitChat.
|
||||
/// - Boots a local Arti client and exposes a SOCKS5 proxy
|
||||
/// on 127.0.0.1:socksPort. All app networking should await readiness and
|
||||
/// route via this proxy. Fails closed by default when Tor is unavailable.
|
||||
@MainActor
|
||||
public final class TorManager: ObservableObject {
|
||||
public static let shared = TorManager()
|
||||
|
||||
// SOCKS endpoint where Arti listens
|
||||
let socksHost: String = "127.0.0.1"
|
||||
let socksPort: Int = 39050
|
||||
|
||||
// State
|
||||
@Published private(set) public var isReady: Bool = false
|
||||
@Published private(set) var isStarting: Bool = false
|
||||
@Published private(set) var lastError: Error?
|
||||
@Published private(set) var bootstrapProgress: Int = 0
|
||||
@Published private(set) var bootstrapSummary: String = ""
|
||||
|
||||
// Internal readiness trackers
|
||||
private var socksReady: Bool = false { didSet { recomputeReady() } }
|
||||
private var restarting: Bool = false
|
||||
|
||||
// Whether the app must enforce Tor for all connections (fail-closed).
|
||||
public var torEnforced: Bool {
|
||||
#if BITCHAT_DEV_ALLOW_CLEARNET
|
||||
return false
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}
|
||||
|
||||
// Returns true only when Tor is actually up (or dev fallback is compiled).
|
||||
var networkPermitted: Bool {
|
||||
if torEnforced { return isReady }
|
||||
return true
|
||||
}
|
||||
|
||||
private var didStart = false
|
||||
private var bootstrapMonitorStarted = false
|
||||
private var pathMonitor: NWPathMonitor?
|
||||
private var isAppForeground: Bool = true
|
||||
private var isDormant: Bool = false
|
||||
private var lastRestartAt: Date? = nil
|
||||
private var startedAt: Date? = nil // Tracks initial startup time for grace period
|
||||
private(set) var allowAutoStart: Bool = false
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
public func startIfNeeded() {
|
||||
guard allowAutoStart else { return }
|
||||
guard isAppForeground else { return }
|
||||
guard !didStart else { return }
|
||||
didStart = true
|
||||
isDormant = false
|
||||
isStarting = true
|
||||
startedAt = Date() // Track startup time for grace period
|
||||
SecureLogger.debug("TorManager: startIfNeeded() - startedAt set", category: .session)
|
||||
lastError = nil
|
||||
NotificationCenter.default.post(name: .TorWillStart, object: nil)
|
||||
ensureFilesystemLayout()
|
||||
startArti()
|
||||
startPathMonitorIfNeeded()
|
||||
}
|
||||
|
||||
public func setAppForeground(_ foreground: Bool) {
|
||||
isAppForeground = foreground
|
||||
}
|
||||
|
||||
public func isForeground() -> Bool { isAppForeground }
|
||||
|
||||
nonisolated
|
||||
public func awaitReady(timeout: TimeInterval = 25.0) async -> Bool {
|
||||
await MainActor.run {
|
||||
if self.isAppForeground { self.startIfNeeded() }
|
||||
}
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
if await MainActor.run(body: { self.networkPermitted }) { return true }
|
||||
while Date() < deadline {
|
||||
try? await Task.sleep(nanoseconds: 200_000_000)
|
||||
if await MainActor.run(body: { self.networkPermitted }) { return true }
|
||||
}
|
||||
return await MainActor.run(body: { self.networkPermitted })
|
||||
}
|
||||
|
||||
// MARK: - Filesystem
|
||||
|
||||
func dataDirectoryURL() -> URL? {
|
||||
do {
|
||||
let base = try FileManager.default.url(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: nil,
|
||||
create: true
|
||||
)
|
||||
let dir = base.appendingPathComponent("bitchat/arti", isDirectory: true)
|
||||
return dir
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func ensureFilesystemLayout() {
|
||||
guard let dir = dataDirectoryURL() else { return }
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
} catch {
|
||||
// Non-fatal; Arti will surface errors during start if paths are missing
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Arti Integration
|
||||
|
||||
private func startArti() {
|
||||
guard let dir = dataDirectoryURL()?.path else {
|
||||
isStarting = false
|
||||
lastError = NSError(domain: "TorManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "No data directory"])
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already running
|
||||
if arti_is_running() != 0 {
|
||||
SecureLogger.info("TorManager: Arti already running", category: .session)
|
||||
startBootstrapMonitor()
|
||||
return
|
||||
}
|
||||
|
||||
let result = dir.withCString { dptr in
|
||||
arti_start(dptr, UInt16(socksPort))
|
||||
}
|
||||
|
||||
if result != 0 {
|
||||
SecureLogger.error("TorManager: arti_start failed rc=\(result)", category: .session)
|
||||
isStarting = false
|
||||
lastError = NSError(domain: "TorManager", code: Int(result), userInfo: [NSLocalizedDescriptionKey: "Arti start failed"])
|
||||
return
|
||||
}
|
||||
|
||||
SecureLogger.info("TorManager: arti_start OK (SOCKS \(socksHost):\(socksPort))", category: .session)
|
||||
startBootstrapMonitor()
|
||||
|
||||
// Start SOCKS readiness probe
|
||||
Task.detached(priority: .userInitiated) { [weak self] in
|
||||
guard let self else { return }
|
||||
let ready = await self.waitForSocksReady(timeout: 60.0)
|
||||
await MainActor.run {
|
||||
self.socksReady = ready
|
||||
if ready {
|
||||
SecureLogger.info("TorManager: SOCKS ready at \(self.socksHost):\(self.socksPort)", category: .session)
|
||||
} else {
|
||||
self.lastError = NSError(domain: "TorManager", code: -14, userInfo: [NSLocalizedDescriptionKey: "SOCKS not reachable"])
|
||||
SecureLogger.error("TorManager: SOCKS not reachable (timeout)", category: .session)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func waitForSocksReady(timeout: TimeInterval) async -> Bool {
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
while Date() < deadline {
|
||||
if await probeSocksOnce() { return true }
|
||||
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func probeSocksOnce() async -> Bool {
|
||||
#if canImport(Network)
|
||||
await withCheckedContinuation { cont in
|
||||
let params = NWParameters.tcp
|
||||
let host = NWEndpoint.Host.ipv4(.loopback)
|
||||
guard let port = NWEndpoint.Port(rawValue: UInt16(socksPort)) else {
|
||||
cont.resume(returning: false)
|
||||
return
|
||||
}
|
||||
let endpoint = NWEndpoint.hostPort(host: host, port: port)
|
||||
let conn = NWConnection(to: endpoint, using: params)
|
||||
|
||||
var resumed = false
|
||||
let resumeOnce: (Bool) -> Void = { value in
|
||||
if !resumed {
|
||||
resumed = true
|
||||
cont.resume(returning: value)
|
||||
}
|
||||
}
|
||||
|
||||
conn.stateUpdateHandler = { state in
|
||||
switch state {
|
||||
case .ready:
|
||||
resumeOnce(true)
|
||||
conn.cancel()
|
||||
case .failed, .cancelled:
|
||||
resumeOnce(false)
|
||||
conn.cancel()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 1.0) {
|
||||
resumeOnce(false)
|
||||
conn.cancel()
|
||||
}
|
||||
|
||||
conn.start(queue: DispatchQueue.global(qos: .utility))
|
||||
}
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Bootstrap Monitoring
|
||||
|
||||
private func startBootstrapMonitor() {
|
||||
guard !bootstrapMonitorStarted else { return }
|
||||
bootstrapMonitorStarted = true
|
||||
Task.detached(priority: .utility) { [weak self] in
|
||||
await self?.bootstrapPollLoop()
|
||||
}
|
||||
}
|
||||
|
||||
private func bootstrapPollLoop() async {
|
||||
let deadline = Date().addingTimeInterval(75)
|
||||
while Date() < deadline {
|
||||
let progress = Int(arti_bootstrap_progress())
|
||||
let summary = getBootstrapSummary()
|
||||
|
||||
await MainActor.run {
|
||||
self.bootstrapProgress = progress
|
||||
self.bootstrapSummary = summary
|
||||
if progress >= 100 { self.isStarting = false }
|
||||
self.recomputeReady()
|
||||
}
|
||||
|
||||
if progress >= 100 { break }
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
private func getBootstrapSummary() -> String {
|
||||
var buf = [CChar](repeating: 0, count: 256)
|
||||
let len = arti_bootstrap_summary(&buf, Int32(buf.count))
|
||||
if len > 0 {
|
||||
return String(cString: buf)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// MARK: - Foreground/Background
|
||||
|
||||
public func ensureRunningOnForeground() {
|
||||
if !allowAutoStart { return }
|
||||
SecureLogger.debug("TorManager: ensureRunningOnForeground() started", category: .session)
|
||||
Task.detached(priority: .userInitiated) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let claimed: Bool = await MainActor.run {
|
||||
if self.isStarting || self.restarting { return false }
|
||||
self.restarting = true
|
||||
return true
|
||||
}
|
||||
if !claimed { return }
|
||||
|
||||
// Check if already ready
|
||||
let alreadyReady = await MainActor.run { self.isReady }
|
||||
if alreadyReady {
|
||||
await MainActor.run { self.restarting = false }
|
||||
return
|
||||
}
|
||||
|
||||
// Arti doesn't support dormant/wake (it's a no-op stub), so always do full restart
|
||||
await self.restartArti()
|
||||
await MainActor.run { self.restarting = false }
|
||||
}
|
||||
}
|
||||
|
||||
public func goDormantOnBackground() {
|
||||
// Arti doesn't support real dormant mode, so just mark as not ready.
|
||||
// iOS will suspend the runtime anyway. On foreground we do a full restart.
|
||||
// Clear isStarting so foreground recovery can proceed if bootstrap was interrupted.
|
||||
SecureLogger.debug("TorManager: goDormantOnBackground() called", category: .session)
|
||||
Task { @MainActor in
|
||||
self.isReady = false
|
||||
self.socksReady = false
|
||||
self.isStarting = false
|
||||
}
|
||||
}
|
||||
|
||||
public func shutdownCompletely() {
|
||||
SecureLogger.debug("TorManager: shutdownCompletely() called", category: .session)
|
||||
Task.detached { [weak self] in
|
||||
guard let self = self else { return }
|
||||
_ = arti_stop()
|
||||
|
||||
// Wait for shutdown
|
||||
var waited = 0
|
||||
while arti_is_running() != 0 && waited < 50 {
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
waited += 1
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
self.isDormant = false
|
||||
self.isReady = false
|
||||
self.socksReady = false
|
||||
self.bootstrapProgress = 0
|
||||
self.bootstrapSummary = ""
|
||||
self.isStarting = false
|
||||
self.didStart = false
|
||||
self.restarting = false
|
||||
self.bootstrapMonitorStarted = false
|
||||
// Note: Don't clear startedAt here - it will be set fresh on next startIfNeeded()
|
||||
// Clearing it here races with startup and defeats the grace period
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func restartArti() async {
|
||||
SecureLogger.debug("TorManager: restartArti() starting", category: .session)
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: .TorWillRestart, object: nil)
|
||||
self.isReady = false
|
||||
self.socksReady = false
|
||||
self.bootstrapProgress = 0
|
||||
self.bootstrapSummary = ""
|
||||
self.isStarting = true
|
||||
self.isDormant = false
|
||||
self.lastRestartAt = Date()
|
||||
}
|
||||
|
||||
_ = arti_stop()
|
||||
|
||||
// Wait for stop
|
||||
var waited = 0
|
||||
while arti_is_running() != 0 && waited < 40 {
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
waited += 1
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
self.bootstrapMonitorStarted = false
|
||||
self.didStart = false
|
||||
}
|
||||
|
||||
await MainActor.run { self.startIfNeeded() }
|
||||
}
|
||||
|
||||
private func recomputeReady() {
|
||||
let ready = socksReady && bootstrapProgress >= 100
|
||||
if ready != isReady {
|
||||
if !ready {
|
||||
SecureLogger.debug("TorManager: isReady -> false (socksReady=\(socksReady), bootstrap=\(bootstrapProgress))", category: .session)
|
||||
}
|
||||
isReady = ready
|
||||
if ready {
|
||||
NotificationCenter.default.post(name: .TorDidBecomeReady, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startPathMonitorIfNeeded() {
|
||||
#if canImport(Network)
|
||||
guard pathMonitor == nil else { return }
|
||||
let monitor = NWPathMonitor()
|
||||
pathMonitor = monitor
|
||||
let queue = DispatchQueue(label: "TorPathMonitor")
|
||||
monitor.pathUpdateHandler = { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
guard let self = self else { return }
|
||||
if self.isAppForeground {
|
||||
self.pokeTorOnPathChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
monitor.start(queue: queue)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func pokeTorOnPathChange() {
|
||||
// Skip if we recently restarted
|
||||
if let last = lastRestartAt, Date().timeIntervalSince(last) < 3.0 {
|
||||
SecureLogger.debug("TorManager: pokeTorOnPathChange() skipped - recent restart", category: .session)
|
||||
return
|
||||
}
|
||||
// Skip during initial startup grace period (15s) to avoid race conditions
|
||||
if let started = startedAt, Date().timeIntervalSince(started) < 15.0 {
|
||||
SecureLogger.debug("TorManager: pokeTorOnPathChange() skipped - startup grace period (\(Int(Date().timeIntervalSince(started)))s)", category: .session)
|
||||
return
|
||||
}
|
||||
if isStarting || restarting {
|
||||
SecureLogger.debug("TorManager: pokeTorOnPathChange() skipped - isStarting=\(isStarting) restarting=\(restarting)", category: .session)
|
||||
return
|
||||
}
|
||||
if isReady { return }
|
||||
SecureLogger.debug("TorManager: pokeTorOnPathChange() - Arti not ready, initiating recovery", category: .session)
|
||||
ensureRunningOnForeground()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Start policy configuration
|
||||
extension TorManager {
|
||||
@MainActor
|
||||
public func setAutoStartAllowed(_ allow: Bool) {
|
||||
allowAutoStart = allow
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func isAutoStartAllowed() -> Bool { allowAutoStart }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "arti-bitchat"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib"]
|
||||
|
||||
[dependencies]
|
||||
# Arti core - minimal features for client-only SOCKS proxy
|
||||
arti-client = { version = "0.38", default-features = false, features = [
|
||||
"tokio",
|
||||
"rustls",
|
||||
] }
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", default-features = false, features = [
|
||||
"rt-multi-thread",
|
||||
"net",
|
||||
"sync",
|
||||
"time",
|
||||
"macros",
|
||||
] }
|
||||
|
||||
# Tor runtime compatibility
|
||||
tor-rtcompat = { version = "0.38", default-features = false, features = ["tokio"] }
|
||||
|
||||
# FFI utilities
|
||||
libc = "0.2"
|
||||
once_cell = "1"
|
||||
|
||||
# Logging (minimal)
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@@ -0,0 +1,13 @@
|
||||
language = "C"
|
||||
include_guard = "ARTI_H"
|
||||
no_includes = true
|
||||
sys_includes = ["stdint.h", "stdbool.h"]
|
||||
|
||||
[export]
|
||||
include = ["arti_start", "arti_stop", "arti_is_running", "arti_bootstrap_progress", "arti_bootstrap_summary", "arti_go_dormant", "arti_wake"]
|
||||
|
||||
[fn]
|
||||
args = "Auto"
|
||||
|
||||
[parse]
|
||||
parse_deps = false
|
||||
@@ -0,0 +1,315 @@
|
||||
//! arti-bitchat: Minimal FFI wrapper around arti-client for BitChat
|
||||
//!
|
||||
//! Provides a C-compatible interface for embedding Arti (Rust Tor) in iOS/macOS apps.
|
||||
//! Exposes a SOCKS5 proxy on localhost that Swift code can route traffic through.
|
||||
|
||||
use std::ffi::{c_char, c_int, CStr};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use arti_client::TorClient;
|
||||
use once_cell::sync::OnceCell;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::oneshot;
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
|
||||
mod socks;
|
||||
|
||||
/// Global state for the Arti instance
|
||||
struct ArtiState {
|
||||
/// Tokio runtime (owned, single instance)
|
||||
runtime: Runtime,
|
||||
/// Shutdown signal sender
|
||||
shutdown_tx: Option<oneshot::Sender<()>>,
|
||||
/// TorClient handle for status queries
|
||||
client: Option<Arc<TorClient<PreferredRuntime>>>,
|
||||
}
|
||||
|
||||
static ARTI_STATE: OnceCell<Mutex<ArtiState>> = OnceCell::new();
|
||||
static BOOTSTRAP_PROGRESS: AtomicI32 = AtomicI32::new(0);
|
||||
static IS_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
static BOOTSTRAP_SUMMARY: Mutex<String> = Mutex::new(String::new());
|
||||
|
||||
/// Initialize the global state with a new runtime
|
||||
fn init_state() -> Result<(), &'static str> {
|
||||
ARTI_STATE.get_or_try_init(|| -> Result<Mutex<ArtiState>, &'static str> {
|
||||
let runtime = Runtime::new().map_err(|_| "Failed to create tokio runtime")?;
|
||||
Ok(Mutex::new(ArtiState {
|
||||
runtime,
|
||||
shutdown_tx: None,
|
||||
client: None,
|
||||
}))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start Arti with a SOCKS5 proxy.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data_dir` - Path to data directory for Tor state (C string)
|
||||
/// * `socks_port` - Port for SOCKS5 proxy (e.g., 39050)
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 if already running
|
||||
/// * -2 if data_dir is invalid
|
||||
/// * -3 if runtime initialization failed
|
||||
/// * -4 if bootstrap failed
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_start(data_dir: *const c_char, socks_port: u16) -> c_int {
|
||||
// Check if already running
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Parse data directory
|
||||
let data_path = match unsafe { CStr::from_ptr(data_dir) }.to_str() {
|
||||
Ok(s) => PathBuf::from(s),
|
||||
Err(_) => return -2,
|
||||
};
|
||||
|
||||
// Initialize runtime if needed
|
||||
if let Err(_) = init_state() {
|
||||
return -3;
|
||||
}
|
||||
|
||||
let state = match ARTI_STATE.get() {
|
||||
Some(s) => s,
|
||||
None => return -3,
|
||||
};
|
||||
|
||||
let mut guard = match state.lock() {
|
||||
Ok(g) => g,
|
||||
Err(_) => return -3,
|
||||
};
|
||||
|
||||
// Create shutdown channel
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
guard.shutdown_tx = Some(shutdown_tx);
|
||||
|
||||
let socks_addr: SocketAddr = format!("127.0.0.1:{}", socks_port)
|
||||
.parse()
|
||||
.expect("valid addr");
|
||||
|
||||
// Spawn the main Arti task
|
||||
let data_path_clone = data_path.clone();
|
||||
guard.runtime.spawn(async move {
|
||||
match run_arti(data_path_clone, socks_addr, shutdown_rx).await {
|
||||
Ok(_) => {
|
||||
tracing::info!("Arti shutdown cleanly");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Arti error: {}", e);
|
||||
update_summary(&format!("Error: {}", e));
|
||||
}
|
||||
}
|
||||
IS_RUNNING.store(false, Ordering::SeqCst);
|
||||
BOOTSTRAP_PROGRESS.store(0, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
IS_RUNNING.store(true, Ordering::SeqCst);
|
||||
BOOTSTRAP_PROGRESS.store(0, Ordering::SeqCst);
|
||||
update_summary("Starting...");
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// Stop Arti gracefully.
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 if not running
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_stop() -> c_int {
|
||||
if !IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let state = match ARTI_STATE.get() {
|
||||
Some(s) => s,
|
||||
None => return -1,
|
||||
};
|
||||
|
||||
let mut guard = match state.lock() {
|
||||
Ok(g) => g,
|
||||
Err(_) => return -1,
|
||||
};
|
||||
|
||||
// Send shutdown signal
|
||||
if let Some(tx) = guard.shutdown_tx.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
|
||||
// Clear client reference
|
||||
guard.client = None;
|
||||
|
||||
// Give async tasks time to complete
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
|
||||
IS_RUNNING.store(false, Ordering::SeqCst);
|
||||
BOOTSTRAP_PROGRESS.store(0, Ordering::SeqCst);
|
||||
update_summary("");
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// Check if Arti is currently running.
|
||||
///
|
||||
/// # Returns
|
||||
/// * 1 if running
|
||||
/// * 0 if not running
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_is_running() -> c_int {
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current bootstrap progress (0-100).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_bootstrap_progress() -> c_int {
|
||||
BOOTSTRAP_PROGRESS.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Get the current bootstrap summary string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `buf` - Buffer to write the summary into
|
||||
/// * `len` - Length of the buffer
|
||||
///
|
||||
/// # Returns
|
||||
/// * Number of bytes written (not including null terminator)
|
||||
/// * -1 if buffer is null or too small
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_bootstrap_summary(buf: *mut c_char, len: c_int) -> c_int {
|
||||
if buf.is_null() || len <= 0 {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let summary = match BOOTSTRAP_SUMMARY.lock() {
|
||||
Ok(s) => s.clone(),
|
||||
Err(_) => return -1,
|
||||
};
|
||||
|
||||
let bytes = summary.as_bytes();
|
||||
let copy_len = std::cmp::min(bytes.len(), (len - 1) as usize);
|
||||
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, copy_len);
|
||||
*buf.add(copy_len) = 0; // null terminator
|
||||
}
|
||||
|
||||
copy_len as c_int
|
||||
}
|
||||
|
||||
/// Signal Arti to go dormant (reduce resource usage).
|
||||
/// This is a hint; Arti may not fully support dormant mode yet.
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 if not running
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_go_dormant() -> c_int {
|
||||
if !IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return -1;
|
||||
}
|
||||
// Arti doesn't have explicit dormant mode yet, but we can note the intent
|
||||
update_summary("Dormant");
|
||||
0
|
||||
}
|
||||
|
||||
/// Signal Arti to wake from dormant mode.
|
||||
///
|
||||
/// # Returns
|
||||
/// * 0 on success
|
||||
/// * -1 if not running
|
||||
#[no_mangle]
|
||||
pub extern "C" fn arti_wake() -> c_int {
|
||||
if !IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return -1;
|
||||
}
|
||||
update_summary("Active");
|
||||
0
|
||||
}
|
||||
|
||||
fn update_summary(s: &str) {
|
||||
if let Ok(mut guard) = BOOTSTRAP_SUMMARY.lock() {
|
||||
guard.clear();
|
||||
guard.push_str(s);
|
||||
}
|
||||
}
|
||||
|
||||
/// Main async entry point for Arti
|
||||
async fn run_arti(
|
||||
data_dir: PathBuf,
|
||||
socks_addr: SocketAddr,
|
||||
mut shutdown_rx: oneshot::Receiver<()>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Ensure data directory exists
|
||||
std::fs::create_dir_all(&data_dir)?;
|
||||
|
||||
update_summary("Configuring...");
|
||||
|
||||
// Build Arti configuration with custom directories
|
||||
let cache_dir = data_dir.join("cache");
|
||||
let state_dir = data_dir.join("state");
|
||||
|
||||
// Use from_directories which sets up storage correctly
|
||||
use arti_client::config::TorClientConfigBuilder;
|
||||
let config = TorClientConfigBuilder::from_directories(state_dir, cache_dir)
|
||||
.build()?;
|
||||
|
||||
update_summary("Bootstrapping...");
|
||||
|
||||
// Create and bootstrap the Tor client
|
||||
let client = TorClient::create_bootstrapped(config).await?;
|
||||
let client = Arc::new(client);
|
||||
|
||||
// Store client reference for status queries
|
||||
if let Some(state) = ARTI_STATE.get() {
|
||||
if let Ok(mut guard) = state.lock() {
|
||||
guard.client = Some(client.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Mark bootstrap complete
|
||||
BOOTSTRAP_PROGRESS.store(100, Ordering::SeqCst);
|
||||
update_summary("Ready");
|
||||
|
||||
// Bind SOCKS listener
|
||||
let listener = TcpListener::bind(socks_addr).await?;
|
||||
tracing::info!("SOCKS5 proxy listening on {}", socks_addr);
|
||||
|
||||
// Accept connections until shutdown
|
||||
loop {
|
||||
tokio::select! {
|
||||
accept_result = listener.accept() => {
|
||||
match accept_result {
|
||||
Ok((stream, peer_addr)) => {
|
||||
let client = client.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = socks::handle_socks_connection(stream, peer_addr, client).await {
|
||||
tracing::debug!("SOCKS connection error from {}: {}", peer_addr, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Accept error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = &mut shutdown_rx => {
|
||||
tracing::info!("Shutdown signal received");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_summary("Shutting down...");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
//! SOCKS5 protocol handler for Arti
|
||||
//!
|
||||
//! Implements a minimal SOCKS5 server that forwards connections through Tor.
|
||||
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arti_client::{TorClient, IntoTorAddr};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tor_rtcompat::PreferredRuntime;
|
||||
|
||||
// SOCKS5 constants
|
||||
const SOCKS5_VERSION: u8 = 0x05;
|
||||
const SOCKS5_AUTH_NONE: u8 = 0x00;
|
||||
const SOCKS5_CMD_CONNECT: u8 = 0x01;
|
||||
const SOCKS5_ATYP_IPV4: u8 = 0x01;
|
||||
const SOCKS5_ATYP_DOMAIN: u8 = 0x03;
|
||||
const SOCKS5_ATYP_IPV6: u8 = 0x04;
|
||||
const SOCKS5_REP_SUCCESS: u8 = 0x00;
|
||||
const SOCKS5_REP_FAILURE: u8 = 0x01;
|
||||
const SOCKS5_REP_CONN_REFUSED: u8 = 0x05;
|
||||
|
||||
/// Handle a single SOCKS5 connection
|
||||
pub async fn handle_socks_connection(
|
||||
mut stream: TcpStream,
|
||||
peer_addr: SocketAddr,
|
||||
client: Arc<TorClient<PreferredRuntime>>,
|
||||
) -> io::Result<()> {
|
||||
// --- Greeting ---
|
||||
// Client sends: VER | NMETHODS | METHODS
|
||||
let mut greeting = [0u8; 2];
|
||||
stream.read_exact(&mut greeting).await?;
|
||||
|
||||
if greeting[0] != SOCKS5_VERSION {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Not SOCKS5",
|
||||
));
|
||||
}
|
||||
|
||||
let nmethods = greeting[1] as usize;
|
||||
let mut methods = vec![0u8; nmethods];
|
||||
stream.read_exact(&mut methods).await?;
|
||||
|
||||
// We only support no-auth
|
||||
if !methods.contains(&SOCKS5_AUTH_NONE) {
|
||||
// Send failure: no acceptable methods
|
||||
stream.write_all(&[SOCKS5_VERSION, 0xFF]).await?;
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"No acceptable auth methods",
|
||||
));
|
||||
}
|
||||
|
||||
// Accept no-auth
|
||||
stream.write_all(&[SOCKS5_VERSION, SOCKS5_AUTH_NONE]).await?;
|
||||
|
||||
// --- Request ---
|
||||
// Client sends: VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT
|
||||
let mut request_header = [0u8; 4];
|
||||
stream.read_exact(&mut request_header).await?;
|
||||
|
||||
if request_header[0] != SOCKS5_VERSION {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Invalid SOCKS5 request version",
|
||||
));
|
||||
}
|
||||
|
||||
let cmd = request_header[1];
|
||||
let atyp = request_header[3];
|
||||
|
||||
if cmd != SOCKS5_CMD_CONNECT {
|
||||
// We only support CONNECT
|
||||
send_reply(&mut stream, SOCKS5_REP_FAILURE).await?;
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Only CONNECT supported",
|
||||
));
|
||||
}
|
||||
|
||||
// Parse destination address
|
||||
let (dest_host, dest_port) = match atyp {
|
||||
SOCKS5_ATYP_IPV4 => {
|
||||
let mut addr = [0u8; 4];
|
||||
stream.read_exact(&mut addr).await?;
|
||||
let mut port_buf = [0u8; 2];
|
||||
stream.read_exact(&mut port_buf).await?;
|
||||
let port = u16::from_be_bytes(port_buf);
|
||||
let host = format!("{}.{}.{}.{}", addr[0], addr[1], addr[2], addr[3]);
|
||||
(host, port)
|
||||
}
|
||||
SOCKS5_ATYP_DOMAIN => {
|
||||
let mut len_buf = [0u8; 1];
|
||||
stream.read_exact(&mut len_buf).await?;
|
||||
let len = len_buf[0] as usize;
|
||||
let mut domain = vec![0u8; len];
|
||||
stream.read_exact(&mut domain).await?;
|
||||
let mut port_buf = [0u8; 2];
|
||||
stream.read_exact(&mut port_buf).await?;
|
||||
let port = u16::from_be_bytes(port_buf);
|
||||
let host = String::from_utf8_lossy(&domain).to_string();
|
||||
(host, port)
|
||||
}
|
||||
SOCKS5_ATYP_IPV6 => {
|
||||
let mut addr = [0u8; 16];
|
||||
stream.read_exact(&mut addr).await?;
|
||||
let mut port_buf = [0u8; 2];
|
||||
stream.read_exact(&mut port_buf).await?;
|
||||
let port = u16::from_be_bytes(port_buf);
|
||||
// Format IPv6 address
|
||||
let segments: Vec<String> = addr
|
||||
.chunks(2)
|
||||
.map(|c| format!("{:02x}{:02x}", c[0], c[1]))
|
||||
.collect();
|
||||
let host = format!("[{}]", segments.join(":"));
|
||||
(host, port)
|
||||
}
|
||||
_ => {
|
||||
send_reply(&mut stream, SOCKS5_REP_FAILURE).await?;
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Unsupported address type",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!("SOCKS5 CONNECT from {} to {}:{}", peer_addr, dest_host, dest_port);
|
||||
|
||||
// Connect through Tor
|
||||
let tor_addr = format!("{}:{}", dest_host, dest_port);
|
||||
let tor_addr = match tor_addr.as_str().into_tor_addr() {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::debug!("Invalid Tor address: {}", e);
|
||||
send_reply(&mut stream, SOCKS5_REP_FAILURE).await?;
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("Invalid Tor address: {}", e),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let tor_stream = match client.connect(tor_addr).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::debug!("Tor connect failed: {}", e);
|
||||
send_reply(&mut stream, SOCKS5_REP_CONN_REFUSED).await?;
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::ConnectionRefused,
|
||||
e.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Send success reply
|
||||
// Reply: VER | REP | RSV | ATYP | BND.ADDR | BND.PORT
|
||||
// We use 0.0.0.0:0 as the bound address since we're proxying
|
||||
let reply = [
|
||||
SOCKS5_VERSION,
|
||||
SOCKS5_REP_SUCCESS,
|
||||
0x00, // RSV
|
||||
SOCKS5_ATYP_IPV4,
|
||||
0, 0, 0, 0, // BND.ADDR
|
||||
0, 0, // BND.PORT
|
||||
];
|
||||
stream.write_all(&reply).await?;
|
||||
|
||||
// Bidirectional copy
|
||||
let (mut client_read, mut client_write) = stream.into_split();
|
||||
let (mut tor_read, mut tor_write) = tor_stream.split();
|
||||
|
||||
let client_to_tor = async {
|
||||
tokio::io::copy(&mut client_read, &mut tor_write).await
|
||||
};
|
||||
let tor_to_client = async {
|
||||
tokio::io::copy(&mut tor_read, &mut client_write).await
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
result = client_to_tor => {
|
||||
if let Err(e) = result {
|
||||
tracing::debug!("Client to Tor copy error: {}", e);
|
||||
}
|
||||
}
|
||||
result = tor_to_client => {
|
||||
if let Err(e) = result {
|
||||
tracing::debug!("Tor to client copy error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_reply(stream: &mut TcpStream, rep: u8) -> io::Result<()> {
|
||||
let reply = [
|
||||
SOCKS5_VERSION,
|
||||
rep,
|
||||
0x00, // RSV
|
||||
SOCKS5_ATYP_IPV4,
|
||||
0, 0, 0, 0, // BND.ADDR
|
||||
0, 0, // BND.PORT
|
||||
];
|
||||
stream.write_all(&reply).await
|
||||
}
|
||||
Executable
+311
@@ -0,0 +1,311 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build arti-bitchat for iOS/macOS with aggressive size optimization
|
||||
#
|
||||
# Output: Frameworks/arti.xcframework containing static libraries for:
|
||||
# - aarch64-apple-ios (iOS device)
|
||||
# - aarch64-apple-ios-sim (iOS simulator, Apple Silicon)
|
||||
# - x86_64-apple-ios (iOS simulator, Intel - optional)
|
||||
# - aarch64-apple-darwin (macOS)
|
||||
#
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Configuration
|
||||
CRATE_NAME="arti-bitchat"
|
||||
LIB_NAME="libarti_bitchat.a"
|
||||
FRAMEWORK_NAME="arti"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/Frameworks"
|
||||
|
||||
# Targets to build
|
||||
TARGETS=(
|
||||
"aarch64-apple-ios" # iOS device
|
||||
"aarch64-apple-ios-sim" # iOS simulator (Apple Silicon)
|
||||
"aarch64-apple-darwin" # macOS
|
||||
)
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
log_info "Checking prerequisites..."
|
||||
|
||||
if ! command -v rustc &> /dev/null; then
|
||||
log_error "Rust is not installed. Please install via rustup."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
log_error "Cargo is not installed. Please install via rustup."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check/install targets
|
||||
for target in "${TARGETS[@]}"; do
|
||||
if ! rustup target list --installed | grep -q "$target"; then
|
||||
log_info "Installing target: $target"
|
||||
rustup target add "$target"
|
||||
fi
|
||||
done
|
||||
|
||||
# Install cbindgen if needed
|
||||
if ! command -v cbindgen &> /dev/null; then
|
||||
log_info "Installing cbindgen..."
|
||||
cargo install cbindgen
|
||||
fi
|
||||
|
||||
log_info "Prerequisites OK"
|
||||
}
|
||||
|
||||
# Set up aggressive size optimization flags and deployment targets
|
||||
setup_rustflags() {
|
||||
local target="$1"
|
||||
|
||||
# Base flags for size optimization
|
||||
export RUSTFLAGS="-C opt-level=z -C lto=fat -C codegen-units=1 -C panic=abort -C strip=symbols"
|
||||
|
||||
# Set deployment targets to suppress linker warnings about version mismatches
|
||||
case "$target" in
|
||||
*-apple-ios-sim*)
|
||||
export IPHONEOS_DEPLOYMENT_TARGET="16.0"
|
||||
# Simulator uses iPhone SDK but needs the sim target
|
||||
;;
|
||||
*-apple-ios*)
|
||||
export IPHONEOS_DEPLOYMENT_TARGET="16.0"
|
||||
;;
|
||||
*-apple-darwin*)
|
||||
export MACOSX_DEPLOYMENT_TARGET="13.0"
|
||||
;;
|
||||
esac
|
||||
|
||||
log_info "RUSTFLAGS: $RUSTFLAGS"
|
||||
log_info "Deployment target: MACOSX=$MACOSX_DEPLOYMENT_TARGET IPHONEOS=$IPHONEOS_DEPLOYMENT_TARGET"
|
||||
}
|
||||
|
||||
# Build for a single target
|
||||
build_target() {
|
||||
local target="$1"
|
||||
log_info "Building for target: $target"
|
||||
|
||||
setup_rustflags "$target"
|
||||
|
||||
# Build release
|
||||
cargo build --release --target "$target" -p "$CRATE_NAME"
|
||||
|
||||
# Check output
|
||||
local lib_path="target/$target/release/$LIB_NAME"
|
||||
if [[ -f "$lib_path" ]]; then
|
||||
local size=$(du -h "$lib_path" | cut -f1)
|
||||
log_info "Built $lib_path ($size)"
|
||||
else
|
||||
log_error "Build failed: $lib_path not found"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Create xcframework from built libraries
|
||||
create_xcframework() {
|
||||
log_info "Creating xcframework..."
|
||||
|
||||
local xcframework_path="$OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
|
||||
|
||||
# Remove existing xcframework
|
||||
rm -rf "$xcframework_path"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Build the xcodebuild command
|
||||
local cmd="xcodebuild -create-xcframework"
|
||||
|
||||
for target in "${TARGETS[@]}"; do
|
||||
local lib_path="$SCRIPT_DIR/target/$target/release/$LIB_NAME"
|
||||
if [[ -f "$lib_path" ]]; then
|
||||
# Strip the library for additional size reduction
|
||||
log_info "Stripping $target library..."
|
||||
strip -x "$lib_path" 2>/dev/null || true
|
||||
|
||||
cmd="$cmd -library $lib_path"
|
||||
|
||||
# Add headers if they exist
|
||||
local header_dir="$OUTPUT_DIR/include"
|
||||
if [[ -d "$header_dir" ]]; then
|
||||
cmd="$cmd -headers $header_dir"
|
||||
fi
|
||||
else
|
||||
log_warn "Skipping missing library: $lib_path"
|
||||
fi
|
||||
done
|
||||
|
||||
cmd="$cmd -output $xcframework_path"
|
||||
|
||||
log_info "Running: $cmd"
|
||||
eval "$cmd"
|
||||
|
||||
if [[ -d "$xcframework_path" ]]; then
|
||||
local size=$(du -sh "$xcframework_path" | cut -f1)
|
||||
log_info "Created $xcframework_path ($size)"
|
||||
else
|
||||
log_error "Failed to create xcframework"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate C header using cbindgen
|
||||
generate_header() {
|
||||
log_info "Generating C header..."
|
||||
|
||||
local header_dir="$OUTPUT_DIR/include"
|
||||
local header_path="$header_dir/arti.h"
|
||||
|
||||
mkdir -p "$header_dir"
|
||||
|
||||
# Create cbindgen.toml if it doesn't exist
|
||||
if [[ ! -f "$CRATE_NAME/cbindgen.toml" ]]; then
|
||||
cat > "$CRATE_NAME/cbindgen.toml" << 'EOF'
|
||||
language = "C"
|
||||
include_guard = "ARTI_H"
|
||||
no_includes = true
|
||||
sys_includes = ["stdint.h", "stdbool.h"]
|
||||
|
||||
[export]
|
||||
include = ["arti_start", "arti_stop", "arti_is_running", "arti_bootstrap_progress", "arti_bootstrap_summary", "arti_go_dormant", "arti_wake"]
|
||||
|
||||
[fn]
|
||||
args = "Auto"
|
||||
|
||||
[parse]
|
||||
parse_deps = false
|
||||
EOF
|
||||
fi
|
||||
|
||||
cbindgen --config "$CRATE_NAME/cbindgen.toml" \
|
||||
--crate "$CRATE_NAME" \
|
||||
--output "$header_path"
|
||||
|
||||
if [[ -f "$header_path" ]]; then
|
||||
log_info "Generated $header_path"
|
||||
cat "$header_path"
|
||||
else
|
||||
log_warn "cbindgen did not generate header, creating manually..."
|
||||
# Fallback: create header manually
|
||||
cat > "$header_path" << 'EOF'
|
||||
#ifndef ARTI_H
|
||||
#define ARTI_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Start Arti with a SOCKS5 proxy.
|
||||
*
|
||||
* @param data_dir Path to data directory for Tor state (C string)
|
||||
* @param socks_port Port for SOCKS5 proxy (e.g., 39050)
|
||||
* @return 0 on success, negative on error
|
||||
*/
|
||||
int32_t arti_start(const char *data_dir, uint16_t socks_port);
|
||||
|
||||
/**
|
||||
* Stop Arti gracefully.
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_stop(void);
|
||||
|
||||
/**
|
||||
* Check if Arti is currently running.
|
||||
*
|
||||
* @return 1 if running, 0 if not running
|
||||
*/
|
||||
int32_t arti_is_running(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap progress (0-100).
|
||||
*
|
||||
* @return Progress percentage
|
||||
*/
|
||||
int32_t arti_bootstrap_progress(void);
|
||||
|
||||
/**
|
||||
* Get the current bootstrap summary string.
|
||||
*
|
||||
* @param buf Buffer to write the summary into
|
||||
* @param len Length of the buffer
|
||||
* @return Number of bytes written, -1 on error
|
||||
*/
|
||||
int32_t arti_bootstrap_summary(char *buf, int32_t len);
|
||||
|
||||
/**
|
||||
* Signal Arti to go dormant (reduce resource usage).
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_go_dormant(void);
|
||||
|
||||
/**
|
||||
* Signal Arti to wake from dormant mode.
|
||||
*
|
||||
* @return 0 on success, -1 if not running
|
||||
*/
|
||||
int32_t arti_wake(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ARTI_H */
|
||||
EOF
|
||||
log_info "Created manual header at $header_path"
|
||||
fi
|
||||
}
|
||||
|
||||
# Print size report
|
||||
print_size_report() {
|
||||
log_info "=== Size Report ==="
|
||||
for target in "${TARGETS[@]}"; do
|
||||
local lib_path="$SCRIPT_DIR/target/$target/release/$LIB_NAME"
|
||||
if [[ -f "$lib_path" ]]; then
|
||||
local size=$(du -h "$lib_path" | cut -f1)
|
||||
echo " $target: $size"
|
||||
fi
|
||||
done
|
||||
|
||||
local xcframework_path="$OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
|
||||
if [[ -d "$xcframework_path" ]]; then
|
||||
local total_size=$(du -sh "$xcframework_path" | cut -f1)
|
||||
echo " xcframework total: $total_size"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
log_info "Building arti-bitchat for iOS/macOS"
|
||||
log_info "=================================="
|
||||
|
||||
check_prerequisites
|
||||
generate_header
|
||||
|
||||
for target in "${TARGETS[@]}"; do
|
||||
build_target "$target"
|
||||
done
|
||||
|
||||
create_xcframework
|
||||
print_size_report
|
||||
|
||||
log_info "Build complete!"
|
||||
log_info "xcframework: $OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
|
||||
}
|
||||
|
||||
# Run
|
||||
main "$@"
|
||||
@@ -1,86 +0,0 @@
|
||||
**BitChat Tor Build Notes**
|
||||
|
||||
- Date: See repo history for the commit you pulled
|
||||
- Output: `tor-nolzma.xcframework` (static, C-only)
|
||||
- Platforms: iOS device (arm64), iOS simulator (arm64), macOS (arm64)
|
||||
- Goal: Minimize binary size while retaining client functionality
|
||||
|
||||
**Overview**
|
||||
- We built a minimal Tor static xcframework with LZMA disabled to reduce size and complexity.
|
||||
- The artifact contains only the C libraries (Tor + libevent + OpenSSL) and their headers. Objective‑C wrappers (`TORThread`, `TORController`, etc.) are not compiled into this minimal artifact to keep size down.
|
||||
- This xcframework is suitable for iOS and macOS targets that link the Objective‑C wrappers as source (or use CocoaPods to bring them in).
|
||||
|
||||
**Component Versions**
|
||||
- Tor: 0.4.8.17
|
||||
- libevent: 2.1.12
|
||||
- OpenSSL: 3.5.1
|
||||
- liblzma: not linked (intentionally disabled)
|
||||
|
||||
**Build Environment**
|
||||
- Xcode with iOS and macOS SDKs
|
||||
- Homebrew tools: `autoconf`, `automake`, `libtool`, `gettext`
|
||||
- Install prerequisites from repo root: `brew bundle`
|
||||
|
||||
**Command Used**
|
||||
- Minimal build (nolzma), with persistent logs: `./build-xcframework.sh -md`
|
||||
- `-m` = minimal mode
|
||||
- `-d` = keep build dir and logs under `build/`
|
||||
|
||||
**What Minimal Mode Does**
|
||||
- Targets: `iphoneos/arm64`, `iphonesimulator/arm64`, `macosx/arm64`.
|
||||
- Disables LZMA in Tor (`--enable-lzma=no`) and removes zstd.
|
||||
- Trims OpenSSL features: `no-zlib no-comp no-ssl3 no-tls1 no-tls1_1 no-dtls no-srp no-psk no-weak-ssl-ciphers no-engine no-ocsp`.
|
||||
- Compiles with size-first flags: `-Os -ffunction-sections -fdata-sections`; bitcode is not embedded.
|
||||
- Statically links Tor, libevent, and OpenSSL into a single library per slice inside the framework.
|
||||
- Copies public headers from Tor/libevent/OpenSSL into the framework `Headers` directory.
|
||||
|
||||
**Resulting Slices (approx sizes)**
|
||||
- Folder size: ~73 MB (`tor-nolzma.xcframework`)
|
||||
- Binaries (non-fat, measured on this build):
|
||||
- iOS arm64 (device): ~16.49 MB
|
||||
- iOS arm64 (simulator): ~15.32 MB
|
||||
- macOS arm64: ~15.60 MB
|
||||
|
||||
Note: Sizes vary slightly by Xcode/SDK versions and environment.
|
||||
|
||||
**Integrating in BitChat**
|
||||
- Add `tor-nolzma.xcframework` to your app target(s). Xcode will select the correct slice for device/simulator/macOS.
|
||||
- Link `libz.tbd` (Tor depends on zlib).
|
||||
- Keep app link-time stripping enabled for best results:
|
||||
- Other Linker Flags: add `-dead_strip`
|
||||
- Avoid `-ObjC` if possible (prevents dead stripping)
|
||||
- Consider enabling ThinLTO/LTO in the app for further size gains
|
||||
- Objective‑C API (wrappers):
|
||||
- Not included in this minimal xcframework. Use one of:
|
||||
- CocoaPods: `Tor/CTor-NoLZMA` subspec (brings `TORThread`, `TORController` sources + links the xcframework), or
|
||||
- Vendor the ObjC sources from `Tor/Classes/CTor` and `Tor/Classes/Core` directly into your project.
|
||||
|
||||
**Rebuilding**
|
||||
- Ensure prerequisites: `brew bundle`
|
||||
- Minimal nolzma, iOS+sim+macOS: `./build-xcframework.sh -m`
|
||||
- Logs (if `-d`): `build/*.log` and per-component logs like `build/libtor-nolzma-<sdk>-<arch>.log`
|
||||
|
||||
**LZMA Trade‑off (for reference)**
|
||||
- We measured that enabling LZMA adds roughly ~0.25 MB per slice to the binary on this setup. For a 3‑slice xcframework, expect ~0.7–0.8 MB more overall.
|
||||
- If you want the LZMA variant with the same minimal trimming: `./build-xcframework.sh -Md` (outputs `tor.xcframework`).
|
||||
|
||||
**Key Flags (for auditing)**
|
||||
- OpenSSL `./Configure` adds: `no-shared` and, in minimal modes, `no-zlib no-comp no-ssl3 no-tls1 no-tls1_1 no-dtls no-srp no-psk no-weak-ssl-ciphers no-engine no-ocsp`
|
||||
- libevent `./configure`: `--disable-openssl --disable-samples --disable-regress --enable-static --disable-shared`
|
||||
- Tor `./configure` (highlights):
|
||||
- `--enable-pic --disable-module-relay --disable-module-dirauth --disable-unittests`
|
||||
- `--enable-static-openssl --enable-static-libevent`
|
||||
- `--disable-asciidoc --disable-manpage --disable-html-manual --disable-zstd`
|
||||
- `--enable-lzma=no` (in this build)
|
||||
- Compiler flags: `-Os -ffunction-sections -fdata-sections`; no bitcode
|
||||
- Minimum OS: iOS 12.0, macOS 10.13
|
||||
|
||||
**Verification Tips**
|
||||
- Check slices: `lipo -info tor-nolzma.xcframework/*/tor-nolzma.framework/tor-nolzma`
|
||||
- Ensure headers present: `ls tor-nolzma.xcframework/*/tor-nolzma.framework/Headers`
|
||||
- Link test: build a small app and add `-dead_strip`; confirm successful run and circuit establishment via control port.
|
||||
|
||||
**Notes**
|
||||
- This minimal build avoids bundling large GeoIP resources. If you need GeoIP, embed the GeoIP bundle (or use the `Tor/GeoIP-NoLZMA` subspec) and set `TORConfiguration.geoipFile`/`geoip6File`.
|
||||
- Static linking maximizes the app’s ability to dead‑strip unused code across the boundary.
|
||||
|
||||
-324
@@ -1,324 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file config.h
|
||||
* \brief Header file for config.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_CONFIG_H
|
||||
#define TOR_CONFIG_H
|
||||
|
||||
#include "app/config/or_options_st.h"
|
||||
#include "lib/testsupport/testsupport.h"
|
||||
#include "app/config/quiet_level.h"
|
||||
|
||||
#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(DARWIN)
|
||||
#define KERNEL_MAY_SUPPORT_IPFW
|
||||
#endif
|
||||
|
||||
/** Lowest allowable value for HeartbeatPeriod; if this is too low, we might
|
||||
* expose more information than we're comfortable with. */
|
||||
#define MIN_HEARTBEAT_PERIOD (30*60)
|
||||
|
||||
/** Maximum default value for MaxMemInQueues, in bytes. */
|
||||
#if SIZEOF_VOID_P >= 8
|
||||
#define MAX_DEFAULT_MEMORY_QUEUE_SIZE (UINT64_C(8) << 30)
|
||||
#else
|
||||
#define MAX_DEFAULT_MEMORY_QUEUE_SIZE (UINT64_C(2) << 30)
|
||||
#endif
|
||||
|
||||
MOCK_DECL(const or_options_t *, get_options, (void));
|
||||
MOCK_DECL(or_options_t *, get_options_mutable, (void));
|
||||
int set_options(or_options_t *new_val, char **msg);
|
||||
void config_free_all(void);
|
||||
const char *safe_str_client(const char *address);
|
||||
const char *safe_str(const char *address);
|
||||
const char *escaped_safe_str_client(const char *address);
|
||||
const char *escaped_safe_str(const char *address);
|
||||
void init_protocol_warning_severity_level(void);
|
||||
int get_protocol_warning_severity_level(void);
|
||||
|
||||
#define LOG_PROTOCOL_WARN (get_protocol_warning_severity_level())
|
||||
|
||||
/** Pattern for backing up configuration files */
|
||||
#define CONFIG_BACKUP_PATTERN "%s.orig.1"
|
||||
|
||||
/** An error from options_trial_assign() or options_init_from_string(). */
|
||||
typedef enum setopt_err_t {
|
||||
SETOPT_OK = 0,
|
||||
SETOPT_ERR_MISC = -1,
|
||||
SETOPT_ERR_PARSE = -2,
|
||||
SETOPT_ERR_TRANSITION = -3,
|
||||
SETOPT_ERR_SETTING = -4,
|
||||
} setopt_err_t;
|
||||
setopt_err_t options_trial_assign(struct config_line_t *list, unsigned flags,
|
||||
char **msg);
|
||||
|
||||
void options_init(or_options_t *options);
|
||||
|
||||
#define OPTIONS_DUMP_MINIMAL 1
|
||||
#define OPTIONS_DUMP_ALL 2
|
||||
char *options_dump(const or_options_t *options, int how_to_dump);
|
||||
int options_init_from_torrc(int argc, char **argv);
|
||||
setopt_err_t options_init_from_string(const char *cf_defaults, const char *cf,
|
||||
int command, const char *command_arg, char **msg);
|
||||
int option_is_recognized(const char *key);
|
||||
const char *option_get_canonical_name(const char *key);
|
||||
struct config_line_t *option_get_assignment(const or_options_t *options,
|
||||
const char *key);
|
||||
int options_save_current(void);
|
||||
const char *get_torrc_fname(int defaults_fname);
|
||||
typedef enum {
|
||||
DIRROOT_DATADIR,
|
||||
DIRROOT_CACHEDIR,
|
||||
DIRROOT_KEYDIR
|
||||
} directory_root_t;
|
||||
|
||||
MOCK_DECL(char *,
|
||||
options_get_dir_fname2_suffix,
|
||||
(const or_options_t *options,
|
||||
directory_root_t roottype,
|
||||
const char *sub1, const char *sub2,
|
||||
const char *suffix));
|
||||
|
||||
/* These macros wrap options_get_dir_fname2_suffix to provide a more
|
||||
* convenient API for finding filenames that Tor uses inside its storage
|
||||
* They are named according to a pattern:
|
||||
* (options_)?get_(cache|key|data)dir_fname(2)?(_suffix)?
|
||||
*
|
||||
* Macros that begin with options_ take an options argument; the others
|
||||
* work with respect to the global options.
|
||||
*
|
||||
* Each macro works relative to the data directory, the key directory,
|
||||
* or the cache directory, as determined by which one is mentioned.
|
||||
*
|
||||
* Macro variants with "2" in their name take two path components; others
|
||||
* take one.
|
||||
*
|
||||
* Macro variants with "_suffix" at the end take an additional suffix
|
||||
* that gets appended to the end of the file
|
||||
*/
|
||||
#define options_get_datadir_fname2_suffix(options, sub1, sub2, suffix) \
|
||||
options_get_dir_fname2_suffix((options), DIRROOT_DATADIR, \
|
||||
(sub1), (sub2), (suffix))
|
||||
#define options_get_cachedir_fname2_suffix(options, sub1, sub2, suffix) \
|
||||
options_get_dir_fname2_suffix((options), DIRROOT_CACHEDIR, \
|
||||
(sub1), (sub2), (suffix))
|
||||
#define options_get_keydir_fname2_suffix(options, sub1, sub2, suffix) \
|
||||
options_get_dir_fname2_suffix((options), DIRROOT_KEYDIR, \
|
||||
(sub1), (sub2), (suffix))
|
||||
|
||||
#define options_get_datadir_fname(opts,sub1) \
|
||||
options_get_datadir_fname2_suffix((opts),(sub1), NULL, NULL)
|
||||
#define options_get_datadir_fname2(opts,sub1,sub2) \
|
||||
options_get_datadir_fname2_suffix((opts),(sub1), (sub2), NULL)
|
||||
|
||||
#define get_datadir_fname2_suffix(sub1, sub2, suffix) \
|
||||
options_get_datadir_fname2_suffix(get_options(), (sub1), (sub2), (suffix))
|
||||
#define get_datadir_fname(sub1) \
|
||||
get_datadir_fname2_suffix((sub1), NULL, NULL)
|
||||
#define get_datadir_fname2(sub1,sub2) \
|
||||
get_datadir_fname2_suffix((sub1), (sub2), NULL)
|
||||
#define get_datadir_fname_suffix(sub1, suffix) \
|
||||
get_datadir_fname2_suffix((sub1), NULL, (suffix))
|
||||
|
||||
/** DOCDOC */
|
||||
#define options_get_keydir_fname(options, sub1) \
|
||||
options_get_keydir_fname2_suffix((options), (sub1), NULL, NULL)
|
||||
#define get_keydir_fname_suffix(sub1, suffix) \
|
||||
options_get_keydir_fname2_suffix(get_options(), (sub1), NULL, suffix)
|
||||
#define get_keydir_fname(sub1) \
|
||||
options_get_keydir_fname2_suffix(get_options(), (sub1), NULL, NULL)
|
||||
|
||||
#define get_cachedir_fname(sub1) \
|
||||
options_get_cachedir_fname2_suffix(get_options(), (sub1), NULL, NULL)
|
||||
#define get_cachedir_fname_suffix(sub1, suffix) \
|
||||
options_get_cachedir_fname2_suffix(get_options(), (sub1), NULL, (suffix))
|
||||
|
||||
#define safe_str_client(address) \
|
||||
safe_str_client_opts(NULL, address)
|
||||
#define safe_str(address) \
|
||||
safe_str_opts(NULL, address)
|
||||
|
||||
const char * safe_str_client_opts(const or_options_t *options,
|
||||
const char *address);
|
||||
const char * safe_str_opts(const or_options_t *options,
|
||||
const char *address);
|
||||
|
||||
int using_default_dir_authorities(const or_options_t *options);
|
||||
|
||||
int create_keys_directory(const or_options_t *options);
|
||||
|
||||
int check_or_create_data_subdir(const char *subdir);
|
||||
int write_to_data_subdir(const char* subdir, const char* fname,
|
||||
const char* str, const char* descr);
|
||||
|
||||
int get_num_cpus(const or_options_t *options);
|
||||
|
||||
MOCK_DECL(const smartlist_t *,get_configured_ports,(void));
|
||||
int port_binds_ipv4(const port_cfg_t *port);
|
||||
int port_binds_ipv6(const port_cfg_t *port);
|
||||
int portconf_get_first_advertised_port(int listener_type,
|
||||
int address_family);
|
||||
#define portconf_get_primary_dir_port() \
|
||||
(portconf_get_first_advertised_port(CONN_TYPE_DIR_LISTENER, AF_INET))
|
||||
const tor_addr_t *portconf_get_first_advertised_addr(int listener_type,
|
||||
int address_family);
|
||||
int port_exists_by_type_addr_port(int listener_type, const tor_addr_t *addr,
|
||||
int port, int check_wildcard);
|
||||
int port_exists_by_type_addr32h_port(int listener_type, uint32_t addr_ipv4h,
|
||||
int port, int check_wildcard);
|
||||
|
||||
char *get_first_listener_addrport_string(int listener_type);
|
||||
|
||||
int options_need_geoip_info(const or_options_t *options,
|
||||
const char **reason_out);
|
||||
|
||||
int getinfo_helper_config(control_connection_t *conn,
|
||||
const char *question, char **answer,
|
||||
const char **errmsg);
|
||||
|
||||
int init_cookie_authentication(const char *fname, const char *header,
|
||||
int cookie_len, int group_readable,
|
||||
uint8_t **cookie_out, int *cookie_is_set_out);
|
||||
|
||||
or_options_t *options_new(void);
|
||||
|
||||
/** Options settings parsed from the command-line. */
|
||||
typedef struct {
|
||||
/** List of options that can only be set from the command-line */
|
||||
struct config_line_t *cmdline_opts;
|
||||
/** List of other options, to be handled by the general Tor configuration
|
||||
system. */
|
||||
struct config_line_t *other_opts;
|
||||
/** Subcommand that Tor has been told to run */
|
||||
tor_cmdline_mode_t command;
|
||||
/** Argument for the command mode, if any. */
|
||||
const char *command_arg;
|
||||
/** How quiet have we been told to be? */
|
||||
quiet_level_t quiet_level;
|
||||
} parsed_cmdline_t;
|
||||
|
||||
parsed_cmdline_t *config_parse_commandline(int argc, char **argv,
|
||||
int ignore_errors);
|
||||
void parsed_cmdline_free_(parsed_cmdline_t *cmdline);
|
||||
#define parsed_cmdline_free(c) \
|
||||
FREE_AND_NULL(parsed_cmdline_t, parsed_cmdline_free_, (c))
|
||||
|
||||
void config_register_addressmaps(const or_options_t *options);
|
||||
/* XXXX move to connection_edge.h */
|
||||
int addressmap_register_auto(const char *from, const char *to,
|
||||
time_t expires,
|
||||
addressmap_entry_source_t addrmap_source,
|
||||
const char **msg);
|
||||
|
||||
int port_cfg_line_extract_addrport(const char *line,
|
||||
char **addrport_out,
|
||||
int *is_unix_out,
|
||||
const char **rest_out);
|
||||
|
||||
/** Represents the information stored in a torrc Bridge line. */
|
||||
typedef struct bridge_line_t {
|
||||
tor_addr_t addr; /* The IP address of the bridge. */
|
||||
uint16_t port; /* The TCP port of the bridge. */
|
||||
char *transport_name; /* The name of the pluggable transport that
|
||||
should be used to connect to the bridge. */
|
||||
char digest[DIGEST_LEN]; /* The bridge's identity key digest. */
|
||||
smartlist_t *socks_args; /* SOCKS arguments for the pluggable
|
||||
transport proxy. */
|
||||
} bridge_line_t;
|
||||
|
||||
void bridge_line_free_(bridge_line_t *bridge_line);
|
||||
#define bridge_line_free(line) \
|
||||
FREE_AND_NULL(bridge_line_t, bridge_line_free_, (line))
|
||||
bridge_line_t *parse_bridge_line(const char *line);
|
||||
|
||||
/* Port helper functions. */
|
||||
int options_any_client_port_set(const or_options_t *options);
|
||||
int port_parse_config(smartlist_t *out,
|
||||
const struct config_line_t *ports,
|
||||
const char *portname,
|
||||
int listener_type,
|
||||
const char *defaultaddr,
|
||||
int defaultport,
|
||||
const unsigned flags);
|
||||
|
||||
#define CL_PORT_NO_STREAM_OPTIONS (1u<<0)
|
||||
#define CL_PORT_WARN_NONLOCAL (1u<<1)
|
||||
/* Was CL_PORT_ALLOW_EXTRA_LISTENADDR (1u<<2) */
|
||||
#define CL_PORT_SERVER_OPTIONS (1u<<3)
|
||||
#define CL_PORT_FORBID_NONLOCAL (1u<<4)
|
||||
#define CL_PORT_TAKES_HOSTNAMES (1u<<5)
|
||||
#define CL_PORT_IS_UNIXSOCKET (1u<<6)
|
||||
#define CL_PORT_DFLT_GROUP_WRITABLE (1u<<7)
|
||||
|
||||
port_cfg_t *port_cfg_new(size_t namelen);
|
||||
#define port_cfg_free(port) \
|
||||
FREE_AND_NULL(port_cfg_t, port_cfg_free_, (port))
|
||||
void port_cfg_free_(port_cfg_t *port);
|
||||
|
||||
int port_count_real_listeners(const smartlist_t *ports,
|
||||
int listenertype,
|
||||
int count_sockets);
|
||||
int pt_parse_transport_line(const or_options_t *options,
|
||||
const char *line, int validate_only,
|
||||
int server);
|
||||
int config_ensure_bandwidth_cap(uint64_t *value, const char *desc, char **msg);
|
||||
|
||||
#ifdef CONFIG_PRIVATE
|
||||
|
||||
MOCK_DECL(STATIC int, options_act,(const or_options_t *old_options));
|
||||
MOCK_DECL(STATIC int, options_act_reversible,(const or_options_t *old_options,
|
||||
char **msg));
|
||||
struct config_mgr_t;
|
||||
STATIC const struct config_mgr_t *get_options_mgr(void);
|
||||
|
||||
#define or_options_free(opt) \
|
||||
FREE_AND_NULL(or_options_t, or_options_free_, (opt))
|
||||
STATIC void or_options_free_(or_options_t *options);
|
||||
STATIC int options_validate_single_onion(or_options_t *options,
|
||||
char **msg);
|
||||
STATIC int parse_tcp_proxy_line(const char *line, or_options_t *options,
|
||||
char **msg);
|
||||
STATIC int consider_adding_dir_servers(const or_options_t *options,
|
||||
const or_options_t *old_options);
|
||||
STATIC void add_default_trusted_dir_authorities(dirinfo_type_t type);
|
||||
MOCK_DECL(STATIC void, add_default_fallback_dir_servers, (void));
|
||||
STATIC int parse_dir_authority_line(const char *line,
|
||||
dirinfo_type_t required_type,
|
||||
int validate_only);
|
||||
STATIC int parse_dir_fallback_line(const char *line, int validate_only);
|
||||
|
||||
STATIC uint64_t compute_real_max_mem_in_queues(const uint64_t val,
|
||||
bool is_server);
|
||||
STATIC int open_and_add_file_log(const log_severity_list_t *severity,
|
||||
const char *fname,
|
||||
int truncate_log);
|
||||
STATIC int options_init_logs(const or_options_t *old_options,
|
||||
const or_options_t *options, int validate_only);
|
||||
|
||||
STATIC int options_create_directories(char **msg_out);
|
||||
struct log_transaction_t;
|
||||
STATIC struct log_transaction_t *options_start_log_transaction(
|
||||
const or_options_t *old_options,
|
||||
char **msg_out);
|
||||
STATIC void options_commit_log_transaction(struct log_transaction_t *xn);
|
||||
STATIC void options_rollback_log_transaction(struct log_transaction_t *xn);
|
||||
|
||||
#ifdef TOR_UNIT_TESTS
|
||||
int options_validate(const or_options_t *old_options,
|
||||
or_options_t *options,
|
||||
char **msg);
|
||||
#endif
|
||||
|
||||
STATIC int parse_ports(or_options_t *options, int validate_only,
|
||||
char **msg, int *n_ports_out,
|
||||
int *world_writable_control_socket);
|
||||
|
||||
#endif /* defined(CONFIG_PRIVATE) */
|
||||
|
||||
#endif /* !defined(TOR_CONFIG_H) */
|
||||
-1107
File diff suppressed because it is too large
Load Diff
-103
@@ -1,103 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file or_state_st.h
|
||||
*
|
||||
* \brief The or_state_t structure, which represents Tor's state file.
|
||||
*/
|
||||
|
||||
#ifndef TOR_OR_STATE_ST_H
|
||||
#define TOR_OR_STATE_ST_H
|
||||
|
||||
#include "lib/cc/torint.h"
|
||||
struct smartlist_t;
|
||||
struct config_suite_t;
|
||||
|
||||
/** Persistent state for an onion router, as saved to disk. */
|
||||
struct or_state_t {
|
||||
uint32_t magic_;
|
||||
/** The time at which we next plan to write the state to the disk. Equal to
|
||||
* TIME_MAX if there are no saveable changes, 0 if there are changes that
|
||||
* should be saved right away. */
|
||||
time_t next_write;
|
||||
|
||||
/** When was the state last written to disk? */
|
||||
time_t LastWritten;
|
||||
|
||||
/** Fields for accounting bandwidth use. */
|
||||
time_t AccountingIntervalStart;
|
||||
uint64_t AccountingBytesReadInInterval;
|
||||
uint64_t AccountingBytesWrittenInInterval;
|
||||
int AccountingSecondsActive;
|
||||
int AccountingSecondsToReachSoftLimit;
|
||||
time_t AccountingSoftLimitHitAt;
|
||||
uint64_t AccountingBytesAtSoftLimit;
|
||||
uint64_t AccountingExpectedUsage;
|
||||
|
||||
/** A list of guard-related configuration lines. */
|
||||
struct config_line_t *Guard;
|
||||
|
||||
struct config_line_t *TransportProxies;
|
||||
|
||||
/** These fields hold information on the history of bandwidth usage for
|
||||
* servers. The "Ends" fields hold the time when we last updated the
|
||||
* bandwidth usage. The "Interval" fields hold the granularity, in seconds,
|
||||
* of the entries of Values. The "Values" lists hold decimal string
|
||||
* representations of the number of bytes read or written in each
|
||||
* interval. The "Maxima" list holds decimal strings describing the highest
|
||||
* rate achieved during the interval.
|
||||
*/
|
||||
time_t BWHistoryReadEnds;
|
||||
int BWHistoryReadInterval;
|
||||
struct smartlist_t *BWHistoryReadValues;
|
||||
struct smartlist_t *BWHistoryReadMaxima;
|
||||
time_t BWHistoryWriteEnds;
|
||||
int BWHistoryWriteInterval;
|
||||
struct smartlist_t *BWHistoryWriteValues;
|
||||
struct smartlist_t *BWHistoryWriteMaxima;
|
||||
time_t BWHistoryIPv6ReadEnds;
|
||||
int BWHistoryIPv6ReadInterval;
|
||||
struct smartlist_t *BWHistoryIPv6ReadValues;
|
||||
struct smartlist_t *BWHistoryIPv6ReadMaxima;
|
||||
time_t BWHistoryIPv6WriteEnds;
|
||||
int BWHistoryIPv6WriteInterval;
|
||||
struct smartlist_t *BWHistoryIPv6WriteValues;
|
||||
struct smartlist_t *BWHistoryIPv6WriteMaxima;
|
||||
time_t BWHistoryDirReadEnds;
|
||||
int BWHistoryDirReadInterval;
|
||||
struct smartlist_t *BWHistoryDirReadValues;
|
||||
struct smartlist_t *BWHistoryDirReadMaxima;
|
||||
time_t BWHistoryDirWriteEnds;
|
||||
int BWHistoryDirWriteInterval;
|
||||
struct smartlist_t *BWHistoryDirWriteValues;
|
||||
struct smartlist_t *BWHistoryDirWriteMaxima;
|
||||
|
||||
/** Build time histogram */
|
||||
struct config_line_t * BuildtimeHistogram;
|
||||
int TotalBuildTimes;
|
||||
int CircuitBuildAbandonedCount;
|
||||
|
||||
/** What version of Tor wrote this state file? */
|
||||
char *TorVersion;
|
||||
|
||||
/** Holds any unrecognized values we found in the state file, in the order
|
||||
* in which we found them. */
|
||||
struct config_line_t *ExtraLines;
|
||||
|
||||
/** When did we last rotate our onion key? "0" for 'no idea'. */
|
||||
time_t LastRotatedOnionKey;
|
||||
|
||||
/**
|
||||
* State objects for individual modules.
|
||||
*
|
||||
* Never access this field or its members directly: instead, use the module
|
||||
* in question to get its relevant state object if you must.
|
||||
*/
|
||||
struct config_suite_t *substates_;
|
||||
};
|
||||
|
||||
#endif /* !defined(TOR_OR_STATE_ST_H) */
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file quiet_level.h
|
||||
* \brief Declare the quiet_level enumeration and global.
|
||||
**/
|
||||
|
||||
#ifndef QUIET_LEVEL_H
|
||||
#define QUIET_LEVEL_H
|
||||
|
||||
/** Enumeration to define how quietly Tor should log at startup. */
|
||||
typedef enum {
|
||||
/** Default quiet level: we log everything of level NOTICE or higher. */
|
||||
QUIET_NONE = 0,
|
||||
/** "--hush" quiet level: we log everything of level WARNING or higher. */
|
||||
QUIET_HUSH = 1 ,
|
||||
/** "--quiet" quiet level: we log nothing at all. */
|
||||
QUIET_SILENT = 2
|
||||
} quiet_level_t;
|
||||
|
||||
/** How quietly should Tor log at startup? */
|
||||
extern quiet_level_t quiet_level;
|
||||
|
||||
void add_default_log_for_quiet_level(quiet_level_t quiet);
|
||||
|
||||
#endif /* !defined(QUIET_LEVEL_H) */
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
/* Copyright (c) 2020-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file resolve_addr.h
|
||||
* \brief Header file for resolve_addr.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_CONFIG_RESOLVE_ADDR_H
|
||||
#define TOR_CONFIG_RESOLVE_ADDR_H
|
||||
|
||||
#include "app/config/config.h"
|
||||
#include "core/mainloop/connection.h"
|
||||
|
||||
#include "app/config/or_options_st.h"
|
||||
|
||||
/** Method used to resolved an address. In other words, how was the address
|
||||
* discovered by tor. */
|
||||
typedef enum {
|
||||
/* Default value. Indicate that no method found the address. */
|
||||
RESOLVED_ADDR_NONE = 0,
|
||||
/* Found from the "Address" configuration option. */
|
||||
RESOLVED_ADDR_CONFIGURED = 1,
|
||||
/* Found from the "ORPort" configuration option. */
|
||||
RESOLVED_ADDR_CONFIGURED_ORPORT = 2,
|
||||
/* Found by resolving the local hostname. */
|
||||
RESOLVED_ADDR_GETHOSTNAME = 3,
|
||||
/* Found by querying the local interface(s). */
|
||||
RESOLVED_ADDR_INTERFACE = 4,
|
||||
/* Found by resolving the hostname from the Address configuration option. */
|
||||
RESOLVED_ADDR_RESOLVED = 5,
|
||||
} resolved_addr_method_t;
|
||||
|
||||
const char *resolved_addr_method_to_str(const resolved_addr_method_t method);
|
||||
|
||||
#define get_orport_addr(family) \
|
||||
(portconf_get_first_advertised_addr(CONN_TYPE_OR_LISTENER, family))
|
||||
|
||||
bool find_my_address(const or_options_t *options, int family,
|
||||
int warn_severity, tor_addr_t *addr_out,
|
||||
resolved_addr_method_t *method_out, char **hostname_out);
|
||||
|
||||
void resolved_addr_get_last(int family, tor_addr_t *addr_out);
|
||||
void resolved_addr_reset_last(int family);
|
||||
void resolved_addr_set_last(const tor_addr_t *addr,
|
||||
const resolved_addr_method_t method_used,
|
||||
const char *hostname_used);
|
||||
|
||||
void resolved_addr_get_suggested(int family, tor_addr_t *addr_out);
|
||||
void resolved_addr_set_suggested(const tor_addr_t *addr);
|
||||
|
||||
bool resolved_addr_is_configured(int family);
|
||||
|
||||
MOCK_DECL(bool, is_local_to_resolve_addr, (const tor_addr_t *addr));
|
||||
|
||||
#ifdef RESOLVE_ADDR_PRIVATE
|
||||
|
||||
#ifdef TOR_UNIT_TESTS
|
||||
|
||||
void resolve_addr_reset_suggested(int family);
|
||||
|
||||
#endif /* TOR_UNIT_TESTS */
|
||||
|
||||
#endif /* defined(RESOLVE_ADDR_PRIVATE) */
|
||||
|
||||
#endif /* !defined(TOR_CONFIG_RESOLVE_ADDR_H) */
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file statefile.h
|
||||
*
|
||||
* \brief Header for statefile.c
|
||||
*/
|
||||
|
||||
#ifndef TOR_STATEFILE_H
|
||||
#define TOR_STATEFILE_H
|
||||
|
||||
MOCK_DECL(or_state_t *,get_or_state,(void));
|
||||
int did_last_state_file_write_fail(void);
|
||||
int or_state_save(time_t now);
|
||||
|
||||
void save_transport_to_state(const char *transport_name,
|
||||
const tor_addr_t *addr, uint16_t port);
|
||||
char *get_stored_bindaddr_for_server_transport(const char *transport);
|
||||
int or_state_load(void);
|
||||
int or_state_loaded(void);
|
||||
void or_state_free_all(void);
|
||||
void or_state_mark_dirty(or_state_t *state, time_t when);
|
||||
|
||||
#ifdef STATEFILE_PRIVATE
|
||||
STATIC struct config_line_t *get_transport_in_state_by_name(
|
||||
const char *transport);
|
||||
STATIC void or_state_free_(or_state_t *state);
|
||||
#define or_state_free(st) FREE_AND_NULL(or_state_t, or_state_free_, (st))
|
||||
STATIC or_state_t *or_state_new(void);
|
||||
struct config_mgr_t;
|
||||
STATIC const struct config_mgr_t *get_state_mgr(void);
|
||||
STATIC void or_state_remove_obsolete_lines(struct config_line_t **extra_lines);
|
||||
#endif /* defined(STATEFILE_PRIVATE) */
|
||||
|
||||
#endif /* !defined(TOR_STATEFILE_H) */
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file tor_cmdline_mode.h
|
||||
* \brief Declare the tor_cmdline_mode_t enumeration
|
||||
**/
|
||||
|
||||
#ifndef TOR_CMDLINE_MODE_H
|
||||
#define TOR_CMDLINE_MODE_H
|
||||
|
||||
/**
|
||||
* Enumeration to describe which command Tor is running. These commands
|
||||
* are controlled by command-line options.
|
||||
**/
|
||||
typedef enum {
|
||||
CMD_RUN_TOR=0, /**< The default: run Tor as a daemon. */
|
||||
CMD_LIST_FINGERPRINT, /**< Running --list-fingerprint. */
|
||||
CMD_HASH_PASSWORD, /**< Running --hash-password. */
|
||||
CMD_VERIFY_CONFIG, /**< Running --verify-config. */
|
||||
CMD_DUMP_CONFIG, /**< Running --dump-config. */
|
||||
CMD_KEYGEN, /**< Running --keygen */
|
||||
CMD_KEY_EXPIRATION, /**< Running --key-expiration */
|
||||
CMD_IMMEDIATE, /**< Special value: indicates a command that is handled
|
||||
* immediately during configuration processing. */
|
||||
CMD_RUN_UNITTESTS, /**< Special value: indicates that we have entered
|
||||
* the Tor code from the unit tests, not from the
|
||||
* regular Tor binary at all. */
|
||||
} tor_cmdline_mode_t;
|
||||
|
||||
#endif /* !defined(TOR_CMDLINE_MODE_H) */
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file main.h
|
||||
* \brief Header file for main.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_MAIN_H
|
||||
#define TOR_MAIN_H
|
||||
|
||||
void handle_signals(void);
|
||||
void activate_signal(int signal_num);
|
||||
|
||||
int try_locking(const or_options_t *options, int err_if_locked);
|
||||
int have_lockfile(void);
|
||||
void release_lockfile(void);
|
||||
|
||||
void tor_remove_file(const char *filename);
|
||||
|
||||
int tor_init(int argc, char **argv);
|
||||
|
||||
int run_tor_main_loop(void);
|
||||
|
||||
void pubsub_install(void);
|
||||
void pubsub_connect(void);
|
||||
|
||||
#endif /* !defined(TOR_MAIN_H) */
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file ntmain.h
|
||||
* \brief Header file for ntmain.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_NTMAIN_H
|
||||
#define TOR_NTMAIN_H
|
||||
|
||||
#ifdef _WIN32
|
||||
#define NT_SERVICE
|
||||
#endif
|
||||
|
||||
#ifdef NT_SERVICE
|
||||
int nt_service_parse_options(int argc, char **argv, int *should_exit);
|
||||
int nt_service_is_stopping(void);
|
||||
void nt_service_set_state(DWORD state);
|
||||
#else
|
||||
#define nt_service_is_stopping() 0
|
||||
#define nt_service_parse_options(a, b, c) (0)
|
||||
#define nt_service_set_state(s) STMT_NIL
|
||||
#endif /* defined(NT_SERVICE) */
|
||||
|
||||
#endif /* !defined(TOR_NTMAIN_H) */
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file risky_options.h
|
||||
* \brief Header for risky_options.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_RISKY_OPTIONS_H
|
||||
#define TOR_RISKY_OPTIONS_H
|
||||
|
||||
extern const char risky_option_list[];
|
||||
|
||||
#endif /* !defined(TOR_RISKY_OPTIONS_H) */
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file shutdown.h
|
||||
* \brief Header file for shutdown.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_SHUTDOWN_H
|
||||
#define TOR_SHUTDOWN_H
|
||||
|
||||
void tor_cleanup(void);
|
||||
void tor_free_all(int postfork);
|
||||
|
||||
#endif /* !defined(TOR_SHUTDOWN_H) */
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
/* Copyright (c) 2003-2004, Roger Dingledine
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file subsysmgr.h
|
||||
* @brief Header for subsysmgr.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_SUBSYSMGR_T
|
||||
#define TOR_SUBSYSMGR_T
|
||||
|
||||
#include "lib/subsys/subsys.h"
|
||||
|
||||
extern const struct subsys_fns_t *tor_subsystems[];
|
||||
extern const unsigned n_tor_subsystems;
|
||||
|
||||
int subsystems_init(void);
|
||||
int subsystems_init_upto(int level);
|
||||
|
||||
struct pubsub_builder_t;
|
||||
int subsystems_add_pubsub_upto(struct pubsub_builder_t *builder,
|
||||
int target_level);
|
||||
int subsystems_add_pubsub(struct pubsub_builder_t *builder);
|
||||
|
||||
void subsystems_shutdown(void);
|
||||
void subsystems_shutdown_downto(int level);
|
||||
|
||||
void subsystems_prefork(void);
|
||||
void subsystems_postfork(void);
|
||||
void subsystems_thread_cleanup(void);
|
||||
|
||||
void subsystems_dump_list(void);
|
||||
|
||||
struct config_mgr_t;
|
||||
int subsystems_register_options_formats(struct config_mgr_t *mgr);
|
||||
int subsystems_register_state_formats(struct config_mgr_t *mgr);
|
||||
struct or_options_t;
|
||||
struct or_state_t;
|
||||
int subsystems_set_options(const struct config_mgr_t *mgr,
|
||||
struct or_options_t *options);
|
||||
int subsystems_set_state(const struct config_mgr_t *mgr,
|
||||
struct or_state_t *state);
|
||||
int subsystems_flush_state(const struct config_mgr_t *mgr,
|
||||
struct or_state_t *state);
|
||||
|
||||
#ifdef TOR_UNIT_TESTS
|
||||
int subsystems_get_options_idx(const subsys_fns_t *sys);
|
||||
int subsystems_get_state_idx(const subsys_fns_t *sys);
|
||||
#endif
|
||||
|
||||
#endif /* !defined(TOR_SUBSYSMGR_T) */
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
/* Copyright (c) 2017-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file hs_ntor.h
|
||||
* @brief Header for hs_ntor.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_HS_NTOR_H
|
||||
#define TOR_HS_NTOR_H
|
||||
|
||||
#include "core/or/or.h"
|
||||
struct ed25519_public_key_t;
|
||||
struct curve25519_public_key_t;
|
||||
struct curve25519_keypair_t;
|
||||
|
||||
/* Output length of KDF for key expansion */
|
||||
#define HS_NTOR_KEY_EXPANSION_KDF_OUT_LEN \
|
||||
(DIGEST256_LEN*2 + CIPHER256_KEY_LEN*2)
|
||||
|
||||
/* Key material needed to encode/decode INTRODUCE1 cells */
|
||||
typedef struct hs_ntor_intro_cell_keys_t {
|
||||
/* Key used for encryption of encrypted INTRODUCE1 blob */
|
||||
uint8_t enc_key[CIPHER256_KEY_LEN];
|
||||
/* MAC key used to protect encrypted INTRODUCE1 blob */
|
||||
uint8_t mac_key[DIGEST256_LEN];
|
||||
} hs_ntor_intro_cell_keys_t;
|
||||
|
||||
/* Key material needed to encode/decode RENDEZVOUS1 cells */
|
||||
typedef struct hs_ntor_rend_cell_keys_t {
|
||||
/* This is the MAC of the HANDSHAKE_INFO field */
|
||||
uint8_t rend_cell_auth_mac[DIGEST256_LEN];
|
||||
/* This is the key seed used to derive further rendezvous crypto keys as
|
||||
* detailed in section 4.2.1 of rend-spec-ng.txt. */
|
||||
uint8_t ntor_key_seed[DIGEST256_LEN];
|
||||
} hs_ntor_rend_cell_keys_t;
|
||||
|
||||
#define SUBCRED_LEN DIGEST256_LEN
|
||||
|
||||
/**
|
||||
* A 'subcredential' used to prove knowledge of a hidden service.
|
||||
**/
|
||||
typedef struct hs_subcredential_t {
|
||||
uint8_t subcred[SUBCRED_LEN];
|
||||
} hs_subcredential_t;
|
||||
|
||||
int hs_ntor_client_get_introduce1_keys(
|
||||
const struct ed25519_public_key_t *intro_auth_pubkey,
|
||||
const struct curve25519_public_key_t *intro_enc_pubkey,
|
||||
const struct curve25519_keypair_t *client_ephemeral_enc_keypair,
|
||||
const hs_subcredential_t *subcredential,
|
||||
hs_ntor_intro_cell_keys_t *hs_ntor_intro_cell_keys_out);
|
||||
|
||||
int hs_ntor_client_get_rendezvous1_keys(
|
||||
const struct ed25519_public_key_t *intro_auth_pubkey,
|
||||
const struct curve25519_keypair_t *client_ephemeral_enc_keypair,
|
||||
const struct curve25519_public_key_t *intro_enc_pubkey,
|
||||
const struct curve25519_public_key_t *service_ephemeral_rend_pubkey,
|
||||
hs_ntor_rend_cell_keys_t *hs_ntor_rend_cell_keys_out);
|
||||
|
||||
int hs_ntor_service_get_introduce1_keys_multi(
|
||||
const struct ed25519_public_key_t *intro_auth_pubkey,
|
||||
const struct curve25519_keypair_t *intro_enc_keypair,
|
||||
const struct curve25519_public_key_t *client_ephemeral_enc_pubkey,
|
||||
size_t n_subcredentials,
|
||||
const hs_subcredential_t *subcredentials,
|
||||
hs_ntor_intro_cell_keys_t *hs_ntor_intro_cell_keys_out);
|
||||
|
||||
int hs_ntor_service_get_introduce1_keys(
|
||||
const struct ed25519_public_key_t *intro_auth_pubkey,
|
||||
const struct curve25519_keypair_t *intro_enc_keypair,
|
||||
const struct curve25519_public_key_t *client_ephemeral_enc_pubkey,
|
||||
const hs_subcredential_t *subcredential,
|
||||
hs_ntor_intro_cell_keys_t *hs_ntor_intro_cell_keys_out);
|
||||
|
||||
int hs_ntor_service_get_rendezvous1_keys(
|
||||
const struct ed25519_public_key_t *intro_auth_pubkey,
|
||||
const struct curve25519_keypair_t *intro_enc_keypair,
|
||||
const struct curve25519_keypair_t *service_ephemeral_rend_keypair,
|
||||
const struct curve25519_public_key_t *client_ephemeral_enc_pubkey,
|
||||
hs_ntor_rend_cell_keys_t *hs_ntor_rend_cell_keys_out);
|
||||
|
||||
int hs_ntor_circuit_key_expansion(const uint8_t *ntor_key_seed,
|
||||
size_t seed_len,
|
||||
uint8_t *keys_out, size_t keys_out_len);
|
||||
|
||||
int hs_ntor_client_rendezvous2_mac_is_good(
|
||||
const hs_ntor_rend_cell_keys_t *hs_ntor_rend_cell_keys,
|
||||
const uint8_t *rcvd_mac);
|
||||
|
||||
#endif /* !defined(TOR_HS_NTOR_H) */
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file onion_crypto.h
|
||||
* \brief Header file for onion_crypto.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_ONION_CRYPTO_H
|
||||
#define TOR_ONION_CRYPTO_H
|
||||
|
||||
#include "lib/crypt_ops/crypto_ed25519.h"
|
||||
|
||||
typedef struct server_onion_keys_t {
|
||||
uint8_t my_identity[DIGEST_LEN];
|
||||
ed25519_public_key_t my_ed_identity;
|
||||
crypto_pk_t *onion_key;
|
||||
crypto_pk_t *last_onion_key;
|
||||
struct di_digest256_map_t *curve25519_key_map;
|
||||
struct curve25519_keypair_t *junk_keypair;
|
||||
} server_onion_keys_t;
|
||||
|
||||
void onion_handshake_state_release(onion_handshake_state_t *state);
|
||||
|
||||
/**
|
||||
* Parameters negotiated as part of a circuit handshake.
|
||||
*/
|
||||
typedef struct circuit_params_t {
|
||||
/** Is true if congestion control is enabled in consensus or param,
|
||||
* as per congestion_control_enabled() result. */
|
||||
bool cc_enabled;
|
||||
/** The number of cells in a sendme increment. Only used if cc_enabled=1. */
|
||||
uint8_t sendme_inc_cells;
|
||||
} circuit_params_t;
|
||||
|
||||
int onion_skin_create(int type,
|
||||
const extend_info_t *node,
|
||||
onion_handshake_state_t *state_out,
|
||||
uint8_t *onion_skin_out,
|
||||
size_t onion_skin_out_maxlen);
|
||||
int onion_skin_server_handshake(int type,
|
||||
const uint8_t *onion_skin, size_t onionskin_len,
|
||||
const server_onion_keys_t *keys,
|
||||
const circuit_params_t *ns_params,
|
||||
uint8_t *reply_out,
|
||||
size_t reply_out_maxlen,
|
||||
uint8_t *keys_out, size_t key_out_len,
|
||||
uint8_t *rend_nonce_out,
|
||||
circuit_params_t *negotiated_params_out);
|
||||
int onion_skin_client_handshake(int type,
|
||||
const onion_handshake_state_t *handshake_state,
|
||||
const uint8_t *reply, size_t reply_len,
|
||||
uint8_t *keys_out, size_t key_out_len,
|
||||
uint8_t *rend_authenticator_out,
|
||||
circuit_params_t *negotiated_params_out,
|
||||
const char **msg_out);
|
||||
|
||||
server_onion_keys_t *server_onion_keys_new(void);
|
||||
void server_onion_keys_free_(server_onion_keys_t *keys);
|
||||
#define server_onion_keys_free(keys) \
|
||||
FREE_AND_NULL(server_onion_keys_t, server_onion_keys_free_, (keys))
|
||||
|
||||
#endif /* !defined(TOR_ONION_CRYPTO_H) */
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file onion_fast.h
|
||||
* \brief Header file for onion_fast.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_ONION_FAST_H
|
||||
#define TOR_ONION_FAST_H
|
||||
|
||||
#define CREATE_FAST_LEN DIGEST_LEN
|
||||
#define CREATED_FAST_LEN (DIGEST_LEN*2)
|
||||
|
||||
typedef struct fast_handshake_state_t {
|
||||
uint8_t state[DIGEST_LEN];
|
||||
} fast_handshake_state_t;
|
||||
|
||||
void fast_handshake_state_free_(fast_handshake_state_t *victim);
|
||||
#define fast_handshake_state_free(st) \
|
||||
FREE_AND_NULL(fast_handshake_state_t, fast_handshake_state_free_, (st))
|
||||
|
||||
int fast_onionskin_create(fast_handshake_state_t **handshake_state_out,
|
||||
uint8_t *handshake_out);
|
||||
|
||||
int fast_server_handshake(const uint8_t *message_in,
|
||||
uint8_t *handshake_reply_out,
|
||||
uint8_t *key_out,
|
||||
size_t key_out_len);
|
||||
|
||||
int fast_client_handshake(const fast_handshake_state_t *handshake_state,
|
||||
const uint8_t *handshake_reply_out,
|
||||
uint8_t *key_out,
|
||||
size_t key_out_len,
|
||||
const char **msg_out);
|
||||
|
||||
#endif /* !defined(TOR_ONION_FAST_H) */
|
||||
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
/* Copyright (c) 2012-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file onion_ntor.h
|
||||
* @brief Header for onion_ntor.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_ONION_NTOR_H
|
||||
#define TOR_ONION_NTOR_H
|
||||
|
||||
#include "lib/cc/torint.h"
|
||||
|
||||
struct di_digest256_map_t;
|
||||
struct curve25519_public_key_t;
|
||||
struct curve25519_keypair_t;
|
||||
|
||||
/** State to be maintained by a client between sending an ntor onionskin
|
||||
* and receiving a reply. */
|
||||
typedef struct ntor_handshake_state_t ntor_handshake_state_t;
|
||||
|
||||
/** Length of an ntor onionskin, as sent from the client to server. */
|
||||
#define NTOR_ONIONSKIN_LEN 84
|
||||
/** Length of an ntor reply, as sent from server to client. */
|
||||
#define NTOR_REPLY_LEN 64
|
||||
|
||||
void ntor_handshake_state_free_(ntor_handshake_state_t *state);
|
||||
#define ntor_handshake_state_free(state) \
|
||||
FREE_AND_NULL(ntor_handshake_state_t, ntor_handshake_state_free_, (state))
|
||||
|
||||
int onion_skin_ntor_create(const uint8_t *router_id,
|
||||
const struct curve25519_public_key_t *router_key,
|
||||
ntor_handshake_state_t **handshake_state_out,
|
||||
uint8_t *onion_skin_out);
|
||||
|
||||
int onion_skin_ntor_server_handshake(const uint8_t *onion_skin,
|
||||
const struct di_digest256_map_t *private_keys,
|
||||
const struct curve25519_keypair_t *junk_keypair,
|
||||
const uint8_t *my_node_id,
|
||||
uint8_t *handshake_reply_out,
|
||||
uint8_t *key_out,
|
||||
size_t key_out_len);
|
||||
|
||||
int onion_skin_ntor_client_handshake(
|
||||
const ntor_handshake_state_t *handshake_state,
|
||||
const uint8_t *handshake_reply,
|
||||
uint8_t *key_out,
|
||||
size_t key_out_len,
|
||||
const char **msg_out);
|
||||
|
||||
#ifdef ONION_NTOR_PRIVATE
|
||||
#include "lib/crypt_ops/crypto_curve25519.h"
|
||||
|
||||
/** Storage held by a client while waiting for an ntor reply from a server. */
|
||||
struct ntor_handshake_state_t {
|
||||
/** Identity digest of the router we're talking to. */
|
||||
uint8_t router_id[DIGEST_LEN];
|
||||
/** Onion key of the router we're talking to. */
|
||||
curve25519_public_key_t pubkey_B;
|
||||
|
||||
/**
|
||||
* Short-lived keypair for use with this handshake.
|
||||
* @{ */
|
||||
curve25519_secret_key_t seckey_x;
|
||||
curve25519_public_key_t pubkey_X;
|
||||
/** @} */
|
||||
};
|
||||
#endif /* defined(ONION_NTOR_PRIVATE) */
|
||||
|
||||
#endif /* !defined(TOR_ONION_NTOR_H) */
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file onion_ntor_v3.h
|
||||
* @brief Header for core/crypto/onion_ntor_v3.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_CORE_CRYPTO_ONION_NTOR_V3_H
|
||||
#define TOR_CORE_CRYPTO_ONION_NTOR_V3_H
|
||||
|
||||
#include "lib/cc/torint.h"
|
||||
#include "lib/testsupport/testsupport.h"
|
||||
#include "lib/crypt_ops/crypto_cipher.h"
|
||||
#include "lib/crypt_ops/crypto_curve25519.h"
|
||||
#include "lib/crypt_ops/crypto_ed25519.h"
|
||||
#include "lib/malloc/malloc.h"
|
||||
|
||||
/**
|
||||
* Client-side state held while an ntor v3 handshake is in progress.
|
||||
**/
|
||||
typedef struct ntor3_handshake_state_t ntor3_handshake_state_t;
|
||||
|
||||
/**
|
||||
* Server-side state held while the relay is handling a client's
|
||||
* encapsulated message, before replying to the v3 handshake.
|
||||
**/
|
||||
typedef struct ntor3_server_handshake_state_t ntor3_server_handshake_state_t;
|
||||
|
||||
void ntor3_handshake_state_free_(ntor3_handshake_state_t *st);
|
||||
#define ntor3_handshake_state_free(ptr) \
|
||||
FREE_AND_NULL(ntor3_handshake_state_t, ntor3_handshake_state_free_, (ptr))
|
||||
void ntor3_server_handshake_state_free_(ntor3_server_handshake_state_t *st);
|
||||
#define ntor3_server_handshake_state_free(ptr) \
|
||||
FREE_AND_NULL(ntor3_server_handshake_state_t, \
|
||||
ntor3_server_handshake_state_free_, (ptr))
|
||||
|
||||
int onion_skin_ntor3_create(const ed25519_public_key_t *relay_id,
|
||||
const curve25519_public_key_t *relay_key,
|
||||
const uint8_t *verification,
|
||||
const size_t verification_len,
|
||||
const uint8_t *message,
|
||||
const size_t message_len,
|
||||
ntor3_handshake_state_t **handshake_state_out,
|
||||
uint8_t **onion_skin_out,
|
||||
size_t *onion_skin_len_out);
|
||||
|
||||
int onion_ntor3_client_handshake(
|
||||
const ntor3_handshake_state_t *handshake_state,
|
||||
const uint8_t *handshake_reply,
|
||||
size_t reply_len,
|
||||
const uint8_t *verification,
|
||||
size_t verification_len,
|
||||
uint8_t *keys_out,
|
||||
size_t keys_out_len,
|
||||
uint8_t **message_out,
|
||||
size_t *message_len_out);
|
||||
|
||||
struct di_digest256_map_t;
|
||||
int onion_skin_ntor3_server_handshake_part1(
|
||||
const struct di_digest256_map_t *private_keys,
|
||||
const curve25519_keypair_t *junk_key,
|
||||
const ed25519_public_key_t *my_id,
|
||||
const uint8_t *client_handshake,
|
||||
size_t client_handshake_len,
|
||||
const uint8_t *verification,
|
||||
size_t verification_len,
|
||||
uint8_t **client_message_out,
|
||||
size_t *client_message_len_out,
|
||||
ntor3_server_handshake_state_t **state_out);
|
||||
|
||||
int onion_skin_ntor3_server_handshake_part2(
|
||||
const ntor3_server_handshake_state_t *state,
|
||||
const uint8_t *verification,
|
||||
size_t verification_len,
|
||||
const uint8_t *server_message,
|
||||
size_t server_message_len,
|
||||
uint8_t **handshake_out,
|
||||
size_t *handshake_len_out,
|
||||
uint8_t *keys_out,
|
||||
size_t keys_out_len);
|
||||
|
||||
#ifdef ONION_NTOR_V3_PRIVATE
|
||||
struct ntor3_handshake_state_t {
|
||||
/** Ephemeral (x,X) keypair. */
|
||||
curve25519_keypair_t client_keypair;
|
||||
/** Relay's ed25519 identity key (ID) */
|
||||
ed25519_public_key_t relay_id;
|
||||
/** Relay's public key (B) */
|
||||
curve25519_public_key_t relay_key;
|
||||
/** Shared secret (Bx). */
|
||||
uint8_t bx[CURVE25519_OUTPUT_LEN];
|
||||
/** MAC of the client's encrypted message data (MAC) */
|
||||
uint8_t msg_mac[DIGEST256_LEN];
|
||||
};
|
||||
|
||||
struct ntor3_server_handshake_state_t {
|
||||
/** Relay's ed25519 identity key (ID) */
|
||||
ed25519_public_key_t my_id;
|
||||
/** Relay's public key (B) */
|
||||
curve25519_public_key_t my_key;
|
||||
/** Client's public ephemeral key (X). */
|
||||
curve25519_public_key_t client_key;
|
||||
|
||||
/** Shared secret (Xb) */
|
||||
uint8_t xb[CURVE25519_OUTPUT_LEN];
|
||||
/** MAC of the client's encrypted message data */
|
||||
uint8_t msg_mac[DIGEST256_LEN];
|
||||
};
|
||||
|
||||
STATIC int onion_skin_ntor3_create_nokeygen(
|
||||
const curve25519_keypair_t *client_keypair,
|
||||
const ed25519_public_key_t *relay_id,
|
||||
const curve25519_public_key_t *relay_key,
|
||||
const uint8_t *verification,
|
||||
const size_t verification_len,
|
||||
const uint8_t *message,
|
||||
const size_t message_len,
|
||||
ntor3_handshake_state_t **handshake_state_out,
|
||||
uint8_t **onion_skin_out,
|
||||
size_t *onion_skin_len_out);
|
||||
|
||||
STATIC int onion_skin_ntor3_server_handshake_part2_nokeygen(
|
||||
const curve25519_keypair_t *relay_keypair_y,
|
||||
const ntor3_server_handshake_state_t *state,
|
||||
const uint8_t *verification,
|
||||
size_t verification_len,
|
||||
const uint8_t *server_message,
|
||||
size_t server_message_len,
|
||||
uint8_t **handshake_out,
|
||||
size_t *handshake_len_out,
|
||||
uint8_t *keys_out,
|
||||
size_t keys_out_len);
|
||||
|
||||
#endif
|
||||
|
||||
#endif /* !defined(TOR_CORE_CRYPTO_ONION_NTOR_V3_H) */
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file onion_tap.h
|
||||
* \brief Header file for onion_tap.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_ONION_TAP_H
|
||||
#define TOR_ONION_TAP_H
|
||||
|
||||
#define TAP_ONIONSKIN_CHALLENGE_LEN (PKCS1_OAEP_PADDING_OVERHEAD+\
|
||||
CIPHER_KEY_LEN+\
|
||||
DH1024_KEY_LEN)
|
||||
#define TAP_ONIONSKIN_REPLY_LEN (DH1024_KEY_LEN+DIGEST_LEN)
|
||||
|
||||
struct crypto_dh_t;
|
||||
struct crypto_pk_t;
|
||||
|
||||
int onion_skin_TAP_create(struct crypto_pk_t *router_key,
|
||||
struct crypto_dh_t **handshake_state_out,
|
||||
char *onion_skin_out);
|
||||
|
||||
int onion_skin_TAP_server_handshake(const char *onion_skin,
|
||||
struct crypto_pk_t *private_key,
|
||||
struct crypto_pk_t *prev_private_key,
|
||||
char *handshake_reply_out,
|
||||
char *key_out,
|
||||
size_t key_out_len);
|
||||
|
||||
int onion_skin_TAP_client_handshake(struct crypto_dh_t *handshake_state,
|
||||
const char *handshake_reply,
|
||||
char *key_out,
|
||||
size_t key_out_len,
|
||||
const char **msg_out);
|
||||
|
||||
#endif /* !defined(TOR_ONION_TAP_H) */
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file relay.h
|
||||
* \brief Header file for relay.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_RELAY_CRYPTO_H
|
||||
#define TOR_RELAY_CRYPTO_H
|
||||
|
||||
int relay_crypto_init(relay_crypto_t *crypto,
|
||||
const char *key_data, size_t key_data_len,
|
||||
int reverse, int is_hs_v3);
|
||||
|
||||
int relay_decrypt_cell(circuit_t *circ, cell_t *cell,
|
||||
cell_direction_t cell_direction,
|
||||
crypt_path_t **layer_hint, char *recognized);
|
||||
void relay_encrypt_cell_outbound(cell_t *cell, origin_circuit_t *or_circ,
|
||||
crypt_path_t *layer_hint);
|
||||
void relay_encrypt_cell_inbound(cell_t *cell, or_circuit_t *or_circ);
|
||||
|
||||
void relay_crypto_clear(relay_crypto_t *crypto);
|
||||
|
||||
void relay_crypto_assert_ok(const relay_crypto_t *crypto);
|
||||
|
||||
uint8_t *relay_crypto_get_sendme_digest(relay_crypto_t *crypto);
|
||||
|
||||
void relay_crypto_record_sendme_digest(relay_crypto_t *crypto,
|
||||
bool is_foward_digest);
|
||||
|
||||
void
|
||||
relay_crypt_one_payload(crypto_cipher_t *cipher, uint8_t *in);
|
||||
|
||||
void
|
||||
relay_set_digest(crypto_digest_t *digest, cell_t *cell);
|
||||
|
||||
#endif /* !defined(TOR_RELAY_CRYPTO_H) */
|
||||
|
||||
-396
@@ -1,396 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file connection.h
|
||||
* \brief Header file for connection.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_CONNECTION_H
|
||||
#define TOR_CONNECTION_H
|
||||
|
||||
#include "lib/smartlist_core/smartlist_core.h"
|
||||
#include "lib/log/log.h"
|
||||
|
||||
#ifdef HAVE_SYS_SOCKET_H
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
struct listener_connection_t;
|
||||
struct connection_t;
|
||||
struct dir_connection_t;
|
||||
struct or_connection_t;
|
||||
struct edge_connection_t;
|
||||
struct entry_connection_t;
|
||||
struct control_connection_t;
|
||||
struct port_cfg_t;
|
||||
struct tor_addr_t;
|
||||
struct or_options_t;
|
||||
|
||||
struct listener_connection_t *TO_LISTENER_CONN(struct connection_t *);
|
||||
const struct listener_connection_t *CONST_TO_LISTENER_CONN(
|
||||
const struct connection_t *);
|
||||
|
||||
struct buf_t;
|
||||
|
||||
#define CONN_TYPE_MIN_ 3
|
||||
/** Type for sockets listening for OR connections. */
|
||||
#define CONN_TYPE_OR_LISTENER 3
|
||||
/** A bidirectional TLS connection transmitting a sequence of cells.
|
||||
* May be from an OR to an OR, or from an OP to an OR. */
|
||||
#define CONN_TYPE_OR 4
|
||||
/** A TCP connection from an onion router to a stream's destination. */
|
||||
#define CONN_TYPE_EXIT 5
|
||||
/** Type for sockets listening for SOCKS connections. */
|
||||
#define CONN_TYPE_AP_LISTENER 6
|
||||
/** A SOCKS proxy connection from the user application to the onion
|
||||
* proxy. */
|
||||
#define CONN_TYPE_AP 7
|
||||
/** Type for sockets listening for HTTP connections to the directory server. */
|
||||
#define CONN_TYPE_DIR_LISTENER 8
|
||||
/** Type for HTTP connections to the directory server. */
|
||||
#define CONN_TYPE_DIR 9
|
||||
/* Type 10 is unused. */
|
||||
/** Type for listening for connections from user interface process. */
|
||||
#define CONN_TYPE_CONTROL_LISTENER 11
|
||||
/** Type for connections from user interface process. */
|
||||
#define CONN_TYPE_CONTROL 12
|
||||
/** Type for sockets listening for transparent connections redirected by pf or
|
||||
* netfilter. */
|
||||
#define CONN_TYPE_AP_TRANS_LISTENER 13
|
||||
/** Type for sockets listening for transparent connections redirected by
|
||||
* natd. */
|
||||
#define CONN_TYPE_AP_NATD_LISTENER 14
|
||||
/** Type for sockets listening for DNS requests. */
|
||||
#define CONN_TYPE_AP_DNS_LISTENER 15
|
||||
|
||||
/** Type for connections from the Extended ORPort. */
|
||||
#define CONN_TYPE_EXT_OR 16
|
||||
/** Type for sockets listening for Extended ORPort connections. */
|
||||
#define CONN_TYPE_EXT_OR_LISTENER 17
|
||||
/** Type for sockets listening for HTTP CONNECT tunnel connections. */
|
||||
#define CONN_TYPE_AP_HTTP_CONNECT_LISTENER 18
|
||||
/** Type for sockets listening for Metrics query connections. */
|
||||
#define CONN_TYPE_METRICS_LISTENER 19
|
||||
/** Type for connections from metrics listener. */
|
||||
#define CONN_TYPE_METRICS 20
|
||||
|
||||
#define CONN_TYPE_MAX_ 21
|
||||
/* !!!! If _CONN_TYPE_MAX is ever over 31, we must grow the type field in
|
||||
* struct connection_t. */
|
||||
|
||||
/* Proxy client handshake states */
|
||||
/* We use a proxy but we haven't even connected to it yet. */
|
||||
#define PROXY_INFANT 1
|
||||
/* We use an HTTP proxy and we've sent the CONNECT command. */
|
||||
#define PROXY_HTTPS_WANT_CONNECT_OK 2
|
||||
/* We use a SOCKS4 proxy and we've sent the CONNECT command. */
|
||||
#define PROXY_SOCKS4_WANT_CONNECT_OK 3
|
||||
/* We use a SOCKS5 proxy and we try to negotiate without
|
||||
any authentication . */
|
||||
#define PROXY_SOCKS5_WANT_AUTH_METHOD_NONE 4
|
||||
/* We use a SOCKS5 proxy and we try to negotiate with
|
||||
Username/Password authentication . */
|
||||
#define PROXY_SOCKS5_WANT_AUTH_METHOD_RFC1929 5
|
||||
/* We use a SOCKS5 proxy and we just sent our credentials. */
|
||||
#define PROXY_SOCKS5_WANT_AUTH_RFC1929_OK 6
|
||||
/* We use a SOCKS5 proxy and we just sent our CONNECT command. */
|
||||
#define PROXY_SOCKS5_WANT_CONNECT_OK 7
|
||||
/* We use an HAPROXY proxy and we just sent the proxy header. */
|
||||
#define PROXY_HAPROXY_WAIT_FOR_FLUSH 8
|
||||
/* We use a proxy and we CONNECTed successfully!. */
|
||||
#define PROXY_CONNECTED 9
|
||||
|
||||
/** State for any listener connection. */
|
||||
#define LISTENER_STATE_READY 0
|
||||
|
||||
/**
|
||||
* This struct associates an old listener connection to be replaced
|
||||
* by new connection described by port configuration. Only used when
|
||||
* moving listeners to/from wildcard IP address.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
struct connection_t *old_conn; /* Old listener connection to be replaced */
|
||||
const struct port_cfg_t *new_port; /* New port configuration */
|
||||
} listener_replacement_t;
|
||||
|
||||
const char *conn_type_to_string(int type);
|
||||
const char *conn_state_to_string(int type, int state);
|
||||
int conn_listener_type_supports_af_unix(int type);
|
||||
|
||||
const char *connection_describe(const connection_t *conn);
|
||||
const char *connection_describe_peer(const connection_t *conn);
|
||||
|
||||
struct dir_connection_t *dir_connection_new(int socket_family);
|
||||
struct or_connection_t *or_connection_new(int type, int socket_family);
|
||||
struct edge_connection_t *edge_connection_new(int type, int socket_family);
|
||||
struct entry_connection_t *entry_connection_new(int type, int socket_family);
|
||||
struct control_connection_t *control_connection_new(int socket_family);
|
||||
struct listener_connection_t *listener_connection_new(int type,
|
||||
int socket_family);
|
||||
struct connection_t *connection_new(int type, int socket_family);
|
||||
int connection_init_accepted_conn(struct connection_t *conn,
|
||||
const struct listener_connection_t *listener);
|
||||
void connection_link_connections(struct connection_t *conn_a,
|
||||
struct connection_t *conn_b);
|
||||
MOCK_DECL(void,connection_free_,(struct connection_t *conn));
|
||||
#define connection_free(conn) \
|
||||
FREE_AND_NULL(struct connection_t, connection_free_, (conn))
|
||||
void connection_free_all(void);
|
||||
void connection_about_to_close_connection(struct connection_t *conn);
|
||||
void connection_close_immediate(struct connection_t *conn);
|
||||
void connection_mark_for_close_(struct connection_t *conn,
|
||||
int line, const char *file);
|
||||
MOCK_DECL(void, connection_mark_for_close_internal_,
|
||||
(struct connection_t *conn, int line, const char *file));
|
||||
|
||||
#define connection_mark_for_close(c) \
|
||||
connection_mark_for_close_((c), __LINE__, SHORT_FILE__)
|
||||
#define connection_mark_for_close_internal(c) \
|
||||
connection_mark_for_close_internal_((c), __LINE__, SHORT_FILE__)
|
||||
|
||||
/**
|
||||
* Mark 'c' for close, but try to hold it open until all the data is written.
|
||||
* Use the _internal versions of connection_mark_for_close; this should be
|
||||
* called when you either are sure that if this is an or_connection_t the
|
||||
* controlling channel has been notified (e.g. with
|
||||
* connection_or_notify_error()), or you actually are the
|
||||
* connection_or_close_for_error() or connection_or_close_normally function.
|
||||
* For all other cases, use connection_mark_and_flush() instead, which
|
||||
* checks for struct or_connection_t properly, instead. See below.
|
||||
*/
|
||||
#define connection_mark_and_flush_internal_(c,line,file) \
|
||||
do { \
|
||||
struct connection_t *tmp_conn__ = (c); \
|
||||
connection_mark_for_close_internal_(tmp_conn__, (line), (file)); \
|
||||
tmp_conn__->hold_open_until_flushed = 1; \
|
||||
} while (0)
|
||||
|
||||
#define connection_mark_and_flush_internal(c) \
|
||||
connection_mark_and_flush_internal_((c), __LINE__, SHORT_FILE__)
|
||||
|
||||
/**
|
||||
* Mark 'c' for close, but try to hold it open until all the data is written.
|
||||
*/
|
||||
#define connection_mark_and_flush_(c,line,file) \
|
||||
do { \
|
||||
struct connection_t *tmp_conn_ = (c); \
|
||||
if (tmp_conn_->type == CONN_TYPE_OR) { \
|
||||
log_warn(LD_CHANNEL | LD_BUG, \
|
||||
"Something tried to close (and flush) an or_connection_t" \
|
||||
" without going through channels at %s:%d", \
|
||||
file, line); \
|
||||
connection_or_close_for_error(TO_OR_CONN(tmp_conn_), 1); \
|
||||
} else { \
|
||||
connection_mark_and_flush_internal_(c, line, file); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define connection_mark_and_flush(c) \
|
||||
connection_mark_and_flush_((c), __LINE__, SHORT_FILE__)
|
||||
|
||||
void connection_expire_held_open(void);
|
||||
|
||||
int connection_connect(struct connection_t *conn, const char *address,
|
||||
const struct tor_addr_t *addr,
|
||||
uint16_t port, int *socket_error);
|
||||
|
||||
#ifdef HAVE_SYS_UN_H
|
||||
|
||||
int connection_connect_unix(struct connection_t *conn, const char *socket_path,
|
||||
int *socket_error);
|
||||
|
||||
#endif /* defined(HAVE_SYS_UN_H) */
|
||||
|
||||
/** Maximum size of information that we can fit into SOCKS5 username
|
||||
or password fields. */
|
||||
#define MAX_SOCKS5_AUTH_FIELD_SIZE 255
|
||||
|
||||
/** Total maximum size of information that we can fit into SOCKS5
|
||||
username and password fields. */
|
||||
#define MAX_SOCKS5_AUTH_SIZE_TOTAL 2*MAX_SOCKS5_AUTH_FIELD_SIZE
|
||||
|
||||
int connection_proxy_connect(struct connection_t *conn, int type);
|
||||
int connection_read_proxy_handshake(struct connection_t *conn);
|
||||
void log_failed_proxy_connection(struct connection_t *conn);
|
||||
int get_proxy_addrport(struct tor_addr_t *addr, uint16_t *port,
|
||||
int *proxy_type,
|
||||
int *is_pt_out, const struct connection_t *conn);
|
||||
|
||||
int retry_all_listeners(struct smartlist_t *new_conns,
|
||||
int close_all_noncontrol);
|
||||
|
||||
void connection_mark_all_noncontrol_listeners(void);
|
||||
void connection_mark_all_noncontrol_connections(void);
|
||||
|
||||
ssize_t connection_bucket_write_limit(struct connection_t *conn, time_t now);
|
||||
bool connection_dir_is_global_write_low(const struct connection_t *conn,
|
||||
size_t attempt);
|
||||
void connection_bucket_init(void);
|
||||
void connection_bucket_adjust(const struct or_options_t *options);
|
||||
void connection_bucket_refill_all(time_t now,
|
||||
uint32_t now_ts);
|
||||
void connection_read_bw_exhausted(struct connection_t *conn,
|
||||
bool is_global_bw);
|
||||
void connection_write_bw_exhausted(struct connection_t *conn,
|
||||
bool is_global_bw);
|
||||
void connection_consider_empty_read_buckets(struct connection_t *conn);
|
||||
void connection_consider_empty_write_buckets(struct connection_t *conn);
|
||||
|
||||
int connection_handle_read(struct connection_t *conn);
|
||||
|
||||
int connection_buf_get_bytes(char *string, size_t len,
|
||||
struct connection_t *conn);
|
||||
int connection_buf_get_line(struct connection_t *conn, char *data,
|
||||
size_t *data_len);
|
||||
int connection_fetch_from_buf_http(struct connection_t *conn,
|
||||
char **headers_out, size_t max_headerlen,
|
||||
char **body_out, size_t *body_used,
|
||||
size_t max_bodylen, int force_complete);
|
||||
|
||||
int connection_wants_to_flush(struct connection_t *conn);
|
||||
int connection_outbuf_too_full(struct connection_t *conn);
|
||||
int connection_handle_write(struct connection_t *conn, int force);
|
||||
int connection_flush(struct connection_t *conn);
|
||||
int connection_process_inbuf(struct connection_t *conn, int package_partial);
|
||||
|
||||
MOCK_DECL(void, connection_write_to_buf_impl_,
|
||||
(const char *string, size_t len, struct connection_t *conn,
|
||||
int zlib));
|
||||
/* DOCDOC connection_write_to_buf */
|
||||
static void connection_buf_add(const char *string, size_t len,
|
||||
struct connection_t *conn);
|
||||
void connection_dir_buf_add(const char *string, size_t len,
|
||||
struct dir_connection_t *dir_conn, int done);
|
||||
static inline void
|
||||
connection_buf_add(const char *string, size_t len, struct connection_t *conn)
|
||||
{
|
||||
connection_write_to_buf_impl_(string, len, conn, 0);
|
||||
}
|
||||
void connection_buf_add_compress(const char *string, size_t len,
|
||||
struct dir_connection_t *conn, int done);
|
||||
void connection_buf_add_buf(struct connection_t *conn, struct buf_t *buf);
|
||||
|
||||
size_t connection_get_inbuf_len(const struct connection_t *conn);
|
||||
size_t connection_get_outbuf_len(const struct connection_t *conn);
|
||||
struct connection_t *connection_get_by_global_id(uint64_t id);
|
||||
|
||||
struct connection_t *connection_get_by_type(int type);
|
||||
MOCK_DECL(struct connection_t *,connection_get_by_type_nonlinked,(int type));
|
||||
MOCK_DECL(struct connection_t *,connection_get_by_type_addr_port_purpose,
|
||||
(int type,
|
||||
const struct tor_addr_t *addr,
|
||||
uint16_t port, int purpose));
|
||||
struct connection_t *connection_get_by_type_state(int type, int state);
|
||||
struct connection_t *connection_get_by_type_state_rendquery(
|
||||
int type, int state,
|
||||
const char *rendquery);
|
||||
struct smartlist_t *connection_list_by_type_state(int type, int state);
|
||||
struct smartlist_t *connection_list_by_type_purpose(int type, int purpose);
|
||||
struct smartlist_t *connection_dir_list_by_purpose_and_resource(
|
||||
int purpose,
|
||||
const char *resource);
|
||||
struct smartlist_t *connection_dir_list_by_purpose_resource_and_state(
|
||||
int purpose,
|
||||
const char *resource,
|
||||
int state);
|
||||
|
||||
#define CONN_LEN_AND_FREE_TEMPLATE(sl) \
|
||||
STMT_BEGIN \
|
||||
int len = smartlist_len(sl); \
|
||||
smartlist_free(sl); \
|
||||
return len; \
|
||||
STMT_END
|
||||
|
||||
/** Return a count of directory connections that are fetching the item
|
||||
* described by <b>purpose</b>/<b>resource</b>. */
|
||||
static inline int
|
||||
connection_dir_count_by_purpose_and_resource(
|
||||
int purpose,
|
||||
const char *resource)
|
||||
{
|
||||
struct smartlist_t *conns = connection_dir_list_by_purpose_and_resource(
|
||||
purpose,
|
||||
resource);
|
||||
CONN_LEN_AND_FREE_TEMPLATE(conns);
|
||||
}
|
||||
|
||||
/** Return a count of directory connections that are fetching the item
|
||||
* described by <b>purpose</b>/<b>resource</b>/<b>state</b>. */
|
||||
static inline int
|
||||
connection_dir_count_by_purpose_resource_and_state(
|
||||
int purpose,
|
||||
const char *resource,
|
||||
int state)
|
||||
{
|
||||
struct smartlist_t *conns =
|
||||
connection_dir_list_by_purpose_resource_and_state(
|
||||
purpose,
|
||||
resource,
|
||||
state);
|
||||
CONN_LEN_AND_FREE_TEMPLATE(conns);
|
||||
}
|
||||
|
||||
#undef CONN_LEN_AND_FREE_TEMPLATE
|
||||
|
||||
int any_other_active_or_conns(const struct or_connection_t *this_conn);
|
||||
|
||||
/* || 0 is for -Wparentheses-equality (-Wall?) appeasement under clang */
|
||||
#define connection_speaks_cells(conn) (((conn)->type == CONN_TYPE_OR) || 0)
|
||||
int connection_is_listener(struct connection_t *conn);
|
||||
int connection_state_is_open(struct connection_t *conn);
|
||||
int connection_state_is_connecting(struct connection_t *conn);
|
||||
|
||||
char *alloc_http_authenticator(const char *authenticator);
|
||||
|
||||
void assert_connection_ok(struct connection_t *conn, time_t now);
|
||||
int connection_or_nonopen_was_started_here(struct or_connection_t *conn);
|
||||
void connection_dump_buffer_mem_stats(int severity);
|
||||
|
||||
MOCK_DECL(void, clock_skew_warning,
|
||||
(const struct connection_t *conn, long apparent_skew, int trusted,
|
||||
log_domain_mask_t domain, const char *received,
|
||||
const char *source));
|
||||
|
||||
int connection_is_moribund(struct connection_t *conn);
|
||||
void connection_check_oos(int n_socks, int failed);
|
||||
|
||||
/** Execute the statement <b>stmt</b>, which may log events concerning the
|
||||
* connection <b>conn</b>. To prevent infinite loops, disable log messages
|
||||
* being sent to controllers if <b>conn</b> is a control connection.
|
||||
*
|
||||
* Stmt must not contain any return or goto statements.
|
||||
*/
|
||||
#define CONN_LOG_PROTECT(conn, stmt) \
|
||||
STMT_BEGIN \
|
||||
int _log_conn_is_control; \
|
||||
tor_assert(conn); \
|
||||
_log_conn_is_control = (conn->type == CONN_TYPE_CONTROL); \
|
||||
if (_log_conn_is_control) \
|
||||
disable_control_logging(); \
|
||||
STMT_BEGIN stmt; STMT_END; \
|
||||
if (_log_conn_is_control) \
|
||||
enable_control_logging(); \
|
||||
STMT_END
|
||||
|
||||
#ifdef CONNECTION_PRIVATE
|
||||
STATIC void connection_free_minimal(struct connection_t *conn);
|
||||
|
||||
/* Used only by connection.c and test*.c */
|
||||
MOCK_DECL(STATIC int,connection_connect_sockaddr,
|
||||
(struct connection_t *conn,
|
||||
const struct sockaddr *sa,
|
||||
socklen_t sa_len,
|
||||
const struct sockaddr *bindaddr,
|
||||
socklen_t bindaddr_len,
|
||||
int *socket_error));
|
||||
MOCK_DECL(STATIC void, kill_conn_list_for_oos, (struct smartlist_t *conns));
|
||||
MOCK_DECL(STATIC struct smartlist_t *, pick_oos_victims, (int n));
|
||||
|
||||
#endif /* defined(CONNECTION_PRIVATE) */
|
||||
|
||||
#endif /* !defined(TOR_CONNECTION_H) */
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2024, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file cpuworker.h
|
||||
* \brief Header file for cpuworker.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_CPUWORKER_H
|
||||
#define TOR_CPUWORKER_H
|
||||
|
||||
int cpuworker_init(void);
|
||||
void cpuworker_free_all(void);
|
||||
void cpuworkers_rotate_keyinfo(void);
|
||||
|
||||
void cpuworker_consensus_has_changed(const networkstatus_t *ns);
|
||||
|
||||
struct workqueue_entry_t;
|
||||
enum workqueue_reply_t;
|
||||
enum workqueue_priority_t;
|
||||
MOCK_DECL(struct workqueue_entry_t *, cpuworker_queue_work, (
|
||||
enum workqueue_priority_t priority,
|
||||
enum workqueue_reply_t (*fn)(void *, void *),
|
||||
void (*reply_fn)(void *),
|
||||
void *arg));
|
||||
|
||||
struct create_cell_t;
|
||||
int assign_onionskin_to_cpuworker(or_circuit_t *circ,
|
||||
struct create_cell_t *onionskin);
|
||||
|
||||
uint64_t estimated_usec_for_onionskins(uint32_t n_requests,
|
||||
uint16_t onionskin_type);
|
||||
void cpuworker_log_onionskin_overhead(int severity, int onionskin_type,
|
||||
const char *onionskin_type_name);
|
||||
void cpuworker_cancel_circ_handshake(or_circuit_t *circ);
|
||||
|
||||
unsigned int cpuworker_get_n_threads(void);
|
||||
|
||||
#endif /* !defined(TOR_CPUWORKER_H) */
|
||||
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file mainloop.h
|
||||
* \brief Header file for mainloop.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_MAINLOOP_H
|
||||
#define TOR_MAINLOOP_H
|
||||
|
||||
int have_completed_a_circuit(void);
|
||||
void note_that_we_completed_a_circuit(void);
|
||||
void note_that_we_maybe_cant_complete_circuits(void);
|
||||
|
||||
int connection_add_impl(connection_t *conn, int is_connecting);
|
||||
#define connection_add(conn) connection_add_impl((conn), 0)
|
||||
#define connection_add_connecting(conn) connection_add_impl((conn), 1)
|
||||
int connection_remove(connection_t *conn);
|
||||
void connection_unregister_events(connection_t *conn);
|
||||
int connection_in_array(connection_t *conn);
|
||||
void add_connection_to_closeable_list(connection_t *conn);
|
||||
int connection_is_on_closeable_list(connection_t *conn);
|
||||
|
||||
MOCK_DECL(smartlist_t *, get_connection_array, (void));
|
||||
MOCK_DECL(uint64_t,get_bytes_read,(void));
|
||||
MOCK_DECL(uint64_t,get_bytes_written,(void));
|
||||
void stats_increment_bytes_read_and_written(uint64_t r, uint64_t w);
|
||||
|
||||
/** Bitmask for events that we can turn on and off with
|
||||
* connection_watch_events. */
|
||||
typedef enum watchable_events {
|
||||
/* Yes, it is intentional that these match Libevent's EV_READ and EV_WRITE */
|
||||
READ_EVENT=0x02, /**< We want to know when a connection is readable */
|
||||
WRITE_EVENT=0x04 /**< We want to know when a connection is writable */
|
||||
} watchable_events_t;
|
||||
void connection_watch_events(connection_t *conn, watchable_events_t events);
|
||||
int connection_is_reading(const connection_t *conn);
|
||||
MOCK_DECL(void,connection_stop_reading,(connection_t *conn));
|
||||
MOCK_DECL(void,connection_start_reading,(connection_t *conn));
|
||||
|
||||
int connection_is_writing(connection_t *conn);
|
||||
MOCK_DECL(void,connection_stop_writing,(connection_t *conn));
|
||||
MOCK_DECL(void,connection_start_writing,(connection_t *conn));
|
||||
|
||||
void tor_shutdown_event_loop_and_exit(int exitcode);
|
||||
int tor_event_loop_shutdown_is_pending(void);
|
||||
|
||||
void connection_stop_reading_from_linked_conn(connection_t *conn);
|
||||
|
||||
MOCK_DECL(int, connection_count_moribund, (void));
|
||||
|
||||
void directory_all_unreachable(time_t now);
|
||||
void directory_info_has_arrived(time_t now, int from_cache, int suppress_logs);
|
||||
|
||||
void ip_address_changed(int on_client_conn);
|
||||
void dns_servers_relaunch_checks(void);
|
||||
void reset_all_main_loop_timers(void);
|
||||
void reschedule_directory_downloads(void);
|
||||
void reschedule_or_state_save(void);
|
||||
void mainloop_schedule_postloop_cleanup(void);
|
||||
void rescan_periodic_events(const or_options_t *options);
|
||||
MOCK_DECL(void, schedule_rescan_periodic_events,(void));
|
||||
|
||||
void update_current_time(time_t now);
|
||||
|
||||
MOCK_DECL(long,get_uptime,(void));
|
||||
MOCK_DECL(void,reset_uptime,(void));
|
||||
|
||||
unsigned get_signewnym_epoch(void);
|
||||
|
||||
int do_main_loop(void);
|
||||
|
||||
void reset_main_loop_counters(void);
|
||||
uint64_t get_main_loop_success_count(void);
|
||||
uint64_t get_main_loop_error_count(void);
|
||||
uint64_t get_main_loop_idle_count(void);
|
||||
|
||||
void periodic_events_on_new_options(const or_options_t *options);
|
||||
|
||||
void do_signewnym(time_t);
|
||||
time_t get_last_signewnym_time(void);
|
||||
|
||||
void mainloop_schedule_shutdown(int delay_sec);
|
||||
|
||||
void tor_init_connection_lists(void);
|
||||
void initialize_mainloop_events(void);
|
||||
void initialize_periodic_events(void);
|
||||
void tor_mainloop_free_all(void);
|
||||
|
||||
struct token_bucket_rw_t;
|
||||
|
||||
extern time_t time_of_process_start;
|
||||
extern struct token_bucket_rw_t global_bucket;
|
||||
extern struct token_bucket_rw_t global_relayed_bucket;
|
||||
|
||||
#ifdef MAINLOOP_PRIVATE
|
||||
STATIC int run_main_loop_until_done(void);
|
||||
STATIC void close_closeable_connections(void);
|
||||
STATIC void teardown_periodic_events(void);
|
||||
STATIC int get_my_roles(const or_options_t *);
|
||||
STATIC int check_network_participation_callback(time_t now,
|
||||
const or_options_t *options);
|
||||
|
||||
#ifdef TOR_UNIT_TESTS
|
||||
extern smartlist_t *connection_array;
|
||||
|
||||
/* We need the periodic_event_item_t definition. */
|
||||
#include "core/mainloop/periodic.h"
|
||||
extern periodic_event_item_t mainloop_periodic_events[];
|
||||
#endif /* defined(TOR_UNIT_TESTS) */
|
||||
#endif /* defined(MAINLOOP_PRIVATE) */
|
||||
|
||||
#endif /* !defined(TOR_MAINLOOP_H) */
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
/* Copyright (c) 2001, Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file mainloop_pubsub.h
|
||||
* @brief Header for mainloop_pubsub.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_MAINLOOP_PUBSUB_H
|
||||
#define TOR_MAINLOOP_PUBSUB_H
|
||||
|
||||
struct pubsub_builder_t;
|
||||
|
||||
/**
|
||||
* Describe when and how messages are delivered on message channel.
|
||||
*
|
||||
* Every message channel must be associated with one of these strategies.
|
||||
**/
|
||||
typedef enum {
|
||||
/**
|
||||
* Never deliver messages automatically.
|
||||
*
|
||||
* If a message channel uses this strategy, then no matter now many
|
||||
* messages are published on it, they are not delivered until something
|
||||
* manually calls dispatch_flush() for that channel
|
||||
**/
|
||||
DELIV_NEVER=0,
|
||||
/**
|
||||
* Deliver messages promptly, via the event loop.
|
||||
*
|
||||
* If a message channel uses this strategy, then publishing a messages
|
||||
* that channel activates an event that causes messages to be handled
|
||||
* later in the mainloop. The messages will be processed at some point
|
||||
* very soon, delaying only for pending IO events and the like.
|
||||
*
|
||||
* Generally this is the best choice for a delivery strategy, since
|
||||
* it avoids stack explosion.
|
||||
**/
|
||||
DELIV_PROMPT,
|
||||
/**
|
||||
* Deliver messages immediately, skipping the event loop.
|
||||
*
|
||||
* Every event on this channel is flushed immediately after it is queued,
|
||||
* using the stack.
|
||||
*
|
||||
* This delivery type should be used with caution, since it can cause
|
||||
* unexpected call chains, resource starvation, and the like.
|
||||
**/
|
||||
DELIV_IMMEDIATE,
|
||||
} deliv_strategy_t;
|
||||
|
||||
int tor_mainloop_connect_pubsub(struct pubsub_builder_t *builder);
|
||||
void tor_mainloop_connect_pubsub_events(void);
|
||||
int tor_mainloop_set_delivery_strategy(const char *msg_channel_name,
|
||||
deliv_strategy_t strategy);
|
||||
void tor_mainloop_disconnect_pubsub(void);
|
||||
|
||||
#endif /* !defined(TOR_MAINLOOP_PUBSUB_H) */
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file mainloop_state_st.h
|
||||
* @brief Declare a state structure for mainloop-relevant fields
|
||||
**/
|
||||
|
||||
#ifndef TOR_CORE_MAINLOOP_MAINLOOP_STATE_ST_H
|
||||
#define TOR_CORE_MAINLOOP_MAINLOOP_STATE_ST_H
|
||||
|
||||
#include "lib/conf/confdecl.h"
|
||||
|
||||
#define CONF_CONTEXT STRUCT
|
||||
#include "core/mainloop/mainloop_state.inc"
|
||||
#undef CONF_CONTEXT
|
||||
|
||||
typedef struct mainloop_state_t mainloop_state_t;
|
||||
|
||||
#endif /* !defined(TOR_CORE_MAINLOOP_MAINLOOP_STATE_ST_H) */
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file mainloop_sys.h
|
||||
* @brief Header for mainloop_sys.c
|
||||
**/
|
||||
|
||||
#ifndef MAINLOOP_SYS_H
|
||||
#define MAINLOOP_SYS_H
|
||||
|
||||
extern const struct subsys_fns_t sys_mainloop;
|
||||
|
||||
#endif /* !defined(MAINLOOP_SYS_H) */
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file netstatus.h
|
||||
* @brief Header for netstatus.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_NETSTATUS_H
|
||||
#define TOR_NETSTATUS_H
|
||||
|
||||
int net_is_disabled(void);
|
||||
int net_is_completely_disabled(void);
|
||||
|
||||
void note_user_activity(time_t now);
|
||||
void reset_user_activity(time_t now);
|
||||
time_t get_last_user_activity_time(void);
|
||||
|
||||
void set_network_participation(bool participation);
|
||||
bool is_participating_on_network(void);
|
||||
|
||||
struct mainloop_state_t;
|
||||
|
||||
void netstatus_flush_to_state(struct mainloop_state_t *state, time_t now);
|
||||
void netstatus_load_from_state(const struct mainloop_state_t *state,
|
||||
time_t now);
|
||||
void netstatus_note_clock_jumped(time_t seconds_diff);
|
||||
|
||||
#endif /* !defined(TOR_NETSTATUS_H) */
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
/* Copyright (c) 2015-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file periodic.h
|
||||
* @brief Header for periodic.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_PERIODIC_H
|
||||
#define TOR_PERIODIC_H
|
||||
|
||||
#define PERIODIC_EVENT_NO_UPDATE (-1)
|
||||
|
||||
/* Tor roles for which a periodic event item is for. An event can be for
|
||||
* multiple roles, they can be combined. */
|
||||
#define PERIODIC_EVENT_ROLE_CLIENT (1U << 0)
|
||||
#define PERIODIC_EVENT_ROLE_RELAY (1U << 1)
|
||||
#define PERIODIC_EVENT_ROLE_BRIDGE (1U << 2)
|
||||
#define PERIODIC_EVENT_ROLE_DIRAUTH (1U << 3)
|
||||
#define PERIODIC_EVENT_ROLE_BRIDGEAUTH (1U << 4)
|
||||
#define PERIODIC_EVENT_ROLE_HS_SERVICE (1U << 5)
|
||||
#define PERIODIC_EVENT_ROLE_DIRSERVER (1U << 6)
|
||||
#define PERIODIC_EVENT_ROLE_CONTROLEV (1U << 7)
|
||||
|
||||
#define PERIODIC_EVENT_ROLE_NET_PARTICIPANT (1U << 8)
|
||||
#define PERIODIC_EVENT_ROLE_ALL (1U << 9)
|
||||
|
||||
/* Helper macro to make it a bit less annoying to defined groups of roles that
|
||||
* are often used. */
|
||||
|
||||
/* Router that is a Bridge or Relay. */
|
||||
#define PERIODIC_EVENT_ROLE_ROUTER \
|
||||
(PERIODIC_EVENT_ROLE_BRIDGE | PERIODIC_EVENT_ROLE_RELAY)
|
||||
/* Authorities that is both bridge and directory. */
|
||||
#define PERIODIC_EVENT_ROLE_AUTHORITIES \
|
||||
(PERIODIC_EVENT_ROLE_BRIDGEAUTH | PERIODIC_EVENT_ROLE_DIRAUTH)
|
||||
|
||||
/*
|
||||
* Event flags which can change the behavior of an event.
|
||||
*/
|
||||
|
||||
/* Indicate that the event needs the network meaning that if we are in
|
||||
* DisableNetwork or hibernation mode, the event won't be enabled. This obey
|
||||
* the net_is_disabled() check. */
|
||||
#define PERIODIC_EVENT_FLAG_NEED_NET (1U << 0)
|
||||
|
||||
/* Indicate that if the event is enabled, it needs to be run once before
|
||||
* it becomes disabled.
|
||||
*/
|
||||
#define PERIODIC_EVENT_FLAG_RUN_ON_DISABLE (1U << 1)
|
||||
|
||||
/** Callback function for a periodic event to take action. The return value
|
||||
* influences the next time the function will get called. Return
|
||||
* PERIODIC_EVENT_NO_UPDATE to not update <b>last_action_time</b> and be polled
|
||||
* again in the next second. If a positive value is returned it will update the
|
||||
* interval time. */
|
||||
typedef int (*periodic_event_helper_t)(time_t now,
|
||||
const or_options_t *options);
|
||||
|
||||
struct mainloop_event_t;
|
||||
|
||||
/** A single item for the periodic-events-function table. */
|
||||
typedef struct periodic_event_item_t {
|
||||
periodic_event_helper_t fn; /**< The function to run the event */
|
||||
time_t last_action_time; /**< The last time the function did something */
|
||||
struct mainloop_event_t *ev; /**< Libevent callback we're using to implement
|
||||
* this */
|
||||
const char *name; /**< Name of the function -- for debug */
|
||||
|
||||
/* Bitmask of roles define above for which this event applies. */
|
||||
uint32_t roles;
|
||||
/* Bitmask of flags which can change the behavior of the event. */
|
||||
uint32_t flags;
|
||||
/* Indicate that this event has been enabled that is scheduled. */
|
||||
unsigned int enabled : 1;
|
||||
} periodic_event_item_t;
|
||||
|
||||
/** events will get their interval from first execution */
|
||||
#ifndef COCCI
|
||||
#define PERIODIC_EVENT(fn, r, f) { fn##_callback, 0, NULL, #fn, r, f, 0 }
|
||||
#define END_OF_PERIODIC_EVENTS { NULL, 0, NULL, NULL, 0, 0, 0 }
|
||||
#endif
|
||||
|
||||
/* Return true iff the given event was setup before thus is enabled to be
|
||||
* scheduled. */
|
||||
static inline int
|
||||
periodic_event_is_enabled(const periodic_event_item_t *item)
|
||||
{
|
||||
return item->enabled;
|
||||
}
|
||||
|
||||
void periodic_event_launch(periodic_event_item_t *event);
|
||||
void periodic_event_connect(periodic_event_item_t *event);
|
||||
//void periodic_event_disconnect(periodic_event_item_t *event);
|
||||
void periodic_event_reschedule(periodic_event_item_t *event);
|
||||
void periodic_event_enable(periodic_event_item_t *event);
|
||||
void periodic_event_disable(periodic_event_item_t *event);
|
||||
void periodic_event_schedule_and_disable(periodic_event_item_t *event);
|
||||
|
||||
void periodic_events_register(periodic_event_item_t *item);
|
||||
void periodic_events_connect_all(void);
|
||||
void periodic_events_reset_all(void);
|
||||
periodic_event_item_t *periodic_events_find(const char *name);
|
||||
void periodic_events_rescan_by_roles(int roles, bool net_disabled);
|
||||
void periodic_events_disconnect_all(void);
|
||||
|
||||
int safe_timer_diff(time_t now, time_t next);
|
||||
|
||||
#endif /* !defined(TOR_PERIODIC_H) */
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file addr_policy_st.h
|
||||
* @brief Address policy structures.
|
||||
**/
|
||||
|
||||
#ifndef TOR_ADDR_POLICY_ST_H
|
||||
#define TOR_ADDR_POLICY_ST_H
|
||||
|
||||
#include "lib/cc/torint.h"
|
||||
#include "lib/net/address.h"
|
||||
|
||||
/** What action type does an address policy indicate: accept or reject? */
|
||||
typedef enum {
|
||||
ADDR_POLICY_ACCEPT=1,
|
||||
ADDR_POLICY_REJECT=2,
|
||||
} addr_policy_action_t;
|
||||
#define addr_policy_action_bitfield_t ENUM_BF(addr_policy_action_t)
|
||||
|
||||
/** A reference-counted address policy rule. */
|
||||
struct addr_policy_t {
|
||||
int refcnt; /**< Reference count */
|
||||
/** What to do when the policy matches.*/
|
||||
addr_policy_action_bitfield_t policy_type:2;
|
||||
unsigned int is_private:1; /**< True iff this is the pseudo-address,
|
||||
* "private". */
|
||||
unsigned int is_canonical:1; /**< True iff this policy is the canonical
|
||||
* copy (stored in a hash table to avoid
|
||||
* duplication of common policies) */
|
||||
maskbits_t maskbits; /**< Accept/reject all addresses <b>a</b> such that the
|
||||
* first <b>maskbits</b> bits of <b>a</b> match
|
||||
* <b>addr</b>. */
|
||||
/** Base address to accept or reject.
|
||||
*
|
||||
* Note that wildcards are treated
|
||||
* differently depending on address family. An AF_UNSPEC address means
|
||||
* "All addresses, IPv4 or IPv6." An AF_INET address with maskbits==0 means
|
||||
* "All IPv4 addresses" and an AF_INET6 address with maskbits == 0 means
|
||||
* "All IPv6 addresses".
|
||||
**/
|
||||
tor_addr_t addr;
|
||||
uint16_t prt_min; /**< Lowest port number to accept/reject. */
|
||||
uint16_t prt_max; /**< Highest port number to accept/reject. */
|
||||
};
|
||||
|
||||
#endif /* !defined(TOR_ADDR_POLICY_ST_H) */
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/* Copyright (c) 2018-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file address_set.h
|
||||
* \brief Types to handle sets of addresses.
|
||||
**/
|
||||
|
||||
#ifndef TOR_ADDRESS_SET_H
|
||||
#define TOR_ADDRESS_SET_H
|
||||
|
||||
#include "orconfig.h"
|
||||
#include "lib/cc/torint.h"
|
||||
#include "lib/container/bloomfilt.h"
|
||||
|
||||
struct tor_addr_t;
|
||||
|
||||
/**
|
||||
* An address_set_t represents a set of tor_addr_t values. The implementation
|
||||
* is probabilistic: false negatives cannot occur but false positives are
|
||||
* possible.
|
||||
*/
|
||||
typedef struct bloomfilt_t address_set_t;
|
||||
|
||||
address_set_t *address_set_new(int max_addresses_guess);
|
||||
#define address_set_free(set) bloomfilt_free(set)
|
||||
void address_set_add(address_set_t *set, const struct tor_addr_t *addr);
|
||||
void address_set_add_ipv4h(address_set_t *set, uint32_t addr);
|
||||
int address_set_probably_contains(const address_set_t *set,
|
||||
const struct tor_addr_t *addr);
|
||||
|
||||
#endif /* !defined(TOR_ADDRESS_SET_H) */
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file cell_queue_st.h
|
||||
* @brief Cell queue structures
|
||||
**/
|
||||
|
||||
#ifndef PACKED_CELL_ST_H
|
||||
#define PACKED_CELL_ST_H
|
||||
|
||||
#include "tor_queue.h"
|
||||
|
||||
/** A cell as packed for writing to the network. */
|
||||
struct packed_cell_t {
|
||||
/** Next cell queued on this circuit. */
|
||||
TOR_SIMPLEQ_ENTRY(packed_cell_t) next;
|
||||
char body[CELL_MAX_NETWORK_SIZE]; /**< Cell as packed for network. */
|
||||
uint32_t inserted_timestamp; /**< Time (in timestamp units) when this cell
|
||||
* was inserted */
|
||||
};
|
||||
|
||||
/** A queue of cells on a circuit, waiting to be added to the
|
||||
* or_connection_t's outbuf. */
|
||||
struct cell_queue_t {
|
||||
/** Linked list of packed_cell_t*/
|
||||
TOR_SIMPLEQ_HEAD(cell_simpleq_t, packed_cell_t) head;
|
||||
int n; /**< The number of cells in the queue. */
|
||||
};
|
||||
|
||||
#endif /* !defined(PACKED_CELL_ST_H) */
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file cell_st.h
|
||||
* @brief Fixed-size cell structure.
|
||||
**/
|
||||
|
||||
#ifndef CELL_ST_H
|
||||
#define CELL_ST_H
|
||||
|
||||
/** Parsed onion routing cell. All communication between nodes
|
||||
* is via cells. */
|
||||
struct cell_t {
|
||||
circid_t circ_id; /**< Circuit which received the cell. */
|
||||
uint8_t command; /**< Type of the cell: one of CELL_PADDING, CELL_CREATE,
|
||||
* CELL_DESTROY, etc */
|
||||
uint8_t payload[CELL_PAYLOAD_SIZE]; /**< Cell body. */
|
||||
};
|
||||
|
||||
#endif /* !defined(CELL_ST_H) */
|
||||
-775
@@ -1,775 +0,0 @@
|
||||
/* * Copyright (c) 2012-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file channel.h
|
||||
* \brief Header file for channel.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_CHANNEL_H
|
||||
#define TOR_CHANNEL_H
|
||||
|
||||
#include "core/or/or.h"
|
||||
#include "core/or/circuitmux.h"
|
||||
#include "lib/container/handles.h"
|
||||
#include "lib/crypt_ops/crypto_ed25519.h"
|
||||
|
||||
#include "ext/ht.h"
|
||||
#include "tor_queue.h"
|
||||
|
||||
#define tor_timer_t timeout
|
||||
struct tor_timer_t;
|
||||
|
||||
/* Channel handler function pointer typedefs */
|
||||
typedef void (*channel_listener_fn_ptr)(channel_listener_t *, channel_t *);
|
||||
typedef void (*channel_cell_handler_fn_ptr)(channel_t *, cell_t *);
|
||||
|
||||
/**
|
||||
* This enum is used by channelpadding to decide when to pad channels.
|
||||
* Don't add values to it without updating the checks in
|
||||
* channelpadding_decide_to_pad_channel().
|
||||
*/
|
||||
typedef enum {
|
||||
CHANNEL_USED_NOT_USED_FOR_FULL_CIRCS = 0,
|
||||
CHANNEL_USED_FOR_FULL_CIRCS,
|
||||
CHANNEL_USED_FOR_USER_TRAFFIC,
|
||||
} channel_usage_info_t;
|
||||
|
||||
/** Possible rules for generating circuit IDs on an OR connection. */
|
||||
typedef enum {
|
||||
CIRC_ID_TYPE_LOWER=0, /**< Pick from 0..1<<15-1. */
|
||||
CIRC_ID_TYPE_HIGHER=1, /**< Pick from 1<<15..1<<16-1. */
|
||||
/** The other side of a connection is an OP: never create circuits to it,
|
||||
* and let it use any circuit ID it wants. */
|
||||
CIRC_ID_TYPE_NEITHER=2
|
||||
} circ_id_type_t;
|
||||
#define circ_id_type_bitfield_t ENUM_BF(circ_id_type_t)
|
||||
|
||||
/* channel states for channel_t */
|
||||
|
||||
typedef enum {
|
||||
/**
|
||||
* Closed state - channel is inactive
|
||||
*
|
||||
* Permitted transitions from:
|
||||
* - CHANNEL_STATE_CLOSING
|
||||
* Permitted transitions to:
|
||||
* - CHANNEL_STATE_OPENING
|
||||
*/
|
||||
CHANNEL_STATE_CLOSED = 0,
|
||||
/**
|
||||
* Opening state - channel is trying to connect
|
||||
*
|
||||
* Permitted transitions from:
|
||||
* - CHANNEL_STATE_CLOSED
|
||||
* Permitted transitions to:
|
||||
* - CHANNEL_STATE_CLOSING
|
||||
* - CHANNEL_STATE_ERROR
|
||||
* - CHANNEL_STATE_OPEN
|
||||
*/
|
||||
CHANNEL_STATE_OPENING,
|
||||
/**
|
||||
* Open state - channel is active and ready for use
|
||||
*
|
||||
* Permitted transitions from:
|
||||
* - CHANNEL_STATE_MAINT
|
||||
* - CHANNEL_STATE_OPENING
|
||||
* Permitted transitions to:
|
||||
* - CHANNEL_STATE_CLOSING
|
||||
* - CHANNEL_STATE_ERROR
|
||||
* - CHANNEL_STATE_MAINT
|
||||
*/
|
||||
CHANNEL_STATE_OPEN,
|
||||
/**
|
||||
* Maintenance state - channel is temporarily offline for subclass specific
|
||||
* maintenance activities such as TLS renegotiation.
|
||||
*
|
||||
* Permitted transitions from:
|
||||
* - CHANNEL_STATE_OPEN
|
||||
* Permitted transitions to:
|
||||
* - CHANNEL_STATE_CLOSING
|
||||
* - CHANNEL_STATE_ERROR
|
||||
* - CHANNEL_STATE_OPEN
|
||||
*/
|
||||
CHANNEL_STATE_MAINT,
|
||||
/**
|
||||
* Closing state - channel is shutting down
|
||||
*
|
||||
* Permitted transitions from:
|
||||
* - CHANNEL_STATE_MAINT
|
||||
* - CHANNEL_STATE_OPEN
|
||||
* Permitted transitions to:
|
||||
* - CHANNEL_STATE_CLOSED,
|
||||
* - CHANNEL_STATE_ERROR
|
||||
*/
|
||||
CHANNEL_STATE_CLOSING,
|
||||
/**
|
||||
* Error state - channel has experienced a permanent error
|
||||
*
|
||||
* Permitted transitions from:
|
||||
* - CHANNEL_STATE_CLOSING
|
||||
* - CHANNEL_STATE_MAINT
|
||||
* - CHANNEL_STATE_OPENING
|
||||
* - CHANNEL_STATE_OPEN
|
||||
* Permitted transitions to:
|
||||
* - None
|
||||
*/
|
||||
CHANNEL_STATE_ERROR,
|
||||
/**
|
||||
* Placeholder for maximum state value
|
||||
*/
|
||||
CHANNEL_STATE_LAST
|
||||
} channel_state_t;
|
||||
|
||||
/* channel listener states for channel_listener_t */
|
||||
|
||||
typedef enum {
|
||||
/**
|
||||
* Closed state - channel listener is inactive
|
||||
*
|
||||
* Permitted transitions from:
|
||||
* - CHANNEL_LISTENER_STATE_CLOSING
|
||||
* Permitted transitions to:
|
||||
* - CHANNEL_LISTENER_STATE_LISTENING
|
||||
*/
|
||||
CHANNEL_LISTENER_STATE_CLOSED = 0,
|
||||
/**
|
||||
* Listening state - channel listener is listening for incoming
|
||||
* connections
|
||||
*
|
||||
* Permitted transitions from:
|
||||
* - CHANNEL_LISTENER_STATE_CLOSED
|
||||
* Permitted transitions to:
|
||||
* - CHANNEL_LISTENER_STATE_CLOSING
|
||||
* - CHANNEL_LISTENER_STATE_ERROR
|
||||
*/
|
||||
CHANNEL_LISTENER_STATE_LISTENING,
|
||||
/**
|
||||
* Closing state - channel listener is shutting down
|
||||
*
|
||||
* Permitted transitions from:
|
||||
* - CHANNEL_LISTENER_STATE_LISTENING
|
||||
* Permitted transitions to:
|
||||
* - CHANNEL_LISTENER_STATE_CLOSED,
|
||||
* - CHANNEL_LISTENER_STATE_ERROR
|
||||
*/
|
||||
CHANNEL_LISTENER_STATE_CLOSING,
|
||||
/**
|
||||
* Error state - channel listener has experienced a permanent error
|
||||
*
|
||||
* Permitted transitions from:
|
||||
* - CHANNEL_STATE_CLOSING
|
||||
* - CHANNEL_STATE_LISTENING
|
||||
* Permitted transitions to:
|
||||
* - None
|
||||
*/
|
||||
CHANNEL_LISTENER_STATE_ERROR,
|
||||
/**
|
||||
* Placeholder for maximum state value
|
||||
*/
|
||||
CHANNEL_LISTENER_STATE_LAST
|
||||
} channel_listener_state_t;
|
||||
|
||||
/**
|
||||
* Channel struct; see the channel_t typedef in or.h. A channel is an
|
||||
* abstract interface for the OR-to-OR connection, similar to connection_or_t,
|
||||
* but without the strong coupling to the underlying TLS implementation. They
|
||||
* are constructed by calling a protocol-specific function to open a channel
|
||||
* to a particular node, and once constructed support the abstract operations
|
||||
* defined below.
|
||||
*/
|
||||
struct channel_t {
|
||||
/** Magic number for type-checking cast macros */
|
||||
uint32_t magic;
|
||||
|
||||
/** List entry for hashtable for global-identifier lookup. */
|
||||
HT_ENTRY(channel_t) gidmap_node;
|
||||
|
||||
/** Handle entry for handle-based lookup */
|
||||
HANDLE_ENTRY(channel, channel_t);
|
||||
|
||||
/** Current channel state */
|
||||
channel_state_t state;
|
||||
|
||||
/** Globally unique ID number for a channel over the lifetime of a Tor
|
||||
* process. This may not be 0.
|
||||
*/
|
||||
uint64_t global_identifier;
|
||||
|
||||
/** Should we expect to see this channel in the channel lists? */
|
||||
unsigned char registered:1;
|
||||
|
||||
/** has this channel ever been open? */
|
||||
unsigned int has_been_open:1;
|
||||
|
||||
/**
|
||||
* This field indicates if the other side has enabled or disabled
|
||||
* padding via either the link protocol version or
|
||||
* channelpadding_negotiate cells.
|
||||
*
|
||||
* Clients can override this with ConnectionPadding in torrc to
|
||||
* disable or force padding to relays, but relays cannot override the
|
||||
* client's request.
|
||||
*/
|
||||
unsigned int padding_enabled:1;
|
||||
|
||||
/** Cached value of our decision to pad (to avoid expensive
|
||||
* checks during critical path statistics counting). */
|
||||
unsigned int currently_padding:1;
|
||||
|
||||
/** Is there a pending netflow padding callback? */
|
||||
unsigned int pending_padding_callback:1;
|
||||
|
||||
/** Is our peer likely to consider this channel canonical? */
|
||||
unsigned int is_canonical_to_peer:1;
|
||||
|
||||
/** Has this channel ever been used for non-directory traffic?
|
||||
* Used to decide what channels to pad, and when. */
|
||||
channel_usage_info_t channel_usage;
|
||||
|
||||
/** When should we send a cell for netflow padding? 0 means no padding is
|
||||
* scheduled. */
|
||||
monotime_coarse_t next_padding_time;
|
||||
|
||||
/** The callback pointer for the padding callbacks */
|
||||
struct tor_timer_t *padding_timer;
|
||||
/** The handle to this channel (to free on canceled timers) */
|
||||
struct channel_handle_t *timer_handle;
|
||||
|
||||
/** If not UNSPEC, the address that the peer says we have. */
|
||||
tor_addr_t addr_according_to_peer;
|
||||
|
||||
/**
|
||||
* These two fields specify the minimum and maximum negotiated timeout
|
||||
* values for inactivity (send or receive) before we decide to pad a
|
||||
* channel. These fields can be set either via a PADDING_NEGOTIATE cell,
|
||||
* or the torrc option ReducedConnectionPadding. The consensus parameters
|
||||
* nf_ito_low and nf_ito_high are used to ensure that padding can only be
|
||||
* negotiated to be less frequent than what is specified in the consensus.
|
||||
* (This is done to prevent wingnut clients from requesting excessive
|
||||
* padding).
|
||||
*
|
||||
* The actual timeout value is randomly chosen between these two values
|
||||
* as per the table in channelpadding_get_netflow_inactive_timeout_ms(),
|
||||
* after ensuring that these values do not specify lower timeouts than
|
||||
* the consensus parameters.
|
||||
*
|
||||
* If these are 0, we have not negotiated or specified custom padding
|
||||
* times, and instead use consensus defaults. */
|
||||
uint16_t padding_timeout_low_ms;
|
||||
uint16_t padding_timeout_high_ms;
|
||||
|
||||
/** Why did we close?
|
||||
*/
|
||||
enum {
|
||||
CHANNEL_NOT_CLOSING = 0,
|
||||
CHANNEL_CLOSE_REQUESTED,
|
||||
CHANNEL_CLOSE_FROM_BELOW,
|
||||
CHANNEL_CLOSE_FOR_ERROR
|
||||
} reason_for_closing;
|
||||
|
||||
/** State variable for use by the scheduler */
|
||||
enum {
|
||||
/**
|
||||
* The channel is not open, or it has a full output buffer but no queued
|
||||
* cells.
|
||||
*/
|
||||
SCHED_CHAN_IDLE = 0,
|
||||
/**
|
||||
* The channel has space on its output buffer to write, but no queued
|
||||
* cells.
|
||||
*/
|
||||
SCHED_CHAN_WAITING_FOR_CELLS,
|
||||
/**
|
||||
* The scheduler has queued cells but no output buffer space to write.
|
||||
*/
|
||||
SCHED_CHAN_WAITING_TO_WRITE,
|
||||
/**
|
||||
* The scheduler has both queued cells and output buffer space, and is
|
||||
* eligible for the scheduler loop.
|
||||
*/
|
||||
SCHED_CHAN_PENDING
|
||||
} scheduler_state;
|
||||
|
||||
/** Heap index for use by the scheduler */
|
||||
int sched_heap_idx;
|
||||
|
||||
/** Timestamps for both cell channels and listeners */
|
||||
time_t timestamp_created; /* Channel created */
|
||||
time_t timestamp_active; /* Any activity */
|
||||
|
||||
/**
|
||||
* This is a monotonic timestamp that marks when we
|
||||
* believe the channel has actually sent or received data to/from
|
||||
* the wire. Right now, it is used to determine when we should send
|
||||
* a padding cell for channelpadding.
|
||||
*
|
||||
* XXX: Are we setting timestamp_xfer_ms in the right places to
|
||||
* accurately reflect actual network data transfer? Or might this be
|
||||
* very wrong wrt when bytes actually go on the wire?
|
||||
*/
|
||||
monotime_coarse_t timestamp_xfer;
|
||||
|
||||
/* Methods implemented by the lower layer */
|
||||
|
||||
/** Free a channel */
|
||||
void (*free_fn)(channel_t *);
|
||||
/** Close an open channel */
|
||||
void (*close)(channel_t *);
|
||||
/** Describe the transport subclass for this channel */
|
||||
const char * (*describe_transport)(channel_t *);
|
||||
/** Optional method to dump transport-specific statistics on the channel */
|
||||
void (*dumpstats)(channel_t *, int);
|
||||
|
||||
/** Registered handlers for incoming cells */
|
||||
channel_cell_handler_fn_ptr cell_handler;
|
||||
|
||||
/* Methods implemented by the lower layer */
|
||||
|
||||
/**
|
||||
* Ask the lower layer for an estimate of the average overhead for
|
||||
* transmissions on this channel.
|
||||
*/
|
||||
double (*get_overhead_estimate)(channel_t *);
|
||||
/*
|
||||
* Ask the underlying transport what the remote endpoint address is, in a
|
||||
* tor_addr_t. Write the address out to the provided tor_addr_t *, and
|
||||
* return 1 if successful or 0 if no address available.
|
||||
*/
|
||||
int (*get_remote_addr)(const channel_t *, tor_addr_t *);
|
||||
int (*get_transport_name)(channel_t *chan, char **transport_out);
|
||||
|
||||
/**
|
||||
* Get a human-readable text description of the remote endpoint, for
|
||||
* logging.
|
||||
*/
|
||||
const char * (*describe_peer)(const channel_t *);
|
||||
/** Check if the lower layer has queued writes */
|
||||
int (*has_queued_writes)(channel_t *);
|
||||
/**
|
||||
* Ask the lower layer if this is 'canonical', for a transport-specific
|
||||
* definition of canonical.
|
||||
*/
|
||||
int (*is_canonical)(channel_t *);
|
||||
/** Check if this channel matches a specified extend_info_t */
|
||||
int (*matches_extend_info)(channel_t *, extend_info_t *);
|
||||
/** Check if this channel matches a target address when extending */
|
||||
int (*matches_target)(channel_t *, const tor_addr_t *);
|
||||
/* Ask the lower layer how many bytes it has queued but not yet sent */
|
||||
size_t (*num_bytes_queued)(channel_t *);
|
||||
/* Ask the lower layer how many cells can be written */
|
||||
int (*num_cells_writeable)(channel_t *);
|
||||
/* Write a cell to an open channel */
|
||||
int (*write_cell)(channel_t *, cell_t *);
|
||||
/** Write a packed cell to an open channel */
|
||||
int (*write_packed_cell)(channel_t *, packed_cell_t *);
|
||||
/** Write a variable-length cell to an open channel */
|
||||
int (*write_var_cell)(channel_t *, var_cell_t *);
|
||||
|
||||
/**
|
||||
* Hash of the public RSA key for the other side's RSA identity key -- or
|
||||
* zeroes if we don't have an RSA identity in mind for the other side, and
|
||||
* it hasn't shown us one.
|
||||
*
|
||||
* Note that this is the RSA identity that we hope the other side has -- not
|
||||
* necessarily its true identity. Don't believe this identity unless
|
||||
* authentication has happened.
|
||||
*/
|
||||
char identity_digest[DIGEST_LEN];
|
||||
/**
|
||||
* Ed25519 key for the other side of this channel -- or zeroes if we don't
|
||||
* have an Ed25519 identity in mind for the other side, and it hasn't shown
|
||||
* us one.
|
||||
*
|
||||
* Note that this is the identity that we hope the other side has -- not
|
||||
* necessarily its true identity. Don't believe this identity unless
|
||||
* authentication has happened.
|
||||
*/
|
||||
struct ed25519_public_key_t ed25519_identity;
|
||||
|
||||
/**
|
||||
* Linked list of channels with the same RSA identity digest, for use with
|
||||
* the digest->channel map
|
||||
*/
|
||||
TOR_LIST_ENTRY(channel_t) next_with_same_id;
|
||||
|
||||
/** Circuit mux for circuits sending on this channel */
|
||||
circuitmux_t *cmux;
|
||||
|
||||
/** Circuit ID generation stuff for use by circuitbuild.c */
|
||||
|
||||
/**
|
||||
* When we send CREATE cells along this connection, which half of the
|
||||
* space should we use?
|
||||
*/
|
||||
circ_id_type_bitfield_t circ_id_type:2;
|
||||
/* DOCDOC */
|
||||
unsigned wide_circ_ids:1;
|
||||
|
||||
/** For how many circuits are we n_chan? What about p_chan? */
|
||||
unsigned int num_n_circuits, num_p_circuits;
|
||||
|
||||
/**
|
||||
* True iff this channel shouldn't get any new circs attached to it,
|
||||
* because the connection is too old, or because there's a better one.
|
||||
* More generally, this flag is used to note an unhealthy connection;
|
||||
* for example, if a bad connection fails we shouldn't assume that the
|
||||
* router itself has a problem.
|
||||
*/
|
||||
unsigned int is_bad_for_new_circs:1;
|
||||
|
||||
/** True iff we have decided that the other end of this connection
|
||||
* is a client or bridge relay. Connections with this flag set should never
|
||||
* be used to satisfy an EXTEND request. */
|
||||
unsigned int is_client:1;
|
||||
|
||||
/** Set if the channel was initiated remotely (came from a listener) */
|
||||
unsigned int is_incoming:1;
|
||||
|
||||
/** Set by lower layer if this is local; i.e., everything it communicates
|
||||
* with for this channel returns true for is_local_addr(). This is used
|
||||
* to decide whether to declare reachability when we receive something on
|
||||
* this channel in circuitbuild.c
|
||||
*/
|
||||
unsigned int is_local:1;
|
||||
|
||||
/** Have we logged a warning about circID exhaustion on this channel?
|
||||
* If so, when? */
|
||||
ratelim_t last_warned_circ_ids_exhausted;
|
||||
|
||||
/** Channel timestamps for cell channels */
|
||||
time_t timestamp_client; /*(< Client used this, according to relay.c */
|
||||
time_t timestamp_recv; /**< Cell received from lower layer */
|
||||
time_t timestamp_xmit; /**< Cell sent to lower layer */
|
||||
|
||||
/** Timestamp for run_connection_housekeeping(). We update this once a
|
||||
* second when we run housekeeping and find a circuit on this channel, and
|
||||
* whenever we add a circuit to the channel. */
|
||||
time_t timestamp_last_had_circuits;
|
||||
|
||||
/** Unique ID for measuring direct network status requests;vtunneled ones
|
||||
* come over a circuit_t, which has a dirreq_id field as well, but is a
|
||||
* distinct namespace. */
|
||||
uint64_t dirreq_id;
|
||||
|
||||
/** Channel counters for cells and bytes we have received. */
|
||||
uint64_t n_cells_recved, n_bytes_recved;
|
||||
/** Channel counters for cells and bytes we have sent. */
|
||||
uint64_t n_cells_xmitted, n_bytes_xmitted;
|
||||
};
|
||||
|
||||
struct channel_listener_t {
|
||||
/** Current channel listener state */
|
||||
channel_listener_state_t state;
|
||||
|
||||
/** Globally unique ID number for a channel over the lifetime of a Tor
|
||||
* process.
|
||||
*/
|
||||
uint64_t global_identifier;
|
||||
|
||||
/** Should we expect to see this channel in the channel lists? */
|
||||
unsigned char registered:1;
|
||||
|
||||
/** Why did we close?
|
||||
*/
|
||||
enum {
|
||||
CHANNEL_LISTENER_NOT_CLOSING = 0,
|
||||
CHANNEL_LISTENER_CLOSE_REQUESTED,
|
||||
CHANNEL_LISTENER_CLOSE_FROM_BELOW,
|
||||
CHANNEL_LISTENER_CLOSE_FOR_ERROR
|
||||
} reason_for_closing;
|
||||
|
||||
/** Timestamps for both cell channels and listeners */
|
||||
time_t timestamp_created; /* Channel created */
|
||||
time_t timestamp_active; /* Any activity */
|
||||
|
||||
/* Methods implemented by the lower layer */
|
||||
|
||||
/** Free a channel */
|
||||
void (*free_fn)(channel_listener_t *);
|
||||
/** Close an open channel */
|
||||
void (*close)(channel_listener_t *);
|
||||
/** Describe the transport subclass for this channel */
|
||||
const char * (*describe_transport)(channel_listener_t *);
|
||||
/** Optional method to dump transport-specific statistics on the channel */
|
||||
void (*dumpstats)(channel_listener_t *, int);
|
||||
|
||||
/** Registered listen handler to call on incoming connection */
|
||||
channel_listener_fn_ptr listener;
|
||||
|
||||
/** List of pending incoming connections */
|
||||
smartlist_t *incoming_list;
|
||||
|
||||
/** Timestamps for listeners */
|
||||
time_t timestamp_accepted;
|
||||
|
||||
/** Counters for listeners */
|
||||
uint64_t n_accepted;
|
||||
};
|
||||
|
||||
/* Channel state manipulations */
|
||||
|
||||
int channel_state_is_valid(channel_state_t state);
|
||||
int channel_listener_state_is_valid(channel_listener_state_t state);
|
||||
|
||||
int channel_state_can_transition(channel_state_t from, channel_state_t to);
|
||||
int channel_listener_state_can_transition(channel_listener_state_t from,
|
||||
channel_listener_state_t to);
|
||||
|
||||
const char * channel_state_to_string(channel_state_t state);
|
||||
const char *
|
||||
channel_listener_state_to_string(channel_listener_state_t state);
|
||||
|
||||
/* Abstract channel operations */
|
||||
|
||||
void channel_mark_for_close(channel_t *chan);
|
||||
int channel_write_packed_cell(channel_t *chan, packed_cell_t *cell);
|
||||
|
||||
void channel_listener_mark_for_close(channel_listener_t *chan_l);
|
||||
void channel_mark_as_used_for_origin_circuit(channel_t *chan);
|
||||
|
||||
/* Channel callback registrations */
|
||||
|
||||
/* Listener callback */
|
||||
void channel_listener_set_listener_fn(channel_listener_t *chan,
|
||||
channel_listener_fn_ptr listener);
|
||||
|
||||
/* Incoming cell callbacks */
|
||||
channel_cell_handler_fn_ptr channel_get_cell_handler(channel_t *chan);
|
||||
|
||||
void channel_set_cell_handlers(channel_t *chan,
|
||||
channel_cell_handler_fn_ptr cell_handler);
|
||||
|
||||
/* Clean up closed channels and channel listeners periodically; these are
|
||||
* called from run_scheduled_events() in main.c.
|
||||
*/
|
||||
void channel_run_cleanup(void);
|
||||
void channel_listener_run_cleanup(void);
|
||||
|
||||
/* Close all channels and deallocate everything */
|
||||
void channel_free_all(void);
|
||||
|
||||
/* Dump some statistics in the log */
|
||||
void channel_dumpstats(int severity);
|
||||
void channel_listener_dumpstats(int severity);
|
||||
|
||||
#ifdef CHANNEL_OBJECT_PRIVATE
|
||||
|
||||
#ifdef CHANNEL_FILE_PRIVATE
|
||||
|
||||
STATIC void channel_add_to_digest_map(channel_t *chan);
|
||||
STATIC bool channel_matches_target_addr_for_extend(
|
||||
channel_t *chan,
|
||||
const tor_addr_t *target_ipv4_addr,
|
||||
const tor_addr_t *target_ipv6_addr);
|
||||
#endif /* defined(CHANNEL_FILE_PRIVATE) */
|
||||
|
||||
/* Channel operations for subclasses and internal use only */
|
||||
|
||||
/* Initialize a newly allocated channel - do this first in subclass
|
||||
* constructors.
|
||||
*/
|
||||
|
||||
void channel_init(channel_t *chan);
|
||||
void channel_init_listener(channel_listener_t *chan);
|
||||
|
||||
/* Channel registration/unregistration */
|
||||
void channel_register(channel_t *chan);
|
||||
void channel_unregister(channel_t *chan);
|
||||
|
||||
/* Channel listener registration/unregistration */
|
||||
void channel_listener_register(channel_listener_t *chan_l);
|
||||
void channel_listener_unregister(channel_listener_t *chan_l);
|
||||
|
||||
/* Close from below */
|
||||
void channel_close_from_lower_layer(channel_t *chan);
|
||||
void channel_close_for_error(channel_t *chan);
|
||||
void channel_closed(channel_t *chan);
|
||||
|
||||
/* Free a channel */
|
||||
void channel_free_(channel_t *chan);
|
||||
#define channel_free(chan) FREE_AND_NULL(channel_t, channel_free_, (chan))
|
||||
void channel_listener_free_(channel_listener_t *chan_l);
|
||||
#define channel_listener_free(chan_l) \
|
||||
FREE_AND_NULL(channel_listener_t, channel_listener_free_, (chan_l))
|
||||
|
||||
/* State/metadata setters */
|
||||
|
||||
void channel_change_state(channel_t *chan, channel_state_t to_state);
|
||||
void channel_change_state_open(channel_t *chan);
|
||||
void channel_clear_identity_digest(channel_t *chan);
|
||||
void channel_clear_remote_end(channel_t *chan);
|
||||
void channel_mark_local(channel_t *chan);
|
||||
void channel_mark_incoming(channel_t *chan);
|
||||
void channel_mark_outgoing(channel_t *chan);
|
||||
void channel_mark_remote(channel_t *chan);
|
||||
void channel_set_identity_digest(channel_t *chan,
|
||||
const char *identity_digest,
|
||||
const struct ed25519_public_key_t *ed_identity);
|
||||
|
||||
void channel_listener_change_state(channel_listener_t *chan_l,
|
||||
channel_listener_state_t to_state);
|
||||
|
||||
/* Timestamp updates */
|
||||
void channel_timestamp_created(channel_t *chan);
|
||||
void channel_timestamp_active(channel_t *chan);
|
||||
void channel_timestamp_recv(channel_t *chan);
|
||||
void channel_timestamp_xmit(channel_t *chan);
|
||||
|
||||
void channel_listener_timestamp_created(channel_listener_t *chan_l);
|
||||
void channel_listener_timestamp_active(channel_listener_t *chan_l);
|
||||
void channel_listener_timestamp_accepted(channel_listener_t *chan_l);
|
||||
|
||||
/* Incoming channel handling */
|
||||
void channel_listener_process_incoming(channel_listener_t *listener);
|
||||
void channel_listener_queue_incoming(channel_listener_t *listener,
|
||||
channel_t *incoming);
|
||||
|
||||
/* Incoming cell handling */
|
||||
void channel_process_cell(channel_t *chan, cell_t *cell);
|
||||
|
||||
/* Request from lower layer for more cells if available */
|
||||
MOCK_DECL(ssize_t, channel_flush_some_cells,
|
||||
(channel_t *chan, ssize_t num_cells));
|
||||
|
||||
/* Query if data available on this channel */
|
||||
MOCK_DECL(int, channel_more_to_flush, (channel_t *chan));
|
||||
|
||||
/* Notify flushed outgoing for dirreq handling */
|
||||
void channel_notify_flushed(channel_t *chan);
|
||||
|
||||
/* Handle stuff we need to do on open like notifying circuits */
|
||||
void channel_do_open_actions(channel_t *chan);
|
||||
|
||||
#endif /* defined(CHANNEL_OBJECT_PRIVATE) */
|
||||
|
||||
/* Helper functions to perform operations on channels */
|
||||
|
||||
int channel_send_destroy(circid_t circ_id, channel_t *chan,
|
||||
int reason);
|
||||
|
||||
/*
|
||||
* Outside abstract interfaces that should eventually get turned into
|
||||
* something transport/address format independent.
|
||||
*/
|
||||
|
||||
channel_t * channel_connect(const tor_addr_t *addr, uint16_t port,
|
||||
const char *rsa_id_digest,
|
||||
const struct ed25519_public_key_t *ed_id);
|
||||
|
||||
MOCK_DECL(channel_t *, channel_get_for_extend,(
|
||||
const char *rsa_id_digest,
|
||||
const struct ed25519_public_key_t *ed_id,
|
||||
const tor_addr_t *target_ipv4_addr,
|
||||
const tor_addr_t *target_ipv6_addr,
|
||||
bool for_origin_circ,
|
||||
const char **msg_out,
|
||||
int *launch_out));
|
||||
|
||||
/* Ask which of two channels is better for circuit-extension purposes */
|
||||
int channel_is_better(channel_t *a, channel_t *b);
|
||||
|
||||
/** Channel lookups
|
||||
*/
|
||||
|
||||
channel_t * channel_find_by_global_id(uint64_t global_identifier);
|
||||
channel_t * channel_find_by_remote_identity(const char *rsa_id_digest,
|
||||
const struct ed25519_public_key_t *ed_id);
|
||||
|
||||
/** For things returned by channel_find_by_remote_digest(), walk the list.
|
||||
* The RSA key will match for all returned elements; the Ed25519 key might not.
|
||||
*/
|
||||
channel_t * channel_next_with_rsa_identity(channel_t *chan);
|
||||
|
||||
/*
|
||||
* Helper macros to lookup state of given channel.
|
||||
*/
|
||||
|
||||
#define CHANNEL_IS_CLOSED(chan) (channel_is_in_state((chan), \
|
||||
CHANNEL_STATE_CLOSED))
|
||||
#define CHANNEL_IS_OPENING(chan) (channel_is_in_state((chan), \
|
||||
CHANNEL_STATE_OPENING))
|
||||
#define CHANNEL_IS_OPEN(chan) (channel_is_in_state((chan), \
|
||||
CHANNEL_STATE_OPEN))
|
||||
#define CHANNEL_IS_MAINT(chan) (channel_is_in_state((chan), \
|
||||
CHANNEL_STATE_MAINT))
|
||||
#define CHANNEL_IS_CLOSING(chan) (channel_is_in_state((chan), \
|
||||
CHANNEL_STATE_CLOSING))
|
||||
#define CHANNEL_IS_ERROR(chan) (channel_is_in_state((chan), \
|
||||
CHANNEL_STATE_ERROR))
|
||||
|
||||
#define CHANNEL_FINISHED(chan) (CHANNEL_IS_CLOSED(chan) || \
|
||||
CHANNEL_IS_ERROR(chan))
|
||||
|
||||
#define CHANNEL_CONDEMNED(chan) (CHANNEL_IS_CLOSING(chan) || \
|
||||
CHANNEL_FINISHED(chan))
|
||||
|
||||
#define CHANNEL_CAN_HANDLE_CELLS(chan) (CHANNEL_IS_OPENING(chan) || \
|
||||
CHANNEL_IS_OPEN(chan) || \
|
||||
CHANNEL_IS_MAINT(chan))
|
||||
|
||||
static inline int
|
||||
channel_is_in_state(channel_t *chan, channel_state_t state)
|
||||
{
|
||||
return chan->state == state;
|
||||
}
|
||||
|
||||
/*
|
||||
* Metadata queries/updates
|
||||
*/
|
||||
|
||||
const char * channel_describe_transport(channel_t *chan);
|
||||
MOCK_DECL(void, channel_dump_statistics, (channel_t *chan, int severity));
|
||||
void channel_dump_transport_statistics(channel_t *chan, int severity);
|
||||
MOCK_DECL(int, channel_get_addr_if_possible, (const channel_t *chan,
|
||||
tor_addr_t *addr_out));
|
||||
MOCK_DECL(const char *, channel_describe_peer,(channel_t *chan));
|
||||
int channel_has_queued_writes(channel_t *chan);
|
||||
int channel_is_bad_for_new_circs(channel_t *chan);
|
||||
void channel_mark_bad_for_new_circs(channel_t *chan);
|
||||
int channel_is_canonical(channel_t *chan);
|
||||
int channel_is_client(const channel_t *chan);
|
||||
int channel_is_local(channel_t *chan);
|
||||
int channel_is_incoming(channel_t *chan);
|
||||
int channel_is_outgoing(channel_t *chan);
|
||||
void channel_mark_client(channel_t *chan);
|
||||
void channel_clear_client(channel_t *chan);
|
||||
int channel_matches_extend_info(channel_t *chan, extend_info_t *extend_info);
|
||||
int channel_remote_identity_matches(const channel_t *chan,
|
||||
const char *rsa_id_digest,
|
||||
const ed25519_public_key_t *ed_id);
|
||||
unsigned int channel_num_circuits(channel_t *chan);
|
||||
MOCK_DECL(void,channel_set_circid_type,(channel_t *chan,
|
||||
crypto_pk_t *identity_rcvd,
|
||||
int consider_identity));
|
||||
void channel_timestamp_client(channel_t *chan);
|
||||
|
||||
const char * channel_listener_describe_transport(channel_listener_t *chan_l);
|
||||
void channel_listener_dump_statistics(channel_listener_t *chan_l,
|
||||
int severity);
|
||||
void channel_listener_dump_transport_statistics(channel_listener_t *chan_l,
|
||||
int severity);
|
||||
void channel_check_for_duplicates(void);
|
||||
|
||||
void channel_update_bad_for_new_circs(const char *digest, int force);
|
||||
|
||||
/* Flow control queries */
|
||||
int channel_num_cells_writeable(channel_t *chan);
|
||||
|
||||
/* Timestamp queries */
|
||||
time_t channel_when_created(channel_t *chan);
|
||||
time_t channel_when_last_client(channel_t *chan);
|
||||
time_t channel_when_last_xmit(channel_t *chan);
|
||||
|
||||
/* Counter queries */
|
||||
int packed_cell_is_destroy(channel_t *chan,
|
||||
const packed_cell_t *packed_cell,
|
||||
circid_t *circid_out);
|
||||
|
||||
/* Declare the handle helpers */
|
||||
HANDLE_DECL(channel, channel_t,)
|
||||
#define channel_handle_free(h) \
|
||||
FREE_AND_NULL(channel_handle_t, channel_handle_free_, (h))
|
||||
#undef tor_timer_t
|
||||
|
||||
#endif /* !defined(TOR_CHANNEL_H) */
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file circuitbuild.h
|
||||
* \brief Header file for circuitbuild.c.
|
||||
**/
|
||||
#ifndef TOR_CHANNELPADDING_H
|
||||
#define TOR_CHANNELPADDING_H
|
||||
|
||||
#include "trunnel/channelpadding_negotiation.h"
|
||||
|
||||
#define CHANNELPADDING_SOS_PARAM "nf_pad_single_onion"
|
||||
#define CHANNELPADDING_SOS_DEFAULT 1
|
||||
|
||||
typedef enum {
|
||||
CHANNELPADDING_WONTPAD,
|
||||
CHANNELPADDING_PADLATER,
|
||||
CHANNELPADDING_PADDING_SCHEDULED,
|
||||
CHANNELPADDING_PADDING_ALREADY_SCHEDULED,
|
||||
CHANNELPADDING_PADDING_SENT,
|
||||
} channelpadding_decision_t;
|
||||
|
||||
channelpadding_decision_t channelpadding_decide_to_pad_channel(channel_t
|
||||
*chan);
|
||||
int channelpadding_update_padding_for_channel(channel_t *,
|
||||
const channelpadding_negotiate_t
|
||||
*chan);
|
||||
|
||||
void channelpadding_disable_padding_on_channel(channel_t *chan);
|
||||
void channelpadding_reduce_padding_on_channel(channel_t *chan);
|
||||
int channelpadding_send_enable_command(channel_t *chan, uint16_t low_timeout,
|
||||
uint16_t high_timeout);
|
||||
|
||||
int channelpadding_get_circuits_available_timeout(void);
|
||||
unsigned int channelpadding_get_channel_idle_timeout(const channel_t *, int);
|
||||
void channelpadding_new_consensus_params(const networkstatus_t *ns);
|
||||
|
||||
#endif /* !defined(TOR_CHANNELPADDING_H) */
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
/* * Copyright (c) 2012-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file channeltls.h
|
||||
* \brief Header file for channeltls.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_CHANNELTLS_H
|
||||
#define TOR_CHANNELTLS_H
|
||||
|
||||
#include "core/or/or.h"
|
||||
#include "core/or/channel.h"
|
||||
|
||||
struct ed25519_public_key_t;
|
||||
struct curve25519_public_key_t;
|
||||
|
||||
#define TLS_PER_CELL_OVERHEAD 29
|
||||
|
||||
#define BASE_CHAN_TO_TLS(c) (channel_tls_from_base((c)))
|
||||
#define TLS_CHAN_TO_BASE(c) (channel_tls_to_base((c)))
|
||||
#define CONST_BASE_CHAN_TO_TLS(c) (channel_tls_from_base_const((c)))
|
||||
#define CONST_TLS_CHAN_TO_BASE(c) (channel_tls_to_base_const((c)))
|
||||
|
||||
#define TLS_CHAN_MAGIC 0x8a192427U
|
||||
|
||||
#ifdef CHANNEL_OBJECT_PRIVATE
|
||||
|
||||
struct channel_tls_t {
|
||||
/* Base channel_t struct */
|
||||
channel_t base_;
|
||||
/* or_connection_t pointer */
|
||||
or_connection_t *conn;
|
||||
};
|
||||
|
||||
#endif /* defined(CHANNEL_OBJECT_PRIVATE) */
|
||||
|
||||
channel_t * channel_tls_connect(const tor_addr_t *addr, uint16_t port,
|
||||
const char *id_digest,
|
||||
const struct ed25519_public_key_t *ed_id);
|
||||
channel_listener_t * channel_tls_get_listener(void);
|
||||
channel_listener_t * channel_tls_start_listener(void);
|
||||
channel_t * channel_tls_handle_incoming(or_connection_t *orconn);
|
||||
|
||||
/* Casts */
|
||||
|
||||
channel_t * channel_tls_to_base(channel_tls_t *tlschan);
|
||||
channel_tls_t * channel_tls_from_base(channel_t *chan);
|
||||
const channel_t * channel_tls_to_base_const(const channel_tls_t *tlschan);
|
||||
const channel_tls_t * channel_tls_from_base_const(const channel_t *chan);
|
||||
|
||||
/* Things for connection_or.c to call back into */
|
||||
void channel_tls_handle_cell(cell_t *cell, or_connection_t *conn);
|
||||
void channel_tls_handle_state_change_on_orconn(channel_tls_t *chan,
|
||||
or_connection_t *conn,
|
||||
uint8_t state);
|
||||
void channel_tls_handle_var_cell(var_cell_t *var_cell,
|
||||
or_connection_t *conn);
|
||||
void channel_tls_update_marks(or_connection_t *conn);
|
||||
|
||||
/* Cleanup at shutdown */
|
||||
void channel_tls_free_all(void);
|
||||
|
||||
extern uint64_t stats_n_authorize_cells_processed;
|
||||
extern uint64_t stats_n_authenticate_cells_processed;
|
||||
extern uint64_t stats_n_versions_cells_processed;
|
||||
extern uint64_t stats_n_netinfo_cells_processed;
|
||||
extern uint64_t stats_n_vpadding_cells_processed;
|
||||
extern uint64_t stats_n_certs_cells_processed;
|
||||
extern uint64_t stats_n_auth_challenge_cells_processed;
|
||||
|
||||
#ifdef CHANNELTLS_PRIVATE
|
||||
STATIC void channel_tls_process_certs_cell(var_cell_t *cell,
|
||||
channel_tls_t *tlschan);
|
||||
STATIC void channel_tls_process_auth_challenge_cell(var_cell_t *cell,
|
||||
channel_tls_t *tlschan);
|
||||
STATIC void channel_tls_common_init(channel_tls_t *tlschan);
|
||||
STATIC void channel_tls_process_authenticate_cell(var_cell_t *cell,
|
||||
channel_tls_t *tlschan);
|
||||
#endif /* defined(CHANNELTLS_PRIVATE) */
|
||||
|
||||
#endif /* !defined(TOR_CHANNELTLS_H) */
|
||||
-274
@@ -1,274 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* @file circuit_st.h
|
||||
* @brief Base circuit structure.
|
||||
**/
|
||||
|
||||
#ifndef CIRCUIT_ST_H
|
||||
#define CIRCUIT_ST_H
|
||||
|
||||
#include "core/or/or.h"
|
||||
|
||||
#include "lib/container/handles.h"
|
||||
|
||||
#include "core/or/cell_queue_st.h"
|
||||
#include "ext/ht.h"
|
||||
|
||||
struct hs_token_t;
|
||||
struct circpad_machine_spec_t;
|
||||
struct circpad_machine_runtime_t;
|
||||
struct congestion_control_t;
|
||||
|
||||
/** Number of padding state machines on a circuit. */
|
||||
#define CIRCPAD_MAX_MACHINES (2)
|
||||
|
||||
/** "magic" value for an origin_circuit_t */
|
||||
#define ORIGIN_CIRCUIT_MAGIC 0x35315243u
|
||||
/** "magic" value for an or_circuit_t */
|
||||
#define OR_CIRCUIT_MAGIC 0x98ABC04Fu
|
||||
/** "magic" value for a circuit that would have been freed by circuit_free,
|
||||
* but which we're keeping around until a cpuworker reply arrives. See
|
||||
* circuit_free() for more documentation. */
|
||||
#define DEAD_CIRCUIT_MAGIC 0xdeadc14c
|
||||
|
||||
/**
|
||||
* A circuit is a path over the onion routing
|
||||
* network. Applications can connect to one end of the circuit, and can
|
||||
* create exit connections at the other end of the circuit. AP and exit
|
||||
* connections have only one circuit associated with them (and thus these
|
||||
* connection types are closed when the circuit is closed), whereas
|
||||
* OR connections multiplex many circuits at once, and stay standing even
|
||||
* when there are no circuits running over them.
|
||||
*
|
||||
* A circuit_t structure can fill one of two roles. First, a or_circuit_t
|
||||
* links two connections together: either an edge connection and an OR
|
||||
* connection, or two OR connections. (When joined to an OR connection, a
|
||||
* circuit_t affects only cells sent to a particular circID on that
|
||||
* connection. When joined to an edge connection, a circuit_t affects all
|
||||
* data.)
|
||||
|
||||
* Second, an origin_circuit_t holds the cipher keys and state for sending data
|
||||
* along a given circuit. At the OP, it has a sequence of ciphers, each
|
||||
* of which is shared with a single OR along the circuit. Separate
|
||||
* ciphers are used for data going "forward" (away from the OP) and
|
||||
* "backward" (towards the OP). At the OR, a circuit has only two stream
|
||||
* ciphers: one for data going forward, and one for data going backward.
|
||||
*/
|
||||
struct circuit_t {
|
||||
uint32_t magic; /**< For memory and type debugging: must equal
|
||||
* ORIGIN_CIRCUIT_MAGIC or OR_CIRCUIT_MAGIC. */
|
||||
|
||||
/** Handle entry for handle-based lookup */
|
||||
HANDLE_ENTRY(circuit, circuit_t);
|
||||
|
||||
/** The channel that is next in this circuit. */
|
||||
channel_t *n_chan;
|
||||
|
||||
/**
|
||||
* The circuit_id used in the next (forward) hop of this circuit;
|
||||
* this is unique to n_chan, but this ordered pair is globally
|
||||
* unique:
|
||||
*
|
||||
* (n_chan->global_identifier, n_circ_id)
|
||||
*/
|
||||
circid_t n_circ_id;
|
||||
|
||||
/** Queue of cells waiting to be transmitted on n_chan */
|
||||
cell_queue_t n_chan_cells;
|
||||
|
||||
/**
|
||||
* The hop to which we want to extend this circuit. Should be NULL if
|
||||
* the circuit has attached to a channel.
|
||||
*/
|
||||
extend_info_t *n_hop;
|
||||
|
||||
/** True iff we are waiting for n_chan_cells to become less full before
|
||||
* allowing any more cells on this circuit. (Origin circuit only.) */
|
||||
unsigned int circuit_blocked_on_n_chan : 1;
|
||||
/** True iff we are waiting for p_chan_cells to become less full before
|
||||
* allowing any more cells on this circuit. (OR circuit only.) */
|
||||
unsigned int circuit_blocked_on_p_chan : 1;
|
||||
|
||||
/** True iff we have queued a delete backwards on this circuit, but not put
|
||||
* it on the output buffer. */
|
||||
unsigned int p_delete_pending : 1;
|
||||
/** True iff we have queued a delete forwards on this circuit, but not put
|
||||
* it on the output buffer. */
|
||||
unsigned int n_delete_pending : 1;
|
||||
|
||||
/** True iff this circuit has received a DESTROY cell in either direction */
|
||||
unsigned int received_destroy : 1;
|
||||
|
||||
/** True iff we have sent a sufficiently random data cell since last
|
||||
* we reset send_randomness_after_n_cells. */
|
||||
unsigned int have_sent_sufficiently_random_cell : 1;
|
||||
|
||||
uint8_t state; /**< Current status of this circuit. */
|
||||
uint8_t purpose; /**< Why are we creating this circuit? */
|
||||
|
||||
/** How many relay data cells can we package (read from edge streams)
|
||||
* on this circuit before we receive a circuit-level sendme cell asking
|
||||
* for more? */
|
||||
int package_window;
|
||||
/** How many relay data cells will we deliver (write to edge streams)
|
||||
* on this circuit? When deliver_window gets low, we send some
|
||||
* circuit-level sendme cells to indicate that we're willing to accept
|
||||
* more. */
|
||||
int deliver_window;
|
||||
/**
|
||||
* How many cells do we have until we need to send one that contains
|
||||
* sufficient randomness? Used to ensure that authenticated SENDME cells
|
||||
* will reflect some unpredictable information.
|
||||
**/
|
||||
uint16_t send_randomness_after_n_cells;
|
||||
|
||||
/** FIFO containing the digest of the cells that are just before a SENDME is
|
||||
* sent by the client. It is done at the last cell before our package_window
|
||||
* goes down to 0 which is when we expect a SENDME.
|
||||
*
|
||||
* Our current circuit package window is capped to 1000
|
||||
* (CIRCWINDOW_START_MAX) which is also the start value. The increment is
|
||||
* set to 100 (CIRCWINDOW_INCREMENT) which means we don't allow more than
|
||||
* 1000/100 = 10 outstanding SENDME cells worth of data. Meaning that this
|
||||
* list can not contain more than 10 digests of DIGEST_LEN bytes (20).
|
||||
*
|
||||
* At position i in the list, the digest corresponds to the
|
||||
* (CIRCWINDOW_INCREMENT * i)-nth cell received since we expect a SENDME to
|
||||
* be received containing that cell digest.
|
||||
*
|
||||
* For example, position 2 (starting at 0) means that we've received 300
|
||||
* cells so the 300th cell digest is kept at index 2.
|
||||
*
|
||||
* At maximum, this list contains 200 bytes plus the smartlist overhead. */
|
||||
smartlist_t *sendme_last_digests;
|
||||
|
||||
/** Temporary field used during circuits_handle_oom. */
|
||||
uint32_t age_tmp;
|
||||
|
||||
/** For storage while n_chan is pending (state CIRCUIT_STATE_CHAN_WAIT). */
|
||||
struct create_cell_t *n_chan_create_cell;
|
||||
|
||||
/** When did circuit construction actually begin (ie send the
|
||||
* CREATE cell or begin cannibalization).
|
||||
*
|
||||
* Note: This timer will get reset if we decide to cannibalize
|
||||
* a circuit. It may also get reset during certain phases of hidden
|
||||
* service circuit use.
|
||||
*
|
||||
* We keep this timestamp with a higher resolution than most so that the
|
||||
* circuit-build-time tracking code can get millisecond resolution.
|
||||
*/
|
||||
struct timeval timestamp_began;
|
||||
|
||||
/** This timestamp marks when the init_circuit_base constructor ran. */
|
||||
struct timeval timestamp_created;
|
||||
|
||||
/** When the circuit was first used, or 0 if the circuit is clean.
|
||||
*
|
||||
* XXXX Note that some code will artificially adjust this value backward
|
||||
* in time in order to indicate that a circuit shouldn't be used for new
|
||||
* streams, but that it can stay alive as long as it has streams on it.
|
||||
* That's a kludge we should fix.
|
||||
*
|
||||
* XXX The CBT code uses this field to record when HS-related
|
||||
* circuits entered certain states. This usage probably won't
|
||||
* interfere with this field's primary purpose, but we should
|
||||
* document it more thoroughly to make sure of that.
|
||||
*
|
||||
* XXX The SocksPort option KeepaliveIsolateSOCKSAuth will artificially
|
||||
* adjust this value forward each time a suitable stream is attached to an
|
||||
* already constructed circuit, potentially keeping the circuit alive
|
||||
* indefinitely.
|
||||
*/
|
||||
time_t timestamp_dirty;
|
||||
|
||||
uint16_t marked_for_close; /**< Should we close this circuit at the end of
|
||||
* the main loop? (If true, holds the line number
|
||||
* where this circuit was marked.) */
|
||||
const char *marked_for_close_file; /**< For debugging: in which file was this
|
||||
* circuit marked for close? */
|
||||
/** For what reason (See END_CIRC_REASON...) is this circuit being closed?
|
||||
* This field is set in circuit_mark_for_close and used later in
|
||||
* circuit_about_to_free. */
|
||||
int marked_for_close_reason;
|
||||
/** As marked_for_close_reason, but reflects the underlying reason for
|
||||
* closing this circuit.
|
||||
*/
|
||||
int marked_for_close_orig_reason;
|
||||
|
||||
/** Unique ID for measuring tunneled network status requests. */
|
||||
uint64_t dirreq_id;
|
||||
|
||||
/** Index in smartlist of all circuits (global_circuitlist). */
|
||||
int global_circuitlist_idx;
|
||||
|
||||
/** Various statistics about cells being added to or removed from this
|
||||
* circuit's queues; used only if CELL_STATS events are enabled and
|
||||
* cleared after being sent to control port. */
|
||||
smartlist_t *testing_cell_stats;
|
||||
|
||||
/** If set, points to an HS token that this circuit might be carrying.
|
||||
* Used by the HS circuitmap. */
|
||||
struct hs_token_t *hs_token;
|
||||
/** Hashtable node: used to look up the circuit by its HS token using the HS
|
||||
circuitmap. */
|
||||
HT_ENTRY(circuit_t) hs_circuitmap_node;
|
||||
|
||||
/** Adaptive Padding state machines: these are immutable. The state machines
|
||||
* that come from the consensus are saved to a global structure, to avoid
|
||||
* per-circuit allocations. This merely points to the global copy in
|
||||
* origin_padding_machines or relay_padding_machines that should never
|
||||
* change or get deallocated.
|
||||
*
|
||||
* Each element of this array corresponds to a different padding machine,
|
||||
* and we can have up to CIRCPAD_MAX_MACHINES such machines. */
|
||||
const struct circpad_machine_spec_t *padding_machine[CIRCPAD_MAX_MACHINES];
|
||||
|
||||
/** Adaptive Padding machine runtime info for above machines. This is
|
||||
* the per-circuit mutable information, such as the current state and
|
||||
* histogram token counts. Some of it is optional (aka NULL).
|
||||
* If a machine is being shut down, these indexes can be NULL
|
||||
* without the corresponding padding_machine being NULL, while we
|
||||
* wait for the other end to respond to our shutdown request.
|
||||
*
|
||||
* Each element of this array corresponds to a different padding machine,
|
||||
* and we can have up to CIRCPAD_MAX_MACHINES such machines. */
|
||||
struct circpad_machine_runtime_t *padding_info[CIRCPAD_MAX_MACHINES];
|
||||
|
||||
/** padding_machine_ctr increments each time a new padding machine
|
||||
* is negotiated. It is used for shutdown conditions, to ensure
|
||||
* that STOP commands actually correspond to the current machine,
|
||||
* and not a previous one. */
|
||||
uint32_t padding_machine_ctr;
|
||||
|
||||
/** Congestion control fields */
|
||||
struct congestion_control_t *ccontrol;
|
||||
|
||||
/** Conflux linked circuit information.
|
||||
*
|
||||
* If this is non-NULL, the circuit is linked and part of a usable set,
|
||||
* and for origin_circuit_t subtypes, the circuit purpose is
|
||||
* CIRCUIT_PURPOSE_CONFLUX_LINKED.
|
||||
*
|
||||
* If this is NULL, the circuit could still be part of a pending conflux
|
||||
* object, in which case the conflux_pending_nonce field is set, and for
|
||||
* origin_circuit_t subtypes, the purpose is
|
||||
* CIRCUIT_PURPOSE_CONFLUX_UNLINKED.
|
||||
*/
|
||||
struct conflux_t *conflux;
|
||||
|
||||
/** If set, this circuit is considered *unlinked* and in the pending pool.
|
||||
* The nonce value is used to find the other legs. Origin circuits that
|
||||
* have this set are in the CIRCUIT_PURPOSE_CONFLUX_UNLINKED purpose.
|
||||
*
|
||||
* If this is NULL, and conflux object is set, it means this circuit is
|
||||
* linked and thus part of a usable set. */
|
||||
uint8_t *conflux_pending_nonce;
|
||||
};
|
||||
|
||||
#endif /* !defined(CIRCUIT_ST_H) */
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file circuitbuild.h
|
||||
* \brief Header file for circuitbuild.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_CIRCUITBUILD_H
|
||||
#define TOR_CIRCUITBUILD_H
|
||||
|
||||
struct ed25519_public_key_t;
|
||||
struct curve25519_public_key_t;
|
||||
|
||||
int route_len_for_purpose(uint8_t purpose, extend_info_t *exit_ei);
|
||||
char *circuit_list_path(origin_circuit_t *circ, int verbose);
|
||||
char *circuit_list_path_for_controller(origin_circuit_t *circ);
|
||||
void circuit_log_path(int severity, unsigned int domain,
|
||||
origin_circuit_t *circ);
|
||||
origin_circuit_t *origin_circuit_init(uint8_t purpose, int flags);
|
||||
origin_circuit_t *circuit_establish_circuit(uint8_t purpose,
|
||||
extend_info_t *exit,
|
||||
int flags);
|
||||
MOCK_DECL(origin_circuit_t *, circuit_establish_circuit_conflux, (
|
||||
const uint8_t *nonce,
|
||||
uint8_t purpose,
|
||||
extend_info_t *exit,
|
||||
int flags));
|
||||
|
||||
struct circuit_guard_state_t *origin_circuit_get_guard_state(
|
||||
origin_circuit_t *circ);
|
||||
int circuit_handle_first_hop(origin_circuit_t *circ);
|
||||
void circuit_n_chan_done(channel_t *chan, int status,
|
||||
int close_origin_circuits);
|
||||
int circuit_timeout_want_to_count_circ(const origin_circuit_t *circ);
|
||||
int circuit_send_next_onion_skin(origin_circuit_t *circ);
|
||||
void circuit_note_clock_jumped(int64_t seconds_elapsed, bool was_idle);
|
||||
struct created_cell_t;
|
||||
int circuit_finish_handshake(origin_circuit_t *circ,
|
||||
const struct created_cell_t *created_cell);
|
||||
int circuit_truncated(origin_circuit_t *circ, int reason);
|
||||
MOCK_DECL(int, circuit_all_predicted_ports_handled, (time_t now,
|
||||
int *need_uptime,
|
||||
int *need_capacity));
|
||||
|
||||
int circuit_append_new_exit(origin_circuit_t *circ, extend_info_t *info);
|
||||
int circuit_extend_to_new_exit(origin_circuit_t *circ, extend_info_t *info);
|
||||
int circuit_can_use_tap(const origin_circuit_t *circ);
|
||||
int circuit_has_usable_onion_key(const origin_circuit_t *circ);
|
||||
const uint8_t *build_state_get_exit_rsa_id(cpath_build_state_t *state);
|
||||
MOCK_DECL(const node_t *,
|
||||
build_state_get_exit_node,(cpath_build_state_t *state));
|
||||
const char *build_state_get_exit_nickname(cpath_build_state_t *state);
|
||||
|
||||
struct circuit_guard_state_t;
|
||||
|
||||
const node_t *choose_good_entry_server(const origin_circuit_t *circ,
|
||||
uint8_t purpose,
|
||||
cpath_build_state_t *state,
|
||||
struct circuit_guard_state_t **guard_state_out);
|
||||
void circuit_upgrade_circuits_from_guard_wait(void);
|
||||
|
||||
MOCK_DECL(channel_t *, channel_connect_for_circuit,(const extend_info_t *ei));
|
||||
|
||||
struct create_cell_t;
|
||||
MOCK_DECL(int,
|
||||
circuit_deliver_create_cell,(circuit_t *circ,
|
||||
const struct create_cell_t *create_cell,
|
||||
int relayed));
|
||||
|
||||
int client_circ_negotiation_message(const extend_info_t *ei,
|
||||
uint8_t **msg_out,
|
||||
size_t *msg_len_out);
|
||||
|
||||
#ifdef CIRCUITBUILD_PRIVATE
|
||||
STATIC circid_t get_unique_circ_id_by_chan(channel_t *chan);
|
||||
STATIC int new_route_len(uint8_t purpose, extend_info_t *exit_ei,
|
||||
const smartlist_t *nodes);
|
||||
MOCK_DECL(STATIC int, count_acceptable_nodes, (const smartlist_t *nodes,
|
||||
int direct));
|
||||
|
||||
STATIC int onion_extend_cpath(origin_circuit_t *circ);
|
||||
|
||||
STATIC int
|
||||
onion_pick_cpath_exit(origin_circuit_t *circ, extend_info_t *exit_ei);
|
||||
STATIC int cpath_build_state_to_crn_flags(const cpath_build_state_t *state);
|
||||
STATIC int cpath_build_state_to_crn_ipv6_extend_flag(
|
||||
const cpath_build_state_t *state,
|
||||
int cur_len);
|
||||
|
||||
#endif /* defined(CIRCUITBUILD_PRIVATE) */
|
||||
|
||||
#endif /* !defined(TOR_CIRCUITBUILD_H) */
|
||||
-273
@@ -1,273 +0,0 @@
|
||||
/* Copyright (c) 2001 Matej Pfajfar.
|
||||
* Copyright (c) 2001-2004, Roger Dingledine.
|
||||
* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
|
||||
* Copyright (c) 2007-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file circuitlist.h
|
||||
* \brief Header file for circuitlist.c.
|
||||
**/
|
||||
|
||||
#ifndef TOR_CIRCUITLIST_H
|
||||
#define TOR_CIRCUITLIST_H
|
||||
|
||||
#include "lib/container/handles.h"
|
||||
#include "lib/testsupport/testsupport.h"
|
||||
#include "feature/hs/hs_ident.h"
|
||||
#include "core/or/ocirc_event.h"
|
||||
|
||||
/** Circuit state: I'm the origin, still haven't done all my handshakes. */
|
||||
#define CIRCUIT_STATE_BUILDING 0
|
||||
/** Circuit state: Waiting to process the onionskin. */
|
||||
#define CIRCUIT_STATE_ONIONSKIN_PENDING 1
|
||||
/** Circuit state: I'd like to deliver a create, but my n_chan is still
|
||||
* connecting. */
|
||||
#define CIRCUIT_STATE_CHAN_WAIT 2
|
||||
/** Circuit state: the circuit is open but we don't want to actually use it
|
||||
* until we find out if a better guard will be available.
|
||||
*/
|
||||
#define CIRCUIT_STATE_GUARD_WAIT 3
|
||||
/** Circuit state: onionskin(s) processed, ready to send/receive cells. */
|
||||
#define CIRCUIT_STATE_OPEN 4
|
||||
|
||||
#define CIRCUIT_PURPOSE_MIN_ 1
|
||||
|
||||
/* these circuits were initiated elsewhere */
|
||||
#define CIRCUIT_PURPOSE_OR_MIN_ 1
|
||||
/** OR-side circuit purpose: normal circuit, at OR. */
|
||||
#define CIRCUIT_PURPOSE_OR 1
|
||||
/** OR-side circuit purpose: At OR, from the service, waiting for intro from
|
||||
* clients. */
|
||||
#define CIRCUIT_PURPOSE_INTRO_POINT 2
|
||||
/** OR-side circuit purpose: At OR, from the client, waiting for the service.
|
||||
*/
|
||||
#define CIRCUIT_PURPOSE_REND_POINT_WAITING 3
|
||||
/** OR-side circuit purpose: At OR, both circuits have this purpose. */
|
||||
#define CIRCUIT_PURPOSE_REND_ESTABLISHED 4
|
||||
#define CIRCUIT_PURPOSE_OR_MAX_ 4
|
||||
|
||||
/* these circuits originate at this node */
|
||||
|
||||
/* here's how circ client-side purposes work:
|
||||
* normal circuits are C_GENERAL.
|
||||
* circuits that are c_introducing are either on their way to
|
||||
* becoming open, or they are open and waiting for a
|
||||
* suitable rendcirc before they send the intro.
|
||||
* circuits that are c_introduce_ack_wait have sent the intro,
|
||||
* but haven't gotten a response yet.
|
||||
* circuits that are c_establish_rend are either on their way
|
||||
* to becoming open, or they are open and have sent the
|
||||
* establish_rendezvous cell but haven't received an ack.
|
||||
* circuits that are c_rend_ready are open and have received a
|
||||
* rend ack, but haven't heard from the service yet.
|
||||
* circuits that are c_rend_ready_intro_acked are open, and
|
||||
* some intro circ has sent its intro and received an ack.
|
||||
* circuits that are c_rend_joined are open, have heard from
|
||||
* the service, and are talking to it.
|
||||
*/
|
||||
/** Client-side circuit purpose: Normal circuit, with cpath. */
|
||||
#define CIRCUIT_PURPOSE_C_GENERAL 5
|
||||
#define CIRCUIT_PURPOSE_C_HS_MIN_ 6
|
||||
/** Client-side circuit purpose: at the client, connecting to intro point. */
|
||||
#define CIRCUIT_PURPOSE_C_INTRODUCING 6
|
||||
/** Client-side circuit purpose: at the client, sent INTRODUCE1 to intro point,
|
||||
* waiting for ACK/NAK. */
|
||||
#define CIRCUIT_PURPOSE_C_INTRODUCE_ACK_WAIT 7
|
||||
/** Client-side circuit purpose: at the client, introduced and acked, closing.
|
||||
*/
|
||||
#define CIRCUIT_PURPOSE_C_INTRODUCE_ACKED 8
|
||||
/** Client-side circuit purpose: at the client, waiting for ack. */
|
||||
#define CIRCUIT_PURPOSE_C_ESTABLISH_REND 9
|
||||
/** Client-side circuit purpose: at the client, waiting for the service. */
|
||||
#define CIRCUIT_PURPOSE_C_REND_READY 10
|
||||
/** Client-side circuit purpose: at the client, waiting for the service,
|
||||
* INTRODUCE has been acknowledged. */
|
||||
#define CIRCUIT_PURPOSE_C_REND_READY_INTRO_ACKED 11
|
||||
/** Client-side circuit purpose: at the client, rendezvous established. */
|
||||
#define CIRCUIT_PURPOSE_C_REND_JOINED 12
|
||||
/** This circuit is used for getting hsdirs */
|
||||
#define CIRCUIT_PURPOSE_C_HSDIR_GET 13
|
||||
#define CIRCUIT_PURPOSE_C_HS_MAX_ 13
|
||||
/** This circuit is used for build time measurement only */
|
||||
#define CIRCUIT_PURPOSE_C_MEASURE_TIMEOUT 14
|
||||
/** This circuit is being held open by circuit padding */
|
||||
#define CIRCUIT_PURPOSE_C_CIRCUIT_PADDING 15
|
||||
#define CIRCUIT_PURPOSE_C_MAX_ 15
|
||||
|
||||
#define CIRCUIT_PURPOSE_S_HS_MIN_ 16
|
||||
/** Hidden-service-side circuit purpose: at the service, waiting for
|
||||
* introductions. */
|
||||
#define CIRCUIT_PURPOSE_S_ESTABLISH_INTRO 16
|
||||
/** Hidden-service-side circuit purpose: at the service, successfully
|
||||
* established intro. */
|
||||
#define CIRCUIT_PURPOSE_S_INTRO 17
|
||||
/** Hidden-service-side circuit purpose: at the service, connecting to rend
|
||||
* point. */
|
||||
#define CIRCUIT_PURPOSE_S_CONNECT_REND 18
|
||||
/** Hidden-service-side circuit purpose: at the service, rendezvous
|
||||
* established. */
|
||||
#define CIRCUIT_PURPOSE_S_REND_JOINED 19
|
||||
/** This circuit is used for uploading hsdirs */
|
||||
#define CIRCUIT_PURPOSE_S_HSDIR_POST 20
|
||||
#define CIRCUIT_PURPOSE_S_HS_MAX_ 20
|
||||
|
||||
/** A testing circuit; not meant to be used for actual traffic. It is used for
|
||||
* bandwidth measurement, reachability test and address discovery from an
|
||||
* authority using the NETINFO cell. */
|
||||
#define CIRCUIT_PURPOSE_TESTING 21
|
||||
/** A controller made this circuit and Tor should not cannibalize it or attach
|
||||
* streams to it without explicitly being told. */
|
||||
#define CIRCUIT_PURPOSE_CONTROLLER 22
|
||||
/** This circuit is used for path bias probing only */
|
||||
#define CIRCUIT_PURPOSE_PATH_BIAS_TESTING 23
|
||||
|
||||
/** This circuit is used for vanguards/restricted paths.
|
||||
*
|
||||
* This type of circuit is *only* created preemptively and never
|
||||
* on-demand. When an HS operation needs to take place (e.g. connect to an
|
||||
* intro point), these circuits are then cannibalized and repurposed to the
|
||||
* actual needed HS purpose. */
|
||||
#define CIRCUIT_PURPOSE_HS_VANGUARDS 24
|
||||
|
||||
/**
|
||||
* These two purposes are for conflux. The first is for circuits that are
|
||||
* being built, but not yet linked. The second is for circuits that are
|
||||
* linked and ready to use for streams. */
|
||||
#define CIRCUIT_PURPOSE_CONFLUX_UNLINKED 25
|
||||
#define CIRCUIT_PURPOSE_CONFLUX_LINKED 26
|
||||
|
||||
#define CIRCUIT_PURPOSE_MAX_ 26
|
||||
/** A catch-all for unrecognized purposes. Currently we don't expect
|
||||
* to make or see any circuits with this purpose. */
|
||||
#define CIRCUIT_PURPOSE_UNKNOWN 255
|
||||
|
||||
/** True iff the circuit purpose <b>p</b> is for a circuit that
|
||||
* originated at this node. */
|
||||
#define CIRCUIT_PURPOSE_IS_ORIGIN(p) ((p)>CIRCUIT_PURPOSE_OR_MAX_)
|
||||
/** True iff the circuit purpose <b>p</b> is for a circuit that originated
|
||||
* here to serve as a client. (Hidden services don't count here.) */
|
||||
#define CIRCUIT_PURPOSE_IS_CLIENT(p) \
|
||||
((p)> CIRCUIT_PURPOSE_OR_MAX_ && \
|
||||
(p)<=CIRCUIT_PURPOSE_C_MAX_)
|
||||
/** True iff the circuit_t <b>c</b> is actually an origin_circuit_t. */
|
||||
#define CIRCUIT_IS_ORIGIN(c) (CIRCUIT_PURPOSE_IS_ORIGIN((c)->purpose))
|
||||
/** True iff the circuit purpose <b>p</b> is for an established rendezvous
|
||||
* circuit. */
|
||||
#define CIRCUIT_PURPOSE_IS_ESTABLISHED_REND(p) \
|
||||
((p) == CIRCUIT_PURPOSE_C_REND_JOINED || \
|
||||
(p) == CIRCUIT_PURPOSE_S_REND_JOINED)
|
||||
/** True iff the circuit_t c is actually an or_circuit_t */
|
||||
#define CIRCUIT_IS_ORCIRC(c) (((circuit_t *)(c))->magic == OR_CIRCUIT_MAGIC)
|
||||
|
||||
/** True iff this circuit purpose should count towards the global
|
||||
* pending rate limit (set by MaxClientCircuitsPending). We count all
|
||||
* general purpose circuits, as well as the first step of client onion
|
||||
* service connections (HSDir gets). */
|
||||
#define CIRCUIT_PURPOSE_COUNTS_TOWARDS_MAXPENDING(p) \
|
||||
((p) == CIRCUIT_PURPOSE_C_GENERAL || \
|
||||
(p) == CIRCUIT_PURPOSE_C_HSDIR_GET)
|
||||
|
||||
/** Stats. */
|
||||
extern double cc_stats_circ_close_cwnd_ma;
|
||||
extern double cc_stats_circ_close_ss_cwnd_ma;
|
||||
extern uint64_t cc_stats_circs_closed;
|
||||
|
||||
/** Convert a circuit_t* to a pointer to the enclosing or_circuit_t. Assert
|
||||
* if the cast is impossible. */
|
||||
or_circuit_t *TO_OR_CIRCUIT(circuit_t *);
|
||||
const or_circuit_t *CONST_TO_OR_CIRCUIT(const circuit_t *);
|
||||
/** Convert a circuit_t* to a pointer to the enclosing origin_circuit_t.
|
||||
* Assert if the cast is impossible. */
|
||||
origin_circuit_t *TO_ORIGIN_CIRCUIT(circuit_t *);
|
||||
const origin_circuit_t *CONST_TO_ORIGIN_CIRCUIT(const circuit_t *);
|
||||
|
||||
MOCK_DECL(smartlist_t *, circuit_get_global_list, (void));
|
||||
smartlist_t *circuit_get_global_origin_circuit_list(void);
|
||||
int circuit_any_opened_circuits(void);
|
||||
int circuit_any_opened_circuits_cached(void);
|
||||
void circuit_cache_opened_circuit_state(int circuits_are_opened);
|
||||
|
||||
const char *circuit_state_to_string(int state);
|
||||
const char *circuit_purpose_to_controller_string(uint8_t purpose);
|
||||
const char *circuit_purpose_to_controller_hs_state_string(uint8_t purpose);
|
||||
const char *circuit_purpose_to_string(uint8_t purpose);
|
||||
void circuit_dump_by_conn(connection_t *conn, int severity);
|
||||
void circuit_set_p_circid_chan(or_circuit_t *circ, circid_t id,
|
||||
channel_t *chan);
|
||||
void circuit_set_n_circid_chan(circuit_t *circ, circid_t id,
|
||||
channel_t *chan);
|
||||
void channel_mark_circid_unusable(channel_t *chan, circid_t id);
|
||||
void channel_mark_circid_usable(channel_t *chan, circid_t id);
|
||||
time_t circuit_id_when_marked_unusable_on_channel(circid_t circ_id,
|
||||
channel_t *chan);
|
||||
int circuit_event_status(origin_circuit_t *circ, circuit_status_event_t tp,
|
||||
int reason_code);
|
||||
void circuit_set_state(circuit_t *circ, uint8_t state);
|
||||
void circuit_close_all_marked(void);
|
||||
int32_t circuit_initial_package_window(void);
|
||||
origin_circuit_t *origin_circuit_new(void);
|
||||
or_circuit_t *or_circuit_new(circid_t p_circ_id, channel_t *p_chan);
|
||||
circuit_t *circuit_get_by_circid_channel(circid_t circ_id,
|
||||
channel_t *chan);
|
||||
circuit_t *
|
||||
circuit_get_by_circid_channel_even_if_marked(circid_t circ_id,
|
||||
channel_t *chan);
|
||||
int circuit_id_in_use_on_channel(circid_t circ_id, channel_t *chan);
|
||||
circuit_t *circuit_get_by_edge_conn(edge_connection_t *conn);
|
||||
void circuit_unlink_all_from_channel(channel_t *chan, int reason);
|
||||
origin_circuit_t *circuit_get_by_global_id(uint32_t id);
|
||||
origin_circuit_t *circuit_get_next_by_purpose(origin_circuit_t *start,
|
||||
uint8_t purpose);
|
||||
origin_circuit_t *circuit_get_next_intro_circ(const origin_circuit_t *start,
|
||||
bool want_client_circ);
|
||||
origin_circuit_t *circuit_get_next_service_rp_circ(origin_circuit_t *start);
|
||||
origin_circuit_t *circuit_get_next_service_hsdir_circ(origin_circuit_t *start);
|
||||
origin_circuit_t *circuit_find_to_cannibalize(uint8_t purpose,
|
||||
extend_info_t *info, int flags);
|
||||
void circuit_mark_all_unused_circs(void);
|
||||
void circuit_mark_all_dirty_circs_as_unusable(void);
|
||||
void circuit_synchronize_written_or_bandwidth(const circuit_t *c,
|
||||
circuit_channel_direction_t dir);
|
||||
MOCK_DECL(void, circuit_mark_for_close_, (circuit_t *circ, int reason,
|
||||
int line, const char *cfile));
|
||||
int circuit_get_cpath_len(origin_circuit_t *circ);
|
||||
int circuit_get_cpath_opened_len(const origin_circuit_t *);
|
||||
void circuit_clear_cpath(origin_circuit_t *circ);
|
||||
crypt_path_t *circuit_get_cpath_hop(origin_circuit_t *circ, int hopnum);
|
||||
void circuit_get_all_pending_on_channel(smartlist_t *out,
|
||||
channel_t *chan);
|
||||
int circuit_count_pending_on_channel(channel_t *chan);
|
||||
|
||||
#define circuit_mark_for_close(c, reason) \
|
||||
circuit_mark_for_close_((c), (reason), __LINE__, SHORT_FILE__)
|
||||
|
||||
MOCK_DECL(void, assert_circuit_ok,(const circuit_t *c));
|
||||
void circuit_free_all(void);
|
||||
size_t circuits_handle_oom(size_t current_allocation);
|
||||
|
||||
void circuit_clear_testing_cell_stats(circuit_t *circ);
|
||||
|
||||
void channel_note_destroy_pending(channel_t *chan, circid_t id);
|
||||
MOCK_DECL(void, channel_note_destroy_not_pending,
|
||||
(channel_t *chan, circid_t id));
|
||||
|
||||
smartlist_t *circuit_find_circuits_to_upgrade_from_guard_wait(void);
|
||||
|
||||
bool circuit_is_queue_full(const circuit_t *circ, cell_direction_t direction);
|
||||
|
||||
/* Declare the handle helpers */
|
||||
HANDLE_DECL(circuit, circuit_t, )
|
||||
#define circuit_handle_free(h) \
|
||||
FREE_AND_NULL(circuit_handle_t, circuit_handle_free_, (h))
|
||||
|
||||
#ifdef CIRCUITLIST_PRIVATE
|
||||
STATIC void circuit_free_(circuit_t *circ);
|
||||
#define circuit_free(circ) FREE_AND_NULL(circuit_t, circuit_free_, (circ))
|
||||
STATIC size_t n_cells_in_circ_queues(const circuit_t *c);
|
||||
STATIC uint32_t circuit_max_queued_data_age(const circuit_t *c, uint32_t now);
|
||||
STATIC uint32_t circuit_max_queued_cell_age(const circuit_t *c, uint32_t now);
|
||||
STATIC uint32_t circuit_max_queued_item_age(const circuit_t *c, uint32_t now);
|
||||
#endif /* defined(CIRCUITLIST_PRIVATE) */
|
||||
|
||||
#endif /* !defined(TOR_CIRCUITLIST_H) */
|
||||
-218
@@ -1,218 +0,0 @@
|
||||
/* * Copyright (c) 2012-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file circuitmux.h
|
||||
* \brief Header file for circuitmux.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_CIRCUITMUX_H
|
||||
#define TOR_CIRCUITMUX_H
|
||||
|
||||
#include "core/or/or.h"
|
||||
#include "lib/testsupport/testsupport.h"
|
||||
|
||||
typedef struct circuitmux_policy_t circuitmux_policy_t;
|
||||
typedef struct circuitmux_policy_data_t circuitmux_policy_data_t;
|
||||
typedef struct circuitmux_policy_circ_data_t circuitmux_policy_circ_data_t;
|
||||
|
||||
struct circuitmux_policy_t {
|
||||
/* Allocate cmux-wide policy-specific data */
|
||||
circuitmux_policy_data_t * (*alloc_cmux_data)(circuitmux_t *cmux);
|
||||
/* Free cmux-wide policy-specific data */
|
||||
void (*free_cmux_data)(circuitmux_t *cmux,
|
||||
circuitmux_policy_data_t *pol_data);
|
||||
/* Allocate circuit policy-specific data for a newly attached circuit */
|
||||
circuitmux_policy_circ_data_t *
|
||||
(*alloc_circ_data)(circuitmux_t *cmux,
|
||||
circuitmux_policy_data_t *pol_data,
|
||||
circuit_t *circ,
|
||||
cell_direction_t direction,
|
||||
unsigned int cell_count);
|
||||
/* Free circuit policy-specific data */
|
||||
void (*free_circ_data)(circuitmux_t *cmux,
|
||||
circuitmux_policy_data_t *pol_data,
|
||||
circuit_t *circ,
|
||||
circuitmux_policy_circ_data_t *pol_circ_data);
|
||||
/* Notify that a circuit has become active/inactive */
|
||||
void (*notify_circ_active)(circuitmux_t *cmux,
|
||||
circuitmux_policy_data_t *pol_data,
|
||||
circuit_t *circ,
|
||||
circuitmux_policy_circ_data_t *pol_circ_data);
|
||||
void (*notify_circ_inactive)(circuitmux_t *cmux,
|
||||
circuitmux_policy_data_t *pol_data,
|
||||
circuit_t *circ,
|
||||
circuitmux_policy_circ_data_t *pol_circ_data);
|
||||
/* Notify of arriving/transmitted cells on a circuit */
|
||||
void (*notify_set_n_cells)(circuitmux_t *cmux,
|
||||
circuitmux_policy_data_t *pol_data,
|
||||
circuit_t *circ,
|
||||
circuitmux_policy_circ_data_t *pol_circ_data,
|
||||
unsigned int n_cells);
|
||||
void (*notify_xmit_cells)(circuitmux_t *cmux,
|
||||
circuitmux_policy_data_t *pol_data,
|
||||
circuit_t *circ,
|
||||
circuitmux_policy_circ_data_t *pol_circ_data,
|
||||
unsigned int n_cells);
|
||||
/* Choose a circuit */
|
||||
circuit_t * (*pick_active_circuit)(circuitmux_t *cmux,
|
||||
circuitmux_policy_data_t *pol_data);
|
||||
/* Optional: channel comparator for use by the scheduler */
|
||||
int (*cmp_cmux)(circuitmux_t *cmux_1, circuitmux_policy_data_t *pol_data_1,
|
||||
circuitmux_t *cmux_2, circuitmux_policy_data_t *pol_data_2);
|
||||
};
|
||||
|
||||
/*
|
||||
* Circuitmux policy implementations can subclass this to store circuitmux-
|
||||
* wide data; it just has the magic number in the base struct.
|
||||
*/
|
||||
|
||||
struct circuitmux_policy_data_t {
|
||||
uint32_t magic;
|
||||
};
|
||||
|
||||
/*
|
||||
* Circuitmux policy implementations can subclass this to store circuit-
|
||||
* specific data; it just has the magic number in the base struct.
|
||||
*/
|
||||
|
||||
struct circuitmux_policy_circ_data_t {
|
||||
uint32_t magic;
|
||||
};
|
||||
|
||||
/*
|
||||
* Upcast #defines for the above types
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a circuitmux_policy_data_t subtype to a circuitmux_policy_data_t.
|
||||
*/
|
||||
|
||||
#define TO_CMUX_POL_DATA(x) (&((x)->base_))
|
||||
|
||||
/**
|
||||
* Convert a circuitmux_policy_circ_data_t subtype to a
|
||||
* circuitmux_policy_circ_data_t.
|
||||
*/
|
||||
|
||||
#define TO_CMUX_POL_CIRC_DATA(x) (&((x)->base_))
|
||||
|
||||
/* Consistency check */
|
||||
void circuitmux_assert_okay(circuitmux_t *cmux);
|
||||
|
||||
/* Create/destroy */
|
||||
circuitmux_t * circuitmux_alloc(void);
|
||||
void circuitmux_detach_all_circuits(circuitmux_t *cmux,
|
||||
smartlist_t *detached_out);
|
||||
void circuitmux_free_(circuitmux_t *cmux);
|
||||
#define circuitmux_free(cmux) \
|
||||
FREE_AND_NULL(circuitmux_t, circuitmux_free_, (cmux))
|
||||
|
||||
/* Policy control */
|
||||
void circuitmux_clear_policy(circuitmux_t *cmux);
|
||||
MOCK_DECL(const circuitmux_policy_t *,
|
||||
circuitmux_get_policy, (circuitmux_t *cmux));
|
||||
void circuitmux_set_policy(circuitmux_t *cmux,
|
||||
const circuitmux_policy_t *pol);
|
||||
|
||||
/* Status inquiries */
|
||||
cell_direction_t circuitmux_attached_circuit_direction(
|
||||
circuitmux_t *cmux,
|
||||
circuit_t *circ);
|
||||
int circuitmux_is_circuit_attached(circuitmux_t *cmux, circuit_t *circ);
|
||||
int circuitmux_is_circuit_active(circuitmux_t *cmux, circuit_t *circ);
|
||||
unsigned int circuitmux_num_cells_for_circuit(circuitmux_t *cmux,
|
||||
circuit_t *circ);
|
||||
MOCK_DECL(unsigned int, circuitmux_num_cells, (circuitmux_t *cmux));
|
||||
unsigned int circuitmux_num_circuits(circuitmux_t *cmux);
|
||||
unsigned int circuitmux_num_active_circuits(circuitmux_t *cmux);
|
||||
|
||||
/* Debugging interface - slow. */
|
||||
int64_t circuitmux_count_queued_destroy_cells(const channel_t *chan,
|
||||
const circuitmux_t *cmux);
|
||||
|
||||
/* Channel interface */
|
||||
circuit_t * circuitmux_get_first_active_circuit(circuitmux_t *cmux,
|
||||
destroy_cell_queue_t **destroy_queue_out);
|
||||
void circuitmux_notify_xmit_cells(circuitmux_t *cmux, circuit_t *circ,
|
||||
unsigned int n_cells);
|
||||
void circuitmux_notify_xmit_destroy(circuitmux_t *cmux);
|
||||
|
||||
/* Circuit interface */
|
||||
MOCK_DECL(void, circuitmux_attach_circuit, (circuitmux_t *cmux,
|
||||
circuit_t *circ,
|
||||
cell_direction_t direction));
|
||||
MOCK_DECL(void, circuitmux_detach_circuit,
|
||||
(circuitmux_t *cmux, circuit_t *circ));
|
||||
void circuitmux_clear_num_cells(circuitmux_t *cmux, circuit_t *circ);
|
||||
void circuitmux_set_num_cells(circuitmux_t *cmux, circuit_t *circ,
|
||||
unsigned int n_cells);
|
||||
|
||||
void circuitmux_append_destroy_cell(channel_t *chan,
|
||||
circuitmux_t *cmux, circid_t circ_id,
|
||||
uint8_t reason);
|
||||
void circuitmux_mark_destroyed_circids_usable(circuitmux_t *cmux,
|
||||
channel_t *chan);
|
||||
|
||||
/* Optional interchannel comparisons for scheduling */
|
||||
MOCK_DECL(int, circuitmux_compare_muxes,
|
||||
(circuitmux_t *cmux_1, circuitmux_t *cmux_2));
|
||||
|
||||
#ifdef CIRCUITMUX_PRIVATE
|
||||
|
||||
#include "core/or/destroy_cell_queue_st.h"
|
||||
|
||||
/*
|
||||
* Map of muxinfos for circuitmux_t to use; struct is defined below (name
|
||||
* of struct must match HT_HEAD line).
|
||||
*/
|
||||
typedef HT_HEAD(chanid_circid_muxinfo_map, chanid_circid_muxinfo_t)
|
||||
chanid_circid_muxinfo_map_t;
|
||||
|
||||
/*
|
||||
* Structures for circuitmux.c
|
||||
*/
|
||||
|
||||
struct circuitmux_t {
|
||||
/* Keep count of attached, active circuits */
|
||||
unsigned int n_circuits, n_active_circuits;
|
||||
|
||||
/* Total number of queued cells on all circuits */
|
||||
unsigned int n_cells;
|
||||
|
||||
/*
|
||||
* Map from (channel ID, circuit ID) pairs to circuit_muxinfo_t
|
||||
*/
|
||||
chanid_circid_muxinfo_map_t *chanid_circid_map;
|
||||
|
||||
/** List of queued destroy cells */
|
||||
destroy_cell_queue_t destroy_cell_queue;
|
||||
/** Boolean: True iff the last cell to circuitmux_get_first_active_circuit
|
||||
* returned the destroy queue. Used to force alternation between
|
||||
* destroy/non-destroy cells.
|
||||
*
|
||||
* XXXX There is no reason to think that alternating is a particularly good
|
||||
* approach -- it's just designed to prevent destroys from starving other
|
||||
* cells completely.
|
||||
*/
|
||||
unsigned int last_cell_was_destroy : 1;
|
||||
/** Destroy counter: increment this when a destroy gets queued, decrement
|
||||
* when we unqueue it, so we can test to make sure they don't starve.
|
||||
*/
|
||||
int64_t destroy_ctr;
|
||||
|
||||
/*
|
||||
* Circuitmux policy; if this is non-NULL, it can override the built-
|
||||
* in round-robin active circuits behavior. This is how EWMA works in
|
||||
* the new circuitmux_t world.
|
||||
*/
|
||||
const circuitmux_policy_t *policy;
|
||||
|
||||
/* Policy-specific data */
|
||||
circuitmux_policy_data_t *policy_data;
|
||||
};
|
||||
|
||||
#endif /* defined(CIRCUITMUX_PRIVATE) */
|
||||
|
||||
#endif /* !defined(TOR_CIRCUITMUX_H) */
|
||||
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
/* * Copyright (c) 2012-2021, The Tor Project, Inc. */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
/**
|
||||
* \file circuitmux_ewma.h
|
||||
* \brief Header file for circuitmux_ewma.c
|
||||
**/
|
||||
|
||||
#ifndef TOR_CIRCUITMUX_EWMA_H
|
||||
#define TOR_CIRCUITMUX_EWMA_H
|
||||
|
||||
#include "core/or/or.h"
|
||||
#include "core/or/circuitmux.h"
|
||||
|
||||
/* The public EWMA policy callbacks object. */
|
||||
extern circuitmux_policy_t ewma_policy;
|
||||
|
||||
/* Externally visible EWMA functions */
|
||||
void cmux_ewma_set_options(const or_options_t *options,
|
||||
const networkstatus_t *consensus);
|
||||
|
||||
void circuitmux_ewma_free_all(void);
|
||||
|
||||
#ifdef CIRCUITMUX_EWMA_PRIVATE
|
||||
|
||||
/*** EWMA structures ***/
|
||||
|
||||
typedef struct cell_ewma_t cell_ewma_t;
|
||||
typedef struct ewma_policy_data_t ewma_policy_data_t;
|
||||
typedef struct ewma_policy_circ_data_t ewma_policy_circ_data_t;
|
||||
|
||||
/**
|
||||
* The cell_ewma_t structure keeps track of how many cells a circuit has
|
||||
* transferred recently. It keeps an EWMA (exponentially weighted moving
|
||||
* average) of the number of cells flushed from the circuit queue onto a
|
||||
* connection in channel_flush_from_first_active_circuit().
|
||||
*/
|
||||
|
||||
struct cell_ewma_t {
|
||||
/** The last 'tick' at which we recalibrated cell_count.
|
||||
*
|
||||
* A cell sent at exactly the start of this tick has weight 1.0. Cells sent
|
||||
* since the start of this tick have weight greater than 1.0; ones sent
|
||||
* earlier have less weight. */
|
||||
unsigned int last_adjusted_tick;
|
||||
/** The EWMA of the cell count. */
|
||||
double cell_count;
|
||||
/** True iff this is the cell count for a circuit's previous
|
||||
* channel. */
|
||||
unsigned int is_for_p_chan : 1;
|
||||
/** The position of the circuit within the OR connection's priority
|
||||
* queue. */
|
||||
int heap_index;
|
||||
};
|
||||
|
||||
struct ewma_policy_data_t {
|
||||
circuitmux_policy_data_t base_;
|
||||
|
||||
/**
|
||||
* Priority queue of cell_ewma_t for circuits with queued cells waiting
|
||||
* for room to free up on the channel that owns this circuitmux. Kept
|
||||
* in heap order according to EWMA. This was formerly in channel_t, and
|
||||
* in or_connection_t before that.
|
||||
*/
|
||||
smartlist_t *active_circuit_pqueue;
|
||||
|
||||
/**
|
||||
* The tick on which the cell_ewma_ts in active_circuit_pqueue last had
|
||||
* their ewma values rescaled. This was formerly in channel_t, and in
|
||||
* or_connection_t before that.
|
||||
*/
|
||||
unsigned int active_circuit_pqueue_last_recalibrated;
|
||||
};
|
||||
|
||||
struct ewma_policy_circ_data_t {
|
||||
circuitmux_policy_circ_data_t base_;
|
||||
|
||||
/**
|
||||
* The EWMA count for the number of cells flushed from this circuit
|
||||
* onto this circuitmux. Used to determine which circuit to flush
|
||||
* from next. This was formerly in circuit_t and or_circuit_t.
|
||||
*/
|
||||
cell_ewma_t cell_ewma;
|
||||
|
||||
/**
|
||||
* Pointer back to the circuit_t this is for; since we're separating
|
||||
* out circuit selection policy like this, we can't attach cell_ewma_t
|
||||
* to the circuit_t any more, so we can't use SUBTYPE_P directly to a
|
||||
* circuit_t like before; instead get it here.
|
||||
*/
|
||||
circuit_t *circ;
|
||||
};
|
||||
|
||||
#define EWMA_POL_DATA_MAGIC 0x2fd8b16aU
|
||||
#define EWMA_POL_CIRC_DATA_MAGIC 0x761e7747U
|
||||
|
||||
/*** Downcasts for the above types ***/
|
||||
|
||||
/**
|
||||
* Downcast a circuitmux_policy_data_t to an ewma_policy_data_t and assert
|
||||
* if the cast is impossible.
|
||||
*/
|
||||
|
||||
static inline ewma_policy_data_t *
|
||||
TO_EWMA_POL_DATA(circuitmux_policy_data_t *pol)
|
||||
{
|
||||
if (!pol) return NULL;
|
||||
else {
|
||||
tor_assertf(pol->magic == EWMA_POL_DATA_MAGIC,
|
||||
"Mismatch: %"PRIu32" != %"PRIu32,
|
||||
pol->magic, EWMA_POL_DATA_MAGIC);
|
||||
return DOWNCAST(ewma_policy_data_t, pol);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downcast a circuitmux_policy_circ_data_t to an ewma_policy_circ_data_t
|
||||
* and assert if the cast is impossible.
|
||||
*/
|
||||
|
||||
static inline ewma_policy_circ_data_t *
|
||||
TO_EWMA_POL_CIRC_DATA(circuitmux_policy_circ_data_t *pol)
|
||||
{
|
||||
if (!pol) return NULL;
|
||||
else {
|
||||
tor_assertf(pol->magic == EWMA_POL_CIRC_DATA_MAGIC,
|
||||
"Mismatch: %"PRIu32" != %"PRIu32,
|
||||
pol->magic, EWMA_POL_CIRC_DATA_MAGIC);
|
||||
return DOWNCAST(ewma_policy_circ_data_t, pol);
|
||||
}
|
||||
}
|
||||
|
||||
STATIC unsigned cell_ewma_get_current_tick_and_fraction(double *remainder_out);
|
||||
STATIC void cell_ewma_initialize_ticks(void);
|
||||
|
||||
#endif /* defined(CIRCUITMUX_EWMA_PRIVATE) */
|
||||
|
||||
#endif /* !defined(TOR_CIRCUITMUX_EWMA_H) */
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user