diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ee8bf08 --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +# Copyright (c) 2023 NTT Communications Corporation +# Copyright (c) 2023 Takeru Hayasaka + +GOCMD=go +BINARY_NAME=fluvia +CLANG ?= clang +CFLAGS := -O2 -g -Wall $(CFLAGS) +DIFF_FROM_BRANCH_NAME ?= origin/main + +GREEN := $(shell tput -Txterm setaf 2) +YELLOW := $(shell tput -Txterm setaf 3) +WHITE := $(shell tput -Txterm setaf 7) +CYAN := $(shell tput -Txterm setaf 6) +RESET := $(shell tput -Txterm sgr0) + +.PHONY: all build clean + +all: go-gen build + +build: + mkdir -p out/bin + $(GOCMD) build -o out/bin/$(BINARY_NAME) ./cmd/$(BINARY_NAME)/main.go + +clean: + rm -fr out + +go-gen: export BPF_CLANG := $(CLANG) +go-gen: export BPF_CFLAGS := $(CFLAGS) +go-gen: + go generate ./... + +help: + @echo '' + @echo 'Usage:' + @echo ' ${YELLOW}make${RESET} ${GREEN}${RESET}' + @echo '' + @echo 'Targets:' + @awk 'BEGIN {FS = ":.*?## "} { \ + if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \ + else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \ + }' $(MAKEFILE_LIST) diff --git a/cmd/fluvia/fluvia.yaml b/cmd/fluvia/fluvia.yaml new file mode 100644 index 0000000..0a91f1a --- /dev/null +++ b/cmd/fluvia/fluvia.yaml @@ -0,0 +1,4 @@ +--- +ipfix: + address: 127.0.0.1 + port: 4739 diff --git a/cmd/fluvia/main.go b/cmd/fluvia/main.go index f73bdd4..3bba3bf 100644 --- a/cmd/fluvia/main.go +++ b/cmd/fluvia/main.go @@ -19,6 +19,7 @@ import ( type flags struct { configFile string + ifName string } func main() { @@ -31,6 +32,7 @@ func main() { // Parse flags f := &flags{} flag.StringVar(&f.configFile, "f", "fluvia.yaml", "Specify a configuration file") + flag.StringVar(&f.ifName, "i", "", "Specify a configuration file") flag.Parse() // Read configuration file @@ -44,5 +46,5 @@ func main() { log.Panic(err) } - client.New(raddr) + client.New(f.ifName, raddr) } diff --git a/go.mod b/go.mod index 1588380..c3d0b70 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,14 @@ module github.com/nttcom/fluvia go 1.20 -require gopkg.in/yaml.v3 v3.0.1 +require ( + github.com/cilium/ebpf v0.11.0 + github.com/google/gopacket v1.1.19 + github.com/pkg/errors v0.9.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect + golang.org/x/sys v0.10.0 // indirect +) diff --git a/go.sum b/go.sum index a62c313..e5f5bf2 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,30 @@ +github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y= +github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/bpf/bpf.go b/pkg/bpf/bpf.go new file mode 100644 index 0000000..4936e2a --- /dev/null +++ b/pkg/bpf/bpf.go @@ -0,0 +1,70 @@ +// Copyright (c) 2023 NTT Communications Corporation +// Copyright (c) 2023 Takeru Hayasaka +// +// This software is released under the MIT License. +// see https://github.com/nttcom/fluvia/blob/main/LICENSE + +package bpf + +import ( + "fmt" + "net" + + "github.com/cilium/ebpf" + "github.com/pkg/errors" +) + +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -no-global-types -cc $BPF_CLANG -cflags $BPF_CFLAGS xdp ../../src/main.c -- -I../../src + +type XdpProbeData struct { + H_dest [6]uint8 + H_source [6]uint8 + H_proto uint16 + _ [2]byte + V6Srcaddr struct{ In6U struct{ U6Addr8 [16]uint8 } } + V6Dstaddr struct{ In6U struct{ U6Addr8 [16]uint8 } } + NextHdr uint8 + HdrExtLen uint8 + RoutingType uint8 + SegmentsLeft uint8 + LastEntry uint8 + Flags uint8 + Tag uint16 + Segments [10]struct{ In6U struct{ U6Addr8 [16]uint8 } } +} + +func ReadXdpObjects(ops *ebpf.CollectionOptions) (*xdpObjects, error) { + obj := &xdpObjects{} + err := loadXdpObjects(obj, ops) + if err != nil { + return nil, errors.WithStack(err) + } + + // TODO: BPF log level remove hardcoding. yaml in config + if err != nil { + return nil, errors.WithStack(err) + } + + return obj, nil +} + +const ( + XDP_ABORTED uint32 = iota + XDP_DROP + XDP_PASS + XDP_TX + XDP_REDIRECT +) + +func PrintEntrys(entry XdpProbeData, count uint64) { + mac := func(mac [6]uint8) string { + return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]) + } + saddr := net.IP(entry.V6Srcaddr.In6U.U6Addr8[:]).String() + daddr := net.IP(entry.V6Dstaddr.In6U.U6Addr8[:]).String() + + fmt.Printf( + "H_dest: %s, H_source: %v, H_proto: %v, V6Dstaddr: %v, V6Srcaddr: %v -> count: %v\n", + mac(entry.H_dest), mac(entry.H_source), entry.H_proto, daddr, saddr, count) + +} diff --git a/pkg/bpf/srv6.go b/pkg/bpf/srv6.go new file mode 100644 index 0000000..2936829 --- /dev/null +++ b/pkg/bpf/srv6.go @@ -0,0 +1,107 @@ +package bpf + +import ( + "encoding/binary" + "errors" + "net" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" +) + +type Srv6Layer struct { + layers.BaseLayer + NextHeader uint8 + HdrExtLen uint8 + RoutingType uint8 + SegmentsLeft uint8 + LastEntry uint8 + Flags uint8 + Tag uint16 + Segments []net.IP +} + +var Srv6LayerType = gopacket.RegisterLayerType( + 2001, + gopacket.LayerTypeMetadata{ + Name: "Srv6LayerType", + Decoder: gopacket.DecodeFunc(decodeSrv6Layer), + }, +) + +func (l *Srv6Layer) LayerType() gopacket.LayerType { + return Srv6LayerType +} + +func (i *Srv6Layer) DecodeFromBytes(data []byte, df gopacket.DecodeFeedback) error { + if len(data) < 8 { + df.SetTruncated() + return errors.New("SRV6 layer less then 8 bytes for SRV6 packet") + } + i.NextHeader = data[0] + i.HdrExtLen = data[1] + i.RoutingType = data[2] + i.SegmentsLeft = data[3] + i.LastEntry = data[4] + i.Flags = data[5] + i.Tag = binary.BigEndian.Uint16(data[6:8]) + + for j := 0; j < int(i.HdrExtLen/2); j++ { + startBit := 8 + 16*j + endBit := 24 + 16*j + var addr []byte + for k := endBit; k >= startBit; k-- { + addr = append(addr, data[k]) + } + i.Segments = append(i.Segments, addr) + } + i.BaseLayer = layers.BaseLayer{ + Contents: data[:8], + Payload: data[8:], + } + return nil +} + +func (i *Srv6Layer) SerializeTo(b gopacket.SerializeBuffer, opts gopacket.SerializeOptions) error { + l := int(i.HdrExtLen)*8 + 8 + bytes, err := b.PrependBytes(l) + if err != nil { + return err + } + bytes[0] = i.NextHeader + bytes[1] = i.HdrExtLen + bytes[2] = i.RoutingType + bytes[3] = i.SegmentsLeft + bytes[4] = i.LastEntry + bytes[5] = i.Flags + binary.BigEndian.PutUint16(bytes[6:], i.Tag) + + for i2, address := range i.Segments { + lsb := binary.BigEndian.Uint64(address[:8]) + msb := binary.BigEndian.Uint64(address[8:]) + binary.BigEndian.PutUint64(bytes[8+16*i2:], lsb) + binary.BigEndian.PutUint64(bytes[16+16*i2:], msb) + } + return nil +} + +func (i *Srv6Layer) NextLayerType() gopacket.LayerType { + return gopacket.LayerTypePayload +} + +// Custom decode function. We can name it whatever we want +// but it should have the same arguments and return value +// When the layer is registered we tell it to use this decode function +func decodeSrv6Layer(data []byte, p gopacket.PacketBuilder) error { + i := &Srv6Layer{} + err := i.DecodeFromBytes(data, p) + if err != nil { + return err + } + p.AddLayer(i) + next := i.NextLayerType() + if next == gopacket.LayerTypeZero { + return nil + } + return p.NextDecoder(next) +} diff --git a/pkg/bpf/xdp_bpfeb.go b/pkg/bpf/xdp_bpfeb.go new file mode 100644 index 0000000..993d162 --- /dev/null +++ b/pkg/bpf/xdp_bpfeb.go @@ -0,0 +1,119 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64 + +package bpf + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +// loadXdp returns the embedded CollectionSpec for xdp. +func loadXdp() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_XdpBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load xdp: %w", err) + } + + return spec, err +} + +// loadXdpObjects loads xdp and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *xdpObjects +// *xdpPrograms +// *xdpMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadXdpObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadXdp() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// xdpSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type xdpSpecs struct { + xdpProgramSpecs + xdpMapSpecs +} + +// xdpSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type xdpProgramSpecs struct { + XdpProg *ebpf.ProgramSpec `ebpf:"xdp_prog"` +} + +// xdpMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type xdpMapSpecs struct { + IpfixProbeMap *ebpf.MapSpec `ebpf:"ipfix_probe_map"` +} + +// xdpObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadXdpObjects or ebpf.CollectionSpec.LoadAndAssign. +type xdpObjects struct { + xdpPrograms + xdpMaps +} + +func (o *xdpObjects) Close() error { + return _XdpClose( + &o.xdpPrograms, + &o.xdpMaps, + ) +} + +// xdpMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadXdpObjects or ebpf.CollectionSpec.LoadAndAssign. +type xdpMaps struct { + IpfixProbeMap *ebpf.Map `ebpf:"ipfix_probe_map"` +} + +func (m *xdpMaps) Close() error { + return _XdpClose( + m.IpfixProbeMap, + ) +} + +// xdpPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadXdpObjects or ebpf.CollectionSpec.LoadAndAssign. +type xdpPrograms struct { + XdpProg *ebpf.Program `ebpf:"xdp_prog"` +} + +func (p *xdpPrograms) Close() error { + return _XdpClose( + p.XdpProg, + ) +} + +func _XdpClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed xdp_bpfeb.o +var _XdpBytes []byte diff --git a/pkg/bpf/xdp_bpfeb.o b/pkg/bpf/xdp_bpfeb.o new file mode 100644 index 0000000..0275301 Binary files /dev/null and b/pkg/bpf/xdp_bpfeb.o differ diff --git a/pkg/bpf/xdp_bpfel.go b/pkg/bpf/xdp_bpfel.go new file mode 100644 index 0000000..12b957d --- /dev/null +++ b/pkg/bpf/xdp_bpfel.go @@ -0,0 +1,119 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 || amd64p32 || arm || arm64 || loong64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64 + +package bpf + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +// loadXdp returns the embedded CollectionSpec for xdp. +func loadXdp() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_XdpBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load xdp: %w", err) + } + + return spec, err +} + +// loadXdpObjects loads xdp and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *xdpObjects +// *xdpPrograms +// *xdpMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadXdpObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadXdp() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// xdpSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type xdpSpecs struct { + xdpProgramSpecs + xdpMapSpecs +} + +// xdpSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type xdpProgramSpecs struct { + XdpProg *ebpf.ProgramSpec `ebpf:"xdp_prog"` +} + +// xdpMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type xdpMapSpecs struct { + IpfixProbeMap *ebpf.MapSpec `ebpf:"ipfix_probe_map"` +} + +// xdpObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadXdpObjects or ebpf.CollectionSpec.LoadAndAssign. +type xdpObjects struct { + xdpPrograms + xdpMaps +} + +func (o *xdpObjects) Close() error { + return _XdpClose( + &o.xdpPrograms, + &o.xdpMaps, + ) +} + +// xdpMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadXdpObjects or ebpf.CollectionSpec.LoadAndAssign. +type xdpMaps struct { + IpfixProbeMap *ebpf.Map `ebpf:"ipfix_probe_map"` +} + +func (m *xdpMaps) Close() error { + return _XdpClose( + m.IpfixProbeMap, + ) +} + +// xdpPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadXdpObjects or ebpf.CollectionSpec.LoadAndAssign. +type xdpPrograms struct { + XdpProg *ebpf.Program `ebpf:"xdp_prog"` +} + +func (p *xdpPrograms) Close() error { + return _XdpClose( + p.XdpProg, + ) +} + +func _XdpClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed xdp_bpfel.o +var _XdpBytes []byte diff --git a/pkg/bpf/xdp_bpfel.o b/pkg/bpf/xdp_bpfel.o new file mode 100644 index 0000000..f936356 Binary files /dev/null and b/pkg/bpf/xdp_bpfel.o differ diff --git a/pkg/bpf/xdp_test.go b/pkg/bpf/xdp_test.go new file mode 100644 index 0000000..eca31c6 --- /dev/null +++ b/pkg/bpf/xdp_test.go @@ -0,0 +1,108 @@ +package bpf + +import ( + "fmt" + "net" + "testing" + + "github.com/cilium/ebpf/rlimit" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" +) + +func generateInput(t *testing.T) []byte { + t.Helper() + opts := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true} + buf := gopacket.NewSerializeBuffer() + + srcIP := net.ParseIP("2001:db8::1") + dstIP := net.ParseIP("2001:db8::2") + srcMAC, _ := net.ParseMAC("02:42:ac:11:00:02") + dstMAC, _ := net.ParseMAC("02:42:ac:11:00:03") + srcPort := layers.UDPPort(12345) + dstPort := layers.UDPPort(54321) + + // Define the SRv6 segment list + segmentList := []net.IP{ + net.ParseIP("2001:db8:dead:beef::1"), + net.ParseIP("2001:db8:dead:beef::2"), + } + + // Create the Ethernet layer + ethernetLayer := &layers.Ethernet{ + SrcMAC: srcMAC, + DstMAC: dstMAC, + EthernetType: layers.EthernetTypeIPv6, + } + + // Create the IPv6 layer + ipv6Layer := &layers.IPv6{ + Version: 6, + NextHeader: layers.IPProtocolIPv6Routing, + HopLimit: 64, + SrcIP: srcIP, + DstIP: dstIP, + } + + // Create the SRv6 extension header layer + seg6layer := &Srv6Layer{ + NextHeader: uint8(layers.IPProtocolUDP), + HdrExtLen: uint8((8+16*len(segmentList))/8 - 1), + RoutingType: 4, // SRH + SegmentsLeft: uint8(len(segmentList)), + LastEntry: uint8(len(segmentList) - 1), + Flags: 0, + Tag: 0, + Segments: segmentList, + } + // Create the UDP layer + udpLayer := &layers.UDP{ + SrcPort: srcPort, + DstPort: dstPort, + } + if err := udpLayer.SetNetworkLayerForChecksum(ipv6Layer); err != nil { + t.Fatal(err) + } + + err := gopacket.SerializeLayers(buf, opts, + ethernetLayer, ipv6Layer, seg6layer, udpLayer, + gopacket.Payload([]byte("Hello, SRv6!")), + ) + if err != nil { + t.Fatal(err) + } + return buf.Bytes() +} + +func TestXDPProg(t *testing.T) { + if err := rlimit.RemoveMemlock(); err != nil { + t.Fatal(err) + } + objs := &xdpObjects{} + err := loadXdpObjects(objs, nil) + if err != nil { + t.Fatal(err) + } + defer objs.Close() + + ret, _, err := objs.XdpProg.Test(generateInput(t)) + if err != nil { + t.Error(err) + } + + // retern code should be XDP_PASS + if ret != 2 { + t.Errorf("got %d want %d", ret, 2) + } + + fmt.Println("debug log") + var entry XdpProbeData + var count uint64 + iter := objs.IpfixProbeMap.Iterate() + for iter.Next(&entry, &count) { + PrintEntrys(entry, count) + } + if err := iter.Err(); err != nil { + fmt.Printf("Failed to iterate map: %v\n", err) + } +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 3677d56..8c3274b 100755 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -11,7 +11,7 @@ import ( "github.com/nttcom/fluvia/pkg/packet/ipfix" ) -func New(raddr *net.UDPAddr) ClientError { +func New(ifName string, raddr *net.UDPAddr) ClientError { ch := make(chan []ipfix.FieldValue) errChan := make(chan ClientError) @@ -25,6 +25,7 @@ func New(raddr *net.UDPAddr) ClientError { } } }() + go NewMeter(ifName, ch) for { clientError := <-errChan diff --git a/pkg/client/exporter.go b/pkg/client/exporter.go index 1405e15..69a41e4 100644 --- a/pkg/client/exporter.go +++ b/pkg/client/exporter.go @@ -6,7 +6,6 @@ package client import ( - "fmt" "log" "net" "os" @@ -69,7 +68,6 @@ func SendMessage(message *ipfix.Message, conn *net.UDPConn) { byteMessage := message.Serialize() _, err := conn.Write(byteMessage) - fmt.Printf("Send IPFIX message: %v", byteMessage) if err != nil { log.Fatalln(err) os.Exit(1) diff --git a/pkg/client/meter.go b/pkg/client/meter.go new file mode 100644 index 0000000..9183bc0 --- /dev/null +++ b/pkg/client/meter.go @@ -0,0 +1,97 @@ +// Copyright (c) 2023 NTT Communications Corporation +// Copyright (c) 2023 Takeru Hayasaka +// +// This software is released under the MIT License. +// see https://github.com/nttcom/fluvia/blob/main/LICENSE + +package client + +import ( + "fmt" + "log" + "net" + "net/netip" + "time" + + "github.com/cilium/ebpf/link" + "github.com/nttcom/fluvia/pkg/bpf" + "github.com/nttcom/fluvia/pkg/packet/ipfix" +) + +func NewMeter(ifaceName string, ch chan []ipfix.FieldValue) { + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + log.Fatalf("lookup network iface %q: %s", ifaceName, err) + } + + // Load the XDP program + objs, err := bpf.ReadXdpObjects(nil) + if err != nil { + log.Fatalf("Could not load XDP program: %s", err) + } + defer objs.Close() + + // Attach the XDP program. + l, err := link.AttachXDP(link.XDPOptions{ + Program: objs.XdpProg, + Interface: iface.Index, + Flags: link.XDPGenericMode, + }) + if err != nil { + log.Fatalf("Could not attach XDP program: %s", err) + } + defer l.Close() + + log.Printf("Attached XDP program to iface %q (index %d)", iface.Name, iface.Index) + log.Printf("Press Ctrl-C to exit and remove the program") + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + mapLogs := map[bpf.XdpProbeData]uint64{} + for range ticker.C { + var entry bpf.XdpProbeData + var count uint64 + + iter := objs.IpfixProbeMap.Iterate() + + for iter.Next(&entry, &count) { + if _, ok := mapLogs[entry]; !ok { + mapLogs[entry] = 0 + } + + dCnt := uint64(count - mapLogs[entry]) + + mapLogs[entry] = count + + sl := []ipfix.SRHSegmentIPv6{} + for _, binSeg := range entry.Segments { + ipSeg, _ := netip.AddrFromSlice(binSeg.In6U.U6Addr8[:]) + + // Ignore zero values received from bpf map + if ipSeg == netip.IPv6Unspecified() { + break + } + seg := ipfix.SRHSegmentIPv6{Val: ipSeg} + sl = append(sl, seg) + } + + actSeg, _ := netip.AddrFromSlice(entry.Segments[entry.SegmentsLeft].In6U.U6Addr8[:]) + + f := []ipfix.FieldValue{ + &ipfix.PacketDeltaCount{Val: dCnt}, + &ipfix.SRHActiveSegmentIPv6{Val: actSeg}, + &ipfix.SRHSegmentsIPv6Left{Val: entry.SegmentsLeft}, + &ipfix.SRHFlagsIPv6{Val: entry.Flags}, + &ipfix.SRHTagIPv6{Val: entry.Tag}, + &ipfix.SRHSegmentIPv6BasicList{ + SegmentList: sl, + }, + } + // Throw to channel + ch <- f + } + if err := iter.Err(); err != nil { + fmt.Printf("Failed to iterate map: %v\n", err) + } + } +} diff --git a/pkg/packet/ipfix/field_value.go b/pkg/packet/ipfix/field_value.go index ff2d886..56a96cb 100644 --- a/pkg/packet/ipfix/field_value.go +++ b/pkg/packet/ipfix/field_value.go @@ -12,11 +12,35 @@ import ( type FieldValue interface { Serialize() []uint8 - Len() uint16 + Len() uint16 // binary length of field value ElementID() uint16 FieldSpecifier() *FieldSpecifier } +type PacketDeltaCount struct { + Val uint64 +} + +func (fv *PacketDeltaCount) ElementID() uint16 { + return IEID_PACKET_DELTA_COUNT +} + +func (fv *PacketDeltaCount) Serialize() []uint8 { + ret := make([]uint8, 8) + binary.BigEndian.PutUint64(ret, fv.Val) + return ret +} + +func (fv *PacketDeltaCount) Len() uint16 { + return 8 +} + +func (fv *PacketDeltaCount) FieldSpecifier() *FieldSpecifier { + templateLen := fv.Len() + fs := NewFieldSpecifier(false, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) + return fs +} + type SRHFlagsIPv6 struct { Val uint8 } @@ -34,8 +58,8 @@ func (fv *SRHFlagsIPv6) Len() uint16 { } func (fv *SRHFlagsIPv6) FieldSpecifier() *FieldSpecifier { - templateLen := uint16(1) - fs := NewFieldSpecifier(true, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) + templateLen := fv.Len() + fs := NewFieldSpecifier(false, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) return fs } @@ -58,8 +82,8 @@ func (fv *SRHTagIPv6) Len() uint16 { } func (fv *SRHTagIPv6) FieldSpecifier() *FieldSpecifier { - templateLen := uint16(2) - fs := NewFieldSpecifier(true, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) + templateLen := fv.Len() + fs := NewFieldSpecifier(false, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) return fs } @@ -80,8 +104,8 @@ func (fv *SRHSegmentIPv6) Len() uint16 { } func (fv *SRHSegmentIPv6) FieldSpecifier() *FieldSpecifier { - templateLen := uint16(16) - fs := NewFieldSpecifier(true, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) + templateLen := fv.Len() + fs := NewFieldSpecifier(false, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) return fs } @@ -102,8 +126,8 @@ func (fv *SRHActiveSegmentIPv6) Len() uint16 { } func (fv *SRHActiveSegmentIPv6) FieldSpecifier() *FieldSpecifier { - templateLen := uint16(16) - fs := NewFieldSpecifier(true, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) + templateLen := fv.Len() + fs := NewFieldSpecifier(false, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) return fs } @@ -146,7 +170,7 @@ func (fv *SRHSegmentIPv6BasicList) Len() uint16 { func (fv *SRHSegmentIPv6BasicList) FieldSpecifier() *FieldSpecifier { templateLen := uint16(0xffff) // valiable - fs := NewFieldSpecifier(true, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) + fs := NewFieldSpecifier(false, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) return fs } @@ -176,7 +200,7 @@ func (fv *SRHSegmentIPv6ListSection) Len() uint16 { func (fv *SRHSegmentIPv6ListSection) FieldSpecifier() *FieldSpecifier { templateLen := uint16(0xffff) // valiable - fs := NewFieldSpecifier(true, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) + fs := NewFieldSpecifier(false, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) return fs } @@ -197,8 +221,8 @@ func (fv *SRHSegmentsIPv6Left) Len() uint16 { } func (fv *SRHSegmentsIPv6Left) FieldSpecifier() *FieldSpecifier { - templateLen := uint16(1) - fs := NewFieldSpecifier(true, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) + templateLen := fv.Len() + fs := NewFieldSpecifier(false, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) return fs } @@ -219,8 +243,8 @@ func (fv *SRHIPv6ActiveSegmentType) Len() uint16 { } func (fv *SRHIPv6ActiveSegmentType) FieldSpecifier() *FieldSpecifier { - templateLen := uint16(1) - fs := NewFieldSpecifier(true, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) + templateLen := fv.Len() + fs := NewFieldSpecifier(false, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) return fs } @@ -241,8 +265,8 @@ func (fv *SRHSegmentIPv6LocatorLength) Len() uint16 { } func (fv *SRHSegmentIPv6LocatorLength) FieldSpecifier() *FieldSpecifier { - templateLen := uint16(1) - fs := NewFieldSpecifier(true, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) + templateLen := fv.Len() + fs := NewFieldSpecifier(false, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) return fs } @@ -265,8 +289,8 @@ func (fv *SRHSegmentIPv6EndpointBehavior) Len() uint16 { } func (fv *SRHSegmentIPv6EndpointBehavior) FieldSpecifier() *FieldSpecifier { - templateLen := uint16(2) - fs := NewFieldSpecifier(true, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) + templateLen := fv.Len() + fs := NewFieldSpecifier(false, fv.ElementID(), templateLen, ENTERPRISE_NUMBER_NTTCOM) return fs } diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..e20757e --- /dev/null +++ b/src/main.c @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023 NTT Communications Corporation + * Copyright (c) 2023 Takeru Hayasaka + */ + +#define KBUILD_MODNAME "xdp_probe" +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "xdp_map.h" + +SEC("xdp") +int xdp_prog(struct xdp_md *ctx) { + void *data_end = (void *)(long)ctx->data_end; + void *data = (void *)(long)ctx->data; + __u32 probe_key = XDP_PASS; + struct ethhdr *eth = data; + + if ((void *)(eth + 1) > data_end) + return XDP_PASS; + + if (eth->h_proto != bpf_htons(ETH_P_IPV6)) + return XDP_PASS; + + struct ipv6hdr *ipv6 = (void *)(eth + 1); + if ((void *)(ipv6 + 1) > data_end) + return XDP_PASS; + + // is srv6 + if (ipv6->nexthdr != IPPROTO_IPV6ROUTE) + return XDP_PASS; + + struct srhhdr *srh = (void *)(ipv6 + 1); + if ((void *)(srh + 1) > data_end) + return XDP_PASS; + + if (srh->routingType != IPV6_SRCRT_TYPE_4) // IPV6_SRCRT_TYPE_4 = SRH + return XDP_PASS; + + struct probe_data key = {}; + __u64 zero = 0, *value; + __builtin_memcpy(&key.h_source, ð->h_source, ETH_ALEN); + __builtin_memcpy(&key.h_dest, ð->h_dest, ETH_ALEN); + key.h_proto = eth->h_proto; + key.v6_srcaddr = ipv6->saddr; + key.v6_dstaddr = ipv6->daddr; + + key.nextHdr = srh->nextHdr; + key.hdrExtLen = srh->hdrExtLen; + key.routingType = srh->routingType; + key.segmentsLeft = srh->segmentsLeft; + key.lastEntry = srh->lastEntry; + key.flags = srh->flags; + key.tag = srh->tag; + + for(int i=0; i data_end) + break; + + __builtin_memcpy(&key.segments[i], &srh->segments[i], sizeof(struct in6_addr)); + } + + value = bpf_map_lookup_elem(&ipfix_probe_map, &key); + if (!value) { + bpf_map_update_elem(&ipfix_probe_map, &key, &zero, BPF_NOEXIST); + value = bpf_map_lookup_elem(&ipfix_probe_map, &key); + if (!value) + return XDP_PASS; + } + (*value)++; + + return XDP_PASS; +} + +char _license[] SEC("license") = "MIT"; diff --git a/src/xdp_consts.h b/src/xdp_consts.h new file mode 100644 index 0000000..5f4c753 --- /dev/null +++ b/src/xdp_consts.h @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2023 NTT Communications Corporation + * Copyright (c) 2023 Takeru Hayasaka + */ + +#ifndef __XDP_CONSTS_H +#define __XDP_CONSTS_H + +#define MAX_MAP_ENTRIES 1024 +#define MAX_SEGMENTLIST_ENTRIES 10 +#define IPPROTO_IPV6ROUTE 43 + +#endif diff --git a/src/xdp_map.h b/src/xdp_map.h new file mode 100644 index 0000000..4371e85 --- /dev/null +++ b/src/xdp_map.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 NTT Communications Corporation + * Copyright (c) 2023 Takeru Hayasaka + */ + +#ifndef __XDP_MAPS_H +#define __XDP_MAPS_H +#include +#include +#include +#include +#include +#include "xdp_consts.h" +#include "xdp_struct.h" +struct { + __uint(type, BPF_MAP_TYPE_LRU_HASH); + __uint(max_entries, MAX_MAP_ENTRIES); + __type(key, struct probe_data); + __type(value, __u64); +} ipfix_probe_map SEC(".maps"); + +#endif diff --git a/src/xdp_struct.h b/src/xdp_struct.h new file mode 100644 index 0000000..34a01bb --- /dev/null +++ b/src/xdp_struct.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 NTT Communications Corporation + * Copyright (c) 2023 Takeru Hayasaka + */ + +#ifndef __XDP_STRUCTS_H +#define __XDP_STRUCTS_H + +#include +#include +#include /* For struct in6_addr. */ +#include "xdp_consts.h" + +// Segment Routing Extension Header (SRH) +// https://datatracker.ietf.org/doc/draft-ietf-6man-segment-routing-header/ +struct srhhdr +{ + __u8 nextHdr; + __u8 hdrExtLen; + __u8 routingType; + __u8 segmentsLeft; + __u8 lastEntry; + __u8 flags; + __u16 tag; + struct in6_addr segments[0]; +}; + +struct probe_data { + __u8 h_dest[ETH_ALEN]; + __u8 h_source[ETH_ALEN]; + __be16 h_proto; + struct in6_addr v6_srcaddr; + struct in6_addr v6_dstaddr; + __u8 nextHdr; + __u8 hdrExtLen; + __u8 routingType; + __u8 segmentsLeft; + __u8 lastEntry; + __u8 flags; + __u16 tag; + struct in6_addr segments[MAX_SEGMENTLIST_ENTRIES]; +}; + +#endif