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(全锥形)。公网端口不可指定,变化周期也无法保证。
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
services:
|
||||
stunmap-probe:
|
||||
stun-nat-probe:
|
||||
build: .
|
||||
container_name: stun-nat-probe
|
||||
command: ["node", "probe-server.js"]
|
||||
network_mode: host
|
||||
environment:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stunmap:
|
||||
stun-nat:
|
||||
build: .
|
||||
container_name: stunmap
|
||||
container_name: stun-nat
|
||||
network_mode: host
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "stunmap-console",
|
||||
"name": "stun-nat",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Rule-driven NAT1 STUN port mapping console",
|
||||
"description": "Rule-driven STUN port mapping console",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"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 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() || '{}'); }
|
||||
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>
|
||||
<meta charset="utf-8">
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div><strong>STUNMap</strong><span> NAT1 动态端口穿透</span></div>
|
||||
<div><strong>STUN-NAT</strong><span> 动态端口穿透</span></div>
|
||||
<div id="summary">加载中</div>
|
||||
</header>
|
||||
<main>
|
||||
|
||||
@@ -33,7 +33,7 @@ function basicAuth(req, res) {
|
||||
const sameLength = encoded.length === expected.length;
|
||||
const valid = sameLength && crypto.timingSafeEqual(Buffer.from(encoded), Buffer.from(expected));
|
||||
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');
|
||||
return false;
|
||||
}
|
||||
@@ -295,7 +295,7 @@ async function reconcileRouterMappings() {
|
||||
runner.ensureFirewallRule(state.privatePort);
|
||||
const fingerprint = `${state.privatePort}:${runner.rule.routerMapping}:${runner.rule.protocol}`;
|
||||
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.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'}`);
|
||||
@@ -334,5 +334,5 @@ const server = http.createServer(async (req, res) => {
|
||||
});
|
||||
|
||||
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)); });
|
||||
|
||||
Reference in New Issue
Block a user