package namedconf import ( "errors" "fmt" "net/netip" "strconv" "strings" ) // AMKind is the kind of an address match list element. type AMKind int const ( AMPrefix AMKind = iota // an address or prefix AMKey // key AMName // an ACL name (or built-in ACL) AMNested // { ... } AMOther // anything else (e.g. geoip), passed through ) // Builtin ACL names. var BuiltinACLs = map[string]bool{"any": true, "none": true, "localhost": true, "localnets": true} // AMElement is one element of an address match list. type AMElement struct { Negated bool Kind AMKind Prefix netip.Prefix // AMPrefix Name string // AMKey, AMName Nested []AMElement // AMNested Raw string // AMOther } // String returns the element in canonical named.conf syntax, without the // trailing semicolon. func (e AMElement) String() string { var s string switch e.Kind { case AMPrefix: if e.Prefix.IsSingleIP() { s = e.Prefix.Addr().String() } else { s = e.Prefix.String() } case AMKey: s = "key " + quoteIfNeeded(e.Name) case AMName: s = quoteIfNeeded(e.Name) case AMNested: parts := make([]string, len(e.Nested)) for i, n := range e.Nested { parts[i] = n.String() + ";" } s = "{ " + strings.Join(parts, " ") + " }" case AMOther: s = e.Raw } if e.Negated { return "!" + s } return s } // IsBuiltin reports whether the element is one of the built-in ACL names. func (e AMElement) IsBuiltin() bool { return e.Kind == AMName && BuiltinACLs[e.Name] } func quoteIfNeeded(s string) string { for i := 0; i < len(s); i++ { c := s[i] if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.') { return QuoteString(s) } } if s == "" { return `""` } return s } // ParseAMList parses the statements of an address match list block. func ParseAMList(b *Block) ([]AMElement, error) { var out []AMElement for _, st := range b.Stmts { e, err := parseAMItems(st.Items) if err != nil { return nil, &Error{Pos: st.Pos, Msg: err.Error()} } out = append(out, e) } return out, nil } // ParseAMElement parses a single element given as text, such as // "!192.168.1.0/24", "key xfer" or "trusted". func ParseAMElement(s string) (AMElement, error) { s = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(s), ";")) if s == "" { return AMElement{}, errors.New("empty element") } st, err := ParseStatement(s) if err != nil { var pe *Error if errors.As(err, &pe) { return AMElement{}, errors.New(pe.Msg) } return AMElement{}, err } return parseAMItems(st.Items) } // ParseAMListString parses a whole list given as "{ a; b; }" or "a; b". func ParseAMListString(s string) ([]AMElement, error) { s = strings.TrimSpace(s) s = strings.TrimSuffix(s, ";") s = strings.TrimSpace(s) if !strings.HasPrefix(s, "{") { s = "{ " + s + "; }" } st, err := ParseStatement("x " + s) if err != nil { return nil, err } b := st.FirstBlock() if b == nil || len(st.Items) != 2 { return nil, errors.New("malformed address match list") } return ParseAMList(b) } func parseAMItems(items []Item) (AMElement, error) { var e AMElement if len(items) > 0 && items[0].Bang { e.Negated = true items = items[1:] } if len(items) == 0 { return e, errors.New("empty address match element") } first := items[0] if first.Block != nil { if len(items) != 1 { return e, errors.New("unexpected text after nested address match list") } nested, err := ParseAMList(first.Block) if err != nil { return e, err } e.Kind = AMNested e.Nested = nested return e, nil } if first.Bang { return e, errors.New("double negation") } word := first.Value.Text lw := strings.ToLower(word) if !first.Value.Quoted && lw == "key" { if len(items) != 2 || items[1].Value == nil { return e, errors.New("'key' must be followed by a key name") } e.Kind = AMKey e.Name = strings.ToLower(strings.TrimSuffix(items[1].Value.Text, ".")) return e, nil } if len(items) > 1 { // geoip and other multi-word elements are passed through. if lw == "geoip" { e.Kind = AMOther e.Raw = Compact(items) return e, nil } return e, fmt.Errorf("unexpected text after %q", word) } if !first.Value.Quoted || BuiltinACLs[lw] { if p, ok, err := parsePrefix(word); ok { if err != nil { return e, err } e.Kind = AMPrefix e.Prefix = p return e, nil } } if BuiltinACLs[lw] { e.Kind = AMName e.Name = lw return e, nil } if err := CheckACLName(word); err != nil { return e, err } e.Kind = AMName e.Name = word return e, nil } // CheckACLName validates the name of a user-defined ACL. func CheckACLName(name string) error { if name == "" { return errors.New("ACL name is empty") } if len(name) > 64 { return errors.New("ACL name is longer than 64 characters") } for i := 0; i < len(name); i++ { c := name[i] if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.') { return fmt.Errorf("ACL name %q may only contain letters, digits, '-', '_' and '.'", name) } } if c := name[0]; c >= '0' && c <= '9' { // Avoid names that look like addresses. if _, ok, _ := parsePrefix(name); ok { return fmt.Errorf("ACL name %q looks like an address", name) } } return nil } // parsePrefix parses an address or prefix as BIND does, including the IPv4 // shorthand where trailing zero octets are omitted ("10/8", "192.168/16"). // ok is false if s doesn't look like an address at all. Prefixes with host // bits set are rejected, matching named's "address/prefix length mismatch". func parsePrefix(s string) (netip.Prefix, bool, error) { addrPart, lenPart, hasLen := strings.Cut(s, "/") if strings.Contains(addrPart, ":") { a, err := netip.ParseAddr(addrPart) if err != nil { return netip.Prefix{}, true, fmt.Errorf("invalid IPv6 address %q", addrPart) } if a.Zone() != "" { return netip.Prefix{}, true, fmt.Errorf("zoned IPv6 address %q is not allowed", addrPart) } return finishPrefix(s, a, lenPart, hasLen, 128) } // IPv4 (possibly shorthand). Must be digits and dots only. if addrPart == "" { return netip.Prefix{}, false, nil } for i := 0; i < len(addrPart); i++ { if c := addrPart[i]; !(c >= '0' && c <= '9' || c == '.') { return netip.Prefix{}, false, nil } } octets := strings.Split(addrPart, ".") if len(octets) > 4 { return netip.Prefix{}, true, fmt.Errorf("invalid IPv4 address %q", addrPart) } if len(octets) < 4 && !hasLen { return netip.Prefix{}, true, fmt.Errorf("invalid IPv4 address %q (abbreviated addresses need a prefix length)", addrPart) } var b [4]byte for i, o := range octets { if o == "" || len(o) > 3 { return netip.Prefix{}, true, fmt.Errorf("invalid IPv4 address %q", addrPart) } n, err := strconv.Atoi(o) if err != nil || n > 255 { return netip.Prefix{}, true, fmt.Errorf("invalid IPv4 address %q", addrPart) } b[i] = byte(n) } return finishPrefix(s, netip.AddrFrom4(b), lenPart, hasLen, 32) } func finishPrefix(orig string, a netip.Addr, lenPart string, hasLen bool, max int) (netip.Prefix, bool, error) { bits := max if hasLen { n, err := strconv.Atoi(lenPart) if err != nil || n < 0 || n > max { return netip.Prefix{}, true, fmt.Errorf("invalid prefix length in %q", orig) } bits = n } p := netip.PrefixFrom(a, bits) if p.Masked() != p { return netip.Prefix{}, true, fmt.Errorf("%q: address/prefix length mismatch (host bits are set; did you mean %s?)", orig, p.Masked()) } return p, true, nil } // ParseAddr parses a plain IP address (no prefix) as used by forwarders, // primaries and similar options. func ParseAddr(s string) (netip.Addr, error) { a, err := netip.ParseAddr(s) if err != nil { return netip.Addr{}, fmt.Errorf("invalid IP address %q", s) } return a, nil } // AMRefs returns the ACL names and key names referenced by elements, // including nested lists. Built-in ACLs are not included. func AMRefs(elems []AMElement) (acls, keys []string) { var walk func([]AMElement) walk = func(es []AMElement) { for _, e := range es { switch e.Kind { case AMName: if !BuiltinACLs[e.Name] { acls = append(acls, e.Name) } case AMKey: keys = append(keys, e.Name) case AMNested: walk(e.Nested) } } } walk(elems) return acls, keys } // Shadowed returns human-readable warnings for elements that can never // match because an earlier element always matches first (BIND uses first // match). Only address/prefix elements and "any" are considered. func Shadowed(elems []AMElement) []string { var warns []string for i, e := range elems { if e.Kind != AMPrefix && e.Kind != AMName { continue } for j := 0; j < i; j++ { prev := elems[j] if prev.Kind == AMName && prev.Name == "any" { warns = append(warns, fmt.Sprintf("element %q never matches: %q before it already matches every address", e.String(), prev.String())) break } if prev.Kind == AMPrefix && e.Kind == AMPrefix && prev.Prefix.Addr().Is4() == e.Prefix.Addr().Is4() && prev.Prefix.Bits() <= e.Prefix.Bits() && prev.Prefix.Contains(e.Prefix.Addr()) { warns = append(warns, fmt.Sprintf("element %q never matches: %q before it already covers it (first match wins; put the more specific element first)", e.String(), prev.String())) break } } } return warns } // ContainsAny reports whether elems contain a non-negated "any" before any // negated element would restrict it, which makes the list match everyone. // It is used for security lints (open resolvers, open transfers). func ContainsAny(elems []AMElement) bool { for _, e := range elems { if e.Negated { return false } if e.Kind == AMName && e.Name == "any" { return true } if e.Kind == AMPrefix && e.Prefix.Bits() == 0 { return true } } return false }