From dd963a343a48571f6acb1b3ff2fba9a2426a4d89 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 19 Oct 2025 14:52:37 +0500 Subject: [PATCH 01/15] Initialize Go module for the project --- go.mod | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 go.mod 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 From f5b18884205e4bd861a105fc69eb4bdb26bbce81 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 19 Oct 2025 14:53:51 +0500 Subject: [PATCH 02/15] Add initial nftables client implementation --- family_type.go | 30 +++++++++++++++++++++++ nft.go | 53 +++++++++++++++++++++++++++++++++++++++++ utils.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 family_type.go create mode 100644 nft.go create mode 100644 utils.go diff --git a/family_type.go b/family_type.go new file mode 100644 index 0000000..82b6dd1 --- /dev/null +++ b/family_type.go @@ -0,0 +1,30 @@ +package nft + +import "fmt" + +type FamilyType int8 + +const ( + IP FamilyType = iota + 1 + IP6 + INET + ARP + BRIDGE +) + +func (f FamilyType) String() string { + switch f { + case IP: + return "ip" + case IP6: + return "ip6" + case INET: + return "inet" + case ARP: + return "arp" + case BRIDGE: + return "bridge" + default: + return fmt.Sprintf("Encoding(%d)", f) + } +} diff --git a/nft.go b/nft.go new file mode 100644 index 0000000..2668d85 --- /dev/null +++ b/nft.go @@ -0,0 +1,53 @@ +package nft + +import ( + "errors" +) + +// NFT A client for working with nftables +type NFT interface { + // Clear clears all rules. + Clear() error + + // AddTable adds a new table. + AddTable(family FamilyType, name string) error +} + +type nft struct { + path string +} + +// 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) { + if err := checkingNFT(path); err != nil { + return nil, err + } + + return &nft{ + path: path, + }, nil +} + +func (n *nft) Clear() error { + args := []string{"flush", "ruleset"} + return executeCommand(n.path, args...) +} + +func (n *nft) AddTable(family FamilyType, name string) error { + args := []string{"add", "table", family.String(), name} + return executeCommand(n.path, args...) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..73ca09e --- /dev/null +++ b/utils.go @@ -0,0 +1,64 @@ +package nft + +import ( + "errors" + "fmt" + "os/exec" + "regexp" + "strings" +) + +func executeCommand(name string, arg ...string) error { + cmd := exec.Command(name, arg...) + out, err := cmd.CombinedOutput() + if err != nil { + if len(out) > 0 { + return errors.New(string(out)) + } + return err + } + + return nil +} + +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 +} From 7cd41f8491e8c276f7566461ece30fefdb4c6372 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 19 Oct 2025 19:55:42 +0500 Subject: [PATCH 03/15] Improve documentation for Clear and AddTable methods in NFT interface. --- nft.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nft.go b/nft.go index 2668d85..7caed9b 100644 --- a/nft.go +++ b/nft.go @@ -7,9 +7,15 @@ import ( // NFT A client for working with nftables type NFT interface { // Clear clears all rules. + // + // This command is equivalent to: + // nft flush ruleset Clear() error // AddTable adds a new table. + // + // This command is equivalent to: + // nft add table (ip|ip6|inet|arp|bridge) {name} AddTable(family FamilyType, name string) error } From 053ca9a37f28cd7cb6caeba174130a2e3c09b936 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 19 Oct 2025 20:11:45 +0500 Subject: [PATCH 04/15] Add DeleteTable method to NFT interface and implementation. --- nft.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nft.go b/nft.go index 7caed9b..7eae5b4 100644 --- a/nft.go +++ b/nft.go @@ -17,6 +17,12 @@ type NFT interface { // This command is equivalent to: // nft add table (ip|ip6|inet|arp|bridge) {name} AddTable(family FamilyType, name string) error + + // DeleteTable deletes a table. + // + // This command is equivalent to: + // nft delete table (ip|ip6|inet|arp|bridge) {name} + DeleteTable(family FamilyType, name string) error } type nft struct { @@ -57,3 +63,8 @@ func (n *nft) AddTable(family FamilyType, name string) error { args := []string{"add", "table", family.String(), name} return executeCommand(n.path, args...) } + +func (n *nft) DeleteTable(family FamilyType, name string) error { + args := []string{"delete", "table", family.String(), name} + return executeCommand(n.path, args...) +} From 1a62968d1d02fe5a2af92abe093e8ea9f026826b Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 19 Oct 2025 20:25:36 +0500 Subject: [PATCH 05/15] Add ClearTable method to NFT interface and implementation. --- nft.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nft.go b/nft.go index 7eae5b4..2aff2fd 100644 --- a/nft.go +++ b/nft.go @@ -23,6 +23,12 @@ type NFT interface { // This command is equivalent to: // nft delete table (ip|ip6|inet|arp|bridge) {name} DeleteTable(family FamilyType, name string) error + + // ClearTable clears all rules in a table. + // + // This command is equivalent to: + // nft flush table (ip|ip6|inet|arp|bridge) {name} + ClearTable(family FamilyType, name string) error } type nft struct { @@ -68,3 +74,8 @@ func (n *nft) DeleteTable(family FamilyType, name string) error { args := []string{"delete", "table", family.String(), name} return executeCommand(n.path, args...) } + +func (n *nft) ClearTable(family FamilyType, name string) error { + args := []string{"flush", "table", family.String(), name} + return executeCommand(n.path, args...) +} From d056f5dbf89a6f7eb56a3db6e19ee40e2a23c26d Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 19 Oct 2025 21:12:25 +0500 Subject: [PATCH 06/15] Changed the name parameter to nameTable in methods for working with tables. --- nft.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/nft.go b/nft.go index 2aff2fd..09aad18 100644 --- a/nft.go +++ b/nft.go @@ -15,20 +15,20 @@ type NFT interface { // AddTable adds a new table. // // This command is equivalent to: - // nft add table (ip|ip6|inet|arp|bridge) {name} - AddTable(family FamilyType, name string) error + // nft add table (ip|ip6|inet|arp|bridge) {table_name} + AddTable(family FamilyType, tableName string) error // DeleteTable deletes a table. // // This command is equivalent to: - // nft delete table (ip|ip6|inet|arp|bridge) {name} - DeleteTable(family FamilyType, name string) error + // nft delete table (ip|ip6|inet|arp|bridge) {table_name} + DeleteTable(family FamilyType, tableName string) error // ClearTable clears all rules in a table. // // This command is equivalent to: - // nft flush table (ip|ip6|inet|arp|bridge) {name} - ClearTable(family FamilyType, name string) error + // nft flush table (ip|ip6|inet|arp|bridge) {table_name} + ClearTable(family FamilyType, tableName string) error } type nft struct { @@ -65,17 +65,17 @@ func (n *nft) Clear() error { return executeCommand(n.path, args...) } -func (n *nft) AddTable(family FamilyType, name string) error { - args := []string{"add", "table", family.String(), name} +func (n *nft) AddTable(family FamilyType, tableName string) error { + args := []string{"add", "table", family.String(), tableName} return executeCommand(n.path, args...) } -func (n *nft) DeleteTable(family FamilyType, name string) error { - args := []string{"delete", "table", family.String(), name} +func (n *nft) DeleteTable(family FamilyType, tableName string) error { + args := []string{"delete", "table", family.String(), tableName} return executeCommand(n.path, args...) } -func (n *nft) ClearTable(family FamilyType, name string) error { - args := []string{"flush", "table", family.String(), name} +func (n *nft) ClearTable(family FamilyType, tableName string) error { + args := []string{"flush", "table", family.String(), tableName} return executeCommand(n.path, args...) } From 5b199933430d3bf62099127df4395f083ab61af7 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 19 Oct 2025 22:50:14 +0500 Subject: [PATCH 07/15] Refactor table management to use a dedicated API and improve command handling. --- family_type.go => family/type.go | 8 ++--- internal/command/command.go | 37 +++++++++++++++++++ utils.go => internal/command/utils.go | 15 +------- internal/table/table.go | 51 +++++++++++++++++++++++++++ nft.go | 48 ++++++++----------------- 5 files changed, 107 insertions(+), 52 deletions(-) rename family_type.go => family/type.go (72%) create mode 100644 internal/command/command.go rename utils.go => internal/command/utils.go (79%) create mode 100644 internal/table/table.go diff --git a/family_type.go b/family/type.go similarity index 72% rename from family_type.go rename to family/type.go index 82b6dd1..d3c32e8 100644 --- a/family_type.go +++ b/family/type.go @@ -1,18 +1,18 @@ -package nft +package family import "fmt" -type FamilyType int8 +type Type int8 const ( - IP FamilyType = iota + 1 + IP Type = iota + 1 IP6 INET ARP BRIDGE ) -func (f FamilyType) String() string { +func (f Type) String() string { switch f { case IP: return "ip" diff --git a/internal/command/command.go b/internal/command/command.go new file mode 100644 index 0000000..560e8df --- /dev/null +++ b/internal/command/command.go @@ -0,0 +1,37 @@ +package command + +import ( + "errors" + "os/exec" +) + +type NFT interface { + Run(arg ...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 +} diff --git a/utils.go b/internal/command/utils.go similarity index 79% rename from utils.go rename to internal/command/utils.go index 73ca09e..4e405a8 100644 --- a/utils.go +++ b/internal/command/utils.go @@ -1,4 +1,4 @@ -package nft +package command import ( "errors" @@ -8,19 +8,6 @@ import ( "strings" ) -func executeCommand(name string, arg ...string) error { - cmd := exec.Command(name, arg...) - out, err := cmd.CombinedOutput() - if err != nil { - if len(out) > 0 { - return errors.New(string(out)) - } - return err - } - - return nil -} - func checkingNFT(path string) error { if path == "" { return errors.New("path is empty") diff --git a/internal/table/table.go b/internal/table/table.go new file mode 100644 index 0000000..1f5b41f --- /dev/null +++ b/internal/table/table.go @@ -0,0 +1,51 @@ +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" +) + +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 index 09aad18..7c4582d 100644 --- a/nft.go +++ b/nft.go @@ -2,6 +2,9 @@ package nft import ( "errors" + + "git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/command" + "git.kor-elf.net/kor-elf-shield/go-nftables-client/internal/table" ) // NFT A client for working with nftables @@ -12,27 +15,13 @@ type NFT interface { // nft flush ruleset Clear() error - // AddTable adds a new table. - // - // This command is equivalent to: - // nft add table (ip|ip6|inet|arp|bridge) {table_name} - AddTable(family FamilyType, tableName string) error - - // DeleteTable deletes a table. - // - // This command is equivalent to: - // nft delete table (ip|ip6|inet|arp|bridge) {table_name} - DeleteTable(family FamilyType, 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} - ClearTable(family FamilyType, tableName string) error + // Table returns an API for working with tables. + Table() table.API } type nft struct { - path string + command command.NFT + table table.API } // New Returns a client for working with nftables. @@ -51,31 +40,22 @@ func New() (NFT, error) { // NewWithPath Returns the client for working with nftables with its path specified. func NewWithPath(path string) (NFT, error) { - if err := checkingNFT(path); err != nil { + nftCommand, err := command.New(path) + if err != nil { return nil, err } return &nft{ - path: path, + command: nftCommand, + table: table.New(nftCommand), }, nil } func (n *nft) Clear() error { args := []string{"flush", "ruleset"} - return executeCommand(n.path, args...) + return n.command.Run(args...) } -func (n *nft) AddTable(family FamilyType, tableName string) error { - args := []string{"add", "table", family.String(), tableName} - return executeCommand(n.path, args...) -} - -func (n *nft) DeleteTable(family FamilyType, tableName string) error { - args := []string{"delete", "table", family.String(), tableName} - return executeCommand(n.path, args...) -} - -func (n *nft) ClearTable(family FamilyType, tableName string) error { - args := []string{"flush", "table", family.String(), tableName} - return executeCommand(n.path, args...) +func (n *nft) Table() table.API { + return n.table } From c3a513f92cab46ebb9cd6acc0b5ee4f571cff5d4 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Sun, 19 Oct 2025 22:54:04 +0500 Subject: [PATCH 08/15] Improve documentation for API interface in table package. --- internal/table/table.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/table/table.go b/internal/table/table.go index 1f5b41f..3ee1522 100644 --- a/internal/table/table.go +++ b/internal/table/table.go @@ -5,6 +5,7 @@ import ( "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. // From 9210448f16ac41f8d2c733aa226dd91767e16516 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 20 Oct 2025 22:44:40 +0500 Subject: [PATCH 09/15] Add Version method to NFT interface and implementation Include `RunWithOutput` support in the command package and introduce version parsing logic, enabling retrieval of the nftables version and options. --- internal/command/command.go | 13 ++++++++++ nft.go | 49 +++++++++++++++++++++++++++++++++++++ version.go | 21 ++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 version.go diff --git a/internal/command/command.go b/internal/command/command.go index 560e8df..a21834d 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -7,6 +7,7 @@ import ( type NFT interface { Run(arg ...string) error + RunWithOutput(arg ...string) (string, error) } type execNFT struct { @@ -35,3 +36,15 @@ func (r *execNFT) Run(arg ...string) error { 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/nft.go b/nft.go index 7c4582d..195f4a3 100644 --- a/nft.go +++ b/nft.go @@ -2,7 +2,10 @@ 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/table" ) @@ -15,6 +18,12 @@ type NFT interface { // 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 } @@ -22,6 +31,7 @@ type NFT interface { type nft struct { command command.NFT table table.API + chain chain.API } // New Returns a client for working with nftables. @@ -48,6 +58,7 @@ func NewWithPath(path string) (NFT, error) { return &nft{ command: nftCommand, table: table.New(nftCommand), + chain: chain.New(nftCommand), }, nil } @@ -56,6 +67,44 @@ func (n *nft) Clear() error { 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(string(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 +} 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 +} From 09ac9993467fa5e2b7b063c6a99d23ebda33eafb Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Mon, 20 Oct 2025 22:45:13 +0500 Subject: [PATCH 10/15] Add NETDEV type to Type enum and update String method --- family/type.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/family/type.go b/family/type.go index d3c32e8..1c7ea34 100644 --- a/family/type.go +++ b/family/type.go @@ -10,10 +10,11 @@ const ( INET ARP BRIDGE + NETDEV ) -func (f Type) String() string { - switch f { +func (t Type) String() string { + switch t { case IP: return "ip" case IP6: @@ -24,7 +25,9 @@ func (f Type) String() string { return "arp" case BRIDGE: return "bridge" + case NETDEV: + return "netdev" default: - return fmt.Sprintf("Encoding(%d)", f) + return fmt.Sprintf("unknown family %d", t) } } From e7e53fc1232db0ec4c11fdbf8579bef92d35fbb3 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Wed, 22 Oct 2025 20:56:46 +0500 Subject: [PATCH 11/15] Add Chain API for managing nftables chains, including Add, Create, Delete, Clear, and Rename operations. --- chain/hook.go | 36 ++++++++++++++++++ chain/policy.go | 21 +++++++++++ chain/type.go | 68 ++++++++++++++++++++++++++++++++++ internal/chain/chain.go | 82 +++++++++++++++++++++++++++++++++++++++++ nft.go | 3 ++ 5 files changed, 210 insertions(+) create mode 100644 chain/hook.go create mode 100644 chain/policy.go create mode 100644 chain/type.go create mode 100644 internal/chain/chain.go 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/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/nft.go b/nft.go index 195f4a3..2e4b86d 100644 --- a/nft.go +++ b/nft.go @@ -26,6 +26,9 @@ type NFT interface { // Table returns an API for working with tables. Table() table.API + + // Chain returns an API for working with chains. + Chain() chain.API } type nft struct { From 90b232aa05d31e9bf3cabbd444d8095c10f0a271 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Wed, 22 Oct 2025 20:59:49 +0500 Subject: [PATCH 12/15] Add Command method to NFT interface and RunWithOutput support to command package --- internal/command/command.go | 3 +++ nft.go | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/internal/command/command.go b/internal/command/command.go index a21834d..e197b91 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -6,7 +6,10 @@ import ( ) type NFT interface { + // Run nft command. Run(arg ...string) error + + // RunWithOutput Run nft command with output. RunWithOutput(arg ...string) (string, error) } diff --git a/nft.go b/nft.go index 2e4b86d..83a953a 100644 --- a/nft.go +++ b/nft.go @@ -12,6 +12,10 @@ import ( // 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: @@ -111,3 +115,7 @@ func (n *nft) Table() table.API { func (n *nft) Chain() chain.API { return n.chain } + +func (n *nft) Command() command.NFT { + return n.command +} From 2ac9272293143cfa4f4c0824b73e89da95a568f8 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Wed, 22 Oct 2025 22:43:33 +0500 Subject: [PATCH 13/15] Add Rule API for managing nftables rules, including Add, Insert, Replace, and Delete operations --- internal/rule/rule.go | 68 +++++++++++++++++++++++++++++++++++++++++++ nft.go | 10 +++++++ 2 files changed, 78 insertions(+) create mode 100644 internal/rule/rule.go 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/nft.go b/nft.go index 83a953a..ec28c78 100644 --- a/nft.go +++ b/nft.go @@ -7,6 +7,7 @@ import ( "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" ) @@ -33,12 +34,16 @@ type NFT interface { // 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. @@ -66,6 +71,7 @@ func NewWithPath(path string) (NFT, error) { command: nftCommand, table: table.New(nftCommand), chain: chain.New(nftCommand), + rule: rule.New(nftCommand), }, nil } @@ -116,6 +122,10 @@ 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 } From a305b7f55a24581b03a111051f86c1fda5677fb3 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Wed, 22 Oct 2025 22:44:23 +0500 Subject: [PATCH 14/15] Fix type conversion by removing unnecessary string conversion for command output parsing --- nft.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nft.go b/nft.go index ec28c78..d423a0a 100644 --- a/nft.go +++ b/nft.go @@ -90,7 +90,7 @@ func (n *nft) Version() (Version, error) { vers := "" opts := make(map[string]string) - lines := regexp.MustCompile("\r?\n").Split(strings.TrimSpace(string(out)), -1) + lines := regexp.MustCompile("\r?\n").Split(strings.TrimSpace(out), -1) for index, line := range lines { line = strings.TrimSpace(line) From 7360c05f9bdcafb65778c21c2b22fdab2e71e326 Mon Sep 17 00:00:00 2001 From: Leonid Nikitin Date: Wed, 22 Oct 2025 23:01:32 +0500 Subject: [PATCH 15/15] Add initial README with usage examples and API documentation --- README.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 README.md 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)