搓了一个NAT64转换器(纯玩具)

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>NAT64 ...
搓了一个NAT64转换器(纯玩具)
搓了一个NAT64转换器(纯玩具)
<!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 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文