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