Skip to content

Plugin Testing & QA ​

TCP Warden plugins handle real-time production network traffic. To ensure stability, every plugin must pass strict unit testing with multithreading race condition detection before deployment.


Why Test with net.Pipe()? ​

Testing network proxies typically requires binding localhost ports, managing background server processes, and cleaning up sockets.

Go's standard library provides net.Pipe(), which creates a synchronous, in-memory duplex network connection:

  • Zero Open Ports: Tests run without binding real ports or needing firewall permissions.
  • Instant Execution: In-memory pipes transfer bytes in microseconds.
  • Deterministic: Network timing, partial reads, and disconnects can be simulated reliably.
[ Test Code ] ──(Write)──► clientA ◄═══ net.Pipe() ═══► clientB ──► [ Inspector.Run() ]
                                                                             │
[ Test Code ] ◄──(Read)───   upB   ◄═══ net.Pipe() ═══►   upA   ◄────────────┘

Writing In-Memory Unit Tests ​

Below is a complete test suite verifying both attack interception and legitimate traffic forwarding:

package echo_guardimport (	"bufio"	"net"	"testing"	"time"	"github.com/routewarden/tcp-warden/plugins/sdk")func TestInspector_BlockedTraffic(t *testing.T) {	// Create two in-memory duplex pipes (client side and upstream side)	clientA, clientB := net.Pipe()	defer clientA.Close()	defer clientB.Close()	upA, upB := net.Pipe()	defer upA.Close()	defer upB.Close()	insp := &Inspector{BlockedWords: []string{"malicious"}}	ctx := &sdk.DefaultContext{ServiceName: "test-echo", ClientAddress: "127.0.0.1"}	done := make(chan struct {		blocked bool		reason  string	}, 1)	go func() {		_, blocked, reason, _ := insp.Run(ctx, clientB, upA)		done <- struct {			blocked bool			reason  string		}{blocked, reason}	}()	// Simulate client writing a blocked payload	_, _ = clientA.Write([]byte("hello malicious world\n"))	select {	case res := <-done:		if !res.blocked {			t.Error("expected keyword to be blocked")		}		if res.reason != "blocked keyword: malicious" {			t.Errorf("unexpected reason: %s", res.reason)		}	case <-time.After(2 * time.Second):		t.Fatal("test timed out")	}}func TestInspector_AllowedTraffic(t *testing.T) {	clientA, clientB := net.Pipe()	defer clientA.Close()	defer clientB.Close()	upA, upB := net.Pipe()	defer upA.Close()	defer upB.Close()	// Capture what the upstream server receives	upstreamReceived := make(chan string, 1)	go func() {		reader := bufio.NewReader(upB)		line, _ := reader.ReadString('\n')		upstreamReceived <- line	}()	insp := &Inspector{BlockedWords: []string{"malicious"}}	ctx := &sdk.DefaultContext{ServiceName: "test-echo", ClientAddress: "127.0.0.1"}	go func() {		_, _, _, _ = insp.Run(ctx, clientB, upA)	}()	// Send clean traffic	_, _ = clientA.Write([]byte("hello clean world\n"))	select {	case received := <-upstreamReceived:		if received != "hello clean world\n" {			t.Errorf("expected clean message, got: %s", received)		}	case <-time.After(2 * time.Second):		t.Fatal("test timed out waiting for upstream forwarding")	}}

Running Tests with the Race Detector ​

Always run tests with Go's built-in race condition detector (-race). The TCP proxy relies on concurrent goroutines for bidirectional traffic forwarding:

# 1. Run unit tests with multithreaded race detector enabledgo test -v -race -count=1 ./...# 2. Run in-memory self-tests across all installed pluginstcp-warden plugins test

Race Detector Failures Abort Installation

The 4-step safety pipeline automatically runs go test -race when installing plugins. If any race condition is found, installation is immediately rejected.


Implementing Daemon Self-Tests (SelfTest) ​

Every plugin can implement the SelfTest() error method on sdk.Plugin:

go
func (p *Plugin) SelfTest() error {
    insp := &Inspector{BlockedWords: []string{"test"}}
    if len(insp.BlockedWords) == 0 {
        return errors.New("self-test failed: uninitialized slice")
    }
    return nil
}

Self-tests are executed:

  1. When starting the daemon (tcp-warden run).
  2. When validating configurations (tcp-warden validate).
  3. On demand via tcp-warden plugins test.

Next Steps ​

  • Packaging & Publishing: Learn how to install your plugin locally, distribute via Git, and review the pre-publication checklist.

Released under the MIT License.