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

Move connectionset from analyzer and protocols from synthesizer #10

Merged
merged 20 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
400 changes: 400 additions & 0 deletions pkg/connection/connectionset.go

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions pkg/connection/connectionset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2020- IBM Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package connection_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/np-guard/models/pkg/connection"
"github.com/np-guard/models/pkg/netp"
)

const ICMPValue = netp.DestinationUnreachable

func TestAllConnections(t *testing.T) {
c := connection.All()
require.Equal(t, "All Connections", c.String())
}

func TestNoConnections(t *testing.T) {
c := connection.None()
require.Equal(t, "No Connections", c.String())
}

func TestBasicSetICMP(t *testing.T) {
c := connection.ICMPConnection(ICMPValue, ICMPValue, 5, 5)
require.Equal(t, "protocol: ICMP icmp-type: 3 icmp-code: 5", c.String())
}

func TestBasicSetTCP(t *testing.T) {
e := connection.TCPorUDPConnection(netp.ProtocolStringTCP, 1, 65535, 1, 65535)
require.Equal(t, "protocol: TCP", e.String())

c := connection.All().Subtract(e)
require.Equal(t, "protocol: UDP,ICMP", c.String())

c = c.Union(e)
require.Equal(t, "All Connections", c.String())
}

func TestBasicSet2(t *testing.T) {
except1 := connection.ICMPConnection(ICMPValue, ICMPValue, 5, 5)

except2 := connection.TCPorUDPConnection(netp.ProtocolStringTCP, 1, 65535, 1, 65535)

d := connection.All().Subtract(except1).Subtract(except2)
require.Equal(t, ""+
"protocol: ICMP icmp-type: 0-2,4-16; "+
"protocol: ICMP icmp-type: 3 icmp-code: 0-4; "+
"protocol: UDP", d.String())
}

func TestBasicSet3(t *testing.T) {
c := connection.ICMPConnection(ICMPValue, ICMPValue, 5, 5)
d := connection.All().Subtract(c).Union(connection.ICMPConnection(ICMPValue, ICMPValue, 5, 5))
require.Equal(t, "All Connections", d.String())
}
87 changes: 87 additions & 0 deletions pkg/connection/statefulness.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2020- IBM Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package connection

import (
"slices"

"github.com/np-guard/models/pkg/hypercube"
"github.com/np-guard/models/pkg/netp"
)

// default is StatefulUnknown
type StatefulState int

const (
// StatefulUnknown is the default value for a Set object,
StatefulUnknown StatefulState = 0
// StatefulTrue represents a connection object for which any allowed TCP (on all allowed src/dst ports)
// has an allowed response connection
StatefulTrue StatefulState = 1
// StatefulFalse represents a connection object for which there exists some allowed TCP
// (on any allowed subset from the allowed src/dst ports) that does not have an allowed response connection
StatefulFalse StatefulState = 2
)

// EnhancedString returns a connection string with possibly added asterisk for stateless connection
func (c *Set) EnhancedString() string {
if c.IsStateful == StatefulFalse {
return c.String() + " *"
}
return c.String()
}

func newTCPSet() *Set {
return TCPorUDPConnection(netp.ProtocolStringTCP, MinPort, MaxPort, MinPort, MaxPort)
}

// WithStatefulness updates `c` object with `IsStateful` property, based on input `secondDirectionConn`.
// `c` represents a src-to-dst connection, and `secondDirectionConn` represents dst-to-src connection.
// The property `IsStateful` of `c` is set as `StatefulFalse` if there is at least some subset within TCP from `c`
// which is not stateful (such that the response direction for this subset is not enabled).
// This function also returns a connection object with the exact subset of the stateful part (within TCP)
// from the entire connection `c`, and with the original connections on other protocols.
func (c *Set) WithStatefulness(secondDirectionConn *Set) *Set {
connTCP := c.Intersect(newTCPSet())
if connTCP.IsEmpty() {
c.IsStateful = StatefulTrue
return c
}
statefulCombinedConnTCP := connTCP.connTCPWithStatefulness(secondDirectionConn.Intersect(newTCPSet()))
c.IsStateful = connTCP.IsStateful
return c.Subtract(connTCP).Union(statefulCombinedConnTCP)
}

