├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── iface_cache.go ├── iptables.go ├── main.go ├── packet.go └── printer.go ├── go.mod ├── go.sum └── modprobe.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !go.mod 4 | !go.sum 5 | !cmd/ 6 | !modprobe.sh 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.7-bookworm AS build 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y --no-install-recommends bison flex && \ 5 | rm -r /var/lib/apt/* 6 | 7 | WORKDIR /build 8 | 9 | ENV LIBPCAP_VERSION=1.10.5 10 | RUN wget http://www.tcpdump.org/release/libpcap-${LIBPCAP_VERSION}.tar.gz && \ 11 | tar xvf libpcap-${LIBPCAP_VERSION}.tar.gz && \ 12 | cd libpcap-${LIBPCAP_VERSION} && \ 13 | ./configure && \ 14 | make 15 | 16 | COPY cmd/* /build 17 | 18 | ENV LD_LIBRARY_PATH="-L/build/libpcap-${LIBPCAP_VERSION}" \ 19 | CGO_LDFLAGS="-L/build/libpcap-${LIBPCAP_VERSION}" \ 20 | CGO_CPPFLAGS="-I/build/libpcap-${LIBPCAP_VERSION}" 21 | ARG TARGETOS TARGETARCH 22 | RUN --mount=type=bind,source=go.mod,target=go.mod \ 23 | --mount=type=bind,source=go.sum,target=go.sum \ 24 | GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-linkmode 'external' -extldflags '-static' -s -w" -o bin/iptables-tracer . 25 | 26 | #################### 27 | 28 | FROM alpine:3.20 AS final 29 | WORKDIR /bin 30 | COPY --from=build /build/bin/iptables-tracer /bin/ 31 | 32 | RUN < sysctl net.netfilter.nf_log.2=nfnetlink_log \ 59 | net.netfilter.nf_log.10=nfnetlink_log 60 | 61 | # Then, attach to a dind container: 62 | $ docker run --rm -it --net=container: --privileged albinkerouanton006/iptables-tracer -family ipv6 -filter 'tcp port 8000' 63 | ``` 64 | 65 | If you see that `iptables-tracer` doesn't start correctly (it's spinning forever), or if you see an error message about procfs not being writable, you should try to add `-skip-modprobe`. In that case, you'll need to make sure the above sysctls are properly set and eventually load the kernel module `nfnetlink_log` manually. 66 | 67 | ### One-liners 68 | 69 | ```console 70 | # Trace only IPv6 Neighbor Solicitation & Neighbor Advertisment 71 | $ sudo iptables-tracer -family ipv6 -iface=br-21502e5b2c6c -filter='icmp6 and (ip6[40] == 135 || ip6[40] == 136)' 72 | 73 | # Execute iptables-tracer into a specific container 74 | $ sudo iptables-tracer -netns="$(docker inspect --format='{{ .NetworkSettings.SandboxKey }}' tender_merkle)" -family ipv6 75 | 76 | # Trace connections to port 8080 going through iface eth0 77 | $ sudo iptables-tracer -iface eth0 -filter 'port 8080' 78 | 79 | # Trace ICMP packets 80 | $ sudo iptables-tracer -filter icmp 81 | ``` 82 | 83 | ### Example 84 | 85 | ```console 86 | $ sudo iptables-tracer -netns="$(docker inspect --format='{{ .NetworkSettings.SandboxKey }}' tender_merkle)" -family ipv4 -filter="tcp port 1242" 87 | INFO[0000] Tracer switched to netns /var/run/docker/netns/e859696c843d. Forking.. 88 | INFO[0000] Waiting for trace events... 89 | IN=hairpin-8-2 OUT= SRC=172.22.0.2 DST=172.17.0.3 LEN=60 TOS=00 TTL=64 ID=3588 PROTO=TCP SPT=53920 DPT=1242 FLAGS=SYN SEQ=3083952015 CSUM=585b 90 | raw PREROUTING NFMARK=0x0 91 | DEFAULT POLICY 92 | => ACCEPT 93 | nat PREROUTING NFMARK=0x0 94 | MATCH RULE (#1): -m addrtype --dst-type LOCAL -j DOCKER 95 | => DOCKER 96 | nat DOCKER NFMARK=0x0 97 | MATCH RULE (#1): -p tcp -m tcp --dport 1242 -j DNAT --to-destination 172.23.0.2:1242 98 | => DNAT: --to-destination 172.23.0.2:1242 99 | filter FORWARD NFMARK=0x0 IN=hairpin-8-2 OUT=hairpin-8-1 (changed by last rule) 100 | MATCH RULE (#1): -j DOCKER-USER 101 | => DOCKER-USER 102 | filter DOCKER-USER NFMARK=0x0 103 | => RETURN 104 | filter FORWARD NFMARK=0x0 105 | MATCH RULE (#2): -j DOCKER-ISOLATION-STAGE-1 106 | => DOCKER-ISOLATION-STAGE-1 107 | filter DOCKER-ISOLATION-STAGE-1 NFMARK=0x0 108 | MATCH RULE (#2): -i hairpin-8-2 ! -o hairpin-8-2 -j DOCKER-ISOLATION-STAGE-2 109 | => DOCKER-ISOLATION-STAGE-2 110 | filter DOCKER-ISOLATION-STAGE-2 NFMARK=0x0 111 | MATCH RULE (#1): -o hairpin-8-1 -j DROP 112 | => DROP 113 | ``` 114 | -------------------------------------------------------------------------------- /cmd/iface_cache.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/vishvananda/netlink" 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | type IfaceCache struct { 12 | mu sync.RWMutex 13 | ifaces map[int]netlink.Link 14 | } 15 | 16 | func (cache *IfaceCache) Watch(ctx context.Context) error { 17 | ch := make(chan netlink.LinkUpdate) 18 | doneCh := make(chan struct{}) 19 | 20 | if err := netlink.LinkSubscribeWithOptions(ch, doneCh, netlink.LinkSubscribeOptions{ 21 | ListExisting: true, 22 | }); err != nil { 23 | return err 24 | } 25 | 26 | cache.ifaces = map[int]netlink.Link{} 27 | 28 | for { 29 | select { 30 | case evt := <-ch: 31 | cache.mu.Lock() 32 | if evt.Header.Type == unix.RTM_NEWLINK { 33 | cache.ifaces[int(evt.Index)] = evt.Link 34 | } else if evt.Header.Type == unix.RTM_DELLINK { 35 | delete(cache.ifaces, int(evt.Index)) 36 | } 37 | cache.mu.Unlock() 38 | case <-ctx.Done(): 39 | close(doneCh) 40 | return nil 41 | } 42 | } 43 | } 44 | 45 | // IndexToName is the equivalent of nlif_index2name from libnfnetlink 46 | func (cache *IfaceCache) IndexToName(id uint32) (string, bool) { 47 | if id == 0 { 48 | return "", true 49 | } 50 | 51 | cache.mu.RLock() 52 | defer cache.mu.RUnlock() 53 | 54 | link, ok := cache.ifaces[int(id)] 55 | if !ok { 56 | return "", false 57 | } 58 | 59 | return link.Attrs().Name, true 60 | } 61 | -------------------------------------------------------------------------------- /cmd/iptables.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os/exec" 10 | "strings" 11 | 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type Direction int 16 | 17 | const ( 18 | Input Direction = iota 19 | Output 20 | ) 21 | 22 | type RuleReverter func() error 23 | 24 | func applyReverters(reverters []RuleReverter) { 25 | for _, reverter := range reverters { 26 | if err := reverter(); err != nil { 27 | logrus.Warnf("Rule reverter failed: %s", err) 28 | } 29 | } 30 | } 31 | 32 | func setupIptRules(family IPFamily, flushRaw bool, ifaces []string, filter Filter) ([]RuleReverter, error) { 33 | reverters := make([]RuleReverter, 0, 2*len(ifaces)) 34 | 35 | if flushRaw { 36 | logrus.Debugf("Flushing RAW/PREROUTING and RAW/OUTPUT chains") 37 | 38 | if err := execIpt(family, []string{"-t", "raw", "-F", "PREROUTING"}); err != nil { 39 | return reverters, err 40 | } 41 | if err := execIpt(family, []string{"-t", "raw", "-F", "OUTPUT"}); err != nil { 42 | return reverters, err 43 | } 44 | } 45 | 46 | for _, iface := range ifaces { 47 | for _, dir := range []Direction{Input, Output} { 48 | reverter, err := addIptRule(family, dir, iface, filter) 49 | if err != nil { 50 | return reverters, err 51 | } 52 | reverters = append(reverters, reverter) 53 | } 54 | } 55 | 56 | return reverters, nil 57 | } 58 | 59 | func addIptRule(family IPFamily, dir Direction, iface string, filter Filter) (RuleReverter, error) { 60 | var iptChain string 61 | var ifaceFlag string 62 | 63 | if dir == Input { 64 | iptChain = "PREROUTING" 65 | ifaceFlag = "-i" 66 | } else { 67 | iptChain = "OUTPUT" 68 | ifaceFlag = "-o" 69 | } 70 | 71 | iptArgs := []string{"-t", "raw", "-A", iptChain} 72 | 73 | if len(iface) > 0 { 74 | iptArgs = append(iptArgs, ifaceFlag, iface) 75 | } 76 | 77 | if len(filter.Bytecode) > 0 { 78 | iptArgs = append( 79 | iptArgs, 80 | "-m", "bpf", "--bytecode", filter.Bytecode, 81 | "-m", "comment", "--comment", fmt.Sprintf("bpf: \"%s\"", filter.Raw)) 82 | } 83 | 84 | iptArgs = append(iptArgs, "-j", "TRACE") 85 | 86 | if err := execIpt(family, iptArgs); err != nil { 87 | return func() error { return nil }, err 88 | } 89 | 90 | return func() error { 91 | iptArgs[2] = "-D" 92 | return execIpt(family, iptArgs) 93 | }, nil 94 | } 95 | 96 | func execIpt(family IPFamily, args []string) error { 97 | bin := "iptables" 98 | if family == AfInet6 { 99 | bin = "ip6tables" 100 | } 101 | 102 | logrus.Debugf("Executing: %s %s", bin, strings.Join(args, " ")) 103 | 104 | var stderr bytes.Buffer 105 | cmd := exec.Command(bin, args...) 106 | cmd.Stderr = &stderr 107 | 108 | if err := cmd.Run(); err != nil { 109 | if _, ok := err.(*exec.ExitError); ok { 110 | return fmt.Errorf("%s: %s", err, stderr.String()) 111 | } 112 | return err 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func execIptSave(family IPFamily, table string) (io.Reader, error) { 119 | bin := "iptables-save" 120 | if family == AfInet6 { 121 | bin = "ip6tables-save" 122 | } 123 | 124 | logrus.Debugf("Executing: %s -t %s", bin, table) 125 | 126 | var stdout bytes.Buffer 127 | var stderr bytes.Buffer 128 | 129 | cmd := exec.Command(bin, "-t", table) 130 | cmd.Stdout = &stdout 131 | cmd.Stderr = &stderr 132 | 133 | if err := cmd.Run(); err != nil { 134 | if _, ok := err.(*exec.ExitError); ok { 135 | return nil, fmt.Errorf("%s: %s", err, stderr.String()) 136 | } 137 | } 138 | 139 | return &stdout, nil 140 | } 141 | 142 | type IptTable map[string]IptChain 143 | 144 | type IptChain struct { 145 | Policy string 146 | Rules []string 147 | } 148 | 149 | func parseIptSave(r io.Reader) (IptTable, error) { 150 | table := make(IptTable, 0) 151 | 152 | scanner := bufio.NewScanner(r) 153 | for scanner.Scan() { 154 | line := scanner.Text() 155 | if len(line) == 0 { 156 | return IptTable{}, errors.New("unexpected new line found in iptables-save output") 157 | } 158 | 159 | // Parse chain header -- :INPUT ACCEPT [658229:723231403] 160 | if line[0] == ':' { 161 | parts := strings.SplitN(line[1:], " ", 3) 162 | chainName := parts[0] 163 | policy := parts[1] 164 | 165 | if policy == "-" { 166 | policy = "RETURN" 167 | } 168 | 169 | table[chainName] = IptChain{ 170 | Policy: policy, 171 | Rules: make([]string, 0), 172 | } 173 | } 174 | 175 | // Parse rule -- -A LIBVIRT_FWO -i virbr1 -j REJECT --reject-with icmp6-port-unreachable 176 | if line[0] == '-' { 177 | parts := strings.SplitN(line, " ", 3) 178 | 179 | chain := table[parts[1]] 180 | chain.Rules = append(chain.Rules, parts[2]) 181 | table[parts[1]] = chain 182 | } 183 | } 184 | 185 | if err := scanner.Err(); err != nil { 186 | return IptTable{}, err 187 | } 188 | 189 | return table, nil 190 | } 191 | 192 | func GetIptChain(family IPFamily, tableName, chainName string) (IptChain, error) { 193 | saveOutput, err := execIptSave(family, tableName) 194 | if err != nil { 195 | return IptChain{}, err 196 | } 197 | 198 | table, err := parseIptSave(saveOutput) 199 | if err != nil { 200 | return IptChain{}, err 201 | } 202 | 203 | chain, ok := table[chainName] 204 | if !ok { 205 | return IptChain{}, fmt.Errorf("chain %s not found in table %s", chainName, tableName) 206 | } 207 | 208 | return chain, nil 209 | } 210 | 211 | func parseIptRuleTarget(raw string) (string, string) { 212 | i := strings.LastIndex(raw, "-j ") 213 | if i == -1 { 214 | logrus.Warnf("Could not find jump flag -j in: %s", raw) 215 | return "", "" 216 | } 217 | 218 | parts := strings.SplitN(raw[i+1:], " ", 3) 219 | 220 | var jumpFlags string 221 | if len(parts) == 3 { 222 | jumpFlags = parts[2] 223 | } 224 | return parts[1], jumpFlags 225 | } 226 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "os/signal" 11 | "runtime" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/florianl/go-nflog/v2" 16 | "github.com/google/gopacket/pcap" 17 | "github.com/mdlayher/netlink" 18 | "github.com/sirupsen/logrus" 19 | "github.com/vishvananda/netns" 20 | ) 21 | 22 | type IPFamily string 23 | 24 | const ( 25 | AfInet4 = IPFamily("ipv4") 26 | AfInet6 = IPFamily("ipv6") 27 | ) 28 | 29 | type Filter struct { 30 | Bytecode string 31 | Raw string 32 | } 33 | 34 | func NewFilter(rawFilter string) (Filter, error) { 35 | instrs, err := pcap.CompileBPFFilter(12, -1, rawFilter) 36 | if err != nil { 37 | return Filter{}, fmt.Errorf("invalid filter %q: %w", rawFilter, err) 38 | } 39 | 40 | if len(instrs) > 64 { 41 | return Filter{}, fmt.Errorf("compiled BPF filter can't be larger than 64 instructions") 42 | } 43 | 44 | var b strings.Builder 45 | 46 | b.WriteString(fmt.Sprintf("%d", len(instrs))) 47 | for _, instr := range instrs { 48 | b.WriteString(fmt.Sprintf(",%d %d %d %d", instr.Code, instr.Jt, instr.Jf, instr.K)) 49 | } 50 | 51 | return Filter{ 52 | Bytecode: b.String(), 53 | Raw: rawFilter, 54 | }, nil 55 | } 56 | 57 | func parseFlags() Config { 58 | if flagLogLvl != "" { 59 | if logLvl, err := logrus.ParseLevel(flagLogLvl); err != nil { 60 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 61 | } else { 62 | logrus.SetLevel(logLvl) 63 | } 64 | } 65 | 66 | if flagFamily != string(AfInet4) && flagFamily != string(AfInet6) { 67 | fmt.Fprintf(os.Stderr, "ERROR: -family should be either ipv4 or ipv6. Got: %s\n", flagFamily) 68 | os.Exit(1) 69 | } 70 | 71 | var filter Filter 72 | if flagFilter != "" { 73 | var err error 74 | if filter, err = NewFilter(flagFilter); err != nil { 75 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) 76 | os.Exit(1) 77 | } 78 | } 79 | 80 | var netnsPath string 81 | if flagNetns != "" { 82 | if _, err := os.Stat(flagNetns); err != nil { 83 | if os.IsNotExist(err) { 84 | fmt.Fprintf(os.Stderr, "ERROR: %s doesn't exist\n", flagNetns) 85 | } else { 86 | fmt.Fprintf(os.Stderr, "ERROR: could not stat netns handle %s: %s\n", flagNetns, err) 87 | } 88 | os.Exit(1) 89 | } else { 90 | netnsPath = flagNetns 91 | } 92 | } 93 | 94 | ifaces := []string{""} 95 | if flagIface != "" { 96 | ifaces = strings.Split(flagIface, ",") 97 | } 98 | 99 | chains := make([][]string, 0) 100 | if flagFilterChain != "" { 101 | for _, part := range strings.Split(flagFilterChain, ",") { 102 | subparts := strings.Split(part, "/") 103 | if len(subparts) != 2 { 104 | fmt.Fprintf(os.Stderr, "ERROR: chain filter %q is badly formated. Format: /.", part) 105 | os.Exit(1) 106 | } 107 | chains = append(chains, subparts) 108 | } 109 | } 110 | 111 | return Config{ 112 | IPFamily: IPFamily(flagFamily), 113 | Filter: filter, 114 | FilterIfaces: ifaces, 115 | FilterChains: chains, 116 | NetnsPath: netnsPath, 117 | FlushRaw: flagFlushRaw, 118 | PrintRaw: flagPrintRaw, 119 | PrintPhys: flagPrintPhys, 120 | PrintIfaceChanges: flagPrintIfaceChanges, 121 | } 122 | } 123 | 124 | var ( 125 | flagFamily string 126 | flagFilter string 127 | flagIface string 128 | flagFilterChain string 129 | flagFlushRaw bool 130 | flagNetns string 131 | flagPrintRaw bool 132 | flagPrintPhys bool 133 | flagPrintIfaceChanges bool 134 | flagSkipModprobe bool 135 | flagLogLvl string 136 | ) 137 | 138 | type Config struct { 139 | IPFamily IPFamily 140 | Filter Filter 141 | FilterIfaces []string 142 | FilterChains [][]string 143 | NetnsPath string 144 | // Whether raw table should be flushed before adding our own rules 145 | FlushRaw bool 146 | // Whether raw ipt rules should be printed when packets hit them 147 | PrintRaw bool 148 | // Whether physin/physout should be printed 149 | PrintPhys bool 150 | // Whether interface changes should be printed. WARNING: memory will grow unbounded. 151 | PrintIfaceChanges bool 152 | } 153 | 154 | func insNfNetlinkLog() { 155 | cmd := exec.Command("modprobe", "nfnetlink_log") 156 | out, err := cmd.CombinedOutput() 157 | if err != nil { 158 | if e, ok := err.(*exec.ExitError); ok { 159 | logrus.Fatalf("Could not modprobe nfnetlink_log (exit code: %d): %s. You can pass -skip-modprobe if you think that's not needed.", e.ExitCode(), string(out)) 160 | } else { 161 | logrus.Fatalf("Could not modprobe nfnetlink_log: %v. You can pass -skip-modprobe if you think that's not needed.", err) 162 | } 163 | } 164 | } 165 | 166 | func reexecSelf() { 167 | args := os.Args[1:] 168 | for i, arg := range args { 169 | if strings.HasPrefix(arg, "-netns=") || strings.HasPrefix(arg, "--netns") { 170 | args = append(args[:i], args[i+1:]...) 171 | break 172 | } else if arg == "-netns" || arg == "--netns" { 173 | args = append(args[:i], args[i+2:]...) 174 | } 175 | } 176 | 177 | reexecCmd := exec.Command("/proc/self/exe", args...) 178 | reexecCmd.Stdin = os.Stdin 179 | reexecCmd.Stdout = os.Stdout 180 | reexecCmd.Stderr = os.Stderr 181 | 182 | err := reexecCmd.Run() 183 | if err == nil { 184 | os.Exit(0) 185 | } else if exitErr, ok := err.(*exec.ExitError); ok { 186 | os.Exit(exitErr.ExitCode()) 187 | } 188 | logrus.Fatal(err) 189 | } 190 | 191 | func main() { 192 | // TODO: add a way to filter in/out specific ipt tables/chains 193 | // TODO: support tracing both AF at the same time 194 | flag.StringVar(&flagFamily, "family", string(AfInet4), "Either: ipv4 or ipv6") 195 | flag.StringVar(&flagFilter, "filter", "", "A cBPF filter to select specific packets") 196 | flag.StringVar(&flagIface, "iface", "", "Only trace packets coming from/to specific interface(s). Use a comma to specify multiple interfaces.") 197 | flag.StringVar(&flagFilterChain, "filter-chain", "", "Print only ipt decisions for given table/chains. Use a comma to specify multiple chains.") 198 | flag.BoolVar(&flagFlushRaw, "flush", true, "Whether the RAW chains should be flushed before adding tracing rules.") 199 | flag.StringVar(&flagNetns, "netns", "", "Path to a netns handle where the tracer should be executed") 200 | flag.BoolVar(&flagPrintRaw, "print-raw", true, "Whether raw iptables rules should be printed when packets hit them") 201 | flag.BoolVar(&flagPrintPhys, "print-phys", false, "Whether physin/physout should be printed") 202 | flag.BoolVar(&flagPrintIfaceChanges, "print-iface-changes", true, "Whether iface changes should be printed. WARNING: memory will grow unbounded.") 203 | flag.BoolVar(&flagSkipModprobe, "skip-modprobe", false, "Whether iptables-tracer should try to modprobe nfnetlink_log module") 204 | flag.StringVar(&flagLogLvl, "log-level", "info", "Log level (panic, fatal, error, warn, info, debug or trace)") 205 | flag.Parse() 206 | 207 | cfg := parseFlags() 208 | 209 | if !flagSkipModprobe { 210 | insNfNetlinkLog() 211 | 212 | // github.com/florianl/go-nflog doesn't set the right value in /proc/sys/net/netfilter/nf_log/* 213 | // If we don't set it, no packets will be transmitted to nfnetlink_log, making the tracer useless. 214 | if cfg.IPFamily == AfInet4 { 215 | if err := os.WriteFile("/proc/sys/net/netfilter/nf_log/2", []byte("nfnetlink_log"), 0644); err != nil { 216 | logrus.Errorf("Could not set nfnetlink_log in /proc/sys/net/netfilter/nf_log/2: %v", err) 217 | os.Exit(1) 218 | } 219 | } else { 220 | if err := os.WriteFile("/proc/sys/net/netfilter/nf_log/10", []byte("nfnetlink_log"), 0644); err != nil { 221 | logrus.Errorf("Could not set nfnetlink_log in /proc/sys/net/netfilter/nf_log/10: %v", err) 222 | os.Exit(1) 223 | } 224 | } 225 | } 226 | 227 | if cfg.NetnsPath != "" { 228 | handle, err := netns.GetFromPath(cfg.NetnsPath) 229 | if err != nil { 230 | logrus.Fatalf("Could not get netns handle from path %s: %s", cfg.NetnsPath, err) 231 | } 232 | 233 | runtime.LockOSThread() 234 | if err := netns.Set(handle); err != nil { 235 | logrus.Fatalf("Could not switch to netns %s: %s", cfg.NetnsPath, err) 236 | } 237 | logrus.Infof("Tracer switched to netns %s. Forking..", cfg.NetnsPath) 238 | reexecSelf() 239 | } 240 | 241 | reverters, err := setupIptRules(cfg.IPFamily, cfg.FlushRaw, cfg.FilterIfaces, cfg.Filter) 242 | if err != nil { 243 | logrus.Error(err) 244 | 245 | applyReverters(reverters) 246 | os.Exit(1) 247 | } 248 | 249 | ifaceCache := &IfaceCache{} 250 | ctxWatchIface, cancelWatchIface := context.WithCancel(context.Background()) 251 | go func() { 252 | if err := ifaceCache.Watch(ctxWatchIface); err != nil { 253 | logrus.Error(err) 254 | applyReverters(reverters) 255 | os.Exit(1) 256 | } 257 | }() 258 | 259 | nf, err := nflog.Open(&nflog.Config{ 260 | // From https://workshop.netfilter.org/2016/wiki/images/3/33/Nft-logging.pdf: 261 | // "nfnetlink_log always uses group 0" 262 | Group: 0, 263 | Copymode: nflog.CopyPacket, 264 | Bufsize: 655360, 265 | }) 266 | if err != nil { 267 | logrus.Error(err) 268 | 269 | applyReverters(reverters) 270 | os.Exit(1) 271 | } 272 | defer nf.Close() 273 | 274 | d := NewPacketDecoder() 275 | 276 | pr := NewPrinter(cfg, ifaceCache) 277 | printer := func(attrs nflog.Attribute) int { 278 | logrus.Tracef("Received a new packet: % 02x\n", *attrs.Payload) 279 | logrus.Tracef(" attrs.Prefix: %s\n", *attrs.Prefix) 280 | 281 | traceEvt, err := newTraceEvent(d, attrs, cfg.IPFamily) 282 | if err != nil { 283 | logrus.Debugf("Invalid nfla: %v", err) 284 | return 0 285 | } 286 | 287 | // Every packet first reach the raw table, so we can dump its headers here first 288 | // and then no need to reprint it on subsequent trace events (that would make the output 289 | // noisy). 290 | if traceEvt.tableName == "raw" { 291 | pr.printPacketHeaders(traceEvt) 292 | } 293 | 294 | if len(cfg.FilterChains) == 0 { 295 | pr.printIptRule(traceEvt, cfg.IPFamily) 296 | } else { 297 | for _, filter := range cfg.FilterChains { 298 | if traceEvt.tableName == filter[0] && traceEvt.chainName == filter[1] { 299 | pr.printIptRule(traceEvt, cfg.IPFamily) 300 | } 301 | } 302 | } 303 | 304 | return 0 305 | } 306 | onErr := func(err error) int { 307 | if opError, ok := err.(*netlink.OpError); ok { 308 | if opError.Timeout() || opError.Temporary() { 309 | return 0 310 | } 311 | } 312 | 313 | logrus.Errorf("Could not receive nflog messages: %v", err) 314 | applyReverters(reverters) 315 | // TODO: revert /proc/sys/net/netfilter/nf_log/* + drain the multicast group to make sure next run won't get 316 | // stale traces. 317 | os.Exit(1) 318 | 319 | return 1 // Unreachable -- just to make Go compiler happy 320 | } 321 | 322 | ctxNflog, cancelNflog := context.WithCancel(context.Background()) 323 | if err := nf.RegisterWithErrorFunc(ctxNflog, printer, onErr); err != nil { 324 | logrus.Errorf("Could not register nflog handler: %v", err) 325 | applyReverters(reverters) 326 | os.Exit(1) 327 | } 328 | 329 | logrus.Info("Waiting for trace events...") 330 | 331 | signalCh := make(chan os.Signal, 1) 332 | signal.Notify(signalCh, os.Interrupt) 333 | // Let the tracer do its job until we receive a SIGINT. 334 | <-signalCh 335 | 336 | cancelWatchIface() 337 | cancelNflog() 338 | applyReverters(reverters) 339 | } 340 | 341 | type traceEvent struct { 342 | attrs nflog.Attribute 343 | pkt packet 344 | inDev uint32 345 | outDev uint32 346 | physInDev uint32 347 | physOutDev uint32 348 | nfMark uint32 349 | tableName string 350 | chainName string 351 | // traceType is either policy, rule or return 352 | traceType string 353 | ruleNum int 354 | } 355 | 356 | func (traceEvt traceEvent) ifaces() [2]uint32 { 357 | return [2]uint32{traceEvt.inDev, traceEvt.outDev} 358 | } 359 | 360 | func newTraceEvent(d PacketDecoder, attrs nflog.Attribute, family IPFamily) (traceEvent, error) { 361 | if attrs.Prefix == nil || !strings.HasPrefix(*attrs.Prefix, "TRACE: ") { 362 | return traceEvent{}, errors.New("not a trace event") 363 | } 364 | if attrs.Payload == nil { 365 | return traceEvent{}, errors.New("nfla has no payload") 366 | } 367 | 368 | prefix := strings.Trim(*attrs.Prefix, " ") 369 | splitPrefix := strings.Split(prefix[7:], ":") 370 | 371 | if len(splitPrefix) != 4 { 372 | return traceEvent{}, fmt.Errorf("invalid prefix format: %s", prefix) 373 | } 374 | 375 | traceType := splitPrefix[2] 376 | if traceType != "policy" && traceType != "rule" && traceType != "return" { 377 | return traceEvent{}, fmt.Errorf("invalid trace type: %q (prefix: %s)", traceType, prefix) 378 | } 379 | 380 | pkt, err := d.Decode(*attrs.Payload, family) 381 | if err != nil { 382 | return traceEvent{}, nil 383 | } 384 | 385 | return traceEvent{ 386 | pkt: pkt, 387 | inDev: derefUint32Or(attrs.InDev, 0), 388 | outDev: derefUint32Or(attrs.OutDev, 0), 389 | physInDev: derefUint32Or(attrs.PhysInDev, 0), 390 | physOutDev: derefUint32Or(attrs.PhysOutDev, 0), 391 | nfMark: derefUint32Or(attrs.Mark, 0), 392 | tableName: splitPrefix[0], 393 | chainName: splitPrefix[1], 394 | traceType: splitPrefix[2], 395 | ruleNum: mustAtoi(splitPrefix[3]), 396 | }, nil 397 | } 398 | 399 | func derefUint32Or(p *uint32, d uint32) uint32 { 400 | if p == nil { 401 | return d 402 | } 403 | return *p 404 | } 405 | 406 | func mustAtoi(s string) int { 407 | r, err := strconv.Atoi(s) 408 | if err != nil { 409 | panic(fmt.Sprintf("Could not convert \"%s\" to an integer: %s", s, err)) 410 | } 411 | return r 412 | } 413 | -------------------------------------------------------------------------------- /cmd/packet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/google/gopacket" 8 | "github.com/google/gopacket/layers" 9 | ) 10 | 11 | type PacketDecoder struct { 12 | // Cache the gopacket.LayerClass used internally to print packets' L4 13 | l4Class gopacket.LayerClass 14 | } 15 | 16 | func NewPacketDecoder() PacketDecoder { 17 | l4Class := make([]gopacket.LayerType, 0) 18 | l4Class = append(l4Class, layers.LayerClassIPTransport.LayerTypes()...) 19 | l4Class = append(l4Class, layers.LayerClassIPControl.LayerTypes()...) 20 | 21 | return PacketDecoder{ 22 | l4Class: gopacket.NewLayerClass(l4Class), 23 | } 24 | } 25 | 26 | type packet struct { 27 | l3 string 28 | l4 string 29 | } 30 | 31 | func (pkt packet) Headers() string { 32 | return pkt.l3 + pkt.l4 33 | } 34 | 35 | func (d PacketDecoder) Decode(payload []byte, family IPFamily) (packet, error) { 36 | var firstLayer gopacket.Decoder 37 | if family == AfInet4 { 38 | firstLayer = layers.LayerTypeIPv4 39 | } else { 40 | firstLayer = layers.LayerTypeIPv6 41 | } 42 | 43 | gopkt := gopacket.NewPacket(payload, firstLayer, gopacket.DecodeOptions{ 44 | NoCopy: true, 45 | Lazy: true, 46 | }) 47 | 48 | var pkt packet 49 | l3 := gopkt.NetworkLayer() 50 | switch l3.(type) { 51 | case *layers.IPv4: 52 | ipv4 := l3.(*layers.IPv4) 53 | pkt.l3 = fmt.Sprintf( 54 | "SRC=%s DST=%s LEN=%d TOS=%02x TTL=%d ID=%d PROTO=%s ", 55 | ipv4.SrcIP, ipv4.DstIP, ipv4.Length, ipv4.TOS, ipv4.TTL, ipv4.Id, ipv4.Protocol) 56 | case *layers.IPv6: 57 | ipv6 := l3.(*layers.IPv6) 58 | pkt.l3 = fmt.Sprintf( 59 | "SRC=%s DST=%s LEN=%d HOP=%d ", 60 | ipv6.SrcIP, ipv6.DstIP, ipv6.Length, ipv6.HopLimit) 61 | 62 | if ipv6.NextHeader == layers.IPProtocolTCP || 63 | ipv6.NextHeader == layers.IPProtocolUDP || 64 | ipv6.NextHeader == layers.IPProtocolICMPv6 { 65 | pkt.l3 += fmt.Sprintf("PROTO=%s ", ipv6.NextHeader) 66 | } 67 | default: 68 | return packet{}, fmt.Errorf("could not parse network layer: %+v", l3) 69 | } 70 | 71 | l4 := gopkt.LayerClass(d.l4Class) 72 | switch l4.(type) { 73 | case *layers.TCP: 74 | tcp := l4.(*layers.TCP) 75 | flags := tcpFlags(tcp) 76 | pkt.l4 = fmt.Sprintf( 77 | "SPT=%d DPT=%d FLAGS=%s SEQ=%d CSUM=%x ", 78 | tcp.SrcPort, tcp.DstPort, strings.Join(flags, ","), tcp.Seq, tcp.Checksum) 79 | case *layers.UDP: 80 | udp := l4.(*layers.UDP) 81 | pkt.l4 = fmt.Sprintf("SPT=%d DPT=%d CSUM=%x ", 82 | udp.SrcPort, udp.DstPort, udp.Checksum) 83 | case *layers.ICMPv4: 84 | icmp := l4.(*layers.ICMPv4) 85 | pkt.l4 = fmt.Sprintf("TYPE/CODE=%s CSUM=%x ", 86 | icmp.TypeCode, icmp.Checksum) 87 | case *layers.ICMPv6: 88 | icmp := l4.(*layers.ICMPv6) 89 | pkt.l4 = fmt.Sprintf("TYPE/CODE=%s CSUM=%x ", 90 | icmp.TypeCode, icmp.Checksum) 91 | default: 92 | return packet{}, fmt.Errorf("could not parse transport layer: %+v", l4) 93 | } 94 | 95 | return pkt, nil 96 | } 97 | 98 | func tcpFlags(tp *layers.TCP) []string { 99 | flags := make([]string, 0) 100 | 101 | if tp.SYN { 102 | flags = append(flags, "SYN") 103 | } 104 | if tp.RST { 105 | flags = append(flags, "RST") 106 | } 107 | if tp.FIN { 108 | flags = append(flags, "FIN") 109 | } 110 | if tp.PSH { 111 | flags = append(flags, "PSH") 112 | } 113 | if tp.ACK { 114 | flags = append(flags, "ACK") 115 | } 116 | if tp.URG { 117 | flags = append(flags, "URG") 118 | } 119 | 120 | return flags 121 | } 122 | -------------------------------------------------------------------------------- /cmd/printer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/fatih/color" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type Printer struct { 12 | // Whether raw ipt rules should be printed when packets hit them 13 | printRaw bool 14 | // Whether physin/physout should be printed 15 | printPhys bool 16 | // Whether ifaces should be tracked and changes printed 17 | printIfaceChanges bool 18 | 19 | ifaceCache *IfaceCache 20 | ifaceTracker map[packet][2]uint32 21 | } 22 | 23 | func NewPrinter(cfg Config, ifaceCache *IfaceCache) Printer { 24 | return Printer{ 25 | printRaw: cfg.PrintRaw, 26 | printPhys: cfg.PrintPhys, 27 | printIfaceChanges: cfg.PrintIfaceChanges, 28 | ifaceCache: ifaceCache, 29 | ifaceTracker: map[packet][2]uint32{}, 30 | } 31 | } 32 | 33 | func (pr Printer) printIptRule(traceEvt traceEvent, family IPFamily) { 34 | b := strings.Builder{} 35 | b.WriteString(fmt.Sprintf("\t%s %s ", traceEvt.tableName, traceEvt.chainName)) 36 | 37 | // TODO: check if the NFMARK is really available through Mark 38 | if traceEvt.attrs.Mark != nil { 39 | b.WriteString(fmt.Sprintf("NFMARK=0x%x ", traceEvt.nfMark)) 40 | } else { 41 | b.WriteString("NFMARK=0x0 ") 42 | } 43 | 44 | // TODO: improve this // IN and OUT are always displayed when one changes 45 | ifaces := traceEvt.ifaces() 46 | trackedIfaces := pr.ifaceTracker[traceEvt.pkt] 47 | if pr.printIfaceChanges { 48 | var changed bool 49 | if trackedIfaces[0] != ifaces[0] { 50 | indev, _ := pr.ifaceCache.IndexToName(traceEvt.inDev) 51 | b.WriteString(fmt.Sprintf("IN=%s ", indev)) 52 | changed = true 53 | } 54 | if trackedIfaces[1] != ifaces[1] { 55 | outdev, _ := pr.ifaceCache.IndexToName(traceEvt.outDev) 56 | b.WriteString(fmt.Sprintf("OUT=%s ", outdev)) 57 | changed = true 58 | } 59 | if changed { 60 | pr.ifaceTracker[traceEvt.pkt] = ifaces 61 | b.WriteString("(changed by last rule)") 62 | } 63 | } 64 | 65 | b.WriteString("\n") 66 | 67 | chain, err := GetIptChain(family, traceEvt.tableName, traceEvt.chainName) 68 | if err != nil { 69 | logrus.Errorf("Could not get iptables chain %s from table %s: %s", traceEvt.chainName, traceEvt.tableName, err) 70 | } 71 | 72 | if traceEvt.traceType == "policy" { 73 | b.WriteString("\t\tDEFAULT POLICY") 74 | b.WriteString(fmt.Sprintf("\n\t\t=> %s", color.HiGreenString(chain.Policy))) 75 | } else if traceEvt.traceType == "rule" { 76 | b.WriteString(fmt.Sprintf("\t\tMATCH RULE (#%d)", traceEvt.ruleNum)) 77 | 78 | if len(chain.Rules) < traceEvt.ruleNum { 79 | b.WriteString(": (rule not found)") 80 | goto print 81 | } 82 | 83 | rule := chain.Rules[traceEvt.ruleNum-1] 84 | 85 | if pr.printRaw { 86 | b.WriteString(fmt.Sprintf(": %s", rule)) 87 | } 88 | 89 | target, targetFlags := parseIptRuleTarget(rule) 90 | 91 | if target == "ACCEPT" { 92 | target = color.HiGreenString(target) 93 | } else if target == "DROP" { 94 | target = color.HiRedString(target) 95 | } else { 96 | target = color.HiBlueString(target) 97 | } 98 | b.WriteString(fmt.Sprintf("\n\t\t=> %s", target)) 99 | 100 | if targetFlags != "" { 101 | b.WriteString(fmt.Sprintf(": %s", targetFlags)) 102 | } 103 | } else if traceEvt.traceType == "return" { 104 | b.WriteString(fmt.Sprintf("\t\t=> %s", color.HiBlueString("RETURN"))) 105 | } 106 | 107 | print: 108 | fmt.Println(b.String()) 109 | } 110 | 111 | func (pr Printer) printPacketHeaders(traceEvt traceEvent) { 112 | b := strings.Builder{} 113 | 114 | if pr.printIfaceChanges { 115 | pr.ifaceTracker[traceEvt.pkt] = traceEvt.ifaces() 116 | } 117 | 118 | indev, _ := pr.ifaceCache.IndexToName(traceEvt.inDev) 119 | outdev, _ := pr.ifaceCache.IndexToName(traceEvt.outDev) 120 | b.WriteString(fmt.Sprintf("IN=%s OUT=%s ", indev, outdev)) 121 | 122 | if pr.printPhys { 123 | physindev, _ := pr.ifaceCache.IndexToName(traceEvt.physInDev) 124 | physoutdev, _ := pr.ifaceCache.IndexToName(traceEvt.physOutDev) 125 | b.WriteString(fmt.Sprintf("PHYSIN=%s PHYSOUT=%s ", physindev, physoutdev)) 126 | } 127 | 128 | b.WriteString(traceEvt.pkt.Headers()) 129 | fmt.Println(b.String()) 130 | } 131 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/akerouanton/iptables-tracer 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/fatih/color v1.15.0 7 | github.com/florianl/go-nflog/v2 v2.0.1 8 | github.com/google/gopacket v1.1.19 9 | github.com/mdlayher/netlink v1.4.1 10 | github.com/sirupsen/logrus v1.9.2 11 | github.com/vishvananda/netlink v1.1.0 12 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df 13 | ) 14 | 15 | require ( 16 | github.com/google/go-cmp v0.5.9 // indirect 17 | github.com/josharian/native v1.1.0 // indirect 18 | github.com/mattn/go-colorable v0.1.13 // indirect 19 | github.com/mattn/go-isatty v0.0.17 // indirect 20 | github.com/mdlayher/socket v0.4.1 // indirect 21 | golang.org/x/net v0.9.0 // indirect 22 | golang.org/x/sync v0.1.0 // indirect 23 | golang.org/x/sys v0.7.0 // indirect 24 | ) 25 | 26 | replace github.com/mdlayher/netlink v1.4.1 => github.com/mdlayher/netlink v1.7.2 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 5 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 6 | github.com/florianl/go-nflog/v2 v2.0.1 h1:8csWIqFQ2vDaZJkxezY3dXDB7bEFg0VRFsYd2Bzj3II= 7 | github.com/florianl/go-nflog/v2 v2.0.1/go.mod h1:g+SOgM/SuePn9bvS/eo3Ild7J71nSb29OzbxR+7cln0= 8 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 9 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 10 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 12 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 13 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 14 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 15 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 16 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 17 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 18 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 19 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 20 | github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= 21 | github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 22 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 23 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= 27 | github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 30 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 31 | github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= 32 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 33 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= 34 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 35 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 36 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 37 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 38 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 39 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 40 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 41 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 42 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 43 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 44 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 45 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 46 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 47 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 48 | golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= 49 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 50 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 51 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 52 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 53 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 54 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 55 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= 66 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 68 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 69 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 70 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 71 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 72 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 73 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 74 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 75 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 76 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 77 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 78 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 79 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 80 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 81 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 82 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 84 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 85 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 86 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 87 | -------------------------------------------------------------------------------- /modprobe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | set -o xtrace 4 | 5 | # "modprobe" without modprobe 6 | # https://twitter.com/lucabruno/status/902934379835662336 7 | 8 | # this isn't 100% fool-proof, but it'll have a much higher success rate than simply using the "real" modprobe 9 | 10 | # Docker often uses "modprobe -va foo bar baz" 11 | # so we ignore modules that start with "-" 12 | for module; do 13 | if [ "${module#-}" = "$module" ]; then 14 | ip link show "$module" || true 15 | lsmod | grep "$module" || true 16 | fi 17 | done 18 | --------------------------------------------------------------------------------