Guides, Nov 11, 2026

# IKEv2 NO\_PROPOSAL\_CHOSEN in strongSwan: causes and fixes

NO\_PROPOSAL\_CHOSEN means the two sides found no set of algorithms they both accept, or the server found no connection that matches the client's address. When it fails, strongSwan logs both lists: compare the \`received proposals\` line with the \`configured proposals\` line and add the missing algorithm on one side. The most common case is the built-in Windows client, which by default offers only the weak MODP\_1024 group, and strongSwan's default proposal has left that group out since version 5.6.1.

Tahir Mehmood, 11 min read

The log lines in this post come from the strongSwan source, both the 5.9.14 release that many LTS distributions still ship and the 6.x branch (6.1.0 is current). Where the two print different text, I show both. The config is swanctl.conf, with the legacy ipsec.conf syntax where it matters.

## What does NO\_PROPOSAL\_CHOSEN mean in IKEv2?

It is notify message type 14 in [RFC 7296](https://www.rfc-editor.org/rfc/rfc7296): "None of the proposed crypto suites was acceptable." An IKEv2 connection negotiates two sets of algorithms, and either one can fail:

| Stage | What is negotiated | strongSwan setting | Typical log on the server |
| --- | --- | --- | --- |
| IKE\_SA\_INIT, first packet | IKE SA: encryption, integrity, PRF, DH group | `proposals` (ipsec.conf: `ike=`) | `received proposals unacceptable` |
| IKE\_AUTH | First CHILD\_SA (ESP): encryption, integrity | `esp_proposals` (ipsec.conf: `esp=`) | `no acceptable proposal found` |
| CREATE\_CHILD\_SA, at rekey | ESP again, plus the PFS group if any | `esp_proposals` | `no acceptable proposal found` |

strongSwan also sends NO\_PROPOSAL\_CHOSEN in one case that has nothing to do with algorithms. If no connection in the config matches the local and remote address of the incoming packet, it logs this and gives up:

```
no IKE config found for 176.61.144.10...176.61.144.77, sending NO_PROPOSAL_CHOSEN
```

That one means `local_addrs` or `remote_addrs` (ipsec.conf: `left` and `right`) does not match. A server with a public IP on a NATed cloud interface, or a connection limited to one address, is the usual reason. For road-warrior servers, leave both at their default, `%any`.

## How do I find the mismatch in the strongSwan log?

Watch the log while the client connects:

```
swanctl --log                       # live log, levels 0 and 1 only
journalctl -u strongswan -f         # swanctl setups (charon-systemd)
journalctl -u strongswan-starter -f # legacy ipsec.conf setups
```

When no proposal matches, strongSwan prints what the peer offered and what it is configured to accept, at the default log level. A Windows client hitting a server with `proposals = default` looks roughly like this (shortened):

```
received proposals: IKE:AES_CBC_256/HMAC_SHA2_256_128/PRF_HMAC_SHA2_256/MODP_1024, IKE:AES_CBC_256/HMAC_SHA1_96/PRF_HMAC_SHA1/MODP_1024, ...
configured proposals: IKE:AES_CBC_128/AES_CBC_192/AES_CBC_256/.../ECP_256/ECP_384/.../MODP_3072/MODP_4096/.../MODP_2048, IKE:AES_GCM_16_128/...
received proposals unacceptable
```

Read it one transform type at a time. Encryption overlaps (AES\_CBC\_256 is on both lines), integrity overlaps (SHA2\_256), and the PRF overlaps. The DH group does not: the client offers only MODP\_1024, which is not in the configured list. One missing type is enough to fail the whole proposal.

The ESP version looks the same, with `ESP:` in front, and ends differently. Here a server limited to `esp_proposals = aes256gcm16` meets a client that only offers AES-CBC:

```
received proposals: ESP:AES_CBC_256/HMAC_SHA1_96/NO_EXT_SEQ
configured proposals: ESP:AES_GCM_16_256/NO_EXT_SEQ
no acceptable proposal found
failed to establish CHILD_SA, keeping IKE_SA
```

On a strongSwan client, the same failure shows up as `received NO_PROPOSAL_CHOSEN notify error` (IKE stage) or `received NO_PROPOSAL_CHOSEN notify, no CHILD_SA built` (ESP stage). When negotiation works, the server logs `selected proposal: IKE:...` at the default level, which is handy for seeing what a client really uses.

`swanctl --log` only shows levels 0 and 1. For more detail, such as the config and identity matching, raise the `cfg` and `ike` subsystems in `strongswan.conf` and restart:

```
charon-systemd {
  journal {
    default = 1
    cfg = 2
    ike = 2
  }
}
```

### INVALID\_KE\_PAYLOAD is not the same problem

You may see this pair of lines and think it is a failure:

```
key exchange method ECP_256 unacceptable, requesting MODP_2048      # server, 6.x
DH group ECP_256 unacceptable, requesting MODP_2048                 # server, 5.9
peer didn't accept key exchange method ECP_256, it requested MODP_2048  # client, 6.x
```

It is normal. The client guessed a DH group and sent a key for it in the first packet. The server accepts the proposal but prefers another group, so it answers INVALID\_KE\_PAYLOAD (notify 17) with the group it wants, and the client retries. It costs one extra round trip. It is only a problem if it repeats, which means the client has no group the server allows.

## How do I write strongSwan proposals?

In swanctl.conf, an IKE proposal is algorithm keywords joined by dashes: encryption, integrity, an optional PRF, and a DH group. ESP proposals are the same without the PRF, and the DH group is optional. Proposals are separated by commas, and the keyword `default` stands for strongSwan's own default set:

```
connections {
  rw {
    # IKE: AEAD needs an explicit PRF; plain AES-CBC derives it from integrity
    proposals = aes256gcm16-prfsha256-ecp256, aes256-sha256-modp2048, default
    children {
      rw {
        esp_proposals = aes256gcm16, aes256-sha256, default
      }
    }
  }
}
```

Two traps in the syntax:

-   An AEAD IKE proposal such as `aes256gcm16-ecp256` has no integrity algorithm to derive the PRF from. strongSwan refuses to load it with `a PRF algorithm is mandatory in IKE proposals`. Add `prfsha256` or similar.
-   In IKEv2, one proposal can list several algorithms of the same kind, such as `aes256-aes128-sha256-sha1-modp2048`, and the peer picks one of each. In IKEv1 that does not work and the extra algorithms are dropped.

In legacy ipsec.conf, the syntax is the same (`ike=aes256-sha256-modp2048`), with one difference: the daemon adds its default proposal after yours unless you end the line with `!`. A config with `ike=...!` accepts exactly what is written and nothing else, which is a common reason an old config rejects a new client. Also note that strongSwan 6.0 no longer enables the legacy stroke plugin by default, so ipsec.conf only works if your build still includes it.

## Why does the tunnel drop after an hour with NO\_PROPOSAL\_CHOSEN?

Because the first CHILD\_SA never used your PFS group. The keys of the CHILD\_SA created inside IKE\_AUTH come from the IKE SA, so a DH group in `esp_proposals` is not negotiated at setup. It is used for the first time when the CHILD\_SA is rekeyed, and swanctl's default `rekey_time` for a CHILD\_SA is one hour. The swanctl.conf documentation says it directly: a proposal mismatch "might, therefore, not immediately be noticed when the SA is established, but may later cause rekeying to fail."

So if users connect fine and drop after about an hour, look at the rekey in the log, not the connect. Either make both sides use the same PFS group, or make PFS optional by adding `none` to the proposal (`aes256gcm16-ecp256-none`). Since strongSwan 6.0.2, the default ESP proposal already includes all supported key exchange methods plus `none`, so PFS is optional with `default`.

## Which proposals do Windows, Apple and Android clients send?

| Client | What it offers by default | What to change |
| --- | --- | --- |
| Windows 10 and 11, built-in | IKE: 3DES and AES-CBC with SHA1 or SHA2, DH group MODP\_1024 only; ESP: AES, 3DES, even DES and NULL, with SHA1 | Tell Windows to use a stronger group (below) |
| iOS and macOS with a .mobileconfig profile | Exactly one IKE and one ESP proposal, from the profile; the profile default is 3DES | Set `EncryptionAlgorithm`, `IntegrityAlgorithm` and `DiffieHellmanGroup` to match the server |
| strongSwan app for Android | AES with SHA2 or SHA1, AES-GCM, and since 1.7.0 ChaCha20-Poly1305 and Curve25519 | Usually nothing; the profile can set custom IKE and ESP proposals since 1.9.5 |

The Windows and Apple rows come from strongSwan's [Windows clients](https://docs.strongswan.org/docs/latest/interop/windowsClients.html) and Apple profile pages. On Apple devices, the profile's `IKESecurityAssociationParameters` and `ChildSecurityAssociationParameters` decide everything, because, as the strongSwan docs say, only one proposal is sent "even if nothing is configured here", so it must match the server.

For Windows, you have two fixes on the client and one on the server. The simplest client fix is a registry value. With `2` (enforce), Windows 11 proposes `aes256-sha1-sha256-sha384-modp2048`, which strongSwan's default accepts:

```
reg add HKLM\SYSTEM\CurrentControlSet\Services\Rasman\Parameters /v NegotiateDH2048_AES256 /t REG_DWORD /d 2 /f
```

The better client fix is to set the algorithms per connection in PowerShell. This combination is inside strongSwan's default set on both stages:

```
Set-VpnConnectionIPsecConfiguration -ConnectionName "vpn.yourdomain.com" `
  -AuthenticationTransformConstants GCMAES256 -CipherTransformConstants GCMAES256 `
  -EncryptionMethod AES256 -IntegrityCheckMethod SHA256 `
  -DHGroup ECP256 -PfsGroup None -Force
```

If you can't touch the clients, the server-side fix is to accept the weak group as well, after the default so that every other client still gets the stronger choice:

```
proposals = default, aes256-sha256-modp1024
```

That works, but MODP\_1024 was removed from the default for a reason. I treat it as a temporary bridge while the clients get the registry or PowerShell change.

## What about AUTHENTICATION\_FAILED and identity problems?

AUTHENTICATION\_FAILED (notify 24) comes one step later, in IKE\_AUTH, after the algorithms matched. The client logs `received AUTHENTICATION_FAILED notify error`, and the reason is usually in the server's log. These are the lines to search for:

| Log line | Meaning |
| --- | --- |
| `no matching peer config found` | No connection accepts this client's identity (`remote.id`, or the EAP setup) |
| `EAP method EAP_MSCHAPV2 failed for peer alice` | Wrong EAP username or password |
| `tried 1 shared key for 'vpn.yourdomain.com' - 'alice', but MAC mismatched` | Pre-shared key differs |
| `no trusted RSA public key found for 'vpn.yourdomain.com'` | Logged by the side that checks a certificate (here, a client checking the server): no trusted certificate contains that identity, or its CA is not trusted |
| `constraint check failed: ...` | The client's identity or auth method does not meet the peer config's rules; the rest of the line says which |

The line before them, `looking for peer configs matching 176.61.144.10[%any]...176.61.144.77[alice]`, shows the identities strongSwan is matching, which usually answers the question.

The identity rules are strict. The server's `local.id` must be contained in its certificate, as subject or subjectAltName, and clients check that the name they dialled is there too. The strongSwan Android app documentation says the server hostname or IP "has to be contained as a `subjectAltName` extension in the VPN server's certificate". A certificate with the name only in the CN is a classic reason for `no trusted RSA public key found` on the client.

Watch out after an upgrade to strongSwan 6.0: "The default identity is now the subject DN instead of the IP address if a certificate is available." A server that never set `local.id` used to send its IP address as its identity, and now sends its certificate's subject DN, which clients that expect the IP or the hostname can reject. Set it explicitly:

```
local {
  auth = pubkey
  certs = vpn.yourdomain.com.crt
  id = vpn.yourdomain.com
}
```

An expired server certificate also ends in an authentication failure. Read its dates with `openssl x509 -noout -enddate`, as in the [OpenVPN certificate post](https://tunnelhq.com/blog/openvpn-certificate-has-expired/); the commands are the same.

## TS\_UNACCEPTABLE, INTERNAL\_ADDRESS\_FAILURE and DNS

These come after authentication, while the CHILD\_SA is set up. The client logs `received TS_UNACCEPTABLE notify, no CHILD_SA built`. The server logs `traffic selectors ... === ... unacceptable` with the two selector lists. It means the subnets the client asked for and the ones the server allows don't overlap at all. strongSwan narrows selectors when they overlap, so this is rarer than it sounds. Look for a `remote_ts` that doesn't include the client's virtual IP, or a client that asks only for IPv6 when the server offers IPv4.

INTERNAL\_ADDRESS\_FAILURE (notify 36) means the client asked for a virtual IP and the server had none to give. The server logs `no virtual IP found, sending INTERNAL_ADDRESS_FAILURE`. Either the connection has no `pools`, or the pool is full.

When the tunnel works but names don't resolve, check the pool. The DNS servers clients get come from the pool's `dns` attribute, and the address must be reachable through the tunnel and willing to answer queries from the pool's subnet:

```
pools {
  rw_pool {
    addrs = 10.10.10.0/24
    dns = 10.10.10.1
  }
}
```

A resolver that only listens on 127.0.0.1, or only allows the server's LAN, gives clients a DNS server that never answers.

## A known-good road-warrior config

This is a minimal swanctl.conf for IKEv2 with EAP-MSCHAPv2, the method the built-in Windows, Apple and Android clients all support. The server certificate goes in `/etc/swanctl/x509/`, its key in `/etc/swanctl/private/`:

```
connections {
  rw {
    version = 2
    pools = rw_pool
    proposals = default
    send_cert = always
    dpd_delay = 30s
    local {
      auth = pubkey
      certs = vpn.yourdomain.com.crt
      id = vpn.yourdomain.com
    }
    remote {
      auth = eap-mschapv2
      eap_id = %any
    }
    children {
      rw {
        local_ts = 0.0.0.0/0
        esp_proposals = default
      }
    }
  }
}

pools {
  rw_pool {
    addrs = 10.10.10.0/24
    dns = 10.10.10.1
  }
}

secrets {
  eap-alice {
    id = alice
    secret = "Xq7-change-this-password"
  }
}
```

Load it with `swanctl --load-all`, then check what is loaded with `swanctl --list-conns` and who is connected with `swanctl --list-sas`. If Windows clients connect to this config with no registry or PowerShell change, they fail on MODP\_1024, exactly as in the log above.

## How does dead peer detection work in strongSwan?

`dpd_delay` sets how long strongSwan waits without receiving any IKE or ESP packet before it sends an empty INFORMATIONAL message to check the peer. It is off by default (`0s`) in swanctl.conf. In legacy ipsec.conf, `dpddelay` defaults to 30 seconds, but DPD only runs when `dpdaction` is set to something other than its default, `none`. In IKEv2, `dpd_timeout` does nothing: a DPD message is retransmitted like any other IKE message, and with the default [retransmission settings](https://docs.strongswan.org/docs/latest/config/retransmission.html) (5 tries, 4 second base timeout, factor 1.8) strongSwan gives up after about 165 seconds.

What happens then is the child's `dpd_action`: `clear` (the default) closes the CHILD\_SA, `trap` installs a trap policy so traffic re-creates the tunnel on demand, and `restart` re-negotiates at once. On a road-warrior server, keep `clear`, since the client is gone. On the initiating side of a site-to-site tunnel, `restart` is usually what you want.

DPD tells the server that an existing peer went away. It tells you nothing about whether a new client can connect today. A proposal change, an expired certificate or a full pool only shows up when someone tries a fresh IKE\_SA\_INIT and IKE\_AUTH. The [VPN server monitoring guide](https://tunnelhq.com/blog/vpn-server-monitoring-guide/) explains why that has to come from outside the server.

Full disclosure: that outside check is what TunnelHQ's [IKEv2 monitor](https://tunnelhq.com/ikev2-monitoring/) does. Every 1 to 5 minutes, depending on plan, it runs IKE\_SA\_INIT and IKE\_AUTH against your server with EAP-MSCHAPv2 by default, like the config above, and alerts when the server stops accepting it. Field details are in the [docs](https://docs.tunnelhq.com/protocols/ikev2/), and every plan has a 14-day free trial.

Source: https://tunnelhq.com/blog/strongswan-ikev2-no-proposal-chosen/
