Improve OpenVPN & OpenConnect interoperability

This commit is contained in:
世界
2026-07-21 09:38:21 +08:00
parent 5cad5ad42d
commit 2ff294c4f4
47 changed files with 3915 additions and 440 deletions
+219
View File
@@ -19,6 +19,7 @@ import (
"os"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"sync"
@@ -29,6 +30,9 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
ovpn "github.com/sagernet/sing-openvpn"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/bufio"
@@ -91,6 +95,21 @@ type openVPNSelfCase struct {
remoteCertificateTLS string
}
type openVPNPacketReturn struct {
packets chan []byte
}
func (r *openVPNPacketReturn) ReturnHeadroom() int {
return 0
}
func (r *openVPNPacketReturn) ReturnPackets(packets [][]byte) [][]byte {
for _, packet := range packets {
r.packets <- slices.Clone(packet)
}
return nil
}
func TestOpenVPNSelfToSelf(t *testing.T) {
testCases := []openVPNSelfCase{
{
@@ -121,6 +140,185 @@ func TestOpenVPNSelfToSelf(t *testing.T) {
}
}
func TestOpenVPNStaticKeyClientDataPath(t *testing.T) {
const (
clientTunnelAddress = "10.91.0.2"
peerTunnelAddress = "10.91.0.1"
)
listener, err := net.Listen(N.NetworkTCP, "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { _ = listener.Close() })
openVPNPort := uint16(listener.Addr().(*net.TCPAddr).Port)
staticKey := createOpenVPNStaticKey(t)
staticKeyPath := writeOpenVPNStaticKeyFile(t, staticKey)
peerContext, cancelPeer := context.WithCancel(context.Background())
peerClient, err := ovpn.NewClient(ovpn.ClientOptions{
Context: peerContext,
Mode: ovpn.ModeStaticKey,
Transport: ovpn.ClientTransportOptions{
Remotes: []ovpn.Remote{{
Host: "127.0.0.1",
Port: openVPNPort,
Protocol: N.NetworkTCP,
}},
Protocol: N.NetworkTCP,
DialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
return listener.Accept()
},
},
DataChannel: ovpn.ClientDataChannelOptions{
MTU: 1500,
Cipher: "AES-256-CBC",
Auth: "SHA256",
},
Tunnel: ovpn.ClientTunnelOptions{
DevType: "tun",
Topology: "p2p",
LocalAddress: []netip.Prefix{netip.MustParsePrefix(peerTunnelAddress + "/30")},
VPNGateway: netip.MustParseAddr(clientTunnelAddress),
},
StaticKey: ovpn.Material{Content: []byte(staticKey)},
KeyDirection: 0,
})
require.NoError(t, err)
t.Cleanup(func() {
cancelPeer()
_ = listener.Close()
_ = peerClient.Close()
})
err = peerClient.Start()
require.NoError(t, err)
clientOptions := option.OpenVPNClientEndpointOptions{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: openVPNPort,
},
Mode: ovpn.ModeStaticKey,
Network: N.NetworkTCP,
Address: []netip.Prefix{netip.MustParsePrefix(clientTunnelAddress + "/30")},
PeerAddress: badoption.Addr(netip.MustParseAddr(peerTunnelAddress)),
Topology: "p2p",
StaticKeyPath: staticKeyPath,
KeyDirection: "client",
Cipher: "AES-256-CBC",
Auth: "SHA256",
MSSFixDisabled: true,
PingRestartDisabled: true,
}
proxyPort := reserveOpenVPNTCPPort(t)
clientInstance := startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort))
clientEndpoint := requireOpenVPNEndpoint(t, clientInstance, "openvpn-client")
connectedStatus := waitForOpenVPNStatus(t, clientEndpoint, 30*time.Second, func(status adapter.OpenVPNStatus) bool {
require.NotEqual(t, adapter.OpenVPNStateError, status.State, status.Error)
return status.State == adapter.OpenVPNStateConnected
})
require.Equal(t, []netip.Prefix{netip.MustParsePrefix(clientTunnelAddress + "/30")}, connectedStatus.TunnelInfo.IPv4)
port, supported := clientEndpoint.(tun.Port)
require.True(t, supported)
returnPath := &openVPNPacketReturn{packets: make(chan []byte, 1)}
err = port.AttachReturn(returnPath)
require.NoError(t, err)
t.Cleanup(func() { _ = port.DetachReturn(returnPath) })
outboundPacket := newOpenVPNDataPathUDPPacket(
netip.MustParseAddrPort(clientTunnelAddress+":12000"),
netip.MustParseAddrPort(peerTunnelAddress+":13000"),
[]byte("sing-box static-key outbound"),
)
err = port.WritePackets([][]byte{outboundPacket})
require.NoError(t, err)
readContext, cancelRead := context.WithTimeout(context.Background(), 10*time.Second)
peerPacket, err := peerClient.ReadDataPacket(readContext)
cancelRead()
require.NoError(t, err)
require.Equal(t, outboundPacket, peerPacket)
inboundPacket := newOpenVPNDataPathUDPPacket(
netip.MustParseAddrPort(peerTunnelAddress+":13000"),
netip.MustParseAddrPort(clientTunnelAddress+":12000"),
[]byte("sing-box static-key inbound"),
)
err = peerClient.WriteDataPacket(inboundPacket)
require.NoError(t, err)
select {
case returnedPacket := <-returnPath.packets:
require.Equal(t, inboundPacket, returnedPacket)
case <-time.After(10 * time.Second):
t.Fatal("timed out waiting for static-key inbound packet")
}
}
func TestOpenVPNStaticKeySelfToSelf(t *testing.T) {
for _, protocol := range []string{N.NetworkTCP, N.NetworkUDP} {
t.Run(protocol, func(t *testing.T) {
runOpenVPNStaticKeySelfToSelf(t, protocol)
})
}
}
func runOpenVPNStaticKeySelfToSelf(t *testing.T, protocol string) {
t.Helper()
const (
serverTunnelAddress = "10.92.0.1"
clientTunnelAddress = "10.92.0.2"
)
openVPNPort := reserveOpenVPNProtocolPort(t, protocol)
var clientOpenVPNPort uint16
var serverRemote string
if protocol == N.NetworkUDP {
clientOpenVPNPort = reserveOpenVPNUDPPort(t)
serverRemote = "127.0.0.1"
}
proxyPort := reserveOpenVPNTCPPort(t)
echoPort := reserveOpenVPNEchoPort(t)
readinessPort := reserveOpenVPNEchoPort(t)
staticKeyPath := writeOpenVPNStaticKeyFile(t, createOpenVPNStaticKey(t))
serverOptions := option.OpenVPNServerEndpointOptions{
ListenOptions: option.ListenOptions{
Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))),
ListenPort: openVPNPort,
},
Mode: ovpn.ModeStaticKey,
Network: protocol,
Remote: serverRemote,
RemotePort: clientOpenVPNPort,
MaxClients: 1,
Address: []netip.Prefix{netip.MustParsePrefix(serverTunnelAddress + "/30")},
PeerAddress: badoption.Addr(netip.MustParseAddr(clientTunnelAddress)),
Topology: "p2p",
StaticKeyPath: staticKeyPath,
KeyDirection: "server",
Cipher: "AES-256-CBC",
Auth: "SHA256",
MSSFixDisabled: true,
}
clientOptions := option.OpenVPNClientEndpointOptions{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: openVPNPort,
},
Mode: ovpn.ModeStaticKey,
Network: protocol,
Address: []netip.Prefix{netip.MustParsePrefix(clientTunnelAddress + "/30")},
PeerAddress: badoption.Addr(netip.MustParseAddr(serverTunnelAddress)),
Topology: "p2p",
StaticKeyPath: staticKeyPath,
KeyDirection: "client",
Cipher: "AES-256-CBC",
Auth: "SHA256",
MSSFixDisabled: true,
PingRestartDisabled: true,
}
clientOptions.UDPBindPort = clientOpenVPNPort
startInstance(t, openVPNServerInstanceOptions(serverOptions))
startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort))
waitForOpenVPNClientReady(t, proxyPort, readinessPort, serverTunnelAddress)
testSuitOpenVPN(t, proxyPort, echoPort, serverTunnelAddress)
}
func TestOpenVPNDockerInterop(t *testing.T) {
t.Run("official_server_to_sing_box_client", func(t *testing.T) {
testOpenVPNDockerOfficialServerToSingBoxClient(t)
@@ -1777,6 +1975,27 @@ func createOpenVPNStaticKey(t *testing.T) string {
return strings.Join(lines, "\n")
}
func newOpenVPNDataPathUDPPacket(source netip.AddrPort, destination netip.AddrPort, payload []byte) []byte {
packet := make([]byte, header.IPv4MinimumSize+header.UDPMinimumSize+len(payload))
ipHeader := header.IPv4(packet)
ipHeader.Encode(&header.IPv4Fields{
TotalLength: uint16(len(packet)),
TTL: 64,
Protocol: uint8(header.UDPProtocolNumber),
SrcAddr: source.Addr(),
DstAddr: destination.Addr(),
})
ipHeader.SetChecksum(^ipHeader.CalculateChecksum())
udpHeader := header.UDP(packet[header.IPv4MinimumSize:])
udpHeader.Encode(&header.UDPFields{
SrcPort: source.Port(),
DstPort: destination.Port(),
Length: uint16(header.UDPMinimumSize + len(payload)),
})
copy(udpHeader.Payload(), payload)
return packet
}
func reserveOpenVPNProtocolPort(t *testing.T, protocol string) uint16 {
t.Helper()
if protocol == N.NetworkTCP {