diff --git a/ README.md b/ README.md new file mode 100644 index 0000000..8f75399 --- /dev/null +++ b/ README.md @@ -0,0 +1,88 @@ +# go-nftables-client + +Go-низкоуровневая обёртка для управления [nftables](https://wiki.nftables.org/wiki-nftables/index.php/Main_Page) через командную строку. + +## Возможности + +- Добавление и удаление таблиц (`add table`, `delete table`) +- Добавление и удаление цепочек (`add chain`, `delete chain`, настройка hook/policy) +- Добавление, удаление, очистка правил (`add rule`, `delete rule`, `flush`) +- Абстракции для работы с IP/IP6/inet/arp/bridge/netdev families +- Интерфейс для выполнения CLI-команд nftables напрямую +- Простой и минималистичный API для быстрой интеграции + +## Установка +```sh +go get git.kor-elf.net/kor-elf-shield/go-nftables-client +``` + +## Пример использования + +```go +package main + +import ( + "log" + "git.kor-elf.net/kor-elf-shield/go-nftables-client" + "git.kor-elf.net/kor-elf-shield/go-nftables-client/family" + "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain" +) + +func main() { + nft, err := nft.New() + if err != nil { + log.Fatalf("nft not found: %v", err) + } + + // Добавить таблицу + if err := nft.Table().Add(family.IP, "test"); err != nil { + log.Fatalf("table add failed: %v", err) + } + + chainType := chain.NewBaseChainOptions(chain.TypeFilter) + chainType.Hook = chain.HookOutput + chainType.Priority = 0 + chainType.Policy = chain.PolicyAccept + if err := nft.Chain().Add(family.IP, "test", "test", chainType); err != nil { + log.Fatalf("table add failed: %v", err) + } + + // Добавить правило (пример: дропать пакеты от 1.2.3.4) + if err := nft.Rule().Add(family.IP, "test", "test", "ip", "saddr", "1.2.3.4", "drop"); err != nil { + log.Fatalf("rule add failed: %v", err) + } +} +``` + +## Краткое описание API + +```go +nft.Table().Add(family.Type, tableName string) error +nft.Table().Delete(family.Type, tableName string) error +nft.Table().Clear(family.Type, tableName string) error + +nft.Chain().Add(family.Type, table, chain string, baseChain chain.ChainOptions) error +nft.Chain().Create(family family.Type, table, chain string, baseChain chain.ChainOptions) error +nft.Chain().Rename(family family.Type, table, oldChainName, newChainName string) error +nft.Chain().Delete(family.Type, table, chain string) error +nft.Chain().Clear(family.Type, table, chain string) error + +nft.Rule().Add(family.Type, table, chain string, expr ...string) error +nft.Rule().Insert(family family.Type, table, chain string, expr ...string) error +nft.Rule().Replace(family family.Type, table, chain string, handle uint64, expr ...string) error +nft.Rule().Delete(family family.Type, table, chain string, handle uint64) error + + +nft.Clear() error // flush ruleset +nft.Version() (Version, error) // информация о версии nftables +nft.Command() command.NFT // ручное выполнение любых nft-команд +``` + +## Требования + +- Go 1.25+ +- Установленный nft (`/usr/sbin/nft`, `/sbin/nft` или доступен через PATH) + +## Лицензия + +[MIT](https://git.kor-elf.net/kor-elf-shield/go-nftables-client/src/branch/main/LICENSE) diff --git a/chain/hook.go b/chain/hook.go new file mode 100644 index 0000000..a1f0252 --- /dev/null +++ b/chain/hook.go @@ -0,0 +1,36 @@ +package chain + +import "fmt" + +type Hook int8 + +const ( + HookInput Hook = iota + HookOutput + HookForward + HookPrerouting + HookPostrouting + HookIngress + HookEgress +) + +func (h Hook) String() string { + switch h { + case HookInput: + return "input" + case HookOutput: + return "output" + case HookForward: + return "forward" + case HookPrerouting: + return "prerouting" + case HookPostrouting: + return "postrouting" + case HookIngress: + return "ingress" + case HookEgress: + return "egress" + default: + return fmt.Sprintf("unknown hook %d", h) + } +} diff --git a/chain/policy.go b/chain/policy.go new file mode 100644 index 0000000..6a15d94 --- /dev/null +++ b/chain/policy.go @@ -0,0 +1,21 @@ +package chain + +import "fmt" + +type Policy int8 + +const ( + PolicyAccept Policy = iota + 1 + PolicyDrop +) + +func (p Policy) String() string { + switch p { + case PolicyAccept: + return "accept" + case PolicyDrop: + return "drop" + default: + return fmt.Sprintf("unknown policy %d", p) + } +} diff --git a/chain/type.go b/chain/type.go new file mode 100644 index 0000000..df5506a --- /dev/null +++ b/chain/type.go @@ -0,0 +1,68 @@ +package chain + +import ( + "fmt" + "strconv" +) + +type ChainOptions interface { + String() string +} + +type Type int8 + +const ( + TypeNone Type = iota + TypeFilter + TypeNat + TypeRoute +) + +func (t Type) String() string { + switch t { + case TypeNone: + return "" + case TypeFilter: + return "filter" + case TypeNat: + return "inet" + case TypeRoute: + return "nat" + default: + return fmt.Sprintf("unknown type %d", t) + } +} + +type BaseChainOptions struct { + Type Type + Hook Hook + Priority int32 + Policy Policy + Device string +} + +func (b BaseChainOptions) String() string { + if b.Type == TypeNone { + return "" + } + + device := "" + if b.Hook == HookEgress || b.Hook == HookIngress { + if b.Device != "" { + device = " device " + b.Device + " " + } + } + + policy := "" + if b.Type == TypeFilter { + policy = "policy " + b.Policy.String() + " ; " + } + + return "{ type " + b.Type.String() + " hook " + b.Hook.String() + " " + device + " priority " + strconv.Itoa(int(b.Priority)) + " ; " + policy + " }" +} + +func NewBaseChainOptions(t Type) BaseChainOptions { + return BaseChainOptions{ + Type: t, + } +} diff --git a/family/type.go b/family/type.go new file mode 100644 index 0000000..1c7ea34 --- /dev/null +++ b/family/type.go @@ -0,0 +1,33 @@ +package family + +import "fmt" + +type Type int8 + +const ( + IP Type = iota + 1 + IP6 + INET + ARP + BRIDGE + NETDEV +) + +func (t Type) String() string { + switch t { + case IP: + return "ip" + case IP6: + return "ip6" + case INET: + return "inet" + case ARP: + return "arp" + case BRIDGE: + return "bridge" + case NETDEV: + return "netdev" + default: + return fmt.Sprintf("unknown family %d", t) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e598429 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.kor-elf.net/kor-elf-shield/go-nftables-client + +go 1.25 diff --git a/internal/chain/chain.go b/internal/chain/chain.go new file mode 100644 index 0000000..1a99b44 --- /dev/null +++ b/internal/chain/chain.go @@ -0,0 +1,82 @@ +package chain + +import ( + chain2 "git.kor-elf.net/kor-elf-shield/go-nftables-client/chain" + "git.kor-elf.net/kor-elf-shield/go-nftables-client/family" + "git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/command" +) + +// API for working with chains. +type API interface { + // Add adds a new chain. + // + // This command is equivalent to: + // nft add chain (ip|ip6|inet|arp|bridge) {table_name} {chain_name} + // nft add chain (ip|ip6|inet|arp|bridge) {table_name} {chain_name} '{ type (filter|route|nat) hook (ingress|prerouting|forward|input|output|postrouting|egress) priority (priority_value = int32) ;}' + // nft add chain (ip|ip6|inet|arp|bridge) {table_name} {chain_name} '{ type filter hook (forward|input|output) priority (priority_value = int32) ; policy (accept|drop) ;}' + // nft add chain (ip|ip6|inet|arp|bridge) {table_name} {chain_name} '{ type (filter|route|nat) hook (ingress|egress) device {device} priority (priority_value = int32) ;}' + Add(family family.Type, tableName string, chainName string, baseChain chain2.ChainOptions) error + + // Create creates a new chain. + // Similar to the Add, but returns an error if the chain already exists. + // + // This command is equivalent to: + // nft create chain (ip|ip6|inet|arp|bridge) {table_name} {chain_name} + // nft create chain (ip|ip6|inet|arp|bridge) {table_name} {chain_name} '{ type (filter|route|nat) hook (ingress|prerouting|forward|input|output|postrouting|egress) priority (priority_value = int32) ;}' + // nft create chain (ip|ip6|inet|arp|bridge) {table_name} {chain_name} '{ type filter hook (forward|input|output) priority (priority_value = int32) ; policy (accept|drop) ;}' + // nft create chain (ip|ip6|inet|arp|bridge) {table_name} {chain_name} '{ type (filter|route|nat) hook (ingress|egress) device {device} priority (priority_value = int32) ;}' + Create(family family.Type, tableName string, chainName string, baseChain chain2.ChainOptions) error + + // Delete deletes a chain. + // + // This command is equivalent to: + // nft delete chain (ip|ip6|inet|arp|bridge) {table_name} {chain_name} + Delete(family family.Type, tableName string, chainName string) error + + // Clear clears all rules in a chain. + // + // This command is equivalent to: + // nft flush chain (ip|ip6|inet|arp|bridge) {table_name} {chain_name} + Clear(family family.Type, tableName string, chainName string) error + + // Rename renames a chain. + // + // This command is equivalent to: + // nft rename chain (ip|ip6|inet|arp|bridge) {table_name} {old_chain_name} {new_chain_name} + Rename(family family.Type, tableName string, oldChainName string, newChainName string) error +} + +type chain struct { + command command.NFT +} + +func New(command command.NFT) API { + return &chain{ + command: command, + } +} + +func (c *chain) Add(family family.Type, tableName string, chainName string, baseChain chain2.ChainOptions) error { + args := []string{"add", "chain", family.String(), tableName, chainName, baseChain.String()} + return c.command.Run(args...) +} + +func (c *chain) Create(family family.Type, tableName string, chainName string, baseChain chain2.ChainOptions) error { + args := []string{"create", "chain", family.String(), tableName, chainName, baseChain.String()} + return c.command.Run(args...) +} + +func (c *chain) Delete(family family.Type, tableName string, chainName string) error { + args := []string{"delete", "chain", family.String(), tableName, chainName} + return c.command.Run(args...) +} + +func (c *chain) Clear(family family.Type, tableName string, chainName string) error { + args := []string{"flush", "chain", family.String(), tableName, chainName} + return c.command.Run(args...) +} + +func (c *chain) Rename(family family.Type, tableName string, oldChainName string, newChainName string) error { + args := []string{"rename", "chain", family.String(), tableName, oldChainName, newChainName} + return c.command.Run(args...) +} diff --git a/internal/command/command.go b/internal/command/command.go new file mode 100644 index 0000000..e197b91 --- /dev/null +++ b/internal/command/command.go @@ -0,0 +1,53 @@ +package command + +import ( + "errors" + "os/exec" +) + +type NFT interface { + // Run nft command. + Run(arg ...string) error + + // RunWithOutput Run nft command with output. + RunWithOutput(arg ...string) (string, error) +} + +type execNFT struct { + nftPath string +} + +func New(path string) (NFT, error) { + if err := checkingNFT(path); err != nil { + return nil, err + } + + return &execNFT{ + nftPath: path, + }, nil +} + +func (r *execNFT) Run(arg ...string) error { + cmd := exec.Command(r.nftPath, arg...) + out, err := cmd.CombinedOutput() + if err != nil { + if len(out) > 0 { + return errors.New(string(out)) + } + return err + } + + return nil +} + +func (r *execNFT) RunWithOutput(arg ...string) (string, error) { + cmd := exec.Command(r.nftPath, arg...) + out, err := cmd.CombinedOutput() + if err != nil { + if len(out) > 0 { + return string(out), err + } + return "", err + } + return string(out), nil +} diff --git a/internal/command/utils.go b/internal/command/utils.go new file mode 100644 index 0000000..4e405a8 --- /dev/null +++ b/internal/command/utils.go @@ -0,0 +1,51 @@ +package command + +import ( + "errors" + "fmt" + "os/exec" + "regexp" + "strings" +) + +func checkingNFT(path string) error { + if path == "" { + return errors.New("path is empty") + } + + cmd := exec.Command(path, "-V") + out, err := cmd.CombinedOutput() + if err != nil { + return errors.New("nftables not found") + } + + lines := regexp.MustCompile("\r?\n").Split(strings.TrimSpace(string(out)), -1) + json := false + for index, line := range lines { + line = strings.TrimSpace(line) + + if index == 0 { + if !strings.HasPrefix(line, "nftables") { + return errors.New("nftables not found") + } + continue + } + + if strings.HasPrefix(line, "json:") && strings.HasSuffix(line, "yes") { + json = true + } + } + + if !json { + return errors.New("nftables disabled json") + } + + cmd = exec.Command(path, "list", "ruleset") + out, err = cmd.CombinedOutput() + + if err != nil { + return fmt.Errorf("nftables is not available or not supported by the kernel: %s", string(out)) + } + + return nil +} diff --git a/internal/rule/rule.go b/internal/rule/rule.go new file mode 100644 index 0000000..3a90968 --- /dev/null +++ b/internal/rule/rule.go @@ -0,0 +1,68 @@ +package rule + +import ( + "strconv" + + "git.kor-elf.net/kor-elf-shield/go-nftables-client/family" + "git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/command" +) + +type API interface { + // Add adds a new rule. + // + // This command is equivalent to: + // nft add rule (ip|ip6|inet|arp|bridge) {table_name} {chain_name} '{ expr }' + Add(family family.Type, tableName string, chainName string, expr ...string) error + + // Insert inserts a new rule. + // Inserted rules are placed at the beginning of the chain, by default. + // + // This command is equivalent to: + // nft insert rule (ip|ip6|inet|arp|bridge) {table_name} {chain_name} '{ expr }' + Insert(family family.Type, tableName string, chainName string, expr ...string) error + + // Replace replaces a rule. + // + // This command is equivalent to: + // nft replace rule (ip|ip6|inet|arp|bridge) {table_name} {chain_name} {handle} '{ expr }' + Replace(family family.Type, tableName string, chainName string, handle uint64, expr ...string) error + + // Delete deletes a rule. + // + // This command is equivalent to: + // nft delete rule (ip|ip6|inet|arp|bridge) {table_name} {chain_name} {handle} + Delete(family family.Type, tableName string, chainName string, handle uint64) error +} + +type rule struct { + command command.NFT +} + +func New(command command.NFT) API { + return &rule{ + command: command, + } +} + +func (r *rule) Add(family family.Type, tableName string, chainName string, expr ...string) error { + args := []string{"add", "rule", family.String(), tableName, chainName} + args = append(args, expr...) + return r.command.Run(args...) +} + +func (r *rule) Insert(family family.Type, tableName string, chainName string, expr ...string) error { + args := []string{"insert", "rule", family.String(), tableName, chainName} + args = append(args, expr...) + return r.command.Run(args...) +} + +func (r *rule) Replace(family family.Type, tableName string, chainName string, handle uint64, expr ...string) error { + args := []string{"replace", "rule", family.String(), tableName, chainName, "handle", strconv.Itoa(int(handle))} + args = append(args, expr...) + return r.command.Run(args...) +} + +func (r *rule) Delete(family family.Type, tableName string, chainName string, handle uint64) error { + args := []string{"delete", "rule", family.String(), tableName, chainName, "handle", strconv.Itoa(int(handle))} + return r.command.Run(args...) +} diff --git a/internal/table/table.go b/internal/table/table.go new file mode 100644 index 0000000..3ee1522 --- /dev/null +++ b/internal/table/table.go @@ -0,0 +1,52 @@ +package table + +import ( + "git.kor-elf.net/kor-elf-shield/go-nftables-client/family" + "git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/command" +) + +// API for working with tables. +type API interface { + // AddTable adds a new table. + // + // This command is equivalent to: + // nft add table (ip|ip6|inet|arp|bridge) {table_name} + Add(family family.Type, tableName string) error + + // DeleteTable deletes a table. + // + // This command is equivalent to: + // nft delete table (ip|ip6|inet|arp|bridge) {table_name} + Delete(family family.Type, tableName string) error + + // ClearTable clears all rules in a table. + // + // This command is equivalent to: + // nft flush table (ip|ip6|inet|arp|bridge) {table_name} + Clear(family family.Type, tableName string) error +} + +type table struct { + command command.NFT +} + +func New(command command.NFT) API { + return &table{ + command: command, + } +} + +func (t *table) Add(family family.Type, tableName string) error { + args := []string{"add", "table", family.String(), tableName} + return t.command.Run(args...) +} + +func (t *table) Delete(family family.Type, tableName string) error { + args := []string{"delete", "table", family.String(), tableName} + return t.command.Run(args...) +} + +func (t *table) Clear(family family.Type, tableName string) error { + args := []string{"flush", "table", family.String(), tableName} + return t.command.Run(args...) +} diff --git a/nft.go b/nft.go new file mode 100644 index 0000000..d423a0a --- /dev/null +++ b/nft.go @@ -0,0 +1,131 @@ +package nft + +import ( + "errors" + "regexp" + "strings" + + "git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/chain" + "git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/command" + "git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/rule" + "git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/table" +) + +// NFT A client for working with nftables +type NFT interface { + // Command returns the command used to execute nft. + // You can execute your raw request. + Command() command.NFT + + // Clear clears all rules. + // + // This command is equivalent to: + // nft flush ruleset + Clear() error + + // Version returns the version of nftables. + // + // This command is equivalent to: + // nft -V + Version() (Version, error) + + // Table returns an API for working with tables. + Table() table.API + + // Chain returns an API for working with chains. + Chain() chain.API + + // Rule returns an API for working with rules. + Rule() rule.API +} + +type nft struct { + command command.NFT + table table.API + chain chain.API + rule rule.API +} + +// New Returns a client for working with nftables. +// Searches for nft in paths: nft, /usr/sbin/nft, /sbin/nft +func New() (NFT, error) { + paths := []string{"nft", "/usr/sbin/nft", "/sbin/nft"} + for _, path := range paths { + nftClient, err := NewWithPath(path) + if err == nil { + return nftClient, nil + } + } + + return nil, errors.New("nft not found") +} + +// NewWithPath Returns the client for working with nftables with its path specified. +func NewWithPath(path string) (NFT, error) { + nftCommand, err := command.New(path) + if err != nil { + return nil, err + } + + return &nft{ + command: nftCommand, + table: table.New(nftCommand), + chain: chain.New(nftCommand), + rule: rule.New(nftCommand), + }, nil +} + +func (n *nft) Clear() error { + args := []string{"flush", "ruleset"} + return n.command.Run(args...) +} + +func (n *nft) Version() (Version, error) { + args := []string{"-V"} + out, err := n.command.RunWithOutput(args...) + if err != nil { + return nil, err + } + + vers := "" + opts := make(map[string]string) + + lines := regexp.MustCompile("\r?\n").Split(strings.TrimSpace(out), -1) + for index, line := range lines { + line = strings.TrimSpace(line) + + if index == 0 { + vers = line + continue + } + + values := strings.Split(line, ":") + if len(values) != 2 { + continue + } + name := strings.TrimSpace(values[0]) + value := strings.TrimSpace(values[1]) + opts[name] = value + } + + return &version{ + version: vers, + opts: opts, + }, nil +} + +func (n *nft) Table() table.API { + return n.table +} + +func (n *nft) Chain() chain.API { + return n.chain +} + +func (n *nft) Rule() rule.API { + return n.rule +} + +func (n *nft) Command() command.NFT { + return n.command +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..78c53fd --- /dev/null +++ b/version.go @@ -0,0 +1,21 @@ +package nft + +type Version interface { + // Version returns the version of the nftables client. + Version() string + // Opts returns the options of the nftables client. + Opts() map[string]string +} + +type version struct { + version string + opts map[string]string +} + +func (v version) Version() string { + return v.version +} + +func (v version) Opts() map[string]string { + return v.opts +}