diff --git a/go.mod b/go.mod index b088520..740ca0d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.3 require ( github.com/go-gost/core v0.5.3 - github.com/go-gost/x v0.13.8 + github.com/go-gost/x v0.13.9 github.com/judwhite/go-svc v1.2.1 github.com/moby/moby/client v0.4.0 github.com/stretchr/testify v1.11.1 @@ -50,7 +50,7 @@ require ( github.com/go-gost/gosocks4 v0.1.0 // indirect github.com/go-gost/gosocks5 v0.5.0 // indirect github.com/go-gost/plugin v0.5.0 // indirect - github.com/go-gost/relay v0.6.1 // indirect + github.com/go-gost/relay v0.6.2 // indirect github.com/go-gost/tls-dissector v0.2.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/go.sum b/go.sum index 335591c..f764ee6 100644 --- a/go.sum +++ b/go.sum @@ -93,12 +93,12 @@ github.com/go-gost/gosocks5 v0.5.0 h1:YE37l1MJwde8diIQdynStqogMotG5enoTdborhA5yi github.com/go-gost/gosocks5 v0.5.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4= github.com/go-gost/plugin v0.5.0 h1:zK3Ezcv+z6bz6aUhaFZcZiJc42w44ohybzrj5tnWzcM= github.com/go-gost/plugin v0.5.0/go.mod h1:oN23l+yGDCIP9G3KnDl/I/0zVGOobZUDCB2Z5yYYXts= -github.com/go-gost/relay v0.6.1 h1:7SqnHFbY8x/DzvjpK03a5zcVH9+TbJAcnW/RT6s1ecc= -github.com/go-gost/relay v0.6.1/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI= +github.com/go-gost/relay v0.6.2 h1:0Rgm1TOZvrilAKj6zn+UMvAGWtS0H08e2wBX03Y6g+I= +github.com/go-gost/relay v0.6.2/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI= github.com/go-gost/tls-dissector v0.2.0 h1:9tE6WOzzpurATTBWn60DU4R8gibpGNY8/qVcc1SicVg= github.com/go-gost/tls-dissector v0.2.0/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs= -github.com/go-gost/x v0.13.8 h1:bde4+2/HdT85fhZqdIqx6lrSz4t8Xco+c+8blQ9w4VE= -github.com/go-gost/x v0.13.8/go.mod h1:P/V1MgP2hSrDLUhnLBxmL7ST+4Dk8EtoFlNc7YykEJo= +github.com/go-gost/x v0.13.9 h1:/pK8rOiueV239i0ntesDnU15UeYrq8vL6iVeGPJTzJo= +github.com/go-gost/x v0.13.9/go.mod h1:etkIOM5JjH9pnb3UOvLKexodznKcK3oRNGniPk0AGxk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/tests/e2e/pht_test.go b/tests/e2e/pht_test.go new file mode 100644 index 0000000..14d59ca --- /dev/null +++ b/tests/e2e/pht_test.go @@ -0,0 +1,157 @@ +package e2e + +import ( + "context" + "fmt" + "io" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go" +) + +type PHTSuite struct { + suite.Suite + ctx context.Context + echoC testcontainers.Container + echoIP string +} + +func (s *PHTSuite) SetupSuite() { + s.ctx = context.Background() + + s.T().Logf("start tcp echo container...") + echoC, err := RunEchoContainer(s.ctx, SharedNetworkName) + s.Require().NoError(err) + s.echoC = echoC + + echoIP, err := echoC.ContainerIP(s.ctx) + s.Require().NoError(err) + s.echoIP = echoIP +} + +func (s *PHTSuite) TearDownSuite() { + if s.echoC != nil { + s.echoC.Terminate(s.ctx) + } +} + +// TestPHTTunnel verifies a basic PHT tunnel: a client HTTP proxy chains +// through a PHT dialer to reach a PHT server, which forwards the request +// to the echo server. +func (s *PHTSuite) TestPHTTunnel() { + serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName, + "testdata/pht/server.yaml", + []string{"pht-server"}, []string{"8443/tcp"}) + s.Require().NoError(err) + defer serverC.Terminate(s.ctx) + + rendered, err := RenderConfig("testdata/pht/client_connector.yaml", + ConfigData{ServerAddr: "pht-server:8443"}) + s.Require().NoError(err) + defer os.Remove(rendered) + + clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, + rendered, "8080/tcp") + s.Require().NoError(err) + defer clientC.Terminate(s.ctx) + + cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080", + fmt.Sprintf("http://%s:5678", s.echoIP)} + code, out, err := clientC.Exec(s.ctx, cmd) + s.Require().NoError(err) + + body, err := io.ReadAll(out) + s.Require().NoError(err) + if code != 0 || !strings.Contains(string(body), "hello-gost") { + DumpLogs(s.T(), s.ctx, "pht client logs", clientC) + DumpLogs(s.T(), s.ctx, "pht server logs", serverC) + } + s.Require().Equal(0, code) + s.Require().Contains(string(body), "hello-gost") +} + +// TestPHTSTunnel verifies PHT over TLS. The PHTs listener auto-generates a +// self-signed cert; the PHTs dialer defaults to InsecureSkipVerify so the +// handshake succeeds without explicit cert configuration. +func (s *PHTSuite) TestPHTSTunnel() { + serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName, + "testdata/pht/server_phts.yaml", + []string{"phts-server"}, []string{"8443/tcp"}) + s.Require().NoError(err) + defer serverC.Terminate(s.ctx) + + rendered, err := RenderConfig("testdata/pht/client_connector_phts.yaml", + ConfigData{ServerAddr: "phts-server:8443"}) + s.Require().NoError(err) + defer os.Remove(rendered) + + clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, + rendered, "8080/tcp") + s.Require().NoError(err) + defer clientC.Terminate(s.ctx) + + cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080", + fmt.Sprintf("http://%s:5678", s.echoIP)} + code, out, err := clientC.Exec(s.ctx, cmd) + s.Require().NoError(err) + + body, err := io.ReadAll(out) + s.Require().NoError(err) + if code != 0 || !strings.Contains(string(body), "hello-gost") { + DumpLogs(s.T(), s.ctx, "phts client logs", clientC) + DumpLogs(s.T(), s.ctx, "phts server logs", serverC) + } + s.Require().Equal(0, code) + s.Require().Contains(string(body), "hello-gost") +} + +// TestPHTHeartbeat verifies that the PHT tunnel stays alive across idle +// periods. Sends a request, waits >readTimeout (default 10s), and sends +// another request to confirm the heartbeat kept the connection open. +func (s *PHTSuite) TestPHTHeartbeat() { + serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName, + "testdata/pht/server.yaml", + []string{"pht-server"}, []string{"8443/tcp"}) + s.Require().NoError(err) + defer serverC.Terminate(s.ctx) + + rendered, err := RenderConfig("testdata/pht/client_connector.yaml", + ConfigData{ServerAddr: "pht-server:8443"}) + s.Require().NoError(err) + defer os.Remove(rendered) + + clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, + rendered, "8080/tcp") + s.Require().NoError(err) + defer clientC.Terminate(s.ctx) + + // First request: establish the tunnel. + cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080", + fmt.Sprintf("http://%s:5678", s.echoIP)} + code, out, err := clientC.Exec(s.ctx, cmd) + s.Require().NoError(err) + body, _ := io.ReadAll(out) + s.Require().Equal(0, code) + s.Require().Contains(string(body), "hello-gost") + + // Wait longer than readTimeout to trigger heartbeat. + // The PHT server's default read timeout is 10s. We wait 12s. + cmd = []string{"sh", "-c", + fmt.Sprintf("sleep 12 && curl -s -x http://127.0.0.1:8080 http://%s:5678", s.echoIP)} + code, out, err = clientC.Exec(s.ctx, cmd) + s.Require().NoError(err) + body, _ = io.ReadAll(out) + if code != 0 || !strings.Contains(string(body), "hello-gost") { + DumpLogs(s.T(), s.ctx, "pht heartbeat client logs", clientC) + DumpLogs(s.T(), s.ctx, "pht heartbeat server logs", serverC) + } + s.Require().Equal(0, code) + s.Require().Contains(string(body), "hello-gost") +} + +func TestPHTSuite(t *testing.T) { + suite.Run(t, new(PHTSuite)) +} diff --git a/tests/e2e/testdata/pht/client_connector.yaml b/tests/e2e/testdata/pht/client_connector.yaml new file mode 100644 index 0000000..7e318a4 --- /dev/null +++ b/tests/e2e/testdata/pht/client_connector.yaml @@ -0,0 +1,20 @@ +services: +- name: pht-proxy + addr: :8080 + handler: + type: http + chain: pht-chain + listener: + type: tcp + +chains: +- name: pht-chain + hops: + - name: pht-hop + nodes: + - name: pht-node + addr: {{.ServerAddr}} + connector: + type: http + dialer: + type: pht diff --git a/tests/e2e/testdata/pht/client_connector_phts.yaml b/tests/e2e/testdata/pht/client_connector_phts.yaml new file mode 100644 index 0000000..75b5e4b --- /dev/null +++ b/tests/e2e/testdata/pht/client_connector_phts.yaml @@ -0,0 +1,20 @@ +services: +- name: phts-proxy + addr: :8080 + handler: + type: http + chain: phts-chain + listener: + type: tcp + +chains: +- name: phts-chain + hops: + - name: phts-hop + nodes: + - name: phts-node + addr: {{.ServerAddr}} + connector: + type: http + dialer: + type: phts diff --git a/tests/e2e/testdata/pht/server.yaml b/tests/e2e/testdata/pht/server.yaml new file mode 100644 index 0000000..6c12f20 --- /dev/null +++ b/tests/e2e/testdata/pht/server.yaml @@ -0,0 +1,7 @@ +services: +- name: pht-server + addr: :8443 + handler: + type: http + listener: + type: pht diff --git a/tests/e2e/testdata/pht/server_phts.yaml b/tests/e2e/testdata/pht/server_phts.yaml new file mode 100644 index 0000000..d04719d --- /dev/null +++ b/tests/e2e/testdata/pht/server_phts.yaml @@ -0,0 +1,7 @@ +services: +- name: phts-server + addr: :8443 + handler: + type: http + listener: + type: phts