VyOS bug — -noesn variants in IKE proposals break interoperability with Cisco FTD
Status: root cause identified and demonstrated; workaround validated in production
Date: 2026-08-27
Addresses, hostnames and identities in this report have been replaced with documentation
equivalents (RFC 5737 / RFC 1918 / RFC 2606). Relative relationships between them are preserved.
Summary
Since a build somewhere between 2026.06.20-0050-rolling and 2026.08.14-0025-rolling, VyOS
emits -noesn variants in the proposals line of swanctl.conf — that is, in the IKE_SA
proposals — in addition to esp_proposals, where they legitimately belong.
ESN (Extended Sequence Numbers, RFC 4304) is a CHILD_SA transform: Transform Type 5 per
RFC 7296 §3.3.2. It carries no meaning inside an IKE_SA proposal.
As a result, a Cisco FTD receives the IKE_SA_INIT, cannot make sense of the proposal, and
silently discards it — no NO_PROPOSAL_CHOSEN, no INVALID_KE_PAYLOAD, no response at all.
The tunnel never comes up, and nothing on the initiator side explains why.
Reproduction
Identical configuration on both nodes (config.boot diffed; ESN is never mentioned):
ike-group hq {
proposal 1 {
dh-group "20"
encryption "aes256gcm128"
hash "sha512"
}
proposal 2 {
hash "sha384"
}
}Generated swanctl.conf — 2026.06.20-0050-rolling (works):
proposals = aes256gcm128-sha512-prfsha512-ecp384,aes128-sha384-modp1024
Generated swanctl.conf — 2026.08.14 and 2026.08.27 (fails):
proposals = aes256gcm128-sha512-prfsha512-ecp384-noesn,
aes256gcm128-sha512-prfsha512-ecp384,
aes128-sha384-modp1024-noesn,
aes128-sha384-modp1024Four proposals instead of two. The IKE_SA_INIT grows from 340 to 436 bytes.
Evidence from the responder (Cisco FTD)
Capture on the FTD outside interface. The initiator sits behind NAT; 203.0.113.10 is its
public address after translation, 198.51.100.20 is the FTD.
1: 09:51:28.362942 203.0.113.10.11969 > 198.51.100.20.500: udp 436 <- 436 B, no reply 2: 09:51:32.363689 203.0.113.10.11969 > 198.51.100.20.500: udp 436 <- retransmit 3: 09:51:39.564683 203.0.113.10.11969 > 198.51.100.20.500: udp 436 <- retransmit 4: 09:51:52.525424 203.0.113.10.11969 > 198.51.100.20.500: udp 436 <- retransmit 5: 09:53:08.498066 203.0.113.10.11969 > 198.51.100.20.500: udp 340 <- 340 B (June image) 6: 09:53:08.504673 198.51.100.20.500 > 203.0.113.10.11969: udp 591 <- REPLY within 6 ms
Same source IP, same NAT-translated source port, same IKEv2 policy on the FTD. The only thing
that changes is the size — therefore the content — of the proposal payload.
The FTD's IKEv2 call admission control is not involved:
Incoming Requests: 1996 Incoming Requests Accepted: 1996 Incoming Requests Rejected: 0 SA_INIT queue drops: 0 In-Negotiation SAs: 0 (max 300)
FTD IKEv2 policy:
crypto ikev2 policy 2 encryption aes-gcm-256 aes-gcm-192 aes-gcm integrity null group 21 20 19 16 15 14 prf sha512 sha384 sha256 lifetime seconds 86400
Offending code
/usr/share/vyos/templates/ipsec/swanctl/peer.j2 — the same filter serves both:
jinja
{# line 20 — IKE proposals #}
proposals = {{ ike | get_esp_ike_cipher | join(',') }}
{# lines 78 and 137 — ESP proposals #}
esp_proposals = {{ tunnel_esp | get_esp_ike_cipher(ike) | join(',') }}/usr/lib/python3/dist-packages/vyos/template.py, line 486:
def get_esp_ike_cipher(group_config, ike_group=None): ... # For 'optional' and 'disabled' we need two values as # proposal without '-esn'/'-noesn' is incompatible with # proposals with any of them. if 'esn' in proposal: if proposal['esn'] == 'required': tmp += '-esn' elif proposal['esn'] == 'optional': ciphers.append(tmp + '-esn-noesn') elif proposal['esn'] == 'disabled': ciphers.append(tmp + '-noesn') ciphers.append(tmp)
The reasoning in that comment is sound for ESP. Nothing in the function distinguishes an IKE
call from an ESP call: the ike_group argument is only used to resolve PFS.
Suggested fix: emit the -esn / -noesn suffixes only when the function is called for ESP
proposals — for example with an explicit parameter
get_esp_ike_cipher(group_config, ike_group=None, esn=True), passing esn=False at line 20 of
the template.
Note: -noesn inside proposals appears as expected output in
/usr/libexec/vyos/tests/smoke/cli/test_vpn_ipsec.py (lines 255, 409, 485, 768, 1191 and
others). The behaviour is therefore intentional as far as the test suite is concerned, and those
assertions will need updating alongside the fix.
Validated workaround
Strip the -noesn variants from the proposals line:
sudo cp -a /etc/swanctl/swanctl.conf /etc/swanctl/swanctl.conf.orig sudo sed -i 's|^\( *proposals = \).*|\1aes256gcm128-sha512-prfsha512-ecp384,aes128-sha384-modp1024|' \ /etc/swanctl/swanctl.conf sudo swanctl --load-all
Immediate result on the affected node, running 2026.08.27-0133-rolling:
Hosted: #1, ESTABLISHED, IKEv2 local 'vpn-gw.example.net' @ 172.16.0.10[4500] remote '198.51.100.20' @ 198.51.100.20[4500] Hosted-tunnel-0: INSTALLED, TUNNEL-in-UDP, ESP:AES_GCM_16-256
The tunnel had failed to establish for weeks on this node; it came up within seconds of the
change, with no other modification.
Note that this workaround is volatile: swanctl.conf is regenerated on every commit and
on every reboot.
Environment
| Working VyOS build | 2026.06.20-0050-rolling |
| Failing VyOS builds | 2026.08.14-0025-rolling, 2026.08.27-0133-rolling |
| strongSwan | 6.0.6-1+vyos0 — identical across working and failing builds |
| Responder | Cisco FTD, IKEv2, PSK, AES-GCM-256, DH group 20 |
| Topology | Initiator behind a third-party NAT gateway, NAT-T detected |
strongSwan being byte-identical between the working and failing builds isolates the regression
to VyOS configuration generation rather than to the IKE daemon itself.
Both nodes of an HA pair were used: one kept on the June build (tunnel establishes reliably),
the other moved across 2026.07.29, 2026.08.14 and 2026.08.27 (never establishes), then
rolled back to the June build — at which point it established within 15 seconds, natively, with
no manual edit to swanctl.conf.