fix: wasm for websocket communication.

Hee Sung Son committed Oct 29, 2025 at 14:30 UTC 237a75bf44ad199c0ac3c2329d5a03f2ac7b0882
15 files changed +2773 -185
Makefile
+3
@@ -16,6 +16,9 @@ build-wasm:
16 @echo "[wasm] copying service workers and E2EE proxy files..."
17 cp relaydns/wasm/sw-proxy.js cmd/relay-server/wasm/
18 cp relaydns/wasm/sw.js cmd/relay-server/wasm/
19 + @echo "[wasm] copying SecureWebSocket E2EE files..."
20 + cp relaydns/wasm/secure-websocket.js cmd/relay-server/wasm/
21 + cp relaydns/wasm/secure-websocket-sw.js cmd/relay-server/wasm/
22
23 # Build Go relay server (embeds WASM from cmd/relay-server/wasm)
24 build-server:
cmd/relay-server/view.go
+26 -1
@@ -193,7 +193,32 @@ func serveHTTP(ctx context.Context, addr string, serv *relaydns.RelayServer, nod
193 }
194 })
195
196 - // Note: removed /api/leases; admin renders server-side for simplicity
196 + // API: Get relay server info for E2EE WebSocket
197 + mux.HandleFunc("/api/relay-info", func(w http.ResponseWriter, r *http.Request) {
198 + // Build relay WebSocket URL from current request
199 + scheme := "ws"
200 + if r.TLS != nil {
201 + scheme = "wss"
202 + }
203 + // Check X-Forwarded-Proto header (for reverse proxy)
204 + if proto := r.Header.Get("X-Forwarded-Proto"); proto == "https" {
205 + scheme = "wss"
206 + }
207 +
208 + relayUrl := fmt.Sprintf("%s://%s/relay", scheme, r.Host)
209 +
210 + type relayInfo struct {
211 + RelayUrl string `json:"relayUrl"`
212 + NodeID string `json:"nodeId"`
213 + }
214 + resp := relayInfo{
215 + RelayUrl: relayUrl,
216 + NodeID: nodeID,
217 + }
218 + w.Header().Set("Content-Type", "application/json")
219 + w.Header().Set("Access-Control-Allow-Origin", "*")
220 + _ = json.NewEncoder(w).Encode(resp)
221 + })
222
223 mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
224 type info struct {
cmd/relay-server/wasm/relaydns_wasm.d.ts
+2 -2
@@ -183,10 +183,10 @@ export interface InitOutput {
183 readonly relayclient_getCredentialId: (a: number) => [number, number];
184 readonly relayclient_requestConnection: (a: number, b: number, c: number, d: number, e: number) => any;
185 readonly init: () => void;
186 - readonly wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621: (a: number, b: number, c: any) => void;
187 - readonly wasm_bindgen__closure__destroy__h8eb17c158a55b496: (a: number, b: number) => void;
186 readonly wasm_bindgen__convert__closures_____invoke__h75a085a52d492ff4: (a: number, b: number, c: any) => void;
187 readonly wasm_bindgen__closure__destroy__h2dcad6e62f01cec1: (a: number, b: number) => void;
188 + readonly wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621: (a: number, b: number, c: any) => void;
189 + readonly wasm_bindgen__closure__destroy__h8eb17c158a55b496: (a: number, b: number) => void;
190 readonly wasm_bindgen__convert__closures_____invoke__h404cda7fa36c69a6: (a: number, b: number, c: any, d: any) => void;
191 readonly __wbindgen_malloc: (a: number, b: number) => number;
192 readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
cmd/relay-server/wasm/relaydns_wasm.js
+4 -4
@@ -243,14 +243,14 @@ export function init() {
243 wasm.init();
244 }
245
246 -function wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621(arg0, arg1, arg2) {
247 - wasm.wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621(arg0, arg1, arg2);
248 -}
249 -
246 function wasm_bindgen__convert__closures_____invoke__h75a085a52d492ff4(arg0, arg1, arg2) {
247 wasm.wasm_bindgen__convert__closures_____invoke__h75a085a52d492ff4(arg0, arg1, arg2);
248 }
249
250 +function wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621(arg0, arg1, arg2) {
251 + wasm.wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621(arg0, arg1, arg2);
252 +}
253 +
254 function wasm_bindgen__convert__closures_____invoke__h404cda7fa36c69a6(arg0, arg1, arg2, arg3) {
255 wasm.wasm_bindgen__convert__closures_____invoke__h404cda7fa36c69a6(arg0, arg1, arg2, arg3);
256 }
cmd/relay-server/wasm/relaydns_wasm_bg.wasm
Binary files a/cmd/relay-server/wasm/relaydns_wasm_bg.wasm and b/cmd/relay-server/wasm/relaydns_wasm_bg.wasm differ
cmd/relay-server/wasm/relaydns_wasm_bg.wasm.d.ts new
+50
@@ -0,0 +1,50 @@
1 +/* tslint:disable */
2 +/* eslint-disable */
3 +export const memory: WebAssembly.Memory;
4 +export const __wbg_httpadapter_free: (a: number, b: number) => void;
5 +export const httpadapter_new: (a: number, b: number) => number;
6 +export const httpadapter_get: (a: number, b: number, c: number) => any;
7 +export const httpadapter_postJson: (a: number, b: number, c: number, d: any) => any;
8 +export const httpadapter_uploadFile: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => any;
9 +export const httpadapter_downloadFile: (a: number, b: number, c: number) => any;
10 +export const __wbg_websocketadapter_free: (a: number, b: number) => void;
11 +export const websocketadapter_new: (a: number, b: number) => number;
12 +export const websocketadapter_connect: (a: number) => any;
13 +export const websocketadapter_onMessage: (a: number, b: any) => void;
14 +export const websocketadapter_onError: (a: number, b: any) => void;
15 +export const websocketadapter_sendText: (a: number, b: number, c: number) => [number, number];
16 +export const websocketadapter_sendBinary: (a: number, b: number, c: number) => [number, number];
17 +export const websocketadapter_close: (a: number) => [number, number];
18 +export const __wbg_datainterpreter_free: (a: number, b: number) => void;
19 +export const datainterpreter_parsePacket: (a: number, b: number) => [number, number, number];
20 +export const datainterpreter_createPacket: (a: any) => [number, number, number, number];
21 +export const __wbg_proxyengine_free: (a: number, b: number) => void;
22 +export const proxyengine_new: (a: number, b: number) => number;
23 +export const proxyengine_shouldIntercept: (a: number, b: number, c: number) => number;
24 +export const proxyengine_handleHttpRequest: (a: number, b: number, c: number, d: number, e: number, f: any, g: number, h: number) => any;
25 +export const proxyengine_openWebSocket: (a: number, b: number, c: number, d: number, e: number) => any;
26 +export const proxyengine_sendWebSocketMessage: (a: number, b: number, c: number, d: any, e: number) => any;
27 +export const proxyengine_receiveWebSocketMessage: (a: number, b: number, c: number) => any;
28 +export const proxyengine_closeWebSocket: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
29 +export const proxyengine_connectTcp: (a: number, b: number, c: number, d: number) => any;
30 +export const proxyengine_getStatus: (a: number) => any;
31 +export const __wbg_relayclient_free: (a: number, b: number) => void;
32 +export const relayclient_connect: (a: number, b: number) => any;
33 +export const relayclient_getRelayInfo: (a: number) => any;
34 +export const relayclient_registerLease: (a: number, b: number, c: number, d: number, e: number) => any;
35 +export const relayclient_getCredentialId: (a: number) => [number, number];
36 +export const relayclient_requestConnection: (a: number, b: number, c: number, d: number, e: number) => any;
37 +export const init: () => void;
38 +export const wasm_bindgen__convert__closures_____invoke__h75a085a52d492ff4: (a: number, b: number, c: any) => void;
39 +export const wasm_bindgen__closure__destroy__h2dcad6e62f01cec1: (a: number, b: number) => void;
40 +export const wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621: (a: number, b: number, c: any) => void;
41 +export const wasm_bindgen__closure__destroy__h8eb17c158a55b496: (a: number, b: number) => void;
42 +export const wasm_bindgen__convert__closures_____invoke__h404cda7fa36c69a6: (a: number, b: number, c: any, d: any) => void;
43 +export const __wbindgen_malloc: (a: number, b: number) => number;
44 +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
45 +export const __wbindgen_exn_store: (a: number) => void;
46 +export const __externref_table_alloc: () => number;
47 +export const __wbindgen_externrefs: WebAssembly.Table;
48 +export const __wbindgen_free: (a: number, b: number, c: number) => void;
49 +export const __externref_table_dealloc: (a: number) => void;
50 +export const __wbindgen_start: () => void;
cmd/relay-server/wasm/secure-websocket-sw.js new
+283
@@ -0,0 +1,283 @@
1 +/**
2 + * SecureWebSocket for Service Worker
3 + *
4 + * SecureWebSocket implementation for use in Service Worker
5 + * Communicates with main thread via MessageChannel
6 + */
7 +
8 +// Service Worker global ProxyEngine (initialized in sw-proxy.js)
9 +// proxyEngine and wasmReady are provided by sw-proxy.js
10 +
11 +/**
12 + * WebSocket tunnel manager in Service Worker
13 + */
14 +class ServiceWorkerWebSocketTunnel {
15 + constructor() {
16 + this.tunnels = new Map(); // tunnelId -> tunnel info
17 + this.messageQueues = new Map(); // tunnelId -> message queue
18 + this.clients = new Map(); // tunnelId -> clientId
19 + }
20 +
21 + /**
22 + * Create WebSocket tunnel
23 + */
24 + async createTunnel(url, protocols, clientId) {
25 + if (!wasmReady || !proxyEngine) {
26 + throw new Error('WASM ProxyEngine not ready');
27 + }
28 +
29 + console.log('[SW-WebSocket] Creating tunnel:', url);
30 +
31 + try {
32 + // Open WebSocket tunnel via WASM ProxyEngine
33 + const result = await proxyEngine.open_websocket(url, protocols || []);
34 + const tunnelId = result.tunnelId;
35 + const protocol = result.protocol || '';
36 +
37 + console.log('[SW-WebSocket] Tunnel created:', tunnelId);
38 +
39 + // Store tunnel information
40 + this.tunnels.set(tunnelId, {
41 + tunnelId,
42 + url,
43 + protocol,
44 + state: 'open',
45 + created: Date.now()
46 + });
47 +
48 + this.messageQueues.set(tunnelId, []);
49 + this.clients.set(tunnelId, clientId);
50 +
51 + // Start receiving messages in background
52 + this._startReceiving(tunnelId);
53 +
54 + return {
55 + tunnelId,
56 + protocol
57 + };
58 +
59 + } catch (error) {
60 + console.error('[SW-WebSocket] Failed to create tunnel:', error);
61 + throw error;
62 + }
63 + }
64 +
65 + /**
66 + * Background message receiving loop
67 + */
68 + async _startReceiving(tunnelId) {
69 + console.log('[SW-WebSocket] Starting receive loop:', tunnelId);
70 +
71 + try {
72 + while (this.tunnels.has(tunnelId)) {
73 + const tunnel = this.tunnels.get(tunnelId);
74 + if (!tunnel || tunnel.state !== 'open') {
75 + break;
76 + }
77 +
78 + // Receive message from WASM
79 + const msg = await proxyEngine.receive_websocket_message(tunnelId);
80 +
81 + console.log('[SW-WebSocket] Received message:', msg.type);
82 +
83 + // Add to message queue
84 + const queue = this.messageQueues.get(tunnelId);
85 + if (queue) {
86 + queue.push(msg);
87 + }
88 +
89 + // Notify client
90 + this._notifyClient(tunnelId, msg);
91 +
92 + // Handle close message
93 + if (msg.type === 'close') {
94 + console.log('[SW-WebSocket] Tunnel closed:', tunnelId);
95 + this._closeTunnel(tunnelId);
96 + break;
97 + }
98 + }
99 +
100 + } catch (error) {
101 + console.error('[SW-WebSocket] Receive loop error:', error);
102 + this._closeTunnel(tunnelId, 1006, error.toString());
103 + }
104 + }
105 +
106 + /**
107 + * Notify client of message
108 + */
109 + async _notifyClient(tunnelId, message) {
110 + const clientId = this.clients.get(tunnelId);
111 + if (!clientId) return;
112 +
113 + try {
114 + const client = await self.clients.get(clientId);
115 + if (client) {
116 + client.postMessage({
117 + type: 'WEBSOCKET_MESSAGE',
118 + tunnelId,
119 + message
120 + });
121 + }
122 + } catch (error) {
123 + console.error('[SW-WebSocket] Failed to notify client:', error);
124 + }
125 + }
126 +
127 + /**
128 + * Send message
129 + */
130 + async sendMessage(tunnelId, data, isBinary) {
131 + if (!this.tunnels.has(tunnelId)) {
132 + throw new Error('Tunnel not found: ' + tunnelId);
133 + }
134 +
135 + console.log('[SW-WebSocket] Sending message:', tunnelId, isBinary ? 'binary' : 'text');
136 +
137 + try {
138 + await proxyEngine.send_websocket_message(tunnelId, data, isBinary);
139 + } catch (error) {
140 + console.error('[SW-WebSocket] Send failed:', error);
141 + throw error;
142 + }
143 + }
144 +
145 + /**
146 + * Close tunnel
147 + */
148 + async closeTunnel(tunnelId, code = 1000, reason = '') {
149 + if (!this.tunnels.has(tunnelId)) {
150 + return;
151 + }
152 +
153 + console.log('[SW-WebSocket] Closing tunnel:', tunnelId, code, reason);
154 +
155 + try {
156 + await proxyEngine.close_websocket(tunnelId, code, reason);
157 + } catch (error) {
158 + console.error('[SW-WebSocket] Close failed:', error);
159 + }
160 +
161 + this._closeTunnel(tunnelId);
162 + }
163 +
164 + /**
165 + * Internal tunnel cleanup
166 + */
167 + _closeTunnel(tunnelId, code = 1000, reason = '') {
168 + const tunnel = this.tunnels.get(tunnelId);
169 + if (tunnel) {
170 + tunnel.state = 'closed';
171 + }
172 +
173 + // Cleanup
174 + this.tunnels.delete(tunnelId);
175 + this.messageQueues.delete(tunnelId);
176 + this.clients.delete(tunnelId);
177 +
178 + console.log('[SW-WebSocket] Tunnel cleaned up:', tunnelId);
179 + }
180 +
181 + /**
182 + * Get tunnel state
183 + */
184 + getTunnelState(tunnelId) {
185 + const tunnel = this.tunnels.get(tunnelId);
186 + return tunnel ? tunnel.state : 'closed';
187 + }
188 +
189 + /**
190 + * Get all tunnel information
191 + */
192 + getAllTunnels() {
193 + return Array.from(this.tunnels.values());
194 + }
195 +}
196 +
197 +// Global tunnel manager instance
198 +let tunnelManager = null;
199 +
200 +/**
201 + * Initialize tunnel manager
202 + */
203 +function initTunnelManager() {
204 + if (!tunnelManager) {
205 + tunnelManager = new ServiceWorkerWebSocketTunnel();
206 + console.log('[SW-WebSocket] Tunnel manager initialized');
207 + }
208 + return tunnelManager;
209 +}
210 +
211 +/**
212 + * WebSocket message handler to add to Service Worker
213 + */
214 +async function handleWebSocketMessage(event) {
215 + const { type, tunnelId, url, protocols, data, isBinary, code, reason } = event.data || {};
216 + const manager = initTunnelManager();
217 +
218 + switch (type) {
219 + case 'WEBSOCKET_OPEN':
220 + try {
221 + const clientId = event.source?.id || event.clientId;
222 + const result = await manager.createTunnel(url, protocols, clientId);
223 +
224 + event.ports[0]?.postMessage({
225 + success: true,
226 + result
227 + });
228 + } catch (error) {
229 + event.ports[0]?.postMessage({
230 + success: false,
231 + error: error.toString()
232 + });
233 + }
234 + break;
235 +
236 + case 'WEBSOCKET_SEND':
237 + try {
238 + await manager.sendMessage(tunnelId, data, isBinary);
239 + event.ports[0]?.postMessage({ success: true });
240 + } catch (error) {
241 + event.ports[0]?.postMessage({
242 + success: false,
243 + error: error.toString()
244 + });
245 + }
246 + break;
247 +
248 + case 'WEBSOCKET_CLOSE':
249 + try {
250 + await manager.closeTunnel(tunnelId, code, reason);
251 + event.ports[0]?.postMessage({ success: true });
252 + } catch (error) {
253 + event.ports[0]?.postMessage({
254 + success: false,
255 + error: error.toString()
256 + });
257 + }
258 + break;
259 +
260 + case 'WEBSOCKET_STATE':
261 + const state = manager.getTunnelState(tunnelId);
262 + event.ports[0]?.postMessage({
263 + success: true,
264 + state
265 + });
266 + break;
267 +
268 + case 'WEBSOCKET_LIST':
269 + const tunnels = manager.getAllTunnels();
270 + event.ports[0]?.postMessage({
271 + success: true,
272 + tunnels
273 + });
274 + break;
275 +
276 + default:
277 + return false; // Not handled
278 + }
279 +
280 + return true; // Handled
281 +}
282 +
283 +console.log('[SW-WebSocket] Service Worker WebSocket module loaded');
cmd/relay-server/wasm/secure-websocket.js new
+710
@@ -0,0 +1,710 @@
1 +/**
2 + * SecureWebSocket - E2EE WebSocket Polyfill using WASM ProxyEngine
3 + *
4 + * Provides transparent end-to-end encryption for WebSocket connections
5 + * through the RelayDNS WASM ProxyEngine.
6 + */
7 +
8 +// Global WASM instance cache
9 +let wasmInstance = null;
10 +let wasmInitPromise = null;
11 +
12 +/**
13 + * Get Relay server URL from server or config
14 + * @returns {Promise<string>} Relay server WebSocket URL
15 + */
16 +async function getRelayUrl() {
17 + // 1. Check if manually configured
18 + if (window.RELAYDNS_RELAY_URL) {
19 + console.log('[SecureWebSocket] Using configured relay URL:', window.RELAYDNS_RELAY_URL);
20 + return window.RELAYDNS_RELAY_URL;
21 + }
22 +
23 + // 2. Try to get from server API
24 + try {
25 + const response = await fetch('/api/relay-info');
26 + if (response.ok) {
27 + const data = await response.json();
28 + if (data.relayUrl) {
29 + console.log('[SecureWebSocket] Got relay URL from server:', data.relayUrl);
30 + return data.relayUrl;
31 + }
32 + }
33 + } catch (error) {
34 + console.warn('[SecureWebSocket] Failed to fetch relay info from server:', error.message);
35 + }
36 +
37 + // 3. Auto-detect from current location
38 + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
39 + const host = window.location.host;
40 + const autoUrl = `${protocol}//${host}/relay`;
41 +
42 + console.log('[SecureWebSocket] Auto-detected relay URL:', autoUrl);
43 + return autoUrl;
44 +}
45 +
46 +/**
47 + * Check if Service Worker is available and has WebSocket support
48 + * @returns {Promise<boolean>}
49 + */
50 +async function hasServiceWorkerWebSocket() {
51 + if (!navigator.serviceWorker || !navigator.serviceWorker.controller) {
52 + return false;
53 + }
54 +
55 + try {
56 + const channel = new MessageChannel();
57 + const response = await new Promise((resolve) => {
58 + channel.port1.onmessage = (event) => resolve(event.data);
59 + navigator.serviceWorker.controller.postMessage(
60 + { type: 'GET_STATUS' },
61 + [channel.port2]
62 + );
63 + setTimeout(() => resolve({ success: false }), 1000);
64 + });
65 +
66 + return response.success && response.status?.hasWebSocket;
67 + } catch {
68 + return false;
69 + }
70 +}
71 +
72 +/**
73 + * Initialize and get the WASM ProxyEngine instance
74 + * @returns {Promise<Object>} WASM module with ProxyEngine
75 + */
76 +async function getProxyEngine() {
77 + if (wasmInstance) {
78 + return wasmInstance;
79 + }
80 +
81 + if (wasmInitPromise) {
82 + return wasmInitPromise;
83 + }
84 +
85 + wasmInitPromise = (async () => {
86 + try {
87 + // Check if we should use Service Worker
88 + const useServiceWorker = await hasServiceWorkerWebSocket();
89 +
90 + if (useServiceWorker) {
91 + console.log('[SecureWebSocket] Using Service Worker for WebSocket');
92 + wasmInstance = {
93 + engine: null,
94 + wasm: null,
95 + useServiceWorker: true
96 + };
97 + return wasmInstance;
98 + }
99 +
100 + // Fallback to direct WASM
101 + console.log('[SecureWebSocket] Using direct WASM');
102 +
103 + // Load WASM module
104 + if (typeof wasm_bindgen === 'undefined') {
105 + throw new Error('WASM module not loaded. Include relaydns_wasm.js first.');
106 + }
107 +
108 + // Initialize WASM
109 + await wasm_bindgen('/pkg/relaydns_wasm_bg.wasm');
110 +
111 + // Get relay server URL
112 + const relayUrl = await getRelayUrl();
113 +
114 + // Create ProxyEngine instance
115 + const engine = new wasm_bindgen.ProxyEngine(relayUrl);
116 +
117 + console.log('[SecureWebSocket] WASM ProxyEngine initialized:', relayUrl);
118 +
119 + wasmInstance = {
120 + engine,
121 + wasm: wasm_bindgen,
122 + useServiceWorker: false
123 + };
124 +
125 + return wasmInstance;
126 +
127 + } catch (error) {
128 + console.error('[SecureWebSocket] Failed to initialize WASM:', error);
129 + wasmInitPromise = null;
130 + throw error;
131 + }
132 + })();
133 +
134 + return wasmInitPromise;
135 +}
136 +
137 +/**
138 + * SecureWebSocket - Drop-in replacement for native WebSocket with E2EE
139 + */
140 +class SecureWebSocket extends EventTarget {
141 + /**
142 + * @param {string} url - WebSocket URL
143 + * @param {string|string[]} protocols - Optional subprotocols
144 + */
145 + constructor(url, protocols = []) {
146 + super();
147 +
148 + // Normalize protocols
149 + if (typeof protocols === 'string') {
150 + protocols = [protocols];
151 + }
152 +
153 + // Public properties (read-only)
154 + Object.defineProperties(this, {
155 + url: { value: url, writable: false, enumerable: true },
156 + protocols: { value: protocols, writable: false, enumerable: true },
157 + });
158 +
159 + // Internal state
160 + this._readyState = WebSocket.CONNECTING;
161 + this._protocol = '';
162 + this._tunnelId = null;
163 + this._bufferedAmount = 0;
164 + this._extensions = '';
165 + this._binaryType = 'blob';
166 +
167 + // Event handlers (nullable)
168 + this.onopen = null;
169 + this.onmessage = null;
170 + this.onerror = null;
171 + this.onclose = null;
172 +
173 + // Start connection
174 + this._connect();
175 + }
176 +
177 + // Public properties with getters
178 + get readyState() { return this._readyState; }
179 + get protocol() { return this._protocol; }
180 + get bufferedAmount() { return this._bufferedAmount; }
181 + get extensions() { return this._extensions; }
182 + get binaryType() { return this._binaryType; }
183 + set binaryType(value) {
184 + if (value === 'blob' || value === 'arraybuffer') {
185 + this._binaryType = value;
186 + }
187 + }
188 +
189 + /**
190 + * Initialize connection through WASM ProxyEngine
191 + * @private
192 + */
193 + async _connect() {
194 + try {
195 + console.log('[SecureWebSocket] Connecting to:', this.url);
196 +
197 + // Get WASM ProxyEngine or Service Worker
198 + const instance = await getProxyEngine();
199 +
200 + if (instance.useServiceWorker) {
201 + // Use Service Worker
202 + await this._connectViaServiceWorker();
203 + } else {
204 + // Use direct WASM
205 + await this._connectViaDirect(instance.engine);
206 + }
207 +
208 + } catch (error) {
209 + console.error('[SecureWebSocket] Connection failed:', error);
210 +
211 + this._readyState = WebSocket.CLOSED;
212 +
213 + // Dispatch error event
214 + this._dispatchEvent('error', {
215 + message: error.toString(),
216 + error: error
217 + });
218 +
219 + // Dispatch close event
220 + this._dispatchEvent('close', {
221 + code: 1006,
222 + reason: error.toString(),
223 + wasClean: false
224 + });
225 + }
226 + }
227 +
228 + /**
229 + * Connect via Service Worker
230 + * @private
231 + */
232 + async _connectViaServiceWorker() {
233 + console.log('[SecureWebSocket] Connecting via Service Worker');
234 +
235 + const channel = new MessageChannel();
236 + const response = await new Promise((resolve, reject) => {
237 + channel.port1.onmessage = (event) => {
238 + if (event.data.success) {
239 + resolve(event.data.result);
240 + } else {
241 + reject(new Error(event.data.error));
242 + }
243 + };
244 +
245 + navigator.serviceWorker.controller.postMessage(
246 + {
247 + type: 'WEBSOCKET_OPEN',
248 + url: this.url,
249 + protocols: this.protocols
250 + },
251 + [channel.port2]
252 + );
253 +
254 + setTimeout(() => reject(new Error('Service Worker timeout')), 10000);
255 + });
256 +
257 + this._tunnelId = response.tunnelId;
258 + this._protocol = response.protocol || '';
259 + this._readyState = WebSocket.OPEN;
260 + this._useServiceWorker = true;
261 +
262 + console.log('[SecureWebSocket] Connected via SW! Tunnel ID:', this._tunnelId);
263 +
264 + // Dispatch open event
265 + this._dispatchEvent('open', {});
266 +
267 + // Listen for messages from Service Worker
268 + this._listenToServiceWorker();
269 + }
270 +
271 + /**
272 + * Connect via direct WASM
273 + * @private
274 + */
275 + async _connectViaDirect(engine) {
276 + console.log('[SecureWebSocket] Connecting via direct WASM');
277 +
278 + // Open WebSocket tunnel through E2EE proxy
279 + const result = await engine.open_websocket(this.url, this.protocols);
280 +
281 + this._tunnelId = result.tunnelId;
282 + this._protocol = result.protocol || '';
283 + this._readyState = WebSocket.OPEN;
284 + this._useServiceWorker = false;
285 +
286 + console.log('[SecureWebSocket] Connected! Tunnel ID:', this._tunnelId);
287 +
288 + // Dispatch open event
289 + this._dispatchEvent('open', {});
290 +
291 + // Start receiving messages in background
292 + this._receiveLoop(engine);
293 + }
294 +
295 + /**
296 + * Listen to Service Worker messages
297 + * @private
298 + */
299 + _listenToServiceWorker() {
300 + const handler = (event) => {
301 + if (event.data.type === 'WEBSOCKET_MESSAGE' &&
302 + event.data.tunnelId === this._tunnelId) {
303 +
304 + const msg = event.data.message;
305 + this._handleMessage(msg);
306 + }
307 + };
308 +
309 + navigator.serviceWorker.addEventListener('message', handler);
310 + this._swMessageHandler = handler;
311 + }
312 +
313 + /**
314 + * Handle incoming message
315 + * @private
316 + */
317 + _handleMessage(msg) {
318 + if (msg.type === 'text') {
319 + // Text message
320 + this._dispatchEvent('message', {
321 + data: msg.data,
322 + type: 'message',
323 + origin: this.url
324 + });
325 +
326 + } else if (msg.type === 'binary') {
327 + // Binary message
328 + let data;
329 + if (this._binaryType === 'arraybuffer') {
330 + data = new Uint8Array(msg.data).buffer;
331 + } else {
332 + data = new Blob([new Uint8Array(msg.data)]);
333 + }
334 +
335 + this._dispatchEvent('message', {
336 + data: data,
337 + type: 'message',
338 + origin: this.url
339 + });
340 +
341 + } else if (msg.type === 'close') {
342 + // Close message
343 + console.log('[SecureWebSocket] Received close:', msg.code, msg.reason);
344 +
345 + this._readyState = WebSocket.CLOSED;
346 +
347 + this._dispatchEvent('close', {
348 + code: msg.code || 1000,
349 + reason: msg.reason || '',
350 + wasClean: true
351 + });
352 +
353 + // Cleanup Service Worker listener
354 + if (this._swMessageHandler) {
355 + navigator.serviceWorker.removeEventListener('message', this._swMessageHandler);
356 + }
357 + }
358 + }
359 +
360 + /**
361 + * Background loop to receive messages from tunnel
362 + * @private
363 + * @param {Object} engine - WASM ProxyEngine instance
364 + */
365 + async _receiveLoop(engine) {
366 + try {
367 + while (this._readyState !== WebSocket.CLOSED && this._readyState !== WebSocket.CLOSING) {
368 + // Receive message from tunnel
369 + const msg = await engine.receive_websocket_message(this._tunnelId);
370 +
371 + if (msg.type === 'text') {
372 + // Text message
373 + this._dispatchEvent('message', {
374 + data: msg.data,
375 + type: 'message',
376 + origin: this.url
377 + });
378 +
379 + } else if (msg.type === 'binary') {
380 + // Binary message
381 + let data;
382 + if (this._binaryType === 'arraybuffer') {
383 + data = new Uint8Array(msg.data).buffer;
384 + } else {
385 + // Convert to Blob
386 + data = new Blob([new Uint8Array(msg.data)]);
387 + }
388 +
389 + this._dispatchEvent('message', {
390 + data: data,
391 + type: 'message',
392 + origin: this.url
393 + });
394 +
395 + } else if (msg.type === 'close') {
396 + // Close message
397 + console.log('[SecureWebSocket] Received close:', msg.code, msg.reason);
398 +
399 + this._readyState = WebSocket.CLOSED;
400 +
401 + this._dispatchEvent('close', {
402 + code: msg.code || 1000,
403 + reason: msg.reason || '',
404 + wasClean: true
405 + });
406 +
407 + break;
408 + }
409 + }
410 +
411 + } catch (error) {
412 + console.error('[SecureWebSocket] Receive loop error:', error);
413 +
414 + if (this._readyState !== WebSocket.CLOSED) {
415 + this._dispatchEvent('error', {
416 + message: error.toString(),
417 + error: error
418 + });
419 +
420 + this.close(1006, error.toString());
421 + }
422 + }
423 + }
424 +
425 + /**
426 + * Send data through the secure tunnel
427 + * @param {string|ArrayBuffer|Uint8Array|Blob} data - Data to send
428 + */
429 + send(data) {
430 + if (this._readyState !== WebSocket.OPEN) {
431 + throw new DOMException(
432 + 'Failed to execute \'send\' on \'WebSocket\': Still in CONNECTING state.',
433 + 'InvalidStateError'
434 + );
435 + }
436 +
437 + // Handle different data types
438 + if (typeof data === 'string') {
439 + // Text message
440 + this._sendMessage(data, false);
441 +
442 + } else if (data instanceof ArrayBuffer) {
443 + // Binary ArrayBuffer
444 + this._sendMessage(new Uint8Array(data), true);
445 +
446 + } else if (data instanceof Uint8Array) {
447 + // Binary Uint8Array
448 + this._sendMessage(data, true);
449 +
450 + } else if (data instanceof Blob) {
451 + // Blob - convert to ArrayBuffer
452 + this._bufferedAmount += data.size;
453 +
454 + data.arrayBuffer().then(buffer => {
455 + this._sendMessage(new Uint8Array(buffer), true);
456 + this._bufferedAmount = Math.max(0, this._bufferedAmount - data.size);
457 + });
458 +
459 + } else {
460 + throw new TypeError('Data must be string, ArrayBuffer, Uint8Array, or Blob');
461 + }
462 + }
463 +
464 + /**
465 + * Send message through WASM ProxyEngine
466 + * @private
467 + * @param {string|Uint8Array} data - Data to send
468 + * @param {boolean} isBinary - Whether data is binary
469 + */
470 + async _sendMessage(data, isBinary) {
471 + try {
472 + // Estimate buffer size
473 + const size = typeof data === 'string' ? data.length : data.length;
474 + this._bufferedAmount += size;
475 +
476 + if (this._useServiceWorker) {
477 + // Send via Service Worker
478 + const channel = new MessageChannel();
479 + await new Promise((resolve, reject) => {
480 + channel.port1.onmessage = (event) => {
481 + if (event.data.success) {
482 + resolve();
483 + } else {
484 + reject(new Error(event.data.error));
485 + }
486 + };
487 +
488 + navigator.serviceWorker.controller.postMessage(
489 + {
490 + type: 'WEBSOCKET_SEND',
491 + tunnelId: this._tunnelId,
492 + data: data,
493 + isBinary: isBinary
494 + },
495 + [channel.port2]
496 + );
497 +
498 + setTimeout(() => reject(new Error('Send timeout')), 5000);
499 + });
500 + } else {
501 + // Send via direct WASM
502 + const { engine } = await getProxyEngine();
503 + await engine.send_websocket_message(this._tunnelId, data, isBinary);
504 + }
505 +
506 + // Decrement buffered amount
507 + this._bufferedAmount = Math.max(0, this._bufferedAmount - size);
508 +
509 + } catch (error) {
510 + console.error('[SecureWebSocket] Send failed:', error);
511 +
512 + this._dispatchEvent('error', {
513 + message: error.toString(),
514 + error: error
515 + });
516 + }
517 + }
518 +
519 + /**
520 + * Close the WebSocket connection
521 + * @param {number} code - Close code (default 1000)
522 + * @param {string} reason - Close reason (default empty)
523 + */
524 + close(code = 1000, reason = '') {
525 + if (this._readyState === WebSocket.CLOSED || this._readyState === WebSocket.CLOSING) {
526 + return;
527 + }
528 +
529 + console.log('[SecureWebSocket] Closing:', code, reason);
530 +
531 + this._readyState = WebSocket.CLOSING;
532 +
533 + // Close tunnel
534 + (async () => {
535 + try {
536 + if (this._useServiceWorker) {
537 + // Close via Service Worker
538 + const channel = new MessageChannel();
539 + await new Promise((resolve, reject) => {
540 + channel.port1.onmessage = (event) => {
541 + if (event.data.success) {
542 + resolve();
543 + } else {
544 + reject(new Error(event.data.error));
545 + }
546 + };
547 +
548 + navigator.serviceWorker.controller.postMessage(
549 + {
550 + type: 'WEBSOCKET_CLOSE',
551 + tunnelId: this._tunnelId,
552 + code: code,
553 + reason: reason
554 + },
555 + [channel.port2]
556 + );
557 +
558 + setTimeout(() => resolve(), 2000); // Don't wait forever
559 + });
560 + } else {
561 + // Close via direct WASM
562 + const { engine } = await getProxyEngine();
563 + await engine.close_websocket(this._tunnelId, code, reason);
564 + }
565 + } catch (error) {
566 + console.error('[SecureWebSocket] Close failed:', error);
567 +
568 + // Force close
569 + this._readyState = WebSocket.CLOSED;
570 + this._dispatchEvent('close', {
571 + code: 1006,
572 + reason: error.toString(),
573 + wasClean: false
574 + });
575 + }
576 + })();
577 + }
578 +
579 + /**
580 + * Dispatch event to both EventTarget and legacy handler
581 + * @private
582 + * @param {string} type - Event type
583 + * @param {Object} detail - Event details
584 + */
585 + _dispatchEvent(type, detail) {
586 + // Create event
587 + const event = new Event(type);
588 + Object.assign(event, detail);
589 +
590 + // Dispatch to EventTarget listeners
591 + this.dispatchEvent(event);
592 +
593 + // Call legacy handler if exists
594 + const handler = this[`on${type}`];
595 + if (typeof handler === 'function') {
596 + try {
597 + handler.call(this, event);
598 + } catch (error) {
599 + console.error(`[SecureWebSocket] Error in on${type} handler:`, error);
600 + }
601 + }
602 + }
603 +}
604 +
605 +// Static constants (same as native WebSocket)
606 +SecureWebSocket.CONNECTING = 0;
607 +SecureWebSocket.OPEN = 1;
608 +SecureWebSocket.CLOSING = 2;
609 +SecureWebSocket.CLOSED = 3;
610 +
611 +// ==============================================================================
612 +// POLYFILL: Replace native WebSocket with SecureWebSocket
613 +// ==============================================================================
614 +
615 +(function() {
616 + // Save reference to native WebSocket
617 + const NativeWebSocket = window.WebSocket;
618 +
619 + // Configuration
620 + const config = {
621 + // Enable E2EE for all WebSockets by default
622 + enabled: window.RELAYDNS_E2EE_ENABLED !== false,
623 +
624 + // Patterns to intercept (regex strings)
625 + interceptPatterns: window.RELAYDNS_INTERCEPT_PATTERNS || [
626 + '.*' // Intercept all by default
627 + ],
628 +
629 + // Patterns to bypass (regex strings) - takes precedence
630 + bypassPatterns: window.RELAYDNS_BYPASS_PATTERNS || [
631 + '^wss?://localhost:4017/', // Don't intercept relay server itself
632 + '^wss?://localhost:8000/', // Don't intercept local dev server
633 + '^wss?://127\\.0\\.0\\.1', // Don't intercept loopback
634 + ],
635 +
636 + // Debug mode
637 + debug: window.RELAYDNS_DEBUG || false
638 + };
639 +
640 + /**
641 + * Check if URL should be intercepted for E2EE
642 + * @param {string} url - WebSocket URL
643 + * @returns {boolean} True if should intercept
644 + */
645 + function shouldIntercept(url) {
646 + if (!config.enabled) {
647 + return false;
648 + }
649 +
650 + // Check bypass patterns first (higher priority)
651 + for (const pattern of config.bypassPatterns) {
652 + const regex = new RegExp(pattern);
653 + if (regex.test(url)) {
654 + if (config.debug) {
655 + console.log('[SecureWebSocket] Bypassing (matched bypass pattern):', url);
656 + }
657 + return false;
658 + }
659 + }
660 +
661 + // Check intercept patterns
662 + for (const pattern of config.interceptPatterns) {
663 + const regex = new RegExp(pattern);
664 + if (regex.test(url)) {
665 + if (config.debug) {
666 + console.log('[SecureWebSocket] Intercepting (matched intercept pattern):', url);
667 + }
668 + return true;
669 + }
670 + }
671 +
672 + if (config.debug) {
673 + console.log('[SecureWebSocket] Not intercepting (no match):', url);
674 + }
675 + return false;
676 + }
677 +
678 + /**
679 + * Polyfilled WebSocket constructor
680 + * @param {string} url - WebSocket URL
681 + * @param {string|string[]} protocols - Optional subprotocols
682 + * @returns {WebSocket|SecureWebSocket}
683 + */
684 + window.WebSocket = function(url, protocols) {
685 + if (shouldIntercept(url)) {
686 + // Use E2EE SecureWebSocket
687 + console.log('[SecureWebSocket] 🔒 Creating encrypted WebSocket:', url);
688 + return new SecureWebSocket(url, protocols);
689 + } else {
690 + // Use native WebSocket
691 + if (config.debug) {
692 + console.log('[SecureWebSocket] Creating native WebSocket:', url);
693 + }
694 + return new NativeWebSocket(url, protocols);
695 + }
696 + };
697 +
698 + // Copy static properties from native WebSocket
699 + window.WebSocket.CONNECTING = NativeWebSocket.CONNECTING;
700 + window.WebSocket.OPEN = NativeWebSocket.OPEN;
701 + window.WebSocket.CLOSING = NativeWebSocket.CLOSING;
702 + window.WebSocket.CLOSED = NativeWebSocket.CLOSED;
703 +
704 + // Expose SecureWebSocket class for direct access if needed
705 + window.SecureWebSocket = SecureWebSocket;
706 + window.NativeWebSocket = NativeWebSocket;
707 +
708 + console.log('[SecureWebSocket] ✅ Polyfill installed. E2EE enabled:', config.enabled);
709 +
710 +})();
cmd/relay-server/wasm/sw-proxy.js
+149 -24
@@ -4,34 +4,110 @@
4 const CACHE_NAME = 'relaydns-proxy-v1';
5 let proxyEngine = null;
6 let wasmReady = false;
7 +let initializationPromise = null;
8
8 -// Install event - Load WASM here (only time importScripts is allowed)
9 +// Dynamic WASM initialization (can be called anytime, not just install)
10 +async function initializeProxyEngine() {
11 + // If already initializing, wait for it
12 + if (initializationPromise) {
13 + return initializationPromise;
14 + }
15 +
16 + // If already initialized, return immediately
17 + if (proxyEngine && wasmReady) {
18 + return proxyEngine;
19 + }
20 +
21 + initializationPromise = (async () => {
22 + try {
23 + console.log('[SW-Proxy] Dynamically initializing ProxyEngine...');
24 +
25 + // Check if wasm_bindgen is available (loaded during install)
26 + if (typeof wasm_bindgen === 'undefined') {
27 + console.log('[SW-Proxy] wasm_bindgen not available, loading via dynamic import...');
28 +
29 + // Use dynamic import for ES6 modules
30 + const wasmModule = await import('/pkg/relaydns_wasm.js');
31 +
32 + // Make wasm_bindgen available globally
33 + self.wasm_bindgen = wasmModule;
34 +
35 + console.log('[SW-Proxy] ✓ WASM JS module loaded');
36 + }
37 +
38 + // Check if SecureWebSocket SW module is loaded
39 + if (typeof ServiceWorkerWebSocketTunnel === 'undefined') {
40 + console.log('[SW-Proxy] SecureWebSocket SW module not available, loading via fetch...');
41 +
42 + const swWsResponse = await fetch('/secure-websocket-sw.js');
43 + const swWsCode = await swWsResponse.text();
44 +
45 + // Use indirect eval to execute in global scope
46 + (1, eval)(swWsCode);
47 + console.log('[SW-Proxy] ✓ SecureWebSocket SW module loaded');
48 + }
49 +
50 + // Initialize WASM module if not already done
51 + if (!wasmReady) {
52 + console.log('[SW-Proxy] Initializing WASM module...');
53 +
54 + // wasm_bindgen is now the module object from dynamic import
55 + const initWasm = self.wasm_bindgen.default || self.wasm_bindgen;
56 + await initWasm('/pkg/relaydns_wasm_bg.wasm');
57 +
58 + console.log('[SW-Proxy] ✓ WASM module initialized');
59 + }
60 +
61 + // Get relay URL from API
62 + let relayUrl = 'ws://localhost:4017/relay'; // Default fallback
63 + try {
64 + const relayInfoResponse = await fetch('/api/relay-info');
65 + if (relayInfoResponse.ok) {
66 + const relayInfo = await relayInfoResponse.json();
67 + if (relayInfo.relayUrl) {
68 + relayUrl = relayInfo.relayUrl;
69 + console.log('[SW-Proxy] Got relay URL from server:', relayUrl);
70 + }
71 + }
72 + } catch (e) {
73 + console.warn('[SW-Proxy] Failed to fetch relay URL, using default:', e.message);
74 + }
75 +
76 + // Create ProxyEngine
77 + console.log('[SW-Proxy] Creating ProxyEngine with URL:', relayUrl);
78 + const ProxyEngine = self.wasm_bindgen.ProxyEngine;
79 + proxyEngine = new ProxyEngine(relayUrl);
80 + wasmReady = true;
81 +
82 + console.log('[SW-Proxy] ✓ ProxyEngine ready');
83 + return proxyEngine;
84 +
85 + } catch (error) {
86 + console.error('[SW-Proxy] Failed to initialize ProxyEngine:', error);
87 + initializationPromise = null; // Reset so we can retry
88 + wasmReady = false;
89 + throw error;
90 + }
91 + })();
92 +
93 + return initializationPromise;
94 +}
95 +
96 +// Install event - Skip importScripts since WASM is ES6 module
97 self.addEventListener('install', (event) => {
98 console.log('[SW-Proxy] Installing...');
99
100 event.waitUntil(
101 (async () => {
102 try {
15 - console.log('[SW-Proxy] Loading WASM module during install...');
16 -
17 - // importScripts can ONLY be called during install
18 - self.importScripts('/pkg/relaydns_wasm_sw.js');
19 - console.log('[SW-Proxy] ✓ WASM script loaded');
103 + console.log('[SW-Proxy] Service Worker installing...');
104
21 - // Initialize WASM
22 - console.log('[SW-Proxy] Initializing WASM...');
23 - await wasm_bindgen('/pkg/relaydns_wasm_sw_bg.wasm');
24 - console.log('[SW-Proxy] ✓ WASM initialized');
25 -
26 - // Create proxy engine
27 - console.log('[SW-Proxy] Creating ProxyEngine...');
28 - proxyEngine = new wasm_bindgen.ProxyEngine('ws://localhost:4017/relay');
29 - wasmReady = true;
30 - console.log('[SW-Proxy] ✓ Proxy engine ready');
105 + // Note: We cannot use importScripts with ES6 modules
106 + // WASM will be loaded dynamically on first request via fetch + dynamic import
107 + console.log('[SW-Proxy] WASM will be loaded dynamically on first request');
108
109 } catch (error) {
33 - console.error('[SW-Proxy] Failed to initialize WASM:', error);
34 - throw error;
110 + console.error('[SW-Proxy] Install error:', error);
111 }
112
113 // Skip waiting to activate immediately
@@ -64,9 +140,31 @@ function shouldProxy(url) {
140 // Handle HTTP request through WASM proxy
141 async function proxyHttpRequest(request) {
142 try {
143 + // Ensure ProxyEngine is initialized (lazy init if needed)
144 if (!wasmReady || !proxyEngine) {
68 - console.warn('[SW-Proxy] WASM not ready, falling back to direct fetch');
69 - return fetch(request);
145 + console.log('[SW-Proxy] ProxyEngine not ready, attempting lazy initialization...');
146 + try {
147 + await initializeProxyEngine();
148 + } catch (initError) {
149 + // Security: Never fallback to direct fetch - this would bypass E2EE!
150 + console.error('[SW-Proxy] Failed to initialize ProxyEngine:', initError);
151 + return new Response(
152 + JSON.stringify({
153 + error: 'E2EE ProxyEngine Initialization Failed',
154 + message: 'Could not initialize secure proxy. Please refresh the page.',
155 + code: 'PROXY_ENGINE_INIT_FAILED',
156 + details: initError.message
157 + }),
158 + {
159 + status: 503,
160 + statusText: 'Service Unavailable',
161 + headers: {
162 + 'Content-Type': 'application/json',
163 + 'X-E2EE-Status': 'init-failed'
164 + }
165 + }
166 + );
167 + }
168 }
169
170 console.log('[SW-Proxy] Proxying:', request.method, request.url);
@@ -113,9 +211,22 @@ async function proxyHttpRequest(request) {
211
212 } catch (error) {
213 console.error('[SW-Proxy] Proxy error:', error);
116 - // Fallback to direct fetch on error
117 - console.log('[SW-Proxy] Falling back to direct fetch');
118 - return fetch(request);
214 + // Security: Never fallback to direct fetch - return error instead
215 + return new Response(
216 + JSON.stringify({
217 + error: 'E2EE Proxy Error',
218 + message: error.message || 'Failed to proxy request through E2EE tunnel',
219 + code: 'PROXY_ERROR'
220 + }),
221 + {
222 + status: 502,
223 + statusText: 'Bad Gateway',
224 + headers: {
225 + 'Content-Type': 'application/json',
226 + 'X-E2EE-Status': 'error'
227 + }
228 + }
229 + );
230 }
231 }
232
@@ -137,13 +248,27 @@ self.addEventListener('fetch', (event) => {
248 self.addEventListener('message', (event) => {
249 const { type } = event.data || {};
250
251 + // Try WebSocket handler first
252 + if (typeof handleWebSocketMessage === 'function') {
253 + const handled = handleWebSocketMessage(event);
254 + if (handled instanceof Promise) {
255 + // Async handler
256 + return;
257 + } else if (handled) {
258 + // Synchronously handled
259 + return;
260 + }
261 + }
262 +
263 + // Standard message handling
264 switch (type) {
265 case 'GET_STATUS':
266 event.ports[0]?.postMessage({
267 success: true,
268 status: {
269 wasmReady,
146 - hasEngine: !!proxyEngine
270 + hasEngine: !!proxyEngine,
271 + hasWebSocket: typeof handleWebSocketMessage === 'function'
272 }
273 });
274 break;
relaydns/e2ee_websocket.go new
+402
@@ -0,0 +1,402 @@
1 +package relaydns
2 +
3 +import (
4 + "errors"
5 + "fmt"
6 + "io"
7 + "net/url"
8 +
9 + "github.com/gorilla/websocket"
10 + "github.com/gosuda/relaydns/relaydns/core/cryptoops"
11 + "github.com/gosuda/relaydns/relaydns/utils/wsstream"
12 + "github.com/rs/zerolog/log"
13 +)
14 +
15 +var (
16 + ErrInvalidURL = errors.New("invalid WebSocket URL")
17 + ErrUnsupportedALPN = errors.New("unsupported ALPN protocol")
18 +)
19 +
20 +// SecureWebSocketClient provides E2EE WebSocket connections through RelayDNS
21 +type SecureWebSocketClient struct {
22 + relayClient *RelayClient
23 + credential *cryptoops.Credential
24 +}
25 +
26 +// NewSecureWebSocketClient creates a new E2EE WebSocket client
27 +//
28 +// Parameters:
29 +// - relayConn: Connection to relay server (usually ws://relay-server/relay)
30 +// - credential: Client credential for E2EE authentication
31 +//
32 +// Example:
33 +//
34 +// // Connect to relay server
35 +// relayConn, _, err := websocket.DefaultDialer.Dial("ws://localhost:4017/relay", nil)
36 +// if err != nil {
37 +// log.Fatal(err)
38 +// }
39 +//
40 +// // Create credential
41 +// cred, err := cryptoops.NewRandomCredential()
42 +// if err != nil {
43 +// log.Fatal(err)
44 +// }
45 +//
46 +// // Create E2EE WebSocket client
47 +// client := NewSecureWebSocketClient(relayConn, cred)
48 +func NewSecureWebSocketClient(relayConn io.ReadWriteCloser, credential *cryptoops.Credential) *SecureWebSocketClient {
49 + relayClient := NewRelayClient(relayConn)
50 +
51 + return &SecureWebSocketClient{
52 + relayClient: relayClient,
53 + credential: credential,
54 + }
55 +}
56 +
57 +// Dial establishes an E2EE WebSocket connection to a target peer
58 +//
59 +// The target URL should use the "peer://" scheme:
60 +// - peer://peer-id/path - Connect to peer via E2EE tunnel
61 +//
62 +// Parameters:
63 +// - targetURL: Target WebSocket URL (peer://peer-id/path)
64 +// - alpn: Application-Layer Protocol Negotiation (e.g., "websocket", "http")
65 +//
66 +// Returns:
67 +// - *SecureWebSocketConn: Established E2EE connection
68 +// - error: Error if connection fails
69 +//
70 +// Example:
71 +//
72 +// conn, err := client.Dial("peer://abc123/chat", "websocket")
73 +// if err != nil {
74 +// log.Fatal(err)
75 +// }
76 +// defer conn.Close()
77 +//
78 +// // Send message
79 +// err = conn.WriteMessage(websocket.TextMessage, []byte("Hello!"))
80 +//
81 +// // Receive message
82 +// messageType, data, err := conn.ReadMessage()
83 +func (c *SecureWebSocketClient) Dial(targetURL, alpn string) (*SecureWebSocketConn, error) {
84 + log.Debug().
85 + Str("target_url", targetURL).
86 + Str("alpn", alpn).
87 + Msg("[SecureWebSocket] Dialing E2EE WebSocket")
88 +
89 + // Parse target URL
90 + parsedURL, err := url.Parse(targetURL)
91 + if err != nil {
92 + return nil, fmt.Errorf("%w: %v", ErrInvalidURL, err)
93 + }
94 +
95 + // Extract peer ID from URL
96 + // peer://peer-id/path -> peer-id is the lease ID
97 + peerID := parsedURL.Host
98 + if peerID == "" {
99 + return nil, fmt.Errorf("%w: missing peer ID", ErrInvalidURL)
100 + }
101 +
102 + log.Debug().
103 + Str("peer_id", peerID).
104 + Str("alpn", alpn).
105 + Msg("[SecureWebSocket] Requesting connection to peer")
106 +
107 + // Request connection through relay
108 + code, secConn, err := c.relayClient.RequestConnection(peerID, alpn, c.credential)
109 + if err != nil {
110 + return nil, fmt.Errorf("connection request failed: %w", err)
111 + }
112 +
113 + if secConn == nil {
114 + return nil, fmt.Errorf("connection rejected with code: %v", code)
115 + }
116 +
117 + log.Debug().
118 + Str("peer_id", peerID).
119 + Str("local_id", secConn.LocalID()).
120 + Str("remote_id", secConn.RemoteID()).
121 + Msg("[SecureWebSocket] E2EE connection established")
122 +
123 + return &SecureWebSocketConn{
124 + conn: secConn,
125 + peerID: peerID,
126 + alpn: alpn,
127 + isClosed: false,
128 + }, nil
129 +}
130 +
131 +// RegisterService registers this client as a WebSocket service provider
132 +//
133 +// After registration, other clients can connect to this service using:
134 +// - peer://your-peer-id/path
135 +//
136 +// Parameters:
137 +// - name: Service name (for human readability)
138 +// - alpns: Supported ALPN protocols (e.g., []string{"websocket", "http"})
139 +//
140 +// Returns incoming connections via IncomingConnections() channel
141 +//
142 +// Example:
143 +//
144 +// err := client.RegisterService("chat-server", []string{"websocket"})
145 +// if err != nil {
146 +// log.Fatal(err)
147 +// }
148 +//
149 +// log.Printf("Service registered with ID: %s", client.PeerID())
150 +//
151 +// for conn := range client.IncomingConnections() {
152 +// go handleConnection(conn)
153 +// }
154 +func (c *SecureWebSocketClient) RegisterService(name string, alpns []string) error {
155 + log.Debug().
156 + Str("name", name).
157 + Strs("alpns", alpns).
158 + Msg("[SecureWebSocket] Registering service")
159 +
160 + err := c.relayClient.RegisterLease(c.credential, name, alpns)
161 + if err != nil {
162 + return fmt.Errorf("failed to register service: %w", err)
163 + }
164 +
165 + log.Info().
166 + Str("peer_id", c.credential.ID()).
167 + Str("name", name).
168 + Strs("alpns", alpns).
169 + Msg("[SecureWebSocket] Service registered successfully")
170 +
171 + return nil
172 +}
173 +
174 +// DeregisterService removes this client from the relay server
175 +func (c *SecureWebSocketClient) DeregisterService() error {
176 + log.Debug().Msg("[SecureWebSocket] Deregistering service")
177 +
178 + err := c.relayClient.DeregisterLease(c.credential)
179 + if err != nil {
180 + return fmt.Errorf("failed to deregister service: %w", err)
181 + }
182 +
183 + log.Info().Str("peer_id", c.credential.ID()).Msg("[SecureWebSocket] Service deregistered")
184 + return nil
185 +}
186 +
187 +// IncomingConnections returns a channel that receives incoming E2EE connections
188 +//
189 +// This channel delivers connections from other peers connecting to this service.
190 +// Each connection is already authenticated and encrypted.
191 +//
192 +// Example:
193 +//
194 +// for conn := range client.IncomingConnections() {
195 +// log.Printf("New connection from: %s", conn.RemoteID())
196 +// go handleConnection(conn)
197 +// }
198 +func (c *SecureWebSocketClient) IncomingConnections() <-chan *SecureWebSocketConn {
199 + ch := make(chan *SecureWebSocketConn)
200 +
201 + go func() {
202 + for incomingConn := range c.relayClient.IncommingConnection() {
203 + log.Debug().
204 + Str("lease_id", incomingConn.LeaseID()).
205 + Str("remote_id", incomingConn.RemoteID()).
206 + Msg("[SecureWebSocket] Incoming E2EE connection")
207 +
208 + ch <- &SecureWebSocketConn{
209 + conn: incomingConn.SecureConnection,
210 + peerID: incomingConn.RemoteID(),
211 + alpn: "", // ALPN is already negotiated
212 + isClosed: false,
213 + }
214 + }
215 + close(ch)
216 + }()
217 +
218 + return ch
219 +}
220 +
221 +// PeerID returns this client's peer ID
222 +//
223 +// This is the identifier that other clients use to connect to this service.
224 +// Example: peer://your-peer-id/path
225 +func (c *SecureWebSocketClient) PeerID() string {
226 + return c.credential.ID()
227 +}
228 +
229 +// Close closes the relay connection and stops the client
230 +func (c *SecureWebSocketClient) Close() error {
231 + log.Debug().Msg("[SecureWebSocket] Closing client")
232 + return c.relayClient.Close()
233 +}
234 +
235 +// SecureWebSocketConn represents an E2EE WebSocket connection
236 +type SecureWebSocketConn struct {
237 + conn *cryptoops.SecureConnection
238 + peerID string
239 + alpn string
240 + isClosed bool
241 +}
242 +
243 +// ReadMessage reads the next message from the connection
244 +//
245 +// Compatible with gorilla/websocket ReadMessage interface.
246 +// Always returns BinaryMessage type (E2EE encrypts everything as binary).
247 +//
248 +// Returns:
249 +// - messageType: Always websocket.BinaryMessage for E2EE
250 +// - data: Decrypted message data
251 +// - error: Error if read fails
252 +func (c *SecureWebSocketConn) ReadMessage() (messageType int, data []byte, err error) {
253 + if c.isClosed {
254 + return 0, nil, io.EOF
255 + }
256 +
257 + // Read encrypted data
258 + buf := make([]byte, 65536) // 64KB buffer
259 + n, err := c.conn.Read(buf)
260 + if err != nil {
261 + if err == io.EOF {
262 + c.isClosed = true
263 + }
264 + return 0, nil, err
265 + }
266 +
267 + // Return decrypted data as binary message
268 + return websocket.BinaryMessage, buf[:n], nil
269 +}
270 +
271 +// WriteMessage writes a message to the connection
272 +//
273 +// Compatible with gorilla/websocket WriteMessage interface.
274 +// Message type is ignored as E2EE encrypts everything as binary.
275 +//
276 +// Parameters:
277 +// - messageType: Ignored (can be TextMessage or BinaryMessage)
278 +// - data: Message data to send (will be encrypted)
279 +//
280 +// Returns:
281 +// - error: Error if write fails
282 +func (c *SecureWebSocketConn) WriteMessage(messageType int, data []byte) error {
283 + if c.isClosed {
284 + return io.ErrClosedPipe
285 + }
286 +
287 + // Write encrypted data
288 + _, err := c.conn.Write(data)
289 + return err
290 +}
291 +
292 +// Read implements io.Reader
293 +func (c *SecureWebSocketConn) Read(p []byte) (n int, err error) {
294 + if c.isClosed {
295 + return 0, io.EOF
296 + }
297 + return c.conn.Read(p)
298 +}
299 +
300 +// Write implements io.Writer
301 +func (c *SecureWebSocketConn) Write(p []byte) (n int, err error) {
302 + if c.isClosed {
303 + return 0, io.ErrClosedPipe
304 + }
305 + return c.conn.Write(p)
306 +}
307 +
308 +// Close closes the connection
309 +func (c *SecureWebSocketConn) Close() error {
310 + if c.isClosed {
311 + return nil
312 + }
313 +
314 + c.isClosed = true
315 + return c.conn.Close()
316 +}
317 +
318 +// RemoteID returns the peer's ID (authenticated identity)
319 +func (c *SecureWebSocketConn) RemoteID() string {
320 + return c.conn.RemoteID()
321 +}
322 +
323 +// LocalID returns this client's ID
324 +func (c *SecureWebSocketConn) LocalID() string {
325 + return c.conn.LocalID()
326 +}
327 +
328 +// PeerID returns the target peer ID
329 +func (c *SecureWebSocketConn) PeerID() string {
330 + return c.peerID
331 +}
332 +
333 +// ALPN returns the negotiated application-layer protocol
334 +func (c *SecureWebSocketConn) ALPN() string {
335 + return c.alpn
336 +}
337 +
338 +// DialWebSocketSecure is a convenience function to establish E2EE WebSocket through relay
339 +//
340 +// This is a high-level helper that:
341 +// 1. Connects to relay server
342 +// 2. Creates E2EE client
343 +// 3. Dials target peer
344 +//
345 +// Parameters:
346 +// - relayURL: Relay server WebSocket URL (e.g., "ws://localhost:4017/relay")
347 +// - targetPeerID: Target peer ID to connect to
348 +// - alpn: Application protocol (e.g., "websocket")
349 +// - credential: Client credential (or nil to generate random)
350 +//
351 +// Returns:
352 +// - *SecureWebSocketConn: Established E2EE connection
353 +// - *SecureWebSocketClient: Client instance (keep alive for connection)
354 +// - error: Error if connection fails
355 +//
356 +// Example:
357 +//
358 +// conn, client, err := DialWebSocketSecure(
359 +// "ws://localhost:4017/relay",
360 +// "target-peer-id",
361 +// "websocket",
362 +// nil, // auto-generate credential
363 +// )
364 +// if err != nil {
365 +// log.Fatal(err)
366 +// }
367 +// defer conn.Close()
368 +// defer client.Close()
369 +//
370 +// conn.WriteMessage(websocket.TextMessage, []byte("Hello!"))
371 +func DialWebSocketSecure(relayURL, targetPeerID, alpn string, credential *cryptoops.Credential) (*SecureWebSocketConn, *SecureWebSocketClient, error) {
372 + // Generate credential if not provided
373 + if credential == nil {
374 + var err error
375 + credential, err = cryptoops.NewCredential()
376 + if err != nil {
377 + return nil, nil, fmt.Errorf("failed to generate credential: %w", err)
378 + }
379 + }
380 +
381 + // Connect to relay server
382 + relayConn, _, err := websocket.DefaultDialer.Dial(relayURL, nil)
383 + if err != nil {
384 + return nil, nil, fmt.Errorf("failed to connect to relay: %w", err)
385 + }
386 +
387 + // Wrap WebSocket connection
388 + relayStream := &wsstream.WsStream{Conn: relayConn}
389 +
390 + // Create E2EE client
391 + client := NewSecureWebSocketClient(relayStream, credential)
392 +
393 + // Dial target peer
394 + targetURL := fmt.Sprintf("peer://%s/", targetPeerID)
395 + conn, err := client.Dial(targetURL, alpn)
396 + if err != nil {
397 + client.Close()
398 + return nil, nil, fmt.Errorf("failed to dial peer: %w", err)
399 + }
400 +
401 + return conn, client, nil
402 +}
relaydns/wasm/.gitignore
+2
@@ -11,6 +11,8 @@ Cargo.lock
11 !examples/*.js
12 !sw.js
13 !sw-proxy.js
14 +!secure-websocket.js
15 +!secure-websocket-sw.js
16
17 # Keep HTML files
18 !*.html
relaydns/wasm/index.html deleted
-130
@@ -1,130 +0,0 @@
1 -<!DOCTYPE html>
2 -<html>
3 -<head>
4 - <meta charset="UTF-8">
5 - <title>RelayDNS E2EE Proxy - Ready!</title>
6 - <style>
7 - body { font-family: Arial; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; }
8 - .container { max-width: 800px; margin: 50px auto; background: white; padding: 40px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
9 - h1 { color: #333; margin: 0 0 10px 0; }
10 - .subtitle { color: #666; margin-bottom: 30px; font-size: 18px; }
11 - button { padding: 15px 30px; margin: 10px 5px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 600; }
12 - button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); transition: all 0.3s; }
13 - .status { padding: 20px; margin: 20px 0; border-radius: 8px; font-size: 16px; }
14 - .status.waiting { background: #fff3cd; color: #856404; }
15 - .status.success { background: #d4edda; color: #155724; }
16 - .status.error { background: #f8d7da; color: #721c24; }
17 - input { padding: 12px; width: 100%; margin: 10px 0; border: 2px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; }
18 - #log { background: #1e1e1e; color: #d4d4d4; padding: 20px; border-radius: 6px; margin-top: 20px; max-height: 400px; overflow-y: auto; font-family: 'Courier New', monospace; font-size: 14px; }
19 - .log-success { color: #4CAF50; }
20 - .log-error { color: #f44336; }
21 - .badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 14px; font-weight: 600; margin-left: 10px; }
22 - .badge-green { background: #4CAF50; color: white; }
23 - .badge-red { background: #f44336; color: white; }
24 - </style>
25 -</head>
26 -<body>
27 - <div class="container">
28 - <h1>🚀 RelayDNS E2EE Network Proxy</h1>
29 - <div class="subtitle">All network requests are encrypted end-to-end</div>
30 -
31 - <div id="status" class="status waiting">⏳ Waiting for Service Worker...</div>
32 -
33 - <h2>Test Request</h2>
34 - <input id="testUrl" value="https://api.github.com/zen" placeholder="Enter URL">
35 - <button onclick="testFetch()" id="testBtn">🌐 Make Request (через E2EE прокси)</button>
36 -
37 - <div id="log"></div>
38 - </div>
39 -
40 - <script>
41 - function log(msg, type = 'info') {
42 - const logDiv = document.getElementById('log');
43 - const time = new Date().toLocaleTimeString();
44 - const className = type === 'error' ? 'log-error' : (type === 'success' ? 'log-success' : '');
45 - logDiv.innerHTML += `<div class="${className}">[${time}] ${msg}</div>`;
46 - logDiv.scrollTop = logDiv.scrollHeight;
47 - }
48 -
49 - function updateStatus(msg, type = 'waiting') {
50 - const statusDiv = document.getElementById('status');
51 - statusDiv.textContent = msg;
52 - statusDiv.className = `status ${type}`;
53 - }
54 -
55 - async function testFetch() {
56 - const url = document.getElementById('testUrl').value;
57 - try {
58 - log(`→ Fetching: ${url}`, 'info');
59 - updateStatus('Making request through E2EE tunnel...', 'waiting');
60 -
61 - const start = Date.now();
62 - const response = await fetch(url);
63 - const duration = Date.now() - start;
64 -
65 - log(`← ${response.status} ${response.statusText} (${duration}ms)`, 'success');
66 -
67 - const text = await response.text();
68 - log(`Response: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`, 'info');
69 -
70 - updateStatus(`✓ Request completed: ${response.status}`, 'success');
71 -
72 - } catch (error) {
73 - log(`✗ Error: ${error.message}`, 'error');
74 - updateStatus(`✗ Error: ${error.message}`, 'error');
75 - }
76 - }
77 -
78 - // Initialize Service Worker automatically
79 - (async () => {
80 - try {
81 - log('Registering Service Worker...', 'info');
82 - updateStatus('🔄 Registering Service Worker...', 'waiting');
83 -
84 - const registration = await navigator.serviceWorker.register('/sw-proxy.js');
85 - log('✓ Service Worker registered', 'success');
86 -
87 - // Wait for it to be ready
88 - await navigator.serviceWorker.ready;
89 - log('✓ Service Worker active', 'success');
90 -
91 - // Check if WASM is ready
92 - const channel = new MessageChannel();
93 - const status = await new Promise((resolve) => {
94 - channel.port1.onmessage = (event) => resolve(event.data);
95 - registration.active.postMessage({ type: 'GET_STATUS' }, [channel.port2]);
96 - setTimeout(() => resolve({ success: false }), 2000);
97 - });
98 -
99 - if (status.success && status.status.wasmReady) {
100 - log('✓ WASM ProxyEngine ready!', 'success');
101 - log('✓ All external requests will be proxied through E2EE tunnel', 'success');
102 - updateStatus('✅ Ready! All requests will be encrypted end-to-end', 'success');
103 - } else {
104 - log('⚠ WASM not ready yet, requests will be direct', 'info');
105 - updateStatus('⚠ Service Worker active, but WASM still loading...', 'waiting');
106 -
107 - // Retry after a moment
108 - setTimeout(async () => {
109 - const channel2 = new MessageChannel();
110 - const status2 = await new Promise((resolve) => {
111 - channel2.port1.onmessage = (event) => resolve(event.data);
112 - registration.active.postMessage({ type: 'GET_STATUS' }, [channel2.port2]);
113 - setTimeout(() => resolve({ success: false }), 2000);
114 - });
115 -
116 - if (status2.success && status2.status.wasmReady) {
117 - log('✓ WASM ProxyEngine now ready!', 'success');
118 - updateStatus('✅ Ready! E2EE proxy is active', 'success');
119 - }
120 - }, 3000);
121 - }
122 -
123 - } catch (error) {
124 - log(`✗ Error: ${error.message}`, 'error');
125 - updateStatus(`✗ Error: ${error.message}`, 'error');
126 - }
127 - })();
128 - </script>
129 -</body>
130 -</html>
relaydns/wasm/secure-websocket-sw.js new
+283
@@ -0,0 +1,283 @@
1 +/**
2 + * SecureWebSocket for Service Worker
3 + *
4 + * SecureWebSocket implementation for use in Service Worker
5 + * Communicates with main thread via MessageChannel
6 + */
7 +
8 +// Service Worker global ProxyEngine (initialized in sw-proxy.js)
9 +// proxyEngine and wasmReady are provided by sw-proxy.js
10 +
11 +/**
12 + * WebSocket tunnel manager in Service Worker
13 + */
14 +class ServiceWorkerWebSocketTunnel {
15 + constructor() {
16 + this.tunnels = new Map(); // tunnelId -> tunnel info
17 + this.messageQueues = new Map(); // tunnelId -> message queue
18 + this.clients = new Map(); // tunnelId -> clientId
19 + }
20 +
21 + /**
22 + * Create WebSocket tunnel
23 + */
24 + async createTunnel(url, protocols, clientId) {
25 + if (!wasmReady || !proxyEngine) {
26 + throw new Error('WASM ProxyEngine not ready');
27 + }
28 +
29 + console.log('[SW-WebSocket] Creating tunnel:', url);
30 +
31 + try {
32 + // Open WebSocket tunnel via WASM ProxyEngine
33 + const result = await proxyEngine.open_websocket(url, protocols || []);
34 + const tunnelId = result.tunnelId;
35 + const protocol = result.protocol || '';
36 +
37 + console.log('[SW-WebSocket] Tunnel created:', tunnelId);
38 +
39 + // Store tunnel information
40 + this.tunnels.set(tunnelId, {
41 + tunnelId,
42 + url,
43 + protocol,
44 + state: 'open',
45 + created: Date.now()
46 + });
47 +
48 + this.messageQueues.set(tunnelId, []);
49 + this.clients.set(tunnelId, clientId);
50 +
51 + // Start receiving messages in background
52 + this._startReceiving(tunnelId);
53 +
54 + return {
55 + tunnelId,
56 + protocol
57 + };
58 +
59 + } catch (error) {
60 + console.error('[SW-WebSocket] Failed to create tunnel:', error);
61 + throw error;
62 + }
63 + }
64 +
65 + /**
66 + * Background message receiving loop
67 + */
68 + async _startReceiving(tunnelId) {
69 + console.log('[SW-WebSocket] Starting receive loop:', tunnelId);
70 +
71 + try {
72 + while (this.tunnels.has(tunnelId)) {
73 + const tunnel = this.tunnels.get(tunnelId);
74 + if (!tunnel || tunnel.state !== 'open') {
75 + break;
76 + }
77 +
78 + // Receive message from WASM
79 + const msg = await proxyEngine.receive_websocket_message(tunnelId);
80 +
81 + console.log('[SW-WebSocket] Received message:', msg.type);
82 +
83 + // Add to message queue
84 + const queue = this.messageQueues.get(tunnelId);
85 + if (queue) {
86 + queue.push(msg);
87 + }
88 +
89 + // Notify client
90 + this._notifyClient(tunnelId, msg);
91 +
92 + // Handle close message
93 + if (msg.type === 'close') {
94 + console.log('[SW-WebSocket] Tunnel closed:', tunnelId);
95 + this._closeTunnel(tunnelId);
96 + break;
97 + }
98 + }
99 +
100 + } catch (error) {
101 + console.error('[SW-WebSocket] Receive loop error:', error);
102 + this._closeTunnel(tunnelId, 1006, error.toString());
103 + }
104 + }
105 +
106 + /**
107 + * Notify client of message
108 + */
109 + async _notifyClient(tunnelId, message) {
110 + const clientId = this.clients.get(tunnelId);
111 + if (!clientId) return;
112 +
113 + try {
114 + const client = await self.clients.get(clientId);
115 + if (client) {
116 + client.postMessage({
117 + type: 'WEBSOCKET_MESSAGE',
118 + tunnelId,
119 + message
120 + });
121 + }
122 + } catch (error) {
123 + console.error('[SW-WebSocket] Failed to notify client:', error);
124 + }
125 + }
126 +
127 + /**
128 + * Send message
129 + */
130 + async sendMessage(tunnelId, data, isBinary) {
131 + if (!this.tunnels.has(tunnelId)) {
132 + throw new Error('Tunnel not found: ' + tunnelId);
133 + }
134 +
135 + console.log('[SW-WebSocket] Sending message:', tunnelId, isBinary ? 'binary' : 'text');
136 +
137 + try {
138 + await proxyEngine.send_websocket_message(tunnelId, data, isBinary);
139 + } catch (error) {
140 + console.error('[SW-WebSocket] Send failed:', error);
141 + throw error;
142 + }
143 + }
144 +
145 + /**
146 + * Close tunnel
147 + */
148 + async closeTunnel(tunnelId, code = 1000, reason = '') {
149 + if (!this.tunnels.has(tunnelId)) {
150 + return;
151 + }
152 +
153 + console.log('[SW-WebSocket] Closing tunnel:', tunnelId, code, reason);
154 +
155 + try {
156 + await proxyEngine.close_websocket(tunnelId, code, reason);
157 + } catch (error) {
158 + console.error('[SW-WebSocket] Close failed:', error);
159 + }
160 +
161 + this._closeTunnel(tunnelId);
162 + }
163 +
164 + /**
165 + * Internal tunnel cleanup
166 + */
167 + _closeTunnel(tunnelId, code = 1000, reason = '') {
168 + const tunnel = this.tunnels.get(tunnelId);
169 + if (tunnel) {
170 + tunnel.state = 'closed';
171 + }
172 +
173 + // Cleanup
174 + this.tunnels.delete(tunnelId);
175 + this.messageQueues.delete(tunnelId);
176 + this.clients.delete(tunnelId);
177 +
178 + console.log('[SW-WebSocket] Tunnel cleaned up:', tunnelId);
179 + }
180 +
181 + /**
182 + * Get tunnel state
183 + */
184 + getTunnelState(tunnelId) {
185 + const tunnel = this.tunnels.get(tunnelId);
186 + return tunnel ? tunnel.state : 'closed';
187 + }
188 +
189 + /**
190 + * Get all tunnel information
191 + */
192 + getAllTunnels() {
193 + return Array.from(this.tunnels.values());
194 + }
195 +}
196 +
197 +// Global tunnel manager instance
198 +let tunnelManager = null;
199 +
200 +/**
201 + * Initialize tunnel manager
202 + */
203 +function initTunnelManager() {
204 + if (!tunnelManager) {
205 + tunnelManager = new ServiceWorkerWebSocketTunnel();
206 + console.log('[SW-WebSocket] Tunnel manager initialized');
207 + }
208 + return tunnelManager;
209 +}
210 +
211 +/**
212 + * WebSocket message handler to add to Service Worker
213 + */
214 +async function handleWebSocketMessage(event) {
215 + const { type, tunnelId, url, protocols, data, isBinary, code, reason } = event.data || {};
216 + const manager = initTunnelManager();
217 +
218 + switch (type) {
219 + case 'WEBSOCKET_OPEN':
220 + try {
221 + const clientId = event.source?.id || event.clientId;
222 + const result = await manager.createTunnel(url, protocols, clientId);
223 +
224 + event.ports[0]?.postMessage({
225 + success: true,
226 + result
227 + });
228 + } catch (error) {
229 + event.ports[0]?.postMessage({
230 + success: false,
231 + error: error.toString()
232 + });
233 + }
234 + break;
235 +
236 + case 'WEBSOCKET_SEND':
237 + try {
238 + await manager.sendMessage(tunnelId, data, isBinary);
239 + event.ports[0]?.postMessage({ success: true });
240 + } catch (error) {
241 + event.ports[0]?.postMessage({
242 + success: false,
243 + error: error.toString()
244 + });
245 + }
246 + break;
247 +
248 + case 'WEBSOCKET_CLOSE':
249 + try {
250 + await manager.closeTunnel(tunnelId, code, reason);
251 + event.ports[0]?.postMessage({ success: true });
252 + } catch (error) {
253 + event.ports[0]?.postMessage({
254 + success: false,
255 + error: error.toString()
256 + });
257 + }
258 + break;
259 +
260 + case 'WEBSOCKET_STATE':
261 + const state = manager.getTunnelState(tunnelId);
262 + event.ports[0]?.postMessage({
263 + success: true,
264 + state
265 + });
266 + break;
267 +
268 + case 'WEBSOCKET_LIST':
269 + const tunnels = manager.getAllTunnels();
270 + event.ports[0]?.postMessage({
271 + success: true,
272 + tunnels
273 + });
274 + break;
275 +
276 + default:
277 + return false; // Not handled
278 + }
279 +
280 + return true; // Handled
281 +}
282 +
283 +console.log('[SW-WebSocket] Service Worker WebSocket module loaded');
relaydns/wasm/secure-websocket.js new
+710
@@ -0,0 +1,710 @@
1 +/**
2 + * SecureWebSocket - E2EE WebSocket Polyfill using WASM ProxyEngine
3 + *
4 + * Provides transparent end-to-end encryption for WebSocket connections
5 + * through the RelayDNS WASM ProxyEngine.
6 + */
7 +
8 +// Global WASM instance cache
9 +let wasmInstance = null;
10 +let wasmInitPromise = null;
11 +
12 +/**
13 + * Get Relay server URL from server or config
14 + * @returns {Promise<string>} Relay server WebSocket URL
15 + */
16 +async function getRelayUrl() {
17 + // 1. Check if manually configured
18 + if (window.RELAYDNS_RELAY_URL) {
19 + console.log('[SecureWebSocket] Using configured relay URL:', window.RELAYDNS_RELAY_URL);
20 + return window.RELAYDNS_RELAY_URL;
21 + }
22 +
23 + // 2. Try to get from server API
24 + try {
25 + const response = await fetch('/api/relay-info');
26 + if (response.ok) {
27 + const data = await response.json();
28 + if (data.relayUrl) {
29 + console.log('[SecureWebSocket] Got relay URL from server:', data.relayUrl);
30 + return data.relayUrl;
31 + }
32 + }
33 + } catch (error) {
34 + console.warn('[SecureWebSocket] Failed to fetch relay info from server:', error.message);
35 + }
36 +
37 + // 3. Auto-detect from current location
38 + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
39 + const host = window.location.host;
40 + const autoUrl = `${protocol}//${host}/relay`;
41 +
42 + console.log('[SecureWebSocket] Auto-detected relay URL:', autoUrl);
43 + return autoUrl;
44 +}
45 +
46 +/**
47 + * Check if Service Worker is available and has WebSocket support
48 + * @returns {Promise<boolean>}
49 + */
50 +async function hasServiceWorkerWebSocket() {
51 + if (!navigator.serviceWorker || !navigator.serviceWorker.controller) {
52 + return false;
53 + }
54 +
55 + try {
56 + const channel = new MessageChannel();
57 + const response = await new Promise((resolve) => {
58 + channel.port1.onmessage = (event) => resolve(event.data);
59 + navigator.serviceWorker.controller.postMessage(
60 + { type: 'GET_STATUS' },
61 + [channel.port2]
62 + );
63 + setTimeout(() => resolve({ success: false }), 1000);
64 + });
65 +
66 + return response.success && response.status?.hasWebSocket;
67 + } catch {
68 + return false;
69 + }
70 +}
71 +
72 +/**
73 + * Initialize and get the WASM ProxyEngine instance
74 + * @returns {Promise<Object>} WASM module with ProxyEngine
75 + */
76 +async function getProxyEngine() {
77 + if (wasmInstance) {
78 + return wasmInstance;
79 + }
80 +
81 + if (wasmInitPromise) {
82 + return wasmInitPromise;
83 + }
84 +
85 + wasmInitPromise = (async () => {
86 + try {
87 + // Check if we should use Service Worker
88 + const useServiceWorker = await hasServiceWorkerWebSocket();
89 +
90 + if (useServiceWorker) {
91 + console.log('[SecureWebSocket] Using Service Worker for WebSocket');
92 + wasmInstance = {
93 + engine: null,
94 + wasm: null,
95 + useServiceWorker: true
96 + };
97 + return wasmInstance;
98 + }
99 +
100 + // Fallback to direct WASM
101 + console.log('[SecureWebSocket] Using direct WASM');
102 +
103 + // Load WASM module
104 + if (typeof wasm_bindgen === 'undefined') {
105 + throw new Error('WASM module not loaded. Include relaydns_wasm.js first.');
106 + }
107 +
108 + // Initialize WASM
109 + await wasm_bindgen('/pkg/relaydns_wasm_bg.wasm');
110 +
111 + // Get relay server URL
112 + const relayUrl = await getRelayUrl();
113 +
114 + // Create ProxyEngine instance
115 + const engine = new wasm_bindgen.ProxyEngine(relayUrl);
116 +
117 + console.log('[SecureWebSocket] WASM ProxyEngine initialized:', relayUrl);
118 +
119 + wasmInstance = {
120 + engine,
121 + wasm: wasm_bindgen,
122 + useServiceWorker: false
123 + };
124 +
125 + return wasmInstance;
126 +
127 + } catch (error) {
128 + console.error('[SecureWebSocket] Failed to initialize WASM:', error);
129 + wasmInitPromise = null;
130 + throw error;
131 + }
132 + })();
133 +
134 + return wasmInitPromise;
135 +}
136 +
137 +/**
138 + * SecureWebSocket - Drop-in replacement for native WebSocket with E2EE
139 + */
140 +class SecureWebSocket extends EventTarget {
141 + /**
142 + * @param {string} url - WebSocket URL
143 + * @param {string|string[]} protocols - Optional subprotocols
144 + */
145 + constructor(url, protocols = []) {
146 + super();
147 +
148 + // Normalize protocols
149 + if (typeof protocols === 'string') {
150 + protocols = [protocols];
151 + }
152 +
153 + // Public properties (read-only)
154 + Object.defineProperties(this, {
155 + url: { value: url, writable: false, enumerable: true },
156 + protocols: { value: protocols, writable: false, enumerable: true },
157 + });
158 +
159 + // Internal state
160 + this._readyState = WebSocket.CONNECTING;
161 + this._protocol = '';
162 + this._tunnelId = null;
163 + this._bufferedAmount = 0;
164 + this._extensions = '';
165 + this._binaryType = 'blob';
166 +
167 + // Event handlers (nullable)
168 + this.onopen = null;
169 + this.onmessage = null;
170 + this.onerror = null;
171 + this.onclose = null;
172 +
173 + // Start connection
174 + this._connect();
175 + }
176 +
177 + // Public properties with getters
178 + get readyState() { return this._readyState; }
179 + get protocol() { return this._protocol; }
180 + get bufferedAmount() { return this._bufferedAmount; }
181 + get extensions() { return this._extensions; }
182 + get binaryType() { return this._binaryType; }
183 + set binaryType(value) {
184 + if (value === 'blob' || value === 'arraybuffer') {
185 + this._binaryType = value;
186 + }
187 + }
188 +
189 + /**
190 + * Initialize connection through WASM ProxyEngine
191 + * @private
192 + */
193 + async _connect() {
194 + try {
195 + console.log('[SecureWebSocket] Connecting to:', this.url);
196 +
197 + // Get WASM ProxyEngine or Service Worker
198 + const instance = await getProxyEngine();
199 +
200 + if (instance.useServiceWorker) {
201 + // Use Service Worker
202 + await this._connectViaServiceWorker();
203 + } else {
204 + // Use direct WASM
205 + await this._connectViaDirect(instance.engine);
206 + }
207 +
208 + } catch (error) {
209 + console.error('[SecureWebSocket] Connection failed:', error);
210 +
211 + this._readyState = WebSocket.CLOSED;
212 +
213 + // Dispatch error event
214 + this._dispatchEvent('error', {
215 + message: error.toString(),
216 + error: error
217 + });
218 +
219 + // Dispatch close event
220 + this._dispatchEvent('close', {
221 + code: 1006,
222 + reason: error.toString(),
223 + wasClean: false
224 + });
225 + }
226 + }
227 +
228 + /**
229 + * Connect via Service Worker
230 + * @private
231 + */
232 + async _connectViaServiceWorker() {
233 + console.log('[SecureWebSocket] Connecting via Service Worker');
234 +
235 + const channel = new MessageChannel();
236 + const response = await new Promise((resolve, reject) => {
237 + channel.port1.onmessage = (event) => {
238 + if (event.data.success) {
239 + resolve(event.data.result);
240 + } else {
241 + reject(new Error(event.data.error));
242 + }
243 + };
244 +
245 + navigator.serviceWorker.controller.postMessage(
246 + {
247 + type: 'WEBSOCKET_OPEN',
248 + url: this.url,
249 + protocols: this.protocols
250 + },
251 + [channel.port2]
252 + );
253 +
254 + setTimeout(() => reject(new Error('Service Worker timeout')), 10000);
255 + });
256 +
257 + this._tunnelId = response.tunnelId;
258 + this._protocol = response.protocol || '';
259 + this._readyState = WebSocket.OPEN;
260 + this._useServiceWorker = true;
261 +
262 + console.log('[SecureWebSocket] Connected via SW! Tunnel ID:', this._tunnelId);
263 +
264 + // Dispatch open event
265 + this._dispatchEvent('open', {});
266 +
267 + // Listen for messages from Service Worker
268 + this._listenToServiceWorker();
269 + }
270 +
271 + /**
272 + * Connect via direct WASM
273 + * @private
274 + */
275 + async _connectViaDirect(engine) {
276 + console.log('[SecureWebSocket] Connecting via direct WASM');
277 +
278 + // Open WebSocket tunnel through E2EE proxy
279 + const result = await engine.open_websocket(this.url, this.protocols);
280 +
281 + this._tunnelId = result.tunnelId;
282 + this._protocol = result.protocol || '';
283 + this._readyState = WebSocket.OPEN;
284 + this._useServiceWorker = false;
285 +
286 + console.log('[SecureWebSocket] Connected! Tunnel ID:', this._tunnelId);
287 +
288 + // Dispatch open event
289 + this._dispatchEvent('open', {});
290 +
291 + // Start receiving messages in background
292 + this._receiveLoop(engine);
293 + }
294 +
295 + /**
296 + * Listen to Service Worker messages
297 + * @private
298 + */
299 + _listenToServiceWorker() {
300 + const handler = (event) => {
301 + if (event.data.type === 'WEBSOCKET_MESSAGE' &&
302 + event.data.tunnelId === this._tunnelId) {
303 +
304 + const msg = event.data.message;
305 + this._handleMessage(msg);
306 + }
307 + };
308 +
309 + navigator.serviceWorker.addEventListener('message', handler);
310 + this._swMessageHandler = handler;
311 + }
312 +
313 + /**
314 + * Handle incoming message
315 + * @private
316 + */
317 + _handleMessage(msg) {
318 + if (msg.type === 'text') {
319 + // Text message
320 + this._dispatchEvent('message', {
321 + data: msg.data,
322 + type: 'message',
323 + origin: this.url
324 + });
325 +
326 + } else if (msg.type === 'binary') {
327 + // Binary message
328 + let data;
329 + if (this._binaryType === 'arraybuffer') {
330 + data = new Uint8Array(msg.data).buffer;
331 + } else {
332 + data = new Blob([new Uint8Array(msg.data)]);
333 + }
334 +
335 + this._dispatchEvent('message', {
336 + data: data,
337 + type: 'message',
338 + origin: this.url
339 + });
340 +
341 + } else if (msg.type === 'close') {
342 + // Close message
343 + console.log('[SecureWebSocket] Received close:', msg.code, msg.reason);
344 +
345 + this._readyState = WebSocket.CLOSED;
346 +
347 + this._dispatchEvent('close', {
348 + code: msg.code || 1000,
349 + reason: msg.reason || '',
350 + wasClean: true
351 + });
352 +
353 + // Cleanup Service Worker listener
354 + if (this._swMessageHandler) {
355 + navigator.serviceWorker.removeEventListener('message', this._swMessageHandler);
356 + }
357 + }
358 + }
359 +
360 + /**
361 + * Background loop to receive messages from tunnel
362 + * @private
363 + * @param {Object} engine - WASM ProxyEngine instance
364 + */
365 + async _receiveLoop(engine) {
366 + try {
367 + while (this._readyState !== WebSocket.CLOSED && this._readyState !== WebSocket.CLOSING) {
368 + // Receive message from tunnel
369 + const msg = await engine.receive_websocket_message(this._tunnelId);
370 +
371 + if (msg.type === 'text') {
372 + // Text message
373 + this._dispatchEvent('message', {
374 + data: msg.data,
375 + type: 'message',
376 + origin: this.url
377 + });
378 +
379 + } else if (msg.type === 'binary') {
380 + // Binary message
381 + let data;
382 + if (this._binaryType === 'arraybuffer') {
383 + data = new Uint8Array(msg.data).buffer;
384 + } else {
385 + // Convert to Blob
386 + data = new Blob([new Uint8Array(msg.data)]);
387 + }
388 +
389 + this._dispatchEvent('message', {
390 + data: data,
391 + type: 'message',
392 + origin: this.url
393 + });
394 +
395 + } else if (msg.type === 'close') {
396 + // Close message
397 + console.log('[SecureWebSocket] Received close:', msg.code, msg.reason);
398 +
399 + this._readyState = WebSocket.CLOSED;
400 +
401 + this._dispatchEvent('close', {
402 + code: msg.code || 1000,
403 + reason: msg.reason || '',
404 + wasClean: true
405 + });
406 +
407 + break;
408 + }
409 + }
410 +
411 + } catch (error) {
412 + console.error('[SecureWebSocket] Receive loop error:', error);
413 +
414 + if (this._readyState !== WebSocket.CLOSED) {
415 + this._dispatchEvent('error', {
416 + message: error.toString(),
417 + error: error
418 + });
419 +
420 + this.close(1006, error.toString());
421 + }
422 + }
423 + }
424 +
425 + /**
426 + * Send data through the secure tunnel
427 + * @param {string|ArrayBuffer|Uint8Array|Blob} data - Data to send
428 + */
429 + send(data) {
430 + if (this._readyState !== WebSocket.OPEN) {
431 + throw new DOMException(
432 + 'Failed to execute \'send\' on \'WebSocket\': Still in CONNECTING state.',
433 + 'InvalidStateError'
434 + );
435 + }
436 +
437 + // Handle different data types
438 + if (typeof data === 'string') {
439 + // Text message
440 + this._sendMessage(data, false);
441 +
442 + } else if (data instanceof ArrayBuffer) {
443 + // Binary ArrayBuffer
444 + this._sendMessage(new Uint8Array(data), true);
445 +
446 + } else if (data instanceof Uint8Array) {
447 + // Binary Uint8Array
448 + this._sendMessage(data, true);
449 +
450 + } else if (data instanceof Blob) {
451 + // Blob - convert to ArrayBuffer
452 + this._bufferedAmount += data.size;
453 +
454 + data.arrayBuffer().then(buffer => {
455 + this._sendMessage(new Uint8Array(buffer), true);
456 + this._bufferedAmount = Math.max(0, this._bufferedAmount - data.size);
457 + });
458 +
459 + } else {
460 + throw new TypeError('Data must be string, ArrayBuffer, Uint8Array, or Blob');
461 + }
462 + }
463 +
464 + /**
465 + * Send message through WASM ProxyEngine
466 + * @private
467 + * @param {string|Uint8Array} data - Data to send
468 + * @param {boolean} isBinary - Whether data is binary
469 + */
470 + async _sendMessage(data, isBinary) {
471 + try {
472 + // Estimate buffer size
473 + const size = typeof data === 'string' ? data.length : data.length;
474 + this._bufferedAmount += size;
475 +
476 + if (this._useServiceWorker) {
477 + // Send via Service Worker
478 + const channel = new MessageChannel();
479 + await new Promise((resolve, reject) => {
480 + channel.port1.onmessage = (event) => {
481 + if (event.data.success) {
482 + resolve();
483 + } else {
484 + reject(new Error(event.data.error));
485 + }
486 + };
487 +
488 + navigator.serviceWorker.controller.postMessage(
489 + {
490 + type: 'WEBSOCKET_SEND',
491 + tunnelId: this._tunnelId,
492 + data: data,
493 + isBinary: isBinary
494 + },
495 + [channel.port2]
496 + );
497 +
498 + setTimeout(() => reject(new Error('Send timeout')), 5000);
499 + });
500 + } else {
501 + // Send via direct WASM
502 + const { engine } = await getProxyEngine();
503 + await engine.send_websocket_message(this._tunnelId, data, isBinary);
504 + }
505 +
506 + // Decrement buffered amount
507 + this._bufferedAmount = Math.max(0, this._bufferedAmount - size);
508 +
509 + } catch (error) {
510 + console.error('[SecureWebSocket] Send failed:', error);
511 +
512 + this._dispatchEvent('error', {
513 + message: error.toString(),
514 + error: error
515 + });
516 + }
517 + }
518 +
519 + /**
520 + * Close the WebSocket connection
521 + * @param {number} code - Close code (default 1000)
522 + * @param {string} reason - Close reason (default empty)
523 + */
524 + close(code = 1000, reason = '') {
525 + if (this._readyState === WebSocket.CLOSED || this._readyState === WebSocket.CLOSING) {
526 + return;
527 + }
528 +
529 + console.log('[SecureWebSocket] Closing:', code, reason);
530 +
531 + this._readyState = WebSocket.CLOSING;
532 +
533 + // Close tunnel
534 + (async () => {
535 + try {
536 + if (this._useServiceWorker) {
537 + // Close via Service Worker
538 + const channel = new MessageChannel();
539 + await new Promise((resolve, reject) => {
540 + channel.port1.onmessage = (event) => {
541 + if (event.data.success) {
542 + resolve();
543 + } else {
544 + reject(new Error(event.data.error));
545 + }
546 + };
547 +
548 + navigator.serviceWorker.controller.postMessage(
549 + {
550 + type: 'WEBSOCKET_CLOSE',
551 + tunnelId: this._tunnelId,
552 + code: code,
553 + reason: reason
554 + },
555 + [channel.port2]
556 + );
557 +
558 + setTimeout(() => resolve(), 2000); // Don't wait forever
559 + });
560 + } else {
561 + // Close via direct WASM
562 + const { engine } = await getProxyEngine();
563 + await engine.close_websocket(this._tunnelId, code, reason);
564 + }
565 + } catch (error) {
566 + console.error('[SecureWebSocket] Close failed:', error);
567 +
568 + // Force close
569 + this._readyState = WebSocket.CLOSED;
570 + this._dispatchEvent('close', {
571 + code: 1006,
572 + reason: error.toString(),
573 + wasClean: false
574 + });
575 + }
576 + })();
577 + }
578 +
579 + /**
580 + * Dispatch event to both EventTarget and legacy handler
581 + * @private
582 + * @param {string} type - Event type
583 + * @param {Object} detail - Event details
584 + */
585 + _dispatchEvent(type, detail) {
586 + // Create event
587 + const event = new Event(type);
588 + Object.assign(event, detail);
589 +
590 + // Dispatch to EventTarget listeners
591 + this.dispatchEvent(event);
592 +
593 + // Call legacy handler if exists
594 + const handler = this[`on${type}`];
595 + if (typeof handler === 'function') {
596 + try {
597 + handler.call(this, event);
598 + } catch (error) {
599 + console.error(`[SecureWebSocket] Error in on${type} handler:`, error);
600 + }
601 + }
602 + }
603 +}
604 +
605 +// Static constants (same as native WebSocket)
606 +SecureWebSocket.CONNECTING = 0;
607 +SecureWebSocket.OPEN = 1;
608 +SecureWebSocket.CLOSING = 2;
609 +SecureWebSocket.CLOSED = 3;
610 +
611 +// ==============================================================================
612 +// POLYFILL: Replace native WebSocket with SecureWebSocket
613 +// ==============================================================================
614 +
615 +(function() {
616 + // Save reference to native WebSocket
617 + const NativeWebSocket = window.WebSocket;
618 +
619 + // Configuration
620 + const config = {
621 + // Enable E2EE for all WebSockets by default
622 + enabled: window.RELAYDNS_E2EE_ENABLED !== false,
623 +
624 + // Patterns to intercept (regex strings)
625 + interceptPatterns: window.RELAYDNS_INTERCEPT_PATTERNS || [
626 + '.*' // Intercept all by default
627 + ],
628 +
629 + // Patterns to bypass (regex strings) - takes precedence
630 + bypassPatterns: window.RELAYDNS_BYPASS_PATTERNS || [
631 + '^wss?://localhost:4017/', // Don't intercept relay server itself
632 + '^wss?://localhost:8000/', // Don't intercept local dev server
633 + '^wss?://127\\.0\\.0\\.1', // Don't intercept loopback
634 + ],
635 +
636 + // Debug mode
637 + debug: window.RELAYDNS_DEBUG || false
638 + };
639 +
640 + /**
641 + * Check if URL should be intercepted for E2EE
642 + * @param {string} url - WebSocket URL
643 + * @returns {boolean} True if should intercept
644 + */
645 + function shouldIntercept(url) {
646 + if (!config.enabled) {
647 + return false;
648 + }
649 +
650 + // Check bypass patterns first (higher priority)
651 + for (const pattern of config.bypassPatterns) {
652 + const regex = new RegExp(pattern);
653 + if (regex.test(url)) {
654 + if (config.debug) {
655 + console.log('[SecureWebSocket] Bypassing (matched bypass pattern):', url);
656 + }
657 + return false;
658 + }
659 + }
660 +
661 + // Check intercept patterns
662 + for (const pattern of config.interceptPatterns) {
663 + const regex = new RegExp(pattern);
664 + if (regex.test(url)) {
665 + if (config.debug) {
666 + console.log('[SecureWebSocket] Intercepting (matched intercept pattern):', url);
667 + }
668 + return true;
669 + }
670 + }
671 +
672 + if (config.debug) {
673 + console.log('[SecureWebSocket] Not intercepting (no match):', url);
674 + }
675 + return false;
676 + }
677 +
678 + /**
679 + * Polyfilled WebSocket constructor
680 + * @param {string} url - WebSocket URL
681 + * @param {string|string[]} protocols - Optional subprotocols
682 + * @returns {WebSocket|SecureWebSocket}
683 + */
684 + window.WebSocket = function(url, protocols) {
685 + if (shouldIntercept(url)) {
686 + // Use E2EE SecureWebSocket
687 + console.log('[SecureWebSocket] 🔒 Creating encrypted WebSocket:', url);
688 + return new SecureWebSocket(url, protocols);
689 + } else {
690 + // Use native WebSocket
691 + if (config.debug) {
692 + console.log('[SecureWebSocket] Creating native WebSocket:', url);
693 + }
694 + return new NativeWebSocket(url, protocols);
695 + }
696 + };
697 +
698 + // Copy static properties from native WebSocket
699 + window.WebSocket.CONNECTING = NativeWebSocket.CONNECTING;
700 + window.WebSocket.OPEN = NativeWebSocket.OPEN;
701 + window.WebSocket.CLOSING = NativeWebSocket.CLOSING;
702 + window.WebSocket.CLOSED = NativeWebSocket.CLOSED;
703 +
704 + // Expose SecureWebSocket class for direct access if needed
705 + window.SecureWebSocket = SecureWebSocket;
706 + window.NativeWebSocket = NativeWebSocket;
707 +
708 + console.log('[SecureWebSocket] ✅ Polyfill installed. E2EE enabled:', config.enabled);
709 +
710 +})();
relaydns/wasm/sw-proxy.js
+149 -24
@@ -4,34 +4,110 @@
4 const CACHE_NAME = 'relaydns-proxy-v1';
5 let proxyEngine = null;
6 let wasmReady = false;
7 +let initializationPromise = null;
8
8 -// Install event - Load WASM here (only time importScripts is allowed)
9 +// Dynamic WASM initialization (can be called anytime, not just install)
10 +async function initializeProxyEngine() {
11 + // If already initializing, wait for it
12 + if (initializationPromise) {
13 + return initializationPromise;
14 + }
15 +
16 + // If already initialized, return immediately
17 + if (proxyEngine && wasmReady) {
18 + return proxyEngine;
19 + }
20 +
21 + initializationPromise = (async () => {
22 + try {
23 + console.log('[SW-Proxy] Dynamically initializing ProxyEngine...');
24 +
25 + // Check if wasm_bindgen is available (loaded during install)
26 + if (typeof wasm_bindgen === 'undefined') {
27 + console.log('[SW-Proxy] wasm_bindgen not available, loading via dynamic import...');
28 +
29 + // Use dynamic import for ES6 modules
30 + const wasmModule = await import('/pkg/relaydns_wasm.js');
31 +
32 + // Make wasm_bindgen available globally
33 + self.wasm_bindgen = wasmModule;
34 +
35 + console.log('[SW-Proxy] ✓ WASM JS module loaded');
36 + }
37 +
38 + // Check if SecureWebSocket SW module is loaded
39 + if (typeof ServiceWorkerWebSocketTunnel === 'undefined') {
40 + console.log('[SW-Proxy] SecureWebSocket SW module not available, loading via fetch...');
41 +
42 + const swWsResponse = await fetch('/secure-websocket-sw.js');
43 + const swWsCode = await swWsResponse.text();
44 +
45 + // Use indirect eval to execute in global scope
46 + (1, eval)(swWsCode);
47 + console.log('[SW-Proxy] ✓ SecureWebSocket SW module loaded');
48 + }
49 +
50 + // Initialize WASM module if not already done
51 + if (!wasmReady) {
52 + console.log('[SW-Proxy] Initializing WASM module...');
53 +
54 + // wasm_bindgen is now the module object from dynamic import
55 + const initWasm = self.wasm_bindgen.default || self.wasm_bindgen;
56 + await initWasm('/pkg/relaydns_wasm_bg.wasm');
57 +
58 + console.log('[SW-Proxy] ✓ WASM module initialized');
59 + }
60 +
61 + // Get relay URL from API
62 + let relayUrl = 'ws://localhost:4017/relay'; // Default fallback
63 + try {
64 + const relayInfoResponse = await fetch('/api/relay-info');
65 + if (relayInfoResponse.ok) {
66 + const relayInfo = await relayInfoResponse.json();
67 + if (relayInfo.relayUrl) {
68 + relayUrl = relayInfo.relayUrl;
69 + console.log('[SW-Proxy] Got relay URL from server:', relayUrl);
70 + }
71 + }
72 + } catch (e) {
73 + console.warn('[SW-Proxy] Failed to fetch relay URL, using default:', e.message);
74 + }
75 +
76 + // Create ProxyEngine
77 + console.log('[SW-Proxy] Creating ProxyEngine with URL:', relayUrl);
78 + const ProxyEngine = self.wasm_bindgen.ProxyEngine;
79 + proxyEngine = new ProxyEngine(relayUrl);
80 + wasmReady = true;
81 +
82 + console.log('[SW-Proxy] ✓ ProxyEngine ready');
83 + return proxyEngine;
84 +
85 + } catch (error) {
86 + console.error('[SW-Proxy] Failed to initialize ProxyEngine:', error);
87 + initializationPromise = null; // Reset so we can retry
88 + wasmReady = false;
89 + throw error;
90 + }
91 + })();
92 +
93 + return initializationPromise;
94 +}
95 +
96 +// Install event - Skip importScripts since WASM is ES6 module
97 self.addEventListener('install', (event) => {
98 console.log('[SW-Proxy] Installing...');
99
100 event.waitUntil(
101 (async () => {
102 try {
15 - console.log('[SW-Proxy] Loading WASM module during install...');
16 -
17 - // importScripts can ONLY be called during install
18 - self.importScripts('/pkg/relaydns_wasm_sw.js');
19 - console.log('[SW-Proxy] ✓ WASM script loaded');
103 + console.log('[SW-Proxy] Service Worker installing...');
104
21 - // Initialize WASM
22 - console.log('[SW-Proxy] Initializing WASM...');
23 - await wasm_bindgen('/pkg/relaydns_wasm_sw_bg.wasm');
24 - console.log('[SW-Proxy] ✓ WASM initialized');
25 -
26 - // Create proxy engine
27 - console.log('[SW-Proxy] Creating ProxyEngine...');
28 - proxyEngine = new wasm_bindgen.ProxyEngine('ws://localhost:4017/relay');
29 - wasmReady = true;
30 - console.log('[SW-Proxy] ✓ Proxy engine ready');
105 + // Note: We cannot use importScripts with ES6 modules
106 + // WASM will be loaded dynamically on first request via fetch + dynamic import
107 + console.log('[SW-Proxy] WASM will be loaded dynamically on first request');
108
109 } catch (error) {
33 - console.error('[SW-Proxy] Failed to initialize WASM:', error);
34 - throw error;
110 + console.error('[SW-Proxy] Install error:', error);
111 }
112
113 // Skip waiting to activate immediately
@@ -64,9 +140,31 @@ function shouldProxy(url) {
140 // Handle HTTP request through WASM proxy
141 async function proxyHttpRequest(request) {
142 try {
143 + // Ensure ProxyEngine is initialized (lazy init if needed)
144 if (!wasmReady || !proxyEngine) {
68 - console.warn('[SW-Proxy] WASM not ready, falling back to direct fetch');
69 - return fetch(request);
145 + console.log('[SW-Proxy] ProxyEngine not ready, attempting lazy initialization...');
146 + try {
147 + await initializeProxyEngine();
148 + } catch (initError) {
149 + // Security: Never fallback to direct fetch - this would bypass E2EE!
150 + console.error('[SW-Proxy] Failed to initialize ProxyEngine:', initError);
151 + return new Response(
152 + JSON.stringify({
153 + error: 'E2EE ProxyEngine Initialization Failed',
154 + message: 'Could not initialize secure proxy. Please refresh the page.',
155 + code: 'PROXY_ENGINE_INIT_FAILED',
156 + details: initError.message
157 + }),
158 + {
159 + status: 503,
160 + statusText: 'Service Unavailable',
161 + headers: {
162 + 'Content-Type': 'application/json',
163 + 'X-E2EE-Status': 'init-failed'
164 + }
165 + }
166 + );
167 + }
168 }
169
170 console.log('[SW-Proxy] Proxying:', request.method, request.url);
@@ -113,9 +211,22 @@ async function proxyHttpRequest(request) {
211
212 } catch (error) {
213 console.error('[SW-Proxy] Proxy error:', error);
116 - // Fallback to direct fetch on error
117 - console.log('[SW-Proxy] Falling back to direct fetch');
118 - return fetch(request);
214 + // Security: Never fallback to direct fetch - return error instead
215 + return new Response(
216 + JSON.stringify({
217 + error: 'E2EE Proxy Error',
218 + message: error.message || 'Failed to proxy request through E2EE tunnel',
219 + code: 'PROXY_ERROR'
220 + }),
221 + {
222 + status: 502,
223 + statusText: 'Bad Gateway',
224 + headers: {
225 + 'Content-Type': 'application/json',
226 + 'X-E2EE-Status': 'error'
227 + }
228 + }
229 + );
230 }
231 }
232
@@ -137,13 +248,27 @@ self.addEventListener('fetch', (event) => {
248 self.addEventListener('message', (event) => {
249 const { type } = event.data || {};
250
251 + // Try WebSocket handler first
252 + if (typeof handleWebSocketMessage === 'function') {
253 + const handled = handleWebSocketMessage(event);
254 + if (handled instanceof Promise) {
255 + // Async handler
256 + return;
257 + } else if (handled) {
258 + // Synchronously handled
259 + return;
260 + }
261 + }
262 +
263 + // Standard message handling
264 switch (type) {
265 case 'GET_STATUS':
266 event.ports[0]?.postMessage({
267 success: true,
268 status: {
269 wasmReady,
146 - hasEngine: !!proxyEngine
270 + hasEngine: !!proxyEngine,
271 + hasWebSocket: typeof handleWebSocketMessage === 'function'
272 }
273 });
274 break;