<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NAT64 转换器 - IPv4 到 IPv6</title>
<style>
:root {
--bg: #f5f7fb;
--card-bg: #ffffff;
--text: #1e293b;
--text-secondary: #475569;
--border: #e2e8f0;
--accent: #2563eb;
--accent-hover: #1d4ed8;
--success: #059669;
--error: #dc2626;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
--radius: 12px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #f0f4ff 0%, #e8edf5 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
color: var(--text);
}
.container {
background: var(--card-bg);
border-radius: var(--radius);
box-shadow: var(--shadow), 0 10px 25px -5px rgba(0, 0, 0, 0.08);
width: 100%;
max-width: 700px;
padding: 2.5rem;
border: 1px solid var(--border);
transition: all 0.2s ease;
}
h1 {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 0.5rem;
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 0.5rem;
}
h1 span {
background: var(--accent);
color: white;
font-size: 0.9rem;
padding: 0.2rem 0.8rem;
border-radius: 20px;
font-weight: 500;
letter-spacing: 0;
}
.subtitle {
color: var(--text-secondary);
margin-bottom: 2rem;
font-size: 0.95rem;
border-left: 3px solid var(--accent);
padding-left: 0.8rem;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.4rem;
color: var(--text);
}
.input-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
background: #f8fafc;
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.5rem 0.8rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-wrapper:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.input-wrapper input {
border: none;
background: transparent;
flex: 1;
font-size: 1rem;
padding: 0.5rem 0;
outline: none;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
color: var(--text);
}
.input-wrapper .icon {
color: var(--text-secondary);
font-size: 1.1rem;
}
select {
width: 100%;
padding: 0.75rem 0.8rem;
border: 1px solid var(--border);
border-radius: 8px;
background: #f8fafc;
font-size: 0.95rem;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
color: var(--text);
outline: none;
cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
appearance: none;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="%23475569" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>');
background-repeat: no-repeat;
background-position: right 0.8rem center;
background-size: 1.2rem;
}
select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.custom-prefix {
margin-top: 0.8rem;
display: none;
}
.custom-prefix.show {
display: block;
}
.result-box {
background: #f1f5f9;
border-radius: 8px;
padding: 1.2rem;
margin: 1.8rem 0 1rem;
border: 1px solid var(--border);
word-break: break-all;
}
.result-label {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-secondary);
margin-bottom: 0.3rem;
}
.result-ipv6 {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 1.3rem;
font-weight: 700;
color: var(--accent);
background: white;
padding: 0.5rem 0.8rem;
border-radius: 6px;
display: inline-block;
max-width: 100%;
overflow-wrap: anywhere;
border: 1px solid #cbd5e1;
}
.error-message {
color: var(--error);
font-size: 0.9rem;
margin-top: 0.3rem;
display: flex;
align-items: center;
gap: 0.3rem;
}
.conversion-detail {
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: 0.8rem;
background: #f8fafc;
border-radius: 6px;
padding: 0.6rem 0.8rem;
font-family: 'JetBrains Mono', monospace;
}
.footer-note {
font-size: 0.8rem;
color: #64748b;
margin-top: 1.5rem;
text-align: center;
border-top: 1px solid var(--border);
padding-top: 1rem;
}
@media (max-width: 500px) {
.container {
padding: 1.5rem;
}
h1 {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<div class="container">
<h1>
NAT64 转换器
<span>IPv4 → IPv6</span>
</h1>
<div class="subtitle">
基于 nat64.xyz 公共 NAT64 前缀列表 · 实时合成地址
</div>
<div class="form-group">
<label for="ipv4Input">IPv4 地址</label>
<div class="input-wrapper">
<span class="icon">🌐</span>
<input type="text" id="ipv4Input" placeholder="例如 104.21.88.129" value="104.21.88.129" autofocus>
</div>
</div>
<div class="form-group">
<label for="prefixSelect">NAT64 前缀 (Provider / Location)</label>
<select id="prefixSelect">
<optgroup label="Kasper Dupont">
<option value="2a00:1098:2b::/96">2a00:1098:2b::/96 – Germany (Nürnberg)</option>
<option value="2a00:1098:2c:1::/96">2a00:1098:2c:1::/96 – Germany (Nürnberg)</option>
<option value="2a01:4f8:c2c:123f:64::/96">2a01:4f8:c2c:123f:64::/96 – Germany (Nürnberg)</option>
<option value="2a01:4f9:c010:3f02:64::/96">2a01:4f9:c010:3f02:64::/96 – Germany (Nürnberg)</option>
</optgroup>
<optgroup label="level66.services">
<option value="2001:67c:2960:6464::/96" selected>2001:67c:2960:6464::/96 – Anycast (Germany)</option>
</optgroup>
<optgroup label="Trex">
<option value="2001:67c:2b0:db32:0:1::/96">2001:67c:2b0:db32:0:1::/96 – Finland (Tampere)</option>
</optgroup>
<optgroup label="ZTVI">
<option value="2602:fc59:b0:64::/96">2602:fc59:b0:64::/96 – USA (Fremont)</option>
<option value="2602:fc59:11:64::/96">2602:fc59:11:64::/96 – USA (Chicago)</option>
</optgroup>
<option value="custom">🔧 自定义前缀 (输入 /96 前缀)</option>
</select>
</div>
<div class="custom-prefix" id="customPrefixWrapper">
<label for="customPrefixInput">自定义 NAT64 前缀 (/96)</label>
<div class="input-wrapper">
<span class="icon">🔹</span>
<input type="text" id="customPrefixInput" placeholder="例如 2001:db8:abcd:1234::/96">
</div>
</div>
<div class="result-box">
<div class="result-label">合成的 IPv6 地址</div>
<div class="result-ipv6" id="resultIPv6">2001:67c:2960:6464::6815:5881</div>
<div class="error-message" id="errorMessage"></div>
<div class="conversion-detail" id="detailMapping"></div>
</div>
<div class="footer-note">
数据来源 <strong>nat64.xyz</strong> · 十六进制嵌入 (RFC 6052) · 仅供学习与测试
</div>
</div>
<script>
(function() {
// DOM 元素
const ipv4Input = document.getElementById('ipv4Input');
const prefixSelect = document.getElementById('prefixSelect');
const customPrefixWrapper = document.getElementById('customPrefixWrapper');
const customPrefixInput = document.getElementById('customPrefixInput');
const resultIPv6 = document.getElementById('resultIPv6');
const errorMessage = document.getElementById('errorMessage');
const detailMapping = document.getElementById('detailMapping');
// 展开 IPv6 地址为 8 个 16-bit 块数组
function expandIPv6(addr) {
// 移除可能的 zone ID (%)
addr = addr.split('%')[0];
if (addr.includes('::')) {
const parts = addr.split('::');
const left = parts[0] ? parts[0].split(':') : [];
const right = parts[1] ? parts[1].split(':') : [];
const missing = 8 - left.length - right.length;
if (missing < 0) return null; // 无效地址
const middle = new Array(missing).fill('0');
const blocks = left.concat(middle, right);
return blocks.map(b => parseInt(b || '0', 16));
} else {
const blocks = addr.split(':');
if (blocks.length !== 8) return null;
return blocks.map(b => parseInt(b || '0', 16));
}
}
// 压缩 IPv6 地址块数组为字符串
function compressIPv6(blocks) {
if (blocks.length !== 8) return null;
const strs = blocks.map(b => b.toString(16));
// 寻找最长连续零块
let bestStart = -1, bestLen = 0;
let currStart = -1, currLen = 0;
for (let i = 0; i < strs.length; i++) {
if (strs[i] === '0') {
if (currStart === -1) currStart = i;
currLen++;
} else {
if (currLen > bestLen) {
bestLen = currLen;
bestStart = currStart;
}
currStart = -1;
currLen = 0;
}
}
if (currLen > bestLen) {
bestLen = currLen;
bestStart = currStart;
}
if (bestLen < 2) {
return strs.join(':');
}
const left = strs.slice(0, bestStart);
const right = strs.slice(bestStart + bestLen);
let result = left.join(':') + '::' + right.join(':');
if (left.length === 0) result = '::' + right.join(':');
if (right.length === 0) result = left.join(':') + '::';
return result;
}
// 验证并解析 IPv4 地址,返回字节数组或 null
function parseIPv4(ipv4) {
const parts = ipv4.trim().split('.');
if (parts.length !== 4) return null;
const bytes = [];
for (let p of parts) {
const num = parseInt(p, 10);
if (isNaN(num) || num < 0 || num > 255 || p !== num.toString()) return null;
bytes.push(num);
}
return bytes;
}
// 获取当前选中的前缀字符串(去除 /96)
function getCurrentPrefix() {
if (prefixSelect.value === 'custom') {
let val = customPrefixInput.value.trim();
if (!val) return null;
// 允许带 /96 或不带
if (val.endsWith('/96')) val = val.slice(0, -3);
return val;
} else {
let val = prefixSelect.value;
if (val.endsWith('/96')) val = val.slice(0, -3);
return val;
}
}
// 执行转换并更新界面
function updateConversion() {
const ipv4 = ipv4Input.value.trim();
const prefixStr = getCurrentPrefix();
// 清除旧错误
errorMessage.textContent = '';
detailMapping.textContent = '';
if (!ipv4) {
resultIPv6.textContent = '请输入 IPv4 地址';
return;
}
const bytes = parseIPv4(ipv4);
if (!bytes) {
errorMessage.textContent = '❌ IPv4 地址格式无效,请输入形如 192.0.2.1 的地址';
resultIPv6.textContent = '—';
return;
}
if (!prefixStr) {
errorMessage.textContent = '❌ 请选择或输入有效的 NAT64 前缀';
resultIPv6.textContent = '—';
return;
}
// 展开前缀
const expanded = expandIPv6(prefixStr);
if (!expanded || expanded.length !== 8) {
errorMessage.textContent = '❌ IPv6 前缀格式无效或不是 /96 长度';
resultIPv6.textContent = '—';
return;
}
// IPv4 字节转十六进制组合
const hexParts = bytes.map(b => b.toString(16).padStart(2, '0'));
const block6 = parseInt(hexParts[0] + hexParts[1], 16);
const block7 = parseInt(hexParts[2] + hexParts[3], 16);
// 替换最后两个块
expanded[6] = block6;
expanded[7] = block7;
const resultAddr = compressIPv6(expanded);
resultIPv6.textContent = resultAddr;
// 显示转换细节
detailMapping.innerHTML = `
IPv4 十进制: ${bytes.join('.')}<br>
十六进制映射: ${bytes[0]} → 0x${hexParts[0]}, ${bytes[1]} → 0x${hexParts[1]}, ${bytes[2]} → 0x${hexParts[2]}, ${bytes[3]} → 0x${hexParts[3]}<br>
嵌入块: 0x${hexParts[0]}${hexParts[1]} : 0x${hexParts[2]}${hexParts[3]} → <strong>${hexParts[0]}${hexParts[1]}:${hexParts[2]}${hexParts[3]}</strong>
`;
}
// 切换自定义前缀显示
function toggleCustomPrefix() {
if (prefixSelect.value === 'custom') {
customPrefixWrapper.classList.add('show');
} else {
customPrefixWrapper.classList.remove('show');
}
updateConversion();
}
// 事件监听
ipv4Input.addEventListener('input', updateConversion);
prefixSelect.addEventListener('change', toggleCustomPrefix);
customPrefixInput.addEventListener('input', updateConversion);
// 初始调用
toggleCustomPrefix();
updateConversion();
})();
</script>
</body>
</html>
3 个帖子 - 2 位参与者