Repository: pion/webrtc
Pull request #2376 (2022-12-22 20:39:01)

Fix negotiation needed panic on nil Sendonly track
In step 5.3.1 (https://www.w3.org/TR/webrtc/#dfn-update-the-negotiation-needed-flag), since we do not have a sender we can only choose to signal that we need negotiation. This typically happens in a case where the offerer asks for a recv only, but the answering side does not provide it. This only triggers if an on negotiation needed handler is set.

I believe the fix I applied is mostly correct by interpreting 5.3.1 and how in the Google WebRTC code, a non-nil sender is created during SetRemoteDescription. My only concern with the fix is that this would basically rely on the handler of negotiation needed to determine they need to go add that track. If they don't you can imagine an endless loop between offerer and answer.

@Sean-Der, while working on this, I realized that `SetRemoteDescription` may be non-compliant due to https://www.rfc-editor.org/rfc/rfc8829#section-5.10-6.6.2.2.2.2. However, if I make us compliant by removing https://github.com/pion/webrtc/blob/1c20cd4d7b48d2ab8f166486730eb2475f3f8333/peerconnection.go#L1084-L1088, the newly added test fails. Not sure how to reconcile that one as my knowledge & reasoning stops there :).

codecov[bot]: # [Codecov](https://codecov.io/gh/pion/webrtc/pull/2376?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) Report
Base: **77.64**% // Head: **79.38**% // Increases project coverage by **`+1.73%`** :tada:
> Coverage data is based on head [(`d408a4d`)](https://codecov.io/gh/pion/webrtc/pull/2376?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) compared to base [(`1c20cd4`)](https://codecov.io/gh/pion/webrtc/commit/1c20cd4d7b48d2ab8f166486730eb2475f3f8333?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion).
> Patch coverage: 100.00% of modified lines in pull request are covered.

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


```diff
@@            Coverage Diff             @@
##           master    #2376      +/-   ##
==========================================
+ Coverage   77.64%   79.38%   +1.73%     
==========================================
  Files          87       76      -11     
  Lines        9275     8382     -893     
==========================================
- Hits         7202     6654     -548     
+ Misses       1640     1340     -300     
+ Partials      433      388      -45     
```

| Flag | Coverage Δ | |
|---|---|---|
| go | `79.38% <100.00%> (-0.05%)` | :arrow_down: |
| wasm | `?` | |

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://codecov.io/gh/pion/webrtc/pull/2376?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | Coverage Δ | |
|---|---|---|
| [peerconnection.go](https://codecov.io/gh/pion/webrtc/pull/2376/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGVlcmNvbm5lY3Rpb24uZ28=) | `78.57% <100.00%> (+0.45%)` | :arrow_up: |
| [sctptransport.go](https://codecov.io/gh/pion/webrtc/pull/2376/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-c2N0cHRyYW5zcG9ydC5nbw==) | `77.15% <0.00%> (-3.38%)` | :arrow_down: |
| [dtlstransport.go](https://codecov.io/gh/pion/webrtc/pull/2376/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-ZHRsc3RyYW5zcG9ydC5nbw==) | `63.04% <0.00%> (-1.87%)` | :arrow_down: |
| [sctptransport\_js.go](https://codecov.io/gh/pion/webrtc/pull/2376/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-c2N0cHRyYW5zcG9ydF9qcy5nbw==) | | |
| [api\_js.go](https://codecov.io/gh/pion/webrtc/pull/2376/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-YXBpX2pzLmdv) | | |
| [peerconnection\_js.go](https://codecov.io/gh/pion/webrtc/pull/2376/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-cGVlcmNvbm5lY3Rpb25fanMuZ28=) | | |
| [iceserver\_js.go](https://codecov.io/gh/pion/webrtc/pull/2376/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-aWNlc2VydmVyX2pzLmdv) | | |
| [dtlstransport\_js.go](https://codecov.io/gh/pion/webrtc/pull/2376/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-ZHRsc3RyYW5zcG9ydF9qcy5nbw==) | | |
| [js\_utils.go](https://codecov.io/gh/pion/webrtc/pull/2376/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-anNfdXRpbHMuZ28=) | | |
| [icetransport\_js.go](https://codecov.io/gh/pion/webrtc/pull/2376/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion#diff-aWNldHJhbnNwb3J0X2pzLmdv) | | |
| ... and [6 more](https://codecov.io/gh/pion/webrtc/pull/2376/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pion)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/pion/webrtc/pull/2376?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).

edaniels: Also not sure why this fails with WASM but maybe that points to something.
yurifedoseev: @edaniels thank you for the PR! I was looking for solving the panic problem too. Could you update your branch to make it actual with the base branch?
yurifedoseev: Hi @Sean-Der, could you help to review this MR - to solve panic in pion? 

Patch:
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 89602e87..5bc6acc4 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -16,11 +16,13 @@ Aleksandr Razumov <ar@gortc.io>
 aler9 <46489434+aler9@users.noreply.github.com>
 Alex Browne <stephenalexbrowne@gmail.com>
 Alex Harford <harford@gmail.com>
+Alexey Khit <alexey.khit@gmail.com>
 AlexWoo(武杰) <wj19840501@gmail.com>
 Ali Error <alipadida@live.com>
 Andrew N. Shalaev <isqad88@gmail.com>
 Antoine Baché <antoine.bache@epitech.eu>
 Antoine Baché <antoine@tenten.app>
+Anton <leonardspbox@gmail.com>
 Artur Shellunts <shellunts.artur@gmail.com>
 Assad Obaid <assad@lap5cg901003r.se.axis.com>
 Ato Araki <ato.araki@gmail.com>
@@ -110,6 +112,7 @@ Luke <luke@street.dev>
 Luke Curley <kixelated@gmail.com>
 Luke S <luke@street.dev>
 Magnus Wahlstrand <magnus.wahlstrand@gmail.com>
+Manish <itzmanish108@gmail.com>
 Markus Tzoe <chou.marcus@gmail.com>
 Marouane <6729798+nindolabs@users.noreply.github.com>
 Marouane <marouane@gamestream.biz>
@@ -123,6 +126,7 @@ Michiel De Backker <38858977+backkem@users.noreply.github.com>
 Mike Coleman <mc@fivebats.com>
 Mindgamesnl <matsmoolhuizen@gmail.com>
 mission-liao <missionaryliao@gmail.com>
+mohammadne <mohammadne@mail.ru>
 mr-shitij <21.shitijagrawal@gmail.com>
 mxmCherry <mxmCherry@gmail.com>
 Nam V. Do <vannam12a7@gmail.com>
@@ -173,6 +177,7 @@ Slugalisk <slugalisk@gmail.com>
 Somers Matthews <somersbmatthews@gmail.com>
 soolaugust <soolaugust@gmail.com>
 spaceCh1mp <drimboat@gmail.com>
+Steffen Vogel <post@steffenvogel.de>
 stephanrotolante <stephanrotolante@gmail.com>
 Suhas Gaddam <suhas.g.2011@gmail.com>
 Suzuki Takeo <takeo@stko.info>
@@ -188,6 +193,7 @@ Vicken Simonian <vsimon@gmail.com>
 wattanakorn495 <wattanakorn.i@ku.th>
 Will Forcey <wsforc3y@gmail.com>
 Will Watson <william.a.watson@gmail.com>
+WofWca <wofwca@protonmail.com>
 Woodrow Douglass <wdouglass@carnegierobotics.com>
 xsbchen <xsbchen@qq.com>
 Yoon SeungYong <simon.y@hpcnt.com>
diff --git a/dtlstransport.go b/dtlstransport.go
index 22a327b7..2f6a7d1b 100644
--- a/dtlstransport.go
+++ b/dtlstransport.go
@@ -142,10 +142,7 @@ func (t *DTLSTransport) WriteRTCP(pkts []rtcp.Packet) (int, error) {
 		return 0, fmt.Errorf("%w: %v", errPeerConnWriteRTCPOpenWriteStream, err)
 	}
 
-	if n, err := writeStream.Write(raw); err != nil {
-		return n, err
-	}
-	return 0, nil
+	return writeStream.Write(raw)
 }
 
 // GetLocalParameters returns the DTLS parameters of the local DTLSTransport upon construction.
@@ -327,9 +324,9 @@ func (t *DTLSTransport) Start(remoteParameters DTLSParameters) error {
 		dtlsConfig.ReplayProtectionWindow = int(*t.api.settingEngine.replayProtection.DTLS)
 	}
 
-	if t.api.settingEngine.dtls.retransmissionInterval != 0 {
-		dtlsConfig.FlightInterval = t.api.settingEngine.dtls.retransmissionInterval
-	}
+	dtlsConfig.FlightInterval = t.api.settingEngine.dtls.retransmissionInterval
+	dtlsConfig.InsecureSkipVerifyHello = t.api.settingEngine.dtls.insecureSkipHelloVerify
+	dtlsConfig.EllipticCurves = t.api.settingEngine.dtls.ellipticCurves
 
 	// Connect as DTLS Client/Server, function is blocking and we
 	// must not hold the DTLSTransport lock
diff --git a/examples/data-channels/README.md b/examples/data-channels/README.md
index 49b9130b..0caf792f 100644
--- a/examples/data-channels/README.md
+++ b/examples/data-channels/README.md
@@ -30,3 +30,40 @@ Now you can put whatever you want in the `Message` textarea, and when you hit `S
 Pion WebRTC will send random messages every 5 seconds that will appear in your browser.
 
 Congrats, you have used Pion WebRTC! Now start building something cool
+
+## Architecture
+
+```mermaid
+flowchart TB
+    Browser--Copy Offer from TextArea-->Pion
+    Pion--Copy Text Print to Console-->Browser
+    subgraph Pion[Go Peer]
+        p1[Create PeerConnection]
+        p2[OnConnectionState Handler]
+        p3[Print Connection State]
+        p2-->p3
+        p4[OnDataChannel Handler]
+        p5[OnDataChannel Open]
+        p6[Send Random Message every 5 seconds to DataChannel]
+        p4-->p5-->p6
+        p7[OnDataChannel Message]
+        p8[Log Incoming Message to Console]
+        p4-->p7-->p8
+        p9[Read Session Description from Standard Input]
+        p10[SetRemoteDescription with Session Description from Standard Input]
+        p11[Create Answer]
+        p12[Block until ICE Gathering is Complete]
+        p13[Print Answer with ICE Candidatens included to Standard Output]
+    end
+    subgraph Browser[Browser Peer]
+        b1[Create PeerConnection]
+        b2[Create DataChannel 'foo']
+        b3[OnDataChannel Message]
+        b4[Log Incoming Message to Console]
+        b3-->b4
+        b5[Create Offer]
+        b6[SetLocalDescription with Offer]
+        b7[Print Offer with ICE Candidates included]
+
+    end
+```
diff --git a/go.mod b/go.mod
index dbb5f9ee..8f37abe6 100644
--- a/go.mod
+++ b/go.mod
@@ -6,8 +6,8 @@ require (
 	github.com/onsi/ginkgo v1.16.5 // indirect
 	github.com/onsi/gomega v1.17.0 // indirect
 	github.com/pion/datachannel v1.5.5
-	github.com/pion/dtls/v2 v2.2.4
-	github.com/pion/ice/v2 v2.3.0
+	github.com/pion/dtls/v2 v2.2.6
+	github.com/pion/ice/v2 v2.3.1
 	github.com/pion/interceptor v0.1.12
 	github.com/pion/logging v0.2.2
 	github.com/pion/randutil v0.1.0
@@ -16,9 +16,9 @@ require (
 	github.com/pion/sctp v1.8.6
 	github.com/pion/sdp/v3 v3.0.6
 	github.com/pion/srtp/v2 v2.0.12
-	github.com/pion/transport/v2 v2.0.1
+	github.com/pion/transport/v2 v2.0.2
 	github.com/sclevine/agouti v3.0.0+incompatible
-	github.com/stretchr/testify v1.8.1
+	github.com/stretchr/testify v1.8.2
 	golang.org/x/crypto v0.6.0 // indirect
-	golang.org/x/net v0.6.0
+	golang.org/x/net v0.7.0
 )
diff --git a/go.sum b/go.sum
index acc92444..78f5e3fb 100644
--- a/go.sum
+++ b/go.sum
@@ -42,10 +42,10 @@ github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
 github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
-github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=
-github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
-github.com/pion/ice/v2 v2.3.0 h1:G+ysriabk1p9wbySDpdsnlD+6ZspLlDLagRduRfzJPk=
-github.com/pion/ice/v2 v2.3.0/go.mod h1:+xO/cXVnnVUr6D2ZJcCT5g9LngucUkkTvfnTMqUxKRM=
+github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
+github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
+github.com/pion/ice/v2 v2.3.1 h1:FQCmUfZe2Jpe7LYStVBOP6z1DiSzbIateih3TztgTjc=
+github.com/pion/ice/v2 v2.3.1/go.mod h1:aq2kc6MtYNcn4XmMhobAv6hTNJiHzvD0yXRz80+bnP8=
 github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
 github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
@@ -70,12 +70,12 @@ github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wC
 github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
 github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
 github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
-github.com/pion/transport/v2 v2.0.1 h1:cbSk3gzoSBEIKVGNYeghXQSp47s1H9ttoP5JyLCgxLE=
-github.com/pion/transport/v2 v2.0.1/go.mod h1:93OYg91+mrGxKW+Jrgzmqr80kgXqD7J0yybOrdr7w0Y=
+github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
+github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
 github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
 github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
-github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
-github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
+github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
+github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/sclevine/agouti v3.0.0+incompatible h1:8IBJS6PWz3uTlMP3YBIR5f+KAldcGuOeFkFbUWfBgK4=
@@ -86,8 +86,9 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -109,8 +110,9 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
-golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/peerconnection.go b/peerconnection.go
index 0b4c300e..5cfaf85b 100644
--- a/peerconnection.go
+++ b/peerconnection.go
@@ -392,7 +392,11 @@ func (pc *PeerConnection) checkNegotiationNeeded() bool { //nolint:gocognit
 			// Step 5.3.1
 			if t.Direction() == RTPTransceiverDirectionSendrecv || t.Direction() == RTPTransceiverDirectionSendonly {
 				descMsid, okMsid := m.Attribute(sdp.AttrKeyMsid)
-				track := t.Sender().Track()
+				sender := t.Sender()
+				if sender == nil {
+					return true
+				}
+				track := sender.Track()
 				if !okMsid || descMsid != track.StreamID()+" "+track.ID() {
 					return true
 				}
@@ -744,15 +748,23 @@ func (pc *PeerConnection) updateConnectionState(iceConnectionState ICEConnection
 	case iceConnectionState == ICEConnectionStateDisconnected:
 		connectionState = PeerConnectionStateDisconnected
 
-	// All RTCIceTransports and RTCDtlsTransports are in the "connected", "completed" or "closed"
-	// state and at least one of them is in the "connected" or "completed" state.
-	case iceConnectionState == ICEConnectionStateConnected && dtlsTransportState == DTLSTransportStateConnected:
-		connectionState = PeerConnectionStateConnected
+	// None of the previous states apply and all RTCIceTransports are in the "new" or "closed" state,
+	// and all RTCDtlsTransports are in the "new" or "closed" state, or there are no transports.
+	case (iceConnectionState == ICEConnectionStateNew || iceConnectionState == ICEConnectionStateClosed) &&
+		(dtlsTransportState == DTLSTransportStateNew || dtlsTransportState == DTLSTransportStateClosed):
+		connectionState = PeerConnectionStateNew
 
-	//  Any of the RTCIceTransports or RTCDtlsTransports are in the "connecting" or
-	// "checking" state and none of them is in the "failed" state.
-	case iceConnectionState == ICEConnectionStateChecking && dtlsTransportState == DTLSTransportStateConnecting:
+	// None of the previous states apply and any RTCIceTransport is in the "new" or "checking" state or
+	// any RTCDtlsTransport is in the "new" or "connecting" state.
+	case (iceConnectionState == ICEConnectionStateNew || iceConnectionState == ICEConnectionStateChecking) ||
+		(dtlsTransportState == DTLSTransportStateNew || dtlsTransportState == DTLSTransportStateConnecting):
 		connectionState = PeerConnectionStateConnecting
+
+	// All RTCIceTransports and RTCDtlsTransports are in the "connected", "completed" or "closed"
+	// state and all RTCDtlsTransports are in the "connected" or "closed" state.
+	case (iceConnectionState == ICEConnectionStateConnected || iceConnectionState == ICEConnectionStateCompleted || iceConnectionState == ICEConnectionStateClosed) &&
+		(dtlsTransportState == DTLSTransportStateConnected || dtlsTransportState == DTLSTransportStateClosed):
+		connectionState = PeerConnectionStateConnected
 	}
 
 	if pc.connectionState.Load() == connectionState {
diff --git a/rtptransceiver.go b/rtptransceiver.go
index f94b9ed7..2d2c6d78 100644
--- a/rtptransceiver.go
+++ b/rtptransceiver.go
@@ -239,9 +239,9 @@ func satisfyTypeAndDirection(remoteKind RTPCodecType, remoteDirection RTPTransce
 	getPreferredDirections := func() []RTPTransceiverDirection {
 		switch remoteDirection {
 		case RTPTransceiverDirectionSendrecv:
-			return []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendrecv}
+			return []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendrecv, RTPTransceiverDirectionSendonly}
 		case RTPTransceiverDirectionSendonly:
-			return []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly}
+			return []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly, RTPTransceiverDirectionSendrecv}
 		case RTPTransceiverDirectionRecvonly:
 			return []RTPTransceiverDirection{RTPTransceiverDirectionSendonly, RTPTransceiverDirectionSendrecv}
 		default:
diff --git a/settingengine.go b/settingengine.go
index 7eee7b01..1466bb90 100644
--- a/settingengine.go
+++ b/settingengine.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/pion/dtls/v2"
+	dtlsElliptic "github.com/pion/dtls/v2/pkg/crypto/elliptic"
 	"github.com/pion/ice/v2"
 	"github.com/pion/logging"
 	"github.com/pion/transport/v2"
@@ -56,7 +57,9 @@ type SettingEngine struct {
 		SRTCP *uint
 	}
 	dtls struct {
-		retransmissionInterval time.Duration
+		insecureSkipHelloVerify bool
+		retransmissionInterval  time.Duration
+		ellipticCurves          []dtlsElliptic.Curve
 	}
 	sctp struct {
 		maxReceiveBufferSize uint32
@@ -100,9 +103,18 @@ func (e *SettingEngine) SetSRTPProtectionProfiles(profiles ...dtls.SRTPProtectio
 }
 
 // SetICETimeouts sets the behavior around ICE Timeouts
-// * disconnectedTimeout is the duration without network activity before an Agent is considered disconnected. Default is 5 Seconds
-// * failedTimeout is the duration without network activity before an Agent is considered failed after disconnected. Default is 25 Seconds
-// * keepAliveInterval is how often the ICE Agent sends extra traffic if there is no activity, if media is flowing no traffic will be sent. Default is 2 seconds
+//
+// disconnectedTimeout:
+//
+//	Duration without network activity before an Agent is considered disconnected. Default is 5 Seconds
+//
+// failedTimeout:
+//
+//	Duration without network activity before an Agent is considered failed after disconnected. Default is 25 Seconds
+//
+// keepAliveInterval:
+//
+//	How often the ICE Agent sends extra traffic if there is no activity, if media is flowing no traffic will be sent. Default is 2 seconds
 func (e *SettingEngine) SetICETimeouts(disconnectedTimeout, failedTimeout, keepAliveInterval time.Duration) {
 	e.timeout.ICEDisconnectedTimeout = &disconnectedTimeout
 	e.timeout.ICEFailedTimeout = &failedTimeout
@@ -171,7 +183,7 @@ func (e *SettingEngine) SetIPFilter(filter func(net.IP) bool) {
 
 // SetNAT1To1IPs sets a list of external IP addresses of 1:1 (D)NAT
 // and a candidate type for which the external IP address is used.
-// This is useful when you are host a server using Pion on an AWS EC2 instance
+// This is useful when you host a server using Pion on an AWS EC2 instance
 // which has a private address, behind a 1:1 DNAT with a public IP (e.g.
 // Elastic IP). In this case, you can give the public IP address so that
 // Pion will use the public IP address in its candidate instead of the private
@@ -185,9 +197,7 @@ func (e *SettingEngine) SetIPFilter(filter func(net.IP) bool) {
 //
 // ICECandidateTypeSrflx:
 //
-//	A server reflexive candidate with the given public IP address will be added
-//
-// to the SDP.
+//	A server reflexive candidate with the given public IP address will be added to the SDP.
 //
 // Please note that if you choose ICECandidateTypeHost, then the private IP address
 // won't be advertised with the peer. Also, this option cannot be used along with mDNS.
@@ -342,6 +352,19 @@ func (e *SettingEngine) SetDTLSRetransmissionInterval(interval time.Duration) {
 	e.dtls.retransmissionInterval = interval
 }
 
+// SetDTLSInsecureSkipHelloVerify sets the skip HelloVerify flag for DTLS.
+// If true and when acting as DTLS server, will allow client to skip hello verify phase and
+// receive ServerHello after initial ClientHello. This will mean faster connect times,
+// but will have lower DoS attack resistance.
+func (e *SettingEngine) SetDTLSInsecureSkipHelloVerify(skip bool) {
+	e.dtls.insecureSkipHelloVerify = skip
+}
+
+// SetDTLSEllipticCurves sets the elliptic curves for DTLS.
+func (e *SettingEngine) SetDTLSEllipticCurves(ellipticCurves ...dtlsElliptic.Curve) {
+	e.dtls.ellipticCurves = ellipticCurves
+}
+
 // SetSCTPMaxReceiveBufferSize sets the maximum receive buffer size.
 // Leave this 0 for the default maxReceiveBufferSize.
 func (e *SettingEngine) SetSCTPMaxReceiveBufferSize(maxReceiveBufferSize uint32) {


Test patch:
diff --git a/peerconnection_go_test.go b/peerconnection_go_test.go
index 010f1eae..a69a456f 100644
--- a/peerconnection_go_test.go
+++ b/peerconnection_go_test.go
@@ -1247,7 +1247,7 @@ func TestPeerConnection_TransceiverDirection(t *testing.T) {
 			"offer sendonly answer sendrecv",
 			RTPTransceiverDirectionSendonly,
 			RTPTransceiverDirectionSendrecv,
-			[]RTPTransceiverDirection{RTPTransceiverDirectionSendrecv, RTPTransceiverDirectionRecvonly},
+			[]RTPTransceiverDirection{RTPTransceiverDirectionSendrecv},
 		},
 		{
 			"offer recvonly answer sendrecv",
@@ -1259,7 +1259,7 @@ func TestPeerConnection_TransceiverDirection(t *testing.T) {
 			"offer sendrecv answer sendonly",
 			RTPTransceiverDirectionSendrecv,
 			RTPTransceiverDirectionSendonly,
-			[]RTPTransceiverDirection{RTPTransceiverDirectionSendonly, RTPTransceiverDirectionRecvonly},
+			[]RTPTransceiverDirection{RTPTransceiverDirectionSendrecv},
 		},
 		{
 			"offer sendonly answer sendonly",
@@ -1584,3 +1584,39 @@ a=ssrc:1455629982 cname:{61fd3093-0326-4b12-8258-86bdc1fe677a}
 
 	assert.NoError(t, peerConnection.Close())
 }
+
+func TestPeerConnectionState(t *testing.T) {
+	pc, err := NewPeerConnection(Configuration{})
+	assert.NoError(t, err)
+	assert.Equal(t, PeerConnectionStateNew, pc.ConnectionState())
+
+	pc.updateConnectionState(ICEConnectionStateChecking, DTLSTransportStateNew)
+	assert.Equal(t, PeerConnectionStateConnecting, pc.ConnectionState())
+
+	pc.updateConnectionState(ICEConnectionStateConnected, DTLSTransportStateNew)
+	assert.Equal(t, PeerConnectionStateConnecting, pc.ConnectionState())
+
+	pc.updateConnectionState(ICEConnectionStateConnected, DTLSTransportStateConnecting)
+	assert.Equal(t, PeerConnectionStateConnecting, pc.ConnectionState())
+
+	pc.updateConnectionState(ICEConnectionStateConnected, DTLSTransportStateConnected)
+	assert.Equal(t, PeerConnectionStateConnected, pc.ConnectionState())
+
+	pc.updateConnectionState(ICEConnectionStateCompleted, DTLSTransportStateConnected)
+	assert.Equal(t, PeerConnectionStateConnected, pc.ConnectionState())
+
+	pc.updateConnectionState(ICEConnectionStateConnected, DTLSTransportStateClosed)
+	assert.Equal(t, PeerConnectionStateConnected, pc.ConnectionState())
+
+	pc.updateConnectionState(ICEConnectionStateDisconnected, DTLSTransportStateConnected)
+	assert.Equal(t, PeerConnectionStateDisconnected, pc.ConnectionState())
+
+	pc.updateConnectionState(ICEConnectionStateFailed, DTLSTransportStateConnected)
+	assert.Equal(t, PeerConnectionStateFailed, pc.ConnectionState())
+
+	pc.updateConnectionState(ICEConnectionStateConnected, DTLSTransportStateFailed)
+	assert.Equal(t, PeerConnectionStateFailed, pc.ConnectionState())
+
+	assert.NoError(t, pc.Close())
+	assert.Equal(t, PeerConnectionStateClosed, pc.ConnectionState())
+}
diff --git a/peerconnection_test.go b/peerconnection_test.go
index 5a5dcd01..d8ff1347 100644
--- a/peerconnection_test.go
+++ b/peerconnection_test.go
@@ -1,8 +1,10 @@
 package webrtc
 
 import (
+	"context"
 	"reflect"
 	"sync"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -88,10 +90,11 @@ func untilConnectionState(state PeerConnectionState, peers ...*PeerConnection) *
 	triggered.Add(len(peers))
 
 	for _, p := range peers {
-		done := false
+		var done atomic.Value
+		done.Store(false)
 		hdlr := func(p PeerConnectionState) {
-			if !done && p == state {
-				done = true
+			if val, ok := done.Load().(bool); ok && (!val && p == state) {
+				done.Store(true)
 				triggered.Done()
 			}
 		}
@@ -749,3 +752,38 @@ func TestTransportChain(t *testing.T) {
 
 	closePairNow(t, offer, answer)
 }
+
+func TestNegotiationNeededWithRecvonlyTrack(t *testing.T) {
+	lim := test.TimeOut(time.Second * 30)
+	defer lim.Stop()
+
+	report := test.CheckRoutines(t)
+	defer report()
+
+	pcOffer, pcAnswer, err := newPair()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	pcAnswer.OnNegotiationNeeded(wg.Done)
+
+	_, err = pcOffer.AddTransceiverFromKind(RTPCodecTypeVideo, RTPTransceiverInit{Direction: RTPTransceiverDirectionRecvonly})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err := signalPair(pcOffer, pcAnswer); err != nil {
+		t.Fatal(err)
+	}
+
+	onDataChannel, onDataChannelCancel := context.WithCancel(context.Background())
+	pcAnswer.OnDataChannel(func(d *DataChannel) {
+		onDataChannelCancel()
+	})
+	<-onDataChannel.Done()
+	wg.Wait()
+
+	closePairNow(t, pcOffer, pcAnswer)
+}
diff --git a/settingengine_test.go b/settingengine_test.go
index 27858e8e..425c88c3 100644
--- a/settingengine_test.go
+++ b/settingengine_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/pion/dtls/v2/pkg/crypto/elliptic"
 	"github.com/pion/transport/v2/test"
 	"github.com/stretchr/testify/assert"
 )
@@ -234,6 +235,20 @@ func TestSetDTLSRetransmissionInterval(t *testing.T) {
 	}
 }
 
+func TestSetDTLSEllipticCurves(t *testing.T) {
+	s := SettingEngine{}
+
+	if len(s.dtls.ellipticCurves) != 0 {
+		t.Fatalf("SettingEngine defaults aren't as expected.")
+	}
+
+	s.SetDTLSEllipticCurves(elliptic.P256)
+	if len(s.dtls.ellipticCurves) == 0 ||
+		s.dtls.ellipticCurves[0] != elliptic.P256 {
+		t.Errorf("Failed to set DTLS elliptic curves")
+	}
+}
+
 func TestSetSCTPMaxReceiverBufferSize(t *testing.T) {
 	s := SettingEngine{}
 	assert.Equal(t, uint32(0), s.sctp.maxReceiveBufferSize)