Documents how to write a great liveblog post and how to submit your post for the GopherCon 2018 liveblog hosted by Sourcegraph at https://sourcegraph.com/gophercon
How do we go from bytes to a complete NDP package?
From bytes to messages
NDP message basics:
ICMPv6 header determines which NDP message is used
Type specifies NDP message, Code always 0
Initial NDP messages and options defined in RFC 4861
Fixed length messages, variable options
Parsing bytes:
An ICMPv6 header will always precede an NDP message
NDP messages on their own are not useful without the ICMPv6 header
Exporting marshal/unmarshal methods bloats the API and GoDoc
Solution: add functions which always add/remove the ICMPv6 header
ndp.Message interface
// A Message is a Neighbor Discovery Protocol message.
type Message interface {
// Type specifies the ICMPv6 type for a Message.
Type() ipv6.ICMPType
// Called via MarshalMessage and ParseMessage.
marshal() ([]byte, error)
unmarshal(b []byte) error
}
Exported Type method for documentation, but other methods unexported
ndp.ParseMessage
ndp.ParseMessage function does bounds checking validation, determines concrete type, continues parsing:
func ParseMessage(b []byte) (Message, error) {
// Bounds check!!!
// Determine ndp.Message from ICMPv6 type.
// Unmarshal ICMPv6 data into ndp.Message implementation.
}
Bounds checking: when using slice elements, you must perform bounds checks to avoid panics:
// The ICMPv6 header is fixed length.
const icmpLen = 4
if len(b) < icmpLen {
return nil, io.ErrUnexpectedEOF
}
Determining ndp.Message type: use a switch to choose the right interface implementation:
// Select the correct ndp.Message type based on ICMPv6 header.
var m Message
switch t := ipv6.ICMPType(b[0]); t {
case ipv6.ICMPTypeNeighborSolicitation:
m = new(NeighborSolicitation)
default:
return nil, fmt.Errorf("ndp: unrecognized ICMPv6 type: %d", t)
}
Unmarshal the ndp.Message implementation: call into the type’s methods to do the rest of the work, skipping the header
// Unmarshal remaining bytes into correct ndp.Message type.
if err := m.unmarshal(b[icmpLen:]); err != nil {
return nil, err
}
A couple of comments on the parsing logic:
Using ndp.ParseMessage, it’s easy to parse ndp.Message types
Concise API: one parsing function
Correctness and simplicity first, performance optimizations later
Our first ndp.Message implementation
Neighbor Solicitation (NS) messages ask a machine for its MAC address
For now, ndp.Option is unimplemented
// An Option is a Neighbor Discovery Protocol option.
type Option interface {
// TODO!
}
What an ICMPv6 + NDP NS message looks like:
The ndp.NeighborSolicitation type mimics the structure defined by the RFC, using doc comments to provide references:
Mimic structure defined by RFC, use doc comments to provide references
// A NeighborSolicitation is a Neighbor Solicitation message as
// described in RFC 4861, Section 4.3.
type NeighborSolicitation struct {
TargetAddress net.IP
Options []Option
}
(Neat godoc feature: it will automatically hyperlink to the RFC as defined above in the comments.)
Checking for IPv4 and IPv6 addresses:
net.IP can contain IPv4, IPv6, or totally invalid IP addresses
A combination of To4 and To16 methods determine the actual type
Don't think this is the friendlies API. Something I’d love to see improved upon in Go 2
net.IP interface? net.IPv4 and net.IPv6 types?
checkIPv6 function:
// checkIPv6 verifies that ip is an IPv6 address.
func checkIPv6(ip net.IP) error {
// To16 returns nil when ip is not a valid IPv4/IPv6 address.
//
// To4 returns non-nil when ip is an IPv4 address.
if ip.To16() == nil || ip.To4() != nil {
return fmt.Errorf("ndp: invalid IPv6 address: %q",
ip.String())
}
return nil
}
ndp.NeighborSolicitation unmarshaling validates incoming bytes and replaces the structure all at once:
func (ns *NeighborSolicitation) unmarshal(b []byte) error {
// Bounds checking!!! (don't want to get paged at 3am in the morning because your code panicked)
// Validation
// Replacing contents of the NeighborSolicitation
}
To validate byte inputs, ensure that field values make sense, typically using rules defined by RFC. I.e., verify that we don't have any sneaky IPv4 addresses:
To replace the structure while unmarshaling, (1) dereference the pointer and replace contents with completed structure. (2) Always make a copy of data from the input slice; don’t assume it’s safe to retain:
Don’t bother allocating memory until you’ve checked your inputs:
// Only accept IPv6 target.
if err := checkIPv6(ns.TargetAddress); err != nil {
return nil, err
}
Allocate once, if possible (allocating once is ideal for speed, but keep it simple):
// Allocate enough space for base message.
b := make([]byte, nsLen)
copy(b[4:], ns.TargetAddress)
// Append any option bytes.
ob, err := marshalOptions(ns.Options)
if err != nil {
return nil, err
}
b = append(b, ob...)
When allocating memory...
Simplicity wins - allocating is okay!
Write comprehensive unit tests to lock in your behavior
Measure for bottlenecks using Go benchmarks and pprof
Optimize only after finding evidence of performance issues
ndp.Message API
ndp.Message types:
ndp.Message usage:
m := &ndp.NeighborSolicitation{
TargetAddress: target,
Options: []ndp.Option{&ndp.LinkLayerAddress{
Direction: ndp.Source,
Addr: addr,
}},
}
b, err := ndp.MarshalMessage(m)
if err != nil {
return fmt.Errorf("failed to marshal: %v", err)
}
m, err := ndp.ParseMessage(b[:n])
if err != nil {
return fmt.Errorf("failed to parse: %v", err)
}
switch m := m.(type) {
case *ndp.NeighborAdvertisement:
printNA(m)
case *ndp.NeighborSolicitation:
printNS(m)
default:
log.Printf("%#v", m)
}
From bytes to options
NDP option basics
Options are encoded in type, length, value (TLV) format:
Fixed length: type
Fixed length: length
Variable length: value/data
TLV options:
ndp.Option interface
// An Option is a Neighbor Discovery Protocol option.
type Option interface {
// Code specifies the NDP option code for an Option.
Code() uint8
// Called when dealing with a Message's Options.
marshal() ([]byte, error)
unmarshal(b []byte) error
}
Parsing options:
An NDP message will always precede options
NDP options on their own are not useful without an NDP message
Exporting marshal/unmarshal methods bloats the API and GoDoc
Solution: use unexported functions with ndp.ParseMessage and ndp.MarshalMessage
marshalOptions function
// marshalOptions marshals Options into a single byte slice.
func marshalOptions(options []Option) ([]byte, error) {
// For each option…
// Marshal the option
// Append it to the output
}
parseOptions function
Parsing is just a little trickier:
// parseOptions parses a slice of Options from a byte slice.
func parseOptions(b []byte) ([]Option, error) {
// Iterate until no bytes remain…
// Bounds check!!!
// Read 2 bytes: type/length
// Determine if option is known
// Append to output slice
}
ndp.Option types include the following:
Source/target link-layer address
MTU
Prefix information
Recursive DNS server
… and more! If we implemented all of them, our API could bloat quickly!
Tips for implementing options:
Consider only implementing the most common options in your package
Prevent API bloat, support 90% of use cases
Tip: add a “raw option” type or similar to enable further extension
ndp.RawOption type
It directly exposes the TLV fields, so code outside this package can pass options that aren't defined in this package. If a particular type of option is used often enough, we can add first-class support for it later.
// A RawOption is an Option in its raw and unprocessed format.
// Unknown Options can be represented using a RawOption.
type RawOption struct {
Type uint8
Length uint8
Value []byte
}
// Code implements Option.
func (r *RawOption) Code() byte { return r.Type }
Add a cmd/ directory with a testing utility during development
Consider building it out to become a useful tool
cmd/ndp: tool for generating and capturing NDP traffic
Introducing the ndp tool:
$ ndp [listen]
# Listens for any NDP messages that pass through the interface
$ ndp rs
# Sends a router solicitation; wait for a router advertisement
$ ndp -t fd00::1 ns
# Sends a neighbor solicitation; wait for a neighbor advertisement
Easier to use than tcpdump. E.g., here's the tcpdump command and output to watch for NDP packets:
$ sudo tcpdump -i enp4s0 'icmp6 && (ip6[40] == 133 or ip6[40] == 134)'
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on enp4s0, link-type EN10MB (Ethernet), capture size 262144 bytes
16:02:42.774725 IP6 nerr-2 > ip6-allrouters: ICMP6, router solicitation, length 16
16:02:42.777116 IP6 _gateway > ip6-allnodes: ICMP6, router advertisement, length 88
Presenter: Matt Layher
Liveblogger: Beyang Liu
An very detailed walkthrough of implementing a networking protocol (NDP in IPv6) in Go, with many, many code snippets.
Matt Layher (@mdlayher, talks) is an engineer at Digital Ocean.
Intro
Outline:
Intro to IPv6
IPv6 adoption:
What is IPv6?
How is IPv6 different from IPv4?
IPv6 tips and tricks
Intro to NDP
What is NDP?
IPv6 and NDP’s big advantage:
SLAAC via NDP
NDP and Go
Package ndp overview
ndp.Message
interface: marshaling/unmarshaling of NDP messagesndp.Option
interface: marshaling/unmarshaling of NDP optionsndp.Conn
struct: manage ICMPv6 connection, read/writendp.Message
sHow do we go from bytes to a complete NDP package?
From bytes to messages
NDP message basics:
Parsing bytes:
ndp.Message
interfacendp.ParseMessage
ndp.ParseMessage
function does bounds checking validation, determines concrete type, continues parsing:Bounds checking: when using slice elements, you must perform bounds checks to avoid panics:
Determining
ndp.Message
type: use a switch to choose the right interface implementation:Unmarshal the
ndp.Message
implementation: call into the type’s methods to do the rest of the work, skipping the headerA couple of comments on the parsing logic:
ndp.ParseMessage
, it’s easy to parsendp.Message
typesOur first
ndp.Message
implementationndp.Option
is unimplementedWhat an ICMPv6 + NDP NS message looks like:
The
ndp.NeighborSolicitation
type mimics the structure defined by the RFC, using doc comments to provide references:(Neat godoc feature: it will automatically hyperlink to the RFC as defined above in the comments.)
Checking for IPv4 and IPv6 addresses:
checkIPv6
function:ndp.NeighborSolicitation
unmarshaling validates incoming bytes and replaces the structure all at once:To validate byte inputs, ensure that field values make sense, typically using rules defined by RFC. I.e., verify that we don't have any sneaky IPv4 addresses:
To replace the structure while unmarshaling, (1) dereference the pointer and replace contents with completed structure. (2) Always make a copy of data from the input slice; don’t assume it’s safe to retain:
From messages to bytes
Things to remember when marshaling messages:
ndp.MarshalMessage
functionMarshal an ndp.Message into binary, prepend ICMPv6 header
When marshaling
ndp.Messages
, simplicity wins. Allocating is okay until your performance needs are not met:Same goes for ICMPv6 messages:
ndp.NeighborSolicitation
marshalingValidate before you allocate:
Don’t bother allocating memory until you’ve checked your inputs:
Allocate once, if possible (allocating once is ideal for speed, but keep it simple):
When allocating memory...
ndp.Message
APIndp.Message
types:ndp.Message
usage:From bytes to options
NDP option basics
Options are encoded in type, length, value (TLV) format:
TLV options:
ndp.Option interface
Parsing options:
ndp.ParseMessage
andndp.MarshalMessage
marshalOptions function
parseOptions function
Parsing is just a little trickier:
ndp.Option
types include the following:Tips for implementing options:
ndp.RawOption type
It directly exposes the TLV fields, so code outside this package can pass options that aren't defined in this package. If a particular type of option is used often enough, we can add first-class support for it later.
ndp.Option
types:ndp.Option
usage:Fuzzing byte parsers
Fuzzing lets you catch and prevent errors arising from unexpected and unhandled input cases. E.g., avoid errors like this one:
Enter Dmitry Vyukov's
go-fuzz
. If you’re parsing raw bytes, there’s a high potential for unexpected behavior:github.com/dvyukov/go-fuzz address this problem:
go-fuzz setup:
go-fuzz usage:
go-fuzz conclusions: Use it! Use go-fuzz on ALL byte parsers: github.com/dvyukov/go-fuzz
ndp.Conn
Let's implement the struct that represents an NDP connection.
“Conn” types represent network connections. They typically have the following:
net
vsx/net
ICMPv6 networking packages in Go:
Here's how you create a ICMPv6 listener (this is a privileged operation, usually requires root):
Reading ICMPv6 messages is similar to standard APIs, but also returns IPv6 control messages:
Writing ICMPv6 messages is similar to standard APIs, but you can specify IPv6 control messages:
ndp.Conn usage:
Create an ndp.Conn by selecting an interface, dialing ICMPv6, and specifying an address to listen on:
How to read messages? Here's a code snippet to keep reading and printing messages until an error occurs:
Writing ndp.Messages: Send a router solicitation to trigger router advertisements on the network:
Build a tool to test your package
Add a cmd/ directory with a testing utility during development
Introducing the ndp tool:
Easier to use than
tcpdump
. E.g., here's thetcpdump
command and output to watch for NDP packets:Compare that with
ndp
:Troubleshooting your ISP’s equipment with Go
You can also use Go to troubleshoot any difficulties your ISP has with IPv6.
Ubiquiti EdgeRouter Lite can run Go programs:
Conclusions
Resources: