Repository: pion/sctp
Pull request #284 (2023-07-20 10:02:14)

Implement draft-ietf-tsvwg-sctp-zero-checksum-01
#### Description

Implementation of [draft-ietf-tsvwg-sctp-zero-checksum-01](https://datatracker.ietf.org/doc/html/draft-ietf-tsvwg-sctp-zero-checksum-01). 

I currently don't have a specific use case for this, but implementing the draft was an interesting exercise. I suggest keeping this as draft PR, at least until the document is finalized.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pion/sctp/pull/284?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) Report
Patch coverage: **`49.05`**% and project coverage change: **`-13.87`** :warning:
> Comparison is base [(`68fe6a1`)](https://app.codecov.io/gh/pion/sctp/commit/68fe6a132f841424f90bb757e184f2ed3ac607ec?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) 80.48% compared to head [(`2227146`)](https://app.codecov.io/gh/pion/sctp/pull/284?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) 66.61%.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           master     #284       +/-   ##
===========================================
- Coverage   80.48%   66.61%   -13.87%     
===========================================
  Files          48       49        +1     
  Lines        4043     4134       +91     
===========================================
- Hits         3254     2754      -500     
- Misses        647     1214      +567     
- Partials      142      166       +24     
```

| Flag | Coverage Δ | |
|---|---|---|
| go | `?` | |
| wasm | `66.61% <49.05%> (-0.44%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://app.codecov.io/gh/pion/sctp/pull/284?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | Coverage Δ | |
|---|---|---|
| [param.go](https://app.codecov.io/gh/pion/sctp/pull/284?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGFyYW0uZ28=) | `76.92% <0.00%> (-6.42%)` | :arrow_down: |
| [paramtype.go](https://app.codecov.io/gh/pion/sctp/pull/284?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGFyYW10eXBlLmdv) | `16.92% <0.00%> (-0.54%)` | :arrow_down: |
| [association.go](https://app.codecov.io/gh/pion/sctp/pull/284?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-YXNzb2NpYXRpb24uZ28=) | `61.44% <44.28%> (-23.12%)` | :arrow_down: |
| [param\_zero\_checksum.go](https://app.codecov.io/gh/pion/sctp/pull/284?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGFyYW1femVyb19jaGVja3N1bS5nbw==) | `60.00% <60.00%> (ø)` | |
| [packet.go](https://app.codecov.io/gh/pion/sctp/pull/284?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGFja2V0Lmdv) | `61.34% <70.58%> (-11.74%)` | :arrow_down: |

... and [16 files with indirect coverage changes](https://app.codecov.io/gh/pion/sctp/pull/284/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pion/sctp/pull/284?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).

tuexen: Question, maybe also for @Sean-Der : What possibilities of collaboration do exist? Only by commenting or is there a way to propose changes to a PR?
mengelbart: Hi @tuexen! You can add suggestions in the comments (there's a little icon in the comments UI where you can directly suggest code changes), or I think you can also open a PR against this branch. I also don't mind if you directly push to this branch, but I think that only works if you are a member of the pion org. (if not, I think @Sean-Der can add you as a member?)
Sean-Der: Done! You can directly push to branches now @tuexen 

Everyone just needs one approval to merge to master :)
tuexen: > Done! You can directly push to branches now @tuexen

Great thanks. This makes things much simpler. 

> 
> Everyone just needs one approval to merge to master :)

Great. I'll be conservative, since I'm new to go...

Sean-Der: Anything I can do to help @mengelbart ? Seems like this is really close, would love to get it in :) 
mengelbart: I think it is almost done. The IETF draft changed a bit since I first implemented it. I don't think it requires changes in this patch but someone should probably review that again. And I think the code needs to be reviewed in general :)
daweifeng: I have been using this branch in my project for a month and I haven't seen any issues so far. It is compatible with the C++ zero checksum implementation in [libdatachannel](https://github.com/paullouisageneau/libdatachannel)
matthewfarstad: Very excited for this to be merged. Until then I will be using the PR.

Patch:
diff --git a/association.go b/association.go
index 507e3aa..cf493ef 100644
--- a/association.go
+++ b/association.go
@@ -177,6 +177,8 @@ type Association struct {
 	cumulativeTSNAckPoint   uint32
 	advancedPeerTSNAckPoint uint32
 	useForwardTSN           bool
+	useZeroChecksum         bool
+	requestZeroChecksum     bool
 
 	// Congestion control parameters
 	maxReceiveBufferSize uint32
@@ -233,6 +235,7 @@ type Config struct {
 	NetConn              net.Conn
 	MaxReceiveBufferSize uint32
 	MaxMessageSize       uint32
+	EnableZeroChecksum   bool
 	LoggerFactory        logging.LoggerFactory
 }
 
@@ -320,6 +323,7 @@ func createAssociation(config Config) *Association {
 		handshakeCompletedCh:    make(chan error),
 		cumulativeTSNAckPoint:   tsn - 1,
 		advancedPeerTSNAckPoint: tsn - 1,
+		requestZeroChecksum:     config.EnableZeroChecksum,
 		silentError:             ErrSilentlyDiscard,
 		stats:                   &associationStats{},
 		log:                     config.LoggerFactory.NewLogger("sctp"),
@@ -362,6 +366,11 @@ func (a *Association) init(isClient bool) {
 		init.initiateTag = a.myVerificationTag
 		init.advertisedReceiverWindowCredit = a.maxReceiveBufferSize
 		setSupportedExtensions(&init.chunkInitCommon)
+
+		if a.requestZeroChecksum {
+			init.params = append(init.params, &paramZeroChecksumAcceptable{edmid: dtlsErrorDetectionMethod})
+		}
+
 		a.storedInit = init
 
 		err := a.sendInit()
@@ -618,10 +627,45 @@ func (a *Association) unregisterStream(s *Stream, err error) {
 	s.readNotifier.Broadcast()
 }
 
+func chunkMandatoryChecksum(cc []chunk) bool {
+	for _, c := range cc {
+		switch c.(type) {
+		case *chunkInit, *chunkInitAck, *chunkCookieEcho:
+			return true
+		}
+	}
+	return false
+}
+
+func (a *Association) marshalPacket(p *packet) ([]byte, error) {
+	return p.marshal(!a.useZeroChecksum || chunkMandatoryChecksum(p.chunks))
+}
+
+func (a *Association) unmarshalPacket(raw []byte) (*packet, error) {
+	p := &packet{}
+	if !a.useZeroChecksum {
+		if err := p.unmarshal(true, raw); err != nil {
+			return nil, err
+		}
+		return p, nil
+	}
+
+	if err := p.unmarshal(false, raw); err != nil {
+		return nil, err
+	}
+	if chunkMandatoryChecksum(p.chunks) {
+		if err := p.unmarshal(true, raw); err != nil {
+			return nil, err
+		}
+	}
+
+	return p, nil
+}
+
 // handleInbound parses incoming raw packets
 func (a *Association) handleInbound(raw []byte) error {
-	p := &packet{}
-	if err := p.unmarshal(raw); err != nil {
+	p, err := a.unmarshalPacket(raw)
+	if err != nil {
 		a.log.Warnf("[%s] unable to parse SCTP packet %s", a.name, err)
 		return nil
 	}
@@ -647,7 +691,7 @@ func (a *Association) handleInbound(raw []byte) error {
 // The caller should hold the lock
 func (a *Association) gatherDataPacketsToRetransmit(rawPackets [][]byte) [][]byte {
 	for _, p := range a.getDataPacketsToRetransmit() {
-		raw, err := p.marshal()
+		raw, err := a.marshalPacket(p)
 		if err != nil {
 			a.log.Warnf("[%s] failed to serialize a DATA packet to be retransmitted", a.name)
 			continue
@@ -668,7 +712,7 @@ func (a *Association) gatherOutboundDataAndReconfigPackets(rawPackets [][]byte)
 		a.log.Tracef("[%s] T3-rtx timer start (pt1)", a.name)
 		a.t3RTX.start(a.rtoMgr.getRTO())
 		for _, p := range a.bundleDataChunksIntoPackets(chunks) {
-			raw, err := p.marshal()
+			raw, err := a.marshalPacket(p)
 			if err != nil {
 				a.log.Warnf("[%s] failed to serialize a DATA packet", a.name)
 				continue
@@ -683,7 +727,7 @@ func (a *Association) gatherOutboundDataAndReconfigPackets(rawPackets [][]byte)
 			a.log.Debugf("[%s] retransmit %d RECONFIG chunk(s)", a.name, len(a.reconfigs))
 			for _, c := range a.reconfigs {
 				p := a.createPacket([]chunk{c})
-				raw, err := p.marshal()
+				raw, err := a.marshalPacket(p)
 				if err != nil {
 					a.log.Warnf("[%s] failed to serialize a RECONFIG packet to be retransmitted", a.name)
 				} else {
@@ -706,7 +750,7 @@ func (a *Association) gatherOutboundDataAndReconfigPackets(rawPackets [][]byte)
 			a.log.Debugf("[%s] sending RECONFIG: rsn=%d tsn=%d streams=%v",
 				a.name, rsn, a.myNextTSN-1, sisToReset)
 			p := a.createPacket([]chunk{c})
-			raw, err := p.marshal()
+			raw, err := a.marshalPacket(p)
 			if err != nil {
 				a.log.Warnf("[%s] failed to serialize a RECONFIG packet to be transmitted", a.name)
 			} else {
@@ -769,7 +813,7 @@ func (a *Association) gatherOutboundFastRetransmissionPackets(rawPackets [][]byt
 		}
 
 		if len(toFastRetrans) > 0 {
-			raw, err := a.createPacket(toFastRetrans).marshal()
+			raw, err := a.marshalPacket(a.createPacket(toFastRetrans))
 			if err != nil {
 				a.log.Warnf("[%s] failed to serialize a DATA packet to be fast-retransmitted", a.name)
 			} else {
@@ -787,7 +831,7 @@ func (a *Association) gatherOutboundSackPackets(rawPackets [][]byte) [][]byte {
 		a.ackState = ackStateIdle
 		sack := a.createSelectiveAckChunk()
 		a.log.Debugf("[%s] sending SACK: %s", a.name, sack)
-		raw, err := a.createPacket([]chunk{sack}).marshal()
+		raw, err := a.marshalPacket(a.createPacket([]chunk{sack}))
 		if err != nil {
 			a.log.Warnf("[%s] failed to serialize a SACK packet", a.name)
 		} else {
@@ -804,7 +848,7 @@ func (a *Association) gatherOutboundForwardTSNPackets(rawPackets [][]byte) [][]b
 		a.willSendForwardTSN = false
 		if sna32GT(a.advancedPeerTSNAckPoint, a.cumulativeTSNAckPoint) {
 			fwdtsn := a.createForwardTSN()
-			raw, err := a.createPacket([]chunk{fwdtsn}).marshal()
+			raw, err := a.marshalPacket(a.createPacket([]chunk{fwdtsn}))
 			if err != nil {
 				a.log.Warnf("[%s] failed to serialize a Forward TSN packet", a.name)
 			} else {
@@ -827,7 +871,7 @@ func (a *Association) gatherOutboundShutdownPackets(rawPackets [][]byte) ([][]by
 			cumulativeTSNAck: a.cumulativeTSNAckPoint,
 		}
 
-		raw, err := a.createPacket([]chunk{shutdown}).marshal()
+		raw, err := a.marshalPacket(a.createPacket([]chunk{shutdown}))
 		if err != nil {
 			a.log.Warnf("[%s] failed to serialize a Shutdown packet", a.name)
 		} else {
@@ -839,7 +883,7 @@ func (a *Association) gatherOutboundShutdownPackets(rawPackets [][]byte) ([][]by
 
 		shutdownAck := &chunkShutdownAck{}
 
-		raw, err := a.createPacket([]chunk{shutdownAck}).marshal()
+		raw, err := a.marshalPacket(a.createPacket([]chunk{shutdownAck}))
 		if err != nil {
 			a.log.Warnf("[%s] failed to serialize a ShutdownAck packet", a.name)
 		} else {
@@ -851,7 +895,7 @@ func (a *Association) gatherOutboundShutdownPackets(rawPackets [][]byte) ([][]by
 
 		shutdownComplete := &chunkShutdownComplete{}
 
-		raw, err := a.createPacket([]chunk{shutdownComplete}).marshal()
+		raw, err := a.marshalPacket(a.createPacket([]chunk{shutdownComplete}))
 		if err != nil {
 			a.log.Warnf("[%s] failed to serialize a ShutdownComplete packet", a.name)
 		} else {
@@ -875,7 +919,7 @@ func (a *Association) gatherAbortPacket() ([]byte, error) {
 		abort.errorCauses = []errorCause{cause}
 	}
 
-	raw, err := a.createPacket([]chunk{abort}).marshal()
+	raw, err := a.marshalPacket(a.createPacket([]chunk{abort}))
 
 	return raw, err
 }
@@ -900,7 +944,7 @@ func (a *Association) gatherOutbound() ([][]byte, bool) {
 
 	if a.controlQueue.size() > 0 {
 		for _, p := range a.controlQueue.popAll() {
-			raw, err := p.marshal()
+			raw, err := a.marshalPacket(p)
 			if err != nil {
 				a.log.Warnf("[%s] failed to serialize a control packet", a.name)
 				continue
@@ -1092,6 +1136,7 @@ func (a *Association) handleInit(p *packet, i *chunkInit) ([]*packet, error) {
 	// subtracting one from it.
 	a.peerLastTSN = i.initialTSN - 1
 
+	peerHasZeroChecksum := false
 	for _, param := range i.params {
 		switch v := param.(type) { // nolint:gocritic
 		case *paramSupportedExtensions:
@@ -1101,8 +1146,11 @@ func (a *Association) handleInit(p *packet, i *chunkInit) ([]*packet, error) {
 					a.useForwardTSN = true
 				}
 			}
+		case *paramZeroChecksumAcceptable:
+			peerHasZeroChecksum = v.edmid == dtlsErrorDetectionMethod
 		}
 	}
+
 	if !a.useForwardTSN {
 		a.log.Warnf("[%s] not using ForwardTSN (on init)", a.name)
 	}
@@ -1129,6 +1177,12 @@ func (a *Association) handleInit(p *packet, i *chunkInit) ([]*packet, error) {
 
 	initAck.params = []param{a.myCookie}
 
+	if peerHasZeroChecksum {
+		initAck.params = append(initAck.params, &paramZeroChecksumAcceptable{edmid: dtlsErrorDetectionMethod})
+		a.useZeroChecksum = true
+	}
+	a.log.Debugf("[%s] useZeroChecksum=%t (on init)", a.name, a.useZeroChecksum)
+
 	setSupportedExtensions(&initAck.chunkInitCommon)
 
 	outbound.chunks = []chunk{initAck}
@@ -1186,8 +1240,13 @@ func (a *Association) handleInitAck(p *packet, i *chunkInitAck) error {
 					a.useForwardTSN = true
 				}
 			}
+		case *paramZeroChecksumAcceptable:
+			a.useZeroChecksum = v.edmid == dtlsErrorDetectionMethod
 		}
 	}
+
+	a.log.Debugf("[%s] useZeroChecksum=%t (on initAck)", a.name, a.useZeroChecksum)
+
 	if !a.useForwardTSN {
 		a.log.Warnf("[%s] not using ForwardTSN (on initAck)", a.name)
 	}
diff --git a/packet.go b/packet.go
index 2d40d29..316ca7d 100644
--- a/packet.go
+++ b/packet.go
@@ -65,7 +65,7 @@ var (
 	ErrChecksumMismatch            = errors.New("checksum mismatch theirs")
 )
 
-func (p *packet) unmarshal(raw []byte) error {
+func (p *packet) unmarshal(doChecksum bool, raw []byte) error {
 	if len(raw) < packetHeaderSize {
 		return fmt.Errorf("%w: raw only %d bytes, %d is the minimum length", ErrPacketRawTooSmall, len(raw), packetHeaderSize)
 	}
@@ -125,15 +125,19 @@ func (p *packet) unmarshal(raw []byte) error {
 		chunkValuePadding := getPadding(c.valueLength())
 		offset += chunkHeaderSize + c.valueLength() + chunkValuePadding
 	}
-	theirChecksum := binary.LittleEndian.Uint32(raw[8:])
-	ourChecksum := generatePacketChecksum(raw)
-	if theirChecksum != ourChecksum {
-		return fmt.Errorf("%w: %d ours: %d", ErrChecksumMismatch, theirChecksum, ourChecksum)
+
+	if doChecksum {
+		theirChecksum := binary.LittleEndian.Uint32(raw[8:])
+		ourChecksum := generatePacketChecksum(raw)
+		if theirChecksum != ourChecksum {
+			return fmt.Errorf("%w: %d ours: %d", ErrChecksumMismatch, theirChecksum, ourChecksum)
+		}
 	}
+
 	return nil
 }
 
-func (p *packet) marshal() ([]byte, error) {
+func (p *packet) marshal(doChecksum bool) ([]byte, error) {
 	raw := make([]byte, packetHeaderSize)
 
 	// Populate static headers
@@ -156,9 +160,12 @@ func (p *packet) marshal() ([]byte, error) {
 		}
 	}
 
-	// Checksum is already in BigEndian
-	// Using LittleEndian.PutUint32 stops it from being flipped
-	binary.LittleEndian.PutUint32(raw[8:], generatePacketChecksum(raw))
+	if doChecksum {
+		// Checksum is already in BigEndian
+		// Using LittleEndian.PutUint32 stops it from being flipped
+		binary.LittleEndian.PutUint32(raw[8:], generatePacketChecksum(raw))
+	}
+
 	return raw, nil
 }
 
diff --git a/param.go b/param.go
index 8035add..c28d7a5 100644
--- a/param.go
+++ b/param.go
@@ -38,6 +38,8 @@ func buildParam(t paramType, rawParam []byte) (param, error) {
 		return (&paramOutgoingResetRequest{}).unmarshal(rawParam)
 	case reconfigResp:
 		return (&paramReconfigResponse{}).unmarshal(rawParam)
+	case zeroChecksumAcceptable:
+		return (&paramZeroChecksumAcceptable{}).unmarshal(rawParam)
 	default:
 		return nil, fmt.Errorf("%w: %v", ErrParamTypeUnhandled, t)
 	}
diff --git a/param_zero_checksum.go b/param_zero_checksum.go
new file mode 100644
index 0000000..ddf7c5a
--- /dev/null
+++ b/param_zero_checksum.go
@@ -0,0 +1,56 @@
+// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
+// SPDX-License-Identifier: MIT
+
+package sctp
+
+import (
+	"encoding/binary"
+	"errors"
+)
+
+//  This parameter is used to inform the receiver that a sender is willing to
+//  accept zero as checksum if some other error detection method is used
+//  instead.
+//
+//  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |   Type = 0x8001 (suggested)   |          Length = 8           |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+// |           Error Detection Method Identifier (EDMID)           |
+// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+type paramZeroChecksumAcceptable struct {
+	paramHeader
+	// The Error Detection Method Identifier (EDMID) specifies an alternate
+	// error detection method the sender of this parameter is willing to use for
+	// received packets.
+	edmid uint32
+}
+
+// Zero Checksum parameter error
+var (
+	ErrZeroChecksumParamTooShort = errors.New("zero checksum parameter too short")
+)
+
+const (
+	dtlsErrorDetectionMethod uint32 = 1
+)
+
+func (r *paramZeroChecksumAcceptable) marshal() ([]byte, error) {
+	r.typ = zeroChecksumAcceptable
+	r.raw = make([]byte, 4)
+	binary.BigEndian.PutUint32(r.raw, r.edmid)
+	return r.paramHeader.marshal()
+}
+
+func (r *paramZeroChecksumAcceptable) unmarshal(raw []byte) (param, error) {
+	err := r.paramHeader.unmarshal(raw)
+	if err != nil {
+		return nil, err
+	}
+	if len(r.raw) < 4 {
+		return nil, ErrZeroChecksumParamTooShort
+	}
+	r.edmid = binary.BigEndian.Uint32(r.raw)
+	return r, nil
+}
diff --git a/paramtype.go b/paramtype.go
index f0a3da3..de1b3dd 100644
--- a/paramtype.go
+++ b/paramtype.go
@@ -13,33 +13,34 @@ import (
 type paramType uint16
 
 const (
-	heartbeatInfo      paramType = 1     // Heartbeat Info	[RFC4960]
-	ipV4Addr           paramType = 5     // IPv4 IP	[RFC4960]
-	ipV6Addr           paramType = 6     // IPv6 IP	[RFC4960]
-	stateCookie        paramType = 7     // State Cookie	[RFC4960]
-	unrecognizedParam  paramType = 8     // Unrecognized Parameters	[RFC4960]
-	cookiePreservative paramType = 9     // Cookie Preservative	[RFC4960]
-	hostNameAddr       paramType = 11    // Host Name IP	[RFC4960]
-	supportedAddrTypes paramType = 12    // Supported IP Types	[RFC4960]
-	outSSNResetReq     paramType = 13    // Outgoing SSN Reset Request Parameter	[RFC6525]
-	incSSNResetReq     paramType = 14    // Incoming SSN Reset Request Parameter	[RFC6525]
-	ssnTSNResetReq     paramType = 15    // SSN/TSN Reset Request Parameter	[RFC6525]
-	reconfigResp       paramType = 16    // Re-configuration Response Parameter	[RFC6525]
-	addOutStreamsReq   paramType = 17    // Add Outgoing Streams Request Parameter	[RFC6525]
-	addIncStreamsReq   paramType = 18    // Add Incoming Streams Request Parameter	[RFC6525]
-	ecnCapable         paramType = 32768 // ECN Capable (0x8000)	[RFC2960]
-	random             paramType = 32770 // Random (0x8002)	[RFC4805]
-	chunkList          paramType = 32771 // Chunk List (0x8003)	[RFC4895]
-	reqHMACAlgo        paramType = 32772 // Requested HMAC Algorithm Parameter (0x8004)	[RFC4895]
-	padding            paramType = 32773 // Padding (0x8005)
-	supportedExt       paramType = 32776 // Supported Extensions (0x8008)	[RFC5061]
-	forwardTSNSupp     paramType = 49152 // Forward TSN supported (0xC000)	[RFC3758]
-	addIPAddr          paramType = 49153 // Add IP IP (0xC001)	[RFC5061]
-	delIPAddr          paramType = 49154 // Delete IP IP (0xC002)	[RFC5061]
-	errClauseInd       paramType = 49155 // Error Cause Indication (0xC003)	[RFC5061]
-	setPriAddr         paramType = 49156 // Set Primary IP (0xC004)	[RFC5061]
-	successInd         paramType = 49157 // Success Indication (0xC005)	[RFC5061]
-	adaptLayerInd      paramType = 49158 // Adaptation Layer Indication (0xC006)	[RFC5061]
+	heartbeatInfo          paramType = 1     // Heartbeat Info	[RFC4960]
+	ipV4Addr               paramType = 5     // IPv4 IP	[RFC4960]
+	ipV6Addr               paramType = 6     // IPv6 IP	[RFC4960]
+	stateCookie            paramType = 7     // State Cookie	[RFC4960]
+	unrecognizedParam      paramType = 8     // Unrecognized Parameters	[RFC4960]
+	cookiePreservative     paramType = 9     // Cookie Preservative	[RFC4960]
+	hostNameAddr           paramType = 11    // Host Name IP	[RFC4960]
+	supportedAddrTypes     paramType = 12    // Supported IP Types	[RFC4960]
+	outSSNResetReq         paramType = 13    // Outgoing SSN Reset Request Parameter	[RFC6525]
+	incSSNResetReq         paramType = 14    // Incoming SSN Reset Request Parameter	[RFC6525]
+	ssnTSNResetReq         paramType = 15    // SSN/TSN Reset Request Parameter	[RFC6525]
+	reconfigResp           paramType = 16    // Re-configuration Response Parameter	[RFC6525]
+	addOutStreamsReq       paramType = 17    // Add Outgoing Streams Request Parameter	[RFC6525]
+	addIncStreamsReq       paramType = 18    // Add Incoming Streams Request Parameter	[RFC6525]
+	ecnCapable             paramType = 32768 // ECN Capable (0x8000)	[RFC2960]
+	zeroChecksumAcceptable paramType = 32769 // Zero Checksum Acceptable [draft-ietf-tsvwg-sctp-zero-checksum-00]
+	random                 paramType = 32770 // Random (0x8002)	[RFC4805]
+	chunkList              paramType = 32771 // Chunk List (0x8003)	[RFC4895]
+	reqHMACAlgo            paramType = 32772 // Requested HMAC Algorithm Parameter (0x8004)	[RFC4895]
+	padding                paramType = 32773 // Padding (0x8005)
+	supportedExt           paramType = 32776 // Supported Extensions (0x8008)	[RFC5061]
+	forwardTSNSupp         paramType = 49152 // Forward TSN supported (0xC000)	[RFC3758]
+	addIPAddr              paramType = 49153 // Add IP IP (0xC001)	[RFC5061]
+	delIPAddr              paramType = 49154 // Delete IP IP (0xC002)	[RFC5061]
+	errClauseInd           paramType = 49155 // Error Cause Indication (0xC003)	[RFC5061]
+	setPriAddr             paramType = 49156 // Set Primary IP (0xC004)	[RFC5061]
+	successInd             paramType = 49157 // Success Indication (0xC005)	[RFC5061]
+	adaptLayerInd          paramType = 49158 // Adaptation Layer Indication (0xC006)	[RFC5061]
 )
 
 // Parameter packet errors
@@ -86,6 +87,8 @@ func (p paramType) String() string {
 		return "Add Incoming Streams Request Parameter"
 	case ecnCapable:
 		return "ECN Capable"
+	case zeroChecksumAcceptable:
+		return "Zero Checksum Acceptable"
 	case random:
 		return "Random"
 	case chunkList:


Test patch:
diff --git a/association_test.go b/association_test.go
index 7e88477..2b6fb73 100644
--- a/association_test.go
+++ b/association_test.go
@@ -1622,7 +1622,7 @@ func TestAssocT1CookieTimer(t *testing.T) {
 		// Drop all COOKIE-ECHO
 		br.Filter(0, func(raw []byte) bool {
 			p := &packet{}
-			err := p.unmarshal(raw)
+			err := p.unmarshal(true, raw)
 			if !assert.Nil(t, err, "failed to parse packet") {
 				return false // drop
 			}
@@ -2285,7 +2285,7 @@ func TestAssocAbort(t *testing.T) {
 			errorCauseHeader: errorCauseHeader{code: protocolViolation},
 		}},
 	}
-	packet, err := a0.createPacket([]chunk{abort}).marshal()
+	packet, err := a0.marshalPacket(a0.createPacket([]chunk{abort}))
 	assert.NoError(t, err)
 
 	_, _, err = establishSessionPair(br, a0, a1, si)
@@ -2964,7 +2964,7 @@ func TestAssociation_HandlePacketInCookieWaitState(t *testing.T) {
 				}()
 			}
 
-			packet, err := testCase.inputPacket.marshal()
+			packet, err := a.marshalPacket(testCase.inputPacket)
 			assert.NoError(t, err)
 			_, err = charlieConn.Write(packet)
 			assert.NoError(t, err)
@@ -3072,3 +3072,87 @@ loop:
 	assert.Error(t, err1, "context canceled")
 	assert.Error(t, err2, "context canceled")
 }
+
+type customLogger struct {
+	expectZeroChecksum bool
+	t                  *testing.T
+}
+
+func (c customLogger) Trace(string)                  {}
+func (c customLogger) Tracef(string, ...interface{}) {}
+func (c customLogger) Debug(string)                  {}
+func (c customLogger) Debugf(format string, args ...interface{}) {
+	if format == "[%s] useZeroChecksum=%t (on initAck)" {
+		assert.Equal(c.t, args[1], c.expectZeroChecksum)
+	}
+}
+func (c customLogger) Info(string)                   {}
+func (c customLogger) Infof(string, ...interface{})  {}
+func (c customLogger) Warn(string)                   {}
+func (c customLogger) Warnf(string, ...interface{})  {}
+func (c customLogger) Error(string)                  {}
+func (c customLogger) Errorf(string, ...interface{}) {}
+
+func (c customLogger) NewLogger(string) logging.LeveledLogger {
+	return c
+}
+
+func TestAssociation_ZeroChecksum(t *testing.T) {
+	checkGoroutineLeaks(t)
+
+	lim := test.TimeOut(time.Second * 10)
+	defer lim.Stop()
+
+	for _, testCase := range []struct {
+		clientZeroChecksum, serverZeroChecksum, expectChecksumEnabled bool
+	}{
+		{true, true, true},
+		{false, false, false},
+		{true, false, true},
+		{false, true, false},
+	} {
+		a1chan, a2chan := make(chan *Association), make(chan *Association)
+
+		udp1, udp2 := createUDPConnPair()
+
+		go func() {
+			a1, err := Client(Config{
+				NetConn:            udp1,
+				LoggerFactory:      &customLogger{testCase.expectChecksumEnabled, t},
+				EnableZeroChecksum: testCase.clientZeroChecksum,
+			})
+			assert.NoError(t, err)
+			a1chan <- a1
+		}()
+
+		go func() {
+			a2, err := Server(Config{
+				NetConn:            udp2,
+				LoggerFactory:      &customLogger{testCase.expectChecksumEnabled, t},
+				EnableZeroChecksum: testCase.serverZeroChecksum,
+			})
+			assert.NoError(t, err)
+			a2chan <- a2
+		}()
+
+		a1, a2 := <-a1chan, <-a2chan
+
+		writeStream, err := a1.OpenStream(1, PayloadTypeWebRTCString)
+		require.NoError(t, err)
+
+		readStream, err := a2.OpenStream(1, PayloadTypeWebRTCString)
+		require.NoError(t, err)
+
+		testData := []byte("test")
+		_, err = writeStream.Write(testData)
+		require.NoError(t, err)
+
+		buf := make([]byte, len(testData))
+		_, err = readStream.Read(buf)
+		assert.NoError(t, err)
+		assert.Equal(t, testData, buf)
+
+		require.NoError(t, a1.Close())
+		require.NoError(t, a2.Close())
+	}
+}
diff --git a/chunk_test.go b/chunk_test.go
index a060eb5..c831e44 100644
--- a/chunk_test.go
+++ b/chunk_test.go
@@ -18,7 +18,7 @@ func TestInitChunk(t *testing.T) {
 		0xc9, 0xbf, 0x75, 0x9c, 0xb1, 0x2c, 0x57, 0x4f, 0xa4, 0x5a, 0x51, 0xba, 0x60, 0x17, 0x78, 0x27, 0x94, 0x5c, 0x31, 0xe6,
 		0x5d, 0x5b, 0x09, 0x47, 0xe2, 0x22, 0x06, 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, 0x00, 0x00,
 	}
-	err := pkt.unmarshal(rawPkt)
+	err := pkt.unmarshal(true, rawPkt)
 	if err != nil {
 		t.Errorf("Unmarshal failed, has chunk")
 	}
@@ -47,7 +47,7 @@ func TestInitChunk(t *testing.T) {
 func TestInitAck(t *testing.T) {
 	pkt := &packet{}
 	rawPkt := []byte{0x13, 0x88, 0x13, 0x88, 0xce, 0x15, 0x79, 0xa2, 0x96, 0x19, 0xe8, 0xb2, 0x02, 0x00, 0x00, 0x1c, 0xeb, 0x81, 0x4e, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0x50, 0xdf, 0x90, 0xd9, 0x00, 0x07, 0x00, 0x08, 0x94, 0x06, 0x2f, 0x93}
-	err := pkt.unmarshal(rawPkt)
+	err := pkt.unmarshal(true, rawPkt)
 	if err != nil {
 		t.Errorf("Unmarshal failed, has chunk: %v", err)
 	}
@@ -63,12 +63,12 @@ func TestInitAck(t *testing.T) {
 func TestChromeChunk1Init(t *testing.T) {
 	pkt := &packet{}
 	rawPkt := []byte{0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0xbc, 0xb3, 0x45, 0xa2, 0x01, 0x00, 0x00, 0x56, 0xce, 0x15, 0x79, 0xa2, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0x94, 0x57, 0x95, 0xc0, 0xc0, 0x00, 0x00, 0x04, 0x80, 0x08, 0x00, 0x09, 0xc0, 0x0f, 0xc1, 0x80, 0x82, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x24, 0xff, 0x5c, 0x49, 0x19, 0x4a, 0x94, 0xe8, 0x2a, 0xec, 0x58, 0x55, 0x62, 0x29, 0x1f, 0x8e, 0x23, 0xcd, 0x7c, 0xe8, 0x46, 0xba, 0x58, 0x1b, 0x3d, 0xab, 0xd7, 0x7e, 0x50, 0xf2, 0x41, 0xb1, 0x2e, 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, 0x00, 0x00}
-	err := pkt.unmarshal(rawPkt)
+	err := pkt.unmarshal(true, rawPkt)
 	if err != nil {
 		t.Errorf("Unmarshal failed, has chunk: %v", err)
 	}
 
-	rawPkt2, err := pkt.marshal()
+	rawPkt2, err := pkt.marshal(true)
 	if err != nil {
 		t.Errorf("Remarshal failed: %v", err)
 	}
@@ -79,12 +79,12 @@ func TestChromeChunk1Init(t *testing.T) {
 func TestChromeChunk2InitAck(t *testing.T) {
 	pkt := &packet{}
 	rawPkt := []byte{0x13, 0x88, 0x13, 0x88, 0xce, 0x15, 0x79, 0xa2, 0xb5, 0xdb, 0x2d, 0x93, 0x02, 0x00, 0x01, 0x90, 0x9b, 0xd5, 0xb3, 0x6f, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0xef, 0xb4, 0x72, 0x87, 0xc0, 0x00, 0x00, 0x04, 0x80, 0x08, 0x00, 0x09, 0xc0, 0x0f, 0xc1, 0x80, 0x82, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x24, 0x2e, 0xf9, 0x9c, 0x10, 0x63, 0x72, 0xed, 0x0d, 0x33, 0xc2, 0xdc, 0x7f, 0x9f, 0xd7, 0xef, 0x1b, 0xc9, 0xc4, 0xa7, 0x41, 0x9a, 0x07, 0x68, 0x6b, 0x66, 0xfb, 0x6a, 0x4e, 0x32, 0x5d, 0xe4, 0x25, 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, 0x00, 0x00, 0x00, 0x07, 0x01, 0x38, 0x4b, 0x41, 0x4d, 0x45, 0x2d, 0x42, 0x53, 0x44, 0x20, 0x31, 0x2e, 0x31, 0x00, 0x00, 0x00, 0x00, 0x9c, 0x1e, 0x49, 0x5b, 0x00, 0x00, 0x00, 0x00, 0xd2, 0x42, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xea, 0x00, 0x00, 0xc4, 0x13, 0x3d, 0xe9, 0x86, 0xb1, 0x85, 0x75, 0xa2, 0x79, 0x15, 0xce, 0x9b, 0xd5, 0xb3, 0x6f, 0x20, 0xe0, 0x9f, 0x89, 0xe0, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x20, 0xe0, 0x9f, 0x89, 0xe0, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x56, 0xce, 0x15, 0x79, 0xa2, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0x94, 0x57, 0x95, 0xc0, 0xc0, 0x00, 0x00, 0x04, 0x80, 0x08, 0x00, 0x09, 0xc0, 0x0f, 0xc1, 0x80, 0x82, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x24, 0xff, 0x5c, 0x49, 0x19, 0x4a, 0x94, 0xe8, 0x2a, 0xec, 0x58, 0x55, 0x62, 0x29, 0x1f, 0x8e, 0x23, 0xcd, 0x7c, 0xe8, 0x46, 0xba, 0x58, 0x1b, 0x3d, 0xab, 0xd7, 0x7e, 0x50, 0xf2, 0x41, 0xb1, 0x2e, 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, 0x00, 0x00, 0x02, 0x00, 0x01, 0x90, 0x9b, 0xd5, 0xb3, 0x6f, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0xef, 0xb4, 0x72, 0x87, 0xc0, 0x00, 0x00, 0x04, 0x80, 0x08, 0x00, 0x09, 0xc0, 0x0f, 0xc1, 0x80, 0x82, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x24, 0x2e, 0xf9, 0x9c, 0x10, 0x63, 0x72, 0xed, 0x0d, 0x33, 0xc2, 0xdc, 0x7f, 0x9f, 0xd7, 0xef, 0x1b, 0xc9, 0xc4, 0xa7, 0x41, 0x9a, 0x07, 0x68, 0x6b, 0x66, 0xfb, 0x6a, 0x4e, 0x32, 0x5d, 0xe4, 0x25, 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, 0x00, 0x00, 0xca, 0x0c, 0x21, 0x11, 0xce, 0xf4, 0xfc, 0xb3, 0x66, 0x99, 0x4f, 0xdb, 0x4f, 0x95, 0x6b, 0x6f, 0x3b, 0xb1, 0xdb, 0x5a}
-	err := pkt.unmarshal(rawPkt)
+	err := pkt.unmarshal(true, rawPkt)
 	if err != nil {
 		t.Errorf("Unmarshal failed, has chunk: %v", err)
 	}
 
-	rawPkt2, err := pkt.marshal()
+	rawPkt2, err := pkt.marshal(true)
 	if err != nil {
 		t.Errorf("Remarshal failed: %v", err)
 	}
@@ -112,13 +112,13 @@ func TestInitMarshalUnmarshal(t *testing.T) {
 	initAck.params = []param{cookie}
 
 	p.chunks = []chunk{initAck}
-	rawPkt, err := p.marshal()
+	rawPkt, err := p.marshal(true)
 	if err != nil {
 		t.Errorf("Failed to marshal packet: %v", err)
 	}
 
 	pkt := &packet{}
-	err = pkt.unmarshal(rawPkt)
+	err = pkt.unmarshal(true, rawPkt)
 	if err != nil {
 		t.Errorf("Unmarshal failed, has chunk: %v", err)
 	}
@@ -147,7 +147,7 @@ func TestInitMarshalUnmarshal(t *testing.T) {
 func TestPayloadDataMarshalUnmarshal(t *testing.T) {
 	pkt := &packet{}
 	rawPkt := []byte{0x13, 0x88, 0x13, 0x88, 0xfc, 0xd6, 0x3f, 0xc6, 0xbe, 0xfa, 0xdc, 0x52, 0x0a, 0x00, 0x00, 0x24, 0x9b, 0x28, 0x7e, 0x48, 0xa3, 0x7b, 0xc1, 0x83, 0xc4, 0x4b, 0x41, 0x04, 0xa4, 0xf7, 0xed, 0x4c, 0x93, 0x62, 0xc3, 0x49, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x1f, 0xa8, 0x79, 0xa1, 0xc7, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x66, 0x6f, 0x6f, 0x00}
-	err := pkt.unmarshal(rawPkt)
+	err := pkt.unmarshal(true, rawPkt)
 	if err != nil {
 		t.Errorf("Unmarshal failed, has chunk: %v", err)
 	}
@@ -161,7 +161,7 @@ func TestPayloadDataMarshalUnmarshal(t *testing.T) {
 func TestSelectAckChunk(t *testing.T) {
 	pkt := &packet{}
 	rawPkt := []byte{0x13, 0x88, 0x13, 0x88, 0xc2, 0x98, 0x98, 0x0f, 0x42, 0x31, 0xea, 0x78, 0x03, 0x00, 0x00, 0x14, 0x87, 0x73, 0xbd, 0xa4, 0x00, 0x01, 0xfe, 0x74, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02}
-	err := pkt.unmarshal(rawPkt)
+	err := pkt.unmarshal(true, rawPkt)
 	if err != nil {
 		t.Errorf("Unmarshal failed, has chunk: %v", err)
 	}
@@ -175,7 +175,7 @@ func TestSelectAckChunk(t *testing.T) {
 func TestReconfigChunk(t *testing.T) {
 	pkt := &packet{}
 	rawPkt := []byte{0x13, 0x88, 0x13, 0x88, 0xb6, 0xa5, 0x12, 0xe5, 0x75, 0x3b, 0x12, 0xd3, 0x82, 0x0, 0x0, 0x16, 0x0, 0xd, 0x0, 0x12, 0x4e, 0x1c, 0xb9, 0xe6, 0x3a, 0x74, 0x8d, 0xff, 0x4e, 0x1c, 0xb9, 0xe6, 0x0, 0x1, 0x0, 0x0}
-	err := pkt.unmarshal(rawPkt)
+	err := pkt.unmarshal(true, rawPkt)
 	if err != nil {
 		t.Errorf("Unmarshal failed, has chunk: %v", err)
 	}
@@ -193,7 +193,7 @@ func TestReconfigChunk(t *testing.T) {
 func TestForwardTSNChunk(t *testing.T) {
 	pkt := &packet{}
 	rawPkt := append([]byte{0x13, 0x88, 0x13, 0x88, 0xb6, 0xa5, 0x12, 0xe5, 0x1f, 0x9d, 0xa0, 0xfb}, testChunkForwardTSN()...)
-	err := pkt.unmarshal(rawPkt)
+	err := pkt.unmarshal(true, rawPkt)
 	if err != nil {
 		t.Errorf("Unmarshal failed, has chunk: %v", err)
 	}
diff --git a/packet_test.go b/packet_test.go
index e36d064..1a270e5 100644
--- a/packet_test.go
+++ b/packet_test.go
@@ -11,12 +11,12 @@ import (
 func TestPacketUnmarshal(t *testing.T) {
 	pkt := &packet{}
 
-	if err := pkt.unmarshal([]byte{}); err == nil {
+	if err := pkt.unmarshal(true, []byte{}); err == nil {
 		t.Errorf("Unmarshal should fail when a packet is too small to be SCTP")
 	}
 
 	headerOnly := []byte{0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0x06, 0xa9, 0x00, 0xe1}
-	err := pkt.unmarshal(headerOnly)
+	err := pkt.unmarshal(true, headerOnly)
 	switch {
 	case err != nil:
 		t.Errorf("Unmarshal failed for SCTP packet with no chunks: %v", err)
@@ -36,7 +36,7 @@ func TestPacketUnmarshal(t *testing.T) {
 		0x5d, 0x5b, 0x09, 0x47, 0xe2, 0x22, 0x06, 0x80, 0x04, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, 0x00, 0x00,
 	}
 
-	if err := pkt.unmarshal(rawChunk); err != nil {
+	if err := pkt.unmarshal(true, rawChunk); err != nil {
 		t.Errorf("Unmarshal failed, has chunk: %v", err)
 	}
 }
@@ -45,11 +45,11 @@ func TestPacketMarshal(t *testing.T) {
 	pkt := &packet{}
 
 	headerOnly := []byte{0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0x06, 0xa9, 0x00, 0xe1}
-	if err := pkt.unmarshal(headerOnly); err != nil {
+	if err := pkt.unmarshal(true, headerOnly); err != nil {
 		t.Errorf("Unmarshal failed for SCTP packet with no chunks: %v", err)
 	}
 
-	headerOnlyMarshaled, err := pkt.marshal()
+	headerOnlyMarshaled, err := pkt.marshal(true)
 	if err != nil {
 		t.Errorf("Marshal failed for SCTP packet with no chunks: %v", err)
 	} else if !bytes.Equal(headerOnly, headerOnlyMarshaled) {
diff --git a/param_zero_checksum_test.go b/param_zero_checksum_test.go
new file mode 100644
index 0000000..562e9e0
--- /dev/null
+++ b/param_zero_checksum_test.go
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
+// SPDX-License-Identifier: MIT
+
+package sctp
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParamZeroChecksum(t *testing.T) {
+	tt := []struct {
+		binary []byte
+		parsed *paramZeroChecksumAcceptable
+	}{
+		{
+			binary: []byte{0x80, 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01},
+			parsed: &paramZeroChecksumAcceptable{
+				paramHeader: paramHeader{
+					typ:                zeroChecksumAcceptable,
+					unrecognizedAction: paramHeaderUnrecognizedActionSkip,
+					len:                8,
+					raw:                []byte{0x00, 0x00, 0x00, 0x01},
+				},
+				edmid: 1,
+			},
+		},
+	}
+
+	for i, tc := range tt {
+		t.Run(fmt.Sprintf("%v", i), func(t *testing.T) {
+			actual := &paramZeroChecksumAcceptable{}
+			_, err := actual.unmarshal(tc.binary)
+			assert.NoError(t, err)
+			assert.Equal(t, tc.parsed, actual)
+			b, err := actual.marshal()
+			assert.NoError(t, err)
+			assert.Equal(t, tc.binary, b)
+		})
+	}
+}
diff --git a/vnet_test.go b/vnet_test.go
index edbf654..6f225bb 100644
--- a/vnet_test.go
+++ b/vnet_test.go
@@ -76,7 +76,7 @@ func buildVNetEnv(cfg *vNetEnvConfig) (*vNetEnv, error) {
 		return func(c vnet.Chunk) bool {
 			var toDrop bool
 			p := &packet{}
-			if err2 := p.unmarshal(c.UserData()); err2 != nil {
+			if err2 := p.unmarshal(true, c.UserData()); err2 != nil {
 				panic(errSCTPPacketParse)
 			}