update iptables
Some checks failed
Lint / golangci-lint (push) Failing after 9m50s
Declarative Tests / test (push) Failing after 9s

This commit is contained in:
Matthew Rich 2024-04-25 00:45:05 -07:00
parent f6f4258609
commit f25fa59449
10 changed files with 524 additions and 42 deletions

View File

@ -47,7 +47,6 @@ resources:
defer ts.Close() defer ts.Close()
yaml, cliErr := exec.Command("./jx", "import", "--resource", ts.URL).Output() yaml, cliErr := exec.Command("./jx", "import", "--resource", ts.URL).Output()
slog.Info("TestCliHTTPSource", "err", cliErr)
assert.Nil(t, cliErr) assert.Nil(t, cliErr)
assert.NotEqual(t, "", string(yaml)) assert.NotEqual(t, "", string(yaml))
assert.Greater(t, len(yaml), 0) assert.Greater(t, len(yaml), 0)

View File

@ -29,7 +29,7 @@ var (
) )
var GlobalOformat *string var GlobalOformat *string
var GlobalOutput *string var GlobalOutput string
var GlobalQuiet *bool var GlobalQuiet *bool
var ImportMerge *bool var ImportMerge *bool
@ -89,7 +89,6 @@ func LoadSourceURI(uri string) []*resource.Document {
if extractErr != nil { if extractErr != nil {
log.Fatal(extractErr) log.Fatal(extractErr)
} }
slog.Info("extract documents", "documents", extractDocuments)
return extractDocuments return extractDocuments
} }
return []*resource.Document{ resource.NewDocument() } return []*resource.Document{ resource.NewDocument() }
@ -103,7 +102,6 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
return e return e
} }
var encoder resource.Encoder
merged := resource.NewDocument() merged := resource.NewDocument()
documents := make([]*resource.Document, 0, 100) documents := make([]*resource.Document, 0, 100)
for _,source := range cmd.Args() { for _,source := range cmd.Args() {
@ -113,20 +111,20 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
} }
} }
/*
switch *GlobalOformat { switch *GlobalOformat {
case FormatYaml: case FormatYaml:
encoder = resource.NewYAMLEncoder(output) encoder = resource.NewYAMLEncoder(output)
case FormatJson: case FormatJson:
encoder = resource.NewJSONEncoder(output) encoder = resource.NewJSONEncoder(output)
} }
*/
if *GlobalOutput != "" { slog.Info("main.ImportResource", "args", os.Args, "output", GlobalOutput)
_, err := target.TargetTypes.New(*GlobalOutput) outputTarget, err := target.TargetTypes.New(GlobalOutput)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
//outputTarget.EmitResources(
}
if len(documents) == 0 { if len(documents) == 0 {
documents = append(documents, resource.NewDocument()) documents = append(documents, resource.NewDocument())
@ -151,18 +149,18 @@ func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
} else { } else {
if *ImportMerge { if *ImportMerge {
merged.ResourceDecls = append(merged.ResourceDecls, d.ResourceDecls...) merged.ResourceDecls = append(merged.ResourceDecls, d.ResourceDecls...)
slog.Info("merging", "doc", merged.ResourceDecls, "src", d.ResourceDecls)
} else { } else {
if documentGenerateErr := encoder.Encode(d); documentGenerateErr != nil { slog.Info("main.ImportResource", "outputTarget", outputTarget, "type", outputTarget.Type())
return documentGenerateErr if outputErr := outputTarget.EmitResources([]*resource.Document{d}, nil); outputErr != nil {
return outputErr
} }
} }
} }
} }
} }
if *ImportMerge { if *ImportMerge {
if documentGenerateErr := encoder.Encode(merged); documentGenerateErr != nil { if outputErr := outputTarget.EmitResources([]*resource.Document{merged}, nil); outputErr != nil {
return documentGenerateErr return outputErr
} }
} }
return err return err
@ -276,8 +274,8 @@ func main() {
for _,subCmd := range jxSubCommands { for _,subCmd := range jxSubCommands {
cmdFlagSet := flag.NewFlagSet(subCmd.Name, flag.ExitOnError) cmdFlagSet := flag.NewFlagSet(subCmd.Name, flag.ExitOnError)
GlobalOformat = cmdFlagSet.String("oformat", "yaml", "Output serialization format") GlobalOformat = cmdFlagSet.String("oformat", "yaml", "Output serialization format")
GlobalOutput = cmdFlagSet.String("output", "-", "Output target (default stdout)") cmdFlagSet.StringVar(&GlobalOutput, "output", "-", "Output target (default stdout)")
GlobalOutput = cmdFlagSet.String("o", "-", "Output target (default stdout)") cmdFlagSet.StringVar(&GlobalOutput, "o", "-", "Output target (default stdout)")
GlobalQuiet = cmdFlagSet.Bool("quiet", false, "Generate terse output.") GlobalQuiet = cmdFlagSet.Bool("quiet", false, "Generate terse output.")
switch subCmd.Name { switch subCmd.Name {
@ -295,22 +293,19 @@ func main() {
} }
case "import": case "import":
cmdFlagSet.Usage = func() { cmdFlagSet.Usage = func() {
fmt.Println("jx import source [source2]") fmt.Println("jx import source...")
cmdFlagSet.PrintDefaults() cmdFlagSet.PrintDefaults()
VersionUsage() VersionUsage()
} }
} }
slog.Info("command", "command", subCmd)
if os.Args[1] == subCmd.Name { if os.Args[1] == subCmd.Name {
if e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil { if e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil {
log.Fatal(e) log.Fatal(e)
} }
return return
} else { }
}
flag.PrintDefaults() flag.PrintDefaults()
VersionUsage() VersionUsage()
os.Exit(1) os.Exit(1)
}
}
} }