// connTCPWithStatefulness assumes that both `c` and `secondDirectionConn` are within TCP.
// it assigns IsStateful a value within `c`, and returns the subset from `c` which is stateful.
func (c *Set) connTCPWithStatefulness(secondDirectionConn *Set) *Set {
// flip src/dst ports before intersection
statefulCombinedConn := c.Intersect(secondDirectionConn.switchSrcDstPortsOnTCP())
if c.Equal(statefulCombinedConn) {
c.IsStateful = StatefulTrue
} else {
c.IsStateful = StatefulFalse
}
return statefulCombinedConn
}

// switchSrcDstPortsOnTCP returns a new Set object, built from the input Set object.
// It assumes the input connection object is only within TCP protocol.
// For TCP the src and dst ports on relevant cubes are being switched.
func (c *Set) switchSrcDstPortsOnTCP() *Set {
if c.IsAll() || c.IsEmpty() {
return c.Copy()
}
res := None()
for _, cube := range c.connectionProperties.GetCubesList() {
// assuming cube[protocol] contains TCP only
// no need to switch if src equals dst
if !cube[srcPort].Equal(cube[dstPort]) {
// Shallow clone should be enough, since we do shallow swap:
cube = slices.Clone(cube)
cube[srcPort], cube[dstPort] = cube[dstPort], cube[srcPort]
}
res.connectionProperties = res.connectionProperties.Union(hypercube.FromCube(cube))
}
return res
}
148 changes: 148 additions & 0 deletions pkg/connection/statefulness_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright 2020- IBM Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package connection_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/np-guard/models/pkg/connection"
"github.com/np-guard/models/pkg/netp"
)

func newTCPConn(t *testing.T, srcMinP, srcMaxP, dstMinP, dstMaxP int64) *connection.Set {
t.Helper()
return connection.TCPorUDPConnection(netp.ProtocolStringTCP, srcMinP, srcMaxP, dstMinP, dstMaxP)
}

func newUDPConn(t *testing.T, srcMinP, srcMaxP, dstMinP, dstMaxP int64) *connection.Set {
t.Helper()
return connection.TCPorUDPConnection(netp.ProtocolStringUDP, srcMinP, srcMaxP, dstMinP, dstMaxP)
}

func newICMPconn(t *testing.T) *connection.Set {
t.Helper()
return connection.ICMPConnection(
connection.MinICMPType, connection.MaxICMPType,
connection.MinICMPCode, connection.MaxICMPCode)
}

func newTCPUDPSet(t *testing.T, p netp.ProtocolString) *connection.Set {
t.Helper()
return connection.TCPorUDPConnection(p,
connection.MinPort, connection.MaxPort,
connection.MinPort, connection.MaxPort)
}

type statefulnessTest struct {
name string
srcToDst *connection.Set
dstToSrc *connection.Set
// expectedIsStateful represents the expected IsStateful computed value for srcToDst,
// which should be either StatefulTrue or StatefulFalse, given the input dstToSrc connection.
// the computation applies only to the TCP protocol within those connections.
expectedIsStateful connection.StatefulState
// expectedStatefulConn represents the subset from srcToDst which is not related to the "non-stateful" mark (*) on the srcToDst connection,
// the stateless part for TCP is srcToDst.Subtract(statefulConn)
expectedStatefulConn *connection.Set
}

func (tt statefulnessTest) runTest(t *testing.T) {
t.Helper()
statefulConn := tt.srcToDst.WithStatefulness(tt.dstToSrc)
require.Equal(t, tt.expectedIsStateful, tt.srcToDst.IsStateful)
require.True(t, tt.expectedStatefulConn.Equal(statefulConn))
}

