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:
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user