ipv6: add {Read,Write}Batch methods to PacketConn

This change provides message IO functionality that may support the
construction of modern datagram transport protocols.

The modern datagram transport protocols on a multihomed node basically
need to control each packet path for traffic engineering by using
information belongs to network- or link-layer implementation. In
addtion, it's desirable to be able to do simultaneous transmission
across multiple network- or link-layer adjacencies wihtout any
additional cost.

The ReadBatch and WriteBatch methods of PacketConn can be used to read
and write an IO message that contains the information of network- or
link-layer implementation, and read and write a batch of IO messages
on Linux. The Marshal and Parse methods of ControlMessage can help to
marshal and parse information contained in IO messages.

Updates golang/go#3661.

Change-Id: I94484e2e135f4969ddf5c2548bf6be0cc16888bf
Reviewed-on: https://go-review.googlesource.com/38276
Run-TryBot: Mikio Hara <mikioh.mikioh@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
diff --git a/ipv6/batch.go b/ipv6/batch.go
new file mode 100644
index 0000000..4f5fe68
--- /dev/null
+++ b/ipv6/batch.go
@@ -0,0 +1,119 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build go1.9
+
+package ipv6
+
+import (
+	"net"
+	"runtime"
+	"syscall"
+
+	"golang.org/x/net/internal/socket"
+)
+
+// BUG(mikio): On Windows, the ReadBatch and WriteBatch methods of
+// PacketConn are not implemented.
+
+// A Message represents an IO message.
+//
+//	type Message struct {
+//		Buffers [][]byte
+//		OOB     []byte
+//		Addr    net.Addr
+//		N       int
+//		NN      int
+//		Flags   int
+//	}
+//
+// The Buffers fields represents a list of contiguous buffers, which
+// can be used for vectored IO, for example, putting a header and a
+// payload in each slice.
+// When writing, the Buffers field must contain at least one byte to
+// write.
+// When reading, the Buffers field will always contain a byte to read.
+//
+// The OOB field contains protocol-specific control or miscellaneous
+// ancillary data known as out-of-band data.
+// It can be nil when not required.
+//
+// The Addr field specifies a destination address when writing.
+// It can be nil when the underlying protocol of the endpoint uses
+// connection-oriented communication.
+// After a successful read, it may contain the source address on the
+// received packet.
+//
+// The N field indicates the number of bytes read or written from/to
+// Buffers.
+//
+// The NN field indicates the number of bytes read or written from/to
+// OOB.
+//
+// The Flags field contains protocol-specific information on the
+// received message.
+type Message = socket.Message
+
+// ReadBatch reads a batch of messages.
+//
+// The provided flags is a set of platform-dependent flags, such as
+// syscall.MSG_PEEK.
+//
+// On a successful read it returns the number of messages received, up
+// to len(ms).
+//
+// On Linux, a batch read will be optimized.
+// On other platforms, this method will read only a single message.
+func (c *payloadHandler) ReadBatch(ms []Message, flags int) (int, error) {
+	if !c.ok() {
+		return 0, syscall.EINVAL
+	}
+	switch runtime.GOOS {
+	case "linux":
+		n, err := c.RecvMsgs([]socket.Message(ms), flags)
+		if err != nil {
+			err = &net.OpError{Op: "read", Net: c.PacketConn.LocalAddr().Network(), Source: c.PacketConn.LocalAddr(), Err: err}
+		}
+		return n, err
+	default:
+		n := 1
+		err := c.RecvMsg(&ms[0], flags)
+		if err != nil {
+			n = 0
+			err = &net.OpError{Op: "read", Net: c.PacketConn.LocalAddr().Network(), Source: c.PacketConn.LocalAddr(), Err: err}
+		}
+		return n, err
+	}
+}
+
+// WriteBatch writes a batch of messages.
+//
+// The provided flags is a set of platform-dependent flags, such as
+// syscall.MSG_DONTROUTE.
+//
+// It returns the number of messages written on a successful write.
+//
+// On Linux, a batch write will be optimized.
+// On other platforms, this method will write only a single message.
+func (c *payloadHandler) WriteBatch(ms []Message, flags int) (int, error) {
+	if !c.ok() {
+		return 0, syscall.EINVAL
+	}
+	switch runtime.GOOS {
+	case "linux":
+		n, err := c.SendMsgs([]socket.Message(ms), flags)
+		if err != nil {
+			err = &net.OpError{Op: "write", Net: c.PacketConn.LocalAddr().Network(), Source: c.PacketConn.LocalAddr(), Err: err}
+		}
+		return n, err
+	default:
+		n := 1
+		err := c.SendMsg(&ms[0], flags)
+		if err != nil {
+			n = 0
+			err = &net.OpError{Op: "write", Net: c.PacketConn.LocalAddr().Network(), Source: c.PacketConn.LocalAddr(), Err: err}
+		}
+		return n, err
+	}
+}
diff --git a/ipv6/control.go b/ipv6/control.go
index 674628d..fe78818 100644
--- a/ipv6/control.go
+++ b/ipv6/control.go
@@ -8,6 +8,9 @@
 	"fmt"
 	"net"
 	"sync"
+
+	"golang.org/x/net/internal/iana"
+	"golang.org/x/net/internal/socket"
 )
 
 // Note that RFC 3542 obsoletes RFC 2292 but OS X Snow Leopard and the
@@ -66,6 +69,105 @@
 	return fmt.Sprintf("tclass=%#x hoplim=%d src=%v dst=%v ifindex=%d nexthop=%v mtu=%d", cm.TrafficClass, cm.HopLimit, cm.Src, cm.Dst, cm.IfIndex, cm.NextHop, cm.MTU)
 }
 
