# How to Configure Tailcat Server to Listen on Specific UDP Ports

> Learn how to configure Tailcat server to listen on specific UDP ports by setting the ServedUDPPorts field before starting your server. Get precise network control.

- Repository: [Tailscale/tailcat](https://github.com/tailscale/tailcat)
- Tags: how-to-guide
- Published: 2026-09-06

---

**Configure a Tailcat server to listen on specific UDP ports by populating the `ServedUDPPorts` field with `filter.PortRange` entries before calling `Server.Start`.**

The `tailcat` package from Tailscale provides a lightweight WireGuard-based tunnel server that can restrict UDP traffic to a predefined set of ports. This article explains how to configure the `Server` struct to enforce these restrictions using the source implementation in `tailscale/tailcat`.

## Understanding UDP Port Filtering in Tailcat

Tailcat's server architecture separates **packet filtering** from **handler logic**. When you start a server, the `buildFilter` function—located in [`main/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat.go)—constructs a packet filter that governs which inbound flows are permitted.

The key decision point occurs at lines 42-46 of [`main/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat.go):

```go
// In main/tailcat.go, buildFilter constructs the filter:
udpPorts = s.ServedUDPPorts
if udpPorts == nil {
    udpPorts = filter.AllPorts() // fallback when not specified
}

```

If `ServedUDPPorts` is non-nil, the filter is built exclusively from your specified ranges. If left nil, the server defaults to accepting traffic on **all ports**. This filter is applied to the underlying WireGuard tunnel, meaning packets to unlisted ports are dropped before reaching your `OnUDP` handler.

## The Server.ServedUDPPorts Field

The `Server` struct definition in [`main/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat.go) (lines 75-90) exposes the configuration field:

```go
type Server struct {
    // ...
    ServedUDPPorts []filter.PortRange
    // ...
}

```

Each `filter.PortRange` specifies an inclusive range using two `uint16` fields:

| Field | Type | Purpose |
|-------|------|---------|
| `First` | `uint16` | Starting port number (inclusive) |
| `Last` | `uint16` | Ending port number (inclusive) |

To allow a single port, set both fields to the same value. For a range, specify the bounds accordingly.

## Configuration Steps

Follow this sequence to configure port-restricted UDP listening:

1. **Create a `Server` value** using the `tailcat` package.
2. **Populate `ServedUDPPorts`** with `filter.PortRange` entries for your desired ports.
3. **Assign an `OnUDP` handler** to process incoming `ConnPacketConn` connections.
4. **Call `Start`** to initialize the server with the restricted filter.

## Practical Implementation

This complete example demonstrates serving DNS (port 53) and NTP (port 123) only:

```go
package main

import (
	"log"
	"net"

	"tailscale.com/tailcfg"
	"tailscale.com/tailcat"
	"tailscale.com/tailcat/internal/filter"
)

func main() {
	// Create a server instance.
	srv := &tailcat.Server{
		// Accept UDP traffic only on ports 53 (DNS) and 123 (NTP).
		ServedUDPPorts: []filter.PortRange{
			{First: 53, Last: 53},
			{First: 123, Last: 123},
		},

		// Handle incoming UDP flows.
		OnUDP: func(port uint16) func(tailcat.ConnPacketConn) {
			return func(pc tailcat.ConnPacketConn) {
				buf := make([]byte, 1500)
				for {
					n, err := pc.Read(buf)
					if err != nil {
						if err != net.ErrClosed {
							log.Printf("udp read error: %v", err)
						}
						return
					}
					// Echo the payload back to the client.
					if _, err := pc.Write(buf[:n]); err != nil {
						log.Printf("udp write error: %v", err)
						return
					}
				}
			}
		},
	}

	// Start the server.
	if err := srv.Start(); err != nil {
		log.Fatalf("failed to start tailcat server: %v", err)
	}

	// Block forever.
	select {}
}

```

### Serving a Contiguous Range

To allow ports 5000 through 5005 inclusive, use a single range entry:

```go
ServedUDPPorts: []filter.PortRange{
    {First: 5000, Last: 5005},
},

```

## Key Source Files

Understanding the implementation requires familiarity with these files in `tailscale/tailcat`:

- **[`main/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat.go)** — Contains the `Server` struct definition including `ServedUDPPorts`, plus the `buildFilter` function that enforces port restrictions.
- **[`internal/filter/portrange.go`](https://github.com/tailscale/tailcat/blob/main/internal/filter/portrange.go)** — Defines the `PortRange` type with `First` and `Last` fields.
- **[`main/tailcat_test.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat_test.go)** — Includes test cases around line 405 demonstrating `ServedUDPPorts` usage for UDP filtering.
- **[`cmd/tailcat/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/cmd/tailcat/tailcat.go)** — Shows how the official CLI binary configures the server, serving as a production reference.

## Summary

- **Primary mechanism:** Set `Server.ServedUDPPorts` before calling `Start` to restrict UDP listening to specific ports.
- **Default behavior:** When `ServedUDPPorts` is nil, the server accepts traffic on all ports.
- **Filter application:** The `buildFilter` function in [`main/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat.go) applies your port list at startup; unauthorized packets are dropped by the WireGuard tunnel filter.
- **Range specification:** Use `filter.PortRange` with inclusive `First` and `Last` fields to define single ports or contiguous ranges.

## Frequently Asked Questions

### What happens if I don't set ServedUDPPorts?

If `ServedUDPPorts` remains nil, `buildFilter` falls back to `filter.AllPorts()`, allowing the server to receive UDP traffic on any port. This is implemented in [`main/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat.go) lines 42-46.

### Can I mix single ports and ranges in the same configuration?

Yes. The `ServedUDPPorts` field accepts a slice of `filter.PortRange`, so you can combine any number of single-port entries (`{First: 80, Last: 80}`) and multi-port ranges (`{First: 1000, Last: 2000}`) in the same slice.

### Does the server need to restart to change allowed ports?

Yes. The packet filter is constructed once during `Server.Start`. To modify allowed ports, you must create a new `Server` instance with updated `ServedUDPPorts` and call `Start` again. The source code shows no runtime reconfiguration of the filter after initialization.

### Where is PortRange defined in the codebase?

The `PortRange` type is defined in [`internal/filter/portrange.go`](https://github.com/tailscale/tailcat/blob/main/internal/filter/portrange.go) (imported as `"tailscale.com/tailcat/internal/filter"`). It consists of two `uint16` fields: `First` and `Last`, representing an inclusive port range.