Add gopacket adapter for libmemif 94/13294/4
authorTomas Slusny <slusnucky@gmail.com>
Thu, 28 Jun 2018 10:23:28 +0000 (12:23 +0200)
committerTomas Slusny <slusnucky@gmail.com>
Mon, 2 Jul 2018 10:20:29 +0000 (12:20 +0200)
Add simple PacketHandle adapter for gopacket PacketDataSource + add
writePacketData convenience method to it.

Change-Id: I2280db33076c497c9b63fd107b6d77ecf85dd23b
Signed-off-by: Tomas Slusny <slusnucky@gmail.com>
Makefile
extras/libmemif/README.md
extras/libmemif/examples/gopacket/gopacket.go [new file with mode: 0644]
extras/libmemif/packethandle.go [new file with mode: 0644]

index 4eed58d..214f632 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -15,6 +15,7 @@ install:
 extras:
        @cd extras/libmemif/examples/raw-data && go build -v
        @cd extras/libmemif/examples/icmp-responder && go build -v
+       @cd extras/libmemif/examples/gopacket && go build -v
 
 clean:
        @rm -f cmd/binapi-generator/binapi-generator
index 3b692b1..854aa96 100644 (file)
@@ -103,6 +103,14 @@ Do not touch memif after it was closed, let garbage collector to remove
 the `Memif` instance. In the end, `Cleanup()` will also ensure that all
 active memif interfaces are closed before the cleanup finalizes.
 
+To use libmemif with `google/gopacket`, simply call `Memif.NewPacketHandle()`
+to create `google/gopacket/PacketDataSource` from memif queue. After this you
+can use gopacket API to read from `MemifPacketHandle` as normal. You can pass
+optional `rxCount` when creating the packet handle and then when reading data,
+handle will try to read more packets at once and cache them for next iteration.
+Handle also includes convenience method `MemifPacketHandle.WritePacketData()`
+that is simply calling 1 `Memif.TxBurst()` for provided data.
+
 ### Examples
 
 **Go-libmemif** ships with two simple examples demonstrating the usage
@@ -185,4 +193,55 @@ vpp$ sh ip arp
         It was actually converted to an ARP request. This is a VPP
         specific feature common to all interface types.
 
