Merge remote-tracking branch 'upstream/master' into HEAD

Change-Id: I4741f31b22bc5a1dedcd61adc5790dfd9c9ef779
diff --git a/tcpip/checker/checker.go b/tcpip/checker/checker.go
index bee4603..e3f494a 100644
--- a/tcpip/checker/checker.go
+++ b/tcpip/checker/checker.go
@@ -686,3 +686,64 @@
 		}
 	}
 }
+
+// NDP creates a checker that checks that the packet contains a valid NDP
+// message for type of ty, with potentially additional checks specified by
+// checkers.
+//
+// checkers may assume that a valid ICMPv6 is passed to it containing a valid
+// NDP message as far as the size of the message (minSize) is concerned. The
+// values within the message are up to checkers to validate.
+func NDP(msgType header.ICMPv6Type, minSize int, checkers ...TransportChecker) NetworkChecker {
+	return func(t *testing.T, h []header.Network) {
+		t.Helper()
+
+		// Check normal ICMPv6 first.
+		ICMPv6(
+			ICMPv6Type(msgType),
+			ICMPv6Code(0))(t, h)
+
+		last := h[len(h)-1]
+
+		icmp := header.ICMPv6(last.Payload())
+		if got := len(icmp.NDPPayload()); got < minSize {
+			t.Fatalf("ICMPv6 NDP (type = %d) payload size of %d is less than the minimum size of %d", msgType, got, minSize)
+		}
+
+		for _, f := range checkers {
+			f(t, icmp)
+		}
+		if t.Failed() {
+			t.FailNow()
+		}
+	}
+}
+
+// NDPNS creates a checker that checks that the packet contains a valid NDP
+// Neighbor Solicitation message (as per the raw wire format), with potentially
+// additional checks specified by checkers.
+//
+// checkers may assume that a valid ICMPv6 is passed to it containing a valid
+// NDPNS message as far as the size of the messages concerned. The values within
+// the message are up to checkers to validate.
+func NDPNS(checkers ...TransportChecker) NetworkChecker {
+	return NDP(header.ICMPv6NeighborSolicit, header.NDPNSMinimumSize, checkers...)
+}
+
+// NDPNSTargetAddress creates a checker that checks the Target Address field of
+// a header.NDPNeighborSolicit.
+//
+// The returned TransportChecker assumes that a valid ICMPv6 is passed to it
+// containing a valid NDPNS message as far as the size is concerned.
+func NDPNSTargetAddress(want tcpip.Address) TransportChecker {
+	return func(t *testing.T, h header.Transport) {
+		t.Helper()
+
+		icmp := h.(header.ICMPv6)
+		ns := header.NDPNeighborSolicit(icmp.NDPPayload())
+
+		if got := ns.TargetAddress(); got != want {
+			t.Fatalf("got %T.TargetAddress = %s, want = %s", ns, got, want)
+		}
+	}
+}
diff --git a/tcpip/header/icmpv6.go b/tcpip/header/icmpv6.go
index 6ad520f..5d517d2 100644
--- a/tcpip/header/icmpv6.go
+++ b/tcpip/header/icmpv6.go
@@ -25,6 +25,12 @@
 type ICMPv6 []byte
 
 const (
+	// ICMPv6HeaderSize is the size of the ICMPv6 header. That is, the
+	// sum of the size of the ICMPv6 Type, Code and Checksum fields, as
+	// per RFC 4443 section 2.1. After the ICMPv6 header, the ICMPv6
+	// message body begins.
+	ICMPv6HeaderSize = 4
+
 	// ICMPv6MinimumSize is the minimum size of a valid ICMP packet.
 	ICMPv6MinimumSize = 8
 
@@ -37,10 +43,16 @@
 
 	// ICMPv6NeighborSolicitMinimumSize is the minimum size of a
 	// neighbor solicitation packet.
-	ICMPv6NeighborSolicitMinimumSize = ICMPv6MinimumSize + 16
+	ICMPv6NeighborSolicitMinimumSize = ICMPv6HeaderSize + NDPNSMinimumSize
 
-	// ICMPv6NeighborAdvertSize is size of a neighbor advertisement.
-	ICMPv6NeighborAdvertSize = 32
+	// ICMPv6NeighborAdvertMinimumSize is the minimum size of a
+	// neighbor advertisement packet.
+	ICMPv6NeighborAdvertMinimumSize = ICMPv6HeaderSize + NDPNAMinimumSize
+
+	// ICMPv6NeighborAdvertSize is size of a neighbor advertisement
+	// including the NDP Target Link Layer option for an Ethernet
+	// address.
+	ICMPv6NeighborAdvertSize = ICMPv6HeaderSize + NDPNAMinimumSize + ndpTargetEthernetLinkLayerAddressSize
 
 	// ICMPv6EchoMinimumSize is the minimum size of a valid ICMP echo packet.
 	ICMPv6EchoMinimumSize = 8
@@ -68,6 +80,13 @@
 	// icmpv6SequenceOffset is the offset of the sequence field
 	// in a ICMPv6 Echo Request/Reply message.
 	icmpv6SequenceOffset = 6
+
+	// NDPHopLimit is the expected IP hop limit value of 255 for received
+	// NDP packets, as per RFC 4861 sections 4.1 - 4.5, 6.1.1, 6.1.2, 7.1.1,
+	// 7.1.2 and 8.1. If the hop limit value is not 255, nodes MUST silently
+	// drop the NDP packet. All outgoing NDP packets must use this value for
+	// its IP hop limit field.
+	NDPHopLimit = 255
 )
 
 // ICMPv6Type is the ICMP type field described in RFC 4443 and friends.
@@ -166,6 +185,13 @@
 	binary.BigEndian.PutUint16(b[icmpv6SequenceOffset:], sequence)
 }
 
+// NDPPayload returns the NDP payload buffer. That is, it returns the ICMPv6
+// packet's message body as defined by RFC 4443 section 2.1; the portion of the
+// ICMPv6 buffer after the first ICMPv6HeaderSize bytes.
+func (b ICMPv6) NDPPayload() []byte {
+	return b[ICMPv6HeaderSize:]
+}
+
 // Payload implements Transport.Payload.
 func (b ICMPv6) Payload() []byte {
 	return b[ICMPv6PayloadOffset:]
diff --git a/tcpip/header/ipv6.go b/tcpip/header/ipv6.go
index ba8e20a..a496402 100644
--- a/tcpip/header/ipv6.go
+++ b/tcpip/header/ipv6.go
@@ -87,7 +87,8 @@
 	// section 5.
 	IPv6MinimumMTU = 1280
 
-	// IPv6Any is the non-routable IPv6 "any" meta address.
+	// IPv6Any is the non-routable IPv6 "any" meta address. It is also
+	// known as the unspecified address.
 	IPv6Any tcpip.Address = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
 )
 
