docs(network): add ipns architecture with cloudflare gateway integration
This commit is contained in:
380
NETWORK.md
Normal file
380
NETWORK.md
Normal file
@@ -0,0 +1,380 @@
|
||||
# Network Architecture: IPNS DNSLink + Cloudflare IPFS Gateway
|
||||
|
||||
Integration guide for Sonr's decentralized storage layer using IPNS DNSLink with Cloudflare's enterprise IPFS gateway.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ SONR NETWORK TOPOLOGY │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
|
||||
│ │ Browser Client │ │ Cloudflare Edge (330+) │ │
|
||||
│ │ │ HTTPS │ │ │
|
||||
│ │ vault.sonr.org ────┼───────────────────►│ IPFS Gateway + CDN Cache │ │
|
||||
│ │ │ │ │ │
|
||||
│ └─────────────────────┘ └──────────────┬──────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ Circuit Relay │ Bitswap │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
|
||||
│ │ Sonr Relay Node │◄──────────────────►│ IPFS Network │ │
|
||||
│ │ relay.sonr.org │ P2P/DHT │ │ │
|
||||
│ │ │ │ • Content Discovery │ │
|
||||
│ │ • Helia + Coord │ │ • Block Exchange │ │
|
||||
│ │ • IPNS Publishing │ │ • DHT Routing │ │
|
||||
│ └─────────────────────┘ └─────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ │ IPNS Publish │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Cloudflare DNS (sonr.org) │ │
|
||||
│ │ │ │
|
||||
│ │ _dnslink.vault.sonr.org TXT "dnslink=/ipns/<PEER_ID>" │ │
|
||||
│ │ vault.sonr.org CNAME cloudflare-ipfs.com (proxied) │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Cloudflare Enterprise Features
|
||||
|
||||
### Plan Comparison
|
||||
|
||||
| Feature | Free/Pro/Business | Enterprise |
|
||||
|---------|-------------------|------------|
|
||||
| Total Gateways | 15 | Unlimited |
|
||||
| Gateway Types | DNSLink only | DNSLink + Universal Path |
|
||||
| Included Bandwidth | 50 GB | 100 GB |
|
||||
| File Size Limit | None | None |
|
||||
| SLA | Standard | Enterprise SLA |
|
||||
| Support | Ticket | 24/7 Dedicated |
|
||||
| Peering Priority | Standard | Custom peering configuration |
|
||||
|
||||
### Enterprise Benefits
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| Unlimited Gateways | No limit on custom domain gateways |
|
||||
| Universal Path Gateway | Access any CID via `gateway.sonr.org/ipfs/<CID>` |
|
||||
| Custom Peering | Direct peering with Sonr relay nodes |
|
||||
| Edge Caching | 330+ PoPs, automatic cache invalidation on IPNS update |
|
||||
| Origin Isolation | XSS protection via subdomain isolation |
|
||||
|
||||
## DNS Configuration
|
||||
|
||||
### Required Records
|
||||
|
||||
| Type | Name | Value | Proxy |
|
||||
|------|------|-------|-------|
|
||||
| CNAME | `vault.sonr.org` | `cloudflare-ipfs.com` | Proxied |
|
||||
| TXT | `_dnslink.vault.sonr.org` | `dnslink=/ipns/<PEER_ID>` | N/A |
|
||||
| CNAME | `gateway.sonr.org` | `cloudflare-ipfs.com` | Proxied |
|
||||
| TXT | `_dnslink.gateway.sonr.org` | `dnslink=/ipns/<PEER_ID>` | N/A |
|
||||
|
||||
### DNSLink Format
|
||||
|
||||
```
|
||||
dnslink=/ipns/<PEER_ID>
|
||||
dnslink=/ipns/<DOMAIN>
|
||||
dnslink=/ipfs/<CID>
|
||||
```
|
||||
|
||||
| Format | Use Case |
|
||||
|--------|----------|
|
||||
| `/ipns/<PEER_ID>` | Mutable content, updates without DNS change |
|
||||
| `/ipns/<DOMAIN>` | Recursive resolution to another DNSLink |
|
||||
| `/ipfs/<CID>` | Immutable content, requires DNS update |
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
dig +short TXT _dnslink.vault.sonr.org
|
||||
```
|
||||
|
||||
## Gateway Types
|
||||
|
||||
### DNSLink Gateway (Restricted)
|
||||
|
||||
Serves content from a single IPNS/CID specified in DNS.
|
||||
|
||||
```
|
||||
https://vault.sonr.org/
|
||||
└── Resolves _dnslink.vault.sonr.org
|
||||
└── /ipns/k51qzi5uqu5...
|
||||
└── /ipfs/bafy...
|
||||
```
|
||||
|
||||
### Universal Path Gateway (Enterprise)
|
||||
|
||||
Serves any content by CID or IPNS path.
|
||||
|
||||
```
|
||||
https://gateway.sonr.org/ipfs/<CID>
|
||||
https://gateway.sonr.org/ipns/<PEER_ID>
|
||||
https://gateway.sonr.org/ipns/<DOMAIN>
|
||||
```
|
||||
|
||||
## IPNS Publishing
|
||||
|
||||
### Helia Integration
|
||||
|
||||
```typescript
|
||||
import { createHelia } from 'helia'
|
||||
import { ipns } from '@helia/ipns'
|
||||
import { unixfs } from '@helia/unixfs'
|
||||
import { generateKeyPair } from '@libp2p/crypto/keys'
|
||||
|
||||
const helia = await createHelia()
|
||||
const name = ipns(helia)
|
||||
const fs = unixfs(helia)
|
||||
|
||||
const cid = await fs.addBytes(encryptedEnclaveBlob)
|
||||
|
||||
await name.publish(privateKey, cid, {
|
||||
lifetime: 24 * 60 * 60 * 1000
|
||||
})
|
||||
```
|
||||
|
||||
### DNSLink Resolution
|
||||
|
||||
```typescript
|
||||
import { dnsLink } from '@helia/dnslink'
|
||||
|
||||
const resolver = dnsLink(helia)
|
||||
const result = await resolver.resolve('vault.sonr.org')
|
||||
```
|
||||
|
||||
## TTL Configuration
|
||||
|
||||
### DNS TTL
|
||||
|
||||
| Use Case | TTL | Rationale |
|
||||
|----------|-----|-----------|
|
||||
| Frequently updated content | 60s | Fast propagation |
|
||||
| Stable content | 3600s | Reduce DNS queries |
|
||||
| Production default | 300s | Balance between freshness and efficiency |
|
||||
|
||||
### IPNS Lifetime
|
||||
|
||||
| Parameter | Value | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `lifetime` | 24h | Record validity period |
|
||||
| `ttl` | 1h | Suggested cache duration for resolvers |
|
||||
|
||||
```typescript
|
||||
await name.publish(privateKey, cid, {
|
||||
lifetime: 24 * 60 * 60 * 1000,
|
||||
ttl: 60 * 60 * 1000
|
||||
})
|
||||
```
|
||||
|
||||
## Cloudflare API Integration
|
||||
|
||||
### Create Web3 Gateway
|
||||
|
||||
```bash
|
||||
curl "https://api.cloudflare.com/client/v4/zones/{zone_id}/web3/hostnames" \
|
||||
--header "Authorization: Bearer <API_TOKEN>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"name": "vault.sonr.org",
|
||||
"description": "Sonr Vault IPFS Gateway",
|
||||
"target": "ipfs",
|
||||
"dnslink": "/ipns/<PEER_ID>"
|
||||
}'
|
||||
```
|
||||
|
||||
### Update DNSLink
|
||||
|
||||
```bash
|
||||
curl "https://api.cloudflare.com/client/v4/zones/{zone_id}/web3/hostnames/{gateway_id}" \
|
||||
--request PATCH \
|
||||
--header "Authorization: Bearer <API_TOKEN>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"dnslink": "/ipns/<NEW_PEER_ID>"
|
||||
}'
|
||||
```
|
||||
|
||||
### Create TXT Record
|
||||
|
||||
```bash
|
||||
curl "https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records" \
|
||||
--header "Authorization: Bearer <API_TOKEN>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"type": "TXT",
|
||||
"name": "_dnslink.vault.sonr.org",
|
||||
"content": "dnslink=/ipns/<PEER_ID>",
|
||||
"ttl": 300
|
||||
}'
|
||||
```
|
||||
|
||||
## Domain Strategy
|
||||
|
||||
### Subdomain Allocation
|
||||
|
||||
| Subdomain | Type | Purpose |
|
||||
|-----------|------|---------|
|
||||
| `vault.sonr.org` | DNSLink | User enclave data (IPNS) |
|
||||
| `did.sonr.org` | DNSLink | DID document resolution (IPNS) |
|
||||
| `gateway.sonr.org` | Universal | General IPFS access (Enterprise) |
|
||||
| `relay.sonr.org` | Direct | Circuit Relay server (WSS) |
|
||||
|
||||
### Resolution Flow
|
||||
|
||||
```
|
||||
User Request: https://vault.sonr.org/
|
||||
│
|
||||
▼
|
||||
Cloudflare Edge
|
||||
│
|
||||
├── Check edge cache
|
||||
│ └── Hit: Return cached content
|
||||
│
|
||||
├── DNS Lookup: _dnslink.vault.sonr.org
|
||||
│ └── TXT: dnslink=/ipns/k51qzi5uqu5...
|
||||
│
|
||||
├── IPNS Resolution
|
||||
│ └── k51qzi5uqu5... → /ipfs/bafy...
|
||||
│
|
||||
├── IPFS Fetch (Bitswap)
|
||||
│ └── Retrieve blocks from network
|
||||
│
|
||||
└── Cache + Return
|
||||
```
|
||||
|
||||
## Caching Behavior
|
||||
|
||||
### Edge Cache
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| First request | Fetch from IPFS, cache at edge |
|
||||
| Subsequent requests | Serve from edge cache |
|
||||
| IPNS update | Cache invalidated on TTL expiry |
|
||||
| Manual purge | API-triggered cache clear |
|
||||
|
||||
### Cache Headers
|
||||
|
||||
```
|
||||
Cache-Control: public, max-age=3600
|
||||
X-IPFS-Path: /ipfs/bafy...
|
||||
X-IPFS-Roots: bafy...
|
||||
ETag: "bafy..."
|
||||
```
|
||||
|
||||
## Content Update Flow
|
||||
|
||||
### Without DNS Change (IPNS)
|
||||
|
||||
```
|
||||
1. Encrypt new enclave blob
|
||||
2. Add to Helia: cid = await fs.addBytes(blob)
|
||||
3. Publish IPNS: await name.publish(key, cid)
|
||||
4. Cloudflare resolves new CID on next request (after TTL)
|
||||
```
|
||||
|
||||
### With DNS Change (CID)
|
||||
|
||||
```
|
||||
1. Add content to IPFS
|
||||
2. Update _dnslink TXT record via Cloudflare API
|
||||
3. Wait for DNS propagation (TTL)
|
||||
4. Cloudflare serves new content
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Transport Security
|
||||
|
||||
| Layer | Protection |
|
||||
|-------|------------|
|
||||
| Client → Cloudflare | TLS 1.3 |
|
||||
| Cloudflare → IPFS | Bitswap (encrypted channels) |
|
||||
| Content | AES-256-GCM (client-side) |
|
||||
|
||||
### Content Integrity
|
||||
|
||||
| Mechanism | Guarantee |
|
||||
|-----------|-----------|
|
||||
| CID | SHA-256 hash of content |
|
||||
| IPNS Signature | Ed25519 signed record |
|
||||
| DNSLink | DNSSEC (if enabled) |
|
||||
|
||||
### Access Control
|
||||
|
||||
| Method | Implementation |
|
||||
|--------|----------------|
|
||||
| Private content | Encrypt before upload |
|
||||
| Rate limiting | Cloudflare WAF rules |
|
||||
| Blocklist | Universal gateway content filtering |
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Cloudflare Analytics
|
||||
|
||||
| Metric | Source |
|
||||
|--------|--------|
|
||||
| Requests | Web3 Gateway dashboard |
|
||||
| Bandwidth | Zone analytics |
|
||||
| Cache hit ratio | Cache analytics |
|
||||
| Error rates | Firewall events |
|
||||
|
||||
### IPNS Health
|
||||
|
||||
```typescript
|
||||
const status = {
|
||||
peerId: helia.libp2p.peerId.toString(),
|
||||
peers: helia.libp2p.getPeers().length,
|
||||
ipnsRecords: await getPublishedRecords()
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Cloudflare Setup
|
||||
|
||||
- [ ] Enable Web3 gateway on zone
|
||||
- [ ] Create DNSLink gateway for `vault.sonr.org`
|
||||
- [ ] Create DNSLink gateway for `did.sonr.org`
|
||||
- [ ] Create Universal gateway for `gateway.sonr.org` (Enterprise)
|
||||
- [ ] Configure TXT records for each subdomain
|
||||
- [ ] Verify CNAME proxying enabled
|
||||
- [ ] Set appropriate TTLs
|
||||
|
||||
### Relay Node Setup
|
||||
|
||||
- [ ] Deploy `ipfs-service-provider` with relay enabled
|
||||
- [ ] Generate persistent IPNS keypair
|
||||
- [ ] Configure IPNS publishing on content update
|
||||
- [ ] Set up monitoring for IPNS record health
|
||||
- [ ] Configure peering with Cloudflare (Enterprise)
|
||||
|
||||
### SDK Integration
|
||||
|
||||
- [ ] Add `@helia/ipns` for publishing
|
||||
- [ ] Add `@helia/dnslink` for resolution
|
||||
- [ ] Implement enclave CID tracking
|
||||
- [ ] Add IPNS publish on enclave save
|
||||
- [ ] Handle offline/online sync
|
||||
|
||||
## Dependencies
|
||||
|
||||
```json
|
||||
{
|
||||
"@helia/ipns": "^8.0.0",
|
||||
"@helia/dnslink": "^4.0.0",
|
||||
"@helia/unixfs": "^4.0.0",
|
||||
"helia": "^5.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Cloudflare Web3 Gateway](https://developers.cloudflare.com/web3/)
|
||||
- [IPFS DNSLink Specification](https://dnslink.dev/)
|
||||
- [Helia IPNS Package](https://github.com/ipfs/helia/tree/main/packages/ipns)
|
||||
- [Cloudflare API - Web3](https://developers.cloudflare.com/api/resources/web3/)
|
||||
404
STORAGE.md
Normal file
404
STORAGE.md
Normal file
@@ -0,0 +1,404 @@
|
||||
# IPFS Storage Architecture for Motr Enclave
|
||||
|
||||
Decentralized storage layer for Nebula wallet's encrypted key enclave using IPFS via Helia with Circuit Relay connectivity.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ NEBULA WALLET STORAGE │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ Browser Environment │ │ Sonr Relay Infrastructure │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ┌───────────────────┐ │ WSS │ ┌───────────────────────────┐ │ │
|
||||
│ │ │ Motr Enclave │ │◄───────►│ │ ipfs-service-provider │ │ │
|
||||
│ │ │ (WASM Plugin) │ │ │ │ + circuitRelayServer() │ │ │
|
||||
│ │ └───────────────────┘ │ │ └───────────────────────────┘ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ ▼ │ │ ▼ │ │
|
||||
│ │ ┌───────────────────┐ │ │ ┌───────────────────────────┐ │ │
|
||||
│ │ │ Helia Client │ │ │ │ Helia Node │ │ │
|
||||
│ │ │ (Browser SDK) │──┼─────────┼──│ (Content Pinning) │ │ │
|
||||
│ │ └───────────────────┘ │ Relay │ └───────────────────────────┘ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ └───────────┼─────────────┘ └──────────────┼──────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ /p2p-circuit │ DHT / Bitswap │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ IPFS Network │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Data Classification
|
||||
|
||||
### MUST Store on IPFS
|
||||
|
||||
| Data Type | Format | Mutability |
|
||||
|-----------|--------|------------|
|
||||
| Encrypted DB Blob | Raw bytes | Immutable (new CID per version) |
|
||||
| DID Document | DAG-CBOR | Mutable via IPNS |
|
||||
| UCAN Delegations | DAG-CBOR | Immutable CID references |
|
||||
| Verification Methods | DAG-CBOR | Part of DID Document |
|
||||
|
||||
### MUST NOT Store on IPFS
|
||||
|
||||
| Data Type | Reason |
|
||||
|-----------|--------|
|
||||
| MPC Key Shares | Core secret material |
|
||||
| WebAuthn PRF Output | Decryption key derivation |
|
||||
| Session Tokens | Ephemeral, local-only |
|
||||
| Plaintext Credentials | Security violation |
|
||||
|
||||
---
|
||||
|
||||
## Browser Client Requirements
|
||||
|
||||
### Transport Configuration
|
||||
|
||||
The browser Helia client MUST configure these transports:
|
||||
|
||||
```typescript
|
||||
transports: [
|
||||
webSockets({ filter: filters.all }),
|
||||
webRTC(),
|
||||
circuitRelayTransport({ discoverRelays: 2 })
|
||||
]
|
||||
```
|
||||
|
||||
### Listen Addresses
|
||||
|
||||
The client MUST listen on:
|
||||
|
||||
```typescript
|
||||
addresses: {
|
||||
listen: ['/p2p-circuit', '/webrtc']
|
||||
}
|
||||
```
|
||||
|
||||
### Required Services
|
||||
|
||||
The `identify` service is MANDATORY for Circuit Relay v2:
|
||||
|
||||
```typescript
|
||||
services: {
|
||||
identify: identify()
|
||||
}
|
||||
```
|
||||
|
||||
### Bootstrap Peers
|
||||
|
||||
The client MUST bootstrap with at least 2 Sonr relay multiaddrs:
|
||||
|
||||
```typescript
|
||||
peerDiscovery: [
|
||||
bootstrap({
|
||||
list: [
|
||||
'/dns4/relay1.sonr.io/tcp/443/wss/p2p/<PEER_ID>',
|
||||
'/dns4/relay2.sonr.io/tcp/443/wss/p2p/<PEER_ID>'
|
||||
]
|
||||
})
|
||||
]
|
||||
```
|
||||
|
||||
### Complete Client Implementation
|
||||
|
||||
```typescript
|
||||
import { createHelia } from 'helia'
|
||||
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
|
||||
import { webRTC } from '@libp2p/webrtc'
|
||||
import { webSockets } from '@libp2p/websockets'
|
||||
import { bootstrap } from '@libp2p/bootstrap'
|
||||
import { identify } from '@libp2p/identify'
|
||||
import { noise } from '@chainsafe/libp2p-noise'
|
||||
import { yamux } from '@chainsafe/libp2p-yamux'
|
||||
import * as filters from '@libp2p/websockets/filters'
|
||||
|
||||
const SONR_RELAYS = [
|
||||
'/dns4/relay1.sonr.io/tcp/443/wss/p2p/<PEER_ID>',
|
||||
'/dns4/relay2.sonr.io/tcp/443/wss/p2p/<PEER_ID>'
|
||||
]
|
||||
|
||||
export async function createStorageClient(): Promise<Helia> {
|
||||
return await createHelia({
|
||||
libp2p: {
|
||||
addresses: {
|
||||
listen: ['/p2p-circuit', '/webrtc']
|
||||
},
|
||||
transports: [
|
||||
webSockets({ filter: filters.all }),
|
||||
webRTC(),
|
||||
circuitRelayTransport({ discoverRelays: 2 })
|
||||
],
|
||||
connectionEncrypters: [noise()],
|
||||
streamMuxers: [yamux()],
|
||||
peerDiscovery: [
|
||||
bootstrap({ list: SONR_RELAYS })
|
||||
],
|
||||
services: {
|
||||
identify: identify()
|
||||
},
|
||||
connectionGater: {
|
||||
denyDialMultiaddr: () => false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relay Server Requirements
|
||||
|
||||
### Circuit Relay Configuration
|
||||
|
||||
The relay server MUST enable `circuitRelayServer` with these constraints:
|
||||
|
||||
```javascript
|
||||
import { circuitRelayServer } from '@libp2p/circuit-relay-v2'
|
||||
|
||||
services: {
|
||||
relay: circuitRelayServer({
|
||||
hopTimeout: 30 * 1000,
|
||||
reservations: {
|
||||
maxReservations: 100,
|
||||
reservationClearInterval: 300 * 1000,
|
||||
applyDefaultLimit: true,
|
||||
defaultDurationLimit: 2 * 60 * 1000,
|
||||
defaultDataLimit: BigInt(1 << 20)
|
||||
},
|
||||
maxInboundHopStreams: 64,
|
||||
maxOutboundHopStreams: 128
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Listen Addresses
|
||||
|
||||
The relay MUST listen on:
|
||||
|
||||
```javascript
|
||||
addresses: {
|
||||
listen: [
|
||||
'/ip4/0.0.0.0/tcp/4001',
|
||||
'/ip4/0.0.0.0/tcp/4003/ws',
|
||||
'/ip4/0.0.0.0/udp/4001/quic-v1',
|
||||
'/webrtc'
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `ENABLE_CIRCUIT_RELAY` | Yes | Set to `1` |
|
||||
| `CR_DOMAIN` | Yes | Relay domain (e.g., `relay.sonr.org`) |
|
||||
| `IPFS_TCP_PORT` | Yes | TCP port (default: `4001`) |
|
||||
| `IPFS_WS_PORT` | Yes | WebSocket port (default: `4003`) |
|
||||
| `COORD_NAME` | No | Node identifier for coordination |
|
||||
|
||||
### Infrastructure Requirements
|
||||
|
||||
| Requirement | Specification |
|
||||
|-------------|---------------|
|
||||
| SSL/TLS | Valid certificate for WSS |
|
||||
| Reverse Proxy | Nginx forwarding 443 → `IPFS_WS_PORT` |
|
||||
| Redundancy | Minimum 2 relay nodes |
|
||||
| Uptime | 99.9% availability target |
|
||||
|
||||
---
|
||||
|
||||
## Storage Operations
|
||||
|
||||
### Enclave Persistence
|
||||
|
||||
```typescript
|
||||
import { unixfs } from '@helia/unixfs'
|
||||
import type { CID } from 'multiformats/cid'
|
||||
import type { Helia } from 'helia'
|
||||
|
||||
export class EnclaveStorage {
|
||||
private fs: UnixFS
|
||||
|
||||
constructor(private helia: Helia) {
|
||||
this.fs = unixfs(helia)
|
||||
}
|
||||
|
||||
async persist(encryptedBlob: Uint8Array): Promise<CID> {
|
||||
const cid = await this.fs.addBytes(encryptedBlob)
|
||||
await this.helia.pins.add(cid)
|
||||
return cid
|
||||
}
|
||||
|
||||
async retrieve(cid: CID): Promise<Uint8Array> {
|
||||
const chunks: Uint8Array[] = []
|
||||
for await (const chunk of this.fs.cat(cid)) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
return concatUint8Arrays(chunks)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### DID Publishing
|
||||
|
||||
```typescript
|
||||
import { ipns } from '@helia/ipns'
|
||||
import { dagCbor } from '@helia/dag-cbor'
|
||||
import type { PrivateKey } from '@libp2p/interface'
|
||||
|
||||
export class DIDPublisher {
|
||||
private name: IPNS
|
||||
private cbor: DagCBOR
|
||||
|
||||
constructor(private helia: Helia) {
|
||||
this.name = ipns(helia)
|
||||
this.cbor = dagCbor(helia)
|
||||
}
|
||||
|
||||
async publish(didDocument: object, key: PrivateKey): Promise<string> {
|
||||
const cid = await this.cbor.add(didDocument)
|
||||
await this.name.publish(key, cid)
|
||||
return `/ipns/${key.publicKey.toCID()}`
|
||||
}
|
||||
|
||||
async resolve(ipnsName: string): Promise<object> {
|
||||
const result = await this.name.resolve(ipnsName)
|
||||
return await this.cbor.get(result.cid)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Requirements
|
||||
|
||||
### Encryption
|
||||
|
||||
| Layer | Requirement |
|
||||
|-------|-------------|
|
||||
| Transport | TLS 1.3 (WSS) + libp2p Noise protocol |
|
||||
| Storage | AES-256-GCM with WebAuthn PRF-derived key |
|
||||
| Integrity | SHA-256 content hash (CID) |
|
||||
|
||||
### Relay Trust Model
|
||||
|
||||
| Property | Guarantee |
|
||||
|----------|-----------|
|
||||
| Zero-Knowledge | Relay MUST NOT see plaintext data |
|
||||
| No Persistence | Relay MUST NOT store relayed content |
|
||||
| Rate Limiting | Relay MUST enforce per-peer limits |
|
||||
|
||||
### Client Responsibilities
|
||||
|
||||
| Responsibility | Requirement |
|
||||
|----------------|-------------|
|
||||
| Encrypt before upload | All data MUST be encrypted client-side |
|
||||
| Verify on download | Client MUST verify CID matches content |
|
||||
| Local CID storage | Latest CID MUST persist in IndexedDB |
|
||||
| Key isolation | Decryption keys MUST NOT leave browser |
|
||||
|
||||
---
|
||||
|
||||
## API Specification
|
||||
|
||||
### StorageClient Interface
|
||||
|
||||
```typescript
|
||||
interface StorageClient {
|
||||
connect(): Promise<void>
|
||||
disconnect(): Promise<void>
|
||||
isConnected(): boolean
|
||||
|
||||
persistEnclave(encrypted: Uint8Array): Promise<CID>
|
||||
retrieveEnclave(cid: CID): Promise<Uint8Array>
|
||||
|
||||
publishDID(doc: DIDDocument, key: PrivateKey): Promise<string>
|
||||
resolveDID(name: string): Promise<DIDDocument>
|
||||
|
||||
getStatus(): StorageStatus
|
||||
}
|
||||
|
||||
interface StorageStatus {
|
||||
peerId: string
|
||||
connected: boolean
|
||||
peerCount: number
|
||||
relayCount: number
|
||||
}
|
||||
```
|
||||
|
||||
### Relay API Endpoints
|
||||
|
||||
| Endpoint | Method | Response |
|
||||
|----------|--------|----------|
|
||||
| `/ipfs` | GET | `{ ipfsId, peers, relays }` |
|
||||
| `/ipfs/node` | GET | Full node configuration |
|
||||
| `/ipfs/relays` | POST | Known relay list |
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Browser SDK
|
||||
|
||||
```json
|
||||
{
|
||||
"helia": "^5.0.0",
|
||||
"@helia/unixfs": "^4.0.0",
|
||||
"@helia/ipns": "^8.0.0",
|
||||
"@helia/dag-cbor": "^4.0.0",
|
||||
"@libp2p/circuit-relay-v2": "^3.0.0",
|
||||
"@libp2p/webrtc": "^5.0.0",
|
||||
"@libp2p/websockets": "^9.0.0",
|
||||
"@libp2p/bootstrap": "^11.0.0",
|
||||
"@libp2p/identify": "^3.0.0",
|
||||
"@chainsafe/libp2p-noise": "^16.0.0",
|
||||
"@chainsafe/libp2p-yamux": "^7.0.0",
|
||||
"multiformats": "^13.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Relay Server
|
||||
|
||||
```json
|
||||
{
|
||||
"helia": "^5.0.0",
|
||||
"helia-coord": "^3.0.0",
|
||||
"@libp2p/circuit-relay-v2": "^3.0.0",
|
||||
"@libp2p/tcp": "^10.0.0",
|
||||
"@libp2p/websockets": "^9.0.0",
|
||||
"@libp2p/webrtc": "^5.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── storage/
|
||||
│ ├── client.ts # Helia client factory
|
||||
│ ├── enclave.ts # Enclave persistence
|
||||
│ ├── did.ts # DID/IPNS operations
|
||||
│ └── types.ts # TypeScript interfaces
|
||||
deploy/
|
||||
├── relay/
|
||||
│ ├── docker-compose.yml
|
||||
│ └── config.env
|
||||
└── nginx/
|
||||
└── relay.conf # WSS proxy config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Helia](https://github.com/ipfs/helia)
|
||||
- [libp2p Circuit Relay v2 Spec](https://github.com/libp2p/specs/blob/master/relay/circuit-v2.md)
|
||||
- [ipfs-service-provider](https://github.com/Permissionless-Software-Foundation/ipfs-service-provider)
|
||||
- [UCAN Spec v1.0.0-rc.1](https://github.com/ucan-wg/spec)
|
||||
86
deploy/nginx/relay.conf
Normal file
86
deploy/nginx/relay.conf
Normal file
@@ -0,0 +1,86 @@
|
||||
limit_req_zone $binary_remote_addr zone=ws_limit:10m rate=10r/s;
|
||||
|
||||
upstream relay_ws {
|
||||
server relay:4003;
|
||||
keepalive 64;
|
||||
}
|
||||
|
||||
upstream relay_api {
|
||||
server relay:5020;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name relay.sonr.org;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name relay.sonr.org;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_tickets off;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
|
||||
location / {
|
||||
limit_req zone=ws_limit burst=20 nodelay;
|
||||
|
||||
proxy_pass http://relay_ws;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_buffering off;
|
||||
proxy_buffer_size 8k;
|
||||
}
|
||||
|
||||
location /ipfs {
|
||||
limit_req zone=ws_limit burst=10 nodelay;
|
||||
|
||||
proxy_pass http://relay_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
|
||||
location /health {
|
||||
proxy_pass http://relay_api/ipfs;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_connect_timeout 5s;
|
||||
proxy_read_timeout 5s;
|
||||
}
|
||||
}
|
||||
26
deploy/relay/Dockerfile
Normal file
26
deploy/relay/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/sonr-io/motr-enclave"
|
||||
LABEL org.opencontainers.image.description="Sonr IPFS Circuit Relay Server"
|
||||
|
||||
RUN apk add --no-cache git python3 make g++
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN git clone --depth 1 https://github.com/Permissionless-Software-Foundation/ipfs-service-provider.git .
|
||||
|
||||
RUN npm ci --only=production
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=5020
|
||||
ENV ENABLE_CIRCUIT_RELAY=1
|
||||
ENV IPFS_TCP_PORT=4001
|
||||
ENV IPFS_WS_PORT=4003
|
||||
ENV DEBUG_LEVEL=1
|
||||
|
||||
EXPOSE 5020 4001 4003
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT}/ipfs || exit 1
|
||||
|
||||
CMD ["npm", "start"]
|
||||
16
deploy/relay/config.env
Normal file
16
deploy/relay/config.env
Normal file
@@ -0,0 +1,16 @@
|
||||
NODE_ENV=production
|
||||
|
||||
ENABLE_CIRCUIT_RELAY=1
|
||||
CR_DOMAIN=relay.sonr.org
|
||||
|
||||
PORT=5020
|
||||
API_PORT=5020
|
||||
IPFS_TCP_PORT=4001
|
||||
IPFS_WS_PORT=4003
|
||||
|
||||
COORD_NAME=sonr-relay-1
|
||||
DEBUG_LEVEL=1
|
||||
|
||||
MAX_RESERVATIONS=100
|
||||
RESERVATION_DURATION=120000
|
||||
RESERVATION_DATA_LIMIT=1048576
|
||||
53
deploy/relay/docker-compose.yml
Normal file
53
deploy/relay/docker-compose.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
relay:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: sonr-relay
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- config.env
|
||||
ports:
|
||||
- "${IPFS_TCP_PORT:-4001}:4001"
|
||||
- "${IPFS_TCP_PORT:-4001}:4001/udp"
|
||||
- "${IPFS_WS_PORT:-4003}:4003"
|
||||
- "${API_PORT:-5020}:5020"
|
||||
volumes:
|
||||
- relay_data:/app/.ipfs
|
||||
networks:
|
||||
- sonr-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:5020/ipfs"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: sonr-relay-proxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ../nginx/relay.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./certs:/etc/nginx/certs:ro
|
||||
depends_on:
|
||||
- relay
|
||||
networks:
|
||||
- sonr-network
|
||||
|
||||
volumes:
|
||||
relay_data:
|
||||
|
||||
networks:
|
||||
sonr-network:
|
||||
driver: bridge
|
||||
Reference in New Issue
Block a user