Compare commits

10 Commits

11 changed files with 477 additions and 3 deletions

88
README.md Normal file
View File

@@ -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)

36
chain/hook.go Normal file
View File

@@ -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)
}
}

21
chain/policy.go Normal file
View File

@@ -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)
}
}

68
chain/type.go Normal file
View File

@@ -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,
}
}

View File

@@ -10,10 +10,11 @@ const (
INET INET
ARP ARP
BRIDGE BRIDGE
NETDEV
) )
func (f Type) String() string { func (t Type) String() string {
switch f { switch t {
case IP: case IP:
return "ip" return "ip"
case IP6: case IP6:
@@ -24,7 +25,9 @@ func (f Type) String() string {
return "arp" return "arp"
case BRIDGE: case BRIDGE:
return "bridge" return "bridge"
case NETDEV:
return "netdev"
default: default:
return fmt.Sprintf("Encoding(%d)", f) return fmt.Sprintf("unknown family %d", t)
} }
} }

82
internal/chain/chain.go Normal file
View File

@@ -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...)
}

View File

@@ -6,7 +6,11 @@ import (
) )
type NFT interface { type NFT interface {
// Run nft command.
Run(arg ...string) error Run(arg ...string) error
// RunWithOutput Run nft command with output.
RunWithOutput(arg ...string) (string, error)
} }
type execNFT struct { type execNFT struct {
@@ -35,3 +39,15 @@ func (r *execNFT) Run(arg ...string) error {
return nil 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
}

68
internal/rule/rule.go Normal file
View File

@@ -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...)
}

View File

@@ -5,6 +5,7 @@ import (
"git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/command" "git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/command"
) )
// API for working with tables.
type API interface { type API interface {
// AddTable adds a new table. // AddTable adds a new table.
// //

70
nft.go
View File

@@ -2,26 +2,48 @@ package nft
import ( import (
"errors" "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/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" "git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/table"
) )
// NFT A client for working with nftables // NFT A client for working with nftables
type NFT interface { type NFT interface {
// Command returns the command used to execute nft.
// You can execute your raw request.
Command() command.NFT
// Clear clears all rules. // Clear clears all rules.
// //
// This command is equivalent to: // This command is equivalent to:
// nft flush ruleset // nft flush ruleset
Clear() error 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 returns an API for working with tables.
Table() table.API 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 { type nft struct {
command command.NFT command command.NFT
table table.API table table.API
chain chain.API
rule rule.API
} }
// New Returns a client for working with nftables. // New Returns a client for working with nftables.
@@ -48,6 +70,8 @@ func NewWithPath(path string) (NFT, error) {
return &nft{ return &nft{
command: nftCommand, command: nftCommand,
table: table.New(nftCommand), table: table.New(nftCommand),
chain: chain.New(nftCommand),
rule: rule.New(nftCommand),
}, nil }, nil
} }
@@ -56,6 +80,52 @@ func (n *nft) Clear() error {
return n.command.Run(args...) 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 { func (n *nft) Table() table.API {
return n.table 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
}

21
version.go Normal file
View File

@@ -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
}