Files
sing-box/test/openvpn_test.go

2043 lines
66 KiB
Go

package main
import (
"archive/tar"
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"encoding/pem"
"io"
"net"
"net/netip"
"os"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/sagernet/sing-box"
"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"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/protocol/socks"
typesapi "github.com/docker/docker/api/types"
containerapi "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/stdcopy"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/require"
)
const (
openVPNTLSUsername = "test-user"
openVPNTLSPassword = "test-password"
openVPNStaticChallengeText = "Enter OTP"
openVPNStaticChallengeResponse = "31337"
openVPNLargeDataTimeout = 3 * time.Minute
openVPNLargeTCPPackets = 1
openVPNLargeTCPSize = 2048
openVPNDockerImage = "sing-box-openvpn-2.6.14-options:test"
openVPNDockerPackageVersion = "2.6.14-0+deb12u2"
openVPNDockerRoot = "/config"
)
var (
openVPNDockerImageOnce sync.Once
openVPNDockerImageErr error
)
type openVPNCertificateBundle struct {
caPath string
serverCertPath string
serverKeyPath string
clientCertPath string
clientKeyPath string
}
type openVPNDockerServerEnvironment struct {
certificates openVPNCertificateBundle
workspace string
openVPNPort uint16
echoPort uint16
container *openVPNDockerContainer
}
type openVPNSelfCase struct {
name string
protocol string
tlsCrypt bool
omitServerKeyUsage bool
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{
{
name: "real_tls_udp",
protocol: N.NetworkUDP,
},
{
name: "real_tls_tcp",
protocol: N.NetworkTCP,
},
{
name: "real_tls_udp_tls_crypt",
protocol: N.NetworkUDP,
tlsCrypt: true,
},
{
name: "missing_server_key_usage",
protocol: N.NetworkUDP,
omitServerKeyUsage: true,
remoteCertificateTLS: "none",
},
}
for i := range testCases {
currentTestCase := testCases[i]
t.Run(currentTestCase.name, func(t *testing.T) {
runOpenVPNSelfToSelf(t, currentTestCase)
})
}
}
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)
})
t.Run("official_client_to_sing_box_server", func(t *testing.T) {
testOpenVPNDockerOfficialClientToSingBoxServer(t)
})
}
func runOpenVPNSelfToSelf(t *testing.T, testCase openVPNSelfCase) {
t.Helper()
const serverAddress = "10.8.0.1"
serverPrefix := netip.MustParsePrefix(serverAddress + "/24")
proxyPort := reserveOpenVPNTCPPort(t)
openVPNPort := reserveOpenVPNProtocolPort(t, testCase.protocol)
echoPort := reserveOpenVPNEchoPort(t)
readinessPort := reserveOpenVPNEchoPort(t)
certificates := createOpenVPNCertificateBundle(t)
if testCase.omitServerKeyUsage {
certificates = createOpenVPNCertificateBundleWithServerKeyUsage(t, 0)
}
serverOptions := option.OpenVPNServerEndpointOptions{
ListenOptions: option.ListenOptions{
Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))),
ListenPort: openVPNPort,
},
Network: testCase.protocol,
Address: []netip.Prefix{serverPrefix},
Users: []auth.User{
{
Username: openVPNTLSUsername,
Password: openVPNTLSPassword,
},
},
TLS: &option.OpenVPNInboundTLSOptions{
CertificatePath: certificates.serverCertPath,
KeyPath: certificates.serverKeyPath,
ClientCertificatePath: certificates.caPath,
},
}
serverOptions.UDPMapping = option.UDPNATBehaviorAddressDependent
serverOptions.UDPFiltering = option.UDPNATBehaviorAddressAndPortDependent
serverOptions.UDPNATMax = 128
clientOptions := newOpenVPNTLSClientOptions(testCase.protocol, openVPNPort, certificates.caPath, certificates.clientCertPath, certificates.clientKeyPath)
clientOptions.TLS.RemoteCertificateTLS = testCase.remoteCertificateTLS
clientOptions.UDPMapping = option.UDPNATBehaviorAddressDependent
clientOptions.UDPFiltering = option.UDPNATBehaviorAddressAndPortDependent
clientOptions.UDPNATMax = 128
if testCase.tlsCrypt {
tlsCryptKeyPath := writeOpenVPNStaticKeyFile(t, createOpenVPNStaticKey(t))
serverOptions.TLS.ControlWrap = &option.OpenVPNInboundControlWrapOptions{
Type: "tls_crypt",
KeyPath: tlsCryptKeyPath,
}
clientOptions.TLS.ControlWrap = &option.OpenVPNControlWrapOptions{
Type: "tls_crypt",
KeyPath: tlsCryptKeyPath,
}
}
clientOptions.Username = openVPNTLSUsername
clientOptions.Password = openVPNTLSPassword
startInstance(t, openVPNServerInstanceOptions(serverOptions))
startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort))
waitForOpenVPNClientReady(t, proxyPort, readinessPort, serverAddress)
testSuitOpenVPN(t, proxyPort, echoPort, serverAddress)
}
func testOpenVPNDockerOfficialServerToSingBoxClient(t *testing.T) {
t.Helper()
environment := startOpenVPNDockerOfficialServer(t, "official-server")
proxyPort := reserveOpenVPNTCPPort(t)
clientOptions := newOpenVPNTLSClientOptions(
N.NetworkUDP,
environment.openVPNPort,
filepath.Join(environment.workspace, "ca.crt"),
filepath.Join(environment.workspace, "client.crt"),
filepath.Join(environment.workspace, "client.key"),
)
clientOptions.Username = openVPNTLSUsername
clientOptions.Password = openVPNTLSPassword
startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort))
waitForOpenVPNRemoteReady(t, proxyPort, "10.8.0.1", environment.echoPort, 30*time.Second)
testRemoteEchoThroughSocks(t, proxyPort, "10.8.0.1", environment.echoPort)
}
func TestOpenVPNDockerMultiRemoteFailover(t *testing.T) {
environment := startOpenVPNDockerOfficialServer(t, "multi-remote-server")
deadPort := reserveOpenVPNTCPPort(t)
proxyPort := reserveOpenVPNTCPPort(t)
clientOptions := newOpenVPNTLSClientOptions(
N.NetworkUDP,
environment.openVPNPort,
filepath.Join(environment.workspace, "ca.crt"),
filepath.Join(environment.workspace, "client.crt"),
filepath.Join(environment.workspace, "client.key"),
)
clientOptions.Server = ""
clientOptions.ServerPort = 0
clientOptions.Servers = []option.OpenVPNRemoteOptions{
{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: deadPort,
},
Network: N.NetworkTCP,
},
{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: environment.openVPNPort,
},
Network: N.NetworkUDP,
},
}
clientOptions.Username = openVPNTLSUsername
clientOptions.Password = openVPNTLSPassword
startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort))
waitForOpenVPNRemoteReady(t, proxyPort, "10.8.0.1", environment.echoPort, 30*time.Second)
testRemoteEchoThroughSocks(t, proxyPort, "10.8.0.1", environment.echoPort)
}
func TestOpenVPNDockerMSSFix(t *testing.T) {
dockerClient := requireOpenVPNDockerEnvironment(t)
certificates := createOpenVPNCertificateBundle(t)
workspace := newOpenVPNDockerWorkspace(t, certificates)
openVPNPort := reserveOpenVPNUDPPort(t)
proxyPort := reserveOpenVPNTCPPort(t)
echoPort := reserveOpenVPNEchoPort(t)
writeOpenVPNDockerServerConfig(t, workspace, openVPNPort, "/config/check_userpass.sh", "mssfix 1200")
writeOpenVPNDockerEchoServer(t, workspace, "10.8.0.1", echoPort)
serverCommand := strings.Join([]string{
"python3 /config/echo_server.py &",
"openvpn --config /config/server.conf &",
"openvpn_pid=$!",
"until grep -q 'Initialization Sequence Completed' /config/openvpn.log; do",
" if ! kill -0 \"$openvpn_pid\" 2>/dev/null; then cat /config/openvpn.log; exit 1; fi",
" sleep 0.1",
"done",
"tcpdump -i tun0 -l -nn -v 'dst host 10.8.0.1 and tcp[tcpflags] & tcp-syn != 0' > /config/mss.log 2>&1 &",
"wait \"$openvpn_pid\"",
}, "\n")
serverContainer := startOpenVPNDockerContainer(t, dockerClient, "mss-fix-server", workspace, serverCommand)
dumpOpenVPNDockerLogsOnFailure(t, serverContainer, workspace)
waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "openvpn.log"), "Initialization Sequence Completed", 30*time.Second)
waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "echo.ready"), "ready", 30*time.Second)
waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "mss.log"), "listening on tun0", 30*time.Second)
clientOptions := newOpenVPNTLSClientOptions(
N.NetworkUDP,
openVPNPort,
filepath.Join(workspace, "ca.crt"),
filepath.Join(workspace, "client.crt"),
filepath.Join(workspace, "client.key"),
)
clientOptions.Username = openVPNTLSUsername
clientOptions.Password = openVPNTLSPassword
clientOptions.MSSFix = 1200
startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort))
waitForOpenVPNRemoteReady(t, proxyPort, "10.8.0.1", echoPort, 30*time.Second)
testRemoteEchoThroughSocks(t, proxyPort, "10.8.0.1", echoPort)
waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "mss.log"), "mss 1136", 30*time.Second)
}
func TestOpenVPNDockerCompressionNegotiation(t *testing.T) {
environment := startOpenVPNDockerOfficialServer(t, "compression-server",
"allow-compression no",
"compress stub",
"push \"compress stub\"",
)
proxyPort := reserveOpenVPNTCPPort(t)
clientOptions := newOpenVPNTLSClientOptions(
N.NetworkUDP,
environment.openVPNPort,
filepath.Join(environment.workspace, "ca.crt"),
filepath.Join(environment.workspace, "client.crt"),
filepath.Join(environment.workspace, "client.key"),
)
clientOptions.Username = openVPNTLSUsername
clientOptions.Password = openVPNTLSPassword
clientOptions.Compression = "stub"
clientOptions.AllowCompression = "asym"
startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort))
waitForOpenVPNRemoteReady(t, proxyPort, "10.8.0.1", environment.echoPort, 30*time.Second)
testRemoteEchoThroughSocks(t, proxyPort, "10.8.0.1", environment.echoPort)
waitForOpenVPNDockerFile(t, environment.container, filepath.Join(environment.workspace, "openvpn.log"), "IV_LZO=1", 30*time.Second)
}
func TestOpenVPNDockerPeerFingerprint(t *testing.T) {
environment := startOpenVPNDockerOfficialServer(t, "peer-fingerprint-server")
fingerprint := openVPNCertificateSHA256Fingerprint(t, environment.certificates.serverCertPath)
wrongFingerprint := "0" + fingerprint[1:]
if fingerprint[0] == '0' {
wrongFingerprint = "1" + fingerprint[1:]
}
t.Run("matching", func(matchingTest *testing.T) {
matchingProxyPort := reserveOpenVPNTCPPort(matchingTest)
matchingClientOptions := newOpenVPNTLSClientOptions(
N.NetworkUDP,
environment.openVPNPort,
"",
filepath.Join(environment.workspace, "client.crt"),
filepath.Join(environment.workspace, "client.key"),
)
matchingClientOptions.Username = openVPNTLSUsername
matchingClientOptions.Password = openVPNTLSPassword
matchingClientOptions.TLS.PeerFingerprint = []string{fingerprint}
startInstance(matchingTest, openVPNClientInstanceOptions(matchingClientOptions, matchingProxyPort))
waitForOpenVPNRemoteReady(matchingTest, matchingProxyPort, "10.8.0.1", environment.echoPort, 30*time.Second)
testRemoteEchoThroughSocks(matchingTest, matchingProxyPort, "10.8.0.1", environment.echoPort)
})
t.Run("mismatch", func(mismatchTest *testing.T) {
mismatchProxyPort := reserveOpenVPNTCPPort(mismatchTest)
mismatchClientOptions := newOpenVPNTLSClientOptions(
N.NetworkUDP,
environment.openVPNPort,
"",
filepath.Join(environment.workspace, "client.crt"),
filepath.Join(environment.workspace, "client.key"),
)
mismatchClientOptions.Username = openVPNTLSUsername
mismatchClientOptions.Password = openVPNTLSPassword
mismatchClientOptions.TLS.PeerFingerprint = []string{wrongFingerprint}
mismatchInstance := startInstance(mismatchTest, openVPNClientInstanceOptions(mismatchClientOptions, mismatchProxyPort))
mismatchEndpoint := requireOpenVPNEndpoint(mismatchTest, mismatchInstance, "openvpn-client")
mismatchStatus := waitForOpenVPNStatus(mismatchTest, mismatchEndpoint, 30*time.Second, func(status adapter.OpenVPNStatus) bool {
return status.State == adapter.OpenVPNStateError
})
require.Contains(mismatchTest, mismatchStatus.Error, "peer fingerprint mismatch")
})
}
func TestOpenVPNDockerClientRoutes(t *testing.T) {
environment := startOpenVPNDockerOfficialServer(t, "client-routes-server")
localRoute := netip.MustParsePrefix("192.0.2.0/24")
t.Run("routes", func(routesTest *testing.T) {
routesProxyPort := reserveOpenVPNTCPPort(routesTest)
routesClientOptions := newOpenVPNTLSClientOptions(
N.NetworkUDP,
environment.openVPNPort,
filepath.Join(environment.workspace, "ca.crt"),
filepath.Join(environment.workspace, "client.crt"),
filepath.Join(environment.workspace, "client.key"),
)
routesClientOptions.Username = openVPNTLSUsername
routesClientOptions.Password = openVPNTLSPassword
routesClientOptions.Routes = []netip.Prefix{localRoute}
routesInstance := startInstance(routesTest, openVPNClientInstanceOptions(routesClientOptions, routesProxyPort))
waitForOpenVPNRemoteReady(routesTest, routesProxyPort, "10.8.0.1", environment.echoPort, 30*time.Second)
routesEndpoint, loaded := routesInstance.Endpoint().Get("openvpn-client")
require.True(routesTest, loaded)
preferredRoutes, supported := routesEndpoint.(adapter.OutboundWithPreferredRoutes)
require.True(routesTest, supported)
require.True(routesTest, preferredRoutes.PreferredAddress(nil, netip.MustParseAddr("192.0.2.1")))
require.False(routesTest, preferredRoutes.PreferredAddress(nil, netip.MustParseAddr("198.51.100.1")))
})
t.Run("redirect_gateway", func(redirectTest *testing.T) {
redirectProxyPort := reserveOpenVPNTCPPort(redirectTest)
redirectClientOptions := newOpenVPNTLSClientOptions(
N.NetworkUDP,
environment.openVPNPort,
filepath.Join(environment.workspace, "ca.crt"),
filepath.Join(environment.workspace, "client.crt"),
filepath.Join(environment.workspace, "client.key"),
)
redirectClientOptions.Username = openVPNTLSUsername
redirectClientOptions.Password = openVPNTLSPassword
redirectClientOptions.Routes = []netip.Prefix{localRoute}
redirectClientOptions.RedirectGateway = true
redirectClientOptions.RedirectGatewayFlags = []string{"def1"}
redirectInstance := startInstance(redirectTest, openVPNClientInstanceOptions(redirectClientOptions, redirectProxyPort))
waitForOpenVPNRemoteReady(redirectTest, redirectProxyPort, "10.8.0.1", environment.echoPort, 30*time.Second)
redirectEndpoint, loaded := redirectInstance.Endpoint().Get("openvpn-client")
require.True(redirectTest, loaded)
preferredRoutes, supported := redirectEndpoint.(adapter.OutboundWithPreferredRoutes)
require.True(redirectTest, supported)
require.True(redirectTest, preferredRoutes.PreferredAddress(nil, netip.MustParseAddr("192.0.2.1")))
require.True(redirectTest, preferredRoutes.PreferredAddress(nil, netip.MustParseAddr("198.51.100.1")))
require.False(redirectTest, preferredRoutes.PreferredAddress(nil, netip.MustParseAddr("2001:db8::1")))
})
}
func startOpenVPNDockerOfficialServer(t *testing.T, nameSuffix string, serverDirectives ...string) openVPNDockerServerEnvironment {
t.Helper()
dockerClient := requireOpenVPNDockerEnvironment(t)
certificates := createOpenVPNCertificateBundle(t)
workspace := newOpenVPNDockerWorkspace(t, certificates)
openVPNPort := reserveOpenVPNUDPPort(t)
echoPort := reserveOpenVPNEchoPort(t)
writeOpenVPNDockerServerConfig(t, workspace, openVPNPort, "/config/check_userpass.sh", serverDirectives...)
writeOpenVPNDockerEchoServer(t, workspace, "10.8.0.1", echoPort)
serverContainer := startOpenVPNDockerContainer(t, dockerClient, nameSuffix, workspace, "python3 /config/echo_server.py & exec openvpn --config /config/server.conf")
dumpOpenVPNDockerLogsOnFailure(t, serverContainer, workspace)
waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "openvpn.log"), "Initialization Sequence Completed", 30*time.Second)
waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "echo.ready"), "ready", 30*time.Second)
return openVPNDockerServerEnvironment{
certificates: certificates,
workspace: workspace,
openVPNPort: openVPNPort,
echoPort: echoPort,
container: serverContainer,
}
}
func TestOpenVPNInteractiveAuth(t *testing.T) {
dockerClient := requireOpenVPNDockerEnvironment(t)
certificates := createOpenVPNCertificateBundle(t)
workspace := newOpenVPNDockerWorkspace(t, certificates)
openVPNPort := reserveOpenVPNUDPPort(t)
proxyPort := reserveOpenVPNTCPPort(t)
echoPort := reserveOpenVPNEchoPort(t)
writeOpenVPNDockerStaticChallengeScript(t, workspace)
writeOpenVPNDockerServerConfig(t, workspace, openVPNPort, "/config/check_scrv1.sh")
writeOpenVPNDockerEchoServer(t, workspace, "10.8.0.1", echoPort)
serverContainer := startOpenVPNDockerContainer(t, dockerClient, "interactive-auth-server", workspace, "python3 /config/echo_server.py & exec openvpn --config /config/server.conf")
dumpOpenVPNDockerLogsOnFailure(t, serverContainer, workspace)
waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "openvpn.log"), "Initialization Sequence Completed", 30*time.Second)
waitForOpenVPNDockerFile(t, serverContainer, filepath.Join(workspace, "echo.ready"), "ready", 30*time.Second)
clientOptions := newOpenVPNTLSClientOptions(
N.NetworkUDP,
openVPNPort,
filepath.Join(workspace, "ca.crt"),
filepath.Join(workspace, "client.crt"),
filepath.Join(workspace, "client.key"),
)
clientOptions.StaticChallenge = openVPNStaticChallengeText
clientOptions.StaticChallengeEcho = true
clientInstance := startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort))
clientEndpoint := requireOpenVPNEndpoint(t, clientInstance, "openvpn-client")
challengeStatus := waitForOpenVPNStatus(t, clientEndpoint, 30*time.Second, func(status adapter.OpenVPNStatus) bool {
require.NotEqual(t, adapter.OpenVPNStateConnected, status.State)
require.NotEqual(t, adapter.OpenVPNStateError, status.State, status.Error)
return status.State == adapter.OpenVPNStateAuthPending
})
challenge := challengeStatus.Challenge
require.NotNil(t, challenge)
require.NotEmpty(t, challenge.ID)
require.Equal(t, "credentials", challenge.Kind)
require.Equal(t, openVPNStaticChallengeText, challenge.SecretMessage)
require.True(t, challenge.Echo)
require.Empty(t, challenge.PreviousError)
err := clientEndpoint.CompleteChallenge(challenge.ID, adapter.OpenVPNChallengeResponse{
Username: openVPNTLSUsername,
Password: openVPNTLSPassword,
Secret: openVPNStaticChallengeResponse,
})
require.NoError(t, err)
waitForOpenVPNStatus(t, clientEndpoint, time.Minute, func(status adapter.OpenVPNStatus) bool {
require.NotEqual(t, adapter.OpenVPNStateError, status.State, status.Error)
return status.State == adapter.OpenVPNStateConnected
})
waitForOpenVPNRemoteReady(t, proxyPort, "10.8.0.1", echoPort, 30*time.Second)
testRemoteEchoThroughSocks(t, proxyPort, "10.8.0.1", echoPort)
}
func writeOpenVPNDockerStaticChallengeScript(t *testing.T, workspace string) {
t.Helper()
script := strings.Join([]string{
"#!/bin/bash",
"set -eu",
"credentials_file=\"$1\"",
"username=\"$(sed -n '1p' \"$credentials_file\")\"",
"password=\"$(sed -n '2p' \"$credentials_file\")\"",
"[ \"$username\" = \"" + openVPNTLSUsername + "\" ] || exit 1",
"case \"$password\" in",
"SCRV1:*) ;;",
"*) exit 1 ;;",
"esac",
"encoded_password=\"$(printf '%s' \"$password\" | cut -d: -f2)\"",
"encoded_response=\"$(printf '%s' \"$password\" | cut -d: -f3)\"",
"[ \"$(printf '%s' \"$encoded_password\" | base64 -d)\" = \"" + openVPNTLSPassword + "\" ] || exit 1",
"[ \"$(printf '%s' \"$encoded_response\" | base64 -d)\" = \"" + openVPNStaticChallengeResponse + "\" ] || exit 1",
"exit 0",
"",
}, "\n")
err := os.WriteFile(filepath.Join(workspace, "check_scrv1.sh"), []byte(script), 0o700)
require.NoError(t, err)
}
func requireOpenVPNEndpoint(t *testing.T, instance *box.Box, tag string) adapter.OpenVPNEndpoint {
t.Helper()
endpoint, loaded := instance.Endpoint().Get(tag)
require.True(t, loaded)
openVPNEndpoint, supported := endpoint.(adapter.OpenVPNEndpoint)
require.True(t, supported)
return openVPNEndpoint
}
func waitForOpenVPNStatus(t *testing.T, endpoint adapter.OpenVPNEndpoint, timeout time.Duration, predicate func(status adapter.OpenVPNStatus) bool) adapter.OpenVPNStatus {
t.Helper()
timeoutChannel := time.After(timeout)
for {
statusUpdated := endpoint.StatusUpdated()
status := endpoint.OpenVPNStatus()
if predicate(status) {
return status
}
select {
case <-statusUpdated:
case <-timeoutChannel:
t.Fatalf("timed out waiting for OpenVPN endpoint status, last state %q, error %q", status.State, status.Error)
}
}
}
func TestOpenVPNClientReconnectSelfToSelf(t *testing.T) {
const serverAddress = "10.8.0.1"
serverPrefix := netip.MustParsePrefix(serverAddress + "/24")
proxyPort := reserveOpenVPNTCPPort(t)
openVPNPort := reserveOpenVPNUDPPort(t)
echoPort := reserveOpenVPNEchoPort(t)
readinessPort := reserveOpenVPNEchoPort(t)
certificates := createOpenVPNCertificateBundle(t)
serverOptions := option.OpenVPNServerEndpointOptions{
ListenOptions: option.ListenOptions{
Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))),
ListenPort: openVPNPort,
},
Network: N.NetworkUDP,
Address: []netip.Prefix{serverPrefix},
TLS: &option.OpenVPNInboundTLSOptions{
CertificatePath: certificates.serverCertPath,
KeyPath: certificates.serverKeyPath,
ClientCertificatePath: certificates.caPath,
},
PingInterval: badoption.Duration(time.Second),
PingRestart: badoption.Duration(4 * time.Second),
Push: &option.OpenVPNPushOptions{
PingInterval: badoption.Duration(time.Second),
PingRestart: badoption.Duration(2 * time.Second),
},
Users: []auth.User{
{
Username: openVPNTLSUsername,
Password: openVPNTLSPassword,
},
},
}
clientOptions := newOpenVPNTLSClientOptions(N.NetworkUDP, openVPNPort, certificates.caPath, certificates.clientCertPath, certificates.clientKeyPath)
clientOptions.Username = openVPNTLSUsername
clientOptions.Password = openVPNTLSPassword
serverInstance := startInstance(t, openVPNServerInstanceOptions(serverOptions))
startInstance(t, openVPNClientInstanceOptions(clientOptions, proxyPort))
waitForOpenVPNClientReady(t, proxyPort, readinessPort, serverAddress)
err := serverInstance.Close()
require.NoError(t, err)
startInstance(t, openVPNServerInstanceOptions(serverOptions))
waitForOpenVPNClientReady(t, proxyPort, readinessPort, serverAddress)
testSuitOpenVPN(t, proxyPort, echoPort, serverAddress)
}
func testOpenVPNDockerOfficialClientToSingBoxServer(t *testing.T) {
t.Helper()
dockerClient := requireOpenVPNDockerEnvironment(t)
certificates := createOpenVPNCertificateBundle(t)
workspace := newOpenVPNDockerWorkspace(t, certificates)
openVPNPort := reserveOpenVPNUDPPort(t)
echoPort := reserveOpenVPNEchoPort(t)
startOpenVPNHostEchoServers(t, echoPort)
serverOptions := option.OpenVPNServerEndpointOptions{
ListenOptions: option.ListenOptions{
Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))),
ListenPort: openVPNPort,
},
Network: N.NetworkUDP,
Address: []netip.Prefix{netip.MustParsePrefix("10.8.0.1/24")},
TLS: &option.OpenVPNInboundTLSOptions{
CertificatePath: filepath.Join(workspace, "server.crt"),
KeyPath: filepath.Join(workspace, "server.key"),
ClientCertificatePath: filepath.Join(workspace, "ca.crt"),
},
Users: []auth.User{
{
Username: openVPNTLSUsername,
Password: openVPNTLSPassword,
},
},
}
startInstance(t, openVPNServerInstanceOptions(serverOptions))
writeOpenVPNDockerClientConfig(t, workspace, openVPNPort)
writeOpenVPNDockerEchoClient(t, workspace, "10.8.0.1", echoPort)
clientCommand := strings.Join([]string{
"openvpn --config /config/client.conf &",
"openvpn_pid=$!",
"until grep -q 'Initialization Sequence Completed' /config/openvpn.log; do",
" if ! kill -0 \"$openvpn_pid\" 2>/dev/null; then cat /config/openvpn.log; exit 1; fi",
" sleep 0.1",
"done",
"python3 /config/echo_client.py",
"kill \"$openvpn_pid\"",
"wait \"$openvpn_pid\" || true",
}, "\n")
clientContainer := startOpenVPNDockerContainer(t, dockerClient, "official-client", workspace, clientCommand)
dumpOpenVPNDockerLogsOnFailure(t, clientContainer, workspace)
waitResult := clientContainer.Wait(t, 60*time.Second)
require.Equal(t, int64(0), waitResult.exitCode, waitResult.logs)
}
func waitForOpenVPNClientReady(t *testing.T, proxyPort uint16, echoPort uint16, tunnelAddress string) {
t.Helper()
closeEcho := startOpenVPNReadinessEcho(t, echoPort)
defer closeEcho()
waitForOpenVPNRemoteReady(t, proxyPort, tunnelAddress, echoPort, 3*time.Minute)
}
func startOpenVPNReadinessEcho(t *testing.T, port uint16) func() {
t.Helper()
listener, err := listen("tcp", ":"+strconv.Itoa(int(port)))
require.NoError(t, err)
done := make(chan struct{})
go func() {
defer close(done)
for {
conn, acceptErr := listener.Accept()
if acceptErr != nil {
return
}
go echoOpenVPNTCPConnection(conn)
}
}()
return func() {
listener.Close()
<-done
}
}
func waitForOpenVPNRemoteReady(t *testing.T, proxyPort uint16, tunnelAddress string, tunnelPort uint16, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
var lastErr error
for time.Now().Before(deadline) {
lastErr = probeOpenVPNTCPWithTimeout(proxyPort, tunnelAddress, tunnelPort, 3*time.Second)
if lastErr == nil {
return
}
time.Sleep(100 * time.Millisecond)
}
require.NoError(t, lastErr)
}
func probeOpenVPNTCPWithTimeout(proxyPort uint16, tunnelAddress string, tunnelPort uint16, timeout time.Duration) error {
resultCh := make(chan error, 1)
go func() {
probeErr := probeOpenVPNTCP(proxyPort, tunnelAddress, tunnelPort)
resultCh <- probeErr
}()
select {
case resultErr := <-resultCh:
return resultErr
case <-time.After(timeout):
return E.New("timeout")
}
}
func probeOpenVPNTCP(proxyPort uint16, tunnelAddress string, tunnelPort uint16) error {
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", proxyPort), socks.Version5, "", "")
destination := M.ParseSocksaddrHostPort(tunnelAddress, tunnelPort)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
conn, err := dialer.DialContext(ctx, N.NetworkTCP, destination)
if err != nil {
return err
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(time.Second))
if err != nil {
return err
}
return writeAndReadEcho(conn, []byte("ready"))
}
func openVPNServerInstanceOptions(serverOptions option.OpenVPNServerEndpointOptions) option.Options {
return option.Options{
Endpoints: []option.Endpoint{
{
Type: C.TypeOpenVPNServer,
Tag: "openvpn-server",
Options: &serverOptions,
},
},
Outbounds: []option.Outbound{
{
Type: C.TypeDirect,
},
},
}
}
func openVPNClientInstanceOptions(clientOptions option.OpenVPNClientEndpointOptions, proxyPort uint16) option.Options {
return option.Options{
Endpoints: []option.Endpoint{
{
Type: C.TypeOpenVPNClient,
Tag: "openvpn-client",
Options: &clientOptions,
},
},
Inbounds: []option.Inbound{
{
Type: C.TypeSOCKS,
Tag: "socks-in",
Options: &option.SocksInboundOptions{
ListenOptions: option.ListenOptions{
Listen: common.Ptr(badoption.Addr(netip.MustParseAddr("127.0.0.1"))),
ListenPort: proxyPort,
},
},
},
},
Outbounds: []option.Outbound{
{
Type: C.TypeDirect,
},
},
Route: &option.RouteOptions{
Rules: []option.Rule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
RawDefaultRule: option.RawDefaultRule{
Inbound: []string{"socks-in"},
},
RuleAction: option.RuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.RouteActionOptions{
Outbound: "openvpn-client",
},
},
},
},
},
},
}
}
func newOpenVPNTLSClientOptions(protocol string, port uint16, certificatePath string, clientCertificatePath string, clientKeyPath string) option.OpenVPNClientEndpointOptions {
return option.OpenVPNClientEndpointOptions{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: port,
},
Network: protocol,
TLS: &option.OpenVPNOutboundTLSOptions{
CertificatePath: certificatePath,
ClientCertificatePath: clientCertificatePath,
ClientKeyPath: clientKeyPath,
},
}
}
func writeOpenVPNStaticKeyFile(t *testing.T, staticKey string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "static.key")
err := os.WriteFile(path, []byte(staticKey), 0o600)
require.NoError(t, err)
return path
}
func newOpenVPNDockerWorkspace(t *testing.T, certificates openVPNCertificateBundle) string {
t.Helper()
workspace := t.TempDir()
copyOpenVPNFile(t, certificates.caPath, filepath.Join(workspace, "ca.crt"), 0o600)
copyOpenVPNFile(t, certificates.serverCertPath, filepath.Join(workspace, "server.crt"), 0o600)
copyOpenVPNFile(t, certificates.serverKeyPath, filepath.Join(workspace, "server.key"), 0o600)
copyOpenVPNFile(t, certificates.clientCertPath, filepath.Join(workspace, "client.crt"), 0o600)
copyOpenVPNFile(t, certificates.clientKeyPath, filepath.Join(workspace, "client.key"), 0o600)
authFileContent := openVPNTLSUsername + "\n" + openVPNTLSPassword + "\n"
err := os.WriteFile(filepath.Join(workspace, "auth-user-pass.txt"), []byte(authFileContent), 0o600)
require.NoError(t, err)
checkUserPassScript := strings.Join([]string{
"#!/bin/sh",
"set -eu",
"credentials_file=\"$1\"",
"username=\"$(sed -n '1p' \"$credentials_file\")\"",
"password=\"$(sed -n '2p' \"$credentials_file\")\"",
"[ \"$username\" = \"" + openVPNTLSUsername + "\" ] && [ \"$password\" = \"" + openVPNTLSPassword + "\" ]",
"",
}, "\n")
err = os.WriteFile(filepath.Join(workspace, "check_userpass.sh"), []byte(checkUserPassScript), 0o700)
require.NoError(t, err)
return workspace
}
func copyOpenVPNFile(t *testing.T, sourcePath string, targetPath string, mode os.FileMode) {
t.Helper()
content, err := os.ReadFile(sourcePath)
require.NoError(t, err)
err = os.WriteFile(targetPath, content, mode)
require.NoError(t, err)
}
func openVPNCertificateSHA256Fingerprint(t *testing.T, certificatePath string) string {
t.Helper()
certificatePEM, err := os.ReadFile(certificatePath)
require.NoError(t, err)
certificateBlock, _ := pem.Decode(certificatePEM)
require.NotNil(t, certificateBlock)
require.Equal(t, "CERTIFICATE", certificateBlock.Type)
fingerprint := sha256.Sum256(certificateBlock.Bytes)
return hex.EncodeToString(fingerprint[:])
}
func writeOpenVPNDockerServerConfig(t *testing.T, workspace string, openVPNPort uint16, verifyScriptPath string, serverDirectives ...string) {
t.Helper()
configLines := []string{
"port " + strconv.Itoa(int(openVPNPort)),
"proto udp4",
"dev tun",
"topology subnet",
"server 10.8.0.0 255.255.255.0",
"ca /config/ca.crt",
"cert /config/server.crt",
"key /config/server.key",
"dh none",
"persist-key",
"persist-tun",
"verb 4",
"script-security 2",
"auth-user-pass-verify " + verifyScriptPath + " via-file",
"explicit-exit-notify 1",
}
configLines = append(configLines, serverDirectives...)
configLines = append(configLines,
"log /config/openvpn.log",
"",
)
config := strings.Join(configLines, "\n")
err := os.WriteFile(filepath.Join(workspace, "server.conf"), []byte(config), 0o600)
require.NoError(t, err)
}
func writeOpenVPNDockerClientConfig(t *testing.T, workspace string, openVPNPort uint16) {
t.Helper()
config := strings.Join([]string{
"client",
"dev tun",
"proto udp4",
"remote 127.0.0.1 " + strconv.Itoa(int(openVPNPort)),
"resolv-retry infinite",
"nobind",
"float",
"persist-key",
"persist-tun",
"remote-cert-tls server",
"ca /config/ca.crt",
"cert /config/client.crt",
"key /config/client.key",
"auth-user-pass /config/auth-user-pass.txt",
"verb 4",
"explicit-exit-notify 1",
"log /config/openvpn.log",
"",
}, "\n")
err := os.WriteFile(filepath.Join(workspace, "client.conf"), []byte(config), 0o600)
require.NoError(t, err)
}
func writeOpenVPNDockerEchoServer(t *testing.T, workspace string, host string, port uint16) {
t.Helper()
script := strings.ReplaceAll(openVPNDockerEchoServerScript, "{{HOST}}", host)
script = strings.ReplaceAll(script, "{{PORT}}", strconv.Itoa(int(port)))
err := os.WriteFile(filepath.Join(workspace, "echo_server.py"), []byte(script), 0o700)
require.NoError(t, err)
}
func writeOpenVPNDockerEchoClient(t *testing.T, workspace string, host string, port uint16) {
t.Helper()
script := strings.ReplaceAll(openVPNDockerEchoClientScript, "{{HOST}}", host)
script = strings.ReplaceAll(script, "{{PORT}}", strconv.Itoa(int(port)))
err := os.WriteFile(filepath.Join(workspace, "echo_client.py"), []byte(script), 0o700)
require.NoError(t, err)
}
func testRemoteEchoThroughSocks(t *testing.T, proxyPort uint16, destinationAddress string, destinationPort uint16) {
t.Helper()
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", proxyPort), socks.Version5, "", "")
destination := M.ParseSocksaddrHostPort(destinationAddress, destinationPort)
err := testRemoteTCPEcho(t, dialer, destination)
require.NoError(t, err)
err = testRemoteUDPEcho(t, dialer, destination)
require.NoError(t, err)
}
func testRemoteTCPEcho(t *testing.T, dialer *socks.Client, destination M.Socksaddr) error {
t.Helper()
conn, err := dialer.DialContext(context.Background(), N.NetworkTCP, destination)
if err != nil {
return err
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(30 * time.Second))
if err != nil {
return err
}
err = writeAndReadEcho(conn, []byte("ping"))
if err != nil {
return err
}
payload := make([]byte, 64*1024)
for i := 0; i < 100; i++ {
_, err = rand.Read(payload[1:])
if err != nil {
return err
}
payload[0] = byte(i)
err = writeAndReadEcho(conn, payload)
if err != nil {
return err
}
}
return nil
}
func testRemoteUDPEcho(t *testing.T, dialer *socks.Client, destination M.Socksaddr) error {
t.Helper()
conn, err := dialer.DialContext(context.Background(), N.NetworkUDP, destination)
if err != nil {
return err
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(30 * time.Second))
if err != nil {
return err
}
err = writeAndReadPacketEcho(conn, []byte("ping"))
if err != nil {
return err
}
payload := make([]byte, 1500)
for i := 0; i < 50; i++ {
_, err = rand.Read(payload[1:])
if err != nil {
return err
}
payload[0] = byte(i)
err = writeAndReadPacketEcho(conn, payload)
if err != nil {
return err
}
}
return nil
}
func writeAndReadEcho(conn net.Conn, payload []byte) error {
_, err := conn.Write(payload)
if err != nil {
return err
}
response := make([]byte, len(payload))
_, err = io.ReadFull(conn, response)
if err != nil {
return err
}
if !bytes.Equal(payload, response) {
return E.New("unexpected TCP echo response")
}
return nil
}
func writeAndReadPacketEcho(conn net.Conn, payload []byte) error {
_, err := conn.Write(payload)
if err != nil {
return err
}
response := make([]byte, len(payload)+512)
n, err := conn.Read(response)
if err != nil {
return err
}
if !bytes.Equal(payload, response[:n]) {
return E.New("unexpected UDP echo response")
}
return nil
}
func startOpenVPNHostEchoServers(t *testing.T, port uint16) {
t.Helper()
tcpListener, err := listen("tcp", ":"+strconv.Itoa(int(port)))
require.NoError(t, err)
udpConnection, err := listenPacket("udp", ":"+strconv.Itoa(int(port)))
require.NoError(t, err)
t.Cleanup(func() {
tcpListener.Close()
udpConnection.Close()
})
go func() {
for {
conn, acceptErr := tcpListener.Accept()
if acceptErr != nil {
return
}
go echoOpenVPNTCPConnection(conn)
}
}()
go func() {
buffer := make([]byte, 64*1024)
for {
n, address, readErr := udpConnection.ReadFrom(buffer)
if readErr != nil {
return
}
_, _ = udpConnection.WriteTo(buffer[:n], address)
}
}()
}
func echoOpenVPNTCPConnection(conn net.Conn) {
defer conn.Close()
buffer := make([]byte, 64*1024)
for {
n, err := conn.Read(buffer)
if err != nil {
return
}
_, err = conn.Write(buffer[:n])
if err != nil {
return
}
}
}
type openVPNDockerContainer struct {
dockerClient *client.Client
containerID string
name string
}
type openVPNDockerWaitResult struct {
exitCode int64
logs string
}
func requireOpenVPNDockerEnvironment(t *testing.T) *client.Client {
t.Helper()
dockerClient := openVPNDockerClientForTest(t)
_, err := dockerClient.Ping(context.Background())
if err != nil {
dockerClient.Close()
t.Skipf("Docker is unavailable: %v", err)
}
openVPNDockerImageOnce.Do(func() {
openVPNDockerImageErr = ensureOpenVPNDockerImage(dockerClient)
})
require.NoError(t, openVPNDockerImageErr)
verifyOpenVPNDockerImage(t, dockerClient)
return dockerClient
}
func openVPNDockerClientForTest(t *testing.T) *client.Client {
t.Helper()
clientOptions := []client.Opt{client.WithAPIVersionNegotiation()}
dockerHost := os.Getenv("DOCKER_HOST")
switch {
case dockerHost != "":
clientOptions = append(clientOptions, client.WithHost(dockerHost))
case openVPNFileExists("/Users/sekai/.orbstack/run/docker.sock"):
clientOptions = append(clientOptions, client.WithHost("unix:///Users/sekai/.orbstack/run/docker.sock"))
case openVPNFileExists("/var/run/docker.sock"):
clientOptions = append(clientOptions, client.WithHost("unix:///var/run/docker.sock"))
default:
t.Skip("Docker is unavailable: docker socket not found")
}
dockerClient, err := client.NewClientWithOpts(clientOptions...)
require.NoError(t, err)
t.Cleanup(func() {
dockerClient.Close()
})
return dockerClient
}
func openVPNFileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func ensureOpenVPNDockerImage(dockerClient *client.Client) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, _, err := dockerClient.ImageInspectWithRaw(ctx, openVPNDockerImage)
if err == nil {
return nil
}
if !errdefs.IsNotFound(err) {
return err
}
return buildOpenVPNDockerImage(dockerClient)
}
func buildOpenVPNDockerImage(dockerClient *client.Client) error {
dockerfile := strings.Join([]string{
"FROM debian:bookworm-slim",
"ARG OPENVPN_VERSION=" + openVPNDockerPackageVersion,
"RUN apt-get update \\",
" && apt-get install -y --no-install-recommends \\",
" bash \\",
" ca-certificates \\",
" grep \\",
" iproute2 \\",
" iputils-ping \\",
" openvpn=${OPENVPN_VERSION} \\",
" procps \\",
" python3 \\",
" tcpdump \\",
" && rm -rf /var/lib/apt/lists/*",
"RUN openvpn --version | head -n 1 | grep 'OpenVPN 2.6.14'",
"",
}, "\n")
var buffer bytes.Buffer
tarWriter := tar.NewWriter(&buffer)
header := &tar.Header{
Name: "Dockerfile",
Mode: 0o644,
Size: int64(len(dockerfile)),
}
err := tarWriter.WriteHeader(header)
if err == nil {
_, err = tarWriter.Write([]byte(dockerfile))
}
closeErr := tarWriter.Close()
if err != nil {
return err
}
if closeErr != nil {
return closeErr
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
buildOptions := typesapi.ImageBuildOptions{
Tags: []string{openVPNDockerImage},
Remove: true,
PullParent: true,
Platform: openVPNDockerPlatform(),
}
response, err := dockerClient.ImageBuild(ctx, bytes.NewReader(buffer.Bytes()), buildOptions)
if err != nil {
return err
}
defer response.Body.Close()
var output bytes.Buffer
err = jsonmessage.DisplayJSONMessagesStream(response.Body, &output, 0, false, nil)
if err != nil {
return E.Cause(err, "build OpenVPN docker image\n", output.String())
}
return nil
}
func verifyOpenVPNDockerImage(t *testing.T, dockerClient *client.Client) {
t.Helper()
containerConfig := &containerapi.Config{
Image: openVPNDockerImage,
Cmd: []string{"openvpn", "--version"},
}
createdContainer, err := dockerClient.ContainerCreate(context.Background(), containerConfig, &containerapi.HostConfig{}, nil, openVPNDockerOCIPlatform(), "")
require.NoError(t, err)
t.Cleanup(func() {
_ = removeOpenVPNDockerContainer(context.Background(), dockerClient, createdContainer.ID)
})
err = dockerClient.ContainerStart(context.Background(), createdContainer.ID, containerapi.StartOptions{})
require.NoError(t, err)
containerHandle := &openVPNDockerContainer{
dockerClient: dockerClient,
containerID: createdContainer.ID,
name: "openvpn-version",
}
waitResult := containerHandle.Wait(t, 30*time.Second)
require.Equal(t, int64(0), waitResult.exitCode, waitResult.logs)
firstLine, _, _ := strings.Cut(waitResult.logs, "\n")
require.Contains(t, firstLine, "OpenVPN 2.6.14")
}
func startOpenVPNDockerContainer(t *testing.T, dockerClient *client.Client, nameSuffix string, workspace string, command string) *openVPNDockerContainer {
t.Helper()
name := "sing-box-openvpn-" + nameSuffix + "-" + sanitizeOpenVPNDockerName(t.Name())
containerConfig := &containerapi.Config{
Image: openVPNDockerImage,
Cmd: []string{"bash", "-lc", command},
}
hostConfig := &containerapi.HostConfig{
NetworkMode: containerapi.NetworkMode("host"),
Binds: []string{workspace + ":" + openVPNDockerRoot},
CapAdd: []string{"NET_ADMIN"},
Resources: containerapi.Resources{
Devices: []containerapi.DeviceMapping{
{
PathOnHost: "/dev/net/tun",
PathInContainer: "/dev/net/tun",
CgroupPermissions: "rwm",
},
},
},
}
createdContainer, err := dockerClient.ContainerCreate(context.Background(), containerConfig, hostConfig, nil, openVPNDockerOCIPlatform(), name)
require.NoError(t, err)
containerHandle := &openVPNDockerContainer{
dockerClient: dockerClient,
containerID: createdContainer.ID,
name: name,
}
t.Cleanup(func() {
_ = removeOpenVPNDockerContainer(context.Background(), dockerClient, createdContainer.ID)
})
err = dockerClient.ContainerStart(context.Background(), createdContainer.ID, containerapi.StartOptions{})
require.NoError(t, err)
return containerHandle
}
func waitForOpenVPNDockerFile(t *testing.T, containerHandle *openVPNDockerContainer, path string, content string, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
fileContent, err := os.ReadFile(path)
if err == nil && strings.Contains(string(fileContent), content) {
return
}
containerHandle.failIfExited(t)
time.Sleep(100 * time.Millisecond)
}
fileContent, _ := os.ReadFile(path)
t.Fatalf("timed out waiting for %q in %s\nfile:\n%s\ncontainer logs:\n%s", content, path, string(fileContent), containerHandle.Logs(context.Background()))
}
func dumpOpenVPNDockerLogsOnFailure(t *testing.T, containerHandle *openVPNDockerContainer, workspace string) {
t.Helper()
t.Cleanup(func() {
if !t.Failed() {
return
}
t.Logf("container logs for %s:\n%s", containerHandle.name, containerHandle.Logs(context.Background()))
openVPNLog, err := os.ReadFile(filepath.Join(workspace, "openvpn.log"))
if err == nil {
t.Logf("openvpn log for %s:\n%s", containerHandle.name, string(openVPNLog))
}
})
}
func (c *openVPNDockerContainer) failIfExited(t *testing.T) {
t.Helper()
containerInfo, err := c.dockerClient.ContainerInspect(context.Background(), c.containerID)
require.NoError(t, err)
if containerInfo.State != nil && !containerInfo.State.Running {
t.Fatalf("docker container %s exited with code %d\nlogs:\n%s", c.name, containerInfo.State.ExitCode, c.Logs(context.Background()))
}
}
func (c *openVPNDockerContainer) Wait(t *testing.T, timeout time.Duration) openVPNDockerWaitResult {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
statusChannel, errChannel := c.dockerClient.ContainerWait(ctx, c.containerID, containerapi.WaitConditionNotRunning)
select {
case waitErr := <-errChannel:
if waitErr != nil {
t.Fatalf("wait docker container %s: %v\nlogs:\n%s", c.name, waitErr, c.Logs(context.Background()))
}
case status := <-statusChannel:
return openVPNDockerWaitResult{
exitCode: status.StatusCode,
logs: c.Logs(context.Background()),
}
case <-ctx.Done():
t.Fatalf("docker container %s timed out\nlogs:\n%s", c.name, c.Logs(context.Background()))
}
return openVPNDockerWaitResult{}
}
func (c *openVPNDockerContainer) Logs(ctx context.Context) string {
logReader, err := c.dockerClient.ContainerLogs(ctx, c.containerID, containerapi.LogsOptions{
ShowStdout: true,
ShowStderr: true,
})
if err != nil {
return "read logs: " + err.Error()
}
defer logReader.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
_, err = stdcopy.StdCopy(&stdout, &stderr, logReader)
if err != nil {
return "decode logs: " + err.Error()
}
if stderr.Len() == 0 {
return stdout.String()
}
if stdout.Len() == 0 {
return stderr.String()
}
return stdout.String() + "\nSTDERR:\n" + stderr.String()
}
func removeOpenVPNDockerContainer(ctx context.Context, dockerClient *client.Client, containerID string) error {
err := dockerClient.ContainerRemove(ctx, containerID, containerapi.RemoveOptions{
Force: true,
RemoveVolumes: true,
})
if err != nil && !errdefs.IsNotFound(err) {
return err
}
return nil
}
func openVPNDockerPlatform() string {
switch runtime.GOARCH {
case "arm64":
return "linux/arm64"
case "amd64":
return "linux/amd64"
default:
return ""
}
}
func openVPNDockerOCIPlatform() *ocispec.Platform {
switch runtime.GOARCH {
case "arm64", "amd64":
return &ocispec.Platform{
OS: "linux",
Architecture: runtime.GOARCH,
}
default:
return nil
}
}
func sanitizeOpenVPNDockerName(name string) string {
replacer := strings.NewReplacer("/", "-", "_", "-", " ", "-")
return replacer.Replace(name)
}
const openVPNDockerEchoServerScript = `#!/usr/bin/env python3
import os
import socket
import threading
import time
HOST = "{{HOST}}"
PORT = {{PORT}}
READY = "/config/echo.ready"
def bind_with_retry(sock, address):
last_error = None
for _ in range(300):
try:
sock.bind(address)
return
except OSError as error:
last_error = error
time.sleep(0.1)
raise last_error
def handle_tcp(conn):
with conn:
while True:
data = conn.recv(65536)
if not data:
return
conn.sendall(data)
def tcp_server():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
bind_with_retry(sock, (HOST, PORT))
sock.listen(64)
print("tcp echo ready", flush=True)
while True:
conn, _ = sock.accept()
threading.Thread(target=handle_tcp, args=(conn,), daemon=True).start()
def udp_server():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
bind_with_retry(sock, (HOST, PORT))
print("udp echo ready", flush=True)
while True:
data, address = sock.recvfrom(65536)
sock.sendto(data, address)
threading.Thread(target=tcp_server, daemon=True).start()
threading.Thread(target=udp_server, daemon=True).start()
time.sleep(0.2)
with open(READY, "w", encoding="utf-8") as ready_file:
ready_file.write("ready\n")
while True:
time.sleep(3600)
`
const openVPNDockerEchoClientScript = `#!/usr/bin/env python3
import os
import socket
HOST = "{{HOST}}"
PORT = {{PORT}}
def check_tcp():
sock = socket.create_connection((HOST, PORT), timeout=10)
sock.settimeout(10)
with sock:
payload = b"ping"
sock.sendall(payload)
response = sock.recv(len(payload))
if response != payload:
raise RuntimeError("unexpected tcp ping response")
for index in range(100):
payload = bytes([index]) + os.urandom(64 * 1024 - 1)
sock.sendall(payload)
response = bytearray()
while len(response) < len(payload):
chunk = sock.recv(len(payload) - len(response))
if not chunk:
raise RuntimeError("tcp echo closed")
response.extend(chunk)
if bytes(response) != payload:
raise RuntimeError("unexpected tcp large response")
def check_udp():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(10)
sock.connect((HOST, PORT))
with sock:
payload = b"ping"
sock.send(payload)
response = sock.recv(65536)
if response != payload:
raise RuntimeError("unexpected udp ping response")
for index in range(50):
payload = bytes([index]) + os.urandom(1499)
sock.send(payload)
response = sock.recv(65536)
if response != payload:
raise RuntimeError("unexpected udp large response")
check_tcp()
check_udp()
print("echo client ok", flush=True)
`
func testSuitOpenVPN(t *testing.T, proxyPort uint16, echoPort uint16, tunnelAddress string) {
t.Helper()
dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", proxyPort), socks.Version5, "", "")
destination := M.ParseSocksaddrHostPort(tunnelAddress, echoPort)
dialTCP := func() (net.Conn, error) {
return dialer.DialContext(context.Background(), N.NetworkTCP, destination)
}
dialUDP := func() (net.PacketConn, error) {
conn, err := dialer.DialContext(context.Background(), N.NetworkUDP, destination)
if err != nil {
return nil, err
}
return bufio.NewUnbindPacketConn(conn), nil
}
require.NoError(t, testOpenVPNEchoWithConn(echoPort, dialTCP))
require.NoError(t, testOpenVPNEchoWithPacketConn(echoPort, dialUDP))
require.NoError(t, testOpenVPNLargeDataWithConn(echoPort, dialTCP))
}
func testOpenVPNEchoWithConn(port uint16, dialTCP func() (net.Conn, error)) error {
listener, err := listen("tcp", ":"+strconv.Itoa(int(port)))
if err != nil {
return err
}
defer listener.Close()
serverErrCh := make(chan error, 1)
go func() {
serverConn, acceptErr := listener.Accept()
if acceptErr != nil {
serverErrCh <- acceptErr
return
}
defer serverConn.Close()
deadlineErr := serverConn.SetDeadline(time.Now().Add(openVPNLargeDataTimeout))
if deadlineErr != nil {
serverErrCh <- deadlineErr
return
}
buffer := make([]byte, 4)
_, readErr := io.ReadFull(serverConn, buffer)
if readErr != nil {
serverErrCh <- readErr
return
}
_, writeErr := serverConn.Write(buffer)
if writeErr != nil {
serverErrCh <- writeErr
return
}
serverErrCh <- nil
}()
conn, err := dialTCP()
if err != nil {
return err
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(openVPNLargeDataTimeout))
if err != nil {
return err
}
err = writeAndReadEcho(conn, []byte("ping"))
if err != nil {
return err
}
return <-serverErrCh
}
func testOpenVPNEchoWithPacketConn(port uint16, listenUDP func() (net.PacketConn, error)) error {
listener, err := listenPacket("udp", ":"+strconv.Itoa(int(port)))
if err != nil {
return err
}
defer listener.Close()
serverErrCh := make(chan error, 1)
go func() {
buffer := make([]byte, 1024)
readCount, address, readErr := listener.ReadFrom(buffer)
if readErr != nil {
serverErrCh <- readErr
return
}
_, writeErr := listener.WriteTo(buffer[:readCount], address)
if writeErr != nil {
serverErrCh <- writeErr
return
}
serverErrCh <- nil
}()
packetConn, err := listenUDP()
if err != nil {
return err
}
defer packetConn.Close()
remoteAddress := &net.UDPAddr{IP: localIP.AsSlice(), Port: int(port)}
payload := []byte("ping")
_, err = packetConn.WriteTo(payload, remoteAddress)
if err != nil {
return err
}
response := make([]byte, 1024)
readCount, err := readOpenVPNPacketWithTimeout(packetConn, response)
if err != nil {
return err
}
if !bytes.Equal(response[:readCount], payload) {
return E.New("unexpected UDP echo response")
}
return <-serverErrCh
}
func testOpenVPNLargeDataWithConn(port uint16, dialTCP func() (net.Conn, error)) error {
listener, err := listen("tcp", ":"+strconv.Itoa(int(port)))
if err != nil {
return err
}
defer listener.Close()
serverErrCh := make(chan error, 1)
go func() {
serverConn, acceptErr := listener.Accept()
if acceptErr != nil {
serverErrCh <- acceptErr
return
}
defer serverConn.Close()
deadlineErr := serverConn.SetDeadline(time.Now().Add(openVPNLargeDataTimeout))
if deadlineErr != nil {
serverErrCh <- deadlineErr
return
}
buffer := make([]byte, openVPNLargeTCPSize)
for range openVPNLargeTCPPackets {
_, readErr := io.ReadFull(serverConn, buffer)
if readErr != nil {
serverErrCh <- readErr
return
}
_, writeErr := serverConn.Write(buffer)
if writeErr != nil {
serverErrCh <- writeErr
return
}
}
serverErrCh <- nil
}()
conn, err := dialTCP()
if err != nil {
return err
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(openVPNLargeDataTimeout))
if err != nil {
return err
}
response := make([]byte, openVPNLargeTCPSize)
for index := range openVPNLargeTCPPackets {
payload := make([]byte, openVPNLargeTCPSize)
payload[0] = byte(index)
_, err = rand.Read(payload[1:])
if err != nil {
return err
}
_, err = conn.Write(payload)
if err != nil {
return err
}
_, err = io.ReadFull(conn, response)
if err != nil {
return err
}
if !bytes.Equal(response, payload) {
return E.New("unexpected tcp large response")
}
}
return <-serverErrCh
}
type openVPNPacketReadResult struct {
readCount int
err error
}
func readOpenVPNPacketWithTimeout(packetConn net.PacketConn, buffer []byte) (int, error) {
resultCh := make(chan openVPNPacketReadResult, 1)
go func() {
readCount, _, readErr := packetConn.ReadFrom(buffer)
resultCh <- openVPNPacketReadResult{
readCount: readCount,
err: readErr,
}
}()
select {
case result := <-resultCh:
return result.readCount, result.err
case <-time.After(openVPNLargeDataTimeout):
return 0, E.New("timeout")
}
}
func createOpenVPNCertificateBundle(t *testing.T) openVPNCertificateBundle {
return createOpenVPNCertificateBundleWithServerKeyUsage(t, x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature)
}
func createOpenVPNCertificateBundleWithServerKeyUsage(t *testing.T, serverKeyUsage x509.KeyUsage) openVPNCertificateBundle {
t.Helper()
tempDir := t.TempDir()
caKey, err := rsa.GenerateKey(rand.Reader, 3072)
require.NoError(t, err)
spkiASN1, err := x509.MarshalPKIXPublicKey(caKey.Public())
require.NoError(t, err)
var spki struct {
Algorithm pkix.AlgorithmIdentifier
SubjectPublicKey asn1.BitString
}
_, err = asn1.Unmarshal(spkiASN1, &spki)
require.NoError(t, err)
subjectKeyID := sha1.Sum(spki.SubjectPublicKey.Bytes)
caTemplate := &x509.Certificate{
SerialNumber: randomSerialNumber(t),
Subject: pkix.Name{
Organization: []string{"sing-box OpenVPN test CA"},
CommonName: "sing-box OpenVPN test CA",
},
SubjectKeyId: subjectKeyID[:],
NotAfter: time.Now().AddDate(1, 0, 0),
NotBefore: time.Now().Add(-time.Minute),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLenZero: true,
}
caCertificate, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, caKey.Public(), caKey)
require.NoError(t, err)
caPath := filepath.Join(tempDir, "ca.crt")
writePEMFile(t, caPath, "CERTIFICATE", caCertificate)
serverCertPath, serverKeyPath := createOpenVPNLeafCertificate(t, tempDir, "server", serverKeyUsage, x509.ExtKeyUsageServerAuth, caTemplate, caKey)
clientCertPath, clientKeyPath := createOpenVPNLeafCertificate(t, tempDir, "client", x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature, x509.ExtKeyUsageClientAuth, caTemplate, caKey)
return openVPNCertificateBundle{
caPath: caPath,
serverCertPath: serverCertPath,
serverKeyPath: serverKeyPath,
clientCertPath: clientCertPath,
clientKeyPath: clientKeyPath,
}
}
func createOpenVPNLeafCertificate(t *testing.T, tempDir string, commonName string, keyUsage x509.KeyUsage, extendedKeyUsage x509.ExtKeyUsage, caTemplate *x509.Certificate, caKey *rsa.PrivateKey) (string, string) {
t.Helper()
leafKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
leafTemplate := &x509.Certificate{
SerialNumber: randomSerialNumber(t),
Subject: pkix.Name{
Organization: []string{"sing-box OpenVPN test"},
CommonName: commonName,
},
NotBefore: time.Now().Add(-time.Minute),
NotAfter: time.Now().AddDate(0, 1, 0),
KeyUsage: keyUsage,
ExtKeyUsage: []x509.ExtKeyUsage{
extendedKeyUsage,
},
}
if extendedKeyUsage == x509.ExtKeyUsageServerAuth {
leafTemplate.IPAddresses = append(leafTemplate.IPAddresses, net.ParseIP("127.0.0.1"))
leafTemplate.DNSNames = append(leafTemplate.DNSNames, "localhost")
}
leafCertificate, err := x509.CreateCertificate(rand.Reader, leafTemplate, caTemplate, leafKey.Public(), caKey)
require.NoError(t, err)
certPath := filepath.Join(tempDir, commonName+".crt")
keyPath := filepath.Join(tempDir, commonName+".key")
writePEMFile(t, certPath, "CERTIFICATE", leafCertificate)
privateKey, err := x509.MarshalPKCS8PrivateKey(leafKey)
require.NoError(t, err)
writePEMFile(t, keyPath, "PRIVATE KEY", privateKey)
return certPath, keyPath
}
func writePEMFile(t *testing.T, path string, blockType string, bytes []byte) {
t.Helper()
content := pem.EncodeToMemory(&pem.Block{
Type: blockType,
Bytes: bytes,
})
err := os.WriteFile(path, content, 0o600)
require.NoError(t, err)
}
func createOpenVPNStaticKey(t *testing.T) string {
t.Helper()
keyMaterial := make([]byte, 256)
_, err := rand.Read(keyMaterial)
require.NoError(t, err)
hexKey := hex.EncodeToString(keyMaterial)
lines := []string{"-----BEGIN OpenVPN Static key V1-----"}
for index := 0; index < len(hexKey); index += 32 {
lines = append(lines, hexKey[index:index+32])
}
lines = append(lines, "-----END OpenVPN Static key V1-----", "")
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 {
return reserveOpenVPNTCPPort(t)
}
return reserveOpenVPNUDPPort(t)
}
func reserveOpenVPNTCPPort(t *testing.T) uint16 {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer listener.Close()
tcpAddress := listener.Addr().(*net.TCPAddr)
return uint16(tcpAddress.Port)
}
func reserveOpenVPNUDPPort(t *testing.T) uint16 {
t.Helper()
listener, err := net.ListenPacket("udp", "127.0.0.1:0")
require.NoError(t, err)
defer listener.Close()
udpAddress := listener.LocalAddr().(*net.UDPAddr)
return uint16(udpAddress.Port)
}
func reserveOpenVPNEchoPort(t *testing.T) uint16 {
t.Helper()
for i := 0; i < 20; i++ {
tcpListener, err := net.Listen("tcp", ":0")
require.NoError(t, err)
tcpAddress := tcpListener.Addr().(*net.TCPAddr)
port := uint16(tcpAddress.Port)
udpListener, err := net.ListenPacket("udp", ":"+strconv.Itoa(int(port)))
if err == nil {
udpListener.Close()
tcpListener.Close()
return port
}
tcpListener.Close()
}
t.Fatal("reserve TCP and UDP echo port")
return 0
}