Building a Custom croc Relay with Allowed Port Ranges: A Complete Guide

You can restrict a custom croc relay to specific TCP ports by configuring the AllowedPorts field in webrelay.Config, which validates port numbers at startup and rejects unauthorized WebSocket bridge connections with a 403 Forbidden response.

The schollz/croc repository includes a web relay package that enables browser-based file transfers through a WebSocket-to-TCP bridge. While the default configuration accepts connections on a broad range of ports, production environments often require strict network segmentation. This guide demonstrates how to build a custom relay that exposes only designated port ranges using the src/webrelay implementation.

How the croc Web Relay Works

The web relay is implemented in the src/webrelay package and functions as a lightweight HTTP server with two primary responsibilities: serving the embedded web client assets and managing a WebSocket-to-TCP bridge. The bridge operates as a transparent proxy that forwards opaque byte streams to an upstream croc relay without interpreting the croc protocol itself.

According to the source code in src/webrelay/webrelay.go, the relay's behavior is governed by the Config struct:

Field Purpose Default
ListenAddress HTTP server bind address 127.0.0.1:9014
RelayHost Upstream croc relay hostname croc.schollz.com
AllowedPorts Permitted bridge destination ports ["9009","9010","9011","9012","9013","9014","9015","9016","9017"]
OriginPatterns WebSocket origin header allowlist –
DialTimeout TCP connection timeout 10s
PublicAddress Address advertised in /config.js Same as ListenAddress
RelayPassword Authentication password pass123
StaticFiles Embedded web assets webassets.Files()

Port Validation and Runtime Enforcement

The relay implements a three-stage validation pipeline to ensure only approved ports are accessible:

1. Configuration Normalization

The normalizeConfig function (lines 45-96 in src/webrelay/webrelay.go) processes the AllowedPorts slice. If the field is empty, it populates the default nine-port range. Each entry undergoes trimming and parsing via strconv.ParseUint (lines 78-84) to verify it represents a valid 16-bit port number.

2. Duplicate Removal and Storage

After validation, the normalized ports are deduplicated and stored in handler.allowedPorts as a map[string]struct{} (line 73), providing O(1) lookup performance during runtime.

3. Request-Time Validation

When a WebSocket connection request arrives at /ws?port=<port>, the handler extracts the query parameter (line 36) and checks membership in h.allowedPorts (line 37). Unauthorized ports trigger an immediate 403 Forbidden response (line 38). Authorized requests proceed to dial RelayHost:<port> (lines 44-48) and establish the byte stream proxy.

Building a Custom Relay with Restricted Ports

Because port restrictions are enforced at the configuration level, you can deploy a custom relay by supplying a tailored webrelay.Config struct. Below are three implementation patterns ranging from simple embedded deployments to production-ready TLS configurations.

Standalone Go Application

This example launches a relay that accepts connections only on ports 9100-9102, targeting a private upstream server:

package main

import (
	"context"
	"log"

	"github.com/schollz/croc/v10/src/webrelay"
)

func main() {
	// Define a custom config – only ports 9100‑9102 are allowed.
	cfg := webrelay.Config{
		ListenAddress: "0.0.0.0:8080",      // expose on all interfaces
		RelayHost:     "myrelay.example.com", // your own relay server
		AllowedPorts:  []string{"9100", "9101", "9102"},
		// Optional: tighten origins to your domain only
		OriginPatterns: []string{"https://mydomain.com/*"},
	}

	// Run the server until the context is cancelled.
	if err := webrelay.Run(context.Background(), cfg); err != nil {
		log.Fatalf("relay exited: %v", err)
	}
}

The webrelay.Run function invokes the same normalization logic used by the production binary, ensuring your custom port list is validated before the server starts accepting connections.

Environment-Based Configuration for Containers

For Docker or Kubernetes deployments, parse configuration from environment variables to allow runtime customization without recompilation:

package main

import (
	"context"
	"log"
	"os"
	"strings"

	"github.com/schollz/croc/v10/src/webrelay"
)