func TestAll(t *testing.T) {
var testCasesStatefulness = []statefulnessTest{
{
name: "tcp_all_ports_on_both_directions",
srcToDst: newTCPUDPSet(t, netp.ProtocolStringTCP), // TCP all ports
dstToSrc: newTCPUDPSet(t, netp.ProtocolStringTCP), // TCP all ports
expectedIsStateful: connection.StatefulTrue,
expectedStatefulConn: newTCPUDPSet(t, netp.ProtocolStringTCP), // TCP all ports
},
{
name: "first_all_cons_second_tcp_with_ports",
srcToDst: connection.All(), // all connections
dstToSrc: newTCPConn(t, 80, 80, connection.MinPort, connection.MaxPort), // TCP , src-ports: 80, dst-ports: all

// there is a subset of the tcp connection which is not stateful
expectedIsStateful: connection.StatefulFalse,

// TCP src-ports: all, dst-port: 80 , union: all non-TCP conns
expectedStatefulConn: connection.All().Subtract(newTCPUDPSet(t, netp.ProtocolStringTCP)).Union(
newTCPConn(t, connection.MinPort, connection.MaxPort, 80, 80)),
},
{
name: "first_all_conns_second_no_tcp",
srcToDst: connection.All(), // all connections
dstToSrc: newICMPconn(t), // ICMP
expectedIsStateful: connection.StatefulFalse,
// UDP, ICMP (all TCP is considered stateless here)
expectedStatefulConn: connection.All().Subtract(newTCPUDPSet(t, netp.ProtocolStringTCP)),
},
{
name: "tcp_with_ports_both_directions_exact_match",
srcToDst: newTCPConn(t, 80, 80, 443, 443),
dstToSrc: newTCPConn(t, 443, 443, 80, 80),
expectedIsStateful: connection.StatefulTrue,
expectedStatefulConn: newTCPConn(t, 80, 80, 443, 443),
},
{
name: "tcp_with_ports_both_directions_partial_match",
srcToDst: newTCPConn(t, 80, 100, 443, 443),
dstToSrc: newTCPConn(t, 443, 443, 80, 80),
expectedIsStateful: connection.StatefulFalse,
expectedStatefulConn: newTCPConn(t, 80, 80, 443, 443),
},
{
name: "tcp_with_ports_both_directions_no_match",
srcToDst: newTCPConn(t, 80, 100, 443, 443),
dstToSrc: newTCPConn(t, 80, 80, 80, 80),
expectedIsStateful: connection.StatefulFalse,
expectedStatefulConn: connection.None(),
},
{
name: "udp_and_tcp_with_ports_both_directions_no_match",
srcToDst: newTCPConn(t, 80, 100, 443, 443).Union(newUDPConn(t, 80, 100, 443, 443)),
dstToSrc: newTCPConn(t, 80, 80, 80, 80).Union(newUDPConn(t, 80, 80, 80, 80)),
expectedIsStateful: connection.StatefulFalse,
expectedStatefulConn: newUDPConn(t, 80, 100, 443, 443),
},
{
name: "no_tcp_in_first_direction",
srcToDst: newUDPConn(t, 70, 100, 443, 443),
dstToSrc: newTCPConn(t, 70, 80, 80, 80).Union(newUDPConn(t, 70, 80, 80, 80)),
expectedIsStateful: connection.StatefulTrue,
expectedStatefulConn: newUDPConn(t, 70, 100, 443, 443),
},
{
name: "empty_conn_in_first_direction",
srcToDst: connection.None(),
dstToSrc: newTCPConn(t, 80, 80, 80, 80).Union(newTCPUDPSet(t, netp.ProtocolStringUDP)),
expectedIsStateful: connection.StatefulTrue,
expectedStatefulConn: connection.None(),
},
{
name: "only_udp_icmp_in_first_direction_and_empty_second_direction",
srcToDst: newTCPUDPSet(t, netp.ProtocolStringUDP).Union(newICMPconn(t)),
dstToSrc: connection.None(),
// stateful analysis does not apply to udp/icmp, thus considered in the result as "stateful"
// (to avoid marking it as stateless in the output)
expectedIsStateful: connection.StatefulTrue,
expectedStatefulConn: newTCPUDPSet(t, netp.ProtocolStringUDP).Union(newICMPconn(t)),
},
}
t.Parallel()
// explainTests is the list of tests to run
for testIdx := range testCasesStatefulness {
tt := testCasesStatefulness[testIdx]
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tt.runTest(t)
})
}
}
18 changes: 15 additions & 3 deletions pkg/hypercube/hypercubeset.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Copyright 2020- IBM Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package hypercube

