netbirdio/netbird · error

unclosed parenthesis

Error message

unclosed parenthesis

What it means

Parse error from the recursive-descent parser of NetBird's capture filter language (util/capture/filter.go). After consuming '(' and successfully parsing the inner or-expression, parser.expect(")") fails because the token stream ended or diverged, so parseUnary returns 'unclosed parenthesis'.

Source

Thrown at util/capture/filter.go:374

}

func (p *parser) parseUnary() (exprNode, error) {
	switch p.peek() {
	case "not":
		p.next()
		inner, err := p.parseUnary()
		if err != nil {
			return nil, err
		}
		return nodeNot(inner), nil
	case "(":
		p.next()
		inner, err := p.parseOr()
		if err != nil {
			return nil, err
		}
		if err := p.expect(")"); err != nil {
			return nil, fmt.Errorf("unclosed parenthesis")
		}
		return inner, nil
	default:
		return p.parseAtom()
	}
}

func (p *parser) parseAtom() (exprNode, error) {
	tok := p.next()
	if tok == "" {
		return nil, fmt.Errorf("unexpected end of expression")
	}

	switch tok {
	case "host":
		addr, err := p.parseAddr()
		if err != nil {
			return nil, fmt.Errorf("host: %w", err)

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Add the missing ')' so every '(' is closed; use editor bracket matching or a paren-count lint on the string
  2. Build nested groups programmatically and close each group in the same scope that opened it
  3. Compile the filter once at config-load time (before starting the capture) so the error surfaces with full context

Example fix

// before
filter := "host 10.0.0.1 and (port 443 or port 8443"

// after
filter := "host 10.0.0.1 and (port 443 or port 8443)"
Defensive patterns

Strategy: validation

Validate before calling

func balancedParens(expr string) bool {
	depth := 0
	for _, r := range expr {
		switch r {
		case '(':
			depth++
		case ')':
			depth--
			if depth < 0 {
				return false
			}
		}
	}
	return depth == 0
}

if !balancedParens(filter) {
	return fmt.Errorf("filter has unbalanced parentheses")
}

Type guard

func isValidFilterExpr(s string) bool {
	return strings.TrimSpace(s) != "" && balancedParens(s)
}

Try / catch

Parse the filter at config-load time and wrap the parse error with the offending expression; show the string with a caret at the position of the last unmatched '('.

Prevention

When it happens

Trigger: A filter with more '(' than ')': 'host 10.0.0.1 and (port 443', '((tcp or udp)', or nested groups like 'not (tcp and (port 53' where a final ')' was never typed.

Common situations: Hand-written packet-capture filter strings; programmatically composed filters where a conditional clause appends '(' without a matching ')'; long one-liners pasted from issues or notes.

Related errors


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