Skip to content

fix: openvpn handshake must not inherit the per-dial timeout#2925

Closed
Easy-Ez wants to merge 1 commit into
MetaCubeX:Alphafrom
Easy-Ez:fix/openvpn-handshake-timeout
Closed

fix: openvpn handshake must not inherit the per-dial timeout#2925
Easy-Ez wants to merge 1 commit into
MetaCubeX:Alphafrom
Easy-Ez:fix/openvpn-handshake-timeout

Conversation

@Easy-Ez

@Easy-Ez Easy-Ez commented Jun 29, 2026

Copy link
Copy Markdown

Problem

The OpenVPN outbound establishes its control-channel tunnel lazily — on the first connection that needs it. The handshake context, however, was derived from that triggering dial's context:

handshakeCtx, handshakeCancel := context.WithTimeout(ctx, ovpn.DefaultHandshakeTimeout)

Because ctx already carries the per-dial deadline (C.DefaultTCPTimeout, 5s), the effective handshake timeout was min(5s, 30s) = 5s — the 30s DefaultHandshakeTimeout never actually applied. Any OpenVPN server whose control-channel handshake takes longer than ~5s (TLS-auth, PAM / --client-connect scripts, a large PUSH_REPLY, slow links) fails the handshake before it can finish, even with correct credentials.

Fix

  • Run the handshake under the outbound's own run context (o.runCtx) with the full DefaultHandshakeTimeout, instead of inheriting the triggering dial's short deadline. The tunnel is shared, long-lived infrastructure reused by every connection — its one-time handshake should not be bounded by whichever single dial happens to trigger it.
  • After the tunnel is established, refresh the dial deadline (afterRunContext) so the first request — which paid for the one-time handshake — is not failed for having spent its own short budget building shared infrastructure.

Notes

This is an independent, pre-existing bug, not tied to any specific feature. It was discovered while implementing custom peer-info support (#2926), but it affects any slow OpenVPN handshake. Submitted as its own PR per the one-change-per-PR guideline.

go build ./adapter/outbound/ passes.


问题

OpenVPN 出站的控制信道隧道是惰性建立的 —— 由第一条需要它的连接触发。但握手 context 继承自那条触发拨号的 context:

handshakeCtx, handshakeCancel := context.WithTimeout(ctx, ovpn.DefaultHandshakeTimeout)

由于 ctx 已经带着单次拨号的 deadline(C.DefaultTCPTimeout,5s),实际握手超时是 min(5s, 30s) = 5s —— 30s 的 DefaultHandshakeTimeout 从未真正生效。任何控制信道握手耗时超过 ~5s 的服务端(TLS-auth、PAM / --client-connect 脚本、较大的 PUSH_REPLY、慢链路)都会在握手完成前超时,即使账密完全正确。

修复

  • 握手改用 outbound 自己的 run context(o.runCtx)跑满完整的 DefaultHandshakeTimeout,不再继承触发拨号的短 deadline。隧道是被所有连接复用的、长生命周期的共享资源,它的一次性握手不应被「恰好触发它的那条拨号」的超时所限制。
  • 隧道建立后刷新拨号 deadline(afterRunContext),使「为一次性握手买单」的首个请求,不因把自己的短预算花在建设共享设施上而失败。

说明

这是一个独立的、早已存在的 bug,与任何具体功能无关。它是在实现自定义 peer-info(#2926)时发现的,但影响任何慢速 OpenVPN 握手。按「一个 PR 只做一件事」的规范,单独成 PR 提交。

go build ./adapter/outbound/ 通过。

🤖 Generated with Claude Code

An OpenVPN server that performs push-peer-info / PAM / client-connect
checks needs longer to finish the control-channel handshake than the
single connection's dial timeout (C.DefaultTCPTimeout, 5s) that happens
to trigger it. handshakeContext was derived from that dial context, so
the 30s DefaultHandshakeTimeout was effectively capped at ~5s and the
handshake (key-method-2 exchange + PUSH_REPLY) timed out before it could
finish — even with correct credentials and peer-info.

- Run the handshake under the outbound's run context with the full
  handshake timeout, since the tunnel is shared infrastructure reused by
  every connection rather than owned by the dial that triggers it.
- After the tunnel is established, refresh the dial deadline so the very
  first request (which paid for the one-time handshake) is not failed for
  exhausting its own budget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@wwqgtxx

wwqgtxx commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

We believe that servers unable to complete a handshake within 5 seconds are not worth using.

In light of this, ovpn.DefaultHandshakeTimeout is unnecessary, and we will remove this logic in the future.

@wwqgtxx wwqgtxx closed this Jun 29, 2026
@Easy-Ez

Easy-Ez commented Jun 29, 2026

Copy link
Copy Markdown
Author

Hi @wwqgtxx,先谢谢你帮忙合了 #2926 🙏 也理解你对握手超时这块的考虑。

不过我这边遇到个有点尴尬的情况,想跟你说一下,看有没有别的办法。

我连的是公司的 OpenVPN 网关,它用 push-peer-info 做设备准入(就是 --client-connect / PAM 去校验 IV_HWADDR,#2926 那个 peer-info 选项也正好是为这种场景加的)。麻烦的是,这台服务器的准入校验是在握手过程里跑的,PUSH_REPLY 稳定要 7 秒左右才回来;偏偏它是公司的服务器,我没权限去动它、让它变快,只能在客户端这边想办法。

现在 Alpha 上,握手是跟着单次拨号的超时走的(C.DefaultTCPTimeout,大概 5s),所以每次都卡在 5 秒整然后挂掉:

make OpenVPN handshake: read push reply: context deadline exceeded   # 5.0s

我在本地试着把这次握手单独给个超时(不跟拨号超时绑),它 7 秒左右就握手成功了,之后走这条隧道的请求都很快,基本 0.1s:

[OpenVPN] handshake complete: peer-id=6 ...   # ~7s
HTTP 204  0.12s                                # 复用隧道

我自己的一点小想法(不一定对哈):握手其实是建隧道时的一次性开销,隧道建好之后所有连接都复用同一条,所以这 7 秒整个会话就花一次,后面都不受影响。所以想问问你,这种情况下能不能让握手用它自己的超时跑、不继承单次拨号的 deadline?

当然完全尊重你们的判断,要是觉得不合适我也理解,那我就自己在本地维护一份补丁。如果你觉得这么改 OK,我很乐意提个最小改动的 PR。再次感谢你维护 mihomo 🙏

@wwqgtxx

wwqgtxx commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

7870f8f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants