Initial scaffold for tomswall

Spiritual successor to shorewall — manages nftables directly via
google/nftables. Reads a single YAML config covering zones, interfaces,
hosts, policy, rules, snat, and named portgroups. Computes differential
changes against the running nftables state and applies them atomically.
Supports detecting and purging rules added outside of tomswall.
This commit is contained in:
2026-06-28 23:43:16 +10:00
commit 2a3eb3b04d
18 changed files with 1672 additions and 0 deletions
+215
View File
@@ -0,0 +1,215 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
"git.unkin.net/unkin/tomswall/internal/config"
"git.unkin.net/unkin/tomswall/internal/nftables"
)
var configPath string
func main() {
root := &cobra.Command{
Use: "tomswall",
Short: "nftables firewall manager — spiritual successor to shorewall",
}
root.PersistentFlags().StringVarP(&configPath, "config", "c", "/etc/tomswall/tomswall.yaml", "path to configuration file")
root.AddCommand(applyCmd(), checkCmd(), statusCmd(), purgeCmd(), flushCmd())
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
func loadConfig() (*config.Config, error) {
cfg, err := config.Load(configPath)
if err != nil {
return nil, err
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("validation: %w", err)
}
return cfg, nil
}
func applyCmd() *cobra.Command {
var dryRun bool
cmd := &cobra.Command{
Use: "apply",
Short: "Apply configuration to nftables (differential)",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
engine, err := nftables.NewEngine(cfg)
if err != nil {
return fmt.Errorf("initializing nftables: %w", err)
}
changes, err := engine.Plan()
if err != nil {
return fmt.Errorf("computing changes: %w", err)
}
if changes.Empty() {
fmt.Println("No changes needed — firewall is up to date.")
return nil
}
fmt.Println(changes.Summary())
if dryRun {
return nil
}
if err := engine.Apply(changes); err != nil {
return fmt.Errorf("applying changes: %w", err)
}
fmt.Println("Changes applied successfully.")
return nil
},
}
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show planned changes without applying")
return cmd
}
func checkCmd() *cobra.Command {
return &cobra.Command{
Use: "check",
Short: "Validate configuration without applying",
RunE: func(cmd *cobra.Command, args []string) error {
_, err := loadConfig()
if err != nil {
return err
}
fmt.Println("Configuration is valid.")
return nil
},
}
}
func statusCmd() *cobra.Command {
return &cobra.Command{
Use: "status",
Short: "Show current firewall state and pending changes",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
engine, err := nftables.NewEngine(cfg)
if err != nil {
return fmt.Errorf("initializing nftables: %w", err)
}
changes, err := engine.Plan()
if err != nil {
return fmt.Errorf("computing changes: %w", err)
}
foreign, err := engine.FindForeignRules()
if err != nil {
return fmt.Errorf("scanning foreign rules: %w", err)
}
if changes.Empty() && len(foreign) == 0 {
fmt.Println("Firewall is up to date. No foreign rules detected.")
return nil
}
if !changes.Empty() {
fmt.Println("Pending changes:")
fmt.Println(changes.Summary())
}
if len(foreign) > 0 {
fmt.Printf("\nForeign rules detected (%d):\n", len(foreign))
for _, r := range foreign {
fmt.Printf(" - %s\n", r)
}
fmt.Println("\nUse 'tomswall purge' to remove foreign rules.")
}
return nil
},
}
}
func purgeCmd() *cobra.Command {
var dryRun bool
cmd := &cobra.Command{
Use: "purge",
Short: "Remove rules not managed by tomswall",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
engine, err := nftables.NewEngine(cfg)
if err != nil {
return fmt.Errorf("initializing nftables: %w", err)
}
foreign, err := engine.FindForeignRules()
if err != nil {
return fmt.Errorf("scanning: %w", err)
}
if len(foreign) == 0 {
fmt.Println("No foreign rules found.")
return nil
}
fmt.Printf("Found %d foreign rule(s) to remove:\n", len(foreign))
for _, r := range foreign {
fmt.Printf(" - %s\n", r)
}
if dryRun {
return nil
}
if err := engine.PurgeForeignRules(foreign); err != nil {
return fmt.Errorf("purging: %w", err)
}
fmt.Println("Foreign rules removed.")
return nil
},
}
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show foreign rules without removing")
return cmd
}
func flushCmd() *cobra.Command {
return &cobra.Command{
Use: "flush",
Short: "Remove all tomswall-managed rules and tables",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
engine, err := nftables.NewEngine(cfg)
if err != nil {
return fmt.Errorf("initializing nftables: %w", err)
}
if err := engine.Flush(); err != nil {
return fmt.Errorf("flushing: %w", err)
}
fmt.Println("All tomswall rules flushed.")
return nil
},
}
}