Idle tcp connections lifetime [set so_keepalive=250s in nginx config]

While operating my private IMAP server, I’ve noticed some cellular operators aggressively delete IPv4 NAT translations after a brief period of inactivity, sometimes after less than five minutes. Combined with android doze mode, which allows apps to wake up only every 15-20 minutes, this causes the app to have a broken connection and reconnect every time it wakes up. This might cause some increased battery use, and problems with new message notifications.

I’ve observed this behavior with a FairEmail client, but it would similarly affect other software. For example, on a Xiaomi phone it wakes up every 20 minutes, and each time the IMAP server connection reports a network error in FairEmail’s network activity log.

In the chatmail installation templates for nginx and dovecot I didn’t find any relevant settings to mitigate this. In my setup I use nginx’s stream module to terminate SSL and pass through client sessions. The relevant nginx config is so_keepalive=250s:10s:5 on a listen directive for IPv4 to enable TCP level keepalives every 250 seconds. The stream configuration is like this:

server {
  listen          993 so_keepalive=250s:10s:5 ssl default_server;
  server_name     example.com;
  ssl_certificate /some/cert.pem;
  ssl_certificate_key /some/cert.key.pem;
  proxy_timeout   2h5m;
  proxy_protocol  on;
  proxy_pass      unix:/run/dovecot-imap;
}

Edit: all of the above is relevant when imap_idle_notify_interval is increased from the default of two minutes. With the default value, the server pings the client at the IMAP protocol level and achieves the same effect.

1 Like

Android actively kills the connections when the app is put into background and does not have a permission to ignore battery optimizations, you get ECONNABORTED immediately when the app is suspended and cannot do anything about it. See https://stackoverflow.com/questions/32316491/network-access-in-doze-mode
You need to allow the app to ignore battery optimizations to even have the connection remaining open and not have the socket killed by Android. Delta Chat requests this permission, it is better to grant it if you want it to even have a chance to have the connection, otherwise you can see from the logs that as soon as you switch to the other app, you get ECONNABORTED.

Dovecot has its own application-level keepalives as well, it looks like this:

? IDLE
+ idling
* OK Still here
* OK Still here
* OK Still here
* OK Still here
DONE

“OK Still here” is sent by Dovecot, but rarely. TCP keepalives are probably better, also because they are cheaper.

From the client side we currently don’t configure TCP keepalives. We may do this with TcpKeepalive in socket2 - Rust, not sure if we should and what is the good value, currently we don’t do anything special.

I am sorry, I forgot to mention that I’ve increased imap_idle_notify_interval from its default of 2 minutes. Having it set to such a low value sort of defeats the purpose of IDLE command.

I think the purpose of IDLE command is to get instant notifications about mailbox changes, so application-level keepalives don’t defeat its purpose.

Anyway, is it correct that your proposal is to disable application-level keepalives and replace them with TCP-level keepalives so they are handled by the kernel without waking up userspace?