func main() {
	cfg := webrelay.Config{
		ListenAddress: getEnv("LISTEN", "0.0.0.0:8080"),
		RelayHost:     getEnv("RELAY_HOST", "myrelay.example.com"),
		AllowedPorts:  splitEnv("ALLOWED_PORTS", "9100,9101,9102"),
		OriginPatterns: []string{
			getEnv("ORIGIN_PATTERN", "https://mydomain.com/*"),
		},
	}
	if err := webrelay.Run(context.Background(), cfg); err != nil {
		log.Fatalf("relay error: %v", err)
	}
}

// Helper: read a single env var with fallback.
func getEnv(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

// Helper: split a comma‑separated list into a []string.
func splitEnv(key, fallback string) []string {
	val := getEnv(key, fallback)
	parts := []string{}
	for _, p := range strings.Split(val, ",") {
		if s := strings.TrimSpace(p); s != "" {
			parts = append(parts, s)
		}
	}
	return parts
}

This pattern allows operators to set ALLOWED_PORTS=9200,9205,9210 at deployment time, with the normalizeConfig function ensuring all entries are valid TCP port numbers before the bridge starts.

TLS-Enabled Custom Deployment

For public-facing relays, wrap the webrelay handler in a custom TLS server:

package main

import (
	"context"
	"crypto/tls"
	"log"
	"net/http"

	"github.com/schollz/croc/v10/src/webrelay"
)

func main() {
	cfg := webrelay.Config{
		RelayHost:    "myrelay.example.com",
		AllowedPorts: []string{"9200"},
	}
	// Build the handler (includes static assets)
	h, err := webrelay.Handler(cfg)
	if err != nil {
		log.Fatalf("handler init: %v", err)
	}

	// TLS configuration (self‑signed for illustration)
	tlsCfg := &tls.Config{
		MinVersion: tls.VersionTLS12,
	}
	srv := &http.Server{
		Addr:      ":8443",
		Handler:   h,
		TLSConfig: tlsCfg,
	}
	if err := srv.ListenAndServeTLS("cert.pem", "key.pem"); err != nil {
		log.Fatalf("TLS server error: %v", err)
	}
}

By calling webrelay.Handler directly, you obtain the fully configured http.Handler while retaining control over the server stack. The allowed-port validation logic remains identical regardless of the transport security layer.

Key Source Files

Understanding these files helps when customizing the relay behavior:

Summary

  • The croc web relay isolates browser clients from the croc protocol through a WebSocket-to-TCP bridge implemented in src/webrelay.
  • Port restrictions are configured via the AllowedPorts field in webrelay.Config, which defaults to ports 9009-9017.
  • The normalizeConfig function validates ports using strconv.ParseUint and stores them in a map for O(1) runtime lookups.
  • Unauthorized port requests receive a 403 Forbidden response before any TCP connection is attempted.
  • Custom relays can be deployed using webrelay.Run() for standard servers or webrelay.Handler() for integration with custom HTTP stacks.

Frequently Asked Questions

What is the default port range for croc web relays?

The default configuration in src/webrelay/webrelay.go allows ports 9009 through 9017 inclusive. If the AllowedPorts slice is empty, normalizeConfig automatically populates these nine ports as the allowable bridge destinations.

How does the relay validate port numbers during startup?

The normalizeConfig function (lines 45-96) iterates through AllowedPorts, trims whitespace, and parses each entry with strconv.ParseUint to verify it is a valid 16-bit integer (lines 78-84). Invalid entries cause immediate startup failure with an error, while duplicates are removed before storage.

Can I restrict the web relay to a single port?

Yes. Provide a single-element slice to AllowedPorts, such as []string{"9200"}. The validation logic accepts any number of ports, and the runtime check enforces exact matches against this list. Requests to any other port will return HTTP 403 Forbidden.

Is the WebSocket bridge secure for public internet exposure?

The bridge is designed to be safe for public exposure because it only forwards opaque byte streams and never participates in the croc protocol logic. However, you should always configure OriginPatterns to prevent cross-site WebSocket hijacking and consider TLS encryption (as shown in the third example) to protect data in transit.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →