pkg/skoop/network/aliyun/assertion.go (755 lines of code) (raw):
package aliyun
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/alibaba/kubeskoop/pkg/skoop/network"
slb "github.com/alibabacloud-go/slb-20140515/v4/client"
"github.com/alibaba/kubeskoop/pkg/skoop/infra/aliyun"
"github.com/alibaba/kubeskoop/pkg/skoop/model"
ecs "github.com/alibabacloud-go/ecs-20140526/v2/client"
vpc "github.com/alibabacloud-go/vpc-20160428/v2/client"
"github.com/samber/lo"
"golang.org/x/exp/slices"
"k8s.io/klog/v2"
)
type securityPolicyVerdict string
const (
securityPolicyVerdictAccept securityPolicyVerdict = "Accept"
securityPolicyVerdictDrop securityPolicyVerdict = "Drop"
)
type vpcAssertion struct {
cloudManager *aliyun.CloudManager
}
func newVPCAssertion(cloudManager *aliyun.CloudManager) (*vpcAssertion, error) {
if cloudManager == nil {
return nil, fmt.Errorf("cloud manager must be provided")
}
return &vpcAssertion{
cloudManager: cloudManager,
}, nil
}
func (a *vpcAssertion) AssertSecurityGroup(srcECS, dstECS string, pkt *model.Packet) ([]model.Suspicion, error) {
if srcECS == "" && dstECS == "" {
return nil, nil
}
var suspicions []model.Suspicion
if srcECS != "" && dstECS != "" {
srcECSInfo, err := a.cloudManager.GetECSInfo(srcECS)
if err != nil {
return nil, err
}
dstECSInfo, err := a.cloudManager.GetECSInfo(dstECS)
if err != nil {
return nil, err
}
var srcECSSecurityGroup map[string]aliyun.SecurityGroupRule
srcENI := a.getENIFromIP(srcECSInfo, pkt.Src.String())
if srcENI != nil {
srcECSSecurityGroup = srcENI.SecurityGroups
} else {
srcECSSecurityGroup = srcECSInfo.Network.SecurityGroups
}
var dstECSSecurityGroup map[string]aliyun.SecurityGroupRule
dstENI := a.getENIFromIP(dstECSInfo, pkt.Dst.String())
if dstENI != nil {
dstECSSecurityGroup = dstENI.SecurityGroups
} else {
dstECSSecurityGroup = dstECSInfo.Network.SecurityGroups
}
sgIntersection := a.intersection(srcECSSecurityGroup, dstECSSecurityGroup)
if len(sgIntersection) == 0 {
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelWarning,
Message: fmt.Sprintf("%s(%s) and %s(%s) do not have same security group",
srcECS, a.getECSIP(srcECSInfo), dstECS, a.getECSIP(dstECSInfo)),
})
}
// eni 只能绑定一个类型sg
// sg 相交场景,且相交的是普通安全组,默认策略为 Allow
// sg 相交场景,且为企业安全组,默认策略 Deny
// sg 不相交,默认 Deny
sgPolicyVerdict := securityPolicyVerdictDrop
if len(sgIntersection) > 0 {
if sgIntersection[0].Type == aliyun.SecurityGroupTypeNormal {
sgPolicyVerdict = securityPolicyVerdictAccept
}
checkResult, err := a.checkSourceOut(pkt, srcECSSecurityGroup)
if err != nil {
return nil, err
}
if !checkResult {
sgIDs := lo.MapToSlice(srcECSSecurityGroup,
func(id string, _ aliyun.SecurityGroupRule) string { return id })
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("%s(%s) security group %v not allow packet to %s:%d",
srcECS, a.getECSIP(srcECSInfo), sgIDs, pkt.Dst.String(), pkt.Dport),
})
}
if !slices.Contains(srcECSInfo.Network.IP, pkt.Src.String()) || len(sgIntersection) == 0 ||
(len(sgIntersection) > 0 && srcECSSecurityGroup[sgIntersection[0].ID].Type == "enterprise") {
checkResult, err := a.checkDestinationIn(pkt, dstECSSecurityGroup, sgPolicyVerdict)
if err != nil {
return nil, err
}
if !checkResult {
sgIDs := lo.MapToSlice(dstECSSecurityGroup,
func(id string, _ aliyun.SecurityGroupRule) string { return id })
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("%s(%s) security group %v not allow packet from %s to port %d",
dstECS, a.getECSIP(dstECSInfo), sgIDs, pkt.Src.String(), pkt.Dport),
})
}
}
}
} else if srcECS != "" && dstECS == "" {
srcECSInfo, err := a.cloudManager.GetECSInfo(srcECS)
if err != nil {
return nil, err
}
srcENI := a.getENIFromIP(srcECSInfo, pkt.Src.String())
var srcECSSecurityGroup map[string]aliyun.SecurityGroupRule
if srcENI == nil {
srcECSSecurityGroup = srcECSInfo.Network.SecurityGroups
} else {
srcECSSecurityGroup = srcENI.SecurityGroups
}
checkResult, err := a.checkSourceOut(pkt, srcECSSecurityGroup)
if err != nil {
return nil, err
}
if !checkResult {
sgIDs := lo.Keys(srcECSSecurityGroup)
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("%s(%s) security group %v not allow packet from to %s:%d",
srcECS, a.getECSIP(srcECSInfo), sgIDs, pkt.Dst.String(), pkt.Dport),
})
}
} else if srcECS == "" && dstECS != "" {
dstECSInfo, err := a.cloudManager.GetECSInfo(dstECS)
if err != nil {
return nil, err
}
dstENI := a.getENIFromIP(dstECSInfo, pkt.Dst.String())
var dstECSSecurityGroup map[string]aliyun.SecurityGroupRule
if dstENI == nil {
dstECSSecurityGroup = dstECSInfo.Network.SecurityGroups
} else {
dstECSSecurityGroup = dstENI.SecurityGroups
}
sgs := lo.Values(dstECSSecurityGroup)
if len(sgs) > 0 {
sgPolicyVerdict := securityPolicyVerdictDrop
checkResult, err := a.checkDestinationIn(pkt, dstECSSecurityGroup, sgPolicyVerdict)
if err != nil {
return nil, err
}
if !checkResult {
sgIDs := lo.MapToSlice(dstECSSecurityGroup,
func(id string, _ aliyun.SecurityGroupRule) string { return id })
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("%s(%s) security group %v not allow packet from %s to port %d",
dstECS, a.getECSIP(dstECSInfo), sgIDs, pkt.Src.String(), pkt.Dport),
})
}
}
}
return suspicions, nil
}
func (a *vpcAssertion) AssertRoute(srcECS, dstECS string, pkt *model.Packet, privateIP string) ([]model.Suspicion, error) {
var suspicions []model.Suspicion
var routeEntries []*vpc.DescribeRouteEntryListResponseBodyRouteEntrysRouteEntry
var srcECSInfo, dstECSInfo *aliyun.ECSInfo
var err error
if srcECS == "" && dstECS == "" {
return nil, nil
}
if srcECS != "" {
srcECSInfo, err = a.cloudManager.GetECSInfo(srcECS)
if err != nil {
return nil, err
}
routeEntries = srcECSInfo.Network.RouteTableEntries
if !slices.Contains(srcECSInfo.Network.IP, pkt.Src.String()) {
routeEntries = srcECSInfo.Network.VpcDefaultRouteTableEntries
}
lo.ForEach(srcECSInfo.Network.NetworkInterfaces, func(ni *aliyun.ENIInfo, _ int) {
lo.ForEach(ni.NetworkInterfaceSet.PrivateIpSets.PrivateIpSet, func(ip *ecs.DescribeNetworkInterfacesResponseBodyNetworkInterfaceSetsNetworkInterfaceSetPrivateIpSetsPrivateIpSet, _ int) {
if ip.PrivateIpAddress != nil && *ip.PrivateIpAddress == pkt.Src.String() {
routeEntries = ni.RouteTableEntries
}
})
})
}
if dstECS != "" {
dstECSInfo, err = a.cloudManager.GetECSInfo(dstECS)
if err != nil {
return nil, err
}
if srcECS == "" {
routeEntries = dstECSInfo.Network.VpcDefaultRouteTableEntries
}
}
dstRouteEntry, err := routeMatchPacket(pkt.Dst.String(), routeEntries)
if err != nil {
return nil, err
}
if dstRouteEntry == nil {
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("no route entry for destination ip %q", pkt.Dst),
})
}
if dstECS != "" && dstRouteEntry != nil && !slices.Contains(dstECSInfo.Network.IP, pkt.Dst.String()) {
// we do not route dst ip in ecs network ips
// is there any situation that len(NextHop) == 0?
nextHop := dstRouteEntry.NextHops.NextHop[0]
if *nextHop.NextHopType != "local" &&
!(*nextHop.NextHopType == "Instance" && *nextHop.NextHopId == dstECS) {
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("error route next hop for destination ip \"%s\", expect: \"Instance-%s\", actually: \"%s-%s\"",
pkt.Dst.String(), dstECS, *nextHop.NextHopType, *nextHop.NextHopId),
})
}
}
// reverse path
if srcECS != "" {
var srcRouteEntry *vpc.DescribeRouteEntryListResponseBodyRouteEntrysRouteEntry
if dstECS != "" {
if !slices.Contains(dstECSInfo.Network.IP, pkt.Dst.String()) {
routeEntries = dstECSInfo.Network.VpcDefaultRouteTableEntries
lo.ForEach(dstECSInfo.Network.NetworkInterfaces, func(ni *aliyun.ENIInfo, _ int) {
lo.ForEach(ni.NetworkInterfaceSet.PrivateIpSets.PrivateIpSet, func(ip *ecs.DescribeNetworkInterfacesResponseBodyNetworkInterfaceSetsNetworkInterfaceSetPrivateIpSetsPrivateIpSet, _ int) {
if ip.PrivateIpAddress != nil && *ip.PrivateIpAddress == pkt.Dst.String() {
routeEntries = ni.RouteTableEntries
}
})
})
} else {
routeEntries = dstECSInfo.Network.RouteTableEntries
}
} else {
routeEntries = srcECSInfo.Network.VpcDefaultRouteTableEntries
}
if privateIP != "" {
srcRouteEntry, err = routeMatchPacket(privateIP, routeEntries)
if err != nil {
return nil, err
}
} else {
srcRouteEntry, err = routeMatchPacket(pkt.Src.String(), routeEntries)
}
if err != nil {
return nil, err
}
if srcRouteEntry == nil {
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("no route entry for src ip %q", pkt.Src.String()),
})
}
if srcRouteEntry != nil && !slices.Contains(srcECSInfo.Network.IP, pkt.Src.String()) {
nextHop := srcRouteEntry.NextHops.NextHop[0]
if nextHop.NextHopRegionId != nil && *nextHop.NextHopRegionId != "local" &&
!(*nextHop.NextHopType == "Instance" && *nextHop.NextHopId == srcECS) {
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("error route next hop for source ip: %q, expect: \"Instance-%s\", actual: \"%s-%s\"",
pkt.Src, srcECSInfo.ID, *nextHop.NextHopType, *nextHop.NextHopId),
})
}
}
}
return suspicions, nil
}
func (a *vpcAssertion) AssertSNAT(srcECS string, pkt *model.Packet, privateIP string) ([]model.Suspicion, error) {
var suspicions []model.Suspicion
eniInfo, err := a.cloudManager.GetENIInfoFromVPCAndPrivateIP(a.cloudManager.VPC(), privateIP)
if err != nil {
return nil, err
}
vswitchID := *eniInfo.VSwitchId
klog.V(3).Infof("vswitch id %s", vswitchID)
nextHop, err := a.findNextHop(pkt, srcECS)
if err != nil {
return nil, err
}
if nextHop == nil {
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("no next hop for destination ip %q", pkt.Dst),
})
return suspicions, nil
}
if *nextHop.NextHopType != "NatGateway" {
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("expect next hop for destination ip %q to be NatGateway, but \"%s-%s\"",
pkt.Dst, *nextHop.NextHopType, *nextHop.NextHopId),
})
return suspicions, nil
}
ngwID := *nextHop.NextHopId
snatEntry, err := a.findSNATEntry(pkt, ngwID, vswitchID)
if err != nil {
return nil, err
}
if snatEntry == nil {
suspicions = append(suspicions, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("no snat entry on nat gateway %q for destination ip %q", ngwID, pkt.Dst),
})
}
return suspicions, nil
}
func (a *vpcAssertion) getENIFromIP(ecsInfo *aliyun.ECSInfo, dstIP string) *aliyun.ENIInfo {
for _, eni := range ecsInfo.Network.NetworkInterfaces {
for _, privateIP := range eni.NetworkInterfaceSet.PrivateIpSets.PrivateIpSet {
if *privateIP.PrivateIpAddress == dstIP {
return eni
}
}
}
return nil
}
func (a *vpcAssertion) intersection(sga, sgb map[string]aliyun.SecurityGroupRule) []aliyun.SecurityGroupRule {
existed := map[string]struct{}{}
for _, sg := range sga {
existed[sg.ID] = struct{}{}
}
var ret []aliyun.SecurityGroupRule
for _, sg := range sgb {
if _, ok := existed[sg.ID]; ok {
ret = append(ret, sg)
}
}
return ret
}
func (a *vpcAssertion) getECSIP(ecs *aliyun.ECSInfo) string {
if len(ecs.Network.IP) == 0 {
return "!NOT FOUND!"
}
return ecs.Network.IP[0]
}
func (a *vpcAssertion) checkSourceOut(pkt *model.Packet, sgs map[string]aliyun.SecurityGroupRule) (bool, error) {
hasEnterpriseSg := false
var outRules []*ecs.DescribeSecurityGroupAttributeResponseBodyPermissionsPermission
for _, sg := range sgs {
if sg.Type == "enterprise" {
hasEnterpriseSg = true
}
outRules = append(outRules, sg.OutRule.Allows...)
outRules = append(outRules, sg.OutRule.Drops...)
}
if hasEnterpriseSg {
return a.packetPassRules(pkt, outRules, securityPolicyVerdictDrop)
}
return a.packetPassRules(pkt, outRules, securityPolicyVerdictAccept)
}
func (a *vpcAssertion) checkDestinationIn(pkt *model.Packet, sgs map[string]aliyun.SecurityGroupRule, defaultPolicy securityPolicyVerdict) (bool, error) {
var inRules []*ecs.DescribeSecurityGroupAttributeResponseBodyPermissionsPermission
for _, sg := range sgs {
inRules = append(inRules, sg.InRule.Allows...)
inRules = append(inRules, sg.InRule.Drops...)
}
if defaultPolicy == "" {
defaultPolicy = securityPolicyVerdictDrop
}
return a.packetPassRules(pkt, inRules, defaultPolicy)
}
func (a *vpcAssertion) packetPassRules(pkt *model.Packet, outRules []*ecs.DescribeSecurityGroupAttributeResponseBodyPermissionsPermission, defaultPolicy securityPolicyVerdict) (bool, error) {
var filteredRules []*ecs.DescribeSecurityGroupAttributeResponseBodyPermissionsPermission
for _, rule := range outRules {
match, err := ruleMatchPacket(pkt, rule)
if err != nil {
return false, err
}
if match {
filteredRules = append(filteredRules, rule)
}
}
sortSecurityGroupRules(filteredRules)
if len(filteredRules) > 0 {
return *filteredRules[0].Policy == string(securityPolicyVerdictAccept), nil
}
klog.V(3).Infof("No rule match for packet, default policy for SecurityGroup is %q", defaultPolicy)
return defaultPolicy == securityPolicyVerdictAccept, nil
}
func (a *vpcAssertion) findNextHop(pkt *model.Packet, srcECS string) (*vpc.DescribeRouteEntryListResponseBodyRouteEntrysRouteEntryNextHopsNextHop, error) {
ecsInfo, err := a.cloudManager.GetECSInfo(srcECS)
if err != nil {
return nil, err
}
routeEntries := ecsInfo.Network.RouteTableEntries
if !slices.Contains(ecsInfo.Network.IP, pkt.Src.String()) {
routeEntries = ecsInfo.Network.VpcDefaultRouteTableEntries
}
lo.ForEach(ecsInfo.Network.NetworkInterfaces, func(ni *aliyun.ENIInfo, _ int) {
lo.ForEach(ni.NetworkInterfaceSet.PrivateIpSets.PrivateIpSet, func(ip *ecs.DescribeNetworkInterfacesResponseBodyNetworkInterfaceSetsNetworkInterfaceSetPrivateIpSetsPrivateIpSet, _ int) {
if ip.PrivateIpAddress != nil && *ip.PrivateIpAddress == pkt.Src.String() {
routeEntries = ni.RouteTableEntries
}
})
})
route, err := routeMatchPacket(pkt.Dst.String(), routeEntries)
if err != nil {
return nil, err
}
return route.NextHops.NextHop[0], nil
}
func (a *vpcAssertion) findSNATEntry(pkt *model.Packet, ngwID, vswitchID string) (*vpc.DescribeSnatTableEntriesResponseBodySnatTableEntriesSnatTableEntry, error) {
vswitch, err := a.cloudManager.GetVSwitch(vswitchID)
if err != nil {
return nil, err
}
ngw, err := a.cloudManager.GetNatGatewayInfo(ngwID)
if err != nil {
return nil, err
}
snatTables := ngw.SnatTableIds.SnatTableId
var defaultEntry *vpc.DescribeSnatTableEntriesResponseBodySnatTableEntriesSnatTableEntry
for _, t := range snatTables {
snatEntries, err := a.cloudManager.GetSNATEntriesBySegment(*t, fmt.Sprintf("%s/32", pkt.Src))
if err != nil {
return nil, err
}
if len(snatEntries) > 0 {
defaultEntry = snatEntries[0]
break
}
snatEntries, err = a.cloudManager.GetSNATEntriesBySegment(*t, *vswitch.CidrBlock)
if err != nil {
return nil, err
}
if len(snatEntries) > 0 {
defaultEntry = snatEntries[0]
}
}
return defaultEntry, nil
}
type slbAssertion struct {
cloudManager *aliyun.CloudManager
}
func newSLBAssertion(cloudManager *aliyun.CloudManager) (*slbAssertion, error) {
return &slbAssertion{
cloudManager: cloudManager,
}, nil
}
func (a *slbAssertion) assertLoadBalancer(lb *slb.DescribeLoadBalancersResponseBodyLoadBalancersLoadBalancer, dport uint16, protocol model.Protocol) ([]model.Suspicion, error) {
var sus []model.Suspicion
if lb.LoadBalancerStatus != nil && *lb.LoadBalancerStatus != "active" {
sus = append(sus, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("loadBalancer(%s) status is %q, not \"active\"", *lb.LoadBalancerId, *lb.LoadBalancerStatus),
})
}
status, err := a.cloudManager.GetSLBHealthStatus(*lb.LoadBalancerId, int32(dport), string(protocol))
if err != nil {
return nil, err
}
if len(status) == 0 {
return sus, nil
}
s := status[0]
if s.ServerHealthStatus != nil && *s.ServerHealthStatus != "normal" {
sus = append(sus, model.Suspicion{
Level: model.SuspicionLevelWarning,
Message: fmt.Sprintf("backend %s health status for port %d is %q, not \"normal\"", *s.ServerIp, *s.Port, *s.ServerHealthStatus),
})
}
return sus, nil
}
func (a *slbAssertion) assertListenerAndServerGroup(lbID string, port int32, protocol string, packet *model.Packet, backends []network.LoadBalancerBackend) ([]model.Suspicion, error) {
var sus []model.Suspicion
lis, err := a.cloudManager.GetSLBListener(lbID, port, protocol)
if err != nil {
return nil, err
}
if lis == nil {
sus = append(sus, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("cannot find listener port %d protocol %q for slb %q", port, protocol, lbID),
})
return sus, nil
}
if lis.TCP != nil {
if lis.TCP.Status != nil && *lis.TCP.Status != "running" {
sus = append(sus, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("status of loadbalancer %q for tcp port %d is %q, not \"running\"",
lbID, port, *lis.TCP.Status),
})
}
if lis.TCP.AclStatus != nil && *lis.TCP.AclStatus == "on" && lis.TCP.AclId != nil {
s, err := a.assertACL(*lis.TCP.AclId, *lis.TCP.AclType, packet)
if err != nil {
return nil, err
}
sus = append(sus, s...)
}
if lis.TCP.VServerGroupId != nil && *lis.TCP.VServerGroupId != "" {
s, err := a.assertServerGroup(*lis.TCP.VServerGroupId, backends)
if err != nil {
return nil, err
}
sus = append(sus, s...)
} else {
sus = append(sus, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("cannot find vserver group of loadbalancer %q port %d", lbID, port),
})
}
return sus, nil
}
if lis.UDP != nil {
if lis.UDP.Status != nil && *lis.UDP.Status != "running" {
sus = append(sus, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("status of loadbalancer %q for udp port %d is %q, not \"running\"",
lbID, port, *lis.TCP.Status),
})
}
if lis.UDP.AclStatus != nil && *lis.UDP.AclStatus == "on" && lis.TCP.AclId != nil {
s, err := a.assertACL(*lis.UDP.AclId, *lis.UDP.AclType, packet)
if err != nil {
return nil, err
}
sus = append(sus, s...)
}
if lis.UDP.VServerGroupId != nil && *lis.UDP.VServerGroupId != "" {
s, err := a.assertServerGroup(*lis.UDP.VServerGroupId, backends)
if err != nil {
return nil, err
}
sus = append(sus, s...)
} else {
sus = append(sus, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("cannot find vserver group of loadbalancer %q port %d", lbID, port),
})
}
return sus, nil
}
return nil, fmt.Errorf("not supported protocol %s", protocol)
}
func (a *slbAssertion) assertACL(aclID, t string, packet *model.Packet) ([]model.Suspicion, error) {
acl, err := a.cloudManager.GetACLFromID(aclID)
if err != nil {
return nil, err
}
if acl == nil {
return nil, fmt.Errorf("cannot find acl %q", aclID)
}
// When there is no entries in an ACL, it will allow all packets to pass.
if len(acl.AclEntrys.AclEntry) == 0 {
return nil, nil
}
ipInACL := lo.ContainsBy(acl.AclEntrys.AclEntry, func(i *slb.DescribeAccessControlListAttributeResponseBodyAclEntrysAclEntry) bool {
if i == nil || i.AclEntryIP == nil {
return false
}
_, ipCIDR, _ := parseIPOrCIDR(*i.AclEntryIP)
return ipCIDR.Contains(packet.Src)
})
var sus []model.Suspicion
if (t == "white" && !ipInACL) || (t == "black" && ipInACL) {
sus = append(sus, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("acl %s type %s on loadbalancer does not allow packet to pass", aclID, t),
})
}
return sus, nil
}
func (a *slbAssertion) assertServerGroup(sgID string, backends []network.LoadBalancerBackend) ([]model.Suspicion, error) {
sg, err := a.cloudManager.GetSLBVserverGroup(sgID)
if err != nil {
return nil, err
}
if sg == nil {
return nil, fmt.Errorf("cannot find vserver group %q", sgID)
}
var sus []model.Suspicion
if len(sg.BackendServers.BackendServer) == 0 {
sus = append(sus, model.Suspicion{
Level: model.SuspicionLevelFatal,
Message: fmt.Sprintf("server group %q has no backend", sgID),
})
}
if len(sg.BackendServers.BackendServer) != len(backends) {
sus = append(sus, model.Suspicion{
Level: model.SuspicionLevelWarning,
Message: fmt.Sprintf("backend count %d is not equal to expected backend count %d",
len(sg.BackendServers.BackendServer), len(backends)),
})
}
for _, s := range sg.BackendServers.BackendServer {
contains := lo.ContainsBy(backends, func(b network.LoadBalancerBackend) bool {
if *s.Port != int32(b.Port) {
return false
}
if s.ServerIp != nil && b.IP != "" {
return *s.ServerIp == b.IP
}
if s.ServerId != nil && b.ID != "" {
return *s.ServerId == b.ID
}
// If we're unable to find backend by the given information,
// return true to prevent suspicions.
return true
})
if !contains {
backend := ""
if s.ServerIp != nil {
backend = fmt.Sprintf("%s:%d", *s.ServerIp, *s.Port)
} else if s.ServerId != nil {
backend = fmt.Sprintf("%s:%d", *s.ServerId, *s.Port)
} else {
backend = fmt.Sprintf("(unknown):%d", *s.Port)
}
sus = append(sus, model.Suspicion{
Level: model.SuspicionLevelWarning,
Message: fmt.Sprintf("backend %q in server group %q is unexpected",
backend, sgID),
})
}
// limit suspicions count to not exceed 5
if len(sus) > 5 {
break
}
}
return sus, nil
}
func ruleMatchPacket(pkt *model.Packet, rule *ecs.DescribeSecurityGroupAttributeResponseBodyPermissionsPermission) (bool, error) {
if rule.DestCidrIp != nil && *rule.DestCidrIp != "" {
_, dstCidrIP, err := parseIPOrCIDR(*rule.DestCidrIp)
if err != nil {
return false, err
}
if dstCidrIP.Contains(pkt.Dst) {
if *rule.IpProtocol == "ALL" || strings.EqualFold(string(pkt.Protocol), *rule.IpProtocol) {
if pkt.Dport == 0 {
return true, nil
}
portRange := strings.Split(*rule.PortRange, "/")
// assert len(portRange) == 2
if portRange[0] == "-1" && portRange[1] == "-1" {
return true, nil
}
pStart, err := strconv.Atoi(portRange[0])
if err != nil {
return false, err
}
pEnd, err := strconv.Atoi(portRange[1])
if err != nil {
return false, nil
}
if pStart <= int(pkt.Dport) && pEnd >= int(pkt.Dport) {
return true, nil
}
}
}
}
if rule.SourceCidrIp != nil && *rule.SourceCidrIp != "" {
_, srcCidrIP, err := parseIPOrCIDR(*rule.SourceCidrIp)
if err != nil {
return false, err
}
if srcCidrIP.Contains(pkt.Src) {
if *rule.IpProtocol == "ALL" || strings.EqualFold(string(pkt.Protocol), *rule.IpProtocol) {
if pkt.Dport == 0 {
return true, nil
}
portRange := strings.Split(*rule.PortRange, "/")
// assert len(portRange) == 2
if portRange[0] == "-1" && portRange[1] == "-1" {
return true, nil
}
pStart, err := strconv.Atoi(portRange[0])
if err != nil {
return false, err
}
pEnd, err := strconv.Atoi(portRange[1])
if err != nil {
return false, err
}
if pStart <= int(pkt.Dport) && pEnd >= int(pkt.Dport) {
return true, nil
}
}
}
}
return false, nil
}
func sortSecurityGroupRules(sgs []*ecs.DescribeSecurityGroupAttributeResponseBodyPermissionsPermission) {
slices.SortStableFunc(sgs, func(a, b *ecs.DescribeSecurityGroupAttributeResponseBodyPermissionsPermission) bool {
portRangeA := strings.Split(*a.PortRange, "/")
pStartA, _ := strconv.Atoi(portRangeA[0])
pEndA, _ := strconv.Atoi(portRangeA[1])
if pStartA == -1 && pEndA == -1 {
pStartA, pEndA = 0, 65535
}
portRangeB := strings.Split(*b.PortRange, "/")
pStartB, _ := strconv.Atoi(portRangeB[0])
pEndB, _ := strconv.Atoi(portRangeB[1])
if pStartB == -1 && pEndB == -1 {
pStartB, pEndB = 0, 65535
}
if *a.Priority != *b.Priority {
return *a.Priority < *b.Priority
}
if (a.SourceCidrIp != nil && *a.SourceCidrIp != "") || (b.SourceCidrIp != nil && *b.SourceCidrIp != "") {
if (a.SourceCidrIp == nil || *a.SourceCidrIp == "") || (b.SourceCidrIp == nil || *b.SourceCidrIp == "") {
return a.SourceCidrIp != nil && *a.SourceCidrIp != ""
}
_, netA, _ := parseIPOrCIDR(*a.SourceCidrIp)
onesA, _ := netA.Mask.Size()
_, netB, _ := parseIPOrCIDR(*a.SourceCidrIp)
onesB, _ := netB.Mask.Size()
if onesA != onesB {
return onesA > onesB
}
}
if (a.DestCidrIp != nil && *a.DestCidrIp != "") || (b.DestCidrIp != nil && *b.DestCidrIp != "") {
if (a.DestCidrIp == nil || *a.DestCidrIp == "") || (b.DestCidrIp == nil || *b.DestCidrIp == "") {
return a.DestCidrIp != nil && *a.DestCidrIp != ""
}
_, netA, _ := parseIPOrCIDR(*a.DestCidrIp)
onesA, _ := netA.Mask.Size()
_, netB, _ := parseIPOrCIDR(*a.DestCidrIp)
onesB, _ := netB.Mask.Size()
if onesA != onesB {
return onesA > onesB
}
}
if *a.Policy != *b.Policy {
return *a.Policy == string(securityPolicyVerdictDrop)
}
return (pEndA - pStartA) < (pEndB - pStartB)
})
}
func routeMatchPacket(ip string, routes []*vpc.DescribeRouteEntryListResponseBodyRouteEntrysRouteEntry) (*vpc.DescribeRouteEntryListResponseBodyRouteEntrysRouteEntry, error) {
type routesAndCIDR struct {
entry *vpc.DescribeRouteEntryListResponseBodyRouteEntrysRouteEntry
cidr *net.IPNet
}
var filteredRoutes []routesAndCIDR
netIP := net.ParseIP(ip)
for _, r := range routes {
_, cidr, err := parseIPOrCIDR(*r.DestinationCidrBlock)
if err != nil {
return nil, fmt.Errorf("parse route table %q destination cidr error: %s", *r.RouteTableId, err)
}
if cidr.Contains(netIP) {
filteredRoutes = append(filteredRoutes, routesAndCIDR{entry: r, cidr: cidr})
}
}
slices.SortFunc(filteredRoutes, func(a, b routesAndCIDR) bool {
onesA, _ := a.cidr.Mask.Size()
onesB, _ := b.cidr.Mask.Size()
return onesA > onesB
})
if len(filteredRoutes) > 0 {
return filteredRoutes[0].entry, nil
}
return nil, nil
}
func parseIPOrCIDR(s string) (net.IP, *net.IPNet, error) {
ip, cidr, err := net.ParseCIDR(s)
if err == nil {
return ip, cidr, err
}
return net.ParseCIDR(fmt.Sprintf("%s/32", s))
}