chore: update CI workflow, golangci-lint config, and code quality

- Update CI to trigger PRs only on main branch and pushes on all branches - Modernize golangci-lint to v2 with default linters for better maintenance - Fix typos, add error handling for stream closes, and translate comments to English in RelayClient

lemon-mint committed Nov 5, 2025 at 09:27 UTC 51a8090df9300459fffa5317da3cd531c3031e2f
12 files changed +308 -736
.github/workflows/ci.yml
+101 -99
@@ -2,119 +2,121 @@ name: CI
2
3 on:
4 pull_request:
5 - branches: [main, master]
5 + branches: [ main ]
6 + push:
7 + branches: "*"
8
9 jobs:
10 test:
11 name: Test
12 runs-on: ubuntu-latest
11 -
13 +
14 steps:
13 - - name: Checkout code
14 - uses: actions/checkout@v5
15 -
16 - - name: Set up Go
17 - uses: actions/setup-go@v6
18 - with:
19 - go-version: "stable"
20 - check-latest: true
21 -
22 - - name: Cache Go modules
23 - uses: actions/cache@v3
24 - with:
25 - path: |
26 - ~/.cache/go-build
27 - ~/go/pkg/mod
28 - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
29 - restore-keys: |
30 - ${{ runner.os }}-go-
31 -
32 - - name: Download dependencies
33 - run: go mod download
34 -
35 - - name: Install protobuf compiler
36 - run: |
37 - sudo apt-get update
38 - sudo apt-get install -y protobuf-compiler
39 -
40 - - name: Install protoc-gen-go and protoc-gen-go-vtproto
41 - run: |
42 - go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
43 - go install github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto@latest
44 -
45 - - name: Generate protobuf files
46 - run: make build-protoc
47 -
48 - - name: Run tests
49 - run: go test -v -race -coverprofile=coverage.out ./...
50 -
51 - - name: Upload coverage to Codecov
52 - uses: codecov/codecov-action@v3
53 - with:
54 - file: ./coverage.out
55 - flags: unittests
56 - name: codecov-umbrella
57 -
15 + - name: Checkout code
16 + uses: actions/checkout@v5
17 +
18 + - name: Set up Go
19 + uses: actions/setup-go@v6
20 + with:
21 + go-version: "stable"
22 + check-latest: true
23 +
24 + - name: Cache Go modules
25 + uses: actions/cache@v3
26 + with:
27 + path: |
28 + ~/.cache/go-build
29 + ~/go/pkg/mod
30 + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
31 + restore-keys: |
32 + ${{ runner.os }}-go-
33 +
34 + - name: Download dependencies
35 + run: go mod download
36 +
37 + - name: Install protobuf compiler
38 + run: |
39 + sudo apt-get update
40 + sudo apt-get install -y protobuf-compiler
41 +
42 + - name: Install protoc-gen-go and protoc-gen-go-vtproto
43 + run: |
44 + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
45 + go install github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto@latest
46 +
47 + - name: Generate protobuf files
48 + run: make build-protoc
49 +
50 + - name: Run tests
51 + run: go test -v -race -coverprofile=coverage.out ./...
52 +
53 + - name: Upload coverage to Codecov
54 + uses: codecov/codecov-action@v3
55 + with:
56 + file: ./coverage.out
57 + flags: unittests
58 + name: codecov-umbrella
59 +
60 build:
61 name: Build
62 runs-on: ubuntu-latest
63 needs: test
62 -
64 +
65 steps:
64 - - name: Checkout code
65 - uses: actions/checkout@v5
66 -
67 - - name: Set up Go
68 - uses: actions/setup-go@v6
69 - with:
70 - go-version: "stable"
71 - check-latest: true
72 -
73 - - name: Cache Go modules
74 - uses: actions/cache@v3
75 - with:
76 - path: |
77 - ~/.cache/go-build
78 - ~/go/pkg/mod
79 - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
80 - restore-keys: |
81 - ${{ runner.os }}-go-
82 -
83 - - name: Download dependencies
84 - run: go mod download
85 -
86 - - name: Install build dependencies
87 - run: |
88 - sudo apt-get update
89 - sudo apt-get install -y protobuf-compiler binaryen
90 -
91 - - name: Install protoc-gen-go and protoc-gen-go-vtproto
92 - run: |
93 - go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
94 - go install github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto@latest
95 -
96 - - name: Build all components
97 - run: make build
98 -
99 - - name: Build Docker image
100 - run: |
101 - docker build -t relaydns:test .
102 -
66 + - name: Checkout code
67 + uses: actions/checkout@v5
68 +
69 + - name: Set up Go
70 + uses: actions/setup-go@v6
71 + with:
72 + go-version: "stable"
73 + check-latest: true
74 +
75 + - name: Cache Go modules
76 + uses: actions/cache@v3
77 + with:
78 + path: |
79 + ~/.cache/go-build
80 + ~/go/pkg/mod
81 + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
82 + restore-keys: |
83 + ${{ runner.os }}-go-
84 +
85 + - name: Download dependencies
86 + run: go mod download
87 +
88 + - name: Install build dependencies
89 + run: |
90 + sudo apt-get update
91 + sudo apt-get install -y protobuf-compiler binaryen
92 +
93 + - name: Install protoc-gen-go and protoc-gen-go-vtproto
94 + run: |
95 + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
96 + go install github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto@latest
97 +
98 + - name: Build all components
99 + run: make build
100 +
101 + - name: Build Docker image
102 + run: |
103 + docker build -t relaydns:test .
104 +
105 lint:
106 name: Lint
107 runs-on: ubuntu-latest
106 -
108 +
109 steps:
108 - - name: Checkout code
109 - uses: actions/checkout@v5
110 -
111 - - name: Set up Go
112 - uses: actions/setup-go@v6
113 - with:
114 - go-version: "stable"
115 - check-latest: true
116 -
117 - - name: golangci-lint
110 + - name: Checkout code
111 + uses: actions/checkout@v5
112 +
113 + - name: Set up Go
114 + uses: actions/setup-go@v6
115 + with:
116 + go-version: "stable"
117 + check-latest: true
118 +
119 + - name: golangci-lint
120 uses: golangci/golangci-lint-action@v8
121 with:
120 - version: v2.6.0
122 + version: v2.6.0
\ No newline at end of file
.golangci.yml
+14 -85
@@ -1,88 +1,17 @@
1 -run:
2 - timeout: 5m
3 - modules-download-mode: readonly
4 -
5 -linters-settings:
6 - govet:
7 - check-shadowing: true
8 - golint:
9 - min-confidence: 0
10 - gocyclo:
11 - min-complexity: 15
12 - maligned:
13 - suggest-new: true
14 - dupl:
15 - threshold: 100
16 - goconst:
17 - min-len: 2
18 - min-occurrences: 2
19 - misspell:
20 - locale: US
21 - lll:
22 - line-length: 140
23 - goimports:
24 - local-prefixes: gosuda.org/portal
25 - gocritic:
26 - enabled-tags:
27 - - diagnostic
28 - - experimental
29 - - opinionated
30 - - performance
31 - - style
32 - disabled-checks:
33 - - dupImport # https://github.com/go-critic/go-critic/issues/845
34 - - ifElseChain
35 - - octalLiteral
36 - - whyNoLint
37 - - wrapperFunc
1 +version: "2"
2
3 linters:
40 - disable-all: true
41 - enable:
42 - - bodyclose
43 - - deadcode
44 - - depguard
45 - - dogsled
46 - - dupl
47 - - errcheck
48 - - funlen
49 - - gochecknoinits
50 - - goconst
51 - - gocritic
52 - - gocyclo
53 - - gofmt
54 - - goimports
55 - - golint
56 - - gomnd
57 - - goprintffuncname
58 - - gosec
59 - - gosimple
60 - - govet
61 - - ineffassign
62 - - interfacer
63 - - lll
64 - - misspell
65 - - nakedret
66 - - rowserrcheck
67 - - scopelint
68 - - staticcheck
69 - - structcheck
70 - - stylecheck
71 - - typecheck
72 - - unconvert
73 - - unparam
74 - - unused
75 - - varcheck
76 - - whitespace
4 + # Default set of linters.
5 + # The value can be:
6 + # - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
7 + # - `all`: enables all linters by default.
8 + # - `none`: disables all linters by default.
9 + # - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
10 + # Default: standard
11 + default: standard
12
78 -issues:
79 - exclude-rules:
80 - - path: _test\.go
81 - linters:
82 - - gomnd
83 - - funlen
84 - - goconst
85 - - gocyclo
86 - - path: cmd/
87 - linters:
88 - - gochecknoinits
\ No newline at end of file
13 + exclusions:
14 + rules:
15 + - linters:
16 + - errcheck
17 + source: "^\\s*defer\\s+"
README.md deleted
-472
@@ -1,472 +0,0 @@
1 -# PORTAL — Public Open Relay To Access Localhost
2 -<p align="center">
3 - <img src="portal.jpg" alt="Portal logo" width="540" />
4 -</p>
5 -
6 -Portal is a secure, encrypted relay service that enables end-to-end encrypted communication between clients through a central relay server. It provides mutual authentication, forward secrecy, and secure connection management with cryptographic identity verification.
7 -
8 -## Table of Contents
9 -
10 -- [Overview](#overview)
11 -- [Features](#features)
12 -- [Architecture](#architecture)
13 -- [Security](#security)
14 -- [Installation](#installation)
15 -- [Usage](#usage)
16 -- [API Reference](#api-reference)
17 -- [Protocol Specification](#protocol-specification)
18 -- [Development](#development)
19 -- [Contributing](#contributing)
20 -- [License](#license)
21 -
22 -## Overview
23 -
24 -Portal implements a secure relay protocol that allows clients to register leases and establish encrypted connections through a central server. The system uses modern cryptographic primitives to ensure:
25 -
26 -- **End-to-end encryption**: All communication is encrypted using ChaCha20-Poly1305 AEAD
27 -- **Mutual authentication**: Ed25519 signatures verify client identities
28 -- **Forward secrecy**: Ephemeral X25519 key exchange per connection
29 -- **Secure relay**: The relay server cannot decrypt client communications
30 -
31 -## Features
32 -
33 -- 🔐 **End-to-End Encryption**: Client-to-client communication is fully encrypted
34 -- 🔑 **Cryptographic Identity**: Ed25519-based identity system with verifiable signatures
35 -- 🔄 **Connection Relay**: Secure connection forwarding through central server
36 -- ⏰ **Lease Management**: Time-based lease system with automatic cleanup
37 -- 🌐 **Protocol Support**: Application-Layer Protocol Negotiation (ALPN)
38 -- 🚀 **High Performance**: Multiplexed connections using yamux
39 -- 🐳 **Docker Support**: Containerized deployment ready
40 -- 🌍 **Browser E2EE Proxy**: WASM-based Service Worker for automatic browser encryption
41 -- 📱 **Multi-Platform**: Go SDK for servers, WASM SDK for browsers
42 -
43 -## Architecture
44 -
45 -### System Architecture
46 -
47 -```mermaid
48 -graph TB
49 - subgraph "Client A"
50 - CA[Client A]
51 - CA --> CA_ID[Identity: Ed25519]
52 - CA --> CA_LEASE[Lease Manager]
53 - end
54 -
55 - subgraph "Client B"
56 - CB[Client B]
57 - CB --> CB_ID[Identity: Ed25519]
58 - CB --> CB_LEASE[Lease Manager]
59 - end
60 -
61 - subgraph "Relay Server"
62 - RS[Relay Server]
63 - RS --> RS_ID[Server Identity]
64 - RS --> LM[Lease Manager]
65 - RS --> CM[Connection Manager]
66 - RS --> FH[Forwarding Handler]
67 - end
68 -
69 - CA -.->|1. Register Lease| RS
70 - CB -.->|2. Register Lease| RS
71 - CB -.->|3. Request Connection| RS
72 - RS -.->|4. Forward Request| CA
73 - CA -.->|5. Accept Connection| RS
74 - RS -.->|6. Establish E2EE| CB
75 -
76 - CA <-->|7. Encrypted Data| CB
77 -```
78 -
79 -### Component Architecture
80 -
81 -```mermaid
82 -graph LR
83 - subgraph "Client Components"
84 - C[RelayClient]
85 - C --> H[Handshaker]
86 - C --> LM[LeaseManager]
87 - C --> SC[SecureConnection]
88 - end
89 -
90 - subgraph "Server Components"
91 - S[RelayServer]
92 - S --> LH[LeaseHandler]
93 - S --> CH[ConnectionHandler]
94 - S --> FH[ForwardingHandler]
95 - S --> LM2[LeaseManager]
96 - end
97 -
98 - subgraph "Crypto Operations"
99 - CO[CryptoOps]
100 - CO --> CRED[Credential]
101 - CO --> SIG[Signature]
102 - CO --> E2EE[End-to-End Encryption]
103 - end
104 -
105 - C <-->|Protocol Messages| S
106 - H --> CO
107 - SC --> CO
108 - LH --> LM2
109 - CH --> FH
110 -```
111 -
112 -### Connection Flow
113 -
114 -```mermaid
115 -sequenceDiagram
116 - participant C1 as Client 1
117 - participant RS as Relay Server
118 - participant C2 as Client 2
119 -
120 - Note over C1,C2: Lease Registration Phase
121 - C1->>RS: Register Lease (Identity, ALPN)
122 - RS->>C1: Lease Confirmation
123 -
124 - C2->>RS: Register Lease (Identity, ALPN)
125 - RS->>C2: Lease Confirmation
126 -
127 - Note over C1,C2: Connection Establishment Phase
128 - C2->>RS: Request Connection (to Client 1)
129 - RS->>C1: Forward Connection Request
130 - C1->>RS: Accept Connection
131 - RS->>C2: Connection Accepted
132 -
133 - Note over C1,C2: Secure Handshake Phase
134 - C2->>C1: X25519 Handshake (via relay)
135 - C1->>C2: X25519 Response (via relay)
136 -
137 - Note over C1,C2: End-to-End Encrypted Communication
138 - C2->>C1: Encrypted Data (ChaCha20-Poly1305)
139 - C1->>C2: Encrypted Data (ChaCha20-Poly1305)
140 -```
141 -
142 -### Cryptographic Handshake Flow
143 -
144 -```mermaid
145 -sequenceDiagram
146 - participant C as Client
147 - participant S as Server
148 -
149 - Note over C,S: Phase 1: Client Init
150 - C->>C: Generate X25519 Ephemeral Key
151 - C->>C: Create ClientInitPayload
152 - C->>C: Sign with Ed25519 Private Key
153 - C->>S: Signed ClientInitPayload
154 -
155 - Note over C,S: Phase 2: Server Response
156 - S->>S: Validate Client Signature
157 - S->>S: Generate X25519 Ephemeral Key
158 - S->>S: Create ServerInitPayload
159 - S->>S: Sign with Ed25519 Private Key
160 - S->>C: Signed ServerInitPayload
161 -
162 - Note over C,S: Phase 3: Key Derivation
163 - C->>C: Derive Shared Secret (X25519)
164 - C->>C: Derive Directional Keys (HKDF-SHA256)
165 - S->>S: Derive Shared Secret (X25519)
166 - S->>S: Derive Directional Keys (HKDF-SHA256)
167 -
168 - Note over C,S: Phase 4: Secure Communication
169 - C->>S: Encrypted Message (ChaCha20-Poly1305)
170 - S->>C: Encrypted Message (ChaCha20-Poly1305)
171 -```
172 -
173 -## Security
174 -
175 -### Cryptographic Primitives
176 -
177 -- **Ed25519**: Digital signatures for identity verification
178 -- **X25519**: Ephemeral key exchange for forward secrecy
179 -- **ChaCha20-Poly1305**: Authenticated encryption for data confidentiality
180 -- **HKDF-SHA256**: Key derivation for session keys
181 -- **HMAC-SHA256**: Identity derivation from public keys
182 -
183 -### Identity Derivation
184 -
185 -Each peer's identity ID is derived from their Ed25519 public key using a deterministic process:
186 -
187 -```go
188 -func DeriveID(publickey ed25519.PublicKey) string {
189 - // HMAC-SHA256 with protocol-specific key
190 - h := hmac.New(sha256.New, []byte("RDVERB_PROTOCOL_VER_01_SHA256_ID"))
191 - h.Write(publickey) // 32-byte Ed25519 public key
192 - hash := h.Sum(nil) // 32-byte SHA256 output
193 -
194 - // Take first 128 bits and encode with Base32 (no padding)
195 - encoding := base32.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567").WithPadding(base32.NoPadding)
196 - return encoding.EncodeToString(hash[:16])
197 -}
198 -```
199 -
200 -**Properties:**
201 -- **Deterministic**: Same public key always produces same ID
202 -- **Collision-resistant**: 128-bit security against birthday attacks
203 -- **Protocol-bound**: HMAC key prevents cross-protocol ID reuse
204 -- **Human-readable**: Base32 encoding produces 26-character alphanumeric IDs
205 -- **Compact**: Fixed-length IDs enable efficient storage and routing
206 -
207 -### Security Properties
208 -
209 -- **Mutual Authentication**: Both parties verify each other's identities
210 -- **Forward Secrecy**: Compromise of long-term keys doesn't compromise past sessions
211 -- **Replay Protection**: Timestamps and random nonces prevent replay attacks
212 -- **Integrity**: AEAD authentication tags prevent tampering
213 -- **Confidentiality**: End-to-end encryption prevents relay server access
214 -
215 -### Threat Mitigation
216 -
217 -- **Man-in-the-Middle**: Prevented by Ed25519 signature verification
218 -- **Replay Attacks**: Mitigated by timestamp validation and unique nonces
219 -- **Downgrade Attacks**: Protocol version validation prevents downgrade
220 -- **Denial of Service**: Packet size limits and silent failure on invalid handshakes
221 -
222 -## Installation
223 -
224 -### Prerequisites
225 -
226 -- Go 1.25.3 or later
227 -- Docker (for containerized deployment)
228 -
229 -### Build from Source
230 -
231 -```bash
232 -# Clone the repository
233 -git clone https://gosuda.org/portal.git
234 -cd portal
235 -
236 -# Build WASM SDK (includes E2EE Proxy Service Worker)
237 -make build-wasm
238 -
239 -# Build relay server (embeds WASM files)
240 -make build-server
241 -
242 -# Run relay server
243 -./bin/relayserver
244 -```
245 -
246 -### Docker Deployment
247 -
248 -```bash
249 -# Build with Docker (multi-stage build)
250 -docker build -t portal-server .
251 -
252 -# Run server
253 -docker run -p 4017:4017 portal-server
254 -
255 -# Access:
256 -# - Admin UI: http://localhost:4017/
257 -```
258 -
259 -See [DOCKER_BUILD_VERIFICATION.md](DOCKER_BUILD_VERIFICATION.md) for detailed build verification steps.
260 -
261 -## Usage
262 -
263 -### Browser E2EE Proxy (Automatic)
264 -
265 -The simplest way to use Portal is through the browser E2EE Proxy:
266 -
267 -```javascript
268 -// 1. Open the E2EE Proxy test page
269 -
270 -// 2. Service Worker automatically registers and intercepts ALL fetch() requests
271 -
272 -// 3. All your requests are now E2EE encrypted!
273 -fetch('https://api.github.com/zen')
274 - .then(r => r.text())
275 - .then(console.log);
276 -// ↑ Automatically encrypted via E2EE tunnel through relay server
277 -```
278 -
279 -The Service Worker intercepts requests and automatically determines message types based on Content-Type:
280 -- `application/json` → Text/API type
281 -- `multipart/form-data` → File type (chunked streaming)
282 -- `application/octet-stream` → Binary type
283 -- `text/*` → Text type
284 -
285 -See [E2EE_PROXY_DEPLOYMENT.md](E2EE_PROXY_DEPLOYMENT.md) for deployment guide and [portal/wasm/](portal/wasm/) for WASM SDK documentation.
286 -
287 -### WASM SDK (JavaScript/Browser)
288 -
289 -For direct WASM usage without Service Worker:
290 -
291 -```javascript
292 -import init, { RelayClient } from '/pkg/portal_wasm.js';
293 -
294 -// Initialize WASM
295 -await init();
296 -
297 -// Connect to relay server
298 -const client = await RelayClient.connect('ws://localhost:4017/relay');
299 -
300 -// Register a service
301 -await client.registerLease('my-service', ['http/1.1', 'h2']);
302 -
303 -// Get server info
304 -const info = await client.getRelayInfo();
305 -console.log('Active leases:', info.leases);
306 -```
307 -
308 -See [portal/wasm/USAGE.md](portal/wasm/USAGE.md) for complete WASM SDK documentation.
309 -
310 -### Server Setup
311 -
312 -```bash
313 -# Run the relay server
314 -cd cmd/relay-server
315 -./relay-server
316 -
317 -# Server endpoints:
318 -# - Admin UI: http://localhost:4017/
319 -# - WebSocket relay: ws://localhost:4017/relay
320 -# - WASM SDK files: http://localhost:4017/pkg/
321 -# - Service Worker: http://localhost:4017/sw-proxy.js
322 -```
323 -
324 -### Go SDK (Client Usage)
325 -
326 -```go
327 -package main
328 -
329 -import (
330 - "gosuda.org/portal/sdk"
331 -)
332 -
333 -func main() {
334 - // Create client
335 - client, err := sdk.NewClient(func(c *sdk.RDClientConfig) {
336 - c.BootstrapServers = []string{"ws://localhost:4017/relay"}
337 - })
338 - if err != nil {
339 - panic(err)
340 - }
341 -
342 - // Create credential
343 - cred := sdk.NewCredential()
344 -
345 - // Dial through relay
346 - conn, err := client.Dial(cred, "target-lease-id", "http/1.1")
347 - if err != nil {
348 - panic(err)
349 - }
350 -
351 - // Use conn as net.Conn
352 - conn.Write([]byte("GET / HTTP/1.1\r\n\r\n"))
353 -}
354 -```
355 -
356 -## API Reference
357 -
358 -### RelayServer
359 -
360 -#### Methods
361 -
362 -- `NewRelayServer(credential *cryptoops.Credential, address []string) *RelayServer`
363 -- `HandleConnection(conn io.ReadWriteCloser) error`
364 -- `Start()`
365 -- `Stop()`
366 -
367 -### RelayClient
368 -
369 -#### Methods
370 -
371 -- `NewRelayClient(conn io.ReadWriteCloser) *RelayClient`
372 -- `Close() error`
373 -- `GetRelayInfo(ctx context.Context) (*rdverb.RelayInfo, error)`
374 -- `RegisterLease(cred *cryptoops.Credential, name string, alpns []string) error`
375 -- `DeregisterLease(cred *cryptoops.Credential) error`
376 -- `RequestConnection(leaseID string, alpn string, clientCred *cryptoops.Credential) (rdverb.ResponseCode, io.ReadWriteCloser, error)`
377 -- `IncommingConnection() <-chan *IncommingConn`
378 -
379 -### CryptoOps
380 -
381 -#### Credential Methods
382 -
383 -- `NewCredential() (*Credential, error)`
384 -- `NewCredentialFromPrivateKey(privateKey ed25519.PrivateKey) (*Credential, error)`
385 -- `ID() string`
386 -- `Sign(data []byte) []byte`
387 -- `Verify(data, sig []byte) bool`
388 -- `PublicKey() ed25519.PublicKey`
389 -- `PrivateKey() ed25519.PrivateKey`
390 -
391 -## Protocol Specification
392 -
393 -### Packet Types
394 -
395 -```protobuf
396 -enum PacketType {
397 - PACKET_TYPE_RELAY_INFO_REQUEST = 0;
398 - PACKET_TYPE_RELAY_INFO_RESPONSE = 1;
399 - PACKET_TYPE_LEASE_UPDATE_REQUEST = 2;
400 - PACKET_TYPE_LEASE_UPDATE_RESPONSE = 3;
401 - PACKET_TYPE_LEASE_DELETE_REQUEST = 4;
402 - PACKET_TYPE_LEASE_DELETE_RESPONSE = 5;
403 - PACKET_TYPE_CONNECTION_REQUEST = 6;
404 - PACKET_TYPE_CONNECTION_RESPONSE = 7;
405 -}
406 -```
407 -
408 -### Message Format
409 -
410 -All messages follow a length-prefixed protobuf format:
411 -
412 -```
413 -+-------------------+-------------------+
414 -| Length (4 bytes) | Protobuf Payload |
415 -| Big Endian Uint32 | (variable length) |
416 -+-------------------+-------------------+
417 -```
418 -
419 -### Encrypted Messages
420 -
421 -End-to-end encrypted messages use the following format:
422 -
423 -```
424 -+-------------------+-------------------+-------------------+-------------------+
425 -| Length (4 bytes) | Nonce (12 bytes) | Ciphertext | Tag (16 bytes) |
426 -| Big Endian Uint32 | Random | (variable length) | Poly1305 MAC |
427 -+-------------------+-------------------+-------------------+-------------------+
428 -```
429 -
430 -## Contributing
431 -
432 -1. Fork the repository
433 -2. Create a feature branch (`git checkout -b feature/amazing-feature`)
434 -3. Commit your changes (`git commit -m 'Add amazing feature'`)
435 -4. Push to the branch (`git push origin feature/amazing-feature`)
436 -5. Open a Pull Request
437 -
438 -### Development Guidelines
439 -
440 -- Follow Go best practices and idioms
441 -- Ensure all cryptographic operations use constant-time implementations
442 -- Add comprehensive tests for new features
443 -- Update documentation for API changes
444 -- Use the provided memory pools for sensitive data
445 -
446 -## License
447 -
448 -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
449 -
450 -## Security Considerations
451 -
452 -- **Never** use `math/rand` for cryptographic operations
453 -- **Always** validate timestamps within reasonable bounds
454 -- **Always** verify signatures before trusting identity claims
455 -- **Always** wipe sensitive data from memory after use
456 -- **Never** reuse nonces with the same encryption key
457 -- **Always** use the provided memory pools for sensitive data
458 -
459 -## Performance Considerations
460 -
461 -- Connection multiplexing using yamux for efficient resource usage
462 -- Memory pooling to reduce GC pressure
463 -- Fragmentation for large messages (32MB chunks)
464 -- Efficient buffer management with aligned allocations
465 -- Constant-time cryptographic operations
466 -
467 -## Compatibility
468 -
469 -- **Go**: 1.25.3 or later
470 -- **Protocol**: Version 1 (current)
471 -- **Ciphers**: ChaCha20-Poly1305, X25519, Ed25519
472 -- **Transport**: TCP, WebSocket (via adapter)
cmd/demo-app/main.go
+16 -4
@@ -79,7 +79,10 @@ func (c *Canvas) register(conn *websocket.Conn) {
79
80 // Send history to new client
81 for _, msg := range c.history {
82 - conn.WriteJSON(msg)
82 + err := conn.WriteJSON(msg)
83 + if err != nil {
84 + log.Error().Err(err).Msg("write to client")
85 + }
86 }
87 }
88
@@ -88,7 +91,10 @@ func (c *Canvas) unregister(conn *websocket.Conn) {
91 defer c.mu.Unlock()
92 if _, ok := c.clients[conn]; ok {
93 delete(c.clients, conn)
91 - conn.Close()
94 + err := conn.Close()
95 + if err != nil {
96 + log.Error().Err(err).Msg("close client")
97 + }
98 }
99 }
100
@@ -109,7 +115,10 @@ func (c *Canvas) broadcast(msg DrawMessage) {
115 err := client.WriteJSON(msg)
116 if err != nil {
117 log.Error().Err(err).Msg("write to client")
112 - client.Close()
118 + err = client.Close()
119 + if err != nil {
120 + log.Error().Err(err).Msg("close client")
121 + }
122 delete(c.clients, client)
123 }
124 }
@@ -119,7 +128,10 @@ func (c *Canvas) closeAll() {
128 c.mu.Lock()
129 defer c.mu.Unlock()
130 for client := range c.clients {
122 - client.Close()
131 + err := client.Close()
132 + if err != nil {
133 + log.Error().Err(err).Msg("close client")
134 + }
135 }
136 c.clients = make(map[*websocket.Conn]bool)
137 }
cmd/relay-server/frontend.go
+1 -1
@@ -426,7 +426,7 @@ func isPortalSubdomain(host string) bool {
426 // isHexString checks if a string contains only hexadecimal characters
427 func isHexString(s string) bool {
428 for _, c := range s {
429 - if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
429 + if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
430 return false
431 }
432 }
cmd/vanity-id/main.go
+1 -1
@@ -105,7 +105,7 @@ func worker(prefix string, attempts, found *uint64, results chan<- *Result, wg *
105 }
106
107 // Generate random seed using randpool
108 - randpool.CSPRNG_RAND(seed[:])
108 + randpool.Rand(seed[:])
109
110 // Generate private key from seed (this is 64 bytes: 32 byte seed + 32 byte public key)
111 privateKey := ed25519.NewKeyFromSeed(seed[:])
cmd/webclient/inject.go
+3 -2
@@ -25,9 +25,10 @@ func InjectHTML(body []byte) []byte {
25 var crawler func(*html.Node)
26 crawler = func(node *html.Node) {
27 if node.Type == html.ElementNode {
28 - if node.Data == "head" {
28 + switch node.Data {
29 + case "head":
30 head = node
30 - } else if node.Data == "body" {
31 + case "body":
32 bodyNode = node
33 }
34 }
portal/client.go
+165 -59
@@ -1,3 +1,6 @@
1 +// Package portal provides client-side functionality for establishing and managing
2 +// relay connections. It handles secure communication channels, lease management,
3 +// and connection multiplexing through the relay server.
4 package portal
5
6 import (
@@ -15,49 +18,81 @@ import (
18 )
19
20 var (
18 - ErrInvalidResponse = errors.New("invalid response")
21 + // ErrInvalidResponse is returned when the relay server sends an unexpected or malformed response
22 + ErrInvalidResponse = errors.New("invalid response")
23 + // ErrConnectionRejected is returned when the relay server rejects a connection request
24 ErrConnectionRejected = errors.New("connection rejected")
20 - ErrRemoteIDMismatch = errors.New("remote ID mismatch")
25 + // ErrRemoteIDMismatch is returned when the remote peer's ID doesn't match the expected lease ID
26 + ErrRemoteIDMismatch = errors.New("remote ID mismatch")
27 )
28
23 -type IncommingConn struct {
29 +// IncomingConn represents an incoming connection from a remote client.
30 +// It wraps a secure connection with the associated lease ID that was used
31 +// for the connection request.
32 +type IncomingConn struct {
33 *cryptoops.SecureConnection
34 leaseID string
35 }
36
28 -func (i *IncommingConn) LeaseID() string {
37 +// LeaseID returns the lease ID associated with this incoming connection
38 +func (i *IncomingConn) LeaseID() string {
39 return i.leaseID
40 }
41
32 -func (i *IncommingConn) LocalID() string {
42 +// LocalID returns the local identity ID from the secure connection
43 +func (i *IncomingConn) LocalID() string {
44 return i.SecureConnection.LocalID()
45 }
46
36 -func (i *IncommingConn) RemoteID() string {
47 +// RemoteID returns the remote peer's identity ID from the secure connection
48 +func (i *IncomingConn) RemoteID() string {
49 return i.SecureConnection.RemoteID()
50 }
51
52 +// RelayClient manages a connection to a relay server and handles:
53 +// - Lease registration and renewal
54 +// - Incoming connection requests
55 +// - Secure connection establishment
56 +//
57 +// The client uses yamux for connection multiplexing, allowing multiple
58 +// concurrent streams over a single underlying connection.
59 +//
60 +// Thread-safety: All public methods are safe for concurrent use.
61 type RelayClient struct {
62 conn io.ReadWriteCloser
63
64 + // sess is the yamux session for multiplexing streams
65 sess *yamux.Session
66
67 + // leases maps lease IDs to their credentials for handling incoming connections
68 leases map[string]*leaseWithCred
69 leasesMu sync.Mutex
70
71 + // stopClientCh signals background workers to shut down
72 stopClientCh chan struct{}
73 stopOnce sync.Once // Ensure stopClientCh is closed only once
74 waitGroup sync.WaitGroup
75
52 - incommingConnCh chan *IncommingConn
76 + // incomingConnCh delivers incoming connections to the application
77 + incomingConnCh chan *IncomingConn
78 }
79
80 +// leaseWithCred pairs a lease with its associated credentials.
81 +// This is used internally to verify and sign messages for lease operations.
82 type leaseWithCred struct {
83 Lease *rdverb.Lease
84 Cred *cryptoops.Credential
85 }
86
60 -// NewRelayClient는 새로운 RelayClient 인스턴스를 생성합니다.
87 +// NewRelayClient creates a new relay client from an established connection.
88 +// It initializes the yamux session for stream multiplexing and starts background
89 +// workers for lease renewal and incoming connection handling.
90 +//
91 +// The client starts two goroutines:
92 +// - leaseUpdateWorker: Periodically renews leases before they expire
93 +// - leaseListenWorker: Accepts and handles incoming connection requests
94 +//
95 +// Returns nil if yamux session creation fails.
96 func NewRelayClient(conn io.ReadWriteCloser) *RelayClient {
97 log.Debug().Msg("[RelayClient] Creating new relay client")
98
@@ -68,18 +103,21 @@ func NewRelayClient(conn io.ReadWriteCloser) *RelayClient {
103 if err != nil {
104 log.Error().Err(err).Msg("[RelayClient] Failed to create yamux session")
105 // If session creation fails, close the connection and return nil
71 - conn.Close()
106 + err = conn.Close()
107 + if err != nil {
108 + log.Error().Err(err).Msg("[RelayClient] Failed to close connection")
109 + }
110 return nil
111 }
112
113 log.Debug().Msg("[RelayClient] Yamux session created successfully")
114
115 g := &RelayClient{
78 - conn: conn,
79 - sess: sess,
80 - leases: make(map[string]*leaseWithCred),
81 - stopClientCh: make(chan struct{}),
82 - incommingConnCh: make(chan *IncommingConn),
116 + conn: conn,
117 + sess: sess,
118 + leases: make(map[string]*leaseWithCred),
119 + stopClientCh: make(chan struct{}),
120 + incomingConnCh: make(chan *IncomingConn),
121 }
122
123 g.waitGroup.Add(2) // One for leaseUpdateWorker, one for leaseListenWorker
@@ -90,11 +128,16 @@ func NewRelayClient(conn io.ReadWriteCloser) *RelayClient {
128 return g
129 }
130
131 +// Ping sends a ping to the relay server and measures the round-trip latency.
132 +// It uses yamux's built-in ping mechanism.
133 func (g *RelayClient) Ping() (time.Duration, error) {
134 return g.sess.Ping()
135 }
136
97 -// Close는 서버와의 연결을 종료합니다.
137 +// Close gracefully shuts down the relay client.
138 +// It signals all background workers to stop, waits for them to finish,
139 +// then closes the yamux session and underlying connection.
140 +// This method is safe to call multiple times.
141 func (g *RelayClient) Close() error {
142 log.Debug().Msg("[RelayClient] Closing relay client")
143
@@ -131,7 +174,9 @@ func (g *RelayClient) Close() error {
174 return nil
175 }
176
134 -// leaseUpdateWorker는 리스 업데이트를 처리하는 워커입니다.
177 +// leaseUpdateWorker is a background goroutine that periodically renews leases
178 +// before they expire. It checks every 5 seconds and renews any lease that
179 +// will expire within the next 30 seconds.
180 func (g *RelayClient) leaseUpdateWorker() {
181 defer g.waitGroup.Done()
182
@@ -144,10 +189,12 @@ func (g *RelayClient) leaseUpdateWorker() {
189 case <-g.stopClientCh:
190 return
191 case <-ticker.C:
192 + // Clear the map for the next update cycle
193 clear(updateRequired)
194
195 g.leasesMu.Lock()
196 for _, lease := range g.leases {
197 + // Check if lease expires within 30 seconds
198 if lease.Lease.Expires < int64(time.Now().Add(30*time.Second).Unix()) {
199 updateRequired[lease] = struct{}{}
200 }
@@ -158,16 +205,22 @@ func (g *RelayClient) leaseUpdateWorker() {
205 lease.Lease.Expires = time.Now().Add(30 * time.Second).Unix()
206 // Check if session is available before updating lease
207 if g.sess != nil {
161 - g.updateLease(lease.Cred, lease.Lease)
208 + _, err := g.updateLease(lease.Cred, lease.Lease)
209 + if err != nil {
210 + log.Error().Err(err).Msg("[RelayClient] Failed to update lease")
211 + }
212 }
213 }
214 }
215 }
216 }
217
218 +// leaseListenWorker is a background goroutine that accepts incoming connection
219 +// requests from the relay server. It blocks on AcceptStream() and spawns a
220 +// new goroutine to handle each connection request.
221 func (g *RelayClient) leaseListenWorker() {
222 defer g.waitGroup.Done()
170 - defer close(g.incommingConnCh)
223 + defer close(g.incomingConnCh)
224 log.Debug().Msg("[RelayClient] Lease listen worker started")
225
226 for {
@@ -200,19 +253,32 @@ func (g *RelayClient) leaseListenWorker() {
253 }
254 }
255
256 +// handleConnectionRequestStream processes an incoming connection request from a client.
257 +// It performs the following steps:
258 +// 1. Reads and validates the connection request packet
259 +// 2. Looks up the requested lease ID
260 +// 3. Sends an accept/reject response
261 +// 4. If accepted, performs server-side cryptographic handshake
262 +// 5. Sends the established secure connection to the incoming channel
263 func (g *RelayClient) handleConnectionRequestStream(stream *yamux.Stream) {
264 log.Debug().Uint32("stream_id", stream.StreamID()).Msg("[RelayClient] Handling connection request stream")
265
266 pkt, err := readPacket(stream)
267 if err != nil {
268 log.Error().Uint32("stream_id", stream.StreamID()).Err(err).Msg("[RelayClient] Failed to read packet from stream")
209 - stream.Close()
269 + err = stream.Close()
270 + if err != nil {
271 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
272 + }
273 return
274 }
275
276 if pkt.Type != rdverb.PacketType_PACKET_TYPE_CONNECTION_REQUEST {
277 log.Warn().Str("packet_type", pkt.Type.String()).Msg("[RelayClient] Unexpected packet type")
215 - stream.Close()
278 + err = stream.Close()
279 + if err != nil {
280 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
281 + }
282 return
283 }
284
@@ -220,7 +286,10 @@ func (g *RelayClient) handleConnectionRequestStream(stream *yamux.Stream) {
286 err = req.UnmarshalVT(pkt.Payload)
287 if err != nil {
288 log.Error().Err(err).Msg("[RelayClient] Failed to unmarshal connection request")
223 - stream.Close()
289 + err = stream.Close()
290 + if err != nil {
291 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
292 + }
293 return
294 }
295
@@ -242,7 +311,10 @@ func (g *RelayClient) handleConnectionRequestStream(stream *yamux.Stream) {
311 respPayload, err := resp.MarshalVT()
312 if err != nil {
313 log.Error().Err(err).Msg("[RelayClient] Failed to marshal response")
245 - stream.Close()
314 + err = stream.Close()
315 + if err != nil {
316 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
317 + }
318 return
319 }
320
@@ -252,12 +324,18 @@ func (g *RelayClient) handleConnectionRequestStream(stream *yamux.Stream) {
324 })
325 if err != nil {
326 log.Error().Err(err).Msg("[RelayClient] Failed to write response packet")
255 - stream.Close()
327 + err = stream.Close()
328 + if err != nil {
329 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
330 + }
331 return
332 }
333
334 if !ok {
260 - stream.Close()
335 + err = stream.Close()
336 + if err != nil {
337 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
338 + }
339 return
340 }
341
@@ -266,7 +344,10 @@ func (g *RelayClient) handleConnectionRequestStream(stream *yamux.Stream) {
344 secConn, err := handshaker.ServerHandshake(stream, lease.Lease.Alpn)
345 if err != nil {
346 log.Error().Err(err).Str("lease_id", req.LeaseId).Msg("[RelayClient] Server handshake failed")
269 - stream.Close()
347 + err = stream.Close()
348 + if err != nil {
349 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
350 + }
351 return
352 }
353
@@ -276,29 +357,27 @@ func (g *RelayClient) handleConnectionRequestStream(stream *yamux.Stream) {
357 Str("remote_id", secConn.RemoteID()).
358 Msg("[RelayClient] Secure connection established, sending to incoming channel")
359
279 - g.incommingConnCh <- &IncommingConn{
360 + g.incomingConnCh <- &IncomingConn{
361 SecureConnection: secConn,
362 leaseID: req.LeaseId,
363 }
364 }
365
285 -// GetRelayInfo는 서버의 릴레이 정보를 요청합니다.
366 +// GetRelayInfo requests relay server information including supported protocols,
367 +// server version, and other metadata.
368 func (g *RelayClient) GetRelayInfo() (*rdverb.RelayInfo, error) {
287 - // 새 스트림 열기
369 stream, err := g.sess.OpenStream()
370 if err != nil {
371 return nil, err
372 }
373 defer stream.Close()
374
294 - // 요청 패킷 생성
375 req := &rdverb.RelayInfoRequest{}
376 reqPayload, err := req.MarshalVT()
377 if err != nil {
378 return nil, err
379 }
380
301 - // 요청 전송
381 err = writePacket(stream, &rdverb.Packet{
382 Type: rdverb.PacketType_PACKET_TYPE_RELAY_INFO_REQUEST,
383 Payload: reqPayload,
@@ -307,7 +386,6 @@ func (g *RelayClient) GetRelayInfo() (*rdverb.RelayInfo, error) {
386 return nil, err
387 }
388
310 - // 응답 수신
389 respPacket, err := readPacket(stream)
390 if err != nil {
391 return nil, err
@@ -326,18 +404,18 @@ func (g *RelayClient) GetRelayInfo() (*rdverb.RelayInfo, error) {
404 return resp.RelayInfo, nil
405 }
406
329 -// updateLease는 서버에 리스 업데이트를 요청합니다.
407 +// updateLease sends a lease update request to the relay server.
408 +// The request is signed with the provided credentials to prove ownership.
409 +// A nonce and timestamp are included to prevent replay attacks.
410 func (g *RelayClient) updateLease(cred *cryptoops.Credential, lease *rdverb.Lease) (rdverb.ResponseCode, error) {
331 - // 새 스트림 열기
411 stream, err := g.sess.OpenStream()
412 if err != nil {
413 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, err
414 }
415 defer stream.Close()
416
338 - // 요청 생성
417 timestamp := time.Now().Unix()
340 - nonce := make([]byte, 12) // 12바이트 nonce
418 + nonce := make([]byte, 12) // 12-byte nonce for replay protection
419 if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
420 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, err
421 }
@@ -348,7 +426,6 @@ func (g *RelayClient) updateLease(cred *cryptoops.Credential, lease *rdverb.Leas
426 Timestamp: timestamp,
427 }
428
351 - // 요청 직렬화 및 서명
429 reqPayload, err := req.MarshalVT()
430 if err != nil {
431 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, err
@@ -364,7 +441,6 @@ func (g *RelayClient) updateLease(cred *cryptoops.Credential, lease *rdverb.Leas
441 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, err
442 }
443
367 - // 요청 전송
444 err = writePacket(stream, &rdverb.Packet{
445 Type: rdverb.PacketType_PACKET_TYPE_LEASE_UPDATE_REQUEST,
446 Payload: signedData,
@@ -373,7 +449,6 @@ func (g *RelayClient) updateLease(cred *cryptoops.Credential, lease *rdverb.Leas
449 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, err
450 }
451
376 - // 응답 수신
452 respPacket, err := readPacket(stream)
453 if err != nil {
454 log.Error().Uint32("stream_id", stream.StreamID()).Err(err).Msg("[RelayClient] Failed to read packet from stream")
@@ -395,18 +470,17 @@ func (g *RelayClient) updateLease(cred *cryptoops.Credential, lease *rdverb.Leas
470 return resp.Code, nil
471 }
472
398 -// deleteLease는 서버에 리스 삭제를 요청합니다.
473 +// deleteLease sends a lease deletion request to the relay server.
474 +// The request is signed with the provided credentials to prove ownership.
475 func (g *RelayClient) deleteLease(cred *cryptoops.Credential, identity *rdsec.Identity) (rdverb.ResponseCode, error) {
400 - // 새 스트림 열기
476 stream, err := g.sess.OpenStream()
477 if err != nil {
478 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, err
479 }
480 defer stream.Close()
481
407 - // 요청 생성
482 timestamp := time.Now().Unix()
409 - nonce := make([]byte, 12) // 12바이트 nonce
483 + nonce := make([]byte, 12)
484 if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
485 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, err
486 }
@@ -417,7 +491,6 @@ func (g *RelayClient) deleteLease(cred *cryptoops.Credential, identity *rdsec.Id
491 Timestamp: timestamp,
492 }
493
420 - // 요청 직렬화 및 서명
494 reqPayload, err := req.MarshalVT()
495 if err != nil {
496 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, err
@@ -433,7 +506,7 @@ func (g *RelayClient) deleteLease(cred *cryptoops.Credential, identity *rdsec.Id
506 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, err
507 }
508
436 - // 요청 전송
509 + // Send deletion request
510 err = writePacket(stream, &rdverb.Packet{
511 Type: rdverb.PacketType_PACKET_TYPE_LEASE_DELETE_REQUEST,
512 Payload: signedData,
@@ -442,7 +515,7 @@ func (g *RelayClient) deleteLease(cred *cryptoops.Credential, identity *rdsec.Id
515 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, err
516 }
517
445 - // 응답 수신
518 + // Receive deletion response
519 respPacket, err := readPacket(stream)
520 if err != nil {
521 log.Error().Uint32("stream_id", stream.StreamID()).Err(err).Msg("[RelayClient] Failed to read packet from stream")
@@ -462,11 +535,23 @@ func (g *RelayClient) deleteLease(cred *cryptoops.Credential, identity *rdsec.Id
535 return resp.Code, nil
536 }
537
465 -// requestConnection은 다른 클라이언트로의 연결을 요청합니다.
538 +// RequestConnection initiates a connection to a remote peer through the relay.
539 +// It performs the following steps:
540 +// 1. Opens a new yamux stream to the relay server
541 +// 2. Sends a connection request for the specified lease ID
542 +// 3. Waits for accept/reject response from the remote peer
543 +// 4. If accepted, performs client-side cryptographic handshake
544 +// 5. Verifies the remote peer's ID matches the lease ID
545 +//
546 +// Parameters:
547 +// - leaseID: The ID of the lease to connect to
548 +// - alpn: Application-Layer Protocol Negotiation string
549 +// - clientCred: Client's cryptographic credentials for the handshake
550 +//
551 +// Returns the response code, established secure connection (if successful), and any error.
552 func (g *RelayClient) RequestConnection(leaseID string, alpn string, clientCred *cryptoops.Credential) (rdverb.ResponseCode, *cryptoops.SecureConnection, error) {
553 log.Debug().Str("lease_id", leaseID).Str("alpn", alpn).Msg("[RelayClient] Requesting connection")
554
469 - // 새 스트림 열기
555 stream, err := g.sess.OpenStream()
556 if err != nil {
557 log.Error().Err(err).Msg("[RelayClient] Failed to open stream for connection request")
@@ -478,7 +563,6 @@ func (g *RelayClient) RequestConnection(leaseID string, alpn string, clientCred
563 PublicKey: clientCred.PublicKey(),
564 }
565
481 - // 요청 생성
566 req := &rdverb.ConnectionRequest{
567 LeaseId: leaseID,
568 ClientIdentity: clientIdentity,
@@ -487,11 +571,13 @@ func (g *RelayClient) RequestConnection(leaseID string, alpn string, clientCred
571 reqPayload, err := req.MarshalVT()
572 if err != nil {
573 log.Error().Err(err).Msg("[RelayClient] Failed to marshal connection request")
490 - stream.Close()
574 + err = stream.Close()
575 + if err != nil {
576 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
577 + }
578 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, nil, err
579 }
580
494 - // 요청 전송
581 log.Debug().Str("lease_id", leaseID).Msg("[RelayClient] Sending connection request")
582 err = writePacket(stream, &rdverb.Packet{
583 Type: rdverb.PacketType_PACKET_TYPE_CONNECTION_REQUEST,
@@ -499,22 +585,30 @@ func (g *RelayClient) RequestConnection(leaseID string, alpn string, clientCred
585 })
586 if err != nil {
587 log.Error().Err(err).Msg("[RelayClient] Failed to write connection request packet")
502 - stream.Close()
588 + err = stream.Close()
589 + if err != nil {
590 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
591 + }
592 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, nil, err
593 }
594
506 - // 응답 수신
595 log.Debug().Str("lease_id", leaseID).Msg("[RelayClient] Waiting for connection response")
596 respPacket, err := readPacket(stream)
597 if err != nil {
598 log.Error().Str("lease_id", leaseID).Err(err).Msg("[RelayClient] Failed to read connection response")
511 - stream.Close()
599 + err = stream.Close()
600 + if err != nil {
601 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
602 + }
603 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, nil, err
604 }
605
606 if respPacket.Type != rdverb.PacketType_PACKET_TYPE_CONNECTION_RESPONSE {
607 log.Warn().Str("packet_type", respPacket.Type.String()).Msg("[RelayClient] Unexpected response packet type")
517 - stream.Close()
608 + err = stream.Close()
609 + if err != nil {
610 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
611 + }
612 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, nil, ErrInvalidResponse
613 }
614
@@ -522,7 +616,10 @@ func (g *RelayClient) RequestConnection(leaseID string, alpn string, clientCred
616 err = resp.UnmarshalVT(respPacket.Payload)
617 if err != nil {
618 log.Error().Str("lease_id", leaseID).Err(err).Msg("[RelayClient] Failed to unmarshal connection response")
525 - stream.Close()
619 + err = stream.Close()
620 + if err != nil {
621 + log.Error().Err(err).Msg("[RelayClient] Failed to close stream")
622 + }
623 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, nil, err
624 }
625
@@ -531,7 +628,6 @@ func (g *RelayClient) RequestConnection(leaseID string, alpn string, clientCred
628 Str("response_code", resp.Code.String()).
629 Msg("[RelayClient] Connection response received")
630
534 - // 거절된 경우 스트림을 닫고 오류 코드 반환
631 if resp.Code != rdverb.ResponseCode_RESPONSE_CODE_ACCEPTED {
632 log.Warn().Str("lease_id", leaseID).Str("code", resp.Code.String()).Msg("[RelayClient] Connection rejected")
633 stream.Close()
@@ -547,6 +643,7 @@ func (g *RelayClient) RequestConnection(leaseID string, alpn string, clientCred
643 return rdverb.ResponseCode_RESPONSE_CODE_UNKNOWN, nil, err
644 }
645
646 + // Verify the remote peer's ID matches the expected lease ID
647 if secConn.RemoteID() != leaseID {
648 log.Warn().Str("lease_id", leaseID).Msg("[RelayClient] Remote ID mismatch")
649 stream.Close()
@@ -562,8 +659,13 @@ func (g *RelayClient) RequestConnection(leaseID string, alpn string, clientCred
659 return resp.Code, secConn, nil
660 }
661
662 +// RegisterLease registers a new lease with the relay server.
663 +// The lease allows remote clients to connect to this client via the relay.
664 +//
665 +// The lease is cloned to avoid modifying the caller's original lease object.
666 +// On registration failure, the lease is automatically removed from the local cache.
667 func (g *RelayClient) RegisterLease(cred *cryptoops.Credential, lease *rdverb.Lease) error {
566 - lease = lease.CloneVT() // copy lease to avoid modifying the original lease
668 + lease = lease.CloneVT() // Clone to avoid modifying the original lease
669
670 identity := &rdsec.Identity{
671 Id: cred.ID(),
@@ -602,6 +704,8 @@ func (g *RelayClient) RegisterLease(cred *cryptoops.Credential, lease *rdverb.Le
704 return nil
705 }
706
707 +// DeregisterLease removes a lease from the relay server.
708 +// It removes the lease from the local cache immediately, then notifies the server.
709 func (g *RelayClient) DeregisterLease(cred *cryptoops.Credential) error {
710 identity := &rdsec.Identity{
711 Id: cred.ID(),
@@ -622,6 +726,8 @@ func (g *RelayClient) DeregisterLease(cred *cryptoops.Credential) error {
726 return nil
727 }
728
625 -func (g *RelayClient) IncommingConnection() <-chan *IncommingConn {
626 - return g.incommingConnCh
729 +// IncomingConnection returns a receive-only channel for incoming connections.
730 +// The channel is closed when the relay client is shut down.
731 +func (g *RelayClient) IncomingConnection() <-chan *IncomingConn {
732 + return g.incomingConnCh
733 }
portal/core/cryptoops/handshaker.go
+1 -1
@@ -167,7 +167,7 @@ func (sc *SecureConnection) writeFragmentation(p []byte) (int, error) {
167
168 binary.BigEndian.PutUint32(buffer.B[:4], uint32(cipherSize))
169
170 - randpool.CSPRNG_RAND(buffer.B[4 : 4+sc.encryptor.NonceSize()])
170 + randpool.Rand(buffer.B[4 : 4+sc.encryptor.NonceSize()])
171
172 sc.encryptor.Seal(
173 buffer.B[4+sc.encryptor.NonceSize():][:0], // len(0), cap(len(p)+Overhead)
portal/relay.go
+1 -7
@@ -132,7 +132,7 @@ func (g *RelayServer) handleStream(stream *yamux.Stream, id int64, connection *C
132 Uint32("stream_id", stream.StreamID()).
133 Msg("[RelayServer] Handling stream")
134
135 - var hijacked bool = false
135 + var hijacked bool
136 defer func() {
137 stream_id := stream.StreamID()
138 if !hijacked {
@@ -241,12 +241,6 @@ func (g *RelayServer) HandleConnection(conn io.ReadWriteCloser) error {
241 }
242
243 func (g *RelayServer) relayInfo() *rdverb.RelayInfo {
244 - leases := g.leaseManager.GetAllLeases()
245 - var leaseIds []string
246 - for _, lease := range leases {
247 - leaseIds = append(leaseIds, string(lease.Identity.Id))
248 - }
249 -
244 return &rdverb.RelayInfo{
245 Identity: g.identity,
246 Address: g.address,
portal/utils/randpool/randpool.go
+4 -4
@@ -27,7 +27,7 @@ type chacha20rng struct {
27 used uint64
28 }
29
30 -var _chacha20rngPool sync.Pool = sync.Pool{
30 +var _chacha20rngPool = sync.Pool{
31 New: func() interface{} {
32 var initdata [12 + 32]byte // 12 byte nonce, 32 byte key
33 _, err := rand.Read(initdata[:])
@@ -50,7 +50,7 @@ func _chacha20rng() *chacha20rng {
50 return _chacha20rngPool.Get().(*chacha20rng)
51 }
52
53 -func _CHACHA20_RAND(dst []byte) {
53 +func chacha20rand(dst []byte) {
54 c := _chacha20rng()
55 c.used += uint64(len(dst))
56 c.c.XORKeyStream(dst, dst)
@@ -60,6 +60,6 @@ func _CHACHA20_RAND(dst []byte) {
60 }
61 }
62
63 -func CSPRNG_RAND(dst []byte) {
64 - _CHACHA20_RAND(dst)
63 +func Rand(dst []byte) {
64 + chacha20rand(dst)
65 }
sdk/sdk.go
+1 -1
@@ -400,7 +400,7 @@ func (g *RDClient) listenerWorker(server *rdRelay) {
400 case <-server.stop:
401 log.Debug().Str("relay", server.addr).Msg("[SDK] Listener worker stopped")
402 return
403 - case conn, ok := <-server.client.IncommingConnection():
403 + case conn, ok := <-server.client.IncomingConnection():
404 if !ok {
405 log.Debug().Str("relay", server.addr).Msg("[SDK] Incoming connection channel closed")
406 return // Channel closed