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 }