add spec sdk.md
Hee Sung Son committed
Oct 23, 2025 at 11:23 UTC
8d5f2e687d2ca66c5978ae238f325ccf63428bc6
1 file changed
+1106
spec/sdk.md
new
+1106
@@ -0,0 +1,1106 @@
1
+# RelayDNS SDK Development Specification
2
+
3
+> **Version**: 1.0.0
4
+> **Last Updated**: 2025-10-23
5
+> **Target Languages**: Go, TypeScript, Python, Rust
6
+
7
+---
8
+
9
+## 📋 Table of Contents
10
+
11
+1. [Overview](#overview)
12
+2. [Core Concepts](#core-concepts)
13
+3. [Architecture](#architecture)
14
+4. [API Specification](#api-specification)
15
+5. [Implementation Guide](#implementation-guide)
16
+6. [Language-Specific Guides](#language-specific-guides)
17
+7. [Testing](#testing)
18
+8. [Example Code](#example-code)
19
+
20
+---
21
+
22
+## Overview
23
+
24
+### What is RelayDNS?
25
+
26
+RelayDNS is a **libp2p-based lightweight P2P proxy layer**. It makes local services (SSH, HTTP, WebSocket, etc.) behind NAT externally accessible and provides DNS-style simple service discovery.
27
+
28
+### Why Do We Need an SDK?
29
+
30
+By **embedding** a RelayDNS client in your application:
31
+- Automatically advertise local services to the P2P network
32
+- Enable external access even behind NAT/firewalls
33
+- Operate without centralized reverse proxies
34
+
35
+### The Role of the SDK
36
+
37
+```
38
+┌─────────────┐
39
+│ Your App │
40
+├─────────────┤
41
+│ SDK Client │ ← This is what we'll implement
42
+├─────────────┤
43
+│ libp2p │
44
+└─────────────┘
45
+ ↕ P2P
46
+┌─────────────┐
47
+│ RelayDNS │
48
+│ Server │
49
+└─────────────┘
50
+```
51
+
52
+---
53
+
54
+## Core Concepts
55
+
56
+### 1. libp2p
57
+
58
+**A peer-to-peer networking framework**
59
+- Each node has a unique **Peer ID**
60
+- Supports multiple **Transports** (TCP, QUIC, WebSocket)
61
+- NAT traversal capabilities (Relay, Hole Punching)
62
+
63
+### 2. GossipSub
64
+
65
+**A Pub/Sub messaging protocol**
66
+- Subscribe to topics to receive messages
67
+- Broadcast peer advertisements
68
+- Provides distributed service discovery
69
+
70
+### 3. Stream Handler
71
+
72
+**Proxies libp2p streams to local TCP**
73
+- Automatically forwards connections from other peers to local services
74
+- Bidirectional byte copying
75
+- Error handling and cleanup
76
+
77
+### 4. Bootstrap Peers
78
+
79
+**Network entry points**
80
+- Known peers for initial connection
81
+- Dynamically fetched from the server's `/health` endpoint
82
+
83
+---
84
+
85
+## Architecture
86
+
87
+### Overall Structure
88
+
89
+```
90
+┌──────────────────────────────────────────────────────────┐
91
+│ Your Application │
92
+│ (HTTP Server, SSH Daemon, WebSocket Service, etc.) │
93
+└───────────────────────────┬──────────────────────────────┘
94
+ │ TCP (127.0.0.1:8081)
95
+┌───────────────────────────▼──────────────────────────────┐
96
+│ SDK Client │
97
+│ ┌────────────────────────────────────────────────────┐ │
98
+│ │ Health Client : Fetch bootstrap peers │ │
99
+│ ├────────────────────────────────────────────────────┤ │
100
+│ │ libp2p Node : P2P networking (TCP/QUIC/WS) │ │
101
+│ ├────────────────────────────────────────────────────┤ │
102
+│ │ GossipSub : Peer discovery (Pub/Sub) │ │
103
+│ ├────────────────────────────────────────────────────┤ │
104
+│ │ Stream Handler : Stream → TCP proxy │ │
105
+│ ├────────────────────────────────────────────────────┤ │
106
+│ │ Advertisement : Advertise service every 30s │ │
107
+│ └────────────────────────────────────────────────────┘ │
108
+└───────────────────────────┬──────────────────────────────┘
109
+ │ libp2p streams
110
+┌───────────────────────────▼──────────────────────────────┐
111
+│ RelayDNS Server │
112
+│ • Bootstrap coordinator │
113
+│ • GossipSub topic relay │
114
+│ • Admin UI (peer list) │
115
+│ • HTTP/WS proxy endpoints │
116
+└──────────────────────────────────────────────────────────┘
117
+```
118
+
119
+### Data Flow
120
+
121
+#### 1. Startup Sequence
122
+```
123
+1. [SDK] GET /health → [Server]
124
+ ← Returns bootstrap peer addresses
125
+
126
+2. [SDK] Create libp2p node
127
+ • Generate Ed25519 key
128
+ • Enable TCP/QUIC transports
129
+ • Configure Noise security
130
+
131
+3. [SDK] Connect to bootstrap peers
132
+ • Dial each peer
133
+ • Join the network
134
+
135
+4. [SDK] Join GossipSub topic
136
+ • Subscribe to "/relaydns/peers/1.0.0"
137
+ • Start receiving advertisements
138
+
139
+5. [SDK] Register stream handler
140
+ • "/relaydns/1.0.0" protocol
141
+ • Wait for inbound connections
142
+
143
+6. [SDK] Publish own advertisement
144
+ • Broadcast peer information
145
+ • Repeat every 30 seconds
146
+```
147
+
148
+#### 2. Connection Flow
149
+```
150
+[User] → [Server] → [SDK] → [Your App]
151
+
152
+1. User selects peer in server UI
153
+2. Server opens libp2p stream
154
+3. SDK receives stream
155
+4. SDK opens local TCP connection (127.0.0.1:8081)
156
+5. Start bidirectional byte copying
157
+```
158
+
159
+---
160
+
161
+## API Specification
162
+
163
+### Configuration
164
+
165
+All SDKs must provide the same configuration interface.
166
+
167
+#### Required Fields (3)
168
+
169
+| Field | Type | Description | Example |
170
+|-------|------|-------------|---------|
171
+| `ServerURL` | string | RelayDNS server base URL | `"http://localhost:8080"` |
172
+| `TargetTCP` | string | Local service address | `"127.0.0.1:8081"` |
173
+| `Name` | string | Display name for UI | `"my-http-service"` |
174
+
175
+#### Optional Fields (3)
176
+
177
+| Field | Type | Default | Description |
178
+|-------|------|---------|-------------|
179
+| `Protocol` | string | `"/relaydns/1.0.0"` | libp2p protocol string |
180
+| `Topic` | string | `"/relaydns/peers/1.0.0"` | GossipSub topic |
181
+| `Bootstrap` | []string | `[]` (fetched from server) | Custom bootstrap peers |
182
+
183
+#### Validation Rules
184
+
185
+- `ServerURL`: Must include `http://` or `https://`
186
+- `TargetTCP`: Must be in `host:port` format
187
+- `Name`: Non-empty string
188
+- `Bootstrap`: Valid multiaddr format (e.g., `/ip4/1.2.3.4/tcp/4001/p2p/QmXxX...`)
189
+
190
+### Client Interface
191
+
192
+#### Methods
193
+
194
+```
195
+NewClient(config: ClientConfig) -> (Client, Error)
196
+```
197
+- Create a new client instance
198
+- Validate configuration
199
+- Initialize libp2p host
200
+- Configure GossipSub
201
+- **Does not start yet** (requires Start call)
202
+
203
+```
204
+Start() -> Error
205
+```
206
+- Connect to bootstrap peers
207
+- Join GossipSub topic
208
+- Register stream handler
209
+- Start advertising
210
+- Runs in background without blocking
211
+
212
+```
213
+Close() -> Error
214
+```
215
+- Close all connections
216
+- Unsubscribe from GossipSub
217
+- Clean up resources
218
+- Graceful shutdown
219
+
220
+```
221
+PeerID() -> string
222
+```
223
+- Returns the client's libp2p Peer ID
224
+- Read-only
225
+- For debugging/logging
226
+
227
+### Health Endpoint
228
+
229
+#### Request
230
+```http
231
+GET {ServerURL}/health
232
+```
233
+
234
+#### Response
235
+```json
236
+{
237
+ "status": "healthy",
238
+ "peers": [
239
+ "/ip4/1.2.3.4/tcp/4001/p2p/QmServerPeerID",
240
+ "/ip4/5.6.7.8/tcp/4001/p2p/QmOtherPeerID"
241
+ ],
242
+ "version": "1.0.0"
243
+}
244
+```
245
+
246
+#### Error Handling
247
+- Server unreachable: Clear error message
248
+- JSON parsing failure: Configuration validation failure
249
+- Empty peers array: Warning log, continue (can use fallback bootstrap)
250
+
251
+### Advertisement Message
252
+
253
+#### Format (JSON)
254
+```json
255
+{
256
+ "peer_id": "QmXxXxXxXxXxXxXxXxXxXxXxXxXxX",
257
+ "name": "my-http-service",
258
+ "protocol": "/relaydns/1.0.0",
259
+ "timestamp": 1729728000
260
+}
261
+```
262
+
263
+#### Publishing Cycle
264
+- Initial: Immediately after Start()
265
+- Thereafter: Every 30 seconds
266
+- On shutdown: Stop advertising (automatic)
267
+
268
+---
269
+
270
+## Implementation Guide
271
+
272
+### Step-by-Step Implementation
273
+
274
+#### Step 1: Health Endpoint Client
275
+
276
+**Goal**: Fetch bootstrap peer information from server
277
+
278
+**Implementation**:
279
+```
280
+function fetchHealth(serverURL):
281
+ url = serverURL + "/health"
282
+ response = HTTP_GET(url)
283
+ if response.status != 200:
284
+ throw Error("Health check failed")
285
+
286
+ data = JSON_PARSE(response.body)
287
+ return data.peers
288
+```
289
+
290
+**Testing**:
291
+```bash
292
+curl http://localhost:8080/health
293
+# Response should contain "peers" array
294
+```
295
+
296
+#### Step 2: libp2p Node Initialization
297
+
298
+**Goal**: Set up P2P networking foundation
299
+
300
+**Required Components**:
301
+1. **Identity**: Generate Ed25519 keypair
302
+2. **Transports**: Enable TCP, QUIC
303
+3. **Security**: Noise protocol
304
+4. **Multiplexing**: Yamux
305
+5. **NAT**: Enable AutoNAT, Relay, Hole Punching
306
+
307
+**Pseudocode**:
308
+```
309
+function createLibp2pHost():
310
+ privateKey = generateEd25519Key()
311
+
312
+ host = newLibp2pHost({
313
+ identity: privateKey,
314
+ transports: [TCP, QUIC],
315
+ security: Noise,
316
+ muxer: Yamux,
317
+ enableNAT: true,
318
+ enableRelay: true,
319
+ enableHolePunching: true
320
+ })
321
+
322
+ return host
323
+```
324
+
325
+#### Step 3: GossipSub Configuration
326
+
327
+**Goal**: Enable Pub/Sub messaging
328
+
329
+**Implementation**:
330
+```
331
+function setupGossipSub(host):
332
+ pubsub = newGossipSub(host)
333
+ topic = pubsub.join("/relaydns/peers/1.0.0")
334
+ subscription = topic.subscribe()
335
+
336
+ return (pubsub, topic, subscription)
337
+```
338
+
339
+**Message Receiving**:
340
+```
341
+function handleMessages(subscription):
342
+ while message = subscription.next():
343
+ data = JSON_PARSE(message.data)
344
+ log("Received peer: " + data.name + " (" + data.peer_id + ")")
345
+```
346
+
347
+#### Step 4: Bootstrap Connection
348
+
349
+**Goal**: Join the network
350
+
351
+**Implementation**:
352
+```
353
+function connectBootstrap(host, bootstrapAddrs):
354
+ for addr in bootstrapAddrs:
355
+ try:
356
+ multiaddr = parseMultiaddr(addr)
357
+ peerInfo = extractPeerInfo(multiaddr)
358
+ host.connect(peerInfo)
359
+ log("Connected to bootstrap: " + peerInfo.id)
360
+ catch error:
361
+ log("Failed to connect: " + addr)
362
+ continue // Try next peer
363
+```
364
+
365
+**Note**: Don't terminate the program if all bootstrap connections fail, just log warnings
366
+
367
+#### Step 5: Stream Handler
368
+
369
+**Goal**: Proxy inbound connections to local service
370
+
371
+**Implementation**:
372
+```
373
+function registerStreamHandler(host, protocol, targetTCP):
374
+ host.setStreamHandler(protocol, function(stream):
375
+ handleStream(stream, targetTCP)
376
+ )
377
+
378
+function handleStream(stream, targetTCP):
379
+ try:
380
+ // Connect to local service
381
+ tcpConn = connectTCP(targetTCP, timeout=5s)
382
+
383
+ // Bidirectional copy
384
+ go copyAsync(stream -> tcpConn)
385
+ go copyAsync(tcpConn -> stream)
386
+
387
+ // Clean up when either side closes
388
+ waitForEither()
389
+
390
+ finally:
391
+ stream.close()
392
+ tcpConn.close()
393
+```
394
+
395
+**Error Handling**:
396
+- Local service down: Close stream immediately, log error
397
+- Copy error: Clean up both connections
398
+- Timeout: Give up after 5 seconds
399
+
400
+#### Step 6: Advertisement Loop
401
+
402
+**Goal**: Periodically advertise service
403
+
404
+**Implementation**:
405
+```
406
+function startAdvertisement(pubsub, topic, peerID, name, protocol):
407
+ // Advertise immediately
408
+ publishAdvertisement(topic, peerID, name, protocol)
409
+
410
+ // Repeat every 30 seconds
411
+ ticker = newTicker(30 seconds)
412
+ while true:
413
+ wait(ticker.tick)
414
+ publishAdvertisement(topic, peerID, name, protocol)
415
+
416
+function publishAdvertisement(topic, peerID, name, protocol):
417
+ message = {
418
+ "peer_id": peerID,
419
+ "name": name,
420
+ "protocol": protocol,
421
+ "timestamp": currentUnixTime()
422
+ }
423
+
424
+ data = JSON_STRINGIFY(message)
425
+ topic.publish(data)
426
+```
427
+
428
+### Error Handling Strategy
429
+
430
+#### 1. Startup Errors
431
+
432
+| Error | Cause | Handling |
433
+|-------|-------|----------|
434
+| Health endpoint failure | Server down or network issue | Fail fast, clear error |
435
+| Bootstrap connection failure | Bad address or firewall | Warning log, continue |
436
+| GossipSub subscription failure | libp2p configuration issue | Fail fast, return error |
437
+
438
+#### 2. Runtime Errors
439
+
440
+| Error | Cause | Handling |
441
+|-------|-------|----------|
442
+| Local service down | Target TCP connection failure | Close stream, log error |
443
+| Peer disconnection | Network instability | Retry with backoff |
444
+| Stream error | Protocol mismatch | Clean up stream, log error |
445
+
446
+#### 3. Reconnection Logic
447
+
448
+```
449
+function reconnectWithBackoff():
450
+ delay = 1 second
451
+ maxDelay = 60 seconds
452
+
453
+ while true:
454
+ try:
455
+ fetchHealth()
456
+ connectBootstrap()
457
+ rejoinGossipSub()
458
+ return SUCCESS
459
+ catch error:
460
+ log("Reconnect failed, retry in " + delay)
461
+ sleep(delay)
462
+ delay = min(delay * 2, maxDelay)
463
+```
464
+
465
+---
466
+
467
+## Language-Specific Guides
468
+
469
+### Go
470
+
471
+#### Dependencies
472
+```go
473
+require (
474
+ github.com/libp2p/go-libp2p v0.32.0
475
+ github.com/libp2p/go-libp2p-pubsub v0.10.0
476
+ github.com/multiformats/go-multiaddr v0.12.0
477
+)
478
+```
479
+
480
+#### Core Types
481
+```go
482
+type ClientConfig struct {
483
+ ServerURL string
484
+ TargetTCP string
485
+ Name string
486
+ Protocol string
487
+ Topic string
488
+ Bootstrap []string
489
+}
490
+
491
+type Client struct {
492
+ config ClientConfig
493
+ host host.Host
494
+ ps *pubsub.PubSub
495
+ topic *pubsub.Topic
496
+ sub *pubsub.Subscription
497
+}
498
+```
499
+
500
+#### Patterns
501
+- Use Context: `ctx context.Context`
502
+- Cleanup: `defer client.Close()`
503
+- Concurrency: goroutines + channels
504
+
505
+#### Example
506
+```go
507
+ctx := context.Background()
508
+client, err := sdk.NewClient(ctx, sdk.ClientConfig{
509
+ ServerURL: "http://localhost:8080",
510
+ TargetTCP: "127.0.0.1:8081",
511
+ Name: "demo",
512
+})
513
+if err != nil {
514
+ log.Fatal(err)
515
+}
516
+defer client.Close()
517
+
518
+if err := client.Start(ctx); err != nil {
519
+ log.Fatal(err)
520
+}
521
+
522
+select {} // Keep running
523
+```
524
+
525
+### TypeScript
526
+
527
+#### Dependencies
528
+```json
529
+{
530
+ "dependencies": {
531
+ "libp2p": "^1.0.0",
532
+ "@chainsafe/libp2p-gossipsub": "^12.0.0",
533
+ "@libp2p/tcp": "^9.0.0",
534
+ "@libp2p/noise": "^14.0.0"
535
+ }
536
+}
537
+```
538
+
539
+#### Core Types
540
+```typescript
541
+interface ClientConfig {
542
+ serverURL: string;
543
+ targetTCP: string;
544
+ name: string;
545
+ protocol?: string;
546
+ topic?: string;
547
+ bootstrap?: string[];
548
+}
549
+
550
+class Client {
551
+ constructor(config: ClientConfig);
552
+ async start(): Promise<void>;
553
+ async close(): Promise<void>;
554
+ getPeerID(): string;
555
+}
556
+```
557
+
558
+#### Patterns
559
+- All I/O uses `async/await`
560
+- Cleanup: `try/finally` or `using`
561
+- Browser: WebRTC only (requires signaling)
562
+
563
+#### Example
564
+```typescript
565
+const client = new Client({
566
+ serverURL: 'http://localhost:8080',
567
+ targetTCP: '127.0.0.1:8081',
568
+ name: 'demo'
569
+});
570
+
571
+await client.start();
572
+console.log(`Peer ID: ${client.getPeerID()}`);
573
+
574
+process.on('SIGINT', async () => {
575
+ await client.close();
576
+ process.exit(0);
577
+});
578
+```
579
+
580
+### Python
581
+
582
+#### Dependencies
583
+```toml
584
+[project]
585
+dependencies = [
586
+ "libp2p>=0.1.0",
587
+ "aiohttp>=3.9.0",
588
+]
589
+```
590
+
591
+#### Core Types
592
+```python
593
+@dataclass
594
+class ClientConfig:
595
+ server_url: str
596
+ target_tcp: str
597
+ name: str
598
+ protocol: str = "/relaydns/1.0.0"
599
+ topic: str = "/relaydns/peers/1.0.0"
600
+ bootstrap: List[str] = field(default_factory=list)
601
+
602
+class Client:
603
+ def __init__(self, config: ClientConfig): ...
604
+ async def start(self) -> None: ...
605
+ async def close(self) -> None: ...
606
+ def peer_id(self) -> str: ...
607
+```
608
+
609
+#### Patterns
610
+- `async/await` (asyncio or trio)
611
+- Context manager: `async with Client(...)`
612
+- Use type hints
613
+
614
+#### Example
615
+```python
616
+async def main():
617
+ config = ClientConfig(
618
+ server_url="http://localhost:8080",
619
+ target_tcp="127.0.0.1:8081",
620
+ name="demo"
621
+ )
622
+
623
+ async with Client(config) as client:
624
+ await client.start()
625
+ print(f"Peer ID: {client.peer_id()}")
626
+ await asyncio.Event().wait()
627
+
628
+asyncio.run(main())
629
+```
630
+
631
+### Rust
632
+
633
+#### Dependencies
634
+```toml
635
+[dependencies]
636
+libp2p = { version = "0.53", features = ["tcp", "quic", "noise", "yamux", "gossipsub"] }
637
+tokio = { version = "1.0", features = ["full"] }
638
+reqwest = { version = "0.11", features = ["json"] }
639
+```
640
+
641
+#### Core Types
642
+```rust
643
+pub struct ClientConfig {
644
+ pub server_url: String,
645
+ pub target_tcp: String,
646
+ pub name: String,
647
+ pub protocol: Option<String>,
648
+ pub topic: Option<String>,
649
+ pub bootstrap: Option<Vec<String>>,
650
+}
651
+
652
+pub struct Client {
653
+ config: ClientConfig,
654
+ swarm: Swarm<Behaviour>,
655
+}
656
+
657
+impl Client {
658
+ pub async fn new(config: ClientConfig) -> Result<Self, Error>;
659
+ pub async fn start(&mut self) -> Result<(), Error>;
660
+ pub async fn close(self) -> Result<(), Error>;
661
+ pub fn peer_id(&self) -> PeerId;
662
+}
663
+```
664
+
665
+#### Patterns
666
+- Swarm-based architecture
667
+- `Result<T, E>` error handling
668
+- Builder pattern (optional)
669
+
670
+#### Example
671
+```rust
672
+#[tokio::main]
673
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
674
+ let config = ClientConfig {
675
+ server_url: "http://localhost:8080".to_string(),
676
+ target_tcp: "127.0.0.1:8081".to_string(),
677
+ name: "demo".to_string(),
678
+ ..Default::default()
679
+ };
680
+
681
+ let mut client = Client::new(config).await?;
682
+ client.start().await?;
683
+
684
+ println!("Peer ID: {}", client.peer_id());
685
+
686
+ tokio::signal::ctrl_c().await?;
687
+ client.close().await?;
688
+
689
+ Ok(())
690
+}
691
+```
692
+
693
+---
694
+
695
+## Testing
696
+
697
+### Test Environment Setup
698
+
699
+#### 1. Start RelayDNS Server
700
+```bash
701
+# Using Docker Compose
702
+docker compose up -d
703
+
704
+# Verify server
705
+curl http://localhost:8080/health
706
+```
707
+
708
+#### 2. Start Local Service
709
+```bash
710
+# Simple HTTP server
711
+python -m http.server 8081
712
+
713
+# Or
714
+echo "Hello RelayDNS" > index.html
715
+python -m http.server 8081
716
+```
717
+
718
+### Unit Tests
719
+
720
+#### Config Validation
721
+```
722
+TEST: Error on missing required field
723
+ config = ClientConfig{Name: "test"}
724
+ client = NewClient(config)
725
+ EXPECT: error "ServerURL is required"
726
+
727
+TEST: Success with valid config
728
+ config = ClientConfig{
729
+ ServerURL: "http://localhost:8080",
730
+ TargetTCP: "127.0.0.1:8081",
731
+ Name: "test"
732
+ }
733
+ client = NewClient(config)
734
+ EXPECT: no error
735
+```
736
+
737
+#### Health Endpoint
738
+```
739
+TEST: Parse valid response
740
+ response = '{"status":"healthy","peers":["addr1"],"version":"1.0"}'
741
+ peers = parseHealth(response)
742
+ EXPECT: peers = ["addr1"]
743
+
744
+TEST: Error when server unavailable
745
+ serverURL = "http://nonexistent:9999"
746
+ EXPECT: error "connection refused"
747
+```
748
+
749
+### Integration Tests
750
+
751
+#### 1. Basic Connection Test
752
+```
753
+GIVEN: RelayDNS server is running
754
+ AND: Local HTTP server is running on port 8081
755
+
756
+WHEN: SDK client starts
757
+ config = ClientConfig{
758
+ ServerURL: "http://localhost:8080",
759
+ TargetTCP: "127.0.0.1:8081",
760
+ Name: "test-client"
761
+ }
762
+ client.Start()
763
+
764
+THEN:
765
+ - Health endpoint call succeeds
766
+ - Bootstrap peers connected successfully
767
+ - GossipSub topic joined successfully
768
+ - "test-client" appears in server Admin UI
769
+```
770
+
771
+#### 2. Proxy Test
772
+```
773
+GIVEN: Client is started
774
+ AND: Local server responds with "Hello"
775
+
776
+WHEN: Click peer in server UI → Click "Open"
777
+
778
+THEN:
779
+ - Page opens in new tab
780
+ - "Hello" text is displayed
781
+ - Stream is proxied to local port 8081
782
+```
783
+
784
+#### 3. Reconnection Test
785
+```
786
+GIVEN: Client is running
787
+
788
+WHEN: Server restarts
789
+ docker compose restart
790
+
791
+THEN:
792
+ - Client attempts to reconnect
793
+ - Backoff logs are printed
794
+ - Reconnection succeeds after server restart
795
+ - Reappears in UI
796
+```
797
+
798
+#### 4. Error Handling Test
799
+```
800
+TEST: When local service is down
801
+GIVEN: Client is running
802
+WHEN: Local server is stopped (kill python)
803
+ AND: Connection attempt from server
804
+THEN:
805
+ - Stream closes immediately
806
+ - Error log: "connection refused"
807
+ - Client continues running
808
+```
809
+
810
+### Test Checklist
811
+
812
+Verify the following in a real environment:
813
+
814
+- [ ] Health endpoint call succeeds
815
+- [ ] Connected to bootstrap peers
816
+- [ ] Joined GossipSub topic
817
+- [ ] Published own advertisement
818
+- [ ] Received other peer advertisements
819
+- [ ] Displayed in server Admin UI
820
+- [ ] Proxy connection works
821
+- [ ] Handles local service down gracefully
822
+- [ ] Graceful shutdown with Ctrl+C
823
+- [ ] Normal operation after restart
824
+
825
+---
826
+
827
+## Example Code
828
+
829
+### Minimal Example (Go)
830
+
831
+```go
832
+package main
833
+
834
+import (
835
+ "context"
836
+ "log"
837
+
838
+ sdk "github.com/your-org/relaydns-sdk-go"
839
+)
840
+
841
+func main() {
842
+ ctx := context.Background()
843
+
844
+ // Create client
845
+ client, err := sdk.NewClient(ctx, sdk.ClientConfig{
846
+ ServerURL: "http://localhost:8080",
847
+ TargetTCP: "127.0.0.1:8081",
848
+ Name: "my-service",
849
+ })
850
+ if err != nil {
851
+ log.Fatalf("Failed to create client: %v", err)
852
+ }
853
+ defer client.Close()
854
+
855
+ // Start
856
+ if err := client.Start(ctx); err != nil {
857
+ log.Fatalf("Failed to start client: %v", err)
858
+ }
859
+
860
+ log.Printf("✓ Started. Peer ID: %s", client.PeerID())
861
+
862
+ // Keep running
863
+ select {}
864
+}
865
+```
866
+
867
+### Custom Bootstrap (TypeScript)
868
+
869
+```typescript
870
+import { Client, ClientConfig } from 'relaydns-sdk';
871
+
872
+const config: ClientConfig = {
873
+ serverURL: 'http://localhost:8080',
874
+ targetTCP: '127.0.0.1:8081',
875
+ name: 'custom-service',
876
+ bootstrap: [
877
+ '/ip4/1.2.3.4/tcp/4001/p2p/QmPeerID1',
878
+ '/ip4/5.6.7.8/tcp/4001/p2p/QmPeerID2'
879
+ ]
880
+};
881
+
882
+const client = new Client(config);
883
+
884
+try {
885
+ await client.start();
886
+ console.log(`✓ Started. Peer ID: ${client.getPeerID()}`);
887
+
888
+ await new Promise(() => {}); // Keep running
889
+} catch (error) {
890
+ console.error('Failed:', error);
891
+ process.exit(1);
892
+}
893
+```
894
+
895
+### Error Handling (Python)
896
+
897
+```python
898
+import asyncio
899
+import logging
900
+from relaydns_sdk import Client, ClientConfig
901
+
902
+logging.basicConfig(level=logging.INFO)
903
+logger = logging.getLogger(__name__)
904
+
905
+async def main():
906
+ config = ClientConfig(
907
+ server_url="http://localhost:8080",
908
+ target_tcp="127.0.0.1:8081",
909
+ name="python-service"
910
+ )
911
+
912
+ client = Client(config)
913
+
914
+ try:
915
+ await client.start()
916
+ logger.info(f"✓ Started. Peer ID: {client.peer_id()}")
917
+
918
+ # Keep running
919
+ await asyncio.Event().wait()
920
+
921
+ except ConnectionError as e:
922
+ logger.error(f"✗ Connection failed: {e}")
923
+ return 1
924
+
925
+ except Exception as e:
926
+ logger.error(f"✗ Unexpected error: {e}")
927
+ return 1
928
+
929
+ finally:
930
+ await client.close()
931
+ logger.info("✓ Closed gracefully")
932
+
933
+ return 0
934
+
935
+if __name__ == "__main__":
936
+ exit_code = asyncio.run(main())
937
+ exit(exit_code)
938
+```
939
+
940
+### Production Example (Rust)
941
+
942
+```rust
943
+use relaydns_sdk::{Client, ClientConfig};
944
+use tokio;
945
+use tracing::{info, error};
946
+
947
+#[tokio::main]
948
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
949
+ // Initialize logging
950
+ tracing_subscriber::fmt::init();
951
+
952
+ // Configuration
953
+ let config = ClientConfig {
954
+ server_url: "http://localhost:8080".to_string(),
955
+ target_tcp: "127.0.0.1:8081".to_string(),
956
+ name: "rust-service".to_string(),
957
+ ..Default::default()
958
+ };
959
+
960
+ // Create client
961
+ let mut client = Client::new(config).await.map_err(|e| {
962
+ error!("Failed to create client: {}", e);
963
+ e
964
+ })?;
965
+
966
+ // Start
967
+ client.start().await.map_err(|e| {
968
+ error!("Failed to start client: {}", e);
969
+ e
970
+ })?;
971
+
972
+ info!("✓ Started. Peer ID: {}", client.peer_id());
973
+
974
+ // Wait for shutdown signal
975
+ tokio::select! {
976
+ _ = tokio::signal::ctrl_c() => {
977
+ info!("Received Ctrl+C, shutting down...");
978
+ }
979
+ }
980
+
981
+ // Graceful shutdown
982
+ client.close().await?;
983
+ info!("✓ Closed gracefully");
984
+
985
+ Ok(())
986
+}
987
+```
988
+
989
+---
990
+
991
+## Appendix
992
+
993
+### A. Multiaddr Format
994
+
995
+Address format used in RelayDNS:
996
+
997
+```
998
+/ip4/1.2.3.4/tcp/4001/p2p/QmPeerID...
999
+│ │ │ │ │ └─ Peer ID (required)
1000
+│ │ │ │ └───── Transport protocol
1001
+│ │ │ └────────── Port
1002
+│ │ └────────────── Transport
1003
+│ └────────────────────── IP address
1004
+└─────────────────────────── IP version
1005
+```
1006
+
1007
+Other examples:
1008
+- `/ip4/127.0.0.1/tcp/4001/p2p/Qm...` - Local TCP
1009
+- `/ip6/::1/tcp/4001/p2p/Qm...` - IPv6
1010
+- `/ip4/1.2.3.4/udp/4001/quic-v1/p2p/Qm...` - QUIC
1011
+
1012
+### B. Debugging Tips
1013
+
1014
+#### "Cannot connect to bootstrap"
1015
+```bash
1016
+# Check server status
1017
+curl http://localhost:8080/health
1018
+
1019
+# Check port
1020
+nc -zv localhost 4001
1021
+
1022
+# Check firewall
1023
+sudo ufw status
1024
+```
1025
+
1026
+#### "Stream handler not called"
1027
+```bash
1028
+# Verify protocol match
1029
+# Client: "/relaydns/1.0.0"
1030
+# Server: Same protocol
1031
+
1032
+# Check GossipSub messages in logs
1033
+# Should see "Published advertisement" messages
1034
+```
1035
+
1036
+#### "Local service connection refused"
1037
+```bash
1038
+# Check local service
1039
+nc -zv 127.0.0.1 8081
1040
+
1041
+# Or
1042
+curl http://127.0.0.1:8081
1043
+```
1044
+
1045
+### C. Performance Considerations
1046
+
1047
+#### Buffer Size
1048
+```
1049
+Recommended: 32KB read/write buffer
1050
+- Larger: Throughput ↑, Memory ↑
1051
+- Smaller: Latency ↓, Memory ↓
1052
+```
1053
+
1054
+#### GossipSub Tuning
1055
+```
1056
+Low latency:
1057
+- heartbeat: 500ms
1058
+- fanout: 8
1059
+
1060
+Bandwidth saving:
1061
+- heartbeat: 2s
1062
+- fanout: 4
1063
+```
1064
+
1065
+#### Connection Pooling
1066
+```
1067
+Problem: New TCP connection per stream
1068
+Solution: Maintain TCP connection pool
1069
+- Pool size: 10-20
1070
+- Idle timeout: 60 seconds
1071
+```
1072
+
1073
+### D. Security
1074
+
1075
+#### Encryption
1076
+- All streams: Encrypted with Noise protocol
1077
+- Forward secrecy guaranteed
1078
+
1079
+#### Authentication
1080
+- Automatic authentication with Peer ID
1081
+- Public key-based
1082
+
1083
+#### Authorization
1084
+- Current: All peers can connect
1085
+- Future: ACL (Peer ID-based)
1086
+
1087
+---
1088
+
1089
+## References
1090
+
1091
+### Official Documentation
1092
+- libp2p: https://docs.libp2p.io/
1093
+- GossipSub: https://github.com/libp2p/specs/tree/master/pubsub/gossipsub
1094
+- RelayDNS: https://github.com/gosuda/relaydns
1095
+
1096
+### Library Documentation
1097
+- Go libp2p: https://pkg.go.dev/github.com/libp2p/go-libp2p
1098
+- TypeScript libp2p: https://github.com/libp2p/js-libp2p
1099
+- Python libp2p: https://github.com/libp2p/py-libp2p
1100
+- Rust libp2p: https://docs.rs/libp2p
1101
+
1102
+---
1103
+
1104
+**Document Version**: 1.0.0
1105
+**Last Updated**: 2025-10-23
1106
+**Feedback**: https://github.com/gosuda/relaydns/issues