+// Marshal returns the binary encoding of cm.
+func (cm *ControlMessage) Marshal() []byte {
+	if cm == nil {
+		return nil
+	}
+	var l int
+	tclass := false
+	if ctlOpts[ctlTrafficClass].name > 0 && cm.TrafficClass > 0 {
+		tclass = true
+		l += socket.ControlMessageSpace(ctlOpts[ctlTrafficClass].length)
+	}
+	hoplimit := false
+	if ctlOpts[ctlHopLimit].name > 0 && cm.HopLimit > 0 {
+		hoplimit = true
+		l += socket.ControlMessageSpace(ctlOpts[ctlHopLimit].length)
+	}
+	pktinfo := false
+	if ctlOpts[ctlPacketInfo].name > 0 && (cm.Src.To16() != nil && cm.Src.To4() == nil || cm.IfIndex > 0) {
+		pktinfo = true
+		l += socket.ControlMessageSpace(ctlOpts[ctlPacketInfo].length)
+	}
+	nexthop := false
+	if ctlOpts[ctlNextHop].name > 0 && cm.NextHop.To16() != nil && cm.NextHop.To4() == nil {
+		nexthop = true
+		l += socket.ControlMessageSpace(ctlOpts[ctlNextHop].length)
+	}
+	var b []byte
+	if l > 0 {
+		b = make([]byte, l)
+		bb := b
+		if tclass {
+			bb = ctlOpts[ctlTrafficClass].marshal(bb, cm)
+		}
+		if hoplimit {
+			bb = ctlOpts[ctlHopLimit].marshal(bb, cm)
+		}
+		if pktinfo {
+			bb = ctlOpts[ctlPacketInfo].marshal(bb, cm)
+		}
+		if nexthop {
+			bb = ctlOpts[ctlNextHop].marshal(bb, cm)
+		}
+	}
+	return b
+}
+
+// Parse parses b as a control message and stores the result in cm.
+func (cm *ControlMessage) Parse(b []byte) error {
+	ms, err := socket.ControlMessage(b).Parse()
+	if err != nil {
+		return err
+	}
+	for _, m := range ms {
+		lvl, typ, l, err := m.ParseHeader()
+		if err != nil {
+			return err
+		}
+		if lvl != iana.ProtocolIPv6 {
+			continue
+		}
+		switch typ {
+		case ctlOpts[ctlTrafficClass].name:
+			ctlOpts[ctlTrafficClass].parse(cm, m.Data(l))
+		case ctlOpts[ctlHopLimit].name:
+			ctlOpts[ctlHopLimit].parse(cm, m.Data(l))
+		case ctlOpts[ctlPacketInfo].name:
+			ctlOpts[ctlPacketInfo].parse(cm, m.Data(l))
+		case ctlOpts[ctlPathMTU].name:
+			ctlOpts[ctlPathMTU].parse(cm, m.Data(l))
+		}
+	}
+	return nil
+}
+
+// NewControlMessage returns a new control message.
+//
+// The returned message is large enough for options specified by cf.
+func NewControlMessage(cf ControlFlags) []byte {
+	opt := rawOpt{cflags: cf}
+	var l int
+	if opt.isset(FlagTrafficClass) && ctlOpts[ctlTrafficClass].name > 0 {
+		l += socket.ControlMessageSpace(ctlOpts[ctlTrafficClass].length)
+	}
+	if opt.isset(FlagHopLimit) && ctlOpts[ctlHopLimit].name > 0 {
+		l += socket.ControlMessageSpace(ctlOpts[ctlHopLimit].length)
+	}
+	if opt.isset(flagPacketInfo) && ctlOpts[ctlPacketInfo].name > 0 {
+		l += socket.ControlMessageSpace(ctlOpts[ctlPacketInfo].length)
+	}
+	if opt.isset(FlagPathMTU) && ctlOpts[ctlPathMTU].name > 0 {
+		l += socket.ControlMessageSpace(ctlOpts[ctlPathMTU].length)
+	}
+	var b []byte
+	if l > 0 {
+		b = make([]byte, l)
+	}
+	return b
+}
+
 // Ancillary data socket options
 const (
 	ctlTrafficClass = iota // header field
diff --git a/ipv6/control_rfc2292_unix.go b/ipv6/control_rfc2292_unix.go
index d1693af..1cf3901 100644
--- a/ipv6/control_rfc2292_unix.go
+++ b/ipv6/control_rfc2292_unix.go
@@ -7,31 +7,26 @@
 package ipv6
 
 import (
-	"syscall"
 	"unsafe"
 
 	"golang.org/x/net/internal/iana"
+	"golang.org/x/net/internal/socket"
 )
 
 func marshal2292HopLimit(b []byte, cm *ControlMessage) []byte {
-	m := (*syscall.Cmsghdr)(unsafe.Pointer(&b[0]))
-	m.Level = iana.ProtocolIPv6
-	m.Type = sysIPV6_2292HOPLIMIT
-	m.SetLen(syscall.CmsgLen(4))
+	m := socket.ControlMessage(b)
+	m.MarshalHeader(iana.ProtocolIPv6, sysIPV6_2292HOPLIMIT, 4)
 	if cm != nil {
-		data := b[syscall.CmsgLen(0):]
-		nativeEndian.PutUint32(data[:4], uint32(cm.HopLimit))
+		nativeEndian.PutUint32(m.Data(4), uint32(cm.HopLimit))
 	}
-	return b[syscall.CmsgSpace(4):]
+	return m.Next(4)
 }
 
 func marshal2292PacketInfo(b []byte, cm *ControlMessage) []byte {
-	m := (*syscall.Cmsghdr)(unsafe.Pointer(&b[0]))
-	m.Level = iana.ProtocolIPv6
-	m.Type = sysIPV6_2292PKTINFO
-	m.SetLen(syscall.CmsgLen(sizeofInet6Pktinfo))
+	m := socket.ControlMessage(b)
+	m.MarshalHeader(iana.ProtocolIPv6, sysIPV6_2292PKTINFO, sizeofInet6Pktinfo)
 	if cm != nil {
-		pi := (*inet6Pktinfo)(unsafe.Pointer(&b[syscall.CmsgLen(0)]))
+		pi := (*inet6Pktinfo)(unsafe.Pointer(&m.Data(sizeofInet6Pktinfo)[0]))
 		if ip := cm.Src.To16(); ip != nil && ip.To4() == nil {
 			copy(pi.Addr[:], ip)
 		}
@@ -39,17 +34,15 @@
 			pi.setIfindex(cm.IfIndex)
 		}
 	}
-	return b[syscall.CmsgSpace(sizeofInet6Pktinfo):]
+	return m.Next(sizeofInet6Pktinfo)
 }
 
 func marshal2292NextHop(b []byte, cm *ControlMessage) []byte {
-	m := (*syscall.Cmsghdr)(unsafe.Pointer(&b[0]))
-	m.Level = iana.ProtocolIPv6
-	m.Type = sysIPV6_2292NEXTHOP
-	m.SetLen(syscall.CmsgLen(sizeofSockaddrInet6))
+	m := socket.ControlMessage(b)
+	m.MarshalHeader(iana.ProtocolIPv6, sysIPV6_2292NEXTHOP, sizeofSockaddrInet6)
 	if cm != nil {
-		sa := (*sockaddrInet6)(unsafe.Pointer(&b[syscall.CmsgLen(0)]))
+		sa := (*sockaddrInet6)(unsafe.Pointer(&m.Data(sizeofSockaddrInet6)[0]))
 		sa.setSockaddr(cm.NextHop, cm.IfIndex)
 	}
-	return b[syscall.CmsgSpace(sizeofSockaddrInet6):]
+	return m.Next(sizeofSockaddrInet6)
 }