import (
Expand Down Expand Up @@ -163,7 +165,7 @@ func (c *CanonicalSet) Subtract(other *CanonicalSet) *CanonicalSet {
}
}

// ContainedIn returns true ic other contained in c
// ContainedIn returns true if c is subset of other
func (c *CanonicalSet) ContainedIn(other *CanonicalSet) (bool, error) {
if c == other {
return true, nil
Expand All @@ -179,8 +181,7 @@ func (c *CanonicalSet) ContainedIn(other *CanonicalSet) (bool, error) {
}

isSubsetCount := 0
for k, v := range c.layers {
currentLayer := k.Copy()
for currentLayer, v := range c.layers {
for otherKey, otherVal := range other.layers {
commonKey := currentLayer.Intersect(otherKey)
remaining := currentLayer.Subtract(commonKey)
Expand Down Expand Up @@ -289,3 +290,14 @@ func FromCube(cube []*interval.CanonicalSet) *CanonicalSet {
res.layers[cube[0].Copy()] = FromCube(cube[1:])
return res
}

// Cube returns a new CanonicalSet created from a single input cube
// the input cube is given as an ordered list of integer values, where each two values
// represent the range (start,end) for a dimension value
func Cube(values ...int64) *CanonicalSet {
cube := []*interval.CanonicalSet{}
for i := 0; i < len(values); i += 2 {
cube = append(cube, interval.New(values[i], values[i+1]).ToSet())
}
return FromCube(cube)
}
9 changes: 3 additions & 6 deletions pkg/hypercube/hypercubeset_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Copyright 2020- IBM Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package hypercube_test

import (
Expand All @@ -6,18 +8,13 @@ import (
"github.com/stretchr/testify/require"

"github.com/np-guard/models/pkg/hypercube"
"github.com/np-guard/models/pkg/interval"
)

// cube returns a new hypercube.CanonicalSet created from a single input cube
// the input cube is given as an ordered list of integer values, where each two values
// represent the range (start,end) for a dimension value
func cube(values ...int64) *hypercube.CanonicalSet {
cube := []*interval.CanonicalSet{}
for i := 0; i < len(values); i += 2 {
cube = append(cube, interval.New(values[i], values[i+1]).ToSet())
}
return hypercube.FromCube(cube)
return hypercube.Cube(values...)
}
elazarg marked this conversation as resolved.
Show resolved Hide resolved

func union(set *hypercube.CanonicalSet, sets ...*hypercube.CanonicalSet) *hypercube.CanonicalSet {
Expand Down
6 changes: 6 additions & 0 deletions pkg/interval/interval.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Copyright 2020- IBM Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package interval

import "fmt"
Expand All @@ -22,6 +24,10 @@ func (i Interval) Equal(x Interval) bool {
return i.Start == x.Start && i.End == x.End
}

func (i Interval) Size() int64 {
return i.End - i.Start + 1
}

func (i Interval) overlaps(other Interval) bool {
return other.End >= i.Start && other.Start <= i.End
}
Expand Down
Loading