android: a tunnel the server dropped silently is noticed in about two minutes instead of never: the NAT keepalive tick sends a DPD once IPsec traffic went out and nothing at all came back for 90 s (charon.keep_alive_dpd_inbound; a session stayed «connected» for 18 min after the server went quiet); no timer of its own, no probe on an idle tunnel, without a path, or counted from before a suspend, and while answered probes are all that comes back the wait doubles up to 6 min; the tick also runs with forced UDP encapsulation without a NAT; diag.log gets the ESP in/out byte counters

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Fable 5.2 committed 2026-11-11 09:18:58 +07:00
1 parent 352be74525
commit 4eae01fcfe
4 files changed
+141 -4

No files matched your search

+8
View File
@@ -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 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). 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 charon.leak_detective.detailed = yes
Includes source file names and line numbers in leak detective output. Includes source file names and line numbers in leak detective output.
@@ -1458,6 +1458,19 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
+ ", ranges=" + sAppliedGeoExclusionsV4.size() + "v4/" + sAppliedGeoExclusionsV6.size() + "v6" + ", ranges=" + sAppliedGeoExclusionsV4.size() + "v4/" + sAppliedGeoExclusionsV6.size() + "v6"
+ ", forceTunnel=" + (sPrivateDnsServers.size() + sTunnelDnsServers.size() + sUserForcePrefixCount) + ", forceTunnel=" + (sPrivateDnsServers.size() + sTunnelDnsServers.size() + sUserForcePrefixCount)
: "none")); : "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; long[] s = null;
try try
@@ -62,6 +62,7 @@
#define ANDROID_RETRANSMIT_BASE 1.4 #define ANDROID_RETRANSMIT_BASE 1.4
#define ANDROID_KEEPALIVE_INTERVAL 45 #define ANDROID_KEEPALIVE_INTERVAL 45
#define ANDROID_KEEPALIVE_DPD_MARGIN 20 #define ANDROID_KEEPALIVE_DPD_MARGIN 20
#define ANDROID_KEEPALIVE_DPD_INBOUND 90
static volatile bool charon_running = FALSE; static volatile bool charon_running = FALSE;
@@ -634,6 +635,14 @@ static void initiate(settings_t *settings)
lib->settings->set_int(lib->settings, lib->settings->set_int(lib->settings,
"charon.keep_alive_dpd_margin", "charon.keep_alive_dpd_margin",
ANDROID_KEEPALIVE_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 */ /* reload plugins after changing settings */
lib->plugins->reload(lib->plugins, NULL); lib->plugins->reload(lib->plugins, NULL);
+111 -4
View File
@@ -253,6 +253,32 @@ struct private_ike_sa_t {
*/ */
uint32_t keepalive_dpd_margin; 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 * 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, &current, NULL, NULL);
use_time = max(use_time, current);
}
enumerator->destroy(enumerator);
return use_time;
}
METHOD(ike_sa_t, send_keepalive, void, METHOD(ike_sa_t, send_keepalive, void,
private_ike_sa_t *this, bool scheduled) private_ike_sa_t *this, bool scheduled)
{ {
time_t last_out, now, diff; time_t last_out, now, diff;
bool nat_here;
if (scheduled) if (scheduled)
{ {
@@ -700,8 +752,13 @@ METHOD(ike_sa_t, send_keepalive, void,
{ /* keepalives disabled either by configuration or for passive IKE_SAs */ { /* keepalives disabled either by configuration or for passive IKE_SAs */
return; return;
} }
if (!(this->conditions & COND_NAT_HERE) || (this->conditions & COND_STALE)) nat_here = this->conditions & COND_NAT_HERE;
{ /* disable keepalives if we are not NATed anymore, or the SA is stale */ 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; return;
} }
@@ -710,7 +767,17 @@ METHOD(ike_sa_t, send_keepalive, void,
diff = now - last_out; 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)) diff > (this->keepalive_interval + this->keepalive_dpd_margin))
{ {
if (!this->task_manager->busy(this->task_manager)) if (!this->task_manager->busy(this->task_manager))
@@ -722,7 +789,7 @@ METHOD(ike_sa_t, send_keepalive, void,
} }
diff = 0; diff = 0;
} }
else if (diff >= this->keepalive_interval) else if (nat_here && diff >= this->keepalive_interval)
{ {
packet_t *packet; packet_t *packet;
chunk_t data; chunk_t data;
@@ -740,6 +807,42 @@ METHOD(ike_sa_t, send_keepalive, void,
this->stats[STAT_OUTBOUND] = now; this->stats[STAT_OUTBOUND] = now;
diff = 0; 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) if (!this->keepalive_job)
{ {
this->keepalive_job = send_keepalive_job_create(this->ike_sa_id); 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: case COND_NAT_THERE:
DBG1(DBG_IKE, "remote host is behind NAT"); DBG1(DBG_IKE, "remote host is behind NAT");
this->conditions |= COND_NAT_ANY; this->conditions |= COND_NAT_ANY;
send_keepalive(this, FALSE);
break; break;
case COND_NAT_FAKE: case COND_NAT_FAKE:
DBG1(DBG_IKE, "faking NAT situation to enforce UDP encapsulation"); DBG1(DBG_IKE, "faking NAT situation to enforce UDP encapsulation");
this->conditions |= COND_NAT_ANY; this->conditions |= COND_NAT_ANY;
send_keepalive(this, FALSE);
break; break;
default: default:
break; 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), "%s.keep_alive", KEEPALIVE_INTERVAL, lib->ns),
.keepalive_dpd_margin = lib->settings->get_time(lib->settings, .keepalive_dpd_margin = lib->settings->get_time(lib->settings,
"%s.keep_alive_dpd_margin", 0, lib->ns), "%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, .retry_initiate_interval = lib->settings->get_time(lib->settings,
"%s.retry_initiate_interval", 0, lib->ns), "%s.retry_initiate_interval", 0, lib->ns),
.flush_auth_cfg = lib->settings->get_bool(lib->settings, .flush_auth_cfg = lib->settings->get_bool(lib->settings,