diff --git a/ipv6/control_rfc3542_unix.go b/ipv6/control_rfc3542_unix.go
index 2800df4..62cded6 100644
--- a/ipv6/control_rfc3542_unix.go
+++ b/ipv6/control_rfc3542_unix.go
@@ -7,22 +7,20 @@
 package ipv6
 
 import (
-	"syscall"
+	"net"
 	"unsafe"
 
 	"golang.org/x/net/internal/iana"
+	"golang.org/x/net/internal/socket"
 )
 
 func marshalTrafficClass(b []byte, cm *ControlMessage) []byte {
-	m := (*syscall.Cmsghdr)(unsafe.Pointer(&b[0]))
-	m.Level = iana.ProtocolIPv6
-	m.Type = sysIPV6_TCLASS
-	m.SetLen(syscall.CmsgLen(4))
+	m := socket.ControlMessage(b)
+	m.MarshalHeader(iana.ProtocolIPv6, sysIPV6_TCLASS, 4)
 	if cm != nil {
-		data := b[syscall.CmsgLen(0):]
-		nativeEndian.PutUint32(data[:4], uint32(cm.TrafficClass))
+		nativeEndian.PutUint32(m.Data(4), uint32(cm.TrafficClass))
 	}
-	return b[syscall.CmsgSpace(4):]
+	return m.Next(4)
 }
 
 func parseTrafficClass(cm *ControlMessage, b []byte) {
@@ -30,15 +28,12 @@
 }
 
 func marshalHopLimit(b []byte, cm *ControlMessage) []byte {
-	m := (*syscall.Cmsghdr)(unsafe.Pointer(&b[0]))
-	m.Level = iana.ProtocolIPv6
-	m.Type = sysIPV6_HOPLIMIT
-	m.SetLen(syscall.CmsgLen(4))
+	m := socket.ControlMessage(b)
+	m.MarshalHeader(iana.ProtocolIPv6, sysIPV6_HOPLIMIT, 4)
 	if cm != nil {
-		data := b[syscall.CmsgLen(0):]
-		nativeEndian.PutUint32(data[:4], uint32(cm.HopLimit))
+		nativeEndian.PutUint32(m.Data(4), uint32(cm.HopLimit))
 	}
-	return b[syscall.CmsgSpace(4):]
+	return m.Next(4)
 }
 
 func parseHopLimit(cm *ControlMessage, b []byte) {
@@ -46,12 +41,10 @@
 }
 
 func marshalPacketInfo(b []byte, cm *ControlMessage) []byte {
-	m := (*syscall.Cmsghdr)(unsafe.Pointer(&b[0]))
-	m.Level = iana.ProtocolIPv6
-	m.Type = sysIPV6_PKTINFO
-	m.SetLen(syscall.CmsgLen(sizeofInet6Pktinfo))
+	m := socket.ControlMessage(b)
+	m.MarshalHeader(iana.ProtocolIPv6, sysIPV6_PKTINFO, sizeofInet6Pktinfo)
 	if cm != nil {
-		pi := (*inet6Pktinfo)(unsafe.Pointer(&b[syscall.CmsgLen(0)]))
+		pi := (*inet6Pktinfo)(unsafe.Pointer(&m.Data(sizeofInet6Pktinfo)[0]))
 		if ip := cm.Src.To16(); ip != nil && ip.To4() == nil {
 			copy(pi.Addr[:], ip)
 		}
@@ -59,41 +52,43 @@
 			pi.setIfindex(cm.IfIndex)
 		}
 	}
-	return b[syscall.CmsgSpace(sizeofInet6Pktinfo):]
+	return m.Next(sizeofInet6Pktinfo)
 }
 
 func parsePacketInfo(cm *ControlMessage, b []byte) {
 	pi := (*inet6Pktinfo)(unsafe.Pointer(&b[0]))
-	cm.Dst = pi.Addr[:]
+	if len(cm.Dst) < net.IPv6len {
+		cm.Dst = make(net.IP, net.IPv6len)
+	}
+	copy(cm.Dst, pi.Addr[:])
 	cm.IfIndex = int(pi.Ifindex)
 }
 
 func marshalNextHop(b []byte, cm *ControlMessage) []byte {
-	m := (*syscall.Cmsghdr)(unsafe.Pointer(&b[0]))
-	m.Level = iana.ProtocolIPv6
-	m.Type = sysIPV6_NEXTHOP
-	m.SetLen(syscall.CmsgLen(sizeofSockaddrInet6))
+	m := socket.ControlMessage(b)
+	m.MarshalHeader(iana.ProtocolIPv6, sysIPV6_NEXTHOP, sizeofSockaddrInet6)
 	if cm != nil {
-		sa := (*sockaddrInet6)(unsafe.Pointer(&b[syscall.CmsgLen(0)]))
+		sa := (*sockaddrInet6)(unsafe.Pointer(&m.Data(sizeofSockaddrInet6)[0]))
 		sa.setSockaddr(cm.NextHop, cm.IfIndex)
 	}
-	return b[syscall.CmsgSpace(sizeofSockaddrInet6):]
+	return m.Next(sizeofSockaddrInet6)
 }
 
 func parseNextHop(cm *ControlMessage, b []byte) {
 }
 
 func marshalPathMTU(b []byte, cm *ControlMessage) []byte {
-	m := (*syscall.Cmsghdr)(unsafe.Pointer(&b[0]))
-	m.Level = iana.ProtocolIPv6
-	m.Type = sysIPV6_PATHMTU
-	m.SetLen(syscall.CmsgLen(sizeofIPv6Mtuinfo))
-	return b[syscall.CmsgSpace(sizeofIPv6Mtuinfo):]
+	m := socket.ControlMessage(b)
+	m.MarshalHeader(iana.ProtocolIPv6, sysIPV6_PATHMTU, sizeofIPv6Mtuinfo)
+	return m.Next(sizeofIPv6Mtuinfo)
 }
 
 func parsePathMTU(cm *ControlMessage, b []byte) {
 	mi := (*ipv6Mtuinfo)(unsafe.Pointer(&b[0]))
-	cm.Dst = mi.Addr.Addr[:]
+	if len(cm.Dst) < net.IPv6len {
+		cm.Dst = make(net.IP, net.IPv6len)
+	}
+	copy(cm.Dst, mi.Addr.Addr[:])
 	cm.IfIndex = int(mi.Addr.Scope_id)
 	cm.MTU = int(mi.Mtu)
 }
diff --git a/ipv6/control_stub.go b/ipv6/control_stub.go
index 963d200..a045f28 100644
--- a/ipv6/control_stub.go
+++ b/ipv6/control_stub.go
@@ -11,15 +11,3 @@
 func setControlMessage(c *socket.Conn, opt *rawOpt, cf ControlFlags, on bool) error {
 	return errOpNoSupport
 }
