interactsh icon indicating copy to clipboard operation
interactsh copied to clipboard

goroutine leaks flagged in Interactsh Client/Server package

Open knakul853 opened this issue 5 months ago • 0 comments

Interactsh version:

  • v1.2.4 (and main)

Current Behavior:

Client: New() starts a keepalive goroutine when KeepAliveInterval > 0. If Close() isn’t called (e.g., short-lived tests), the goroutine leaks (goleak flags github.com/projectdiscovery/interactsh/pkg/client.New.func1).

Server: HTTPServer.ListenAndServe spawns serving goroutines. Without an exported Close(), tests/tools can’t gracefully stop; goroutines remain in Accept/Serve and goleak flags them.

Example stack (trimmed):

    request_test.go:527: found unexpected goroutines:
        [Goroutine 59 in state select, with github.com/projectdiscovery/interactsh/pkg/client.(*Client).StartPolling.func1 on top of the stack:
        github.com/projectdiscovery/interactsh/pkg/client.(*Client).StartPolling.func1()
        	/Users/runner/go/pkg/mod/github.com/projectdiscovery/[email protected]/pkg/client/client.go:379 +0xe0
        created by github.com/projectdiscovery/interactsh/pkg/client.(*Client).StartPolling in goroutine 55
        	/Users/runner/go/pkg/mod/github.com/projectdiscovery/[email protected]/pkg/client/client.go:373 +0x1f4
         Goroutine 58 in state select, with github.com/projectdiscovery/interactsh/pkg/client.New.func1 on top of the stack:
        github.com/projectdiscovery/interactsh/pkg/client.New.func1()
        	/Users/runner/go/pkg/mod/github.com/projectdiscovery/[email protected]/pkg/client/client.go:205 +0x11c
        created by github.com/projectdiscovery/interactsh/pkg/client.New in goroutine 55
        	/Users/runner/go/pkg/mod/github.com/projectdiscovery/[email protected]/pkg/client/client.go:199 +0xcc0
        ]
--- FAIL: TestExecuteParallelHTTP_GoroutineLeaks (0.79s)

Expected Behavior:

  • New() should not start any background goroutines.
  • Keepalive should start only when StartPolling() is called and be terminated by StopPolling()/Close().
  • No leaked goroutines when constructing a client or after a clean shutdown.

Steps To Reproduce:

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"time"

	"github.com/projectdiscovery/interactsh/pkg/client"
	iserver "github.com/projectdiscovery/interactsh/pkg/server"
	"go.uber.org/goleak"
)

func reproClient() error {
	mux := http.NewServeMux()
	mux.HandleFunc("/register", func(w http.ResponseWriter, _ *http.Request) {
		_ = json.NewEncoder(w).Encode(map[string]string{"message": "registration successful"})
	})
	ts := httptest.NewServer(mux)
	defer ts.Close()

	opts := &client.Options{ServerURL: ts.URL, KeepAliveInterval: 100 * time.Millisecond}
	c, err := client.New(opts)
	if err != nil {
		return err
	}
	_ = c

	time.Sleep(300 * time.Millisecond)
	if err := goleak.Find(); err != nil {
		return err
	}
	return nil
}

func reproServer() error {
	opts := &iserver.Options{
		Domains:                  []string{"example.com"},
		ListenIP:                 "127.0.0.1",
		HttpPort:                 0,
		HttpsPort:                0,
		CorrelationIdLength:      8,
		CorrelationIdNonceLength: 6,
	}
	s, err := iserver.NewHTTPServer(opts)
	if err != nil {
		return err
	}
	httpAlive := make(chan bool, 1)
	httpsAlive := make(chan bool, 1)
	go s.ListenAndServe(nil, httpAlive, httpsAlive)
	select {
	case <-httpAlive:
	case <-time.After(500 * time.Millisecond):
		return fmt.Errorf("server did not start")
	}
	time.Sleep(300 * time.Millisecond)
	if err := goleak.Find(); err != nil {
		return err
	}
	return nil
}

func main() {
	mode := flag.String("mode", "client", "repro mode: client or server")
	flag.Parse()

	var err error
	switch *mode {
	case "client":
		fmt.Println("Reproducing client goroutine leak (pre-fix versions):")
		err = reproClient()
	case "server":
		fmt.Println("Reproducing server goroutine leak when not closed:")
		err = reproServer()
	default:
		fmt.Println("unknown mode; use -mode=client or -mode=server")
		os.Exit(2)
	}
	if err != nil {
		fmt.Println("LEAK DETECTED:", err)
		os.Exit(1)
	}
	fmt.Println("No leaks detected.")
}

knakul853 avatar Sep 23 '25 11:09 knakul853