View File

@ -25,7 +25,7 @@ func (d *Document) Filter(filter ResourceSelector) []*Declaration {
resources := make([]*Declaration, 0, len(d.ResourceDecls)) resources := make([]*Declaration, 0, len(d.ResourceDecls))
for i := range d.ResourceDecls { for i := range d.ResourceDecls {
filterResource := &d.ResourceDecls[i] filterResource := &d.ResourceDecls[i]
if filter(filterResource) { if filter == nil || filter(filterResource) {
resources = append(resources, &d.ResourceDecls[i]) resources = append(resources, &d.ResourceDecls[i])
} }
} }

View File

@ -45,6 +45,7 @@ func init() {
// Manage the state of file system objects // Manage the state of file system objects
type File struct { type File struct {
normalizePath bool `json:"-" yaml:"-"`
Path string `json:"path" yaml:"path"` Path string `json:"path" yaml:"path"`
Owner string `json:"owner" yaml:"owner"` Owner string `json:"owner" yaml:"owner"`
Group string `json:"group" yaml:"group"` Group string `json:"group" yaml:"group"`
@ -69,13 +70,20 @@ type ResourceFileInfo struct {
func NewFile() *File { func NewFile() *File {
currentUser, _ := user.Current() currentUser, _ := user.Current()
group, _ := user.LookupGroupId(currentUser.Gid) group, _ := user.LookupGroupId(currentUser.Gid)
f := &File{Owner: currentUser.Username, Group: group.Name, Mode: "0644", FileType: RegularFile} f := &File{ normalizePath: false, Owner: currentUser.Username, Group: group.Name, Mode: "0644", FileType: RegularFile}
slog.Info("NewFile()", "file", f) slog.Info("NewFile()", "file", f)
return f return f
} }
func NewNormalizedFile() *File {
f := NewFile()
f.normalizePath = true
return f
}
func (f *File) Clone() Resource { func (f *File) Clone() Resource {
return &File { return &File {
normalizePath: f.normalizePath,
Path: f.Path, Path: f.Path,
Owner: f.Owner, Owner: f.Owner,
Group: f.Group, Group: f.Group,
@ -100,10 +108,9 @@ func (f *File) SetURI(uri string) error {
resourceUri, e := url.Parse(uri) resourceUri, e := url.Parse(uri)
if e == nil { if e == nil {
if resourceUri.Scheme == "file" { if resourceUri.Scheme == "file" {
if absFilePath, err := filepath.Abs(filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())); err != nil { f.Path = filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())
if err := f.NormalizePath(); err != nil {
return err return err
} else {
f.Path = absFilePath
} }
} else { } else {
e = fmt.Errorf("%w: %s is not a file", ErrInvalidResourceURI, uri) e = fmt.Errorf("%w: %s is not a file", ErrInvalidResourceURI, uri)
@ -187,26 +194,31 @@ func (f *File) Apply() error {
return nil return nil
} }
func (f *File) LoadDecl(yamlResourceDeclaration string) error { func (f *File) LoadDecl(yamlResourceDeclaration string) (err error) {
d := NewYAMLStringDecoder(yamlResourceDeclaration) d := NewYAMLStringDecoder(yamlResourceDeclaration)
return d.Decode(f) err = d.Decode(f)
if err == nil {
f.UpdateContentAttributes()
}
return
} }
func (f *File) ResolveId(ctx context.Context) string { func (f *File) ResolveId(ctx context.Context) string {
filePath, fileAbsErr := filepath.Abs(f.Path) if e := f.NormalizePath(); e != nil {
if fileAbsErr != nil { panic(e)
panic(fileAbsErr)
} }
f.Path = filePath return f.Path
return filePath
} }
func (f *File) NormalizePath() error { func (f *File) NormalizePath() error {
if f.normalizePath {
filePath, fileAbsErr := filepath.Abs(f.Path) filePath, fileAbsErr := filepath.Abs(f.Path)
if fileAbsErr == nil { if fileAbsErr == nil {
f.Path = filePath f.Path = filePath
} }
return fileAbsErr return fileAbsErr
}
return nil
} }
func (f *File) FileInfo() fs.FileInfo { func (f *File) FileInfo() fs.FileInfo {
@ -214,7 +226,8 @@ func (f *File) FileInfo() fs.FileInfo {
} }
func (f *ResourceFileInfo) Name() string { func (f *ResourceFileInfo) Name() string {
return filepath.Base(f.resource.Path) // return filepath.Base(f.resource.Path)
return f.resource.Path
} }
func (f *ResourceFileInfo) Size() int64 { func (f *ResourceFileInfo) Size() int64 {
@ -246,6 +259,11 @@ func (f *ResourceFileInfo) Sys() any {
return nil return nil
} }
func (f *File) UpdateContentAttributes() {
f.Size = int64(len(f.Content))
f.Sha256 = fmt.Sprintf("%x", sha256.Sum256([]byte(f.Content)))
}
func (f *File) UpdateAttributesFromFileInfo(info os.FileInfo) error { func (f *File) UpdateAttributesFromFileInfo(info os.FileInfo) error {
if info != nil { if info != nil {
f.Mtime = info.ModTime() f.Mtime = info.ModTime()

View File

@ -0,0 +1,307 @@
// 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
}

View File

@ -0,0 +1,76 @@
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
package resource
import (
"context"
_ "encoding/json"
_ "fmt"
"github.com/stretchr/testify/assert"
_ "gopkg.in/yaml.v3"
_ "io"
_ "log"
_ "net/http"
_ "net/http/httptest"
_ "net/url"
_ "os"
_ "path/filepath"
_ "strings"
_ "syscall"
"testing"
_ "time"
)
func TestNewIptableResource(t *testing.T) {
i := NewIptable()
assert.NotNil(t, i)
}
func TestIptableApplyResourceTransformation(t *testing.T) {
i := NewIptable()
assert.NotNil(t, i)
//e := f.Apply()
//assert.Equal(t, nil, e)
}
func TestReadIptable(t *testing.T) {
ctx := context.Background()
testRule := NewIptable()
assert.NotNil(t, testRule)
declarationAttributes := `
id: 0
table: "filter"
chain: "INPUT"
source: "192.168.0.0/24"
destination: "192.168.0.1"
jump: "ACCEPT"
state: present
`
m := &MockCommand{
Executor: func(value any) ([]byte, error) {
return nil, nil
},
Extractor: func(output []byte, target any) error {
testRule.Table = "filter"
testRule.Chain = "INPUT"
testRule.Id = 0
testRule.In = "eth0"
testRule.Source = "192.168.0.0/24"
testRule.State = "present"
return nil
},
}
e := testRule.LoadDecl(declarationAttributes)
assert.Nil(t, e)
testRule.ReadCommand = (*Command)(m)
// testRuleErr := testRule.Apply()
// assert.Nil(t, testRuleErr)
r, e := testRule.Read(ctx)
assert.Nil(t, e)
assert.NotNil(t, r)
assert.Equal(t, "eth0", testRule.In)
}

View File

@ -38,6 +38,7 @@ func TestResolveId(t *testing.T) {
testFile := NewResource("file://../../README.md") testFile := NewResource("file://../../README.md")
assert.NotNil(t, testFile) assert.NotNil(t, testFile)
testFile.(*File).normalizePath = true
absolutePath, e := filepath.Abs("../../README.md") absolutePath, e := filepath.Abs("../../README.md")
assert.Nil(t, e) assert.Nil(t, e)

View File

@ -15,7 +15,8 @@
{ "$ref": "http-declaration.jsonschema" }, { "$ref": "http-declaration.jsonschema" },
{ "$ref": "user-declaration.jsonschema" }, { "$ref": "user-declaration.jsonschema" },
{ "$ref": "exec-declaration.jsonschema" }, { "$ref": "exec-declaration.jsonschema" },
{ "$ref": "network_route-declaration.jsonschema" } { "$ref": "network_route-declaration.jsonschema" },
{ "$ref": "iptable-declaration.jsonschema" }
] ]
} }
} }

View File

@ -0,0 +1,17 @@
{
"$id": "iptable-declaration.jsonschema",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "iptable-declaration",
"type": "object",
"required": [ "type", "attributes" ],
"properties": {
"type": {
"type": "string",
"description": "Resource type name.",
"enum": [ "iptable" ]
},
"attributes": {
"$ref": "iptable.jsonschema"
}
}
}

View File

@ -0,0 +1,68 @@
{
"$id": "iptable.jsonschema",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "iptable",
"type": "object",
"required": [ "chain" ],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"table": {
"type": "string",
"description": "Rule table name"
},
"chain": {
"type": "string",
"description": "Rule chain name"
},
"destination": {
"type": "string",
"description": "Destination CIDR",
"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$"
},
"source": {
"type": "string",
"description": "Source CIDR",
"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$"
},
"in": {
"type": "string",
"description": "Input ethernet device"
},
"out": {
"type": "string",
"description": "Output ethernet device"
},
"match": {
"type": "array",
"description": "Rule match extensions",
"items": {
"type": "string"
}
},
"extension_flags": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"proto": {
"type": "string",
"description": "Rule protocol",
"pattern": "^(tcp|udp|udplite|icmp|icmpv6|ESP|AH|sctp|mh|all|[0-9]+)$"
},
"jump": {
"type": "string"
}
}
}