ipv6-bridge 1.0.0
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/LICENSE +21 -0
- package/README.md +151 -0
- package/examples/basic-usage.js +38 -0
- package/package.json +50 -0
- package/src/cli.js +99 -0
- package/src/config.js +42 -0
- package/src/detect.js +77 -0
- package/src/dns64.js +90 -0
- package/src/index.js +62 -0
- package/src/proxy.js +141 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# IPv6 Bridge
|
|
2
|
+
|
|
3
|
+
> Local DNS64/NAT64 proxy for IPv6-only networks — access IPv4 sites seamlessly.
|
|
4
|
+
|
|
5
|
+
IPv4 addresses are exhausted globally. Many ISPs now deploy IPv6-only networks, but millions of websites still only support IPv4. IPv6 Bridge solves this by running a local proxy that translates traffic using DNS64 and NAT64 standards.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Zero dependencies** — pure Node.js, nothing to install
|
|
10
|
+
- **Auto-detection** — starts only when needed (IPv6-only network with broken NAT64)
|
|
11
|
+
- **HTTP & HTTPS** — full proxy support including CONNECT tunneling
|
|
12
|
+
- **RFC compliant** — implements RFC 6052 (DNS64) and RFC 6146 (NAT64)
|
|
13
|
+
- **Cross-platform** — works on Windows, macOS, and Linux
|
|
14
|
+
- **Programmatic API** — use from your Node.js app or the CLI
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### CLI
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx ipv6-bridge start
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The bridge auto-detects whether it's needed. To force it:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
FORCE_BRIDGE=1 npx ipv6-bridge start
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Programmatic
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
const { start, stop } = require('ipv6-bridge');
|
|
34
|
+
|
|
35
|
+
const server = await start(8080);
|
|
36
|
+
// → returns the server, or null if bridge isn't needed
|
|
37
|
+
|
|
38
|
+
// Later:
|
|
39
|
+
await stop();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Install as a Dependency
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install ipv6-bridge
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## How It Works
|
|
49
|
+
|
|
50
|
+
**The "Language Translator" Analogy**
|
|
51
|
+
> Imagine you only speak English (IPv6), but you need to call a business in Japan where they only speak Japanese (IPv4). If you call them directly, you won't understand each other.
|
|
52
|
+
>
|
|
53
|
+
> This project acts like a live, bilingual phone operator sitting right next to you. When you try to make the call, the software intercepts it, looks up the Japanese translation for the phone number (**DNS64**), and then acts as a middleman translating your English sentences into Japanese and back again in real-time (**NAT64 proxy**). The result is that you have a seamless conversation without even realizing a translation is happening.
|
|
54
|
+
|
|
55
|
+
### Technical Flow
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
Your App → HTTP request → IPv6 Bridge (localhost:8080)
|
|
59
|
+
│
|
|
60
|
+
DNS64 resolution
|
|
61
|
+
example.com → 142.251.32.14 → 64:ff9b::8efb:200e
|
|
62
|
+
│
|
|
63
|
+
Outbound via IPv6
|
|
64
|
+
│
|
|
65
|
+
ISP NAT64 Gateway
|
|
66
|
+
│
|
|
67
|
+
IPv4 Internet (google.com)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
1. **Detection** — checks if you're on an IPv6-only network
|
|
71
|
+
2. **DNS64** — resolves hostnames; if only an IPv4 address exists, synthesizes an IPv6 address using the NAT64 prefix (`64:ff9b::`)
|
|
72
|
+
3. **Proxy** — routes HTTP/HTTPS through IPv6; the ISP's NAT64 gateway translates to IPv4
|
|
73
|
+
4. **Response** — data flows back through the same path, transparently
|
|
74
|
+
|
|
75
|
+
For a deep dive, see [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
| Environment Variable | Default | Description |
|
|
80
|
+
|---------------------|---------|-------------|
|
|
81
|
+
| `IPV6_BRIDGE_PORT` | `8080` | Proxy listen port |
|
|
82
|
+
| `FORCE_BRIDGE` | _(unset)_ | Start even if not needed |
|
|
83
|
+
| `NAT64_PREFIX` | `64:ff9b::` | Custom NAT64 prefix |
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
### `start(port?): Promise<http.Server | null>`
|
|
88
|
+
|
|
89
|
+
Starts the proxy. Returns the server instance, or `null` if the bridge isn't needed.
|
|
90
|
+
|
|
91
|
+
### `stop(): Promise<void>`
|
|
92
|
+
|
|
93
|
+
Stops the running bridge.
|
|
94
|
+
|
|
95
|
+
See [docs/API.md](docs/API.md) for the full API reference.
|
|
96
|
+
|
|
97
|
+
## Testing
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm test
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Demo Application
|
|
104
|
+
|
|
105
|
+
An interactive diagnostics tool:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
cd demo-app && npm start
|
|
109
|
+
# Open http://localhost:3000
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Dual-Stack Test Server
|
|
113
|
+
|
|
114
|
+
Test IPv4 and IPv6 endpoints with real-time logging:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
cd test-server && node server.js
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
See [test-server/README.md](test-server/README.md) for details.
|
|
121
|
+
|
|
122
|
+
## Project Structure
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
src/
|
|
126
|
+
cli.js CLI entry point
|
|
127
|
+
config.js Configuration constants
|
|
128
|
+
detect.js Network detection
|
|
129
|
+
dns64.js DNS64 resolver
|
|
130
|
+
index.js Public API (start/stop)
|
|
131
|
+
proxy.js HTTP/HTTPS proxy
|
|
132
|
+
tests/ Test suite
|
|
133
|
+
demo-app/ Interactive demo
|
|
134
|
+
test-server/ Dual-stack test server
|
|
135
|
+
examples/ Usage examples
|
|
136
|
+
docs/ Extended documentation
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Standards
|
|
140
|
+
|
|
141
|
+
- [RFC 6052](https://tools.ietf.org/html/rfc6052) — IPv6 Addressing of IPv4/IPv6 Translators
|
|
142
|
+
- [RFC 6146](https://tools.ietf.org/html/rfc6146) — Stateful NAT64
|
|
143
|
+
- [RFC 6147](https://tools.ietf.org/html/rfc6147) — DNS64
|
|
144
|
+
|
|
145
|
+
## Contributing
|
|
146
|
+
|
|
147
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPv6 Bridge — Basic Usage Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates how to integrate IPv6 Bridge into a Node.js application.
|
|
5
|
+
*
|
|
6
|
+
* Run:
|
|
7
|
+
* node examples/basic-usage.js
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { start, stop } = require('../src/index');
|
|
11
|
+
|
|
12
|
+
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);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
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",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ipv6-bridge": "src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/cli.js start",
|
|
12
|
+
"test": "node --test tests/*.test.js",
|
|
13
|
+
"demo": "cd demo-app && npm install && npm start",
|
|
14
|
+
"prepublishOnly": "npm test"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ipv6",
|
|
18
|
+
"ipv4",
|
|
19
|
+
"nat64",
|
|
20
|
+
"dns64",
|
|
21
|
+
"proxy",
|
|
22
|
+
"networking",
|
|
23
|
+
"bridge",
|
|
24
|
+
"translation",
|
|
25
|
+
"connectivity",
|
|
26
|
+
"http-proxy",
|
|
27
|
+
"https-proxy"
|
|
28
|
+
],
|
|
29
|
+
"author": "CS Hari Krishna <csharikrishna1806@gmail.com>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/csharikrishna/ipv6-bridge.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/csharikrishna/ipv6-bridge/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/csharikrishna/ipv6-bridge#readme",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"src/",
|
|
44
|
+
"examples/",
|
|
45
|
+
"LICENSE",
|
|
46
|
+
"README.md",
|
|
47
|
+
"CHANGELOG.md"
|
|
48
|
+
],
|
|
49
|
+
"dependencies": {}
|
|
50
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IPv6 Bridge - Command-Line Interface
|
|
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.
|
|
17
|
+
*
|
|
18
|
+
* @file CLI entry point for IPv6 Bridge
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { start, stop } = require('./index');
|
|
22
|
+
const { DEFAULT_PORT } = require('./config');
|
|
23
|
+
const { version } = require('../package.json');
|
|
24
|
+
|
|
25
|
+
const HELP_TEXT = `
|
|
26
|
+
IPv6 Bridge v${version}
|
|
27
|
+
Local DNS64/NAT64 proxy for IPv6-only networks.
|
|
28
|
+
|
|
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
|
|
33
|
+
|
|
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::)
|
|
38
|
+
|
|
39
|
+
Stop the bridge with Ctrl+C or by sending SIGTERM.
|
|
40
|
+
`.trim();
|
|
41
|
+
|
|
42
|
+
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
|
+
|
|
47
|
+
if (command === '--help' || command === '-h' || !command) {
|
|
48
|
+
console.log(HELP_TEXT);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (command === '--version' || command === '-v') {
|
|
53
|
+
console.log(version);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
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.`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
start(port)
|
|
64
|
+
.then((server) => {
|
|
65
|
+
if (!server) {
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
console.log(`\nIPv6 Bridge running on http://localhost:${port}`);
|
|
69
|
+
console.log(`Configure your browser/system proxy to localhost:${port}\n`);
|
|
70
|
+
})
|
|
71
|
+
.catch((err) => {
|
|
72
|
+
console.error(`Error: ${err.message}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
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);
|
|
84
|
+
|
|
85
|
+
// Catch unhandled errors to prevent silent crashes
|
|
86
|
+
process.on('uncaughtException', (err) => {
|
|
87
|
+
console.error('Uncaught exception:', err.message);
|
|
88
|
+
stop().then(() => process.exit(1));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
process.on('unhandledRejection', (reason) => {
|
|
92
|
+
console.error('Unhandled rejection:', reason);
|
|
93
|
+
stop().then(() => process.exit(1));
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
console.error(`Unknown command: "${command}"\n`);
|
|
97
|
+
console.log(HELP_TEXT);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPv6 Bridge - Configuration
|
|
3
|
+
*
|
|
4
|
+
* RFC 6052 Well-Known NAT64 Prefix and network test constants.
|
|
5
|
+
* All values can be overridden via environment variables.
|
|
6
|
+
*
|
|
7
|
+
* @module config
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
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::',
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default port for the proxy server.
|
|
26
|
+
* Override with the IPV6_BRIDGE_PORT environment variable.
|
|
27
|
+
*/
|
|
28
|
+
DEFAULT_PORT: 8080,
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Test URL for IPv6 connectivity detection.
|
|
32
|
+
* Must be a server with IPv6 support.
|
|
33
|
+
*/
|
|
34
|
+
IPV6_GOOGLE: 'http://ipv6.google.com',
|
|
35
|
+
|
|
36
|
+
/**
|
|
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.
|
|
40
|
+
*/
|
|
41
|
+
IPV4_GOOGLE: 'ipv4.google.com',
|
|
42
|
+
};
|
package/src/detect.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPv6 Bridge - Network Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects whether the system is on an IPv6-only network and whether
|
|
5
|
+
* the bridge is needed to reach IPv4-only servers.
|
|
6
|
+
*
|
|
7
|
+
* @module detect
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const http = require('http');
|
|
11
|
+
const { resolveIPv6 } = require('./dns64');
|
|
12
|
+
const { IPV6_GOOGLE, IPV4_GOOGLE } = require('./config');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Test if the network has IPv6 connectivity.
|
|
16
|
+
*
|
|
17
|
+
* Connects to an IPv6-capable server to verify that IPv6 is available.
|
|
18
|
+
*
|
|
19
|
+
* @returns {Promise<boolean>} true if IPv6 is available
|
|
20
|
+
*/
|
|
21
|
+
async function hasIPv6() {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const req = http.get(IPV6_GOOGLE, { family: 6 }, (res) => {
|
|
24
|
+
// Consume response body to free resources
|
|
25
|
+
res.resume();
|
|
26
|
+
resolve(res.statusCode === 200);
|
|
27
|
+
});
|
|
28
|
+
req.on('error', () => resolve(false));
|
|
29
|
+
req.setTimeout(5000, () => {
|
|
30
|
+
req.destroy();
|
|
31
|
+
resolve(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Determine if the bridge is needed.
|
|
38
|
+
*
|
|
39
|
+
* The bridge is needed when:
|
|
40
|
+
* 1. IPv6 is available, AND
|
|
41
|
+
* 2. IPv4 servers are NOT reachable via the ISP's NAT64 gateway
|
|
42
|
+
*
|
|
43
|
+
* @returns {Promise<boolean>} true if bridge is needed
|
|
44
|
+
*/
|
|
45
|
+
async function needsBridge() {
|
|
46
|
+
const hasV6 = await hasIPv6();
|
|
47
|
+
if (!hasV6) {
|
|
48
|
+
// No IPv6 means we're on IPv4 or a broken network.
|
|
49
|
+
// Either way, the bridge can't help.
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const ipv6 = await resolveIPv6(IPV4_GOOGLE);
|
|
55
|
+
if (!ipv6 || ipv6.length === 0) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Try connecting to the synthesized IPv6 address.
|
|
60
|
+
// If this works, the ISP has a working NAT64 gateway.
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
const req = http.get(`http://[${ipv6[0]}]`, { family: 6 }, (res) => {
|
|
63
|
+
res.resume();
|
|
64
|
+
resolve(res.statusCode !== 200);
|
|
65
|
+
});
|
|
66
|
+
req.on('error', () => resolve(true));
|
|
67
|
+
req.setTimeout(5000, () => {
|
|
68
|
+
req.destroy();
|
|
69
|
+
resolve(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { hasIPv6, needsBridge };
|
package/src/dns64.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPv6 Bridge - DNS64 Resolver
|
|
3
|
+
*
|
|
4
|
+
* Implements DNS64 (RFC 6052) for synthesizing IPv6 addresses from IPv4.
|
|
5
|
+
*
|
|
6
|
+
* @module dns64
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const dns = require('dns').promises;
|
|
10
|
+
const { NAT64_PREFIX } = require('./config');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect the IP version of an address string.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} addr - Address or hostname to check
|
|
16
|
+
* @returns {'ipv4'|'ipv6'|'hostname'|null} Address type
|
|
17
|
+
*/
|
|
18
|
+
function detectIPVersion(addr) {
|
|
19
|
+
if (!addr) return null;
|
|
20
|
+
|
|
21
|
+
// IPv4: dotted decimal (0-255 per octet)
|
|
22
|
+
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(addr)) {
|
|
23
|
+
const parts = addr.split('.').map(Number);
|
|
24
|
+
if (parts.every((p) => p >= 0 && p <= 255)) {
|
|
25
|
+
return 'ipv4';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// IPv6: colon-delimited hex (includes :: shorthand)
|
|
30
|
+
if (/^[a-f0-9:]+$/i.test(addr) && addr.includes(':')) {
|
|
31
|
+
return 'ipv6';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Everything else is a hostname
|
|
35
|
+
return 'hostname';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convert an IPv4 address to IPv6 using the NAT64 prefix (RFC 6052).
|
|
40
|
+
*
|
|
41
|
+
* The IPv4 address is embedded in the lower 32 bits of the IPv6 address:
|
|
42
|
+
* 192.0.2.1 → 64:ff9b::c000:0201
|
|
43
|
+
*
|
|
44
|
+
* @param {string} ipv4 - IPv4 address (e.g., '192.0.2.1')
|
|
45
|
+
* @returns {string} IPv6 address with NAT64 prefix
|
|
46
|
+
* @throws {Error} If the input is not a valid IPv4 address
|
|
47
|
+
*/
|
|
48
|
+
function ipv4ToIPv6(ipv4) {
|
|
49
|
+
if (!ipv4 || typeof ipv4 !== 'string') {
|
|
50
|
+
throw new Error('ipv4ToIPv6: address must be a non-empty string');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parts = ipv4.split('.').map(Number);
|
|
54
|
+
if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
|
|
55
|
+
throw new Error(`ipv4ToIPv6: invalid IPv4 address "${ipv4}"`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hex1 = ((parts[0] << 8) | parts[1]).toString(16).padStart(4, '0');
|
|
59
|
+
const hex2 = ((parts[2] << 8) | parts[3]).toString(16).padStart(4, '0');
|
|
60
|
+
return `${NAT64_PREFIX}${hex1}:${hex2}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a hostname to IPv6 addresses using DNS64 (RFC 6052).
|
|
65
|
+
*
|
|
66
|
+
* 1. Try native AAAA resolution first.
|
|
67
|
+
* 2. Fall back to A resolution and synthesize IPv6 via NAT64 prefix.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} hostname - Domain name to resolve
|
|
70
|
+
* @returns {Promise<string[]>} Array of IPv6 addresses
|
|
71
|
+
* @throws {Error} If DNS resolution fails completely
|
|
72
|
+
*/
|
|
73
|
+
async function resolveIPv6(hostname) {
|
|
74
|
+
try {
|
|
75
|
+
const resolver = dns.resolve6(hostname);
|
|
76
|
+
const timeout = new Promise((_, reject) =>
|
|
77
|
+
setTimeout(() => reject(new Error('DNS timeout')), 5000)
|
|
78
|
+
);
|
|
79
|
+
return await Promise.race([resolver, timeout]);
|
|
80
|
+
} catch {
|
|
81
|
+
try {
|
|
82
|
+
const ipv4 = await dns.resolve4(hostname);
|
|
83
|
+
return ipv4.map(ipv4ToIPv6);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
throw new Error(`DNS resolution failed for ${hostname}: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { ipv4ToIPv6, resolveIPv6, detectIPVersion };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPv6 Bridge - Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Coordinates bridge startup and shutdown. The bridge consists of:
|
|
5
|
+
*
|
|
6
|
+
* 1. Detection (detect.js): Checks if the bridge is needed.
|
|
7
|
+
* 2. DNS64 Resolver (dns64.js): Synthesizes IPv6 addresses from IPv4.
|
|
8
|
+
* 3. Proxy (proxy.js): HTTP/HTTPS proxy with NAT64 routing.
|
|
9
|
+
*
|
|
10
|
+
* @module ipv6-bridge
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { createProxy } = require('./proxy');
|
|
14
|
+
const { needsBridge } = require('./detect');
|
|
15
|
+
|
|
16
|
+
let activeServer = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Start the IPv6 bridge.
|
|
20
|
+
*
|
|
21
|
+
* @param {number} [port=8080] - Port to listen on
|
|
22
|
+
* @returns {Promise<http.Server|null>} Server instance if started, null if not needed
|
|
23
|
+
* @throws {Error} If already running or startup fails
|
|
24
|
+
*/
|
|
25
|
+
async function start(port = 8080) {
|
|
26
|
+
if (activeServer) {
|
|
27
|
+
throw new Error('IPv6 Bridge is already running');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Step 1: Detect if bridge is needed
|
|
31
|
+
const needed = await needsBridge();
|
|
32
|
+
if (!needed && !process.env.FORCE_BRIDGE) {
|
|
33
|
+
console.log('IPv6 bridge not needed — you have dual-stack or working NAT64.');
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!needed) {
|
|
38
|
+
console.log('IPv6 bridge not needed, but FORCE_BRIDGE is set — starting anyway.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Step 2: Start the proxy server
|
|
42
|
+
activeServer = await createProxy(port);
|
|
43
|
+
return activeServer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Stop the IPv6 bridge.
|
|
48
|
+
*
|
|
49
|
+
* @returns {Promise<void>} Resolves when the server has closed
|
|
50
|
+
*/
|
|
51
|
+
function stop() {
|
|
52
|
+
if (!activeServer) return Promise.resolve();
|
|
53
|
+
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
activeServer.close(() => {
|
|
56
|
+
activeServer = null;
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { start, stop };
|
package/src/proxy.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPv6 Bridge - HTTP/HTTPS Proxy
|
|
3
|
+
*
|
|
4
|
+
* Implements application-level NAT64 (RFC 6146) by intercepting HTTP/HTTPS
|
|
5
|
+
* requests, resolving hostnames via DNS64, and routing through IPv6.
|
|
6
|
+
*
|
|
7
|
+
* @module proxy
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const http = require('http');
|
|
11
|
+
const net = require('net');
|
|
12
|
+
const { resolveIPv6, detectIPVersion } = require('./dns64');
|
|
13
|
+
const { DEFAULT_PORT } = require('./config');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a target host to an IPv6 address if needed.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} hostname - The hostname or IP to resolve
|
|
19
|
+
* @returns {Promise<{host: string, family: number}>} Resolved host and IP family
|
|
20
|
+
*/
|
|
21
|
+
async function resolveTarget(hostname) {
|
|
22
|
+
const ipVersion = detectIPVersion(hostname);
|
|
23
|
+
|
|
24
|
+
if (ipVersion === 'ipv6') {
|
|
25
|
+
return { host: hostname, family: 6 };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// For both IPv4 addresses and hostnames, use DNS64 resolution
|
|
29
|
+
// to get an IPv6 address with the NAT64 prefix.
|
|
30
|
+
try {
|
|
31
|
+
const ipv6Addresses = await resolveIPv6(hostname);
|
|
32
|
+
if (ipv6Addresses && ipv6Addresses.length > 0) {
|
|
33
|
+
return { host: ipv6Addresses[0], family: 6 };
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Fall through to direct connection
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { host: hostname, family: 4 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create an HTTP/HTTPS proxy server with NAT64 support.
|
|
44
|
+
*
|
|
45
|
+
* @param {number} port - Port to listen on (default: 8080)
|
|
46
|
+
* @returns {Promise<http.Server>} Resolves with the server once it's listening
|
|
47
|
+
*/
|
|
48
|
+
function createProxy(port = DEFAULT_PORT) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const server = http.createServer(async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const url = new URL(`http://${req.headers.host}${req.url}`);
|
|
53
|
+
const hostname = url.hostname;
|
|
54
|
+
const { host: targetHost, family: ipFamily } = await resolveTarget(hostname);
|
|
55
|
+
|
|
56
|
+
const options = {
|
|
57
|
+
hostname: targetHost,
|
|
58
|
+
port: url.port || 80,
|
|
59
|
+
path: url.pathname + url.search,
|
|
60
|
+
method: req.method,
|
|
61
|
+
headers: req.headers,
|
|
62
|
+
family: ipFamily,
|
|
63
|
+
timeout: 10000,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const proxy = http.request(options, (proxyRes) => {
|
|
67
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
68
|
+
proxyRes.pipe(res);
|
|
69
|
+
proxyRes.on('error', () => {
|
|
70
|
+
if (!res.headersSent) {
|
|
71
|
+
res.writeHead(502).end('Bad Gateway');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
proxy.on('error', () => {
|
|
77
|
+
if (!res.headersSent) {
|
|
78
|
+
res.writeHead(502).end('Bad Gateway');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
proxy.on('timeout', () => {
|
|
83
|
+
proxy.destroy();
|
|
84
|
+
if (!res.headersSent) {
|
|
85
|
+
res.writeHead(504).end('Gateway Timeout');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
req.pipe(proxy);
|
|
90
|
+
req.on('error', () => proxy.destroy());
|
|
91
|
+
} catch {
|
|
92
|
+
if (!res.headersSent) {
|
|
93
|
+
res.writeHead(500).end('Internal Server Error');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// HTTPS CONNECT tunnel handler
|
|
99
|
+
server.on('connect', async (req, socket, head) => {
|
|
100
|
+
try {
|
|
101
|
+
const [hostname, rawPort] = req.url.split(':');
|
|
102
|
+
const targetPort = parseInt(rawPort, 10) || 443;
|
|
103
|
+
|
|
104
|
+
if (targetPort < 1 || targetPort > 65535) {
|
|
105
|
+
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { host: targetHost, family: ipFamily } = await resolveTarget(hostname);
|
|
110
|
+
|
|
111
|
+
const conn = net.connect(
|
|
112
|
+
{ port: targetPort, host: targetHost, family: ipFamily },
|
|
113
|
+
() => {
|
|
114
|
+
socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
115
|
+
conn.write(head);
|
|
116
|
+
conn.pipe(socket).pipe(conn);
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
conn.on('error', () => socket.end());
|
|
121
|
+
socket.on('error', () => conn.end());
|
|
122
|
+
conn.setTimeout(10000, () => {
|
|
123
|
+
conn.destroy();
|
|
124
|
+
socket.end();
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
socket.end();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
server.on('error', (err) => {
|
|
132
|
+
reject(new Error(`Failed to start proxy: ${err.message}`));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
server.listen(port, () => {
|
|
136
|
+
resolve(server);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { createProxy };
|