| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- // TODO(#8502): add support for more architectures
- //go:build linux && (arm64 || amd64)
- package linuxfw
- import (
- "cmp"
- "encoding/binary"
- "fmt"
- "sort"
- "strings"
- "github.com/google/nftables"
- "github.com/google/nftables/expr"
- "github.com/google/nftables/xt"
- "golang.org/x/sys/unix"
- "tailscale.com/types/logger"
- )
- // DebugNetfilter prints debug information about netfilter rules to the
- // provided log function.
- func DebugNetfilter(logf logger.Logf) error {
- conn, err := nftables.New()
- if err != nil {
- return err
- }
- chains, err := conn.ListChains()
- if err != nil {
- return fmt.Errorf("cannot list chains: %w", err)
- }
- if len(chains) == 0 {
- logf("netfilter: no chains")
- return nil
- }
- for _, chain := range chains {
- logf("netfilter: table=%s chain=%s", chain.Table.Name, chain.Name)
- rules, err := conn.GetRules(chain.Table, chain)
- if err != nil {
- continue
- }
- sort.Slice(rules, func(i, j int) bool {
- return rules[i].Position < rules[j].Position
- })
- for i, rule := range rules {
- logf("netfilter: rule[%d]: pos=%d flags=%d", i, rule.Position, rule.Flags)
- for _, ex := range rule.Exprs {
- switch v := ex.(type) {
- case *expr.Meta:
- key := cmp.Or(metaKeyNames[v.Key], "UNKNOWN")
- logf("netfilter: Meta: key=%s source_register=%v register=%d", key, v.SourceRegister, v.Register)
- case *expr.Cmp:
- op := cmp.Or(cmpOpNames[v.Op], "UNKNOWN")
- logf("netfilter: Cmp: op=%s register=%d data=%s", op, v.Register, formatMaybePrintable(v.Data))
- case *expr.Counter:
- // don't print
- case *expr.Verdict:
- kind := cmp.Or(verdictNames[v.Kind], "UNKNOWN")
- logf("netfilter: Verdict: kind=%s data=%s", kind, v.Chain)
- case *expr.Target:
- logf("netfilter: Target: name=%s info=%s", v.Name, printTargetInfo(v.Name, v.Info))
- case *expr.Match:
- logf("netfilter: Match: name=%s info=%+v", v.Name, printMatchInfo(v.Name, v.Info))
- case *expr.Payload:
- logf("netfilter: Payload: op=%s src=%d dst=%d base=%s offset=%d len=%d",
- payloadOperationTypeNames[v.OperationType],
- v.SourceRegister, v.DestRegister,
- payloadBaseNames[v.Base],
- v.Offset, v.Len)
- // TODO(andrew): csum
- case *expr.Bitwise:
- var xor string
- for _, b := range v.Xor {
- if b != 0 {
- xor = fmt.Sprintf(" xor=%v", v.Xor)
- break
- }
- }
- logf("netfilter: Bitwise: src=%d dst=%d len=%d mask=%v%s",
- v.SourceRegister, v.DestRegister, v.Len, v.Mask, xor)
- default:
- logf("netfilter: unknown %T: %+v", v, v)
- }
- }
- }
- }
- return nil
- }
- func init() {
- hookDetectNetfilter.Set(detectNetfilter)
- }
- // detectNetfilter returns the number of nftables rules present in the system.
- func detectNetfilter() (int, error) {
- // Frist try creating a dummy postrouting chain. Emperically, we have
- // noticed that on some devices there is partial nftables support and the
- // kernel rejects some chains that are valid on other devices. This is a
- // workaround to detect that case.
- //
- // This specifically allows us to run in on GKE nodes using COS images which
- // have partial nftables support (as of 2023-10-18). When we try to create a
- // dummy postrouting chain, we get an error like:
- // add chain: conn.Receive: netlink receive: no such file or directory
- nft, err := newNfTablesRunner(logger.Discard)
- if err != nil {
- return 0, FWModeNotSupportedError{
- Mode: FirewallModeNfTables,
- Err: fmt.Errorf("cannot create nftables runner: %w", err),
- }
- }
- if err := nft.createDummyPostroutingChains(); err != nil {
- return 0, FWModeNotSupportedError{
- Mode: FirewallModeNfTables,
- Err: err,
- }
- }
- conn, err := nftables.New()
- if err != nil {
- return 0, FWModeNotSupportedError{
- Mode: FirewallModeNfTables,
- Err: err,
- }
- }
- chains, err := conn.ListChains()
- if err != nil {
- return 0, FWModeNotSupportedError{
- Mode: FirewallModeNfTables,
- Err: fmt.Errorf("cannot list chains: %w", err),
- }
- }
- var validRules int
- for _, chain := range chains {
- rules, err := conn.GetRules(chain.Table, chain)
- if err != nil {
- continue
- }
- validRules += len(rules)
- }
- return validRules, nil
- }
- func printMatchInfo(name string, info xt.InfoAny) string {
- var sb strings.Builder
- sb.WriteString(`{`)
- var handled bool = true
- switch v := info.(type) {
- // TODO(andrew): we should support these common types
- //case *xt.ConntrackMtinfo3:
- //case *xt.ConntrackMtinfo2:
- case *xt.Tcp:
- fmt.Fprintf(&sb, "Src:%s Dst:%s", formatPortRange(v.SrcPorts), formatPortRange(v.DstPorts))
- if v.Option != 0 {
- fmt.Fprintf(&sb, " Option:%d", v.Option)
- }
- if v.FlagsMask != 0 {
- fmt.Fprintf(&sb, " FlagsMask:%d", v.FlagsMask)
- }
- if v.FlagsCmp != 0 {
- fmt.Fprintf(&sb, " FlagsCmp:%d", v.FlagsCmp)
- }
- if v.InvFlags != 0 {
- fmt.Fprintf(&sb, " InvFlags:%d", v.InvFlags)
- }
- case *xt.Udp:
- fmt.Fprintf(&sb, "Src:%s Dst:%s", formatPortRange(v.SrcPorts), formatPortRange(v.DstPorts))
- if v.InvFlags != 0 {
- fmt.Fprintf(&sb, " InvFlags:%d", v.InvFlags)
- }
- case *xt.AddrType:
- var sprefix, dprefix string
- if v.InvertSource {
- sprefix = "!"
- }
- if v.InvertDest {
- dprefix = "!"
- }
- // TODO(andrew): translate source/dest
- fmt.Fprintf(&sb, "Source:%s%d Dest:%s%d", sprefix, v.Source, dprefix, v.Dest)
- case *xt.AddrTypeV1:
- // TODO(andrew): translate source/dest
- fmt.Fprintf(&sb, "Source:%d Dest:%d", v.Source, v.Dest)
- var flags []string
- for flag, name := range addrTypeFlagNames {
- if v.Flags&flag != 0 {
- flags = append(flags, name)
- }
- }
- if len(flags) > 0 {
- sort.Strings(flags)
- fmt.Fprintf(&sb, "Flags:%s", strings.Join(flags, ","))
- }
- default:
- handled = false
- }
- if handled {
- sb.WriteString(`}`)
- return sb.String()
- }
- unknown, ok := info.(*xt.Unknown)
- if !ok {
- return fmt.Sprintf("(%T)%+v", info, info)
- }
- data := []byte(*unknown)
- // Things where upstream has no type
- handled = true
- switch name {
- case "pkttype":
- if len(data) != 8 {
- handled = false
- break
- }
- pkttype := int(binary.NativeEndian.Uint32(data[0:4]))
- invert := int(binary.NativeEndian.Uint32(data[4:8]))
- var invertPrefix string
- if invert != 0 {
- invertPrefix = "!"
- }
- pkttypeName := packetTypeNames[pkttype]
- if pkttypeName != "" {
- fmt.Fprintf(&sb, "PktType:%s%s", invertPrefix, pkttypeName)
- } else {
- fmt.Fprintf(&sb, "PktType:%s%d", invertPrefix, pkttype)
- }
- default:
- handled = true
- }
- if !handled {
- return fmt.Sprintf("(%T)%+v", info, info)
- }
- sb.WriteString(`}`)
- return sb.String()
- }
- func printTargetInfo(name string, info xt.InfoAny) string {
- var sb strings.Builder
- sb.WriteString(`{`)
- unknown, ok := info.(*xt.Unknown)
- if !ok {
- return fmt.Sprintf("(%T)%+v", info, info)
- }
- data := []byte(*unknown)
- // Things where upstream has no type
- switch name {
- case "LOG":
- if len(data) != 32 {
- fmt.Fprintf(&sb, `Error:"bad size; want 32, got %d"`, len(data))
- break
- }
- level := data[0]
- logflags := data[1]
- prefix := unix.ByteSliceToString(data[2:])
- fmt.Fprintf(&sb, "Level:%d LogFlags:%d Prefix:%q", level, logflags, prefix)
- default:
- return fmt.Sprintf("(%T)%+v", info, info)
- }
- sb.WriteString(`}`)
- return sb.String()
- }
|