-
-func newControlMessage(opt *rawOpt) (oob []byte) {
-	return nil
-}
-
-func parseControlMessage(b []byte) (*ControlMessage, error) {
-	return nil, errOpNoSupport
-}
-
-func marshalControlMessage(cm *ControlMessage) (oob []byte) {
-	return nil
-}
diff --git a/ipv6/control_unix.go b/ipv6/control_unix.go
index 29a59e0..6651506 100644
--- a/ipv6/control_unix.go
+++ b/ipv6/control_unix.go
@@ -6,13 +6,7 @@
 
 package ipv6
 
-import (
-	"os"
-	"syscall"
-
-	"golang.org/x/net/internal/iana"
-	"golang.org/x/net/internal/socket"
-)
+import "golang.org/x/net/internal/socket"
 
 func setControlMessage(c *socket.Conn, opt *rawOpt, cf ControlFlags, on bool) error {
 	opt.Lock()
@@ -59,96 +53,3 @@
 	}
 	return nil
 }
-
-func newControlMessage(opt *rawOpt) (oob []byte) {
-	opt.RLock()
-	var l int
-	if opt.isset(FlagTrafficClass) && ctlOpts[ctlTrafficClass].name > 0 {
-		l += syscall.CmsgSpace(ctlOpts[ctlTrafficClass].length)
-	}
-	if opt.isset(FlagHopLimit) && ctlOpts[ctlHopLimit].name > 0 {
-		l += syscall.CmsgSpace(ctlOpts[ctlHopLimit].length)
-	}
-	if opt.isset(flagPacketInfo) && ctlOpts[ctlPacketInfo].name > 0 {
-		l += syscall.CmsgSpace(ctlOpts[ctlPacketInfo].length)
-	}
-	if opt.isset(FlagPathMTU) && ctlOpts[ctlPathMTU].name > 0 {
-		l += syscall.CmsgSpace(ctlOpts[ctlPathMTU].length)
-	}
-	if l > 0 {
-		oob = make([]byte, l)
-	}
-	opt.RUnlock()
-	return
-}
-
-func parseControlMessage(b []byte) (*ControlMessage, error) {
-	if len(b) == 0 {
-		return nil, nil
-	}
-	cmsgs, err := syscall.ParseSocketControlMessage(b)
-	if err != nil {
-		return nil, os.NewSyscallError("parse socket control message", err)
-	}
-	cm := &ControlMessage{}
-	for _, m := range cmsgs {
-		if m.Header.Level != iana.ProtocolIPv6 {
-			continue
-		}
-		switch int(m.Header.Type) {
-		case ctlOpts[ctlTrafficClass].name:
-			ctlOpts[ctlTrafficClass].parse(cm, m.Data[:])
-		case ctlOpts[ctlHopLimit].name:
-			ctlOpts[ctlHopLimit].parse(cm, m.Data[:])
-		case ctlOpts[ctlPacketInfo].name:
-			ctlOpts[ctlPacketInfo].parse(cm, m.Data[:])
-		case ctlOpts[ctlPathMTU].name:
-			ctlOpts[ctlPathMTU].parse(cm, m.Data[:])
-		}
-	}
-	return cm, nil
-}
-
-func marshalControlMessage(cm *ControlMessage) (oob []byte) {
-	if cm == nil {
-		return
-	}
-	var l int
-	tclass := false
-	if ctlOpts[ctlTrafficClass].name > 0 && cm.TrafficClass > 0 {
-		tclass = true
-		l += syscall.CmsgSpace(ctlOpts[ctlTrafficClass].length)
-	}
-	hoplimit := false
-	if ctlOpts[ctlHopLimit].name > 0 && cm.HopLimit > 0 {
-		hoplimit = true
-		l += syscall.CmsgSpace(ctlOpts[ctlHopLimit].length)
-	}
-	pktinfo := false
-	if ctlOpts[ctlPacketInfo].name > 0 && (cm.Src.To16() != nil && cm.Src.To4() == nil || cm.IfIndex > 0) {
-		pktinfo = true
-		l += syscall.CmsgSpace(ctlOpts[ctlPacketInfo].length)
-	}
-	nexthop := false
-	if ctlOpts[ctlNextHop].name > 0 && cm.NextHop.To16() != nil && cm.NextHop.To4() == nil {
-		nexthop = true
-		l += syscall.CmsgSpace(ctlOpts[ctlNextHop].length)
-	}
-	if l > 0 {
-		oob = make([]byte, l)
-		b := oob
-		if tclass {
-			b = ctlOpts[ctlTrafficClass].marshal(b, cm)
-		}
-		if hoplimit {
-			b = ctlOpts[ctlHopLimit].marshal(b, cm)
-		}
-		if pktinfo {
-			b = ctlOpts[ctlPacketInfo].marshal(b, cm)
-		}
-		if nexthop {
-			b = ctlOpts[ctlNextHop].marshal(b, cm)
-		}
-	}
-	return
-}
diff --git a/ipv6/control_windows.go b/ipv6/control_windows.go
index 97bb1e4..ef2563b 100644
--- a/ipv6/control_windows.go
+++ b/ipv6/control_windows.go
@@ -14,18 +14,3 @@
 	// TODO(mikio): implement this
 	return syscall.EWINDOWS
 }
-
-func newControlMessage(opt *rawOpt) (oob []byte) {
-	// TODO(mikio): implement this
-	return nil
-}
-
-func parseControlMessage(b []byte) (*ControlMessage, error) {
-	// TODO(mikio): implement this
-	return nil, syscall.EWINDOWS
-}
-
-func marshalControlMessage(cm *ControlMessage) (oob []byte) {
-	// TODO(mikio): implement this
-	return nil
-}
diff --git a/ipv6/endpoint.go b/ipv6/endpoint.go
index 001d267..0624c17 100644
--- a/ipv6/endpoint.go
+++ b/ipv6/endpoint.go
@@ -123,6 +123,6 @@
 	return &PacketConn{
 		genericOpt:     genericOpt{Conn: cc},
 		dgramOpt:       dgramOpt{Conn: cc},
-		payloadHandler: payloadHandler{PacketConn: c},
+		payloadHandler: payloadHandler{PacketConn: c, Conn: cc},
 	}
 }
diff --git a/ipv6/payload.go b/ipv6/payload.go
index d9f8225..a8197f1 100644
--- a/ipv6/payload.go
+++ b/ipv6/payload.go
@@ -4,7 +4,11 @@
 
 package ipv6
 
-import "net"
+import (
+	"net"
+
+	"golang.org/x/net/internal/socket"
+)
 
 // BUG(mikio): On Windows, the ControlMessage for ReadFrom and WriteTo
 // methods of PacketConn is not implemented.
@@ -12,7 +16,8 @@
 // A payloadHandler represents the IPv6 datagram payload handler.
 type payloadHandler struct {
 	net.PacketConn
+	*socket.Conn
 	rawOpt
 }
 
