diff --git a/go.mod b/go.mod index 57a3495e72..933c80763c 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,11 @@ require ( github.com/multiformats/go-multistream v0.4.1 github.com/multiformats/go-varint v0.0.7 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 + github.com/pion/datachannel v1.5.5 + github.com/pion/ice/v2 v2.2.13 + github.com/pion/logging v0.2.2 + github.com/pion/stun v0.4.0 + github.com/pion/webrtc/v3 v3.1.51 github.com/prometheus/client_golang v1.14.0 github.com/quic-go/quic-go v0.33.0 github.com/quic-go/webtransport-go v0.5.2 @@ -49,9 +54,9 @@ require ( github.com/stretchr/testify v1.8.1 go.uber.org/fx v1.18.2 go.uber.org/goleak v1.1.12 - golang.org/x/crypto v0.4.0 + golang.org/x/crypto v0.5.0 golang.org/x/sync v0.1.0 - golang.org/x/sys v0.3.0 + golang.org/x/sys v0.4.0 golang.org/x/tools v0.3.0 google.golang.org/protobuf v1.28.1 nhooyr.io/websocket v1.8.7 @@ -60,13 +65,12 @@ require ( require ( github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/containerd/cgroups v1.0.4 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgraph-io/badger v1.6.2 // indirect - github.com/dgraph-io/ristretto v0.0.2 // indirect + github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/elastic/gosigar v0.14.2 // indirect @@ -75,24 +79,38 @@ require ( github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.0.0 // indirect github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/pprof v0.0.0-20221203041831-ce31453925ec // indirect github.com/google/uuid v1.3.0 // indirect github.com/huin/goupnp v1.0.3 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/goprocess v0.1.4 // indirect - github.com/klauspost/cpuid/v2 v2.2.1 // indirect + github.com/klauspost/cpuid/v2 v2.2.3 // indirect github.com/koron/go-ssdp v0.0.3 // indirect + github.com/kr/pretty v0.3.0 // indirect github.com/libp2p/go-cidranger v1.1.0 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/miekg/dns v1.1.50 // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/ginkgo/v2 v2.5.1 // indirect github.com/opencontainers/runtime-spec v1.0.2 // indirect + github.com/pion/dtls/v2 v2.1.5 // indirect + github.com/pion/interceptor v0.1.12 // indirect + github.com/pion/mdns v0.0.5 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.10 // indirect + github.com/pion/rtp v1.7.13 // indirect + github.com/pion/sctp v1.8.6 // indirect + github.com/pion/sdp/v3 v3.0.6 // indirect + github.com/pion/srtp/v2 v2.0.11 // indirect + github.com/pion/transport v0.14.1 // indirect + github.com/pion/transport/v2 v2.0.0 // indirect + github.com/pion/turn/v2 v2.0.9 // indirect + github.com/pion/udp v0.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect @@ -102,15 +120,15 @@ require ( github.com/quic-go/qtls-go1-19 v0.2.1 // indirect github.com/quic-go/qtls-go1-20 v0.1.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/dig v1.15.0 // indirect - go.uber.org/multierr v1.8.0 // indirect + go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.7.0 // indirect - golang.org/x/net v0.4.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/net v0.5.0 // indirect + golang.org/x/text v0.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.1.7 // indirect ) diff --git a/go.sum b/go.sum index 18249c1eb2..999c33c947 100644 --- a/go.sum +++ b/go.sum @@ -42,7 +42,6 @@ github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIo github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -61,7 +60,6 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -86,6 +84,7 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -96,8 +95,9 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= -github.com/dgraph-io/ristretto v0.0.2 h1:a5WaUrDa0qm0YrAAS1tUykT5El3kt62KNZZeMxQn3po= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -165,6 +165,8 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -195,8 +197,9 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -294,8 +297,8 @@ github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kE github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.1 h1:U33DW0aiEj633gHYw3LoDNfkDiYnE5Q8M/TKJn2f2jI= -github.com/klauspost/cpuid/v2 v2.2.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk= @@ -304,12 +307,14 @@ github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= @@ -345,8 +350,8 @@ github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8 github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -411,6 +416,8 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.5.1 h1:auzK7OI497k6x4OvWq+TKAcpcSAlod0doAH72oIN0Jw= @@ -418,6 +425,7 @@ github.com/onsi/ginkgo/v2 v2.5.1/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjd github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg= github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= @@ -425,6 +433,49 @@ github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTm github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +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.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c= +github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY= +github.com/pion/ice/v2 v2.2.13 h1:NvLtzwcyob6wXgFqLmVQbGB3s9zzWmOegNMKYig5l9M= +github.com/pion/ice/v2 v2.2.13/go.mod h1:eFO4/1zCI+a3OFVt7l7kP+5jWCuZo8FwU2UwEa3+164= +github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8= +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= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= +github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo= +github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc= +github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= +github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= +github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= +github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI= +github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= +github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= +github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= +github.com/pion/srtp/v2 v2.0.11 h1:6cEEgT1oCLWgE+BynbfaSMAxtsqU0M096x9dNH6olY0= +github.com/pion/srtp/v2 v2.0.11/go.mod h1:vzHprzbuVoYJ9NfaRMycnFrkHcLSaLVuBZDOtFQNZjY= +github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= +github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk= +github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw= +github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= +github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g= +github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg= +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 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4= +github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= +github.com/pion/turn/v2 v2.0.9 h1:jcDPw0Vfd5I4iTc7s0Upfc2aMnyu2lgJ9vV0SUrNC1o= +github.com/pion/turn/v2 v2.0.9/go.mod h1:DQlwUwx7hL8Xya6TTAabbd9DdKXTNR96Xf5g5Qqso/M= +github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= +github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= +github.com/pion/webrtc/v3 v3.1.51 h1:uU9vHdY63O3uRFJiDskH0qFJ+219bAH28qOt5csSWcM= +github.com/pion/webrtc/v3 v3.1.51/go.mod h1:sbRNshM9l0zRDQgZRP9K5RTzlsdBmqmyO8KbxngG8jQ= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -473,8 +524,11 @@ github.com/quic-go/webtransport-go v0.5.2/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2Gk github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= @@ -522,13 +576,15 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -544,6 +600,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -563,8 +620,8 @@ go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= @@ -584,8 +641,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -621,6 +681,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -656,18 +717,29 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +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.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -689,6 +761,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -732,11 +805,13 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -752,12 +827,21 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -765,8 +849,10 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -822,6 +908,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/p2p/test/webrtc/webrtc_test.go b/p2p/test/webrtc/webrtc_test.go new file mode 100644 index 0000000000..29f0c1b7dc --- /dev/null +++ b/p2p/test/webrtc/webrtc_test.go @@ -0,0 +1,49 @@ +package webrtc_test + +import ( + "context" + "fmt" + "io" + "testing" + "time" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + libp2pwebrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc" + "github.com/stretchr/testify/require" +) + +func TestWebRTCStream(t *testing.T) { + h1, err := libp2p.New( + libp2p.Transport(libp2pwebrtc.New), + libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/webrtc"), + ) + require.NoError(t, err) + + const proto = "/testing" + h1.SetStreamHandler(proto, func(str network.Stream) { + data, err := io.ReadAll(str) + require.NoError(t, err) + fmt.Println("read:", string(data)) + }) + + h2, err := libp2p.New( + libp2p.Transport(libp2pwebrtc.New), + libp2p.NoListenAddrs, + ) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err = h2.Connect(ctx, peer.AddrInfo{ID: h1.ID(), Addrs: h1.Addrs()}) + require.NoError(t, err) + + str, err := h2.NewStream(ctx, h1.ID(), proto) + require.NoError(t, err) + defer str.Close() + + _, err = str.Write([]byte("foobar")) + require.NoError(t, err) +} diff --git a/p2p/transport/webrtc/connection.go b/p2p/transport/webrtc/connection.go new file mode 100644 index 0000000000..dd71d182d5 --- /dev/null +++ b/p2p/transport/webrtc/connection.go @@ -0,0 +1,369 @@ +package libp2pwebrtc + +import ( + "context" + "errors" + "fmt" + "math" + "math/rand" + "os" + "sync" + "sync/atomic" + + ic "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + tpt "github.com/libp2p/go-libp2p/core/transport" + pb "github.com/libp2p/go-libp2p/p2p/transport/webrtc/pb" + "github.com/libp2p/go-msgio" + ma "github.com/multiformats/go-multiaddr" + "github.com/pion/datachannel" + "github.com/pion/webrtc/v3" + "google.golang.org/protobuf/proto" +) + +var _ tpt.CapableConn = &connection{} + +const ( + maxAcceptQueueLen = 10 +) + +type acceptStream struct { + stream datachannel.ReadWriteCloser + channel *webrtc.DataChannel +} + +type connection struct { + // debug identifier for the connection + dbgId int + pc *webrtc.PeerConnection + transport tpt.Transport + scope network.ConnManagementScope + + localPeer peer.ID + localMultiaddr ma.Multiaddr + + remotePeer peer.ID + remoteKey ic.PubKey + remoteMultiaddr ma.Multiaddr + + m sync.Mutex + streams map[uint16]*webRTCStream + + acceptQueue chan acceptStream + idAllocator *sidAllocator + + ctx context.Context + cancel context.CancelFunc +} + +func NewWebRTCConnection( + direction network.Direction, + pc *webrtc.PeerConnection, + transport tpt.Transport, + scope network.ConnManagementScope, + + localPeer peer.ID, + localMultiaddr ma.Multiaddr, + + remotePeer peer.ID, + remoteKey ic.PubKey, + remoteMultiaddr ma.Multiaddr, +) (*connection, error) { + idAllocator := newSidAllocator(direction) + + ctx, cancel := context.WithCancel(context.Background()) + conn := &connection{ + dbgId: rand.Intn(65536), + pc: pc, + transport: transport, + scope: scope, + + localPeer: localPeer, + localMultiaddr: localMultiaddr, + + remotePeer: remotePeer, + remoteKey: remoteKey, + remoteMultiaddr: remoteMultiaddr, + ctx: ctx, + cancel: cancel, + streams: make(map[uint16]*webRTCStream), + idAllocator: idAllocator, + + acceptQueue: make(chan acceptStream, maxAcceptQueueLen), + } + + pc.OnConnectionStateChange(conn.onConnectionStateChange) + pc.OnDataChannel(func(dc *webrtc.DataChannel) { + if conn.IsClosed() { + return + } + dc.OnOpen(func() { + rwc, err := dc.Detach() + if err != nil { + log.Warnf("could not detach datachannel: id: %d", *dc.ID()) + return + } + select { + case conn.acceptQueue <- acceptStream{rwc, dc}: + default: + log.Warnf("connection busy, rejecting stream") + b, _ := proto.Marshal(&pb.Message{Flag: pb.Message_RESET.Enum()}) + w := msgio.NewWriter(rwc) + w.WriteMsg(b) + rwc.Close() + } + }) + }) + + return conn, nil +} + +func (c *connection) resetStreams() { + if c.IsClosed() { + return + } + c.m.Lock() + defer c.m.Unlock() + for k, stream := range c.streams { + // reset the streams, but we do not need to be notified + // of stream closure + stream.close(true, false) + delete(c.streams, k) + } + +} + +// ConnState implements transport.CapableConn +func (c *connection) ConnState() network.ConnectionState { + return network.ConnectionState{ + Transport: "webrtc", + } +} + +// Close closes the underlying peerconnection. +func (c *connection) Close() error { + if c.IsClosed() { + return nil + } + + c.scope.Done() + c.cancel() + return c.pc.Close() +} + +func (c *connection) IsClosed() bool { + select { + case <-c.ctx.Done(): + return true + default: + return false + } +} + +func (c *connection) OpenStream(ctx context.Context) (network.MuxedStream, error) { + if c.IsClosed() { + return nil, os.ErrClosed + } + + streamID, err := c.idAllocator.nextID() + if err != nil { + return nil, err + } + dc, err := c.pc.CreateDataChannel("", &webrtc.DataChannelInit{ID: streamID}) + if err != nil { + return nil, err + } + rwc, err := c.detachChannel(ctx, dc) + if err != nil { + return nil, fmt.Errorf("open stream: %w", err) + } + stream := newStream( + c, + dc, + rwc, + nil, + nil, + ) + c.addStream(stream) + return stream, nil +} + +func (c *connection) AcceptStream() (network.MuxedStream, error) { + select { + case <-c.ctx.Done(): + return nil, os.ErrClosed + case accStream := <-c.acceptQueue: + stream := newStream( + c, + accStream.channel, + accStream.stream, + nil, + nil, + ) + c.addStream(stream) + return stream, nil + } +} + +// implement network.ConnSecurity +func (c *connection) LocalPeer() peer.ID { + return c.localPeer +} + +func (c *connection) RemotePeer() peer.ID { + return c.remotePeer +} + +func (c *connection) RemotePublicKey() ic.PubKey { + return c.remoteKey +} + +// implement network.ConnMultiaddrs +func (c *connection) LocalMultiaddr() ma.Multiaddr { + return c.localMultiaddr +} + +func (c *connection) RemoteMultiaddr() ma.Multiaddr { + return c.remoteMultiaddr +} + +// implement network.ConnScoper +func (c *connection) Scope() network.ConnScope { + return c.scope +} + +func (c *connection) Transport() tpt.Transport { + return c.transport +} + +func (c *connection) addStream(stream *webRTCStream) { + c.m.Lock() + defer c.m.Unlock() + // This is a private function called from the same object only when a + // 1) new stream is initiated by the remote + // or 2) we create a new stream. + // In case 1 the stream ID is extracted from the incoming datachannel, + // so it is guaranteed to exist. In case 2, we get the id from the sidAllocator which already + // performs the odd even check, and addStream is only called after a datachannel is created with the new stream ID. + // + // Finally, since this function is called for both outgoing and incoming streams, + // we need not perform an odd-even check here since it has been done by the stream ID allocator. + // If such a check must be performed (and it should), it should be done when the remote creates a new datachannel. + // + // In both cases, the stream ID is guaranteed not to exist yet. + c.streams[stream.id] = stream +} + +func (c *connection) removeStream(id uint16) { + c.m.Lock() + defer c.m.Unlock() + delete(c.streams, id) +} + +func (c *connection) onConnectionStateChange(state webrtc.PeerConnectionState) { + log.Debugf("[%s][%d] handling peerconnection state: %s", c.localPeer, c.dbgId, state) + if state == webrtc.PeerConnectionStateFailed || state == webrtc.PeerConnectionStateClosed { + // reset any streams + c.resetStreams() + c.cancel() + c.scope.Done() + c.pc.Close() + } +} + +// detachChannel detaches an outgoing channel by taking into account the context +// passed to `OpenStream` as well the closure of the underlying peerconnection +// +// The underlying SCTP stream for a datachannel implements a net.Conn interface. +// However, the datachannel creates a goroutine which continuously reads from +// the SCTP stream and surfaces the data using an OnMessage callback. +// +// The actual abstractions are as follows: webrtc.DataChannel +// wraps pion.DataChannel, which wraps sctp.Stream. +// +// The goroutine for reading, Detach method, +// and the OnMessage callback are present at the webrtc.DataChannel level. +// Detach provides us abstracted access to the underlying pion.DataChannel, +// which allows us to issue Read calls to the datachannel. +// This was desired because it was not feasible to introduce backpressure +// with the OnMessage callbacks. The tradeoff is a change in the semantics of +// the OnOpen callback, and having to force close Read locally. +func (c *connection) detachChannel(ctx context.Context, dc *webrtc.DataChannel) (rwc datachannel.ReadWriteCloser, err error) { + done := make(chan struct{}) + // OnOpen will return immediately for detached datachannels + // refer: https://github.com/pion/webrtc/blob/7ab3174640b3ce15abebc2516a2ca3939b5f105f/datachannel.go#L278-L282 + dc.OnOpen(func() { + rwc, err = dc.Detach() + // this is safe since the function should return instantly if the peerconnection is closed + close(done) + }) + select { + case <-c.ctx.Done(): + return nil, errors.New("connection closed") + case <-ctx.Done(): + return nil, ctx.Err() + case <-done: + } + return +} + +// A note on these setters and why they are needed: +// +// The connection object sets up receiving datachannels (streams) from the remote peer. +// Please consider the XX noise handshake pattern from a peer A to peer B as described at: +// https://noiseexplorer.com/patterns/XX/ +// +// The initiator A completes the noise handshake before B. +// This would allow A to create new datachannels before B has set up the callbacks to process incoming datachannels. +// This would create a situation where A has successfully created a stream but B is not aware of it. +// Moving the construction of the connection object before the noise handshake eliminates this issue, +// as callbacks have been set up for both peers. +// +// This could lead to a case where streams are created during the noise handshake, +// and the handshake fails. In this case, we would close the underlying peerconnection. + +// only used during connection setup +func (c *connection) setRemotePeer(id peer.ID) { + c.remotePeer = id +} + +// only used during connection setup +func (c *connection) setRemotePublicKey(key ic.PubKey) { + c.remoteKey = key +} + +// sidAllocator is a helper struct to allocate stream IDs for datachannels. ID +// reuse is not currently implemented. This prevents streams in pion from hanging +// with `invalid DCEP message` errors. +// The id is picked using the scheme described in: +// https://datatracker.ietf.org/doc/html/draft-ietf-rtcweb-data-channel-08#section-6.5 +// By definition, the DTLS role for inbound connections is set to DTLS Server, +// and outbound connections are DTLS Client. +type sidAllocator struct { + n atomic.Uint32 +} + +func newSidAllocator(direction network.Direction) *sidAllocator { + switch direction { + case network.DirInbound: + // server will use odd values + a := new(sidAllocator) + a.n.Store(1) + return a + case network.DirOutbound: + // client will use even values + return new(sidAllocator) + default: + panic(fmt.Sprintf("create SID allocator for direction: %s", direction)) + } +} + +func (a *sidAllocator) nextID() (*uint16, error) { + nxt := a.n.Add(2) + if nxt > math.MaxUint16 { + return nil, fmt.Errorf("sid exhausted") + } + result := uint16(nxt) + return &result, nil +} diff --git a/p2p/transport/webrtc/datachannel.go b/p2p/transport/webrtc/datachannel.go new file mode 100644 index 0000000000..aedca6d5b0 --- /dev/null +++ b/p2p/transport/webrtc/datachannel.go @@ -0,0 +1,28 @@ +package libp2pwebrtc + +import ( + "context" + + "github.com/pion/datachannel" + "github.com/pion/webrtc/v3" +) + +// only use this if the datachannels are detached, since the OnOpen callback +// will be called immediately. Only use after the peerconnection is open. +// The context should close if the peerconnection underlying the datachannel +// is closed. +func getDetachedChannel(ctx context.Context, dc *webrtc.DataChannel) (rwc datachannel.ReadWriteCloser, err error) { + done := make(chan struct{}) + dc.OnOpen(func() { + defer close(done) + rwc, err = dc.Detach() + }) + // this is safe since for detached datachannels, the peerconnection runs the onOpen + // callback immediately if the SCTP transport is also connected. + select { + case <-done: + case <-ctx.Done(): + return nil, ctx.Err() + } + return +} diff --git a/p2p/transport/webrtc/fetch_ip_linux_test.go b/p2p/transport/webrtc/fetch_ip_linux_test.go new file mode 100644 index 0000000000..8bfe033e13 --- /dev/null +++ b/p2p/transport/webrtc/fetch_ip_linux_test.go @@ -0,0 +1,10 @@ +//go:build linux +// +build linux + +package libp2pwebrtc + +import "net" + +func getListenerAndDialerIP() (net.IP, net.IP) { + return net.IPv4(0, 0, 0, 0), net.IPv4(127, 0, 0, 1) +} diff --git a/p2p/transport/webrtc/fetch_ip_test.go b/p2p/transport/webrtc/fetch_ip_test.go new file mode 100644 index 0000000000..62dde5f434 --- /dev/null +++ b/p2p/transport/webrtc/fetch_ip_test.go @@ -0,0 +1,40 @@ +//go:build !linux +// +build !linux + +package libp2pwebrtc + +import "net" + +// non-linux builds need to bind to a non-loopback interface +// to accept incoming connections. 0.0.0.0 does not work since +// Pion will bind to a local interface which is not loopback +// and there may not be a route from, say 192.168.0.0/16 to 0.0.0.0. + +func getListenerAndDialerIP() (listenerIp net.IP, dialerIp net.IP) { + listenerIp = net.IPv4(0, 0, 0, 0) + dialerIp = net.IPv4(0, 0, 0, 0) + ifaces, err := net.Interfaces() + if err != nil { + return + } + for _, iface := range ifaces { + log.Debugf("checking interface: %s", iface.Name) + if iface.Flags&net.FlagUp == 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + return + } + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.IsPrivate() { + if ipnet.IP.To4() != nil { + listenerIp = ipnet.IP.To4() + dialerIp = listenerIp + return + } + } + } + } + return +} diff --git a/p2p/transport/webrtc/internal/encoding/hex.go b/p2p/transport/webrtc/internal/encoding/hex.go new file mode 100644 index 0000000000..d53b3214ca --- /dev/null +++ b/p2p/transport/webrtc/internal/encoding/hex.go @@ -0,0 +1,155 @@ +package encoding + +import ( + "encoding/hex" + "errors" + "strings" +) + +// EncodeInterspersedHex encodes a byte slice into a string of hex characters, +// separating each encoded byte with a colon (':'). +// +// Example: { 0x01, 0x02, 0x03 } -> "01:02:03" +func EncodeInterspersedHex(src []byte) string { + var builder strings.Builder + EncodeInterspersedHexToBuilder(src, &builder) + return builder.String() +} + +// EncodeInterspersedHexToBuilder encodes a byte slice into a of hex characters, +// separating each encoded byte with a colon (':'). String is written to the builder. +// +// Example: { 0x01, 0x02, 0x03 } -> "01:02:03" +func EncodeInterspersedHexToBuilder(src []byte, builder *strings.Builder) { + if len(src) == 0 { + return + } + builder.Grow(len(src)*3 - 1) + v := src[0] + builder.WriteByte(hextable[v>>4]) + builder.WriteByte(hextable[v&0x0f]) + for _, v = range src[1:] { + builder.WriteByte(':') + builder.WriteByte(hextable[v>>4]) + builder.WriteByte(hextable[v&0x0f]) + } +} + +// DecodeInterspersedHex decodes a byte slice string of hex characters into a byte slice, +// where the hex characters are expected to be separated by a colon (':'). +// +// Example: {'0', '1', ':', '0', '2', ':', '0', '3'} -> { 0x01, 0x02, 0x03 } +func DecodeInterspersedHex(src []byte) ([]byte, error) { + if len(src) == 0 { + return []byte{}, nil + } + if len(src) < 2 { + return nil, hex.ErrLength + } + + dst := make([]byte, (len(src)+1)/3) + i, j := 0, 1 + for ; j < len(src); j += 3 { // jump one extra byte for the separator (:) + p := src[j-1] + q := src[j] + if j+1 < len(src) && src[j+1] != ':' { + return nil, errUnexpectedInterperseHexChar + } + + a := reverseHexTable[p] + b := reverseHexTable[q] + if a > 0x0f { + return nil, hex.InvalidByteError(p) + } + if b > 0x0f { + return nil, hex.InvalidByteError(q) + } + dst[i] = (a << 4) | b + i++ + } + if (len(src)+1)%3 != 0 { + if len(src)%3 == 0 { + j -= 1 + } + // Check for invalid char before reporting bad length, + // since the invalid char (if present) is an earlier problem. + if reverseHexTable[src[j-1]] > 0x0f { + return nil, hex.InvalidByteError(src[j-1]) + } + return nil, hex.ErrLength + } + return dst[:i], nil +} + +// DecodeInterpersedHexFromASCIIString decodes an ASCII string of hex characters into a byte slice, +// where the hex characters are expected to be separated by a colon (':'). +// +// NOTE that this function returns an error in case the input string contains non-ASCII characters. +// +// Example: "01:02:03" -> { 0x01, 0x02, 0x03 } +func DecodeInterpersedHexFromASCIIString(src string) ([]byte, error) { + if len(src) == 0 { + return []byte{}, nil + } + if len(src) < 2 { + return nil, hex.ErrLength + } + + dst := make([]byte, (len(src)+1)/3) + i, j := 0, 1 + for ; j < len(src); j += 3 { // jump one extra byte for the separator (:) + p := src[j-1] + q := src[j] + if j+1 < len(src) && src[j+1] != ':' { + return nil, errUnexpectedInterperseHexChar + } + + a := reverseHexTable[p] + b := reverseHexTable[q] + if a > 0x0f { + return nil, hex.InvalidByteError(p) + } + if b > 0x0f { + return nil, hex.InvalidByteError(q) + } + dst[i] = (a << 4) | b + i++ + } + if (len(src)+1)%3 != 0 { + if len(src)%3 == 0 { + j -= 1 + } + // Check for invalid char before reporting bad length, + // since the invalid char (if present) is an earlier problem. + if reverseHexTable[src[j-1]] > 0x0f { + return nil, hex.InvalidByteError(src[j-1]) + } + return nil, hex.ErrLength + } + return dst[:i], nil +} + +var ( + errUnexpectedInterperseHexChar = errors.New("unexpected character in interspersed hex string") +) + +const ( + hextable = "0123456789abcdef" + reverseHexTable = "" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\xff\xff\xff\xff\xff\xff" + + "\xff\x0a\x0b\x0c\x0d\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\x0a\x0b\x0c\x0d\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +) diff --git a/p2p/transport/webrtc/internal/encoding/hex_test.go b/p2p/transport/webrtc/internal/encoding/hex_test.go new file mode 100644 index 0000000000..71f83647c1 --- /dev/null +++ b/p2p/transport/webrtc/internal/encoding/hex_test.go @@ -0,0 +1,126 @@ +package encoding + +import ( + "encoding/hex" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEncodeInterspersedHex(t *testing.T) { + b, err := hex.DecodeString("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") + require.NoError(t, err) + require.Equal(t, "ba:78:16:bf:8f:01:cf:ea:41:41:40:de:5d:ae:22:23:b0:03:61:a3:96:17:7a:9c:b4:10:ff:61:f2:00:15:ad", EncodeInterspersedHex(b)) +} + +func TestEncodeInterspersedHexToBuilder(t *testing.T) { + b, err := hex.DecodeString("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") + require.NoError(t, err) + var builder strings.Builder + EncodeInterspersedHexToBuilder(b, &builder) + require.Equal(t, "ba:78:16:bf:8f:01:cf:ea:41:41:40:de:5d:ae:22:23:b0:03:61:a3:96:17:7a:9c:b4:10:ff:61:f2:00:15:ad", builder.String()) +} + +func TestDecodeInterpersedHexStringLowerCase(t *testing.T) { + b, err := DecodeInterpersedHexFromASCIIString("ba:78:16:bf:8f:01:cf:ea:41:41:40:de:5d:ae:22:23:b0:03:61:a3:96:17:7a:9c:b4:10:ff:61:f2:00:15:ad") + require.NoError(t, err) + require.Equal(t, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", hex.EncodeToString(b)) +} + +func TestDecodeInterpersedHexStringMixedCase(t *testing.T) { + b, err := DecodeInterpersedHexFromASCIIString("Ba:78:16:BF:8F:01:cf:ea:41:41:40:De:5d:ae:22:23:b0:03:61:a3:96:17:7a:9c:b4:10:FF:61:f2:00:15:ad") + require.NoError(t, err) + require.Equal(t, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", hex.EncodeToString(b)) +} + +func TestDecodeInterpersedHexStringOneByte(t *testing.T) { + b, err := DecodeInterpersedHexFromASCIIString("ba") + require.NoError(t, err) + require.Equal(t, "ba", hex.EncodeToString(b)) +} + +func TestDecodeInterpersedHexBytesLowerCase(t *testing.T) { + b, err := DecodeInterspersedHex([]byte("ba:78:16:bf:8f:01:cf:ea:41:41:40:de:5d:ae:22:23:b0:03:61:a3:96:17:7a:9c:b4:10:ff:61:f2:00:15:ad")) + require.NoError(t, err) + require.Equal(t, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", hex.EncodeToString(b)) +} + +func TestDecodeInterpersedHexBytesMixedCase(t *testing.T) { + b, err := DecodeInterspersedHex([]byte("Ba:78:16:BF:8F:01:cf:ea:41:41:40:De:5d:ae:22:23:b0:03:61:a3:96:17:7a:9c:b4:10:FF:61:f2:00:15:ad")) + require.NoError(t, err) + require.Equal(t, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", hex.EncodeToString(b)) +} + +func TestDecodeInterpersedHexBytesOneByte(t *testing.T) { + b, err := DecodeInterspersedHex([]byte("ba")) + require.NoError(t, err) + require.Equal(t, "ba", hex.EncodeToString(b)) +} + +func TestEncodeInterperseHexNilSlice(t *testing.T) { + require.Equal(t, "", EncodeInterspersedHex(nil)) + require.Equal(t, "", EncodeInterspersedHex([]byte{})) +} + +func TestDecodeInterspersedHexNilSlice(t *testing.T) { + for _, v := range [][]byte{nil, {}} { + b, err := DecodeInterspersedHex(v) + require.NoError(t, err) + require.Equal(t, []byte{}, b) + } +} + +func TestDecodeInterpersedHexFromASCIIStringEmpty(t *testing.T) { + b, err := DecodeInterpersedHexFromASCIIString("") + require.NoError(t, err) + require.Equal(t, []byte{}, b) +} + +func TestDecodeInterpersedHexInvalid(t *testing.T) { + for _, v := range []string{"0", "0000", "000"} { + _, err := DecodeInterspersedHex([]byte(v)) + require.Error(t, err) + } +} + +func TestDecodeInterpersedHexValid(t *testing.T) { + b, err := DecodeInterspersedHex([]byte("00")) + require.NoError(t, err) + require.Equal(t, []byte{0}, b) +} + +func TestDecodeInterpersedHexFromASCIIStringInvalid(t *testing.T) { + for _, v := range []string{"0", "0000", "000"} { + _, err := DecodeInterpersedHexFromASCIIString(v) + require.Error(t, err) + } +} + +func TestDecodeInterpersedHexFromASCIIStringValid(t *testing.T) { + b, err := DecodeInterpersedHexFromASCIIString("00") + require.NoError(t, err) + require.Equal(t, []byte{0}, b) +} + +func FuzzInterpersedHex(f *testing.F) { + f.Fuzz(func(t *testing.T, b []byte) { + decoded, err := DecodeInterspersedHex(b) + if err != nil { + return + } + encoded := EncodeInterspersedHex(decoded) + require.Equal(t, strings.ToLower(string(b)), encoded) + }) +} + +func FuzzInterspersedHexASCII(f *testing.F) { + f.Fuzz(func(t *testing.T, s string) { + decoded, err := DecodeInterpersedHexFromASCIIString(s) + if err != nil { + return + } + encoded := EncodeInterspersedHex(decoded) + require.Equal(t, strings.ToLower(s), encoded) + }) +} diff --git a/p2p/transport/webrtc/internal/fingerprint.go b/p2p/transport/webrtc/internal/fingerprint.go new file mode 100644 index 0000000000..75b02cd6b7 --- /dev/null +++ b/p2p/transport/webrtc/internal/fingerprint.go @@ -0,0 +1,26 @@ +package internal + +import ( + "crypto" + "crypto/x509" + "errors" +) + +// Fingerprint is forked from pion to avoid bytes to string alloc, +// and to avoid the entire hex interpersing when we do not need it anyway + +var ( + errHashUnavailable = errors.New("fingerprint: hash algorithm is not linked into the binary") +) + +// Fingerprint creates a fingerprint for a certificate using the specified hash algorithm +func Fingerprint(cert *x509.Certificate, algo crypto.Hash) ([]byte, error) { + if !algo.Available() { + return nil, errHashUnavailable + } + h := algo.New() + h.Write(cert.Raw) + // Hash.Writer is specified to be never returning an error. + // https://golang.org/pkg/hash/#Hash + return h.Sum(nil), nil +} diff --git a/p2p/transport/webrtc/internal/sdp.go b/p2p/transport/webrtc/internal/sdp.go new file mode 100644 index 0000000000..70ca46b632 --- /dev/null +++ b/p2p/transport/webrtc/internal/sdp.go @@ -0,0 +1,144 @@ +package internal + +import ( + "crypto" + "fmt" + "net" + "strings" + + "github.com/libp2p/go-libp2p/p2p/transport/webrtc/internal/encoding" + multihash "github.com/multiformats/go-multihash" +) + +// clientSDP describes an SDP format string which can be used +// to infer a client's SDP offer from the incoming STUN message. +// The fingerprint used to render a client SDP is arbitrary since +// it fingerprint verification is disabled in favour of a noise +// handshake. The max message size is fixed to 16384 bytes. +const clientSDP = `v=0 +o=- 0 0 IN %[1]s %[2]s +s=- +c=IN %[1]s %[2]s +t=0 0 + +m=application %[3]d UDP/DTLS/SCTP webrtc-datachannel +a=mid:0 +a=ice-options:ice2 +a=ice-ufrag:%[4]s +a=ice-pwd:%[4]s +a=fingerprint:sha-256 ba:78:16:bf:8f:01:cf:ea:41:41:40:de:5d:ae:22:23:b0:03:61:a3:96:17:7a:9c:b4:10:ff:61:f2:00:15:ad +a=setup:actpass +a=sctp-port:5000 +a=max-message-size:16384 +` + +func RenderClientSDP(addr *net.UDPAddr, ufrag string) string { + ipVersion := "IP4" + if addr.IP.To4() == nil { + ipVersion = "IP6" + } + return fmt.Sprintf( + clientSDP, + ipVersion, + addr.IP, + addr.Port, + ufrag, + ) +} + +// serverSDP defines an SDP format string used by a dialer +// to infer the SDP answer of a server based on the provided +// multiaddr, and the locally set ICE credentials. The max +// message size is fixed to 16384 bytes. +const serverSDP = `v=0 +o=- 0 0 IN %[1]s %[2]s +s=- +t=0 0 +a=ice-lite +m=application %[3]d UDP/DTLS/SCTP webrtc-datachannel +c=IN %[1]s %[2]s +a=mid:0 +a=ice-options:ice2 +a=ice-ufrag:%[4]s +a=ice-pwd:%[4]s +a=fingerprint:%[5]s + +a=setup:passive +a=sctp-port:5000 +a=max-message-size:16384 +a=candidate:1 1 UDP 1 %[2]s %[3]d typ host +a=end-of-candidates +` + +func RenderServerSDP(addr *net.UDPAddr, ufrag string, fingerprint multihash.DecodedMultihash) (string, error) { + ipVersion := "IP4" + if addr.IP.To4() == nil { + ipVersion = "IP6" + } + + sdpString, err := getSupportedSDPString(fingerprint.Code) + if err != nil { + return "", err + } + + var builder strings.Builder + builder.Grow(len(fingerprint.Digest)*3 + 8) + builder.WriteString(sdpString) + builder.WriteByte(' ') + encoding.EncodeInterspersedHexToBuilder(fingerprint.Digest, &builder) + fp := builder.String() + + return fmt.Sprintf( + serverSDP, + ipVersion, + addr.IP, + addr.Port, + ufrag, + fp, + ), nil +} + +// GetSupportedSDPHash converts a multihash code to the +// corresponding crypto.Hash for supported protocols. If a +// crypto.Hash cannot be found, it returns `(crypto.SHA256, false)` +func GetSupportedSDPHash(code uint64) (crypto.Hash, bool) { + switch code { + case multihash.MD5: + return crypto.MD5, true + case multihash.SHA1: + return crypto.SHA1, true + case multihash.SHA3_224: + return crypto.SHA3_224, true + case multihash.SHA2_256: + return crypto.SHA256, true + case multihash.SHA3_384: + return crypto.SHA3_384, true + case multihash.SHA2_512: + return crypto.SHA512, true + default: + return 0, false + } +} + +// getSupportedSDPString converts a multihash code +// to a string format recognised by pion for fingerprint +// algorithms +func getSupportedSDPString(code uint64) (string, error) { + // values based on (cryto.Hash).String() + switch code { + case multihash.MD5: + return "md5", nil + case multihash.SHA1: + return "sha-1", nil + case multihash.SHA3_224: + return "sha3-224", nil + case multihash.SHA2_256: + return "sha-256", nil + case multihash.SHA3_384: + return "sha3-384", nil + case multihash.SHA2_512: + return "sha-512", nil + default: + return "", fmt.Errorf("unsupported hash code (%d)", code) + } +} diff --git a/p2p/transport/webrtc/internal/util.go b/p2p/transport/webrtc/internal/util.go new file mode 100644 index 0000000000..4dd32d2478 --- /dev/null +++ b/p2p/transport/webrtc/internal/util.go @@ -0,0 +1,35 @@ +package internal + +import ( + "github.com/libp2p/go-libp2p/p2p/transport/webrtc/internal/encoding" + ma "github.com/multiformats/go-multiaddr" + "github.com/multiformats/go-multibase" + mh "github.com/multiformats/go-multihash" + + // "github.com/pion/datachannel" + "github.com/pion/webrtc/v3" +) + +func DecodeRemoteFingerprint(maddr ma.Multiaddr) (*mh.DecodedMultihash, error) { + remoteFingerprintMultibase, err := maddr.ValueForProtocol(ma.P_CERTHASH) + if err != nil { + return nil, err + } + _, data, err := multibase.Decode(remoteFingerprintMultibase) + if err != nil { + return nil, err + } + return mh.Decode(data) +} + +func EncodeDTLSFingerprint(fp webrtc.DTLSFingerprint) (string, error) { + digest, err := encoding.DecodeInterpersedHexFromASCIIString(fp.Value) + if err != nil { + return "", err + } + encoded, err := mh.Encode(digest, mh.SHA2_256) + if err != nil { + return "", err + } + return multibase.Encode(multibase.Base64url, encoded) +} diff --git a/p2p/transport/webrtc/internal/util_test.go b/p2p/transport/webrtc/internal/util_test.go new file mode 100644 index 0000000000..20fc9c7874 --- /dev/null +++ b/p2p/transport/webrtc/internal/util_test.go @@ -0,0 +1,101 @@ +package internal + +import ( + "encoding/hex" + "net" + "testing" + + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" +) + +const expectedServerSDP = `v=0 +o=- 0 0 IN IP4 0.0.0.0 +s=- +t=0 0 +a=ice-lite +m=application 37826 UDP/DTLS/SCTP webrtc-datachannel +c=IN IP4 0.0.0.0 +a=mid:0 +a=ice-options:ice2 +a=ice-ufrag:d2c0fc07-8bb3-42ae-bae2-a6fce8a0b581 +a=ice-pwd:d2c0fc07-8bb3-42ae-bae2-a6fce8a0b581 +a=fingerprint:sha-256 ba:78:16:bf:8f:01:cf:ea:41:41:40:de:5d:ae:22:23:b0:03:61:a3:96:17:7a:9c:b4:10:ff:61:f2:00:15:ad + +a=setup:passive +a=sctp-port:5000 +a=max-message-size:16384 +a=candidate:1 1 UDP 1 0.0.0.0 37826 typ host +a=end-of-candidates +` + +func TestRenderServerSDP(t *testing.T) { + encoded, err := hex.DecodeString("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") + require.NoError(t, err) + + testMultihash := multihash.DecodedMultihash{ + Code: multihash.SHA2_256, + Name: multihash.Codes[multihash.SHA2_256], + Digest: encoded, + Length: len(encoded), + } + addr := &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 37826} + ufrag := "d2c0fc07-8bb3-42ae-bae2-a6fce8a0b581" + fingerprint := testMultihash + + sdp, err := RenderServerSDP(addr, ufrag, fingerprint) + require.NoError(t, err) + require.Equal(t, expectedServerSDP, sdp) +} + +const expectedClientSDP = `v=0 +o=- 0 0 IN IP4 0.0.0.0 +s=- +c=IN IP4 0.0.0.0 +t=0 0 + +m=application 37826 UDP/DTLS/SCTP webrtc-datachannel +a=mid:0 +a=ice-options:ice2 +a=ice-ufrag:d2c0fc07-8bb3-42ae-bae2-a6fce8a0b581 +a=ice-pwd:d2c0fc07-8bb3-42ae-bae2-a6fce8a0b581 +a=fingerprint:sha-256 ba:78:16:bf:8f:01:cf:ea:41:41:40:de:5d:ae:22:23:b0:03:61:a3:96:17:7a:9c:b4:10:ff:61:f2:00:15:ad +a=setup:actpass +a=sctp-port:5000 +a=max-message-size:16384 +` + +func TestRenderClientSDP(t *testing.T) { + addr := &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 37826} + ufrag := "d2c0fc07-8bb3-42ae-bae2-a6fce8a0b581" + sdp := RenderClientSDP(addr, ufrag) + require.Equal(t, expectedClientSDP, sdp) +} + +func BenchmarkRenderClientSDP(b *testing.B) { + addr := &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 37826} + ufrag := "d2c0fc07-8bb3-42ae-bae2-a6fce8a0b581" + + for i := 0; i < b.N; i++ { + RenderClientSDP(addr, ufrag) + } +} + +func BenchmarkRenderServerSDP(b *testing.B) { + encoded, _ := hex.DecodeString("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") + + testMultihash := multihash.DecodedMultihash{ + Code: multihash.SHA2_256, + Name: multihash.Codes[multihash.SHA2_256], + Digest: encoded, + Length: len(encoded), + } + addr := &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 37826} + ufrag := "d2c0fc07-8bb3-42ae-bae2-a6fce8a0b581" + fingerprint := testMultihash + + for i := 0; i < b.N; i++ { + RenderServerSDP(addr, ufrag, fingerprint) + } + +} diff --git a/p2p/transport/webrtc/listener.go b/p2p/transport/webrtc/listener.go new file mode 100644 index 0000000000..c19a0a38d8 --- /dev/null +++ b/p2p/transport/webrtc/listener.go @@ -0,0 +1,351 @@ +package libp2pwebrtc + +import ( + "context" + "crypto" + "encoding/hex" + "fmt" + "net" + "os" + "strings" + "sync" + "time" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/p2p/transport/webrtc/internal" + "github.com/libp2p/go-libp2p/p2p/transport/webrtc/udpmux" + + tpt "github.com/libp2p/go-libp2p/core/transport" + ma "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" + "github.com/multiformats/go-multibase" + "github.com/multiformats/go-multihash" + + "github.com/pion/ice/v2" + pionlogger "github.com/pion/logging" + "github.com/pion/webrtc/v3" +) + +var _ tpt.Listener = &listener{} + +const ( + candidateSetupTimeout = 20 * time.Second + DefaultMaxInFlightConnections = 10 +) + +type candidateAddr struct { + ufrag string + raddr *net.UDPAddr +} + +type listener struct { + transport *WebRTCTransport + + mux ice.UDPMux + + config webrtc.Configuration + localFingerprint webrtc.DTLSFingerprint + localFingerprintMultibase string + + localAddr net.Addr + localMultiaddr ma.Multiaddr + + // buffered incoming connections + acceptQueue chan tpt.CapableConn + + // Accepting a connection requires instantiating a peerconnection + // and a noise connection which is expensive. We therefore limit + // the number of in-flight connection requests. A connection + // is considered to be in flight from the instant it is handled + // until it is dequeued by a call to Accept, or errors out in some + // way. + inFlightInputQueue chan candidateAddr + maxInFlightConnections uint32 + + // used to control the lifecycle of the listener + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup +} + +func newListener(transport *WebRTCTransport, laddr ma.Multiaddr, socket net.PacketConn, config webrtc.Configuration) (*listener, error) { + localFingerprints, err := config.Certificates[0].GetFingerprints() + if err != nil { + return nil, err + } + + localMh, err := hex.DecodeString(strings.ReplaceAll(localFingerprints[0].Value, ":", "")) + if err != nil { + return nil, err + } + localMhBuf, err := multihash.Encode(localMh, multihash.SHA2_256) + if err != nil { + return nil, err + } + localFpMultibase, err := multibase.Encode(multibase.Base64url, localMhBuf) + if err != nil { + return nil, err + } + + inFlightQueueCh := make(chan candidateAddr, 1) + + ctx, cancel := context.WithCancel(context.Background()) + mux := udpmux.NewUDPMux(socket, func(ufrag string, addr net.Addr) bool { + // Push to the inFlightQueue asynchronously to avoid blocking the mux goroutine + // on candidates being processed. This can cause new connections to fail at high + // throughput but will allow packets for existing connections to be processed. + select { + case inFlightQueueCh <- candidateAddr{ufrag: ufrag, raddr: addr.(*net.UDPAddr)}: + return true + default: + log.Debug("candidate chan full, dropping incoming candidate") + return false + } + }) + + l := &listener{ + mux: mux, + transport: transport, + config: config, + localFingerprint: localFingerprints[0], + localFingerprintMultibase: localFpMultibase, + localMultiaddr: laddr, + ctx: ctx, + cancel: cancel, + localAddr: socket.LocalAddr(), + acceptQueue: make(chan tpt.CapableConn, transport.maxInFlightConnections-1), + inFlightInputQueue: inFlightQueueCh, + maxInFlightConnections: transport.maxInFlightConnections, + } + + l.wg.Add(1) + go func() { + defer l.wg.Done() + l.inFlightWorker() + }() + + return l, err +} + +func (l *listener) inFlightWorker() { + for { + select { + case <-l.ctx.Done(): + return + + case addr := <-l.inFlightInputQueue: + if !l.handleInFlightInput(&addr) { + return + } + } + } +} + +func (l *listener) handleInFlightInput(addr *candidateAddr) bool { + ctx, cancel := context.WithTimeout(l.ctx, candidateSetupTimeout) + defer cancel() + + conn, err := l.handleCandidate(ctx, addr) + if err != nil { + log.Debugf("could not accept connection: %s: %v", addr.ufrag, err) + return false + } + select { + case <-ctx.Done(): + log.Warn("could not push connection: ctx done") + conn.Close() + case l.acceptQueue <- conn: + // block until the connection is accepted, + // or until we are done, this effectively blocks our in flight from continuing to progress + } + + return true +} + +func (l *listener) handleCandidate(ctx context.Context, addr *candidateAddr) (tpt.CapableConn, error) { + remoteMultiaddr, err := manet.FromNetAddr(addr.raddr) + if err != nil { + return nil, err + } + scope, err := l.transport.rcmgr.OpenConnection(network.DirInbound, false, remoteMultiaddr) + if err != nil { + return nil, err + } + conn, err := l.setupConnection(ctx, scope, remoteMultiaddr, addr) + if err != nil { + scope.Done() + return nil, err + } + return conn, nil +} + +func (l *listener) setupConnection( + ctx context.Context, scope network.ConnManagementScope, + remoteMultiaddr ma.Multiaddr, addr *candidateAddr, +) (tConn tpt.CapableConn, err error) { + var pc *webrtc.PeerConnection + defer func() { + if err != nil { + if pc != nil { + _ = pc.Close() + } + if tConn != nil { + _ = tConn.Close() + } + } + }() + + settingEngine := webrtc.SettingEngine{} + + // suppress pion logs + loggerFactory := pionlogger.NewDefaultLoggerFactory() + loggerFactory.DefaultLogLevel = pionlogger.LogLevelWarn + settingEngine.LoggerFactory = loggerFactory + + settingEngine.SetAnsweringDTLSRole(webrtc.DTLSRoleServer) + settingEngine.SetICECredentials(addr.ufrag, addr.ufrag) + settingEngine.SetLite(true) + settingEngine.SetICEUDPMux(l.mux) + settingEngine.SetIncludeLoopbackCandidate(true) + settingEngine.DisableCertificateFingerprintVerification(true) + settingEngine.SetICETimeouts( + l.transport.peerConnectionTimeouts.Disconnect, + l.transport.peerConnectionTimeouts.Failed, + l.transport.peerConnectionTimeouts.Keepalive, + ) + settingEngine.DetachDataChannels() + + api := webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine)) + + pc, err = api.NewPeerConnection(l.config) + if err != nil { + return nil, err + } + + negotiated, id := handshakeChannelNegotiated, handshakeChannelID + rawDatachannel, err := pc.CreateDataChannel("", &webrtc.DataChannelInit{ + Negotiated: &negotiated, + ID: &id, + }) + if err != nil { + return nil, err + } + + errC := awaitPeerConnectionOpen(addr.ufrag, pc) + // we infer the client sdp from the incoming STUN connectivity check + // by setting the ice-ufrag equal to the incoming check. + clientSdpString := internal.RenderClientSDP(addr.raddr, addr.ufrag) + clientSdp := webrtc.SessionDescription{SDP: clientSdpString, Type: webrtc.SDPTypeOffer} + pc.SetRemoteDescription(clientSdp) + + answer, err := pc.CreateAnswer(nil) + if err != nil { + return nil, err + } + + err = pc.SetLocalDescription(answer) + if err != nil { + return nil, err + } + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case err := <-errC: + if err != nil { + return nil, fmt.Errorf("peer connection error: %w", err) + } + + } + + rwc, err := getDetachedChannel(ctx, rawDatachannel) + if err != nil { + return nil, err + } + + handshakeChannel := newStream(nil, rawDatachannel, rwc, l.localAddr, addr.raddr) + // The connection is instantiated before performing the Noise handshake. This is + // to handle the case where the remote is faster and attempts to initiate a stream + // before the ondatachannel callback can be set. + conn, err := NewWebRTCConnection( + network.DirInbound, + pc, + l.transport, + scope, + l.transport.localPeerId, + l.localMultiaddr, + "", // remotePeer + nil, // remoteKey + remoteMultiaddr, + ) + if err != nil { + return nil, err + } + + // we do not yet know A's peer ID so accept any inbound + secureConn, err := l.transport.noiseHandshake(ctx, pc, handshakeChannel, "", crypto.SHA256, true) + if err != nil { + return nil, err + } + + // earliest point where we know the remote's peerID + err = scope.SetPeer(secureConn.RemotePeer()) + if err != nil { + return nil, err + } + + conn.setRemotePeer(secureConn.RemotePeer()) + conn.setRemotePublicKey(secureConn.RemotePublicKey()) + + return conn, err +} + +func (l *listener) Accept() (tpt.CapableConn, error) { + select { + case <-l.ctx.Done(): + return nil, os.ErrClosed + case conn := <-l.acceptQueue: + return conn, nil + } +} + +func (l *listener) Close() error { + select { + case <-l.ctx.Done(): + default: + l.cancel() + l.wg.Wait() + } + return nil +} + +func (l *listener) Addr() net.Addr { + return l.localAddr +} + +func (l *listener) Multiaddr() ma.Multiaddr { + return l.localMultiaddr +} + +func awaitPeerConnectionOpen(ufrag string, pc *webrtc.PeerConnection) <-chan error { + errC := make(chan error, 1) + var once sync.Once + pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { + switch state { + case webrtc.PeerConnectionStateConnected: + once.Do(func() { close(errC) }) + case webrtc.PeerConnectionStateFailed: + once.Do(func() { + errC <- fmt.Errorf("peerconnection failed: %s", ufrag) + close(errC) + }) + case webrtc.PeerConnectionStateDisconnected: + // the connection can move to a disconnected state and back to a connected state without ICE renegotiation. + // This could happen when underlying UDP packets are lost, and therefore the connection moves to the disconnected state. + // If the connection then receives packets on the connection, it can move back to the connected state. + // If no packets are received until the failed timeout is triggered, the connection moves to the failed state. + log.Warn("peerconnection disconnected") + } + }) + return errC +} diff --git a/p2p/transport/webrtc/message.go b/p2p/transport/webrtc/message.go new file mode 100644 index 0000000000..8b57fc8078 --- /dev/null +++ b/p2p/transport/webrtc/message.go @@ -0,0 +1,3 @@ +package libp2pwebrtc + +//go:generate protoc --go_out=. --go_opt=Mpb/message.proto=./pb pb/message.proto diff --git a/p2p/transport/webrtc/pb/message.pb.go b/p2p/transport/webrtc/pb/message.pb.go new file mode 100644 index 0000000000..b27bf9e10a --- /dev/null +++ b/p2p/transport/webrtc/pb/message.pb.go @@ -0,0 +1,222 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.21.12 +// source: pb/message.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Message_Flag int32 + +const ( + // The sender will no longer send messages on the stream. + Message_FIN Message_Flag = 0 + // The sender will no longer read messages on the stream. Incoming data is + // being discarded on receipt. + Message_STOP_SENDING Message_Flag = 1 + // The sender abruptly terminates the sending part of the stream. The + // receiver can discard any data that it already received on that stream. + Message_RESET Message_Flag = 2 +) + +// Enum value maps for Message_Flag. +var ( + Message_Flag_name = map[int32]string{ + 0: "FIN", + 1: "STOP_SENDING", + 2: "RESET", + } + Message_Flag_value = map[string]int32{ + "FIN": 0, + "STOP_SENDING": 1, + "RESET": 2, + } +) + +func (x Message_Flag) Enum() *Message_Flag { + p := new(Message_Flag) + *p = x + return p +} + +func (x Message_Flag) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Message_Flag) Descriptor() protoreflect.EnumDescriptor { + return file_pb_message_proto_enumTypes[0].Descriptor() +} + +func (Message_Flag) Type() protoreflect.EnumType { + return &file_pb_message_proto_enumTypes[0] +} + +func (x Message_Flag) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Do not use. +func (x *Message_Flag) UnmarshalJSON(b []byte) error { + num, err := protoimpl.X.UnmarshalJSONEnum(x.Descriptor(), b) + if err != nil { + return err + } + *x = Message_Flag(num) + return nil +} + +// Deprecated: Use Message_Flag.Descriptor instead. +func (Message_Flag) EnumDescriptor() ([]byte, []int) { + return file_pb_message_proto_rawDescGZIP(), []int{0, 0} +} + +type Message struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Flag *Message_Flag `protobuf:"varint,1,opt,name=flag,enum=webrtc.pb.Message_Flag" json:"flag,omitempty"` + Message []byte `protobuf:"bytes,2,opt,name=message" json:"message,omitempty"` +} + +func (x *Message) Reset() { + *x = Message{} + if protoimpl.UnsafeEnabled { + mi := &file_pb_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_pb_message_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_pb_message_proto_rawDescGZIP(), []int{0} +} + +func (x *Message) GetFlag() Message_Flag { + if x != nil && x.Flag != nil { + return *x.Flag + } + return Message_FIN +} + +func (x *Message) GetMessage() []byte { + if x != nil { + return x.Message + } + return nil +} + +var File_pb_message_proto protoreflect.FileDescriptor + +var file_pb_message_proto_rawDesc = []byte{ + 0x0a, 0x10, 0x70, 0x62, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x09, 0x77, 0x65, 0x62, 0x72, 0x74, 0x63, 0x2e, 0x70, 0x62, 0x22, 0x7e, 0x0a, + 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x66, 0x6c, 0x61, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x77, 0x65, 0x62, 0x72, 0x74, 0x63, 0x2e, + 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x46, 0x6c, 0x61, 0x67, 0x52, + 0x04, 0x66, 0x6c, 0x61, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x2c, 0x0a, 0x04, 0x46, 0x6c, 0x61, 0x67, 0x12, 0x07, 0x0a, 0x03, 0x46, 0x49, 0x4e, 0x10, 0x00, + 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x4f, 0x50, 0x5f, 0x53, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, + 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x45, 0x53, 0x45, 0x54, 0x10, 0x02, +} + +var ( + file_pb_message_proto_rawDescOnce sync.Once + file_pb_message_proto_rawDescData = file_pb_message_proto_rawDesc +) + +func file_pb_message_proto_rawDescGZIP() []byte { + file_pb_message_proto_rawDescOnce.Do(func() { + file_pb_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_pb_message_proto_rawDescData) + }) + return file_pb_message_proto_rawDescData +} + +var file_pb_message_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_pb_message_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_pb_message_proto_goTypes = []interface{}{ + (Message_Flag)(0), // 0: webrtc.pb.Message.Flag + (*Message)(nil), // 1: webrtc.pb.Message +} +var file_pb_message_proto_depIdxs = []int32{ + 0, // 0: webrtc.pb.Message.flag:type_name -> webrtc.pb.Message.Flag + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_pb_message_proto_init() } +func file_pb_message_proto_init() { + if File_pb_message_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pb_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pb_message_proto_rawDesc, + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_pb_message_proto_goTypes, + DependencyIndexes: file_pb_message_proto_depIdxs, + EnumInfos: file_pb_message_proto_enumTypes, + MessageInfos: file_pb_message_proto_msgTypes, + }.Build() + File_pb_message_proto = out.File + file_pb_message_proto_rawDesc = nil + file_pb_message_proto_goTypes = nil + file_pb_message_proto_depIdxs = nil +} diff --git a/p2p/transport/webrtc/pb/message.proto b/p2p/transport/webrtc/pb/message.proto new file mode 100644 index 0000000000..eab3ceb720 --- /dev/null +++ b/p2p/transport/webrtc/pb/message.proto @@ -0,0 +1,20 @@ +syntax = "proto2"; + +package webrtc.pb; + +message Message { + enum Flag { + // The sender will no longer send messages on the stream. + FIN = 0; + // The sender will no longer read messages on the stream. Incoming data is + // being discarded on receipt. + STOP_SENDING = 1; + // The sender abruptly terminates the sending part of the stream. The + // receiver can discard any data that it already received on that stream. + RESET = 2; + } + + optional Flag flag=1; + + optional bytes message = 2; +} diff --git a/p2p/transport/webrtc/stream.go b/p2p/transport/webrtc/stream.go new file mode 100644 index 0000000000..e6210d428d --- /dev/null +++ b/p2p/transport/webrtc/stream.go @@ -0,0 +1,203 @@ +package libp2pwebrtc + +import ( + "bufio" + "context" + "net" + "sync" + "time" + + "github.com/libp2p/go-libp2p/core/network" + pb "github.com/libp2p/go-libp2p/p2p/transport/webrtc/pb" + "github.com/libp2p/go-msgio/pbio" + "github.com/pion/datachannel" + "github.com/pion/webrtc/v3" +) + +var _ network.MuxedStream = &webRTCStream{} + +const ( + // maxMessageSize is limited to 16384 bytes in the SDP. + maxMessageSize = 16384 + // Pion SCTP association has an internal receive buffer of 1MB (roughly, 1MB per connection). + // We can change this value in the SettingEngine before creating the peerconnection. + // https://github.com/pion/webrtc/blob/v3.1.49/sctptransport.go#L341 + maxBufferedAmount = 2 * maxMessageSize + // bufferedAmountLowThreshold and maxBufferedAmount are bound + // to a stream but congestion control is done on the whole + // SCTP association. This means that a single stream can monopolize + // the complete congestion control window (cwnd) if it does not + // read stream data and it's remote continues to send. We can + // add messages to the send buffer once there is space for 1 full + // sized message. + bufferedAmountLowThreshold = maxBufferedAmount / 2 + + // Proto overhead assumption is 5 bytes + protoOverhead = 5 + // Varint overhead is assumed to be 2 bytes. This is safe since + // 1. This is only used and when writing message, and + // 2. We only send messages in chunks of `maxMessageSize - varintOverhead` + // which includes the data and the protobuf header. Since `maxMessageSize` + // is less than or equal to 2 ^ 14, the varint will not be more than + // 2 bytes in length. + varintOverhead = 2 +) + +// Package pion detached data channel into a net.Conn +// and then a network.MuxedStream +type webRTCStream struct { + reader pbio.Reader + // pbio.Reader is not thread safe, + // and while our Read is not promised to be thread safe, + // we ourselves internally read from multiple routines... + readerMux sync.Mutex + // this buffer is limited up to a single message. Reason we need it + // is because a reader might read a message midway, and so we need a + // wait to buffer that for as long as the remaining part is not (yet) read + readBuffer []byte + + writer pbio.Writer + // public write API is not promised to be thread safe, however we also write from + // read functionality due to our spec limiations, so (e.g. 1 channel for state and data) + // and thus we do need to protect the writer + writerMux sync.Mutex + + writerDeadline time.Time + writerDeadlineMux sync.Mutex + + writerDeadlineUpdated chan struct{} + writeAvailable chan struct{} + + readLoopOnce sync.Once + + stateHandler webRTCStreamState + + conn *connection + id uint16 + dataChannel *datachannel.DataChannel + + laddr net.Addr + raddr net.Addr + + ctx context.Context + cancel context.CancelFunc + + closeOnce sync.Once +} + +func newStream( + connection *connection, + channel *webrtc.DataChannel, + rwc datachannel.ReadWriteCloser, + laddr, raddr net.Addr, +) *webRTCStream { + ctx, cancel := context.WithCancel(context.Background()) + + // allocating 16KiB per stream might seem wasteful, + // but problem is that we also write max up to this amount, + // and pion does not allow us to read chunks. Should you try to do so, + // and you read less then that there's written you'll notice + // undefined behaviour where the unread part is dropped. + reader := bufio.NewReaderSize(rwc, maxMessageSize) + + result := &webRTCStream{ + reader: pbio.NewDelimitedReader(reader, maxMessageSize), + writer: pbio.NewDelimitedWriter(rwc), + + writerDeadlineUpdated: make(chan struct{}, 1), + writeAvailable: make(chan struct{}, 1), + + conn: connection, + id: *channel.ID(), + dataChannel: rwc.(*datachannel.DataChannel), + + laddr: laddr, + raddr: raddr, + + ctx: ctx, + cancel: cancel, + } + + channel.SetBufferedAmountLowThreshold(bufferedAmountLowThreshold) + channel.OnBufferedAmountLow(func() { + select { + case result.writeAvailable <- struct{}{}: + default: + } + }) + + return result +} + +func (s *webRTCStream) Close() error { + return s.close(false, true) +} + +func (s *webRTCStream) Reset() error { + return s.close(true, true) +} + +func (s *webRTCStream) LocalAddr() net.Addr { + return s.laddr +} + +func (s *webRTCStream) RemoteAddr() net.Addr { + return s.raddr +} + +func (s *webRTCStream) SetDeadline(t time.Time) error { + return s.SetWriteDeadline(t) +} + +func (s *webRTCStream) processIncomingFlag(flag pb.Message_Flag) { + if s.isClosed() { + return + } + state, reset := s.stateHandler.HandleInboundFlag(flag) + if state == stateClosed { + log.Debug("closing: after handle inbound flag") + s.close(reset, true) + } +} + +// this is used to force reset a stream +func (s *webRTCStream) close(isReset bool, notifyConnection bool) error { + if s.isClosed() { + return nil + } + + var err error + s.closeOnce.Do(func() { + log.Debugf("closing stream %d: reset: %t, notify: %t", s.id, isReset, notifyConnection) + s.stateHandler.Close() + // force close reads + s.SetReadDeadline(time.Now()) // pion ignores zero times + if isReset { + // write the RESET message. The error is explicitly ignored + // because we do not know if the remote is still connected + s.writeMessage(&pb.Message{Flag: pb.Message_RESET.Enum()}) + } else { + // write a FIN message for standard stream closure + s.writeMessage(&pb.Message{Flag: pb.Message_FIN.Enum()}) + } + // close the context + s.cancel() + // close the channel. We do not care about the error message in + // this case + err = s.dataChannel.Close() + if notifyConnection && s.conn != nil { + s.conn.removeStream(s.id) + } + }) + + return err +} + +func (s *webRTCStream) isClosed() bool { + select { + case <-s.ctx.Done(): + return true + default: + return false + } +} diff --git a/p2p/transport/webrtc/stream_read.go b/p2p/transport/webrtc/stream_read.go new file mode 100644 index 0000000000..f0fa86a86d --- /dev/null +++ b/p2p/transport/webrtc/stream_read.go @@ -0,0 +1,101 @@ +package libp2pwebrtc + +import ( + "errors" + "fmt" + "io" + "time" + + pb "github.com/libp2p/go-libp2p/p2p/transport/webrtc/pb" +) + +// Read from the underlying datachannel. This also +// process sctp control messages such as DCEP, which is +// handled internally by pion, and stream closure which +// is signaled by `Read` on the datachannel returning +// io.EOF. +func (s *webRTCStream) Read(b []byte) (int, error) { + if len(b) == 0 { + return 0, nil + } + + var ( + readErr error + read int + ) + for read == 0 && readErr == nil { + if s.isClosed() { + return 0, io.ErrClosedPipe + } + read, readErr = s.readMessage(b) + } + return read, readErr +} + +func (s *webRTCStream) readMessage(b []byte) (int, error) { + read := copy(b, s.readBuffer) + s.readBuffer = s.readBuffer[read:] + remaining := len(s.readBuffer) + + if remaining == 0 && !s.stateHandler.AllowRead() { + log.Debug("[2] stream has no more data to read") + return read, io.EOF + } + + if read > 0 { + return read, nil + } + + // read from datachannel + var msg pb.Message + err := s.readMessageFromDataChannel(&msg) + if err != nil { + // This case occurs when the remote node goes away + // without writing a FIN message + if errors.Is(err, io.EOF) { + s.Reset() + return read, io.ErrClosedPipe + } + return read, err + } + + // append incoming data to read readBuffer + if s.stateHandler.AllowRead() && msg.Message != nil { + s.readBuffer = append(s.readBuffer, msg.GetMessage()...) + } + + // process any flags on the message + if msg.Flag != nil { + s.processIncomingFlag(msg.GetFlag()) + } + return read, nil +} + +func (s *webRTCStream) readMessageFromDataChannel(msg *pb.Message) error { + s.readerMux.Lock() + defer s.readerMux.Unlock() + return s.reader.ReadMsg(msg) +} + +func (s *webRTCStream) SetReadDeadline(t time.Time) error { + return s.dataChannel.SetReadDeadline(t) +} + +func (s *webRTCStream) CloseRead() error { + if s.isClosed() { + return nil + } + var err error + s.closeOnce.Do(func() { + err = s.writeMessageToWriter(&pb.Message{Flag: pb.Message_STOP_SENDING.Enum()}) + if err != nil { + log.Debug("could not write STOP_SENDING message") + err = fmt.Errorf("could not close stream for reading: %w", err) + return + } + if s.stateHandler.CloseRead() == stateClosed { + s.close(false, true) + } + }) + return err +} diff --git a/p2p/transport/webrtc/stream_state.go b/p2p/transport/webrtc/stream_state.go new file mode 100644 index 0000000000..aac0b66b23 --- /dev/null +++ b/p2p/transport/webrtc/stream_state.go @@ -0,0 +1,124 @@ +package libp2pwebrtc + +import ( + "sync" + + pb "github.com/libp2p/go-libp2p/p2p/transport/webrtc/pb" +) + +type channelState uint8 + +const ( + stateOpen channelState = iota + stateReadClosed + stateWriteClosed + stateClosed +) + +type webRTCStreamState struct { + mu sync.RWMutex + state channelState + reset bool +} + +func (ss *webRTCStreamState) HandleInboundFlag(flag pb.Message_Flag) (channelState, bool) { + ss.mu.Lock() + defer ss.mu.Unlock() + + if ss.state == stateClosed { + return ss.state, ss.reset + } + + switch flag { + case pb.Message_FIN: + ss.closeReadInner() + + case pb.Message_STOP_SENDING: + ss.closeWriteInner() + + case pb.Message_RESET: + ss.closeInner(true) + + default: + // ignore values that are invalid for flags + } + + return ss.state, ss.reset +} + +func (ss *webRTCStreamState) State() channelState { + ss.mu.RLock() + defer ss.mu.RUnlock() + return ss.state +} + +func (ss *webRTCStreamState) AllowRead() bool { + ss.mu.RLock() + defer ss.mu.RUnlock() + return ss.state == stateOpen || ss.state == stateWriteClosed +} + +func (ss *webRTCStreamState) CloseRead() channelState { + ss.mu.Lock() + defer ss.mu.Unlock() + + if ss.state == stateClosed { + return ss.state + } + + ss.closeReadInner() + return ss.state +} + +func (ss *webRTCStreamState) closeReadInner() { + if ss.state == stateOpen { + ss.state = stateReadClosed + } else if ss.state == stateWriteClosed { + ss.closeInner(false) + } +} + +func (ss *webRTCStreamState) AllowWrite() bool { + ss.mu.RLock() + defer ss.mu.RUnlock() + return ss.state == stateOpen || ss.state == stateReadClosed +} + +func (ss *webRTCStreamState) CloseWrite() channelState { + ss.mu.Lock() + defer ss.mu.Unlock() + + if ss.state == stateClosed { + return ss.state + } + + ss.closeWriteInner() + return ss.state +} + +func (ss *webRTCStreamState) closeWriteInner() { + if ss.state == stateOpen { + ss.state = stateWriteClosed + } else if ss.state == stateReadClosed { + ss.closeInner(false) + } +} + +func (ss *webRTCStreamState) Closed() bool { + ss.mu.RLock() + defer ss.mu.RUnlock() + return ss.state == stateClosed +} +func (ss *webRTCStreamState) Close() { + ss.mu.Lock() + defer ss.mu.Unlock() + ss.state = stateClosed + ss.reset = false +} + +func (ss *webRTCStreamState) closeInner(reset bool) { + if ss.state != stateClosed { + ss.state = stateClosed + ss.reset = reset + } +} diff --git a/p2p/transport/webrtc/stream_write.go b/p2p/transport/webrtc/stream_write.go new file mode 100644 index 0000000000..decb629271 --- /dev/null +++ b/p2p/transport/webrtc/stream_write.go @@ -0,0 +1,168 @@ +package libp2pwebrtc + +import ( + "errors" + "fmt" + "io" + "math" + "os" + "time" + + pb "github.com/libp2p/go-libp2p/p2p/transport/webrtc/pb" + "google.golang.org/protobuf/proto" +) + +func (s *webRTCStream) Write(b []byte) (int, error) { + if !s.stateHandler.AllowWrite() { + return 0, io.ErrClosedPipe + } + + // Check if there is any message on the wire. This is used for control + // messages only when the read side of the stream is closed + if s.stateHandler.State() == stateReadClosed { + s.readLoopOnce.Do(s.spawnControlMessageReader) + } + + const chunkSize = maxMessageSize - protoOverhead - varintOverhead + + var n int + + for len(b) > 0 { + end := len(b) + if chunkSize < end { + end = chunkSize + } + + err := s.writeMessage(&pb.Message{Message: b[:end]}) + n += end + b = b[end:] + if err != nil { + return n, err + } + } + + return n, nil +} + +// used for reading control messages while writing, in case the reader is closed, +// as to ensure we do still get control messages. This is important as according to the spec +// our data and control channels are intermixed on the same conn. +func (s *webRTCStream) spawnControlMessageReader() { + go func() { + // zero the read deadline, so read call only returns + // when the underlying datachannel closes or there is + // a message on the channel + s.dataChannel.SetReadDeadline(time.Time{}) + var msg pb.Message + for { + if s.stateHandler.Closed() { + return + } + err := s.readMessageFromDataChannel(&msg) + if err != nil { + if errors.Is(err, io.EOF) { + s.close(true, true) + } + return + } + if msg.Flag != nil { + state, reset := s.stateHandler.HandleInboundFlag(msg.GetFlag()) + if state == stateClosed { + log.Debug("closing: after handle inbound flag") + s.close(reset, true) + } + } + } + }() +} + +func (s *webRTCStream) writeMessage(msg *pb.Message) error { + var writeDeadlineTimer *time.Timer + defer func() { + if writeDeadlineTimer != nil { + writeDeadlineTimer.Stop() + } + }() + + for { + if !s.stateHandler.AllowWrite() { + return io.ErrClosedPipe + } + + writeDeadline, hasWriteDeadline := s.getWriteDeadline() + if !hasWriteDeadline { + writeDeadline = time.Unix(math.MaxInt64, 0) + } + if writeDeadlineTimer == nil { + writeDeadlineTimer = time.NewTimer(time.Until(writeDeadline)) + } else { + writeDeadlineTimer.Reset(time.Until(writeDeadline)) + } + + bufferedAmount := int(s.dataChannel.BufferedAmount()) + addedBuffer := bufferedAmount + varintOverhead + proto.Size(msg) + if addedBuffer > maxBufferedAmount { + select { + case <-writeDeadlineTimer.C: + return os.ErrDeadlineExceeded + case <-s.writeAvailable: + return s.writeMessageToWriter(msg) + case <-s.ctx.Done(): + return io.ErrClosedPipe + case <-s.writerDeadlineUpdated: + } + } else { + return s.writeMessageToWriter(msg) + } + } +} + +func (s *webRTCStream) writeMessageToWriter(msg *pb.Message) error { + s.writerMux.Lock() + defer s.writerMux.Unlock() + return s.writer.WriteMsg(msg) +} + +func (s *webRTCStream) SetWriteDeadline(t time.Time) error { + s.writerDeadlineMux.Lock() + defer s.writerDeadlineMux.Unlock() + s.writerDeadline = t + select { + case s.writerDeadlineUpdated <- struct{}{}: + default: + } + return nil +} + +func (s *webRTCStream) getWriteDeadline() (time.Time, bool) { + s.writerDeadlineMux.Lock() + defer s.writerDeadlineMux.Unlock() + return s.writerDeadline, !s.writerDeadline.IsZero() +} + +func (s *webRTCStream) CloseWrite() error { + if s.isClosed() { + return nil + } + var err error + s.closeOnce.Do(func() { + err = s.writeMessage(&pb.Message{Flag: pb.Message_FIN.Enum()}) + if err != nil { + log.Debug("could not write FIN message") + err = fmt.Errorf("close stream for writing: %w", err) + return + } + // if successfully written, process the outgoing flag + state := s.stateHandler.CloseRead() + // unblock and fail any ongoing writes + select { + case s.writeAvailable <- struct{}{}: + default: + } + // check if closure required + if state == stateClosed { + s.close(false, true) + } + }) + return err +} diff --git a/p2p/transport/webrtc/transport.go b/p2p/transport/webrtc/transport.go new file mode 100644 index 0000000000..7897eae02a --- /dev/null +++ b/p2p/transport/webrtc/transport.go @@ -0,0 +1,504 @@ +package libp2pwebrtc + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "errors" + "fmt" + "net" + "time" + + "github.com/libp2p/go-libp2p/core/connmgr" + ic "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/pnet" + "github.com/libp2p/go-libp2p/core/sec" + tpt "github.com/libp2p/go-libp2p/core/transport" + "github.com/libp2p/go-libp2p/p2p/security/noise" + "github.com/libp2p/go-libp2p/p2p/transport/webrtc/internal" + "github.com/libp2p/go-libp2p/p2p/transport/webrtc/internal/encoding" + + logging "github.com/ipfs/go-log/v2" + ma "github.com/multiformats/go-multiaddr" + mafmt "github.com/multiformats/go-multiaddr-fmt" + manet "github.com/multiformats/go-multiaddr/net" + "github.com/multiformats/go-multihash" + pionlogger "github.com/pion/logging" + + "github.com/pion/webrtc/v3" +) + +var log = logging.Logger("webrtc-transport") + +var dialMatcher = mafmt.And(mafmt.UDP, mafmt.Base(ma.P_WEBRTC), mafmt.Base(ma.P_CERTHASH)) + +const ( + // handshakeChannelNegotiated is used to specify that the + // handshake data channel does not need negotiation via DCEP. + // A constant is used since the `DataChannelInit` struct takes + // references instead of values. + handshakeChannelNegotiated = true + // handshakeChannelID is the agreed ID for the handshake data + // channel. A constant is used since the `DataChannelInit` struct takes + // references instead of values. We specify the type here as this + // value is only ever copied and passed by reference + handshakeChannelID = uint16(0) +) + +// timeout values for the peerconnection +// https://github.com/pion/webrtc/blob/v3.1.50/settingengine.go#L102-L109 +const ( + DefaultDisconnectedTimeout = 20 * time.Second + DefaultFailedTimeout = 30 * time.Second + DefaultKeepaliveTimeout = 15 * time.Second +) + +type WebRTCTransport struct { + webrtcConfig webrtc.Configuration + rcmgr network.ResourceManager + privKey ic.PrivKey + noiseTpt *noise.Transport + localPeerId peer.ID + + // timeouts + peerConnectionTimeouts iceTimeouts + + // in-flight connections + maxInFlightConnections uint32 +} + +var _ tpt.Transport = &WebRTCTransport{} + +type Option func(*WebRTCTransport) error + +type iceTimeouts struct { + Disconnect time.Duration + Failed time.Duration + Keepalive time.Duration +} + +// WithListenerMaxInFlightConnections sets the maximum number of connections that are in-flight, i.e +// they are being negotiated, or are waiting to be accepted. +func WithListenerMaxInFlightConnections(m uint32) Option { + return func(t *WebRTCTransport) error { + if m == 0 { + t.maxInFlightConnections = DefaultMaxInFlightConnections + } else { + t.maxInFlightConnections = m + } + return nil + } +} + +func New(privKey ic.PrivKey, psk pnet.PSK, gater connmgr.ConnectionGater, rcmgr network.ResourceManager, opts ...Option) (*WebRTCTransport, error) { + if psk != nil { + log.Error("WebRTC doesn't support private networks yet.") + return nil, fmt.Errorf("WebRTC doesn't support private networks yet") + } + localPeerID, err := peer.IDFromPrivateKey(privKey) + if err != nil { + return nil, fmt.Errorf("get local peer ID: %w", err) + } + // We use elliptic P-256 since it is widely supported by browsers. + // + // Implementation note: Testing with the browser, + // it seems like Chromium only supports ECDSA P-256 or RSA key signatures in the webrtc TLS certificate. + // We tried using P-228 and P-384 which caused the DTLS handshake to fail with Illegal Parameter + // + // Please refer to this is a list of suggested algorithms for the WebCrypto API. + // The algorithm for generating a certificate for an RTCPeerConnection + // must adhere to the WebCrpyto API. From my observation, + // RSA and ECDSA P-256 is supported on almost all browsers. + // Ed25519 is not present on the list. + pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate key for cert: %w", err) + } + cert, err := webrtc.GenerateCertificate(pk) + if err != nil { + return nil, fmt.Errorf("generate certificate: %w", err) + } + config := webrtc.Configuration{ + Certificates: []webrtc.Certificate{*cert}, + } + noiseTpt, err := noise.New(noise.ID, privKey, nil) + if err != nil { + return nil, fmt.Errorf("unable to create noise transport: %w", err) + } + transport := &WebRTCTransport{ + rcmgr: rcmgr, + webrtcConfig: config, + privKey: privKey, + noiseTpt: noiseTpt, + localPeerId: localPeerID, + + peerConnectionTimeouts: iceTimeouts{ + Disconnect: DefaultDisconnectedTimeout, + Failed: DefaultFailedTimeout, + Keepalive: DefaultKeepaliveTimeout, + }, + + maxInFlightConnections: DefaultMaxInFlightConnections, + } + for _, opt := range opts { + if err := opt(transport); err != nil { + return nil, err + } + } + return transport, nil +} + +func (t *WebRTCTransport) Protocols() []int { + return []int{ma.P_WEBRTC} +} + +func (t *WebRTCTransport) Proxy() bool { + return false +} + +func (t *WebRTCTransport) CanDial(addr ma.Multiaddr) bool { + return dialMatcher.Matches(addr) +} + +var webRTCMultiAddr = ma.StringCast("/webrtc") + +func (t *WebRTCTransport) Listen(addr ma.Multiaddr) (tpt.Listener, error) { + addr, wrtcComponent := ma.SplitLast(addr) + isWebrtc := wrtcComponent.Equal(webRTCMultiAddr) + if !isWebrtc { + return nil, fmt.Errorf("must listen on webrtc multiaddr") + } + nw, host, err := manet.DialArgs(addr) + if err != nil { + return nil, fmt.Errorf("listener could not fetch dialargs: %w", err) + } + udpAddr, err := net.ResolveUDPAddr(nw, host) + if err != nil { + return nil, fmt.Errorf("listener could not resolve udp address: %w", err) + } + + socket, err := net.ListenUDP(nw, udpAddr) + if err != nil { + return nil, fmt.Errorf("listen on udp: %w", err) + } + + listener, err := t.listenSocket(socket) + if err != nil { + socket.Close() + return nil, err + } + return listener, nil +} + +func (t *WebRTCTransport) listenSocket(socket *net.UDPConn) (tpt.Listener, error) { + listenerMultiaddr, err := manet.FromNetAddr(socket.LocalAddr()) + if err != nil { + return nil, err + } + + listenerFingerprint, err := t.getCertificateFingerprint() + if err != nil { + return nil, err + } + + encodedLocalFingerprint, err := internal.EncodeDTLSFingerprint(listenerFingerprint) + if err != nil { + return nil, err + } + + certMultiaddress, err := ma.NewMultiaddr(fmt.Sprintf("/webrtc/certhash/%s", encodedLocalFingerprint)) + if err != nil { + return nil, err + } + + listenerMultiaddr = listenerMultiaddr.Encapsulate(certMultiaddress) + + return newListener( + t, + listenerMultiaddr, + socket, + t.webrtcConfig, + ) +} + +func (t *WebRTCTransport) Dial(ctx context.Context, remoteMultiaddr ma.Multiaddr, p peer.ID) (tpt.CapableConn, error) { + scope, err := t.rcmgr.OpenConnection(network.DirOutbound, false, remoteMultiaddr) + if err != nil { + return nil, err + } + err = scope.SetPeer(p) + if err != nil { + scope.Done() + return nil, err + } + conn, err := t.dial(ctx, scope, remoteMultiaddr, p) + if err != nil { + scope.Done() + return nil, err + } + return conn, nil +} + +func (t *WebRTCTransport) dial( + ctx context.Context, + scope network.ConnManagementScope, + remoteMultiaddr ma.Multiaddr, + p peer.ID, +) (tConn tpt.CapableConn, err error) { + var pc *webrtc.PeerConnection + defer func() { + if err != nil { + if pc != nil { + _ = pc.Close() + } + if tConn != nil { + _ = tConn.Close() + } + } + }() + + remoteMultihash, err := internal.DecodeRemoteFingerprint(remoteMultiaddr) + if err != nil { + return nil, fmt.Errorf("decode fingerprint: %w", err) + } + remoteHashFunction, ok := internal.GetSupportedSDPHash(remoteMultihash.Code) + if !ok { + return nil, fmt.Errorf("unsupported hash function: %w", nil) + } + + rnw, rhost, err := manet.DialArgs(remoteMultiaddr) + if err != nil { + return nil, fmt.Errorf("generate dial args: %w", err) + } + + raddr, err := net.ResolveUDPAddr(rnw, rhost) + if err != nil { + return nil, fmt.Errorf("resolve udp address: %w", err) + } + + // Instead of encoding the local fingerprint we + // generate a random UUID as the connection ufrag. + // The only requirement here is that the ufrag and password + // must be equal, which will allow the server to determine + // the password using the STUN message. + ufrag := genUfrag() + + settingEngine := webrtc.SettingEngine{} + // suppress pion logs + loggerFactory := pionlogger.NewDefaultLoggerFactory() + loggerFactory.DefaultLogLevel = pionlogger.LogLevelDisabled + settingEngine.LoggerFactory = loggerFactory + + settingEngine.SetICECredentials(ufrag, ufrag) + settingEngine.DetachDataChannels() + settingEngine.SetICETimeouts( + t.peerConnectionTimeouts.Disconnect, + t.peerConnectionTimeouts.Failed, + t.peerConnectionTimeouts.Keepalive, + ) + settingEngine.SetIncludeLoopbackCandidate(true) + + api := webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine)) + + pc, err = api.NewPeerConnection(t.webrtcConfig) + if err != nil { + return nil, fmt.Errorf("instantiate peerconnection: %w", err) + } + + errC := awaitPeerConnectionOpen(ufrag, pc) + // We need to set negotiated = true for this channel on both + // the client and server to avoid DCEP errors. + negotiated, id := handshakeChannelNegotiated, handshakeChannelID + rawHandshakeChannel, err := pc.CreateDataChannel("", &webrtc.DataChannelInit{ + Negotiated: &negotiated, + ID: &id, + }) + if err != nil { + return nil, fmt.Errorf("create datachannel: %w", err) + } + + // do offer-answer exchange + offer, err := pc.CreateOffer(nil) + if err != nil { + return nil, fmt.Errorf("create offer: %w", err) + } + + err = pc.SetLocalDescription(offer) + if err != nil { + return nil, fmt.Errorf("set local description: %w", err) + } + + answerSDPString, err := internal.RenderServerSDP(raddr, ufrag, *remoteMultihash) + if err != nil { + return nil, fmt.Errorf("render server SDP: %w", err) + } + + answer := webrtc.SessionDescription{SDP: answerSDPString, Type: webrtc.SDPTypeAnswer} + err = pc.SetRemoteDescription(answer) + if err != nil { + return nil, fmt.Errorf("set remote description: %w", err) + } + + // await peerconnection opening + select { + case err := <-errC: + if err != nil { + return nil, err + } + case <-ctx.Done(): + return nil, errors.New("peerconnection opening timed out") + } + + detached, err := getDetachedChannel(ctx, rawHandshakeChannel) + if err != nil { + return nil, err + } + // set the local address from the candidate pair + cp, err := rawHandshakeChannel.Transport().Transport().ICETransport().GetSelectedCandidatePair() + if cp == nil { + return nil, errors.New("ice connection did not have selected candidate pair: nil result") + } + if err != nil { + return nil, fmt.Errorf("ice connection did not have selected candidate pair: error: %w", err) + } + laddr := &net.UDPAddr{IP: net.ParseIP(cp.Local.Address), Port: int(cp.Local.Port)} + + channel := newStream(nil, rawHandshakeChannel, detached, laddr, raddr) + // the local address of the selected candidate pair should be the + // local address for the connection, since different datachannels + // are multiplexed over the same SCTP connection + localAddr, err := manet.FromNetAddr(channel.LocalAddr()) + if err != nil { + return nil, err + } + + // we can only know the remote public key after the noise handshake, + // but need to set up the callbacks on the peerconnection + conn, err := NewWebRTCConnection( + network.DirOutbound, + pc, + t, + scope, + t.localPeerId, + localAddr, + p, + nil, + remoteMultiaddr, + ) + if err != nil { + return nil, err + } + + secConn, err := t.noiseHandshake(ctx, pc, channel, p, remoteHashFunction, false) + if err != nil { + return conn, err + } + conn.setRemotePublicKey(secConn.RemotePublicKey()) + return conn, nil +} + +func genUfrag() string { + const ( + uFragAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" + uFragPrefix = "libp2p+webrtc+v1/" + uFragIdLength = 32 + uFragIdOffset = len(uFragPrefix) + uFragLength = uFragIdOffset + uFragIdLength + ) + + b := make([]byte, uFragLength) + copy(b[:], uFragPrefix[:]) + rand.Read(b[uFragIdOffset:]) + for i := uFragIdOffset; i < uFragLength; i++ { + b[i] = uFragAlphabet[int(b[i])%len(uFragAlphabet)] + } + return string(b) +} + +func (t *WebRTCTransport) getCertificateFingerprint() (webrtc.DTLSFingerprint, error) { + fps, err := t.webrtcConfig.Certificates[0].GetFingerprints() + if err != nil { + return webrtc.DTLSFingerprint{}, err + } + return fps[0], nil +} + +func (t *WebRTCTransport) generateNoisePrologue(pc *webrtc.PeerConnection, hash crypto.Hash, inbound bool) ([]byte, error) { + raw := pc.SCTP().Transport().GetRemoteCertificate() + cert, err := x509.ParseCertificate(raw) + if err != nil { + return nil, err + } + + // NOTE: should we want we can fork the cert code as well to avoid + // all the extra allocations due to unneeded string interpersing (hex) + localFp, err := t.getCertificateFingerprint() + if err != nil { + return nil, err + } + + remoteFpBytes, err := internal.Fingerprint(cert, hash) + if err != nil { + return nil, err + } + + localFpBytes, err := encoding.DecodeInterpersedHexFromASCIIString(localFp.Value) + if err != nil { + return nil, err + } + + localEncoded, err := multihash.Encode(localFpBytes, multihash.SHA2_256) + if err != nil { + log.Debugf("could not encode multihash for local fingerprint") + return nil, err + } + remoteEncoded, err := multihash.Encode(remoteFpBytes, multihash.SHA2_256) + if err != nil { + log.Debugf("could not encode multihash for remote fingerprint") + return nil, err + } + + result := []byte("libp2p-webrtc-noise:") + if inbound { + result = append(result, remoteEncoded...) + result = append(result, localEncoded...) + } else { + result = append(result, localEncoded...) + result = append(result, remoteEncoded...) + } + return result, nil +} + +func (t *WebRTCTransport) noiseHandshake(ctx context.Context, pc *webrtc.PeerConnection, datachannel *webRTCStream, peer peer.ID, hash crypto.Hash, inbound bool) (sec.SecureConn, error) { + prologue, err := t.generateNoisePrologue(pc, hash, inbound) + if err != nil { + return nil, fmt.Errorf("generate prologue: %w", err) + } + sessionTransport, err := t.noiseTpt.WithSessionOptions( + noise.Prologue(prologue), + noise.DisablePeerIDCheck(), + ) + if err != nil { + return nil, fmt.Errorf("instantiate transport: %w", err) + } + var secureConn sec.SecureConn + if inbound { + secureConn, err = sessionTransport.SecureOutbound(ctx, datachannel, peer) + if err != nil { + err = fmt.Errorf("failed to secure inbound [noise outbound]: %w %v", err, ctx.Value("id")) + return secureConn, err + } + } else { + secureConn, err = sessionTransport.SecureInbound(ctx, datachannel, peer) + if err != nil { + err = fmt.Errorf("failed to secure outbound [noise inbound]: %w %v", err, ctx.Value("id")) + return secureConn, err + } + } + return secureConn, nil +} diff --git a/p2p/transport/webrtc/transport_test.go b/p2p/transport/webrtc/transport_test.go new file mode 100644 index 0000000000..8d89f763ad --- /dev/null +++ b/p2p/transport/webrtc/transport_test.go @@ -0,0 +1,804 @@ +package libp2pwebrtc + +import ( + "context" + "encoding/hex" + "fmt" + "io" + "net" + "os" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + ttransport "github.com/libp2p/go-libp2p/p2p/transport/testsuite" + "github.com/multiformats/go-multiaddr" + "github.com/multiformats/go-multibase" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/sha3" +) + +func getTransport(t *testing.T, opts ...Option) (*WebRTCTransport, peer.ID) { + t.Helper() + privKey, _, err := crypto.GenerateKeyPair(crypto.Ed25519, -1) + require.NoError(t, err) + rcmgr := &network.NullResourceManager{} + transport, err := New(privKey, nil, nil, rcmgr, opts...) + require.NoError(t, err) + peerID, err := peer.IDFromPrivateKey(privKey) + require.NoError(t, err) + t.Cleanup(func() { rcmgr.Close() }) + return transport, peerID +} + +var ( + listenerIp net.IP + dialerIp net.IP +) + +func TestMain(m *testing.M) { + listenerIp, dialerIp = getListenerAndDialerIP() + os.Exit(m.Run()) +} + +func TestTransportWebRTC_CanDial(t *testing.T) { + tr, _ := getTransport(t) + invalid := []string{ + "/ip4/1.2.3.4/udp/1234/webrtc", + "/dns/test.test/udp/1234/webrtc", + } + + valid := []string{ + "/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAsGPzpiPGQzSlVHRXrUCT5EkTV7YFrV4VZ3hpEKTd_zg", + "/ip6/0:0:0:0:0:0:0:1/udp/1234/webrtc/certhash/uEiAsGPzpiPGQzSlVHRXrUCT5EkTV7YFrV4VZ3hpEKTd_zg", + "/ip6/::1/udp/1234/webrtc/certhash/uEiAsGPzpiPGQzSlVHRXrUCT5EkTV7YFrV4VZ3hpEKTd_zg", + "/dns/test.test/udp/1234/webrtc/certhash/uEiAsGPzpiPGQzSlVHRXrUCT5EkTV7YFrV4VZ3hpEKTd_zg", + } + + for _, addr := range invalid { + ma, err := multiaddr.NewMultiaddr(addr) + require.NoError(t, err) + require.Equal(t, false, tr.CanDial(ma)) + } + + for _, addr := range valid { + ma, err := multiaddr.NewMultiaddr(addr) + require.NoError(t, err) + require.Equal(t, true, tr.CanDial(ma), addr) + } +} + +func TestTransportWebRTC_ListenFailsOnNonWebRTCMultiaddr(t *testing.T) { + tr, _ := getTransport(t) + testAddrs := []string{ + "/ip4/0.0.0.0/udp/0", + "/ip4/0.0.0.0/tcp/0/wss", + } + for _, addr := range testAddrs { + listenMultiaddr, err := multiaddr.NewMultiaddr(addr) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.Error(t, err) + require.Nil(t, listener) + } +} + +// using assert inside goroutines, refer: https://github.com/stretchr/testify/issues/772#issuecomment-945166599 +func TestTransportWebRTC_DialFailsOnUnsupportedHashFunction(t *testing.T) { + tr, _ := getTransport(t) + hash := sha3.New512() + certhash := func() string { + _, err := hash.Write([]byte("test-data")) + require.NoError(t, err) + mh, err := multihash.Encode(hash.Sum([]byte{}), multihash.SHA3_512) + require.NoError(t, err) + certhash, err := multibase.Encode(multibase.Base58BTC, mh) + require.NoError(t, err) + return certhash + }() + testaddr, err := multiaddr.NewMultiaddr("/ip4/1.2.3.4/udp/1234/webrtc/certhash/" + certhash) + require.NoError(t, err) + _, err = tr.Dial(context.Background(), testaddr, "") + require.ErrorContains(t, err, "unsupported hash function") +} + +func TestTransportWebRTC_CanListenSingle(t *testing.T) { + tr, listeningPeer := getTransport(t) + tr1, connectingPeer := getTransport(t) + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + + done := make(chan struct{}) + go func() { + _, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + assert.NoError(t, err) + close(done) + }() + + conn, err := listener.Accept() + require.NoError(t, err) + require.NotNil(t, conn) + + require.Equal(t, connectingPeer, conn.RemotePeer()) + select { + case <-done: + case <-time.After(10 * time.Second): + t.FailNow() + } + +} + +func TestTransportWebRTC_CanListenMultiple(t *testing.T) { + count := 3 + tr, listeningPeer := getTransport(t, WithListenerMaxInFlightConnections(uint32(count))) + + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var wg sync.WaitGroup + go func() { + for i := 0; i < count; i++ { + conn, err := listener.Accept() + assert.NoError(t, err) + assert.NotNil(t, conn) + } + wg.Wait() + cancel() + }() + + for i := 0; i < count; i++ { + wg.Add(1) + go func() { + defer wg.Done() + ctr, _ := getTransport(t) + conn, err := ctr.Dial(ctx, listener.Multiaddr(), listeningPeer) + select { + case <-ctx.Done(): + default: + assert.NoError(t, err) + assert.NotNil(t, conn) + } + }() + } + + select { + case <-ctx.Done(): + case <-time.After(30 * time.Second): + t.Fatalf("timed out") + } + +} + +func TestTransportWebRTC_CanCreateSuccessiveConnections(t *testing.T) { + tr, listeningPeer := getTransport(t) + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + count := 2 + + go func() { + for i := 0; i < count; i++ { + ctr, _ := getTransport(t) + conn, err := ctr.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + require.Equal(t, conn.RemotePeer(), listeningPeer) + } + }() + + for i := 0; i < count; i++ { + _, err := listener.Accept() + require.NoError(t, err) + } +} + +func TestTransportWebRTC_ListenerCanCreateStreams(t *testing.T) { + tr, listeningPeer := getTransport(t) + tr1, connectingPeer := getTransport(t) + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + + streamChan := make(chan network.MuxedStream) + go func() { + conn, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + t.Logf("connection opened by dialer") + stream, err := conn.AcceptStream() + require.NoError(t, err) + t.Logf("dialer accepted stream") + streamChan <- stream + }() + + conn, err := listener.Accept() + require.NoError(t, err) + t.Logf("listener accepted connection") + require.Equal(t, connectingPeer, conn.RemotePeer()) + + stream, err := conn.OpenStream(context.Background()) + require.NoError(t, err) + t.Logf("listener opened stream") + _, err = stream.Write([]byte("test")) + require.NoError(t, err) + + var str network.MuxedStream + select { + case str = <-streamChan: + case <-time.After(3 * time.Second): + t.Fatal("stream opening timed out") + } + buf := make([]byte, 100) + stream.SetReadDeadline(time.Now().Add(3 * time.Second)) + n, err := str.Read(buf) + require.NoError(t, err) + require.Equal(t, "test", string(buf[:n])) + +} + +func TestTransportWebRTC_DialerCanCreateStreams(t *testing.T) { + tr, listeningPeer := getTransport(t) + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + + tr1, connectingPeer := getTransport(t) + done := make(chan struct{}) + + go func() { + lconn, err := listener.Accept() + require.NoError(t, err) + require.Equal(t, connectingPeer, lconn.RemotePeer()) + + stream, err := lconn.AcceptStream() + require.NoError(t, err) + buf := make([]byte, 100) + n, err := stream.Read(buf) + require.NoError(t, err) + require.Equal(t, "test", string(buf[:n])) + + done <- struct{}{} + }() + + go func() { + conn, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + t.Logf("dialer opened connection") + stream, err := conn.OpenStream(context.Background()) + require.NoError(t, err) + t.Logf("dialer opened stream") + _, err = stream.Write([]byte("test")) + require.NoError(t, err) + }() + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("timed out") + } + +} + +func TestTransportWebRTC_DialerCanCreateStreamsMultiple(t *testing.T) { + count := 5 + tr, listeningPeer := getTransport(t) + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + + tr1, connectingPeer := getTransport(t) + done := make(chan struct{}) + + go func() { + lconn, err := listener.Accept() + require.NoError(t, err) + require.Equal(t, connectingPeer, lconn.RemotePeer()) + var wg sync.WaitGroup + + for i := 0; i < count; i++ { + stream, err := lconn.AcceptStream() + require.NoError(t, err) + wg.Add(1) + go func() { + defer wg.Done() + buf := make([]byte, 100) + n, err := stream.Read(buf) + require.NoError(t, err) + require.Equal(t, "test", string(buf[:n])) + _, err = stream.Write([]byte("test")) + require.NoError(t, err) + }() + } + + wg.Wait() + done <- struct{}{} + }() + + conn, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + t.Logf("dialer opened connection") + + for i := 0; i < count; i++ { + idx := i + go func() { + stream, err := conn.OpenStream(context.Background()) + require.NoError(t, err) + t.Logf("dialer opened stream: %d", idx) + buf := make([]byte, 100) + _, err = stream.Write([]byte("test")) + require.NoError(t, err) + n, err := stream.Read(buf) + require.NoError(t, err) + require.Equal(t, "test", string(buf[:n])) + }() + if i%10 == 0 && i > 0 { + time.Sleep(100 * time.Millisecond) + } + } + select { + case <-done: + case <-time.After(100 * time.Second): + t.Fatal("timed out") + } +} + +func TestTransportWebRTC_Deadline(t *testing.T) { + tr, listeningPeer := getTransport(t) + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + tr1, connectingPeer := getTransport(t) + + t.Run("SetReadDeadline", func(t *testing.T) { + go func() { + lconn, err := listener.Accept() + require.NoError(t, err) + require.Equal(t, connectingPeer, lconn.RemotePeer()) + _, err = lconn.AcceptStream() + require.NoError(t, err) + }() + + conn, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + stream, err := conn.OpenStream(context.Background()) + require.NoError(t, err) + + // deadline set to the past + stream.SetReadDeadline(time.Now().Add(-200 * time.Millisecond)) + _, err = stream.Read([]byte{0, 0}) + require.ErrorIs(t, err, os.ErrDeadlineExceeded) + + // future deadline exceeded + stream.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + _, err = stream.Read([]byte{0, 0}) + require.ErrorIs(t, err, os.ErrDeadlineExceeded) + }) + + t.Run("SetWriteDeadline", func(t *testing.T) { + go func() { + lconn, err := listener.Accept() + require.NoError(t, err) + require.Equal(t, connectingPeer, lconn.RemotePeer()) + _, err = lconn.AcceptStream() + require.NoError(t, err) + }() + + conn, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + stream, err := conn.OpenStream(context.Background()) + require.NoError(t, err) + + stream.SetWriteDeadline(time.Now().Add(200 * time.Millisecond)) + largeBuffer := make([]byte, 2*1024*1024) + _, err = stream.Write(largeBuffer) + require.ErrorIs(t, err, os.ErrDeadlineExceeded) + }) +} + +func TestTransportWebRTC_StreamWriteBufferContention(t *testing.T) { + tr, listeningPeer := getTransport(t) + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + + tr1, connectingPeer := getTransport(t) + + for i := 0; i < 2; i++ { + go func() { + lconn, err := listener.Accept() + require.NoError(t, err) + require.Equal(t, connectingPeer, lconn.RemotePeer()) + _, err = lconn.AcceptStream() + require.NoError(t, err) + }() + + } + + conn, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + + errC := make(chan error) + // writers + for i := 0; i < 2; i++ { + go func() { + stream, err := conn.OpenStream(context.Background()) + require.NoError(t, err) + + stream.SetWriteDeadline(time.Now().Add(200 * time.Millisecond)) + largeBuffer := make([]byte, 2*1024*1024) + _, err = stream.Write(largeBuffer) + errC <- err + }() + } + + require.ErrorIs(t, <-errC, os.ErrDeadlineExceeded) + require.ErrorIs(t, <-errC, os.ErrDeadlineExceeded) + +} + +func TestTransportWebRTC_Read(t *testing.T) { + tr, listeningPeer := getTransport(t) + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + + tr1, connectingPeer := getTransport(t) + + createListener := func() { + lconn, err := listener.Accept() + require.NoError(t, err) + require.Equal(t, connectingPeer, lconn.RemotePeer()) + stream, err := lconn.AcceptStream() + require.NoError(t, err) + _, err = stream.Write(make([]byte, 2*1024*1024)) + // we expect an error in both cases + require.Error(t, err) + } + + t.Run("read partial message", func(t *testing.T) { + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + createListener() + }() + + conn, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + stream, err := conn.OpenStream(context.Background()) + require.NoError(t, err) + + buf := make([]byte, 10) + stream.SetReadDeadline(time.Now().Add(10 * time.Second)) + n, err := stream.Read(buf) + require.NoError(t, err) + require.Equal(t, n, 10) + + wg.Wait() + }) + + t.Run("read zero bytes", func(t *testing.T) { + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + createListener() + }() + + conn, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + stream, err := conn.OpenStream(context.Background()) + require.NoError(t, err) + + stream.SetReadDeadline(time.Now().Add(10 * time.Second)) + n, err := stream.Read([]byte{}) + require.NoError(t, err) + require.Equal(t, n, 0) + + wg.Wait() + }) +} + +func TestTransportWebRTC_Close(t *testing.T) { + tr, listeningPeer := getTransport(t) + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + + tr1, connectingPeer := getTransport(t) + + t.Run("StreamCanCloseWhenReadActive", func(t *testing.T) { + done := make(chan struct{}) + + go func() { + lconn, err := listener.Accept() + require.NoError(t, err) + t.Logf("listener accepted connection") + require.Equal(t, connectingPeer, lconn.RemotePeer()) + done <- struct{}{} + }() + + conn, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + t.Logf("dialer opened connection") + stream, err := conn.OpenStream(context.Background()) + require.NoError(t, err) + + time.AfterFunc(100*time.Millisecond, func() { + err := stream.Close() + require.NoError(t, err) + }) + + _, err = stream.Read(make([]byte, 19)) + require.ErrorIs(t, err, os.ErrDeadlineExceeded) + + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("timed out") + } + }) + + t.Run("RemoteClosesStream", func(t *testing.T) { + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + lconn, err := listener.Accept() + require.NoError(t, err) + require.Equal(t, connectingPeer, lconn.RemotePeer()) + stream, err := lconn.AcceptStream() + require.NoError(t, err) + time.Sleep(100 * time.Millisecond) + _ = stream.Close() + + }() + + buf := make([]byte, 2) + + conn, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + stream, err := conn.OpenStream(context.Background()) + require.NoError(t, err) + + err = stream.SetReadDeadline(time.Now().Add(2 * time.Second)) + require.NoError(t, err) + _, err = stream.Read(buf) + require.ErrorIs(t, err, io.ErrClosedPipe) + + wg.Wait() + }) +} + +func TestTransportWebRTC_ReceiveFlagsAfterReadClosed(t *testing.T) { + tr, listeningPeer := getTransport(t) + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + + tr1, connectingPeer := getTransport(t) + done := make(chan struct{}) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + + lconn, err := listener.Accept() + require.NoError(t, err) + t.Logf("listener accepted connection") + require.Equal(t, connectingPeer, lconn.RemotePeer()) + stream, err := lconn.AcceptStream() + require.NoError(t, err) + n, err := stream.Read(make([]byte, 10)) + require.NoError(t, err) + require.Equal(t, 10, n) + // stop reader + err = stream.Reset() + require.NoError(t, err) + done <- struct{}{} + }() + + conn, err := tr1.Dial(context.Background(), listener.Multiaddr(), listeningPeer) + require.NoError(t, err) + t.Logf("dialer opened connection") + stream, err := conn.OpenStream(context.Background()) + require.NoError(t, err) + + err = stream.CloseRead() + require.NoError(t, err) + _, err = stream.Read([]byte{0}) + require.ErrorIs(t, err, io.EOF) + _, err = stream.Write(make([]byte, 10)) + require.NoError(t, err) + <-done + _, err = stream.Write(make([]byte, 2*1024*1024)) + // can be an error closed or timeout + require.Error(t, err) + + wg.Wait() +} + +func TestTransportWebRTC_PeerConnectionDTLSFailed(t *testing.T) { + // test multihash + encoded, err := hex.DecodeString("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") + require.NoError(t, err) + + testMultihash := &multihash.DecodedMultihash{ + Code: multihash.SHA2_256, + Name: multihash.Codes[multihash.SHA2_256], + Digest: encoded, + Length: len(encoded), + } + + tr, listeningPeer := getTransport(t) + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + + tr1, _ := getTransport(t) + + go listener.Accept() + + badMultiaddr, _ := multiaddr.SplitFunc(listener.Multiaddr(), func(component multiaddr.Component) bool { + return component.Protocol().Code == multiaddr.P_CERTHASH + }) + + encodedCerthash, err := multihash.Encode(testMultihash.Digest, testMultihash.Code) + require.NoError(t, err) + badEncodedCerthash, err := multibase.Encode(multibase.Base58BTC, encodedCerthash) + require.NoError(t, err) + badCerthash, err := multiaddr.NewMultiaddr(fmt.Sprintf("/certhash/%s", badEncodedCerthash)) + require.NoError(t, err) + badMultiaddr = badMultiaddr.Encapsulate(badCerthash) + + conn, err := tr1.Dial(context.Background(), badMultiaddr, listeningPeer) + require.Nil(t, conn) + t.Log(err) + require.Error(t, err) + + require.ErrorContains(t, err, "failed") +} + +func TestTransportWebRTC_StreamResetOnPeerConnectionFailure(t *testing.T) { + tr, listeningPeer := getTransport(t) + tr.peerConnectionTimeouts.Disconnect = 2 * time.Second + tr.peerConnectionTimeouts.Failed = 3 * time.Second + tr.peerConnectionTimeouts.Keepalive = time.Second + + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + lsnr, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + + tr1, connectingPeer := getTransport(t) + tr1.peerConnectionTimeouts.Disconnect = 2 * time.Second + tr1.peerConnectionTimeouts.Failed = 3 * time.Second + tr1.peerConnectionTimeouts.Keepalive = time.Second + + done := make(chan struct{}) + go func() { + lconn, err := lsnr.Accept() + require.NoError(t, err) + require.Equal(t, connectingPeer, lconn.RemotePeer()) + + stream, err := lconn.AcceptStream() + require.NoError(t, err) + _, err = stream.Write([]byte("test")) + require.NoError(t, err) + // force close the mux + lsnr.(*listener).mux.Close() + // stream.Write can keep buffering data until failure, + // so we need to loop on writing. + for { + _, err := stream.Write([]byte("test")) + if err != nil { + assert.ErrorIs(t, err, os.ErrDeadlineExceeded) + close(done) + return + } + } + }() + + dialctx, dialcancel := context.WithTimeout(context.Background(), 10*time.Second) + defer dialcancel() + conn, err := tr1.Dial(dialctx, lsnr.Multiaddr(), listeningPeer) + require.NoError(t, err) + stream, err := conn.OpenStream(dialctx) + require.NoError(t, err) + _, err = io.ReadAll(stream) + require.Error(t, err) + + select { + case <-done: + case <-time.After(30 * time.Second): + t.Fatal("timed out") + } +} + +func TestTransportWebRTC_MaxInFlightRequests(t *testing.T) { + count := uint32(3) + tr, listeningPeer := getTransport(t, + WithListenerMaxInFlightConnections(count), + ) + tr.peerConnectionTimeouts.Disconnect = 8 * time.Second + tr.peerConnectionTimeouts.Failed = 10 * time.Second + tr.peerConnectionTimeouts.Keepalive = 5 * time.Second + listenMultiaddr, err := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp)) + require.NoError(t, err) + listener, err := tr.Listen(listenMultiaddr) + require.NoError(t, err) + defer listener.Close() + + dialerCount := count + 2 + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var wg sync.WaitGroup + start := make(chan struct{}) + ready := make(chan struct{}, dialerCount) + var success uint32 + for i := 0; uint32(i) < dialerCount; i++ { + wg.Add(1) + go func() { + defer wg.Done() + dialer, _ := getTransport(t) + dialer.peerConnectionTimeouts.Disconnect = 8 * time.Second + dialer.peerConnectionTimeouts.Failed = 10 * time.Second + dialer.peerConnectionTimeouts.Keepalive = 5 * time.Second + ready <- struct{}{} + <-start + _, err := dialer.Dial(ctx, listener.Multiaddr(), listeningPeer) + if err == nil { + atomic.AddUint32(&success, 1) + } else { + t.Logf("dial error: %v", err) + } + }() + } + + for i := 0; uint32(i) < dialerCount; i++ { + <-ready + } + close(start) + wg.Wait() + successCount := atomic.LoadUint32(&success) + require.Equal(t, count, successCount) +} + +// TestWebrtcTransport implements the standard go-libp2p transport test. +// It's a test that however not works for many transports, and neither does it for WebRTC. +// +// Reason it doens't work for WebRTC is that it opens too many streams too rapidly, which +// in a regular environment would be seen as an attack that we wish to block/stop. +// +// Leaving it here for now only for documentation purposes, +// and to make it explicitly clear this test doesn't work for WebRTC. +func TestWebrtcTransport(t *testing.T) { + t.Skip("This test does not work for WebRTC due to the way it is setup, see comments for more explanation") + ta, _ := getTransport(t) + tb, _ := getTransport(t) + ttransport.SubtestTransport(t, ta, tb, fmt.Sprintf("/ip4/%s/udp/0/webrtc", listenerIp), "peerA") +} diff --git a/p2p/transport/webrtc/udpmux/mux.go b/p2p/transport/webrtc/udpmux/mux.go new file mode 100644 index 0000000000..ab957591a1 --- /dev/null +++ b/p2p/transport/webrtc/udpmux/mux.go @@ -0,0 +1,293 @@ +package udpmux + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net" + "sync" + + logging "github.com/ipfs/go-log/v2" + pool "github.com/libp2p/go-buffer-pool" + "github.com/pion/ice/v2" + "github.com/pion/stun" +) + +var log = logging.Logger("mux") + +var ( + errConnNotFound = errors.New("connection not found") +) + +const ReceiveMTU = 1500 + +var _ ice.UDPMux = &udpMux{} + +type ufragConnKey struct { + ufrag string + isIPv6 bool +} + +// udpMux multiplexes multiple ICE connections over a single net.PacketConn, +// generally a UDP socket. +// +// The connections are indexed by (ufrag, IP address family) +// and by remote address from which the connection has received valid STUN/RTC +// packets. +// +// When a new packet is received on the underlying net.PacketConn, we +// first check the address map to see if there is a connection associated with the +// remote address. If found we forward the packet to the connection. If an associated +// connection is not found, we check to see if the packet is a STUN packet. We then +// fetch the ufrag of the remote from the STUN packet and use it to check if there +// is a connection associated with the (ufrag, IP address family) pair. If found +// we add the association to the address map. If not found, it is a previously +// unseen IP address and the `unknownUfragCallback` callback is invoked. +type udpMux struct { + socket net.PacketConn + unknownUfragCallback func(string, net.Addr) bool + + storage *udpMuxStorage + + // the context controls the lifecycle of the mux + wg sync.WaitGroup + ctx context.Context + cancel context.CancelFunc +} + +func NewUDPMux(socket net.PacketConn, unknownUfragCallback func(string, net.Addr) bool) *udpMux { + ctx, cancel := context.WithCancel(context.Background()) + mux := &udpMux{ + ctx: ctx, + cancel: cancel, + socket: socket, + unknownUfragCallback: unknownUfragCallback, + storage: newUDPMuxStorage(), + } + + mux.wg.Add(1) + go func() { + defer mux.wg.Done() + mux.readLoop() + }() + return mux +} + +// GetListenAddresses implements ice.UDPMux +func (mux *udpMux) GetListenAddresses() []net.Addr { + return []net.Addr{mux.socket.LocalAddr()} +} + +// GetConn implements ice.UDPMux +// It creates a net.PacketConn for a given ufrag if an existing +// one cannot be found. We differentiate IPv4 and IPv6 addresses +// as a remote is capable of being reachable through multiple different +// UDP addresses of the same IP address family (eg. Server-reflexive addresses +// and peer-reflexive addresses). +func (mux *udpMux) GetConn(ufrag string, addr net.Addr) (net.PacketConn, error) { + a, ok := addr.(*net.UDPAddr) + if !ok && addr != nil { + return nil, fmt.Errorf("unexpected address type: %T", addr) + } + isIPv6 := ok && a.IP.To4() == nil + return mux.getOrCreateConn(ufrag, isIPv6, addr) +} + +// Close implements ice.UDPMux +func (mux *udpMux) Close() error { + select { + case <-mux.ctx.Done(): + return nil + default: + } + mux.cancel() + mux.socket.Close() + mux.wg.Wait() + return nil +} + +// RemoveConnByUfrag implements ice.UDPMux +func (mux *udpMux) RemoveConnByUfrag(ufrag string) { + if ufrag != "" { + mux.storage.RemoveConnByUfrag(ufrag) + } +} + +func (mux *udpMux) getOrCreateConn(ufrag string, isIPv6 bool, addr net.Addr) (net.PacketConn, error) { + select { + case <-mux.ctx.Done(): + return nil, io.ErrClosedPipe + default: + conn, _, err := mux.storage.GetOrCreateConn(ufrag, isIPv6, mux, addr) + return conn, err + } +} + +// writeTo writes a packet to the underlying net.PacketConn +func (mux *udpMux) writeTo(buf []byte, addr net.Addr) (int, error) { + return mux.socket.WriteTo(buf, addr) +} + +func (mux *udpMux) readLoop() { + for { + select { + case <-mux.ctx.Done(): + return + default: + } + + buf := pool.Get(ReceiveMTU) + + n, addr, err := mux.socket.ReadFrom(buf) + if err != nil { + log.Errorf("error reading from socket: %v", err) + pool.Put(buf) + return + } + buf = buf[:n] + + // a non-nil error signifies that the packet was not + // passed on to any connection, and therefore the current + // function has ownership of the packet. Otherwise, the + // ownership of the packet is passed to a connection + if err := mux.processPacket(buf, addr); err != nil { + pool.Put(buf) + } + } +} + +func (mux *udpMux) processPacket(buf []byte, addr net.Addr) error { + udpAddr, ok := addr.(*net.UDPAddr) + if !ok { + return fmt.Errorf("underlying connection did not return a UDP address") + } + isIPv6 := udpAddr.IP.To4() == nil + + // Connections are indexed by remote address. We first + // check if the remote address has a connection associated + // with it. If yes, we push the received packet to the connection + if conn, ok := mux.storage.GetConnByAddr(udpAddr); ok { + err := conn.Push(buf) + if err != nil { + log.Errorf("could not push packet: %v", err) + } + return nil + } + + if !stun.IsMessage(buf) { + return errConnNotFound + } + + msg := &stun.Message{Raw: buf} + if err := msg.Decode(); err != nil || msg.Type != stun.BindingRequest { + log.Debug("incoming message should be a STUN binding request") + return err + } + + ufrag, err := ufragFromSTUNMessage(msg) + if err != nil { + log.Debug("could not find STUN username: %w", err) + return err + } + + var connCreated bool + conn, connCreated, err := mux.storage.GetOrCreateConn(ufrag, isIPv6, mux, udpAddr) + if err != nil { + log.Debug("could not find create conn: %w", err) + return err + } + if connCreated && mux.unknownUfragCallback != nil { + if !mux.unknownUfragCallback(ufrag, udpAddr) { + conn.Close() + return io.ErrClosedPipe + } + } + + if err := conn.Push(buf); err != nil { + log.Errorf("could not push packet: %v", err) + } + return nil +} + +// ufragFromSTUNMessage returns the local or ufrag +// from the STUN username attribute. Local ufrag is the ufrag of the +// peer which initiated the connectivity check, e.g in a connectivity +// check from A to B, the username attribute will be B_ufrag:A_ufrag +// with the local ufrag value being A_ufrag. In case of ice-lite, the +// localUfrag value will always be the remote peer's ufrag since ICE-lite +// implementations do not generate connectivity checks. In our specific +// case, since the local and remote ufrag is equal, we can return +// either value. +func ufragFromSTUNMessage(msg *stun.Message) (string, error) { + attr, err := msg.Get(stun.AttrUsername) + if err != nil { + return "", err + } + index := bytes.Index(attr, []byte{':'}) + if index == -1 { + return "", fmt.Errorf("invalid STUN username attribute") + } + return string(attr[index+1:]), nil +} + +type udpMuxStorage struct { + sync.Mutex + + ufragMap map[ufragConnKey]*muxedConnection + addrMap map[string]*muxedConnection +} + +func newUDPMuxStorage() *udpMuxStorage { + return &udpMuxStorage{ + ufragMap: make(map[ufragConnKey]*muxedConnection), + addrMap: make(map[string]*muxedConnection), + } +} + +func (s *udpMuxStorage) RemoveConnByUfrag(ufrag string) { + s.Lock() + defer s.Unlock() + + for _, isIPv6 := range [...]bool{true, false} { + key := ufragConnKey{ufrag: ufrag, isIPv6: isIPv6} + if conn, ok := s.ufragMap[key]; ok { + _ = conn.closeConnection() + delete(s.ufragMap, key) + delete(s.addrMap, conn.Address().String()) + } + } +} + +func (s *udpMuxStorage) GetConn(ufrag string, isIPv6 bool) (*muxedConnection, bool) { + key := ufragConnKey{ufrag: ufrag, isIPv6: isIPv6} + s.Lock() + conn, ok := s.ufragMap[key] + s.Unlock() + return conn, ok +} + +func (s *udpMuxStorage) GetOrCreateConn(ufrag string, isIPv6 bool, mux *udpMux, addr net.Addr) (*muxedConnection, bool, error) { + key := ufragConnKey{ufrag: ufrag, isIPv6: isIPv6} + + s.Lock() + defer s.Unlock() + + if conn, ok := s.ufragMap[key]; ok { + return conn, false, nil + } + + conn := newMuxedConnection(mux, ufrag, addr) + s.ufragMap[key] = conn + s.addrMap[addr.String()] = conn + + return conn, true, nil +} + +func (s *udpMuxStorage) GetConnByAddr(addr *net.UDPAddr) (*muxedConnection, bool) { + s.Lock() + conn, ok := s.addrMap[addr.String()] + s.Unlock() + return conn, ok +} diff --git a/p2p/transport/webrtc/udpmux/mux_test.go b/p2p/transport/webrtc/udpmux/mux_test.go new file mode 100644 index 0000000000..5d7fc7960d --- /dev/null +++ b/p2p/transport/webrtc/udpmux/mux_test.go @@ -0,0 +1,87 @@ +package udpmux + +import ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +var _ net.PacketConn = dummyPacketConn{} + +type dummyPacketConn struct{} + +// Close implements net.PacketConn +func (dummyPacketConn) Close() error { + return nil +} + +// LocalAddr implements net.PacketConn +func (dummyPacketConn) LocalAddr() net.Addr { + return nil +} + +// ReadFrom implements net.PacketConn +func (dummyPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + return 0, &net.UDPAddr{}, nil +} + +// SetDeadline implements net.PacketConn +func (dummyPacketConn) SetDeadline(t time.Time) error { + return nil +} + +// SetReadDeadline implements net.PacketConn +func (dummyPacketConn) SetReadDeadline(t time.Time) error { + return nil +} + +// SetWriteDeadline implements net.PacketConn +func (dummyPacketConn) SetWriteDeadline(t time.Time) error { + return nil +} + +// WriteTo implements net.PacketConn +func (dummyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + return 0, nil +} + +func hasConn(m *udpMux, ufrag string, isIPv6 bool) *muxedConnection { + conn, _ := m.storage.GetConn(ufrag, isIPv6) + return conn +} + +var ( + addrV4 = net.UDPAddr{IP: net.IPv4zero, Port: 1234} + addrV6 = net.UDPAddr{IP: net.IPv6zero, Port: 1234} +) + +func TestUDPMux_GetConn(t *testing.T) { + m := NewUDPMux(dummyPacketConn{}, nil) + require.Nil(t, hasConn(m, "test", false)) + conn, err := m.GetConn("test", &addrV4) + require.NoError(t, err) + require.NotNil(t, conn) + + require.Nil(t, hasConn(m, "test", true)) + connv6, err := m.GetConn("test", &addrV6) + require.NoError(t, err) + require.NotNil(t, connv6) + + require.NotEqual(t, conn, connv6) +} + +func TestUDPMux_RemoveConnectionOnClose(t *testing.T) { + mux := NewUDPMux(dummyPacketConn{}, nil) + conn, err := mux.GetConn("test", &addrV4) + require.NoError(t, err) + require.NotNil(t, conn) + + require.NotNil(t, hasConn(mux, "test", false)) + + err = conn.Close() + require.NoError(t, err) + + require.Nil(t, hasConn(mux, "test", false)) +} diff --git a/p2p/transport/webrtc/udpmux/muxed_connection.go b/p2p/transport/webrtc/udpmux/muxed_connection.go new file mode 100644 index 0000000000..bafc728b9b --- /dev/null +++ b/p2p/transport/webrtc/udpmux/muxed_connection.go @@ -0,0 +1,98 @@ +package udpmux + +import ( + "context" + "errors" + "net" + "time" +) + +var _ net.PacketConn = &muxedConnection{} + +var errAlreadyClosed = errors.New("already closed") + +// muxedConnection provides a net.PacketConn abstraction +// over packetQueue and adds the ability to store addresses +// from which this connection (indexed by ufrag) received +// data. +type muxedConnection struct { + ctx context.Context + cancel context.CancelFunc + pq *packetQueue + ufrag string + addr net.Addr + mux *udpMux +} + +var _ net.PacketConn = (*muxedConnection)(nil) + +func newMuxedConnection(mux *udpMux, ufrag string, addr net.Addr) *muxedConnection { + ctx, cancel := context.WithCancel(mux.ctx) + return &muxedConnection{ + ctx: ctx, + cancel: cancel, + pq: newPacketQueue(), + ufrag: ufrag, + addr: addr, + mux: mux, + } +} + +func (conn *muxedConnection) Push(buf []byte) error { + return conn.pq.Push(conn.ctx, buf) +} + +// Close implements net.PacketConn +func (conn *muxedConnection) Close() error { + _ = conn.closeConnection() + conn.mux.RemoveConnByUfrag(conn.ufrag) + return nil +} + +// LocalAddr implements net.PacketConn +func (conn *muxedConnection) LocalAddr() net.Addr { + return conn.mux.socket.LocalAddr() +} + +func (conn *muxedConnection) Address() net.Addr { + return conn.addr +} + +// ReadFrom implements net.PacketConn +func (conn *muxedConnection) ReadFrom(p []byte) (int, net.Addr, error) { + n, err := conn.pq.Pop(conn.ctx, p) + return n, conn.addr, err +} + +// SetDeadline implements net.PacketConn +func (*muxedConnection) SetDeadline(t time.Time) error { + // no deadline is desired here + return nil +} + +// SetReadDeadline implements net.PacketConn +func (*muxedConnection) SetReadDeadline(t time.Time) error { + // no read deadline is desired here + return nil +} + +// SetWriteDeadline implements net.PacketConn +func (*muxedConnection) SetWriteDeadline(t time.Time) error { + // no write deadline is desired here + return nil +} + +// WriteTo implements net.PacketConn +func (conn *muxedConnection) WriteTo(p []byte, addr net.Addr) (n int, err error) { + return conn.mux.writeTo(p, addr) +} + +func (conn *muxedConnection) closeConnection() error { + select { + case <-conn.ctx.Done(): + return errAlreadyClosed + default: + } + conn.cancel() + return nil +} diff --git a/p2p/transport/webrtc/udpmux/packetqueue.go b/p2p/transport/webrtc/udpmux/packetqueue.go new file mode 100644 index 0000000000..107ee416bc --- /dev/null +++ b/p2p/transport/webrtc/udpmux/packetqueue.go @@ -0,0 +1,92 @@ +package udpmux + +import ( + "context" + "errors" + "sync" + + pool "github.com/libp2p/go-buffer-pool" +) + +type packet struct { + buf []byte +} + +var ( + errTooManyPackets = errors.New("too many packets in queue; dropping") + errEmptyPacketQueue = errors.New("packet queue is empty") + errPacketQueueClosed = errors.New("packet queue closed") +) + +const maxPacketsInQueue = 128 + +type packetQueue struct { + packetsMux sync.Mutex + packetsCh chan struct{} + packets []packet +} + +func newPacketQueue() *packetQueue { + return &packetQueue{ + packetsCh: make(chan struct{}, 1), + } +} + +// Pop reads a packet from the packetQueue or blocks until +// either a packet becomes available or the queue is closed. +func (pq *packetQueue) Pop(ctx context.Context, buf []byte) (int, error) { + select { + case <-pq.packetsCh: + pq.packetsMux.Lock() + defer pq.packetsMux.Unlock() + + if len(pq.packets) == 0 { + return 0, errEmptyPacketQueue + } + p := pq.packets[0] + + n := copy(buf, p.buf) + if n == len(p.buf) { + // only move packet from queue if we read all + pq.packets = pq.packets[1:] + pool.Put(p.buf) + } else { + // otherwise we need to keep the packet in the queue + // but do update the buf + pq.packets[0].buf = p.buf[n:] + } + + if len(pq.packets) > 0 { + // to make sure a next pop call will work + select { + case pq.packetsCh <- struct{}{}: + default: + } + } + + return n, nil + + // It is desired to allow reads of this channel even + // when pq.ctx.Done() is already closed. + case <-ctx.Done(): + return 0, errPacketQueueClosed + } +} + +// Push adds a packet to the packetQueue +func (pq *packetQueue) Push(ctx context.Context, buf []byte) error { + pq.packetsMux.Lock() + defer pq.packetsMux.Unlock() + + if len(pq.packets) >= maxPacketsInQueue { + return errTooManyPackets + } + + pq.packets = append(pq.packets, packet{buf}) + select { + case pq.packetsCh <- struct{}{}: + default: + } + + return nil +} diff --git a/p2p/transport/webrtc/udpmux/packetqueue_bench_test.go b/p2p/transport/webrtc/udpmux/packetqueue_bench_test.go new file mode 100644 index 0000000000..dbbb3b1bc1 --- /dev/null +++ b/p2p/transport/webrtc/udpmux/packetqueue_bench_test.go @@ -0,0 +1,41 @@ +package udpmux + +import ( + "context" + "fmt" + "testing" + + pool "github.com/libp2p/go-buffer-pool" +) + +var sizes = []int{ + 1, + 10, + 100, + maxPacketsInQueue, +} + +func BenchmarkQueue(b *testing.B) { + ctx := context.Background() + for _, dequeue := range [...]bool{true, false} { + for _, input := range sizes { + testCase := fmt.Sprintf("enqueue_%d", input) + if dequeue { + testCase = testCase + "_dequeue" + } + b.Run(testCase, func(b *testing.B) { + pq := newPacketQueue() + buf := make([]byte, 256) + b.ResetTimer() + for i := 0; i < b.N; i++ { + for k := 0; k < input; k++ { + pq.Push(ctx, pool.Get(255)) + } + for k := 0; k < input; k++ { + pq.Pop(ctx, buf) + } + } + }) + } + } +} diff --git a/p2p/transport/webrtc/udpmux/packetqueue_test.go b/p2p/transport/webrtc/udpmux/packetqueue_test.go new file mode 100644 index 0000000000..7abe0c1a4b --- /dev/null +++ b/p2p/transport/webrtc/udpmux/packetqueue_test.go @@ -0,0 +1,61 @@ +package udpmux + +import ( + "context" + "testing" + "time" + + pool "github.com/libp2p/go-buffer-pool" + "github.com/stretchr/testify/require" +) + +func TestPacketQueue_QueuePacketsForRead(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + pq := newPacketQueue() + pq.Push(ctx, []byte{1, 2, 3}) + pq.Push(ctx, []byte{5, 6, 7, 8}) + + buf := pool.Get(100) + size, err := pq.Pop(ctx, buf) + require.NoError(t, err) + require.Equal(t, size, 3) + + size, err = pq.Pop(ctx, buf) + require.NoError(t, err) + require.Equal(t, size, 4) +} + +func TestPacketQueue_WaitsForData(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + pq := newPacketQueue() + buf := pool.Get(100) + + timer := time.AfterFunc(200*time.Millisecond, func() { + pq.Push(ctx, []byte{5, 6, 7, 8}) + }) + + defer timer.Stop() + size, err := pq.Pop(ctx, buf) + require.NoError(t, err) + require.Equal(t, size, 4) +} + +func TestPacketQueue_DropsPacketsWhenQueueIsFull(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + pq := newPacketQueue() + for i := 0; i < maxPacketsInQueue; i++ { + buf := pool.Get(255) + err := pq.Push(ctx, buf) + require.NoError(t, err) + } + + buf := pool.Get(255) + err := pq.Push(ctx, buf) + require.ErrorIs(t, err, errTooManyPackets) +} diff --git a/p2p/transport/webrtc/webrtc.go b/p2p/transport/webrtc/webrtc.go new file mode 100644 index 0000000000..35bfcbc7d0 --- /dev/null +++ b/p2p/transport/webrtc/webrtc.go @@ -0,0 +1,15 @@ +// libp2pwebrtc implements the WebRTC transport for go-libp2p, +// as officially described in https://github.com/libp2p/specs/tree/cfcf0230b2f5f11ed6dd060f97305faa973abed2/webrtc. +// +// Benchmarks on how this transport compares to other transports can be found in +// https://github.com/libp2p/libp2p-go-webrtc-benchmarks. +// +// Entrypoint for the logic of this Transport can be found in `transport.go`, where the WebRTC transport is implemented, +// used both by the client for Dialing as well as the server for Listening. Starting from there you should be able to follow +// the logic from start to finish. +// +// In the udpmux subpackage you can find the logic for multiplexing multiple WebRTC (ICE) connections over a single UDP socket. +// +// The pb subpackage contains the protobuf definitions for the signaling protocol used by this transport, +// which is taken verbatim from the "Multiplexing" chapter of the WebRTC spec. +package libp2pwebrtc diff --git a/p2p/transport/webrtc_w3c/handlers_test.go b/p2p/transport/webrtc_w3c/handlers_test.go new file mode 100644 index 0000000000..0d85bbd6a9 --- /dev/null +++ b/p2p/transport/webrtc_w3c/handlers_test.go @@ -0,0 +1,117 @@ +package webrtc_w3c + +import ( + "context" + "io" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/protocol" + "github.com/pion/webrtc/v3" + "github.com/stretchr/testify/require" +) + +var _ network.Stream = &stream{} + +type stream struct { + r io.Reader + w io.Writer +} + +// Read implements network.Stream +func (s *stream) Read(p []byte) (n int, err error) { + return s.r.Read(p) +} + +// Write implements network.Stream +func (s *stream) Write(p []byte) (n int, err error) { + return s.w.Write(p) +} + +// Close implements network.Stream +func (s *stream) Close() error { + return nil +} + +// CloseRead implements network.Stream +func (*stream) CloseRead() error { + return nil +} + +// CloseWrite implements network.Stream +func (*stream) CloseWrite() error { + return nil +} + +// Reset implements network.Stream +func (*stream) Reset() error { + return nil +} + +// SetDeadline implements network.Stream +func (*stream) SetDeadline(time.Time) error { + return nil +} + +// SetReadDeadline implements network.Stream +func (*stream) SetReadDeadline(time.Time) error { + return nil +} + +// SetWriteDeadline implements network.Stream +func (*stream) SetWriteDeadline(time.Time) error { + return nil +} + +// Conn implements network.Stream +func (*stream) Conn() network.Conn { + return nil +} + +// ID implements network.Stream +func (*stream) ID() string { + return "" +} + +// Protocol implements network.Stream +func (*stream) Protocol() protocol.ID { + return "" +} + +// Scope implements network.Stream +func (*stream) Scope() network.StreamScope { + panic("unimplemented") +} + +// SetProtocol implements network.Stream +func (*stream) SetProtocol(id protocol.ID) error { + panic("unimplemented") +} + +// Stat implements network.Stream +func (*stream) Stat() network.Stats { + panic("unimplemented") +} + +func makeStreamPair() (network.Stream, network.Stream) { + ra, wb := io.Pipe() + rb, wa := io.Pipe() + return &stream{ra, wa}, &stream{rb, wb} +} + +func TestWebrtcW3Handlers_ShouldConnect(t *testing.T) { + sa, sb := makeStreamPair() + errC := make(chan error) + go func() { + defer close(errC) + _, err := handleIncoming(context.Background(), webrtc.Configuration{}, sa) + if err != nil { + errC <- err + } + }() + + _, err := connect(context.Background(), webrtc.Configuration{}, sb) + require.NoError(t, err) + require.NoError(t, <-errC) +} diff --git a/p2p/transport/webrtc_w3c/pb/webrtc.pb.go b/p2p/transport/webrtc_w3c/pb/webrtc.pb.go new file mode 100644 index 0000000000..c8971bd4c5 --- /dev/null +++ b/p2p/transport/webrtc_w3c/pb/webrtc.pb.go @@ -0,0 +1,215 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.21.12 +// source: pb/webrtc.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Specifies type in `data` field. +type Message_Type int32 + +const ( + // String of `RTCSessionDescription.sdp` + Message_SDP_OFFER Message_Type = 0 + // String of `RTCSessionDescription.sdp` + Message_SDP_ANSWER Message_Type = 1 + // String of `RTCIceCandidate.toJSON()` + Message_ICE_CANDIDATE Message_Type = 2 +) + +// Enum value maps for Message_Type. +var ( + Message_Type_name = map[int32]string{ + 0: "SDP_OFFER", + 1: "SDP_ANSWER", + 2: "ICE_CANDIDATE", + } + Message_Type_value = map[string]int32{ + "SDP_OFFER": 0, + "SDP_ANSWER": 1, + "ICE_CANDIDATE": 2, + } +) + +func (x Message_Type) Enum() *Message_Type { + p := new(Message_Type) + *p = x + return p +} + +func (x Message_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Message_Type) Descriptor() protoreflect.EnumDescriptor { + return file_pb_webrtc_proto_enumTypes[0].Descriptor() +} + +func (Message_Type) Type() protoreflect.EnumType { + return &file_pb_webrtc_proto_enumTypes[0] +} + +func (x Message_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Message_Type.Descriptor instead. +func (Message_Type) EnumDescriptor() ([]byte, []int) { + return file_pb_webrtc_proto_rawDescGZIP(), []int{0, 0} +} + +type Message struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type *Message_Type `protobuf:"varint,1,opt,name=type,proto3,enum=webrtc_w3c.pb.Message_Type,oneof" json:"type,omitempty"` + Data *string `protobuf:"bytes,2,opt,name=data,proto3,oneof" json:"data,omitempty"` +} + +func (x *Message) Reset() { + *x = Message{} + if protoimpl.UnsafeEnabled { + mi := &file_pb_webrtc_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_pb_webrtc_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_pb_webrtc_proto_rawDescGZIP(), []int{0} +} + +func (x *Message) GetType() Message_Type { + if x != nil && x.Type != nil { + return *x.Type + } + return Message_SDP_OFFER +} + +func (x *Message) GetData() string { + if x != nil && x.Data != nil { + return *x.Data + } + return "" +} + +var File_pb_webrtc_proto protoreflect.FileDescriptor + +var file_pb_webrtc_proto_rawDesc = []byte{ + 0x0a, 0x0f, 0x70, 0x62, 0x2f, 0x77, 0x65, 0x62, 0x72, 0x74, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x0d, 0x77, 0x65, 0x62, 0x72, 0x74, 0x63, 0x5f, 0x77, 0x33, 0x63, 0x2e, 0x70, 0x62, + 0x22, 0xa4, 0x01, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x34, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x77, 0x65, 0x62, + 0x72, 0x74, 0x63, 0x5f, 0x77, 0x33, 0x63, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x48, 0x00, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x88, + 0x01, 0x01, 0x12, 0x17, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x01, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x22, 0x38, 0x0a, 0x04, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x44, 0x50, 0x5f, 0x4f, 0x46, 0x46, 0x45, 0x52, + 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x44, 0x50, 0x5f, 0x41, 0x4e, 0x53, 0x57, 0x45, 0x52, + 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x44, 0x49, 0x44, + 0x41, 0x54, 0x45, 0x10, 0x02, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x07, + 0x0a, 0x05, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_pb_webrtc_proto_rawDescOnce sync.Once + file_pb_webrtc_proto_rawDescData = file_pb_webrtc_proto_rawDesc +) + +func file_pb_webrtc_proto_rawDescGZIP() []byte { + file_pb_webrtc_proto_rawDescOnce.Do(func() { + file_pb_webrtc_proto_rawDescData = protoimpl.X.CompressGZIP(file_pb_webrtc_proto_rawDescData) + }) + return file_pb_webrtc_proto_rawDescData +} + +var file_pb_webrtc_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_pb_webrtc_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_pb_webrtc_proto_goTypes = []interface{}{ + (Message_Type)(0), // 0: webrtc_w3c.pb.Message.Type + (*Message)(nil), // 1: webrtc_w3c.pb.Message +} +var file_pb_webrtc_proto_depIdxs = []int32{ + 0, // 0: webrtc_w3c.pb.Message.type:type_name -> webrtc_w3c.pb.Message.Type + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_pb_webrtc_proto_init() } +func file_pb_webrtc_proto_init() { + if File_pb_webrtc_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pb_webrtc_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_pb_webrtc_proto_msgTypes[0].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pb_webrtc_proto_rawDesc, + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_pb_webrtc_proto_goTypes, + DependencyIndexes: file_pb_webrtc_proto_depIdxs, + EnumInfos: file_pb_webrtc_proto_enumTypes, + MessageInfos: file_pb_webrtc_proto_msgTypes, + }.Build() + File_pb_webrtc_proto = out.File + file_pb_webrtc_proto_rawDesc = nil + file_pb_webrtc_proto_goTypes = nil + file_pb_webrtc_proto_depIdxs = nil +} diff --git a/p2p/transport/webrtc_w3c/pb/webrtc.proto b/p2p/transport/webrtc_w3c/pb/webrtc.proto new file mode 100644 index 0000000000..4a8d078155 --- /dev/null +++ b/p2p/transport/webrtc_w3c/pb/webrtc.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package webrtc_w3c.pb; + +message Message { + // Specifies type in `data` field. + enum Type { + // String of `RTCSessionDescription.sdp` + SDP_OFFER = 0; + // String of `RTCSessionDescription.sdp` + SDP_ANSWER = 1; + // String of `RTCIceCandidate.toJSON()` + ICE_CANDIDATE = 2; + } + optional Type type = 1; + optional string data = 2; +} diff --git a/p2p/transport/webrtc_w3c/proto.go b/p2p/transport/webrtc_w3c/proto.go new file mode 100644 index 0000000000..8a1e411ac7 --- /dev/null +++ b/p2p/transport/webrtc_w3c/proto.go @@ -0,0 +1,3 @@ +package webrtc_w3c + +//go:generate protoc --proto_path=$PWD:$PWD/../../.. --go_out=. --go_opt=Mpb/webrtc.proto=./pb pb/webrtc.proto diff --git a/p2p/transport/webrtc_w3c/stream_util.go b/p2p/transport/webrtc_w3c/stream_util.go new file mode 100644 index 0000000000..af12c0072c --- /dev/null +++ b/p2p/transport/webrtc_w3c/stream_util.go @@ -0,0 +1,384 @@ +package webrtc_w3c + +import ( + "context" + "encoding/json" + "errors" + "sync" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/p2p/transport/webrtc_w3c/pb" + "github.com/libp2p/go-msgio/pbio" + "github.com/pion/webrtc/v3" +) + +const maxMessageSize = 4096 + +var ( + errExpectedOffer = errors.New("expected an SDP offer") + errEmptyData = errors.New("empty data") + errConnectionFailed = errors.New("peerconnection failed to connect") + errExpectedAnswer = errors.New("expected an SDP answer") +) + +func handleIncoming(ctx context.Context, config webrtc.Configuration, stream network.Stream) (*webrtc.PeerConnection, error) { + pc, err := webrtc.NewPeerConnection(config) + if err != nil { + log.Warn("error creating a peer connection: %v", err) + return nil, err + } + // handshake deadline + if deadline, ok := ctx.Deadline(); ok { + err = stream.SetDeadline(deadline) + if err != nil { + log.Warn("failed to set stream deadline: %v", err) + return nil, err + } + } + + reader := pbio.NewDelimitedReader(stream, maxMessageSize) + writer := pbio.NewDelimitedWriter(stream) + + // set up callback for when peerconnection moves to the connected + // state. If the connection succeeds, this will be overwritten by + // the webrtc.connection's callback. + connectedChan := make(chan error, 1) + closeRead := make(chan struct{}) + var connectedOnce sync.Once + pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { + switch state { + case webrtc.PeerConnectionStateConnected: + connectedOnce.Do(func() { + close(connectedChan) + close(closeRead) + }) + case webrtc.PeerConnectionStateFailed: + fallthrough + case webrtc.PeerConnectionStateClosed: + connectedOnce.Do(func() { + // safe since the channel is only written to once + connectedChan <- errConnectionFailed + close(connectedChan) + close(closeRead) + }) + default: + // do nothing + } + }) + + // setup the ICE candidate callback + pc.OnICECandidate(func(candiate *webrtc.ICECandidate) { + data := "" + if candiate != nil { + b, err := json.Marshal(candiate.ToJSON()) + if err != nil { + log.Warn("failed to marshal candidate to JSON") + return + } + data = string(b) + } + + msg := &pb.Message{ + Type: pb.Message_ICE_CANDIDATE.Enum(), + Data: &data, + } + // TODO: Do something with this error + _ = writer.WriteMsg(msg) + + }) + + defer func() { + // de-register candidate callback + pc.OnICECandidate(func(_ *webrtc.ICECandidate) {}) + }() + + // read an incoming offer + var msg pb.Message + if err := reader.ReadMsg(&msg); err != nil { + log.Warn("failed to read SDP offer: %v", err) + return nil, err + } + if msg.Type == nil || msg.GetType() != pb.Message_SDP_OFFER { + log.Warn("expected SDP offer, instead got: %v", msg.GetType()) + return nil, errExpectedOffer + } + if msg.Data == nil { + log.Warn("message is empty") + return nil, errEmptyData + } + offer := webrtc.SessionDescription{ + Type: webrtc.SDPTypeOffer, + SDP: *msg.Data, + } + if err := pc.SetRemoteDescription(offer); err != nil { + log.Warn("failed to set remote description: %v", err) + return nil, err + } + + // create and write an answer + answer, err := pc.CreateAnswer(nil) + if err != nil { + log.Warn("failed to create answer: %v", err) + return nil, err + } + + answerMessage := &pb.Message{ + Type: pb.Message_SDP_ANSWER.Enum(), + Data: &answer.SDP, + } + if err := writer.WriteMsg(answerMessage); err != nil { + log.Warn("failed to send answer: %v", err) + return nil, err + } + if err := pc.SetLocalDescription(answer); err != nil { + log.Warn("failed to set local description: %v", err) + return nil, err + } + + done := ctx.Done() + readErr := make(chan error) + // start a goroutine to read candidates + go func() { + for { + select { + case <-done: + return + case <-closeRead: + return + default: + } + + var msg pb.Message + if err := reader.ReadMsg(&msg); err != nil { + readErr <- err + return + } + if msg.Type == nil || msg.GetType() != pb.Message_ICE_CANDIDATE { + readErr <- errors.New("got non-candidate message") + return + } + if msg.Data == nil { + readErr <- errEmptyData + return + } + if *msg.Data == "" { + return + } + + // unmarshal IceCandidateInit + var init webrtc.ICECandidateInit + if err := json.Unmarshal([]byte(*msg.Data), &init); err != nil { + log.Debugf("could not unmarshal candidate: %v, %s", err, *msg.Data) + readErr <- err + return + } + if err := pc.AddICECandidate(init); err != nil { + log.Debugf("bad candidate: %v", err) + readErr <- err + return + } + } + }() + + select { + case <-done: + _ = pc.Close() + return nil, ctx.Err() + case err := <-connectedChan: + if err != nil { + _ = pc.Close() + return nil, err + } + break + case err := <-readErr: + if err == nil { + log.Error("err: %v", err) + panic("nil error should never be written to this channel %v") + } + _ = pc.Close() + return nil, err + } + return pc, nil +} + +func connect(ctx context.Context, config webrtc.Configuration, stream network.Stream) (*webrtc.PeerConnection, error) { + pc, err := webrtc.NewPeerConnection(config) + if err != nil { + log.Warn("error creating a peer connection: %v", err) + return nil, err + } + // handshake deadline + if deadline, ok := ctx.Deadline(); ok { + err = stream.SetDeadline(deadline) + if err != nil { + log.Warn("failed to set stream deadline: %v", err) + return nil, err + } + } + + reader := pbio.NewDelimitedReader(stream, maxMessageSize) + writer := pbio.NewDelimitedWriter(stream) + + // set up callback for when peerconnection moves to the connected + // state. If the connection succeeds, this will be overwritten by + // the webrtc.connection's callback. + connectedChan := make(chan error, 1) + closeRead := make(chan struct{}) + var connectedOnce sync.Once + pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { + switch state { + case webrtc.PeerConnectionStateConnected: + connectedOnce.Do(func() { + close(connectedChan) + close(closeRead) + }) + case webrtc.PeerConnectionStateFailed: + fallthrough + case webrtc.PeerConnectionStateClosed: + connectedOnce.Do(func() { + // safe since the channel is only written to once + connectedChan <- errConnectionFailed + close(connectedChan) + close(closeRead) + }) + default: + // do nothing + } + }) + + // setup the ICE candidate callback + pc.OnICECandidate(func(candiate *webrtc.ICECandidate) { + data := "" + if candiate != nil { + b, err := json.Marshal(candiate.ToJSON()) + if err != nil { + log.Warn("failed to marshal candidate to JSON") + return + } + data = string(b) + } + + msg := &pb.Message{ + Type: pb.Message_ICE_CANDIDATE.Enum(), + Data: &data, + } + // TODO: Do something with this error + _ = writer.WriteMsg(msg) + + }) + + defer func() { + // de-register candidate callback + pc.OnICECandidate(func(_ *webrtc.ICECandidate) {}) + }() + + // we initialize a datachannel so that we have an ICE component for which to collect candidates + if _, err := pc.CreateDataChannel("init", nil); err != nil { + log.Warn("could not initialize peerconnection") + return nil, err + } + + // create and write an offer + offer, err := pc.CreateOffer(nil) + if err != nil { + log.Warn("failed to create offer: %v", err) + return nil, err + } + + offerMessage := &pb.Message{ + Type: pb.Message_SDP_OFFER.Enum(), + Data: &offer.SDP, + } + if err := writer.WriteMsg(offerMessage); err != nil { + log.Warn("failed to send offer: %v", err) + return nil, err + } + if err := pc.SetLocalDescription(offer); err != nil { + log.Warn("failed to set local description: %v", err) + return nil, err + } + + // read an incoming answer + var msg pb.Message + if err := reader.ReadMsg(&msg); err != nil { + log.Warn("failed to read SDP answer: %v", err) + return nil, err + } + if msg.Type == nil || msg.GetType() != pb.Message_SDP_ANSWER { + return nil, errExpectedAnswer + } + if msg.Data == nil { + return nil, errEmptyData + } + answer := webrtc.SessionDescription{ + Type: webrtc.SDPTypeAnswer, + SDP: *msg.Data, + } + if err := pc.SetRemoteDescription(answer); err != nil { + log.Warn("failed to set remote description: %v", err) + return nil, err + } + + done := ctx.Done() + readErr := make(chan error) + // start a goroutine to read candidates + go func() { + for { + select { + case <-done: + return + case <-closeRead: + return + default: + } + + var msg pb.Message + if err := reader.ReadMsg(&msg); err != nil { + readErr <- err + return + } + if msg.Type == nil || msg.GetType() != pb.Message_ICE_CANDIDATE { + readErr <- errors.New("got non-candidate message") + return + } + if msg.Data == nil { + readErr <- errEmptyData + return + } + + // unmarshal IceCandidateInit + var init webrtc.ICECandidateInit + if *msg.Data == "" { + return + } + if err := json.Unmarshal([]byte(*msg.Data), &init); err != nil { + readErr <- err + return + } + if err := pc.AddICECandidate(init); err != nil { + readErr <- err + return + } + } + }() + + select { + case <-done: + _ = pc.Close() + return nil, ctx.Err() + case err := <-connectedChan: + if err != nil { + _ = pc.Close() + return nil, err + } + break + case err := <-readErr: + if err == nil { + panic("nil error should never be written to this channel") + } + log.Warn("error: %v", err) + _ = pc.Close() + return nil, err + } + return pc, nil +} diff --git a/p2p/transport/webrtc_w3c/transport.go b/p2p/transport/webrtc_w3c/transport.go new file mode 100644 index 0000000000..fe700876cf --- /dev/null +++ b/p2p/transport/webrtc_w3c/transport.go @@ -0,0 +1,99 @@ +package webrtc_w3c + +import ( + "context" + + logging "github.com/ipfs/go-log/v2" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/peerstore" + tpt "github.com/libp2p/go-libp2p/core/transport" + wrtc "github.com/libp2p/go-libp2p/p2p/transport/webrtc" + ma "github.com/multiformats/go-multiaddr" + "github.com/pion/webrtc/v3" +) + +var log = logging.Logger("webrtc_w3c") + +var _ tpt.Transport = &Client{} + +const MULTIADDR_PROTOCOL = "/webrtc-w3c" +const SIGNALING_PROTOCOL = "/webrtc-signaling" + +type Client struct { + host host.Host + config webrtc.Configuration +} + +// CanDial implements transport.Transport +func (*Client) CanDial(addr ma.Multiaddr) bool { + panic("unimplemented") +} + +// Dial implements transport.Transport +func (c *Client) Dial(ctx context.Context, raddr ma.Multiaddr, p peer.ID) (tpt.CapableConn, error) { + baseAddr, err := getBaseMultiaddr(raddr) + if err != nil { + return nil, err + } + + scope, err := c.host.Network().ResourceManager().OpenConnection(network.DirOutbound, false, raddr) + if err != nil { + return nil, err + } + + remotePubKey := c.host.Peerstore().PubKey(p) + var localAddress ma.Multiaddr + if len(c.host.Addrs()) != 0 { + localAddress = c.host.Addrs()[0] + } + // ensure there is an addr to connect to the remote peer + c.host.Peerstore().AddAddr(p, baseAddr, peerstore.TempAddrTTL) + + // create a new stream to the remote + stream, err := c.host.NewStream(ctx, p, SIGNALING_PROTOCOL) + if err != nil { + defer scope.Done() + return nil, err + } + // close the stream after connection is established + defer stream.Close() + // attempt webrtc connection + peerConnection, err := connect(ctx, c.config, stream) + if err != nil { + defer scope.Done() + return nil, err + } + conn, err := wrtc.NewWebRTCConnection( + network.DirOutbound, + peerConnection, + c, + scope, + c.host.ID(), + localAddress, + p, + remotePubKey, + raddr, + ) + if err != nil { + defer scope.Done() + return nil, err + } + return conn, nil +} + +// Listen implements transport.Transport +func (*Client) Listen(laddr ma.Multiaddr) (tpt.Listener, error) { + panic("unimplemented") +} + +// Protocols implements transport.Transport +func (*Client) Protocols() []int { + return []int{} +} + +// Proxy implements transport.Transport +func (*Client) Proxy() bool { + return false +} diff --git a/p2p/transport/webrtc_w3c/util.go b/p2p/transport/webrtc_w3c/util.go new file mode 100644 index 0000000000..ee4d8df74d --- /dev/null +++ b/p2p/transport/webrtc_w3c/util.go @@ -0,0 +1,21 @@ +package webrtc_w3c + +import ( + "errors" + "strings" + + "github.com/multiformats/go-multiaddr" +) + +/* +* getBaseMultiaddr removes the webrtc w3c component of the provided multiaddr +* and returns the base multiaddress to which we can connect. + */ +func getBaseMultiaddr(addr multiaddr.Multiaddr) (multiaddr.Multiaddr, error) { + addrString := addr.String() + splitResult := strings.Split(addrString, MULTIADDR_PROTOCOL) + if len(splitResult) < 2 { + return nil, errors.New("address does not contain w3c protocol") + } + return multiaddr.NewMultiaddr(splitResult[0]) +} diff --git a/test-plans/go.mod b/test-plans/go.mod index 62c8db1b2f..b03943011d 100644 --- a/test-plans/go.mod +++ b/test-plans/go.mod @@ -35,7 +35,7 @@ require ( github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/klauspost/compress v1.15.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.1 // indirect + github.com/klauspost/cpuid/v2 v2.2.3 // indirect github.com/koron/go-ssdp v0.0.3 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-cidranger v1.1.0 // indirect @@ -48,7 +48,7 @@ require ( github.com/libp2p/go-reuseport v0.2.0 // indirect github.com/libp2p/go-yamux/v4 v4.0.0 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/miekg/dns v1.1.50 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect @@ -78,19 +78,20 @@ require ( github.com/quic-go/quic-go v0.33.0 // indirect github.com/quic-go/webtransport-go v0.5.2 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/dig v1.15.0 // indirect go.uber.org/fx v1.18.2 // indirect - go.uber.org/multierr v1.8.0 // indirect + go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.4.0 // indirect + golang.org/x/crypto v0.5.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.7.0 // indirect - golang.org/x/net v0.4.0 // indirect + golang.org/x/net v0.5.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/sys v0.4.0 // indirect + golang.org/x/text v0.6.0 // indirect golang.org/x/tools v0.3.0 // indirect google.golang.org/protobuf v1.28.1 // indirect lukechampine.com/blake3 v1.1.7 // indirect diff --git a/test-plans/go.sum b/test-plans/go.sum index 095d94f3ba..41e9dbb117 100644 --- a/test-plans/go.sum +++ b/test-plans/go.sum @@ -257,8 +257,8 @@ github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kE github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.1 h1:U33DW0aiEj633gHYw3LoDNfkDiYnE5Q8M/TKJn2f2jI= -github.com/klauspost/cpuid/v2 v2.2.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk= @@ -266,8 +266,8 @@ github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= @@ -303,8 +303,8 @@ github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8 github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -418,6 +418,8 @@ github.com/quic-go/webtransport-go v0.5.2/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2Gk github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -489,8 +491,8 @@ go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= @@ -507,8 +509,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -586,8 +588,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -666,8 +668,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -677,8 +679,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=