console/filter: let parser tells us if we need the main table or not

This is more robust this way. We also introduce the ability to reverse
the direction of a filter.
This commit is contained in:
Vincent Bernat
2022-08-04 18:32:44 +02:00
parent 2610bd48d1
commit 1aa260bae2
13 changed files with 322 additions and 193 deletions

View File

@@ -12,10 +12,7 @@ import (
"time" "time"
) )
var ( var resolutionRegexp = regexp.MustCompile(`{resolution->(\d+)}`)
addressOrPortRegexp = regexp.MustCompile(`\b(?:Src|Dst)(?:Port|Addr)\b`)
resolutionRegexp = regexp.MustCompile(`{resolution->(\d+)}`)
)
// flowsTable describe a consolidated or unconsolidated flows table. // flowsTable describe a consolidated or unconsolidated flows table.
type flowsTable struct { type flowsTable struct {
@@ -29,14 +26,14 @@ type flowsTable struct {
// should contain `{table}` which will be replaced by the appropriate // should contain `{table}` which will be replaced by the appropriate
// flows table and {timefilter} which will be replaced by the // flows table and {timefilter} which will be replaced by the
// appropriate time filter. // appropriate time filter.
func (c *Component) queryFlowsTable(query string, start, end time.Time, targetResolution time.Duration) string { func (c *Component) queryFlowsTable(query string, mainTableRequired bool, start, end time.Time, targetResolution time.Duration) string {
c.flowsTablesLock.RLock() c.flowsTablesLock.RLock()
defer c.flowsTablesLock.RUnlock() defer c.flowsTablesLock.RUnlock()
// Select table // Select table
table := "flows" table := "flows"
resolution := time.Second resolution := time.Second
if !addressOrPortRegexp.MatchString(query) { if !mainTableRequired {
// We can use the consolidated data. The first // We can use the consolidated data. The first
// criteria is to find the tables matching the time // criteria is to find the tables matching the time
// criteria. // criteria.

View File

@@ -75,6 +75,7 @@ func TestQueryFlowsTables(t *testing.T) {
Description string Description string
Tables []flowsTable Tables []flowsTable
Query string Query string
MainTableRequired bool
Start time.Time Start time.Time
End time.Time End time.Time
Resolution time.Duration Resolution time.Duration
@@ -83,6 +84,7 @@ func TestQueryFlowsTables(t *testing.T) {
{ {
Description: "query with source port", Description: "query with source port",
Query: "SELECT TimeReceived, SrcPort FROM {table} WHERE {timefilter}", Query: "SELECT TimeReceived, SrcPort FROM {table} WHERE {timefilter}",
MainTableRequired: true,
Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC), Start: time.Date(2022, 04, 10, 15, 45, 10, 0, time.UTC),
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Expected: "SELECT TimeReceived, SrcPort FROM flows WHERE TimeReceived BETWEEN toDateTime('2022-04-10 15:45:10', 'UTC') AND toDateTime('2022-04-11 15:45:10', 'UTC')", Expected: "SELECT TimeReceived, SrcPort FROM flows WHERE TimeReceived BETWEEN toDateTime('2022-04-10 15:45:10', 'UTC') AND toDateTime('2022-04-11 15:45:10', 'UTC')",
@@ -194,7 +196,7 @@ func TestQueryFlowsTables(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.Description, func(t *testing.T) { t.Run(tc.Description, func(t *testing.T) {
c.flowsTables = tc.Tables c.flowsTables = tc.Tables
got := c.queryFlowsTable(tc.Query, tc.Start, tc.End, tc.Resolution) got := c.queryFlowsTable(tc.Query, tc.MainTableRequired, tc.Start, tc.End, tc.Resolution)
if diff := helpers.Diff(got, tc.Expected); diff != "" { if diff := helpers.Diff(got, tc.Expected); diff != "" {
t.Fatalf("queryFlowsTable(): (-got, +want):\n%s", diff) t.Fatalf("queryFlowsTable(): (-got, +want):\n%s", diff)
} }

View File

@@ -42,7 +42,7 @@ func (c *Component) filterValidateHandlerFunc(gc *gin.Context) {
}) })
return return
} }
got, err := filter.Parse("", []byte(input.Filter)) got, err := filter.Parse("", []byte(input.Filter), filter.GlobalStore("meta", &filter.Meta{}))
if err == nil { if err == nil {
gc.JSON(http.StatusOK, filterValidateHandlerOutput{ gc.JSON(http.StatusOK, filterValidateHandlerOutput{
Message: "ok", Message: "ok",
@@ -84,7 +84,8 @@ func (c *Component) filterCompleteHandlerFunc(gc *gin.Context) {
completions := []filterCompletion{} completions := []filterCompletion{}
switch input.What { switch input.What {
case "column": case "column":
_, err := filter.Parse("", []byte{}, filter.Entrypoint("ConditionExpr")) _, err := filter.Parse("", []byte{},
filter.Entrypoint("ConditionExpr"), filter.GlobalStore("meta", &filter.Meta{}))
if err != nil { if err != nil {
for _, candidate := range filter.Expected(err) { for _, candidate := range filter.Expected(err) {
if !strings.HasSuffix(candidate, `"i`) { if !strings.HasSuffix(candidate, `"i`) {
@@ -100,7 +101,8 @@ func (c *Component) filterCompleteHandlerFunc(gc *gin.Context) {
case "operator": case "operator":
_, err := filter.Parse("", _, err := filter.Parse("",
[]byte(fmt.Sprintf("%s ", input.Column)), []byte(fmt.Sprintf("%s ", input.Column)),
filter.Entrypoint("ConditionExpr")) filter.Entrypoint("ConditionExpr"),
filter.GlobalStore("meta", &filter.Meta{}))
if err != nil { if err != nil {
for _, candidate := range filter.Expected(err) { for _, candidate := range filter.Expected(err) {
if !strings.HasPrefix(candidate, `"`) { if !strings.HasPrefix(candidate, `"`) {

View File

@@ -13,7 +13,7 @@ func TestFilterHumanError(t *testing.T) {
_, err := Parse("", []byte(` _, err := Parse("", []byte(`
InIfDescription = "Gi0/0/0/0" InIfDescription = "Gi0/0/0/0"
AND Proto = 1000 AND Proto = 1000
OR `)) OR `), GlobalStore("meta", &Meta{}))
expected := "at line 3, position 13: expecting an unsigned 8-bit integer" expected := "at line 3, position 13: expecting an unsigned 8-bit integer"
if diff := helpers.Diff(HumanError(err), expected); diff != "" { if diff := helpers.Diff(HumanError(err), expected); diff != "" {
t.Errorf("HumanError() (-got, +want):\n%s", diff) t.Errorf("HumanError() (-got, +want):\n%s", diff)
@@ -24,7 +24,7 @@ func TestAllErrors(t *testing.T) {
_, err := Parse("", []byte(` _, err := Parse("", []byte(`
InIfDescription = "Gi0/0/0/0" InIfDescription = "Gi0/0/0/0"
AND Proto = 1000 AND Proto = 1000
OR`)) OR`), GlobalStore("meta", &Meta{}))
// Currently, the parser stops at the first error. // Currently, the parser stops at the first error.
expected := Errors{ expected := Errors{
oneError{ oneError{
@@ -40,7 +40,7 @@ OR`))
} }
func TestExpected(t *testing.T) { func TestExpected(t *testing.T) {
_, err := Parse("", []byte{}, Entrypoint("ConditionBoundaryExpr")) _, err := Parse("", []byte{}, Entrypoint("ConditionBoundaryExpr"), GlobalStore("meta", &Meta{}))
expected := []string{`"InIfBoundary"i`, `"OutIfBoundary"i`} expected := []string{`"InIfBoundary"i`, `"OutIfBoundary"i`}
if diff := helpers.Diff(Expected(err), expected); diff != "" { if diff := helpers.Diff(Expected(err), expected); diff != "" {
t.Errorf("AllErrors() (-got, +want):\n%s", diff) t.Errorf("AllErrors() (-got, +want):\n%s", diff)

View File

@@ -6,8 +6,36 @@ package filter
import ( import (
"encoding/binary" "encoding/binary"
"net" "net"
"strings"
) )
// Meta is used to inject/retrieve state from the parser.
type Meta struct {
// ReverseDirection tells if we require the reverse direction for the provided filter (used as input)
ReverseDirection bool
// MainTableRequired tells if the main table is required to execute the expression (used as output)
MainTableRequired bool
}
func (c *current) reverseDirection(direct string) string {
if c.globalStore["meta"].(*Meta).ReverseDirection {
if strings.HasPrefix(direct, "Src") {
return "Dst" + direct[3:]
}
if strings.HasPrefix(direct, "Dst") {
return "Src" + direct[3:]
}
if strings.HasPrefix(direct, "In") {
return "Out" + direct[2:]
}
if strings.HasPrefix(direct, "Out") {
return "In" + direct[3:]
}
panic("no reverse?")
}
return direct
}
func lastIP(subnet *net.IPNet) net.IP { func lastIP(subnet *net.IPNet) net.IP {
if subnet.IP.To4() != nil { if subnet.IP.To4() != nil {
// IPv4 case // IPv4 case
@@ -24,3 +52,25 @@ func lastIP(subnet *net.IPNet) net.IP {
} }
return ip return ip
} }
func quote(v interface{}) string {
return "'" + strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(toString(v)) + "'"
}
func toSlice(v interface{}) []interface{} {
if v == nil {
return nil
}
return v.([]interface{})
}
func toString(v interface{}) string {
switch s := v.(type) {
case string:
return s
case []byte:
return string(s)
default:
panic("not a string")
}
}

View File

@@ -4,39 +4,18 @@
package filter package filter
// Convert SQL-like language for filters to SQL.
import ( import (
"fmt" "fmt"
"net" "net"
"akvorado/common/helpers" "akvorado/common/helpers"
) )
func quote(v interface{}) string {
return "'" + strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(toString(v)) + "'"
}
func toSlice(v interface{}) []interface{} {
if v == nil {
return nil
}
return v.([]interface{})
}
func toString(v interface{}) string {
switch s := v.(type) {
case string:
return s
case []byte:
return string(s)
default:
panic("not a string")
}
}
} }
Input ← _ expr:Expr _ EOF { Input ← _ expr:Expr _ EOF {
meta := c.globalStore["meta"].(*Meta)
_, ok := c.state["main-table-only"]
meta.MainTableRequired = ok
return expr, nil return expr, nil
} }
@@ -69,8 +48,10 @@ ConditionExpr "conditional" ←
ColumnIP ← ColumnIP ←
"ExporterAddress"i { return "ExporterAddress", nil } "ExporterAddress"i { return "ExporterAddress", nil }
/ "SrcAddr"i { return "SrcAddr", nil } / "SrcAddr"i #{ c.state["main-table-only"] = true ; return nil }
/ "DstAddr"i { return "DstAddr", nil } { return c.reverseDirection("SrcAddr"), nil }
/ "DstAddr"i #{ c.state["main-table-only"] = true ; return nil }
{ return c.reverseDirection("DstAddr"), nil }
ConditionIPExpr "condition on IP" ← ConditionIPExpr "condition on IP" ←
column:ColumnIP _ column:ColumnIP _
operator:("=" / "!=") _ ip:IP { operator:("=" / "!=") _ ip:IP {
@@ -92,26 +73,26 @@ ConditionStringExpr "condition on string" ←
/ "ExporterSite"i { return "ExporterSite", nil } / "ExporterSite"i { return "ExporterSite", nil }
/ "ExporterRegion"i { return "ExporterRegion", nil } / "ExporterRegion"i { return "ExporterRegion", nil }
/ "ExporterTenant"i { return "ExporterTenant", nil } / "ExporterTenant"i { return "ExporterTenant", nil }
/ "SrcCountry"i { return "SrcCountry", nil } / "SrcCountry"i { return c.reverseDirection("SrcCountry"), nil }
/ "DstCountry"i { return "DstCountry", nil } / "DstCountry"i { return c.reverseDirection("DstCountry"), nil }
/ "SrcNetName"i { return "SrcNetName", nil } / "SrcNetName"i { return c.reverseDirection("SrcNetName"), nil }
/ "DstNetName"i { return "DstNetName", nil } / "DstNetName"i { return c.reverseDirection("DstNetName"), nil }
/ "SrcNetRole"i { return "SrcNetRole", nil } / "SrcNetRole"i { return c.reverseDirection("SrcNetRole"), nil }
/ "DstNetRole"i { return "DstNetRole", nil } / "DstNetRole"i { return c.reverseDirection("DstNetRole"), nil }
/ "SrcNetSite"i { return "SrcNetSite", nil } / "SrcNetSite"i { return c.reverseDirection("SrcNetSite"), nil }
/ "DstNetSite"i { return "DstNetSite", nil } / "DstNetSite"i { return c.reverseDirection("DstNetSite"), nil }
/ "SrcNetRegion"i { return "SrcNetRegion", nil } / "SrcNetRegion"i { return c.reverseDirection("SrcNetRegion"), nil }
/ "DstNetRegion"i { return "DstNetRegion", nil } / "DstNetRegion"i { return c.reverseDirection("DstNetRegion"), nil }
/ "SrcNetTenant"i { return "SrcNetTenant", nil } / "SrcNetTenant"i { return c.reverseDirection("SrcNetTenant"), nil }
/ "DstNetTenant"i { return "DstNetTenant", nil } / "DstNetTenant"i { return c.reverseDirection("DstNetTenant"), nil }
/ "InIfName"i { return "InIfName", nil } / "InIfName"i { return c.reverseDirection("InIfName"), nil }
/ "OutIfName"i { return "OutIfName", nil } / "OutIfName"i { return c.reverseDirection("OutIfName"), nil }
/ "InIfDescription"i { return "InIfDescription", nil } / "InIfDescription"i { return c.reverseDirection("InIfDescription"), nil }
/ "OutIfDescription"i { return "OutIfDescription", nil } / "OutIfDescription"i { return c.reverseDirection("OutIfDescription"), nil }
/ "InIfConnectivity"i { return "InIfConnectivity", nil } / "InIfConnectivity"i { return c.reverseDirection("InIfConnectivity"), nil }
/ "OutIfConnectivity"i { return "OutIfConnectivity", nil } / "OutIfConnectivity"i { return c.reverseDirection("OutIfConnectivity"), nil }
/ "InIfProvider"i { return "InIfProvider", nil } / "InIfProvider"i { return c.reverseDirection("InIfProvider"), nil }
/ "OutIfProvider"i { return "OutIfProvider", nil }) _ / "OutIfProvider"i { return c.reverseDirection("OutIfProvider"), nil }) _
rcond:RConditionStringExpr { rcond:RConditionStringExpr {
return fmt.Sprintf("%s %s", toString(column), toString(rcond)), nil return fmt.Sprintf("%s %s", toString(column), toString(rcond)), nil
} }
@@ -124,16 +105,16 @@ RConditionStringExpr "condition on string" ←
} }
ConditionBoundaryExpr "condition on boundary" ← ConditionBoundaryExpr "condition on boundary" ←
column:("InIfBoundary"i { return "InIfBoundary", nil } column:("InIfBoundary"i { return c.reverseDirection("InIfBoundary"), nil }
/ "OutIfBoundary"i { return "OutIfBoundary", nil }) _ / "OutIfBoundary"i { return c.reverseDirection("OutIfBoundary"), nil }) _
operator:("=" / "!=") _ operator:("=" / "!=") _
boundary:("external"i / "internal"i / "undefined"i) { boundary:("external"i / "internal"i / "undefined"i) {
return fmt.Sprintf("%s %s %s", toString(column), toString(operator), return fmt.Sprintf("%s %s %s", toString(column), toString(operator),
quote(strings.ToLower(toString(boundary)))), nil quote(strings.ToLower(toString(boundary)))), nil
} }
ConditionSpeedExpr "condition on speed" ← ConditionSpeedExpr "condition on speed" ←
column:("InIfSpeed"i { return "InIfSpeed", nil } column:("InIfSpeed"i { return c.reverseDirection("InIfSpeed"), nil }
/ "OutIfSpeed"i { return "OutIfSpeed", nil }) _ / "OutIfSpeed"i { return c.reverseDirection("OutIfSpeed"), nil }) _
operator:("=" / ">=" / "<=" / "<" / ">" / "!=") _ operator:("=" / ">=" / "<=" / "<" / ">" / "!=") _
value:Unsigned64 { value:Unsigned64 {
return fmt.Sprintf("%s %s %s", toString(column), toString(operator), toString(value)), nil return fmt.Sprintf("%s %s %s", toString(column), toString(operator), toString(value)), nil
@@ -145,15 +126,15 @@ ConditionForwardingStatusExpr "condition on forwarding status" ←
return fmt.Sprintf("%s %s %s", toString(column), toString(operator), toString(value)), nil return fmt.Sprintf("%s %s %s", toString(column), toString(operator), toString(value)), nil
} }
ConditionPortExpr "condition on port" ← ConditionPortExpr "condition on port" ←
column:("SrcPort"i { return "SrcPort", nil } column:("SrcPort"i #{ c.state["main-table-only"] = true ; return nil } { return c.reverseDirection("SrcPort"), nil }
/ "DstPort"i { return "DstPort", nil }) _ / "DstPort"i #{ c.state["main-table-only"] = true ; return nil } { return c.reverseDirection("DstPort"), nil }) _
operator:("=" / ">=" / "<=" / "<" / ">" / "!=") _ value:Unsigned16 { operator:("=" / ">=" / "<=" / "<" / ">" / "!=") _ value:Unsigned16 {
return fmt.Sprintf("%s %s %s", toString(column), toString(operator), toString(value)), nil return fmt.Sprintf("%s %s %s", toString(column), toString(operator), toString(value)), nil
} }
ConditionASExpr "condition on AS number" ← ConditionASExpr "condition on AS number" ←
column:("SrcAS"i { return "SrcAS", nil } column:("SrcAS"i { return c.reverseDirection("SrcAS"), nil }
/ "DstAS"i { return "DstAS", nil }) _ / "DstAS"i { return c.reverseDirection("DstAS"), nil }) _
rcond:RConditionASExpr { rcond:RConditionASExpr {
return fmt.Sprintf("%s %s", toString(column), toString(rcond)), nil return fmt.Sprintf("%s %s", toString(column), toString(rcond)), nil
} }

View File

@@ -13,110 +13,192 @@ func TestValidFilter(t *testing.T) {
cases := []struct { cases := []struct {
Input string Input string
Output string Output string
MetaIn Meta
MetaOut Meta
}{ }{
{`ExporterName = 'something'`, `ExporterName = 'something'`}, {Input: `ExporterName = 'something'`, Output: `ExporterName = 'something'`},
{`exportername = 'something'`, `ExporterName = 'something'`}, {Input: `ExporterName = 'something'`, Output: `ExporterName = 'something'`,
{`ExporterName='something'`, `ExporterName = 'something'`}, MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{`ExporterName="something"`, `ExporterName = 'something'`}, {Input: `exportername = 'something'`, Output: `ExporterName = 'something'`},
{`ExporterName="something'"`, `ExporterName = 'something\''`}, {Input: `ExporterName='something'`, Output: `ExporterName = 'something'`},
{`ExporterName="something\"`, `ExporterName = 'something\\'`}, {Input: `ExporterName="something"`, Output: `ExporterName = 'something'`},
{`ExporterName!="something"`, `ExporterName != 'something'`}, {Input: `ExporterName="something'"`, Output: `ExporterName = 'something\''`},
{`ExporterName IN ("something")`, `ExporterName IN ('something')`}, {Input: `ExporterName="something\"`, Output: `ExporterName = 'something\\'`},
{`ExporterName IN ("something","something else")`, `ExporterName IN ('something', 'something else')`}, {Input: `ExporterName!="something"`, Output: `ExporterName != 'something'`},
{`ExporterName LIKE "something%"`, `ExporterName LIKE 'something%'`}, {Input: `ExporterName IN ("something")`, Output: `ExporterName IN ('something')`},
{`ExporterName UNLIKE "something%"`, `ExporterName NOT LIKE 'something%'`}, {Input: `ExporterName IN ("something","something else")`, Output: `ExporterName IN ('something', 'something else')`},
{`ExporterName IUNLIKE "something%"`, `ExporterName NOT ILIKE 'something%'`}, {Input: `ExporterName LIKE "something%"`, Output: `ExporterName LIKE 'something%'`},
{`ExporterName="something with spaces"`, `ExporterName = 'something with spaces'`}, {Input: `ExporterName UNLIKE "something%"`, Output: `ExporterName NOT LIKE 'something%'`},
{`ExporterName="something with 'quotes'"`, `ExporterName = 'something with \'quotes\''`}, {Input: `ExporterName IUNLIKE "something%"`, Output: `ExporterName NOT ILIKE 'something%'`},
{`ExporterAddress=203.0.113.1`, `ExporterAddress = toIPv6('203.0.113.1')`}, {Input: `ExporterName="something with spaces"`, Output: `ExporterName = 'something with spaces'`},
{`ExporterAddress=2001:db8::1`, `ExporterAddress = toIPv6('2001:db8::1')`}, {Input: `ExporterName="something with 'quotes'"`, Output: `ExporterName = 'something with \'quotes\''`},
{`ExporterAddress=2001:db8:0::1`, `ExporterAddress = toIPv6('2001:db8::1')`}, {Input: `ExporterAddress=203.0.113.1`, Output: `ExporterAddress = toIPv6('203.0.113.1')`},
{`ExporterAddress << 2001:db8:0::/64`, {Input: `ExporterAddress=2001:db8::1`, Output: `ExporterAddress = toIPv6('2001:db8::1')`},
`ExporterAddress BETWEEN toIPv6('2001:db8::') AND toIPv6('2001:db8::ffff:ffff:ffff:ffff')`}, {Input: `ExporterAddress=2001:db8:0::1`, Output: `ExporterAddress = toIPv6('2001:db8::1')`},
{`ExporterAddress << 2001:db8::c000/115`, {
`ExporterAddress BETWEEN toIPv6('2001:db8::c000') AND toIPv6('2001:db8::dfff')`}, Input: `ExporterAddress << 2001:db8:0::/64`,
{`ExporterAddress << 192.168.0.0/24`, Output: `ExporterAddress BETWEEN toIPv6('2001:db8::') AND toIPv6('2001:db8::ffff:ffff:ffff:ffff')`,
`ExporterAddress BETWEEN toIPv6('::ffff:192.168.0.0') AND toIPv6('::ffff:192.168.0.255')`}, }, {
{`DstAddr << 192.168.0.0/24`, Input: `ExporterAddress << 2001:db8::c000/115`,
`DstAddr BETWEEN toIPv6('::ffff:192.168.0.0') AND toIPv6('::ffff:192.168.0.255')`}, Output: `ExporterAddress BETWEEN toIPv6('2001:db8::c000') AND toIPv6('2001:db8::dfff')`,
{`SrcAddr << 192.168.0.1/24`, }, {
`SrcAddr BETWEEN toIPv6('::ffff:192.168.0.0') AND toIPv6('::ffff:192.168.0.255')`}, Input: `ExporterAddress << 192.168.0.0/24`,
{`DstAddr !<< 192.168.0.0/24`, Output: `ExporterAddress BETWEEN toIPv6('::ffff:192.168.0.0') AND toIPv6('::ffff:192.168.0.255')`,
`DstAddr NOT BETWEEN toIPv6('::ffff:192.168.0.0') AND toIPv6('::ffff:192.168.0.255')`}, }, {
{`DstAddr !<< 192.168.0.128/27`, Input: `DstAddr << 192.168.0.0/24`,
`DstAddr NOT BETWEEN toIPv6('::ffff:192.168.0.128') AND toIPv6('::ffff:192.168.0.159')`}, Output: `DstAddr BETWEEN toIPv6('::ffff:192.168.0.0') AND toIPv6('::ffff:192.168.0.255')`,
{`ExporterGroup= "group"`, `ExporterGroup = 'group'`}, MetaOut: Meta{MainTableRequired: true},
{`SrcAddr=203.0.113.1`, `SrcAddr = toIPv6('203.0.113.1')`}, }, {
{`DstAddr=203.0.113.2`, `DstAddr = toIPv6('203.0.113.2')`}, Input: `DstAddr << 192.168.0.0/24`,
{`SrcNetName="alpha"`, `SrcNetName = 'alpha'`}, Output: `SrcAddr BETWEEN toIPv6('::ffff:192.168.0.0') AND toIPv6('::ffff:192.168.0.255')`,
{`DstNetName="alpha"`, `DstNetName = 'alpha'`}, MetaIn: Meta{ReverseDirection: true},
{`DstNetRole="stuff"`, `DstNetRole = 'stuff'`}, MetaOut: Meta{ReverseDirection: true, MainTableRequired: true},
{`SrcNetTenant="mobile"`, `SrcNetTenant = 'mobile'`}, }, {
{`SrcAS=12322`, `SrcAS = 12322`}, Input: `SrcAddr << 192.168.0.1/24`,
{`SrcAS=AS12322`, `SrcAS = 12322`}, Output: `SrcAddr BETWEEN toIPv6('::ffff:192.168.0.0') AND toIPv6('::ffff:192.168.0.255')`,
{`SrcAS=as12322`, `SrcAS = 12322`}, MetaOut: Meta{MainTableRequired: true},
{`SrcAS IN(12322, 29447)`, `SrcAS IN (12322, 29447)`}, }, {
{`SrcAS IN( 12322 , 29447 )`, `SrcAS IN (12322, 29447)`}, Input: `DstAddr !<< 192.168.0.0/24`,
{`SrcAS NOTIN(12322, 29447)`, `SrcAS NOT IN (12322, 29447)`}, Output: `DstAddr NOT BETWEEN toIPv6('::ffff:192.168.0.0') AND toIPv6('::ffff:192.168.0.255')`,
{`SrcAS NOTIN (AS12322, 29447)`, `SrcAS NOT IN (12322, 29447)`}, MetaOut: Meta{MainTableRequired: true},
{`DstAS=12322`, `DstAS = 12322`}, }, {
{`SrcCountry='FR'`, `SrcCountry = 'FR'`}, Input: `DstAddr !<< 192.168.0.128/27`,
{`DstCountry='FR'`, `DstCountry = 'FR'`}, Output: `DstAddr NOT BETWEEN toIPv6('::ffff:192.168.0.128') AND toIPv6('::ffff:192.168.0.159')`,
{`InIfName='Gi0/0/0/1'`, `InIfName = 'Gi0/0/0/1'`}, MetaOut: Meta{MainTableRequired: true},
{`OutIfName = 'Gi0/0/0/1'`, `OutIfName = 'Gi0/0/0/1'`}, },
{`InIfDescription='Some description'`, `InIfDescription = 'Some description'`}, {Input: `ExporterGroup= "group"`, Output: `ExporterGroup = 'group'`},
{`OutIfDescription='Some other description'`, `OutIfDescription = 'Some other description'`}, {Input: `SrcAddr=203.0.113.1`, Output: `SrcAddr = toIPv6('203.0.113.1')`,
{`InIfSpeed>=1000`, `InIfSpeed >= 1000`}, MetaOut: Meta{MainTableRequired: true}},
{`InIfSpeed!=1000`, `InIfSpeed != 1000`}, {Input: `DstAddr=203.0.113.2`, Output: `DstAddr = toIPv6('203.0.113.2')`,
{`InIfSpeed<1000`, `InIfSpeed < 1000`}, MetaOut: Meta{MainTableRequired: true}},
{`OutIfSpeed!=1000`, `OutIfSpeed != 1000`}, {Input: `SrcNetName="alpha"`, Output: `SrcNetName = 'alpha'`},
{`InIfConnectivity = 'pni'`, `InIfConnectivity = 'pni'`}, {Input: `DstNetName="alpha"`, Output: `DstNetName = 'alpha'`},
{`OutIfConnectivity = 'ix'`, `OutIfConnectivity = 'ix'`}, {Input: `DstNetRole="stuff"`, Output: `DstNetRole = 'stuff'`},
{`InIfProvider = 'cogent'`, `InIfProvider = 'cogent'`}, {Input: `SrcNetTenant="mobile"`, Output: `SrcNetTenant = 'mobile'`},
{`OutIfProvider = 'telia'`, `OutIfProvider = 'telia'`}, {Input: `SrcAS=12322`, Output: `SrcAS = 12322`},
{`InIfBoundary = external`, `InIfBoundary = 'external'`}, {Input: `SrcAS=AS12322`, Output: `SrcAS = 12322`},
{`InIfBoundary = EXTERNAL`, `InIfBoundary = 'external'`}, {Input: `SrcAS=AS12322`, Output: `DstAS = 12322`,
{`OutIfBoundary != internal`, `OutIfBoundary != 'internal'`}, MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{`EType = ipv4`, `EType = 2048`}, {Input: `SrcAS=as12322`, Output: `SrcAS = 12322`},
{`EType != ipv6`, `EType != 34525`}, {Input: `SrcAS IN(12322, 29447)`, Output: `SrcAS IN (12322, 29447)`},
{`Proto = 1`, `Proto = 1`}, {Input: `SrcAS IN( 12322 , 29447 )`, Output: `SrcAS IN (12322, 29447)`},
{`Proto = 'gre'`, `dictGetOrDefault('protocols', 'name', Proto, '???') = 'gre'`}, {Input: `SrcAS NOTIN(12322, 29447)`, Output: `SrcAS NOT IN (12322, 29447)`},
{`SrcPort = 80`, `SrcPort = 80`}, {Input: `SrcAS NOTIN (AS12322, 29447)`, Output: `SrcAS NOT IN (12322, 29447)`},
{`DstPort > 1024`, `DstPort > 1024`}, {Input: `DstAS=12322`, Output: `DstAS = 12322`},
{`ForwardingStatus >= 128`, `ForwardingStatus >= 128`}, {Input: `SrcCountry='FR'`, Output: `SrcCountry = 'FR'`},
{`PacketSize > 1500`, `Bytes/Packets > 1500`}, {Input: `SrcCountry='FR'`, Output: `DstCountry = 'FR'`,
{`DstPort > 1024 AND SrcPort < 1024`, `DstPort > 1024 AND SrcPort < 1024`}, MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{`DstPort > 1024 OR SrcPort < 1024`, `DstPort > 1024 OR SrcPort < 1024`}, {Input: `DstCountry='FR'`, Output: `DstCountry = 'FR'`},
{`NOT DstPort > 1024 AND SrcPort < 1024`, `NOT DstPort > 1024 AND SrcPort < 1024`}, {Input: `DstCountry='FR'`, Output: `SrcCountry = 'FR'`,
{`not DstPort > 1024 and SrcPort < 1024`, `NOT DstPort > 1024 AND SrcPort < 1024`}, MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{`DstPort > 1024 AND SrcPort < 1024 OR InIfSpeed >= 1000`, {Input: `InIfName='Gi0/0/0/1'`, Output: `InIfName = 'Gi0/0/0/1'`},
`DstPort > 1024 AND SrcPort < 1024 OR InIfSpeed >= 1000`}, {Input: `InIfName='Gi0/0/0/1'`, Output: `OutIfName = 'Gi0/0/0/1'`,
{`DstPort > 1024 AND (SrcPort < 1024 OR InIfSpeed >= 1000)`, MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
`DstPort > 1024 AND (SrcPort < 1024 OR InIfSpeed >= 1000)`}, {Input: `OutIfName = 'Gi0/0/0/1'`, Output: `OutIfName = 'Gi0/0/0/1'`},
{` DstPort > 1024 AND ( SrcPort < 1024 OR InIfSpeed >= 1000 ) `, {Input: `OutIfName = 'Gi0/0/0/1'`, Output: `InIfName = 'Gi0/0/0/1'`,
`DstPort > 1024 AND (SrcPort < 1024 OR InIfSpeed >= 1000)`}, MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{`DstPort > 1024 AND(SrcPort < 1024 OR InIfSpeed >= 1000)`, {Input: `InIfDescription='Some description'`, Output: `InIfDescription = 'Some description'`},
`DstPort > 1024 AND (SrcPort < 1024 OR InIfSpeed >= 1000)`}, {Input: `InIfDescription='Some description'`, Output: `OutIfDescription = 'Some description'`,
{`DstPort > 1024 MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{Input: `OutIfDescription='Some other description'`, Output: `OutIfDescription = 'Some other description'`},
{Input: `OutIfDescription='Some other description'`, Output: `InIfDescription = 'Some other description'`,
MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{Input: `InIfSpeed>=1000`, Output: `InIfSpeed >= 1000`},
{Input: `InIfSpeed>=1000`, Output: `OutIfSpeed >= 1000`,
MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{Input: `InIfSpeed!=1000`, Output: `InIfSpeed != 1000`},
{Input: `InIfSpeed<1000`, Output: `InIfSpeed < 1000`},
{Input: `OutIfSpeed!=1000`, Output: `OutIfSpeed != 1000`},
{Input: `OutIfSpeed!=1000`, Output: `InIfSpeed != 1000`,
MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{Input: `InIfConnectivity = 'pni'`, Output: `InIfConnectivity = 'pni'`},
{Input: `InIfConnectivity = 'pni'`, Output: `OutIfConnectivity = 'pni'`,
MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{Input: `OutIfConnectivity = 'ix'`, Output: `OutIfConnectivity = 'ix'`},
{Input: `OutIfConnectivity = 'ix'`, Output: `InIfConnectivity = 'ix'`,
MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{Input: `InIfProvider = 'cogent'`, Output: `InIfProvider = 'cogent'`},
{Input: `InIfProvider = 'cogent'`, Output: `OutIfProvider = 'cogent'`,
MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{Input: `OutIfProvider = 'telia'`, Output: `OutIfProvider = 'telia'`},
{Input: `OutIfProvider = 'telia'`, Output: `InIfProvider = 'telia'`,
MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{Input: `InIfBoundary = external`, Output: `InIfBoundary = 'external'`},
{Input: `InIfBoundary = external`, Output: `OutIfBoundary = 'external'`,
MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{Input: `InIfBoundary = EXTERNAL`, Output: `InIfBoundary = 'external'`},
{Input: `InIfBoundary = EXTERNAL`, Output: `OutIfBoundary = 'external'`,
MetaIn: Meta{ReverseDirection: true}, MetaOut: Meta{ReverseDirection: true}},
{Input: `OutIfBoundary != internal`, Output: `OutIfBoundary != 'internal'`},
{Input: `EType = ipv4`, Output: `EType = 2048`},
{Input: `EType != ipv6`, Output: `EType != 34525`},
{Input: `Proto = 1`, Output: `Proto = 1`},
{Input: `Proto = 'gre'`, Output: `dictGetOrDefault('protocols', 'name', Proto, '???') = 'gre'`},
{Input: `SrcPort = 80`, Output: `SrcPort = 80`,
MetaOut: Meta{MainTableRequired: true}},
{Input: `SrcPort = 80`, Output: `DstPort = 80`,
MetaIn: Meta{ReverseDirection: true},
MetaOut: Meta{ReverseDirection: true, MainTableRequired: true}},
{Input: `DstPort > 1024`, Output: `DstPort > 1024`,
MetaOut: Meta{MainTableRequired: true}},
{Input: `ForwardingStatus >= 128`, Output: `ForwardingStatus >= 128`},
{Input: `PacketSize > 1500`, Output: `Bytes/Packets > 1500`},
{Input: `DstPort > 1024 AND SrcPort < 1024`, Output: `DstPort > 1024 AND SrcPort < 1024`,
MetaOut: Meta{MainTableRequired: true}},
{Input: `DstPort > 1024 OR SrcPort < 1024`, Output: `DstPort > 1024 OR SrcPort < 1024`,
MetaOut: Meta{MainTableRequired: true}},
{Input: `NOT DstPort > 1024 AND SrcPort < 1024`, Output: `NOT DstPort > 1024 AND SrcPort < 1024`,
MetaOut: Meta{MainTableRequired: true}},
{Input: `not DstPort > 1024 and SrcPort < 1024`, Output: `NOT DstPort > 1024 AND SrcPort < 1024`,
MetaOut: Meta{MainTableRequired: true}},
{
Input: `DstPort > 1024 AND SrcPort < 1024 OR InIfSpeed >= 1000`,
Output: `DstPort > 1024 AND SrcPort < 1024 OR InIfSpeed >= 1000`,
MetaOut: Meta{MainTableRequired: true},
}, {
Input: `DstPort > 1024 AND (SrcPort < 1024 OR InIfSpeed >= 1000)`,
Output: `DstPort > 1024 AND (SrcPort < 1024 OR InIfSpeed >= 1000)`,
MetaOut: Meta{MainTableRequired: true},
}, {
Input: ` DstPort > 1024 AND ( SrcPort < 1024 OR InIfSpeed >= 1000 ) `,
Output: `DstPort > 1024 AND (SrcPort < 1024 OR InIfSpeed >= 1000)`,
MetaOut: Meta{MainTableRequired: true},
}, {
Input: `DstPort > 1024 AND(SrcPort < 1024 OR InIfSpeed >= 1000)`,
Output: `DstPort > 1024 AND (SrcPort < 1024 OR InIfSpeed >= 1000)`,
MetaOut: Meta{MainTableRequired: true},
}, {
Input: `DstPort > 1024
AND (SrcPort < 1024 OR InIfSpeed >= 1000)`, AND (SrcPort < 1024 OR InIfSpeed >= 1000)`,
`DstPort > 1024 AND (SrcPort < 1024 OR InIfSpeed >= 1000)`}, Output: `DstPort > 1024 AND (SrcPort < 1024 OR InIfSpeed >= 1000)`,
{`(ExporterAddress=203.0.113.1)`, `(ExporterAddress = toIPv6('203.0.113.1'))`}, MetaOut: Meta{MainTableRequired: true},
{`ForwardingStatus >= 128 -- Nothing`, `ForwardingStatus >= 128`}, },
{` {Input: `(ExporterAddress=203.0.113.1)`, Output: `(ExporterAddress = toIPv6('203.0.113.1'))`},
{Input: `ForwardingStatus >= 128 -- Nothing`, Output: `ForwardingStatus >= 128`},
{
Input: `
-- Example of commented request -- Example of commented request
-- Here we go -- Here we go
DstPort > 1024 -- Non-privileged port DstPort > 1024 -- Non-privileged port
AND SrcAS = AS12322 -- Proxad ASN`, `DstPort > 1024 AND SrcAS = 12322`}, AND SrcAS = AS12322 -- Proxad ASN`,
{`InIfDescription = "This contains a -- comment" -- nope`, Output: `DstPort > 1024 AND SrcAS = 12322`,
`InIfDescription = 'This contains a -- comment'`}, MetaOut: Meta{MainTableRequired: true},
{`InIfDescription = "This contains a /* comment"`, }, {
`InIfDescription = 'This contains a /* comment'`}, Input: `InIfDescription = "This contains a -- comment" -- nope`,
{`OutIfProvider /* That's the output provider */ = 'telia'`, `OutIfProvider = 'telia'`}, Output: `InIfDescription = 'This contains a -- comment'`,
{`OutIfProvider /* That's the }, {
output provider */ = 'telia'`, `OutIfProvider = 'telia'`}, Input: `InIfDescription = "This contains a /* comment"`,
Output: `InIfDescription = 'This contains a /* comment'`,
},
{Input: `OutIfProvider /* That's the output provider */ = 'telia'`, Output: `OutIfProvider = 'telia'`},
{
Input: `OutIfProvider /* That's the
output provider */ = 'telia'`,
Output: `OutIfProvider = 'telia'`,
},
} }
for _, tc := range cases { for _, tc := range cases {
got, err := Parse("", []byte(tc.Input)) got, err := Parse("", []byte(tc.Input), GlobalStore("meta", &tc.MetaIn))
if err != nil { if err != nil {
t.Errorf("Parse(%q) error:\n%+v", tc.Input, err) t.Errorf("Parse(%q) error:\n%+v", tc.Input, err)
continue continue
@@ -124,6 +206,9 @@ output provider */ = 'telia'`, `OutIfProvider = 'telia'`},
if diff := helpers.Diff(got.(string), tc.Output); diff != "" { if diff := helpers.Diff(got.(string), tc.Output); diff != "" {
t.Errorf("Parse(%q) (-got, +want):\n%s", tc.Input, diff) t.Errorf("Parse(%q) (-got, +want):\n%s", tc.Input, diff)
} }
if diff := helpers.Diff(tc.MetaIn, tc.MetaOut); diff != "" {
t.Errorf("Parse(%q) meta (-got, +want):\n%s", tc.Input, diff)
}
} }
} }
@@ -157,7 +242,7 @@ func TestInvalidFilter(t *testing.T) {
{`SrcAS IN (AS12322,`}, {`SrcAS IN (AS12322,`},
} }
for _, tc := range cases { for _, tc := range cases {
out, err := Parse("", []byte(tc.Input)) out, err := Parse("", []byte(tc.Input), GlobalStore("meta", &Meta{}))
t.Logf("out: %v", out) t.Logf("out: %v", out)
if err == nil { if err == nil {
t.Errorf("Parse(%q) didn't throw an error", tc.Input) t.Errorf("Parse(%q) didn't throw an error", tc.Input)

View File

@@ -126,7 +126,7 @@ func (c *Component) graphHandlerFunc(gc *gin.Context) {
if resolution < time.Second { if resolution < time.Second {
resolution = time.Second resolution = time.Second
} }
sqlQuery = c.queryFlowsTable(sqlQuery, sqlQuery = c.queryFlowsTable(sqlQuery, input.Filter.mainTableRequired,
input.Start, input.End, resolution) input.Start, input.End, resolution)
gc.Header("X-SQL-Query", strings.ReplaceAll(sqlQuery, "\n", " ")) gc.Header("X-SQL-Query", strings.ReplaceAll(sqlQuery, "\n", " "))

View File

@@ -93,7 +93,7 @@ ORDER BY time WITH FILL
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Points: 100, Points: 100,
Dimensions: []queryColumn{}, Dimensions: []queryColumn{},
Filter: queryFilter{"DstCountry = 'FR' AND SrcCountry = 'US'"}, Filter: queryFilter{filter: "DstCountry = 'FR' AND SrcCountry = 'US'"},
Units: "l3bps", Units: "l3bps",
}, },
Expected: ` Expected: `

View File

@@ -124,6 +124,8 @@ func (gc *queryColumn) UnmarshalText(input []byte) error {
type queryFilter struct { type queryFilter struct {
filter string filter string
reverseFilter string
mainTableRequired bool
} }
func (gf queryFilter) MarshalText() ([]byte, error) { func (gf queryFilter) MarshalText() ([]byte, error) {
@@ -131,14 +133,21 @@ func (gf queryFilter) MarshalText() ([]byte, error) {
} }
func (gf *queryFilter) UnmarshalText(input []byte) error { func (gf *queryFilter) UnmarshalText(input []byte) error {
if strings.TrimSpace(string(input)) == "" { if strings.TrimSpace(string(input)) == "" {
*gf = queryFilter{""} *gf = queryFilter{}
return nil return nil
} }
got, err := filter.Parse("", input) meta := &filter.Meta{}
direct, err := filter.Parse("", input, filter.GlobalStore("meta", meta))
if err != nil { if err != nil {
return fmt.Errorf("cannot parse filter: %s", filter.HumanError(err)) return fmt.Errorf("cannot parse filter: %s", filter.HumanError(err))
} }
*gf = queryFilter{got.(string)} meta = &filter.Meta{ReverseDirection: true}
reverse, err := filter.Parse("", input, filter.GlobalStore("meta", meta))
*gf = queryFilter{
filter: direct.(string),
reverseFilter: reverse.(string),
mainTableRequired: meta.MainTableRequired,
}
return nil return nil
} }

View File

@@ -115,7 +115,7 @@ func (c *Component) sankeyHandlerFunc(gc *gin.Context) {
} }
// Prepare and execute query // Prepare and execute query
sqlQuery = c.queryFlowsTable(sqlQuery, sqlQuery = c.queryFlowsTable(sqlQuery, input.Filter.mainTableRequired,
input.Start, input.End, resolution) input.Start, input.End, resolution)
gc.Header("X-SQL-Query", strings.ReplaceAll(sqlQuery, "\n", " ")) gc.Header("X-SQL-Query", strings.ReplaceAll(sqlQuery, "\n", " "))
results := []struct { results := []struct {

View File

@@ -93,7 +93,7 @@ ORDER BY xps DESC`,
End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC), End: time.Date(2022, 04, 11, 15, 45, 10, 0, time.UTC),
Dimensions: []queryColumn{queryColumnSrcAS, queryColumnExporterName}, Dimensions: []queryColumn{queryColumnSrcAS, queryColumnExporterName},
Limit: 10, Limit: 10,
Filter: queryFilter{"DstCountry = 'FR'"}, Filter: queryFilter{filter: "DstCountry = 'FR'"},
Units: "l3bps", Units: "l3bps",
}, },
Expected: ` Expected: `

View File

@@ -108,6 +108,7 @@ func (c *Component) widgetTopHandlerFunc(gc *gin.Context) {
selector string selector string
groupby string groupby string
filter string filter string
mainTableRequired bool
) )
switch gc.Param("name") { switch gc.Param("name") {
@@ -139,9 +140,11 @@ func (c *Component) widgetTopHandlerFunc(gc *gin.Context) {
case "src-port": case "src-port":
selector = `concat(dictGetOrDefault('protocols', 'name', Proto, '???'), '/', toString(SrcPort))` selector = `concat(dictGetOrDefault('protocols', 'name', Proto, '???'), '/', toString(SrcPort))`
groupby = `Proto, SrcPort` groupby = `Proto, SrcPort`
mainTableRequired = true
case "dst-port": case "dst-port":
selector = `concat(dictGetOrDefault('protocols', 'name', Proto, '???'), '/', toString(DstPort))` selector = `concat(dictGetOrDefault('protocols', 'name', Proto, '???'), '/', toString(DstPort))`
groupby = `Proto, DstPort` groupby = `Proto, DstPort`
mainTableRequired = true
} }
if groupby == "" { if groupby == "" {
groupby = selector groupby = selector
@@ -160,7 +163,7 @@ WHERE {timefilter}
GROUP BY %s GROUP BY %s
ORDER BY Percent DESC ORDER BY Percent DESC
LIMIT 5 LIMIT 5
`, filter, selector, selector, filter, groupby), now.Add(-5*time.Minute), now, time.Minute) `, filter, selector, selector, filter, groupby), mainTableRequired, now.Add(-5*time.Minute), now, time.Minute)
gc.Header("X-SQL-Query", query) gc.Header("X-SQL-Query", query)
results := []topResult{} results := []topResult{}
@@ -202,7 +205,7 @@ GROUP BY Time
ORDER BY Time WITH FILL ORDER BY Time WITH FILL
FROM toStartOfInterval({timefilter.Start}, INTERVAL %s second) FROM toStartOfInterval({timefilter.Start}, INTERVAL %s second)
TO {timefilter.Stop} TO {timefilter.Stop}
STEP %s`, slot, slot, slot, slot), now.Add(-24*time.Hour), now, time.Duration(interval)*time.Second) STEP %s`, slot, slot, slot, slot), false, now.Add(-24*time.Hour), now, time.Duration(interval)*time.Second)
gc.Header("X-SQL-Query", query) gc.Header("X-SQL-Query", query)
results := []struct { results := []struct {