mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-08-04 14:36:07 +00:00
618 lines
20 KiB
Go
618 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"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"
|
|
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/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
openConnectInteropEnvironment = "OPENCONNECT_IT"
|
|
openConnectOcservVersion = "1.3.0-2"
|
|
openConnectOcservImage = "sing-box-openconnect-ocserv:" + openConnectOcservVersion
|
|
openConnectUsername = "test"
|
|
openConnectPassword = "test"
|
|
openConnectTunnelAddress = "192.168.77.1"
|
|
openConnectEchoPort = 18080
|
|
)
|
|
|
|
const openConnectOcservPasswordFile = "test:tost,group1,group2:$5$i6SNmLDCgBNjyJ7q$SZ4bVJb7I/DLgXo3txHBVohRFBjOtdbxGQZp.DOnrA.\n"
|
|
|
|
const openConnectOcservConfiguration = `auth = "plain[passwd=/fixture/ocpasswd]"
|
|
|
|
tcp-port = 443
|
|
udp-port = 443
|
|
|
|
run-as-user = nobody
|
|
run-as-group = nogroup
|
|
socket-file = /run/ocserv-socket
|
|
use-occtl = true
|
|
occtl-socket-file = /run/occtl.socket
|
|
|
|
server-cert = /fixture/server-cert.pem
|
|
server-key = /fixture/server-key.pem
|
|
tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT"
|
|
|
|
isolate-workers = false
|
|
max-clients = 4
|
|
max-same-clients = 2
|
|
rate-limit-ms = 0
|
|
max-ban-score = 0
|
|
auth-timeout = 30
|
|
cookie-timeout = 300
|
|
keepalive = 1
|
|
dpd = 2
|
|
try-mtu-discovery = false
|
|
|
|
device = vpns
|
|
ipv4-network = 192.168.77.0
|
|
ipv4-netmask = 255.255.255.0
|
|
route = 192.168.77.0/255.255.255.0
|
|
ping-leases = false
|
|
mtu = 1400
|
|
|
|
cisco-client-compat = false
|
|
dtls-psk = true
|
|
dtls-legacy = false
|
|
match-tls-dtls-ciphers = false
|
|
rekey-time = 0
|
|
rekey-method = new-tunnel
|
|
`
|
|
|
|
type openConnectOcservContainer struct {
|
|
name string
|
|
tcpAddress string
|
|
serverAddress string
|
|
certificateAuthorityPath string
|
|
passwordPath string
|
|
}
|
|
|
|
type openConnectTCPProxy struct {
|
|
listener net.Listener
|
|
target string
|
|
access sync.Mutex
|
|
connections map[*openConnectTCPProxyConnection]struct{}
|
|
closed bool
|
|
accepted atomic.Uint64
|
|
}
|
|
|
|
type openConnectTCPProxyConnection struct {
|
|
proxy *openConnectTCPProxy
|
|
downstream net.Conn
|
|
upstream net.Conn
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
func TestOpenConnectDockerInterop(t *testing.T) {
|
|
if testing.Short() || strings.TrimSpace(os.Getenv(openConnectInteropEnvironment)) == "" {
|
|
t.Skip(openConnectInteropEnvironment + " is not set or short testing is enabled")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
t.Cleanup(cancel)
|
|
requireOpenConnectDockerImage(t, ctx)
|
|
|
|
t.Run("prefilled_credentials_and_tcp_echo", func(subtest *testing.T) {
|
|
container := startOpenConnectOcservContainer(subtest, ctx)
|
|
instance := startInstance(subtest, openConnectInstanceOptions(
|
|
container.serverAddress,
|
|
container.certificateAuthorityPath,
|
|
openConnectUsername,
|
|
openConnectPassword,
|
|
))
|
|
endpoint := requireOpenConnectEndpoint(subtest, instance)
|
|
status := waitForOpenConnectState(subtest, endpoint, adapter.OpenConnectStateConnected, 45*time.Second)
|
|
require.Nil(subtest, status.AuthChallenge)
|
|
err := exchangeOpenConnectTCPEcho(endpoint, 256*1024, 30*time.Second)
|
|
require.NoError(subtest, err)
|
|
err = exchangeOpenConnectUDPEcho(endpoint, 1400, 30*time.Second)
|
|
require.NoError(subtest, err)
|
|
})
|
|
|
|
t.Run("interactive_password_auth", func(subtest *testing.T) {
|
|
container := startOpenConnectOcservContainer(subtest, ctx)
|
|
instance := startInstance(subtest, openConnectInstanceOptions(
|
|
container.serverAddress,
|
|
container.certificateAuthorityPath,
|
|
"",
|
|
"",
|
|
))
|
|
endpoint := requireOpenConnectEndpoint(subtest, instance)
|
|
driveOpenConnectInteractiveAuthentication(subtest, endpoint, 45*time.Second)
|
|
err := exchangeOpenConnectTCPEcho(endpoint, 64*1024, 30*time.Second)
|
|
require.NoError(subtest, err)
|
|
})
|
|
|
|
t.Run("cstp_reconnect_reuses_cookie", func(subtest *testing.T) {
|
|
container := startOpenConnectOcservContainer(subtest, ctx)
|
|
proxy := startOpenConnectTCPProxy(subtest, container.tcpAddress)
|
|
instance := startInstance(subtest, openConnectInstanceOptions(
|
|
openConnectLocalhostAddress(subtest, proxy.listener.Addr().String()),
|
|
container.certificateAuthorityPath,
|
|
openConnectUsername,
|
|
openConnectPassword,
|
|
))
|
|
endpoint := requireOpenConnectEndpoint(subtest, instance)
|
|
waitForOpenConnectState(subtest, endpoint, adapter.OpenConnectStateConnected, 45*time.Second)
|
|
waitForOpenConnectTCPEcho(subtest, endpoint, 30*time.Second)
|
|
|
|
acceptedBeforeDrop := proxy.accepted.Load()
|
|
err := os.WriteFile(container.passwordPath, []byte("test:tost,group1,group2:!\n"), 0o644)
|
|
require.NoError(subtest, err)
|
|
droppedConnections := proxy.dropConnections()
|
|
require.Positive(subtest, droppedConnections)
|
|
waitForOpenConnectProxyAccept(subtest, proxy, acceptedBeforeDrop, 30*time.Second)
|
|
waitForOpenConnectTCPEcho(subtest, endpoint, 60*time.Second)
|
|
|
|
status := endpoint.OpenConnectStatus()
|
|
require.Equal(subtest, adapter.OpenConnectStateConnected, status.State, status.Error)
|
|
require.Nil(subtest, status.AuthChallenge)
|
|
logs, err := openConnectDockerOutput(ctx, "logs", container.name)
|
|
require.NoError(subtest, err)
|
|
require.GreaterOrEqual(subtest, strings.Count(logs, "HTTP CONNECT /CSCOSSLC/tunnel"), 2, logs)
|
|
require.Equal(subtest, 1, strings.Count(logs, "user '"+openConnectUsername+"' obtained cookie"), logs)
|
|
})
|
|
}
|
|
|
|
func openConnectInstanceOptions(server string, certificateAuthorityPath string, username string, password string) option.Options {
|
|
endpointOptions := option.OpenConnectEndpointOptions{
|
|
Server: server,
|
|
Flavor: "anyconnect",
|
|
Username: username,
|
|
Password: password,
|
|
NoUDP: true,
|
|
UDPTimeout: badoption.Duration(time.Minute),
|
|
UDPMapping: option.UDPNATBehaviorAddressDependent,
|
|
UDPFiltering: option.UDPNATBehaviorAddressAndPortDependent,
|
|
UDPNATMax: 128,
|
|
TLS: option.OpenConnectTLSOptions{
|
|
CertificateAuthorityPath: certificateAuthorityPath,
|
|
},
|
|
}
|
|
return option.Options{
|
|
Endpoints: []option.Endpoint{
|
|
{
|
|
Type: C.TypeOpenConnect,
|
|
Tag: "openconnect-client",
|
|
Options: &endpointOptions,
|
|
},
|
|
},
|
|
Outbounds: []option.Outbound{
|
|
{
|
|
Type: C.TypeDirect,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func requireOpenConnectEndpoint(t *testing.T, instance *box.Box) adapter.OpenConnectEndpoint {
|
|
t.Helper()
|
|
endpoint, loaded := instance.Endpoint().Get("openconnect-client")
|
|
require.True(t, loaded)
|
|
openConnectEndpoint, supported := endpoint.(adapter.OpenConnectEndpoint)
|
|
require.True(t, supported)
|
|
return openConnectEndpoint
|
|
}
|
|
|
|
func waitForOpenConnectState(t *testing.T, endpoint adapter.OpenConnectEndpoint, expectedState string, timeout time.Duration) adapter.OpenConnectStatus {
|
|
t.Helper()
|
|
timeoutTimer := time.NewTimer(timeout)
|
|
defer timeoutTimer.Stop()
|
|
for {
|
|
statusUpdated := endpoint.StatusUpdated()
|
|
status := endpoint.OpenConnectStatus()
|
|
if status.State == expectedState {
|
|
return status
|
|
}
|
|
if status.State == adapter.OpenConnectStateError {
|
|
t.Fatalf("OpenConnect endpoint failed while waiting for %q: %s", expectedState, status.Error)
|
|
}
|
|
select {
|
|
case <-statusUpdated:
|
|
case <-timeoutTimer.C:
|
|
t.Fatalf("timed out waiting for OpenConnect state %q; last state %q, error %q", expectedState, status.State, status.Error)
|
|
}
|
|
}
|
|
}
|
|
|
|
func driveOpenConnectInteractiveAuthentication(t *testing.T, endpoint adapter.OpenConnectEndpoint, timeout time.Duration) {
|
|
t.Helper()
|
|
timeoutTimer := time.NewTimer(timeout)
|
|
defer timeoutTimer.Stop()
|
|
completedForms := make(map[string]struct{})
|
|
sawUsername := false
|
|
sawPassword := false
|
|
for {
|
|
statusUpdated := endpoint.StatusUpdated()
|
|
status := endpoint.OpenConnectStatus()
|
|
switch status.State {
|
|
case adapter.OpenConnectStateConnected:
|
|
require.True(t, sawUsername)
|
|
require.True(t, sawPassword)
|
|
return
|
|
case adapter.OpenConnectStateError:
|
|
t.Fatal(status.Error)
|
|
case adapter.OpenConnectStateAuthPending:
|
|
challenge := status.AuthChallenge
|
|
require.NotNil(t, challenge)
|
|
require.NotNil(t, challenge.Form)
|
|
require.Nil(t, challenge.Browser)
|
|
require.NotEmpty(t, challenge.ID)
|
|
_, completed := completedForms[challenge.ID]
|
|
if !completed {
|
|
values := make(map[string]string, len(challenge.Form.Fields))
|
|
for _, field := range challenge.Form.Fields {
|
|
require.NotEmpty(t, field.SubmissionKey)
|
|
switch field.Name {
|
|
case "username":
|
|
sawUsername = true
|
|
values[field.SubmissionKey] = openConnectUsername
|
|
case "password":
|
|
sawPassword = true
|
|
values[field.SubmissionKey] = openConnectPassword
|
|
default:
|
|
t.Fatalf("unexpected ocserv authentication field: %#v", field)
|
|
}
|
|
}
|
|
require.NotEmpty(t, values)
|
|
err := endpoint.CompleteAuthChallenge(challenge.ID, adapter.OpenConnectAuthResponse{
|
|
Form: &adapter.OpenConnectAuthFormResponse{Values: values},
|
|
})
|
|
require.NoError(t, err)
|
|
completedForms[challenge.ID] = struct{}{}
|
|
continue
|
|
}
|
|
}
|
|
select {
|
|
case <-statusUpdated:
|
|
case <-timeoutTimer.C:
|
|
t.Fatalf("timed out driving OpenConnect authentication; last state %q, error %q", status.State, status.Error)
|
|
}
|
|
}
|
|
}
|
|
|
|
func exchangeOpenConnectTCPEcho(endpoint adapter.OpenConnectEndpoint, payloadSize int, timeout time.Duration) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
conn, err := endpoint.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddrHostPort(openConnectTunnelAddress, openConnectEchoPort))
|
|
if err != nil {
|
|
return E.Cause(err, "dial ocserv tunnel echo")
|
|
}
|
|
defer conn.Close()
|
|
err = conn.SetDeadline(time.Now().Add(timeout))
|
|
if err != nil {
|
|
return E.Cause(err, "set ocserv tunnel echo deadline")
|
|
}
|
|
payload := make([]byte, payloadSize)
|
|
_, err = rand.Read(payload)
|
|
if err != nil {
|
|
return E.Cause(err, "generate ocserv tunnel echo payload")
|
|
}
|
|
written := 0
|
|
for written < len(payload) {
|
|
var n int
|
|
n, err = conn.Write(payload[written:])
|
|
if err != nil {
|
|
return E.Cause(err, "write ocserv tunnel echo payload")
|
|
}
|
|
written += n
|
|
}
|
|
response := make([]byte, len(payload))
|
|
_, err = io.ReadFull(conn, response)
|
|
if err != nil {
|
|
return E.Cause(err, "read ocserv tunnel echo payload")
|
|
}
|
|
if !bytes.Equal(response, payload) {
|
|
return E.New("ocserv tunnel echo payload mismatch")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func exchangeOpenConnectUDPEcho(endpoint adapter.OpenConnectEndpoint, payloadSize int, timeout time.Duration) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
conn, err := endpoint.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddrHostPort(openConnectTunnelAddress, openConnectEchoPort))
|
|
if err != nil {
|
|
return E.Cause(err, "dial ocserv tunnel UDP echo")
|
|
}
|
|
defer conn.Close()
|
|
err = conn.SetDeadline(time.Now().Add(timeout))
|
|
if err != nil {
|
|
return E.Cause(err, "set ocserv tunnel UDP echo deadline")
|
|
}
|
|
payload := make([]byte, payloadSize)
|
|
_, err = rand.Read(payload)
|
|
if err != nil {
|
|
return E.Cause(err, "generate ocserv tunnel UDP echo payload")
|
|
}
|
|
_, err = conn.Write(payload)
|
|
if err != nil {
|
|
return E.Cause(err, "write ocserv tunnel UDP echo payload")
|
|
}
|
|
response := make([]byte, payloadSize+1)
|
|
responseLength, err := conn.Read(response)
|
|
if err != nil {
|
|
return E.Cause(err, "read ocserv tunnel UDP echo payload")
|
|
}
|
|
if !bytes.Equal(response[:responseLength], payload) {
|
|
return E.New("ocserv tunnel UDP echo payload mismatch")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func waitForOpenConnectTCPEcho(t *testing.T, endpoint adapter.OpenConnectEndpoint, timeout time.Duration) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(timeout)
|
|
var lastErr error
|
|
for time.Now().Before(deadline) {
|
|
lastErr = exchangeOpenConnectTCPEcho(endpoint, 4096, 3*time.Second)
|
|
if lastErr == nil {
|
|
return
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
if lastErr == nil {
|
|
t.Fatal("timed out before attempting OpenConnect tunnel echo")
|
|
}
|
|
t.Fatal(E.Cause(lastErr, "timed out waiting for OpenConnect tunnel echo"))
|
|
}
|
|
|
|
func requireOpenConnectDockerImage(t *testing.T, ctx context.Context) {
|
|
t.Helper()
|
|
_, err := openConnectDockerOutput(ctx, "version", "--format", "{{.Server.Version}}")
|
|
require.NoError(t, err)
|
|
buildContext, err := filepath.Abs(filepath.Join("testdata", "openconnect", "ocserv"))
|
|
require.NoError(t, err)
|
|
_, err = openConnectDockerOutput(ctx, "build", "--pull=false", "--tag", openConnectOcservImage, buildContext)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func startOpenConnectOcservContainer(t *testing.T, ctx context.Context) openConnectOcservContainer {
|
|
t.Helper()
|
|
certificateAuthorityPath, certificatePath, keyPath := createSelfSignedCertificate(t, "localhost")
|
|
workspace := t.TempDir()
|
|
err := os.Chmod(workspace, 0o755)
|
|
require.NoError(t, err)
|
|
certificate, err := os.ReadFile(certificatePath)
|
|
require.NoError(t, err)
|
|
key, err := os.ReadFile(keyPath)
|
|
require.NoError(t, err)
|
|
serverCertificatePath := filepath.Join(workspace, "server-cert.pem")
|
|
serverKeyPath := filepath.Join(workspace, "server-key.pem")
|
|
passwordPath := filepath.Join(workspace, "ocpasswd")
|
|
err = os.WriteFile(serverCertificatePath, certificate, 0o644)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(serverKeyPath, key, 0o600)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(passwordPath, []byte(openConnectOcservPasswordFile), 0o644)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(filepath.Join(workspace, "ocserv.conf"), []byte(openConnectOcservConfiguration), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
containerName := "sing-box-openconnect-" + strconv.FormatInt(time.Now().UnixNano(), 10)
|
|
_, err = openConnectDockerOutput(
|
|
ctx,
|
|
"run", "--detach", "--rm", "--name", containerName,
|
|
"--cap-add", "NET_ADMIN", "--device", "/dev/net/tun",
|
|
"--publish", "127.0.0.1::443/tcp",
|
|
"--mount", "type=bind,source="+workspace+",target=/fixture",
|
|
"--entrypoint", "sh",
|
|
openConnectOcservImage,
|
|
"-c", "python3 /usr/local/bin/openconnect-echo-server & exec ocserv -f -d 4 -c /fixture/ocserv.conf",
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
if t.Failed() {
|
|
logsContext, cancelLogs := context.WithTimeout(context.Background(), 5*time.Second)
|
|
logs, logsErr := openConnectDockerOutput(logsContext, "logs", containerName)
|
|
cancelLogs()
|
|
if logsErr == nil {
|
|
t.Log("ocserv logs:\n" + logs)
|
|
}
|
|
}
|
|
removeContext, cancelRemove := context.WithTimeout(context.Background(), 5*time.Second)
|
|
_, _ = openConnectDockerOutput(removeContext, "rm", "--force", containerName)
|
|
cancelRemove()
|
|
})
|
|
waitForOpenConnectContainerLog(t, ctx, containerName, "openconnect echo ready")
|
|
tcpAddress := openConnectDockerPublishedAddress(t, ctx, containerName, "443/tcp")
|
|
waitForOpenConnectTCP(t, ctx, containerName, tcpAddress)
|
|
versionOutput, err := openConnectDockerOutput(ctx, "exec", containerName, "dpkg-query", "-W", "-f=${Version}", "ocserv")
|
|
require.NoError(t, err)
|
|
require.Equal(t, openConnectOcservVersion, strings.TrimSpace(versionOutput))
|
|
return openConnectOcservContainer{
|
|
name: containerName,
|
|
tcpAddress: tcpAddress,
|
|
serverAddress: openConnectLocalhostAddress(t, tcpAddress),
|
|
certificateAuthorityPath: certificateAuthorityPath,
|
|
passwordPath: passwordPath,
|
|
}
|
|
}
|
|
|
|
func openConnectDockerPublishedAddress(t *testing.T, ctx context.Context, containerName string, port string) string {
|
|
t.Helper()
|
|
for {
|
|
output, err := openConnectDockerOutput(ctx, "port", containerName, port)
|
|
if err == nil {
|
|
address := strings.TrimSpace(output)
|
|
_, _, splitErr := net.SplitHostPort(address)
|
|
if splitErr == nil {
|
|
return address
|
|
}
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal(E.Cause(ctx.Err(), "wait for Docker published address"))
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
|
|
func openConnectLocalhostAddress(t *testing.T, address string) string {
|
|
t.Helper()
|
|
_, port, err := net.SplitHostPort(address)
|
|
require.NoError(t, err)
|
|
return net.JoinHostPort("localhost", port)
|
|
}
|
|
|
|
func waitForOpenConnectContainerLog(t *testing.T, ctx context.Context, containerName string, expected string) {
|
|
t.Helper()
|
|
for {
|
|
logs, logsErr := openConnectDockerOutput(ctx, "logs", containerName)
|
|
if logsErr == nil && strings.Contains(logs, expected) {
|
|
return
|
|
}
|
|
running, inspectErr := openConnectDockerOutput(ctx, "inspect", "--format", "{{.State.Running}}", containerName)
|
|
if inspectErr == nil && strings.TrimSpace(running) != "true" {
|
|
t.Fatalf("ocserv container exited while waiting for %q:\n%s", expected, logs)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal(E.Cause(ctx.Err(), "wait for ocserv container log ", expected))
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
|
|
func waitForOpenConnectTCP(t *testing.T, ctx context.Context, containerName string, address string) {
|
|
t.Helper()
|
|
for {
|
|
conn, err := net.DialTimeout("tcp", address, 250*time.Millisecond)
|
|
if err == nil {
|
|
_ = conn.Close()
|
|
return
|
|
}
|
|
running, inspectErr := openConnectDockerOutput(ctx, "inspect", "--format", "{{.State.Running}}", containerName)
|
|
if inspectErr == nil && strings.TrimSpace(running) != "true" {
|
|
logs, _ := openConnectDockerOutput(ctx, "logs", containerName)
|
|
t.Fatalf("ocserv container exited before TCP readiness:\n%s", logs)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatal(E.Cause(ctx.Err(), "wait for ocserv TCP listener"))
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
|
|
func openConnectDockerOutput(ctx context.Context, arguments ...string) (string, error) {
|
|
command := exec.CommandContext(ctx, "docker", arguments...)
|
|
output, err := command.CombinedOutput()
|
|
if err != nil {
|
|
return "", E.Cause(err, "docker ", strings.Join(arguments, " "), ": ", strings.TrimSpace(string(output)))
|
|
}
|
|
return string(output), nil
|
|
}
|
|
|
|
func startOpenConnectTCPProxy(t *testing.T, target string) *openConnectTCPProxy {
|
|
t.Helper()
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
proxy := &openConnectTCPProxy{
|
|
listener: listener,
|
|
target: target,
|
|
connections: make(map[*openConnectTCPProxyConnection]struct{}),
|
|
}
|
|
go proxy.acceptLoop()
|
|
t.Cleanup(proxy.close)
|
|
return proxy
|
|
}
|
|
|
|
func (p *openConnectTCPProxy) acceptLoop() {
|
|
for {
|
|
downstream, err := p.listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
upstream, err := net.DialTimeout("tcp", p.target, 5*time.Second)
|
|
if err != nil {
|
|
_ = downstream.Close()
|
|
continue
|
|
}
|
|
connection := &openConnectTCPProxyConnection{
|
|
proxy: p,
|
|
downstream: downstream,
|
|
upstream: upstream,
|
|
}
|
|
p.access.Lock()
|
|
if p.closed {
|
|
p.access.Unlock()
|
|
connection.close()
|
|
return
|
|
}
|
|
p.connections[connection] = struct{}{}
|
|
p.accepted.Add(1)
|
|
p.access.Unlock()
|
|
go connection.copy(upstream, downstream)
|
|
go connection.copy(downstream, upstream)
|
|
}
|
|
}
|
|
|
|
func (c *openConnectTCPProxyConnection) copy(destination net.Conn, source net.Conn) {
|
|
_, _ = io.Copy(destination, source)
|
|
c.close()
|
|
}
|
|
|
|
func (c *openConnectTCPProxyConnection) close() {
|
|
c.closeOnce.Do(func() {
|
|
_ = c.downstream.Close()
|
|
_ = c.upstream.Close()
|
|
c.proxy.access.Lock()
|
|
delete(c.proxy.connections, c)
|
|
c.proxy.access.Unlock()
|
|
})
|
|
}
|
|
|
|
func (p *openConnectTCPProxy) dropConnections() int {
|
|
p.access.Lock()
|
|
connections := make([]*openConnectTCPProxyConnection, 0, len(p.connections))
|
|
for connection := range p.connections {
|
|
connections = append(connections, connection)
|
|
}
|
|
p.access.Unlock()
|
|
for _, connection := range connections {
|
|
connection.close()
|
|
}
|
|
return len(connections)
|
|
}
|
|
|
|
func (p *openConnectTCPProxy) close() {
|
|
p.access.Lock()
|
|
p.closed = true
|
|
p.access.Unlock()
|
|
_ = p.listener.Close()
|
|
p.dropConnections()
|
|
}
|
|
|
|
func waitForOpenConnectProxyAccept(t *testing.T, proxy *openConnectTCPProxy, previous uint64, timeout time.Duration) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
if proxy.accepted.Load() > previous {
|
|
return
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
t.Fatalf("OpenConnect proxy accepted %d connections, expected more than %d after CSTP drop", proxy.accepted.Load(), previous)
|
|
}
|