glider/netset/manager.go
nadoo 43552b60c9
Some checks failed
Build / Build (push) Has been cancelled
netset: add nftables set support
2026-07-15 23:41:21 +08:00

201 lines
4.6 KiB
Go

package netset
import (
"errors"
"fmt"
"net/netip"
"strings"
"github.com/nadoo/glider/rule"
)
type addressSet interface {
AddAddr(netip.Addr) error
AddPrefix(netip.Prefix) error
}
type setPair struct {
label string
ipv4 addressSet
ipv6 addressSet
}
func (p *setPair) addAddr(ip netip.Addr) error {
if ip.Is4() {
return p.ipv4.AddAddr(ip)
}
return p.ipv6.AddAddr(ip)
}
func (p *setPair) addPrefix(prefix netip.Prefix) error {
if prefix.Addr().Is4() {
return p.ipv4.AddPrefix(prefix)
}
return p.ipv6.AddPrefix(prefix)
}
type backend interface {
IPSetPair(name string) (*setPair, error)
NFTSetPair(config nftSetConfig) (*setPair, error)
Close() error
}
// Manager manages the ipset and nftables sets associated with rules.
type Manager struct {
backend backend
domainSets map[string][]*setPair
pairs map[string]*setPair
}
// NewManager creates and populates all sets configured by rules.
func NewManager(rules []*rule.Config) (*Manager, error) {
if !hasSetConfig(rules) {
return nil, nil
}
b, err := newBackend()
if err != nil {
return nil, err
}
return newManager(rules, b)
}
func newManager(rules []*rule.Config, b backend) (*Manager, error) {
m := &Manager{
backend: b,
domainSets: make(map[string][]*setPair),
pairs: make(map[string]*setPair),
}
for _, config := range rules {
if err := m.addRule(config); err != nil {
return nil, errors.Join(err, b.Close())
}
}
return m, nil
}
func hasSetConfig(rules []*rule.Config) bool {
for _, config := range rules {
if config.IPSet != "" || config.NFTSet != "" {
return true
}
}
return false
}
func (m *Manager) addRule(config *rule.Config) error {
pairs := make([]*setPair, 0, 2)
if config.IPSet != "" {
pair, err := m.pair("ipset:"+config.IPSet, func() (*setPair, error) {
return m.backend.IPSetPair(config.IPSet)
})
if err != nil {
return fmt.Errorf("rule %q: initialize ipset %q: %w", config.RulePath, config.IPSet, err)
}
pairs = append(pairs, pair)
}
if config.NFTSet != "" {
nftConfig, err := parseNFTSetConfig(config.NFTSet)
if err != nil {
return fmt.Errorf("rule %q: %w", config.RulePath, err)
}
pair, err := m.pair("nftset:"+nftConfig.key(), func() (*setPair, error) {
return m.backend.NFTSetPair(nftConfig)
})
if err != nil {
return fmt.Errorf("rule %q: initialize nftset %q: %w", config.RulePath, nftConfig.key(), err)
}
pairs = append(pairs, pair)
}
for _, item := range config.IP {
ip, err := netip.ParseAddr(item)
if err != nil {
return fmt.Errorf("rule %q: parse set IP %q: %w", config.RulePath, item, err)
}
for _, pair := range pairs {
if err := pair.addAddr(ip); err != nil {
return fmt.Errorf("rule %q: add IP %q to %s: %w", config.RulePath, item, pair.label, err)
}
}
}
for _, item := range config.CIDR {
prefix, err := netip.ParsePrefix(item)
if err != nil {
return fmt.Errorf("rule %q: parse set CIDR %q: %w", config.RulePath, item, err)
}
for _, pair := range pairs {
if err := pair.addPrefix(prefix); err != nil {
return fmt.Errorf("rule %q: add CIDR %q to %s: %w", config.RulePath, item, pair.label, err)
}
}
}
for _, domain := range config.Domain {
domain = strings.ToLower(domain)
for _, pair := range pairs {
m.domainSets[domain] = appendPair(m.domainSets[domain], pair)
}
}
return nil
}
func (m *Manager) pair(key string, create func() (*setPair, error)) (*setPair, error) {
if pair, ok := m.pairs[key]; ok {
return pair, nil
}
pair, err := create()
if err != nil {
return nil, err
}
m.pairs[key] = pair
return pair, nil
}
func appendPair(pairs []*setPair, pair *setPair) []*setPair {
for _, item := range pairs {
if item == pair {
return pairs
}
}
return append(pairs, pair)
}
// AddDomainIP updates all sets associated with domain or one of its parents.
func (m *Manager) AddDomainIP(domain string, ip netip.Addr) error {
if !ip.IsValid() {
return errors.New("netset: invalid DNS answer address")
}
domain = strings.ToLower(domain)
seen := make(map[*setPair]struct{})
var errs []error
for i := len(domain); i != -1; {
i = strings.LastIndexByte(domain[:i], '.')
for _, pair := range m.domainSets[domain[i+1:]] {
if _, ok := seen[pair]; ok {
continue
}
seen[pair] = struct{}{}
if err := pair.addAddr(ip); err != nil {
errs = append(errs, fmt.Errorf("add DNS answer %s for %q to %s: %w", ip, domain, pair.label, err))
}
}
}
return errors.Join(errs...)
}
// Close closes all netlink sockets owned by the manager.
func (m *Manager) Close() error {
if m == nil || m.backend == nil {
return nil
}
return m.backend.Close()
}