jx/internal/resource/iptables.go
Matthew Rich 8094d1c063
Some checks are pending
Lint / golangci-lint (push) Waiting to run
Declarative Tests / test (push) Waiting to run
Declarative Tests / build-fedora (push) Waiting to run
Declarative Tests / build-ubuntu-focal (push) Waiting to run
add build support to container_image resource; add more testing
2024-09-19 08:11:57 +00:00

844 lines
21 KiB
Go

// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/command"
"decl/internal/data"
"decl/internal/folio"
)
const (
IptableTypeName TypeName = "iptable"
)
func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"iptable"}, func(u *url.URL) data.Resource {
i := NewIptable()
i.Table = IptableName(u.Hostname())
if len(u.Path) > 0 {
fields := strings.FieldsFunc(u.Path, func(c rune) bool { return c == '/' })
slog.Info("iptables factory", "iptable", i, "uri", u, "fields", fields, "number_fields", len(fields))
if len(fields) > 0 {
i.Chain = IptableChain(fields[0])
if len(fields) < 3 {
i.ResourceType = IptableTypeChain
} else {
i.ResourceType = IptableTypeRule
id, _ := strconv.ParseUint(fields[1], 10, 32)
i.Id = uint(id)
}
}
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD()
}
return i
})
}
type IptableIPVersion string
const (
IptableIPv4 IptableIPVersion = "ipv4"
IPtableIPv6 IptableIPVersion = "ipv6"
)
type IptableName string
const (
IptableNameFilter IptableName = "filter"
IptableNameNat IptableName = "nat"
IptableNameMangel IptableName = "mangle"
IptableNameRaw IptableName = "raw"
IptableNameSecurity IptableName = "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
type IptableType string
const (
IptableTypeRule = "rule"
IptableTypeChain = "chain"
)
var (
ErrInvalidIptableName error = errors.New("The IptableName is not a valid table")
)
// Manage the state of iptables rules
// iptable://filter/INPUT/0
type Iptable struct {
*Common `json:",inline" yaml:",inline"`
stater machine.Stater `json:"-" yaml:"-"`
parsedURI *url.URL `json:"-" yaml:"-"`
Id uint `json:"id,omitempty" yaml:"id,omitempty"`
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,omitempty" yaml:"jump,omitempty"`
ChainLength uint `json:"-" yaml:"-"`
ResourceType IptableType `json:"resourcetype,omitempty" yaml:"resourcetype,omitempty"`
CreateCommand *command.Command `yaml:"-" json:"-"`
ReadCommand *command.Command `yaml:"-" json:"-"`
UpdateCommand *command.Command `yaml:"-" json:"-"`
DeleteCommand *command.Command `yaml:"-" json:"-"`
config data.ConfigurationValueGetter
Resources data.ResourceMapper `yaml:"-" json:"-"`
}
func (n IptableName) Validate() error {
switch n {
case IptableNameFilter, IptableNameNat, IptableNameMangel, IptableNameRaw, IptableNameSecurity:
return nil
default:
return ErrInvalidIptableName
}
}
func NewIptable() *Iptable {
i := &Iptable{ ResourceType: IptableTypeRule, Common: &Common{ resourceType: IptableTypeName } }
i.CreateCommand, i.ReadCommand, i.UpdateCommand, i.DeleteCommand = i.ResourceType.NewCRUD()
return i
}
func (i *Iptable) SetResourceMapper(resources data.ResourceMapper) {
i.Resources = resources
}
func (i *Iptable) Clone() data.Resource {
newIpt := &Iptable {
Common: i.Common,
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,
ResourceType: i.ResourceType,
}
newIpt.CreateCommand, newIpt.ReadCommand, newIpt.UpdateCommand, newIpt.DeleteCommand = newIpt.ResourceType.NewCRUD()
return newIpt
}
func (i *Iptable) StateMachine() machine.Stater {
if i.stater == nil {
i.stater = StorageMachine(i)
}
return i.stater
}
func (i *Iptable) Notify(m *machine.EventMessage) {
ctx := context.Background()
switch m.On {
case machine.ENTERSTATEEVENT:
switch m.Dest {
case "start_create":
if e := i.Create(ctx); e == nil {
if triggerErr := i.stater.Trigger("created"); triggerErr == nil {
return
}
}
i.Common.State = "absent"
case "present":
i.Common.State = "present"
}
case machine.EXITSTATEEVENT:
}
}
func (i *Iptable) URI() string {
return fmt.Sprintf("iptable://%s/%s/%d", i.Table, i.Chain, i.Id)
}
func (i *Iptable) SetURI(uri string) (err error) {
i.parsedURI, err = url.Parse(uri)
if err == nil {
fields := strings.FieldsFunc(i.parsedURI.Path, func(c rune) bool { return c == '/' })
fieldsLen := len(fields)
if i.parsedURI.Scheme == "iptable" && fieldsLen > 0 {
i.Table = IptableName(i.parsedURI.Hostname())
if err = i.Table.Validate(); err != nil {
return err
}
i.Chain = IptableChain(fields[0])
if fieldsLen < 2 {
i.ResourceType = IptableTypeChain
} else {
i.ResourceType = IptableTypeRule
id, _ := strconv.ParseUint(fields[1], 10, 32)
i.Id = uint(id)
}
} else {
err = fmt.Errorf("%w: %s is not an iptable rule", ErrInvalidResourceURI, uri)
}
}
return
}
func (i *Iptable) UseConfig(config data.ConfigurationValueGetter) {
i.config = config
}
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.ResourceType.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.ResourceType.NewCRUD()
return nil
}
func (i *Iptable) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand()
}
func (i *Iptable) Apply() error {
ctx := context.Background()
switch i.Common.State {
case "absent":
case "present":
err := i.Create(ctx)
if err != nil {
return err
}
}
_,e := i.Read(context.Background())
return e
}
func (i *Iptable) Load(docData []byte, f codec.Format) (err error) {
err = f.StringDecoder(string(docData)).Decode(i)
return
}
func (i *Iptable) LoadReader(r io.ReadCloser, f codec.Format) (err error) {
err = f.Decoder(r).Decode(i)
return
}
func (i *Iptable) LoadString(docData string, f codec.Format) (err error) {
err = f.StringDecoder(docData).Decode(i)
return
}
func (i *Iptable) LoadDecl(yamlResourceDeclaration string) error {
return i.LoadString(yamlResourceDeclaration, codec.FormatYaml)
}
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) SetFlagValue(opt, value string) bool {
switch opt {
case "-i":
i.In = value
return true
case "-o":
i.Out = value
return true
case "-m":
for _,search := range i.Match {
if search == value {
return true
}
}
i.Match = append(i.Match, value)
return true
case "-s":
i.Source = IptableCIDR(value)
return true
case "-d":
i.Destination = IptableCIDR(value)
return true
case "-p":
i.Proto = IptableProto(value)
return true
case "-j":
i.Jump = value
return true
case "--dport":
port,_ := strconv.ParseUint(value, 10, 16)
i.Dport = IptablePort(port)
return true
case "--sport":
port,_ := strconv.ParseUint(value, 10, 16)
i.Sport = IptablePort(port)
return true
default:
if opt[0] == '-' {
i.Flags = append(i.Flags, ExtensionFlag{ Name: strings.Trim(opt, "-"), Value: strings.TrimSpace(value)})
return true
}
}
return false
}
func (i *Iptable) GetFlagValue(opt string) any {
switch opt {
case "-i":
return i.In
case "-o":
return i.Out
case "-m":
return i.Match
case "-s":
return i.Source
case "-d":
return i.Destination
case "-p":
return i.Proto
case "-j":
return i.Jump
case "--dport":
return strconv.Itoa(int(i.Dport))
case "--sport":
return strconv.Itoa(int(i.Sport))
default:
if opt[0] == '-' {
return i.Flags
}
}
return nil
}
func (i *Iptable) SetRule(flags []string) (assigned bool) {
assigned = true
for index, flag := range flags {
if flag[0] == '-' {
flag := flags[index]
value := flags[index + 1]
if value[0] != '-' {
if ! i.SetFlagValue(flag, value) {
assigned = false
}
}
}
}
return
}
func (i *Iptable) MatchRule(flags []string) (match bool) {
match = true
for index, flag := range flags {
if flag[0] == '-' {
value := flags[index + 1]
switch v := i.GetFlagValue(flag).(type) {
case []string:
for _,element := range v {
if element == value {
continue
}
}
match = false
case []ExtensionFlag:
for _,element := range v {
if element.Name == flag && element.Value == value {
continue
}
}
match = false
case IptableCIDR:
if v == IptableCIDR(value) {
continue
}
match = false
case IptableName:
if v == IptableName(value) {
continue
}
match = false
case IptableChain:
if v == IptableChain(value) {
continue
}
match = false
default:
if v.(string) == value {
continue
}
match = false
}
}
}
return
}
func (i *Iptable) ReadChainLength() error {
c := command.NewCommand()
c.Path = "iptables"
c.Args = []command.CommandArg{
command.CommandArg("-S"),
command.CommandArg("{{ .Chain }}"),
}
output,err := c.Execute(i)
if err == nil {
linesCount := strings.Count(string(output), "\n")
if linesCount > 0 {
i.ChainLength = uint(linesCount) - 1
} else {
i.ChainLength = 0
}
}
return err
}
func (i *Iptable) Create(ctx context.Context) error {
if i.Id > 0 {
if lenErr := i.ReadChainLength(); lenErr != nil {
return lenErr
}
}
_, err := i.CreateCommand.Execute(i)
//slog.Info("IptableChain Create()", "err", err, "errstr", err.Error(), "iptable", i, "createcommand", i.CreateCommand)
// TODO add Command status/error handler rather than using the read extractor
if i.CreateCommand.Extractor != nil {
if err != nil {
return i.CreateCommand.Extractor([]byte(err.Error()), i)
}
}
return nil
}
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) Update(ctx context.Context) error {
return i.Create(ctx)
}
func (i *Iptable) Delete(ctx context.Context) error {
return nil
}
func (i *Iptable) Type() string { return "iptable" }
func (i *IptableType) NewCRUD() (create *command.Command, read *command.Command, update *command.Command, del *command.Command) {
switch *i {
case IptableTypeRule:
return NewIptableCreateCommand(), NewIptableReadCommand(), NewIptableUpdateCommand(), NewIptableDeleteCommand()
case IptableTypeChain:
return NewIptableChainCreateCommand(), NewIptableChainReadCommand(), NewIptableChainUpdateCommand(), NewIptableChainDeleteCommand()
default:
}
return nil, nil, nil, nil
}
func (i *IptableType) UnmarshalValue(value string) error {
switch value {
case string(IptableTypeRule), string(IptableTypeChain):
*i = IptableType(value)
return nil
default:
return errors.New("invalid IptableType value")
}
}
func (i *IptableType) UnmarshalJSON(data []byte) error {
var s string
if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
return unmarshalRouteTypeErr
}
return i.UnmarshalValue(s)
}
func (i *IptableType) UnmarshalYAML(value *yaml.Node) error {
var s string
if err := value.Decode(&s); err != nil {
return err
}
return i.UnmarshalValue(s)
}
func NewIptableCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "iptables"
c.Args = []command.CommandArg{
command.CommandArg("-t"),
command.CommandArg("{{ .Table }}"),
command.CommandArg("{{ if le .Id .ChainLength }}-R{{ else }}-A{{ end }}"),
command.CommandArg("{{ .Chain }}"),
command.CommandArg("{{ if le .Id .ChainLength }}{{ .Id }}{{ end }}"),
command.CommandArg("{{ if .In }}-i {{ .In }}{{ else if .Out }}-o {{ .Out }}{{ end }}"),
command.CommandArg("{{ range .Match }}-m {{ . }} {{- end }}"),
command.CommandArg("{{ if .Source }}-s {{ .Source }}{{ end }}"),
command.CommandArg("{{ if .Sport }}--sport {{ .Sport }}{{ end }}"),
command.CommandArg("{{ if .Destination }}-d {{ .Destination }}{{ end }}"),
command.CommandArg("{{ if .Dport }}--dport {{ .Dport }}{{ end }}"),
command.CommandArg("{{ if .Proto }}-p {{ .Proto }}{{ end }}"),
command.CommandArg("{{ range .Flags }} --{{ .Name }} {{ .Value }} {{ end }}"),
command.CommandArg("{{ if .Jump }}-j {{ .Jump }}{{ end }}"),
}
return c
}
func IptableExtractRule(lineNumber uint, ruleLine string, target *Iptable) (state string, err error) {
state = "absent"
ruleFields := strings.Split(strings.TrimSpace(ruleLine), " ")
slog.Info("IptableExtractRule()", "lineNumber", lineNumber, "ruleLine", ruleLine, "target", target)
if ruleFields[0] == "-A" {
flags := ruleFields[2:]
if target.Id > 0 {
if target.Id == lineNumber {
slog.Info("IptableExtractRule() SetRule", "lineNumber", lineNumber, "flags", flags, "target", target)
if target.SetRule(flags) {
state = "present"
err = nil
}
}
} else {
if target.MatchRule(flags) {
target.Id = lineNumber
state = "present"
err = nil
}
}
} else {
err = fmt.Errorf("Invalid rule %d %s", lineNumber, ruleLine)
}
return
}
func NewIptableReadCommand() *command.Command {
c := command.NewCommand()
c.Path = "iptables"
c.Args = []command.CommandArg{
command.CommandArg("-t"),
command.CommandArg("{{ .Table }}"),
command.CommandArg("-S"),
command.CommandArg("{{ .Chain }}"),
command.CommandArg("{{ if .Id }}{{ .Id }}{{ end }}"),
}
c.Extractor = func(out []byte, target any) error {
i := target.(*Iptable)
if i.Id > 0 {
return RuleExtractor(out, target)
}
state := "absent"
var lineNumber uint = 1
lines := strings.Split(string(out), "\n")
numberOfLines := len(lines)
for _, line := range lines {
matchState, err := IptableExtractRule(lineNumber, line, i)
if matchState == "present" {
state = matchState
break
}
if err == nil {
lineNumber++
}
}
i.Common.State = state
if numberOfLines > 0 {
i.ChainLength = uint(numberOfLines) - 1
} else {
i.ChainLength = 0
}
return nil
}
return c
}
func NewIptableReadChainCommand() *command.Command {
c := command.NewCommand()
c.Path = "iptables"
c.Args = []command.CommandArg{
command.CommandArg("-t"),
command.CommandArg("{{ .Table }}"),
command.CommandArg("-S"),
command.CommandArg("{{ .Chain }}"),
}
c.Extractor = func(out []byte, target any) error {
IptableChainRules := target.(*[]*Iptable)
numberOfChainRules := len(*IptableChainRules)
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
numberOfLines := len(lines)
diff := (numberOfLines - 1) - numberOfChainRules
if diff > 0 {
for i := 0; i < diff; i++ {
*IptableChainRules = append(*IptableChainRules, NewIptable())
}
}
for lineIndex, line := range lines[1:] {
i := (*IptableChainRules)[lineIndex]
i.Id = uint(lineIndex + 1)
ruleFields := strings.Split(strings.TrimSpace(line), " ")
if ruleFields[0] == "-A" {
flags := ruleFields[2:]
if i.SetRule(flags) {
i.Common.State = "present"
} else {
i.Common.State = "absent"
}
}
}
return nil
}
return c
}
func NewIptableUpdateCommand() *command.Command {
return NewIptableCreateCommand()
}
func NewIptableDeleteCommand() *command.Command {
return nil
}
func NewIptableChainCreateCommand() *command.Command {
c := command.NewCommand()
c.Path = "iptables"
c.Args = []command.CommandArg{
command.CommandArg("-t"),
command.CommandArg("{{ .Table }}"),
command.CommandArg("-N"),
command.CommandArg("{{ .Chain }}"),
}
c.Extractor = func(out []byte, target any) error {
slog.Info("IptableChain Extractor", "output", out, "command", c)
for _,line := range strings.Split(string(out), "\n") {
if line == "iptables: Chain already exists." {
return nil
}
}
return fmt.Errorf(string(out))
}
return c
}
func ChainExtractor(out []byte, target any) error {
i := target.(*Iptable)
rules := strings.Split(string(out), "\n")
for _,rule := range rules {
ruleFields := strings.Split(strings.TrimSpace(string(rule)), " ")
switch ruleFields[0] {
case "-N", "-A":
chain := ruleFields[1]
if chain == string(i.Chain) {
i.Common.State = "present"
return nil
} else {
i.Common.State = "absent"
}
default:
i.Common.State = "absent"
}
}
return nil
}
func RuleExtractor(out []byte, target any) (err error) {
ipt := target.(*Iptable)
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
err = fmt.Errorf("Failed to extract rule by Id: %d", ipt.Id)
ipt.Common.State = "absent"
var lineIndex uint = 1
if uint(len(lines)) >= ipt.Id {
lineIndex = ipt.Id
} else if len(lines) > 2 {
return
}
ruleFields := strings.Split(strings.TrimSpace(lines[lineIndex]), " ")
slog.Info("RuleExtractor()", "lines", lines, "line", lines[lineIndex], "fields", ruleFields, "index", lineIndex)
if ruleFields[0] == "-A" {
if ipt.SetRule(ruleFields[2:]) {
ipt.Common.State = "present"
err = nil
}
}
return
}
func RuleExtractorMatchFlags(out []byte, target any) (err error) {
ipt := target.(*Iptable)
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
var linesCount uint = uint(len(lines))
err = fmt.Errorf("Failed to extract rule")
if linesCount > 0 {
ipt.ChainLength = linesCount - 1
ipt.Common.State = "absent"
for linesIndex, line := range lines {
ruleFields := strings.Split(strings.TrimSpace(line), " ")
slog.Info("RuleExtractorMatchFlags()", "lines", lines, "line", line, "fields", ruleFields, "index", linesIndex)
if ruleFields[0] == "-A" {
flags := ruleFields[2:]
if ipt.MatchRule(flags) {
slog.Info("RuleExtractorMatchFlags()", "flags", flags, "ipt", ipt)
err = nil
ipt.Common.State = "present"
ipt.Id = uint(linesIndex)
return
}
}
}
}
return
}
func RuleExtractorById(out []byte, target any) (err error) {
ipt := target.(*Iptable)
state := "absent"
lines := strings.Split(string(out), "\n")
err = fmt.Errorf("Failed to extract rule by Id: %d", ipt.Id)
ipt.ChainLength = 0
for _, line := range lines {
ruleFields := strings.Split(strings.TrimSpace(line), " ")
if ruleFields[0] == "-A" {
ipt.ChainLength++
flags := ruleFields[2:]
slog.Info("RuleExtractorById()", "target", ipt)
if ipt.Id == ipt.ChainLength {
if ipt.SetRule(flags) {
slog.Info("RuleExtractorById() SetRule", "flags", flags, "target", ipt)
state = "present"
err = nil
}
}
}
}
ipt.Common.State = state
return
}
func NewIptableChainReadCommand() *command.Command {
c := command.NewCommand()
c.Path = "iptables"
c.Args = []command.CommandArg{
command.CommandArg("-t"),
command.CommandArg("{{ .Table }}"),
command.CommandArg("-S"),
command.CommandArg("{{ .Chain }}"),
}
c.Extractor = func(out []byte, target any) error {
i := target.(*Iptable)
rules := strings.Split(string(out), "\n")
for _,rule := range rules {
ruleFields := strings.Split(strings.TrimSpace(string(rule)), " ")
slog.Info("IptableChain Extract()", "fields", ruleFields)
switch ruleFields[0] {
case "-N", "-A":
chain := ruleFields[1]
if chain == string(i.Chain) {
i.Common.State = "present"
return nil
} else {
i.Common.State = "absent"
}
default:
i.Common.State = "absent"
}
}
return nil
}
return c
}
func NewIptableChainUpdateCommand() *command.Command {
return NewIptableChainCreateCommand()
}
func NewIptableChainDeleteCommand() *command.Command {
return nil
}