Turn the proxy code into a library

Allow other go programs to easily import the snowflake proxy library and
start/stop a snowflake proxy.
This commit is contained in:
idk 2021-10-25 22:51:40 -04:00 committed by Cecylia Bocovich
parent 54ab79384f
commit 50e4f4fd61
7 changed files with 184 additions and 99 deletions

497
proxy/lib/proxy-go_test.go Normal file
View file

@ -0,0 +1,497 @@
package snowflake
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"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/v3"
. "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")},
// local addresses only
{`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 10.47.16.5/127
t=2873397496 2873404696
a=recvonly
m=audio 49170 RTP/AVP 0
m=video 51372 RTP/AVP 99
a=rtpmap:99 h263-1998/90000
`, nil},
// Remote IP in candidate attribute only
{`v=0
o=- 4358805017720277108 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 56688 DTLS/SCTP 5000
c=IN IP4 0.0.0.0
a=candidate:3769337065 1 udp 2122260223 1.2.3.4 56688 typ host generation 0 network-id 1 network-cost 50
a=ice-ufrag:aMAZ
a=ice-pwd:jcHb08Jjgrazp2dzjdrvPPvV
a=ice-options:trickle
a=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
a=setup:actpass
a=mid:data
a=sctpmap:5000 webrtc-datachannel 1024
`, net.ParseIP("1.2.3.4")},
// Unspecified address
{`v=0
o=jdoe 2890844526 2890842807 IN IP4 0.0.0.0
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},
// 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
{`v=0
o=- 4358805017720277108 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 56688 DTLS/SCTP 5000
c=IN IP4 224.2.1.1
`, net.ParseIP("224.2.1.1")},
// Same, with TTL
{`v=0
o=- 4358805017720277108 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 56688 DTLS/SCTP 5000
c=IN IP4 224.2.1.1/127
`, net.ParseIP("224.2.1.1")},
// Same, with TTL and multicast addresses
{`v=0
o=- 4358805017720277108 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 56688 DTLS/SCTP 5000
c=IN IP4 224.2.1.1/127/3
`, net.ParseIP("224.2.1.1")},
// IPv6, address only
{`v=0
o=- 4358805017720277108 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 56688 DTLS/SCTP 5000
c=IN IP6 FF15::101
`, net.ParseIP("ff15::101")},
// Same, with multicast addresses
{`v=0
o=- 4358805017720277108 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 56688 DTLS/SCTP 5000
c=IN IP6 FF15::101/3
`, net.ParseIP("ff15::101")},
// Multiple c= lines
{`v=0
o=- 4358805017720277108 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 56688 DTLS/SCTP 5000
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
{`v=0
o=- 4358805017720277108 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 56688 DTLS/SCTP 5000
c=IN IP4 224.2z.1.1
`, nil},
// Improper character within IPv6
{`v=0
o=- 4358805017720277108 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 56688 DTLS/SCTP 5000
c=IN IP6 ff15:g::101
`, nil},
// Bogus "IP7" addrtype
{`v=0
o=- 4358805017720277108 2 IN IP4 0.0.0.0
s=-
t=0 0
a=group:BUNDLE data
a=msid-semantic: WMS
m=application 56688 DTLS/SCTP 5000
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() {
var err error
broker, err = newSignalingServer("localhost", false)
So(err, ShouldEqual, nil)
tokens = newTokens(0)
//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, nil)
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, nil)
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,
"error sending answer to broker: remote returned status code 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("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, nil)
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)
})
}

586
proxy/lib/snowflake.go Normal file
View file

@ -0,0 +1,586 @@
package snowflake
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"git.torproject.org/pluggable-transports/snowflake.git/common/messages"
"git.torproject.org/pluggable-transports/snowflake.git/common/util"
"git.torproject.org/pluggable-transports/snowflake.git/common/websocketconn"
"github.com/gorilla/websocket"
"github.com/pion/webrtc/v3"
)
// DefaultBrokerURL is the bamsoftware.com broker, https://snowflake-broker.bamsoftware.com
// Changing this will change the default broker. The recommended way of changing
// the broker that gets used is by passing an argument to Main.
const DefaultBrokerURL = "https://snowflake-broker.bamsoftware.com/"
// DefaultProbeURL is the torproject.org ProbeURL, https://snowflake-broker.torproject.net:8443/probe
// Changing this will change the default Probe URL. The recommended way of changing
// the probe that gets used is by passing an argument to Main.
const DefaultProbeURL = "https://snowflake-broker.torproject.net:8443/probe"
// DefaultRelayURL is the bamsoftware.com Websocket Relay, wss://snowflake.bamsoftware.com/
// Changing this will change the default Relay URL. The recommended way of changing
// the relay that gets used is by passing an argument to Main.
const DefaultRelayURL = "wss://snowflake.bamsoftware.com/"
// DefaultSTUNURL is a stunprotocol.org STUN URL. stun:stun.stunprotocol.org:3478
// Changing this will change the default STUN URL. The recommended way of changing
// the STUN Server that gets used is by passing an argument to Main.
const DefaultSTUNURL = "stun:stun.stunprotocol.org:3478"
const pollInterval = 5 * time.Second
const (
// NATUnknown represents a NAT type which is unknown.
NATUnknown = "unknown"
// NATRestricted represents a restricted NAT.
NATRestricted = "restricted"
// NATUnrestricted represents an unrestricted NAT.
NATUnrestricted = "unrestricted"
)
//amount of time after sending an SDP answer before the proxy assumes the
//client is not going to connect
const dataChannelTimeout = 20 * time.Second
const readLimit = 100000 //Maximum number of bytes to be read from an HTTP request
var broker *SignalingServer
var currentNATType = NATUnknown
const (
sessionIDLength = 16
)
var (
tokens *tokens_t
config webrtc.Configuration
client http.Client
)
// SnowflakeProxy is a structure which is used to configure an embedded
// Snowflake in another Go application.
type SnowflakeProxy struct {
Capacity uint
StunURL string
RawBrokerURL string
KeepLocalAddresses bool
RelayURL string
LogOutput io.Writer
shutdown chan struct{}
}
// Checks whether an IP address is a remote address for the client
func isRemoteAddress(ip net.IP) bool {
return !(util.IsLocal(ip) || ip.IsUnspecified() || ip.IsLoopback())
}
func genSessionID() string {
buf := make([]byte, sessionIDLength)
_, err := rand.Read(buf)
if err != nil {
panic(err.Error())
}
return strings.TrimRight(base64.StdEncoding.EncodeToString(buf), "=")
}
func limitedRead(r io.Reader, limit int64) ([]byte, error) {
p, err := ioutil.ReadAll(&io.LimitedReader{R: r, N: limit + 1})
if err != nil {
return p, err
} else if int64(len(p)) == limit+1 {
return p[0:limit], io.ErrUnexpectedEOF
}
return p, err
}
// SignalingServer keeps track of the SignalingServer in use by the Snowflake
type SignalingServer struct {
url *url.URL
transport http.RoundTripper
keepLocalAddresses bool
}
func newSignalingServer(rawURL string, keepLocalAddresses bool) (*SignalingServer, error) {
var err error
s := new(SignalingServer)
s.keepLocalAddresses = keepLocalAddresses
s.url, err = url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("invalid broker url: %s", err)
}
s.transport = http.DefaultTransport.(*http.Transport)
s.transport.(*http.Transport).ResponseHeaderTimeout = 30 * time.Second
return s, nil
}
// Post sends a POST request to the SignalingServer
func (s *SignalingServer) Post(path string, payload io.Reader) ([]byte, error) {
req, err := http.NewRequest("POST", path, payload)
if err != nil {
return nil, err
}
resp, err := s.transport.RoundTrip(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("remote returned status code %d", resp.StatusCode)
}
defer resp.Body.Close()
return limitedRead(resp.Body, readLimit)
}
func (s *SignalingServer) pollOffer(sid string, shutdown chan struct{}) *webrtc.SessionDescription {
brokerPath := s.url.ResolveReference(&url.URL{Path: "proxy"})
ticker := time.NewTicker(pollInterval)
defer ticker.Stop()
// Run the loop once before hitting the ticker
for ; true; <-ticker.C {
select {
case <-shutdown:
return nil
default:
numClients := int((tokens.count() / 8) * 8) // Round down to 8
body, err := messages.EncodePollRequest(sid, "standalone", currentNATType, numClients)
if err != nil {
log.Printf("Error encoding poll message: %s", err.Error())
return nil
}
resp, err := s.Post(brokerPath.String(), bytes.NewBuffer(body))
if err != nil {
log.Printf("error polling broker: %s", err.Error())
}
offer, _, err := messages.DecodePollResponse(resp)
if err != nil {
log.Printf("Error reading broker response: %s", err.Error())
log.Printf("body: %s", resp)
return nil
}
if offer != "" {
offer, err := util.DeserializeSessionDescription(offer)
if err != nil {
log.Printf("Error processing session description: %s", err.Error())
return nil
}
return offer
}
}
}
return nil
}
func (s *SignalingServer) sendAnswer(sid string, pc *webrtc.PeerConnection) error {
brokerPath := s.url.ResolveReference(&url.URL{Path: "answer"})
ld := pc.LocalDescription()
if !s.keepLocalAddresses {
ld = &webrtc.SessionDescription{
Type: ld.Type,
SDP: util.StripLocalAddresses(ld.SDP),
}
}
answer, err := util.SerializeSessionDescription(ld)
if err != nil {
return err
}
body, err := messages.EncodeAnswerRequest(answer, sid)
if err != nil {
return err
}
resp, err := s.Post(brokerPath.String(), bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("error sending answer to broker: %s", err.Error())
}
success, err := messages.DecodeAnswerResponse(resp)
if err != nil {
return err
}
if !success {
return fmt.Errorf("broker returned client timeout")
}
return nil
}
func copyLoop(c1 io.ReadWriteCloser, c2 io.ReadWriteCloser, shutdown chan struct{}) {
var once sync.Once
defer c2.Close()
defer c1.Close()
done := make(chan struct{})
copyer := func(dst io.ReadWriteCloser, src io.ReadWriteCloser) {
// Ignore io.ErrClosedPipe because it is likely caused by the
// termination of copyer in the other direction.
if _, err := io.Copy(dst, src); err != nil && err != io.ErrClosedPipe {
log.Printf("io.Copy inside CopyLoop generated an error: %v", err)
}
once.Do(func() {
close(done)
})
}
go copyer(c1, c2)
go copyer(c2, c1)
select {
case <-done:
case <-shutdown:
}
log.Println("copy loop ended")
}
// We pass conn.RemoteAddr() as an additional parameter, rather than calling
// conn.RemoteAddr() inside this function, as a workaround for a hang that
// otherwise occurs inside of conn.pc.RemoteDescription() (called by
// RemoteAddr). https://bugs.torproject.org/18628#comment:8
func (sf *SnowflakeProxy) datachannelHandler(conn *webRTCConn, remoteAddr net.Addr) {
defer conn.Close()
defer tokens.ret()
u, err := url.Parse(sf.RelayURL)
if err != nil {
log.Fatalf("invalid relay url: %s", err)
}
if remoteAddr != nil {
// Encode client IP address in relay URL
q := u.Query()
clientIP := remoteAddr.String()
q.Set("client_ip", clientIP)
u.RawQuery = q.Encode()
} else {
log.Printf("no remote address given in websocket")
}
ws, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
log.Printf("error dialing relay: %s", err)
return
}
wsConn := websocketconn.New(ws)
log.Printf("connected to relay")
defer wsConn.Close()
copyLoop(conn, wsConn, sf.shutdown)
log.Printf("datachannelHandler ends")
}
// Create a PeerConnection from an SDP offer. Blocks until the gathering of ICE
// candidates is complete and the answer is available in LocalDescription.
// Installs an OnDataChannel callback that creates a webRTCConn and passes it to
// datachannelHandler.
func (sf *SnowflakeProxy) makePeerConnectionFromOffer(sdp *webrtc.SessionDescription,
config webrtc.Configuration,
dataChan chan struct{},
handler func(conn *webRTCConn, remoteAddr net.Addr)) (*webrtc.PeerConnection, error) {
pc, err := webrtc.NewPeerConnection(config)
if err != nil {
return nil, fmt.Errorf("accept: NewPeerConnection: %s", err)
}
pc.OnDataChannel(func(dc *webrtc.DataChannel) {
log.Println("OnDataChannel")
close(dataChan)
pr, pw := io.Pipe()
conn := &webRTCConn{pc: pc, dc: dc, pr: pr}
conn.bytesLogger = NewBytesSyncLogger()
dc.OnOpen(func() {
log.Println("OnOpen channel")
})
dc.OnClose(func() {
conn.lock.Lock()
defer conn.lock.Unlock()
log.Println("OnClose channel")
log.Println(conn.bytesLogger.ThroughputSummary())
conn.dc = nil
dc.Close()
pw.Close()
})
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
var n int
n, err = pw.Write(msg.Data)
if err != nil {
if inerr := pw.CloseWithError(err); inerr != nil {
log.Printf("close with error generated an error: %v", inerr)
}
}
conn.bytesLogger.AddOutbound(n)
if n != len(msg.Data) {
panic("short write")
}
})
go handler(conn, conn.RemoteAddr())
})
// As of v3.0.0, pion-webrtc uses trickle ICE by default.
// We have to wait for candidate gathering to complete
// before we send the offer
done := webrtc.GatheringCompletePromise(pc)
err = pc.SetRemoteDescription(*sdp)
if err != nil {
if inerr := pc.Close(); inerr != nil {
log.Printf("unable to call pc.Close after pc.SetRemoteDescription with error: %v", inerr)
}
return nil, fmt.Errorf("accept: SetRemoteDescription: %s", err)
}
log.Println("sdp offer successfully received.")
log.Println("Generating answer...")
answer, err := pc.CreateAnswer(nil)
// blocks on ICE gathering. we need to add a timeout if needed
// not putting this in a separate go routine, because we need
// SetLocalDescription(answer) to be called before sendAnswer
if err != nil {
if inerr := pc.Close(); inerr != nil {
log.Printf("ICE gathering has generated an error when calling pc.Close: %v", inerr)
}
return nil, err
}
err = pc.SetLocalDescription(answer)
if err != nil {
if err = pc.Close(); err != nil {
log.Printf("pc.Close after setting local description returned : %v", err)
}
return nil, err
}
// Wait for ICE candidate gathering to complete
<-done
return pc, nil
}
// Create a new PeerConnection. Blocks until the gathering of ICE
// candidates is complete and the answer is available in LocalDescription.
func (sf *SnowflakeProxy) makeNewPeerConnection(config webrtc.Configuration,
dataChan chan struct{}) (*webrtc.PeerConnection, error) {
pc, err := webrtc.NewPeerConnection(config)
if err != nil {
return nil, fmt.Errorf("accept: NewPeerConnection: %s", err)
}
// Must create a data channel before creating an offer
// https://github.com/pion/webrtc/wiki/Release-WebRTC@v3.0.0
dc, err := pc.CreateDataChannel("test", &webrtc.DataChannelInit{})
if err != nil {
log.Printf("CreateDataChannel ERROR: %s", err)
return nil, err
}
dc.OnOpen(func() {
log.Println("WebRTC: DataChannel.OnOpen")
close(dataChan)
})
dc.OnClose(func() {
log.Println("WebRTC: DataChannel.OnClose")
dc.Close()
})
offer, err := pc.CreateOffer(nil)
// TODO: Potentially timeout and retry if ICE isn't working.
if err != nil {
log.Println("Failed to prepare offer", err)
pc.Close()
return nil, err
}
log.Println("WebRTC: Created offer")
// As of v3.0.0, pion-webrtc uses trickle ICE by default.
// We have to wait for candidate gathering to complete
// before we send the offer
done := webrtc.GatheringCompletePromise(pc)
err = pc.SetLocalDescription(offer)
if err != nil {
log.Println("Failed to prepare offer", err)
pc.Close()
return nil, err
}
log.Println("WebRTC: Set local description")
// Wait for ICE candidate gathering to complete
<-done
return pc, nil
}
func (sf *SnowflakeProxy) runSession(sid string) {
offer := broker.pollOffer(sid, sf.shutdown)
if offer == nil {
log.Printf("bad offer from broker")
tokens.ret()
return
}
dataChan := make(chan struct{})
pc, err := sf.makePeerConnectionFromOffer(offer, config, dataChan, sf.datachannelHandler)
if err != nil {
log.Printf("error making WebRTC connection: %s", err)
tokens.ret()
return
}
err = broker.sendAnswer(sid, pc)
if err != nil {
log.Printf("error sending answer to client through broker: %s", err)
if inerr := pc.Close(); inerr != nil {
log.Printf("error calling pc.Close: %v", inerr)
}
tokens.ret()
return
}
// Set a timeout on peerconnection. If the connection state has not
// advanced to PeerConnectionStateConnected in this time,
// destroy the peer connection and return the token.
select {
case <-dataChan:
log.Println("Connection successful.")
case <-time.After(dataChannelTimeout):
log.Println("Timed out waiting for client to open data channel.")
if err := pc.Close(); err != nil {
log.Printf("error calling pc.Close: %v", err)
}
tokens.ret()
}
}
// Start configures and starts a Snowflake, fully formed and special. In the
// case of an empty map, defaults are configured automatically and can be
// found in the GoDoc and in main.go
func (sf *SnowflakeProxy) Start() {
sf.shutdown = make(chan struct{})
log.SetFlags(log.LstdFlags | log.LUTC)
log.Println("starting")
var err error
broker, err = newSignalingServer(sf.RawBrokerURL, sf.KeepLocalAddresses)
if err != nil {
log.Fatal(err)
}
_, err = url.Parse(sf.StunURL)
if err != nil {
log.Fatalf("invalid stun url: %s", err)
}
_, err = url.Parse(sf.RelayURL)
if err != nil {
log.Fatalf("invalid relay url: %s", err)
}
config = webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{sf.StunURL},
},
},
}
tokens = newTokens(sf.Capacity)
// use probetest to determine NAT compatability
sf.checkNATType(config, DefaultProbeURL)
log.Printf("NAT type: %s", currentNATType)
ticker := time.NewTicker(pollInterval)
defer ticker.Stop()
for ; true; <-ticker.C {
select {
case <-sf.shutdown:
return
default:
tokens.get()
sessionID := genSessionID()
sf.runSession(sessionID)
}
}
}
// Stop calls close on the sf.shutdown channel shutting down the Snowflake.
func (sf *SnowflakeProxy) Stop() {
close(sf.shutdown)
}
func (sf *SnowflakeProxy) checkNATType(config webrtc.Configuration, probeURL string) {
probe, err := newSignalingServer(probeURL, false)
if err != nil {
log.Printf("Error parsing url: %s", err.Error())
}
// create offer
dataChan := make(chan struct{})
pc, err := sf.makeNewPeerConnection(config, dataChan)
if err != nil {
log.Printf("error making WebRTC connection: %s", err)
return
}
offer := pc.LocalDescription()
sdp, err := util.SerializeSessionDescription(offer)
log.Printf("Offer: %s", sdp)
if err != nil {
log.Printf("Error encoding probe message: %s", err.Error())
return
}
// send offer
body, err := messages.EncodePollResponse(sdp, true, "")
if err != nil {
log.Printf("Error encoding probe message: %s", err.Error())
return
}
resp, err := probe.Post(probe.url.String(), bytes.NewBuffer(body))
if err != nil {
log.Printf("error polling probe: %s", err.Error())
return
}
sdp, _, err = messages.DecodeAnswerRequest(resp)
if err != nil {
log.Printf("Error reading probe response: %s", err.Error())
return
}
answer, err := util.DeserializeSessionDescription(sdp)
if err != nil {
log.Printf("Error setting answer: %s", err.Error())
return
}
err = pc.SetRemoteDescription(*answer)
if err != nil {
log.Printf("Error setting answer: %s", err.Error())
return
}
select {
case <-dataChan:
currentNATType = NATUnrestricted
case <-time.After(dataChannelTimeout):
currentNATType = NATRestricted
}
if err := pc.Close(); err != nil {
log.Printf("error calling pc.Close: %v", err)
}
}

44
proxy/lib/tokens.go Normal file
View file

@ -0,0 +1,44 @@
package snowflake
import (
"sync/atomic"
)
type tokens_t struct {
ch chan struct{}
capacity uint
clients int64
}
func newTokens(capacity uint) *tokens_t {
var ch chan struct{}
if capacity != 0 {
ch = make(chan struct{}, capacity)
}
return &tokens_t{
ch: ch,
capacity: capacity,
clients: 0,
}
}
func (t *tokens_t) get() {
atomic.AddInt64(&t.clients, 1)
if t.capacity != 0 {
t.ch <- struct{}{}
}
}
func (t *tokens_t) ret() {
atomic.AddInt64(&t.clients, -1)
if t.capacity != 0 {
<-t.ch
}
}
func (t tokens_t) count() int64 {
return atomic.LoadInt64(&t.clients)
}

28
proxy/lib/tokens_test.go Normal file
View file

@ -0,0 +1,28 @@
package snowflake
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestTokens(t *testing.T) {
Convey("Tokens", t, func() {
tokens := newTokens(2)
So(tokens.count(), ShouldEqual, 0)
tokens.get()
So(tokens.count(), ShouldEqual, 1)
tokens.ret()
So(tokens.count(), ShouldEqual, 0)
})
Convey("Tokens capacity 0", t, func() {
tokens := newTokens(0)
So(tokens.count(), ShouldEqual, 0)
for i := 0; i < 20; i++ {
tokens.get()
}
So(tokens.count(), ShouldEqual, 20)
tokens.ret()
So(tokens.count(), ShouldEqual, 19)
})
}

94
proxy/lib/util.go Normal file
View file

@ -0,0 +1,94 @@
package snowflake
import (
"fmt"
"time"
)
// BytesLogger is an interface which is used to allow logging the throughput
// of the Snowflake. A default BytesLogger(BytesNullLogger) does nothing.
type BytesLogger interface {
AddOutbound(int)
AddInbound(int)
ThroughputSummary() string
}
// BytesNullLogger Default BytesLogger does nothing.
type BytesNullLogger struct{}
// AddOutbound in BytesNullLogger does nothing
func (b BytesNullLogger) AddOutbound(amount int) {}
// AddInbound in BytesNullLogger does nothing
func (b BytesNullLogger) AddInbound(amount int) {}
// ThroughputSummary in BytesNullLogger does nothing
func (b BytesNullLogger) ThroughputSummary() string { return "" }
// BytesSyncLogger uses channels to safely log from multiple sources with output
// occuring at reasonable intervals.
type BytesSyncLogger struct {
outboundChan, inboundChan chan int
outbound, inbound, outEvents, inEvents int
start time.Time
}
// NewBytesSyncLogger returns a new BytesSyncLogger and starts it loggin.
func NewBytesSyncLogger() *BytesSyncLogger {
b := &BytesSyncLogger{
outboundChan: make(chan int, 5),
inboundChan: make(chan int, 5),
}
go b.log()
b.start = time.Now()
return b
}
func (b *BytesSyncLogger) log() {
for {
select {
case amount := <-b.outboundChan:
b.outbound += amount
b.outEvents++
case amount := <-b.inboundChan:
b.inbound += amount
b.inEvents++
}
}
}
// AddOutbound add a number of bytes to the outbound total reported by the logger
func (b *BytesSyncLogger) AddOutbound(amount int) {
b.outboundChan <- amount
}
// AddInbound add a number of bytes to the inbound total reported by the logger
func (b *BytesSyncLogger) AddInbound(amount int) {
b.inboundChan <- amount
}
// ThroughputSummary view a formatted summary of the throughput totals
func (b *BytesSyncLogger) ThroughputSummary() string {
var inUnit, outUnit string
units := []string{"B", "KB", "MB", "GB"}
inbound := b.inbound
outbound := b.outbound
for i, u := range units {
inUnit = u
if (inbound < 1000) || (i == len(units)-1) {
break
}
inbound = inbound / 1000
}
for i, u := range units {
outUnit = u
if (outbound < 1000) || (i == len(units)-1) {
break
}
outbound = outbound / 1000
}
t := time.Now()
return fmt.Sprintf("Traffic throughput (up|down): %d %s|%d %s -- (%d OnMessages, %d Sends, over %d seconds)", inbound, inUnit, outbound, outUnit, b.outEvents, b.inEvents, int(t.Sub(b.start).Seconds()))
}

121
proxy/lib/webrtcconn.go Normal file
View file

@ -0,0 +1,121 @@
package snowflake
import (
"fmt"
"io"
"log"
"net"
"regexp"
"sync"
"time"
"github.com/pion/ice/v2"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3"
)
var remoteIPPatterns = []*regexp.Regexp{
/* IPv4 */
regexp.MustCompile(`(?m)^c=IN IP4 ([\d.]+)(?:(?:\/\d+)?\/\d+)?(:? |\r?\n)`),
/* IPv6 */
regexp.MustCompile(`(?m)^c=IN IP6 ([0-9A-Fa-f:.]+)(?:\/\d+)?(:? |\r?\n)`),
}
type webRTCConn struct {
dc *webrtc.DataChannel
pc *webrtc.PeerConnection
pr *io.PipeReader
lock sync.Mutex // Synchronization for DataChannel destruction
once sync.Once // Synchronization for PeerConnection destruction
bytesLogger BytesLogger
}
func (c *webRTCConn) Read(b []byte) (int, error) {
return c.pr.Read(b)
}
func (c *webRTCConn) Write(b []byte) (int, error) {
c.bytesLogger.AddInbound(len(b))
c.lock.Lock()
defer c.lock.Unlock()
if c.dc != nil {
c.dc.Send(b)
}
return len(b), nil
}
func (c *webRTCConn) Close() (err error) {
c.once.Do(func() {
err = c.pc.Close()
})
return
}
func (c *webRTCConn) LocalAddr() net.Addr {
return nil
}
func (c *webRTCConn) RemoteAddr() net.Addr {
//Parse Remote SDP offer and extract client IP
clientIP := remoteIPFromSDP(c.pc.RemoteDescription().SDP)
if clientIP == nil {
return nil
}
return &net.IPAddr{IP: clientIP, Zone: ""}
}
func (c *webRTCConn) SetDeadline(t time.Time) error {
// nolint: golint
return fmt.Errorf("SetDeadline not implemented")
}
func (c *webRTCConn) SetReadDeadline(t time.Time) error {
// nolint: golint
return fmt.Errorf("SetReadDeadline not implemented")
}
func (c *webRTCConn) SetWriteDeadline(t time.Time) error {
// nolint: golint
return fmt.Errorf("SetWriteDeadline not implemented")
}
func remoteIPFromSDP(str string) net.IP {
// Look for remote IP in "a=candidate" attribute fields
// https://tools.ietf.org/html/rfc5245#section-15.1
var desc sdp.SessionDescription
err := desc.Unmarshal([]byte(str))
if err != nil {
log.Println("Error parsing SDP: ", err.Error())
return nil
}
for _, m := range desc.MediaDescriptions {
for _, a := range m.Attributes {
if a.IsICECandidate() {
c, err := ice.UnmarshalCandidate(a.Value)
if err == nil {
ip := net.ParseIP(c.Address())
if ip != nil && isRemoteAddress(ip) {
return ip
}
}
}
}
}
// Finally look for remote IP in "c=" Connection Data field
// https://tools.ietf.org/html/rfc4566#section-5.7
for _, pattern := range remoteIPPatterns {
m := pattern.FindStringSubmatch(str)
if m != nil {
// Ignore parsing errors, ParseIP returns nil.
ip := net.ParseIP(m[1])
if ip != nil && isRemoteAddress(ip) {
return ip
}
}
}
return nil
}