Page MenuHomeVyOS Platform

VyOS bug — `-noesn` variants in IKE proposals break interoperability with Cisco FTD
Closed, ResolvedPublicBUG

Description

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.conf2026.06.20-0050-rolling (works):

proposals = aes256gcm128-sha512-prfsha512-ecp384,aes128-sha384-modp1024

Generated swanctl.conf2026.08.14 and 2026.08.27 (fails):

proposals = aes256gcm128-sha512-prfsha512-ecp384-noesn,
            aes256gcm128-sha512-prfsha512-ecp384,
            aes128-sha384-modp1024-noesn,
            aes128-sha384-modp1024

Four 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 build2026.06.20-0050-rolling
Failing VyOS builds2026.08.14-0025-rolling, 2026.08.27-0133-rolling
strongSwan6.0.6-1+vyos0identical across working and failing builds
ResponderCisco FTD, IKEv2, PSK, AES-GCM-256, DH group 20
TopologyInitiator 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.

Details

Version
all versions after `2026.06.20-0050-rolling`
Is it a breaking change?
Unspecified (possibly destroys the router)
Issue type
Bug (incorrect behavior)
Forum thread
IPSec tunnel

Event Timeline

Viacheslav subscribed.

@rockfish do you want to claim the task and add a PR fix?

@Viacheslav
I used an AI assistant to draft the write-up. The bug, the diagnosis and the production validation are mine — I've been chasing this for two days on a live HA pair. Happy to answer anything about the code.

I let you tell me if this is incorrect, and if you prefer my own comments instead of this structured one.

Update, now that the fix itself has been validated rather than the workaround.

I upgraded the standby node of the pair to 2026.08.28-0255-rolling, so the
comparison runs on the affected image instead of a hand-edited swanctl.conf.
Same config.boot throughout, only the image and the patch change:

2026.06.20-0050 (works)
  proposals = aes256gcm128-sha512-prfsha512-ecp384,aes128-sha384-modp1024

2026.08.28-0255 stock
  proposals = aes256gcm128-sha512-prfsha512-ecp384-noesn,
              aes256gcm128-sha512-prfsha512-ecp384,
              aes128-sha384-modp1024-noesn,
              aes128-sha384-modp1024

2026.08.28-0255 with PR 5429
  proposals = aes256gcm128-sha512-prfsha512-ecp384,aes128-sha384-modp1024

The patched build produces a line byte-identical to the last known-good image.
I then failed VRRP over so that node became active, and the tunnel established
against the FTD and has been carrying production traffic since. The image also
still ships get_esp_ike_cipher() without the esn parameter, so the defect is
present in rolling as of that build.

On severity. The report above notes that ESN is never mentioned in config.boot,
but I did not draw the conclusion at the time. esn carries
<defaultValue>disabled</defaultValue> in
interface-definitions/include/vpn-ipsec-esn.xml.i, and the disabled branch of
get_esp_ike_cipher() is precisely the one that appends the -noesn variant. So
this is not limited to configurations that set ESN. Every IPsec connection on
an affected image gets ESN transforms in its IKE proposals, with no user action
at all, and any peer that rejects the unexpected transform fails the same way.
That widens the blast radius well beyond the FTD case I reported.

Scope of what I exercised on hardware, to be explicit: the peer.j2 path only,
site-to-site IKEv2 with PSK. The profile.j2, remote_access.j2 and l2tp.j2 call
sites that the PR also changes rest on the smoketests, not on my testing.

PR 5429 is blocked on two things I cannot do myself. The workflows on the head
commit sit at action_required, so the ISO integration test has not run against
the current code, and the invalid-task-id label is still set - Mergify commands
are restricted to the Maintainers team. Could someone unblock either of those?

Viacheslav moved this task from Backlog - Bug to Completed on the VyOS Rolling board.