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.
@@ -0,0 +1,69 @@
1
+ # Roadmap & Project Vision
2
+
3
+ **IPv6 Bridge** is a zero-dependency DNS64/NAT64 bridge for hosts stranded on
4
+ IPv6-only networks. This document tracks what has shipped and what is still
5
+ open.
6
+
7
+ ## Shipped in 2.0
8
+
9
+ These were the Tier 1 and Tier 2 items on the previous roadmap.
10
+
11
+ - **DNS cache** — bounded LRU with TTL expiry, so repeat requests skip resolution.
12
+ - **Connection pooling & keep-alive** — upstream sockets are reused across requests.
13
+ - **Connection failover** — candidates are tried in preference order (native IPv6,
14
+ then NAT64, then direct IPv4) instead of failing on the first unreachable address.
15
+ - **Improved network detection** — IPv4 reachability is checked first, so the bridge
16
+ no longer activates on healthy dual-stack networks.
17
+ - **Custom NAT64 prefix formats** — every RFC 6052 prefix length (`/32`, `/40`,
18
+ `/48`, `/56`, `/64`, `/96`), validated against the RFC's own test vectors.
19
+ - **NAT64 prefix discovery (RFC 7050)** — the network's real prefix is discovered
20
+ from `ipv4only.arpa` rather than assumed.
21
+ - **SOCKS5 support** — ssh, git, databases and any other TCP protocol.
22
+ - **PAC (Proxy Auto-Configuration)** — served at `/proxy.pac`.
23
+ - **Per-domain routing policies** — `IPV6_BRIDGE_BYPASS` for split routing.
24
+ - **Authentication** — Basic credentials and a client CIDR allowlist, enforced on
25
+ the HTTP, CONNECT and SOCKS5 paths.
26
+ - **Observability** — `/healthz`, `/status` and Prometheus `/metrics`, including a
27
+ `translationRate` that shows whether translation is actually happening.
28
+ - **Diagnostics** — `ipv6-bridge doctor`.
29
+
30
+ ## Open
31
+
32
+ ### Reliability
33
+
34
+ - **Full Happy Eyeballs (RFC 8305)** — connection attempts are currently
35
+ sequential with a per-attempt timeout. True Happy Eyeballs races families with
36
+ a staggered delay, which lowers worst-case latency on partially broken networks.
37
+ - **Real DNS TTLs** — the system resolver does not expose them, so the cache uses
38
+ a fixed TTL. Honouring real TTLs would need a resolver that reports them without
39
+ reintroducing the `dns.resolve*` failure mode on DoH-only hosts.
40
+ - **Circuit breaking** — remember recently failed upstreams instead of retrying
41
+ every candidate on every request.
42
+
43
+ ### Protocol coverage
44
+
45
+ - **HTTP/2 and HTTP/3 to the origin** — upstream requests are HTTP/1.1. This needs
46
+ a move away from Node's core `http` module for the upstream leg.
47
+ - **SOCKS5 UDP (`UDP ASSOCIATE`)** — would extend coverage to DNS, QUIC and
48
+ game traffic.
49
+ - **WebSocket** — works today inside a CONNECT tunnel, but not for plain-HTTP
50
+ `Upgrade` requests, which the proxy currently strips.
51
+
52
+ ### Operations
53
+
54
+ - **Rate limiting** — per-client request and bandwidth caps for shared deployments.
55
+ - **Structured JSON logging** — for log aggregation pipelines.
56
+ - **Container image** — a published image with sensible defaults.
57
+
58
+ ### Reach
59
+
60
+ - **Transparent interception** — the largest remaining adoption barrier is that
61
+ applications must be configured to use the proxy. A TUN-based mode would remove
62
+ that, at the cost of admin rights and platform-specific code, so it would need
63
+ to be an opt-in mode rather than a replacement for the user-space design.
64
+
65
+ ---
66
+
67
+ *Contributions are welcome! If you're interested in tackling any of these roadmap
68
+ items, please check out our [CONTRIBUTING.md](CONTRIBUTING.md) and open an issue
69
+ to discuss the implementation plan.*
@@ -5,34 +5,42 @@
5
5
  *
6
6
  * Run:
7
7
  * node examples/basic-usage.js
8
+ *
9
+ * The bridge only starts if this machine actually needs it. To try it on a
10
+ * dual-stack network, force it:
11
+ * FORCE_BRIDGE=1 node examples/basic-usage.js
8
12
  */
