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/README.md +206 -31
- package/docs/API.md +365 -0
- package/docs/ARCHITECTURE.md +218 -0
- package/docs/CHANGELOG.md +159 -0
- package/docs/CONTRIBUTING.md +88 -0
- package/docs/GUIDE.md +675 -0
- package/docs/ROADMAP.md +69 -0
- package/examples/basic-usage.js +31 -23
- package/examples/embedded-usage.js +100 -0
- package/examples/production-usage.js +106 -0
- package/package.json +6 -6
- package/src/agent.js +271 -0
- package/src/cache.js +108 -0
- package/src/cli.js +265 -48
- package/src/config.js +159 -24
- package/src/connect.js +142 -0
- package/src/detect.js +82 -43
- package/src/discovery.js +122 -0
- package/src/dns64.js +226 -44
- package/src/doctor.js +222 -0
- package/src/index.js +157 -62
- package/src/ipv6.js +232 -0
- package/src/logger.js +47 -0
- package/src/netmatch.js +122 -0
- package/src/proxy.js +419 -101
- package/src/socks5.js +283 -0
- package/src/stats.js +140 -0
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
22
|
-
const { DEFAULT_PORT } = require('./config');
|
|
14
|
+
const http = require('http');
|
|
23
15
|
const { version } = require('../package.json');
|
|
24
16
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
ipv6-bridge
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 (
|
|
48
|
-
console.log(
|
|
232
|
+
if (!command) {
|
|
233
|
+
console.log(SHORT_HELP);
|
|
49
234
|
process.exit(0);
|
|
50
235
|
}
|
|
51
236
|
|
|
52
|
-
if (command === '--
|
|
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 === '
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
305
|
+
shutdown(1);
|
|
89
306
|
});
|
|
90
307
|
|
|
91
308
|
process.on('unhandledRejection', (reason) => {
|
|
92
309
|
console.error('Unhandled rejection:', reason);
|
|
93
|
-
|
|
310
|
+
shutdown(1);
|
|
94
311
|
});
|
|
95
312
|
} else {
|
|
96
313
|
console.error(`Unknown command: "${command}"\n`);
|
|
97
|
-
console.log(
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
32
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
|
|
176
|
+
NAT64_TEST_HOST: process.env.NAT64_TEST_HOST || 'ipv4.google.com',
|
|
42
177
|
};
|