-func (c *payloadHandler) ok() bool { return c != nil && c.PacketConn != nil }
+func (c *payloadHandler) ok() bool { return c != nil && c.PacketConn != nil && c.Conn != nil }
diff --git a/ipv6/payload_cmsg.go b/ipv6/payload_cmsg.go
index e853c80..4ee4b06 100644
--- a/ipv6/payload_cmsg.go
+++ b/ipv6/payload_cmsg.go
@@ -19,27 +19,7 @@
 	if !c.ok() {
 		return 0, nil, nil, syscall.EINVAL
 	}
-	oob := newControlMessage(&c.rawOpt)
-	var oobn int
-	switch c := c.PacketConn.(type) {
-	case *net.UDPConn:
-		if n, oobn, _, src, err = c.ReadMsgUDP(b, oob); err != nil {
-			return 0, nil, nil, err
-		}
-	case *net.IPConn:
-		if n, oobn, _, src, err = c.ReadMsgIP(b, oob); err != nil {
-			return 0, nil, nil, err
-		}
-	default:
-		return 0, nil, nil, errInvalidConnType
-	}
-	if cm, err = parseControlMessage(oob[:oobn]); err != nil {
-		return 0, nil, nil, err
-	}
-	if cm != nil {
-		cm.Src = netAddrToIP16(src)
-	}
-	return
+	return c.readFrom(b)
 }
 
 // WriteTo writes a payload of the IPv6 datagram, to the destination
@@ -51,20 +31,5 @@
 	if !c.ok() {
 		return 0, syscall.EINVAL
 	}
-	oob := marshalControlMessage(cm)
-	if dst == nil {
-		return 0, errMissingAddress
-	}
-	switch c := c.PacketConn.(type) {
-	case *net.UDPConn:
-		n, _, err = c.WriteMsgUDP(b, oob, dst.(*net.UDPAddr))
-	case *net.IPConn:
-		n, _, err = c.WriteMsgIP(b, oob, dst.(*net.IPAddr))
-	default:
-		return 0, errInvalidConnType
-	}
-	if err != nil {
-		return 0, err
-	}
-	return
+	return c.writeTo(b, cm, dst)
 }