9
13
 
10
14
  const { start, stop } = require('../src/index');
11
15
 
12
16
  async function main() {
13
- try {
14
- console.log('Starting IPv6 Bridge...\n');
15
- const server = await start(8080);
16
-
17
- if (server) {
18
- const addr = server.address();
19
- console.log(`Bridge started on port ${addr.port}`);
20
- console.log(`Configure your browser/system proxy to localhost:${addr.port}\n`);
21
- console.log('Press Ctrl+C to stop.\n');
22
-
23
- process.on('SIGINT', async () => {
24
- console.log('\nShutting down...');
25
- await stop();
26
- process.exit(0);
27
- });
28
- } else {
29
- console.log('Bridge not needed — you have IPv4 connectivity or working NAT64.');
30
- process.exit(0);
31
- }
32
- } catch (error) {
33
- console.error('Error:', error.message);
34
- process.exit(1);
17
+ console.log('Starting IPv6 Bridge...\n');
18
+
19
+ const server = await start(8080);
20
+
21
+ if (!server) {
22
+ console.log('Bridge not needed — IPv4 is reachable, or NAT64 already works.');
23
+ console.log('Run with FORCE_BRIDGE=1 to start it anyway.');
24
+ return;
35
25
  }
26
+
27
+ const { port } = server.address();
28
+ console.log(`Bridge started on port ${port}`);
29
+ console.log(`Configure your browser/system proxy to 127.0.0.1:${port}`);
30
+ console.log(`Check what it is doing: http://127.0.0.1:${port}/status\n`);
31
+ console.log('Press Ctrl+C to stop.\n');
32
+
33
+ const shutdown = async () => {
34
+ console.log('\nShutting down...');
35
+ await stop();
36
+ process.exit(0);
37
+ };
38
+
39
+ process.on('SIGINT', shutdown);
40
+ process.on('SIGTERM', shutdown);
36
41
  }
37
42
 
38
- main();
43
+ main().catch((error) => {
44
+ console.error('Error:', error.message);
45
+ process.exit(1);
46
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * IPv6 Bridge — Embedded Usage Example
3
+ *
4
+ * Using the bridge from inside an application: no proxy, no ports, no system
5
+ * configuration. Outbound connections gain DNS64 translation, address-family
6
+ * failover and connection pooling.
7
+ *
8
+ * This is the way to use the package in a deployed service.
9
+ *
10
+ * Run:
11
+ * node examples/embedded-usage.js
12
+ */
13
+
14
+ const https = require('https');
15
+ const net = require('net');
16
+ const {
17
+ createHttpsAgent,
18
+ createLookup,
19
+ resolve,
20
+ getStats,
21
+ } = require('../src/index');
22
+
23
+ // One agent for the lifetime of the process: it pools connections, so creating
24
+ // a new one per request would throw that away.
25
+ const agent = createHttpsAgent();
26
+
27
+ function get(url) {
28
+ return new Promise((resolve, reject) => {
29
+ const options = { agent, headers: { 'User-Agent': 'ipv6-bridge-example' } };
30
+ const req = https.get(url, options, (res) => {
31
+ const via = res.socket.remoteAddress;
32
+ res.resume();
33
+ res.on('end', () => resolve({ status: res.statusCode, via }));
34
+ });
35
+ req.on('error', reject);
36
+ req.setTimeout(15000, () => {
37
+ req.destroy();
38
+ reject(new Error('request timed out'));
39
+ });
40
+ });
41
+ }
42
+
43
+ async function main() {
44
+ console.log('1. Which route would be used for each destination?\n');
45
+ for (const host of ['example.com', '8.8.8.8', '192.168.1.1']) {
46
+ const candidates = await resolve(host);
47
+ const preferred = candidates[0];
48
+ console.log(` ${host.padEnd(16)} ${preferred.host} (${preferred.mode})`);
49
+ if (candidates.length > 1) {
50
+ console.log(` ${''.padEnd(16)} fallbacks: ${candidates.slice(1).map((c) => c.mode).join(', ')}`);
51
+ }
52
+ }
53
+
54
+ console.log('\n2. Making real requests through the agent\n');
55
+ for (const url of ['https://example.com/', 'https://api.github.com/']) {
56
+ try {
57
+ const { status, via } = await get(url);
58
+ console.log(` ${url.padEnd(28)} ${status} via ${via}`);
59
+ } catch (err) {
60
+ console.log(` ${url.padEnd(28)} failed: ${err.message}`);
61
+ }
62
+ }
63
+
64
+ console.log('\n3. Using the lookup function with a raw socket\n');
65
+ await new Promise((resolve) => {
66
+ const socket = net.connect({ host: 'example.com', port: 443, lookup: createLookup() }, () => {
67
+ console.log(` connected to example.com:443 via ${socket.remoteAddress}`);
68
+ socket.destroy();
69
+ resolve();
70
+ });
71
+ socket.on('error', (err) => {
72
+ console.log(` connection failed: ${err.message}`);
73
+ resolve();
74
+ });
75
+ });
76
+
77
+ console.log('\n4. Did translation actually happen?\n');
78
+ const stats = getStats();
79
+ console.log(` NAT64 prefix ${stats.nat64Prefix}`);
80
+ console.log(` translated ${stats.routes.nat64}`);
81
+ console.log(` native IPv6 ${stats.routes.nativeIpv6}`);
82
+ console.log(` untranslated ${stats.routes.directIpv4 + stats.routes.directIpv4Fallback}`);
83
+ console.log(` translation rate ${stats.translationRate ?? 'n/a'}`);
84
+ console.log(` DNS cache hit rate ${stats.dnsCache.hitRate}`);
85
+
86
+ if (stats.routes.directIpv4Fallback > 0 && stats.routes.nat64 === 0) {
87
+ console.log('\n WARNING: connections fell back to untranslated IPv4.');
88
+ console.log(' On an IPv6-only network this means DNS64 is failing.');
89
+ console.log(' Run "ipv6-bridge doctor" to find out why.');
90
+ }
91
+
92
+ // Release pooled sockets so the process can exit.
93
+ agent.destroy();
94
+ }
95
+
96
+ main().catch((err) => {
97
+ console.error('Error:', err.message);
98
+ agent.destroy();
99
+ process.exit(1);
100
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * IPv6 Bridge — Production Usage Example
3
+ *
4
+ * Shows the pieces that matter when the bridge is more than a local
5
+ * convenience: access control, SOCKS5 for non-HTTP traffic, prefix discovery,
6
+ * health monitoring and a clean shutdown.
7
+ *
8
+ * Run:
9
+ * FORCE_BRIDGE=1 node examples/production-usage.js
10
+ */
11
+
12
+ const http = require('http');
13
+ const { start, stop } = require('../src/index');
14
+
15
+ const PROXY_PORT = Number(process.env.IPV6_BRIDGE_PORT) || 8080;
16
+ const SOCKS_PORT = Number(process.env.IPV6_BRIDGE_SOCKS_PORT) || 1080;
17
+
18
+ // Access control must be configured before the bridge is required, because
19
+ // configuration is read and validated at load time.
20
+ if (!process.env.IPV6_BRIDGE_HOST) {
21
+ // Loopback is the default; set IPV6_BRIDGE_HOST to expose the proxy, and
22
+ // always pair that with IPV6_BRIDGE_AUTH or IPV6_BRIDGE_ALLOW.
23
+ process.env.IPV6_BRIDGE_HOST = '127.0.0.1';
24
+ }
25
+
26
+ /** Poll /status and report whether translation is actually happening. */
27
+ function fetchStatus(port) {
28
+ return new Promise((resolve, reject) => {
29
+ const req = http.get({ host: '127.0.0.1', port, path: '/status' }, (res) => {
30
+ let body = '';
31
+ res.on('data', (chunk) => { body += chunk; });
32
+ res.on('end', () => {
33
+ try {
34
+ resolve(JSON.parse(body));
35
+ } catch (err) {
36
+ reject(err);
37
+ }
38
+ });
39
+ });
40
+ req.on('error', reject);
41
+ req.setTimeout(3000, () => {
42
+ req.destroy();
43
+ reject(new Error('status request timed out'));
44
+ });
45
+ });
46
+ }
47
+
48
+ async function main() {
49
+ const server = await start(PROXY_PORT, {
50
+ // Run RFC 7050 discovery so the network's real NAT64 prefix is used.
51
+ discoverPrefix: true,
52
+ // Serve SOCKS5 as well, for ssh/git/database clients.
53
+ socksPort: SOCKS_PORT,
54
+ });
55
+
56
+ if (!server) {
57
+ console.log('Bridge not needed on this network.');
58
+ return;
59
+ }
60
+
61
+ const { port } = server.address();
62
+ console.log(`Proxy: http://127.0.0.1:${port}`);
63
+ console.log(`SOCKS5: 127.0.0.1:${SOCKS_PORT}`);
64
+ console.log(`Health: http://127.0.0.1:${port}/healthz`);
65
+ console.log(`Metrics: http://127.0.0.1:${port}/metrics\n`);
66
+
67
+ // Periodically check that the bridge is doing what it claims. A translation
68
+ // rate of zero alongside rising fallbacks means DNS64 is failing.
69
+ const monitor = setInterval(async () => {
70
+ try {
71
+ const status = await fetchStatus(port);
72
+ const { nat64, directIpv4Fallback } = status.routes;
73
+
74
+ if (directIpv4Fallback > 0 && nat64 === 0) {
75
+ console.warn(
76
+ `[warn] ${directIpv4Fallback} connection(s) fell back to untranslated IPv4 ` +
77
+ `and none were translated. Run "ipv6-bridge doctor" to find out why.`
78
+ );
79
+ } else {
80
+ console.log(
81
+ `[ok] translated=${nat64} fallback=${directIpv4Fallback} ` +
82
+ `rate=${status.translationRate ?? 'n/a'} ` +
83
+ `cache=${status.dnsCache.hitRate}`
84
+ );
85
+ }
86
+ } catch (err) {
87
+ console.error(`[error] status check failed: ${err.message}`);
88
+ }
89
+ }, 15000);
90
+
91
+ const shutdown = async (signal) => {
92
+ console.log(`\nReceived ${signal}, shutting down...`);
93
+ clearInterval(monitor);
94
+ await stop();
95
+ console.log('Stopped cleanly.');
96
+ process.exit(0);
97
+ };
98
+
99
+ process.on('SIGINT', () => shutdown('SIGINT'));
100
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
101
+ }
102
+
103
+ main().catch((error) => {
104
+ console.error('Failed to start:', error.message);
105
+ process.exit(1);
106
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ipv6-bridge",
3
- "version": "1.0.0",
4
- "description": "Local DNS64/NAT64 proxy for IPv6-only networks — access IPv4 sites from IPv6-only environments",
3
+ "version": "2.1.1",
4
+ "description": "DNS64-aware HTTP proxy for IPv6-only networks — access IPv4 sites from IPv6-only environments",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -9,8 +9,8 @@
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node src/cli.js start",
12
- "test": "node --test tests/*.test.js",
13
- "demo": "cd demo-app && npm install && npm start",
12
+ "test": "node --test",
13
+ "demo": "node demo-app/server.js",
14
14
  "prepublishOnly": "npm test"
15
15
  },
16
16
  "keywords": [
@@ -42,9 +42,9 @@
42
42
  "files": [
43
43
  "src/",
44
44
  "examples/",
45
+ "docs/",
45
46
  "LICENSE",
46
- "README.md",
47
- "CHANGELOG.md"
47
+ "README.md"
48
48
  ],
49
49
  "dependencies": {}
50
50
  }
package/src/agent.js ADDED
@@ -0,0 +1,271 @@
1
+ /**
2
+ * IPv6 Bridge - Embeddable connection primitives
3
+ *
4
+ * Everything here lets an application use DNS64/NAT64 translation directly,
5
+ * without running a proxy or changing any system configuration. Outbound
6
+ * connections are made over native IPv6 where possible, through a synthesized
7
+ * NAT64 address where translation is needed, and over IPv4 as a last resort.
8
+ *
9
+ * None of this can manufacture connectivity the host does not have. It makes
10
+ * the host reach everything it *can* reach, regardless of address family.
11
+ *
12
+ * @module agent
13
+ */
14
+
15
+ const http = require('http');
16
+ const https = require('https');
17
+ const tls = require('tls');
18
+ const { connectWithFallback } = require('./connect');
19
+ const { resolveCandidates } = require('./dns64');
20
+ const config = require('./config');
21
+
22
+ function shouldBypass(hostname) {
23
+ return !config.BYPASS.isEmpty && config.BYPASS.matches(hostname);
24
+ }
25
+
26
+ /**
27
+ * Open a bridged TCP connection for an agent.
28
+ *
29
+ * @param {object} options - Connection options from the agent
30
+ * @param {number} defaultPort - Port to use when none is given
31
+ * @returns {Promise<net.Socket>} Connected socket
32
+ */
33
+ function openSocket(options, defaultPort) {
34
+ const hostname = options.host;
35
+ const port = Number(options.port) || defaultPort;
36
+ return connectWithFallback(hostname, port, { bypass: shouldBypass(hostname) })
37
+ .then(({ socket }) => socket);
38
+ }
39
+
40
+ /**
41
+ * Layer TLS onto an already-connected plaintext socket, reporting success or
42
+ * failure through a Node-style callback.
43
+ *
44
+ * tls.connect() can throw synchronously — a caller-supplied option such as an
45
+ * invalid secureProtocol or cipher list fails while it builds the security
46
+ * context, before any socket event fires. Left uncaught, that throw would
47
+ * escape as an uncaught exception and crash the embedding host application on
48
+ * its very first request, which is a much worse outcome than the one failed
49
+ * connection a caught error produces.
50
+ *
51
+ * @param {net.Socket} socket - Connected plaintext socket to upgrade
52
+ * @param {object} tlsOptions - Options for tls.connect
53
+ * @param {string} hostname - Hostname to validate the certificate against
54
+ * @param {(err: Error|null, socket?: tls.TLSSocket) => void} callback - Result callback
55
+ */
56
+ function upgradeToTls(socket, tlsOptions, hostname, callback) {
57
+ let secure;
58
+ try {
59
+ secure = tls.connect({
60
+ ...tlsOptions,
61
+ socket,
62
+ // SNI and certificate identity must use the real hostname, never the
63
+ // synthesized address the plaintext socket was dialled through.
64
+ host: hostname,
65
+ servername: tlsOptions.servername || hostname,
66
+ });
67
+ } catch (err) {
68
+ socket.destroy();
69
+ callback(err);
70
+ return;
71
+ }
72
+
73
+ const onError = (err) => {
74
+ socket.destroy();
75
+ callback(err);
76
+ };
77
+
78
+ secure.once('error', onError);
79
+ secure.once('secureConnect', () => {
80
+ secure.removeListener('error', onError);
81
+ callback(null, secure);
82
+ });
83
+ }
84
+
85
+ /**
86
+ * HTTP agent that resolves through DNS64 and pools the resulting sockets.
87
+ *
88
+ * Sockets are keyed by the original hostname, so pooling is unaffected by the
89
+ * fact that the address actually dialled may be synthesized.
90
+ */
91
+ class BridgeHttpAgent extends http.Agent {
92
+ constructor(options = {}) {
93
+ super({
94
+ keepAlive: true,
95
+ keepAliveMsecs: config.KEEP_ALIVE_MS,
96
+ maxSockets: config.MAX_SOCKETS_PER_HOST,
97
+ timeout: config.CONNECTION_TIMEOUT,
98
+ ...options,
99
+ });
100
+ }
101
+
102
+ createConnection(options, callback) {
103
+ openSocket(options, 80).then(
104
+ (socket) => callback(null, socket),
105
+ (err) => callback(err)
106
+ );
107
+ }
108
+ }
109
+
110
+ /**
111
+ * HTTPS agent that performs the TLS handshake over a bridged socket.
112
+ *
113
+ * The certificate is validated against the original hostname, never against
114
+ * the synthesized address it was reached through — otherwise validation could
115
+ * never succeed and users would be pushed into disabling it.
116
+ */
117
+ class BridgeHttpsAgent extends https.Agent {
118
+ constructor(options = {}) {
119
+ super({
120
+ keepAlive: true,
121
+ keepAliveMsecs: config.KEEP_ALIVE_MS,
122
+ maxSockets: config.MAX_SOCKETS_PER_HOST,
123
+ timeout: config.CONNECTION_TIMEOUT,
124
+ ...options,
125
+ });
126
+ this.tlsOptions = options;
127
+ }
128
+
129
+ createConnection(options, callback) {
130
+ const hostname = options.host;
131
+
132
+ openSocket(options, 443).then(
133
+ (socket) => upgradeToTls(socket, { ...this.tlsOptions, ...options }, hostname, callback),
134
+ (err) => callback(err)
135
+ );
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Create an HTTP agent backed by the bridge.
141
+ *
142
+ * @param {object} [options] - http.Agent options
143
+ * @returns {http.Agent} Agent for use with http.request / axios / got
144
+ */
145
+ function createAgent(options) {
146
+ return new BridgeHttpAgent(options);
147
+ }
148
+
149
+ /**
150
+ * Create an HTTPS agent backed by the bridge.
151
+ *
152
+ * @param {object} [options] - https.Agent options
153
+ * @returns {https.Agent} Agent for use with https.request / axios / got
154
+ */
155
+ function createHttpsAgent(options) {
156
+ return new BridgeHttpsAgent(options);
157
+ }
158
+
159
+ /**
160
+ * Create agents for both protocols.
161
+ *
162
+ * @param {object} [options] - Agent options applied to both
163
+ * @returns {{http: http.Agent, https: https.Agent}} Agents by protocol
164
+ */
165
+ function createAgents(options) {
166
+ return {
167
+ http: createAgent(options),
168
+ https: createHttpsAgent(options),
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Create a dns.lookup-compatible function that applies DNS64 synthesis.
174
+ *
175
+ * Anything accepting a `lookup` option — net.connect, http.request, most
176
+ * client libraries — can use this without further changes. Matches the real
177
+ * dns.lookup contract: `options` may be omitted, a plain object, or (per
178
+ * Node's documented shorthand) an integer meaning the requested address
179
+ * family.
180
+ *
181
+ * @returns {Function} Function with the dns.lookup signature
182
+ */
183
+ function createLookup() {
184
+ return function bridgeLookup(hostname, options, callback) {
185
+ if (typeof options === 'function') {
186
+ callback = options;
187
+ options = {};
188
+ } else if (typeof options === 'number') {
189
+ options = { family: options };
190
+ } else {
191
+ options = options || {};
192
+ }
193
+
194
+ resolveCandidates(hostname).then((candidates) => {
195
+ const matching = options.family
196
+ ? candidates.filter((c) => c.family === options.family)
197
+ : candidates;
198
+
199
+ if (options.family && matching.length === 0) {
200
+ const err = new Error(
201
+ `No family ${options.family} address available for ${hostname}`
202
+ );
203
+ err.code = 'EAI_ADDRFAMILY';
204
+ callback(err);
205
+ return;
206
+ }
207
+
208
+ const selected = matching.length > 0 ? matching : candidates;
209
+
210
+ if (options.all) {
211
+ callback(null, selected.map((c) => ({ address: c.host, family: c.family })));
212
+ } else {
213
+ callback(null, selected[0].host, selected[0].family);
214
+ }
215
+ }, (err) => callback(err));
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Create a connector for undici (and therefore Node's global fetch).
221
+ *
222
+ * undici is not a dependency of this package. Applications that already use it
223
+ * can wire the bridge in:
224
+ *
225
+ * const { Agent, setGlobalDispatcher } = require('undici');
226
+ * const { createConnector } = require('ipv6-bridge');
227
+ * setGlobalDispatcher(new Agent({ connect: createConnector() }));
228
+ *
229
+ * @returns {Function} Function matching undici's `connect` option
230
+ */
231
+ function createConnector() {
232
+ return function bridgeConnect(options, callback) {
233
+ const { hostname, port, protocol } = options;
234
+ const isSecure = protocol === 'https:';
235
+ const targetPort = Number(port) || (isSecure ? 443 : 80);
236
+
237
+ connectWithFallback(hostname, targetPort, { bypass: shouldBypass(hostname) }).then(
238
+ ({ socket }) => {
239
+ if (!isSecure) {
240
+ callback(null, socket);
241
+ return;
242
+ }
243
+ upgradeToTls(socket, options, hostname, callback);
244
+ },
245
+ (err) => callback(err)
246
+ );
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Resolve a hostname the way the bridge would, without connecting.
252
+ *
253
+ * Useful for logging or asserting in tests which route would be taken.
254
+ *
255
+ * @param {string} hostname - Hostname or IP literal
256
+ * @returns {Promise<Array<{host: string, family: number, mode: string}>>} Candidates
257
+ */
258
+ function resolve(hostname) {
259
+ return resolveCandidates(hostname);
260
+ }
261
+
262
+ module.exports = {
263
+ createAgent,
264
+ createHttpsAgent,
265
+ createAgents,
266
+ createLookup,
267
+ createConnector,
268
+ resolve,
269
+ BridgeHttpAgent,
270
+ BridgeHttpsAgent,
271
+ };