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/src/cache.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * IPv6 Bridge - Bounded TTL cache
3
+ *
4
+ * A small LRU cache with per-entry expiry, used to avoid a DNS lookup on every
5
+ * single request.
6
+ *
7
+ * Note on TTLs: entries expire after a fixed, configurable interval rather than
8
+ * the record's own DNS TTL. The bridge resolves through dns.lookup (the system
9
+ * resolver), which does not expose TTLs — that is a deliberate trade, because
10
+ * the alternative, dns.resolve*, ignores the hosts file and DoH configuration
11
+ * and fails outright on many modern hosts.
12
+ *
13
+ * @module cache
14
+ */
15
+
16
+ class TtlCache {
17
+ /**
18
+ * @param {object} [options] - Cache options
19
+ * @param {number} [options.max=1000] - Maximum number of entries
20
+ * @param {number} [options.ttl=30000] - Entry lifetime in milliseconds
21
+ */
22
+ constructor({ max = 1000, ttl = 30000 } = {}) {
23
+ this.max = max;
24
+ this.ttl = ttl;
25
+ this.entries = new Map();
26
+ this.hits = 0;
27
+ this.misses = 0;
28
+ this.expirations = 0;
29
+ }
30
+
31
+ /**
32
+ * Read a live entry.
33
+ *
34
+ * @param {string} key - Cache key
35
+ * @returns {*} The stored value, or undefined if absent or expired
36
+ */
37
+ get(key) {
38
+ const entry = this.entries.get(key);
39
+ if (!entry) {
40
+ this.misses += 1;
41
+ return undefined;
42
+ }
43
+
44
+ if (entry.expiresAt <= Date.now()) {
45
+ this.entries.delete(key);
46
+ this.expirations += 1;
47
+ this.misses += 1;
48
+ return undefined;
49
+ }
50
+
51
+ // Refresh recency for LRU eviction.
52
+ this.entries.delete(key);
53
+ this.entries.set(key, entry);
54
+ this.hits += 1;
55
+ return entry.value;
56
+ }
57
+
58
+ /**
59
+ * Store an entry, evicting the least recently used if the cache is full.
60
+ *
61
+ * @param {string} key - Cache key
62
+ * @param {*} value - Value to store
63
+ * @param {number} [ttl] - Override lifetime for this entry
64
+ */
65
+ set(key, value, ttl = this.ttl) {
66
+ if (this.entries.has(key)) this.entries.delete(key);
67
+ this.entries.set(key, { value, expiresAt: Date.now() + ttl });
68
+
69
+ while (this.entries.size > this.max) {
70
+ const oldest = this.entries.keys().next().value;
71
+ this.entries.delete(oldest);
72
+ }
73
+ }
74
+
75
+ /** Remove every entry. */
76
+ clear() {
77
+ this.entries.clear();
78
+ }
79
+
80
+ /** Drop expired entries without waiting for them to be read. */
81
+ prune() {
82
+ const now = Date.now();
83
+ for (const [key, entry] of this.entries) {
84
+ if (entry.expiresAt <= now) {
85
+ this.entries.delete(key);
86
+ this.expirations += 1;
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * @returns {{size: number, max: number, ttl: number, hits: number, misses: number, hitRate: number, expirations: number}}
93
+ */
94
+ stats() {
95
+ const lookups = this.hits + this.misses;
96
+ return {
97
+ size: this.entries.size,
98
+ max: this.max,
99
+ ttl: this.ttl,
100
+ hits: this.hits,
101
+ misses: this.misses,
102
+ hitRate: lookups === 0 ? 0 : Number((this.hits / lookups).toFixed(4)),
103
+ expirations: this.expirations,
104
+ };
105
+ }
106
+ }
107
+
108
+ module.exports = { TtlCache };
package/src/cli.js CHANGED
@@ -3,97 +3,314 @@
3
3
  /**
4
4
  * IPv6 Bridge - Command-Line Interface
5
5
  *
6
- * Usage:
7
- * npx ipv6-bridge start Start the bridge (auto-detects if needed)
8
- * npx ipv6-bridge --help Show this help message
9
- * npx ipv6-bridge --version Show version
10
- *
11
- * Environment variables:
12
- * IPV6_BRIDGE_PORT=<port> Port to listen on (default: 8080)
13
- * FORCE_BRIDGE=1 Start even if bridge is not needed
14
- * NAT64_PREFIX=<prefix> Custom NAT64 prefix (default: 64:ff9b::)
15
- *
16
- * Stop the bridge with Ctrl+C or SIGTERM.
6
+ * Commands:
7
+ * start Start the bridge (auto-detects whether it is needed)
8
+ * doctor Diagnose IPv6, DNS64 and NAT64 on this network
9
+ * status Report what a running bridge is doing
17
10
  *
18
11
  * @file CLI entry point for IPv6 Bridge
19
12
  */
20
13
 
21
- const { start, stop } = require('./index');
22
- const { DEFAULT_PORT } = require('./config');
14
+ const http = require('http');
23
15
  const { version } = require('../package.json');
24
16
 
25
- const HELP_TEXT = `
17
+ let config;
18
+ let bridge;
19
+ try {
20
+ config = require('./config');
21
+ bridge = require('./index');
22
+ } catch (err) {
23
+ // Configuration is validated at load time so misconfiguration fails here
24
+ // rather than as unexplainable connection errors later.
25
+ console.error(`Configuration error: ${err.message}`);
26
+ console.error('\nRun "ipv6-bridge --help" to see the accepted values.');
27
+ process.exit(1);
28
+ }
29
+
30
+ const { start, stop } = bridge;
31
+
32
+ const SHORT_HELP = `
26
33
  IPv6 Bridge v${version}
27
- Local DNS64/NAT64 proxy for IPv6-only networks.
34
+ Access IPv4-only sites from an IPv6-only network.
35
+
36
+ Usage: ipv6-bridge <command>
37
+
38
+ Commands:
39
+ start Start the proxy (auto-detects whether it is needed)
40
+ doctor Diagnose IPv6, DNS64 and NAT64 on this network
41
+ status Show what a running bridge is doing
42
+
43
+ Run "ipv6-bridge --help" for all options and examples.
44
+ `.trim();
45
+
46
+ const FULL_HELP = `
47
+ IPv6 Bridge v${version}
48
+ Access IPv4-only sites from an IPv6-only network, without kernel modules or
49
+ admin rights. Runs a local DNS64/NAT64-aware proxy in user space.
50
+
51
+ USAGE
52
+ ipv6-bridge <command>
53
+
54
+ COMMANDS
55
+ start Start the proxy. Exits immediately if the bridge is not
56
+ needed (IPv4 already works, or NAT64 already works).
57
+ doctor Check resolvers, IPv4/IPv6 reachability, NAT64 gateway
58
+ availability, prefix configuration and listener exposure.
59
+ Exits non-zero if any check fails.
60
+ status Query a running bridge and report whether traffic is
61
+ actually being translated.
62
+ --help, -h Show this message
63
+ --version, -v Show the version number
64
+
65
+ GETTING STARTED
66
+ ipv6-bridge doctor Find out whether you need the bridge
67
+ ipv6-bridge start Start it
68
+ ipv6-bridge status Confirm it is translating traffic
28
69
 
29
- Usage:
30
- ipv6-bridge start Start the bridge proxy
31
- ipv6-bridge --help, -h Show this help message
32
- ipv6-bridge --version, -v Show version number
70
+ EXAMPLES
71
+ # Start on a different port
72
+ IPV6_BRIDGE_PORT=9090 ipv6-bridge start
33
73
 
34
- Environment variables:
35
- IPV6_BRIDGE_PORT Port to listen on (default: ${DEFAULT_PORT})
36
- FORCE_BRIDGE Set to any value to force start
37
- NAT64_PREFIX Custom NAT64 prefix (default: 64:ff9b::)
74
+ # Try it on a normal dual-stack network (detection would otherwise skip it)
75
+ FORCE_BRIDGE=1 ipv6-bridge start
76
+
77
+ # Also serve SOCKS5, so ssh / git / database clients can use it
78
+ IPV6_BRIDGE_SOCKS_PORT=1080 ipv6-bridge start
79
+
80
+ # Share it with a trusted LAN, with credentials required
81
+ IPV6_BRIDGE_HOST=0.0.0.0 IPV6_BRIDGE_AUTH=user:secret \\
82
+ IPV6_BRIDGE_ALLOW=192.168.1.0/24 ipv6-bridge start
83
+
84
+ # Reach internal hosts directly instead of through NAT64
85
+ IPV6_BRIDGE_BYPASS='*.internal.company.com,10.0.0.0/8' ipv6-bridge start
86
+
87
+ # Use a prefix your operator assigned instead of the well-known one
88
+ NAT64_PREFIX=2001:db8:122:344::/64 ipv6-bridge start
89
+
90
+ CONNECTING CLIENTS
91
+ Browser / system proxy 127.0.0.1:${config.DEFAULT_PORT}
92
+ Automatic (PAC) config http://127.0.0.1:${config.DEFAULT_PORT}/proxy.pac
93
+ curl curl -x http://127.0.0.1:${config.DEFAULT_PORT} https://example.com
94
+ git over SOCKS5 git config --global http.proxy socks5h://127.0.0.1:1080
95
+ ssh over SOCKS5 ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:1080 %h %p' host
96
+
97
+ ENDPOINTS (while running)
98
+ /healthz Liveness probe; reachable even when auth is enabled
99
+ /status JSON: counters, routing modes, DNS cache, active prefix
100
+ /metrics The same data in Prometheus format
101
+ /proxy.pac Proxy auto-configuration file for browsers
102
+
103
+ CONFIGURATION — network
104
+ IPV6_BRIDGE_PORT Proxy listen port (default: 8080)
105
+ IPV6_BRIDGE_HOST Interface to bind (default: 127.0.0.1)
106
+ IPV6_BRIDGE_SOCKS_PORT Serve SOCKS5 on this port (default: off)
107
+ NAT64_PREFIX NAT64 prefix with optional /length
108
+ (default: 64:ff9b::/96; RFC 6052 allows
109
+ /32, /40, /48, /56, /64, /96)
110
+ IPV6_BRIDGE_DISCOVER_PREFIX Discover the prefix via RFC 7050 (default: on)
111
+
112
+ CONFIGURATION — access control
113
+ IPV6_BRIDGE_AUTH Require "user:password" from clients
114
+ IPV6_BRIDGE_ALLOW Client allowlist, e.g. "192.168.1.0/24"
115
+ IPV6_BRIDGE_BYPASS Hosts to reach directly, e.g. "*.internal.com"
116
+
117
+ CONFIGURATION — behaviour
118
+ FORCE_BRIDGE Start even when detection says it is not needed
119
+ IPV6_BRIDGE_CONTROL Serve the endpoints above (default: on)
120
+ LOG_LEVEL silent, error, warn, info, debug (default: info)
121
+
122
+ CONFIGURATION — tuning
123
+ IPV6_DNS_TIMEOUT DNS timeout in ms (default: 5000)
124
+ IPV6_DNS_CACHE_TTL DNS cache lifetime in ms (default: 30000)
125
+ IPV6_DNS_CACHE_MAX Maximum cached entries (default: 1000)
126
+ IPV6_CONN_TIMEOUT Connection timeout in ms (default: 10000)
127
+ IPV6_CONNECT_ATTEMPT_TIMEOUT Per-address timeout before trying the next
128
+ candidate, in ms (default: 3000)
129
+ IPV6_KEEP_ALIVE_MS Idle lifetime of pooled sockets (default: 15000)
130
+ IPV6_MAX_SOCKETS_PER_HOST Pooled sockets per host (default: 64)
131
+
132
+ CONFIGURATION — detection endpoints
133
+ IPV4_TEST_URL Used to detect working IPv4
134
+ IPV6_TEST_URL Used to detect working IPv6
135
+ NAT64_TEST_HOST IPv4-only host used to probe for NAT64
136
+
137
+ SECURITY
138
+ The proxy binds to loopback and requires no credentials by default. If you
139
+ set IPV6_BRIDGE_HOST to a routable address, also set IPV6_BRIDGE_AUTH or
140
+ IPV6_BRIDGE_ALLOW — otherwise anyone who can reach this machine can relay
141
+ traffic through it under your IP address.
142
+
143
+ DOCUMENTATION
144
+ Guide docs/GUIDE.md (when to use it, worked examples)
145
+ API docs/API.md (programmatic API, every setting)
146
+ Architecture docs/ARCHITECTURE.md
38
147
 
39
148
  Stop the bridge with Ctrl+C or by sending SIGTERM.
40
149
  `.trim();
41
150
 
151
+ /** Fetch JSON over HTTP with a short timeout. */
152
+ function fetchJson(options) {
153
+ return new Promise((resolve, reject) => {
154
+ const req = http.get(options, (res) => {
155
+ let body = '';
156
+ res.on('data', (chunk) => { body += chunk; });
157
+ res.on('end', () => {
158
+ try {
159
+ resolve(JSON.parse(body));
160
+ } catch {
161
+ reject(new Error(`unexpected response from ${options.path}`));
162
+ }
163
+ });
164
+ });
165
+ req.on('error', reject);
166
+ req.setTimeout(3000, () => {
167
+ req.destroy();
168
+ reject(new Error('request timed out'));
169
+ });
170
+ });
171
+ }
172
+
173
+ async function runStatus() {
174
+ const host = config.isLoopbackBind() ? '127.0.0.1' : config.BIND_HOST;
175
+ const port = config.DEFAULT_PORT;
176
+
177
+ let status;
178
+ try {
179
+ status = await fetchJson({ host, port, path: '/status' });
180
+ } catch (err) {
181
+ console.error(`No bridge is responding on ${host}:${port} (${err.message}).`);
182
+ console.error('\nStart one with "ipv6-bridge start", or set IPV6_BRIDGE_PORT');
183
+ console.error('if it is running on another port.');
184
+ process.exit(1);
185
+ }
186
+
187
+ const { counters, routes, dnsCache } = status;
188
+ const translated = routes.nat64;
189
+ const untranslated = routes.directIpv4 + routes.directIpv4Fallback;
190
+
191
+ console.log(`\nIPv6 Bridge on ${host}:${port}`);
192
+ console.log(` Uptime ${status.uptimeSeconds}s`);
193
+ console.log(` NAT64 prefix ${status.nat64Prefix}`);
194
+ console.log('');
195
+ console.log(` HTTP requests ${counters.httpRequests}`);
196
+ console.log(` CONNECT tunnels ${counters.connectRequests}`);
197
+ console.log(` SOCKS5 sessions ${counters.socksRequests}`);
198
+ console.log(` Errors ${counters.proxyErrors} (${counters.timeouts} timeouts)`);
199
+ console.log('');
200
+ console.log(' Routing');
201
+ console.log(` via NAT64 ${routes.nat64}`);
202
+ console.log(` native IPv6 ${routes.nativeIpv6}`);
203
+ console.log(` direct IPv4 ${routes.directIpv4}`);
204
+ console.log(` untranslated fallback ${routes.directIpv4Fallback}`);
205
+ console.log('');
206
+ console.log(` DNS cache ${dnsCache.size} entries, hit rate ${dnsCache.hitRate}`);
207
+ console.log('');
208
+
209
+ const routed = Object.values(routes).reduce((sum, count) => sum + count, 0);
210
+
211
+ if (routed === 0) {
212
+ console.log(' No traffic yet. Send a request through the proxy, then check again.');
213
+ } else if (translated > 0) {
214
+ console.log(` Translating ${Math.round(status.translationRate * 100)}% of connections`);
215
+ console.log(' that needed it. The bridge is doing its job.');
216
+ } else if (routes.directIpv4Fallback > 0) {
217
+ console.log(' WARNING: connections fell back to untranslated IPv4.');
218
+ console.log(' DNS64 is failing, so the bridge is not translating anything.');
219
+ console.log(' Run "ipv6-bridge doctor" to find out why.');
220
+ } else if (untranslated > 0) {
221
+ console.log(' Nothing needed translating: these destinations were private or');
222
+ console.log(' loopback addresses, which are always reached directly.');
223
+ } else {
224
+ console.log(' Nothing needed translating: every destination already had an');
225
+ console.log(' IPv6 address, so no NAT64 synthesis was required.');
226
+ }
227
+ console.log('');
228
+ }
229
+
42
230
  const command = process.argv[2];
43
- const port = process.env.IPV6_BRIDGE_PORT
44
- ? parseInt(process.env.IPV6_BRIDGE_PORT, 10)
45
- : DEFAULT_PORT;
46
231
 
47
- if (command === '--help' || command === '-h' || !command) {
48
- console.log(HELP_TEXT);
232
+ if (!command) {
233
+ console.log(SHORT_HELP);
49
234
  process.exit(0);
50
235
  }
51
236
 
52
- if (command === '--version' || command === '-v') {
237
+ if (command === '--help' || command === '-h' || command === 'help') {
238
+ console.log(FULL_HELP);
239
+ process.exit(0);
240
+ }
241
+
242
+ if (command === '--version' || command === '-v' || command === 'version') {
53
243
  console.log(version);
54
244
  process.exit(0);
55
245
  }
56
246
 
57
- if (command === 'start') {
58
- if (Number.isNaN(port) || port < 1 || port > 65535) {
59
- console.error(`Error: Invalid port "${process.env.IPV6_BRIDGE_PORT}". Must be 1-65535.`);
247
+ if (command === 'doctor') {
248
+ const { diagnose, format } = require('./doctor');
249
+ diagnose()
250
+ .then((report) => {
251
+ console.log(format(report));
252
+ process.exit(report.summary.fail > 0 ? 1 : 0);
253
+ })
254
+ .catch((err) => {
255
+ console.error(`Diagnostics failed: ${err.message}`);
256
+ process.exit(1);
257
+ });
258
+ } else if (command === 'status') {
259
+ runStatus().catch((err) => {
260
+ console.error(`Error: ${err.message}`);
60
261
  process.exit(1);
61
- }
262
+ });
263
+ } else if (command === 'start') {
264
+ let shuttingDown = false;
265
+ const shutdown = (exitCode = 0) => {
266
+ if (shuttingDown) return;
267
+ shuttingDown = true;
268
+ console.log('\nStopping IPv6 Bridge...');
269
+ stop()
270
+ .then(() => process.exit(exitCode))
271
+ .catch(() => process.exit(1));
272
+ };
62
273
 
63
- start(port)
274
+ start(config.DEFAULT_PORT)
64
275
  .then((server) => {
65
276
  if (!server) {
277
+ console.log('Run "ipv6-bridge doctor" for details, or set FORCE_BRIDGE=1 to start anyway.');
66
278
  process.exit(0);
67
279
  }
68
- console.log(`\nIPv6 Bridge running on http://localhost:${port}`);
69
- console.log(`Configure your browser/system proxy to localhost:${port}\n`);
280
+ const { port } = server.address();
281
+ const prefix = config.getPrefix();
282
+
283
+ console.log(`\nIPv6 Bridge running on http://${config.BIND_HOST}:${port}`);
284
+ console.log(`Configure your browser/system proxy to ${config.BIND_HOST}:${port}`);
285
+ console.log(`NAT64 prefix: ${prefix.prefix}/${prefix.length}`);
286
+ if (config.SOCKS_PORT) {
287
+ console.log(`SOCKS5 running on ${config.BIND_HOST}:${config.SOCKS_PORT}`);
288
+ }
289
+ if (config.CONTROL_ENDPOINTS) {
290
+ console.log(`Status: http://${config.BIND_HOST}:${port}/status`);
291
+ console.log(`PAC: http://${config.BIND_HOST}:${port}/proxy.pac`);
292
+ }
293
+ console.log('');
70
294
  })
71
295
  .catch((err) => {
72
296
  console.error(`Error: ${err.message}`);
73
297
  process.exit(1);
74
298
  });
75
299
 
76
- // Graceful shutdown on SIGINT (Ctrl+C) and SIGTERM (container/daemon stop)
77
- function shutdown() {
78
- console.log('\nStopping IPv6 Bridge...');
79
- stop().then(() => process.exit(0));
80
- }
81
-
82
- process.on('SIGINT', shutdown);
83
- process.on('SIGTERM', shutdown);
300
+ process.on('SIGINT', () => shutdown(0));
301
+ process.on('SIGTERM', () => shutdown(0));
84
302
 
85
- // Catch unhandled errors to prevent silent crashes
86
303
  process.on('uncaughtException', (err) => {
87
304
  console.error('Uncaught exception:', err.message);
88
- stop().then(() => process.exit(1));
305
+ shutdown(1);
89
306
  });
90
307
 
91
308
  process.on('unhandledRejection', (reason) => {
92
309
  console.error('Unhandled rejection:', reason);
93
- stop().then(() => process.exit(1));
310
+ shutdown(1);
94
311
  });
95
312
  } else {
96
313
  console.error(`Unknown command: "${command}"\n`);
97
- console.log(HELP_TEXT);
314
+ console.log(SHORT_HELP);
98
315
  process.exit(1);
99
316
  }
