# Maximum UDP Payload Size in Tailcat: IPv4 and IPv6 Limits Explained

> Discover Tailcat's maximum UDP payload size for IPv4 and IPv6. Learn about standard Internet protocol limits and optimize your network performance. Get the details now.

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

---

**The maximum UDP payload size in Tailcat is 65,507 bytes for IPv4 and 65,527 bytes for IPv6**, determined by standard Internet protocol limits rather than custom implementation constraints.

Tailcat is a UDP tunneling proxy built on the `gvisor.dev/gvisor/pkg/tcpip` netstack. When forwarding UDP traffic, it relies on the underlying network stack's protocol enforcement, meaning the **maximum UDP payload size** follows classical IP datagram boundaries without additional truncation or application-level caps.

## How Tailcat Handles UDP Packet Sizes

Tailcat delegates all UDP packet construction to the gVisor netstack. The total IP packet length field is a 16-bit unsigned integer, capped at 65,535 bytes. After subtracting fixed header overhead, the remaining space defines the payload ceiling.

### IPv4 UDP Payload Limit

IPv4 carries 20 bytes of header plus 8 bytes of UDP header:

| Component | Size |
|-----------|------|
| IPv4 header | 20 bytes |
| UDP header | 8 bytes |
| **Maximum payload** | **65,507 bytes** |

```go
// Sending the largest IPv4 UDP payload Tailcat can forward
payload := make([]byte, 65507) // 65,507 bytes — max IPv4 UDP payload
for i := range payload {
    payload[i] = byte(i)
}
conn, _ := net.DialUDP("udp4", nil, net.UDPAddr{
    IP:   net.ParseIP("127.0.0.1"),
    Port: 12345,
})
conn.Write(payload) // Tailcat forwards the complete datagram

```

### IPv6 UDP Payload Limit

IPv6 uses a 40-byte header with the same 8-byte UDP header:

| Component | Size |
|-----------|------|
| IPv6 header | 40 bytes |
| UDP header | 8 bytes |
| **Maximum payload** | **65,527 bytes** |

```go
// IPv6 — maximum payload increases due to fixed 40-byte header
payload := make([]byte, 65527) // 65,527 bytes — max IPv6 UDP payload
conn, _ := net.DialUDP("udp6", nil, net.UDPAddr{
    IP:   net.ParseIP("::1"),
    Port: 12345,
})
conn.Write(payload) // Full IPv6 UDP packet forwarded by Tailcat

```

## Source Code Implementation

The server implementation in [`main/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat.go) initializes the netstack and establishes UDP handling through `DialContextUDPWithBind`. No custom size checks appear in the application code—the gVisor netstack enforces boundaries automatically.

Key files demonstrating this behavior:

- [`main/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat.go) — Core server; netstack initialization and UDP socket creation
- [`main/tailcat_test.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat_test.go) — Unit tests validating UDP payload handling at size limits
- [`main/cmd/tailcat/socks_test.go`](https://github.com/tailscale/tailcat/blob/main/main/cmd/tailcat/socks_test.go) — SOCKS5 UDP-ASSOCIATE tests exercising maximum payloads
- [`main/wire.go`](https://github.com/tailscale/tailcat/blob/main/main/wire.go) — Low-level packet encoding respecting IP/UDP length fields

Tests in these files construct UDP packets up to the protocol-defined maximums, confirming Tailcat forwards them without truncation.

## Why No Smaller Limit Exists

Some tunneling implementations impose arbitrary ceilings (1,500 bytes for MTU alignment, or 8,192 bytes for buffer sizing). Tailcat avoids this by:

1. **Delegating to netstack** — The gVisor `tcpip` package manages fragmentation and reassembly
2. **Virtual interface abstraction** — Packets traverse an internal stack before wire transmission
3. **Test coverage** — Explicit validation that 65,507/65,527-byte payloads traverse cleanly

The result: **maximum UDP payload size in Tailcat equals the theoretical IP limit**, not a constrained practical subset.

## Summary

- **IPv4 maximum UDP payload**: 65,507 bytes (65,535 − 20 − 8)
- **IPv6 maximum UDP payload**: 65,527 bytes (65,535 − 40 − 8)
- Tailcat imposes no additional limits beyond protocol-defined ceilings
- Implementation in [`main/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat.go) uses `gvisor.dev/gvisor/pkg/tcpip` for standard-conforming behavior
- Tests in [`main/tailcat_test.go`](https://github.com/tailscale/tailcat/blob/main/main/tailcat_test.go) and [`main/cmd/tailcat/socks_test.go`](https://github.com/tailscale/tailcat/blob/main/main/cmd/tailcat/socks_test.go) verify full-size packet forwarding

## Frequently Asked Questions

### What happens if I send a UDP payload larger than 65,507 bytes through Tailcat?

Packets exceeding the IP maximum transmission unit trigger fragmentation at lower network layers, or the sending kernel rejects them with `EMSGSIZE`. Tailcat receives only what the netstack permits; oversized sends fail before reaching the tunnel.

### Does Tailcat support jumbo frames or Ethernet MTU adjustments?

Tailcat operates above the Ethernet layer via the gVisor netstack virtual interface. Jumbo frame handling depends on the underlying physical network between Tailcat instances, not the internal UDP payload limit.

### Why is the IPv6 payload limit 20 bytes larger than IPv4?

IPv6 eliminates header options and checksum fields present in IPv4, using a fixed 40-byte base header versus IPv4's minimum 20 bytes. The 20-byte difference (40 − 20) transfers directly to additional payload capacity.

### Where does Tailcat validate UDP packet sizes during forwarding?

Validation occurs implicitly through the gVisor netstack imported at `gvisor.dev/gvisor/pkg/tcpip`. The [`main/wire.go`](https://github.com/tailscale/tailcat/blob/main/main/wire.go) encoding routines respect the 16-bit length fields without additional application checks.