From 43552b60c9c8a00a68219d0dc1202e9899cb5fa5 Mon Sep 17 00:00:00 2001 From: nadoo <287492+nadoo@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:41:21 +0800 Subject: [PATCH] netset: add nftables set support --- README.md | 8 +- config/README.md | 16 +- .../README.md | 23 +- config/glider.conf.example | 2 +- config/rules.d/office.rule.example | 12 +- dns/client.go | 4 +- go.mod | 2 +- go.sum | 4 +- ipset/ipset_linux.go | 78 ------- ipset/ipset_other.go | 23 -- main.go | 17 +- netset/config.go | 92 ++++++++ netset/manager.go | 200 ++++++++++++++++++ netset/manager_linux.go | 130 ++++++++++++ netset/manager_other.go | 13 ++ rule/config.go | 2 + systemd/glider@.service | 4 +- 17 files changed, 504 insertions(+), 126 deletions(-) delete mode 100644 ipset/ipset_linux.go delete mode 100644 ipset/ipset_other.go create mode 100644 netset/config.go create mode 100644 netset/manager.go create mode 100644 netset/manager_linux.go create mode 100644 netset/manager_other.go diff --git a/README.md b/README.md index 3aefeed..57cdb97 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Actions Status](https://img.shields.io/github/actions/workflow/status/nadoo/glider/build.yml?branch=dev&style=flat-square)](https://github.com/nadoo/glider/actions) [![DockerHub](https://img.shields.io/docker/image-size/nadoo/glider?color=blue&label=docker&style=flat-square)](https://hub.docker.com/r/nadoo/glider) -glider is a forward proxy with multiple protocols support, and also a dns/dhcp server with ipset management features. +glider is a forward proxy with multiple protocols support, and also a dns/dhcp server with ipset and nftables set management features. we can set up local listeners as proxy servers, and forward requests to internet via forwarders. @@ -28,10 +28,10 @@ we can set up local listeners as proxy servers, and forward requests to internet - dns over proxy - force upstream querying by tcp - association rules between dns and forwarder choosing - - association rules between dns and ipset + - association rules between dns and ipset/nftables sets - dns cache support - custom dns record -- IPSet management (linux kernel version >= 2.6.32): +- IPSet and nftables set management on Linux: - add ip/cidrs from rule files on startup - add resolved ips for domains from rule files by dns forwarding server - Serve http and socks5 on the same port @@ -541,7 +541,7 @@ glider -config CONFIG_PATH ## Links -- [ipset](https://github.com/nadoo/ipset): netlink ipset package for Go. +- [netset](https://github.com/nadoo/netset): netlink ipset and nftables set package for Go. - [conflag](https://github.com/nadoo/conflag): a drop-in replacement for Go's standard flag package with config file support. - [ArchLinux](https://archlinux.org/packages/extra/x86_64/glider): a great linux distribution with glider pre-built package. - [urlencode](https://www.w3schools.com/tags/ref_urlencode.asp): you should encode special characters in scheme url. e.g., `@`->`%40` diff --git a/config/README.md b/config/README.md index 1916ecb..409f583 100644 --- a/config/README.md +++ b/config/README.md @@ -73,14 +73,22 @@ checkinterval=30 # DNS SERVER for domains in this rule file dnsserver=208.67.222.222:53 -# IPSET MANAGEMENT -# ---------------- -# Create and mange ipset on linux based on destinations in rule files +# NETFILTER SET MANAGEMENT +# ------------------------ +# Create and manage ipset or nftables sets on linux based on destinations in rule files # - add ip/cidrs in rule files on startup -# - add resolved ips for domains in rule files by dns forwarding server +# - add resolved ips for domains in rule files by dns forwarding server # Usually used in transparent proxy mode on linux +# This creates glider for ipv4 and glider6 for ipv6 ipset=glider +# nftset uses [[FAMILY/]TABLE/]NAME. FAMILY defaults to inet and TABLE to glider. +# The nftables table must already exist. This also creates NAME and NAME6. +# nftset=glider +# nftset=custom_table/glider +# nftset=inet/custom_table/glider +# ipset and nftset can both be enabled in the same rule file during migration. + # YOU CAN SPECIFY DESTINATIONS TO USE THE ABOVE FORWARDERS # matches abc.com and *.abc.com domain=abc.com diff --git a/config/examples/9.transparent_proxy_without_dnsmasq/README.md b/config/examples/9.transparent_proxy_without_dnsmasq/README.md index 231f146..fc01bfc 100644 --- a/config/examples/9.transparent_proxy_without_dnsmasq/README.md +++ b/config/examples/9.transparent_proxy_without_dnsmasq/README.md @@ -6,7 +6,7 @@ PC Client -> Gateway with glider running(linux box) -> Upstream Forwarders -> In #### In this mode, glider will act as the following roles: 1. A transparent proxy server 2. A dns forwarding server -3. A ipset manager +3. An ipset or nftables set manager so you don't need any dns server in your network. @@ -76,6 +76,27 @@ iptables -t nat -I PREROUTING -p tcp -m set --match-set glider dst -j REDIRECT - iptables -t nat -I OUTPUT -p tcp -m set --match-set glider dst -j REDIRECT --to-ports 1081 ``` +#### Nftables alternative + +Use `nftset=glider` instead of `ipset=glider` in the rule file. It means +`inet/glider/glider` for IPv4 and `inet/glider/glider6` for IPv6. The table +must already exist: + +```bash +nft add table inet glider +``` + +Glider creates both interval sets and flushes them on startup. If nftables +rules are loaded before glider starts, create compatible sets first: + +```bash +nft 'add set inet glider glider { type ipv4_addr; flags interval; }' +nft 'add set inet glider glider6 { type ipv6_addr; flags interval; }' +nft 'add chain inet glider prerouting { type nat hook prerouting priority dstnat; policy accept; }' +nft 'add rule inet glider prerouting ip daddr @glider tcp redirect to :1081' +nft 'add rule inet glider prerouting ip6 daddr @glider6 tcp redirect to :1081' +``` + #### Server DNS settings Set server's nameserver to glider: ```bash diff --git a/config/glider.conf.example b/config/glider.conf.example index f294d2a..4c51582 100644 --- a/config/glider.conf.example +++ b/config/glider.conf.example @@ -3,7 +3,7 @@ # / /`_ | | | | | | \ | |_ | |_) # \_\_/ |_|__ |_| |_|_/ |_|__ |_| \ # -# Glider is a forward proxy with multiple protocols support, and also a dns forwarding server with ipset management features(like dnsmasq). +# Glider is a forward proxy with multiple protocols support, and also a dns forwarding server with ipset and nftables set management features(like dnsmasq). # # We can set up local listeners as proxy, and forward requests to internet via forwarders. # diff --git a/config/rules.d/office.rule.example b/config/rules.d/office.rule.example index 16c59c1..3d6a417 100644 --- a/config/rules.d/office.rule.example +++ b/config/rules.d/office.rule.example @@ -20,15 +20,19 @@ checkinterval=30 # DNS SERVER for domains in this rule file dnsserver=208.67.222.222:53 -# IPSET MANAGEMENT -# ---------------- -# Create and mange ipset on linux based on destinations in rule files +# NETFILTER SET MANAGEMENT +# ------------------------ +# Create and manage ipset or nftables sets on linux based on destinations in rule files # - add ip/cidrs in rule files on startup -# - add resolved ips for domains in rule files by dns forwarding server +# - add resolved ips for domains in rule files by dns forwarding server # Usually used in transparent proxy mode on linux # Note: this will create 2 ipsets, glider for ipv4 and glider6 for ipv6 ipset=glider +# nftset uses [[FAMILY/]TABLE/]NAME. FAMILY defaults to inet and TABLE to glider. +# The nftables table must already exist. This creates glider and glider6. +# nftset=glider + # DESTINATIONS # ------------ # ALL destinations matches the following rules will be forward using forwarders specified above diff --git a/dns/client.go b/dns/client.go index e82f293..4e24546 100644 --- a/dns/client.go +++ b/dns/client.go @@ -143,7 +143,9 @@ func (c *Client) extractAnswer(resp *Message) ([]string, int) { if answer.TYPE == QTypeA || answer.TYPE == QTypeAAAA { if answer.IP.IsValid() && !answer.IP.IsUnspecified() { for _, h := range c.handlers { - h(resp.Question.QNAME, answer.IP) + if err := h(resp.Question.QNAME, answer.IP); err != nil { + log.F("[dns] answer handler error for %s/%s: %s", resp.Question.QNAME, answer.IP, err) + } } ips = append(ips, answer.IP.String()) } diff --git a/go.mod b/go.mod index 43f8121..a43cfdc 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da github.com/insomniacslk/dhcp v0.0.0-20260603135910-a415979eb11e github.com/nadoo/conflag v0.3.1 - github.com/nadoo/ipset v0.5.0 + github.com/nadoo/netset v0.1.0 github.com/xtaci/kcp-go/v5 v5.6.72 golang.org/x/crypto v0.54.0 golang.org/x/sys v0.47.0 diff --git a/go.sum b/go.sum index f95cc30..8c42c62 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,8 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/nadoo/conflag v0.3.1 h1:4pHkLIz8PUsfg6ajNYRRSY3bt6m2LPsu6KOzn5uIXQw= github.com/nadoo/conflag v0.3.1/go.mod h1:dzFfDUpXdr2uS2oV+udpy5N2vfNOu/bFzjhX1WI52co= -github.com/nadoo/ipset v0.5.0 h1:5GJUAuZ7ITQQQGne5J96AmFjRtI8Avlbk6CabzYWVUc= -github.com/nadoo/ipset v0.5.0/go.mod h1:rYF5DQLRGGoQ8ZSWeK+6eX5amAuPqwFkWjhQlEITGJQ= +github.com/nadoo/netset v0.1.0 h1:FNdGCVtoSjBX0Ctzve3BWwTOSXRxK/oSPBzS3/SemY4= +github.com/nadoo/netset v0.1.0/go.mod h1:w2GPCnz+TxqH+HJIrhOEC4oZAeQ5Crefw4lToBIxEww= github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/ipset/ipset_linux.go b/ipset/ipset_linux.go deleted file mode 100644 index 44a4c5f..0000000 --- a/ipset/ipset_linux.go +++ /dev/null @@ -1,78 +0,0 @@ -package ipset - -import ( - "net/netip" - "strings" - "sync" - - "github.com/nadoo/ipset" - - "github.com/nadoo/glider/rule" -) - -// Manager struct. -type Manager struct { - domainSet sync.Map -} - -// NewManager returns a Manager -func NewManager(rules []*rule.Config) (*Manager, error) { - if err := ipset.Init(); err != nil { - return nil, err - } - - m := &Manager{} - sets := make(map[string]struct{}) - - for _, r := range rules { - if r.IPSet == "" { - continue - } - - if _, ok := sets[r.IPSet]; !ok { - sets[r.IPSet] = struct{}{} - ipset.Create(r.IPSet) - ipset.Flush(r.IPSet) - ipset.Create(r.IPSet+"6", ipset.OptIPv6()) - ipset.Flush(r.IPSet + "6") - } - - for _, domain := range r.Domain { - m.domainSet.Store(domain, r.IPSet) - } - for _, ip := range r.IP { - addToSet(r.IPSet, ip) - } - for _, cidr := range r.CIDR { - addToSet(r.IPSet, cidr) - } - } - - return m, nil -} - -// AddDomainIP implements the dns AnswerHandler function, used to update ipset according to domainSet rule. -func (m *Manager) AddDomainIP(domain string, ip netip.Addr) error { - domain = strings.ToLower(domain) - for i := len(domain); i != -1; { - i = strings.LastIndexByte(domain[:i], '.') - if setName, ok := m.domainSet.Load(domain[i+1:]); ok { - addAddrToSet(setName.(string), ip) - } - } - return nil -} - -func addToSet(s, item string) error { - if strings.IndexByte(item, '.') == -1 { - return ipset.Add(s+"6", item) - } - return ipset.Add(s, item) -} - -func addAddrToSet(s string, ip netip.Addr) error { - if ip.Is4() { - return ipset.AddAddr(s, ip) - } - return ipset.AddAddr(s+"6", ip) -} diff --git a/ipset/ipset_other.go b/ipset/ipset_other.go deleted file mode 100644 index 94e2692..0000000 --- a/ipset/ipset_other.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build !linux - -package ipset - -import ( - "errors" - "net/netip" - - "github.com/nadoo/glider/rule" -) - -// Manager struct -type Manager struct{} - -// NewManager returns a Manager -func NewManager(rules []*rule.Config) (*Manager, error) { - return nil, errors.New("ipset not supported on this os") -} - -// AddDomainIP implements the DNSAnswerHandler function -func (m *Manager) AddDomainIP(domain string, ip netip.Addr) error { - return errors.New("ipset not supported on this os") -} diff --git a/main.go b/main.go index 21862ae..9b6ea00 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( "time" "github.com/nadoo/glider/dns" - "github.com/nadoo/glider/ipset" + "github.com/nadoo/glider/netset" "github.com/nadoo/glider/pkg/log" "github.com/nadoo/glider/proxy" "github.com/nadoo/glider/rule" @@ -25,8 +25,15 @@ func main() { // global rule proxy pxy := rule.NewProxy(config.Forwards, &config.Strategy, config.rules) - // ipset manager - ipsetM, _ := ipset.NewManager(config.rules) + // ipset and nftables set manager + netsetM, _ := netset.NewManager(config.rules) + if netsetM != nil { + defer func() { + if err := netsetM.Close(); err != nil { + log.F("[netset] close error: %s", err) + } + }() + } // check and setup dns server if config.DNS != "" { @@ -46,8 +53,8 @@ func main() { // add a handler to update proxy rules when a domain resolved d.AddHandler(pxy.AddDomainIP) - if ipsetM != nil { - d.AddHandler(ipsetM.AddDomainIP) + if netsetM != nil { + d.AddHandler(netsetM.AddDomainIP) } d.Start() diff --git a/netset/config.go b/netset/config.go new file mode 100644 index 0000000..d920ef0 --- /dev/null +++ b/netset/config.go @@ -0,0 +1,92 @@ +package netset + +import ( + "fmt" + "strings" + + libnetset "github.com/nadoo/netset" +) + +const ( + defaultNFTFamily = "inet" + defaultNFTTable = "glider" + maxNFTNameLength = 256 +) + +type nftSetConfig struct { + family libnetset.Family + familyName string + table string + name string +} + +func parseNFTSetConfig(value string) (nftSetConfig, error) { + parts := strings.Split(strings.TrimSpace(value), "/") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + config := nftSetConfig{ + family: libnetset.FamilyINet, + familyName: defaultNFTFamily, + table: defaultNFTTable, + } + + switch len(parts) { + case 1: + config.name = parts[0] + case 2: + config.table = parts[0] + config.name = parts[1] + case 3: + config.familyName = parts[0] + config.table = parts[1] + config.name = parts[2] + default: + return nftSetConfig{}, fmt.Errorf("invalid nftset %q: expected [[FAMILY/]TABLE/]NAME", value) + } + + if config.familyName == "" || config.table == "" || config.name == "" { + return nftSetConfig{}, fmt.Errorf("invalid nftset %q: family, table, and set name must not be empty", value) + } + + family, ok := nftFamilies[strings.ToLower(config.familyName)] + if !ok { + return nftSetConfig{}, fmt.Errorf("invalid nftset %q: unsupported family %q", value, config.familyName) + } + config.family = family + config.familyName = strings.ToLower(config.familyName) + if err := validateNFTName("table", config.table); err != nil { + return nftSetConfig{}, fmt.Errorf("invalid nftset %q: %w", value, err) + } + if err := validateNFTName("set", config.name); err != nil { + return nftSetConfig{}, fmt.Errorf("invalid nftset %q: %w", value, err) + } + if err := validateNFTName("IPv6 set", config.name+"6"); err != nil { + return nftSetConfig{}, fmt.Errorf("invalid nftset %q: %w", value, err) + } + + return config, nil +} + +func validateNFTName(kind, name string) error { + if strings.IndexByte(name, 0) >= 0 { + return fmt.Errorf("%s name must not contain NUL", kind) + } + if len(name) >= maxNFTNameLength { + return fmt.Errorf("%s name is too long", kind) + } + return nil +} + +func (c nftSetConfig) key() string { + return c.familyName + "/" + c.table + "/" + c.name +} + +var nftFamilies = map[string]libnetset.Family{ + "inet": libnetset.FamilyINet, + "ip": libnetset.FamilyIPv4, + "ip6": libnetset.FamilyIPv6, + "arp": libnetset.FamilyARP, + "netdev": libnetset.FamilyNetdev, + "bridge": libnetset.FamilyBridge, +} diff --git a/netset/manager.go b/netset/manager.go new file mode 100644 index 0000000..c7b6376 --- /dev/null +++ b/netset/manager.go @@ -0,0 +1,200 @@ +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() +} diff --git a/netset/manager_linux.go b/netset/manager_linux.go new file mode 100644 index 0000000..20dcedc --- /dev/null +++ b/netset/manager_linux.go @@ -0,0 +1,130 @@ +//go:build linux + +package netset + +import ( + "errors" + "fmt" + "net/netip" + "syscall" + + libnetset "github.com/nadoo/netset" +) + +type linuxBackend struct { + ipset *libnetset.IPSet + nftsets []*libnetset.NFTSet +} + +func newBackend() (backend, error) { + return &linuxBackend{}, nil +} + +func (b *linuxBackend) IPSetPair(name string) (*setPair, error) { + if b.ipset == nil { + client, err := libnetset.NewIPSet() + if err != nil { + return nil, err + } + b.ipset = client + } + + if err := ensureIPSet(b.ipset, name); err != nil { + return nil, err + } + if err := ensureIPSet(b.ipset, name+"6", libnetset.OptIPv6()); err != nil { + return nil, err + } + + return &setPair{ + label: "ipset " + name, + ipv4: ipSetTarget{client: b.ipset, name: name}, + ipv6: ipSetTarget{client: b.ipset, name: name + "6"}, + }, nil +} + +func ensureIPSet(client *libnetset.IPSet, name string, opts ...libnetset.Option) error { + if err := client.Create(name, opts...); err != nil && !errors.Is(err, syscall.EEXIST) { + return fmt.Errorf("create %q: %w", name, err) + } + if err := client.Flush(name); err != nil { + return fmt.Errorf("flush %q: %w", name, err) + } + return nil +} + +func (b *linuxBackend) NFTSetPair(config nftSetConfig) (*setPair, error) { + ipv4, err := b.newNFTSet(config, config.name, false) + if err != nil { + return nil, err + } + ipv6, err := b.newNFTSet(config, config.name+"6", true) + if err != nil { + return nil, err + } + + return &setPair{ + label: "nftset " + config.key(), + ipv4: nftSetTarget{set: ipv4}, + ipv6: nftSetTarget{set: ipv6}, + }, nil +} + +func (b *linuxBackend) newNFTSet(config nftSetConfig, name string, ipv6 bool) (*libnetset.NFTSet, error) { + opts := []libnetset.Option{libnetset.OptFamily(config.family), libnetset.OptInterval()} + if ipv6 { + opts = append(opts, libnetset.OptIPv6()) + } + + set, err := libnetset.NewNFTSet(config.table, name, opts...) + if err != nil { + return nil, fmt.Errorf("open %s/%s/%s: %w", config.familyName, config.table, name, err) + } + b.nftsets = append(b.nftsets, set) + + if err := set.Create(); err != nil && !errors.Is(err, syscall.EEXIST) { + return nil, fmt.Errorf("create %s/%s/%s: %w", config.familyName, config.table, name, err) + } + if err := set.Flush(); err != nil { + return nil, fmt.Errorf("flush %s/%s/%s: %w", config.familyName, config.table, name, err) + } + return set, nil +} + +func (b *linuxBackend) Close() error { + var errs []error + if b.ipset != nil { + errs = append(errs, b.ipset.Close()) + b.ipset = nil + } + for _, set := range b.nftsets { + errs = append(errs, set.Close()) + } + b.nftsets = nil + return errors.Join(errs...) +} + +type ipSetTarget struct { + client *libnetset.IPSet + name string +} + +func (s ipSetTarget) AddAddr(ip netip.Addr) error { + return s.client.AddAddr(s.name, ip) +} + +func (s ipSetTarget) AddPrefix(prefix netip.Prefix) error { + return s.client.AddPrefix(s.name, prefix) +} + +type nftSetTarget struct { + set *libnetset.NFTSet +} + +func (s nftSetTarget) AddAddr(ip netip.Addr) error { + return s.set.AddAddr(ip) +} + +func (s nftSetTarget) AddPrefix(prefix netip.Prefix) error { + return s.set.AddPrefix(prefix) +} diff --git a/netset/manager_other.go b/netset/manager_other.go new file mode 100644 index 0000000..efa124f --- /dev/null +++ b/netset/manager_other.go @@ -0,0 +1,13 @@ +//go:build !linux + +package netset + +import ( + "fmt" + + libnetset "github.com/nadoo/netset" +) + +func newBackend() (backend, error) { + return nil, fmt.Errorf("netset manager is supported only on linux: %w", libnetset.ErrUnsupported) +} diff --git a/rule/config.go b/rule/config.go index 77e653f..c8a0a87 100644 --- a/rule/config.go +++ b/rule/config.go @@ -16,6 +16,7 @@ type Config struct { DNSServers []string IPSet string + NFTSet string Domain []string IP []string @@ -57,6 +58,7 @@ func NewConfFromFile(ruleFile string) (*Config, error) { f.StringSliceUniqVar(&p.DNSServers, "dnsserver", nil, "remote dns server") f.StringVar(&p.IPSet, "ipset", "", "ipset NAME, will create 2 sets: NAME for ipv4 and NAME6 for ipv6") + f.StringVar(&p.NFTSet, "nftset", "", "nftables set [[FAMILY/]TABLE/]NAME, defaults to FAMILY=inet and TABLE=glider; will create NAME for ipv4 and NAME6 for ipv6") f.StringSliceVar(&p.Domain, "domain", nil, "domain") f.StringSliceVar(&p.IP, "ip", nil, "ip") diff --git a/systemd/glider@.service b/systemd/glider@.service index 92cbb06..d14810d 100644 --- a/systemd/glider@.service +++ b/systemd/glider@.service @@ -1,6 +1,6 @@ [Unit] Description=Glider Service (%i) -After=network.target iptables.service ip6tables.service +After=network.target iptables.service ip6tables.service nftables.service [Service] Type=simple @@ -13,7 +13,7 @@ ExecStart=/usr/bin/glider -config /etc/glider/%i.conf # NOTE: # work with systemd v229 or later, so glider can listen on port below 1024 with none-root user -# CAP_NET_ADMIN: ipset, setsockopt: IP_TRANSPARENT +# CAP_NET_ADMIN: ipset, nftables set, setsockopt: IP_TRANSPARENT # CAP_NET_BIND_SERVICE: bind ports under 1024 # CAP_NET_RAW: bind raw socket and broadcasting (used by dhcpd) CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW