So you've got some code and you'd like it to be part of gopacket... wonderful! We're happy to accept contributions, whether they're fixes to old protocols, new protocols entirely, or anything else you think would improve the gopacket library. This document is designed to help you to do just that.
The first section deals with the plumbing: how to actually get a change submitted.
The second section deals with coding style... Go is great in that it has a uniform style implemented by 'go fmt', but there's still some decisions we've made that go above and beyond, and if you follow them, they won't come up in your code review.
The third section deals with some of the implementation decisions we've made, which may help you to understand the current code and which we may ask you to conform to (or provide compelling reasons for ignoring).
Overall, we hope this document will help you to understand our system and write great code which fits in, and help us to turn around on your code review quickly so the code can make it into the master branch as quickly as possible.
We use github.com's Pull Request feature to receive code contributions from external contributors. See https://help.github.com/articles/creating-a-pull-request/ for details on how to create a request.
Also, there's a local script gc
in the base directory of GoPacket that
runs a local set of checks, which should give you relatively high confidence
that your pull won't fail github pull checks.
go get github.com/google/gopacket
cd $GOROOT/src/pkg/github.com/google/gopacket
git checkout -b <mynewfeature> # create a new branch to work from
... code code code ...
./gc # Run this to do local commits, it performs a number of checks
To sum up:
go fmt
, go vet
, and golint
Many times, you'll be decoding a protocol and run across something bad, a packet corruption or the like. How do you handle this? First off, ALWAYS report the error. You can do this either by returning the error from the decode() function (most common), or if you're up for it you can implement and add an ErrorLayer through the packet builder (the first method is a simple shortcut that does exactly this, then stops any future decoding).
Often, you'll already have decode some part of your protocol by the time you hit your error. Use your own discretion to determine whether the stuff you've already decoded should be returned to the caller or not:
func decodeMyProtocol(data []byte, p gopacket.PacketBuilder) error {
prot := &MyProtocol{}
if len(data) < 10 {
// This error occurred before we did ANYTHING, so there's nothing in my
// protocol that the caller could possibly want. Just return the error.
return fmt.Errorf("Length %d less than 10", len(data))
}
prot.ImportantField1 = data[:5]
prot.ImportantField2 = data[5:10]
// At this point, we've already got enough information in 'prot' to
// warrant returning it to the caller, so we'll add it now.
p.AddLayer(prot)
if len(data) < 15 {
// We encountered an error later in the packet, but the caller already
// has the important info we've gleaned so far.
return fmt.Errorf("Length %d less than 15", len(data))
}
prot.ImportantField3 = data[10:15]
return nil // We've already added the layer, we can just return success.
}
In general, our code follows the approach of returning the first error it encounters. In general, we don't trust any bytes after the first error we see.
The definition of a layer is up to the discretion of the coder. It should be something important enough that it's actually useful to the caller (IE: every TLV value should probably NOT be a layer). However, it can be more granular than a single protocol... IPv6 and SCTP both implement many layers to handle the various parts of the protocol. Use your best judgement, and prepare to defend your decisions during code review. ;)
We strive to make gopacket as fast as possible while still providing lots of features. In general, this means:
Don't fear micro-optimizations. With the above in mind, we welcome micro-optimizations that we think will have positive/neutral impacts on the majority of workloads. A prime example of this is pre-allocating certain structs within a larger one:
type MyProtocol struct {
// Most packets have 1-4 of VeryCommon, so we preallocate it here.
initialAllocation [4]uint32
VeryCommon []uint32
}
func decodeMyProtocol(data []byte, p gopacket.PacketBuilder) error {
prot := &MyProtocol{}
prot.VeryCommon = proto.initialAllocation[:0]
for len(data) > 4 {
field := binary.BigEndian.Uint32(data[:4])
data = data[4:]
// Since we're using the underlying initialAllocation, we won't need to
// allocate new memory for the following append unless we more than 16
// bytes of data, which should be the uncommon case.
prot.VeryCommon = append(prot.VeryCommon, field)
}
p.AddLayer(prot)
if len(data) > 0 {
return fmt.Errorf("MyProtocol packet has %d bytes left after decoding", len(data))
}
return nil
}
If you're pulling a slice from the data you're decoding, don't copy it. Just use the slice itself.
type MyProtocol struct {
A, B net.IP
}
func decodeMyProtocol(data []byte, p gopacket.PacketBuilder) error {
p.AddLayer(&MyProtocol{
A: data[:4],
B: data[4:8],
})
return nil
}
The caller has already agreed, by using this library, that they won't modify the set of bytes they pass in to the decoder, or the library has already copied the set of bytes to a read-only location. See DecodeOptions.NoCopy for more information.
If a protocol has an integer field (uint8, uint16, etc) with a couple of known values that mean something special, make it a type. This allows us to do really nice things like adding a String() function to them, so we can more easily display those to users. Check out layers/enums.go for one example, as well as layers/icmp.go for layer-specific enums.
When naming things, try for descriptiveness over suscinctness. For example, choose DNSResponseRecord over DNSRR.