npm/pkg/dataplane/debug/trafficanalyzer.go (379 lines of code) (raw):
package debug
import (
"fmt"
"log"
"net"
"sort"
"strconv"
"strings"
npmconfig "github.com/Azure/azure-container-networking/npm/config"
common "github.com/Azure/azure-container-networking/npm/pkg/controlplane/controllers/common"
"github.com/Azure/azure-container-networking/npm/pkg/dataplane/pb"
"github.com/Azure/azure-container-networking/npm/util"
"google.golang.org/protobuf/encoding/protojson"
)
type TupleAndRule struct {
Tuple *Tuple
Rule *pb.RuleResponse
}
// Tuple struct
type Tuple struct {
RuleType string `json:"ruleType"`
Direction string `json:"direction"`
SrcIP string `json:"srcIP"`
SrcPort string `json:"srcPort"`
DstIP string `json:"dstIP"`
DstPort string `json:"dstPort"`
Protocol string `json:"protocol"`
}
func PrettyPrintTuples(tuples []*TupleAndRule, srcList map[string]*pb.RuleResponse_SetInfo, dstList map[string]*pb.RuleResponse_SetInfo) { //nolint: gocritic
allowedrules := []*TupleAndRule{}
for _, tuple := range tuples {
if tuple.Tuple.RuleType == "ALLOWED" {
allowedrules = append(allowedrules, tuple)
continue
}
/*tuple.Tuple.Direction == "EGRESS" {
fmt.Printf("\tProtocol: %s, Port: %s\n, Chain: %v", tuple.Tuple.Protocol, tuple.Tuple.SrcPort, tuple.Rule.Chain)
}*/
}
sort.Slice(allowedrules, func(i, j int) bool {
return allowedrules[i].Tuple.Direction == "EGRESS"
})
tuplechains := make(map[Tuple]string)
fmt.Printf("Allowed:\n")
section := ""
for _, tuple := range allowedrules {
if tuple.Tuple.Direction != section {
fmt.Printf("\t%s:\n", tuple.Tuple.Direction)
section = tuple.Tuple.Direction
}
t := *tuple
if chain, ok := tuplechains[*t.Tuple]; ok {
// doesn't exist in map
if chain != t.Rule.Chain {
// we've seen this tuple before with a different chain, need to print
fmt.Printf("\t\tProtocol: %s, Port: %s, Chain: %v, Comment: %v\n", tuple.Tuple.Protocol, tuple.Tuple.DstPort, tuple.Rule.Chain, tuple.Rule.Comment)
}
} else {
// we haven't seen this tuple before, print everything
tuplechains[*t.Tuple] = t.Rule.Chain
fmt.Printf("\t\tProtocol: %s, Port: %s, Chain: %v, Comment: %v\n", tuple.Tuple.Protocol, tuple.Tuple.DstPort, tuple.Rule.Chain, tuple.Rule.Comment)
}
}
fmt.Printf("Key:\n")
fmt.Printf("IPSets:")
fmt.Printf("\tSource IPSets:\n")
for i := range srcList {
fmt.Printf("\t\tName: %s, HashedName: %s,\n", srcList[i].Name, srcList[i].HashedSetName)
}
fmt.Printf("\tDestination IPSets:\n")
for i := range dstList {
fmt.Printf("\t\tName: %s, HashedName: %s,\n", dstList[i].Name, dstList[i].HashedSetName)
}
}
// GetNetworkTuple read from node's NPM cache and iptables-save and
// returns a list of hit rules between the source and the destination in
// JSON format and a list of tuples from those rules.
func (c *Converter) GetNetworkTuple(src, dst *common.Input, config *npmconfig.Config) ([][]byte, []*TupleAndRule, map[string]*pb.RuleResponse_SetInfo, map[string]*pb.RuleResponse_SetInfo, error) { //nolint: gocritic,lll
allRules, err := c.GetProtobufRulesFromIptable("filter")
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("error occurred during get network tuple : %w", err)
}
// after we have all rules from the AZURE-NPM chains in the filter table, get the network tuples of src and dst
return getNetworkTupleCommon(src, dst, c.NPMCache, allRules)
}
// GetNetworkTupleFile read from NPM cache and iptables-save files and
// returns a list of hit rules between the source and the destination in
// JSON format and a list of tuples from those rules.
func (c *Converter) GetNetworkTupleFile( //nolint:gocritic
src, dst *common.Input,
npmCacheFile string,
iptableSaveFile string,
) ([][]byte, []*TupleAndRule, map[string]*pb.RuleResponse_SetInfo, map[string]*pb.RuleResponse_SetInfo, error) {
allRules, err := c.GetProtobufRulesFromIptableFile(util.IptablesFilterTable, npmCacheFile, iptableSaveFile)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("error occurred during get network tuple : %w", err)
}
return getNetworkTupleCommon(src, dst, c.NPMCache, allRules)
}
// Common function.
func getNetworkTupleCommon(
src, dst *common.Input,
npmCache common.GenericCache,
allRules map[*pb.RuleResponse]struct{},
) ([][]byte, []*TupleAndRule, map[string]*pb.RuleResponse_SetInfo, map[string]*pb.RuleResponse_SetInfo, error) {
srcPod, err := npmCache.GetPod(src)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("error occurred during get source pod : %w", err)
}
dstPod, err := npmCache.GetPod(dst)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("error occurred during get destination pod : %w", err)
}
// find all rules where the source pod and dest pod exist
hitRules, srcSets, dstSets, err := getHitRules(srcPod, dstPod, allRules, npmCache)
if err != nil {
return nil, nil, srcSets, dstSets, fmt.Errorf("%w", err)
}
ruleResListJSON := make([][]byte, 0)
m := protojson.MarshalOptions{
Indent: " ",
EmitUnpopulated: true,
}
for _, rule := range hitRules {
ruleJSON, err := m.Marshal(rule) // pretty print
if err != nil {
return nil, nil, srcSets, dstSets, fmt.Errorf("error occurred during marshalling : %w", err)
}
ruleResListJSON = append(ruleResListJSON, ruleJSON)
}
resTupleList := make([]*TupleAndRule, 0)
for _, rule := range hitRules {
tuple := generateTuple(srcPod, dstPod, rule)
resTupleList = append(resTupleList, tuple)
}
// tupleResListJson := make([][]byte, 0)
// for _, rule := range resTupleList {
// ruleJson, err := json.MarshalIndent(rule, "", " ")
// if err != nil {
// log.Fatalf("Error occurred during marshaling. Error: %s", err.Error())
// }
// tupleResListJson = append(tupleResListJson, ruleJson)
// }
return ruleResListJSON, resTupleList, srcSets, dstSets, nil
}
func generateTuple(src, dst *common.NpmPod, rule *pb.RuleResponse) *TupleAndRule {
tuple := &Tuple{}
if rule.Allowed {
tuple.RuleType = "ALLOWED"
} else {
tuple.RuleType = "NOT ALLOWED"
}
switch rule.Direction {
case pb.Direction_EGRESS:
tuple.Direction = "EGRESS"
case pb.Direction_INGRESS:
tuple.Direction = "INGRESS"
case pb.Direction_UNDEFINED:
// not sure if this is correct
tuple.Direction = ANY
default:
tuple.Direction = ANY
}
if len(rule.SrcList) == 0 {
tuple.SrcIP = ANY
} else {
tuple.SrcIP = src.IP()
}
if rule.SPort != 0 {
tuple.SrcPort = strconv.Itoa(int(rule.SPort))
} else {
tuple.SrcPort = ANY
}
if len(rule.DstList) == 0 {
tuple.DstIP = ANY
} else {
tuple.DstIP = dst.IP()
}
if rule.DPort != 0 {
tuple.DstPort = strconv.Itoa(int(rule.DPort))
} else {
tuple.DstPort = ANY
}
if rule.Protocol != "" {
tuple.Protocol = rule.Protocol
} else {
tuple.Protocol = ANY
}
return &TupleAndRule{
Tuple: tuple,
Rule: rule,
}
}
func getHitRules(
src, dst *common.NpmPod,
rules map[*pb.RuleResponse]struct{},
npmCache common.GenericCache,
) ([]*pb.RuleResponse, map[string]*pb.RuleResponse_SetInfo, map[string]*pb.RuleResponse_SetInfo, error) {
res := make([]*pb.RuleResponse, 0)
srcSets := make(map[string]*pb.RuleResponse_SetInfo, 0)
dstSets := make(map[string]*pb.RuleResponse_SetInfo, 0)
for rule := range rules {
matchedSrc := false
matchedDst := false
// evalute all match set in src
for _, setInfo := range rule.SrcList {
if src.Namespace == "" {
// internet
break
}
matchedSource, err := evaluateSetInfo("src", setInfo, src, rule, npmCache)
if err != nil {
return nil, nil, nil, fmt.Errorf("error occurred during evaluating source's set info : %w", err)
}
if matchedSource {
matchedSrc = true
srcSets[setInfo.HashedSetName] = setInfo
break
}
}
// evaluate all match set in dst
for _, setInfo := range rule.DstList {
if dst.Namespace == "" {
// internet
break
}
matchedDestination, err := evaluateSetInfo("dst", setInfo, dst, rule, npmCache)
if err != nil {
return nil, nil, nil, fmt.Errorf("error occurred during evaluating destination's set info : %w", err)
}
if matchedDestination {
dstSets[setInfo.HashedSetName] = setInfo
matchedDst = true
break
}
}
// conditions:
// add if src matches and there's no dst
// add if dst matches and there's no src
// add if src and dst match with both src and dst specified
if (matchedSrc && len(rule.DstList) == 0) ||
(matchedDst && len(rule.SrcList) == 0) ||
(matchedSrc && matchedDst) {
res = append(res, rule)
}
}
if len(res) == 0 {
// either no hit rules or no rules at all. Both cases allow all traffic
res = append(res, &pb.RuleResponse{Allowed: true})
}
return res, srcSets, dstSets, nil
}
// evalute an ipset to find out whether the pod's attributes match with the set
func evaluateSetInfo(
origin string,
setInfo *pb.RuleResponse_SetInfo,
pod *common.NpmPod,
rule *pb.RuleResponse,
npmCache common.GenericCache,
) (bool, error) {
switch setInfo.Type {
case pb.SetType_KEYVALUELABELOFNAMESPACE:
return matchKEYVALUELABELOFNAMESPACE(pod, npmCache, setInfo), nil
case pb.SetType_NESTEDLABELOFPOD:
return matchNESTEDLABELOFPOD(pod, setInfo), nil
case pb.SetType_KEYLABELOFNAMESPACE:
return matchKEYLABELOFNAMESPACE(pod, npmCache, setInfo), nil
case pb.SetType_NAMESPACE:
return matchNAMESPACE(pod, setInfo), nil
case pb.SetType_KEYVALUELABELOFPOD:
return matchKEYVALUELABELOFPOD(pod, setInfo), nil
case pb.SetType_KEYLABELOFPOD:
return matchKEYLABELOFPOD(pod, setInfo), nil
case pb.SetType_NAMEDPORTS:
return matchNAMEDPORTS(pod, setInfo, rule, origin), nil
case pb.SetType_CIDRBLOCKS:
return matchCIDRBLOCKS(pod, setInfo), nil
default:
return false, common.ErrSetType
}
}
func matchKEYVALUELABELOFNAMESPACE(pod *common.NpmPod, npmCache common.GenericCache, setInfo *pb.RuleResponse_SetInfo) bool {
srcNamespace := util.NamespacePrefix + pod.Namespace
key, expectedValue := processKeyValueLabelOfNameSpace(setInfo.Name)
actualValue := npmCache.GetNamespaceLabel(srcNamespace, key)
if expectedValue != actualValue {
// if the value is required but does not match
if setInfo.Included {
return false
}
} else {
if !setInfo.Included {
return false
}
}
return true
}
func matchNESTEDLABELOFPOD(pod *common.NpmPod, setInfo *pb.RuleResponse_SetInfo) bool {
// a function to split the key and the values and then combine the key with each value
// return list of key value pairs which are keyvaluelabel of pod
// one match then break
kvList := processNestedLabelOfPod(setInfo.Name)
hasOneKeyValuePair := false
for _, kvPair := range kvList {
key, value := processKeyValueLabelOfPod(kvPair)
if pod.Labels[key] == value {
if !setInfo.Included {
return false
}
hasOneKeyValuePair = true
break
}
}
if !hasOneKeyValuePair && setInfo.Included {
return false
}
return true
}
func matchKEYLABELOFNAMESPACE(pod *common.NpmPod, npmCache common.GenericCache, setInfo *pb.RuleResponse_SetInfo) bool {
srcNamespace := pod.Namespace
key := strings.Split(strings.TrimPrefix(setInfo.Name, util.NamespaceLabelPrefix), ":")
included := npmCache.GetNamespaceLabel(srcNamespace, key[0])
if included != "" && included == key[1] {
return setInfo.Included
}
if setInfo.Included {
// if key does not exist but required in rule
return false
}
return true
}
func matchNAMESPACE(pod *common.NpmPod, setInfo *pb.RuleResponse_SetInfo) bool {
srcNamespace := util.NamespacePrefix + pod.Namespace
if setInfo.Name != srcNamespace || (setInfo.Name == srcNamespace && !setInfo.Included) {
return false
}
return true
}
func matchKEYVALUELABELOFPOD(pod *common.NpmPod, setInfo *pb.RuleResponse_SetInfo) bool {
key, value := processKeyValueLabelOfPod(setInfo.Name)
if pod.Labels[key] != value || (pod.Labels[key] == value && !setInfo.Included) {
return false
}
log.Printf("matched key value label of pod")
return true
}
func matchKEYLABELOFPOD(pod *common.NpmPod, setInfo *pb.RuleResponse_SetInfo) bool {
key := setInfo.Name
if _, ok := pod.Labels[key]; ok {
return setInfo.Included
}
if setInfo.Included {
// if key does not exist but required in rule
return false
}
return true
}
func matchNAMEDPORTS(pod *common.NpmPod, setInfo *pb.RuleResponse_SetInfo, rule *pb.RuleResponse, origin string) bool {
portname := strings.TrimPrefix(setInfo.Name, util.NamedPortIPSetPrefix)
for _, namedPort := range pod.ContainerPorts {
if namedPort.Name == portname {
if !setInfo.Included {
return false
}
if rule.Protocol != "" && rule.Protocol != strings.ToLower(string(namedPort.Protocol)) {
return false
}
if rule.Protocol == "" {
rule.Protocol = strings.ToLower(string(namedPort.Protocol))
}
if origin == "src" {
rule.SPort = namedPort.ContainerPort
} else {
rule.DPort = namedPort.ContainerPort
}
return true
}
}
return false
}
func matchCIDRBLOCKS(pod *common.NpmPod, setInfo *pb.RuleResponse_SetInfo) bool {
matched := false
for _, entry := range setInfo.Contents {
entrySplitted := strings.Split(entry, " ")
if len(entrySplitted) > 1 { // nomatch condition. i.e [172.17.1.0/24 nomatch]
_, ipnet, _ := net.ParseCIDR(strings.TrimSpace(entrySplitted[0]))
podIP := net.ParseIP(pod.PodIP)
if ipnet.Contains(podIP) {
matched = false
break
}
} else {
_, ipnet, _ := net.ParseCIDR(strings.TrimSpace(entrySplitted[0]))
podIP := net.ParseIP(pod.PodIP)
if ipnet.Contains(podIP) {
matched = true
}
}
}
return matched
}
func processKeyValueLabelOfNameSpace(kv string) (string, string) {
str := strings.TrimPrefix(kv, util.NamespacePrefix)
ret := strings.Split(str, ":")
return ret[0], ret[1]
}
func processKeyValueLabelOfPod(kv string) (string, string) {
ret := strings.Split(kv, ":")
return ret[0], ret[1]
}
func processNestedLabelOfPod(kv string) []string {
str := strings.TrimPrefix(kv, util.NestedLabelPrefix)
kvList := strings.Split(str, ":")
key := kvList[0]
ret := make([]string, 0)
for _, value := range kvList[1:] {
ret = append(ret, key+":"+value)
}
return ret
}