ipv6-bridge 1.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/API.md ADDED
@@ -0,0 +1,365 @@
1
+ # API Reference
2
+
3
+ Complete API documentation for IPv6 Bridge.
4
+
5
+ ## Embedded API
6
+
7
+ These let an application use DNS64/NAT64 translation directly, with no proxy
8
+ and no system configuration. Every outbound connection tries native IPv6 first,
9
+ then a NAT64-synthesized address, then direct IPv4.
10
+
11
+ None of this can create connectivity the host does not have.
12
+
13
+ ### `createAgent(options?)`
14
+
15
+ An `http.Agent` that resolves through DNS64, fails over across candidate
16
+ addresses, and pools sockets keyed by the original hostname.
17
+
18
+ ```javascript
19
+ const { createAgent } = require('ipv6-bridge');
20
+ http.get('http://example.com', { agent: createAgent() }, handler);
21
+ ```
22
+
23
+ **Parameters:** any `http.Agent` option. Defaults: `keepAlive: true`,
24
+ `keepAliveMsecs` from `IPV6_KEEP_ALIVE_MS`, `maxSockets` from
25
+ `IPV6_MAX_SOCKETS_PER_HOST`.
26
+
27
+ **Returns:** `http.Agent`
28
+
29
+ ### `createHttpsAgent(options?)`
30
+
31
+ The same, performing the TLS handshake over the bridged socket.
32
+
33
+ ```javascript
34
+ const { createHttpsAgent } = require('ipv6-bridge');
35
+ https.get('https://example.com', { agent: createHttpsAgent() }, handler);
36
+ ```
37
+
38
+ The certificate is validated against the **requested hostname**, never the
39
+ synthesized address the connection travelled over, so certificate verification
40
+ works normally and must not be disabled.
41
+
42
+ **Returns:** `https.Agent`
43
+
44
+ ### `createAgents(options?)`
45
+
46
+ Both at once, for clients that take a pair.
47
+
48
+ **Returns:** `{ http: http.Agent, https: https.Agent }`
49
+
50
+ ```javascript
51
+ const agents = createAgents();
52
+ axios.create({ httpAgent: agents.http, httpsAgent: agents.https });
53
+ ```
54
+
55
+ ### `createLookup()`
56
+
57
+ A `dns.lookup`-compatible function applying DNS64 synthesis. Usable anywhere a
58
+ `lookup` option is accepted.
59
+
60
+ ```javascript
61
+ net.connect({ host: 'db.example', port: 5432, lookup: createLookup() });
62
+ ```
63
+
64
+ Supports the `all` and `family` options. Lighter-touch than an agent, but it
65
+ only changes resolution — no failover or pooling.
66
+
67
+ **Returns:** `(hostname, options, callback) => void`
68
+
69
+ ### `createConnector()`
70
+
71
+ A connector for undici, and therefore Node's global `fetch`. undici is not a
72
+ dependency; this is for projects that already use it.
73
+
74
+ ```javascript
75
+ const { Agent, setGlobalDispatcher } = require('undici');
76
+ setGlobalDispatcher(new Agent({ connect: createConnector() }));
77
+ ```
78
+
79
+ **Returns:** `(options, callback) => void`
80
+
81
+ ### `resolve(hostname)`
82
+
83
+ Resolve the way the bridge would, without connecting. Useful for logging and
84
+ assertions.
85
+
86
+ ```javascript
87
+ await resolve('8.8.8.8');
88
+ // → [ { host: '64:ff9b::808:808', family: 6, mode: 'nat64' },
89
+ // { host: '8.8.8.8', family: 4, mode: 'direct-ipv4' } ]
90
+ ```
91
+
92
+ **Returns:** `Promise<Array<{host: string, family: number, mode: string}>>`
93
+
94
+ ### `getStats()`
95
+
96
+ A snapshot of counters, routing modes, DNS cache statistics and the active
97
+ prefix — the same data the proxy serves at `/status`, available to embedded
98
+ users.
99
+
100
+ ```javascript
101
+ const stats = getStats();
102
+ stats.translationRate; // 0 means nothing is being translated
103
+ stats.routes.directIpv4Fallback; // connections that could not be translated
104
+ ```
105
+
106
+ **Returns:** `object`
107
+
108
+ ### `discoverPrefix()`
109
+
110
+ Run RFC 7050 discovery and report the network's NAT64 prefix, without changing
111
+ configuration.
112
+
113
+ **Returns:** `Promise<{prefix, length, bytes, source}|null>`
114
+
115
+ ---
116
+
117
+ ## Proxy API
118
+
119
+ ### `start(port?, options?)`
120
+
121
+ Starts the IPv6 Bridge proxy server.
122
+
123
+ ```javascript
124
+ const { start, stop } = require('ipv6-bridge');
125
+
126
+ const server = await start(8080);
127
+ ```
128
+
129
+ **Parameters:**
130
+ - `port` (number, default: `8080`) — Port to listen on.
131
+ - `options.host` (string, default: `127.0.0.1`) — Interface to bind to.
132
+ - `options.force` (boolean) — Start even if detection says the bridge isn't needed.
133
+ - `options.discoverPrefix` (boolean, default: `true`) — Run RFC 7050 prefix discovery.
134
+ - `options.socksPort` (number|null) — Also start a SOCKS5 listener on this port.
135
+
136
+ **Returns:** `Promise<http.Server | null>`
137
+ - Returns the HTTP server instance if the bridge started.
138
+ - Returns `null` if the bridge was not needed (IPv4 works, or NAT64 already works).
139
+
140
+ **Throws:** `Error` if the bridge is already running or startup fails.
141
+
142
+ **Behavior:**
143
+ 1. Runs network detection (`needsBridge()`).
144
+ 2. If the bridge is not needed and neither `options.force` nor `FORCE_BRIDGE` is set, returns `null`.
145
+ 3. Optionally discovers the network's NAT64 prefix (RFC 7050).
146
+ 4. Creates and starts the proxy, plus a SOCKS5 listener if configured.
147
+
148
+ Concurrent calls are safe: a second `start()` while one is in flight rejects rather than leaving an untracked server running.
149
+
150
+ ---
151
+
152
+ ### `stop()`
153
+
154
+ Stops the running bridge and any SOCKS5 listener, tearing down live connections.
155
+
156
+ ```javascript
157
+ await stop();
158
+ ```
159
+
160
+ Open CONNECT tunnels are destroyed rather than waited on, so `stop()` always resolves — important when the bridge is started and stopped inside a test suite.
161
+
162
+ Safe to call when nothing is running.
163
+
164
+ ---
165
+
166
+ ## CLI
167
+
168
+ ```bash
169
+ # Start on the default port (8080)
170
+ npx ipv6-bridge start
171
+
172
+ # Custom port
173
+ IPV6_BRIDGE_PORT=9090 npx ipv6-bridge start
174
+
175
+ # Force start even if the bridge is not needed
176
+ FORCE_BRIDGE=1 npx ipv6-bridge start
177
+
178
+ # Diagnose this network
179
+ npx ipv6-bridge doctor
180
+
181
+ # Show help
182
+ npx ipv6-bridge --help
183
+ ```
184
+
185
+ Stop the bridge with `Ctrl+C` or by sending `SIGTERM`.
186
+
187
+ ### `doctor`
188
+
189
+ Runs diagnostics and explains what it found:
190
+
191
+ - whether the system resolver and direct DNS queries agree
192
+ - whether IPv4, IPv6 and an upstream NAT64 gateway are reachable
193
+ - which NAT64 prefix is configured, and which one the network advertises
194
+ - whether the listener is exposed without access control
195
+
196
+ Exits non-zero if any check fails, so it can be used in provisioning scripts.
197
+
198
+ ---
199
+
200
+ ## Configuration
201
+
202
+ ### Environment Variables
203
+
204
+ | Variable | Default | Description |
205
+ |----------|---------|-------------|
206
+ | `IPV6_BRIDGE_PORT` | `8080` | Port for the proxy server |
207
+ | `IPV6_BRIDGE_HOST` | `127.0.0.1` | Interface to bind to |
208
+ | `IPV6_BRIDGE_SOCKS_PORT` | _(off)_ | Serve SOCKS5 on this port |
209
+ | `IPV6_BRIDGE_AUTH` | _(none)_ | Require `user:password` from clients |
210
+ | `IPV6_BRIDGE_ALLOW` | _(any)_ | Client allowlist, e.g. `192.168.1.0/24` |
211
+ | `IPV6_BRIDGE_BYPASS` | _(none)_ | Hosts to reach directly, e.g. `*.internal.com` |
212
+ | `IPV6_BRIDGE_CONTROL` | `on` | Serve `/healthz`, `/status`, `/metrics`, `/proxy.pac` |
213
+ | `IPV6_BRIDGE_DISCOVER_PREFIX` | `on` | Discover the NAT64 prefix via RFC 7050 |
214
+ | `FORCE_BRIDGE` | _(unset)_ | Start the bridge even when detection says it isn't needed |
215
+ | `NAT64_PREFIX` | `64:ff9b::/96` | NAT64 prefix, with an optional `/length` |
216
+ | `IPV6_DNS_TIMEOUT` | `5000` | DNS resolution timeout (ms) |
217
+ | `IPV6_DNS_CACHE_TTL` | `30000` | DNS cache entry lifetime (ms) |
218
+ | `IPV6_DNS_CACHE_MAX` | `1000` | Maximum cached DNS entries |
219
+ | `IPV6_CONN_TIMEOUT` | `10000` | Proxy connection timeout (ms) |
220
+ | `IPV6_CONNECT_ATTEMPT_TIMEOUT` | `3000` | Timeout per candidate address before trying the next |
221
+ | `IPV6_KEEP_ALIVE_MS` | `15000` | Idle lifetime of pooled upstream sockets |
222
+ | `IPV6_MAX_SOCKETS_PER_HOST` | `64` | Maximum pooled sockets per upstream host |
223
+ | `IPV4_TEST_URL` | `http://ipv4.google.com` | Endpoint used to detect working IPv4 |
224
+ | `IPV6_TEST_URL` | `http://ipv6.google.com` | Endpoint used to detect working IPv6 |
225
+ | `NAT64_TEST_HOST` | `ipv4.google.com` | IPv4-only host used to probe for NAT64 |
226
+ | `LOG_LEVEL` | `info` | `silent`, `error`, `warn`, `info` or `debug` |
227
+
228
+ Invalid values are rejected at startup with an explanatory message rather than causing confusing failures later.
229
+
230
+ ### NAT64 prefix formats
231
+
232
+ `NAT64_PREFIX` accepts any prefix length RFC 6052 defines — `/32`, `/40`, `/48`, `/56`, `/64` or `/96`. Without a length, `/96` is assumed.
233
+
234
+ ```bash
235
+ NAT64_PREFIX=64:ff9b::/96 # well-known prefix (default)
236
+ NAT64_PREFIX=2001:db8:122:344::/64 # operator-assigned prefix
237
+ ```
238
+
239
+ The well-known prefix `64:ff9b::` is only valid at `/96`, and per RFC 6052 section 3.1 it is never used to carry non-global IPv4 addresses.
240
+
241
+ ---
242
+
243
+ ## Operational endpoints
244
+
245
+ When `IPV6_BRIDGE_CONTROL` is on, the proxy answers these paths directly (as ordinary origin-form requests, not proxied ones):
246
+
247
+ | Path | Description |
248
+ |------|-------------|
249
+ | `/healthz` | Liveness check. Always reachable, even when authentication is enabled. |
250
+ | `/status` | JSON snapshot of counters, routing modes, DNS cache and active prefix. |
251
+ | `/metrics` | The same data in Prometheus text exposition format. |
252
+ | `/proxy.pac` | A proxy auto-configuration file pointing clients at the bridge. |
253
+
254
+ The most useful field in `/status` is `translationRate`: the share of connections that actually went through NAT64. A rate of `0` with a rising `routes.directIpv4Fallback` means the bridge is not translating anything.
255
+
256
+ ```bash
257
+ curl http://127.0.0.1:8080/status
258
+ ```
259
+
260
+ ---
261
+
262
+ ## Internal Modules
263
+
264
+ ### `dns64.js`
265
+
266
+ #### `resolveCandidates(hostname)`
267
+
268
+ Resolves a hostname into an ordered list of connection candidates, most preferred first: native IPv6, then NAT64-synthesized, then direct IPv4.
269
+
270
+ **Returns:** `Promise<Array<{host: string, family: number, mode: string}>>`
271
+
272
+ #### `resolveIPv6(hostname)`
273
+
274
+ Resolves a hostname to IPv6 addresses using DNS64.
275
+
276
+ **Returns:** `Promise<string[]>`
277
+
278
+ #### `ipv4ToIPv6(ipv4)` / `ipv6ToIPv4(address)`
279
+
280
+ Converts between an IPv4 address and its IPv4-embedded IPv6 form using the active prefix.
281
+
282
+ ```javascript
283
+ ipv4ToIPv6('192.0.2.1'); // → '64:ff9b::c000:201'
284
+ ipv6ToIPv4('64:ff9b::c000:201'); // → '192.0.2.1'
285
+ ```
286
+
287
+ Output is RFC 5952 canonical form, so leading zeros are suppressed.
288
+
289
+ #### `isGlobalIPv4(address)` / `canSynthesize(address)`
290
+
291
+ Whether an address is globally routable, and whether it may be synthesized with the active prefix.
292
+
293
+ #### `detectIPVersion(addr)`
294
+
295
+ **Returns:** `'ipv4'` | `'ipv6'` | `'hostname'` | `null`
296
+
297
+ ---
298
+
299
+ ### `ipv6.js`
300
+
301
+ Address primitives: `parseIPv6`, `formatIPv6`, `parsePrefix`, `embedIPv4`, `extractIPv4`. Implements the RFC 6052 section 2.2 embedding rules for every prefix length, including the reserved `u` octet at bits 64–71.
302
+
303
+ ---
304
+
305
+ ### `discovery.js`
306
+
307
+ #### `discoverPrefix()`
308
+
309
+ Discovers the network's NAT64 prefix per RFC 7050 by resolving `ipv4only.arpa` and looking for its known IPv4 addresses inside the synthesized AAAA records.
310
+
311
+ **Returns:** `Promise<{prefix, length, bytes, source}|null>`
312
+
313
+ ---
314
+
315
+ ### `detect.js`
316
+
317
+ #### `hasIPv4()` / `hasIPv6()` / `hasWorkingNAT64()`
318
+
319
+ Individual reachability probes. Any 2xx or 3xx response counts as reachable.
320
+
321
+ #### `needsBridge()`
322
+
323
+ Returns `true` only when IPv4 is unreachable, IPv6 works, and no upstream NAT64 gateway responds.
324
+
325
+ ---
326
+
327
+ ### `socks5.js`
328
+
329
+ #### `createSocksServer(port, host?)`
330
+
331
+ Starts a SOCKS5 listener (RFC 1928) supporting the CONNECT command, with optional username/password authentication (RFC 1929). Lets non-HTTP protocols — ssh, git, database clients — use the same DNS64/NAT64 translation.
332
+
333
+ ---
334
+
335
+ ## Troubleshooting
336
+
337
+ Run `npx ipv6-bridge doctor` first; it checks everything below automatically.
338
+
339
+ ### Bridge says "not needed" but I want to test it
340
+
341
+ ```bash
342
+ FORCE_BRIDGE=1 npx ipv6-bridge start
343
+ ```
344
+
345
+ ### Everything returns 502, or nothing seems translated
346
+
347
+ Check `/status`. If `translationRate` is `0` and `routes.directIpv4Fallback` is climbing, DNS64 is failing and the bridge is passing traffic through untranslated. Common causes:
348
+
349
+ - The network has no NAT64 gateway. Confirm with `doctor`.
350
+ - The NAT64 prefix is wrong. `doctor` reports the prefix the network advertises.
351
+
352
+ ### DNS resolution fails
353
+
354
+ - The bridge uses the system resolver, so if `ping example.com` fails, so will the bridge.
355
+ - `dns.resolve` failing while `dns.lookup` works is normal on DoH-only hosts and does not affect the bridge.
356
+
357
+ ### Connection timeouts
358
+
359
+ - Each candidate address gets `IPV6_CONNECT_ATTEMPT_TIMEOUT` (3s) before the next is tried.
360
+ - On a dual-stack network with `FORCE_BRIDGE`, expect a delay while the unreachable NAT64 route times out before falling back.
361
+
362
+ ### Port already in use
363
+
364
+ - Change the port with `IPV6_BRIDGE_PORT=9090 npx ipv6-bridge start`.
365
+ - Check what's using it: `lsof -i :8080` (macOS/Linux) or `netstat -ano | findstr 8080` (Windows).
@@ -0,0 +1,218 @@
1
+ # Architecture
2
+
3
+ This document explains the technical architecture of IPv6 Bridge, focusing on why DNS64 and application-level NAT64 are used and how they work together.
4
+
5
+ ## DNS64 vs NAT64
6
+
7
+ ### DNS64 (RFC 6147)
8
+
9
+ DNS64 translates domain names to IPv6 addresses by synthesizing them from IPv4 records. The address format itself — how the 32 bits of IPv4 are embedded into an IPv6 address — is defined by RFC 6052.
10
+
11
+ ```
12
+ Query: example.com
13
+ → DNS returns IPv4: 192.0.2.1
14
+ → DNS64 synthesizes IPv6: 64:ff9b::c000:0201
15
+ → Application gets a routable IPv6 address
16
+ ```
17
+
18
+ **Implementation:** `src/dns64.js`
19
+
20
+ 1. Look the hostname up through the system resolver
21
+ 2. Use native AAAA records if any exist
22
+ 3. Otherwise synthesize IPv6 from the A records using the NAT64 prefix
23
+
24
+ ### NAT64 (RFC 6146)
25
+
26
+ NAT64 translates packets between IPv6 and IPv4 at the network level. There are two complementary approaches:
27
+
28
+ - **ISP-level NAT64**: The ISP operates a gateway that recognizes the `64:ff9b::` prefix and translates packets automatically.
29
+ - **Application-level NAT64** (this project): An HTTP/HTTPS proxy intercepts requests, applies DNS64, and routes traffic through IPv6 so the ISP gateway can translate it.
30
+
31
+ Both approaches work together. If the ISP provides NAT64, the bridge is optional. If not, the bridge provides application-level translation.
32
+
33
+ ## Why the system resolver, not `dns.resolve*`
34
+
35
+ `src/dns64.js` resolves names with `dns.lookup`, which calls the operating system's resolver (`getaddrinfo`).
36
+
37
+ The obvious alternative, `dns.resolve4` / `dns.resolve6`, talks directly to DNS servers over port 53 and ignores the hosts file, mDNS, DNS-over-HTTPS, and split-DNS configuration. On a host configured for DoH-only resolution — increasingly common, and exactly the kind of modern network this project targets — `dns.resolve*` fails outright with `ECONNREFUSED` even though ordinary name resolution works perfectly.
38
+
39
+ Using the system resolver means the bridge resolves names the same way every other program on the machine does.
40
+
41
+ ## Address selection rules
42
+
43
+ RFC 6052 section 3.1 forbids representing **non-global** IPv4 addresses with the well-known prefix `64:ff9b::/96`. A NAT64 gateway will not route `64:ff9b::7f00:1` anywhere useful, because `127.0.0.1` is meaningless outside the local host.
44
+
45
+ The bridge therefore classifies every IPv4 address before synthesizing:
46
+
47
+ | Address | Action |
48
+ |---------|--------|
49
+ | Global (e.g. `8.8.8.8`) | Synthesize `64:ff9b::0808:0808` and route via NAT64 |
50
+ | Non-global (e.g. `127.0.0.1`, `192.168.1.5`, `10.0.0.2`) | Connect directly over IPv4 |
51
+
52
+ When an operator-assigned network-specific prefix (NSP) is configured instead of the well-known prefix, the section 3.1 restriction does not apply and all addresses are synthesized.
53
+
54
+ The non-global ranges are those listed in RFC 6890: `0.0.0.0/8`, `10/8`, `100.64/10`, `127/8`, `169.254/16`, `172.16/12`, `192.0.0/24`, `192.0.2/24`, `192.88.99/24`, `192.168/16`, `198.18/15`, `198.51.100/24`, `203.0.113/24`, `224/4` and `240/4`.
55
+
56
+ ## End-to-End Flow
57
+
58
+ ```
59
+ Step 1: Browser sends GET http://google.com/ via proxy (localhost:8080)
60
+
61
+ Step 2: proxy.js parses the absolute-form request target (RFC 7230 5.3.2)
62
+ → "GET http://google.com/ HTTP/1.1" yields hostname "google.com"
63
+
64
+ Step 3: dns64.js resolves the hostname through the system resolver
65
+ ├─ No AAAA records (IPv4-only site)
66
+ ├─ A record: 142.251.32.14
67
+ ├─ Address is global, so synthesis is permitted
68
+ └─ Synthesize: 64:ff9b::8efb:200e
69
+
70
+ Step 4: proxy.js strips hop-by-hop headers and connects outbound to
71
+ 64:ff9b::8efb:200e (family: 6)
72
+
73
+ Step 5: ISP NAT64 gateway recognizes 64:ff9b:: prefix
74
+ → Extracts 142.251.32.14
75
+ → Forwards request over IPv4
76
+
77
+ Step 6: Google responds → NAT64 gateway translates back to IPv6
78
+
79
+ Step 7: proxy.js pipes response to the client
80
+ ```
81
+
82
+ ## Request handling
83
+
84
+ The proxy serves two distinct paths:
85
+
86
+ **Plain HTTP** — clients configured to use a forward proxy send an *absolute-form* request target (`GET http://example.com/path HTTP/1.1`) per RFC 7230 section 5.3.2. The proxy parses that directly, and accepts origin-form (`GET /path` plus a `Host` header) as a fallback for gateway-style use.
87
+
88
+ **HTTPS via CONNECT** — the client sends `CONNECT example.com:443`, the proxy opens a TCP tunnel to the resolved address and relays bytes in both directions without inspecting them. Authority parsing handles bracketed IPv6 literals (`[2001:db8::1]:443`), bare IPv6 literals, and `host:port`.
89
+
90
+ ### Header handling
91
+
92
+ Hop-by-hop headers are removed in both directions per RFC 7230 section 6.1: `Connection`, `Proxy-Connection`, `Keep-Alive`, `Proxy-Authenticate`, `Proxy-Authorization`, `TE`, `Trailer`, `Transfer-Encoding` and `Upgrade`, plus any header named inside the `Connection` header.
93
+
94
+ `Proxy-Authorization` matters most: it carries credentials meant for the proxy itself, and forwarding it would leak them to every origin server the client visits.
95
+
96
+ ### Failure reporting
97
+
98
+ When DNS64 resolution fails, the proxy falls back to a direct IPv4 connection — but logs a warning saying the request is **not** being translated. Silent fallback would make the bridge indistinguishable from a plain proxy, leaving users unable to tell whether translation ever happened.
99
+
100
+ Connection failures return a real status code (`502 Bad Gateway`, `504 Gateway Timeout`) on both the HTTP and CONNECT paths, rather than closing the socket and leaving the client to time out.
101
+
102
+ ## Connection handling
103
+
104
+ Resolution produces an ordered list of candidates rather than a single address:
105
+
106
+ ```
107
+ resolveCandidates("example.com")
108
+ → [ { native IPv6 }, { NAT64-synthesized }, { direct IPv4 } ]
109
+ ```
110
+
111
+ `connect.js` tries them in order, giving each `IPV6_CONNECT_ATTEMPT_TIMEOUT`
112
+ before moving on. A single unreachable address is the normal case on a partially
113
+ broken network, so failing on the first attempt would make the bridge less
114
+ reliable than the stack it replaces.
115
+
116
+ Reaching the direct-IPv4 candidate when a NAT64 route existed means translation
117
+ failed, so that case is logged as a warning and counted separately from a
118
+ deliberate direct connection.
119
+
120
+ Successful HTTP connections are pooled by a keep-alive agent keyed on the
121
+ original hostname, so pooling survives the fact that the dialled address is
122
+ synthesized rather than literal.
123
+
124
+ ## Component Diagram
125
+
126
+ ```
127
+ cli.js ──→ index.js ──→ detect.js (is the bridge needed?)
128
+
129
+ ├──→ discovery.js (RFC 7050: what prefix does this network use?)
130
+
131
+ ├──→ proxy.js ──┐
132
+ │ ├──→ connect.js ──→ dns64.js ──→ ipv6.js
133
+ └──→ socks5.js ─┘ │
134
+ cache.js
135
+
136
+ config.js ──→ validated settings, consumed by everything
137
+ stats.js ──→ counters, surfaced at /status and /metrics
138
+ doctor.js ──→ standalone diagnostics
139
+ ```
140
+
141
+ ### Component Roles
142
+
143
+ | Component | Role | Necessity |
144
+ |-----------|------|-----------|
145
+ | `dns64.js` | DNS64 resolution and candidate ordering | Critical |
146
+ | `ipv6.js` | Address parsing and RFC 6052 embedding | Critical |
147
+ | `connect.js` | Outbound connections, failover, pooling | Critical |
148
+ | `proxy.js` | HTTP/HTTPS proxy with IPv6 routing | Critical |
149
+ | `config.js` | Centralized configuration and validation | Required |
150
+ | `index.js` | Public API (`start`/`stop`) | Required |
151
+ | `cli.js` | CLI entry point | Required |
152
+ | `detect.js` | Auto-detects networks needing the bridge | Important |
153
+ | `discovery.js` | RFC 7050 NAT64 prefix discovery | Important |
154
+ | `socks5.js` | SOCKS5 listener for non-HTTP protocols | Optional |
155
+ | `doctor.js` | Diagnostics | Optional |
156
+ | `stats.js` | Runtime counters for `/status` and `/metrics` | Optional |
157
+ | `cache.js` | Bounded TTL cache for DNS results | Optional |
158
+ | `netmatch.js` | CIDR and hostname matching for allow/bypass lists | Optional |
159
+ | `logger.js` | Level-filtered structured logging | Required |
160
+
161
+ ## Prefix discovery (RFC 7050)
162
+
163
+ Most networks that provide NAT64 assign their own prefix rather than using the
164
+ well-known one, so assuming `64:ff9b::/96` is wrong more often than it is right.
165
+
166
+ RFC 7050 defines the discovery mechanism: the name `ipv4only.arpa` has exactly
167
+ two A records, `192.0.0.170` and `192.0.0.171`, and no AAAA records of its own.
168
+ A DNS64 resolver therefore synthesizes AAAA records for it, and whatever wraps
169
+ those known IPv4 addresses reveals the prefix and its length.
170
+
171
+ At startup the bridge resolves that name and, for each returned address, tries
172
+ every RFC 6052 prefix length until the embedded bytes match a known address. An
173
+ explicitly configured `NAT64_PREFIX` always wins; discovery only fills in a
174
+ default, and logs when the network disagrees with the configuration.
175
+
176
+ ## Detection logic
177
+
178
+ `needsBridge()` answers one question: *would this machine be unable to reach IPv4-only servers without help?*
179
+
180
+ ```
181
+ 1. Does plain IPv4 work? → yes: bridge NOT needed (dual-stack)
182
+ 2. Does IPv6 work? → no: bridge cannot help
183
+ 3. Does the ISP already do NAT64? → yes: bridge NOT needed
184
+ 4. Otherwise → bridge IS needed
185
+ ```
186
+
187
+ Step 1 is what keeps the bridge from activating on an ordinary dual-stack network, where routing traffic through a nonexistent NAT64 gateway would break connections that already work.
188
+
189
+ Probes accept any 2xx or 3xx response as "reachable"; requiring exactly `200` would misreport a network as broken the moment a test endpoint starts redirecting. All three endpoints are configurable so detection still works where the defaults are blocked.
190
+
191
+ ## Design Decisions
192
+
193
+ ### Why Application-Level NAT64?
194
+
195
+ - **No kernel changes**: Works without modifying the system network stack.
196
+ - **Cross-platform**: Same code on Windows, macOS, Linux, and containers.
197
+ - **No admin rights**: Runs as a normal user process.
198
+ - **Covers primary use cases**: HTTP/HTTPS is 95%+ of internet traffic.
199
+
200
+ ### Why loopback-only by default
201
+
202
+ The proxy has no authentication. Bound to a routable interface it is an open relay: anyone on the same network can push traffic through it under the host's IP address. Loopback is the only safe default, and exposing it is an explicit, warned-about opt-in.
203
+
204
+ ### What We Don't Do
205
+
206
+ - **Packet-level translation**: The ISP gateway handles this.
207
+ - **Kernel-level NAT64**: Would require kernel modules and admin rights.
208
+ - **DNS server**: We use the system DNS resolver and enhance results with DNS64 synthesis.
209
+
210
+ ## Deployment Scenarios
211
+
212
+ | Scenario | Behavior |
213
+ |----------|----------|
214
+ | Dual-stack (IPv4 + IPv6) | Detection sees working IPv4; `start()` returns `null` |
215
+ | ISP has NAT64 | Detection reaches an IPv4-only host over the synthesized address; `start()` returns `null` |
216
+ | IPv6-only, no ISP NAT64 | Bridge starts proxy; DNS64 synthesizes addresses; ISP gateway translates |
217
+ | No connectivity at all | `start()` returns `null` — the bridge cannot help |
218
+ | Forced start (`FORCE_BRIDGE=1`) | Bridge starts regardless of detection |