pkg/skoop/netstack/iptables.go (852 lines of code) (raw):

package netstack import ( "context" "fmt" "net" "reflect" "strconv" "strings" "github.com/alibaba/kubeskoop/pkg/skoop/model" "github.com/alibaba/kubeskoop/pkg/skoop/utils" "github.com/beevik/etree" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) type XTablesVerdict uint const ( XTablesVerdictAccept XTablesVerdict = 0 XTablesVerdictDrop XTablesVerdict = 1 XTablesVerdictReject XTablesVerdict = 2 XTablesVerdictReturn XTablesVerdict = 3 XTablesVerdictContinue XTablesVerdict = 4 ) type IPTablesRuleError struct { Rule string Message string } func (e *IPTablesRuleError) Error() string { return fmt.Sprintf("rule: %q, message: %q", e.Rule, e.Message) } type ErrIptablesUnsupported struct { Message string } func (u ErrIptablesUnsupported) Error() string { return fmt.Sprintf("unsupported %s", u.Message) } type Target interface{} type ExtensionTarget interface { Do(ctx context.Context, packet *model.Packet, iif, oif string) (XTablesVerdict, error) } type NopTarget struct{} type AcceptTarget struct{} type DropTarget struct{} type MasqueradeTarget struct{} type RejectTarget struct{} type ReturnTarget struct{} type CallTarget struct { Chain string } type GotoTarget struct { Chain string } type DNATTarget struct { ToDestination string `ipt:"--to-destination"` Random bool `ipt:"--random"` Persistent bool `ipt:"--persistent"` } type SNATTarget struct { ToSource string `ipt:"--to-source"` Random bool `ipt:"--random"` RandomFully bool `ipt:"--random-fully"` Persistent bool `ipt:"--persistent"` } func (s *SNATTarget) Do(_ context.Context, _ *model.Packet, _, _ string) (XTablesVerdict, error) { return XTablesVerdictAccept, nil } func (t *DNATTarget) Do(_ context.Context, _ *model.Packet, _, _ string) (XTablesVerdict, error) { return XTablesVerdictAccept, nil } type MarkTarget struct { } func (t *MarkTarget) Do(_ context.Context, _ *model.Packet, _, _ string) (XTablesVerdict, error) { return XTablesVerdictAccept, nil } type NoTrackTarget struct { } func (t *NoTrackTarget) Do(_ context.Context, _ *model.Packet, _, _ string) (XTablesVerdict, error) { return XTablesVerdictAccept, nil } type TPProxyTarget struct { } func (t *TPProxyTarget) Do(_ context.Context, _ *model.Packet, _, _ string) (XTablesVerdict, error) { return XTablesVerdictAccept, nil } type Matcher interface { Match(ctx context.Context, packet *model.Packet, iif, oif string) (bool, error) } type TCP struct { Option string Value uint16 } func (t *TCP) Match(_ context.Context, packet *model.Packet, _, _ string) (bool, error) { if packet.Protocol != model.TCP { return false, nil } switch t.Option { case "dport": return packet.Dport == t.Value, nil case "sport": return packet.Sport == t.Value, nil default: return false, ErrIptablesUnsupported{fmt.Sprintf("tcp match option %s", t.Option)} } } func (t *TCP) String() string { return fmt.Sprintf("--%s %d", t.Option, t.Value) } type IP struct { Option string Value string } func (ip *IP) Match(_ context.Context, packet *model.Packet, iif, oif string) (bool, error) { switch ip.Option { case "i": return ip.Value == iif, nil case "o": return ip.Value == oif, nil case "s": return utils.IPMatchPrefix(packet.Src, ip.Value) case "d": return utils.IPMatchPrefix(packet.Dst, ip.Value) case "p": return strings.EqualFold(string(packet.Protocol), ip.Value), nil default: return false, ErrIptablesUnsupported{fmt.Sprintf("match option %s", ip.Option)} } } func (ip *IP) String() string { return fmt.Sprintf("-%s %s", ip.Option, ip.Value) } type UDP struct { Option string Value uint16 } func (udp *UDP) Match(_ context.Context, packet *model.Packet, _, _ string) (bool, error) { if packet.Protocol != model.UDP { return false, nil } switch udp.Option { case "dport": return packet.Dport == udp.Value, nil case "sport": return packet.Sport == udp.Value, nil default: return false, ErrIptablesUnsupported{fmt.Sprintf("udp match option %s", udp.Option)} } } func (udp *UDP) String() string { return fmt.Sprintf("--%s %d", udp.Option, udp.Value) } type Conntrack struct { Option string Value string } func (conntrack *Conntrack) Match(_ context.Context, _ *model.Packet, _, _ string) (bool, error) { return true, nil } func (conntrack *Conntrack) String() string { return fmt.Sprintf("-m conntrack --%s %s", conntrack.Option, conntrack.Value) } type Set struct { Option string Value string } func (set *Set) parseSetArgument() (string, string, error) { arr := strings.Fields(set.Value) if len(arr) != 2 { return "", "", fmt.Errorf("invalid set argument format %s", set.Value) } return arr[0], arr[1], nil } func (set *Set) Match(ctx context.Context, packet *model.Packet, _, _ string) (bool, error) { setName, flags, err := set.parseSetArgument() if err != nil { return false, err } ipsetManager, ok := ctx.Value(ContextIPSetKey).(*IPSetManager) if !ok || ipsetManager == nil { return false, fmt.Errorf("cannot get ipset from context") } ipset := ipsetManager.GetIPSet(setName) if ipset == nil { return false, nil } ip := func(field string) net.IP { switch field { case "src": return packet.Src case "dst": return packet.Dst default: return net.IPv4(0, 0, 0, 0) } } port := func(field string) uint16 { switch field { case "src": return packet.Sport case "dst": return packet.Dport default: return 0 } } var key string switch ipset.Type { case "hash:net": addr := ip(flags) for m := range ipset.Members { if match, _ := utils.IPMatchPrefix(addr, m); match { return true, nil } } return false, nil case "hash:ip,port": fields := strings.Split(flags, ",") addr := ip(fields[0]) port := port(fields[1]) key = fmt.Sprintf("%s,%s:%d", addr, packet.Protocol, port) case "hash:ip,port,ip": fields := strings.Split(flags, ",") addr := ip(fields[0]) port := port(fields[1]) addr2 := ip(fields[2]) key = fmt.Sprintf("%s,%s:%d,%s", addr, packet.Protocol, port, addr2) case "bitmap:port": port := port(flags) key = fmt.Sprintf("%d", port) default: return false, ErrIptablesUnsupported{fmt.Sprintf("ipset type %s of %s", ipset.Type, setName)} } _, ok = ipset.Members[key] return ok, nil } func (set *Set) String() string { return fmt.Sprintf("-m set --match-set %s %s", set.Option, set.Value) } type Comment struct { Option string Value string } func (c *Comment) Match(_ context.Context, _ *model.Packet, _, _ string) (bool, error) { return true, nil } func (c *Comment) String() string { return fmt.Sprintf("-m comment --%s %s", c.Option, c.Value) } type MultiPort struct { Option string Value string } func (mp *MultiPort) matchPort(port uint16) (bool, error) { fields := strings.Split(mp.Value, ",") for _, f := range fields { if strings.Contains(f, ":") { portRange := strings.Split(f, ":") first, _ := strconv.Atoi(portRange[0]) last, _ := strconv.Atoi(portRange[1]) if port >= uint16(first) && port <= uint16(last) { return true, nil } } else { p, _ := strconv.Atoi(f) if port == uint16(p) { return true, nil } } } return false, nil } func (mp *MultiPort) Match(_ context.Context, packet *model.Packet, _, _ string) (bool, error) { switch mp.Option { case "dports": return mp.matchPort(packet.Dport) case "sports": return mp.matchPort(packet.Sport) case "ports": ok, err := mp.matchPort(packet.Sport) if err != nil { return false, err } if ok { return true, nil } ok, err = mp.matchPort(packet.Dport) if err != nil { return false, err } return ok, nil default: return false, ErrIptablesUnsupported{fmt.Sprintf("multiport option %s", mp.Option)} } } func (mp *MultiPort) String() string { return fmt.Sprintf("-m multiport --%s %s", mp.Option, mp.Value) } func parseMarkValue(s string) (uint32, uint32) { if strings.Contains(s, "/") { fields := strings.Split(s, "/") val, _ := strconv.ParseInt(fields[0], 0, 32) mask, _ := strconv.ParseInt(fields[1], 0, 32) return uint32(val), uint32(mask) } val, _ := strconv.ParseInt(s, 0, 32) return uint32(val), 0xffffffff } type Mark struct { Option string Value string } func (m *Mark) Match(_ context.Context, packet *model.Packet, _, _ string) (bool, error) { mark, mask := parseMarkValue(m.Value) return mark == packet.Mark&mask, nil } func (m *Mark) String() string { return fmt.Sprintf("-m mark --mark %s", m.Value) } const ( RtnUnspec = 0x0 RtnUnicast = 0x1 RtnLocal = 0x2 RtnBroadcast = 0x3 RtnAnycast = 0x4 RtnMulticast = 0x5 RtnBlackhole = 0x6 RtnUnreachable = 0x7 RtnProhibit = 0x8 RtnThrow = 0x9 RtnNat = 0xa RtnXresolve = 0xb ) type AddrType struct { Option string Value string } func (t *AddrType) Match(ctx context.Context, packet *model.Packet, iif, oif string) (bool, error) { var addr net.IP switch t.Option { case "src-type": addr = packet.Src case "dst-type": addr = packet.Dst case "limit-iface-in": return iif == t.Value, nil case "limit-iface-out": return oif == t.Value, nil } switch t.Value { case "UNSPEC": return addr.IsUnspecified(), nil case "MULTICAST": return addr.IsMulticast(), nil } router, ok := ctx.Value(ContextRouterKey).(Router) if !ok { return false, fmt.Errorf("cannot get router from context, router: %#+v", router) } var addrType int route, err := router.TableRoute(RtTableLocal, packet) if err != nil { if err == ErrNoRouteToHost { addrType = RtnUnicast } else { return false, err } } else { addrType = route.Type } switch t.Value { case "UNICAST": return addrType == RtnUnicast, nil case "LOCAL": return addrType == RtnLocal, nil case "BROADCAST": return addrType == RtnBroadcast, nil case "ANYCAST": return addrType == RtnAnycast, nil case "MULTICAST": return addrType == RtnMulticast, nil case "BLACKHOLE": return addrType == RtnBlackhole, nil case "UNREACHABLE": return addrType == RtnUnreachable, nil case "PROHIBIT": return addrType == RtnProhibit, nil } return false, nil } func (t *AddrType) String() string { return fmt.Sprintf("-m addrtype --%s %s", t.Option, t.Value) } type Statistic struct { Option string Value string } func (s *Statistic) Match(_ context.Context, _ *model.Packet, _, _ string) (bool, error) { return true, nil } func (s *Statistic) String() string { return fmt.Sprintf("-m statistic %s %s", s.Option, s.Value) } type Physdev struct { Option string Value string } func (physdev *Physdev) Match(_ context.Context, _ *model.Packet, _, _ string) (bool, error) { //FIXME 这里有问题 return true, nil } func (physdev *Physdev) String() string { return fmt.Sprintf("-m physdev %s %s", physdev.Option, physdev.Value) } type Socket struct { Option string Value string } func (s *Socket) Socket(_ context.Context, _ *model.Packet, _, _ string) (bool, error) { //FIXME 这里有问题 return true, nil } func (s *Socket) String() string { return fmt.Sprintf("-m socket %s %s", s.Option, s.Value) } type RPFilter struct { Option string Value string } func (rp *RPFilter) Match(ctx context.Context, packet *model.Packet, iif, oif string) (bool, error) { if rp.Option == "loose" { return true, nil } router, ok := ctx.Value(ContextRouterKey).(Router) if !ok { return false, fmt.Errorf("cannot get router from context, router: %#+v", router) } route, err := router.Route(packet, iif, oif) if err != nil { if err == ErrNoRouteToHost { return false, nil } return false, err } match := route.OifName == iif if rp.Option == "invert" { match = !match } return match, nil } func (rp *RPFilter) String() string { return fmt.Sprintf("-m rpfilter %s %s", rp.Option, rp.Value) } type match struct { matcher Matcher invert bool } func (m *match) match(ctx context.Context, packet *model.Packet, iif, oif string) (bool, error) { v, err := m.matcher.Match(ctx, packet, iif, oif) if err != nil { return v, err } if m.invert { return !v, nil } return v, nil } type rule struct { matches []*match target interface{} } func (r *rule) match(ctx context.Context, packet *model.Packet, iif, oif string) (bool, error) { for _, m := range r.matches { v, err := m.match(ctx, packet, iif, oif) if err != nil { return false, err } if !v { return false, nil } } return true, nil } func (r *rule) String() string { var matches []string for _, m := range r.matches { matches = append(matches, fmt.Sprintf("%v", m)) } return fmt.Sprintf("%s -j %v", strings.Join(matches, " "), r.target) } type iptChain struct { name string policy Verdict rules []*rule } type xTable struct { name string chains map[string]*iptChain } func (xt *xTable) tracePacket(ctx context.Context, hook NFHook, packet *model.Packet, iif, oif string) (verdict Verdict, trace Trace, err error) { type stackFrame struct { chain *iptChain pos int } chain := xt.chains[hook.String()] stack := utils.NewStack[*stackFrame](&stackFrame{ chain: chain, pos: 0, }) verdict = VerdictDrop var frame *stackFrame buildTrace := func(frame *stackFrame) { if frame == nil { return } if frame.pos >= len(frame.chain.rules) { return } trace = append(trace, fmt.Sprintf("%s %s %s", xt.name, chain.name, frame.chain.rules[frame.pos])) } defer func() { if verdict != VerdictAccept { buildTrace(frame) for !stack.Empty() { buildTrace(stack.Pop()) } } }() for !stack.Empty() { frame = stack.Pop() chain: for pos := frame.pos; pos < len(frame.chain.rules); { rule := frame.chain.rules[pos] pos++ v, err := rule.match(ctx, packet, iif, oif) if err != nil { return VerdictDrop, trace, &IPTablesRuleError{Rule: rule.String(), Message: err.Error()} } if v { trace = append(trace, fmt.Sprintf("%s %s %s", xt.name, chain.name, rule)) switch target := rule.target.(type) { case *AcceptTarget: return VerdictAccept, trace, nil case *DropTarget, RejectTarget: return VerdictDrop, trace, nil case *NopTarget: continue case *ReturnTarget: break chain case *CallTarget: stack.Push(&stackFrame{chain, pos}) targetChain := xt.chains[target.Chain] stack.Push(&stackFrame{targetChain, 0}) break chain case *GotoTarget: targetChain := xt.chains[target.Chain] stack.Push(&stackFrame{targetChain, 0}) break chain case ExtensionTarget: verdict, err := target.Do(ctx, packet, iif, oif) if err != nil { return VerdictDrop, trace, err } switch verdict { case XTablesVerdictAccept: return VerdictAccept, trace, nil case XTablesVerdictDrop, XTablesVerdictReject: return VerdictDrop, trace, nil case XTablesVerdictContinue: continue case XTablesVerdictReturn: break chain default: panic(fmt.Sprintf("unknown verdict %v", verdict)) } default: return VerdictDrop, trace, &IPTablesRuleError{Rule: rule.String(), Message: fmt.Sprintf("unsupported target (%T)%v", target, target)} } } } } return chain.policy, trace, nil } type IPTables interface { TracePacket(ctx context.Context, hook NFHook, table string, packet *model.Packet, iif, oif string) (Verdict, Trace, error) Empty() error DefaultAccept() error } func createEmptyTable(name string, chains ...string) *xTable { iptChains := make(map[string]*iptChain) for _, n := range chains { iptChains[n] = &iptChain{name: n, policy: VerdictAccept} } return &xTable{ name: name, chains: iptChains, } } type emptyIPTables struct { xTables map[string]*xTable } func (ipt *emptyIPTables) TracePacket(_ context.Context, _ NFHook, _ string, _ *model.Packet, _, _ string) (Verdict, Trace, error) { return VerdictAccept, nil, nil } func (ipt *emptyIPTables) Empty() error { return nil } func (ipt *emptyIPTables) DefaultAccept() error { return nil } func createEmptyIPTables() *emptyIPTables { return &emptyIPTables{ xTables: map[string]*xTable{ "raw": createEmptyTable("raw", "PREROUTING", "OUTPUT"), "mangle": createEmptyTable("mangle", "PREROUTING", "INPUT", "FORWARD", "OUTPUT", "POSTROUTING"), "nat": createEmptyTable("nat", "PREROUTING", "INPUT", "OUTPUT", "POSTROUTING"), "filter": createEmptyTable("filter", "INPUT", "FORWARD", "OUTPUT"), }, } } type defaultIPTables struct { xTables map[string]*xTable } type Trace []string func (t Trace) String() string { var indent []string for _, s := range t { indent = append(indent, fmt.Sprintf(" %s", s)) } return strings.Join(indent, "\n") } func (ipt *defaultIPTables) TracePacket(ctx context.Context, hook NFHook, table string, packet *model.Packet, iif, oif string) (Verdict, Trace, error) { var trace Trace xtable := ipt.xTables[table] if xtable == nil { return VerdictAccept, trace, nil } verdict, trace, err := xtable.tracePacket(ctx, hook, packet, iif, oif) return verdict, trace, err } func (ipt *defaultIPTables) Empty() error { for _, tbl := range ipt.xTables { for _, chain := range tbl.chains { if len(chain.rules) > 0 { return fmt.Errorf("table %s chain %s is not empty", tbl.name, chain.name) } } } return nil } func (ipt *defaultIPTables) DefaultAccept() error { for _, tbl := range ipt.xTables { for _, chain := range tbl.chains { if chain.policy != VerdictAccept { return fmt.Errorf("table %s chain %s default policy is not ACCEPT", tbl.name, chain.name) } } } return nil } var ModuleTypes = map[string]reflect.Type{ "tcp": reflect.TypeOf(TCP{}), "udp": reflect.TypeOf(UDP{}), "match": reflect.TypeOf(IP{}), "set": reflect.TypeOf(Set{}), "comment": reflect.TypeOf(Comment{}), "multiport": reflect.TypeOf(MultiPort{}), "mark": reflect.TypeOf(Mark{}), "statistic": reflect.TypeOf(Statistic{}), "conntrack": reflect.TypeOf(Conntrack{}), "addrtype": reflect.TypeOf(AddrType{}), "rpfilter": reflect.TypeOf(RPFilter{}), } var ActionTypes = map[string]reflect.Type{ "DNAT": reflect.TypeOf(DNATTarget{}), "SNAT": reflect.TypeOf(SNATTarget{}), "MASQUERADE": reflect.TypeOf(MasqueradeTarget{}), "MARK": reflect.TypeOf(MarkTarget{}), "ACCEPT": reflect.TypeOf(AcceptTarget{}), "DROP": reflect.TypeOf(DropTarget{}), "RETURN": reflect.TypeOf(ReturnTarget{}), "REJECT": reflect.TypeOf(RejectTarget{}), "NOTRACK": reflect.TypeOf(NoTrackTarget{}), "TPROXY": reflect.TypeOf(TPProxyTarget{}), } func setField(s reflect.Value, fieldName string, value string) error { t := s.Type() if t.Kind() != reflect.Struct { return fmt.Errorf("%s is not a struct", s) } _, ok := t.FieldByName(fieldName) if !ok { return fmt.Errorf("%s has no field %s", s, fieldName) } field := s.FieldByName(fieldName) switch field.Type().Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: i, err := strconv.ParseInt(value, 10, 64) if err != nil { return errors.Wrap(err, fmt.Sprintf("error parse %s to int", value)) } field.SetInt(i) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: i, err := strconv.ParseUint(value, 10, 64) if err != nil { return errors.Wrap(err, fmt.Sprintf("error parse %s to uint", value)) } field.SetUint(i) case reflect.Float32: f, err := strconv.ParseFloat(value, 32) if err != nil { return errors.Wrap(err, fmt.Sprintf("error parse %s to float32", value)) } field.SetFloat(f) case reflect.Float64: f, err := strconv.ParseFloat(value, 64) if err != nil { return errors.Wrap(err, fmt.Sprintf("error parse %s to float64", value)) } field.SetFloat(f) case reflect.String: field.SetString(value) default: return fmt.Errorf("unsupported field type %s", field.Type().Kind().String()) } return nil } func createMatcher(moduleType reflect.Type, matchField string, value string) (Matcher, error) { module := reflect.New(moduleType) if err := setField(module.Elem(), "Option", matchField); err != nil { return nil, err } if err := setField(module.Elem(), "Value", value); err != nil { return nil, err } return module.Interface().(Matcher), nil } func createTarget(actionType reflect.Type, params map[string]string) (Target, error) { action := reflect.New(actionType) for i := 0; i < actionType.NumField(); i++ { field := actionType.Field(i) tag, ok := field.Tag.Lookup("ipt") if !ok { continue } val, ok := params[tag] if !ok { continue } if err := setField(action.Elem(), field.Name, val); err != nil { return nil, err } } return action.Interface(), nil } func parseOneRule(xmlRule *etree.Element) (*rule, error) { var conditions []*match xmlConditions := xmlRule.FindElement("conditions") if xmlConditions != nil { for _, xmlModule := range xmlConditions.ChildElements() { moduleKey := xmlModule.Tag moduleType, ok := ModuleTypes[moduleKey] if !ok { return nil, fmt.Errorf("unspported match module %s", moduleKey) } for _, xmlExpr := range xmlModule.ChildElements() { key := xmlExpr.Tag value := xmlExpr.Text() invertAttr := xmlExpr.SelectAttr("invert") invert := invertAttr != nil && invertAttr.Value == "true" matcher, err := createMatcher(moduleType, key, value) if err != nil { panic(err) } conditions = append(conditions, &match{matcher: matcher, invert: invert}) } } } var target Target xmlAction := xmlRule.FindElement("actions") if xmlAction == nil || len(xmlAction.ChildElements()) == 0 { target = &NopTarget{} return &rule{matches: conditions, target: target}, nil } action := xmlAction.ChildElements()[0] if action.Tag == "call" { target = &CallTarget{Chain: action.ChildElements()[0].Tag} } else if action.Tag == "goto" { target = &CallTarget{Chain: action.ChildElements()[0].Tag} } else { actionType, ok := ActionTypes[action.Tag] if !ok { return nil, fmt.Errorf("unsupported action %s", action.Tag) } params := make(map[string]string) for _, child := range action.ChildElements() { params[child.Tag] = child.Text() } var err error target, err = createTarget(actionType, params) if err != nil { return nil, errors.Wrapf(err, "error create target %s, err: %v", action.Tag, err) } } return &rule{matches: conditions, target: target}, nil } func parseOneTable(xmlTable *etree.Element) (*xTable, error) { tableName := xmlTable.SelectAttr("name").Value chains := make(map[string]*iptChain) for _, xmlChain := range xmlTable.ChildElements() { policy := VerdictAccept name := xmlChain.SelectAttr("name").Value policyAttr := xmlChain.SelectAttr("policy") if policyAttr != nil && policyAttr.Value == "DROP" { policy = VerdictDrop } var rules []*rule for _, xmlRule := range xmlChain.ChildElements() { rule, err := parseOneRule(xmlRule) if err != nil { return nil, err } rules = append(rules, rule) } chains[name] = &iptChain{ name: name, policy: policy, rules: rules, } } return &xTable{name: tableName, chains: chains}, nil } func ParseIPTables(dump string) (ipt IPTables) { defer func() { if r := recover(); r != nil { log.Errorf("error parse iptables, err: %v", r) ipt = createEmptyIPTables() } }() if dump == "" { return createEmptyIPTables() } doc := etree.NewDocument() err := doc.ReadFromString(dump) if err != nil { panic(err) } tables := make(map[string]*xTable) for _, xmlTable := range doc.Root().ChildElements() { table, err := parseOneTable(xmlTable) if err != nil { panic(err) } tables[table.name] = table } return &defaultIPTables{xTables: tables} }