diff --git a/ipv6/payload_cmsg_go1_8.go b/ipv6/payload_cmsg_go1_8.go
new file mode 100644
index 0000000..431cff4
--- /dev/null
+++ b/ipv6/payload_cmsg_go1_8.go
@@ -0,0 +1,55 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !go1.9
+// +build !nacl,!plan9,!windows
+
+package ipv6
+
+import "net"
+
+func (c *payloadHandler) readFrom(b []byte) (n int, cm *ControlMessage, src net.Addr, err error) {
+	c.rawOpt.RLock()
+	oob := NewControlMessage(c.rawOpt.cflags)
+	c.rawOpt.RUnlock()
+	var nn int
+	switch c := c.PacketConn.(type) {
+	case *net.UDPConn:
+		if n, nn, _, src, err = c.ReadMsgUDP(b, oob); err != nil {
+			return 0, nil, nil, err
+		}
+	case *net.IPConn:
+		if n, nn, _, src, err = c.ReadMsgIP(b, oob); err != nil {
+			return 0, nil, nil, err
+		}
+	default:
+		return 0, nil, nil, &net.OpError{Op: "read", Net: c.LocalAddr().Network(), Source: c.LocalAddr(), Err: errInvalidConnType}
+	}
+	if nn > 0 {
+		cm = new(ControlMessage)
+		if err = cm.Parse(oob[:nn]); err != nil {
+			return 0, nil, nil, &net.OpError{Op: "read", Net: c.PacketConn.LocalAddr().Network(), Source: c.PacketConn.LocalAddr(), Err: err}
+		}
+	}
+	if cm != nil {
+		cm.Src = netAddrToIP16(src)
+	}
+	return
+}
+
+func (c *payloadHandler) writeTo(b []byte, cm *ControlMessage, dst net.Addr) (n int, err error) {
+	oob := cm.Marshal()
+	if dst == nil {
+		return 0, &net.OpError{Op: "write", Net: c.PacketConn.LocalAddr().Network(), Source: c.PacketConn.LocalAddr(), Err: errMissingAddress}
+	}
+	switch c := c.PacketConn.(type) {
+	case *net.UDPConn:
+		n, _, err = c.WriteMsgUDP(b, oob, dst.(*net.UDPAddr))
+	case *net.IPConn:
+		n, _, err = c.WriteMsgIP(b, oob, dst.(*net.IPAddr))
+	default:
+		return 0, &net.OpError{Op: "write", Net: c.LocalAddr().Network(), Source: c.LocalAddr(), Err: errInvalidConnType}
+	}
+	return
+}
diff --git a/ipv6/payload_cmsg_go1_9.go b/ipv6/payload_cmsg_go1_9.go
new file mode 100644
index 0000000..4072c58
--- /dev/null
+++ b/ipv6/payload_cmsg_go1_9.go
@@ -0,0 +1,57 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build go1.9
+// +build !nacl,!plan9,!windows
+
+package ipv6
+
+import (
+	"net"
+
+	"golang.org/x/net/internal/socket"
+)
+
+func (c *payloadHandler) readFrom(b []byte) (int, *ControlMessage, net.Addr, error) {
+	c.rawOpt.RLock()
+	m := socket.Message{
+		Buffers: [][]byte{b},
+		OOB:     NewControlMessage(c.rawOpt.cflags),
+	}
+	c.rawOpt.RUnlock()
+	switch c.PacketConn.(type) {
+	case *net.UDPConn:
+		if err := c.RecvMsg(&m, 0); err != nil {
+			return 0, nil, nil, &net.OpError{Op: "read", Net: c.PacketConn.LocalAddr().Network(), Source: c.PacketConn.LocalAddr(), Err: err}
+		}
+	case *net.IPConn:
+		if err := c.RecvMsg(&m, 0); err != nil {
+			return 0, nil, nil, &net.OpError{Op: "read", Net: c.PacketConn.LocalAddr().Network(), Source: c.PacketConn.LocalAddr(), Err: err}
+		}
+	default:
+		return 0, nil, nil, &net.OpError{Op: "read", Net: c.PacketConn.LocalAddr().Network(), Source: c.PacketConn.LocalAddr(), Err: errInvalidConnType}
+	}
+	var cm *ControlMessage
+	if m.NN > 0 {
+		cm = new(ControlMessage)
+		if err := cm.Parse(m.OOB[:m.NN]); err != nil {
+			return 0, nil, nil, &net.OpError{Op: "read", Net: c.PacketConn.LocalAddr().Network(), Source: c.PacketConn.LocalAddr(), Err: err}
+		}
+		cm.Src = netAddrToIP16(m.Addr)
+	}
+	return m.N, cm, m.Addr, nil
+}
+
+func (c *payloadHandler) writeTo(b []byte, cm *ControlMessage, dst net.Addr) (int, error) {
+	m := socket.Message{
+		Buffers: [][]byte{b},
+		OOB:     cm.Marshal(),
+		Addr:    dst,
+	}
+	err := c.SendMsg(&m, 0)
+	if err != nil {
+		err = &net.OpError{Op: "write", Net: c.PacketConn.LocalAddr().Network(), Source: c.PacketConn.LocalAddr(), Err: err}
+	}
+	return m.N, err
+}
diff --git a/ipv6/readwrite_go1_8_test.go b/ipv6/readwrite_go1_8_test.go
new file mode 100644
index 0000000..c11d92a
--- /dev/null
+++ b/ipv6/readwrite_go1_8_test.go
@@ -0,0 +1,242 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build !go1.9
+
+package ipv6_test
+
+import (
+	"bytes"
+	"fmt"
+	"net"
+	"runtime"
+	"strings"
+	"sync"
+	"testing"
+
+	"golang.org/x/net/internal/iana"
+	"golang.org/x/net/internal/nettest"
+	"golang.org/x/net/ipv6"
+)
+
+func BenchmarkPacketConnReadWriteUnicast(b *testing.B) {
+	switch runtime.GOOS {
+	case "nacl", "plan9", "windows":
+		b.Skipf("not supported on %s", runtime.GOOS)
+	}
+
+	payload := []byte("HELLO-R-U-THERE")
+	iph := []byte{
+		0x69, 0x8b, 0xee, 0xf1, 0xca, 0xfe, 0xff, 0x01,
+		0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+		0x20, 0x01, 0x0d, 0xb8, 0x00, 0x02, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	}
+	greh := []byte{0x00, 0x00, 0x86, 0xdd, 0x00, 0x00, 0x00, 0x00}
+	datagram := append(greh, append(iph, payload...)...)
+	bb := make([]byte, 128)
+	cm := ipv6.ControlMessage{
+		TrafficClass: iana.DiffServAF11 | iana.CongestionExperienced,
+		HopLimit:     1,
+		Src:          net.IPv6loopback,
+	}
+	if ifi := nettest.RoutedInterface("ip6", net.FlagUp|net.FlagLoopback); ifi != nil {
+		cm.IfIndex = ifi.Index
+	}
+
+	b.Run("UDP", func(b *testing.B) {
+		c, err := nettest.NewLocalPacketListener("udp6")
+		if err != nil {
+			b.Skipf("not supported on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
+		}
+		defer c.Close()
+		p := ipv6.NewPacketConn(c)
+		dst := c.LocalAddr()
+		cf := ipv6.FlagHopLimit | ipv6.FlagInterface
+		if err := p.SetControlMessage(cf, true); err != nil {
+			b.Fatal(err)
+		}
+		b.Run("Net", func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := c.WriteTo(payload, dst); err != nil {
+					b.Fatal(err)
+				}
+				if _, _, err := c.ReadFrom(bb); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+		b.Run("ToFrom", func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := p.WriteTo(payload, &cm, dst); err != nil {
+					b.Fatal(err)
+				}
+				if _, _, _, err := p.ReadFrom(bb); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+	})
+	b.Run("IP", func(b *testing.B) {
+		switch runtime.GOOS {
+		case "netbsd":
+			b.Skip("need to configure gre on netbsd")
+		case "openbsd":
+			b.Skip("net.inet.gre.allow=0 by default on openbsd")
+		}
+
+		c, err := net.ListenPacket(fmt.Sprintf("ip6:%d", iana.ProtocolGRE), "::1")
+		if err != nil {
+			b.Skipf("not supported on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
+		}
+		defer c.Close()
+		p := ipv6.NewPacketConn(c)
+		dst := c.LocalAddr()
+		cf := ipv6.FlagTrafficClass | ipv6.FlagHopLimit | ipv6.FlagSrc | ipv6.FlagDst | ipv6.FlagInterface | ipv6.FlagPathMTU
+		if err := p.SetControlMessage(cf, true); err != nil {
+			b.Fatal(err)
+		}
+		b.Run("Net", func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := c.WriteTo(datagram, dst); err != nil {
+					b.Fatal(err)
+				}
+				if _, _, err := c.ReadFrom(bb); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+		b.Run("ToFrom", func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := p.WriteTo(datagram, &cm, dst); err != nil {
+					b.Fatal(err)
+				}
+				if _, _, _, err := p.ReadFrom(bb); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+	})
+}
+
+func TestPacketConnConcurrentReadWriteUnicast(t *testing.T) {
+	switch runtime.GOOS {
+	case "nacl", "plan9", "windows":
+		t.Skipf("not supported on %s", runtime.GOOS)
+	}
+
+	payload := []byte("HELLO-R-U-THERE")
+	iph := []byte{
+		0x69, 0x8b, 0xee, 0xf1, 0xca, 0xfe, 0xff, 0x01,
+		0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+		0x20, 0x01, 0x0d, 0xb8, 0x00, 0x02, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	}
+	greh := []byte{0x00, 0x00, 0x86, 0xdd, 0x00, 0x00, 0x00, 0x00}
+	datagram := append(greh, append(iph, payload...)...)
+
+	t.Run("UDP", func(t *testing.T) {
+		c, err := nettest.NewLocalPacketListener("udp6")
+		if err != nil {
+			t.Skipf("not supported on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
+		}
+		defer c.Close()
+		p := ipv6.NewPacketConn(c)
+		t.Run("ToFrom", func(t *testing.T) {
+			testPacketConnConcurrentReadWriteUnicast(t, p, payload, c.LocalAddr())
+		})
+	})
+	t.Run("IP", func(t *testing.T) {
+		switch runtime.GOOS {
+		case "netbsd":
+			t.Skip("need to configure gre on netbsd")
+		case "openbsd":
+			t.Skip("net.inet.gre.allow=0 by default on openbsd")
+		}
+
+		c, err := net.ListenPacket(fmt.Sprintf("ip6:%d", iana.ProtocolGRE), "::1")
+		if err != nil {
+			t.Skipf("not supported on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
+		}
+		defer c.Close()
+		p := ipv6.NewPacketConn(c)
+		t.Run("ToFrom", func(t *testing.T) {
+			testPacketConnConcurrentReadWriteUnicast(t, p, datagram, c.LocalAddr())
+		})
+	})
+}
+
+func testPacketConnConcurrentReadWriteUnicast(t *testing.T, p *ipv6.PacketConn, data []byte, dst net.Addr) {
+	ifi := nettest.RoutedInterface("ip6", net.FlagUp|net.FlagLoopback)
+	cf := ipv6.FlagTrafficClass | ipv6.FlagHopLimit | ipv6.FlagSrc | ipv6.FlagDst | ipv6.FlagInterface | ipv6.FlagPathMTU
+
+	if err := p.SetControlMessage(cf, true); err != nil { // probe before test
+		if nettest.ProtocolNotSupported(err) {
+			t.Skipf("not supported on %s", runtime.GOOS)
+		}
+		t.Fatal(err)
+	}
+
+	var wg sync.WaitGroup
+	reader := func() {
+		defer wg.Done()
+		b := make([]byte, 128)
+		n, cm, _, err := p.ReadFrom(b)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		if !bytes.Equal(b[:n], data) {
+			t.Errorf("got %#v; want %#v", b[:n], data)
+			return
+		}
+		s := cm.String()
+		if strings.Contains(s, ",") {
+			t.Errorf("should be space-separated values: %s", s)
+			return
+		}
+	}
+	writer := func(toggle bool) {
+		defer wg.Done()
+		cm := ipv6.ControlMessage{
+			TrafficClass: iana.DiffServAF11 | iana.CongestionExperienced,
+			HopLimit:     1,
+			Src:          net.IPv6loopback,
+		}
+		if ifi != nil {
+			cm.IfIndex = ifi.Index
+		}
+		if err := p.SetControlMessage(cf, toggle); err != nil {
+			t.Error(err)
+			return
+		}
+		n, err := p.WriteTo(data, &cm, dst)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		if n != len(data) {
+			t.Errorf("got %d; want %d", n, len(data))
+			return
+		}
+	}
+
+	const N = 10
+	wg.Add(N)
+	for i := 0; i < N; i++ {
+		go reader()
+	}
+	wg.Add(2 * N)
+	for i := 0; i < 2*N; i++ {
+		go writer(i%2 != 0)
+
+	}
+	wg.Add(N)
+	for i := 0; i < N; i++ {
+		go reader()
+	}
+	wg.Wait()
+}
diff --git a/ipv6/readwrite_go1_9_test.go b/ipv6/readwrite_go1_9_test.go
new file mode 100644
index 0000000..e2fd733
--- /dev/null
+++ b/ipv6/readwrite_go1_9_test.go
@@ -0,0 +1,373 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build go1.9
+
+package ipv6_test
+
+import (
+	"bytes"
+	"fmt"
+	"net"
+	"runtime"
+	"strings"
+	"sync"
+	"testing"
+
+	"golang.org/x/net/internal/iana"
+	"golang.org/x/net/internal/nettest"
+	"golang.org/x/net/ipv6"
+)
+
+func BenchmarkPacketConnReadWriteUnicast(b *testing.B) {
+	switch runtime.GOOS {
+	case "nacl", "plan9", "windows":
+		b.Skipf("not supported on %s", runtime.GOOS)
+	}
+
+	payload := []byte("HELLO-R-U-THERE")
+	iph := []byte{
+		0x69, 0x8b, 0xee, 0xf1, 0xca, 0xfe, 0xff, 0x01,
+		0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+		0x20, 0x01, 0x0d, 0xb8, 0x00, 0x02, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	}
+	greh := []byte{0x00, 0x00, 0x86, 0xdd, 0x00, 0x00, 0x00, 0x00}
+	datagram := append(greh, append(iph, payload...)...)
+	bb := make([]byte, 128)
+	cm := ipv6.ControlMessage{
+		TrafficClass: iana.DiffServAF11 | iana.CongestionExperienced,
+		HopLimit:     1,
+		Src:          net.IPv6loopback,
+	}
+	if ifi := nettest.RoutedInterface("ip6", net.FlagUp|net.FlagLoopback); ifi != nil {
+		cm.IfIndex = ifi.Index
+	}
+
+	b.Run("UDP", func(b *testing.B) {
+		c, err := nettest.NewLocalPacketListener("udp6")
+		if err != nil {
+			b.Skipf("not supported on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
+		}
+		defer c.Close()
+		p := ipv6.NewPacketConn(c)
+		dst := c.LocalAddr()
+		cf := ipv6.FlagHopLimit | ipv6.FlagInterface
+		if err := p.SetControlMessage(cf, true); err != nil {
+			b.Fatal(err)
+		}
+		wms := []ipv6.Message{
+			{
+				Buffers: [][]byte{payload},
+				Addr:    dst,
+				OOB:     cm.Marshal(),
+			},
+		}
+		rms := []ipv6.Message{
+			{
+				Buffers: [][]byte{bb},
+				OOB:     ipv6.NewControlMessage(cf),
+			},
+		}
+		b.Run("Net", func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := c.WriteTo(payload, dst); err != nil {
+					b.Fatal(err)
+				}
+				if _, _, err := c.ReadFrom(bb); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+		b.Run("ToFrom", func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := p.WriteTo(payload, &cm, dst); err != nil {
+					b.Fatal(err)
+				}
+				if _, _, _, err := p.ReadFrom(bb); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+		b.Run("Batch", func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := p.WriteBatch(wms, 0); err != nil {
+					b.Fatal(err)
+				}
+				if _, err := p.ReadBatch(rms, 0); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+	})
+	b.Run("IP", func(b *testing.B) {
+		switch runtime.GOOS {
+		case "netbsd":
+			b.Skip("need to configure gre on netbsd")
+		case "openbsd":
+			b.Skip("net.inet.gre.allow=0 by default on openbsd")
+		}
+
+		c, err := net.ListenPacket(fmt.Sprintf("ip6:%d", iana.ProtocolGRE), "::1")
+		if err != nil {
+			b.Skipf("not supported on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
+		}
+		defer c.Close()
+		p := ipv6.NewPacketConn(c)
+		dst := c.LocalAddr()
+		cf := ipv6.FlagTrafficClass | ipv6.FlagHopLimit | ipv6.FlagSrc | ipv6.FlagDst | ipv6.FlagInterface | ipv6.FlagPathMTU
+		if err := p.SetControlMessage(cf, true); err != nil {
+			b.Fatal(err)
+		}
+		wms := []ipv6.Message{
+			{
+				Buffers: [][]byte{datagram},
+				Addr:    dst,
+				OOB:     cm.Marshal(),
+			},
+		}
+		rms := []ipv6.Message{
+			{
+				Buffers: [][]byte{bb},
+				OOB:     ipv6.NewControlMessage(cf),
+			},
+		}
+		b.Run("Net", func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := c.WriteTo(datagram, dst); err != nil {
+					b.Fatal(err)
+				}
+				if _, _, err := c.ReadFrom(bb); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+		b.Run("ToFrom", func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := p.WriteTo(datagram, &cm, dst); err != nil {
+					b.Fatal(err)
+				}
+				if _, _, _, err := p.ReadFrom(bb); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+		b.Run("Batch", func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				if _, err := p.WriteBatch(wms, 0); err != nil {
+					b.Fatal(err)
+				}
+				if _, err := p.ReadBatch(rms, 0); err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+	})
+}
+
+func TestPacketConnConcurrentReadWriteUnicast(t *testing.T) {
+	switch runtime.GOOS {
+	case "nacl", "plan9", "windows":
+		t.Skipf("not supported on %s", runtime.GOOS)
+	}
+
+	payload := []byte("HELLO-R-U-THERE")
+	iph := []byte{
+		0x69, 0x8b, 0xee, 0xf1, 0xca, 0xfe, 0xff, 0x01,
+		0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+		0x20, 0x01, 0x0d, 0xb8, 0x00, 0x02, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	}
+	greh := []byte{0x00, 0x00, 0x86, 0xdd, 0x00, 0x00, 0x00, 0x00}
+	datagram := append(greh, append(iph, payload...)...)
+
+	t.Run("UDP", func(t *testing.T) {
+		c, err := nettest.NewLocalPacketListener("udp6")
+		if err != nil {
+			t.Skipf("not supported on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
+		}
+		defer c.Close()
+		p := ipv6.NewPacketConn(c)
+		t.Run("ToFrom", func(t *testing.T) {
+			testPacketConnConcurrentReadWriteUnicast(t, p, payload, c.LocalAddr(), false)
+		})
+		t.Run("Batch", func(t *testing.T) {
+			testPacketConnConcurrentReadWriteUnicast(t, p, payload, c.LocalAddr(), true)
+		})
+	})
+	t.Run("IP", func(t *testing.T) {
+		switch runtime.GOOS {
+		case "netbsd":
+			t.Skip("need to configure gre on netbsd")
+		case "openbsd":
+			t.Skip("net.inet.gre.allow=0 by default on openbsd")
+		}
+
+		c, err := net.ListenPacket(fmt.Sprintf("ip6:%d", iana.ProtocolGRE), "::1")
+		if err != nil {
+			t.Skipf("not supported on %s/%s: %v", runtime.GOOS, runtime.GOARCH, err)
+		}
+		defer c.Close()
+		p := ipv6.NewPacketConn(c)
+		t.Run("ToFrom", func(t *testing.T) {
+			testPacketConnConcurrentReadWriteUnicast(t, p, datagram, c.LocalAddr(), false)
+		})
+		t.Run("Batch", func(t *testing.T) {
+			testPacketConnConcurrentReadWriteUnicast(t, p, datagram, c.LocalAddr(), true)
+		})
+	})
+}
+
+func testPacketConnConcurrentReadWriteUnicast(t *testing.T, p *ipv6.PacketConn, data []byte, dst net.Addr, batch bool) {
+	ifi := nettest.RoutedInterface("ip6", net.FlagUp|net.FlagLoopback)
+	cf := ipv6.FlagTrafficClass | ipv6.FlagHopLimit | ipv6.FlagSrc | ipv6.FlagDst | ipv6.FlagInterface | ipv6.FlagPathMTU
+
+	if err := p.SetControlMessage(cf, true); err != nil { // probe before test
+		if nettest.ProtocolNotSupported(err) {
+			t.Skipf("not supported on %s", runtime.GOOS)
+		}
+		t.Fatal(err)
+	}
+
+	var wg sync.WaitGroup
+	reader := func() {
+		defer wg.Done()
+		b := make([]byte, 128)
+		n, cm, _, err := p.ReadFrom(b)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		if !bytes.Equal(b[:n], data) {
+			t.Errorf("got %#v; want %#v", b[:n], data)
+			return
+		}
+		s := cm.String()
+		if strings.Contains(s, ",") {
+			t.Errorf("should be space-separated values: %s", s)
+			return
+		}
+	}
+	batchReader := func() {
+		defer wg.Done()
+		ms := []ipv6.Message{
+			{
+				Buffers: [][]byte{make([]byte, 128)},
+				OOB:     ipv6.NewControlMessage(cf),
+			},
+		}
+		n, err := p.ReadBatch(ms, 0)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		if n != len(ms) {
+			t.Errorf("got %d; want %d", n, len(ms))
+			return
+		}
+		var cm ipv6.ControlMessage
+		if err := cm.Parse(ms[0].OOB[:ms[0].NN]); err != nil {
+			t.Error(err)
+			return
+		}
+		b := ms[0].Buffers[0][:ms[0].N]
+		if !bytes.Equal(b, data) {
+			t.Errorf("got %#v; want %#v", b, data)
+			return
+		}
+		s := cm.String()
+		if strings.Contains(s, ",") {
+			t.Errorf("should be space-separated values: %s", s)
+			return
+		}
+	}
+	writer := func(toggle bool) {
+		defer wg.Done()
+		cm := ipv6.ControlMessage{
+			TrafficClass: iana.DiffServAF11 | iana.CongestionExperienced,
+			HopLimit:     1,
+			Src:          net.IPv6loopback,
+		}
+		if ifi != nil {
+			cm.IfIndex = ifi.Index
+		}
+		if err := p.SetControlMessage(cf, toggle); err != nil {
+			t.Error(err)
+			return
+		}
+		n, err := p.WriteTo(data, &cm, dst)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		if n != len(data) {
+			t.Errorf("got %d; want %d", n, len(data))
+			return
+		}
+	}
+	batchWriter := func(toggle bool) {
+		defer wg.Done()
+		cm := ipv6.ControlMessage{
+			TrafficClass: iana.DiffServAF11 | iana.CongestionExperienced,
+			HopLimit:     1,
+			Src:          net.IPv6loopback,
+		}
+		if ifi != nil {
+			cm.IfIndex = ifi.Index
+		}
+		if err := p.SetControlMessage(cf, toggle); err != nil {
+			t.Error(err)
+			return
+		}
+		ms := []ipv6.Message{
+			{
+				Buffers: [][]byte{data},
+				OOB:     cm.Marshal(),
+				Addr:    dst,
+			},
+		}
+		n, err := p.WriteBatch(ms, 0)
+		if err != nil {
+			t.Error(err)
+			return
+		}
+		if n != len(ms) {
+			t.Errorf("got %d; want %d", n, len(ms))
+			return
+		}
+		if ms[0].N != len(data) {
+			t.Errorf("got %d; want %d", ms[0].N, len(data))
+			return
+		}
+	}
+
+	const N = 10
+	wg.Add(N)
+	for i := 0; i < N; i++ {
+		if batch {
+			go batchReader()
+		} else {
+			go reader()
+		}
+	}
+	wg.Add(2 * N)
+	for i := 0; i < 2*N; i++ {
+		if batch {
+			go batchWriter(i%2 != 0)
+		} else {
+			go writer(i%2 != 0)
+		}
+	}
+	wg.Add(N)
+	for i := 0; i < N; i++ {
+		if batch {
+			go batchReader()
+		} else {
+			go reader()
+		}
+	}
+	wg.Wait()
+}