Rename project to STUN-NAT
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# STUNMap Console
|
# STUN-NAT
|
||||||
|
|
||||||
独立的 NAT1 STUN 内网穿透管理系统,按 Lucky 的 STUN 模块工作方式实现:在一台设备上维持 TCP 或 UDP 的 NAT 映射,获得动态公网端口,再将流量转发到指定内网服务,或交由路由器直转。
|
独立的 STUN 内网穿透管理系统,按 Lucky 的 STUN 模块工作方式实现:在一台设备上维持 TCP 或 UDP 的 NAT 映射,获得动态公网端口,再将流量转发到指定内网服务,或交由路由器直转。
|
||||||
|
|
||||||
它不是双节点 P2P 打洞系统,也不提供中继服务。映射是否可用取决于运营商和每一层 NAT 是否为 NAT1(全锥形)。公网端口不可指定,变化周期也无法保证。
|
它不是双节点 P2P 打洞系统,也不提供中继服务。映射是否可用取决于运营商和每一层 NAT 是否为 NAT1(全锥形)。公网端口不可指定,变化周期也无法保证。
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
stunmap-probe:
|
stun-nat-probe:
|
||||||
build: .
|
build: .
|
||||||
|
container_name: stun-nat-probe
|
||||||
command: ["node", "probe-server.js"]
|
command: ["node", "probe-server.js"]
|
||||||
network_mode: host
|
network_mode: host
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
stunmap:
|
stun-nat:
|
||||||
build: .
|
build: .
|
||||||
container_name: stunmap
|
container_name: stun-nat
|
||||||
network_mode: host
|
network_mode: host
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "stunmap-console",
|
"name": "stun-nat",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Rule-driven NAT1 STUN port mapping console",
|
"description": "Rule-driven STUN port mapping console",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ function sameToken(value) { const input = Buffer.from(String(value || '')); cons
|
|||||||
function tcpProbe(address, probePort) { return new Promise((resolve) => { const socket = net.connect({ host: address, port: probePort }); const timer = setTimeout(() => { socket.destroy(); resolve(false); }, 4000); socket.once('connect', () => { clearTimeout(timer); socket.destroy(); resolve(true); }); socket.once('error', () => { clearTimeout(timer); resolve(false); }); }); }
|
function tcpProbe(address, probePort) { return new Promise((resolve) => { const socket = net.connect({ host: address, port: probePort }); const timer = setTimeout(() => { socket.destroy(); resolve(false); }, 4000); socket.once('connect', () => { clearTimeout(timer); socket.destroy(); resolve(true); }); socket.once('error', () => { clearTimeout(timer); resolve(false); }); }); }
|
||||||
function udpProbe(address, probePort) { return new Promise((resolve) => { const socket = dgram.createSocket('udp4'); const payload = Buffer.from(`SMHP:${token}:${crypto.randomUUID()}`); const timer = setTimeout(() => { socket.close(); resolve(false); }, 4000); socket.on('message', (message) => { clearTimeout(timer); socket.close(); resolve(message.length === payload.length && crypto.timingSafeEqual(message, payload)); }); socket.on('error', () => { clearTimeout(timer); socket.close(); resolve(false); }); socket.send(payload, probePort, address); }); }
|
function udpProbe(address, probePort) { return new Promise((resolve) => { const socket = dgram.createSocket('udp4'); const payload = Buffer.from(`SMHP:${token}:${crypto.randomUUID()}`); const timer = setTimeout(() => { socket.close(); resolve(false); }, 4000); socket.on('message', (message) => { clearTimeout(timer); socket.close(); resolve(message.length === payload.length && crypto.timingSafeEqual(message, payload)); }); socket.on('error', () => { clearTimeout(timer); socket.close(); resolve(false); }); socket.send(payload, probePort, address); }); }
|
||||||
async function body(req) { const chunks=[]; for await (const chunk of req) chunks.push(chunk); return JSON.parse(Buffer.concat(chunks).toString() || '{}'); }
|
async function body(req) { const chunks=[]; for await (const chunk of req) chunks.push(chunk); return JSON.parse(Buffer.concat(chunks).toString() || '{}'); }
|
||||||
http.createServer(async (req, res) => { if (req.method !== 'POST' || req.url !== '/probe') { res.writeHead(404); return res.end(); } try { const input=await body(req); if (!sameToken(input.token)) { res.writeHead(403); return res.end(JSON.stringify({ error:'invalid token' })); } if (!['tcp','udp'].includes(input.protocol) || !Number.isInteger(input.port) || input.port < 1 || input.port > 65535 || !net.isIPv4(input.address)) throw new Error('invalid probe request'); const reachable = input.protocol === 'tcp' ? await tcpProbe(input.address, input.port) : await udpProbe(input.address, input.port); res.writeHead(200, { 'content-type':'application/json' }); res.end(JSON.stringify({ reachable, checkedAt:new Date().toISOString() })); } catch (error) { res.writeHead(400, { 'content-type':'application/json' }); res.end(JSON.stringify({ error:error.message })); } }).listen(port, '0.0.0.0', () => console.log(`STUNMap Probe listening on :${port}`));
|
http.createServer(async (req, res) => { if (req.method !== 'POST' || req.url !== '/probe') { res.writeHead(404); return res.end(); } try { const input=await body(req); if (!sameToken(input.token)) { res.writeHead(403); return res.end(JSON.stringify({ error:'invalid token' })); } if (!['tcp','udp'].includes(input.protocol) || !Number.isInteger(input.port) || input.port < 1 || input.port > 65535 || !net.isIPv4(input.address)) throw new Error('invalid probe request'); const reachable = input.protocol === 'tcp' ? await tcpProbe(input.address, input.port) : await udpProbe(input.address, input.port); res.writeHead(200, { 'content-type':'application/json' }); res.end(JSON.stringify({ reachable, checkedAt:new Date().toISOString() })); } catch (error) { res.writeHead(400, { 'content-type':'application/json' }); res.end(JSON.stringify({ error:error.message })); } }).listen(port, '0.0.0.0', () => console.log(`STUN-NAT Probe listening on :${port}`));
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>STUNMap Console</title>
|
<title>STUN-NAT Console</title>
|
||||||
<link rel="stylesheet" href="/app.css">
|
<link rel="stylesheet" href="/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div><strong>STUNMap</strong><span> NAT1 动态端口穿透</span></div>
|
<div><strong>STUN-NAT</strong><span> 动态端口穿透</span></div>
|
||||||
<div id="summary">加载中</div>
|
<div id="summary">加载中</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function basicAuth(req, res) {
|
|||||||
const sameLength = encoded.length === expected.length;
|
const sameLength = encoded.length === expected.length;
|
||||||
const valid = sameLength && crypto.timingSafeEqual(Buffer.from(encoded), Buffer.from(expected));
|
const valid = sameLength && crypto.timingSafeEqual(Buffer.from(encoded), Buffer.from(expected));
|
||||||
if (valid) return true;
|
if (valid) return true;
|
||||||
res.writeHead(401, { 'www-authenticate': 'Basic realm="STUNMap Console"' });
|
res.writeHead(401, { 'www-authenticate': 'Basic realm="STUN-NAT Console"' });
|
||||||
res.end('Authentication required');
|
res.end('Authentication required');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -295,7 +295,7 @@ async function reconcileRouterMappings() {
|
|||||||
runner.ensureFirewallRule(state.privatePort);
|
runner.ensureFirewallRule(state.privatePort);
|
||||||
const fingerprint = `${state.privatePort}:${runner.rule.routerMapping}:${runner.rule.protocol}`;
|
const fingerprint = `${state.privatePort}:${runner.rule.routerMapping}:${runner.rule.protocol}`;
|
||||||
if (runner.rule.routerMapping !== 'none' && runner.routerFingerprint !== fingerprint) try {
|
if (runner.rule.routerMapping !== 'none' && runner.routerFingerprint !== fingerprint) try {
|
||||||
const result = runner.rule.routerMapping === 'nat-pmp' ? await natPmpMap(runner.rule.protocol, state.privatePort) : await upnpMap(runner.rule.protocol, state.privatePort, `STUNMap ${runner.rule.name}`);
|
const result = runner.rule.routerMapping === 'nat-pmp' ? await natPmpMap(runner.rule.protocol, state.privatePort) : await upnpMap(runner.rule.protocol, state.privatePort, `STUN-NAT ${runner.rule.name}`);
|
||||||
runner.routerFingerprint = fingerprint; const sameWan = !result.externalAddress || result.externalAddress === state.publicAddress;
|
runner.routerFingerprint = fingerprint; const sameWan = !result.externalAddress || result.externalAddress === state.publicAddress;
|
||||||
runner.routerState = { type: runner.rule.routerMapping, externalAddress: result.externalAddress || null, externalPort: result.externalPort, matchesStunAddress: sameWan, verifiedAt: new Date().toISOString() };
|
runner.routerState = { type: runner.rule.routerMapping, externalAddress: result.externalAddress || null, externalPort: result.externalPort, matchesStunAddress: sameWan, verifiedAt: new Date().toISOString() };
|
||||||
runner.log('info', `${runner.rule.routerMapping.toUpperCase()} 映射成功,路由 WAN ${result.externalAddress || '未知'},外部端口 ${result.externalPort}${sameWan ? '' : ';与 STUN 地址不一致,存在上游 NAT'}`);
|
runner.log('info', `${runner.rule.routerMapping.toUpperCase()} 映射成功,路由 WAN ${result.externalAddress || '未知'},外部端口 ${result.externalPort}${sameWan ? '' : ';与 STUN 地址不一致,存在上游 NAT'}`);
|
||||||
@@ -334,5 +334,5 @@ const server = http.createServer(async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await ensureStorage(); await refreshRunners(); setInterval(reconcileRouterMappings, 1000).unref();
|
await ensureStorage(); await refreshRunners(); setInterval(reconcileRouterMappings, 1000).unref();
|
||||||
server.listen(port, '0.0.0.0', () => console.log(`STUNMap Console listening on :${port}`));
|
server.listen(port, '0.0.0.0', () => console.log(`STUN-NAT Console listening on :${port}`));
|
||||||
process.on('SIGTERM', () => { for (const runner of runners.values()) runner.stop(); server.close(() => process.exit(0)); });
|
process.on('SIGTERM', () => { for (const runner of runners.values()) runner.stop(); server.close(() => process.exit(0)); });
|
||||||
|
|||||||
Reference in New Issue
Block a user