-Stop the example with an interrupt signal (^C).
\ No newline at end of file
+Stop the example with an interrupt signal (^C).
+
+#### GoPacket ICMP Responder
+
+*gopacket* is a simple example showing how to answer APR and ICMP echo
+requests through a memif interface. This example is mostly identical
+to icmp-responder example, but it is using MemifPacketHandle API to
+read and write packets using gopacket API.
+
+The appropriate VPP configuration for the opposite memif is:
+```
+vpp$ create memif socket id 1 filename /tmp/gopacket-example
+vpp$ create interface memif id 1 socket-id 1 slave secret secret no-zero-copy
+vpp$ set int state memif1/1 up
+vpp$ set int ip address memif1/1 192.168.1.2/24
+```
+
+To start the example, simply type:
+```
+root$ ./gopacket
+```
+
+gopacket needs to be run as root so that it can access the socket
+created by VPP.
+
+Normally, the memif interface is in the master mode. Pass CLI flag "--slave"
+to create memif in the slave mode:
+```
+root$ ./gopacket --slave
+```
+
+Don't forget to put the opposite memif into the master mode in that case.
+
+To verify the connection, run:
+```
+vpp$ ping 192.168.1.1
+64 bytes from 192.168.1.1: icmp_seq=2 ttl=255 time=.6974 ms
+64 bytes from 192.168.1.1: icmp_seq=3 ttl=255 time=.6310 ms
+64 bytes from 192.168.1.1: icmp_seq=4 ttl=255 time=1.0350 ms
+64 bytes from 192.168.1.1: icmp_seq=5 ttl=255 time=.5359 ms
+
+Statistics: 5 sent, 4 received, 20% packet loss
+vpp$ sh ip arp
+Time           IP4       Flags      Ethernet              Interface
+68.5648   192.168.1.1     D    aa:aa:aa:aa:aa:aa memif0/1
+```
+
+*Note*: it is expected that the first ping is shown as lost.
+        It was actually converted to an ARP request. This is a VPP
+        specific feature common to all interface types.
+
+Stop the example with an interrupt signal (^C).
diff --git a/extras/libmemif/examples/gopacket/gopacket.go b/extras/libmemif/examples/gopacket/gopacket.go
new file mode 100644 (file)
index 0000000..ee452a2
--- /dev/null
@@ -0,0 +1,362 @@
+// gopacket is a simple example showing how to answer APR and ICMP echo
+// requests through a memif interface. This example is mostly identical
+// to icmp-responder example, but it is using MemifPacketHandle API to
+// read and write packets using gopacket API.
+//
+// The appropriate VPP configuration for the opposite memif is:
+//   vpp$ create memif socket id 1 filename /tmp/gopacket-example
+//   vpp$ create interface memif id 1 socket-id 1 slave secret secret no-zero-copy
+//   vpp$ set int state memif1/1 up
+//   vpp$ set int ip address memif1/1 192.168.1.2/24
+//
+// To start the example, simply type:
+//   root$ ./gopacket
+//
+// gopacket needs to be run as root so that it can access the socket
+// created by VPP.
+//
+// Normally, the memif interface is in the master mode. Pass CLI flag "--slave"
+// to create memif in the slave mode:
+//   root$ ./gopacket --slave
+//
+// Don't forget to put the opposite memif into the master mode in that case.
+//
+// To verify the connection, run:
+//   vpp$ ping 192.168.1.1
+//   64 bytes from 192.168.1.1: icmp_seq=2 ttl=255 time=.6974 ms
+//   64 bytes from 192.168.1.1: icmp_seq=3 ttl=255 time=.6310 ms
+//   64 bytes from 192.168.1.1: icmp_seq=4 ttl=255 time=1.0350 ms
+//   64 bytes from 192.168.1.1: icmp_seq=5 ttl=255 time=.5359 ms
+//
+//   Statistics: 5 sent, 4 received, 20% packet loss
+//   vpp$ sh ip arp
+//   Time           IP4       Flags      Ethernet              Interface
+//   68.5648   192.168.1.1     D    aa:aa:aa:aa:aa:aa memif0/1
+//
+// Note: it is expected that the first ping is shown as lost. It was actually
+// converted to an ARP request. This is a VPP feature common to all interface
+// types.
+//
+// Stop the example with an interrupt signal.
+package main
+
+import (
+       "errors"
+       "fmt"
+       "io"
+       "net"
+       "os"
+       "os/signal"
+       "github.com/google/gopacket"
+       "github.com/google/gopacket/layers"
+       "git.fd.io/govpp.git/extras/libmemif"
+)
+
+var (
+       // Used to signalize interrupt goroutines to stop
+       stopCh chan struct{}
+
+       // MAC address assigned to the memif interface.
+       hwAddr net.HardwareAddr
+
+       // IPAddress assigned to the memif interface.
+       ipAddr net.IP
+
+       // ErrUnhandledPacket is thrown and printed when an unexpected packet is received.
+       ErrUnhandledPacket = errors.New("received an unhandled packet")
+)
+
+// OnConnect is called when a memif connection gets established.
+func OnConnect(memif *libmemif.Memif) (err error) {
+       // Use Memif.GetDetails to get the number of queues.
+       details, err := memif.GetDetails()
+       if err != nil {
+               fmt.Printf("libmemif.GetDetails() error: %v\n", err)
+               return
+       }
+
+       fmt.Printf("memif %s has been connected: %+v\n", memif.IfName, details)
+       stopCh = make(chan struct{})
+
+       // Start a separate go routine for each RX queue.
+       // (memif queue is a unit of parallelism for Rx/Tx).
+       // Beware: the number of queues created may be lower than what was requested
+       // in MemifConfiguration (the master makes the final decision).
+       for _, queue := range details.RxQueues {
+               ch, err := memif.GetQueueInterruptChan(queue.QueueID)
+               if err != nil {
+                       fmt.Printf("libmemif.Memif.GetQueueInterruptChan() error %v\n", err)
+                       continue
+               }
+
+               go CreateInterruptCallback(memif.NewPacketHandle(queue.QueueID, 10), ch, OnInterrupt)
+       }
+
+       return
+}
+
+// OnDisconnect is called when a memif connection is lost.
+func OnDisconnect(memif *libmemif.Memif) (err error) {
+       fmt.Printf("memif %s has been disconnected\n", memif.IfName)
+       // Stop all packet producers and consumers.
+       close(stopCh)
+       return nil
+}
+
+// OnInterrupt is called when interrupted
+func OnInterrupt(handle *libmemif.MemifPacketHandle) {
+       source := gopacket.NewPacketSource(handle, layers.LayerTypeEthernet)
+       var responses []gopacket.Packet
+
+       // Process ICMP pings
+       for packet := range source.Packets() {
+               fmt.Println("Received new packet:")
+               fmt.Println(packet.Dump())
+
+               response, err := GeneratePacketResponse(packet)
+               if err != nil {
+                       fmt.Printf("Failed to generate response: %v\n", err)
+                       continue
+               }
+
+               fmt.Println("Sending response:")
+               fmt.Println(response.Dump())
+               responses = append(responses, response)
+       }
+
+       // Answer with ICMP pongs
+       for i, response := range responses {
+               err := handle.WritePacketData(response.Data())
+
+               switch err {
+               case io.EOF:
+                       return
+               case nil:
+                       fmt.Printf("Succesfully sent packet #%v %v\n", i, len(response.Data()))
+               default:
+                       fmt.Printf("Got error while sending packet #%v %v\n", i, err)
+               }
+       }
+}
+
+// Creates user-friendly memif interrupt callback
+func CreateInterruptCallback(handle *libmemif.MemifPacketHandle, interruptCh <-chan struct{}, callback func(handle *libmemif.MemifPacketHandle)) {
+       for {
+               select {
+               case <-interruptCh:
+                       callback(handle)
+               case <-stopCh:
+                       handle.Close()
+                       return
+               }
+       }
+}
+
+// GeneratePacketResponse returns an appropriate answer to an ARP request
+// or an ICMP echo request.
+func GeneratePacketResponse(packet gopacket.Packet) (response gopacket.Packet, err error) {
+       ethLayer := packet.Layer(layers.LayerTypeEthernet)
+       eth, ok := ethLayer.(*layers.Ethernet)
+       if !ok {
+               fmt.Println("Missing ETH layer.")
+               return nil, ErrUnhandledPacket
+       }
+
+       // Set up buffer and options for serialization.
+       buf := gopacket.NewSerializeBuffer()
+       opts := gopacket.SerializeOptions{
+               FixLengths:       true,
+               ComputeChecksums: true,
+       }
+
+       switch eth.EthernetType {
+       case layers.EthernetTypeARP:
+               // Handle ARP request.
+               arpLayer := packet.Layer(layers.LayerTypeARP)
+               arp, ok := arpLayer.(*layers.ARP)
+               if !ok {
+                       fmt.Println("Missing ARP layer.")
+                       return nil, ErrUnhandledPacket
+               }
+
+               if arp.Operation != layers.ARPRequest {
+                       fmt.Println("Not ARP request.")
+                       return nil, ErrUnhandledPacket
+               }
+
+               fmt.Println("Received an ARP request.")
+
+               // Build packet layers.
+               ethResp := layers.Ethernet{
+                       SrcMAC:       hwAddr,
+                       DstMAC:       eth.SrcMAC,
+                       EthernetType: layers.EthernetTypeARP,
+               }
+
+               arpResp := layers.ARP{
+                       AddrType:          layers.LinkTypeEthernet,
+                       Protocol:          layers.EthernetTypeIPv4,
+                       HwAddressSize:     6,
+                       ProtAddressSize:   4,
+                       Operation:         layers.ARPReply,
+                       SourceHwAddress:   []byte(hwAddr),
+                       SourceProtAddress: []byte(ipAddr),
+                       DstHwAddress:      arp.SourceHwAddress,
+                       DstProtAddress:    arp.SourceProtAddress,
+               }
+
+               if err := gopacket.SerializeLayers(buf, opts, &ethResp, &arpResp); err != nil {
+                       fmt.Println("SerializeLayers error: ", err)
+                       return nil, ErrUnhandledPacket
+               }
+       case layers.EthernetTypeIPv4:
+               // Respond to ICMP request.
+               ipLayer := packet.Layer(layers.LayerTypeIPv4)
+               ipv4, ok := ipLayer.(*layers.IPv4)
+               if !ok {
+                       fmt.Println("Missing IPv4 layer.")
+                       return nil, ErrUnhandledPacket
+               }
+
+               if ipv4.Protocol != layers.IPProtocolICMPv4 {
+                       fmt.Println("Not ICMPv4 protocol.")
+                       return nil, ErrUnhandledPacket
+               }
+
+               icmpLayer := packet.Layer(layers.LayerTypeICMPv4)
+               icmp, ok := icmpLayer.(*layers.ICMPv4)
+               if !ok {
+                       fmt.Println("Missing ICMPv4 layer.")
+                       return nil, ErrUnhandledPacket
+               }
+
+               if icmp.TypeCode.Type() != layers.ICMPv4TypeEchoRequest {
+                       fmt.Println("Not ICMPv4 echo request.")
+                       return nil, ErrUnhandledPacket
+               }
+
+               fmt.Println("Received an ICMPv4 echo request.")
+
+               // Build packet layers.
+               ethResp := layers.Ethernet{
+                       SrcMAC:       hwAddr,
+                       DstMAC:       eth.SrcMAC,
+                       EthernetType: layers.EthernetTypeIPv4,
+               }
+
+               ipv4Resp := layers.IPv4{
+                       Version:    4,
+                       IHL:        5,
+                       TOS:        0,
+                       Id:         0,
+                       Flags:      0,
+                       FragOffset: 0,
+                       TTL:        255,
+                       Protocol:   layers.IPProtocolICMPv4,
+                       SrcIP:      ipAddr,
+                       DstIP:      ipv4.SrcIP,
+               }
+
+               icmpResp := layers.ICMPv4{
+                       TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoReply, 0),
+                       Id:       icmp.Id,
+                       Seq:      icmp.Seq,
+               }
+
+               if err := gopacket.SerializeLayers(buf, opts, &ethResp, &ipv4Resp, &icmpResp, gopacket.Payload(icmp.Payload)); err != nil {
+                       fmt.Println("SerializeLayers error: ", err)
+                       return nil, ErrUnhandledPacket
+               }
+       default:
+               return nil, ErrUnhandledPacket
+       }
+
+       return gopacket.NewPacket(buf.Bytes(), layers.LayerTypeEthernet, gopacket.Default), nil
+}
+
+func main() {
+       fmt.Println("Starting 'gopacket' example...")
+       var err error
+
+       // Parse MAC address associated with memif interface
+       hwAddr, err = net.ParseMAC("aa:aa:aa:aa:aa:aa")
+       if err != nil {
+               fmt.Printf("Failed to parse the MAC address: %v/n", err)
+               return
+       }
+
+       // Parse IP address associated with memif interface
+       ip := net.ParseIP("192.168.1.1")
+       if ip != nil {
+               ipAddr = ip.To4()
+       }
+       if ipAddr == nil {
+               fmt.Println("Failed to parse the IP address")
+               return
+       }
+
+       // If run with the "--slave" option, create memif in the slave mode.
+       var isMaster = true
+       var appSuffix string
+       if len(os.Args) > 1 && (os.Args[1] == "--slave" || os.Args[1] == "-slave") {
+               isMaster = false
+               appSuffix = "-slave"
+       }
+
+       // Initialize libmemif first.
+       appName := "gopacket" + appSuffix
+       fmt.Println("Initializing libmemif as ", appName)
+       err = libmemif.Init(appName)
+       if err != nil {
+               fmt.Printf("libmemif.Init() error: %v\n", err)
+               return
+       }
+
+       // Schedule automatic cleanup.
+       defer libmemif.Cleanup()
+
+       // Prepare callbacks to use with the memif.
+       // The same callbacks could be used with multiple memifs.
+       // The first input argument (*libmemif.Memif) can be used to tell which
+       // memif the callback was triggered for.
+       memifCallbacks := &libmemif.MemifCallbacks{
+               OnConnect:    OnConnect,
+               OnDisconnect: OnDisconnect,
+       }
+
+       // Prepare memif1 configuration.
+       memifConfig := &libmemif.MemifConfig{
+               MemifMeta: libmemif.MemifMeta{
+                       IfName:         "memif1",
+                       ConnID:         1,                       // ConnectionID is an identifier used to match opposite memifs.
+                       SocketFilename: "/tmp/gopacket-example", // Socket through which the opposite memifs will establish the connection.
+                       Secret:         "secret",                // Secret used to authenticate the memif connection.
+                       IsMaster:       isMaster,
+                       Mode:           libmemif.IfModeEthernet,
+               },
+               MemifShmSpecs: libmemif.MemifShmSpecs{
+                       NumRxQueues:  3, // NumQueues is the (configured!) number of queues for both Rx & Tx.
+                       NumTxQueues:  3, // The actual number agreed during connection establishment may be smaller!
+                       BufferSize:   2048,
+                       Log2RingSize: 10,
+               },
+       }
+
+       fmt.Printf("Callbacks: %+v\n", memifCallbacks)
+       fmt.Printf("Config: %+v\n", memifConfig)
+
+       // Create memif1 interface.
+       memif, err := libmemif.CreateInterface(memifConfig, memifCallbacks)
+       if err != nil {
+               fmt.Printf("libmemif.CreateInterface() error: %v\n", err)
+               return
+       }
+
+       // Schedule automatic cleanup of the interface.
+       defer memif.Close()
+
+       // Wait until an interrupt signal is received.
+       sigChan := make(chan os.Signal, 1)
+       signal.Notify(sigChan, os.Interrupt)
+       <-sigChan
+}
diff --git a/extras/libmemif/packethandle.go b/extras/libmemif/packethandle.go
new file mode 100644 (file)
index 0000000..83bf90a
--- /dev/null
@@ -0,0 +1,150 @@
+// Copyright (c) 2017 Cisco and/or its affiliates.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at:
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package libmemif
+
+import (
+       "github.com/google/gopacket"
+       "time"
+       "sync"
+       "io"
+)
+
+type memoizedPacket struct {
+       data RawPacketData
+       ci   gopacket.CaptureInfo
+}
+
+type MemifPacketHandle struct {
+       memif   *Memif
+       queueId uint8
+       rxCount uint16
+
+       // Used for caching packets when larger rxburst is called
+       packetQueue []*memoizedPacket
+
+       // Used for synchronization of read/write calls
+       readMu  sync.Mutex
+       writeMu sync.Mutex
+       closeMu sync.Mutex
+       stop    bool
+}
+
+// Create new GoPacket packet handle from libmemif queue. rxCount determines how many packets will be read
+// at once, minimum value is 1
+func (memif *Memif) NewPacketHandle(queueId uint8, rxCount uint16) *MemifPacketHandle {
+       if rxCount == 0 {
+               rxCount = 1
+       }
+
+       return &MemifPacketHandle{
+               memif:   memif,
+               queueId: queueId,
+               rxCount: rxCount,
+       }
+}
+
+// Reads packet data from memif in bursts, based on previously configured rxCount parameterer. Then caches the
+// resulting packets and returns them 1 by 1 from this method until queue is empty then tries to call new rx burst
+// to read more data. If no data is returned, io.EOF error is thrown and caller should stop reading.
+func (handle *MemifPacketHandle) ReadPacketData() (data []byte, ci gopacket.CaptureInfo, err error) {
+       handle.readMu.Lock()
+       defer handle.readMu.Unlock()
+
+       if handle.stop {
+               err = io.EOF
+               return
+       }
+
+       queueLen := len(handle.packetQueue)
+
+       if queueLen == 0 {
+               packets, burstErr := handle.memif.RxBurst(handle.queueId, handle.rxCount)
+               packetsLen := len(packets)
+
+               if burstErr != nil {
+                       err = burstErr
+                       return
+               }
+
+               if packetsLen == 0 {
+                       err = io.EOF
+                       return
+               }
+
+               handle.packetQueue = make([]*memoizedPacket, packetsLen)
+
+               for i, packet := range packets {
+                       packetLen := len(packet)
+
+                       handle.packetQueue[i] = &memoizedPacket{
+                               data: []byte(packet),
+                               ci: gopacket.CaptureInfo{
+                                       Timestamp:     time.Now(),
+                                       CaptureLength: packetLen,
+                                       Length:        packetLen,
+                               },
+                       }
+               }
+       }
+
+       packet := handle.packetQueue[0]
+       handle.packetQueue = handle.packetQueue[1:]
+       data = packet.data
+       ci = packet.ci
+
+       return
+}
+
+// Writes packet data to memif in burst of 1 packet. In case no packet is sent, this method throws io.EOF error and
+// called should stop trying to write packets.
+func (handle *MemifPacketHandle) WritePacketData(data []byte) (err error) {
+       handle.writeMu.Lock()
+       defer handle.writeMu.Unlock()
+
+       if handle.stop {
+               err = io.EOF
+               return
+       }
+
+       count, err := handle.memif.TxBurst(handle.queueId, []RawPacketData{data})
+
+       if err != nil {
+               return
+       }
+
+       if count == 0 {
+               err = io.EOF
+       }
+
+       return
+}
+
+// Waits for all read and write operations to finish and then prevents more from occurring. Handle can be closed only
+// once and then can never be opened again.
+func (handle *MemifPacketHandle) Close() {
+       handle.closeMu.Lock()
+       defer handle.closeMu.Unlock()
+
+       // wait for packet reader to stop
+       handle.readMu.Lock()
+       defer handle.readMu.Unlock()
+
+       // wait for packet writer to stop
+       handle.writeMu.Lock()
+       defer handle.writeMu.Unlock()
+
+       // stop reading and writing
+       handle.stop = true
+}