From 4eae01fcfe8084b6f7c7b5c994719ab881058824 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 11 Nov 2026 09:18:58 +0700 Subject: [PATCH] =?UTF-8?q?android:=20a=20tunnel=20the=20server=20dropped?= =?UTF-8?q?=20silently=20is=20noticed=20in=20about=20two=20minutes=20inste?= =?UTF-8?q?ad=20of=20never:=20the=20NAT=20keepalive=20tick=20sends=20a=20D?= =?UTF-8?q?PD=20once=20IPsec=20traffic=20went=20out=20and=20nothing=20at?= =?UTF-8?q?=20all=20came=20back=20for=2090=20s=20(charon.keep=5Falive=5Fdp?= =?UTF-8?q?d=5Finbound;=20a=20session=20stayed=20=C2=ABconnected=C2=BB=20f?= =?UTF-8?q?or=2018=20min=20after=20the=20server=20went=20quiet);=20no=20ti?= =?UTF-8?q?mer=20of=20its=20own,=20no=20probe=20on=20an=20idle=20tunnel,?= =?UTF-8?q?=20without=20a=20path,=20or=20counted=20from=20before=20a=20sus?= =?UTF-8?q?pend,=20and=20while=20answered=20probes=20are=20all=20that=20co?= =?UTF-8?q?mes=20back=20the=20wait=20doubles=20up=20to=206=20min;=20the=20?= =?UTF-8?q?tick=20also=20runs=20with=20forced=20UDP=20encapsulation=20with?= =?UTF-8?q?out=20a=20NAT;=20diag.log=20gets=20the=20ESP=20in/out=20byte=20?= =?UTF-8?q?counters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.2 --- conf/options/charon.opt | 8 ++ .../android/logic/CharonVpnService.java | 13 ++ .../main/jni/libandroidbridge/charonservice.c | 9 ++ src/libcharon/sa/ike_sa.c | 115 +++++++++++++++++- 4 files changed, 141 insertions(+), 4 deletions(-) diff --git a/conf/options/charon.opt b/conf/options/charon.opt index 2f95552f7..a8b8e2bf2 100644 --- a/conf/options/charon.opt +++ b/conf/options/charon.opt @@ -259,6 +259,14 @@ charon.keep_alive_dpd_margin = 0s sent instead of a NAT keep alive (0 to disable). This is only useful if a clock is used that includes time spent suspended (e.g. CLOCK_BOOTTIME). +charon.keep_alive_dpd_inbound = 0s + Number of seconds without any inbound packet, while IPsec traffic was sent, + after which a DPD is sent from the NAT keep alive tick (0 to disable). It + detects a peer that dropped the SA silently without a separate DPD timer; + the first tick after a suspend or a lost path is skipped. While answered + probes are all that comes back (traffic nobody replies to), the time is + doubled after each probe, up to four times the configured value. + charon.leak_detective.detailed = yes Includes source file names and line numbers in leak detective output. diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java index 061c9e0b5..1bf4a1da0 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java @@ -1458,6 +1458,19 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe + ", ranges=" + sAppliedGeoExclusionsV4.size() + "v4/" + sAppliedGeoExclusionsV6.size() + "v6" + ", forceTunnel=" + (sPrivateDnsServers.size() + sTunnelDnsServers.size() + sUserForcePrefixCount) : "none")); + /* inbound bytes that stop growing while outbound keeps going = a dead tunnel + * (the counters cover the current CHILD_SAs, so they restart at each rekey) */ + try + { + long[] esp = queryTraffic(); + if (esp != null && esp.length >= 2) + { + DiagLog.i(TAG, "route diag: esp in=" + esp[0] + " B, out=" + esp[1] + " B"); + } + } + catch (Throwable e) + { /* native library not loaded: nothing to report */ + } long[] s = null; try diff --git a/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c b/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c index 2672acd6f..0422a689c 100644 --- a/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c +++ b/src/frontends/android/app/src/main/jni/libandroidbridge/charonservice.c @@ -62,6 +62,7 @@ #define ANDROID_RETRANSMIT_BASE 1.4 #define ANDROID_KEEPALIVE_INTERVAL 45 #define ANDROID_KEEPALIVE_DPD_MARGIN 20 +#define ANDROID_KEEPALIVE_DPD_INBOUND 90 static volatile bool charon_running = FALSE; @@ -634,6 +635,14 @@ static void initiate(settings_t *settings) lib->settings->set_int(lib->settings, "charon.keep_alive_dpd_margin", ANDROID_KEEPALIVE_DPD_MARGIN); + /* a tunnel the server dropped silently (restart, NAT rebinding at the + * carrier) stayed "connected" for as long as nobody looked: probe the peer + * from the keepalive tick once traffic went out and nothing came back for + * this long. No timer of its own, no probe on an idle tunnel, without a + * network or on the first tick after the device slept */ + lib->settings->set_int(lib->settings, + "charon.keep_alive_dpd_inbound", + ANDROID_KEEPALIVE_DPD_INBOUND); /* reload plugins after changing settings */ lib->plugins->reload(lib->plugins, NULL); diff --git a/src/libcharon/sa/ike_sa.c b/src/libcharon/sa/ike_sa.c index 28ef341a9..9516911e3 100644 --- a/src/libcharon/sa/ike_sa.c +++ b/src/libcharon/sa/ike_sa.c @@ -253,6 +253,32 @@ struct private_ike_sa_t { */ uint32_t keepalive_dpd_margin; + /** + * Seconds without any inbound packet, while IPsec traffic was sent, after + * which a DPD is sent from the NAT keep alive tick (0 to disable) + */ + uint32_t keepalive_dpd_inbound; + + /** + * Time of the previous NAT keep alive tick, 0 before the first one + */ + time_t keepalive_tick; + + /** + * Earliest time from which missing inbound packets count as silence + */ + time_t keepalive_silence; + + /** + * Time of the last DPD sent for missing inbound packets, 0 if none + */ + time_t keepalive_probe; + + /** + * How often the silence threshold was doubled after such a DPD + */ + u_int keepalive_backoff; + /** * The scheduled keep alive job, if any */ @@ -687,10 +713,36 @@ static void set_dscp(private_ike_sa_t *this, packet_t *packet) } } +/** + * The threshold of keep_alive_dpd_inbound is doubled at most this often + */ +#define KEEPALIVE_DPD_MAX_BACKOFF 2 + +/** + * Time IPsec traffic was last sent or received on any CHILD_SA (unlike the use + * time of the IKE_SA this includes neither IKE messages nor NAT keep alives) + */ +static time_t get_ipsec_time(private_ike_sa_t *this, bool inbound) +{ + enumerator_t *enumerator; + child_sa_t *child_sa; + time_t use_time = 0, current; + + enumerator = array_create_enumerator(this->child_sas); + while (enumerator->enumerate(enumerator, &child_sa)) + { + child_sa->get_usestats(child_sa, inbound, ¤t, NULL, NULL); + use_time = max(use_time, current); + } + enumerator->destroy(enumerator); + return use_time; +} + METHOD(ike_sa_t, send_keepalive, void, private_ike_sa_t *this, bool scheduled) { time_t last_out, now, diff; + bool nat_here; if (scheduled) { @@ -700,8 +752,13 @@ METHOD(ike_sa_t, send_keepalive, void, { /* keepalives disabled either by configuration or for passive IKE_SAs */ return; } - if (!(this->conditions & COND_NAT_HERE) || (this->conditions & COND_STALE)) - { /* disable keepalives if we are not NATed anymore, or the SA is stale */ + nat_here = this->conditions & COND_NAT_HERE; + if ((this->conditions & COND_STALE) || + !(nat_here || (this->keepalive_dpd_inbound && + (this->conditions & COND_NAT_ANY)))) + { /* disable keepalives if we are not NATed anymore, or the SA is stale; + * with the inbound check the tick also runs for forced UDP + * encapsulation, it just sends no keep alives then */ return; } @@ -710,7 +767,17 @@ METHOD(ike_sa_t, send_keepalive, void, diff = now - last_out; - if (this->keepalive_dpd_margin && + if (!this->keepalive_tick || now < this->keepalive_tick || + now - this->keepalive_tick >= 2 * this->keepalive_interval) + { /* the first tick, or one that does not follow the previous one in time + * (the host was suspended, the SA was stale, the clock was set): the + * path may not be usable yet, so silence only counts from here on */ + this->keepalive_silence = now; + this->keepalive_backoff = 0; + } + this->keepalive_tick = now; + + if (nat_here && this->keepalive_dpd_margin && diff > (this->keepalive_interval + this->keepalive_dpd_margin)) { if (!this->task_manager->busy(this->task_manager)) @@ -722,7 +789,7 @@ METHOD(ike_sa_t, send_keepalive, void, } diff = 0; } - else if (diff >= this->keepalive_interval) + else if (nat_here && diff >= this->keepalive_interval) { packet_t *packet; chunk_t data; @@ -740,6 +807,42 @@ METHOD(ike_sa_t, send_keepalive, void, this->stats[STAT_OUTBOUND] = now; diff = 0; } + else if (this->keepalive_dpd_inbound) + { + time_t last_in = max(get_use_time(this, TRUE), this->keepalive_silence); + + if (get_ipsec_time(this, TRUE) > this->keepalive_probe) + { /* IPsec traffic came back since the last probe: two-way again */ + this->keepalive_backoff = 0; + } + /* IPsec traffic went out after the last packet we received and nothing + * came back since: the peer may have dropped the SA without us + * noticing (restart, changed NAT mapping). An idle tunnel sends no + * IPsec traffic and is never probed here */ + if (now - last_in >= ((time_t)this->keepalive_dpd_inbound << + this->keepalive_backoff) && + get_ipsec_time(this, FALSE) > last_in && + !this->task_manager->busy(this->task_manager)) + { + DBG1(DBG_IKE, "sending DPD, nothing received for %ds while " + "sending traffic", (int)(now - last_in)); + this->task_manager->queue_dpd(this->task_manager); + this->task_manager->initiate(this->task_manager); + /* an answered probe proves the peer alive although nothing comes + * back through the tunnel (traffic nobody replies to): wait twice + * as long for the next one, up to four times the base, until + * IPsec traffic is received again */ + this->keepalive_silence = this->keepalive_probe = now; + if (this->keepalive_backoff < KEEPALIVE_DPD_MAX_BACKOFF) + { + this->keepalive_backoff++; + } + } + } + if (diff < 0 || diff >= this->keepalive_interval) + { /* nothing was sent above (no keep alives without a NAT): full interval */ + diff = 0; + } if (!this->keepalive_job) { this->keepalive_job = send_keepalive_job_create(this->ike_sa_id); @@ -811,10 +914,12 @@ METHOD(ike_sa_t, set_condition, void, case COND_NAT_THERE: DBG1(DBG_IKE, "remote host is behind NAT"); this->conditions |= COND_NAT_ANY; + send_keepalive(this, FALSE); break; case COND_NAT_FAKE: DBG1(DBG_IKE, "faking NAT situation to enforce UDP encapsulation"); this->conditions |= COND_NAT_ANY; + send_keepalive(this, FALSE); break; default: break; @@ -3284,6 +3389,8 @@ ike_sa_t * ike_sa_create(ike_sa_id_t *ike_sa_id, bool initiator, "%s.keep_alive", KEEPALIVE_INTERVAL, lib->ns), .keepalive_dpd_margin = lib->settings->get_time(lib->settings, "%s.keep_alive_dpd_margin", 0, lib->ns), + .keepalive_dpd_inbound = lib->settings->get_time(lib->settings, + "%s.keep_alive_dpd_inbound", 0, lib->ns), .retry_initiate_interval = lib->settings->get_time(lib->settings, "%s.retry_initiate_interval", 0, lib->ns), .flush_auth_cfg = lib->settings->get_bool(lib->settings,