mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-14 05:11:19 -04:00
This will allow browser-based proxies that are unable to determine their NAT type to conservatively label themselves as restricted NATs if they fail to work with clients that have restricted NATs.
398 lines
10 KiB
Go
398 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
|
|
"git.torproject.org/pluggable-transports/snowflake.git/common/util"
|
|
"github.com/pion/webrtc/v2"
|
|
. "github.com/smartystreets/goconvey/convey"
|
|
)
|
|
|
|
// Set up a mock broker to communicate with
|
|
type MockTransport struct {
|
|
statusOverride int
|
|
body []byte
|
|
}
|
|
|
|
// Just returns a response with fake SDP answer.
|
|
func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
s := ioutil.NopCloser(bytes.NewReader(m.body))
|
|
r := &http.Response{
|
|
StatusCode: m.statusOverride,
|
|
Body: s,
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// Set up a mock faulty transport
|
|
type FaultyTransport struct {
|
|
statusOverride int
|
|
body []byte
|
|
}
|
|
|
|
// Just returns a response with fake SDP answer.
|
|
func (f *FaultyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return nil, fmt.Errorf("TransportFailed")
|
|
}
|
|
|
|
func TestRemoteIPFromSDP(t *testing.T) {
|
|
tests := []struct {
|
|
sdp string
|
|
expected net.IP
|
|
}{
|
|
// https://tools.ietf.org/html/rfc4566#section-5
|
|
{`v=0
|
|
o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5
|
|
s=SDP Seminar
|
|
i=A Seminar on the session description protocol
|
|
u=http://www.example.com/seminars/sdp.pdf
|
|
e=j.doe@example.com (Jane Doe)
|
|
c=IN IP4 224.2.17.12/127
|
|
t=2873397496 2873404696
|
|
a=recvonly
|
|
m=audio 49170 RTP/AVP 0
|
|
m=video 51372 RTP/AVP 99
|
|
a=rtpmap:99 h263-1998/90000
|
|
`, net.ParseIP("224.2.17.12")},
|
|
// Missing c= line
|
|
{`v=0
|
|
o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5
|
|
s=SDP Seminar
|
|
i=A Seminar on the session description protocol
|
|
u=http://www.example.com/seminars/sdp.pdf
|
|
e=j.doe@example.com (Jane Doe)
|
|
t=2873397496 2873404696
|
|
a=recvonly
|
|
m=audio 49170 RTP/AVP 0
|
|
m=video 51372 RTP/AVP 99
|
|
a=rtpmap:99 h263-1998/90000
|
|
`, nil},
|
|
// Single line, IP address only
|
|
{`c=IN IP4 224.2.1.1
|
|
`, net.ParseIP("224.2.1.1")},
|
|
// Same, with TTL
|
|
{`c=IN IP4 224.2.1.1/127
|
|
`, net.ParseIP("224.2.1.1")},
|
|
// Same, with TTL and multicast addresses
|
|
{`c=IN IP4 224.2.1.1/127/3
|
|
`, net.ParseIP("224.2.1.1")},
|
|
// IPv6, address only
|
|
{`c=IN IP6 FF15::101
|
|
`, net.ParseIP("ff15::101")},
|
|
// Same, with multicast addresses
|
|
{`c=IN IP6 FF15::101/3
|
|
`, net.ParseIP("ff15::101")},
|
|
// Multiple c= lines
|
|
{`c=IN IP4 1.2.3.4
|
|
c=IN IP4 5.6.7.8
|
|
`, net.ParseIP("1.2.3.4")},
|
|
// Modified from SDP sent by snowflake-client.
|
|
{`v=0
|
|
o=- 7860378660295630295 2 IN IP4 127.0.0.1
|
|
s=-
|
|
t=0 0
|
|
a=group:BUNDLE data
|
|
a=msid-semantic: WMS
|
|
m=application 54653 DTLS/SCTP 5000
|
|
c=IN IP4 1.2.3.4
|
|
a=candidate:3581707038 1 udp 2122260223 192.168.0.1 54653 typ host generation 0 network-id 1 network-cost 50
|
|
a=candidate:2617212910 1 tcp 1518280447 192.168.0.1 59673 typ host tcptype passive generation 0 network-id 1 network-cost 50
|
|
a=candidate:2082671819 1 udp 1686052607 1.2.3.4 54653 typ srflx raddr 192.168.0.1 rport 54653 generation 0 network-id 1 network-cost 50
|
|
a=ice-ufrag:IBdf
|
|
a=ice-pwd:G3lTrrC9gmhQx481AowtkhYz
|
|
a=fingerprint:sha-256 53:F8:84:D9:3C:1F:A0:44:AA:D6:3C:65:80:D3:CB:6F:23:90:17:41:06:F9:9C:10:D8:48:4A:A8:B6:FA:14:A1
|
|
a=setup:actpass
|
|
a=mid:data
|
|
a=sctpmap:5000 webrtc-datachannel 1024
|
|
`, net.ParseIP("1.2.3.4")},
|
|
// Improper character within IPv4
|
|
{`c=IN IP4 224.2z.1.1
|
|
`, nil},
|
|
// Improper character within IPv6
|
|
{`c=IN IP6 ff15:g::101
|
|
`, nil},
|
|
// Bogus "IP7" addrtype
|
|
{`c=IN IP7 1.2.3.4
|
|
`, nil},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
// https://tools.ietf.org/html/rfc4566#section-5: "The sequence
|
|
// CRLF (0x0d0a) is used to end a record, although parsers
|
|
// SHOULD be tolerant and also accept records terminated with a
|
|
// single newline character." We represent the test cases with
|
|
// LF line endings for convenience, and test them both that way
|
|
// and with CRLF line endings.
|
|
lfSDP := test.sdp
|
|
crlfSDP := strings.Replace(lfSDP, "\n", "\r\n", -1)
|
|
|
|
ip := remoteIPFromSDP(lfSDP)
|
|
if !ip.Equal(test.expected) {
|
|
t.Errorf("expected %q, got %q from %q", test.expected, ip, lfSDP)
|
|
}
|
|
ip = remoteIPFromSDP(crlfSDP)
|
|
if !ip.Equal(test.expected) {
|
|
t.Errorf("expected %q, got %q from %q", test.expected, ip, crlfSDP)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionDescriptions(t *testing.T) {
|
|
Convey("Session description deserialization", t, func() {
|
|
for _, test := range []struct {
|
|
msg string
|
|
ret *webrtc.SessionDescription
|
|
}{
|
|
{
|
|
"test",
|
|
nil,
|
|
},
|
|
{
|
|
`{"type":"answer"}`,
|
|
nil,
|
|
},
|
|
{
|
|
`{"sdp":"test"}`,
|
|
nil,
|
|
},
|
|
{
|
|
`{"type":"test", "sdp":"test"}`,
|
|
nil,
|
|
},
|
|
{
|
|
`{"type":"answer", "sdp":"test"}`,
|
|
&webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypeAnswer,
|
|
SDP: "test",
|
|
},
|
|
},
|
|
{
|
|
`{"type":"pranswer", "sdp":"test"}`,
|
|
&webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypePranswer,
|
|
SDP: "test",
|
|
},
|
|
},
|
|
{
|
|
`{"type":"rollback", "sdp":"test"}`,
|
|
&webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypeRollback,
|
|
SDP: "test",
|
|
},
|
|
},
|
|
{
|
|
`{"type":"offer", "sdp":"test"}`,
|
|
&webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypeOffer,
|
|
SDP: "test",
|
|
},
|
|
},
|
|
} {
|
|
desc, _ := util.DeserializeSessionDescription(test.msg)
|
|
So(desc, ShouldResemble, test.ret)
|
|
}
|
|
})
|
|
Convey("Session description serialization", t, func() {
|
|
for _, test := range []struct {
|
|
desc *webrtc.SessionDescription
|
|
ret string
|
|
}{
|
|
{
|
|
&webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypeOffer,
|
|
SDP: "test",
|
|
},
|
|
`{"type":"offer","sdp":"test"}`,
|
|
},
|
|
} {
|
|
msg, err := util.SerializeSessionDescription(test.desc)
|
|
So(msg, ShouldResemble, test.ret)
|
|
So(err, ShouldBeNil)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBrokerInteractions(t *testing.T) {
|
|
const sampleSDP = `"v=0\r\no=- 4358805017720277108 2 IN IP4 8.8.8.8\r\ns=-\r\nt=0 0\r\na=group:BUNDLE data\r\na=msid-semantic: WMS\r\nm=application 56688 DTLS/SCTP 5000\r\nc=IN IP4 8.8.8.8\r\na=candidate:3769337065 1 udp 2122260223 8.8.8.8 56688 typ host generation 0 network-id 1 network-cost 50\r\na=candidate:2921887769 1 tcp 1518280447 8.8.8.8 35441 typ host tcptype passive generation 0 network-id 1 network-cost 50\r\na=ice-ufrag:aMAZ\r\na=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV\r\na=ice-options:trickle\r\na=fingerprint:sha-256 C8:88:EE:B9:E7:02:2E:21:37:ED:7A:D1:EB:2B:A3:15:A2:3B:5B:1C:3D:D4:D5:1F:06:CF:52:40:03:F8:DD:66\r\na=setup:actpass\r\na=mid:data\r\na=sctpmap:5000 webrtc-datachannel 1024\r\n"`
|
|
|
|
const sampleOffer = `{"type":"offer","sdp":` + sampleSDP + `}`
|
|
const sampleAnswer = `{"type":"answer","sdp":` + sampleSDP + `}`
|
|
|
|
Convey("Proxy connections to broker", t, func() {
|
|
broker := new(Broker)
|
|
broker.url, _ = url.Parse("localhost")
|
|
|
|
//Mock peerConnection
|
|
config = webrtc.Configuration{
|
|
ICEServers: []webrtc.ICEServer{
|
|
{
|
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
|
},
|
|
},
|
|
}
|
|
pc, _ := webrtc.NewPeerConnection(config)
|
|
offer, _ := util.DeserializeSessionDescription(sampleOffer)
|
|
pc.SetRemoteDescription(*offer)
|
|
answer, _ := pc.CreateAnswer(nil)
|
|
pc.SetLocalDescription(answer)
|
|
|
|
Convey("polls broker correctly", func() {
|
|
var err error
|
|
|
|
b, err := messages.EncodePollResponse(sampleOffer, true, "unknown")
|
|
So(err, ShouldEqual, nil)
|
|
broker.transport = &MockTransport{
|
|
http.StatusOK,
|
|
b,
|
|
}
|
|
|
|
sdp := broker.pollOffer(sampleOffer)
|
|
expectedSDP, _ := strconv.Unquote(sampleSDP)
|
|
So(sdp.SDP, ShouldResemble, expectedSDP)
|
|
})
|
|
Convey("handles poll error", func() {
|
|
var err error
|
|
|
|
b := []byte("test")
|
|
So(err, ShouldEqual, nil)
|
|
broker.transport = &MockTransport{
|
|
http.StatusOK,
|
|
b,
|
|
}
|
|
|
|
sdp := broker.pollOffer(sampleOffer)
|
|
So(sdp, ShouldBeNil)
|
|
})
|
|
Convey("sends answer to broker", func() {
|
|
var err error
|
|
|
|
b, err := messages.EncodeAnswerResponse(true)
|
|
So(err, ShouldEqual, nil)
|
|
broker.transport = &MockTransport{
|
|
http.StatusOK,
|
|
b,
|
|
}
|
|
|
|
err = broker.sendAnswer(sampleAnswer, pc)
|
|
So(err, ShouldEqual, nil)
|
|
|
|
b, err = messages.EncodeAnswerResponse(false)
|
|
So(err, ShouldEqual, nil)
|
|
broker.transport = &MockTransport{
|
|
http.StatusOK,
|
|
b,
|
|
}
|
|
|
|
err = broker.sendAnswer(sampleAnswer, pc)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
Convey("handles answer error", func() {
|
|
//Error if faulty transport
|
|
broker.transport = &FaultyTransport{}
|
|
err := broker.sendAnswer(sampleAnswer, pc)
|
|
So(err, ShouldNotBeNil)
|
|
|
|
//Error if status code is not ok
|
|
broker.transport = &MockTransport{
|
|
http.StatusGone,
|
|
[]byte(""),
|
|
}
|
|
err = broker.sendAnswer("test", pc)
|
|
So(err, ShouldNotEqual, nil)
|
|
So(err.Error(), ShouldResemble, "broker returned 410")
|
|
|
|
//Error if we can't parse broker message
|
|
broker.transport = &MockTransport{
|
|
http.StatusOK,
|
|
[]byte("test"),
|
|
}
|
|
err = broker.sendAnswer("test", pc)
|
|
So(err, ShouldNotBeNil)
|
|
|
|
//Error if broker message surpasses read limit
|
|
broker.transport = &MockTransport{
|
|
http.StatusOK,
|
|
make([]byte, 100001),
|
|
}
|
|
err = broker.sendAnswer("test", pc)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestUtilityFuncs(t *testing.T) {
|
|
Convey("LimitedRead", t, func() {
|
|
c, s := net.Pipe()
|
|
Convey("Successful read", func() {
|
|
go func() {
|
|
bytes := make([]byte, 50)
|
|
c.Write(bytes)
|
|
c.Close()
|
|
}()
|
|
bytes, err := limitedRead(s, 60)
|
|
So(len(bytes), ShouldEqual, 50)
|
|
So(err, ShouldBeNil)
|
|
})
|
|
Convey("Large read", func() {
|
|
go func() {
|
|
bytes := make([]byte, 50)
|
|
c.Write(bytes)
|
|
c.Close()
|
|
}()
|
|
bytes, err := limitedRead(s, 49)
|
|
So(len(bytes), ShouldEqual, 49)
|
|
So(err, ShouldEqual, io.ErrUnexpectedEOF)
|
|
})
|
|
Convey("Failed read", func() {
|
|
s.Close()
|
|
bytes, err := limitedRead(s, 49)
|
|
So(len(bytes), ShouldEqual, 0)
|
|
So(err, ShouldEqual, io.ErrClosedPipe)
|
|
})
|
|
})
|
|
Convey("Tokens", t, func() {
|
|
tokens = make(chan bool, 2)
|
|
for i := uint(0); i < 2; i++ {
|
|
tokens <- true
|
|
}
|
|
So(len(tokens), ShouldEqual, 2)
|
|
getToken()
|
|
So(len(tokens), ShouldEqual, 1)
|
|
retToken()
|
|
So(len(tokens), ShouldEqual, 2)
|
|
})
|
|
Convey("SessionID Generation", t, func() {
|
|
sid1 := genSessionID()
|
|
sid2 := genSessionID()
|
|
So(sid1, ShouldNotEqual, sid2)
|
|
})
|
|
Convey("CopyLoop", t, func() {
|
|
c1, s1 := net.Pipe()
|
|
c2, s2 := net.Pipe()
|
|
go CopyLoop(s1, s2)
|
|
go func() {
|
|
bytes := []byte("Hello!")
|
|
c1.Write(bytes)
|
|
}()
|
|
bytes := make([]byte, 6)
|
|
n, err := c2.Read(bytes)
|
|
So(n, ShouldEqual, 6)
|
|
So(err, ShouldEqual, nil)
|
|
So(bytes, ShouldResemble, []byte("Hello!"))
|
|
s1.Close()
|
|
|
|
//Check that copy loop has closed other connection
|
|
_, err = s2.Write(bytes)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
}
|