diff --git a/tcpip/header/ndp_neighbor_advert.go b/tcpip/header/ndp_neighbor_advert.go
new file mode 100644
index 0000000..3abc5e1
--- /dev/null
+++ b/tcpip/header/ndp_neighbor_advert.go
@@ -0,0 +1,108 @@
+// Copyright 2019 The gVisor Authors.
+//
+// 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 header
+
+import "github.com/google/netstack/tcpip"
+
+// NDPNeighborAdvert is an NDP Neighbor Advertisement message. It will
+// only contain the body of an ICMPv6 packet.
+type NDPNeighborAdvert []byte
+
+const (
+	// NDPNAMinimumSize is the minimum size of a valid NDP Neighbor
+	// Advertisement message (body of an ICMPv6 packet).
+	NDPNAMinimumSize = 20
+
+	// ndpNATargetAddressOffset is the start of the Target Address
+	// field within an NDPNeighborAdvert.
+	ndpNATargetAddressOffset = 4
+
+	// ndpNAOptionsOffset is the start of the NDP options in an
+	// NDPNeighborAdvert.
+	ndpNAOptionsOffset = ndpNATargetAddressOffset + IPv6AddressSize
+
+	// ndpNAFlagsOffset is the offset of the flags within an
+	// NDPNeighborAdvert
+	ndpNAFlagsOffset = 0
+
+	// ndpNARouterFlagMask is the mask of the Router Flag field in
+	// the flags byte within in an NDPNeighborAdvert.
+	ndpNARouterFlagMask = (1 << 7)
+
+	// ndpNASolicitedFlagMask is the mask of the Solicited Flag field in
+	// the flags byte within in an NDPNeighborAdvert.
+	ndpNASolicitedFlagMask = (1 << 6)
+
+	// ndpNAOverrideFlagMask is the mask of the Override Flag field in
+	// the flags byte within in an NDPNeighborAdvert.
+	ndpNAOverrideFlagMask = (1 << 5)
+)
+
+// TargetAddress returns the value within the Target Address field.
+func (b NDPNeighborAdvert) TargetAddress() tcpip.Address {
+	return tcpip.Address(b[ndpNATargetAddressOffset:][:IPv6AddressSize])
+}
+
+// SetTargetAddress sets the value within the Target Address field.
+func (b NDPNeighborAdvert) SetTargetAddress(addr tcpip.Address) {
+	copy(b[ndpNATargetAddressOffset:][:IPv6AddressSize], addr)
+}
+
+// RouterFlag returns the value of the Router Flag field.
+func (b NDPNeighborAdvert) RouterFlag() bool {
+	return b[ndpNAFlagsOffset]&ndpNARouterFlagMask != 0
+}
+
+// SetRouterFlag sets the value in the Router Flag field.
+func (b NDPNeighborAdvert) SetRouterFlag(f bool) {
+	if f {
+		b[ndpNAFlagsOffset] |= ndpNARouterFlagMask
+	} else {
+		b[ndpNAFlagsOffset] &^= ndpNARouterFlagMask
+	}
+}
+
+// SolicitedFlag returns the value of the Solicited Flag field.
+func (b NDPNeighborAdvert) SolicitedFlag() bool {
+	return b[ndpNAFlagsOffset]&ndpNASolicitedFlagMask != 0
+}
+
+// SetSolicitedFlag sets the value in the Solicited Flag field.
+func (b NDPNeighborAdvert) SetSolicitedFlag(f bool) {
+	if f {
+		b[ndpNAFlagsOffset] |= ndpNASolicitedFlagMask
+	} else {
+		b[ndpNAFlagsOffset] &^= ndpNASolicitedFlagMask
+	}
+}
+
+// OverrideFlag returns the value of the Override Flag field.
+func (b NDPNeighborAdvert) OverrideFlag() bool {
+	return b[ndpNAFlagsOffset]&ndpNAOverrideFlagMask != 0
+}
+
+// SetOverrideFlag sets the value in the Override Flag field.
+func (b NDPNeighborAdvert) SetOverrideFlag(f bool) {
+	if f {
+		b[ndpNAFlagsOffset] |= ndpNAOverrideFlagMask
+	} else {
+		b[ndpNAFlagsOffset] &^= ndpNAOverrideFlagMask
+	}
+}
+
+// Options returns an NDPOptions of the the options body.
+func (b NDPNeighborAdvert) Options() NDPOptions {
+	return NDPOptions(b[ndpNAOptionsOffset:])
+}
diff --git a/tcpip/header/ndp_neighbor_solicit.go b/tcpip/header/ndp_neighbor_solicit.go
new file mode 100644
index 0000000..2637759
--- /dev/null
+++ b/tcpip/header/ndp_neighbor_solicit.go
@@ -0,0 +1,50 @@
+// Copyright 2019 The gVisor Authors.
+//
+// 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 header
+
+import "github.com/google/netstack/tcpip"
+
+// NDPNeighborSolicit is an NDP Neighbor Solicitation message. It will only
+// contain the body of an ICMPv6 packet.
+type NDPNeighborSolicit []byte
+
+const (
+	// NDPNSMinimumSize is the minimum size of a valid NDP Neighbor
+	// Solicitation message (body of an ICMPv6 packet).
+	NDPNSMinimumSize = 20
+
+	// ndpNSTargetAddessOffset is the start of the Target Address
+	// field within an NDPNeighborSolicit.
+	ndpNSTargetAddessOffset = 4
+
+	// ndpNSOptionsOffset is the start of the NDP options in an
+	// NDPNeighborSolicit.
+	ndpNSOptionsOffset = ndpNSTargetAddessOffset + IPv6AddressSize
+)
+
+// TargetAddress returns the value within the Target Address field.
+func (b NDPNeighborSolicit) TargetAddress() tcpip.Address {
+	return tcpip.Address(b[ndpNSTargetAddessOffset:][:IPv6AddressSize])
+}
+
+// SetTargetAddress sets the value within the Target Address field.
+func (b NDPNeighborSolicit) SetTargetAddress(addr tcpip.Address) {
+	copy(b[ndpNSTargetAddessOffset:][:IPv6AddressSize], addr)
+}
+
+// Options returns an NDPOptions of the the options body.
+func (b NDPNeighborSolicit) Options() NDPOptions {
+	return NDPOptions(b[ndpNSOptionsOffset:])
+}
diff --git a/tcpip/header/ndp_options.go b/tcpip/header/ndp_options.go
new file mode 100644
index 0000000..dd02a04
--- /dev/null
+++ b/tcpip/header/ndp_options.go
@@ -0,0 +1,172 @@
+// Copyright 2019 The gVisor Authors.
+//
+// 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 header
+
+import (
+	"github.com/google/netstack/tcpip"
+)
+
+const (
+	// NDPTargetLinkLayerAddressOptionType is the type of the Target
+	// Link-Layer Address option, as per RFC 4861 section 4.6.1.
+	NDPTargetLinkLayerAddressOptionType = 2
+
+	// ndpTargetEthernetLinkLayerAddressSize is the size of a Target
+	// Link Layer Option for an Ethernet address.
+	ndpTargetEthernetLinkLayerAddressSize = 8
+
+	// lengthByteUnits is the multiplier factor for the Length field of an
+	// NDP option. That is, the length field for NDP options is in units of
+	// 8 octets, as per RFC 4861 section 4.6.
+	lengthByteUnits = 8
+)
+
+// NDPOptions is a buffer of NDP options as defined by RFC 4861 section 4.6.
+type NDPOptions []byte
+
+// Serialize serializes the provided list of NDP options into o.
+//
+// Note, b must be of sufficient size to hold all the options in s. See
+// NDPOptionsSerializer.Length for details on the getting the total size
+// of a serialized NDPOptionsSerializer.
+//
+// Serialize may panic if b is not of sufficient size to hold all the options
+// in s.
+func (b NDPOptions) Serialize(s NDPOptionsSerializer) int {
+	done := 0
+
+	for _, o := range s {
+		l := paddedLength(o)
+
+		if l == 0 {
+			continue
+		}
+
+		b[0] = o.Type()
+
+		// We know this safe because paddedLength would have returned
+		// 0 if o had an invalid length (> 255 * lengthByteUnits).
+		b[1] = uint8(l / lengthByteUnits)
+
+		// Serialize NDP option body.
+		used := o.serializeInto(b[2:])
+
+		// Zero out remaining (padding) bytes, if any exists.
+		for i := used + 2; i < l; i++ {
+			b[i] = 0
+		}
+
+		b = b[l:]
+		done += l
+	}
+
+	return done
+}
+
+// ndpOption is the set of functions to be implemented by all NDP option types.
+type ndpOption interface {
+	// Type returns the type of this ndpOption.
+	Type() uint8
+
+	// Length returns the length of the body of this ndpOption, in bytes.
+	Length() int
+
+	// serializeInto serializes this ndpOption into the provided byte
+	// buffer.
+	//
+	// Note, the caller MUST provide a byte buffer with size of at least
+	// Length. Implementers of this function may assume that the byte buffer
+	// is of sufficient size. serializeInto MAY panic if the provided byte
+	// buffer is not of sufficient size.
+	//
+	// serializeInto will return the number of bytes that was used to
+	// serialize this ndpOption. Implementers must only use the number of
+	// bytes required to serialize this ndpOption. Callers MAY provide a
+	// larger buffer than required to serialize into.
+	serializeInto([]byte) int
+}
+
+// paddedLength returns the length of o, in bytes, with any padding bytes, if
+// required.
+func paddedLength(o ndpOption) int {
+	l := o.Length()
+
+	if l == 0 {
+		return 0
+	}
+
+	// Length excludes the 2 Type and Length bytes.
+	l += 2
+
+	// Add extra bytes if needed to make sure the option is
+	// lengthByteUnits-byte aligned. We do this by adding lengthByteUnits-1
+	// to l and then stripping off the last few LSBits from l. This will
+	// make sure that l is rounded up to the nearest unit of
+	// lengthByteUnits. This works since lengthByteUnits is a power of 2
+	// (= 8).
+	mask := lengthByteUnits - 1
+	l += mask
+	l &^= mask
+
+	if l/lengthByteUnits > 255 {
+		// Should never happen because an option can only have a max
+		// value of 255 for its Length field, so just return 0 so this
+		// option does not get serialized.
+		//
+		// Returning 0 here will make sure that this option does not get
+		// serialized when NDPOptions.Serialize is called with the
+		// NDPOptionsSerializer that holds this option, effectively
+		// skipping this option during serialization. Also note that
+		// a value of zero for the Length field in an NDP option is
+		// invalid so this is another sign to the caller that this NDP
+		// option is malformed, as per RFC 4861 section 4.6.
+		return 0
+	}
+
+	return l
+}
+
+// NDPOptionsSerializer is a serializer for NDP options.
+type NDPOptionsSerializer []ndpOption
+
+// Length returns the total number of bytes required to serialize.
+func (b NDPOptionsSerializer) Length() int {
+	l := 0
+
+	for _, o := range b {
+		l += paddedLength(o)
+	}
+
+	return l
+}
+
+// NDPTargetLinkLayerAddressOption is the NDP Target Link Layer Option
+// as defined by RFC 4861 section 4.6.1.
+type NDPTargetLinkLayerAddressOption tcpip.LinkAddress
+
+// Type implements ndpOption.Type.
+func (o NDPTargetLinkLayerAddressOption) Type() uint8 {
+	return NDPTargetLinkLayerAddressOptionType
+}
+
+// Length implements ndpOption.Length.
+func (o NDPTargetLinkLayerAddressOption) Length() int {
+	return len(o)
+}
+
+// serializeInto implements ndpOption.serializeInto.
+func (o NDPTargetLinkLayerAddressOption) serializeInto(b []byte) int {
+	return copy(b, o)
+}
diff --git a/tcpip/header/ndp_test.go b/tcpip/header/ndp_test.go
new file mode 100644
index 0000000..7d66e70
--- /dev/null
+++ b/tcpip/header/ndp_test.go
@@ -0,0 +1,164 @@
+// Copyright 2019 The gVisor Authors.
+//
+// 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 header
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/google/netstack/tcpip"
+)
+
+// TestNDPNeighborSolicit tests the functions of NDPNeighborSolicit.
+func TestNDPNeighborSolicit(t *testing.T) {
+	b := []byte{
+		0, 0, 0, 0,
+		1, 2, 3, 4,
+		5, 6, 7, 8,
+		9, 10, 11, 12,
+		13, 14, 15, 16,
+	}
+
+	// Test getting the Target Address.
+	ns := NDPNeighborSolicit(b)
+	addr := tcpip.Address("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10")
+	if got := ns.TargetAddress(); got != addr {
+		t.Fatalf("got ns.TargetAddress = %s, want %s", got, addr)
+	}
+
+	// Test updating the Target Address.
+	addr2 := tcpip.Address("\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x11")
+	ns.SetTargetAddress(addr2)
+	if got := ns.TargetAddress(); got != addr2 {
+		t.Fatalf("got ns.TargetAddress = %s, want %s", got, addr2)
+	}
+	// Make sure the address got updated in the backing buffer.
+	if got := tcpip.Address(b[ndpNSTargetAddessOffset:][:IPv6AddressSize]); got != addr2 {
+		t.Fatalf("got targetaddress buffer = %s, want %s", got, addr2)
+	}
+}
+
+// TestNDPNeighborAdvert tests the functions of NDPNeighborAdvert.
+func TestNDPNeighborAdvert(t *testing.T) {
+	b := []byte{
+		160, 0, 0, 0,
+		1, 2, 3, 4,
+		5, 6, 7, 8,
+		9, 10, 11, 12,
+		13, 14, 15, 16,
+	}
+
+	// Test getting the Target Address.
+	na := NDPNeighborAdvert(b)
+	addr := tcpip.Address("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10")
+	if got := na.TargetAddress(); got != addr {
+		t.Fatalf("got TargetAddress = %s, want %s", got, addr)
+	}
+
+	// Test getting the Router Flag.
+	if got := na.RouterFlag(); !got {
+		t.Fatalf("got RouterFlag = false, want = true")
+	}
+
+	// Test getting the Solicited Flag.
+	if got := na.SolicitedFlag(); got {
+		t.Fatalf("got SolicitedFlag = true, want = false")
+	}
+
+	// Test getting the Override Flag.
+	if got := na.OverrideFlag(); !got {
+		t.Fatalf("got OverrideFlag = false, want = true")
+	}
+
+	// Test updating the Target Address.
+	addr2 := tcpip.Address("\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x11")
+	na.SetTargetAddress(addr2)
+	if got := na.TargetAddress(); got != addr2 {
+		t.Fatalf("got TargetAddress = %s, want %s", got, addr2)
+	}
+	// Make sure the address got updated in the backing buffer.
+	if got := tcpip.Address(b[ndpNATargetAddressOffset:][:IPv6AddressSize]); got != addr2 {
+		t.Fatalf("got targetaddress buffer = %s, want %s", got, addr2)
+	}
+
+	// Test updating the Router Flag.
+	na.SetRouterFlag(false)
+	if got := na.RouterFlag(); got {
+		t.Fatalf("got RouterFlag = true, want = false")
+	}
+
+	// Test updating the Solicited Flag.
+	na.SetSolicitedFlag(true)
+	if got := na.SolicitedFlag(); !got {
+		t.Fatalf("got SolicitedFlag = false, want = true")
+	}
+
+	// Test updating the Override Flag.
+	na.SetOverrideFlag(false)
+	if got := na.OverrideFlag(); got {
+		t.Fatalf("got OverrideFlag = true, want = false")
+	}
+
+	// Make sure flags got updated in the backing buffer.
+	if got := b[ndpNAFlagsOffset]; got != 64 {
+		t.Fatalf("got flags byte = %d, want = 64")
+	}
+}
+
+// TestNDPTargetLinkLayerAddressOptionSerialize tests serializing a
+// NDPTargetLinkLayerAddressOption.
+func TestNDPTargetLinkLayerAddressOptionSerialize(t *testing.T) {
+	tests := []struct {
+		name        string
+		buf         []byte
+		expectedBuf []byte
+		addr        tcpip.LinkAddress
+	}{
+		{
+			"Ethernet",
+			make([]byte, 8),
+			[]byte{2, 1, 1, 2, 3, 4, 5, 6},
+			"\x01\x02\x03\x04\x05\x06",
+		},
+		{
+			"Padding",
+			[]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
+			[]byte{2, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0},
+			"\x01\x02\x03\x04\x05\x06\x07\x08",
+		},
+		{
+			"Empty",
+			[]byte{},
+			[]byte{},
+			"",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			opts := NDPOptions(test.buf)
+			serializer := NDPOptionsSerializer{
+				NDPTargetLinkLayerAddressOption(test.addr),
+			}
+			if got, want := int(serializer.Length()), len(test.expectedBuf); got != want {
+				t.Fatalf("got Length = %d, want = %d", got, want)
+			}
+			opts.Serialize(serializer)
+			if !bytes.Equal(test.buf, test.expectedBuf) {
+				t.Fatalf("got b = %d, want = %d", test.buf, test.expectedBuf)
+			}
+		})
+	}
+}
diff --git a/tcpip/network/fragmentation/fragmentation.go b/tcpip/network/fragmentation/fragmentation.go
index 83c4ec0..e6deedf 100644
--- a/tcpip/network/fragmentation/fragmentation.go
+++ b/tcpip/network/fragmentation/fragmentation.go
@@ -17,6 +17,7 @@
 package fragmentation
 
 import (
+	"fmt"
 	"log"
 	"sync"
 	"time"
@@ -82,7 +83,7 @@
 
 // Process processes an incoming fragment belonging to an ID
 // and returns a complete packet when all the packets belonging to that ID have been received.
-func (f *Fragmentation) Process(id uint32, first, last uint16, more bool, vv buffer.VectorisedView) (buffer.VectorisedView, bool) {
+func (f *Fragmentation) Process(id uint32, first, last uint16, more bool, vv buffer.VectorisedView) (buffer.VectorisedView, bool, error) {
 	f.mu.Lock()
 	r, ok := f.reassemblers[id]
 	if ok && r.tooOld(f.timeout) {
@@ -97,8 +98,15 @@
 	}
 	f.mu.Unlock()
 
-	res, done, consumed := r.process(first, last, more, vv)
-
+	res, done, consumed, err := r.process(first, last, more, vv)
+	if err != nil {
+		// We probably got an invalid sequence of fragments. Just
+		// discard the reassembler and move on.
+		f.mu.Lock()
+		f.release(r)
+		f.mu.Unlock()
+		return buffer.VectorisedView{}, false, fmt.Errorf("fragmentation processing error: %v", err)
+	}
 	f.mu.Lock()
 	f.size += consumed
 	if done {
@@ -114,7 +122,7 @@
 		}
 	}
 	f.mu.Unlock()
-	return res, done
+	return res, done, nil
 }
 
 func (f *Fragmentation) release(r *reassembler) {
diff --git a/tcpip/network/fragmentation/fragmentation_test.go b/tcpip/network/fragmentation/fragmentation_test.go
index f3d2ba0..b6488c9 100644
--- a/tcpip/network/fragmentation/fragmentation_test.go
+++ b/tcpip/network/fragmentation/fragmentation_test.go
@@ -83,7 +83,10 @@
 		t.Run(c.comment, func(t *testing.T) {
 			f := NewFragmentation(1024, 512, DefaultReassembleTimeout)
 			for i, in := range c.in {
-				vv, done := f.Process(in.id, in.first, in.last, in.more, in.vv)
+				vv, done, err := f.Process(in.id, in.first, in.last, in.more, in.vv)
+				if err != nil {
+					t.Fatalf("f.Process(%+v, %+d, %+d, %t, %+v) failed: %v", in.id, in.first, in.last, in.more, in.vv, err)
+				}
 				if !reflect.DeepEqual(vv, c.out[i].vv) {
 					t.Errorf("got Process(%d) = %+v, want = %+v", i, vv, c.out[i].vv)
 				}
@@ -114,7 +117,10 @@
 	time.Sleep(2 * timeout)
 	// Send another fragment that completes a packet.
 	// However, no packet should be reassembled because the fragment arrived after the timeout.
-	_, done := f.Process(0, 1, 1, false, vv(1, "1"))
+	_, done, err := f.Process(0, 1, 1, false, vv(1, "1"))
+	if err != nil {
+		t.Fatalf("f.Process(0, 1, 1, false, vv(1, \"1\")) failed: %v", err)
+	}
 	if done {
 		t.Errorf("Fragmentation does not respect the reassembling timeout.")
 	}
diff --git a/tcpip/network/fragmentation/reassembler.go b/tcpip/network/fragmentation/reassembler.go
index 19b61de..bae60ff 100644
--- a/tcpip/network/fragmentation/reassembler.go
+++ b/tcpip/network/fragmentation/reassembler.go
@@ -78,7 +78,7 @@
 	return used
 }
 
-func (r *reassembler) process(first, last uint16, more bool, vv buffer.VectorisedView) (buffer.VectorisedView, bool, int) {
+func (r *reassembler) process(first, last uint16, more bool, vv buffer.VectorisedView) (buffer.VectorisedView, bool, int, error) {
 	r.mu.Lock()
 	defer r.mu.Unlock()
 	consumed := 0
@@ -86,7 +86,7 @@
 		// A concurrent goroutine might have already reassembled
 		// the packet and emptied the heap while this goroutine
 		// was waiting on the mutex. We don't have to do anything in this case.
-		return buffer.VectorisedView{}, false, consumed
+		return buffer.VectorisedView{}, false, consumed, nil
 	}
 	if r.updateHoles(first, last, more) {
 		// We store the incoming packet only if it filled some holes.
@@ -96,13 +96,13 @@
 	}
 	// Check if all the holes have been deleted and we are ready to reassamble.
 	if r.deleted < len(r.holes) {
-		return buffer.VectorisedView{}, false, consumed
+		return buffer.VectorisedView{}, false, consumed, nil
 	}
 	res, err := r.heap.reassemble()
 	if err != nil {
-		panic(fmt.Sprintf("reassemble failed with: %v. There is probably a bug in the code handling the holes.", err))
+		return buffer.VectorisedView{}, false, consumed, fmt.Errorf("fragment reassembly failed: %v", err)
 	}
-	return res, true, consumed
+	return res, true, consumed, nil
 }
 
 func (r *reassembler) tooOld(timeout time.Duration) bool {
diff --git a/tcpip/network/ipv4/ipv4.go b/tcpip/network/ipv4/ipv4.go
index 7fc5b36..30176c4 100644
--- a/tcpip/network/ipv4/ipv4.go
+++ b/tcpip/network/ipv4/ipv4.go
@@ -326,7 +326,13 @@
 			return
 		}
 		var ready bool
-		vv, ready = e.fragmentation.Process(hash.IPv4FragmentHash(h), h.FragmentOffset(), last, more, vv)
+		var err error
+		vv, ready, err = e.fragmentation.Process(hash.IPv4FragmentHash(h), h.FragmentOffset(), last, more, vv)
+		if err != nil {
+			r.Stats().IP.MalformedPacketsReceived.Increment()
+			r.Stats().IP.MalformedFragmentsReceived.Increment()
+			return
+		}
 		if !ready {
 			return
 		}
diff --git a/tcpip/network/ipv4/ipv4_test.go b/tcpip/network/ipv4/ipv4_test.go
index 7982ef5..7d4a896 100644
--- a/tcpip/network/ipv4/ipv4_test.go
+++ b/tcpip/network/ipv4/ipv4_test.go
@@ -47,10 +47,6 @@
 		t.Fatalf("CreateNIC failed: %v", err)
 	}
 
-	if err := s.AddAddress(1, ipv4.ProtocolNumber, header.IPv4Any); err != nil {
-		t.Fatalf("AddAddress failed: %v", err)
-	}
-
 	s.SetRouteTable([]tcpip.Route{{
 		Destination: header.IPv4EmptySubnet,
 		NIC:         1,
@@ -441,6 +437,16 @@
 			1,
 			1,
 		},
+		{
+			"multiple_fragments_with_more_fragments_set_to_false",
+			[][]byte{
+				{0x45, 0x00, 0x00, 0x1c, 0x30, 0x40, 0x00, 0x10, 0x00, 0x06, 0x34, 0x69, 0x73, 0x73, 0x69, 0x6e, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
+				{0x45, 0x00, 0x00, 0x1c, 0x30, 0x40, 0x00, 0x01, 0x61, 0x06, 0x34, 0x69, 0x73, 0x73, 0x69, 0x6e, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
+				{0x45, 0x00, 0x00, 0x1c, 0x30, 0x40, 0x20, 0x00, 0x00, 0x06, 0x34, 0x1e, 0x73, 0x73, 0x69, 0x6e, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
+			},
+			1,
+			1,
+		},
 	}
 
 	for _, tc := range testCases {
diff --git a/tcpip/network/ipv6/icmp.go b/tcpip/network/ipv6/icmp.go
index 14aa110..cfec146 100644
--- a/tcpip/network/ipv6/icmp.go
+++ b/tcpip/network/ipv6/icmp.go
@@ -21,15 +21,6 @@
 	"github.com/google/netstack/tcpip/stack"
 )
 
-const (
-	// ndpHopLimit is the expected IP hop limit value of 255 for received
-	// NDP packets, as per RFC 4861 sections 4.1 - 4.5, 6.1.1, 6.1.2, 7.1.1,
-	// 7.1.2 and 8.1. If the hop limit value is not 255, nodes MUST silently
-	// drop the NDP packet. All outgoing NDP packets must use this value for
-	// its IP hop limit field.
-	ndpHopLimit = 255
-)
-
 // handleControl handles the case when an ICMP packet contains the headers of
 // the original packet that caused the ICMP one to be sent. This information is
 // used to find out which transport endpoint must be notified about the ICMP
@@ -79,6 +70,7 @@
 		return
 	}
 	h := header.ICMPv6(v)
+	iph := header.IPv6(netHeader)
 
 	// As per RFC 4861 sections 4.1 - 4.5, 6.1.1, 6.1.2, 7.1.1, 7.1.2 and
 	// 8.1, nodes MUST silently drop NDP packets where the Hop Limit field
@@ -89,7 +81,7 @@
 		header.ICMPv6RouterSolicit,
 		header.ICMPv6RouterAdvert,
 		header.ICMPv6RedirectMsg:
-		if header.IPv6(netHeader).HopLimit() != ndpHopLimit {
+		if iph.HopLimit() != header.NDPHopLimit {
 			received.Invalid.Increment()
 			return
 		}
@@ -125,19 +117,68 @@
 			received.Invalid.Increment()
 			return
 		}
-		targetAddr := tcpip.Address(v[8:][:header.IPv6AddressSize])
+
+		ns := header.NDPNeighborSolicit(h.NDPPayload())
+		targetAddr := ns.TargetAddress()
+		s := r.Stack()
+		rxNICID := r.NICID()
+
+		isTentative, err := s.IsAddrTentative(rxNICID, targetAddr)
+		if err != nil {
+			// We will only get an error if rxNICID is unrecognized,
+			// which should not happen. For now short-circuit this
+			// packet.
+			//
+			// TODO(b/141002840): Handle this better?
+			return
+		}
+
+		if isTentative {
+			// If the target address is tentative and the source
+			// of the packet is a unicast (specified) address, then
+			// the source of the packet is attempting to perform
+			// address resolution on the target. In this case, the
+			// solicitation is silently ignored, as per RFC 4862
+			// section 5.4.3.
+			//
+			// If the target address is tentative and the source of
+			// the packet is the unspecified address (::), then we
+			// know another node is also performing DAD for the
+			// same address (since targetAddr is tentative for us,
+			// we know we are also performing DAD on it). In this
+			// case we let the stack know so it can handle such a
+			// scenario and do nothing further with the NDP NS.
+			if iph.SourceAddress() == header.IPv6Any {
+				s.DupTentativeAddrDetected(rxNICID, targetAddr)
+			}
+
+			// Do not handle neighbor solicitations targeted
+			// to an address that is tentative on the received
+			// NIC any further.
+			return
+		}
+
+		// At this point we know that targetAddr is not tentative on
+		// rxNICID so the packet is processed as defined in RFC 4861,
+		// as per RFC 4862 section 5.4.3.
+
 		if e.linkAddrCache.CheckLocalAddress(e.nicid, ProtocolNumber, targetAddr) == 0 {
 			// We don't have a useful answer; the best we can do is ignore the request.
 			return
 		}
-		hdr := buffer.NewPrependable(int(r.MaxHeaderLength()) + header.ICMPv6NeighborAdvertSize)
+
+		optsSerializer := header.NDPOptionsSerializer{
+			header.NDPTargetLinkLayerAddressOption(r.LocalLinkAddress[:]),
+		}
+		hdr := buffer.NewPrependable(int(r.MaxHeaderLength()) + header.ICMPv6NeighborAdvertMinimumSize + int(optsSerializer.Length()))
 		pkt := header.ICMPv6(hdr.Prepend(header.ICMPv6NeighborAdvertSize))
 		pkt.SetType(header.ICMPv6NeighborAdvert)
-		pkt[icmpV6FlagOffset] = ndpSolicitedFlag | ndpOverrideFlag
-		copy(pkt[icmpV6OptOffset-len(targetAddr):], targetAddr)
-		pkt[icmpV6OptOffset] = ndpOptDstLinkAddr
-		pkt[icmpV6LengthOffset] = 1
-		copy(pkt[icmpV6LengthOffset+1:], r.LocalLinkAddress[:])
+		na := header.NDPNeighborAdvert(pkt.NDPPayload())
+		na.SetSolicitedFlag(true)
+		na.SetOverrideFlag(true)
+		na.SetTargetAddress(targetAddr)
+		opts := na.Options()
+		opts.Serialize(optsSerializer)
 
 		// ICMPv6 Neighbor Solicit messages are always sent to
 		// specially crafted IPv6 multicast addresses. As a result, the
@@ -167,7 +208,7 @@
 		//
 		// The IP Hop Limit field has a value of 255, i.e., the packet
 		// could not possibly have been forwarded by a router.
-		if err := r.WritePacket(nil /* gso */, hdr, buffer.VectorisedView{}, stack.NetworkHeaderParams{Protocol: header.ICMPv6ProtocolNumber, TTL: ndpHopLimit, TOS: stack.DefaultTOS}); err != nil {
+		if err := r.WritePacket(nil /* gso */, hdr, buffer.VectorisedView{}, stack.NetworkHeaderParams{Protocol: header.ICMPv6ProtocolNumber, TTL: header.NDPHopLimit, TOS: stack.DefaultTOS}); err != nil {
 			sent.Dropped.Increment()
 			return
 		}
@@ -179,7 +220,42 @@
 			received.Invalid.Increment()
 			return
 		}
-		targetAddr := tcpip.Address(v[8:][:header.IPv6AddressSize])
+
+		na := header.NDPNeighborAdvert(h.NDPPayload())
+		targetAddr := na.TargetAddress()
+		stack := r.Stack()
+		rxNICID := r.NICID()
+
+		isTentative, err := stack.IsAddrTentative(rxNICID, targetAddr)
+		if err != nil {
+			// We will only get an error if rxNICID is unrecognized,
+			// which should not happen. For now short-circuit this
+			// packet.
+			//
+			// TODO(b/141002840): Handle this better?
+			return
+		}
+
+		if isTentative {
+			// We just got an NA from a node that owns an address we
+			// are performing DAD on, implying the address is not
+			// unique. In this case we let the stack know so it can
+			// handle such a scenario and do nothing furthur with
+			// the NDP NA.
+			stack.DupTentativeAddrDetected(rxNICID, targetAddr)
+			return
+		}
+
+		// At this point we know that the targetAddress is not tentaive
+		// on rxNICID. However, targetAddr may still be assigned to
+		// rxNICID but not tentative (it could be permanent). Such a
+		// scenario is beyond the scope of RFC 4862. As such, we simply
+		// ignore such a scenario for now and proceed as normal.
+		//
+		// TODO(b/140896005): Handle the scenario described above
+		// (inform the netstack integration that a duplicate address was
+		// was detected)
+
 		e.linkAddrCache.AddLinkAddress(e.nicid, targetAddr, r.RemoteLinkAddress)
 		if targetAddr != r.RemoteAddress {
 			e.linkAddrCache.AddLinkAddress(e.nicid, r.RemoteAddress, r.RemoteLinkAddress)
@@ -274,7 +350,7 @@
 	ip.Encode(&header.IPv6Fields{
 		PayloadLength: length,
 		NextHeader:    uint8(header.ICMPv6ProtocolNumber),
-		HopLimit:      ndpHopLimit,
+		HopLimit:      header.NDPHopLimit,
 		SrcAddr:       r.LocalAddress,
 		DstAddr:       r.RemoteAddress,
 	})
diff --git a/tcpip/network/ipv6/icmp_test.go b/tcpip/network/ipv6/icmp_test.go
index 4448102..ecac4f1 100644
--- a/tcpip/network/ipv6/icmp_test.go
+++ b/tcpip/network/ipv6/icmp_test.go
@@ -143,7 +143,7 @@
 		ip.Encode(&header.IPv6Fields{
 			PayloadLength: uint16(payloadLength),
 			NextHeader:    uint8(header.ICMPv6ProtocolNumber),
-			HopLimit:      ndpHopLimit,
+			HopLimit:      header.NDPHopLimit,
 			SrcAddr:       r.LocalAddress,
 			DstAddr:       r.RemoteAddress,
 		})
diff --git a/tcpip/network/ipv6/ndp_test.go b/tcpip/network/ipv6/ndp_test.go
index 2adba16..3818263 100644
--- a/tcpip/network/ipv6/ndp_test.go
+++ b/tcpip/network/ipv6/ndp_test.go
@@ -150,7 +150,7 @@
 
 			// Receive the NDP packet with an invalid hop limit
 			// value.
-			handleIPv6Payload(hdr, ndpHopLimit-1, ep, &r)
+			handleIPv6Payload(hdr, header.NDPHopLimit-1, ep, &r)
 
 			// Invalid count should have increased.
 			if got := invalid.Value(); got != 1 {
@@ -164,7 +164,7 @@
 			}
 
 			// Receive the NDP packet with a valid hop limit value.
-			handleIPv6Payload(hdr, ndpHopLimit, ep, &r)
+			handleIPv6Payload(hdr, header.NDPHopLimit, ep, &r)
 
 			// Rx count of NDP packet of type typ.typ should have
 			// increased.
diff --git a/tcpip/stack/ndp.go b/tcpip/stack/ndp.go
new file mode 100644
index 0000000..05a4561
--- /dev/null
+++ b/tcpip/stack/ndp.go
@@ -0,0 +1,279 @@
+// Copyright 2019 The gVisor Authors.
+//
+// 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 stack
+
+import (
+	"fmt"
+	"log"
+	"time"
+
+	"github.com/google/netstack/tcpip"
+	"github.com/google/netstack/tcpip/buffer"
+	"github.com/google/netstack/tcpip/header"
+)
+
+const (
+	// defaultDupAddrDetectTransmits is the default number of NDP Neighbor
+	// Solicitation messages to send when doing Duplicate Address Detection
+	// for a tentative address.
+	//
+	// Default = 1 (from RFC 4862 section 5.1)
+	defaultDupAddrDetectTransmits = 1
+
+	// defaultRetransmitTimer is the default amount of time to wait between
+	// sending NDP Neighbor solicitation messages.
+	//
+	// Default = 1s (from RFC 4861 section 10).
+	defaultRetransmitTimer = time.Second
+
+	// minimumRetransmitTimer is the minimum amount of time to wait between
+	// sending NDP Neighbor solicitation messages. Note, RFC 4861 does
+	// not impose a minimum Retransmit Timer, but we do here to make sure
+	// the messages are not sent all at once. We also come to this value
+	// because in the RetransmitTimer field of a Router Advertisement, a
+	// value of 0 means unspecified, so the smallest valid value is 1.
+	// Note, the unit of the RetransmitTimer field in the Router
+	// Advertisement is milliseconds.
+	//
+	// Min = 1ms.
+	minimumRetransmitTimer = time.Millisecond
+)
+
+// NDPConfigurations is the NDP configurations for the netstack.
+type NDPConfigurations struct {
+	// The number of Neighbor Solicitation messages to send when doing
+	// Duplicate Address Detection for a tentative address.
+	//
+	// Note, a value of zero effectively disables DAD.
+	DupAddrDetectTransmits uint8
+
+	// The amount of time to wait between sending Neighbor solicitation
+	// messages.
+	//
+	// Must be greater than 0.5s.
+	RetransmitTimer time.Duration
+}
+
+// DefaultNDPConfigurations returns an NDPConfigurations populated with
+// default values.
+func DefaultNDPConfigurations() NDPConfigurations {
+	return NDPConfigurations{
+		DupAddrDetectTransmits: defaultDupAddrDetectTransmits,
+		RetransmitTimer:        defaultRetransmitTimer,
+	}
+}
+
+// validate modifies an NDPConfigurations with valid values. If invalid values
+// are present in c, the corresponding default values will be used instead.
+//
+// If RetransmitTimer is less than minimumRetransmitTimer, then a value of
+// defaultRetransmitTimer will be used.
+func (c *NDPConfigurations) validate() {
+	if c.RetransmitTimer < minimumRetransmitTimer {
+		c.RetransmitTimer = defaultRetransmitTimer
+	}
+}
+
+// ndpState is the per-interface NDP state.
+type ndpState struct {
+	// The DAD state to send the next NS message, or resolve the address.
+	dad map[tcpip.Address]dadState
+}
+
+// dadState holds the Duplicate Address Detection timer and channel to signal
+// to the DAD goroutine that DAD should stop.
+type dadState struct {
+	// The DAD timer to send the next NS message, or resolve the address.
+	timer *time.Timer
+
+	// Used to let the DAD timer know that it has been stopped.
+	//
+	// Must only be read from or written to while protected by the lock of
+	// the NIC this dadState is associated with.
+	done *bool
+}
+
+// startDuplicateAddressDetection performs Duplicate Address Detection.
+//
+// This function must only be called by IPv6 addresses that are currently
+// tentative.
+//
+// The NIC that ndp belongs to (n) MUST be locked.
+func (ndp *ndpState) startDuplicateAddressDetection(n *NIC, addr tcpip.Address, ref *referencedNetworkEndpoint) *tcpip.Error {
+	// addr must be a valid unicast IPv6 address.
+	if !header.IsV6UnicastAddress(addr) {
+		return tcpip.ErrAddressFamilyNotSupported
+	}
+
+	// Should not attempt to perform DAD on an address that is currently in
+	// the DAD process.
+	if _, ok := ndp.dad[addr]; ok {
+		// Should never happen because we should only ever call this
+		// function for newly created addresses. If we attemped to
+		// "add" an address that already existed, we would returned an
+		// error since we attempted to add a duplicate address, or its
+		// reference count would have been increased without doing the
+		// work that would have been done for an address that was brand
+		// new. See NIC.addPermanentAddressLocked.
+		panic(fmt.Sprintf("ndpdad: already performing DAD for addr %s on NIC(%d)", addr, n.ID()))
+	}
+
+	remaining := n.stack.ndpConfigs.DupAddrDetectTransmits
+
+	{
+		done, err := ndp.doDuplicateAddressDetection(n, addr, remaining, ref)
+		if err != nil {
+			return err
+		}
+		if done {
+			return nil
+		}
+	}
+
+	remaining--
+
+	var done bool
+	var timer *time.Timer
+	timer = time.AfterFunc(n.stack.ndpConfigs.RetransmitTimer, func() {
+		n.mu.Lock()
+		defer n.mu.Unlock()
+
+		if done {
+			// If we reach this point, it means that the DAD timer
+			// fired after another goroutine already obtained the
+			// NIC lock and stopped DAD before it this function
+			// obtained the NIC lock. Simply return here and do
+			// nothing further.
+			return
+		}
+
+		ref, ok := n.endpoints[NetworkEndpointID{addr}]
+		if !ok {
+			// This should never happen.
+			// We should have an endpoint for addr since we are
+			// still performing DAD on it. If the endpoint does not
+			// exist, but we are doing DAD on it, then we started
+			// DAD at some point, but forgot to stop it when the
+			// endpoint was deleted.
+			panic(fmt.Sprintf("ndpdad: unrecognized addr %s for NIC(%d)", addr, n.ID()))
+		}
+
+		if done, err := ndp.doDuplicateAddressDetection(n, addr, remaining, ref); err != nil || done {
+			if err != nil {
+				log.Printf("ndpdad: Error occured during DAD iteration for addr (%s) on NIC(%d); err = %s", addr, n.ID(), err)
+			}
+
+			ndp.stopDuplicateAddressDetection(addr)
+			return
+		}
+
+		timer.Reset(n.stack.ndpConfigs.RetransmitTimer)
+		remaining--
+
+	})
+
+	ndp.dad[addr] = dadState{
+		timer: timer,
+		done:  &done,
+	}
+
+	return nil
+}
+
+// doDuplicateAddressDetection is called on every iteration of the timer, and
+// when DAD starts.
+//
+// It handles resolving the address (if there are no more NS to send), or
+// sending the next NS if there are more NS to send.
+//
+// This function must only be called by IPv6 addresses that are currently
+// tentative.
+//
+// The NIC that ndp belongs to (n) MUST be locked.
+//
+// Returns true if DAD has resolved; false if DAD is still ongoing.
+func (ndp *ndpState) doDuplicateAddressDetection(n *NIC, addr tcpip.Address, remaining uint8, ref *referencedNetworkEndpoint) (bool, *tcpip.Error) {
+	if ref.getKind() != permanentTentative {
+		// The endpoint should still be marked as tentative
+		// since we are still performing DAD on it.
+		panic(fmt.Sprintf("ndpdad: addr %s is not tentative on NIC(%d)", addr, n.ID()))
+	}
+
+	if remaining == 0 {
+		// DAD has resolved.
+		ref.setKind(permanent)
+		return true, nil
+	}
+
+	// Send a new NS.
+	snmc := header.SolicitedNodeAddr(addr)
+	snmcRef, ok := n.endpoints[NetworkEndpointID{snmc}]
+	if !ok {
+		// This should never happen as if we have the
+		// address, we should have the solicited-node
+		// address.
+		panic(fmt.Sprintf("ndpdad: NIC(%d) is not in the solicited-node multicast group (%s) but it has addr %s", n.ID(), snmc, addr))
+	}
+
+	// Use the unspecified address as the source address when performing
+	// DAD.
+	r := makeRoute(header.IPv6ProtocolNumber, header.IPv6Any, snmc, n.linkEP.LinkAddress(), snmcRef, false, false)
+
+	hdr := buffer.NewPrependable(int(r.MaxHeaderLength()) + header.ICMPv6NeighborSolicitMinimumSize)
+	pkt := header.ICMPv6(hdr.Prepend(header.ICMPv6NeighborSolicitMinimumSize))
+	pkt.SetType(header.ICMPv6NeighborSolicit)
+	ns := header.NDPNeighborSolicit(pkt.NDPPayload())
+	ns.SetTargetAddress(addr)
+	pkt.SetChecksum(header.ICMPv6Checksum(pkt, r.LocalAddress, r.RemoteAddress, buffer.VectorisedView{}))
+
+	sent := r.Stats().ICMP.V6PacketsSent
+	if err := r.WritePacket(nil, hdr, buffer.VectorisedView{}, NetworkHeaderParams{Protocol: header.ICMPv6ProtocolNumber, TTL: header.NDPHopLimit, TOS: DefaultTOS}); err != nil {
+		sent.Dropped.Increment()
+		return false, err
+	}
+	sent.NeighborSolicit.Increment()
+
+	return false, nil
+}
+
+// stopDuplicateAddressDetection ends a running Duplicate Address Detection
+// process. Note, this may leave the DAD process for a tentative address in
+// such a state forever, unless some other external event resolves the DAD
+// process (receiving an NA from the true owner of addr, or an NS for addr
+// (implying another node is attempting to use addr)). It is up to the caller
+// of this function to handle such a scenario. Normally, addr will be removed
+// from n right after this function returns or the address successfully
+// resolved.
+//
+// The NIC that ndp belongs to MUST be locked.
+func (ndp *ndpState) stopDuplicateAddressDetection(addr tcpip.Address) {
+	dad, ok := ndp.dad[addr]
+	if !ok {
+		// Not currently performing DAD on addr, just return.
+		return
+	}
+
+	if dad.timer != nil {
+		dad.timer.Stop()
+		dad.timer = nil
+
+		*dad.done = true
+		dad.done = nil
+	}
+
+	delete(ndp.dad, addr)
+
+	return
+}
diff --git a/tcpip/stack/ndp_test.go b/tcpip/stack/ndp_test.go
new file mode 100644
index 0000000..40f26b2
--- /dev/null
+++ b/tcpip/stack/ndp_test.go
@@ -0,0 +1,353 @@
+// Copyright 2019 The gVisor Authors.
+//
+// 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 stack_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/google/netstack/tcpip"
+	"github.com/google/netstack/tcpip/buffer"
+	"github.com/google/netstack/tcpip/checker"
+	"github.com/google/netstack/tcpip/header"
+	"github.com/google/netstack/tcpip/link/channel"
+	"github.com/google/netstack/tcpip/network/ipv6"
+	"github.com/google/netstack/tcpip/stack"
+	"github.com/google/netstack/tcpip/transport/icmp"
+)
+
+const (
+	addr1     = "\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"
+	addr2     = "\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02"
+	linkAddr1 = "\x01\x02\x03\x04\x05\x06"
+	linkAddr2 = "\x01\x02\x03\x04\x05\x07"
+)
+
+// TestDADDisabled tests that an address successfully resolves immediately
+// when DAD is not enabled (the default for an empty stack.Options).
+func TestDADDisabled(t *testing.T) {
+	opts := stack.Options{
+		NetworkProtocols: []stack.NetworkProtocol{ipv6.NewProtocol()},
+	}
+
+	e := channel.New(10, 1280, linkAddr1)
+	s := stack.New(opts)
+	if err := s.CreateNIC(1, e); err != nil {
+		t.Fatalf("CreateNIC(_) = %s", err)
+	}
+
+	if err := s.AddAddress(1, header.IPv6ProtocolNumber, addr1); err != nil {
+		t.Fatalf("AddAddress(_, %d, %s) = %s", header.IPv6ProtocolNumber, addr1, err)
+	}
+
+	// Should get the address immediately since we should not have performed
+	// DAD on it.
+	addr, err := s.GetMainNICAddress(1, header.IPv6ProtocolNumber)
+	if err != nil {
+		t.Fatalf("stack.GetMainNICAddress(_, _) err = %s", err)
+	}
+	if addr.Address != addr1 {
+		t.Fatalf("got stack.GetMainNICAddress(_, _) = %s, want = %s", addr, addr1)
+	}
+
+	// We should not have sent any NDP NS messages.
+	if got := s.Stats().ICMP.V6PacketsSent.NeighborSolicit.Value(); got != 0 {
+		t.Fatalf("got NeighborSolicit = %d, want = 0", got)
+	}
+}
+
+// TestDADResolve tests that an address successfully resolves after performing
+// DAD for various values of DupAddrDetectTransmits and RetransmitTimer.
+// Included in the subtests is a test to make sure that an invalid
+// RetransmitTimer (<1ms) values get fixed to the default RetransmitTimer of 1s.
+func TestDADResolve(t *testing.T) {
+	tests := []struct {
+		name                    string
+		dupAddrDetectTransmits  uint8
+		retransTimer            time.Duration
+		expectedRetransmitTimer time.Duration
+	}{
+		{"1:1s:1s", 1, time.Second, time.Second},
+		{"2:1s:1s", 2, time.Second, time.Second},
+		{"1:2s:2s", 1, 2 * time.Second, 2 * time.Second},
+		// 0s is an invalid RetransmitTimer timer and will be fixed to
+		// the default RetransmitTimer value of 1s.
+		{"1:0s:1s", 1, 0, time.Second},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			opts := stack.Options{
+				NetworkProtocols: []stack.NetworkProtocol{ipv6.NewProtocol()},
+			}
+			opts.NDPConfigs.RetransmitTimer = test.retransTimer
+			opts.NDPConfigs.DupAddrDetectTransmits = test.dupAddrDetectTransmits
+
+			e := channel.New(10, 1280, linkAddr1)
+			s := stack.New(opts)
+			if err := s.CreateNIC(1, e); err != nil {
+				t.Fatalf("CreateNIC(_) = %s", err)
+			}
+
+			if err := s.AddAddress(1, header.IPv6ProtocolNumber, addr1); err != nil {
+				t.Fatalf("AddAddress(_, %d, %s) = %s", header.IPv6ProtocolNumber, addr1, err)
+			}
+
+			stat := s.Stats().ICMP.V6PacketsSent.NeighborSolicit
+
+			// Should have sent an NDP NS almost immediately.
+			time.Sleep(100 * time.Millisecond)
+			if got := stat.Value(); got != 1 {
+				t.Fatalf("got NeighborSolicit = %d, want = 1", got)
+
+			}
+
+			// Address should not be considered bound to the NIC yet
+			// (DAD ongoing).
+			addr, err := s.GetMainNICAddress(1, header.IPv6ProtocolNumber)
+			if err != nil {
+				t.Fatalf("got stack.GetMainNICAddress(_, _) = (_, %v), want = (_, nil)", err)
+			}
+			if want := (tcpip.AddressWithPrefix{}); addr != want {
+				t.Fatalf("got stack.GetMainNICAddress(_, _) = (%s, nil), want = (%s, nil)", addr, want)
+			}
+
+			// Wait for the remaining time - 500ms, to make sure
+			// the address is still not resolved. Note, we subtract
+			// 600ms because we already waited for 100ms earlier,
+			// so our remaining time is 100ms less than the expected
+			// time.
+			// (X - 100ms) - 500ms = X - 600ms
+			//
+			// TODO(b/140896005): Use events from the netstack to
+			// be signalled before DAD resolves.
+			time.Sleep(test.expectedRetransmitTimer*time.Duration(test.dupAddrDetectTransmits) - 600*time.Millisecond)
+			addr, err = s.GetMainNICAddress(1, header.IPv6ProtocolNumber)
+			if err != nil {
+				t.Fatalf("got stack.GetMainNICAddress(_, _) = (_, %v), want = (_, nil)", err)
+			}
+			if want := (tcpip.AddressWithPrefix{}); addr != want {
+				t.Fatalf("got stack.GetMainNICAddress(_, _) = (%s, nil), want = (%s, nil)", addr, want)
+			}
+
+			// Wait for the remaining time + 250ms, at which point
+			// the address should be resolved. Note, the remaining
+			// time is 500ms. See above comments.
+			//
+			// TODO(b/140896005): Use events from the netstack to
+			// know immediately when DAD completes.
+			time.Sleep(750 * time.Millisecond)
+			addr, err = s.GetMainNICAddress(1, header.IPv6ProtocolNumber)
+			if err != nil {
+				t.Fatalf("stack.GetMainNICAddress(_, _) err = %s", err)
+			}
+			if addr.Address != addr1 {
+				t.Fatalf("got stack.GetMainNICAddress(_, _) = %s, want = %s", addr, addr1)
+			}
+
+			// Should not have sent any more NS messages.
+			if got := stat.Value(); got != uint64(test.dupAddrDetectTransmits) {
+				t.Fatalf("got NeighborSolicit = %d, want = %d", got, test.dupAddrDetectTransmits)
+			}
+
+			// Validate the sent Neighbor Solicitation messages.
+			for i := uint8(0); i < test.dupAddrDetectTransmits; i++ {
+				p := <-e.C
+
+				// Make sure its an IPv6 packet.
+				if p.Proto != header.IPv6ProtocolNumber {
+					t.Fatalf("got Proto = %d, want = %d", p.Proto, header.IPv6ProtocolNumber)
+				}
+
+				// Check NDP packet.
+				checker.IPv6(t, p.Header.ToVectorisedView().First(),
+					checker.TTL(header.NDPHopLimit),
+					checker.NDPNS(
+						checker.NDPNSTargetAddress(addr1)))
+			}
+		})
+	}
+
+}
+
+// TestDADFail tests to make sure that the DAD process fails if another node is
+// detected to be performing DAD on the same address (receive an NS message from
+// a node doing DAD for the same address), or if another node is detected to own
+// the address already (receive an NA message for the tentative address).
+func TestDADFail(t *testing.T) {
+	tests := []struct {
+		name    string
+		makeBuf func(tgt tcpip.Address) buffer.Prependable
+		getStat func(s tcpip.ICMPv6ReceivedPacketStats) *tcpip.StatCounter
+	}{
+		{
+			"RxSolicit",
+			func(tgt tcpip.Address) buffer.Prependable {
+				hdr := buffer.NewPrependable(header.IPv6MinimumSize + header.ICMPv6NeighborSolicitMinimumSize)
+				pkt := header.ICMPv6(hdr.Prepend(header.ICMPv6NeighborSolicitMinimumSize))
+				pkt.SetType(header.ICMPv6NeighborSolicit)
+				ns := header.NDPNeighborSolicit(pkt.NDPPayload())
+				ns.SetTargetAddress(tgt)
+				snmc := header.SolicitedNodeAddr(tgt)
+				pkt.SetChecksum(header.ICMPv6Checksum(pkt, header.IPv6Any, snmc, buffer.VectorisedView{}))
+				payloadLength := hdr.UsedLength()
+				ip := header.IPv6(hdr.Prepend(header.IPv6MinimumSize))
+				ip.Encode(&header.IPv6Fields{
+					PayloadLength: uint16(payloadLength),
+					NextHeader:    uint8(icmp.ProtocolNumber6),
+					HopLimit:      255,
+					SrcAddr:       header.IPv6Any,
+					DstAddr:       snmc,
+				})
+
+				return hdr
+
+			},
+			func(s tcpip.ICMPv6ReceivedPacketStats) *tcpip.StatCounter {
+				return s.NeighborSolicit
+			},
+		},
+		{
+			"RxAdvert",
+			func(tgt tcpip.Address) buffer.Prependable {
+				hdr := buffer.NewPrependable(header.IPv6MinimumSize + header.ICMPv6NeighborAdvertSize)
+				pkt := header.ICMPv6(hdr.Prepend(header.ICMPv6NeighborAdvertSize))
+				pkt.SetType(header.ICMPv6NeighborAdvert)
+				na := header.NDPNeighborAdvert(pkt.NDPPayload())
+				na.SetSolicitedFlag(true)
+				na.SetOverrideFlag(true)
+				na.SetTargetAddress(tgt)
+				pkt.SetChecksum(header.ICMPv6Checksum(pkt, tgt, header.IPv6AllNodesMulticastAddress, buffer.VectorisedView{}))
+				payloadLength := hdr.UsedLength()
+				ip := header.IPv6(hdr.Prepend(header.IPv6MinimumSize))
+				ip.Encode(&header.IPv6Fields{
+					PayloadLength: uint16(payloadLength),
+					NextHeader:    uint8(icmp.ProtocolNumber6),
+					HopLimit:      255,
+					SrcAddr:       tgt,
+					DstAddr:       header.IPv6AllNodesMulticastAddress,
+				})
+
+				return hdr
+
+			},
+			func(s tcpip.ICMPv6ReceivedPacketStats) *tcpip.StatCounter {
+				return s.NeighborAdvert
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			opts := stack.Options{
+				NetworkProtocols: []stack.NetworkProtocol{ipv6.NewProtocol()},
+				NDPConfigs:       stack.DefaultNDPConfigurations(),
+			}
+			opts.NDPConfigs.RetransmitTimer = time.Second * 2
+
+			e := channel.New(10, 1280, linkAddr1)
+			s := stack.New(opts)
+			if err := s.CreateNIC(1, e); err != nil {
+				t.Fatalf("CreateNIC(_) = %s", err)
+			}
+
+			if err := s.AddAddress(1, header.IPv6ProtocolNumber, addr1); err != nil {
+				t.Fatalf("AddAddress(_, %d, %s) = %s", header.IPv6ProtocolNumber, addr1, err)
+			}
+
+			// Address should not be considered bound to the NIC yet
+			// (DAD ongoing).
+			addr, err := s.GetMainNICAddress(1, header.IPv6ProtocolNumber)
+			if err != nil {
+				t.Fatalf("got stack.GetMainNICAddress(_, _) = (_, %v), want = (_, nil)", err)
+			}
+			if want := (tcpip.AddressWithPrefix{}); addr != want {
+				t.Fatalf("got stack.GetMainNICAddress(_, _) = (%s, nil), want = (%s, nil)", addr, want)
+			}
+
+			// Receive a packet to simulate multiple nodes owning or
+			// attempting to own the same address.
+			hdr := test.makeBuf(addr1)
+			e.Inject(header.IPv6ProtocolNumber, hdr.View().ToVectorisedView())
+
+			stat := test.getStat(s.Stats().ICMP.V6PacketsReceived)
+			if got := stat.Value(); got != 1 {
+				t.Fatalf("got stat = %d, want = 1", got)
+			}
+
+			// Wait 3 seconds to make sure that DAD did not resolve
+			time.Sleep(3 * time.Second)
+			addr, err = s.GetMainNICAddress(1, header.IPv6ProtocolNumber)
+			if err != nil {
+				t.Fatalf("got stack.GetMainNICAddress(_, _) = (_, %v), want = (_, nil)", err)
+			}
+			if want := (tcpip.AddressWithPrefix{}); addr != want {
+				t.Fatalf("got stack.GetMainNICAddress(_, _) = (%s, nil), want = (%s, nil)", addr, want)
+			}
+		})
+	}
+}
+
+// TestDADStop tests to make sure that the DAD process stops when an address is
+// removed.
+func TestDADStop(t *testing.T) {
+	opts := stack.Options{
+		NetworkProtocols: []stack.NetworkProtocol{ipv6.NewProtocol()},
+	}
+	opts.NDPConfigs.RetransmitTimer = time.Second
+	opts.NDPConfigs.DupAddrDetectTransmits = 2
+
+	e := channel.New(10, 1280, linkAddr1)
+	s := stack.New(opts)
+	if err := s.CreateNIC(1, e); err != nil {
+		t.Fatalf("CreateNIC(_) = %s", err)
+	}
+
+	if err := s.AddAddress(1, header.IPv6ProtocolNumber, addr1); err != nil {
+		t.Fatalf("AddAddress(_, %d, %s) = %s", header.IPv6ProtocolNumber, addr1, err)
+	}
+
+	// Address should not be considered bound to the NIC yet (DAD ongoing).
+	addr, err := s.GetMainNICAddress(1, header.IPv6ProtocolNumber)
+	if err != nil {
+		t.Fatalf("got stack.GetMainNICAddress(_, _) = (_, %v), want = (_, nil)", err)
+	}
+	if want := (tcpip.AddressWithPrefix{}); addr != want {
+		t.Fatalf("got stack.GetMainNICAddress(_, _) = (%s, nil), want = (%s, nil)", addr, want)
+	}
+
+	// Remove the address. This should stop DAD.
+	if err := s.RemoveAddress(1, addr1); err != nil {
+		t.Fatalf("RemoveAddress(_, %s) = %s", addr1, err)
+	}
+
+	// Wait for the time to normally resolve
+	// DupAddrDetectTransmits(2) * RetransmitTimer(1s) = 2s.
+	// An extra 250ms is added to make sure that if DAD was still running
+	// it resolves and the check below fails.
+	time.Sleep(2*time.Second + 250*time.Millisecond)
+	addr, err = s.GetMainNICAddress(1, header.IPv6ProtocolNumber)
+	if err != nil {
+		t.Fatalf("got stack.GetMainNICAddress(_, _) = (_, %v), want = (_, nil)", err)
+	}
+	if want := (tcpip.AddressWithPrefix{}); addr != want {
+		t.Fatalf("got stack.GetMainNICAddress(_, _) = (%s, nil), want = (%s, nil)", addr, want)
+	}
+
+	// Should not have sent more than 1 NS message.
+	if got := s.Stats().ICMP.V6PacketsSent.NeighborSolicit.Value(); got > 1 {
+		t.Fatalf("got NeighborSolicit = %d, want <= 1", got)
+	}
+}
diff --git a/tcpip/stack/nic.go b/tcpip/stack/nic.go
index 5838b1c..3fc6dc4 100644
--- a/tcpip/stack/nic.go
+++ b/tcpip/stack/nic.go
@@ -43,6 +43,8 @@
 	mcastJoins    map[NetworkEndpointID]int32
 
 	stats NICStats
+
+	ndp ndpState
 }
 
 // NICStats includes transmitted and received stats.
@@ -96,6 +98,9 @@
 				Bytes:   &tcpip.StatCounter{},
 			},
 		},
+		ndp: ndpState{
+			dad: make(map[tcpip.Address]dadState),
+		},
 	}
 }
 
