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