@decentnetwork/lan 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +31 -0
- package/README.md +296 -0
- package/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/acl/acl-engine.d.ts +43 -0
- package/dist/acl/acl-engine.js +189 -0
- package/dist/acl/audit.d.ts +70 -0
- package/dist/acl/audit.js +144 -0
- package/dist/acl/index.d.ts +4 -0
- package/dist/acl/index.js +3 -0
- package/dist/acl/policy.d.ts +31 -0
- package/dist/acl/policy.js +102 -0
- package/dist/acl/types.d.ts +18 -0
- package/dist/acl/types.js +4 -0
- package/dist/carrier/frame.d.ts +18 -0
- package/dist/carrier/frame.js +66 -0
- package/dist/carrier/index.d.ts +5 -0
- package/dist/carrier/index.js +4 -0
- package/dist/carrier/packet-session.d.ts +32 -0
- package/dist/carrier/packet-session.js +151 -0
- package/dist/carrier/peer-manager.d.ts +113 -0
- package/dist/carrier/peer-manager.js +392 -0
- package/dist/carrier/types.d.ts +10 -0
- package/dist/carrier/types.js +11 -0
- package/dist/cli/commands.d.ts +223 -0
- package/dist/cli/commands.js +932 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +196 -0
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.js +152 -0
- package/dist/daemon/index.d.ts +1 -0
- package/dist/daemon/index.js +1 -0
- package/dist/daemon/ipc.d.ts +60 -0
- package/dist/daemon/ipc.js +144 -0
- package/dist/daemon/server.d.ts +63 -0
- package/dist/daemon/server.js +510 -0
- package/dist/dns/index.d.ts +1 -0
- package/dist/dns/index.js +1 -0
- package/dist/dns/resolver.d.ts +44 -0
- package/dist/dns/resolver.js +82 -0
- package/dist/dns/server.d.ts +70 -0
- package/dist/dns/server.js +393 -0
- package/dist/dora/dora-integration.d.ts +90 -0
- package/dist/dora/dora-integration.js +325 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +15 -0
- package/dist/ipam/index.d.ts +1 -0
- package/dist/ipam/index.js +1 -0
- package/dist/ipam/ipam.d.ts +99 -0
- package/dist/ipam/ipam.js +254 -0
- package/dist/proxy/connect-proxy.d.ts +78 -0
- package/dist/proxy/connect-proxy.js +204 -0
- package/dist/router/index.d.ts +5 -0
- package/dist/router/index.js +4 -0
- package/dist/router/ip-parser.d.ts +36 -0
- package/dist/router/ip-parser.js +127 -0
- package/dist/router/packet-router.d.ts +49 -0
- package/dist/router/packet-router.js +251 -0
- package/dist/router/session-manager.d.ts +50 -0
- package/dist/router/session-manager.js +138 -0
- package/dist/router/types.d.ts +21 -0
- package/dist/router/types.js +6 -0
- package/dist/tun/index.d.ts +3 -0
- package/dist/tun/index.js +2 -0
- package/dist/tun/route-manager.d.ts +59 -0
- package/dist/tun/route-manager.js +353 -0
- package/dist/tun/tun-device.d.ts +45 -0
- package/dist/tun/tun-device.js +265 -0
- package/dist/tun/types.d.ts +28 -0
- package/dist/tun/types.js +4 -0
- package/dist/types.d.ts +176 -0
- package/dist/types.js +4 -0
- package/dist/utils/logger.d.ts +20 -0
- package/dist/utils/logger.js +43 -0
- package/docs/CONFIGURATION.md +197 -0
- package/docs/INSTALL.md +145 -0
- package/package.json +93 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages Carrier peer identity and connections
|
|
3
|
+
* Wraps @decentnetwork/peer SDK
|
|
4
|
+
*/
|
|
5
|
+
import { EventEmitter } from "events";
|
|
6
|
+
import { PacketSession } from "./packet-session.js";
|
|
7
|
+
import type { PeerIdentity, RemotePeer } from "./types.js";
|
|
8
|
+
import type { BootstrapNode } from "../types.js";
|
|
9
|
+
export interface PeerManagerOptions {
|
|
10
|
+
keyFile: string;
|
|
11
|
+
bootstrapNodes: BootstrapNode[];
|
|
12
|
+
expressNodes?: BootstrapNode[];
|
|
13
|
+
}
|
|
14
|
+
export declare class PeerManager extends EventEmitter {
|
|
15
|
+
private peer;
|
|
16
|
+
private identity;
|
|
17
|
+
private sessions;
|
|
18
|
+
private sessionCounter;
|
|
19
|
+
private logger;
|
|
20
|
+
constructor();
|
|
21
|
+
create(opts: PeerManagerOptions): Promise<void>;
|
|
22
|
+
start(): Promise<void>;
|
|
23
|
+
joinNetwork(): Promise<void>;
|
|
24
|
+
announceSelf(timeoutMs?: number): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Accept a pending friend request while the daemon is running.
|
|
27
|
+
* Replaces the daemon-down `friend-accept --wait` ceremony — the
|
|
28
|
+
* running daemon can just call this in response to an incoming
|
|
29
|
+
* onFriendRequest event. Idempotent: re-accepting an already-accepted
|
|
30
|
+
* pubkey is a no-op at the SDK level.
|
|
31
|
+
*/
|
|
32
|
+
acceptFriendRequest(pubkey: string): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Send an outbound friend request to a Carrier address (NOT a bare
|
|
35
|
+
* userid — sendFriendRequest needs the address form because it
|
|
36
|
+
* includes the recipient's nospam token). Used by DoraIntegration
|
|
37
|
+
* to auto-friend every peer in the roster.
|
|
38
|
+
*
|
|
39
|
+
* The SDK is idempotent for already-accepted peers (it short-
|
|
40
|
+
* circuits via the `acceptedAt` cache), so callers can spam this
|
|
41
|
+
* on every roster refresh without re-prompting iOS Beagle users.
|
|
42
|
+
*/
|
|
43
|
+
sendFriendRequest(address: string, hello?: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Returns true if the given userid is already an accepted friend of
|
|
46
|
+
* this peer. Lets DoraIntegration skip sendFriendRequest for peers
|
|
47
|
+
* we've already established a session with.
|
|
48
|
+
*/
|
|
49
|
+
isFriend(userid: string): boolean;
|
|
50
|
+
stop(): Promise<void>;
|
|
51
|
+
getIdentity(): PeerIdentity;
|
|
52
|
+
/**
|
|
53
|
+
* Returns the userid (base58) — this is the identifier used in the
|
|
54
|
+
* friend store, in onText callbacks, and as the argument to sendText().
|
|
55
|
+
* Despite the historical name "pubkey", this is NOT the hex public key.
|
|
56
|
+
*/
|
|
57
|
+
getPubkey(): string;
|
|
58
|
+
/**
|
|
59
|
+
* Returns the hex-encoded raw public key (32 bytes hex).
|
|
60
|
+
* Use this for cryptographic operations, not for friend lookups.
|
|
61
|
+
*/
|
|
62
|
+
getHexPubkey(): string;
|
|
63
|
+
getAddress(): string;
|
|
64
|
+
getFriends(): RemotePeer[];
|
|
65
|
+
/**
|
|
66
|
+
* Check if a specific friend is currently online (direct UDP path established).
|
|
67
|
+
* Returns false for unknown pubkeys.
|
|
68
|
+
*/
|
|
69
|
+
isFriendOnline(pubkey: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Actively wait for a friend to come online. The SDK uses this signal to
|
|
72
|
+
* accelerate route discovery + UDP holepunching for that friend.
|
|
73
|
+
* Returns true if the friend went online within the timeout.
|
|
74
|
+
*/
|
|
75
|
+
waitForFriendConnected(pubkey: string, timeoutMs?: number): Promise<boolean>;
|
|
76
|
+
/**
|
|
77
|
+
* Trigger the SDK's session-establishment path for a peer by issuing a
|
|
78
|
+
* dummy sendText. peer.sendText() internally calls #initiateSession when
|
|
79
|
+
* no session exists. We swallow the resulting "friend offline" error.
|
|
80
|
+
*
|
|
81
|
+
* This is a workaround for the SDK quirk where one side's friendOnline
|
|
82
|
+
* notification never fires, leaving the friend perpetually "offline" even
|
|
83
|
+
* when both sides are connected to the same TCP relays.
|
|
84
|
+
*/
|
|
85
|
+
kickSessionEstablishment(pubkey: string): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Send a raw text payload to a friend. Used by application-layer
|
|
88
|
+
* protocols (e.g. dora) that ride on the same Carrier text channel as
|
|
89
|
+
* our base64-encoded packet frames but carry a different wire prefix
|
|
90
|
+
* (DORA:). Packet-router code never calls this directly — it goes
|
|
91
|
+
* through PacketSession.
|
|
92
|
+
*/
|
|
93
|
+
sendText(pubkey: string, text: string): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Open an outbound packet session (initiator).
|
|
96
|
+
* Sends HANDSHAKE_REQ and waits for HANDSHAKE_ACK.
|
|
97
|
+
*/
|
|
98
|
+
openPacketSession(pubkey: string): Promise<PacketSession>;
|
|
99
|
+
closePacketSession(pubkey: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* Get an existing session for a peer (for routing logic).
|
|
102
|
+
*/
|
|
103
|
+
getSession(pubkey: string): PacketSession | null;
|
|
104
|
+
/**
|
|
105
|
+
* Internal: Create and register a session.
|
|
106
|
+
*/
|
|
107
|
+
private createSession;
|
|
108
|
+
/**
|
|
109
|
+
* Send a HANDSHAKE_ACK frame to a peer.
|
|
110
|
+
*/
|
|
111
|
+
private sendHandshakeAck;
|
|
112
|
+
private setupEventHandlers;
|
|
113
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages Carrier peer identity and connections
|
|
3
|
+
* Wraps @decentnetwork/peer SDK
|
|
4
|
+
*/
|
|
5
|
+
import { Peer } from "@decentnetwork/peer";
|
|
6
|
+
import { EventEmitter } from "events";
|
|
7
|
+
import { PacketSession } from "./packet-session.js";
|
|
8
|
+
import { FrameCodec } from "./frame.js";
|
|
9
|
+
import { FRAME_OPCODE_HANDSHAKE_ACK, } from "./types.js";
|
|
10
|
+
import { Logger } from "../utils/logger.js";
|
|
11
|
+
// Dora wire-prefix. Kept inline (rather than imported from @decentnetwork/dora) to
|
|
12
|
+
// avoid coupling peer-manager — a transport-layer module — to an
|
|
13
|
+
// application-protocol package. The prefix is part of decentlan's text
|
|
14
|
+
// channel contract and must match the constant defined in dora's types.ts.
|
|
15
|
+
const DORA_PREFIX = "DORA:";
|
|
16
|
+
export class PeerManager extends EventEmitter {
|
|
17
|
+
peer = null;
|
|
18
|
+
identity = null;
|
|
19
|
+
sessions = new Map();
|
|
20
|
+
sessionCounter = 1;
|
|
21
|
+
logger;
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
this.logger = new Logger({ prefix: "PeerManager" });
|
|
25
|
+
}
|
|
26
|
+
async create(opts) {
|
|
27
|
+
this.logger.info("Creating Peer instance");
|
|
28
|
+
this.peer = await Peer.create({
|
|
29
|
+
keyFile: opts.keyFile,
|
|
30
|
+
compatibilityMode: "legacy",
|
|
31
|
+
bootstrapNodes: opts.bootstrapNodes,
|
|
32
|
+
expressNodes: opts.expressNodes,
|
|
33
|
+
});
|
|
34
|
+
this.setupEventHandlers();
|
|
35
|
+
this.logger.info("Peer instance created (not yet started)");
|
|
36
|
+
}
|
|
37
|
+
async start() {
|
|
38
|
+
if (!this.peer) {
|
|
39
|
+
throw new Error("Peer not created. Call create() first.");
|
|
40
|
+
}
|
|
41
|
+
this.logger.info("Starting Peer");
|
|
42
|
+
await this.peer.start();
|
|
43
|
+
// Identity is only available after start()
|
|
44
|
+
this.identity = {
|
|
45
|
+
pubkey: this.peer.pubkey(),
|
|
46
|
+
address: this.peer.address(),
|
|
47
|
+
userid: this.peer.userid(),
|
|
48
|
+
};
|
|
49
|
+
this.logger.info(`Peer started: ${this.identity.address}`);
|
|
50
|
+
}
|
|
51
|
+
async joinNetwork() {
|
|
52
|
+
if (!this.peer) {
|
|
53
|
+
throw new Error("Peer not created. Call create() first.");
|
|
54
|
+
}
|
|
55
|
+
this.logger.info("Joining Carrier network");
|
|
56
|
+
await this.peer.joinNetwork();
|
|
57
|
+
this.logger.info("Joined Carrier network");
|
|
58
|
+
}
|
|
59
|
+
async announceSelf(timeoutMs) {
|
|
60
|
+
if (!this.peer) {
|
|
61
|
+
throw new Error("Peer not created. Call create() first.");
|
|
62
|
+
}
|
|
63
|
+
this.logger.info("Announcing self on DHT");
|
|
64
|
+
await this.peer.announceSelf(timeoutMs);
|
|
65
|
+
this.logger.info("Announced self on DHT");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Accept a pending friend request while the daemon is running.
|
|
69
|
+
* Replaces the daemon-down `friend-accept --wait` ceremony — the
|
|
70
|
+
* running daemon can just call this in response to an incoming
|
|
71
|
+
* onFriendRequest event. Idempotent: re-accepting an already-accepted
|
|
72
|
+
* pubkey is a no-op at the SDK level.
|
|
73
|
+
*/
|
|
74
|
+
async acceptFriendRequest(pubkey) {
|
|
75
|
+
if (!this.peer) {
|
|
76
|
+
throw new Error("Peer not created. Call create() first.");
|
|
77
|
+
}
|
|
78
|
+
await this.peer.acceptFriendRequest(pubkey);
|
|
79
|
+
this.logger.info(`Accepted friend request from ${pubkey}`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Send an outbound friend request to a Carrier address (NOT a bare
|
|
83
|
+
* userid — sendFriendRequest needs the address form because it
|
|
84
|
+
* includes the recipient's nospam token). Used by DoraIntegration
|
|
85
|
+
* to auto-friend every peer in the roster.
|
|
86
|
+
*
|
|
87
|
+
* The SDK is idempotent for already-accepted peers (it short-
|
|
88
|
+
* circuits via the `acceptedAt` cache), so callers can spam this
|
|
89
|
+
* on every roster refresh without re-prompting iOS Beagle users.
|
|
90
|
+
*/
|
|
91
|
+
async sendFriendRequest(address, hello) {
|
|
92
|
+
if (!this.peer) {
|
|
93
|
+
throw new Error("Peer not created. Call create() first.");
|
|
94
|
+
}
|
|
95
|
+
await this.peer.sendFriendRequest(address, hello);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Returns true if the given userid is already an accepted friend of
|
|
99
|
+
* this peer. Lets DoraIntegration skip sendFriendRequest for peers
|
|
100
|
+
* we've already established a session with.
|
|
101
|
+
*/
|
|
102
|
+
isFriend(userid) {
|
|
103
|
+
if (!this.peer)
|
|
104
|
+
return false;
|
|
105
|
+
// FriendRecord stores both pubkey (hex) and userid (base58). The
|
|
106
|
+
// dora roster gives us userid; match on either form to be safe
|
|
107
|
+
// against older friends.json entries that only set `pubkey`.
|
|
108
|
+
return this.peer
|
|
109
|
+
.friends()
|
|
110
|
+
.some((f) => f.userid === userid || f.pubkey === userid);
|
|
111
|
+
}
|
|
112
|
+
async stop() {
|
|
113
|
+
if (!this.peer) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.logger.info("Stopping Peer");
|
|
117
|
+
// Close all sessions
|
|
118
|
+
for (const session of this.sessions.values()) {
|
|
119
|
+
session.close();
|
|
120
|
+
}
|
|
121
|
+
this.sessions.clear();
|
|
122
|
+
await this.peer.stop();
|
|
123
|
+
this.logger.info("Peer stopped");
|
|
124
|
+
}
|
|
125
|
+
getIdentity() {
|
|
126
|
+
if (!this.identity) {
|
|
127
|
+
throw new Error("Peer not created. Call create() first.");
|
|
128
|
+
}
|
|
129
|
+
return this.identity;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Returns the userid (base58) — this is the identifier used in the
|
|
133
|
+
* friend store, in onText callbacks, and as the argument to sendText().
|
|
134
|
+
* Despite the historical name "pubkey", this is NOT the hex public key.
|
|
135
|
+
*/
|
|
136
|
+
getPubkey() {
|
|
137
|
+
if (!this.peer) {
|
|
138
|
+
throw new Error("Peer not created. Call create() first.");
|
|
139
|
+
}
|
|
140
|
+
return this.peer.userid();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Returns the hex-encoded raw public key (32 bytes hex).
|
|
144
|
+
* Use this for cryptographic operations, not for friend lookups.
|
|
145
|
+
*/
|
|
146
|
+
getHexPubkey() {
|
|
147
|
+
if (!this.peer) {
|
|
148
|
+
throw new Error("Peer not created. Call create() first.");
|
|
149
|
+
}
|
|
150
|
+
return this.peer.pubkey();
|
|
151
|
+
}
|
|
152
|
+
getAddress() {
|
|
153
|
+
if (!this.peer) {
|
|
154
|
+
throw new Error("Peer not created. Call create() first.");
|
|
155
|
+
}
|
|
156
|
+
return this.peer.address();
|
|
157
|
+
}
|
|
158
|
+
getFriends() {
|
|
159
|
+
if (!this.peer) {
|
|
160
|
+
throw new Error("Peer not created. Call create() first.");
|
|
161
|
+
}
|
|
162
|
+
return this.peer.friends().map((friend) => ({
|
|
163
|
+
pubkey: friend.pubkey,
|
|
164
|
+
name: friend.name,
|
|
165
|
+
status: friend.status === "online" ? "online" : "offline",
|
|
166
|
+
lastSeen: friend.acceptedAt,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check if a specific friend is currently online (direct UDP path established).
|
|
171
|
+
* Returns false for unknown pubkeys.
|
|
172
|
+
*/
|
|
173
|
+
isFriendOnline(pubkey) {
|
|
174
|
+
if (!this.peer)
|
|
175
|
+
return false;
|
|
176
|
+
const friend = this.peer.friends().find((f) => f.pubkey === pubkey);
|
|
177
|
+
return friend?.status === "online";
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Actively wait for a friend to come online. The SDK uses this signal to
|
|
181
|
+
* accelerate route discovery + UDP holepunching for that friend.
|
|
182
|
+
* Returns true if the friend went online within the timeout.
|
|
183
|
+
*/
|
|
184
|
+
async waitForFriendConnected(pubkey, timeoutMs = 60000) {
|
|
185
|
+
if (!this.peer)
|
|
186
|
+
return false;
|
|
187
|
+
return this.peer.waitForFriendConnected(pubkey, timeoutMs);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Trigger the SDK's session-establishment path for a peer by issuing a
|
|
191
|
+
* dummy sendText. peer.sendText() internally calls #initiateSession when
|
|
192
|
+
* no session exists. We swallow the resulting "friend offline" error.
|
|
193
|
+
*
|
|
194
|
+
* This is a workaround for the SDK quirk where one side's friendOnline
|
|
195
|
+
* notification never fires, leaving the friend perpetually "offline" even
|
|
196
|
+
* when both sides are connected to the same TCP relays.
|
|
197
|
+
*/
|
|
198
|
+
async kickSessionEstablishment(pubkey) {
|
|
199
|
+
if (!this.peer)
|
|
200
|
+
return;
|
|
201
|
+
try {
|
|
202
|
+
await this.peer.sendText(pubkey, "");
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// expected when friend isn't online yet — the call still triggered
|
|
206
|
+
// #initiateSession internally, which is the side effect we want.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Send a raw text payload to a friend. Used by application-layer
|
|
211
|
+
* protocols (e.g. dora) that ride on the same Carrier text channel as
|
|
212
|
+
* our base64-encoded packet frames but carry a different wire prefix
|
|
213
|
+
* (DORA:). Packet-router code never calls this directly — it goes
|
|
214
|
+
* through PacketSession.
|
|
215
|
+
*/
|
|
216
|
+
async sendText(pubkey, text) {
|
|
217
|
+
if (!this.peer) {
|
|
218
|
+
throw new Error("Peer not created. Call create() first.");
|
|
219
|
+
}
|
|
220
|
+
await this.peer.sendText(pubkey, text);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Open an outbound packet session (initiator).
|
|
224
|
+
* Sends HANDSHAKE_REQ and waits for HANDSHAKE_ACK.
|
|
225
|
+
*/
|
|
226
|
+
async openPacketSession(pubkey) {
|
|
227
|
+
const existing = this.sessions.get(pubkey);
|
|
228
|
+
if (existing && existing.isConnected()) {
|
|
229
|
+
return existing;
|
|
230
|
+
}
|
|
231
|
+
const sessionId = this.sessionCounter++;
|
|
232
|
+
const session = this.createSession(pubkey, sessionId);
|
|
233
|
+
try {
|
|
234
|
+
await session.handshake();
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
this.sessions.delete(pubkey);
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
// Emit session-opened so PacketRouter can attach its "packet" listener.
|
|
241
|
+
// The responder side already emits this when handling handshake-req;
|
|
242
|
+
// the initiator side needs to emit it after the handshake completes.
|
|
243
|
+
this.emit("session-opened", { pubkey, sessionId });
|
|
244
|
+
return session;
|
|
245
|
+
}
|
|
246
|
+
closePacketSession(pubkey) {
|
|
247
|
+
const session = this.sessions.get(pubkey);
|
|
248
|
+
if (session) {
|
|
249
|
+
session.close();
|
|
250
|
+
this.sessions.delete(pubkey);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Get an existing session for a peer (for routing logic).
|
|
255
|
+
*/
|
|
256
|
+
getSession(pubkey) {
|
|
257
|
+
return this.sessions.get(pubkey) || null;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Internal: Create and register a session.
|
|
261
|
+
*/
|
|
262
|
+
createSession(pubkey, sessionId) {
|
|
263
|
+
const session = new PacketSession({
|
|
264
|
+
peerId: pubkey,
|
|
265
|
+
sessionId,
|
|
266
|
+
sendText: async (text) => {
|
|
267
|
+
if (!this.peer) {
|
|
268
|
+
throw new Error("Peer not available");
|
|
269
|
+
}
|
|
270
|
+
await this.peer.sendText(pubkey, text);
|
|
271
|
+
},
|
|
272
|
+
onClose: () => {
|
|
273
|
+
this.sessions.delete(pubkey);
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
// Listen for handshake requests on this session — auto-respond with ACK
|
|
277
|
+
session.on("handshake-req", async () => {
|
|
278
|
+
this.logger.info(`Received HANDSHAKE_REQ from ${pubkey}, sending ACK`);
|
|
279
|
+
try {
|
|
280
|
+
await this.sendHandshakeAck(pubkey, sessionId);
|
|
281
|
+
session.acceptIncoming(); // Mark session as active (responder side)
|
|
282
|
+
this.emit("session-opened", { pubkey, sessionId });
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
this.logger.error(`Failed to send HANDSHAKE_ACK to ${pubkey}:`, error);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
this.sessions.set(pubkey, session);
|
|
289
|
+
return session;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Send a HANDSHAKE_ACK frame to a peer.
|
|
293
|
+
*/
|
|
294
|
+
async sendHandshakeAck(pubkey, sessionId) {
|
|
295
|
+
if (!this.peer) {
|
|
296
|
+
throw new Error("Peer not available");
|
|
297
|
+
}
|
|
298
|
+
const frame = FrameCodec.encode(new Uint8Array(0), sessionId, FRAME_OPCODE_HANDSHAKE_ACK);
|
|
299
|
+
const frameBase64 = Buffer.from(frame).toString("base64");
|
|
300
|
+
await this.peer.sendText(pubkey, frameBase64);
|
|
301
|
+
}
|
|
302
|
+
setupEventHandlers() {
|
|
303
|
+
if (!this.peer) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Friend connection events
|
|
307
|
+
this.peer.onFriendConnection((event) => {
|
|
308
|
+
const evt = {
|
|
309
|
+
pubkey: event.pubkey,
|
|
310
|
+
status: event.status === "connected" ? "connected" : "disconnected",
|
|
311
|
+
timestamp: Date.now(),
|
|
312
|
+
};
|
|
313
|
+
this.emit("friend-connection", evt);
|
|
314
|
+
this.logger.info(`Friend ${event.pubkey} ${event.status}`);
|
|
315
|
+
// If a friend goes offline (or, more importantly, comes BACK
|
|
316
|
+
// online after we already had a session — e.g. they restarted
|
|
317
|
+
// their daemon), the Carrier crypto session under us has
|
|
318
|
+
// rotated. Our cached PacketSession is bound to the previous
|
|
319
|
+
// session's frame-sequence state. Sending DATA frames through
|
|
320
|
+
// it produces ciphertext the new peer can't decrypt; the
|
|
321
|
+
// packets land in their PeerManager.onText but no session
|
|
322
|
+
// matches and they're dropped silently. Result: 100% loss
|
|
323
|
+
// that looks like "the link is up but pings time out."
|
|
324
|
+
//
|
|
325
|
+
// Fix: drop our PacketSession on every friend-status change.
|
|
326
|
+
// The next outbound packet triggers a fresh handshake (or the
|
|
327
|
+
// remote auto-creates from our HANDSHAKE_REQ); fast, clean,
|
|
328
|
+
// no stale-session lingering.
|
|
329
|
+
const existing = this.sessions.get(event.pubkey);
|
|
330
|
+
if (existing) {
|
|
331
|
+
this.logger.debug(`Closing stale packet session to ${event.pubkey} on friend-${event.status}`);
|
|
332
|
+
existing.close();
|
|
333
|
+
this.sessions.delete(event.pubkey);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
// Text messages contain our framed packets
|
|
337
|
+
this.peer.onText((message) => {
|
|
338
|
+
try {
|
|
339
|
+
// Silently skip empty messages — kickSessionEstablishment sends
|
|
340
|
+
// an empty sendText to trigger the SDK's #initiateSession path,
|
|
341
|
+
// and the receiver doesn't need to log a warning for that.
|
|
342
|
+
if (message.text.length === 0) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Dora messages share the same text channel as packet frames but
|
|
346
|
+
// carry a `DORA:` ASCII prefix that's not valid base64. Route them
|
|
347
|
+
// to whoever is subscribed (DoraClient/DoraServer in daemon).
|
|
348
|
+
if (message.text.startsWith(DORA_PREFIX)) {
|
|
349
|
+
this.emit("dora-message", message.pubkey, message.text);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const frameData = Buffer.from(message.text, "base64");
|
|
353
|
+
const frame = FrameCodec.decode(new Uint8Array(frameData));
|
|
354
|
+
if (!frame) {
|
|
355
|
+
this.logger.warn(`Invalid frame from ${message.pubkey}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
let session = this.sessions.get(message.pubkey);
|
|
359
|
+
// For HANDSHAKE_REQ: always create or REPLACE the session with the
|
|
360
|
+
// new sessionId. Without this, after a Carrier session drop + re-pair
|
|
361
|
+
// the peer's new HANDSHAKE_REQ would be dropped because the existing
|
|
362
|
+
// local session has the old sessionId — so the responder never sends
|
|
363
|
+
// HANDSHAKE_ACK and the initiator times out forever.
|
|
364
|
+
if (FrameCodec.isHandshakeRequest(frame.opcode)) {
|
|
365
|
+
if (session && session.getSessionId() !== frame.sessionId) {
|
|
366
|
+
this.logger.info(`Replacing stale session for ${message.pubkey} (old=${session.getSessionId()}, new=${frame.sessionId})`);
|
|
367
|
+
session.close();
|
|
368
|
+
session = undefined;
|
|
369
|
+
}
|
|
370
|
+
if (!session) {
|
|
371
|
+
this.logger.info(`Auto-creating session for inbound peer ${message.pubkey} (sessionId=${frame.sessionId})`);
|
|
372
|
+
session = this.createSession(message.pubkey, frame.sessionId);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (session) {
|
|
376
|
+
session.handleIncomingFrame(frame);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
this.logger.debug(`No session for peer ${message.pubkey}, ignoring frame opcode=0x${frame.opcode.toString(16)}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
this.logger.error(`Error processing frame from ${message.pubkey}:`, error);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
// Friend requests
|
|
387
|
+
this.peer.onFriendRequest((request) => {
|
|
388
|
+
this.logger.info(`Friend request from ${request.pubkey}`);
|
|
389
|
+
this.emit("friend-request", request);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrier-specific constants and re-exports
|
|
3
|
+
* Shared types live in src/types.ts (single source of truth)
|
|
4
|
+
*/
|
|
5
|
+
export type { PeerIdentity, RemotePeer, FriendConnectionEvent, PacketFrame, } from "../types.js";
|
|
6
|
+
export declare const FRAME_OPCODE_HANDSHAKE_REQ = 1;
|
|
7
|
+
export declare const FRAME_OPCODE_HANDSHAKE_ACK = 2;
|
|
8
|
+
export declare const FRAME_OPCODE_DATA = 16;
|
|
9
|
+
export declare const FRAME_MAGIC = 170;
|
|
10
|
+
export declare const FRAME_HEADER_SIZE = 6;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrier-specific constants and re-exports
|
|
3
|
+
* Shared types live in src/types.ts (single source of truth)
|
|
4
|
+
*/
|
|
5
|
+
// Frame protocol opcodes
|
|
6
|
+
export const FRAME_OPCODE_HANDSHAKE_REQ = 0x01;
|
|
7
|
+
export const FRAME_OPCODE_HANDSHAKE_ACK = 0x02;
|
|
8
|
+
export const FRAME_OPCODE_DATA = 0x10;
|
|
9
|
+
// Frame structure
|
|
10
|
+
export const FRAME_MAGIC = 0xaa;
|
|
11
|
+
export const FRAME_HEADER_SIZE = 6; // magic(1) + length(2) + sessionId(2) + opcode(1)
|