Skip to content

Fix: OpenVPN TCP data packet drops due to improper replay window - #3

Open
tangyuanfu wants to merge 1 commit into
SagerNet:mainfrom
tangyuanfu:main
Open

Fix: OpenVPN TCP data packet drops due to improper replay window #3
tangyuanfu wants to merge 1 commit into
SagerNet:mainfrom
tangyuanfu:main

Conversation

@tangyuanfu

Copy link
Copy Markdown

Symptoms

WARN endpoint/openvpn-client[ovn]: dropped invalid data packet: replayed tls data packet

Errors occur:

  1. replayed tls data packet — Triggered by the replay window check on TCP.

Root Cause Analysis

1. Replay window check should not run on TCP

In sing-openvpn (the embedded OpenVPN implementation), the function dataReplayWindowParameters in data_codec.go (lines 35–36) forced a zero-sized replay window for TCP:

func dataReplayWindowParameters(transportProtocol string, ...) (uint32, time.Duration) {
    if strings.HasPrefix(strings.ToLower(transportProtocol), "tcp") {
        return 0, 0  // windowSize=0, windowTime=0
    }
    // ...
}

A window of zero means packetID == highest + 1 — no tolerance for normal TCP retransmission or slight out-of-order delivery. However, the Accept() check was still called. Any TCP retransmission was flagged as "replayed" and the packet was dropped.

OpenVPN official behavior: The --replay-window option is documented as only relevant for UDP. OpenVPN's own implementation skips replay checking when proto tcp-client or proto tcp-server is set. Over TCP, the transport layer already guarantees ordering and handles loss recovery — there is no need for replay protection.

Fix applied:

In replay_window.go, newReplayWindow():

func newReplayWindow(windowSize uint32, timeWindow time.Duration) *replayWindow {
    if windowSize == 0 {
        return nil   // TCP: no replay window, check naturally skipped
    }
    // ...
}

This causes all replayWindow != nil && !replayWindow.Accept(packetID) checks to naturally skip for TCP connections. Result: replayed tls data packet errors stopped completely.

Summary

Issue Root Cause Fix
replayed tls data packet TCP replay window check not skipped newReplayWindow returns nil for windowSize==0

OpenVPN's --replay-window only applies to UDP mode. TCP provides
    its own ordered, reliable transport and does not need replay protection.
    
    Previously, windowSize=0 (TCP) created an empty replayWindow whose
    acceptLocked() only accepted strictly monotonic packetIDs. Normal
    TCP retransmission or slight out-of-order delivery was rejected as
    'replayed tls data packet', causing data loss and session failure.
    
    Fix: newReplayWindow returns nil when windowSize==0. All callers
    (data_codec_aead.go, data_codec_cbc.go, data_codec_static.go,
    data_codec_stream.go) already guard with:
        if c.replayWindow != nil && !c.replayWindow.Accept(...)
    so the nil case naturally skips replay checks for TCP connections.
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.

1 participant