SagerNet / SagerNet/sing-box

[OpenVPN] Framed data-channel keepalive ping reaches IP stack and terminates endpoint

Open
#4,344 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
38.1k
Forks
4.6k
Avg merge
19d 15h
Merged PRs (30d)
1

Description

Operating system

macOS

System version

macOS 26.5.2 (25F84), arm64

Installation type

Original sing-box Command Line

If you are using a graphical client, please provide the version of the client.

Also reproduced in SFM 1.14.0-beta.1 (Build 1); minimal reproduction uses the original CLI.

Version
sing-box version 1.14.0-beta.1

Environment: go1.26.5 darwin/arm64
Tags: with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,with_ccm,with_ocm,with_cloudflared,with_naive_outbound,with_usbip,with_openvpn,with_openconnect,badlinkname,tfogo_checklinkname0
Revision: 8bc6787c7ff785e5f6343241affdadd5ca239bd7
CGO: enabled
Description

An OpenVPN client using legacy LZO framing establishes successfully, then the endpoint terminates when the server sends a framed data-channel keepalive ping.

Expected behavior

The OpenVPN data-channel keepalive is consumed internally after compression/fragment framing is decoded, and is never delivered to the IP device.

Actual behavior

The endpoint reports write packet to device: invalid IP packet and enters the terminal error state.

I instrumented the rejected packet. After LZO framing is removed, it is exactly the standard 16-byte OpenVPN data-channel keepalive payload:

2a187bf3641eb4cb07ed2d0a981fc748

The apparent cause is the ordering between peer_data.go and client_data.go in sing-openvpn commit 64b754d1c277:

  1. peer_data.go checks openVPNDataChannelPingPayload before compression framing is decoded.
  2. A framed ping (for example, the no-compress LZO marker followed by the ping) does not match at that stage.
  3. dataChannelFraming.Decode later removes the framing marker.
  4. The restored 16-byte ping is queued as an incoming tunnel packet.
  5. sing-box's OpenVPN stack device rejects it as a non-IP packet and the client read loop terminates.

I tested compression_lzo values adaptive, asym, yes, and no; changing the client configuration does not fix the filtering-order issue.

A small patch that filters the keepalive again after framing decode, in both incoming receive paths, fixes the problem. With the patch, the same local reproduction stayed connected across multiple server ping intervals and HTTP/HTTPS transfers completed without an invalid packet error.

Reproduction

The following script is fully local and does not use TUN, a graphical client, or a remote VPN server.

Requirements:

  • Original sing-box 1.14.0-beta.1 CLI
  • OpenVPN 2.7.x built with LZO support
  • OpenSSL and Python 3

Save as reproduce-openvpn-framed-ping.sh and run:

chmod +x reproduce-openvpn-framed-ping.sh
./reproduce-openvpn-framed-ping.sh /path/to/sing-box-1.14.0-beta.1

Complete reproduction script:

#!/usr/bin/env bash
set -euo pipefail

SING_BOX="${1:-sing-box}"
OPENVPN="${OPENVPN:-openvpn}"
WORK="$(mktemp -d "${TMPDIR:-/tmp}/openvpn-framed-ping.XXXXXX")"
trap 'pkill -P $$ openvpn 2>/dev/null || true; rm -rf "$WORK"' EXIT
cd "$WORK"

cat > ca.cnf <<'CNF'
[req]
distinguished_name=dn
x509_extensions=v3_ca
prompt=no
[dn]
CN=OpenVPN Repro CA
[v3_ca]
basicConstraints=critical,CA:TRUE
keyUsage=critical,keyCertSign,cRLSign
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
CNF
openssl req -x509 -newkey rsa:2048 -nodes -days 2 \
  -keyout ca.key -out ca.crt -config ca.cnf >/dev/null 2>&1

cat > leaf.cnf <<'CNF'
[req]
distinguished_name=dn
req_extensions=v3_req
prompt=no
[dn]
CN=PLACEHOLDER
[v3_req]
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=PLACEHOLDER_USAGE
subjectAltName=DNS:PLACEHOLDER
CNF

