pkg/skoop/assertions/netstack.go (402 lines of code) (raw):
package assertions
import (
"fmt"
"net"
"reflect"
"strings"
"github.com/alibaba/kubeskoop/pkg/skoop/model"
"github.com/alibaba/kubeskoop/pkg/skoop/netstack"
"github.com/samber/lo"
"golang.org/x/exp/slices"
)
type NetstackAssertion struct {
Assertion
netns *netstack.NetNS
}
func NewNetstackAssertion(assertion Assertion, netns *netstack.NetNS) *NetstackAssertion {
return &NetstackAssertion{Assertion: assertion, netns: netns}
}
func (na *NetstackAssertion) AssertSysctls(expectSysctls map[string]string, suspicionLevel model.SuspicionLevel) {
for s, expect := range expectSysctls {
actual, ok := na.netns.NetNSInfo.SysctlInfo[s]
if !ok {
na.AddSuspicion(suspicionLevel, fmt.Sprintf("expect sysctl %s not exist in actual netns", s))
continue
}
AssertTrue(na, actual == expect, suspicionLevel,
fmt.Sprintf("expect sysctl %s in actual netns: %s not equal to expect: %s", s, actual, expect))
}
}
func (na *NetstackAssertion) AssertIPForwardedEnabled() {
na.AssertSysctls(map[string]string{"net.ipv4.ip_forward": "1"}, model.SuspicionLevelFatal)
}
func (na *NetstackAssertion) AssertRpFilterDisabled(dev string) {
if dev == "" {
dev = "all"
}
na.AssertSysctls(map[string]string{
fmt.Sprintf("net.ipv4.conf.%s.rp_filter", dev): "0",
}, model.SuspicionLevelFatal)
}
func (na *NetstackAssertion) AssertDefaultRule() {
for _, r := range na.netns.NetNSInfo.RuleInfo {
// main & local table
if r.Table == netstack.RtTableMain || r.Table == netstack.RtTableLocal {
if r.Src != nil || r.IifName != "" || r.OifName != "" || r.Dst != nil {
na.AddSuspicion(model.SuspicionLevelCritical,
"default policy to table main is wrong with non zero fields")
return
}
return
}
}
na.AddSuspicion(model.SuspicionLevelCritical,
"default policy to table main is deleted")
}
func (na *NetstackAssertion) AssertNetDevice(s string, expect netstack.Interface) {
for _, ni := range na.netns.Interfaces {
if ni.Name == s {
niType := reflect.TypeOf(expect)
expectv := reflect.ValueOf(expect)
actualv := reflect.ValueOf(ni)
for i := 0; i < niType.NumField(); i++ {
AssertTrue(na, expectv.Field(i).IsZero() || (expectv.Field(i).Interface() == actualv.Field(i).Interface()),
model.SuspicionLevelFatal,
fmt.Sprintf("netdevice %q field: %v is not expect: actual(%v) != expect(%v)", s, niType.Field(i).Name, actualv.Field(i), expectv.Field(i)),
)
}
return
}
}
na.AddSuspicion(model.SuspicionLevelCritical,
fmt.Sprintf("cannot find interface: %s to assert", s),
)
}
func (na *NetstackAssertion) AssertNoPolicyRoute() {
defaultRoutes := []int{netstack.RtTableMain, netstack.RtTableLocal, netstack.RtTableDefault}
var policyRoutes []int
for _, rule := range na.netns.NetNSInfo.RuleInfo {
if !slices.Contains(defaultRoutes, rule.Table) {
policyRoutes = append(policyRoutes, rule.Table)
}
}
AssertTrue(na, len(policyRoutes) == 0, model.SuspicionLevelWarning,
fmt.Sprintf("policy route enabled, tables: %+v", policyRoutes))
}
func (na *NetstackAssertion) AssertListen(localIP net.IP, localPort uint16, protocol model.Protocol) {
socks := lo.Filter(na.netns.NetNSInfo.ConnStats, func(stat netstack.ConnStat, _ int) bool {
if stat.State == netstack.SockStatListen && strings.EqualFold(string(stat.Protocol), string(protocol)) && localPort == stat.LocalPort {
if localIP.String() == stat.LocalIP {
return true
}
if slices.Contains([]string{"0.0.0.0", "::"}, stat.LocalIP) {
return true
}
}
return false
})
AssertTrue(na, len(socks) != 0, model.SuspicionLevelFatal,
fmt.Sprintf("no process listening on 0.0.0.0:%v or %v:%v protocol %s", localPort, localIP, localPort, protocol))
}
func (na *NetstackAssertion) AssertHostBridge(name string) {
bridge, ok := lo.Find(na.netns.Interfaces, func(iface netstack.Interface) bool { return iface.Name == name })
if !ok {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("bridge %s is not existed", name))
return
}
AssertTrue(na, bridge.State == netstack.LinkUP, model.SuspicionLevelFatal, fmt.Sprintf("bridge %s state is down", name))
}
func (na *NetstackAssertion) AssertVEthPeerBridge(peerInterfaceName string, peerNS *netstack.NetNSInfo, expectedBridgeName string) {
peer, ok := lo.Find(peerNS.Interfaces, func(iface netstack.Interface) bool { return iface.Name == peerInterfaceName })
if !ok {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("cannot find eth0 in peer netns %s", peerNS.NetnsID))
return
}
dev, ok := lo.Find(na.netns.Interfaces, func(iface netstack.Interface) bool { return iface.Index == peer.PeerIndex })
if !ok {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("veth index %d is not existed", peer.PeerIndex),
)
return
}
if dev.Driver != netstack.LinkDriverVeth {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("%s is not a veth interface", dev.Name))
return
}
AssertTrue(na, dev.State == netstack.LinkUP, model.SuspicionLevelWarning,
fmt.Sprintf("state of veth peer %s is DOWN", dev.Name))
if dev.MasterIndex == 0 {
na.AddSuspicion(model.SuspicionLevelFatal, fmt.Sprintf("%s has no master", dev.Name))
return
}
bridge, ok := lo.Find(na.netns.Interfaces, func(iface netstack.Interface) bool { return iface.Index == dev.MasterIndex })
if !ok {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("cannot find master interface %d in peer ns %s", dev.MasterIndex, na.netns.NetNSInfo.NetnsID))
return
}
if expectedBridgeName != "" {
AssertTrue(na, bridge.Name == expectedBridgeName, model.SuspicionLevelFatal,
fmt.Sprintf("bridge of %s is %s, not %s", dev.Name, bridge.Name, expectedBridgeName))
}
// todo: br_port_state
}
func (na *NetstackAssertion) AssertVEthOnBridge(index int, expectedBridgeName string) {
dev, ok := lo.Find(na.netns.Interfaces, func(iface netstack.Interface) bool { return iface.Index == index })
if !ok {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("veth peer index %d is not existed", index),
)
return
}
if dev.Driver != netstack.LinkDriverVeth {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("%s is not a veth interface", dev.Name))
return
}
AssertTrue(na, dev.State == netstack.LinkUP, model.SuspicionLevelWarning,
fmt.Sprintf("state of veth peer %s is DOWN", dev.Name))
if dev.MasterIndex == 0 {
na.AddSuspicion(model.SuspicionLevelFatal, fmt.Sprintf("%s has no master", dev.Name))
return
}
bridge, ok := lo.Find(na.netns.Interfaces, func(iface netstack.Interface) bool { return iface.Index == dev.MasterIndex })
if !ok {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("cannot find master interface %d, for dev %s", dev.MasterIndex, dev.Name))
return
}
if expectedBridgeName != "" {
AssertTrue(na, bridge.Name == expectedBridgeName, model.SuspicionLevelFatal,
fmt.Sprintf("bridge of %s is %s, not %s", dev.Name, bridge.Name, expectedBridgeName))
}
// todo: br_port_state
}
type RouteAssertion struct {
Dev *string
Scope *netstack.Scope
Type *int
Dst *net.IPNet
Src *net.IP
Gw *net.IP
Protocol *int
}
func (a RouteAssertion) String() string {
var formattedString []string
if a.Dev != nil {
formattedString = append(formattedString, fmt.Sprintf("dev: %s", *a.Dev))
}
if a.Scope != nil {
formattedString = append(formattedString, fmt.Sprintf("scope: %s", netstack.RouteScopeToString(*a.Scope)))
}
if a.Type != nil {
formattedString = append(formattedString, fmt.Sprintf("type: %s", netstack.RouteTypeToString(*a.Type)))
}
if a.Src != nil {
formattedString = append(formattedString, fmt.Sprintf("src: %s", *a.Src))
}
if a.Dst != nil {
formattedString = append(formattedString, fmt.Sprintf("dst: %s", *a.Dst))
}
if a.Gw != nil {
formattedString = append(formattedString, fmt.Sprintf("gateway: %s", *a.Gw))
}
if a.Protocol != nil {
formattedString = append(formattedString, fmt.Sprintf("protocol: %s", netstack.RouteProtocolToString(*a.Protocol)))
}
return strings.Join(formattedString, " ")
}
func (na *NetstackAssertion) AssertRoute(expected RouteAssertion, packet model.Packet, iif, oif string) error {
router := na.netns.Router
route, err := router.Route(&packet, iif, oif)
if err == netstack.ErrNoRouteToHost {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("no route to host %s", packet.Dst))
return nil
}
if err != nil {
return err
}
if slices.Contains([]int{netstack.RtnUnreachable, netstack.RtnBlackhole, netstack.RtnProhibit, netstack.RtnThrow}, route.Type) {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("route with type %d which indicates %s is unreachable", route.Type, packet.Dst))
return nil
}
if route.Type == netstack.RtnLocal {
return nil
}
if expected.Dev != nil && *expected.Dev != route.OifName {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("invalid route %q for packet {src=%s, dst=%s}, expected: %q",
route, packet.Src, packet.Dst, expected))
return nil
}
if expected.Scope != nil && *expected.Scope != route.Scope {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("invalid route %q for packet {src=%s, dst=%s}, expected: %q",
route, packet.Src, packet.Dst, expected))
return nil
}
if expected.Src != nil && !route.Src.Equal(*expected.Src) {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("invalid route %q for packet {src=%s, dst=%s}, expected: %q",
route, packet.Src, packet.Dst, expected))
return nil
}
if expected.Dst != nil && (*route.Dst).String() != (*expected.Dst).String() {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("invalid route %q for packet {src=%s, dst=%s}, expected: %q",
route, packet.Src, packet.Dst, expected))
return nil
}
if expected.Gw != nil && !route.Gw.Equal(*expected.Gw) {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("invalid route %q for packet {src=%s, dst=%s}, expected: %q",
route, packet.Src, packet.Dst, expected))
return nil
}
if expected.Type != nil && *expected.Type != route.Type {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("invalid route %q for packet {src=%s, dst=%s}, expected: %q",
route, packet.Src, packet.Dst, expected))
return nil
}
if expected.Protocol != nil && *expected.Protocol != route.Protocol {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("invalid route %q for packet {src=%s, dst=%s}, expected: %q",
route, packet.Src, packet.Dst, expected))
return nil
}
return nil
}
func (na *NetstackAssertion) AssertVxlanVtep(vtep, dstHost net.IP, vxlanInterface string) error {
iface, ok := lo.Find(na.netns.Interfaces, func(i netstack.Interface) bool { return i.Name == vxlanInterface })
if !ok {
na.AddSuspicion(model.SuspicionLevelFatal, fmt.Sprintf("invalid vxlan interface %s", vxlanInterface))
return nil
}
neighResult, err := na.netns.Neighbour.ProbeNeigh(vtep, iface.Index)
if err != nil {
return err
}
if neighResult == nil {
na.AddSuspicion(model.SuspicionLevelCritical,
fmt.Sprintf("no neigh for next node hop: %s on %s", vtep, vxlanInterface))
return nil
}
if neighResult.DST == nil || neighResult.DST.IsUnspecified() {
na.AddSuspicion(model.SuspicionLevelCritical,
fmt.Sprintf("no fdb table for %s", vxlanInterface))
return nil
}
if !neighResult.DST.Equal(dstHost) {
na.AddSuspicion(model.SuspicionLevelCritical,
fmt.Sprintf("fdb table for %q not equal to expect vtep %q", vtep, dstHost))
}
return nil
}
func (na *NetstackAssertion) AssertDefaultIPIPTunnel(ifName string) {
dev, ok := lo.Find(na.netns.Interfaces, func(i netstack.Interface) bool { return i.Name == ifName })
if !ok {
na.AddSuspicion(
model.SuspicionLevelFatal,
fmt.Sprintf("interface %q does not exist", ifName))
return
}
if dev.Driver != netstack.LinkDriverIPIP {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("driver of interface %q is %q, not %q", ifName, dev.Driver, netstack.LinkDriverIPIP))
return
}
// fixme: need driver specific info for ipip
}
// netfilter assertion functions
// AssertNoIPTables assertion no iptables rules
func (na *NetstackAssertion) AssertNoIPTables() {
if err := na.netns.IPTables.Empty(); err != nil {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("iptables: %s", err))
}
}
func (na *NetstackAssertion) AssertDefaultAccept() {
if err := na.netns.IPTables.DefaultAccept(); err != nil {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("iptables: %s", err))
}
}
func (na *NetstackAssertion) checkNetfilterResult(_ netstack.Verdict, err error) bool {
if err != nil {
if err == netstack.ErrIPTablesUnsupported {
na.AddSuspicion(model.SuspicionLevelWarning,
"iptables contains unsupported rules, which is not expected")
return false
}
if e, ok := err.(*netstack.IPTablesRuleError); ok {
na.AddSuspicion(model.SuspicionLevelWarning,
fmt.Sprintf("iptables contains unsupported rule: %v", e))
return false
}
if e, ok := err.(*netstack.IPTableDropError); ok {
na.AddSuspicion(model.SuspicionLevelWarning,
fmt.Sprintf("packet drop by iptables, trace: %v", e.Trace))
return false
}
}
return true
}
func (na *NetstackAssertion) AssertNetfilterSend(pktIn model.Packet, pktOut []model.Packet, iif string) {
verdict, _, err := na.netns.Netfilter.Hook(netstack.NFHookOutput, pktIn, iif, "")
if !na.checkNetfilterResult(verdict, err) {
return
}
for _, pkt := range pktOut {
verdict, _, err := na.netns.Netfilter.Hook(netstack.NFHookPostRouting, pkt, iif, "")
if !na.checkNetfilterResult(verdict, err) {
continue
}
//should we check snat here?
}
}
func (na *NetstackAssertion) AssertNetfilterForward(pktIn model.Packet, pktOut []model.Packet, iif string) {
verdict, filteredPkt, err := na.netns.Netfilter.Hook(netstack.NFHookPreRouting, pktIn, iif, "")
if !na.checkNetfilterResult(verdict, err) {
return
}
if len(pktOut) == 0 {
pktOut = []model.Packet{pktIn}
}
pktCopy := pktIn
for _, pkt := range pktOut {
pktCopy.Dst = pkt.Dst
verdict, filteredPkt, err = na.netns.Netfilter.Hook(netstack.NFHookForward, pktCopy, iif, "")
na.checkNetfilterResult(verdict, err)
verdict, filteredPkt, err = na.netns.Netfilter.Hook(netstack.NFHookPostRouting, filteredPkt, iif, "")
na.checkNetfilterResult(verdict, err)
if !pktCopy.Src.Equal(filteredPkt.Src) {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("pkt %v is SNATed to %v, which is not expected", pktIn, filteredPkt))
}
}
}
func (na *NetstackAssertion) AssertNetfilterServe(pktIn model.Packet, iif string) {
verdict, filteredPkt, err := na.netns.Netfilter.Hook(netstack.NFHookPreRouting, pktIn, iif, "")
if !na.checkNetfilterResult(verdict, err) {
return
}
verdict, _, err = na.netns.Netfilter.Hook(netstack.NFHookInput, filteredPkt, iif, "")
na.checkNetfilterResult(verdict, err)
}
func (na *NetstackAssertion) AssertIPVSServerExists(service string, servicePort uint16, protocol model.Protocol,
backend string, backendPort uint16) {
key := fmt.Sprintf("%s:%s:%d", protocol, service, servicePort)
_, ok := na.netns.NetNSInfo.IPVSInfo[key]
if !ok {
return
}
svc := na.netns.IPVS.GetService(protocol, service, servicePort)
if svc == nil || svc.RS == nil {
na.AddSuspicion(model.SuspicionLevelFatal,
fmt.Sprintf("ipvs has no service %s or service has no rs info", key))
}
found := lo.ContainsBy(svc.RS, func(rs netstack.RealServer) bool {
return rs.IP == backend && rs.Port == backendPort
})
AssertTrue(na, found, model.SuspicionLevelWarning,
fmt.Sprintf("ipvs service %s has no endpoint %s:%d", service, backend, backendPort))
}