mirror of https://github.com/go-gost/gost.git
test(e2e): add TLS listener rejectUnknownSNI test suite
Covers rejectUnknownSNI with and without a serverNames allow list, verifying allowed/unknown/empty SNI handshakes via openssl s_client.master
parent
a8bd4a5f16
commit
fa815bf4a0
|
|
@ -1,4 +1,4 @@
|
||||||
FROM alpine:3.22
|
FROM alpine:3.22
|
||||||
|
|
||||||
# add tools needed by e2e containers and health checks
|
# add tools needed by e2e containers and health checks
|
||||||
RUN apk add --no-cache iptables curl netcat-openbsd python3
|
RUN apk add --no-cache iptables curl netcat-openbsd python3 openssl
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
services:
|
||||||
|
- name: tls-reject
|
||||||
|
addr: :8443
|
||||||
|
handler:
|
||||||
|
type: http
|
||||||
|
listener:
|
||||||
|
type: tls
|
||||||
|
tls:
|
||||||
|
rejectUnknownSNI: true
|
||||||
|
serverNames:
|
||||||
|
- example.com
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
services:
|
||||||
|
- name: tls-reject
|
||||||
|
addr: :8443
|
||||||
|
handler:
|
||||||
|
type: http
|
||||||
|
listener:
|
||||||
|
type: tls
|
||||||
|
tls:
|
||||||
|
rejectUnknownSNI: true
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/testcontainers/testcontainers-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TLSSuite covers TLS listener behavior, in particular the rejectUnknownSNI
|
||||||
|
// option which drops TLS handshakes with an unknown or empty SNI before any
|
||||||
|
// certificate is sent.
|
||||||
|
type TLSSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TLSSuite) SetupSuite() {
|
||||||
|
s.ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
// startGost starts a gost container with the given TLS listener config. The
|
||||||
|
// listener always binds :8443; checks run openssl inside the container.
|
||||||
|
func (s *TLSSuite) startGost(yamlPath string) testcontainers.Container {
|
||||||
|
s.T().Helper()
|
||||||
|
rendered, err := RenderConfig(yamlPath, ConfigData{})
|
||||||
|
s.Require().NoError(err)
|
||||||
|
s.T().Cleanup(func() { os.Remove(rendered) })
|
||||||
|
|
||||||
|
c, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, rendered, "8443/tcp")
|
||||||
|
s.Require().NoError(err)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// sniHandshake runs openssl s_client against the TLS listener and returns its
|
||||||
|
// exit code: 0 means the handshake completed (a certificate was exchanged),
|
||||||
|
// non-zero means the handshake was rejected. An empty sni sends no SNI
|
||||||
|
// extension.
|
||||||
|
func (s *TLSSuite) sniHandshake(c testcontainers.Container, sni string) int {
|
||||||
|
s.T().Helper()
|
||||||
|
args := []string{"openssl", "s_client", "-brief", "-connect", "127.0.0.1:8443"}
|
||||||
|
if sni == "" {
|
||||||
|
args = append(args, "-noservername")
|
||||||
|
} else {
|
||||||
|
args = append(args, "-servername", sni)
|
||||||
|
}
|
||||||
|
code, out, err := c.Exec(s.ctx, args)
|
||||||
|
if err != nil {
|
||||||
|
s.T().Logf("openssl exec error: %v", err)
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if b, e := io.ReadAll(out); e == nil {
|
||||||
|
s.T().Logf("openssl output (sni=%q):\n%s", sni, string(b))
|
||||||
|
}
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRejectWithAllowList covers rejectUnknownSNI with a populated serverNames
|
||||||
|
// list: the allowed SNI completes the handshake, while a wrong or missing SNI
|
||||||
|
// is rejected.
|
||||||
|
func (s *TLSSuite) TestRejectWithAllowList() {
|
||||||
|
c := s.startGost("testdata/tls/reject_sni.yaml")
|
||||||
|
defer c.Terminate(s.ctx)
|
||||||
|
|
||||||
|
s.Require().Zero(s.sniHandshake(c, "example.com"),
|
||||||
|
"handshake with allowed SNI should succeed")
|
||||||
|
|
||||||
|
s.Require().NotZero(s.sniHandshake(c, "wrong.com"),
|
||||||
|
"handshake with disallowed SNI should fail")
|
||||||
|
|
||||||
|
s.Require().NotZero(s.sniHandshake(c, ""),
|
||||||
|
"handshake without SNI should fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRejectEmptyOnly covers rejectUnknownSNI with an empty serverNames list:
|
||||||
|
// only a missing SNI is rejected, any named SNI is allowed.
|
||||||
|
func (s *TLSSuite) TestRejectEmptyOnly() {
|
||||||
|
c := s.startGost("testdata/tls/reject_sni_empty.yaml")
|
||||||
|
defer c.Terminate(s.ctx)
|
||||||
|
|
||||||
|
s.Require().NotZero(s.sniHandshake(c, ""),
|
||||||
|
"handshake without SNI should fail")
|
||||||
|
|
||||||
|
s.Require().Zero(s.sniHandshake(c, "anything.test"),
|
||||||
|
"named SNI should be allowed when allow list is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTLSSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(TLSSuite))
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue