├── .github └── workflows │ ├── go.yml │ └── sourcegraph-lsif-indexing.yml ├── LICENSE ├── README.md ├── attribute.go ├── attribute_test.go ├── doc.go ├── example_test.go ├── go.mod ├── go.sum ├── internal └── unix │ ├── doc.go │ ├── types_linux.go │ └── types_other.go ├── nflog.go ├── nflog_linux_integration_test.go ├── nflog_test.go └── types.go /.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.24.x, 1.25.x] 14 | platform: [ubuntu-latest, macos-latest, windows-latest] 15 | exclude: 16 | # There is no arm64 version of Go for darwin 17 | - go-version: "1.13.x" 18 | platform: "macos-latest" 19 | runs-on: ${{ matrix.platform }} 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | 24 | - name: Install Go 25 | uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | 29 | - name: Ensure gofmt formatting 30 | if: matrix.platform == 'ubuntu-latest' && startsWith(matrix.go-version, '1.25') 31 | run: | 32 | [ "$(gofmt -l $(find . -name '*.go') 2>&1)" = "" ] 33 | 34 | - name: Download Go dependencies 35 | env: 36 | GOPROXY: "https://proxy.golang.org" 37 | run: go mod download 38 | 39 | - name: Test with -race 40 | run: go test -race -count=1 ./... 41 | 42 | - name: Integration test 43 | if: matrix.platform == 'ubuntu-latest' && startsWith(matrix.go-version, '1.25') 44 | run: go test -v -tags integration -exec=sudo -count=1 ./... 45 | 46 | - name: staticcheck.io 47 | if: matrix.platform == 'ubuntu-latest' && startsWith(matrix.go-version, '1.25') 48 | uses: dominikh/staticcheck-action@v1.3.1 49 | with: 50 | version: "2025.1" 51 | install-go: false 52 | cache-key: ${{ matrix.go }} 53 | -------------------------------------------------------------------------------- /.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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-nflog [![PkgGoDev](https://pkg.go.dev/badge/github.com/florianl/go-nflog/v2)](https://pkg.go.dev/github.com/florianl/go-nflog/v2) [![Go Report Card](https://goreportcard.com/badge/github.com/florianl/go-nflog/v2)](https://goreportcard.com/report/github.com/florianl/go-nflog/v2) [![Go](https://github.com/florianl/go-nflog/actions/workflows/go.yml/badge.svg)](https://github.com/florianl/go-nflog/actions/workflows/go.yml) 2 | ============ 3 | 4 | This is `go-nflog` 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 log subsystem of the [Linux kernel](https://www.kernel.org). 5 | 6 | ## Example 7 | 8 | ```golang 9 | func main() { 10 | // Send outgoing pings to nflog group 100 11 | // # sudo iptables -I OUTPUT -p icmp -j NFLOG --nflog-group 100 12 | 13 | //Set configuration parameters 14 | config := nflog.Config{ 15 | Group: 100, 16 | Copymode: nflog.CopyPacket, 17 | } 18 | 19 | nf, err := nflog.Open(&config) 20 | if err != nil { 21 | fmt.Fprintln(os.Stderr, "could not open nflog socket:", err) 22 | return 23 | } 24 | defer nf.Close() 25 | 26 | // Increase socket read buffer size to 512kB. 27 | if err := nf.Con.SetReadBuffer(512 * 1024); err != nil { 28 | fmt.Fprintf(os.Stderr, "failed to set read buffer: %v", err) 29 | return 30 | } 31 | 32 | // Avoid receiving ENOBUFS errors. 33 | if err := nf.SetOption(netlink.NoENOBUFS, true); err != nil { 34 | fmt.Fprintf(os.Stderr, "failed to set netlink option %v: %v", 35 | netlink.NoENOBUFS, err) 36 | return 37 | } 38 | 39 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 40 | defer cancel() 41 | 42 | // hook that is called for every received packet by the nflog group 43 | hook := func(attrs nflog.Attribute) int { 44 | // Just print out the payload of the nflog packet 45 | fmt.Fprintf(os.Stdout, "%#v\n", attrs.Payload) 46 | return 0 47 | } 48 | 49 | // errFunc that is called for every error on the registered hook 50 | errFunc := func(e error) int { 51 | // Just log the error and return 0 to continue receiving packets 52 | fmt.Fprintf(os.Stderr, "received error on hook: %v", e) 53 | return 0 54 | } 55 | 56 | // Register your function to listen on nflog group 100 57 | err = nf.RegisterWithErrorFunc(ctx, hook, errFunc) 58 | if err != nil { 59 | fmt.Fprintf(os.Stderr, "failed to register hook function: %v", err) 60 | return 61 | } 62 | 63 | // Block till the context expires 64 | <-ctx.Done() 65 | } 66 | ``` 67 | 68 | ## Privileges 69 | 70 | This package processes information directly from the kernel and therefore it requires special privileges. You 71 | can provide this privileges by adjusting the `CAP_NET_ADMIN` capabilities. 72 | ``` 73 | setcap 'cap_net_admin=+ep' /your/executable 74 | ``` 75 | 76 | For documentation and more examples please take a look at [![PkgGoDev](https://pkg.go.dev/badge/github.com/florianl/go-nflog)](https://pkg.go.dev/github.com/florianl/go-nflog) 77 | 78 | ## Requirements 79 | 80 | * A version of Go that is [supported by upstream](https://golang.org/doc/devel/release.html#policy) 81 | -------------------------------------------------------------------------------- /attribute.go: -------------------------------------------------------------------------------- 1 | package nflog 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/florianl/go-nflog/v2/internal/unix" 10 | 11 | "github.com/mdlayher/netlink" 12 | ) 13 | 14 | func extractAttribute(a *Attribute, logger Logger, data []byte) error { 15 | ad, err := netlink.NewAttributeDecoder(data) 16 | if err != nil { 17 | return err 18 | } 19 | ad.ByteOrder = nativeEndian 20 | for ad.Next() { 21 | switch ad.Type() { 22 | case nfUlaAttrPacketHdr: 23 | hwProtocol := binary.BigEndian.Uint16(ad.Bytes()[:2]) 24 | a.HwProtocol = &hwProtocol 25 | hook := uint8(ad.Bytes()[2]) 26 | a.Hook = &hook 27 | case nfUlaAttrMark: 28 | ad.ByteOrder = binary.BigEndian 29 | mark := ad.Uint32() 30 | a.Mark = &mark 31 | ad.ByteOrder = nativeEndian 32 | case nfUlaAttrTimestamp: 33 | var sec, usec int64 34 | r := bytes.NewReader(ad.Bytes()[:8]) 35 | if err := binary.Read(r, binary.BigEndian, &sec); err != nil { 36 | return err 37 | } 38 | r = bytes.NewReader(ad.Bytes()[8:]) 39 | if err := binary.Read(r, binary.BigEndian, &usec); err != nil { 40 | return err 41 | } 42 | timestamp := time.Unix(sec, usec*1000) 43 | a.Timestamp = ×tamp 44 | case nfUlaAttrIfindexIndev: 45 | ad.ByteOrder = binary.BigEndian 46 | inDev := ad.Uint32() 47 | a.InDev = &inDev 48 | ad.ByteOrder = nativeEndian 49 | case nfUlaAttrIfindexOutdev: 50 | ad.ByteOrder = binary.BigEndian 51 | outDev := ad.Uint32() 52 | a.OutDev = &outDev 53 | ad.ByteOrder = nativeEndian 54 | case nfUlaAttrIfindexPhysIndev: 55 | ad.ByteOrder = binary.BigEndian 56 | physInDev := ad.Uint32() 57 | a.PhysInDev = &physInDev 58 | ad.ByteOrder = nativeEndian 59 | case nfUlaAttrIfindexPhysOutdev: 60 | ad.ByteOrder = binary.BigEndian 61 | physOutDev := ad.Uint32() 62 | a.PhysOutDev = &physOutDev 63 | ad.ByteOrder = nativeEndian 64 | case nfUlaAttrHwaddr: 65 | hwAddrLen := binary.BigEndian.Uint16(ad.Bytes()[:2]) 66 | hwAddr := (ad.Bytes())[4 : 4+hwAddrLen] 67 | a.HwAddr = &hwAddr 68 | case nfUlaAttrPayload: 69 | payload := ad.Bytes() 70 | a.Payload = &payload 71 | case nfUlaAttrPrefix: 72 | prefix := ad.String() 73 | a.Prefix = &prefix 74 | case nfUlaAttrUID: 75 | ad.ByteOrder = binary.BigEndian 76 | uid := ad.Uint32() 77 | a.UID = &uid 78 | ad.ByteOrder = nativeEndian 79 | case nfUlaAttrSeq: 80 | ad.ByteOrder = binary.BigEndian 81 | seq := ad.Uint32() 82 | a.Seq = &seq 83 | ad.ByteOrder = nativeEndian 84 | case nfUlaAttrSeqGlobal: 85 | ad.ByteOrder = binary.BigEndian 86 | seqGlobal := ad.Uint32() 87 | a.SeqGlobal = &seqGlobal 88 | ad.ByteOrder = nativeEndian 89 | case nfUlaAttrGID: 90 | ad.ByteOrder = binary.BigEndian 91 | gid := ad.Uint32() 92 | a.GID = &gid 93 | ad.ByteOrder = nativeEndian 94 | case nfUlaAttrHwType: 95 | ad.ByteOrder = binary.BigEndian 96 | hwType := ad.Uint16() 97 | a.HwType = &hwType 98 | ad.ByteOrder = nativeEndian 99 | case nfUlaAttrHwHeader: 100 | hwHeader := ad.Bytes() 101 | a.HwHeader = &hwHeader 102 | case nfUlaAttrHwLen: 103 | ad.ByteOrder = binary.BigEndian 104 | hwLen := ad.Uint16() 105 | a.HwLen = &hwLen 106 | ad.ByteOrder = nativeEndian 107 | case nfUlaAttrCt: 108 | ad.ByteOrder = binary.BigEndian 109 | ct := ad.Bytes() 110 | a.Ct = &ct 111 | ad.ByteOrder = nativeEndian 112 | case nfUlaAttrCtInfo: 113 | ad.ByteOrder = binary.BigEndian 114 | ctInfo := ad.Uint32() 115 | a.CtInfo = &ctInfo 116 | ad.ByteOrder = nativeEndian 117 | case nfulaAttrL2Hdr: 118 | ad.ByteOrder = binary.BigEndian 119 | l2hdr := ad.Bytes() 120 | a.Layer2Hdr = &l2hdr 121 | ad.ByteOrder = nativeEndian 122 | case nfUlaAttrVlan: 123 | ad.ByteOrder = binary.BigEndian 124 | info := &VLAN{} 125 | if err := extractVLAN(ad.Bytes(), info); err != nil { 126 | return err 127 | } 128 | a.VLAN = info 129 | ad.ByteOrder = nativeEndian 130 | default: 131 | logger.Debugf("Unknown attribute: %d %v\n", ad.Type(), ad.Bytes()) 132 | } 133 | } 134 | 135 | return ad.Err() 136 | } 137 | 138 | func checkHeader(data []byte) int { 139 | if (data[0] == unix.AF_INET || data[0] == unix.AF_INET6 || data[0] == unix.AF_BRIDGE) && data[1] == unix.NFNETLINK_V0 { 140 | return 4 141 | } 142 | return 0 143 | } 144 | 145 | func extractAttributes(logger Logger, msg []byte) (Attribute, error) { 146 | attrs := Attribute{} 147 | 148 | offset := checkHeader(msg[:2]) 149 | if err := extractAttribute(&attrs, logger, msg[offset:]); err != nil { 150 | return attrs, err 151 | } 152 | return attrs, nil 153 | } 154 | 155 | const ( 156 | _ = iota 157 | nfulaVLANProto /* __be16 */ 158 | nfulaVLANTCI /* __be16 */ 159 | ) 160 | 161 | func extractVLAN(data []byte, info *VLAN) error { 162 | ad, err := netlink.NewAttributeDecoder(data) 163 | if err != nil { 164 | return err 165 | } 166 | for ad.Next() { 167 | switch ad.Type() { 168 | case nfulaVLANProto: 169 | info.Proto = ad.Uint16() 170 | case nfulaVLANTCI: 171 | info.TCI = ad.Uint16() 172 | default: 173 | return fmt.Errorf("extractVLAN()\t%d\n\t%v", ad.Type(), ad.Bytes()) 174 | } 175 | } 176 | return nil 177 | } 178 | -------------------------------------------------------------------------------- /attribute_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package nflog 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | "golang.org/x/sys/unix" 12 | ) 13 | 14 | func pUint8(v uint8) *uint8 { 15 | return &v 16 | } 17 | 18 | func pUint16(v uint16) *uint16 { 19 | return &v 20 | } 21 | 22 | func pUint32(v uint32) *uint32 { 23 | return &v 24 | } 25 | 26 | func pString(v string) *string { 27 | return &v 28 | } 29 | 30 | func pBytes(v []byte) *[]byte { 31 | return &v 32 | } 33 | 34 | func pTime(sec, usec int64) *time.Time { 35 | t := time.Unix(sec, usec) 36 | return &t 37 | } 38 | 39 | func TestExtractAttributes(t *testing.T) { 40 | tests := map[string]struct { 41 | data []byte 42 | a Attribute 43 | }{ 44 | "SimplePing": { 45 | data: []byte{0x02, 0x00, 0x00, 0x64, 0x08, 0x00, 0x01, 0x00, 0x08, 0x00, 0x03, 0x00, 0x05, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x03, 0x08, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x08, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x58, 0x00, 0x09, 0x00, 0x45, 0x00, 0x00, 0x54, 0x3d, 0x98, 0x40, 0x00, 0x40, 0x01, 0xf0, 0x52, 0x0a, 0x00, 0x00, 0xbd, 0x01, 0x01, 0x01, 0x01, 0x08, 0x00, 0xfe, 0x4b, 0x46, 0xd2, 0x00, 0x02, 0x4e, 0x01, 0x85, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xb0, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37}, 46 | a: Attribute{ 47 | Hook: pUint8(3), Prefix: pString(""), HwProtocol: pUint16(unix.ETH_P_IP), UID: pUint32(0x03e8), GID: pUint32(0x03e8), OutDev: pUint32(0x03), 48 | Payload: pBytes([]byte{0x45, 0x00, 0x00, 0x54, 0x3d, 0x98, 0x40, 0x00, 0x40, 0x01, 0xf0, 0x52, 0x0a, 0x00, 0x00, 0xbd, 0x01, 0x01, 0x01, 0x01, 0x08, 0x00, 0xfe, 0x4b, 0x46, 0xd2, 0x00, 0x02, 0x4e, 0x01, 0x85, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xb0, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37}), 49 | }, 50 | }, 51 | "IPv6 TCP packet with conntrack data": { 52 | data: []byte{0x0a, 0x00, 0x00, 0x7b, 0x08, 0x00, 0x01, 0x00, 0x86, 0xdd, 0x01, 0x00, 0x11, 0x00, 0x0a, 0x00, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x3a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x08, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x1b, 0x21, 0x9f, 0x57, 0x31, 0x00, 0x00, 0x06, 0x00, 0x0f, 0x00, 0x00, 0x01, 0x00, 0x00, 0x06, 0x00, 0x11, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x12, 0x00, 0x10, 0x00, 0xce, 0x7e, 0x7c, 0x41, 0x0f, 0xdb, 0x00, 0x1b, 0x21, 0x9f, 0x57, 0x31, 0x86, 0xdd, 0x00, 0x00, 0x14, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x9d, 0x2f, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x64, 0x04, 0x08, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x21, 0x08, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x21, 0xe4, 0x00, 0x12, 0x80, 0x4c, 0x00, 0x01, 0x80, 0x2c, 0x00, 0x01, 0x80, 0x14, 0x00, 0x03, 0x00, 0x20, 0x01, 0x04, 0x70, 0xb7, 0x50, 0x00, 0x01, 0xf5, 0x18, 0x96, 0xf6, 0xe8, 0xae, 0x07, 0x2d, 0x14, 0x00, 0x04, 0x00, 0x2a, 0x07, 0x57, 0x41, 0x00, 0x00, 0x11, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x1c, 0x00, 0x02, 0x80, 0x05, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0xfe, 0x0f, 0x00, 0x00, 0x06, 0x00, 0x03, 0x00, 0x01, 0xbb, 0x00, 0x00, 0x4c, 0x00, 0x02, 0x80, 0x2c, 0x00, 0x01, 0x80, 0x14, 0x00, 0x03, 0x00, 0x2a, 0x07, 0x57, 0x41, 0x00, 0x00, 0x11, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x04, 0x00, 0x20, 0x01, 0x04, 0x70, 0xb7, 0x50, 0x00, 0x01, 0xf5, 0x18, 0x96, 0xf6, 0xe8, 0xae, 0x07, 0x2d, 0x1c, 0x00, 0x02, 0x80, 0x05, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0x01, 0xbb, 0x00, 0x00, 0x06, 0x00, 0x03, 0x00, 0xfe, 0x0f, 0x00, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x18, 0x9d, 0x23, 0xd6, 0x08, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x2c, 0x30, 0x00, 0x04, 0x80, 0x2c, 0x00, 0x01, 0x80, 0x05, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, 0x06, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0x23, 0x00, 0x00, 0x00, 0x06, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00, 0x08, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x09, 0x00, 0x60, 0x0e, 0xb3, 0xfe, 0x00, 0x20, 0x06, 0x37, 0x20, 0x01, 0x04, 0x70, 0xb7, 0x50, 0x00, 0x01, 0xf5, 0x18, 0x96, 0xf6, 0xe8, 0xae, 0x07, 0x2d, 0x2a, 0x07, 0x57, 0x41, 0x00, 0x00, 0x11, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfe, 0x0f, 0x01, 0xbb, 0x74, 0xde, 0x6b, 0xcb, 0x4a, 0xf3, 0x26, 0xdc, 0x80, 0x10, 0x07, 0xdc, 0x0b, 0xfa, 0x00, 0x00, 0x01, 0x01, 0x08, 0x0a, 0x4d, 0xcd, 0xeb, 0x52, 0x87, 0x98, 0x65, 0x7b}, 53 | a: Attribute{ 54 | Hook: pUint8(1), Prefix: pString("testprefix: "), HwProtocol: pUint16(unix.ETH_P_IPV6), UID: pUint32(33), GID: pUint32(33), InDev: pUint32(0x02), 55 | Timestamp: pTime(1604136762, 156676000), HwType: pUint16(1), HwLen: pUint16(14), 56 | HwAddr: pBytes([]byte{0x00, 0x1b, 0x21, 0x9f, 0x57, 0x31}), 57 | HwHeader: pBytes([]byte{0xce, 0x7e, 0x7c, 0x41, 0x0f, 0xdb, 0x00, 0x1b, 0x21, 0x9f, 0x57, 0x31, 0x86, 0xdd}), 58 | CtInfo: pUint32(0), 59 | Ct: pBytes([]byte{0x4c, 0x00, 0x01, 0x80, 0x2c, 0x00, 0x01, 0x80, 0x14, 0x00, 0x03, 0x00, 0x20, 0x01, 0x04, 0x70, 0xb7, 0x50, 0x00, 0x01, 0xf5, 0x18, 0x96, 0xf6, 0xe8, 0xae, 0x07, 0x2d, 0x14, 0x00, 0x04, 0x00, 0x2a, 0x07, 0x57, 0x41, 0x00, 0x00, 0x11, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x1c, 0x00, 0x02, 0x80, 0x05, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0xfe, 0x0f, 0x00, 0x00, 0x06, 0x00, 0x03, 0x00, 0x01, 0xbb, 0x00, 0x00, 0x4c, 0x00, 0x02, 0x80, 0x2c, 0x00, 0x01, 0x80, 0x14, 0x00, 0x03, 0x00, 0x2a, 0x07, 0x57, 0x41, 0x00, 0x00, 0x11, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x14, 0x00, 0x04, 0x00, 0x20, 0x01, 0x04, 0x70, 0xb7, 0x50, 0x00, 0x01, 0xf5, 0x18, 0x96, 0xf6, 0xe8, 0xae, 0x07, 0x2d, 0x1c, 0x00, 0x02, 0x80, 0x05, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x00, 0x02, 0x00, 0x01, 0xbb, 0x00, 0x00, 0x06, 0x00, 0x03, 0x00, 0xfe, 0x0f, 0x00, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x18, 0x9d, 0x23, 0xd6, 0x08, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x08, 0x00, 0x07, 0x00, 0x00, 0x00, 0x01, 0x2c, 0x30, 0x00, 0x04, 0x80, 0x2c, 0x00, 0x01, 0x80, 0x05, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x05, 0x00, 0x02, 0x00, 0x06, 0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x00, 0x04, 0x00, 0x23, 0x00, 0x00, 0x00, 0x06, 0x00, 0x05, 0x00, 0x33, 0x00, 0x00, 0x00}), 60 | Payload: pBytes([]byte{0x60, 0x0e, 0xb3, 0xfe, 0x00, 0x20, 0x06, 0x37, 0x20, 0x01, 0x04, 0x70, 0xb7, 0x50, 0x00, 0x01, 0xf5, 0x18, 0x96, 0xf6, 0xe8, 0xae, 0x07, 0x2d, 0x2a, 0x07, 0x57, 0x41, 0x00, 0x00, 0x11, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfe, 0x0f, 0x01, 0xbb, 0x74, 0xde, 0x6b, 0xcb, 0x4a, 0xf3, 0x26, 0xdc, 0x80, 0x10, 0x07, 0xdc, 0x0b, 0xfa, 0x00, 0x00, 0x01, 0x01, 0x08, 0x0a, 0x4d, 0xcd, 0xeb, 0x52, 0x87, 0x98, 0x65, 0x7b}), 61 | }, 62 | }, 63 | } 64 | for name, tc := range tests { 65 | t.Run(name, func(t *testing.T) { 66 | a, err := extractAttributes(nil, tc.data) 67 | if err != nil { 68 | t.Fatalf("Unexpected error: %v", err) 69 | } 70 | if diff := cmp.Diff(tc.a, a); diff != "" { 71 | t.Fatalf("unexpected number of request messages (-want +got):\n%s", diff) 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package nflog provides an API to interact with the log 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 nflog 10 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package nflog_test 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | "time" 11 | 12 | "github.com/florianl/go-nflog/v2" 13 | ) 14 | 15 | func ExampleNflog_Register() { 16 | // Send outgoing pings to nflog group 100 17 | // # sudo iptables -I OUTPUT -p icmp -j NFLOG --nflog-group 100 18 | 19 | // Set configuration parameters 20 | config := nflog.Config{ 21 | Group: 100, 22 | Copymode: nflog.CopyPacket, 23 | } 24 | 25 | nf, err := nflog.Open(&config) 26 | if err != nil { 27 | fmt.Println("could not open nflog socket:", err) 28 | return 29 | } 30 | defer nf.Close() 31 | 32 | // Increase socket read buffer size to 512kB. 33 | if err := nf.Con.SetReadBuffer(512 * 1024); err != nil { 34 | fmt.Fprintf(os.Stderr, "failed to set read buffer: %v", err) 35 | return 36 | } 37 | 38 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 39 | defer cancel() 40 | 41 | // hook that is called for every received packet by the nflog group 42 | hook := func(attrs nflog.Attribute) int { 43 | // Just print out the payload of the nflog packet 44 | fmt.Fprintf(os.Stdout, "%#v\n", attrs.Payload) 45 | return 0 46 | } 47 | 48 | // errFunc that is called for every error on the registered hook 49 | errFunc := func(e error) int { 50 | // Just log the error and return 0 to continue receiving packets 51 | fmt.Fprintf(os.Stderr, "received error on hook: %v", e) 52 | return 0 53 | } 54 | 55 | // Register your function to listen on nflog group 100 56 | err = nf.RegisterWithErrorFunc(ctx, hook, errFunc) 57 | if err != nil { 58 | fmt.Fprintf(os.Stderr, "failed to register hook function: %v", err) 59 | return 60 | } 61 | 62 | // Block till the context expires 63 | <-ctx.Done() 64 | } 65 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/florianl/go-nflog/v2 2 | 3 | require ( 4 | github.com/google/go-cmp v0.6.0 5 | github.com/mdlayher/netlink v1.7.1 6 | golang.org/x/sys v0.30.0 7 | ) 8 | 9 | require ( 10 | github.com/josharian/native v1.1.0 // indirect 11 | github.com/mdlayher/socket v0.4.0 // indirect 12 | golang.org/x/net v0.35.0 // indirect 13 | golang.org/x/sync v0.11.0 // indirect 14 | ) 15 | 16 | go 1.18 17 | -------------------------------------------------------------------------------- /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.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTmg= 6 | github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ= 7 | github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw= 8 | github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc= 9 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 10 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 11 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 12 | golang.org/x/sync v0.11.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 | -------------------------------------------------------------------------------- /internal/unix/doc.go: -------------------------------------------------------------------------------- 1 | // Package unix maps constants from golang.org/x/sys/unix to local constants and makes 2 | // them available for other platforms as well. 3 | package unix 4 | -------------------------------------------------------------------------------- /internal/unix/types_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package unix 5 | 6 | import linux "golang.org/x/sys/unix" 7 | 8 | // various constants 9 | const ( 10 | NETLINK_NETFILTER = linux.NETLINK_NETFILTER 11 | NFNETLINK_V0 = linux.NFNETLINK_V0 12 | AF_UNSPEC = linux.AF_UNSPEC 13 | AF_INET = linux.AF_INET 14 | AF_INET6 = linux.AF_INET6 15 | AF_BRIDGE = linux.AF_BRIDGE 16 | ) 17 | -------------------------------------------------------------------------------- /internal/unix/types_other.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package unix 5 | 6 | // various constants 7 | const ( 8 | NETLINK_NETFILTER = 0xc 9 | NFNETLINK_V0 = 0x0 10 | AF_UNSPEC = 0x0 11 | AF_INET = 0x2 12 | AF_INET6 = 0xa 13 | AF_BRIDGE = 0x7 14 | ) 15 | -------------------------------------------------------------------------------- /nflog.go: -------------------------------------------------------------------------------- 1 | package nflog 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | "sync" 8 | "time" 9 | "unsafe" 10 | 11 | "github.com/florianl/go-nflog/v2/internal/unix" 12 | 13 | "github.com/mdlayher/netlink" 14 | ) 15 | 16 | // Nflog represents a netfilter log handler 17 | type Nflog struct { 18 | // Con is the pure representation of a netlink socket 19 | Con *netlink.Conn 20 | 21 | logger Logger 22 | 23 | wg sync.WaitGroup 24 | 25 | flags []byte // uint16 26 | bufsize []byte // uint32 27 | qthresh []byte // uint32 28 | timeout []byte // uint32 29 | group uint16 30 | copyMode uint8 31 | settings uint16 32 | } 33 | 34 | var _ Logger = (*devNull)(nil) 35 | 36 | // devNull satisfies the Logger interface. 37 | type devNull struct{} 38 | 39 | func (dn *devNull) Debugf(format string, args ...interface{}) {} 40 | func (dn *devNull) Errorf(format string, args ...interface{}) {} 41 | 42 | // for detailes see https://github.com/tensorflow/tensorflow/blob/master/tensorflow/go/tensor.go#L488-L505 43 | var nativeEndian binary.ByteOrder 44 | 45 | func init() { 46 | buf := [2]byte{} 47 | *(*uint16)(unsafe.Pointer(&buf[0])) = uint16(0xABCD) 48 | 49 | switch buf { 50 | case [2]byte{0xCD, 0xAB}: 51 | nativeEndian = binary.LittleEndian 52 | case [2]byte{0xAB, 0xCD}: 53 | nativeEndian = binary.BigEndian 54 | default: 55 | panic("Could not determine native endianness.") 56 | } 57 | } 58 | 59 | // Open a connection to the netfilter log subsystem 60 | func Open(config *Config) (*Nflog, error) { 61 | var nflog Nflog 62 | 63 | if config == nil { 64 | config = &Config{} 65 | } 66 | 67 | if err := checkFlags(config.Flags); err != nil { 68 | return nil, err 69 | } 70 | 71 | if config.Copymode != CopyNone && config.Copymode != CopyMeta && config.Copymode != CopyPacket { 72 | return nil, ErrCopyMode 73 | } 74 | 75 | con, err := netlink.Dial(unix.NETLINK_NETFILTER, &netlink.Config{NetNS: config.NetNS}) 76 | if err != nil { 77 | return nil, err 78 | } 79 | nflog.Con = con 80 | 81 | if config.Logger == nil { 82 | nflog.logger = new(devNull) 83 | } else { 84 | nflog.logger = config.Logger 85 | } 86 | 87 | nflog.flags = []byte{0x00, 0x00} 88 | binary.BigEndian.PutUint16(nflog.flags, config.Flags) 89 | nflog.timeout = []byte{0x00, 0x00, 0x00, 0x00} 90 | binary.BigEndian.PutUint32(nflog.timeout, config.Timeout) 91 | nflog.bufsize = []byte{0x00, 0x00, 0x00, 0x00} 92 | binary.BigEndian.PutUint32(nflog.bufsize, config.Bufsize) 93 | nflog.qthresh = []byte{0x00, 0x00, 0x00, 0x00} 94 | binary.BigEndian.PutUint32(nflog.qthresh, config.QThresh) 95 | nflog.group = config.Group 96 | nflog.copyMode = config.Copymode 97 | nflog.settings = config.Settings 98 | 99 | return &nflog, nil 100 | } 101 | 102 | func checkFlags(flags uint16) error { 103 | if flags > FlagConntrack { 104 | return ErrUnknownFlag 105 | } 106 | return nil 107 | } 108 | 109 | // Close the connection to the netfilter log subsystem 110 | func (nflog *Nflog) Close() error { 111 | err := nflog.Con.Close() 112 | nflog.wg.Wait() 113 | return err 114 | } 115 | 116 | // SetOption allows to enable or disable netlink socket options. 117 | func (nflog *Nflog) SetOption(o netlink.ConnOption, enable bool) error { 118 | return nflog.Con.SetOption(o, enable) 119 | } 120 | 121 | // Register your own function as callback for a netfilter log group. 122 | // Errors other than net.Timeout() will be reported via the provided log interface 123 | // and the receiving of netfilter log messages will be stopped. 124 | // 125 | // To handle errors and continue receiving data with the 126 | // registered callback use RegisterWithErrorFunc() instead. 127 | // 128 | // Deprecated: Use RegisterWithErrorFunc() instead. 129 | func (nflog *Nflog) Register(ctx context.Context, fn HookFunc) error { 130 | return nflog.RegisterWithErrorFunc(ctx, fn, func(err error) int { 131 | if opError, ok := err.(*netlink.OpError); ok { 132 | if opError.Timeout() || opError.Temporary() { 133 | return 0 134 | } 135 | } 136 | nflog.logger.Errorf("Could not receive message: %v\n", err) 137 | return 1 138 | }) 139 | } 140 | 141 | // RegisterWithErrorFunc attaches a callback function to a callback to a netfilter log group and allows 142 | // custom error handling for errors encountered when reading from the underlying netlink socket. 143 | func (nflog *Nflog) RegisterWithErrorFunc(ctx context.Context, fn HookFunc, errfn ErrorFunc) error { 144 | // Avoid race conditions when dealing with the socket and receiving content. 145 | nflog.wg.Add(1) 146 | defer nflog.wg.Done() 147 | 148 | // unbinding existing handler (if any) 149 | seq, err := nflog.setConfig(unix.AF_UNSPEC, 0, 0, []netlink.Attribute{ 150 | {Type: nfUlACfgCmd, Data: []byte{nfUlnlCfgCmdPfUnbind}}, 151 | }) 152 | if err != nil { 153 | return fmt.Errorf("could not unbind existing handlers from socket: %v", err) 154 | } 155 | 156 | // binding to family 157 | _, err = nflog.setConfig(unix.AF_UNSPEC, seq, 0, []netlink.Attribute{ 158 | {Type: nfUlACfgCmd, Data: []byte{nfUlnlCfgCmdPfBind}}, 159 | }) 160 | if err != nil { 161 | return fmt.Errorf("could not bind socket to family: %v", err) 162 | } 163 | 164 | if (nflog.settings & GenericGroup) == GenericGroup { 165 | // binding to generic group 166 | _, err = nflog.setConfig(unix.AF_UNSPEC, seq, 0, []netlink.Attribute{ 167 | {Type: nfUlACfgCmd, Data: []byte{nfUlnlCfgCmdPfBind}}, 168 | }) 169 | if err != nil { 170 | return fmt.Errorf("could not bind to generic group: %v", err) 171 | } 172 | } 173 | 174 | // binding to the requested group 175 | _, err = nflog.setConfig(unix.AF_UNSPEC, seq, nflog.group, []netlink.Attribute{ 176 | {Type: nfUlACfgCmd, Data: []byte{nfUlnlCfgCmdBind}}, 177 | }) 178 | if err != nil { 179 | return fmt.Errorf("could not bind to requested group %d: %v", nflog.group, err) 180 | } 181 | 182 | // set copy mode and buffer size 183 | data := append(nflog.bufsize, nflog.copyMode) 184 | data = append(data, 0x0) 185 | _, err = nflog.setConfig(unix.AF_UNSPEC, seq, nflog.group, []netlink.Attribute{ 186 | {Type: nfUlACfgMode, Data: data}, 187 | }) 188 | if err != nil { 189 | return fmt.Errorf("could not set copy mode %d and buffer size %d: %v", nflog.copyMode, nflog.bufsize, err) 190 | } 191 | 192 | var attrs []netlink.Attribute 193 | if nflog.flags[0] != 0 || nflog.flags[1] != 0 { 194 | // set flags 195 | attrs = append(attrs, netlink.Attribute{Type: nfUlACfgFlags, Data: nflog.flags}) 196 | } 197 | 198 | if nflog.timeout[0] != 0 || nflog.timeout[1] != 0 || nflog.timeout[2] != 0 || nflog.timeout[3] != 0 { 199 | // set timeout 200 | attrs = append(attrs, netlink.Attribute{Type: nfUlACfgTimeOut, Data: nflog.timeout}) 201 | } 202 | 203 | if nflog.qthresh[0] != 0 || nflog.qthresh[1] != 0 || nflog.qthresh[2] != 0 || nflog.qthresh[3] != 0 { 204 | // set qthresh 205 | attrs = append(attrs, netlink.Attribute{Type: nfUlACfgQThresh, Data: nflog.qthresh}) 206 | } 207 | 208 | if len(attrs) != 0 { 209 | _, err = nflog.setConfig(unix.AF_UNSPEC, seq, nflog.group, attrs) 210 | if err != nil { 211 | return err 212 | } 213 | } 214 | go func() { 215 | defer func() { 216 | // unbinding from group 217 | _, err = nflog.setConfig(unix.AF_UNSPEC, seq, nflog.group, []netlink.Attribute{ 218 | {Type: nfUlACfgCmd, Data: []byte{nfUlnlCfgCmdUnbind}}, 219 | }) 220 | if err != nil { 221 | nflog.logger.Errorf("Could not unbind socket from configuration: %v", err) 222 | return 223 | } 224 | }() 225 | 226 | nflog.wg.Add(1) 227 | go func() { 228 | // block until context is done 229 | <-ctx.Done() 230 | // Set the read deadline to a point in the past to interrupt 231 | // possible blocking Receive() calls. 232 | nflog.Con.SetReadDeadline(time.Now().Add(-1 * time.Second)) 233 | nflog.wg.Done() 234 | }() 235 | for { 236 | if err := ctx.Err(); err != nil { 237 | nflog.logger.Errorf("Error and stopping receiving nflog messages: %v", err) 238 | return 239 | } 240 | reply, err := nflog.Con.Receive() 241 | if err != nil { 242 | if ret := errfn(err); ret != 0 { 243 | return 244 | } 245 | continue 246 | } 247 | 248 | for _, msg := range reply { 249 | if msg.Header.Type == netlink.Done { 250 | // this is the last message of a batch 251 | // continue to receive messages 252 | break 253 | } 254 | attrs, err := parseMsg(nflog.logger, msg) 255 | if err != nil { 256 | nflog.logger.Debugf("Could not parse message: %v", err) 257 | continue 258 | } 259 | if ret := fn(attrs); ret != 0 { 260 | return 261 | } 262 | } 263 | } 264 | }() 265 | 266 | return nil 267 | } 268 | 269 | // /include/uapi/linux/netfilter/nfnetlink.h:struct nfgenmsg{} 270 | func putExtraHeader(familiy, version uint8, resid uint16) []byte { 271 | buf := make([]byte, 2) 272 | binary.BigEndian.PutUint16(buf, resid) 273 | return append([]byte{familiy, version}, buf...) 274 | } 275 | 276 | func (nflog *Nflog) setConfig(afFamily uint8, oseq uint32, resid uint16, attrs []netlink.Attribute) (uint32, error) { 277 | ad := netlink.NewAttributeEncoder() 278 | 279 | for _, attr := range attrs { 280 | ad.Bytes(attr.Type, attr.Data) 281 | } 282 | cmd, err := ad.Encode() 283 | if err != nil { 284 | return 0, err 285 | } 286 | data := putExtraHeader(afFamily, unix.NFNETLINK_V0, resid) 287 | data = append(data, cmd...) 288 | req := netlink.Message{ 289 | Header: netlink.Header{ 290 | Type: netlink.HeaderType((nfnlSubSysUlog << 8) | nfUlnlMsgConfig), 291 | Flags: netlink.Request | netlink.Acknowledge, 292 | Sequence: oseq, 293 | }, 294 | Data: data, 295 | } 296 | return nflog.execute(req) 297 | } 298 | 299 | func (nflog *Nflog) execute(req netlink.Message) (uint32, error) { 300 | var seq uint32 301 | 302 | reply, e := nflog.Con.Execute(req) 303 | if e != nil { 304 | return 0, e 305 | } 306 | 307 | if e := netlink.Validate(req, reply); e != nil { 308 | return 0, e 309 | } 310 | for _, msg := range reply { 311 | if seq != 0 { 312 | return 0, fmt.Errorf("received more than one message from the kernel") 313 | } 314 | seq = msg.Header.Sequence 315 | } 316 | 317 | return seq, nil 318 | } 319 | 320 | func parseMsg(logger Logger, msg netlink.Message) (Attribute, error) { 321 | a, err := extractAttributes(logger, msg.Data) 322 | if err != nil { 323 | return Attribute{}, err 324 | } 325 | return a, nil 326 | } 327 | -------------------------------------------------------------------------------- /nflog_linux_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration && linux 2 | // +build integration,linux 3 | 4 | package nflog 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestLinuxNflog(t *testing.T) { 13 | // Set configuration parameters 14 | config := Config{ 15 | Group: 100, 16 | Copymode: CopyPacket, 17 | } 18 | // Open a socket to the netfilter log subsystem 19 | nf, err := Open(&config) 20 | if err != nil { 21 | t.Fatalf("failed to open nflog socket: %v", err) 22 | } 23 | defer nf.Close() 24 | 25 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 26 | defer cancel() 27 | 28 | // hook function that is called for every received packet by the nflog group 29 | hook := func(a Attribute) int { 30 | // Just print out the payload of the nflog packet 31 | t.Logf("%v\n", *a.Payload) 32 | return 0 33 | } 34 | 35 | errFunc := func(e error) int { 36 | t.Logf("received error on hook: %v", e) 37 | return 0 38 | } 39 | 40 | // Register your function to listen on nflog group 100 41 | err = nf.RegisterWithErrorFunc(ctx, hook, errFunc) 42 | if err != nil { 43 | t.Fatalf("failed to register hook function: %v", err) 44 | } 45 | 46 | // Block till the context expires 47 | <-ctx.Done() 48 | } 49 | 50 | func startNflog(ctx context.Context, t *testing.T, group uint16) (func(), error) { 51 | t.Helper() 52 | 53 | config := Config{ 54 | Group: group, 55 | Copymode: CopyPacket, 56 | } 57 | 58 | nf, err := Open(&config) 59 | if err != nil { 60 | return func() {}, err 61 | } 62 | fn := func(a Attribute) int { 63 | t.Logf("--nflog-group %d\t%v\n", group, *a.Payload) 64 | return 1 65 | } 66 | 67 | err = nf.Register(ctx, fn) 68 | if err != nil { 69 | return func() {}, err 70 | } 71 | 72 | return func() { nf.Close() }, nil 73 | } 74 | 75 | func TestLinuxMultiNflog(t *testing.T) { 76 | var cleanUp []func() 77 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 78 | defer cancel() 79 | 80 | for i := 32; i <= 42; i++ { 81 | function, err := startNflog(ctx, t, uint16(i)) 82 | if err != nil { 83 | t.Fatalf("failed to open nflog socket for group %d: %v", i, err) 84 | } 85 | cleanUp = append(cleanUp, function) 86 | } 87 | 88 | // Block till the context expires 89 | <-ctx.Done() 90 | 91 | for _, function := range cleanUp { 92 | function() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /nflog_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package nflog 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestOpen(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | group uint16 14 | copymode uint8 15 | err error 16 | flags uint16 17 | }{ 18 | {name: "InvalidCopymode", copymode: 0x5, err: ErrCopyMode}, 19 | {name: "InvalidFlags", flags: 0x5, err: ErrUnknownFlag}, 20 | } 21 | 22 | for _, tc := range tests { 23 | t.Run(tc.name, func(t *testing.T) { 24 | config := Config{ 25 | Copymode: tc.copymode, 26 | Group: tc.group, 27 | Flags: tc.flags, 28 | } 29 | _, err := Open(&config) 30 | if err != tc.err { 31 | t.Fatalf("Unexpected error - want: %v\tgot: %v\n", tc.err, err) 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package nflog 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | // Various constants 9 | const ( 10 | // Available copy modes for Config.Copymode. 11 | CopyNone byte = 0x00 12 | CopyMeta byte = 0x01 13 | // Provides a complete copy of the packet in the Msg map. 14 | // But can be limited by setting Config.Bufsize. 15 | CopyPacket byte = 0x02 16 | 17 | // Flags that can be set on a connection 18 | FlagSeq uint16 = 0x0001 19 | FlagGlobal uint16 = 0x0002 20 | // Requires Kernel configuration of CONFIG_NETFILTER_NETLINK_GLUE_CT 21 | FlagConntrack uint16 = 0x0004 22 | ) 23 | 24 | // Various optional settings 25 | const ( 26 | GenericGroup uint16 = 0x1 27 | ) 28 | 29 | // Various errors 30 | var ( 31 | ErrCopyMode = errors.New("unsupported copy mode") 32 | ErrUnknownFlag = errors.New("unsupported flag") 33 | ) 34 | 35 | // nfLogSubSysUlog the netlink subsystem we will query 36 | const nfnlSubSysUlog = 0x04 37 | 38 | // Message types from nfulnl_msg_types 39 | const ( 40 | // Userspace to kernel 41 | nfUlnlMsgConfig = 1 42 | ) 43 | 44 | const ( 45 | _ = iota 46 | nfUlACfgCmd 47 | nfUlACfgMode 48 | nfUlACfgNlBufSize 49 | nfUlACfgTimeOut /* in 1/100 s */ 50 | nfUlACfgQThresh 51 | nfUlACfgFlags 52 | ) 53 | 54 | const ( 55 | _ = iota 56 | nfUlnlCfgCmdBind 57 | nfUlnlCfgCmdUnbind 58 | nfUlnlCfgCmdPfBind 59 | nfUlnlCfgCmdPfUnbind 60 | ) 61 | 62 | const ( 63 | _ = iota 64 | nfUlaAttrPacketHdr 65 | nfUlaAttrMark 66 | nfUlaAttrTimestamp 67 | nfUlaAttrIfindexIndev 68 | nfUlaAttrIfindexOutdev 69 | nfUlaAttrIfindexPhysIndev 70 | nfUlaAttrIfindexPhysOutdev 71 | nfUlaAttrHwaddr 72 | nfUlaAttrPayload 73 | nfUlaAttrPrefix 74 | nfUlaAttrUID 75 | nfUlaAttrSeq 76 | nfUlaAttrSeqGlobal 77 | nfUlaAttrGID 78 | nfUlaAttrHwType 79 | nfUlaAttrHwHeader 80 | nfUlaAttrHwLen 81 | nfUlaAttrCt 82 | nfUlaAttrCtInfo 83 | nfUlaAttrVlan 84 | nfulaAttrL2Hdr 85 | ) 86 | 87 | // Attribute contains various elements for nflog elements. 88 | // As not every value is contained in every nflog message, 89 | // the elements inside Attribute are pointers to these values 90 | // or nil, if not present. 91 | type Attribute struct { 92 | Hook *uint8 93 | Mark *uint32 94 | Timestamp *time.Time 95 | InDev *uint32 96 | PhysInDev *uint32 97 | OutDev *uint32 98 | PhysOutDev *uint32 99 | Payload *[]byte 100 | Prefix *string 101 | UID *uint32 102 | Seq *uint32 103 | SeqGlobal *uint32 104 | GID *uint32 105 | HwType *uint16 106 | HwAddr *[]byte 107 | HwHeader *[]byte 108 | HwLen *uint16 109 | HwProtocol *uint16 110 | CtInfo *uint32 111 | Ct *[]byte 112 | Layer2Hdr *[]byte 113 | VLAN *VLAN 114 | } 115 | 116 | // VLAN holds the VLAN information. 117 | type VLAN struct { 118 | Proto uint16 119 | TCI uint16 120 | } 121 | 122 | // Logger provides logging functionality. 123 | type Logger interface { 124 | Debugf(format string, args ...interface{}) 125 | Errorf(format string, args ...interface{}) 126 | } 127 | 128 | // Config contains options for a Conn. 129 | type Config struct { 130 | // Network namespace the Nflog needs to operate in. If set to 0 (default), 131 | // no network namespace will be entered. 132 | NetNS int 133 | 134 | // Optional flags for the nflog communication 135 | Flags uint16 136 | 137 | // Specifies the number of packets in the group, 138 | // until they will be pushed to userspace. 139 | QThresh uint32 140 | 141 | // Maximum time in 1/100s that a packet in the nflog group will be queued, 142 | // until it is pushed to userspace. 143 | Timeout uint32 144 | 145 | // Nflog group this socket will be assigned to. 146 | Group uint16 147 | 148 | // Specifies how the kernel handles a packet in the nflog group. 149 | Copymode uint8 150 | 151 | // If NfUlnlCopyPacket is set as CopyMode, 152 | // this parameter specifies the maximum number of bytes, 153 | // that will be copied to userspace. 154 | Bufsize uint32 155 | 156 | // Optional settings to enable/disable features 157 | Settings uint16 158 | 159 | // Time till a read action times out - only available for Go >= 1.12 160 | // 161 | // Deprecated: Cancel the context passed to RegisterWithErrorFunc() or Register() 162 | // to remove the hook from the nfloq gracefully. Setting this value does no longer 163 | // have an effect on nflog. 164 | ReadTimeout time.Duration 165 | 166 | // Interface to log internals. 167 | Logger Logger 168 | } 169 | 170 | // ErrorFunc is a function that receives all errors that happen while reading 171 | // from a Netlinkgroup. To stop receiving messages return something different than 0. 172 | type ErrorFunc func(e error) int 173 | 174 | // HookFunc is a function, that receives events from a Netlinkgroup 175 | // To stop receiving messages on this HookFunc, return something different than 0 176 | type HookFunc func(a Attribute) int 177 | --------------------------------------------------------------------------------