├── internal └── unix │ ├── doc.go │ ├── types_other.go │ └── types_linux.go ├── .github ├── dependabot.yml └── workflows │ ├── sourcegraph-lsif-indexing.yml │ └── go.yml ├── doc.go ├── go.mod ├── README.md ├── LICENSE ├── go.sum ├── example_test.go ├── nfqueue_linux_integration_test.go ├── verdict.go ├── attribute.go ├── types.go └── nfqueue.go /internal/unix/doc.go: -------------------------------------------------------------------------------- 1 | // Package unix maps constants from golang.org/x/sys/unix to local constant 2 | // and makes them available for other platforms as well. 3 | package unix 4 | -------------------------------------------------------------------------------- /internal/unix/types_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package unix 5 | 6 | const ( 7 | AF_INET = 0x2 8 | AF_INET6 = 0xa 9 | AF_UNSPEC = 0x0 10 | NFNETLINK_V0 = 0x0 11 | NETLINK_NETFILTER = 0xc 12 | ) 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | open-pull-requests-limit: 5 9 | directory: / 10 | schedule: 11 | interval: weekly -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package nfqueue provides an API to interact with the nfqueue subsystem of the netfilter family from the linux kernel. 3 | 4 | This package processes information directly from the kernel and therefore it requires special privileges. You 5 | can provide this privileges by adjusting the CAP_NET_ADMIN capabilities. 6 | 7 | setcap 'cap_net_admin=+ep' /your/executable 8 | */ 9 | package nfqueue 10 | -------------------------------------------------------------------------------- /internal/unix/types_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package unix 5 | 6 | import ( 7 | linux "golang.org/x/sys/unix" 8 | ) 9 | 10 | // various constants 11 | const ( 12 | AF_INET = linux.AF_INET 13 | AF_INET6 = linux.AF_INET6 14 | AF_UNSPEC = linux.AF_UNSPEC 15 | NFNETLINK_V0 = linux.NFNETLINK_V0 16 | NETLINK_NETFILTER = linux.NETLINK_NETFILTER 17 | ) 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/florianl/go-nfqueue/v2 2 | 3 | require ( 4 | github.com/mdlayher/netlink v1.7.2 5 | golang.org/x/sys v0.30.0 6 | ) 7 | 8 | require ( 9 | github.com/google/go-cmp v0.6.0 // indirect 10 | github.com/josharian/native v1.1.0 // indirect 11 | github.com/mdlayher/socket v0.4.1 // indirect 12 | golang.org/x/net v0.25.0 // indirect 13 | golang.org/x/sync v0.7.0 // indirect 14 | ) 15 | 16 | go 1.18 17 | -------------------------------------------------------------------------------- /.github/workflows/sourcegraph-lsif-indexing.yml: -------------------------------------------------------------------------------- 1 | name: "sourcegraph LSIF indexing" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | # Set default permissions as read only. 8 | permissions: read-all 9 | 10 | jobs: 11 | lsif-go: 12 | runs-on: ubuntu-latest 13 | container: sourcegraph/lsif-go:latest 14 | steps: 15 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 16 | - name: Generate LSIF data 17 | run: lsif-go 18 | - name: Upload LSIF data 19 | # this will upload to Sourcegraph.com, you may need to substitute a different command. 20 | # by default, we ignore failures to avoid disrupting CI pipelines with non-critical errors. 21 | run: src lsif upload -github-token=${{ secrets.GITHUB_TOKEN }} -ignore-upload-failure 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-nfqueue [![PkgGoDev](https://pkg.go.dev/badge/github.com/florianl/go-nfqueue/v2)](https://pkg.go.dev/github.com/florianl/go-nfqueue/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/florianl/go-nfqueue/v2)](https://goreportcard.com/report/github.com/florianl/go-nfqueue/v2) 2 | ============ 3 | 4 | This is `go-nfqueue` and it is written in [golang](https://golang.org/). It provides a [C](https://en.wikipedia.org/wiki/C_(programming_language))-binding free API to the netfilter based queue subsystem of the [Linux kernel](https://www.kernel.org). 5 | 6 | ## Privileges 7 | 8 | This package processes information directly from the kernel and therefore it requires special privileges. You can provide this privileges by adjusting the `CAP_NET_ADMIN` capabilities. 9 | ``` 10 | setcap 'cap_net_admin=+ep' /your/executable 11 | ``` 12 | 13 | For documentation and more examples please take a look at [documentation](https://pkg.go.dev/github.com/florianl/go-nfqueue). 14 | 15 | ## Requirements 16 | 17 | * A version of Go that is [supported by upstream](https://golang.org/doc/devel/release.html#policy) 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (C) 2018-2020 Florian Lehner 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 4 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 5 | github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= 6 | github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= 7 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 8 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 9 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 10 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 11 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 12 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 13 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 14 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 15 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ main ] 4 | pull_request: 5 | branches: [ '**' ] 6 | 7 | name: Go 8 | jobs: 9 | 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: [1.18.x, 1.22.x, 1.23.x, 1.24.x] 14 | platform: [ubuntu-latest, macos-latest, windows-latest] 15 | runs-on: ${{ matrix.platform }} 16 | steps: 17 | - name: Install Go 18 | uses: actions/setup-go@v6 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | - name: Checkout code 22 | uses: actions/checkout@v6.0.0 23 | - name: Download Go dependencies 24 | env: 25 | GOPROXY: "https://proxy.golang.org" 26 | run: go mod download 27 | - name: Test with -race 28 | run: go test -race -count=1 ./... 29 | - name: Integration test 30 | if: matrix.platform == 'ubuntu-latest' && startsWith(matrix.go-version, '1.24') 31 | run: | 32 | sudo modprobe nfnetlink_queue 33 | sudo ip6tables -I OUTPUT -p ipv6-icmp -j NFQUEUE --queue-num 100 34 | sudo iptables -I OUTPUT -p icmp -j NFQUEUE --queue-num 100 35 | go test -v -tags integration -exec=sudo -count=1 ./... 36 | - name: staticcheck.io 37 | if: matrix.platform == 'ubuntu-latest' && startsWith(matrix.go-version, '1.24') 38 | uses: dominikh/staticcheck-action@v1.4.0 39 | with: 40 | version: "2025.1" 41 | install-go: false 42 | cache-key: ${{ matrix.go-version }} 43 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package nfqueue_test 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "time" 10 | 11 | nfqueue "github.com/florianl/go-nfqueue/v2" 12 | "github.com/mdlayher/netlink" 13 | ) 14 | 15 | func ExampleNfqueue_RegisterWithErrorFunc() { 16 | // Send outgoing pings to nfqueue queue 100 17 | // # sudo iptables -I OUTPUT -p icmp -j NFQUEUE --queue-num 100 18 | 19 | // Set configuration options for nfqueue 20 | config := nfqueue.Config{ 21 | NfQueue: 100, 22 | MaxPacketLen: 0xFFFF, 23 | MaxQueueLen: 0xFF, 24 | Copymode: nfqueue.NfQnlCopyPacket, 25 | WriteTimeout: 15 * time.Millisecond, 26 | } 27 | 28 | nf, err := nfqueue.Open(&config) 29 | if err != nil { 30 | fmt.Println("could not open nfqueue socket:", err) 31 | return 32 | } 33 | defer nf.Close() 34 | 35 | // Avoid receiving ENOBUFS errors. 36 | if err := nf.SetOption(netlink.NoENOBUFS, true); err != nil { 37 | fmt.Printf("failed to set netlink option %v: %v\n", 38 | netlink.NoENOBUFS, err) 39 | return 40 | } 41 | 42 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 43 | defer cancel() 44 | 45 | fn := func(a nfqueue.Attribute) int { 46 | id := *a.PacketID 47 | // Just print out the id and payload of the nfqueue packet 48 | fmt.Printf("[%d]\t%v\n", id, *a.Payload) 49 | nf.SetVerdict(id, nfqueue.NfAccept) 50 | return 0 51 | } 52 | 53 | // Register your function to listen on nflqueue queue 100 54 | err = nf.RegisterWithErrorFunc(ctx, fn, func(e error) int { 55 | fmt.Println(err) 56 | return -1 57 | }) 58 | if err != nil { 59 | fmt.Println(err) 60 | return 61 | } 62 | 63 | // Block till the context expires 64 | <-ctx.Done() 65 | } 66 | -------------------------------------------------------------------------------- /nfqueue_linux_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration && linux 2 | // +build integration,linux 3 | 4 | package nfqueue 5 | 6 | import ( 7 | "context" 8 | "os/exec" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func startDummyPingTraffic(t *testing.T, ctx context.Context) { 14 | t.Helper() 15 | 16 | if err := exec.CommandContext(ctx, "ping6", "2606:4700:4700::1111").Start(); err != nil { 17 | t.Fatalf("failed to start IPv6 ping: %v", err) 18 | } 19 | if err := exec.CommandContext(ctx, "ping", "1.1.1.1").Start(); err != nil { 20 | t.Fatalf("failed to start IPv4 ping: %v", err) 21 | } 22 | } 23 | 24 | func TestLinuxNfqueue(t *testing.T) { 25 | pingCtx, pingCancel := context.WithCancel(context.Background()) 26 | defer pingCancel() 27 | 28 | startDummyPingTraffic(t, pingCtx) 29 | 30 | // Set configuration options for nfqueue 31 | config := Config{ 32 | NfQueue: 100, 33 | MaxPacketLen: 0xFFFF, 34 | MaxQueueLen: 0xFF, 35 | Copymode: NfQnlCopyPacket, 36 | } 37 | // Open a socket to the netfilter log subsystem 38 | nfq, err := Open(&config) 39 | if err != nil { 40 | t.Fatalf("failed to open nfqueue socket: %v", err) 41 | } 42 | defer nfq.Close() 43 | 44 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 45 | defer cancel() 46 | 47 | fn := func(a Attribute) int { 48 | id := *a.PacketID 49 | // Just print out the id and payload of the nfqueue packet 50 | t.Logf("[%d]\t%v\n", id, *a.Payload) 51 | nfq.SetVerdict(id, NfAccept) 52 | return 0 53 | } 54 | 55 | // Register your function to listen on nflog group 100 56 | err = nfq.Register(ctx, fn) 57 | if err != nil { 58 | t.Fatalf("failed to register hook function: %v", err) 59 | } 60 | 61 | // Block till the context expires 62 | <-ctx.Done() 63 | } 64 | 65 | func TestTimeout(t *testing.T) { 66 | // Set configuration options for nfqueue 67 | config := Config{ 68 | NfQueue: 123, 69 | MaxPacketLen: 0xFFFF, 70 | MaxQueueLen: 0xFF, 71 | Copymode: NfQnlCopyPacket, 72 | } 73 | 74 | nfq, err := Open(&config) 75 | if err != nil { 76 | t.Fatalf("failed to open nfqueue socket: %v", err) 77 | } 78 | defer nfq.Close() 79 | 80 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) 81 | 82 | fn := func(a Attribute) int { 83 | id := *a.PacketID 84 | // Just print out the id and payload of the nfqueue packet 85 | t.Logf("[%d]\t%v\n", id, *a.Payload) 86 | nfq.SetVerdict(id, NfAccept) 87 | return 0 88 | } 89 | 90 | // Register your function to listen on nflog group 123 91 | // This also does a reading on the netlink socket 92 | err = nfq.Register(ctx, fn) 93 | if err != nil { 94 | t.Fatalf("failed to register hook function: %v", err) 95 | } 96 | // cancel the context to remove the registered hook from the nfqueue. 97 | cancel() 98 | 99 | // Block till the context expires 100 | <-ctx.Done() 101 | } 102 | -------------------------------------------------------------------------------- /verdict.go: -------------------------------------------------------------------------------- 1 | package nfqueue 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | 7 | "github.com/mdlayher/netlink" 8 | ) 9 | 10 | // VerdictOption configures additional verdict parameters like mark, label, or packet payload. 11 | type VerdictOption func(*verdictOptions) error 12 | 13 | type verdictOptions struct { 14 | attrs []netlink.Attribute 15 | ctAttrs []netlink.Attribute 16 | } 17 | 18 | // WithMark sets the packet mark. 19 | func WithMark(mark uint32) VerdictOption { 20 | return func(vo *verdictOptions) error { 21 | buf := make([]byte, 4) 22 | binary.BigEndian.PutUint32(buf, mark) 23 | vo.attrs = append(vo.attrs, netlink.Attribute{ 24 | Type: nfQaMark, 25 | Data: buf, 26 | }) 27 | return nil 28 | } 29 | } 30 | 31 | // WithConnMark sets the packet connmark. 32 | func WithConnMark(mark uint32) VerdictOption { 33 | return func(vo *verdictOptions) error { 34 | buf := make([]byte, 4) 35 | binary.BigEndian.PutUint32(buf, mark) 36 | // collect conntrack attributes; will be nested under nfQaCt later 37 | vo.ctAttrs = append(vo.ctAttrs, netlink.Attribute{ 38 | Type: ctaMark, 39 | Data: buf, 40 | }) 41 | return nil 42 | } 43 | } 44 | 45 | // WithLabel sets the packet label. 46 | func WithLabel(label []byte) VerdictOption { 47 | return func(vo *verdictOptions) error { 48 | if len(label) != 16 { 49 | return fmt.Errorf("conntrack CTA_LABELS must be 16 bytes, got %d", len(label)) 50 | } 51 | // collect conntrack attributes; will be nested under nfQaCt later 52 | vo.ctAttrs = append(vo.ctAttrs, netlink.Attribute{ 53 | Type: ctaLabels, 54 | Data: label, 55 | }) 56 | return nil 57 | } 58 | } 59 | 60 | // WithAlteredPacket sets the altered packet payload. 61 | func WithAlteredPacket(packet []byte) VerdictOption { 62 | return func(vo *verdictOptions) error { 63 | vo.attrs = append(vo.attrs, netlink.Attribute{ 64 | Type: nfQaPayload, 65 | Data: packet, 66 | }) 67 | return nil 68 | } 69 | } 70 | 71 | // SetVerdictWithOption signals the kernel the next action for a specified packet id 72 | // and applies any number of verdict options like WithMark, WithLabel, WithPacket. 73 | func (nfqueue *Nfqueue) SetVerdictWithOption(id uint32, verdict int, options ...VerdictOption) error { 74 | vo := &verdictOptions{} 75 | for _, opt := range options { 76 | if err := opt(vo); err != nil { 77 | return err 78 | } 79 | } 80 | 81 | // If conntrack attributes were provided, nest them under nfQaCt 82 | if len(vo.ctAttrs) > 0 { 83 | ctData, err := netlink.MarshalAttributes(vo.ctAttrs) 84 | if err != nil { 85 | return err 86 | } 87 | vo.attrs = append(vo.attrs, netlink.Attribute{ 88 | Type: netlink.Nested | nfQaCt, 89 | Data: ctData, 90 | }) 91 | } 92 | 93 | data, err := netlink.MarshalAttributes(vo.attrs) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | return nfqueue.setVerdict(id, verdict, false, data) 99 | } 100 | -------------------------------------------------------------------------------- /attribute.go: -------------------------------------------------------------------------------- 1 | package nfqueue 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/florianl/go-nfqueue/v2/internal/unix" 10 | 11 | "github.com/mdlayher/netlink" 12 | ) 13 | 14 | func extractAttribute(log Logger, a *Attribute, data []byte) error { 15 | ad, err := netlink.NewAttributeDecoder(data) 16 | if err != nil { 17 | return err 18 | } 19 | ad.ByteOrder = binary.BigEndian 20 | for ad.Next() { 21 | switch ad.Type() { 22 | case nfQaPacketHdr: 23 | data := ad.Bytes() 24 | if len(data) < 7 { 25 | return fmt.Errorf("nfQaPacketHdr: insufficient data length: %d", len(data)) 26 | } 27 | packetID := binary.BigEndian.Uint32(data[:4]) 28 | a.PacketID = &packetID 29 | hwProto := binary.BigEndian.Uint16(data[4:6]) 30 | a.HwProtocol = &hwProto 31 | hook := uint8(data[6]) 32 | a.Hook = &hook 33 | case nfQaMark: 34 | mark := ad.Uint32() 35 | a.Mark = &mark 36 | case nfQaTimestamp: 37 | data := ad.Bytes() 38 | if len(data) < 16 { 39 | return fmt.Errorf("nfQaTimestamp: insufficient data length: %d", len(data)) 40 | } 41 | var sec, usec int64 42 | r := bytes.NewReader(data[:8]) 43 | if err := binary.Read(r, binary.BigEndian, &sec); err != nil { 44 | return err 45 | } 46 | r = bytes.NewReader(data[8:]) 47 | if err := binary.Read(r, binary.BigEndian, &usec); err != nil { 48 | return err 49 | } 50 | timestamp := time.Unix(sec, usec*1000) 51 | a.Timestamp = ×tamp 52 | case nfQaIfIndexInDev: 53 | inDev := ad.Uint32() 54 | a.InDev = &inDev 55 | case nfQaIfIndexOutDev: 56 | outDev := ad.Uint32() 57 | a.OutDev = &outDev 58 | case nfQaIfIndexPhysInDev: 59 | physInDev := ad.Uint32() 60 | a.PhysInDev = &physInDev 61 | case nfQaIfIndexPhysOutDev: 62 | physOutDev := ad.Uint32() 63 | a.PhysOutDev = &physOutDev 64 | case nfQaHwAddr: 65 | data := ad.Bytes() 66 | if len(data) < 4 { 67 | return fmt.Errorf("nfQaHwAddr: insufficient data length: %d", len(data)) 68 | } 69 | hwAddrLen := binary.BigEndian.Uint16(data[:2]) 70 | if len(data) < int(4+hwAddrLen) { 71 | return fmt.Errorf("nfQaHwAddr: insufficient data for hwAddrLen %d: got %d", hwAddrLen, len(data)) 72 | } 73 | hwAddr := data[4 : 4+hwAddrLen] 74 | a.HwAddr = &hwAddr 75 | case nfQaPayload: 76 | payload := ad.Bytes() 77 | a.Payload = &payload 78 | case nfQaCt: 79 | ct := ad.Bytes() 80 | a.Ct = &ct 81 | case nfQaCtInfo: 82 | ctInfo := ad.Uint32() 83 | a.CtInfo = &ctInfo 84 | case nfQaCapLen: 85 | capLen := ad.Uint32() 86 | a.CapLen = &capLen 87 | case nfQaSkbInfo: 88 | skbInfo := ad.Bytes() 89 | a.SkbInfo = &skbInfo 90 | case nfQaExp: 91 | exp := ad.Bytes() 92 | a.Exp = &exp 93 | case nfQaUID: 94 | uid := ad.Uint32() 95 | a.UID = &uid 96 | case nfQaGID: 97 | gid := ad.Uint32() 98 | a.GID = &gid 99 | case nfQaSecCtx: 100 | secCtx := ad.String() 101 | a.SecCtx = &secCtx 102 | case nfQaL2HDR: 103 | l2hdr := ad.Bytes() 104 | a.L2Hdr = &l2hdr 105 | case nfQaPriority: 106 | skbPrio := ad.Uint32() 107 | a.SkbPrio = &skbPrio 108 | default: 109 | log.Errorf("Unknown attribute Type: 0x%x\tData: %v", ad.Type(), ad.Bytes()) 110 | } 111 | } 112 | 113 | return ad.Err() 114 | } 115 | 116 | func checkHeader(data []byte) int { 117 | if len(data) < 2 { 118 | return 0 119 | } 120 | if (data[0] == unix.AF_INET || data[0] == unix.AF_INET6) && data[1] == unix.NFNETLINK_V0 { 121 | return 4 122 | } 123 | return 0 124 | } 125 | 126 | func extractAttributes(log Logger, msg []byte) (Attribute, error) { 127 | attrs := Attribute{} 128 | 129 | if len(msg) == 0 { 130 | return attrs, nil 131 | } 132 | 133 | offset := checkHeader(msg) 134 | if offset >= len(msg) { 135 | offset = 0 136 | } 137 | if err := extractAttribute(log, &attrs, msg[offset:]); err != nil { 138 | return attrs, err 139 | } 140 | return attrs, nil 141 | } 142 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package nfqueue 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | // Attribute contains various elements for nfqueue elements. 9 | // As not every value is contained in every nfqueue message, 10 | // the elements inside Attribute are pointers to these values 11 | // or nil, if not present. 12 | type Attribute struct { 13 | PacketID *uint32 14 | Hook *uint8 15 | Timestamp *time.Time 16 | Mark *uint32 17 | InDev *uint32 18 | PhysInDev *uint32 19 | OutDev *uint32 20 | PhysOutDev *uint32 21 | Payload *[]byte 22 | CapLen *uint32 23 | UID *uint32 24 | GID *uint32 25 | SecCtx *string 26 | L2Hdr *[]byte 27 | HwAddr *[]byte 28 | HwProtocol *uint16 29 | Ct *[]byte 30 | CtInfo *uint32 31 | SkbInfo *[]byte 32 | Exp *[]byte 33 | SkbPrio *uint32 34 | } 35 | 36 | // HookFunc is a function, that receives events from a Netlinkgroup 37 | // To stop receiving messages on this HookFunc, return something different than 0. 38 | type HookFunc func(a Attribute) int 39 | 40 | // ErrorFunc is a function that receives all errors that happen while reading 41 | // from a Netlinkgroup. To stop receiving messages return something different than 0. 42 | type ErrorFunc func(e error) int 43 | 44 | // Config contains options for a Conn. 45 | type Config struct { 46 | // Network namespace the Nfqueue needs to operate in. If set to 0 (default), 47 | // no network namespace will be entered. 48 | NetNS int 49 | 50 | // Queue this Nfqueue socket will be assigned to 51 | NfQueue uint16 52 | // Maximum number of packages within the Nfqueue. 53 | // If not set or set to 0, the kernel default (1024) will be used. 54 | MaxQueueLen uint32 55 | 56 | // Only used in combination with NfQnlCopyPacket. 57 | MaxPacketLen uint32 58 | 59 | // Specifies how the kernel handles a packet in the nfqueue queue. 60 | Copymode uint8 61 | 62 | // Optional flags for this Nfqueue socket. 63 | Flags uint32 64 | 65 | // AfFamily for this Nfqueue socket. 66 | AfFamily uint8 67 | 68 | // Deprecated: Cancel the context passed to RegisterWithErrorFunc() or Register() 69 | // to remove the hook from the nfqueue gracefully. 70 | ReadTimeout time.Duration 71 | 72 | // Time till a write action times out - only available for Go >= 1.12 73 | WriteTimeout time.Duration 74 | 75 | // Interface to log internals. 76 | Logger Logger 77 | } 78 | 79 | // Various errors 80 | var ( 81 | ErrRecvMsg = errors.New("received error message") 82 | ErrUnexpMsg = errors.New("received unexpected message from kernel") 83 | ErrInvFlag = errors.New("invalid Flag") 84 | ErrNotLinux = errors.New("not implemented for OS other than linux") 85 | ErrInvalidVerdict = errors.New("invalid verdict") 86 | ) 87 | 88 | // nfLogSubSysQueue the netlink subsystem we will query 89 | const nfnlSubSysQueue = 0x03 90 | 91 | const ( 92 | nfQaUnspec = iota 93 | nfQaPacketHdr 94 | nfQaVerdictHdr /* nfqnl_msg_verdict_hrd */ 95 | nfQaMark /* __u32 nfmark */ 96 | nfQaTimestamp /* nfqnl_msg_packet_timestamp */ 97 | nfQaIfIndexInDev /* __u32 ifindex */ 98 | nfQaIfIndexOutDev /* __u32 ifindex */ 99 | nfQaIfIndexPhysInDev /* __u32 ifindex */ 100 | nfQaIfIndexPhysOutDev /* __u32 ifindex */ 101 | nfQaHwAddr /* nfqnl_msg_packet_hw */ 102 | nfQaPayload /* opaque data payload */ 103 | nfQaCt /* nf_conntrack_netlink.h */ 104 | nfQaCtInfo /* enum ip_conntrack_info */ 105 | nfQaCapLen /* __u32 length of captured packet */ 106 | nfQaSkbInfo /* __u32 skb meta information */ 107 | nfQaExp /* nf_conntrack_netlink.h */ 108 | nfQaUID /* __u32 sk uid */ 109 | nfQaGID /* __u32 sk gid */ 110 | nfQaSecCtx /* security context string */ 111 | nfQaVLAN /* nested attribute: packet vlan info */ 112 | nfQaL2HDR /* full L2 header */ 113 | nfQaPriority /* skb->priority */ 114 | ) 115 | 116 | const ( 117 | _ = iota 118 | nfQaCfgCmd /* nfqnl_msg_config_cmd */ 119 | nfQaCfgParams /* nfqnl_msg_config_params */ 120 | nfQaCfgQueueMaxLen /* __u32 */ 121 | nfQaCfgMask /* identify which flags to change */ 122 | nfQaCfgFlags /* value of these flags (__u32) */ 123 | ) 124 | 125 | const ( 126 | _ = iota 127 | nfUlnlCfgCmdBind 128 | nfUlnlCfgCmdUnbind 129 | nfUlnlCfgCmdPfBind 130 | nfUlnlCfgCmdPfUnbind 131 | ) 132 | 133 | const ( 134 | nfQnlMsgPacket = iota 135 | nfQnlMsgVerdict /* verdict from userspace to kernel */ 136 | nfQnlMsgConfig /* connect to a particular queue */ 137 | nfQnlMsgVerdictBatch /* batch from userspace to kernel */ 138 | 139 | ) 140 | 141 | // Various configuration flags 142 | const ( 143 | NfQaCfgFlagFailOpen = (1 << iota) 144 | NfQaCfgFlagConntrack = (1 << iota) 145 | NfQaCfgFlagGSO = (1 << iota) 146 | NfQaCfgFlagUIDGid = (1 << iota) 147 | NfQaCfgFlagSecCx = (1 << iota) 148 | nfQaCfgFlagMax = (1 << iota) 149 | ) 150 | 151 | // copy modes 152 | const ( 153 | NfQnlCopyNone = iota 154 | NfQnlCopyMeta 155 | NfQnlCopyPacket 156 | ) 157 | 158 | // Verdicts 159 | const ( 160 | NfDrop = iota 161 | NfAccept 162 | NfStolen 163 | NfQeueue 164 | NfRepeat 165 | ) 166 | 167 | // conntrack attributes 168 | // include/uapi/linux/netfilter/nfnetlink_conntrack.h 169 | const ( 170 | ctaMark = 8 171 | ctaLabels = 22 // CTA_LABELS 172 | ) 173 | 174 | // kernelDefaultMaxQueueLen is the default maximum queue length used by the kernel 175 | const kernelDefaultMaxQueueLen = 1024 176 | -------------------------------------------------------------------------------- /nfqueue.go: -------------------------------------------------------------------------------- 1 | package nfqueue 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | "sync" 8 | "time" 9 | 10 | "github.com/florianl/go-nfqueue/v2/internal/unix" 11 | 12 | "github.com/mdlayher/netlink" 13 | ) 14 | 15 | var _ Logger = (*devNull)(nil) 16 | 17 | // devNull satisfies the Logger interface. 18 | type devNull struct{} 19 | 20 | func (dn *devNull) Debugf(format string, args ...interface{}) {} 21 | func (dn *devNull) Errorf(format string, args ...interface{}) {} 22 | 23 | // Close the connection to the netfilter queue subsystem 24 | func (nfqueue *Nfqueue) Close() error { 25 | err := nfqueue.Con.Close() 26 | nfqueue.wg.Wait() 27 | return err 28 | } 29 | 30 | // SetVerdictWithMark signals the kernel the next action and the mark for a specified package id 31 | // 32 | // Deprecated: Use SetVerdictWithOption() instead. 33 | func (nfqueue *Nfqueue) SetVerdictWithMark(id uint32, verdict, mark int) error { 34 | return nfqueue.SetVerdictWithOption(id, verdict, WithMark(uint32(mark))) 35 | } 36 | 37 | // SetVerdictWithConnMark signals the kernel the next action and the connmark for a specified package id 38 | // 39 | // Deprecated: Use SetVerdictWithOption() instead. 40 | func (nfqueue *Nfqueue) SetVerdictWithConnMark(id uint32, verdict, mark int) error { 41 | return nfqueue.SetVerdictWithOption(id, verdict, WithConnMark(uint32(mark))) 42 | } 43 | 44 | // SetVerdictWithLabel signals the kernel the next action and the label for a specified package id 45 | // 46 | // Deprecated: Use SetVerdictWithOption() instead. 47 | func (nfqueue *Nfqueue) SetVerdictWithLabel(id uint32, verdict int, label []byte) error { 48 | return nfqueue.SetVerdictWithOption(id, verdict, WithLabel(label)) 49 | } 50 | 51 | // SetVerdictModPacket signals the kernel the next action for an altered packet 52 | // 53 | // Deprecated: Use SetVerdictWithOption() instead. 54 | func (nfqueue *Nfqueue) SetVerdictModPacket(id uint32, verdict int, packet []byte) error { 55 | return nfqueue.SetVerdictWithOption(id, verdict, WithAlteredPacket(packet)) 56 | } 57 | 58 | // SetVerdictModPacketWithMark signals the kernel the next action and mark for an altered packet 59 | // 60 | // Deprecated: Use SetVerdictWithOption() instead. 61 | func (nfqueue *Nfqueue) SetVerdictModPacketWithMark(id uint32, verdict, mark int, packet []byte) error { 62 | return nfqueue.SetVerdictWithOption(id, verdict, WithMark(uint32(mark)), WithAlteredPacket(packet)) 63 | } 64 | 65 | // SetVerdictModPacketWithConnMark signals the kernel the next action and connmark for an altered packet 66 | // 67 | // Deprecated: Use SetVerdictWithOption() instead. 68 | func (nfqueue *Nfqueue) SetVerdictModPacketWithConnMark(id uint32, verdict, mark int, packet []byte) error { 69 | return nfqueue.SetVerdictWithOption(id, verdict, WithConnMark(uint32(mark)), WithAlteredPacket(packet)) 70 | } 71 | 72 | // SetVerdictModPacketWithLabel signals the kernel the next action and label for an altered packet 73 | // 74 | // Deprecated: Use SetVerdictWithOption() instead. 75 | func (nfqueue *Nfqueue) SetVerdictModPacketWithLabel(id uint32, verdict int, label []byte, packet []byte) error { 76 | return nfqueue.SetVerdictWithOption(id, verdict, WithAlteredPacket(packet), WithLabel(label)) 77 | } 78 | 79 | // SetVerdict signals the kernel the next action for a specified package id 80 | func (nfqueue *Nfqueue) SetVerdict(id uint32, verdict int) error { 81 | return nfqueue.setVerdict(id, verdict, false, []byte{}) 82 | } 83 | 84 | // SetVerdictBatch signals the kernel the next action for a batch of packages till id 85 | func (nfqueue *Nfqueue) SetVerdictBatch(id uint32, verdict int) error { 86 | return nfqueue.setVerdict(id, verdict, true, []byte{}) 87 | } 88 | 89 | // SetOption allows to enable or disable netlink socket options. 90 | func (nfqueue *Nfqueue) SetOption(o netlink.ConnOption, enable bool) error { 91 | return nfqueue.Con.SetOption(o, enable) 92 | } 93 | 94 | // Register your own function as callback for a netfilter queue. 95 | // 96 | // The registered callback will stop receiving data if an error 97 | // happened. To handle errors and continue receiving data with the 98 | // registered callback use RegisterWithErrorFunc() instead. 99 | // 100 | // Deprecated: Use RegisterWithErrorFunc() instead. 101 | func (nfqueue *Nfqueue) Register(ctx context.Context, fn HookFunc) error { 102 | return nfqueue.RegisterWithErrorFunc(ctx, fn, func(err error) int { 103 | if opError, ok := err.(*netlink.OpError); ok { 104 | if opError.Timeout() || opError.Temporary() { 105 | return 0 106 | } 107 | } 108 | nfqueue.logger.Errorf("Could not receive message: %v", err) 109 | return 1 110 | }) 111 | } 112 | 113 | // RegisterWithErrorFunc attaches a callback function to a netfilter queue and allows 114 | // custom error handling for errors encountered when reading from the underlying netlink socket. 115 | func (nfqueue *Nfqueue) RegisterWithErrorFunc(ctx context.Context, fn HookFunc, errfn ErrorFunc) error { 116 | // unbinding existing handler (if any) 117 | seq, err := nfqueue.setConfig(unix.AF_UNSPEC, 0, 0, []netlink.Attribute{ 118 | {Type: nfQaCfgCmd, Data: []byte{nfUlnlCfgCmdPfUnbind, 0x0, 0x0, byte(nfqueue.family)}}, 119 | }) 120 | if err != nil { 121 | return fmt.Errorf("could not unbind existing handlers (if any): %w", err) 122 | } 123 | 124 | // binding to family 125 | _, err = nfqueue.setConfig(unix.AF_UNSPEC, seq, 0, []netlink.Attribute{ 126 | {Type: nfQaCfgCmd, Data: []byte{nfUlnlCfgCmdPfBind, 0x0, 0x0, byte(nfqueue.family)}}, 127 | }) 128 | if err != nil { 129 | return fmt.Errorf("could not bind to family %d: %w", nfqueue.family, err) 130 | } 131 | 132 | // binding to the requested queue 133 | _, err = nfqueue.setConfig(uint8(unix.AF_UNSPEC), seq, nfqueue.queue, []netlink.Attribute{ 134 | {Type: nfQaCfgCmd, Data: []byte{nfUlnlCfgCmdBind, 0x0, 0x0, byte(nfqueue.family)}}, 135 | }) 136 | if err != nil { 137 | return fmt.Errorf("could not bind to requested queue %d: %w", nfqueue.queue, err) 138 | } 139 | 140 | // set copy mode and buffer size 141 | data := append(nfqueue.maxPacketLen, nfqueue.copymode) 142 | _, err = nfqueue.setConfig(uint8(unix.AF_UNSPEC), seq, nfqueue.queue, []netlink.Attribute{ 143 | {Type: nfQaCfgParams, Data: data}, 144 | }) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | var attrs []netlink.Attribute 150 | if nfqueue.flags[0] != 0 || nfqueue.flags[1] != 0 || nfqueue.flags[2] != 0 || nfqueue.flags[3] != 0 { 151 | // set flags 152 | attrs = append(attrs, netlink.Attribute{Type: nfQaCfgFlags, Data: nfqueue.flags}) 153 | attrs = append(attrs, netlink.Attribute{Type: nfQaCfgMask, Data: nfqueue.flags}) 154 | } 155 | attrs = append(attrs, netlink.Attribute{Type: nfQaCfgQueueMaxLen, Data: nfqueue.maxQueueLen}) 156 | 157 | _, err = nfqueue.setConfig(uint8(unix.AF_UNSPEC), seq, nfqueue.queue, attrs) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | nfqueue.wg.Add(1) 163 | go func() { 164 | defer nfqueue.wg.Done() 165 | nfqueue.socketCallback(ctx, fn, errfn, seq) 166 | }() 167 | 168 | return nil 169 | } 170 | 171 | // /include/uapi/linux/netfilter/nfnetlink.h:struct nfgenmsg{} res_id is Big Endian 172 | func putExtraHeader(familiy, version uint8, resid uint16) []byte { 173 | buf := make([]byte, 2) 174 | binary.BigEndian.PutUint16(buf, resid) 175 | return append([]byte{familiy, version}, buf...) 176 | } 177 | 178 | func (nfqueue *Nfqueue) setConfig(afFamily uint8, oseq uint32, resid uint16, attrs []netlink.Attribute) (uint32, error) { 179 | cmd, err := netlink.MarshalAttributes(attrs) 180 | if err != nil { 181 | return 0, err 182 | } 183 | data := putExtraHeader(afFamily, unix.NFNETLINK_V0, resid) 184 | data = append(data, cmd...) 185 | req := netlink.Message{ 186 | Header: netlink.Header{ 187 | Type: netlink.HeaderType((nfnlSubSysQueue << 8) | nfQnlMsgConfig), 188 | Flags: netlink.Request | netlink.Acknowledge, 189 | Sequence: oseq, 190 | }, 191 | Data: data, 192 | } 193 | return nfqueue.execute(req) 194 | } 195 | 196 | func (nfqueue *Nfqueue) execute(req netlink.Message) (uint32, error) { 197 | var seq uint32 198 | 199 | reply, e := nfqueue.Con.Execute(req) 200 | if e != nil { 201 | return 0, e 202 | } 203 | 204 | if e := netlink.Validate(req, reply); e != nil { 205 | return 0, e 206 | } 207 | for _, msg := range reply { 208 | if seq != 0 { 209 | return 0, fmt.Errorf("number of received messages: %d: %w", len(reply), ErrUnexpMsg) 210 | } 211 | seq = msg.Header.Sequence 212 | } 213 | 214 | return seq, nil 215 | } 216 | 217 | func parseMsg(log Logger, msg netlink.Message) (Attribute, error) { 218 | a, err := extractAttributes(log, msg.Data) 219 | if err != nil { 220 | return a, err 221 | } 222 | return a, nil 223 | } 224 | 225 | // Nfqueue represents a netfilter queue handler 226 | type Nfqueue struct { 227 | // Con is the pure representation of a netlink socket 228 | Con *netlink.Conn 229 | 230 | logger Logger 231 | 232 | wg sync.WaitGroup 233 | 234 | flags []byte // uint32 235 | maxPacketLen []byte // uint32 236 | family uint8 237 | queue uint16 238 | maxQueueLen []byte // uint32 239 | copymode uint8 240 | 241 | setWriteTimeout func() error 242 | } 243 | 244 | // Logger provides logging functionality. 245 | type Logger interface { 246 | Debugf(format string, args ...interface{}) 247 | Errorf(format string, args ...interface{}) 248 | } 249 | 250 | // Open a connection to the netfilter queue subsystem 251 | func Open(config *Config) (*Nfqueue, error) { 252 | var nfqueue Nfqueue 253 | 254 | if config.Flags >= nfQaCfgFlagMax { 255 | return nil, ErrInvFlag 256 | } 257 | 258 | con, err := netlink.Dial(unix.NETLINK_NETFILTER, &netlink.Config{NetNS: config.NetNS}) 259 | if err != nil { 260 | return nil, err 261 | } 262 | nfqueue.Con = con 263 | // default size of copied packages to userspace 264 | nfqueue.maxPacketLen = []byte{0x00, 0x00, 0x00, 0x00} 265 | binary.BigEndian.PutUint32(nfqueue.maxPacketLen, config.MaxPacketLen) 266 | nfqueue.flags = []byte{0x00, 0x00, 0x00, 0x00} 267 | binary.BigEndian.PutUint32(nfqueue.flags, config.Flags) 268 | nfqueue.queue = config.NfQueue 269 | nfqueue.family = config.AfFamily 270 | 271 | nfqueue.maxQueueLen = []byte{0x00, 0x00, 0x00, 0x00} 272 | if config.MaxQueueLen == 0 { 273 | config.MaxQueueLen = kernelDefaultMaxQueueLen 274 | } 275 | binary.BigEndian.PutUint32(nfqueue.maxQueueLen, config.MaxQueueLen) 276 | 277 | if config.Logger == nil { 278 | nfqueue.logger = new(devNull) 279 | } else { 280 | nfqueue.logger = config.Logger 281 | } 282 | nfqueue.copymode = config.Copymode 283 | 284 | if config.WriteTimeout > 0 { 285 | nfqueue.setWriteTimeout = func() error { 286 | deadline := time.Now().Add(config.WriteTimeout) 287 | return nfqueue.Con.SetWriteDeadline(deadline) 288 | } 289 | } else { 290 | nfqueue.setWriteTimeout = func() error { return nil } 291 | } 292 | 293 | return &nfqueue, nil 294 | } 295 | 296 | func (nfqueue *Nfqueue) setVerdict(id uint32, verdict int, batch bool, attributes []byte) error { 297 | /* 298 | struct nfqnl_msg_verdict_hdr { 299 | __be32 verdict; 300 | __be32 id; 301 | }; 302 | */ 303 | 304 | if verdict != NfDrop && verdict != NfAccept && verdict != NfStolen && verdict != NfQeueue && verdict != NfRepeat { 305 | return ErrInvalidVerdict 306 | } 307 | 308 | buf := make([]byte, 4) 309 | binary.BigEndian.PutUint32(buf, uint32(id)) 310 | verdictData := append([]byte{0x0, 0x0, 0x0, byte(verdict)}, buf...) 311 | cmd, err := netlink.MarshalAttributes([]netlink.Attribute{ 312 | {Type: nfQaVerdictHdr, Data: verdictData}, 313 | }) 314 | if err != nil { 315 | return err 316 | } 317 | data := putExtraHeader(nfqueue.family, unix.NFNETLINK_V0, nfqueue.queue) 318 | data = append(data, cmd...) 319 | data = append(data, attributes...) 320 | req := netlink.Message{ 321 | Header: netlink.Header{ 322 | Flags: netlink.Request, 323 | Sequence: 0, 324 | }, 325 | Data: data, 326 | } 327 | if batch { 328 | req.Header.Type = netlink.HeaderType((nfnlSubSysQueue << 8) | nfQnlMsgVerdictBatch) 329 | } else { 330 | req.Header.Type = netlink.HeaderType((nfnlSubSysQueue << 8) | nfQnlMsgVerdict) 331 | } 332 | 333 | if err := nfqueue.setWriteTimeout(); err != nil { 334 | nfqueue.logger.Errorf("could not set write timeout: %v\n", err) 335 | } 336 | _, sErr := nfqueue.Con.Send(req) 337 | return sErr 338 | } 339 | 340 | func (nfqueue *Nfqueue) socketCallback(ctx context.Context, fn HookFunc, errfn ErrorFunc, seq uint32) { 341 | defer func() { 342 | // unbinding from queue 343 | _, err := nfqueue.setConfig(uint8(unix.AF_UNSPEC), seq, nfqueue.queue, []netlink.Attribute{ 344 | {Type: nfQaCfgCmd, Data: []byte{nfUlnlCfgCmdUnbind, 0x0, 0x0, byte(nfqueue.family)}}, 345 | }) 346 | if err != nil { 347 | nfqueue.logger.Errorf("Could not unbind from queue: %v", err) 348 | } 349 | }() 350 | 351 | nfqueue.wg.Add(1) 352 | go func() { 353 | defer nfqueue.wg.Done() 354 | 355 | // block until context is done 356 | <-ctx.Done() 357 | // Set the read deadline to a point in the past to interrupt 358 | // possible blocking Receive() calls. 359 | nfqueue.Con.SetReadDeadline(time.Now().Add(-1 * time.Second)) 360 | }() 361 | 362 | for { 363 | if err := ctx.Err(); err != nil { 364 | nfqueue.logger.Errorf("Stop receiving nfqueue messages: %v", err) 365 | return 366 | } 367 | replys, err := nfqueue.Con.Receive() 368 | if err != nil { 369 | if ret := errfn(err); ret != 0 { 370 | return 371 | } 372 | continue 373 | } 374 | for _, msg := range replys { 375 | if msg.Header.Type == netlink.Done { 376 | // this is the last message of a batch 377 | // continue to receive messages 378 | break 379 | } 380 | m, err := parseMsg(nfqueue.logger, msg) 381 | if err != nil { 382 | nfqueue.logger.Errorf("Could not parse message: %v", err) 383 | continue 384 | } 385 | if ret := fn(m); ret != 0 { 386 | return 387 | } 388 | } 389 | } 390 | } 391 | --------------------------------------------------------------------------------