netbirdio/netbird · error

invalid translated port: %v

Error message

invalid translated port: %v

What it means

handleTranslatedPort dispatches on rule.TranslatedPort: two values with IsRange map to a range, zero values to address-only DNAT, one value to a single port. Any other shape (three or more values, or IsRange with one value) falls to default and returns 'invalid translated port' printing the whole Port struct. It is a deterministic validation error raised while building expressions, before any netlink message is queued.

Source

Thrown at client/firewall/nftables/router_linux.go:1668

		Exprs:    dnatExprs,
		UserData: []byte(ruleKey + dnatSuffix),
	}
	r.conn.AddRule(dnatRule)
	r.rules[ruleKey+dnatSuffix] = dnatRule

	return nil
}

func (r *router) handleTranslatedPort(rule firewall.ForwardRule) ([]expr.Any, uint32, uint32, error) {
	switch {
	case rule.TranslatedPort.IsRange && len(rule.TranslatedPort.Values) == 2:
		return r.handlePortRange(rule)
	case len(rule.TranslatedPort.Values) == 0:
		return r.handleAddressOnly(rule)
	case len(rule.TranslatedPort.Values) == 1:
		return r.handleSinglePort(rule)
	default:
		return nil, 0, 0, fmt.Errorf("invalid translated port: %v", rule.TranslatedPort)
	}
}

func (r *router) handlePortRange(rule firewall.ForwardRule) ([]expr.Any, uint32, uint32, error) {
	exprs := []expr.Any{
		&expr.Immediate{
			Register: 1,
			Data:     rule.TranslatedAddress.AsSlice(),
		},
		&expr.Immediate{
			Register: 2,
			Data:     binaryutil.BigEndian.PutUint16(rule.TranslatedPort.Values[0]),
		},
		&expr.Immediate{
			Register: 3,
			Data:     binaryutil.BigEndian.PutUint16(rule.TranslatedPort.Values[1]),
		},
	}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Validate the TranslatedPort shape before building rules: exactly 0, 1, or (IsRange with exactly 2) values.
  2. Map many ports to one translated port by emitting one ForwardRule per port, not one rule with 3+ values.
  3. Always set IsRange=true when supplying two boundary ports.
  4. Print the rule's Port struct (the %v payload shows IsRange/Values) to find the misbehaving producer.

Example fix

// before: two ports without IsRange -> default case -> error
rule.TranslatedPort = firewall.Port{Values: []uint16{8080, 8443}}

// after: either a proper range
rule.TranslatedPort = firewall.Port{IsRange: true, Values: []uint16{8080, 8443}}
// or one rule per port
ruleA.TranslatedPort = firewall.Port{Values: []uint16{8080}}
ruleB.TranslatedPort = firewall.Port{Values: []uint16{8443}}
Defensive patterns

Strategy: validation

Validate before calling

func validTranslatedPort(p firewall.Port) bool {
    switch {
    case len(p.Values) == 0:
        return true // address-only DNAT
    case len(p.Values) == 1:
        return true
    case p.IsRange && len(p.Values) == 2:
        return p.Values[0] <= p.Values[1]
    }
    return false
}

// use before creating forward rules
if !validTranslatedPort(rule.TranslatedPort) {
    return fmt.Errorf("invalid translated port shape: %+v", rule.TranslatedPort)
}

Type guard

func isRangePort(p firewall.Port) bool {
    return p.IsRange && len(p.Values) == 2 && p.Values[0] <= p.Values[1]
}

Try / catch

if _, _, _, err := r.handleTranslatedPort(rule); err != nil {
    // deterministic config error: fix the producer, do not retry
    return fmt.Errorf("rejecting forward rule with malformed translated port %+v: %w", rule.TranslatedPort, err)
}

Prevention

When it happens

Trigger: A ForwardRule whose TranslatedPort.Values has length 3 or more, or IsRange=true with a single value, reaches addDnatRedirect: hand-built rules or a management payload that filled several ports without setting IsRange.

Common situations: Port-forwarding APIs that forward a list of ports to one translated port; version skew introducing new port shapes; test fixtures writing Port{Values: []uint16{80, 8080}} and forgetting IsRange.

Related errors


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