sed 's/PLACEHOLDER/server/g; s/server_USAGE/serverAuth/' leaf.cnf > server.cnf
openssl req -new -newkey rsa:2048 -nodes -keyout server.key \
  -out server.csr -config server.cnf >/dev/null 2>&1
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 2 -out server.crt -extfile server.cnf -extensions v3_req >/dev/null 2>&1

sed 's/PLACEHOLDER/client/g; s/client_USAGE/clientAuth/' leaf.cnf > client.cnf
openssl req -new -newkey rsa:2048 -nodes -keyout client.key \
  -out client.csr -config client.cnf >/dev/null 2>&1
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 2 -out client.crt -extfile client.cnf -extensions v3_req >/dev/null 2>&1

cat > server.ovpn <<CFG
mode server
tls-server
dev null
proto udp
local 127.0.0.1
port 31194
ca $WORK/ca.crt
cert $WORK/server.crt
key $WORK/server.key
dh none
ifconfig-noexec
route-noexec
script-security 0
topology net30
ifconfig 10.8.0.1 10.8.0.2
push "topology net30"
push "ifconfig 10.8.0.2 10.8.0.1"
push "ping 2"
push "ping-restart 10"
ping 2
ping-restart 10
data-ciphers AES-256-GCM
comp-lzo adaptive
allow-compression yes
verb 4
CFG

python3 - <<'PY'
import json, pathlib
root = pathlib.Path.cwd()
config = {
  "log": {"level": "debug", "timestamp": True},
  "endpoints": [{
    "type": "openvpn-client",
    "tag": "openvpn",
    "mode": "tls",
    "server": "127.0.0.1",
    "server_port": 31194,
    "network": "udp",
    "tls": {
      "server_name": "server",
      "certificate": (root / "ca.crt").read_text(),
      "client_certificate": (root / "client.crt").read_text(),
      "client_key": (root / "client.key").read_text(),
      "remote_certificate_tls": "server"
    },
    "data_ciphers": ["AES-256-GCM"],
    "compression_lzo": "asym",
    "allow_compression": "asym"
  }]
}
(root / "client.json").write_text(json.dumps(config, indent=2) + "\n")
PY

"$OPENVPN" --config "$WORK/server.ovpn" >server.log 2>&1 &
SERVER_PID=$!
sleep 1

echo "The client establishes, then should fail in about 2 seconds:"
"$SING_BOX" run -c "$WORK/client.json" 2>&1 | tee client.log
wait "$SERVER_PID"

The client establishes immediately and fails when the first framed server keepalive is delivered.

Logs
Stock sing-box 1.14.0-beta.1 client log:


+0800 2026-07-24 13:18:47 INFO endpoint/openvpn-client[openvpn]: tunnel established to 127.0.0.1:31194 over udp
+0800 2026-07-24 13:18:49 ERROR endpoint/openvpn-client[openvpn]: write packet to device: invalid IP packet


Instrumented error:


write packet to device: invalid IP packet: length=16 head=2a187bf3641eb4cb07ed2d0a981fc748


OpenVPN server confirms negotiated LZO framing and the 2-second timers:


Data Channel: cipher 'AES-256-GCM', peer-id: 0, compression: 'lzo'
Timers: ping 2, ping-restart 10
Supporter
Integrity requirements
  • I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values.
  • I confirm that I have provided the server and client configuration files and process that can be reproduced locally, instead of a complicated client configuration file that has been stripped of sensitive data.
  • I confirm that I have provided the simplest configuration that can be used to reproduce the error I reported, instead of depending on remote servers, TUN, graphical interface clients, or other closed-source software.
  • I confirm that I have provided the complete configuration files and logs, rather than just providing parts I think are useful out of confidence in my own intelligence.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with peer_data.go and client_data.go in sing-openvpn, then trace dataChannelFraming.Decode in both incoming receive paths. Run the supplied reproduce-openvpn-framed-ping.sh script with OpenVPN 2.7.x and the specified sing-box binary. Done means the framed keepalive is consumed internally, the client remains connected across server ping intervals, and no invalid IP packet error occurs.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.