package model import ( "fmt" "strings" ) // Subnet access modes: who a subnet answers. const ( // AccessDenyList answers every client except those on the deny list // (the default, stored as ""). AccessDenyList = "deny-list" // AccessAllowList answers only clients on the allow list or with a // reservation in the subnet, and not on the deny list. AccessAllowList = "allow-list" ) // ClientMatch is an entry of a subnet's allow or deny list: a MAC address // ("aa:bb:cc:dd:ee:ff"), a MAC address prefix for a maker's devices // ("aa:bb:cc:*"), or a client identifier ("id 01aabbccddeeff"). type ClientMatch struct { Match string `json:"match"` Comment string `json:"comment,omitempty"` } // ParseClientMatch reads and normalizes an allow or deny list entry. func ParseClientMatch(s string) (string, error) { s = strings.ToLower(strings.TrimSpace(s)) if rest, ok := strings.CutPrefix(s, "id"); ok && (strings.HasPrefix(rest, " ") || strings.HasPrefix(rest, ":") || strings.HasPrefix(rest, "=")) { id, err := NormalizeClientID(rest[1:]) if err != nil { return "", err } return "id " + id, nil } if prefix, ok := strings.CutSuffix(s, "*"); ok { prefix = strings.TrimRight(strings.ReplaceAll(prefix, "-", ":"), ":") parts := strings.Split(prefix, ":") if prefix == "" || len(parts) > 5 { return "", fmt.Errorf("%q: a MAC address prefix has 1 to 5 bytes, as aa:bb:cc:*", s) } for i, p := range parts { if len(p) == 1 { p = "0" + p } if len(p) != 2 || strings.Trim(p, "0123456789abcdef") != "" { return "", fmt.Errorf("%q: a MAC address prefix is written as aa:bb:cc:*", s) } parts[i] = p } return strings.Join(parts, ":") + ":*", nil } mac, err := NormalizeMAC(s) if err != nil { return "", fmt.Errorf("%q is not a MAC address (aa:bb:cc:dd:ee:ff), a prefix (aa:bb:cc:*) or a client ID (id 01aabbccddeeff)", s) } return mac, nil } // Matches reports whether the entry is the client's: mac as // "aa:bb:cc:dd:ee:ff", id the client identifier in hex ("" for none). func (m ClientMatch) Matches(mac, id string) bool { switch { case strings.HasPrefix(m.Match, "id "): return id != "" && m.Match[3:] == id case strings.HasSuffix(m.Match, "*"): return mac != "" && strings.HasPrefix(mac, m.Match[:len(m.Match)-1]) } return mac != "" && m.Match == mac } // Admits says whether the subnet answers a client, and if not why. resv // tells whether the client has an (enabled) reservation in the subnet. func (s *Subnet) Admits(mac, id string, resv bool) (bool, string) { for _, m := range s.Deny { if m.Matches(mac, id) { why := "on the deny list of " + s.Label() if m.Comment != "" { why += " (" + m.Comment + ")" } return false, why } } if s.Access != AccessAllowList || resv { return true, "" } for _, m := range s.Allow { if m.Matches(mac, id) { return true, "" } } return false, "not on the allow list of " + s.Label() }