308 lines
7.6 KiB
Go
308 lines
7.6 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"
|
||
|
)
|
||
|
|
||
|
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
|
||
|
}
|