package model import ( "fmt" "sort" "strconv" "strings" ) // ServiceEntry is a parsed service entry: ports of a transport protocol // ("tcp/22", "udp/60000-61000") or an ICMP type ("icmp/echo-request", // "icmpv6/packet-too-big"). type ServiceEntry struct { Proto string // tcp, udp, sctp, icmp, icmpv6 // Ports: "22" or "1000-2000" (transport protocols). Ports string // Type: the ICMP type's name (icmp, icmpv6). Type string } func (e ServiceEntry) String() string { if e.Type != "" { return e.Proto + "/" + e.Type } return e.Proto + "/" + e.Ports } // IsICMP reports whether the entry is an ICMP or ICMPv6 type. func (e ServiceEntry) IsICMP() bool { return e.Proto == "icmp" || e.Proto == "icmpv6" } // ICMP types nftables knows, by protocol. var icmpTypes = map[string][]string{ "icmp": {"echo-reply", "destination-unreachable", "source-quench", "redirect", "echo-request", "router-advertisement", "router-solicitation", "time-exceeded", "parameter-problem", "timestamp-request", "timestamp-reply", "info-request", "info-reply", "address-mask-request", "address-mask-reply"}, "icmpv6": {"destination-unreachable", "packet-too-big", "time-exceeded", "parameter-problem", "echo-request", "echo-reply", "mld-listener-query", "mld-listener-report", "mld-listener-done", "mld-listener-reduction", "nd-router-solicit", "nd-router-advert", "nd-neighbor-solicit", "nd-neighbor-advert", "nd-redirect", "router-renumbering", "ind-neighbor-solicit", "ind-neighbor-advert", "mld2-listener-report"}, } // ParseServiceEntry reads "tcp/22", "udp/1000-2000", "tcp/ssh"-style // entries are not accepted (use the service's name instead). func ParseServiceEntry(s string) (ServiceEntry, error) { s = strings.ToLower(strings.TrimSpace(s)) proto, rest, ok := strings.Cut(s, "/") if !ok { return ServiceEntry{}, fmt.Errorf("%q is not a service entry (tcp/22, udp/1000-2000, icmp/echo-request) or a service name", s) } switch proto { case "tcp", "udp", "sctp": ports, err := NormalizePorts(rest) if err != nil { return ServiceEntry{}, fmt.Errorf("%q: %v", s, err) } return ServiceEntry{Proto: proto, Ports: ports}, nil case "icmp", "icmpv6": for _, t := range icmpTypes[proto] { if t == rest { return ServiceEntry{Proto: proto, Type: rest}, nil } } return ServiceEntry{}, fmt.Errorf("%q: unknown %s type (%s)", s, proto, strings.Join(icmpTypes[proto], ", ")) } return ServiceEntry{}, fmt.Errorf("%q: the protocol is tcp, udp, sctp, icmp or icmpv6", s) } // IsServiceEntry reports whether a reference is an entry ("tcp/22") // rather than a service name. func IsServiceEntry(ref string) bool { return strings.Contains(ref, "/") } // NormalizePorts checks a port or range ("22", "1000-2000"). func NormalizePorts(s string) (string, error) { s = strings.TrimSpace(s) a, b, isRange := strings.Cut(s, "-") lo, err1 := strconv.Atoi(strings.TrimSpace(a)) hi := lo var err2 error if isRange { hi, err2 = strconv.Atoi(strings.TrimSpace(b)) } if err1 != nil || err2 != nil || lo < 1 || hi > 65535 || hi < lo { return "", fmt.Errorf("%q is not a port (1-65535) or a range (1000-2000)", s) } if lo == hi { return strconv.Itoa(lo), nil } return fmt.Sprintf("%d-%d", lo, hi), nil } // PortRange returns a port spec's bounds. func PortRange(s string) (lo, hi int) { a, b, isRange := strings.Cut(s, "-") lo, _ = strconv.Atoi(a) hi = lo if isRange { hi, _ = strconv.Atoi(b) } return lo, hi } // builtinServices are the services every configuration has. var builtinServices = map[string][]string{ "ssh": {"tcp/22"}, "dns": {"udp/53", "tcp/53"}, "dot": {"tcp/853"}, "http": {"tcp/80"}, "https": {"tcp/443", "udp/443"}, "smtp": {"tcp/25"}, "submission": {"tcp/587"}, "smtps": {"tcp/465"}, "imaps": {"tcp/993"}, "ntp": {"udp/123"}, "dhcp": {"udp/67"}, "dhcpv6": {"udp/547"}, "wireguard": {"udp/51820"}, "openvpn": {"udp/1194"}, "ping": {"icmp/echo-request", "icmpv6/echo-request"}, "snmp": {"udp/161"}, "syslog": {"udp/514"}, "mdns": {"udp/5353"}, "rdp": {"tcp/3389"}, "ipsec": {"udp/500", "udp/4500"}, } // BuiltinService returns a built-in service by name (nil if there is // none). func BuiltinService(name string) *Service { e, ok := builtinServices[strings.ToLower(name)] if !ok { return nil } return &Service{Name: strings.ToLower(name), Entries: append([]string(nil), e...), Builtin: true} } // BuiltinServices lists the built-in services by name. func BuiltinServices() []*Service { var out []*Service for n := range builtinServices { out = append(out, BuiltinService(n)) } sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) return out }