netbirdio/netbird · error

mixed address families: src=%s dst=%s

Error message

mixed address families: src=%s dst=%s

What it means

The tracer's PacketBuilder refuses to synthesize an IP header when source and destination addresses belong to different address families (one Is4, the other not). gopacket cannot build a coherent IPv4 or IPv6 layer from mixed inputs, and the checksum/protocol selection logic (getIPProtocolNumber with Is6) would be ambiguous. Notably, an unset netip.Addr is neither Is4 nor Is6, so leaving DstIP (or SrcIP) at its zero value while setting the other to a real address also trips this check.

Source

Thrown at client/firewall/uspfilter/tracer.go:139

	pktLayers := []gopacket.SerializableLayer{ipLayer}

	transportLayer, err := p.buildTransportLayer(ipLayer)
	if err != nil {
		return nil, err
	}
	pktLayers = append(pktLayers, transportLayer...)

	if p.PayloadSize > 0 {
		payload := make([]byte, p.PayloadSize)
		pktLayers = append(pktLayers, gopacket.Payload(payload))
	}

	return serializePacket(pktLayers)
}

func (p *PacketBuilder) buildIPLayer() (gopacket.SerializableLayer, error) {
	if p.SrcIP.Is4() != p.DstIP.Is4() {
		return nil, fmt.Errorf("mixed address families: src=%s dst=%s", p.SrcIP, p.DstIP)
	}
	proto := getIPProtocolNumber(p.Protocol, p.SrcIP.Is6())
	if p.SrcIP.Is6() {
		return &layers.IPv6{
			Version:    6,
			HopLimit:   64,
			NextHeader: proto,
			SrcIP:      p.SrcIP.AsSlice(),
			DstIP:      p.DstIP.AsSlice(),
		}, nil
	}
	return &layers.IPv4{
		Version:  4,
		TTL:      64,
		Protocol: proto,
		SrcIP:    p.SrcIP.AsSlice(),
		DstIP:    p.DstIP.AsSlice(),
	}, nil

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Validate both addresses before calling Build: ensure SrcIP.IsValid(), DstIP.IsValid(), and SrcIP.Is4() == DstIP.Is4()
  2. Normalize inputs with Unmap() at the boundary so a v4-mapped v6 address becomes plain v4 and families line up
  3. Default the unset side explicitly (e.g. copy the interface family) instead of leaving the zero value
  4. Fail early at the API edge of your tooling with a clear message about which side is missing

Example fix

// before
b := &tracer.PacketBuilder{SrcIP: localV4, DstIP: peerAddr, ...}
data, err := b.Build()

// after
if !b.SrcIP.IsValid() || !b.DstIP.IsValid() || b.SrcIP.Is4() != b.DstIP.Is4() {
    return fmt.Errorf("trace endpoints must be valid and same-family: src=%s dst=%s", b.SrcIP, b.DstIP)
}
peerAddr = peerAddr.Unmap()
data, err := b.Build()
Defensive patterns

Strategy: validation

Validate before calling

func sameFamily(a, b netip.Addr) bool {
    return a.IsValid() && b.IsValid() && a.Is4() == b.Is4()
}

if !sameFamily(b.SrcIP, b.DstIP) {
    return fmt.Errorf("trace needs same-family endpoints: %s -> %s", b.SrcIP, b.DstIP)
}
trace, err := m.TracePacketFromBuilder(b)

Type guard

func validTraceAddrs(src, dst netip.Addr) bool {
    return src.IsValid() && dst.IsValid() && src.Is4() == dst.Is4()
}

Try / catch

if _, err := m.TracePacketFromBuilder(b); err != nil {
    if strings.Contains(err.Error(), "mixed address families") {
        // fix address selection (e.g. pick the v6 peer address for a v6 trace)
    }
}

Prevention

When it happens

Trigger: Building a trace packet with an IPv4 SrcIP and an IPv6 DstIP or vice versa; omitting one of the two addresses so it stays as the invalid zero-value netip.Addr; copying addresses from a config map where one side was parsed with a different family than the other.

Common situations: Writing new trace/troubleshooting code on top of the uspfilter tracer and forgetting to set both endpoints; mixing a peer's v6 overlay address with the local v4 interface address in diagnostics tooling; parsing one address with netip.MustParseAddr and the other from a legacy net.IP slice of 16-byte v4-mapped form that was never Unmap()-ed.

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/3cab59bc078f8f7f. Report an issue: GitHub.