package/src/config.js CHANGED
@@ -1,42 +1,177 @@
1
1
  /**
2
2
  * IPv6 Bridge - Configuration
3
3
  *
4
- * RFC 6052 Well-Known NAT64 Prefix and network test constants.
5
- * All values can be overridden via environment variables.
4
+ * All values can be overridden via environment variables. Invalid values throw
5
+ * at load time rather than surfacing later as unexplainable connection failures.
6
6
  *
7
7
  * @module config
8
8
  */
9
9
 
10
+ const { parsePrefix, WELL_KNOWN_PREFIX_ADDRESS } = require('./ipv6');
11
+ const netmatch = require('./netmatch');
12
+
13
+ /** The RFC 6052 section 3.1 well-known prefix. */
14
+ const WELL_KNOWN_PREFIX = WELL_KNOWN_PREFIX_ADDRESS;
15
+
16
+ function intFromEnv(name, fallback, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
17
+ const raw = process.env[name];
18
+ if (raw === undefined || raw.trim() === '') return fallback;
19
+
20
+ const trimmed = raw.trim();
21
+ if (!/^\d+$/.test(trimmed)) {
22
+ throw new Error(`${name} must be a positive integer, got "${raw}"`);
23
+ }
24
+
25
+ const value = Number(trimmed);
26
+ if (value < min || value > max) {
27
+ throw new Error(`${name} must be between ${min} and ${max}, got "${raw}"`);
28
+ }
29
+ return value;
30
+ }
31
+
32
+ function boolFromEnv(name, fallback) {
33
+ const raw = process.env[name];
34
+ if (raw === undefined || raw.trim() === '') return fallback;
35
+ return !['0', 'false', 'no', 'off'].includes(raw.trim().toLowerCase());
36
+ }
37
+
38
+ /**
39
+ * The active NAT64 prefix. Held in a mutable slot because RFC 7050 discovery
40
+ * can replace the configured default with the one the network actually uses.
41
+ */
42
+ let activePrefix;
43
+ try {
44
+ activePrefix = parsePrefix(process.env.NAT64_PREFIX || `${WELL_KNOWN_PREFIX}/96`);
45
+ } catch (err) {
46
+ throw new Error(`Invalid NAT64_PREFIX: ${err.message}`);
47
+ }
48
+
49
+ const configuredPrefix = activePrefix;
50
+
51
+ /** @returns {{prefix: string, length: number, bytes: Buffer}} The active prefix */
52
+ function getPrefix() {
53
+ return activePrefix;
54
+ }
55
+
56
+ /**
57
+ * Replace the active NAT64 prefix (used by RFC 7050 discovery).
58
+ *
59
+ * @param {{prefix: string, length: number, bytes: Buffer}} prefix - Parsed prefix
60
+ */
61
+ function setPrefix(prefix) {
62
+ activePrefix = prefix;
63
+ }
64
+
65
+ /** Restore the prefix that was configured at startup. */
66
+ function resetPrefix() {
67
+ activePrefix = configuredPrefix;
68
+ }
69
+
70
+ /** @returns {boolean} true if the active prefix is the well-known one */
71
+ function usingWellKnownPrefix() {
72
+ return activePrefix.prefix === WELL_KNOWN_PREFIX;
73
+ }
74
+
75
+ function parseAuth() {
76
+ const raw = process.env.IPV6_BRIDGE_AUTH;
77
+ if (!raw || raw.trim() === '') return null;
78
+
79
+ const separator = raw.indexOf(':');
80
+ if (separator < 1 || separator === raw.length - 1) {
81
+ throw new Error('IPV6_BRIDGE_AUTH must be in "user:password" form');
82
+ }
83
+
84
+ return {
85
+ username: raw.slice(0, separator),
86
+ password: raw.slice(separator + 1),
87
+ header: 'Basic ' + Buffer.from(raw).toString('base64'),
88
+ };
89
+ }
90
+
91
+ const BIND_HOST = process.env.IPV6_BRIDGE_HOST || '127.0.0.1';
92
+ const AUTH = parseAuth();
93
+ const ALLOW_FROM = netmatch.compile(process.env.IPV6_BRIDGE_ALLOW);
94
+ const BYPASS = netmatch.compile(process.env.IPV6_BRIDGE_BYPASS);
95
+
96
+ function isLoopbackBind(host = BIND_HOST) {
97
+ return host === '127.0.0.1' || host === '::1' || host === 'localhost';
98
+ }
99
+
10
100
  module.exports = {
11
- /**
12
- * NAT64 Prefix (RFC 6052)
13
- *
14
- * The well-known prefix used to synthesize IPv6 addresses from IPv4.
15
- * Override with the NAT64_PREFIX environment variable if your ISP
16
- * uses a non-standard prefix.
17
- *
18
- * Example: 192.0.2.1 → 64:ff9b::c000:0201
19
- *
20
- * @see https://tools.ietf.org/html/rfc6052#section-2.1
21
- */
22
- NAT64_PREFIX: process.env.NAT64_PREFIX || '64:ff9b::',
101
+ WELL_KNOWN_PREFIX,
102
+ getPrefix,
103
+ setPrefix,
104
+ resetPrefix,
105
+ usingWellKnownPrefix,
106
+ isLoopbackBind,
107
+
108
+ /** Configured NAT64 prefix as written by the user. */
109
+ NAT64_PREFIX: configuredPrefix.prefix,
110
+ NAT64_PREFIX_LENGTH: configuredPrefix.length,
111
+
112
+ /** Default proxy listen port. */
113
+ DEFAULT_PORT: intFromEnv('IPV6_BRIDGE_PORT', 8080, { max: 65535 }),
23
114
 
24
115
  /**
25
- * Default port for the proxy server.
26
- * Override with the IPV6_BRIDGE_PORT environment variable.
116
+ * Interface the proxy binds to. Defaults to loopback: without authentication
117
+ * a routable bind turns the host into an open relay.
27
118
  */
28
- DEFAULT_PORT: 8080,
119
+ BIND_HOST,
120
+
121
+ /** Optional SOCKS5 listener port; disabled when unset. */
122
+ SOCKS_PORT: process.env.IPV6_BRIDGE_SOCKS_PORT
123
+ ? intFromEnv('IPV6_BRIDGE_SOCKS_PORT', 1080, { max: 65535 })
124
+ : null,
125
+
126
+ /** Optional Basic credentials required from proxy clients. */
127
+ AUTH,
128
+
129
+ /** Client addresses permitted to use the proxy (empty means "any"). */
130
+ ALLOW_FROM,
131
+
132
+ /** Hosts that should bypass NAT64 and be reached directly. */
133
+ BYPASS,
134
+
135
+ /** Whether to serve /healthz, /status, /metrics and /proxy.pac. */
136
+ CONTROL_ENDPOINTS: boolFromEnv('IPV6_BRIDGE_CONTROL', true),
137
+
138
+ /** Whether to attempt RFC 7050 NAT64 prefix discovery at startup. */
139
+ PREFIX_DISCOVERY: boolFromEnv('IPV6_BRIDGE_DISCOVER_PREFIX', true),
140
+
141
+ /** DNS resolution timeout in milliseconds. */
142
+ DNS_TIMEOUT: intFromEnv('IPV6_DNS_TIMEOUT', 5000),
143
+
144
+ /** Lifetime of a cached DNS result, in milliseconds. */
145
+ DNS_CACHE_TTL: intFromEnv('IPV6_DNS_CACHE_TTL', 30000),
146
+
147
+ /** Maximum number of cached DNS results. */
148
+ DNS_CACHE_MAX: intFromEnv('IPV6_DNS_CACHE_MAX', 1000),
149
+
150
+ /** Proxy connection timeout in milliseconds. */
151
+ CONNECTION_TIMEOUT: intFromEnv('IPV6_CONN_TIMEOUT', 10000),
152
+
153
+ /** How long to wait for one candidate address before trying the next. */
154
+ CONNECT_ATTEMPT_TIMEOUT: intFromEnv('IPV6_CONNECT_ATTEMPT_TIMEOUT', 3000),
155
+
156
+ /** Idle keep-alive socket lifetime for pooled upstream connections. */
157
+ KEEP_ALIVE_MS: intFromEnv('IPV6_KEEP_ALIVE_MS', 15000),
158
+
159
+ /** Maximum pooled sockets per upstream host. */
160
+ MAX_SOCKETS_PER_HOST: intFromEnv('IPV6_MAX_SOCKETS_PER_HOST', 64),
29
161
 
30
162
  /**
31
- * Test URL for IPv6 connectivity detection.
32
- * Must be a server with IPv6 support.
163
+ * Endpoint used to test native IPv6 connectivity. Overridable so detection
164
+ * still works where the default host is unreachable or blocked.
33
165
  */
34
- IPV6_GOOGLE: 'http://ipv6.google.com',
166
+ IPV6_TEST_URL: process.env.IPV6_TEST_URL || 'http://ipv6.google.com',
167
+
168
+ /** Endpoint used to test whether plain IPv4 connectivity already works. */
169
+ IPV4_TEST_URL: process.env.IPV4_TEST_URL || 'http://ipv4.google.com',
35
170
 
36
171
  /**
37
- * Hostname for NAT64 availability testing.
38
- * An IPv4-only hostname that should be reachable via NAT64
39
- * if the ISP gateway is properly configured.
172
+ * IPv4-only hostname used to probe for an upstream NAT64 gateway. Resolved
173
+ * via DNS64 and connected to over IPv6; success means the ISP already
174
+ * provides NAT64 and the bridge is unnecessary.
40
175
  */
41
- IPV4_GOOGLE: 'ipv4.google.com',
176
+ NAT64_TEST_HOST: process.env.NAT64_TEST_HOST || 'ipv4.google.com',
42
177
  };