Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added StructuredStats method to returned structured stat data #56

Merged
merged 2 commits into from
May 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions iptables/iptables.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ type IPTables struct {
mode string // the underlying iptables operating mode, e.g. nf_tables
}

// Stat represents a structured statistic entry.
type Stat struct {
Packets uint64 `json:"pkts"`
Bytes uint64 `json:"bytes"`
Target string `json:"target"`
Protocol string `json:"prot"`
Opt string `json:"opt"`
Input string `json:"in"`
Output string `json:"out"`
Source *net.IPNet `json:"source"`
Destination *net.IPNet `json:"destination"`
Options string `json:"options"`
}

// New creates a new IPTables.
// For backwards compatibility, this always uses IPv4, i.e. "iptables".
func New() (*IPTables, error) {
Expand Down Expand Up @@ -263,6 +277,63 @@ func (ipt *IPTables) Stats(table, chain string) ([][]string, error) {
return rows, nil
}

// ParseStat parses a single statistic row into a Stat struct. The input should
// be a string slice that is returned from calling the Stat method.
func (ipt *IPTables) ParseStat(stat []string) (parsed Stat, err error) {
// For forward-compatibility, expect at least 10 fields in the stat
if len(stat) < 10 {
return parsed, fmt.Errorf("stat contained fewer fields than expected")
}

// Convert the fields that are not plain strings
parsed.Packets, err = strconv.ParseUint(stat[0], 0, 64)
if err != nil {
return parsed, fmt.Errorf(err.Error(), "could not parse packets")
}
parsed.Bytes, err = strconv.ParseUint(stat[1], 0, 64)
if err != nil {
return parsed, fmt.Errorf(err.Error(), "could not parse bytes")
}
_, parsed.Source, err = net.ParseCIDR(stat[7])
if err != nil {
return parsed, fmt.Errorf(err.Error(), "could not parse source")
}
_, parsed.Destination, err = net.ParseCIDR(stat[8])
if err != nil {
return parsed, fmt.Errorf(err.Error(), "could not parse destination")
}

// Put the fields that are strings
parsed.Target = stat[2]
parsed.Protocol = stat[3]
parsed.Opt = stat[4]
parsed.Input = stat[5]
parsed.Output = stat[6]
parsed.Options = stat[9]

return parsed, nil
}

// StructuredStats returns statistics as structured data which may be further
// parsed and marshaled.
func (ipt *IPTables) StructuredStats(table, chain string) ([]Stat, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you add a separate ParseStats function that just takes a []string and parses the stats accordingly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally was going to do exactly that. The only issue I foresee is that exposing a ParseStats method that accepts an arbitrary []string (or [][]string) may be subject to misuse.

Anyway, two questions I'm looking for your feedback on:

  1. I can make a ParseStat(stat []string) (Stat, error) and/or a ParseStats(stats [][]string) ([]Stat, error). Any preference for either or both?

  2. Would you like me to remove StructuredStats(table, chain string) ([]Stat, error) completely?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I pushed up a contribution with ParseStat(stat []string) (Stat, error) and left StructuredStats(table, chain string) ([]Stat, error) for now pending feedback.

rawStats, err := ipt.Stats(table, chain)
if err != nil {
return nil, err
}

structStats := []Stat{}
for _, rawStat := range rawStats {
stat, err := ipt.ParseStat(rawStat)
if err != nil {
return nil, err
}
structStats = append(structStats, stat)
}

return structStats, nil
}

func (ipt *IPTables) executeList(args []string) ([]string, error) {
var stdout bytes.Buffer
if err := ipt.runWithOutput(args, &stdout); err != nil {
Expand Down
36 changes: 36 additions & 0 deletions iptables/iptables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"crypto/rand"
"fmt"
"math/big"
"net"
"os"
"reflect"
"testing"
Expand Down Expand Up @@ -307,6 +308,41 @@ func runRulesTests(t *testing.T, ipt *IPTables) {
t.Fatalf("Stats mismatch: \ngot %#v \nneed %#v", stats, expectedStats)
}

structStats, err := ipt.StructuredStats("filter", chain)
if err != nil {
t.Fatalf("StructuredStats failed: %v", err)
}

// It's okay to not check the following errors as they will be evaluated
// in the subsequent usage
_, address1CIDR, _ := net.ParseCIDR(address1)
_, address2CIDR, _ := net.ParseCIDR(address2)
_, subnet1CIDR, _ := net.ParseCIDR(subnet1)
_, subnet2CIDR, _ := net.ParseCIDR(subnet2)

expectedStructStats := []Stat{
{0, 0, "ACCEPT", "all", opt, "*", "*", subnet1CIDR, address1CIDR, ""},
{0, 0, "ACCEPT", "all", opt, "*", "*", subnet2CIDR, address2CIDR, ""},
{0, 0, "ACCEPT", "all", opt, "*", "*", subnet2CIDR, address1CIDR, ""},
{0, 0, "ACCEPT", "all", opt, "*", "*", address1CIDR, subnet2CIDR, ""},
}

if !reflect.DeepEqual(structStats, expectedStructStats) {
t.Fatalf("StructuredStats mismatch: \ngot %#v \nneed %#v",
structStats, expectedStructStats)
}

for i, stat := range expectedStats {
stat, err := ipt.ParseStat(stat)
if err != nil {
t.Fatalf("ParseStat failed: %v", err)
}
if !reflect.DeepEqual(stat, expectedStructStats[i]) {
t.Fatalf("ParseStat mismatch: \ngot %#v \nneed %#v",
stat, expectedStructStats[i])
}
}

// Clear the chain that was created.
err = ipt.ClearChain("filter", chain)
if err != nil {
Expand Down