schollz/croc · critical

not enough open ports to run local relay

Error message

not enough open ports to run local relay

What it means

Go panic in setupLocalRelay(): utils.FindOpenPorts on 127.0.0.1 starting at RelayPorts[0] returned fewer open ports than len(Options.RelayPorts). The local relay must bind every configured port simultaneously; if any are occupied, croc panics instead of silently degrading.

Source

Thrown at src/croc/croc.go:1083

	fmt.Fprintf(os.Stderr, "\r                                 ")
	output, colorEnabled := termui.Output(os.Stderr)
	if displayName != "" {
		fname = quotedFilename(displayName, colorEnabled)
	}
	if c.TotalNumberFolders > 0 {
		fmt.Fprintf(output, "\rSending %s and %s (%s)\n", fname, folderName, utils.ByteCountDecimal(totalFilesSize))
	} else {
		fmt.Fprintf(output, "\rSending %s (%s)\n", fname, utils.ByteCountDecimal(totalFilesSize))
	}
	return
}

func (c *Client) setupLocalRelay() {
	// setup the relay locally
	firstPort, _ := strconv.Atoi(c.Options.RelayPorts[0])
	openPorts := utils.FindOpenPorts("127.0.0.1", firstPort, len(c.Options.RelayPorts))
	if len(openPorts) < len(c.Options.RelayPorts) {
		panic("not enough open ports to run local relay")
	}
	for i, port := range openPorts {
		c.Options.RelayPorts[i] = fmt.Sprint(port)
	}
	// Capture the local relay control port before any goroutine that handles
	// the external relay can overwrite c.Options.RelayPorts.
	c.localRelayPort = c.Options.RelayPorts[0]
	localRelayPorts := append([]string(nil), c.Options.RelayPorts...)
	localRelayBanner := strings.Join(localRelayPorts[1:], ",")
	for _, port := range localRelayPorts {
		go func(portStr string) {
			debugString := "warn"
			if c.Options.Debug {
				debugString = "debug"
			}
			err := c.stop.run(
				debugString,
				"",

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Find and stop the squatter: lsof -i :9009-9013 or ss -ltnp, then kill the process (pkill -f croc for stale instances)
  2. Pass a different range: --relay-ports 10001-10006
  3. Library embedders: probe with utils.FindOpenPorts yourself and supply a confirmed-open list instead of panicking

Example fix

# before
croc --relay --relay-ports 9009-9013 send file.bin  # 9010 occupied -> panic

# after
lsof -i :9009-9013  # find and stop the conflicting listener
croc --relay --relay-ports 10001-10006 send file.bin
Defensive patterns

Strategy: validation

Validate before calling

// Go: probe ports before starting the local relay
first, _ := strconv.Atoi(opts.RelayPorts[0])
if len(utils.FindOpenPorts("127.0.0.1", first, len(opts.RelayPorts))) < len(opts.RelayPorts) {
    return fmt.Errorf("relay ports %v unavailable; pass --relay-ports with a free range", opts.RelayPorts)
}

Try / catch

// Panic path: pre-check ports as above, or run croc in a subprocess and map its non-zero exit/panic output to a 'ports busy' message for the user

Prevention

When it happens

Trigger: Running with a local relay while another process already listens on one of the configured ports: a previous croc instance still running, a concurrent second transfer, or another service (dev server, docker container publishing the range). Default ports occupy several consecutive ports starting at 9009.

Common situations: Two croc transfers at once on one machine; leftover croc process from a crashed session; docker publishing 9009-9013; ephemeral-port pressure breaking the consecutive run.

Related errors


AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15). Data as JSON: /api/errors/e25fcc4e475ff2f0. Report an issue: GitHub.