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

> Learn to build a custom croc relay with allowed port ranges using the schollz/croc repository. Secure your file transfers by restricting TCP ports for authorized WebSocket connections.

- Repository: [Zack/croc](https://github.com/schollz/croc)
- Tags: how-to-guide
- Published: 2026-07-26

---

**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`](https://github.com/schollz/croc/blob/main/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`](https://github.com/schollz/croc/blob/main//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`](https://github.com/schollz/croc/blob/main/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:

```go
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:

```go
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:

```go
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:

- **[`src/webrelay/webrelay.go`](https://github.com/schollz/croc/blob/main/src/webrelay/webrelay.go)** — Core implementation containing the `Config` struct, `normalizeConfig` validation (lines 45-96), and WebSocket bridge logic.
- **[`src/webassets/assets.go`](https://github.com/schollz/croc/blob/main/src/webassets/assets.go)** — Embedded static files ([`index.html`](https://github.com/schollz/croc/blob/main/index.html), JavaScript) served by the relay.
- **[`src/webrelay/webrelay_test.go`](https://github.com/schollz/croc/blob/main/src/webrelay/webrelay_test.go)** — Test coverage for configuration normalization and port enforcement.
- **[`src/cli/cli.go`](https://github.com/schollz/croc/blob/main/src/cli/cli.go)** — Command-line entry point that constructs `webrelay.Config` from CLI flags for the `croc relay` command.

## 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`](https://github.com/schollz/croc/blob/main/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.