@@ -168,11 +173,6 @@
 
 	for e := list.Front(); e != nil; e = e.Next() {
 		r := e.(*referencedNetworkEndpoint)
-		// TODO(crawshaw): allow broadcast address when SO_BROADCAST is set.
-		switch r.ep.ID().LocalAddress {
-		case header.IPv4Broadcast, header.IPv4Any:
-			continue
-		}
 		if r.isValidForOutgoing() && r.tryIncRef() {
 			return r
 		}
@@ -282,7 +282,7 @@
 	id := NetworkEndpointID{protocolAddress.AddressWithPrefix.Address}
 	if ref, ok := n.endpoints[id]; ok {
 		switch ref.getKind() {
-		case permanent:
+		case permanentTentative, permanent:
 			// The NIC already have a permanent endpoint with that address.
 			return nil, tcpip.ErrDuplicateAddress
 		case permanentExpired, temporary:
@@ -298,6 +298,7 @@
 			n.removeEndpointLocked(ref)
 		}
 	}
+
 	return n.addAddressLocked(protocolAddress, peb, permanent)
 }
 
@@ -321,6 +322,15 @@
 	if err != nil {
 		return nil, err
 	}
+
+	isIPv6Unicast := protocolAddress.Protocol == header.IPv6ProtocolNumber && header.IsV6UnicastAddress(protocolAddress.AddressWithPrefix.Address)
+
+	// If the address is an IPv6 address and it is a permanent address,
+	// mark it as tentative so it goes through the DAD process.
+	if isIPv6Unicast && kind == permanent {
+		kind = permanentTentative
+	}
+
 	ref := &referencedNetworkEndpoint{
 		refs:     1,
 		ep:       ep,
@@ -338,7 +348,7 @@
 
 	// If we are adding an IPv6 unicast address, join the solicited-node
 	// multicast address.
-	if protocolAddress.Protocol == header.IPv6ProtocolNumber && header.IsV6UnicastAddress(protocolAddress.AddressWithPrefix.Address) {
+	if isIPv6Unicast {
 		snmc := header.SolicitedNodeAddr(protocolAddress.AddressWithPrefix.Address)
 		if err := n.joinGroupLocked(protocolAddress.Protocol, snmc); err != nil {
 			return nil, err
@@ -360,6 +370,13 @@
 		l.PushFront(ref)
 	}
 
+	// If we are adding a tentative IPv6 address, start DAD.
+	if isIPv6Unicast && kind == permanentTentative {
+		if err := n.ndp.startDuplicateAddressDetection(n, protocolAddress.AddressWithPrefix.Address, ref); err != nil {
+			return nil, err
+		}
+	}
+
 	return ref, nil
 }
 
@@ -382,10 +399,12 @@
 
 	addrs := make([]tcpip.ProtocolAddress, 0, len(n.endpoints))
 	for nid, ref := range n.endpoints {
-		// Don't include expired or temporary endpoints to avoid confusion and
-		// prevent the caller from using those.
+		// Don't include tentative, expired or temporary endpoints to
+		// avoid confusion and prevent the caller from using those.
 		switch ref.getKind() {
-		case permanentExpired, temporary:
+		case permanentTentative, permanentExpired, temporary:
+			// TODO(b/140898488): Should tentative addresses be
+			//                    returned?
 			continue
 		}
 		addrs = append(addrs, tcpip.ProtocolAddress{
@@ -408,10 +427,11 @@
 	for proto, list := range n.primary {
 		for e := list.Front(); e != nil; e = e.Next() {
 			ref := e.(*referencedNetworkEndpoint)
-			// Don't include expired or tempory endpoints to avoid confusion and
-			// prevent the caller from using those.
+			// Don't include tentative, expired or tempory endpoints
+			// to avoid confusion and prevent the caller from using
+			// those.
 			switch ref.getKind() {
-			case permanentExpired, temporary:
+			case permanentTentative, permanentExpired, temporary:
 				continue
 			}
 
@@ -504,10 +524,22 @@
 
 func (n *NIC) removePermanentAddressLocked(addr tcpip.Address) *tcpip.Error {
 	r, ok := n.endpoints[NetworkEndpointID{addr}]
-	if !ok || r.getKind() != permanent {
+	if !ok {
 		return tcpip.ErrBadLocalAddress
 	}
 
+	kind := r.getKind()
+	if kind != permanent && kind != permanentTentative {
+		return tcpip.ErrBadLocalAddress
+	}
+
+	isIPv6Unicast := r.protocol == header.IPv6ProtocolNumber && header.IsV6UnicastAddress(addr)
+
+	// If we are removing a tentative IPv6 unicast address, stop DAD.
+	if isIPv6Unicast && kind == permanentTentative {
+		n.ndp.stopDuplicateAddressDetection(addr)
+	}
+
 	r.setKind(permanentExpired)
 	if !r.decRefLocked() {
 		// The endpoint still has references to it.
@@ -518,7 +550,7 @@
 
 	// If we are removing an IPv6 unicast address, leave the solicited-node
 	// multicast address.
-	if r.protocol == header.IPv6ProtocolNumber && header.IsV6UnicastAddress(addr) {
+	if isIPv6Unicast {
 		snmc := header.SolicitedNodeAddr(addr)
 		if err := n.leaveGroupLocked(snmc); err != nil {
 			return err
@@ -767,14 +799,58 @@
 	return n.stack
 }
 
+// isAddrTentative returns true if addr is tentative on n.
+//
+// Note that if addr is not associated with n, then this function will return
+// false. It will only return true if the address is associated with the NIC
+// AND it is tentative.
+func (n *NIC) isAddrTentative(addr tcpip.Address) bool {
+	ref, ok := n.endpoints[NetworkEndpointID{addr}]
+	if !ok {
+		return false
+	}
+
+	return ref.getKind() == permanentTentative
+}
+
+// dupTentativeAddrDetected attempts to inform n that a tentative addr
+// is a duplicate on a link.
+//
+// dupTentativeAddrDetected will delete the tentative address if it exists.
+func (n *NIC) dupTentativeAddrDetected(addr tcpip.Address) *tcpip.Error {
+	n.mu.Lock()
+	defer n.mu.Unlock()
+
+	ref, ok := n.endpoints[NetworkEndpointID{addr}]
+	if !ok {
+		return tcpip.ErrBadAddress
+	}
+
+	if ref.getKind() != permanentTentative {
+		return tcpip.ErrInvalidEndpointState
+	}
+
+	return n.removePermanentAddressLocked(addr)
+}
+
 type networkEndpointKind int32
 
 const (
+	// A permanentTentative endpoint is a permanent address that is not yet
+	// considered to be fully bound to an interface in the traditional
+	// sense. That is, the address is associated with a NIC, but packets
+	// destined to the address MUST NOT be accepted and MUST be silently
+	// dropped, and the address MUST NOT be used as a source address for
+	// outgoing packets. For IPv6, addresses will be of this kind until
+	// NDP's Duplicate Address Detection has resolved, or be deleted if
+	// the process results in detecting a duplicate address.
+	permanentTentative networkEndpointKind = iota
+
 	// A permanent endpoint is created by adding a permanent address (vs. a
 	// temporary one) to the NIC. Its reference count is biased by 1 to avoid
 	// removal when no route holds a reference to it. It is removed by explicitly
 	// removing the permanent address from the NIC.
-	permanent networkEndpointKind = iota
+	permanent
 
 	// An expired permanent endoint is a permanent endoint that had its address
 	// removed from the NIC, and it is waiting to be removed once no more routes
diff --git a/tcpip/stack/stack.go b/tcpip/stack/stack.go
index 2153c0e..d6a0471 100644
--- a/tcpip/stack/stack.go
+++ b/tcpip/stack/stack.go
@@ -399,6 +399,9 @@
 	//
 	// TODO(gvisor.dev/issue/940): S/R this field.
 	portSeed uint32
+
+	// ndpConfigs is the NDP configurations used by interfaces.
+	ndpConfigs NDPConfigurations
 }
 
 // Options contains optional Stack configuration.
@@ -425,6 +428,13 @@
 	// UnassociatedFactory produces unassociated endpoints raw endpoints.
 	// Raw endpoints are enabled only if this is non-nil.
 	UnassociatedFactory UnassociatedEndpointFactory
+
+	// NDPConfigs is the NDP configurations used by interfaces.
+	//
+	// By default, NDPConfigs will have a zero value for its
+	// DupAddrDetectTransmits field, implying that DAD will not be performed
+	// before assigning an address to a NIC.
+	NDPConfigs NDPConfigurations
 }
 
 // TransportEndpointInfo holds useful information about a transport endpoint
@@ -458,6 +468,9 @@
 // New allocates a new networking stack with only the requested networking and
 // transport protocols configured with default options.
 //
+// Note, NDPConfigurations will be fixed before being used by the Stack. That
+// is, if an invalid value was provided, it will be reset to the default value.
+//
 // Protocol options can be changed by calling the
 // SetNetworkProtocolOption/SetTransportProtocolOption methods provided by the
 // stack. Please refer to individual protocol implementations as to what options
@@ -468,6 +481,9 @@
 		clock = &tcpip.StdClock{}
 	}
 
+	// Make sure opts.NDPConfigs contains valid values only.
+	opts.NDPConfigs.validate()
+
 	s := &Stack{
 		transportProtocols: make(map[tcpip.TransportProtocolNumber]*transportProtocolState),
 		networkProtocols:   make(map[tcpip.NetworkProtocolNumber]NetworkProtocol),
@@ -480,6 +496,7 @@
 		handleLocal:        opts.HandleLocal,
 		icmpRateLimiter:    NewICMPRateLimiter(),
 		portSeed:           generateRandUint32(),
+		ndpConfigs:         opts.NDPConfigs,
 	}
 
 	// Add specified network protocols.
@@ -1238,6 +1255,37 @@
 	return s.icmpRateLimiter.Allow()
 }
 
+// IsAddrTentative returns true if addr is tentative on the NIC with ID id.
+//
+// Note that if addr is not associated with a NIC with id ID, then this
+// function will return false. It will only return true if the address is
+// associated with the NIC AND it is tentative.
+func (s *Stack) IsAddrTentative(id tcpip.NICID, addr tcpip.Address) (bool, *tcpip.Error) {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
+	nic, ok := s.nics[id]
+	if !ok {
+		return false, tcpip.ErrUnknownNICID
+	}
+
+	return nic.isAddrTentative(addr), nil
+}
+
+// DupTentativeAddrDetected attempts to inform the NIC with ID id that a
+// tentative addr on it is a duplicate on a link.
+func (s *Stack) DupTentativeAddrDetected(id tcpip.NICID, addr tcpip.Address) *tcpip.Error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	nic, ok := s.nics[id]
+	if !ok {
+		return tcpip.ErrUnknownNICID
+	}
+
+	return nic.dupTentativeAddrDetected(addr)
+}
+
 // PortSeed returns a 32 bit value that can be used as a seed value for port
 // picking.
 //