// Copyright 2024 Matthew Rich . All rights reserved. package resource import ( "context" _ "encoding/hex" "encoding/json" _ "errors" "fmt" "gopkg.in/yaml.v3" "io" "net/url" _ "os/exec" "regexp" "strconv" "strings" "log/slog" ) func init() { ResourceTypes.Register("iptable", func(u *url.URL) Resource { i := NewIptable() i.Table = IptableName(u.Hostname()) fields := strings.Split(u.Path, "/") slog.Info("iptables factory", "iptable", i, "uri", u, "field", fields) i.Chain = IptableChain(fields[1]) id, _ := strconv.ParseUint(fields[2], 10, 32) i.Id = uint(id) return i }) } type IptableIPVersion string const ( IptableIPv4 IptableIPVersion = "ipv4" IPtableIPv6 IptableIPVersion = "ipv6" ) type IptableName string const ( IptableNameFilter = "filter" IptableNameNat = "nat" IptableNameMangel = "mangle" IptableNameRaw = "raw" IptableNameSecurity = "security" ) var IptableNumber = regexp.MustCompile(`^[0-9]+$`) type IptableChain string const ( IptableChainInput = "INPUT" IptableChainOutput = "OUTPUT" IptableChainForward = "FORWARD" IptableChainPreRouting = "PREROUTING" IptableChainPostRouting = "POSTROUTING" ) type IptableProto string const ( IptableProtoTCP = "tcp" IptableProtoUDP = "udp" IptableProtoUDPLite = "udplite" IptableProtoICMP = "icmp" IptableProtoICMPv6 = "icmpv6" IptableProtoESP = "ESP" IptableProtoAH = "AH" IptableProtoSCTP = "sctp" IptableProtoMH = "mh" IptableProtoAll = "all" ) type IptableCIDR string type ExtensionFlag struct { Name string `json:"name" yaml:"name"` Value string `json:"value" yaml:"value"` } type IptablePort uint16 // Manage the state of iptables rules // iptable://filter/INPUT/0 type Iptable struct { Id uint `json:"id" yaml:"id"` Table IptableName `json:"table" yaml:"table"` Chain IptableChain `json:"chain" yaml:"chain"` Destination IptableCIDR `json:"destination,omitempty" yaml:"destination,omitempty"` Source IptableCIDR `json:"source,omitempty" yaml:"source,omitempty"` Dport IptablePort `json:"dport,omitempty" yaml:"dport,omitempty"` Sport IptablePort `json:"sport,omitempty" yaml:"sport,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` Out string `json:"out,omitempty" yaml:"out,omitempty"` Match []string `json:"match,omitempty" yaml:"match,omitempty"` Flags []ExtensionFlag `json:"extension_flags,omitempty" yaml:"extension_flags,omitempty"` Proto IptableProto `json:"proto,omitempty" yaml:"proto,omitempty"` Jump string `json:"jump" yaml:"jump"` State string `json:"state" yaml:"state"` CreateCommand *Command `yaml:"-" json:"-"` ReadCommand *Command `yaml:"-" json:"-"` UpdateCommand *Command `yaml:"-" json:"-"` DeleteCommand *Command `yaml:"-" json:"-"` } func NewIptable() *Iptable { i := &Iptable{} i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.NewCRUD() return i } func (i *Iptable) Clone() Resource { return &Iptable { Id: i.Id, Table: i.Table, Chain: i.Chain, Destination: i.Destination, Source: i.Source, In: i.In, Out: i.Out, Match: i.Match, Proto: i.Proto, State: i.State, } } func (i *Iptable) URI() string { return fmt.Sprintf("iptable://%s/%s/%d", i.Table, i.Chain, i.Id) } func (i *Iptable) SetURI(uri string) error { resourceUri, e := url.Parse(uri) if e == nil { if resourceUri.Scheme == "iptable" { i.Table = IptableName(resourceUri.Hostname()) fields := strings.Split(resourceUri.Path, "/") i.Chain = IptableChain(fields[1]) id, _ := strconv.ParseUint(fields[2], 10, 32) i.Id = uint(id) } else { e = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, uri) } } return e } func (i *Iptable) Validate() error { s := NewSchema(i.Type()) jsonDoc, jsonErr := i.JSON() if jsonErr == nil { return s.Validate(string(jsonDoc)) } return jsonErr } func (i *Iptable) JSON() ([]byte, error) { return json.Marshal(i) } func (i *Iptable) UnmarshalJSON(data []byte) error { if unmarshalErr := json.Unmarshal(data, i); unmarshalErr != nil { return unmarshalErr } i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.NewCRUD() return nil } func (i *Iptable) UnmarshalYAML(value *yaml.Node) error { type decodeIptable Iptable if unmarshalErr := value.Decode((*decodeIptable)(i)); unmarshalErr != nil { return unmarshalErr } i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.NewCRUD() return nil } func (i *Iptable) NewCRUD() (create *Command, read *Command, update *Command, del *Command) { return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand() } func (i *Iptable) Apply() error { switch i.State { case "absent": case "present": } return nil } func (i *Iptable) Load(r io.Reader) error { return NewYAMLDecoder(r).Decode(i) } func (i *Iptable) LoadDecl(yamlResourceDeclaration string) error { return NewYAMLStringDecoder(yamlResourceDeclaration).Decode(i) } func (i *Iptable) ResolveId(ctx context.Context) string { // uri := fmt.Sprintf("%s?gateway=%s&interface=%s&rtid=%s&metric=%d&type=%s&scope=%s", // n.To, n.Gateway, n.Interface, n.Rtid, n.Metric, n.RouteType, n.Scope) // n.Id = hex.EncodeToString([]byte(uri)) return fmt.Sprintf("%d", i.Id) } func (i *Iptable) Read(ctx context.Context) ([]byte, error) { out, err := i.ReadCommand.Execute(i) if err != nil { return nil, err } exErr := i.ReadCommand.Extractor(out, i) if exErr != nil { return nil, exErr } return yaml.Marshal(i) } func (i *Iptable) Type() string { return "iptable" } func NewIptableCreateCommand() *Command { c := NewCommand() c.Path = "iptables" c.Args = []CommandArg{ CommandArg("-t"), CommandArg("{{ .Table }}"), CommandArg("-R"), CommandArg("{{ .Chain }}"), CommandArg("{{ .Id }}"), CommandArg("{{ if .In }}-i {{ .In }}{{ else if .Out }}-o {{ .Out }}{{ end }}"), CommandArg("{{ range .Match }}-m {{ . }} {{ end }}"), CommandArg("{{ if .Source }}-s {{ .Source }}{{ end }}"), CommandArg("{{ if .Destination }}-d {{ .Destination }}{{ end }}"), CommandArg("{{ if .Jump }}-j {{ .Jump }}{{ end }}"), } return c } func NewIptableReadCommand() *Command { c := NewCommand() c.Path = "iptables" c.Args = []CommandArg{ CommandArg("-t"), CommandArg("{{ .Table }}"), CommandArg("-S"), CommandArg("{{ .Chain }}"), CommandArg("{{ .Id }}"), } c.Extractor = func(out []byte, target any) error { i := target.(*Iptable) ruleFields := strings.Split(strings.TrimSpace(string(out)), " ") switch ruleFields[0] { case "-A": //chain := ruleFields[1] flags := ruleFields[2:] for optind,opt := range flags { if optind > len(flags) - 2 { break } optValue := flags[optind + 1] switch opt { case "-i": i.In = optValue case "-o": i.Out = optValue case "-m": i.Match = append(i.Match, optValue) case "-s": i.Source = IptableCIDR(optValue) case "-d": i.Destination = IptableCIDR(optValue) case "-p": i.Proto = IptableProto(optValue) case "-j": i.Jump = optValue case "--dport": port,_ := strconv.ParseUint(optValue, 10, 16) i.Dport = IptablePort(port) case "--sport": port,_ := strconv.ParseUint(optValue, 10, 16) i.Sport = IptablePort(port) default: if opt[0] == '-' { i.Flags = append(i.Flags, ExtensionFlag{ Name: strings.Trim(opt, "-"), Value: strings.TrimSpace(optValue)}) } } } i.State = "present" default: i.State = "absent" } return nil } return c } func NewIptableUpdateCommand() *Command { return nil } func NewIptableDeleteCommand() *Command { return nil }