Search code examples
webrtcbackendcommunicationp2p

Does WebRTC work with symmetric NAT while both peers are in the same local network?


Should peers be able to connect via WebRTC while they are on the same network, but behind a router with symmetric NAT? In this case there is no need for the ICE server, so there should be no problem right?

I have two peers that are on the same machine trying to connect to each other and the connection fails, but it's my first time trying to do something with WebRTC so my implementation can be flawed :P

Offering Peer in Go:

package main

import (
    "fmt"
    "io"
    "net/http"
    "strconv"

    "github.com/pion/webrtc/v4"
)

func main() {
    answerC := HTTPSDPServer(8000) // feeds sdp into

    // Create a new RTCPeerConnection
    peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{})
    if err != nil {
        panic(err)
    }
    defer func() {
        if cErr := peerConnection.Close(); cErr != nil {
            fmt.Printf("cannot close peerConnection: %v\n", cErr)
        }
    }()

    peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
        fmt.Printf("Peer Connection State has changed: %s\n", s.String())
    })

    c, err := peerConnection.CreateDataChannel("channel", nil)
    if err != nil {
        panic(err)
    }

    c.OnOpen(func() {
        fmt.Println("Data Channel OPEN")
    })
    c.OnClose(func() {
        fmt.Println("Data Channel CLOSE")
    })

    offer, err := peerConnection.CreateOffer(nil)
    if err != nil {
        panic(err)
    }

    err = peerConnection.SetLocalDescription(offer)
    if err != nil {
        panic(err)
    }

    fmt.Println(strconv.Quote(offer.SDP)) // Copy this to frontend app

    answerStr := <-answerC
    answer := webrtc.SessionDescription{
        Type: webrtc.SDPTypeAnswer,
        SDP:  answerStr,
    }

    err = peerConnection.SetRemoteDescription(answer)
    if err != nil {
        panic(err)
    }

    select {}
}

func HTTPSDPServer(port int) chan string {
    sdpChan := make(chan string)
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        body, _ := io.ReadAll(r.Body)
        fmt.Fprintf(w, "done")
        sdpChan <- string(body)
    })

    go func() {
        // nolint: gosec
        err := http.ListenAndServe(":"+strconv.Itoa(port), nil)
        if err != nil {
            panic(err)
        }
    }()

    return sdpChan
}

Vue frontend which runs this script on launch:

const remoteConnection = new RTCPeerConnection()

remoteConnection.onicecandidate = e => {
    console.log(" NEW ice candidnat!! on localconnection reprinting SDP ")
    console.log(JSON.stringify(remoteConnection.localDescription))
}


remoteConnection.ondatachannel = e => {
    const receiveChannel = e.channel;
    receiveChannel.onmessage = e => console.log("message: " + e.data)
    receiveChannel.onopen = e => console.log("Data Channel OPEN");
    receiveChannel.onclose = e => console.log("Data Channel CLOSED");
    remoteConnection.channel = receiveChannel;

}

var sdp = "PASTE OFFER HERE" // Paste SDP copied from the offering peer
var offer = { "type": "offer", "sdp": sdp }

remoteConnection.setRemoteDescription(offer).then(a => console.log("done"))

//create answer
await remoteConnection.createAnswer().then(a => remoteConnection.setLocalDescription(a)).then(a =>
    console.log(JSON.stringify(remoteConnection.localDescription)))

After the frontend app prints it's sdp it's sent in a request to offering peer's server to unblock it and proceed. There is no error, the connection just goes from "connecting" to "failed".


Solution

  • Turn's out I was handling offer creation wrong. I created an offer, set it as local description and then used it directly

        offer, err := peerConnection.CreateOffer(nil)
        if err != nil {
            panic(err)
        }
    
        err = peerConnection.SetLocalDescription(offer)
        if err != nil {
            panic(err)
        }
    
    // then I referenced offer.SPD to get the SPD.
    

    What I needed to do was to call webrtc.GatheringCompletePromise(peerConnection) and wait until it finishes and then use peerConnection.LocalDescription().SPD instead of offer.SPD

    Here is a full example

        offer, err := peerConnection.CreateOffer(nil)
        if err != nil {
            panic(err)
        }
    
        // Create channel that is blocked until ICE Gathering is complete
        gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
    
        err = peerConnection.SetLocalDescription(offer)
        if err != nil {
            panic(err)
        }
    
        <-gatherComplete
    
    // get SPD from peerConnection.LocalDescription().SPD