mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 23:05: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
|
wget -q https://raw.githubusercontent.com/permissionlesstech/georelays/refs/heads/main/nostr_relays.csv
|
||||||
mv nostr_relays.csv ./relays/online_relays_gps.csv
|
mv nostr_relays.csv ./relays/online_relays_gps.csv
|
||||||
|
|
||||||
- name: Configure git
|
- name: Check for changes
|
||||||
|
id: git-check
|
||||||
run: |
|
run: |
|
||||||
git config user.email "action@github.com"
|
git diff --exit-code || echo "changes=true" >> $GITHUB_OUTPUT
|
||||||
git config user.name "GitHub Action"
|
|
||||||
|
- name: Commit and push changes
|
||||||
- name: Create update branch if changes
|
if: steps.git-check.outputs.changes == 'true'
|
||||||
id: create_branch
|
|
||||||
run: |
|
run: |
|
||||||
# exit early if no changes
|
git config --local user.email "action@github.com"
|
||||||
if git diff --quiet --relays/online_relays_gps.csv; then
|
git config --local user.name "GitHub Action"
|
||||||
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 add relays/online_relays_gps.csv
|
git add relays/online_relays_gps.csv
|
||||||
git commit -m "Automated update of relay data - $(date -u --rfc-3339=seconds)"
|
git commit -m "Automated update of relay data - $(date -u)"
|
||||||
echo "changed=true" >> $GITHUB_OUTPUT
|
git push
|
||||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- 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"
|
|
||||||
+2
-2
@@ -16,7 +16,7 @@ let package = Package(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
dependencies:[
|
dependencies:[
|
||||||
.package(path: "localPackages/Tor"),
|
.package(path: "localPackages/Arti"),
|
||||||
.package(path: "localPackages/BitLogger"),
|
.package(path: "localPackages/BitLogger"),
|
||||||
.package(url: "https://github.com/21-DOT-DEV/swift-secp256k1", exact: "0.21.1")
|
.package(url: "https://github.com/21-DOT-DEV/swift-secp256k1", exact: "0.21.1")
|
||||||
],
|
],
|
||||||
@@ -26,7 +26,7 @@ let package = Package(
|
|||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "P256K", package: "swift-secp256k1"),
|
.product(name: "P256K", package: "swift-secp256k1"),
|
||||||
.product(name: "BitLogger", package: "BitLogger"),
|
.product(name: "BitLogger", package: "BitLogger"),
|
||||||
.product(name: "Tor", package: "Tor")
|
.product(name: "Tor", package: "Arti")
|
||||||
],
|
],
|
||||||
path: "bitchat",
|
path: "bitchat",
|
||||||
exclude: [
|
exclude: [
|
||||||
|
|||||||
Generated
+5
-7
@@ -14,7 +14,6 @@
|
|||||||
A6E3E5722E7703760032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E5712E7703760032EA8A /* BitLogger */; };
|
A6E3E5722E7703760032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E5712E7703760032EA8A /* BitLogger */; };
|
||||||
A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3EA7E2E7706720032EA8A /* Tor */; };
|
A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3EA7E2E7706720032EA8A /* Tor */; };
|
||||||
A6E3EA812E7706A80032EA8A /* Tor in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3EA802E7706A80032EA8A /* 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 */; };
|
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 */; };
|
E0A1B2C3D4E5F6012345678E /* relays/online_relays_gps.csv in Resources */ = {isa = PBXBuildFile; fileRef = E0A1B2C3D4E5F6012345678A /* relays/online_relays_gps.csv */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@@ -162,7 +161,6 @@
|
|||||||
B5A5CC493FFB3D8966548140 /* Frameworks */ = {
|
B5A5CC493FFB3D8966548140 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
files = (
|
files = (
|
||||||
A6F183FD2E948783006A9046 /* tor-nolzma.xcframework in Frameworks */,
|
|
||||||
A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */,
|
A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */,
|
||||||
885BBED78092484A5B069461 /* P256K in Frameworks */,
|
885BBED78092484A5B069461 /* P256K in Frameworks */,
|
||||||
A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */,
|
A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */,
|
||||||
@@ -345,7 +343,7 @@
|
|||||||
packageReferences = (
|
packageReferences = (
|
||||||
B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */,
|
B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */,
|
||||||
A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */,
|
A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */,
|
||||||
A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Tor" */,
|
A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Arti" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 90;
|
preferredProjectObjectVersion = 90;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -913,9 +911,9 @@
|
|||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
relativePath = localPackages/BitLogger;
|
relativePath = localPackages/BitLogger;
|
||||||
};
|
};
|
||||||
A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Tor" */ = {
|
A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Arti" */ = {
|
||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
relativePath = localPackages/Tor;
|
relativePath = localPackages/Arti;
|
||||||
};
|
};
|
||||||
/* End XCLocalSwiftPackageReference section */
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
@@ -924,8 +922,8 @@
|
|||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/21-DOT-DEV/swift-secp256k1";
|
repositoryURL = "https://github.com/21-DOT-DEV/swift-secp256k1";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = exactVersion;
|
||||||
minimumVersion = 0.21.1;
|
version = 0.21.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
@@ -63,6 +63,10 @@ struct BitchatApp: App {
|
|||||||
|
|
||||||
// Initialize network activation policy; will start Tor/Nostr only when allowed
|
// Initialize network activation policy; will start Tor/Nostr only when allowed
|
||||||
NetworkActivationService.shared.start()
|
NetworkActivationService.shared.start()
|
||||||
|
|
||||||
|
// Start presence service (will wait for Tor readiness)
|
||||||
|
GeohashPresenceService.shared.start()
|
||||||
|
|
||||||
// Check for shared content
|
// Check for shared content
|
||||||
checkForSharedContent()
|
checkForSharedContent()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,19 +165,23 @@ final class NoiseCipherState {
|
|||||||
// MARK: - Sliding Window Replay Protection
|
// MARK: - Sliding Window Replay Protection
|
||||||
|
|
||||||
/// Check if nonce is valid for 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 {
|
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
|
return false // Too old, outside window
|
||||||
}
|
}
|
||||||
|
|
||||||
if receivedNonce > highestReceivedNonce {
|
if receivedNonce > highestReceivedNonce {
|
||||||
return true // Always accept newer nonces
|
return true // Always accept newer nonces
|
||||||
}
|
}
|
||||||
|
|
||||||
let offset = Int(highestReceivedNonce - receivedNonce)
|
let offset = Int(highestReceivedNonce - receivedNonce)
|
||||||
let byteIndex = offset / 8
|
let byteIndex = offset / 8
|
||||||
let bitIndex = offset % 8
|
let bitIndex = offset % 8
|
||||||
|
|
||||||
return (replayWindow[byteIndex] & (1 << bitIndex)) == 0 // Not yet seen
|
return (replayWindow[byteIndex] & (1 << bitIndex)) == 0 // Not yet seen
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,16 +351,20 @@ final class NoiseCipherState {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let plaintext = try ChaChaPoly.open(sealedBox, using: key, authenticating: associatedData)
|
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 {
|
if useExtractedNonce {
|
||||||
// Mark nonce as seen after successful decryption
|
|
||||||
markNonceAsSeen(decryptionNonce)
|
markNonceAsSeen(decryptionNonce)
|
||||||
}
|
}
|
||||||
nonce += 1
|
nonce += 1
|
||||||
|
|
||||||
return plaintext
|
return plaintext
|
||||||
} catch {
|
} catch {
|
||||||
|
// Decryption failed - nonce state remains unchanged (atomic rollback)
|
||||||
SecureLogger.debug("Decrypt failed: \(error) for nonce \(decryptionNonce)")
|
SecureLogger.debug("Decrypt failed: \(error) for nonce \(decryptionNonce)")
|
||||||
// Log authentication failures with nonce info
|
|
||||||
SecureLogger.error("Decryption failed at nonce \(decryptionNonce)", category: .encryption)
|
SecureLogger.error("Decryption failed at nonce \(decryptionNonce)", category: .encryption)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -455,13 +463,36 @@ final class NoiseSymmetricState {
|
|||||||
let output = hkdf(chainingKey: chainingKey, inputKeyMaterial: Data(), numOutputs: 2)
|
let output = hkdf(chainingKey: chainingKey, inputKeyMaterial: Data(), numOutputs: 2)
|
||||||
let tempKey1 = SymmetricKey(data: output[0])
|
let tempKey1 = SymmetricKey(data: output[0])
|
||||||
let tempKey2 = SymmetricKey(data: output[1])
|
let tempKey2 = SymmetricKey(data: output[1])
|
||||||
|
|
||||||
let c1 = NoiseCipherState(key: tempKey1, useExtractedNonce: useExtractedNonce)
|
let c1 = NoiseCipherState(key: tempKey1, useExtractedNonce: useExtractedNonce)
|
||||||
let c2 = NoiseCipherState(key: tempKey2, 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)
|
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
|
// HKDF implementation
|
||||||
private func hkdf(chainingKey: Data, inputKeyMaterial: Data, numOutputs: Int) -> [Data] {
|
private func hkdf(chainingKey: Data, inputKeyMaterial: Data, numOutputs: Int) -> [Data] {
|
||||||
let tempKey = HMAC<SHA256>.authenticationCode(for: inputKeyMaterial, using: SymmetricKey(data: chainingKey))
|
let tempKey = HMAC<SHA256>.authenticationCode(for: inputKeyMaterial, using: SymmetricKey(data: chainingKey))
|
||||||
@@ -807,16 +838,20 @@ final class NoiseHandshakeState {
|
|||||||
return currentPattern >= messagePatterns.count
|
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 {
|
guard isHandshakeComplete() else {
|
||||||
throw NoiseError.handshakeNotComplete
|
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)
|
let (c1, c2) = symmetricState.split(useExtractedNonce: useExtractedNonce)
|
||||||
|
|
||||||
// Initiator uses c1 for sending, c2 for receiving
|
// Initiator uses c1 for sending, c2 for receiving
|
||||||
// Responder uses c2 for sending, c1 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? {
|
func getRemoteStaticPublicKey() -> Curve25519.KeyAgreement.PublicKey? {
|
||||||
@@ -877,22 +912,47 @@ enum NoiseError: Error {
|
|||||||
case nonceExceeded
|
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
|
// MARK: - Key Validation
|
||||||
|
|
||||||
extension NoiseHandshakeState {
|
extension NoiseHandshakeState {
|
||||||
/// Validate a Curve25519 public key
|
/// Validate a Curve25519 public key
|
||||||
/// Checks for weak/invalid keys that could compromise security
|
/// 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 {
|
static func validatePublicKey(_ keyData: Data) throws -> Curve25519.KeyAgreement.PublicKey {
|
||||||
// Check key length
|
// Check key length
|
||||||
guard keyData.count == 32 else {
|
guard keyData.count == 32 else {
|
||||||
throw NoiseError.invalidPublicKey
|
throw NoiseError.invalidPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for all-zero key (point at infinity)
|
// BCH-01-010: Constant-time check for all-zero key (point at infinity)
|
||||||
if keyData.allSatisfy({ $0 == 0 }) {
|
if constantTimeIsZero(keyData) {
|
||||||
throw NoiseError.invalidPublicKey
|
throw NoiseError.invalidPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for low-order points that could enable small subgroup attacks
|
// Check for low-order points that could enable small subgroup attacks
|
||||||
// These are the known bad points for Curve25519
|
// These are the known bad points for Curve25519
|
||||||
let lowOrderPoints: [Data] = [
|
let lowOrderPoints: [Data] = [
|
||||||
@@ -913,13 +973,21 @@ extension NoiseHandshakeState {
|
|||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) // Another bad point
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) // Another bad point
|
||||||
]
|
]
|
||||||
|
|
||||||
// Check against known bad points
|
// BCH-01-010: Constant-time check against known bad points
|
||||||
if lowOrderPoints.contains(keyData) {
|
// 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)
|
SecureLogger.warning("Low-order point detected", category: .security)
|
||||||
throw NoiseError.invalidPublicKey
|
throw NoiseError.invalidPublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to create the key - CryptoKit will validate curve points internally
|
// Try to create the key - CryptoKit will validate curve points internally
|
||||||
do {
|
do {
|
||||||
let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyData)
|
let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyData)
|
||||||
|
|||||||
@@ -102,23 +102,23 @@ class NoiseSession {
|
|||||||
|
|
||||||
// Check if handshake is complete
|
// Check if handshake is complete
|
||||||
if handshake.isHandshakeComplete() {
|
if handshake.isHandshakeComplete() {
|
||||||
// Get transport ciphers
|
// Get transport ciphers and handshake hash (hash captured before split clears state)
|
||||||
let (send, receive) = try handshake.getTransportCiphers(useExtractedNonce: true)
|
let (send, receive, hash) = try handshake.getTransportCiphers(useExtractedNonce: true)
|
||||||
sendCipher = send
|
sendCipher = send
|
||||||
receiveCipher = receive
|
receiveCipher = receive
|
||||||
|
|
||||||
// Store remote static key
|
// Store remote static key
|
||||||
remoteStaticPublicKey = handshake.getRemoteStaticPublicKey()
|
remoteStaticPublicKey = handshake.getRemoteStaticPublicKey()
|
||||||
|
|
||||||
// Store handshake hash for channel binding
|
// Store handshake hash for channel binding
|
||||||
handshakeHash = handshake.getHandshakeHash()
|
handshakeHash = hash
|
||||||
|
|
||||||
state = .established
|
state = .established
|
||||||
handshakeState = nil // Clear handshake state
|
handshakeState = nil // Clear handshake state
|
||||||
|
|
||||||
SecureLogger.debug("NoiseSession[\(peerID)]: Handshake complete (no response needed), transitioning to established")
|
SecureLogger.debug("NoiseSession[\(peerID)]: Handshake complete (no response needed), transitioning to established")
|
||||||
SecureLogger.info(.handshakeCompleted(peerID: peerID.id))
|
SecureLogger.info(.handshakeCompleted(peerID: peerID.id))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
// Generate response
|
// Generate response
|
||||||
@@ -128,20 +128,20 @@ class NoiseSession {
|
|||||||
|
|
||||||
// Check if handshake is complete after writing
|
// Check if handshake is complete after writing
|
||||||
if handshake.isHandshakeComplete() {
|
if handshake.isHandshakeComplete() {
|
||||||
// Get transport ciphers
|
// Get transport ciphers and handshake hash (hash captured before split clears state)
|
||||||
let (send, receive) = try handshake.getTransportCiphers(useExtractedNonce: true)
|
let (send, receive, hash) = try handshake.getTransportCiphers(useExtractedNonce: true)
|
||||||
sendCipher = send
|
sendCipher = send
|
||||||
receiveCipher = receive
|
receiveCipher = receive
|
||||||
|
|
||||||
// Store remote static key
|
// Store remote static key
|
||||||
remoteStaticPublicKey = handshake.getRemoteStaticPublicKey()
|
remoteStaticPublicKey = handshake.getRemoteStaticPublicKey()
|
||||||
|
|
||||||
// Store handshake hash for channel binding
|
// Store handshake hash for channel binding
|
||||||
handshakeHash = handshake.getHandshakeHash()
|
handshakeHash = hash
|
||||||
|
|
||||||
state = .established
|
state = .established
|
||||||
handshakeState = nil // Clear handshake state
|
handshakeState = nil // Clear handshake state
|
||||||
|
|
||||||
SecureLogger.debug("NoiseSession[\(peerID)]: Handshake complete after writing response, transitioning to established")
|
SecureLogger.debug("NoiseSession[\(peerID)]: Handshake complete after writing response, transitioning to established")
|
||||||
SecureLogger.info(.handshakeCompleted(peerID: peerID.id))
|
SecureLogger.info(.handshakeCompleted(peerID: peerID.id))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct NostrProtocol {
|
|||||||
case seal = 13 // NIP-17 sealed event
|
case seal = 13 // NIP-17 sealed event
|
||||||
case giftWrap = 1059 // NIP-59 gift wrap
|
case giftWrap = 1059 // NIP-59 gift wrap
|
||||||
case ephemeralEvent = 20000
|
case ephemeralEvent = 20000
|
||||||
|
case geohashPresence = 20001
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a NIP-17 private message
|
/// Create a NIP-17 private message
|
||||||
@@ -125,6 +126,24 @@ struct NostrProtocol {
|
|||||||
return try event.sign(with: schnorrKey)
|
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.
|
/// Create a persistent location note (kind 1: text note) tagged to a street-level geohash.
|
||||||
static func createGeohashTextNote(
|
static func createGeohashTextNote(
|
||||||
content: String,
|
content: String,
|
||||||
|
|||||||
@@ -887,10 +887,10 @@ struct NostrFilter: Encodable {
|
|||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
|
|
||||||
// For location channels: geohash-scoped ephemeral events (kind 20000)
|
// For location channels: geohash-scoped ephemeral events (kind 20000) and presence (kind 20001)
|
||||||
static func geohashEphemeral(_ geohash: String, since: Date? = nil, limit: Int = 200) -> NostrFilter {
|
static func geohashEphemeral(_ geohash: String, since: Date? = nil, limit: Int = 1000) -> NostrFilter {
|
||||||
var filter = NostrFilter()
|
var filter = NostrFilter()
|
||||||
filter.kinds = [20000]
|
filter.kinds = [20000, 20001]
|
||||||
filter.since = since?.timeIntervalSince1970.toInt()
|
filter.since = since?.timeIntervalSince1970.toInt()
|
||||||
filter.tagFilters = ["g": [geohash]]
|
filter.tagFilters = ["g": [geohash]]
|
||||||
filter.limit = limit
|
filter.limit = limit
|
||||||
|
|||||||
@@ -161,7 +161,9 @@ struct BinaryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let lengthFieldBytes = lengthFieldSize(for: version)
|
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 }
|
if originalRoute.contains(where: { $0.isEmpty }) { return nil }
|
||||||
let sanitizedRoute: [Data] = originalRoute.map { hop in
|
let sanitizedRoute: [Data] = originalRoute.map { hop in
|
||||||
if hop.count == senderIDSize { return hop }
|
if hop.count == senderIDSize { return hop }
|
||||||
@@ -175,13 +177,14 @@ struct BinaryProtocol {
|
|||||||
let hasRoute = !sanitizedRoute.isEmpty
|
let hasRoute = !sanitizedRoute.isEmpty
|
||||||
let routeLength = hasRoute ? 1 + sanitizedRoute.count * senderIDSize : 0
|
let routeLength = hasRoute ? 1 + sanitizedRoute.count * senderIDSize : 0
|
||||||
let originalSizeFieldBytes = isCompressed ? lengthFieldBytes : 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 == 1 && payloadDataSize > Int(UInt16.max) { return nil }
|
||||||
if version == 2 && payloadDataSize > Int(UInt32.max) { return nil }
|
if version == 2 && payloadDataSize > Int(UInt32.max) { return nil }
|
||||||
|
|
||||||
guard let headerSize = headerSize(for: version) else { 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 estimatedPayload = payloadDataSize
|
||||||
let estimatedSignature = (packet.signature == nil ? 0 : signatureSize)
|
let estimatedSignature = (packet.signature == nil ? 0 : signatureSize)
|
||||||
var data = Data()
|
var data = Data()
|
||||||
@@ -199,7 +202,8 @@ struct BinaryProtocol {
|
|||||||
if packet.recipientID != nil { flags |= Flags.hasRecipient }
|
if packet.recipientID != nil { flags |= Flags.hasRecipient }
|
||||||
if packet.signature != nil { flags |= Flags.hasSignature }
|
if packet.signature != nil { flags |= Flags.hasSignature }
|
||||||
if isCompressed { flags |= Flags.isCompressed }
|
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)
|
data.append(flags)
|
||||||
|
|
||||||
if version == 2 {
|
if version == 2 {
|
||||||
@@ -323,6 +327,8 @@ struct BinaryProtocol {
|
|||||||
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
||||||
let hasSignature = (flags & Flags.hasSignature) != 0
|
let hasSignature = (flags & Flags.hasSignature) != 0
|
||||||
let isCompressed = (flags & Flags.isCompressed) != 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
|
let payloadLength: Int
|
||||||
if version == 2 {
|
if version == 2 {
|
||||||
@@ -343,27 +349,24 @@ struct BinaryProtocol {
|
|||||||
if recipientID == nil { return nil }
|
if recipientID == nil { return nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Route (optional, v2+ only): route bytes are NOT included in payloadLength
|
||||||
var route: [Data]? = nil
|
var route: [Data]? = nil
|
||||||
var remainingPayloadBytes = payloadLength
|
if hasRoute {
|
||||||
|
guard let routeCount = read8() else { return nil }
|
||||||
if (flags & Flags.hasRoute) != 0 {
|
|
||||||
guard remainingPayloadBytes >= 1, let routeCount = read8() else { return nil }
|
|
||||||
remainingPayloadBytes -= 1
|
|
||||||
if routeCount > 0 {
|
if routeCount > 0 {
|
||||||
var hops: [Data] = []
|
var hops: [Data] = []
|
||||||
for _ in 0..<Int(routeCount) {
|
for _ in 0..<Int(routeCount) {
|
||||||
guard remainingPayloadBytes >= senderIDSize,
|
guard let hop = readData(senderIDSize) else { return nil }
|
||||||
let hop = readData(senderIDSize) else { return nil }
|
|
||||||
remainingPayloadBytes -= senderIDSize
|
|
||||||
hops.append(hop)
|
hops.append(hop)
|
||||||
}
|
}
|
||||||
route = hops
|
route = hops
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Payload: payloadLength is exactly the payload size (+ compression preamble if compressed)
|
||||||
let payload: Data
|
let payload: Data
|
||||||
if isCompressed {
|
if isCompressed {
|
||||||
guard remainingPayloadBytes >= lengthFieldBytes else { return nil }
|
guard payloadLength >= lengthFieldBytes else { return nil }
|
||||||
let originalSize: Int
|
let originalSize: Int
|
||||||
if version == 2 {
|
if version == 2 {
|
||||||
guard let rawSize = read32() else { return nil }
|
guard let rawSize = read32() else { return nil }
|
||||||
@@ -372,11 +375,9 @@ struct BinaryProtocol {
|
|||||||
guard let rawSize = read16() else { return nil }
|
guard let rawSize = read16() else { return nil }
|
||||||
originalSize = Int(rawSize)
|
originalSize = Int(rawSize)
|
||||||
}
|
}
|
||||||
remainingPayloadBytes -= lengthFieldBytes
|
|
||||||
guard originalSize >= 0 && originalSize <= FileTransferLimits.maxFramedFileBytes else { return nil }
|
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 }
|
guard compressedSize > 0, let compressed = readData(compressedSize) else { return nil }
|
||||||
remainingPayloadBytes = 0
|
|
||||||
|
|
||||||
let compressionRatio = Double(originalSize) / Double(compressedSize)
|
let compressionRatio = Double(originalSize) / Double(compressedSize)
|
||||||
guard compressionRatio <= 50_000.0 else {
|
guard compressionRatio <= 50_000.0 else {
|
||||||
@@ -388,9 +389,7 @@ struct BinaryProtocol {
|
|||||||
decompressed.count == originalSize else { return nil }
|
decompressed.count == originalSize else { return nil }
|
||||||
payload = decompressed
|
payload = decompressed
|
||||||
} else {
|
} else {
|
||||||
guard remainingPayloadBytes >= 0,
|
guard let rawPayload = readData(payloadLength) else { return nil }
|
||||||
let rawPayload = readData(remainingPayloadBytes) else { return nil }
|
|
||||||
remainingPayloadBytes = 0
|
|
||||||
payload = rawPayload
|
payload = rawPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
// Default per-fragment chunk size when link limits are unknown
|
// Default per-fragment chunk size when link limits are unknown
|
||||||
private let defaultFragmentSize = TransportConfig.bleDefaultFragmentSize
|
private let defaultFragmentSize = TransportConfig.bleDefaultFragmentSize
|
||||||
|
private let bleMaxMTU = 512
|
||||||
private let maxMessageLength = InputValidator.Limits.maxMessageLength
|
private let maxMessageLength = InputValidator.Limits.maxMessageLength
|
||||||
private let messageTTL: UInt8 = TransportConfig.messageTTLDefault
|
private let messageTTL: UInt8 = TransportConfig.messageTTLDefault
|
||||||
// Flood/battery controls
|
// Flood/battery controls
|
||||||
@@ -51,6 +52,15 @@ final class BLEService: NSObject {
|
|||||||
// 2. BLE Centrals (when acting as peripheral)
|
// 2. BLE Centrals (when acting as peripheral)
|
||||||
private var subscribedCentrals: [CBCentral] = []
|
private var subscribedCentrals: [CBCentral] = []
|
||||||
private var centralToPeerID: [String: PeerID] = [:] // Central UUID -> Peer ID mapping
|
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)
|
// 3. Peer Information (single source of truth)
|
||||||
private struct PeerInfo {
|
private struct PeerInfo {
|
||||||
@@ -549,7 +559,7 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
func emergencyDisconnectAll() {
|
func emergencyDisconnectAll() {
|
||||||
stopServices()
|
stopServices()
|
||||||
|
|
||||||
// Clear all sessions and peers
|
// Clear all sessions and peers
|
||||||
let cancelledTransfers: [(id: String, items: [DispatchWorkItem])] = collectionsQueue.sync(flags: .barrier) {
|
let cancelledTransfers: [(id: String, items: [DispatchWorkItem])] = collectionsQueue.sync(flags: .barrier) {
|
||||||
let entries = activeTransfers.map { ($0.key, $0.value.workItems) }
|
let entries = activeTransfers.map { ($0.key, $0.value.workItems) }
|
||||||
@@ -564,16 +574,19 @@ final class BLEService: NSObject {
|
|||||||
entry.items.forEach { $0.cancel() }
|
entry.items.forEach { $0.cancel() }
|
||||||
TransferProgressManager.shared.cancel(id: entry.id)
|
TransferProgressManager.shared.cancel(id: entry.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear processed messages
|
// Clear processed messages
|
||||||
messageDeduplicator.reset()
|
messageDeduplicator.reset()
|
||||||
|
|
||||||
// Clear peripheral references
|
// Clear peripheral references
|
||||||
peripherals.removeAll()
|
peripherals.removeAll()
|
||||||
peerToPeripheralUUID.removeAll()
|
peerToPeripheralUUID.removeAll()
|
||||||
subscribedCentrals.removeAll()
|
subscribedCentrals.removeAll()
|
||||||
centralToPeerID.removeAll()
|
centralToPeerID.removeAll()
|
||||||
meshTopology.reset()
|
meshTopology.reset()
|
||||||
|
|
||||||
|
// BCH-01-004: Clear rate-limit state
|
||||||
|
centralSubscriptionRateLimits.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Connectivity and peers
|
// MARK: Connectivity and peers
|
||||||
@@ -715,7 +728,10 @@ final class BLEService: NSObject {
|
|||||||
SecureLogger.error("❌ Failed to encode file packet for private send", category: .session)
|
SecureLogger.error("❌ Failed to encode file packet for private send", category: .session)
|
||||||
return
|
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)
|
SecureLogger.error("❌ Invalid recipient peer ID for file transfer: \(peerID)", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -731,8 +747,6 @@ final class BLEService: NSObject {
|
|||||||
version: 2
|
version: 2
|
||||||
)
|
)
|
||||||
|
|
||||||
self.applyRouteIfAvailable(&packet, to: peerID)
|
|
||||||
|
|
||||||
if let signed = self.noiseService.signPacket(packet) {
|
if let signed = self.noiseService.signPacket(packet) {
|
||||||
packet = signed
|
packet = signed
|
||||||
}
|
}
|
||||||
@@ -752,7 +766,7 @@ final class BLEService: NSObject {
|
|||||||
SecureLogger.debug("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)", category: .session)
|
SecureLogger.debug("📤 Sending READ receipt for message \(receipt.originalMessageID) to \(peerID)", category: .session)
|
||||||
do {
|
do {
|
||||||
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
||||||
var packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -761,7 +775,6 @@ final class BLEService: NSObject {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
applyRouteIfAvailable(&packet, to: peerID)
|
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.error("Failed to send read receipt: \(error)")
|
SecureLogger.error("Failed to send read receipt: \(error)")
|
||||||
@@ -780,21 +793,29 @@ final class BLEService: NSObject {
|
|||||||
// MARK: - Packet Broadcasting
|
// MARK: - Packet Broadcasting
|
||||||
|
|
||||||
private func broadcastPacket(_ packet: BitchatPacket, transferId: String? = nil) {
|
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
|
// Encode once using a small per-type padding policy, then delegate by type
|
||||||
let padForBLE = padPolicy(for: packet.type)
|
let padForBLE = padPolicy(for: packetToSend.type)
|
||||||
if packet.type == MessageType.fileTransfer.rawValue {
|
if packetToSend.type == MessageType.fileTransfer.rawValue {
|
||||||
sendFragmentedPacket(packet, pad: padForBLE, maxChunk: nil, directedOnlyPeer: nil, transferId: transferId)
|
sendFragmentedPacket(packetToSend, pad: padForBLE, maxChunk: nil, directedOnlyPeer: nil, transferId: transferId)
|
||||||
return
|
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)
|
SecureLogger.error("❌ Failed to convert packet to binary data", category: .session)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if packet.type == MessageType.noiseEncrypted.rawValue {
|
if packetToSend.type == MessageType.noiseEncrypted.rawValue {
|
||||||
sendEncrypted(packet, data: data, pad: padForBLE)
|
sendEncrypted(packetToSend, data: data, pad: padForBLE)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sendGenericBroadcast(packet, data: data, pad: padForBLE)
|
sendGenericBroadcast(packetToSend, data: data, pad: padForBLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Broadcast helpers (single responsibility)
|
// MARK: - Broadcast helpers (single responsibility)
|
||||||
@@ -1149,6 +1170,9 @@ final class BLEService: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BCH-01-002: Enforce storage quota before saving
|
||||||
|
enforceIncomingFilesQuota(reservingBytes: filePacket.content.count)
|
||||||
|
|
||||||
let fallbackExt = mime.defaultExtension
|
let fallbackExt = mime.defaultExtension
|
||||||
let subdirectory: String
|
let subdirectory: String
|
||||||
switch mime.category {
|
switch mime.category {
|
||||||
@@ -1234,7 +1258,7 @@ final class BLEService: NSObject {
|
|||||||
if noiseService.hasEstablishedSession(with: peerID) {
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
do {
|
do {
|
||||||
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
let encrypted = try noiseService.encrypt(payload, for: peerID)
|
||||||
var packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -1243,7 +1267,6 @@ final class BLEService: NSObject {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
applyRouteIfAvailable(&packet, to: peerID)
|
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.error("Failed to send delivery ACK: \(error)")
|
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() {
|
private func sendLeave() {
|
||||||
SecureLogger.debug("👋 Sending leave announcement", category: .session)
|
SecureLogger.debug("👋 Sending leave announcement", category: .session)
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
@@ -1753,8 +1842,9 @@ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeriph
|
|||||||
peers[peerID] = info
|
peers[peerID] = info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clearDirectLink(with: peerID)
|
refreshLocalTopology()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Restart scanning with allow duplicates for faster rediscovery
|
// Restart scanning with allow duplicates for faster rediscovery
|
||||||
if centralManager?.state == .poweredOn {
|
if centralManager?.state == .poweredOn {
|
||||||
@@ -2041,7 +2131,7 @@ extension BLEService: CBPeripheralDelegate {
|
|||||||
peripherals[peripheralUUID] = state
|
peripherals[peripheralUUID] = state
|
||||||
}
|
}
|
||||||
peerToPeripheralUUID[senderID] = peripheralUUID
|
peerToPeripheralUUID[senderID] = peripheralUUID
|
||||||
registerDirectLink(with: senderID)
|
refreshLocalTopology()
|
||||||
}
|
}
|
||||||
|
|
||||||
let msgID = makeMessageID(for: packet)
|
let msgID = makeMessageID(for: packet)
|
||||||
@@ -2173,9 +2263,70 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
|
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)
|
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
|
messageQueue.asyncAfter(deadline: .now() + TransportConfig.blePostAnnounceDelaySeconds) { [weak self] in
|
||||||
self?.sendAnnounce(forceSend: true)
|
self?.sendAnnounce(forceSend: true)
|
||||||
// Flush any spooled directed packets now that we have a central subscribed
|
// Flush any spooled directed packets now that we have a central subscribed
|
||||||
@@ -2184,6 +2335,15 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
self?.rebroadcastRecentAnnounces()
|
self?.rebroadcastRecentAnnounces()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
|
||||||
SecureLogger.debug("📤 Central unsubscribed: \(central.identifier.uuidString)", category: .session)
|
SecureLogger.debug("📤 Central unsubscribed: \(central.identifier.uuidString)", category: .session)
|
||||||
@@ -2208,7 +2368,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
|
|
||||||
// Clean up mappings
|
// Clean up mappings
|
||||||
centralToPeerID.removeValue(forKey: centralUUID)
|
centralToPeerID.removeValue(forKey: centralUUID)
|
||||||
clearDirectLink(with: peerID)
|
refreshLocalTopology()
|
||||||
|
|
||||||
// Update UI immediately
|
// Update UI immediately
|
||||||
notifyUI { [weak self] in
|
notifyUI { [weak self] in
|
||||||
@@ -2327,7 +2487,7 @@ extension BLEService: CBPeripheralManagerDelegate {
|
|||||||
if packet.type == MessageType.announce.rawValue {
|
if packet.type == MessageType.announce.rawValue {
|
||||||
if packet.ttl == messageTTL {
|
if packet.ttl == messageTTL {
|
||||||
centralToPeerID[centralUUID] = senderID
|
centralToPeerID[centralUUID] = senderID
|
||||||
registerDirectLink(with: senderID)
|
refreshLocalTopology()
|
||||||
}
|
}
|
||||||
// Record ingress link for last-hop suppression then process
|
// Record ingress link for last-hop suppression then process
|
||||||
let msgID = makeMessageID(for: packet)
|
let msgID = makeMessageID(for: packet)
|
||||||
@@ -2442,27 +2602,39 @@ extension BLEService {
|
|||||||
peerID.toShort().routingData
|
peerID.toShort().routingData
|
||||||
}
|
}
|
||||||
|
|
||||||
private func registerDirectLink(with peerID: PeerID) {
|
private func refreshLocalTopology() {
|
||||||
meshTopology.recordDirectLink(between: myPeerIDData, and: routingData(for: peerID))
|
let neighbors: [Data] = collectionsQueue.sync {
|
||||||
}
|
peers.values.filter { $0.isConnected }.compactMap { $0.peerID.routingData }
|
||||||
|
}
|
||||||
private func clearDirectLink(with peerID: PeerID) {
|
meshTopology.updateNeighbors(for: myPeerIDData, neighbors: neighbors)
|
||||||
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 computeRoute(to peerID: PeerID) -> [Data]? {
|
private func computeRoute(to peerID: PeerID) -> [Data]? {
|
||||||
meshTopology.computeRoute(from: myPeerIDData, to: routingData(for: peerID))
|
meshTopology.computeRoute(from: myPeerIDData, to: routingData(for: peerID))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyRouteIfAvailable(_ packet: inout BitchatPacket, to recipient: PeerID) {
|
private func applyRouteIfAvailable(_ packet: BitchatPacket, to recipient: PeerID) -> BitchatPacket {
|
||||||
guard let route = computeRoute(to: recipient), route.count >= 2 else { return }
|
guard let route = computeRoute(to: recipient), route.count >= 1 else {
|
||||||
packet.route = route
|
return packet
|
||||||
meshTopology.recordRoute(route)
|
}
|
||||||
|
// 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? {
|
private func routingPeer(from data: Data) -> PeerID? {
|
||||||
@@ -2472,23 +2644,46 @@ extension BLEService {
|
|||||||
private func forwardAlongRouteIfNeeded(_ packet: BitchatPacket) -> Bool {
|
private func forwardAlongRouteIfNeeded(_ packet: BitchatPacket) -> Bool {
|
||||||
guard let route = packet.route, !route.isEmpty else { return false }
|
guard let route = packet.route, !route.isEmpty else { return false }
|
||||||
let myRoutingData = routingData(for: myPeerID) ?? (myPeerIDData.isEmpty ? nil : myPeerIDData)
|
let myRoutingData = routingData(for: myPeerID) ?? (myPeerIDData.isEmpty ? nil : myPeerIDData)
|
||||||
guard let selfData = myRoutingData,
|
guard let selfData = myRoutingData else { return false }
|
||||||
let index = route.firstIndex(of: selfData) else { return false }
|
|
||||||
|
// Route contains only intermediate hops (start and end excluded)
|
||||||
// No further hops: respect explicit route termination
|
// If we're not in the route, we're the sender - forward to first hop
|
||||||
if index == route.count - 1 {
|
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
|
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]
|
let nextHopData = route[index + 1]
|
||||||
guard let nextPeer = routingPeer(from: nextHopData),
|
guard let nextPeer = routingPeer(from: nextHopData),
|
||||||
isPeerConnected(nextPeer) else {
|
isPeerConnected(nextPeer) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
registerDirectLink(with: nextPeer)
|
|
||||||
var relayPacket = packet
|
var relayPacket = packet
|
||||||
relayPacket.ttl = packet.ttl - 1
|
relayPacket.ttl = packet.ttl - 1
|
||||||
sendPacketDirected(relayPacket, to: nextPeer)
|
sendPacketDirected(relayPacket, to: nextPeer)
|
||||||
@@ -2547,7 +2742,7 @@ extension BLEService {
|
|||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let encrypted = try noiseService.encrypt(typedPayload, for: peerID)
|
let encrypted = try noiseService.encrypt(typedPayload, for: peerID)
|
||||||
var packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -2556,7 +2751,6 @@ extension BLEService {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
applyRouteIfAvailable(&packet, to: peerID)
|
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.error("Failed to send verification payload: \(error)")
|
SecureLogger.error("Failed to send verification payload: \(error)")
|
||||||
@@ -2774,8 +2968,8 @@ extension BLEService {
|
|||||||
recipientData.append(byte)
|
recipientData.append(byte)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: recipientData,
|
recipientID: recipientData,
|
||||||
@@ -2784,7 +2978,6 @@ extension BLEService {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
applyRouteIfAvailable(&packet, to: recipientID)
|
|
||||||
|
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
|
|
||||||
@@ -2824,7 +3017,7 @@ extension BLEService {
|
|||||||
let handshakeData = try noiseService.initiateHandshake(with: peerID)
|
let handshakeData = try noiseService.initiateHandshake(with: peerID)
|
||||||
|
|
||||||
// Send handshake init
|
// Send handshake init
|
||||||
var packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseHandshake.rawValue,
|
type: MessageType.noiseHandshake.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -2833,7 +3026,6 @@ extension BLEService {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
applyRouteIfAvailable(&packet, to: peerID)
|
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.error("Failed to initiate handshake: \(error)")
|
SecureLogger.error("Failed to initiate handshake: \(error)")
|
||||||
@@ -2867,7 +3059,7 @@ extension BLEService {
|
|||||||
|
|
||||||
let encrypted = try noiseService.encrypt(messagePayload, for: peerID)
|
let encrypted = try noiseService.encrypt(messagePayload, for: peerID)
|
||||||
|
|
||||||
var packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.noiseEncrypted.rawValue,
|
type: MessageType.noiseEncrypted.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -2876,7 +3068,6 @@ extension BLEService {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
applyRouteIfAvailable(&packet, to: peerID)
|
|
||||||
|
|
||||||
// We're already on messageQueue from the callback
|
// We're already on messageQueue from the callback
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
@@ -2970,7 +3161,21 @@ extension BLEService {
|
|||||||
}
|
}
|
||||||
// Fragment the unpadded frame; each fragment will be encoded independently
|
// Fragment the unpadded frame; each fragment will be encoded independently
|
||||||
let fragmentID = Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
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 safeChunk = max(64, chunk)
|
||||||
let fragments = stride(from: 0, to: fullData.count, by: safeChunk).map { offset in
|
let fragments = stride(from: 0, to: fullData.count, by: safeChunk).map { offset in
|
||||||
Data(fullData[offset..<min(offset + safeChunk, fullData.count)])
|
Data(fullData[offset..<min(offset + safeChunk, fullData.count)])
|
||||||
@@ -3029,6 +3234,7 @@ extension BLEService {
|
|||||||
payload: payload,
|
payload: payload,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: packet.ttl,
|
ttl: packet.ttl,
|
||||||
|
version: fragmentVersion,
|
||||||
route: packet.route
|
route: packet.route
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3245,10 +3451,6 @@ extension BLEService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRoute(packet.route)
|
|
||||||
if peerID != myPeerID && packet.ttl == messageTTL {
|
|
||||||
registerDirectLink(with: peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deduplication (thread-safe)
|
// Deduplication (thread-safe)
|
||||||
let senderID = PeerID(hexData: packet.senderID)
|
let senderID = PeerID(hexData: packet.senderID)
|
||||||
@@ -3450,6 +3652,9 @@ extension BLEService {
|
|||||||
// Require verified announce; ignore otherwise (no backward compatibility)
|
// Require verified announce; ignore otherwise (no backward compatibility)
|
||||||
if !verified {
|
if !verified {
|
||||||
SecureLogger.warning("❌ Ignoring unverified announce from \(peerID.id.prefix(8))…", category: .security)
|
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
|
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
|
// Persist cryptographic identity and signing key for robust offline verification
|
||||||
identityManager.upsertCryptographicIdentity(
|
identityManager.upsertCryptographicIdentity(
|
||||||
fingerprint: announcement.noisePublicKey.sha256Fingerprint(),
|
fingerprint: announcement.noisePublicKey.sha256Fingerprint(),
|
||||||
@@ -3683,7 +3893,7 @@ extension BLEService {
|
|||||||
do {
|
do {
|
||||||
if let response = try noiseService.processHandshakeMessage(from: peerID, message: packet.payload) {
|
if let response = try noiseService.processHandshakeMessage(from: peerID, message: packet.payload) {
|
||||||
// Send response
|
// Send response
|
||||||
var responsePacket = BitchatPacket(
|
let responsePacket = BitchatPacket(
|
||||||
type: MessageType.noiseHandshake.rawValue,
|
type: MessageType.noiseHandshake.rawValue,
|
||||||
senderID: myPeerIDData,
|
senderID: myPeerIDData,
|
||||||
recipientID: Data(hexString: peerID.id),
|
recipientID: Data(hexString: peerID.id),
|
||||||
@@ -3692,7 +3902,6 @@ extension BLEService {
|
|||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: messageTTL
|
||||||
)
|
)
|
||||||
applyRouteIfAvailable(&responsePacket, to: peerID)
|
|
||||||
// We're on messageQueue from delegate callback
|
// We're on messageQueue from delegate callback
|
||||||
broadcastPacket(responsePacket)
|
broadcastPacket(responsePacket)
|
||||||
}
|
}
|
||||||
@@ -3977,6 +4186,11 @@ extension BLEService {
|
|||||||
self.delegate?.didUpdatePeerList(currentPeerIDs)
|
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() {
|
private func performCleanup() {
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ public final class GeohashParticipantTracker: ObservableObject {
|
|||||||
var map = participants[geohash] ?? [:]
|
var map = participants[geohash] ?? [:]
|
||||||
map[key] = Date()
|
map[key] = Date()
|
||||||
participants[geohash] = map
|
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
|
// Only refresh visible list if this geohash is currently active
|
||||||
if activeGeohash == geohash {
|
if activeGeohash == geohash {
|
||||||
|
|||||||
@@ -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 Foundation
|
||||||
import Security
|
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 {
|
protocol KeychainManagerProtocol {
|
||||||
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool
|
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool
|
||||||
func getIdentityKey(forKey key: String) -> Data?
|
func getIdentityKey(forKey key: String) -> Data?
|
||||||
@@ -21,6 +62,12 @@ protocol KeychainManagerProtocol {
|
|||||||
|
|
||||||
func verifyIdentityKeyExists() -> Bool
|
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)
|
// MARK: - Generic Data Storage (consolidated from KeychainHelper)
|
||||||
/// Save data with a custom service name
|
/// Save data with a custom service name
|
||||||
func save(key: String, data: Data, service: String, accessible: CFString?)
|
func save(key: String, data: Data, service: String, accessible: CFString?)
|
||||||
@@ -54,7 +101,181 @@ final class KeychainManager: KeychainManagerProtocol {
|
|||||||
SecureLogger.logKeyOperation(.delete, keyType: key, success: result)
|
SecureLogger.logKeyOperation(.delete, keyType: key, success: result)
|
||||||
return result
|
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
|
// MARK: - Generic Operations
|
||||||
|
|
||||||
private func save(_ value: String, forKey key: String) -> Bool {
|
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 queue = DispatchQueue(label: "mesh.topology", attributes: .concurrent)
|
||||||
private let hopSize = 8
|
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() {
|
func reset() {
|
||||||
queue.sync(flags: .barrier) {
|
queue.sync(flags: .barrier) {
|
||||||
self.adjacency.removeAll()
|
self.claims.removeAll()
|
||||||
|
self.lastSeen.removeAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordDirectLink(between a: Data?, and b: Data?) {
|
/// Update the topology with a node's self-reported neighbor list
|
||||||
guard let left = sanitize(a), let right = sanitize(b), left != right else { return }
|
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])
|
||||||
|
|
||||||
queue.sync(flags: .barrier) {
|
queue.sync(flags: .barrier) {
|
||||||
var setA = self.adjacency[left] ?? []
|
self.claims[source] = validNeighbors
|
||||||
setA.insert(right)
|
self.lastSeen[source] = Date()
|
||||||
self.adjacency[left] = setA
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removePeer(_ data: Data?) {
|
func removePeer(_ data: Data?) {
|
||||||
guard let peer = sanitize(data) else { return }
|
guard let peer = sanitize(data) else { return }
|
||||||
queue.sync(flags: .barrier) {
|
queue.sync(flags: .barrier) {
|
||||||
guard let neighbors = self.adjacency.removeValue(forKey: peer) else { return }
|
self.claims.removeValue(forKey: peer)
|
||||||
for neighbor in neighbors {
|
self.lastSeen.removeValue(forKey: peer)
|
||||||
if var set = self.adjacency[neighbor] {
|
|
||||||
set.remove(peer)
|
|
||||||
self.adjacency[neighbor] = set.isEmpty ? nil : set
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordRoute(_ hops: [Data]) {
|
/// Prune nodes that haven't updated their topology in `age` seconds
|
||||||
let sanitized = hops.compactMap { sanitize($0) }
|
func prune(olderThan age: TimeInterval) {
|
||||||
guard sanitized.count >= 2 else { return }
|
let deadline = Date().addingTimeInterval(-age)
|
||||||
queue.sync(flags: .barrier) {
|
queue.sync(flags: .barrier) {
|
||||||
for idx in 0..<(sanitized.count - 1) {
|
let stale = self.lastSeen.filter { $0.value < deadline }
|
||||||
let left = sanitized[idx]
|
for (peer, _) in stale {
|
||||||
let right = sanitized[idx + 1]
|
self.claims.removeValue(forKey: peer)
|
||||||
guard left != right else { continue }
|
self.lastSeen.removeValue(forKey: peer)
|
||||||
|
|
||||||
var setA = self.adjacency[left] ?? []
|
|
||||||
setA.insert(right)
|
|
||||||
self.adjacency[left] = setA
|
|
||||||
|
|
||||||
var setB = self.adjacency[right] ?? []
|
|
||||||
setB.insert(left)
|
|
||||||
self.adjacency[right] = setB
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 }
|
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 }
|
return queue.sync {
|
||||||
guard graph[source] != nil, graph[target] != nil else { return nil }
|
// BFS
|
||||||
|
var visited: Set<RoutingID> = [source]
|
||||||
|
// Queue stores paths: [Start, Hop1, Hop2, ..., Current]
|
||||||
|
var queuePaths: [[RoutingID]] = [[source]]
|
||||||
|
|
||||||
|
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 }
|
||||||
|
|
||||||
|
guard let last = path.last else { continue }
|
||||||
|
|
||||||
|
// Get neighbors that 'last' claims to see
|
||||||
|
guard let neighbors = claims[last] else { continue }
|
||||||
|
|
||||||
var visited: Set<RoutingID> = [source]
|
for neighbor in neighbors {
|
||||||
var queuePaths: [[RoutingID]] = [[source]]
|
if visited.contains(neighbor) { continue }
|
||||||
var index = 0
|
|
||||||
|
// CONFIRMED EDGE CHECK:
|
||||||
while index < queuePaths.count {
|
// 'last' claims 'neighbor' (checked above)
|
||||||
let path = queuePaths[index]
|
// Does 'neighbor' claim 'last'?
|
||||||
index += 1
|
guard let neighborClaims = claims[neighbor],
|
||||||
guard path.count <= maxHops else { continue }
|
neighborClaims.contains(last) else {
|
||||||
guard let last = path.last, let neighbors = graph[last] else { continue }
|
continue
|
||||||
|
}
|
||||||
for neighbor in neighbors {
|
|
||||||
if visited.contains(neighbor) { continue }
|
var nextPath = path
|
||||||
var nextPath = path
|
nextPath.append(neighbor)
|
||||||
nextPath.append(neighbor)
|
|
||||||
if neighbor == target { return nextPath }
|
if neighbor == target {
|
||||||
if nextPath.count <= maxHops {
|
// Return only intermediate hops
|
||||||
|
// Path: [Source, I1, I2, Target] -> [I1, I2]
|
||||||
|
return Array(nextPath.dropFirst().dropLast())
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.insert(neighbor)
|
||||||
queuePaths.append(nextPath)
|
queuePaths.append(nextPath)
|
||||||
}
|
}
|
||||||
visited.insert(neighbor)
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|||||||
@@ -199,64 +199,153 @@ final class NoiseEncryptionService {
|
|||||||
|
|
||||||
init(keychain: KeychainManagerProtocol) {
|
init(keychain: KeychainManagerProtocol) {
|
||||||
self.keychain = keychain
|
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
|
let loadedKey: Curve25519.KeyAgreement.PrivateKey
|
||||||
|
|
||||||
// Try to load from keychain
|
// Try to load from keychain with proper error classification
|
||||||
if let identityData = keychain.getIdentityKey(forKey: "noiseStaticKey"),
|
let noiseKeyResult = keychain.getIdentityKeyWithResult(forKey: "noiseStaticKey")
|
||||||
let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) {
|
|
||||||
loadedKey = key
|
switch noiseKeyResult {
|
||||||
SecureLogger.logKeyOperation(.load, keyType: "noiseStaticKey", success: true)
|
case .success(let identityData):
|
||||||
}
|
if let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) {
|
||||||
// If no identity exists, create new one
|
loadedKey = key
|
||||||
else {
|
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()
|
loadedKey = Curve25519.KeyAgreement.PrivateKey()
|
||||||
let keyData = loadedKey.rawRepresentation
|
|
||||||
|
|
||||||
// Save to keychain
|
|
||||||
let saved = keychain.saveIdentityKey(keyData, forKey: "noiseStaticKey")
|
|
||||||
SecureLogger.logKeyOperation(.create, keyType: "noiseStaticKey", success: saved)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now assign the final value
|
// Now assign the final value
|
||||||
self.staticIdentityKey = loadedKey
|
self.staticIdentityKey = loadedKey
|
||||||
self.staticIdentityPublicKey = staticIdentityKey.publicKey
|
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
|
let loadedSigningKey: Curve25519.Signing.PrivateKey
|
||||||
|
|
||||||
// Try to load from keychain
|
let signingKeyResult = keychain.getIdentityKeyWithResult(forKey: "ed25519SigningKey")
|
||||||
if let signingData = keychain.getIdentityKey(forKey: "ed25519SigningKey"),
|
|
||||||
let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
|
switch signingKeyResult {
|
||||||
loadedSigningKey = key
|
case .success(let signingData):
|
||||||
SecureLogger.logKeyOperation(.load, keyType: "ed25519SigningKey", success: true)
|
if let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
|
||||||
}
|
loadedSigningKey = key
|
||||||
// If no signing key exists, create new one
|
SecureLogger.logKeyOperation(.load, keyType: "ed25519SigningKey", success: true)
|
||||||
else {
|
} 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()
|
loadedSigningKey = Curve25519.Signing.PrivateKey()
|
||||||
let keyData = loadedSigningKey.rawRepresentation
|
|
||||||
|
|
||||||
// Save to keychain
|
|
||||||
let saved = keychain.saveIdentityKey(keyData, forKey: "ed25519SigningKey")
|
|
||||||
SecureLogger.logKeyOperation(.create, keyType: "ed25519SigningKey", success: saved)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now assign the signing keys
|
// Now assign the signing keys
|
||||||
self.signingKey = loadedSigningKey
|
self.signingKey = loadedSigningKey
|
||||||
self.signingPublicKey = signingKey.publicKey
|
self.signingPublicKey = signingKey.publicKey
|
||||||
|
|
||||||
// Initialize session manager
|
// Initialize session manager
|
||||||
self.sessionManager = NoiseSessionManager(localStaticKey: staticIdentityKey, keychain: keychain)
|
self.sessionManager = NoiseSessionManager(localStaticKey: staticIdentityKey, keychain: keychain)
|
||||||
|
|
||||||
// Set up session callbacks
|
// Set up session callbacks
|
||||||
sessionManager.onSessionEstablished = { [weak self] peerID, remoteStaticKey in
|
sessionManager.onSessionEstablished = { [weak self] peerID, remoteStaticKey in
|
||||||
self?.handleSessionEstablished(peerID: peerID, remoteStaticKey: remoteStaticKey)
|
self?.handleSessionEstablished(peerID: peerID, remoteStaticKey: remoteStaticKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start session maintenance timer
|
// Start session maintenance timer
|
||||||
startRekeyTimer()
|
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
|
// MARK: - Public Interface
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ struct NotificationStreamAssembler {
|
|||||||
let hasRecipient = (flags & BinaryProtocol.Flags.hasRecipient) != 0
|
let hasRecipient = (flags & BinaryProtocol.Flags.hasRecipient) != 0
|
||||||
let hasSignature = (flags & BinaryProtocol.Flags.hasSignature) != 0
|
let hasSignature = (flags & BinaryProtocol.Flags.hasSignature) != 0
|
||||||
let isCompressed = (flags & BinaryProtocol.Flags.isCompressed) != 0
|
let isCompressed = (flags & BinaryProtocol.Flags.isCompressed) != 0
|
||||||
|
let hasRoute = (version >= 2) && (flags & BinaryProtocol.Flags.hasRoute) != 0
|
||||||
|
|
||||||
let lengthOffset = 12
|
let lengthOffset = 12
|
||||||
let payloadLength: Int
|
let payloadLength: Int
|
||||||
@@ -80,6 +81,15 @@ struct NotificationStreamAssembler {
|
|||||||
var frameLength = framePrefix + payloadLength
|
var frameLength = framePrefix + payloadLength
|
||||||
if hasRecipient { frameLength += BinaryProtocol.recipientIDSize }
|
if hasRecipient { frameLength += BinaryProtocol.recipientIDSize }
|
||||||
if hasSignature { frameLength += BinaryProtocol.signatureSize }
|
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 {
|
if isCompressed {
|
||||||
let rawLengthFieldBytes = (version == 2) ? 4 : 2
|
let rawLengthFieldBytes = (version == 2) ? 4 : 2
|
||||||
if payloadLength < rawLengthFieldBytes {
|
if payloadLength < rawLengthFieldBytes {
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ protocol Transport: AnyObject {
|
|||||||
// QR verification (optional for transports)
|
// QR verification (optional for transports)
|
||||||
func sendVerifyChallenge(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
|
func sendVerifyChallenge(to peerID: PeerID, noiseKeyHex: String, nonceA: Data)
|
||||||
func sendVerifyResponse(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 {
|
extension Transport {
|
||||||
@@ -70,6 +74,9 @@ extension Transport {
|
|||||||
func sendMessage(_ content: String, mentions: [String], messageID: String, timestamp: Date) {
|
func sendMessage(_ content: String, mentions: [String], messageID: String, timestamp: Date) {
|
||||||
sendMessage(content, mentions: mentions)
|
sendMessage(content, mentions: mentions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func acceptPendingFile(id: String) -> URL? { nil }
|
||||||
|
func declinePendingFile(id: String) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol TransportPeerEventsDelegate: AnyObject {
|
protocol TransportPeerEventsDelegate: AnyObject {
|
||||||
|
|||||||
@@ -162,6 +162,14 @@ enum TransportConfig {
|
|||||||
static let blePostAnnounceDelaySeconds: TimeInterval = 0.4
|
static let blePostAnnounceDelaySeconds: TimeInterval = 0.4
|
||||||
static let bleForceAnnounceMinIntervalSeconds: TimeInterval = 0.15
|
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
|
// Store-and-forward for directed packets at relays
|
||||||
static let bleDirectedSpoolWindowSeconds: TimeInterval = 15.0
|
static let bleDirectedSpoolWindowSeconds: TimeInterval = 15.0
|
||||||
|
|
||||||
|
|||||||
@@ -52,11 +52,13 @@ struct InputValidator {
|
|||||||
// MessageType/NoisePayloadType enums; keeping validator free of stale lists.
|
// MessageType/NoisePayloadType enums; keeping validator free of stale lists.
|
||||||
|
|
||||||
/// Validates timestamp is reasonable (not too far in past or future)
|
/// 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 {
|
static func validateTimestamp(_ timestamp: Date) -> Bool {
|
||||||
let now = Date()
|
let now = Date()
|
||||||
let oneHourAgo = now.addingTimeInterval(-3600)
|
// 5 minutes = 300 seconds (industry standard for replay protection)
|
||||||
let oneHourFromNow = now.addingTimeInterval(3600)
|
let fiveMinutesAgo = now.addingTimeInterval(-300)
|
||||||
return timestamp >= oneHourAgo && timestamp <= oneHourFromNow
|
let fiveMinutesFromNow = now.addingTimeInterval(300)
|
||||||
|
return timestamp >= fiveMinutesAgo && timestamp <= fiveMinutesFromNow
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2055,13 +2055,42 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, CommandContextProv
|
|||||||
} catch {
|
} catch {
|
||||||
SecureLogger.error("Failed to clear media files during panic: \(error)", category: .session)
|
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
|
// Force immediate UI update for panic mode
|
||||||
// UI updates immediately - no flushing needed
|
// UI updates immediately - no flushing needed
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
// MARK: - Autocomplete
|
||||||
|
|
||||||
func updateAutocomplete(for text: String, cursorPosition: Int) {
|
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
|
// Low-level BLE events
|
||||||
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
switch type {
|
switch type {
|
||||||
case .privateMessage:
|
case .privateMessage:
|
||||||
guard let pm = PrivateMessagePacket.decode(from: payload) else { return }
|
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 senderName = unifiedPeerService.getPeer(by: peerID)?.nickname ?? "Unknown"
|
||||||
let pmMentions = parseMentions(from: pm.content)
|
let pmMentions = parseMentions(from: pm.content)
|
||||||
let msg = BitchatMessage(
|
let msg = BitchatMessage(
|
||||||
id: pm.messageID,
|
id: pm.messageID,
|
||||||
sender: senderName,
|
sender: senderName,
|
||||||
content: pm.content,
|
content: pm.content,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
isRelay: false,
|
isRelay: false,
|
||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
recipientNickname: nickname,
|
recipientNickname: nickname,
|
||||||
senderPeerID: peerID,
|
senderPeerID: peerID,
|
||||||
mentions: pmMentions.isEmpty ? nil : pmMentions
|
mentions: pmMentions.isEmpty ? nil : pmMentions
|
||||||
)
|
)
|
||||||
handlePrivateMessage(msg)
|
handlePrivateMessage(msg)
|
||||||
// Send delivery ACK back over BLE
|
// Send delivery ACK back over BLE
|
||||||
meshService.sendDeliveryAck(for: pm.messageID, to: peerID)
|
meshService.sendDeliveryAck(for: pm.messageID, to: peerID)
|
||||||
|
|
||||||
case .delivered:
|
case .delivered:
|
||||||
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
||||||
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
|
guard let name = unifiedPeerService.getPeer(by: peerID)?.nickname,
|
||||||
if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
let (foundPeerID, idx) = findMessageIndex(messageID: messageID, peerID: peerID) else { return }
|
||||||
privateChats[peerID]?[idx].deliveryStatus = .delivered(to: name, at: Date())
|
|
||||||
objectWillChange.send()
|
// 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:
|
case .readReceipt:
|
||||||
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
guard let messageID = String(data: payload, encoding: .utf8) else { return }
|
||||||
if let name = unifiedPeerService.getPeer(by: peerID)?.nickname {
|
guard let name = unifiedPeerService.getPeer(by: peerID)?.nickname,
|
||||||
if let messages = privateChats[peerID], let idx = messages.firstIndex(where: { $0.id == messageID }) {
|
let (foundPeerID, idx) = findMessageIndex(messageID: messageID, peerID: peerID) else { return }
|
||||||
privateChats[peerID]?[idx].deliveryStatus = .read(by: name, at: Date())
|
|
||||||
objectWillChange.send()
|
// 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:
|
case .verifyChallenge:
|
||||||
// Parse and respond
|
// Parse and respond
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ extension ChatViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func subscribeNostrEvent(_ event: NostrEvent) {
|
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)
|
!deduplicationService.hasProcessedNostrEvent(event.id)
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
@@ -86,6 +87,11 @@ extension ChatViewModel {
|
|||||||
// Update participants last-seen for this pubkey
|
// Update participants last-seen for this pubkey
|
||||||
participantTracker.recordParticipant(pubkeyHex: event.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
|
// Track teleported tag (only our format ["t","teleport"]) for icon state
|
||||||
let hasTeleportTag = event.tags.contains(where: { tag in
|
let hasTeleportTag = event.tags.contains(where: { tag in
|
||||||
tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport"
|
tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport"
|
||||||
@@ -124,12 +130,21 @@ extension ChatViewModel {
|
|||||||
mentions: mentions.isEmpty ? nil : mentions
|
mentions: mentions.isEmpty ? nil : mentions
|
||||||
)
|
)
|
||||||
Task { @MainActor in
|
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)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribeGiftWrap(_ giftWrap: NostrEvent, id: NostrIdentity) {
|
func subscribeGiftWrap(_ giftWrap: NostrEvent, id: NostrIdentity) {
|
||||||
guard !deduplicationService.hasProcessedNostrEvent(giftWrap.id) else { return }
|
guard !deduplicationService.hasProcessedNostrEvent(giftWrap.id) else { return }
|
||||||
deduplicationService.recordNostrEvent(giftWrap.id)
|
deduplicationService.recordNostrEvent(giftWrap.id)
|
||||||
@@ -239,8 +254,9 @@ extension ChatViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleNostrEvent(_ event: NostrEvent) {
|
func handleNostrEvent(_ event: NostrEvent) {
|
||||||
// Only handle ephemeral kind 20000 with matching tag
|
// Only handle ephemeral kind 20000 or presence kind 20001 with matching tag
|
||||||
guard event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue else { return }
|
guard (event.kind == NostrProtocol.EventKind.ephemeralEvent.rawValue ||
|
||||||
|
event.kind == NostrProtocol.EventKind.geohashPresence.rawValue) else { return }
|
||||||
|
|
||||||
// Deduplicate
|
// Deduplicate
|
||||||
if deduplicationService.hasProcessedNostrEvent(event.id) { return }
|
if deduplicationService.hasProcessedNostrEvent(event.id) { return }
|
||||||
@@ -250,6 +266,11 @@ extension ChatViewModel {
|
|||||||
let tagSummary = event.tags.map { "[" + $0.joined(separator: ",") + "]" }.joined(separator: ",")
|
let tagSummary = event.tags.map { "[" + $0.joined(separator: ",") + "]" }.joined(separator: ",")
|
||||||
SecureLogger.debug("GeoTeleport: recv pub=\(event.pubkey.prefix(8))… tags=\(tagSummary)", category: .session)
|
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"]
|
// Track teleport tag for participants – only our format ["t", "teleport"]
|
||||||
let hasTeleportTag: Bool = event.tags.contains { tag in
|
let hasTeleportTag: Bool = event.tags.contains { tag in
|
||||||
tag.count >= 2 && tag[0].lowercased() == "t" && tag[1].lowercased() == "teleport"
|
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
|
// Skip only very recent self-echo from relay; include older self events for hydration
|
||||||
if isSelf {
|
if isSelf {
|
||||||
let eventTime = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
let eventTime = Date(timeIntervalSince1970: TimeInterval(event.created_at))
|
||||||
@@ -287,17 +311,14 @@ extension ChatViewModel {
|
|||||||
geoNicknames[event.pubkey.lowercased()] = nick
|
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
|
// Store mapping for geohash DM initiation
|
||||||
nostrKeyMapping[PeerID(nostr_: event.pubkey)] = event.pubkey
|
nostrKeyMapping[PeerID(nostr_: event.pubkey)] = event.pubkey
|
||||||
nostrKeyMapping[PeerID(nostr: event.pubkey)] = event.pubkey
|
nostrKeyMapping[PeerID(nostr: event.pubkey)] = event.pubkey
|
||||||
|
|
||||||
// Update participants last-seen for this pubkey
|
// If presence heartbeat (Kind 20001), stop here - no content to display
|
||||||
participantTracker.recordParticipant(pubkeyHex: event.pubkey)
|
if event.kind == NostrProtocol.EventKind.geohashPresence.rawValue {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let senderName = displayNameForNostrPubkey(event.pubkey)
|
let senderName = displayNameForNostrPubkey(event.pubkey)
|
||||||
let content = event.content
|
let content = event.content
|
||||||
@@ -464,7 +485,8 @@ extension ChatViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func subscribeNostrEvent(_ event: NostrEvent, gh: String) {
|
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
|
// Compute current participant count (5-minute window) BEFORE updating with this event
|
||||||
let existingCount = participantTracker.participantCount(for: gh)
|
let existingCount = participantTracker.participantCount(for: gh)
|
||||||
|
|||||||
@@ -740,15 +740,11 @@ extension ChatViewModel {
|
|||||||
|
|
||||||
if isViewing {
|
if isViewing {
|
||||||
// Mark read immediately if viewing
|
// 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)
|
let receipt = ReadReceipt(originalMessageID: message.id, readerID: meshService.myPeerID, readerNickname: nickname)
|
||||||
if let key = noiseKey {
|
meshService.sendReadReceipt(receipt, to: peerID)
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
sentReadReceipts.insert(message.id)
|
sentReadReceipts.insert(message.id)
|
||||||
} else {
|
} else {
|
||||||
// Notify
|
// Notify
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ extension ChatViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func handleTorWillRestart() {
|
@objc func handleTorWillRestart() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.torRestartPending = true
|
self.torRestartPending = true
|
||||||
|
|||||||
@@ -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 {
|
var body: some View {
|
||||||
@@ -192,24 +188,6 @@ struct AppInfoView: View {
|
|||||||
|
|
||||||
FeatureRow(info: Strings.Privacy.panic)
|
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()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,31 @@ struct LocationChannelsSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func levelTitle(for level: GeohashChannelLevel, count: Int) -> String {
|
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)
|
return rowTitle(label: level.displayName, count: count)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func bookmarkTitle(geohash: String, count: Int) -> String {
|
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)
|
return rowTitle(label: "#\(geohash)", count: count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,19 @@ final class PreviewKeychainManager: KeychainManagerProtocol {
|
|||||||
storage["identity_noiseStaticKey"] != nil
|
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)
|
// MARK: - Generic Data Storage (consolidated from KeychainHelper)
|
||||||
|
|
||||||
func save(key: String, data: Data, service: String, accessible: CFString?) {
|
func save(key: String, data: Data, service: String, accessible: CFString?) {
|
||||||
|
|||||||
@@ -32,29 +32,28 @@ struct FragmentationTests {
|
|||||||
)
|
)
|
||||||
let capture = CaptureDelegate()
|
let capture = CaptureDelegate()
|
||||||
ble.delegate = capture
|
ble.delegate = capture
|
||||||
|
|
||||||
// Construct a big packet (3KB) from a remote sender (not our own ID)
|
// Construct a big packet (3KB) from a remote sender (not our own ID)
|
||||||
let remoteShortID = PeerID(str: "1122334455667788")
|
let remoteShortID = PeerID(str: "1122334455667788")
|
||||||
let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 3_000)
|
let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 3_000)
|
||||||
|
|
||||||
// Use a small fragment size to ensure multiple pieces
|
// Use a small fragment size to ensure multiple pieces
|
||||||
let fragments = fragmentPacket(original, fragmentSize: 400)
|
let fragments = fragmentPacket(original, fragmentSize: 400)
|
||||||
|
|
||||||
// Shuffle fragments to simulate out-of-order arrival
|
// Shuffle fragments to simulate out-of-order arrival
|
||||||
let shuffled = fragments.shuffled()
|
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() {
|
for (i, fragment) in shuffled.enumerated() {
|
||||||
let delay = 5 * Double(i) * 0.001
|
if i > 0 {
|
||||||
Task {
|
try await Task.sleep(for: .milliseconds(5))
|
||||||
try await sleep(delay)
|
|
||||||
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
|
|
||||||
}
|
}
|
||||||
|
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow async processing
|
// Wait for delegate callback with proper timeout
|
||||||
try await sleep(0.5)
|
try await capture.waitForPublicMessages(count: 1, timeout: .seconds(2))
|
||||||
|
|
||||||
#expect(capture.publicMessages.count == 1)
|
#expect(capture.publicMessages.count == 1)
|
||||||
#expect(capture.publicMessages.first?.content.count == 3_000)
|
#expect(capture.publicMessages.first?.content.count == 3_000)
|
||||||
}
|
}
|
||||||
@@ -68,26 +67,26 @@ struct FragmentationTests {
|
|||||||
)
|
)
|
||||||
let capture = CaptureDelegate()
|
let capture = CaptureDelegate()
|
||||||
ble.delegate = capture
|
ble.delegate = capture
|
||||||
|
|
||||||
let remoteShortID = PeerID(str: "A1B2C3D4E5F60708")
|
let remoteShortID = PeerID(str: "A1B2C3D4E5F60708")
|
||||||
let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 2048)
|
let original = makeLargePublicPacket(senderShortHex: remoteShortID, size: 2048)
|
||||||
var frags = fragmentPacket(original, fragmentSize: 300)
|
var frags = fragmentPacket(original, fragmentSize: 300)
|
||||||
|
|
||||||
// Duplicate one fragment
|
// Duplicate one fragment
|
||||||
if let dup = frags.first {
|
if let dup = frags.first {
|
||||||
frags.insert(dup, at: 1)
|
frags.insert(dup, at: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send fragments sequentially with small delays (no fire-and-forget Tasks)
|
||||||
for (i, fragment) in frags.enumerated() {
|
for (i, fragment) in frags.enumerated() {
|
||||||
let delay = 5 * Double(i) * 0.001
|
if i > 0 {
|
||||||
Task {
|
try await Task.sleep(for: .milliseconds(5))
|
||||||
try await sleep(delay)
|
|
||||||
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
|
|
||||||
}
|
}
|
||||||
|
ble._test_handlePacket(fragment, fromPeerID: remoteShortID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow async processing
|
// Wait for delegate callback with proper timeout
|
||||||
try await sleep(0.5)
|
try await capture.waitForPublicMessages(count: 1, timeout: .seconds(2))
|
||||||
|
|
||||||
#expect(capture.publicMessages.count == 1)
|
#expect(capture.publicMessages.count == 1)
|
||||||
#expect(capture.publicMessages.first?.content.count == 2048)
|
#expect(capture.publicMessages.first?.content.count == 2048)
|
||||||
@@ -196,12 +195,128 @@ struct FragmentationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension FragmentationTests {
|
extension FragmentationTests {
|
||||||
private final class CaptureDelegate: BitchatDelegate {
|
/// Thread-safe delegate that supports awaiting message delivery
|
||||||
var publicMessages: [(peerID: PeerID, nickname: String, content: String)] = []
|
private final class CaptureDelegate: BitchatDelegate, @unchecked Sendable {
|
||||||
var receivedMessages: [BitchatMessage] = []
|
private let lock = NSLock()
|
||||||
func didReceiveMessage(_ message: BitchatMessage) {
|
private var _publicMessages: [(peerID: PeerID, nickname: String, content: String)] = []
|
||||||
receivedMessages.append(message)
|
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 didConnectToPeer(_ peerID: PeerID) {}
|
||||||
func didDisconnectFromPeer(_ peerID: PeerID) {}
|
func didDisconnectFromPeer(_ peerID: PeerID) {}
|
||||||
func didUpdatePeerList(_ peers: [PeerID]) {}
|
func didUpdatePeerList(_ peers: [PeerID]) {}
|
||||||
@@ -209,9 +324,6 @@ extension FragmentationTests {
|
|||||||
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
|
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
|
||||||
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {}
|
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {}
|
||||||
func didUpdateBluetoothState(_ state: CBManagerState) {}
|
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) {}
|
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
|
// MARK: - Timestamp Validation Tests
|
||||||
|
// BCH-01-011: Window reduced from ±1 hour to ±5 minutes
|
||||||
|
|
||||||
@Test func currentTimestampIsValid() throws {
|
@Test func currentTimestampIsValid() throws {
|
||||||
let now = Date()
|
let now = Date()
|
||||||
@@ -138,31 +139,48 @@ struct InputValidatorTests {
|
|||||||
#expect(result == true)
|
#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 thirtyMinutesAgo = Date().addingTimeInterval(-30 * 60)
|
||||||
let result = InputValidator.validateTimestamp(thirtyMinutesAgo)
|
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)
|
#expect(result == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func timestampTwoHoursInFutureIsInvalid() throws {
|
@Test func timestampTenMinutesAgoIsInvalid() throws {
|
||||||
let twoHoursFromNow = Date().addingTimeInterval(2 * 3600)
|
// 10 minutes is outside the 5-minute window
|
||||||
let result = InputValidator.validateTimestamp(twoHoursFromNow)
|
let tenMinutesAgo = Date().addingTimeInterval(-10 * 60)
|
||||||
|
let result = InputValidator.validateTimestamp(tenMinutesAgo)
|
||||||
#expect(result == false)
|
#expect(result == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func timestampAtOneHourBoundaryIsValid() throws {
|
@Test func timestampTenMinutesInFutureIsInvalid() throws {
|
||||||
// Just slightly within the one-hour window
|
// 10 minutes in future is outside the 5-minute window
|
||||||
let almostOneHourAgo = Date().addingTimeInterval(-3599)
|
let tenMinutesFromNow = Date().addingTimeInterval(10 * 60)
|
||||||
let result = InputValidator.validateTimestamp(almostOneHourAgo)
|
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)
|
#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
|
// MARK: - Edge Cases
|
||||||
|
|
||||||
@Test func singleCharacterStringIsAccepted() throws {
|
@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 storage: [String: Data] = [:]
|
||||||
private var serviceStorage: [String: [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 {
|
func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool {
|
||||||
storage[key] = keyData
|
storage[key] = keyData
|
||||||
return true
|
return true
|
||||||
@@ -45,6 +49,25 @@ final class MockKeychain: KeychainManagerProtocol {
|
|||||||
storage["identity_noiseStaticKey"] != nil
|
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)
|
// MARK: - Generic Data Storage (consolidated from KeychainHelper)
|
||||||
|
|
||||||
func save(key: String, data: Data, service: String, accessible: CFString?) {
|
func save(key: String, data: Data, service: String, accessible: CFString?) {
|
||||||
@@ -76,6 +99,10 @@ final class TrackingMockKeychain: KeychainManagerProtocol {
|
|||||||
private var _secureClearDataCallCount = 0
|
private var _secureClearDataCallCount = 0
|
||||||
private var _secureClearStringCallCount = 0
|
private var _secureClearStringCallCount = 0
|
||||||
|
|
||||||
|
// BCH-01-009: Configurable error simulation for testing
|
||||||
|
var simulatedReadError: KeychainReadResult?
|
||||||
|
var simulatedSaveError: KeychainSaveResult?
|
||||||
|
|
||||||
var secureClearDataCallCount: Int {
|
var secureClearDataCallCount: Int {
|
||||||
lock.lock()
|
lock.lock()
|
||||||
defer { lock.unlock() }
|
defer { lock.unlock() }
|
||||||
@@ -137,6 +164,25 @@ final class TrackingMockKeychain: KeychainManagerProtocol {
|
|||||||
storage["identity_noiseStaticKey"] != nil
|
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?) {
|
func save(key: String, data: Data, service: String, accessible: CFString?) {
|
||||||
if serviceStorage[service] == nil {
|
if serviceStorage[service] == nil {
|
||||||
serviceStorage[service] = [:]
|
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)
|
#expect(decodedPacket.signature == TestConstants.testSignature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Source-Based Routing Tests (v2 only)
|
||||||
|
|
||||||
@Test func packetWithRouteRoundTrip() throws {
|
@Test func packetWithRouteRoundTrip() throws {
|
||||||
let route: [Data] = [
|
let route: [Data] = [
|
||||||
try #require(Data(hexString: "0102030405060708")),
|
try #require(Data(hexString: "0102030405060708")),
|
||||||
@@ -62,6 +64,7 @@ struct BinaryProtocolTests {
|
|||||||
try #require(Data(hexString: "2122232425262728"))
|
try #require(Data(hexString: "2122232425262728"))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Route is only supported for v2+ packets
|
||||||
var packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: 0x01,
|
type: 0x01,
|
||||||
senderID: route[0],
|
senderID: route[0],
|
||||||
@@ -69,7 +72,8 @@ struct BinaryProtocolTests {
|
|||||||
timestamp: 1_720_000_000_000,
|
timestamp: 1_720_000_000_000,
|
||||||
payload: Data("route-test".utf8),
|
payload: Data("route-test".utf8),
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 6
|
ttl: 6,
|
||||||
|
version: 2
|
||||||
)
|
)
|
||||||
packet.route = route
|
packet.route = route
|
||||||
|
|
||||||
@@ -78,6 +82,7 @@ struct BinaryProtocolTests {
|
|||||||
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) != 0)
|
#expect((flagsByte & BinaryProtocol.Flags.hasRoute) != 0)
|
||||||
|
|
||||||
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with route")
|
let decoded = try #require(BinaryProtocol.decode(encoded), "Failed to decode packet with route")
|
||||||
|
#expect(decoded.version == 2)
|
||||||
let decodedRoute = try #require(decoded.route)
|
let decodedRoute = try #require(decoded.route)
|
||||||
#expect(decodedRoute.count == route.count)
|
#expect(decodedRoute.count == route.count)
|
||||||
for (expected, actual) in zip(route, decodedRoute) {
|
for (expected, actual) in zip(route, decodedRoute) {
|
||||||
@@ -90,6 +95,7 @@ struct BinaryProtocolTests {
|
|||||||
let destination = try #require(Data(hexString: "8899aabbccddeeff"))
|
let destination = try #require(Data(hexString: "8899aabbccddeeff"))
|
||||||
let shortHop = Data([0xAA, 0xBB, 0xCC])
|
let shortHop = Data([0xAA, 0xBB, 0xCC])
|
||||||
|
|
||||||
|
// Route is only supported for v2+ packets
|
||||||
var packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: 0x02,
|
type: 0x02,
|
||||||
senderID: sender,
|
senderID: sender,
|
||||||
@@ -97,7 +103,8 @@ struct BinaryProtocolTests {
|
|||||||
timestamp: 1_730_000_000_000,
|
timestamp: 1_730_000_000_000,
|
||||||
payload: Data("pad-test".utf8),
|
payload: Data("pad-test".utf8),
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 5
|
ttl: 5,
|
||||||
|
version: 2
|
||||||
)
|
)
|
||||||
packet.route = [shortHop, destination]
|
packet.route = [shortHop, destination]
|
||||||
|
|
||||||
@@ -117,6 +124,7 @@ struct BinaryProtocolTests {
|
|||||||
try #require(Data(hexString: "0202020202020202"))
|
try #require(Data(hexString: "0202020202020202"))
|
||||||
]
|
]
|
||||||
let repeatedString = String(repeating: "compress-me", count: 150)
|
let repeatedString = String(repeating: "compress-me", count: 150)
|
||||||
|
// Route is only supported for v2+ packets
|
||||||
var packet = BitchatPacket(
|
var packet = BitchatPacket(
|
||||||
type: 0x03,
|
type: 0x03,
|
||||||
senderID: route[0],
|
senderID: route[0],
|
||||||
@@ -124,7 +132,8 @@ struct BinaryProtocolTests {
|
|||||||
timestamp: 1_740_000_000_000,
|
timestamp: 1_740_000_000_000,
|
||||||
payload: Data(repeatedString.utf8),
|
payload: Data(repeatedString.utf8),
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 7
|
ttl: 7,
|
||||||
|
version: 2
|
||||||
)
|
)
|
||||||
packet.route = route
|
packet.route = route
|
||||||
|
|
||||||
@@ -135,6 +144,146 @@ struct BinaryProtocolTests {
|
|||||||
#expect(decodedRoute == route)
|
#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
|
// MARK: - Compression Tests
|
||||||
|
|
||||||
@Test("Create a large, compressible payload above current threshold (2048B)")
|
@Test("Create a large, compressible payload above current threshold (2048B)")
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ struct MeshTopologyTrackerTests {
|
|||||||
let a = try hex("0102030405060708")
|
let a = try hex("0102030405060708")
|
||||||
let b = try hex("1112131415161718")
|
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))
|
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 {
|
@Test func multiHopRouteComputation() throws {
|
||||||
@@ -32,41 +36,38 @@ struct MeshTopologyTrackerTests {
|
|||||||
let c = try hex("2021222324252627")
|
let c = try hex("2021222324252627")
|
||||||
let d = try hex("3031323334353637")
|
let d = try hex("3031323334353637")
|
||||||
|
|
||||||
tracker.recordDirectLink(between: a, and: b)
|
// Bidirectional announcements for A-B, B-C, C-D
|
||||||
tracker.recordDirectLink(between: b, and: c)
|
tracker.updateNeighbors(for: a, neighbors: [b])
|
||||||
tracker.recordDirectLink(between: c, and: d)
|
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))
|
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 {
|
@Test func unconfirmedEdgeDoesNotRoute() 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 {
|
|
||||||
let tracker = MeshTopologyTracker()
|
let tracker = MeshTopologyTracker()
|
||||||
let a = try hex("0101010101010101")
|
let a = try hex("0101010101010101")
|
||||||
let b = try hex("0202020202020202")
|
let b = try hex("0202020202020202")
|
||||||
let c = try hex("0303030303030303")
|
let c = try hex("0303030303030303")
|
||||||
|
|
||||||
tracker.recordDirectLink(between: a, and: b)
|
// A announces B (confirmed)
|
||||||
tracker.recordDirectLink(between: b, and: c)
|
// B announces A, C (confirmed A-B, unconfirmed B-C)
|
||||||
let initialRoute = try #require(tracker.computeRoute(from: a, to: c))
|
// C does NOT announce B
|
||||||
#expect(initialRoute == [a, b, c])
|
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)
|
#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 {
|
@Test func removingPeerClearsEdges() throws {
|
||||||
@@ -75,12 +76,28 @@ struct MeshTopologyTrackerTests {
|
|||||||
let b = try hex("0A0B0C0D0E0F0001")
|
let b = try hex("0A0B0C0D0E0F0001")
|
||||||
let c = try hex("0011223344556677")
|
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))
|
let initialRoute = try #require(tracker.computeRoute(from: a, to: c))
|
||||||
#expect(initialRoute == [a, b, c])
|
#expect(initialRoute == [b])
|
||||||
|
|
||||||
tracker.removePeer(b)
|
tracker.removePeer(b)
|
||||||
#expect(tracker.computeRoute(from: a, to: c) == nil)
|
#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)
|
||||||
#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>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>BinaryPath</key>
|
<key>BinaryPath</key>
|
||||||
<string>tor-nolzma.framework/tor-nolzma</string>
|
<string>libarti_bitchat.a</string>
|
||||||
|
<key>HeadersPath</key>
|
||||||
|
<string>Headers</string>
|
||||||
<key>LibraryIdentifier</key>
|
<key>LibraryIdentifier</key>
|
||||||
<string>macos-arm64</string>
|
<string>macos-arm64</string>
|
||||||
<key>LibraryPath</key>
|
<key>LibraryPath</key>
|
||||||
<string>tor-nolzma.framework</string>
|
<string>libarti_bitchat.a</string>
|
||||||
<key>SupportedArchitectures</key>
|
<key>SupportedArchitectures</key>
|
||||||
<array>
|
<array>
|
||||||
<string>arm64</string>
|
<string>arm64</string>
|
||||||
@@ -20,11 +22,29 @@
|
|||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>BinaryPath</key>
|
<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>
|
<key>LibraryIdentifier</key>
|
||||||
<string>ios-arm64-simulator</string>
|
<string>ios-arm64-simulator</string>
|
||||||
<key>LibraryPath</key>
|
<key>LibraryPath</key>
|
||||||
<string>tor-nolzma.framework</string>
|
<string>libarti_bitchat.a</string>
|
||||||
<key>SupportedArchitectures</key>
|
<key>SupportedArchitectures</key>
|
||||||
<array>
|
<array>
|
||||||
<string>arm64</string>
|
<string>arm64</string>
|
||||||
@@ -34,20 +54,6 @@
|
|||||||
<key>SupportedPlatformVariant</key>
|
<key>SupportedPlatformVariant</key>
|
||||||
<string>simulator</string>
|
<string>simulator</string>
|
||||||
</dict>
|
</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>
|
</array>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XFWK</string>
|
<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) */
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user