IPsec 在 IP 层按策略保护流量,IKEv2 负责认证对端、协商算法和维护安全关联。以总部 10.10.0.0/24 访问分部 10.20.0.0/24 为例:业务主机把包交给网关,网关判断是否需要保护,用已建立的出站 SA 封装;对端网关查找入站 SA、认证解密并核对策略,再转发到内网。
这里有三件独立的事:系统路由把包送到哪里、策略要求怎样保护、SA 是否拥有可用的密钥和状态。IKE 显示已连接,只证明其中一部分成立。
| 快速定位 | 应看什么 |
| IKE SA 与 Child SA | 控制消息状态与业务流量状态 |
| ESP 和 AH | 是否加密、哪些字段被认证 |
| Tunnel 与 Transport | 是否封装完整原始 IP 包 |
| IKE_AUTH 能否看见头 | 受保护载荷与外层头的边界 |
| UDP 4500 为什么有两种格式 | IKE 的零标记与 ESP 的非零 SPI |
| 换钥是否重新验证用户 | Rekey 与 Reauthentication |
| 有 SA 却没有业务计数 | 路由、选择器、转发与回程 |
策略与安全关联:为什么要分开保存
安全策略数据库 SPD 表达流量应丢弃、绕过还是使用 IPsec 保护;安全关联数据库 SAD 保存具体算法、密钥、SPI、序号和生命周期等状态。一个策略可以在不同时间指向不同代际的 SA,换钥时也可能短暂保留新旧 SA。IPsec 架构 RFC 4301
ESP SA 是单向的:A→B 与 B→A 使用各自的 SPI、密钥与序号。IKEv2 一次 Child SA 协商通常创建这样一对 SA,管理工具可能将其整体显示为一个 CHILD_SA。IKE SA 则保护双方后续控制消息,不能把它的 SPI 和 ESP 的 SPI 当成同一个编号空间。strongSwan IPsec 介绍
入站 ESP 使用接收侧认可的 SPI 查找安全关联;SPI 不是密码,也不是用户身份。即使报文找到 SA,还必须通过认证、重放检查与策略约束。不能只凭看到正确 SPI 就把包当成合法业务。
Traffic Selector 如何限制一条隧道
Traffic Selector 包括地址范围、协议与端口范围。发起方可以提议较宽范围,响应方依据策略接受更窄范围。例如总部提议整个 10.10.0.0/24 访问分部 10.20.0.0/24,最终只允许分部服务器 10.20.0.8 的 TCP 443,就不能把协商成功解释为整个分部网段任意端口可访问。
以下只演示一个方向上已经确定的选择器匹配,不模拟完整 IKE 协商或内核 SPD 优先级。
from ipaddress import ip_address, ip_network
def protected_flow(src, dst, protocol, dport):
return (ip_address(src) in ip_network('10.10.0.0/24')
and ip_address(dst) == ip_address('10.20.0.8')
and protocol == 6 and dport == 443)
assert protected_flow('10.10.0.9', '10.20.0.8', 6, 443)
assert not protected_flow('10.10.0.9', '10.20.0.8', 6, 22)
assert not protected_flow('10.10.0.9', '10.20.0.9', 6, 443)
assert not protected_flow('10.11.0.9', '10.20.0.8', 6, 443)
print('地址、协议、端口共同匹配的局部模型通过')
找到SPI,就能把内层包交付吗?
把正确SPI换成出站SA的SPI,即使同一设备保存它,也不能当作本方向的入站状态。恢复后再改目标端口,密码学检查通过仍可能因选择器拒绝。SPI不是通行密码。
正在呈现知识画面
找到SPI,就能把内层包交付吗?SPI定位单向密钥状态,选择器约束被保护的流量。查到SA、认证成功、策略允许是三个相继的判据。
{"id":"ipsec-sa-and-selector","title":"找到SPI,就能把内层包交付吗?","summary":"SPI定位单向密钥状态,选择器约束被保护的流量。查到SA、认证成功、策略允许是三个相继的判据。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"form-field\"><span data-i18n=\"spi\"></span><select id=\"spi\"><option value=\"258\">0x102 inbound</option><option value=\"513\">0x201 outbound</option><option value=\"999\">0x3e7 unknown</option></select></label><label class=\"check\"><input type=\"checkbox\" id=\"auth\" checked><span data-i18n=\"auth\"></span></label><div class=\"control\"><label for=\"dst\" data-i18n=\"dst\"></label><output for=\"dst\" id=\"dst-value\"></output><input id=\"dst\" type=\"range\" min=\"7\" max=\"9\" value=\"8\" step=\"1\"></div><label class=\"form-field\"><span data-i18n=\"port\"></span><select id=\"port\"><option value=\"443\">TCP443</option><option value=\"22\">TCP22</option></select></label><div class=\"track-row\"><span data-i18n=\"trace\"></span><div id=\"trace\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"result\"></span><strong id=\"result\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.metrics strong{font-size:17px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){const sa=+$('#spi').value===258,auth=sa&&$('#auth').checked,policy=auth&&+$('#dst').value===8&&+$('#port').value===443;set('dst-value','10.20.0.'+$('#dst').value);cells('trace',[{text:'SAD',cls:sa?'on':'error'},{text:'AEAD/replay',cls:auth?'on':'empty'},{text:'selector',cls:policy?'on':'empty'}]);set('result',!sa?'DROP: no inbound SA':!auth?'DROP: authentication':!policy?'DROP: selector':'DELIVER');$('#result').dataset.deliver=String(policy);}document.querySelectorAll('input,select').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"auth":"认证与重放检查通过","dst":"内层目的末字节","heading":"找到SPI,就能把内层包交付吗?","intro":"修改入站SPI、认证结果和内层目标,追踪SAD查找后继续执行的检查。","port":"内层目的端口","reset":"重置","result":"处理结果","scope":"固定入站SA SPI0x102,出站SA0x201;只允许源10.10.0.9到10.20.0.8 TCP443。认证开关包括完整性及重放前提;无真实解密或SPD优先级。","scopeLabel":"模型范围与约定","spi":"收到的 SPI","trace":"入站检查链"}}
ESP:可见字段、加密内容与认证
ESP 使用 IP 协议号 50,AH 使用 51;这里的数字是协议号,不是 TCP/UDP 端口。ESP 可提供机密性与完整性,具体由协商变换决定;AH 提供认证而不提供加密,并覆盖选定的 IP 头字段,对 NAT 地址改写更敏感。不能把所有 ESP 组合都描述为固定一种加密算法。
| 部分 | 可见性与作用 |
| SPI 4 B、Sequence Number 4 B | 明文头,帮助定位 SA 与重放检查 |
| 显式 IV | 算法相关,AES-GCM-ESP 为 8 B |
| Payload、Padding、Pad Length、Next Header | 在加密变换下被保护的内容 |
| ICV / Tag | 完整性检查,长度由变换决定 |
Transport 模式通常保护原始 IP 头之后的上层内容;Tunnel 模式把完整原始 IP 包作为内层载荷,新增外层 IP 头。前者不意味着原始 IP 头完全不可修改,后者也不等于业务端到端加密:网关到网关隧道会在对端网关终止。ESP RFC 4303
AES-GCM 的三个长度不要混
AES-GCM-ESP 的 nonce 由 4 B salt 和 8 B 显式 IV 组成。salt 随密钥材料分配,不是每包再在 ESP 头里发送一个 4 B 字段;显式 IV 出现在包中,同一密钥下不得重复。SPI 与序号参与附加认证数据,因而不能任意改写。
Tag 支持长度由具体变换标识决定:RFC 4106 要求支持 16 B,并允许 8 B 或 12 B,不能一律写成“GCM 的 ICV 是 12 B”。下例选 16 B Tag、最少四字节对齐、IPv4 无选项与 UDP NAT-T;没有额外流量隐藏填充。AES-GCM-ESP RFC 4106
def esp_tunnel_size(inner, outer_ip=20, udp=True):
padding = (-(inner + 2)) % 4
total = outer_ip + (8 if udp else 0) + 8 + 8 + inner + padding + 2 + 16
return total, padding
assert esp_tunnel_size(1000) == (1064, 2)
assert esp_tunnel_size(1000, udp=False) == (1056, 2)
assert esp_tunnel_size(1000, outer_ip=40) == (1084, 2)
assert max(n for n in range(1501) if esp_tunnel_size(n)[0] <= 1500) == 1438
assert esp_tunnel_size(1438) == (1500, 0)
assert esp_tunnel_size(1439)[0] == 1504
print('明确算法与封装假设下的 ESP 长度预算通过')
1438 B 是该组假设下的最大完整内层 IP 包,不是 TCP 应用载荷。内层 IPv4/TCP 还要扣除自己的头部;算法、外层 IPv6、IP 选项和其他隧道变化后应重新计算。
序号与接收重放窗口允许一定程度的乱序,又拒绝重复包。ESN 使用扩展序号,但线上 ESP 序号字段仍为低 32 位,高位由状态推断并参与认证。重放窗口不是可靠传输窗口:丢失的 ESP 包不会因此被 ESP 自动重传。
IKEv2:先派生保护密钥,再完成身份认证
没有额外挑战、EAP 或扩展交换时,初始建立通常包含 IKE_SA_INIT 与 IKE_AUTH 两个请求—响应交换,即约两个网络 RTT,另加计算时间。Cookie、错误 DH 组重试、认证方法和丢包都可能增加时间,不能写成所有连接恒定四个包。IKEv2 初始交换
IKE_SA_INIT 交换算法提案、临时 DH 公钥和 nonce。这时获得共同秘密,不代表已经证明对方身份:身份验证要绑定协商记录,防止只建立一个与攻击者共享的加密通道。
IKE_AUTH 的加密载荷包含身份、认证材料和第一条 Child SA 所需参数;外层 IP/UDP 以及 IKE 头仍可见。AUTH 可以基于签名或共享密钥等机制,不能限定为“用证书私钥给第一条消息签名”。基础签名认证的输入还包含对方 nonce 和用 SK_pi/SK_pr 处理的身份载荷,建立握手与身份之间的绑定。RFC 7296 §2.15
EAP 认证可能需要多个 IKE_AUTH 往返;声明 IDi 也不等于已经完成 EAP 身份验证。认证通过之后仍要检查该身份能否请求相应选择器。
密钥派生中的次序与用途
在基础 DH 流程中,记 g^ir 为双方各自使用“自己的私钥、对方的公钥”计算的共享秘密;不是对两个公开 KE 值直接做 DH。
| 阶段 | 基础派生表达式 |
| 初始 IKE 种子 | SKEYSEED = prf(Ni | Nr, g^ir) |
| IKE 密钥展开 | prf+(SKEYSEED, Ni | Nr | SPIi | SPIr) |
| 首个 Child / 无新 DH 的 Child | KEYMAT = prf+(SK_d, Ni | Nr) |
| 有新 DH 的 CREATE_CHILD_SA | KEYMAT = prf+(SK_d, g^ir_new | Ni | Nr) |
prf+ 是 IKE 定义的迭代展开函数,不是把一个 HMAC 结果无限重复。展开出的 IKE 材料依次服务于 SK_d、两个方向的完整性/加密及身份认证辅助用途;AEAD 组合不另需传统完整性算法的密钥。SK_pi/SK_pr 不是证书私钥。RFC 7296 §2.13–2.17
首个 Child 使用 IKE_SA_INIT 的 nonce;后续创建使用对应新交换的 nonce。存在新的 DH 时,共享秘密在 Ni/Nr 之前,不是可随意放到末尾的附加字段。基础首个 Child 没有自己的额外 KE 交换,不等于整个连接完全没有临时 DH。
NAT-T:UDP 4500 上如何区分 IKE 和 ESP
原生 ESP 没有 TCP/UDP 端口,不少 NAT 难以正确维护多客户端 ESP 映射。NAT-T 将 ESP 封装在 UDP 中,常用 4500;IKE 通常先从 UDP 500 建立,检测 NAT 或按功能条件切到 4500。仅看到 4500 不足以断言一定有 NAT,MOBIKE 等行为也会影响端口使用。strongSwan NAT Traversal
| UDP 4500 的 payload | 判断方式 |
| IKE 消息 | 前置四个零字节 Non-ESP Marker,再接 IKE 头 |
| UDP 封装 ESP | 直接从非零 SPI 开始,不额外加这四个零 |
| NAT keepalive | 特定单字节 0xff,用于维持映射 |
因此前面的 ESP 长度预算只增加 8 B UDP 头,不再加 IKE 专用的 4 B 标记。NAT keepalive 维持映射,受保护的 IKE 活性检查确认 peer 响应,两者不能互相替代。UDP 封装 RFC 3948
换钥、重新认证与地址迁移
CREATE_CHILD_SA 可建立额外 Child、替换已有 Child,也可用于 IKE SA 换钥。基础交换是一个请求加一个响应,约一个 RTT;“小于 0.5 RTT 完成协商”没有对应的协议保证。
Rekey 更新密钥和关联,不等于重新验证证书当前状态或再次执行用户认证。Reauthentication 重走认证过程,影响的状态不同。实现通常让新旧 SA 短暂重叠,接收在途旧包;到期、字节/包计数、重放序号和双方同时发起换钥都需要处理。strongSwan SA Renewal
MOBIKE 是需双方支持并协商的 IKEv2 扩展,让隧道随外层地址变化更新路径;不能因为设备写着“IKEv2”就断言一定支持迁移。它并不替应用创建新的身份,也不保证切换期间零丢包。MOBIKE RFC 4555
证书链较大时,IKE 消息可能受 MTU 与分片限制。IKEv2 的加密消息分片扩展和 IP 层分片不同,也不等于 ESP 数据包可以忽略 PMTU。IKE Fragmentation RFC 7383
从 IKE 成功走到业务成功
Linux 的策略式 IPsec 根据策略捕获匹配流量;XFRM 接口等路由式方式让路由管理更直观,但仍依赖正确的策略、SA 与接口关联。创建接口或添加路由本身不会建立密码学会话。strongSwan Route-based VPN
| 现象 | 应继续核对的证据 |
| 收不到 IKE_SA_INIT 响应 | 外层可达性、UDP 端口、防火墙、对端监听 |
| 收到 NO_PROPOSAL_CHOSEN | 双方算法提案和匹配到的连接配置 |
| AUTHENTICATION_FAILED | 实际身份、凭据、信任链及认证方法 |
| IKE SA 存在但 Child 失败 | ESP 提案、TS、地址分配与授权 |
| Child 成立但出站计数不增 | 业务包是否到网关、路由和策略是否匹配 |
| 出站增长但入站不增 | 对端计数、回程、过滤、NAT-T 与 SA 状态 |
| 小包可用、大传输卡住 | PMTU、封装预算、ICMP 反馈与分片 |
可以从 swanctl --list-sas 查看实际协商结果,再对照业务流和路由;Linux ip -s xfrm policy 能观察策略。需要导出 SA 状态时留意某些原始状态命令会包含密钥材料,诊断共享应优先选不输出密钥的摘要。判断性能也要依据实际流量和卸载是否生效,而不是用“企业级”或“内核实现”直接推导吞吐上限。