A disconnect record lands after a second connection under the same name has already started, and a handler that updates the newest row for that user closes the wrong session. The mistake is not the timestamp comparison itself. It is the assumption that a name plus a whole second identifies one session. Hook events describe connections, and a connection needs a key that belongs to it alone.

Start with what the disconnect variables actually mean. Only then does the choice of identifier make sense.

What the disconnect hook really carries

In Community OpenVPN 2.6, the hook reference defines time_unix as the moment the client connected, set before the connect hook runs, and it is not redefined when the disconnect hook later fires. time_duration gives the length of that session in seconds, so the end is roughly the connection time plus the duration. Both are whole seconds, and neither is the moment your database processed the event. Keeping those three times apart is what makes a correlation hold instead of merely looking right during testing.

The arithmetic deserves writing down, because it is the only end time the hook itself offers. Processing delay and clock differences between hosts still sit outside it.

Why the obvious pair is not unique

Pairing the name with the connection time narrows the search and still does not identify a session. A client can reconnect inside the same second, so two sessions can share both values. Two daemon instances, or a server replacement that keeps the same configuration, can produce that pair from different processes. Tunnel addresses get recycled for the same reason, which makes them attributes of a session rather than identifiers for one.

The failure mode is quiet. A late disconnect for the first session matches the newest row and retires an allocation the second session is still using, so the user experiences a random loss of access they did nothing to cause. A row count that looks correct afterwards makes the problem harder to spot.

Where a stable identity comes from

The management interface assigns each client a client-instance identifier, and that identifier is scoped to the daemon instance that issued it. A workable external key combines a stable server identifier, an epoch that changes when the daemon starts, and that client ID. The epoch matters because identifiers restart with the process, and a bare process ID can be reused later by something unrelated.

Environment variables set inside a child connect script do not travel to the parent daemon's later disconnect script, because those scripts run as separate processes with their own environments. Generating a UUID at connect time is therefore half a solution. The disconnect side has to be able to name the same UUID, which means the identity has to live somewhere both events can reach.

Make retirement conditional and idempotent

Retire by session owner, not by user. A handler that deletes whatever allocation currently belongs to a name will eventually delete the wrong one, and the only defence is to check that the record being removed is the record this event describes.

The management interface accepts one connected management client at a time, so integrate with whoever already owns it rather than attaching a second observer that competes for the same socket.

Handle repeated events without changing the outcome twice, and keep unpaired events for reconciliation instead of discarding them. A disconnect callback has documented lifecycle conditions and may never arrive after a crash or a hard stop, so the design needs a way to represent a session whose ending event is missing.

Test the awkward cases on purpose: two reconnects inside one second, two daemon instances, a connect whose processing failed, and a daemon restart in the middle of a session. A key that survives all four is a key worth trusting.

Winndannde jowitiiɗe heen