OpenVPN 钩子事件:为什么名称加时间戳不是键
断开连接的钩子报告的是连接时间,而不是断开时间。与用户名配对时,它可能错误地结束会话;不如拥有一个标识。
一条断开记录到达时,同名下的第二次连接已经开始,而更新该用户最新行的处理程序关闭了错误的会话。错误不在时间戳比较本身。错误在于假设名称加一个完整的秒就能标识一个会话。钩子事件描述的是连接,而连接需要一个只属于它的键。
先从断开变量实际的含义说起。只有在此基础上,选择标识符才有意义。
断开钩子究竟携带什么
在 Community OpenVPN 2.6 中,钩子参考文档 time_unix 将其定义为客户端连接的时刻;它在连接钩子运行之前设置,并且在断开钩子稍后触发时不会重新定义。time_duration 给出该会话的秒数长度,因此结束时间大致是连接时间加上持续时间。两者都是整秒,也都不是你的数据库处理该事件的时刻。正是把这三个时间区分开来,相关性才能成立,而不只是在测试时看起来正确。
这套算术值得写下来,因为它是钩子本身提供的唯一结束时间。处理延迟和主机之间的时钟差异仍然不在其中。
为什么显而易见的组合并不唯一
把名称与连接时间配对缩小了搜索范围,却仍然无法标识一个会话。客户端可以在同一秒内重新连接,因此两个会话可以共享这两个值。两个守护进程实例,或者保留相同配置的服务器替换,都可能从不同进程产生这样一对值。隧道地址也会因同样原因被回收,这使它们成为会话的属性,而不是会话的标识符。
故障方式是安静的。第一个会话的迟到断开会匹配到最新行,并结束第二次会话仍在使用的一条分配,于是用户体验到一次并非自己造成的随机访问中断。事后看起来正确的行数反而让问题更难被发现。
稳定标识从何而来
管理接口为每个客户端分配一个客户端实例标识符,而该标识符的作用域限于签发它的守护进程实例。可用的外部键会组合一个稳定的服务器标识符、一个在守护进程启动时变化的纪元,以及该客户端 ID。纪元之所以重要,是因为标识符会随进程重新开始,而裸的进程 ID 之后可能被无关的东西复用。
在子连接脚本内部设置的环境变量不会传到父守护进程后来的断开脚本,因为这些脚本作为独立进程运行,拥有各自的环境。因此,在连接时生成 UUID 只是半个方案。断开侧必须能够指称同一个 UUID,这意味着标识必须存在于两个事件都能到达的地方。
让回收成为有条件且幂等的操作
按会话所有者回收,而不是按用户。任何删除当前属于某个名称的分配的处理程序,最终都会删错,而唯一的防御是确认被删除的记录正是该事件所描述的记录。
管理接口一次只接受一个已连接的管理客户端,因此应与已经拥有它的那一方集成,而不是再附加一个争用同一套接字的观察者。
处理重复事件时不要让结果改变两次,并保留未配对的事件用于核对,而不是丢弃它们。断开回调有已记录的生命周期条件,在崩溃或强制停止之后可能永远不会到达,因此设计需要一种方式来表示其结束事件缺失的会话。
有意测试那些别扭的情况:一秒内两次重连、两个守护进程实例、一次处理失败的连接,以及会话中途守护进程重启。能经受这四种情况的键才是值得信任的键。