├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── as.go ├── config.centrifugo.test.json ├── eventHandler.go ├── go.mod ├── go.sum ├── how.png ├── main.go ├── main_test.go └── to.go /.gitignore: -------------------------------------------------------------------------------- 1 | webrtc-socket-proxy 2 | centrifugo 3 | .vscode 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - master 5 | 6 | before_script: 7 | - wget https://github.com/centrifugal/centrifugo/releases/download/v2.2.2/centrifugo_2.2.2_linux_amd64.tar.gz 8 | - tar xzf centrifugo_2.2.2_linux_amd64.tar.gz 9 | - ./centrifugo --config config.centrifugo.test.json --log_level=debug & 10 | 11 | script: 12 | - go build 13 | - go test 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Chieh Po 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | webrtc-socket-proxy 3 |

4 |

Seamless peer-to-peer TCP socket proxy using WebRTC, with centrifugo as the signal server

5 | 6 |

7 | 8 | 9 | License: MIT
10 |
11 |
12 |

13 | 14 | ## Setup 15 | 16 | * Install `webrtc-socket-proxy` 17 | 18 | ``` 19 | $ go get -u github.com/poga/webrtc-socket-proxy 20 | ``` 21 | 22 | * Setup [centrifugo](https://github.com/centrifugal/centrifugo/releases) with [example config](config.centrifugo.test.json). 23 | 24 | ## Usage 25 | 26 | ``` 27 | # the `As` proxy 28 | $ webrtc-socket-proxy -signal= -secret= -as= -upstreamAddr=localhost:8000 29 | # the `To` proxy 30 | $ webrtc-socket-proxy -signal= -secret= -to= -listen=:4444 31 | ``` 32 | 33 | You can send data to your `` via connecting to `:4444` now. 34 | 35 | ## Roadmap 36 | 37 | - [ ] TURN server support 38 | - [ ] Multiplex Connections. Currently we only support one connnection per proxy-pair 39 | 40 | ## License 41 | 42 | The MIT License -------------------------------------------------------------------------------- /as.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/pion/webrtc/v2" 9 | "github.com/rs/zerolog/log" 10 | 11 | centrifuge "github.com/centrifugal/centrifuge-go" 12 | jwt "github.com/dgrijalva/jwt-go" 13 | ) 14 | 15 | // As listen to new webRTC connection and proxy it to upstream tcp socket 16 | type As struct { 17 | UpstreamAddr string 18 | SignalServerAddr string 19 | ID string 20 | 21 | peerConn *webrtc.PeerConnection 22 | 23 | UpstreamChan chan []byte 24 | } 25 | 26 | // NewAs returns a new "As" proxy 27 | func NewAs(ID string, secret string, upstreamAddr string, signalServerAddr string) *As { 28 | log.Print("hello") 29 | 30 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 31 | "sub": ID, 32 | }) 33 | 34 | // Sign and get the complete encoded token as a string using the secret 35 | tokenString, err := token.SignedString([]byte(secret)) 36 | 37 | as := As{ID: ID, UpstreamAddr: upstreamAddr, SignalServerAddr: signalServerAddr} 38 | upChan := make(chan []byte) 39 | as.UpstreamChan = upChan 40 | 41 | signalConfig := centrifuge.DefaultConfig() 42 | log.Print(as.SignalServerAddr) 43 | signalClient := centrifuge.New(as.SignalServerAddr, signalConfig) 44 | signalClient.SetToken(tokenString) 45 | signalClient.Connect() 46 | 47 | webRTCconfig := webrtc.Configuration{ 48 | ICEServers: []webrtc.ICEServer{ 49 | { 50 | URLs: []string{"stun:stun.l.google.com:19302"}, 51 | }, 52 | }, 53 | } 54 | peerConnection, err := webrtc.NewPeerConnection(webRTCconfig) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | dataChannel, err := peerConnection.CreateDataChannel("data", nil) 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | dataChannel.OnOpen(func() { 65 | log.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds", dataChannel.Label(), dataChannel.ID()) 66 | 67 | if as.UpstreamAddr == "" { 68 | log.Error().Msgf("cannot connect to upstream \"%s\"", as.UpstreamAddr) 69 | return 70 | } 71 | go func(upChan chan []byte) { 72 | upAddr, err := net.ResolveTCPAddr("tcp", as.UpstreamAddr) 73 | if err != nil { 74 | panic(err) 75 | } 76 | upConn, err := net.DialTCP("tcp", nil, upAddr) 77 | if err != nil { 78 | panic(err) 79 | } 80 | // read from socket 81 | go func() { 82 | for { 83 | data := make([]byte, 1024) 84 | n, err := upConn.Read(data) 85 | if err != nil { 86 | panic(err) 87 | } 88 | log.Printf("sending to down stream %#v", string(data[:n])) 89 | dataChannel.SendText(string(data[:n])) 90 | } 91 | }() 92 | 93 | for { 94 | data := <-upChan 95 | 96 | if _, err := upConn.Write(data); err != nil { 97 | panic(err) 98 | } 99 | } 100 | }(upChan) 101 | }) 102 | dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) { 103 | log.Printf("Message from DataChannel '%s': %#v", dataChannel.Label(), string(msg.Data)) 104 | 105 | as.UpstreamChan <- msg.Data 106 | }) 107 | 108 | // Create an offer to send to the browser 109 | offer, err := peerConnection.CreateOffer(nil) 110 | if err != nil { 111 | panic(err) 112 | } 113 | offerData, err := json.Marshal(offer) 114 | if err != nil { 115 | panic(err) 116 | } 117 | err = signalClient.Publish(fmt.Sprintf("offer-%s", ID), offerData) 118 | if err != nil { 119 | panic(err) 120 | } 121 | err = peerConnection.SetLocalDescription(offer) 122 | if err != nil { 123 | panic(err) 124 | } 125 | 126 | answerChan := make(chan []byte) 127 | sub, err := signalClient.NewSubscription(fmt.Sprintf("answer-%s", ID)) 128 | if err != nil { 129 | panic(err) 130 | } 131 | subHandler := &subEventHandler{answerChan} 132 | sub.OnPublish(subHandler) 133 | sub.OnJoin(subHandler) 134 | sub.OnLeave(subHandler) 135 | 136 | err = sub.Subscribe() 137 | if err != nil { 138 | panic(err) 139 | } 140 | 141 | log.Print("waiting answer") 142 | 143 | // wait answer 144 | answerData := <-answerChan 145 | log.Print("got answer") 146 | var answer webrtc.SessionDescription 147 | err = json.Unmarshal(answerData, &answer) 148 | if err != nil { 149 | panic(err) 150 | } 151 | 152 | log.Printf("%v", answer) 153 | 154 | // Apply the answer as the remote description 155 | err = peerConnection.SetRemoteDescription(answer) 156 | if err != nil { 157 | panic(err) 158 | } 159 | 160 | return &as 161 | } 162 | -------------------------------------------------------------------------------- /config.centrifugo.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "secret": "secret", 3 | "publish": true, 4 | "join_leave": true, 5 | "history_size": 10, 6 | "history_lifetime": 30 7 | } -------------------------------------------------------------------------------- /eventHandler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | centrifuge "github.com/centrifugal/centrifuge-go" 7 | "github.com/rs/zerolog/log" 8 | ) 9 | 10 | type subEventHandler struct { 11 | answerChan chan []byte 12 | } 13 | 14 | func (h *subEventHandler) OnPublish(sub *centrifuge.Subscription, e centrifuge.PublishEvent) { 15 | log.Print(fmt.Sprintf("New publication received from channel %s: %s", sub.Channel(), string(e.Data))) 16 | h.answerChan <- e.Data 17 | } 18 | 19 | func (h *subEventHandler) OnJoin(sub *centrifuge.Subscription, e centrifuge.JoinEvent) { 20 | log.Print(fmt.Sprintf("User %s (client ID %s) joined channel %s", e.User, e.Client, sub.Channel())) 21 | } 22 | 23 | func (h *subEventHandler) OnLeave(sub *centrifuge.Subscription, e centrifuge.LeaveEvent) { 24 | log.Print(fmt.Sprintf("User %s (client ID %s) left channel %s", e.User, e.Client, sub.Channel())) 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/poga/webrtc-socket-proxy 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/centrifugal/centrifuge-go v0.2.3 7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 8 | github.com/gogo/protobuf v1.3.0 // indirect 9 | github.com/gorilla/websocket v1.4.1 // indirect 10 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 // indirect 11 | github.com/pion/webrtc/v2 v2.1.0 12 | github.com/rs/zerolog v1.15.0 13 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a 14 | google.golang.org/grpc v1.23.1 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/centrifugal/centrifuge-go v0.2.3 h1:eopSUVHJTl3oU1B4Lq1eTZGkJvchVZxbMq8tBfSb/sM= 4 | github.com/centrifugal/centrifuge-go v0.2.3/go.mod h1:yQliRRkEQKOuekQGqKqtUIzk0KaDCk164JumfRWDXsc= 5 | github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= 6 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 7 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 11 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 12 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 13 | github.com/gogo/protobuf v1.3.0 h1:G8O7TerXerS4F6sx9OV7/nRfJdnXgHZu/S/7F2SN+UE= 14 | github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 15 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 16 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 17 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 18 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 19 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 20 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 21 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 22 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 23 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME= 24 | github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= 25 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 26 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 27 | github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= 28 | github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= 29 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 30 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 31 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 32 | github.com/pion/datachannel v1.4.5 h1:paz18kYAetpTdK8tlMAtDY+Ayxrv5fndZ5XPZwiZHrU= 33 | github.com/pion/datachannel v1.4.5/go.mod h1:SpMJbuu8v+qbA94m6lWQwSdCf8JKQvgmdSHDNtcbe+w= 34 | github.com/pion/dtls v1.5.0 h1:GtN3DJ0Fv5wC/Y04uOXT1zPeGA4C3HrsP1aAKsIBiU8= 35 | github.com/pion/dtls v1.5.0/go.mod h1:CjlPLfQdsTg3G4AEXjJp8FY5bRweBlxHrgoFrN+fQsk= 36 | github.com/pion/ice v0.5.5 h1:1S16+2EkN1aaH9uC4mp6HUqpbKg8Y69GbTybLGu9GcI= 37 | github.com/pion/ice v0.5.5/go.mod h1:FSTDLP+ian3PtxRjervtyDP2AOqt2c6cvfebZ7dwLnI= 38 | github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 39 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 40 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 41 | github.com/pion/mdns v0.0.3 h1:DxdOYd0pgwLKiDlIIxfU0qdG5iWh1Xn6CsS9vc6cMAY= 42 | github.com/pion/mdns v0.0.3/go.mod h1:VrN3wefVgtfL8QgpEblPUC46ag1reLIfpqekCnKunLE= 43 | github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k= 44 | github.com/pion/rtcp v1.2.1 h1:S3yG4KpYAiSmBVqKAfgRa5JdwBNj4zK3RLUa8JYdhak= 45 | github.com/pion/rtcp v1.2.1/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM= 46 | github.com/pion/rtp v1.1.3 h1:GTYSTsSLF5vH+UqShGYQEBdoYasWjTTC9UeYglnUO+o= 47 | github.com/pion/rtp v1.1.3/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE= 48 | github.com/pion/sctp v1.6.3/go.mod h1:cCqpLdYvgEUdl715+qbWtgT439CuQrAgy8BZTp0aEfA= 49 | github.com/pion/sctp v1.6.4 h1:edUNxTabSErLWOdeUUAxds8gwx5kGnFam4zL5DWpILk= 50 | github.com/pion/sctp v1.6.4/go.mod h1:cCqpLdYvgEUdl715+qbWtgT439CuQrAgy8BZTp0aEfA= 51 | github.com/pion/sdp/v2 v2.3.0 h1:5EhwPh1xKWYYjjvMuubHoMLy6M0B9U26Hh7q3f7vEGk= 52 | github.com/pion/sdp/v2 v2.3.0/go.mod h1:idSlWxhfWQDtTy9J05cgxpHBu/POwXN2VDRGYxT/EjU= 53 | github.com/pion/srtp v1.2.6 h1:mHQuAMh0P67R7/j1F260u3O+fbRWLyjKLRPZYYvODFM= 54 | github.com/pion/srtp v1.2.6/go.mod h1:rd8imc5htjfs99XiEoOjLMEOcVjME63UHx9Ek9IGst0= 55 | github.com/pion/stun v0.3.1 h1:d09JJzOmOS8ZzIp8NppCMgrxGZpJ4Ix8qirfNYyI3BA= 56 | github.com/pion/stun v0.3.1/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= 57 | github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= 58 | github.com/pion/transport v0.7.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= 59 | github.com/pion/transport v0.8.6 h1:xHQq2mxAjB+UrFs90aUBaXwlmIACfQAZnOiVAX3uqMw= 60 | github.com/pion/transport v0.8.6/go.mod h1:nAmRRnn+ArVtsoNuwktvAD+jrjSD7pA+H3iRmZwdUno= 61 | github.com/pion/turn v1.3.3 h1:jO8bYTgUZ7ls6BCVa5lIQhxu56UFc5afX1A50vfxFio= 62 | github.com/pion/turn v1.3.3/go.mod h1:zGPB7YYB/HTE9MWn0Sbznz8NtyfeVeanZ834cG/MXu0= 63 | github.com/pion/webrtc/v2 v2.1.0 h1:ZaZQI+wmhVtAO11afRg0p+15sxhs9w+CZlSd8Ur+grE= 64 | github.com/pion/webrtc/v2 v2.1.0/go.mod h1:74qrJ4vD600EX5sEDRUxbmr5eu7Ug0GwITuj7c7aZnI= 65 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 66 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 67 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 70 | github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= 71 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 72 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 73 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 74 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 75 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 76 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 77 | golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 78 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a h1:Igim7XhdOpBnWPuYJ70XcNpq8q3BCACtVgNfoJxOV7g= 79 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 80 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 81 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 82 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 83 | golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 84 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= 85 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 86 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 87 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 88 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 89 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 90 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 91 | golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 92 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= 94 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 96 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 97 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 98 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 99 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 100 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 101 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 102 | google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 103 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 104 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 105 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 106 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 107 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 108 | -------------------------------------------------------------------------------- /how.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ailabstw/webrtc-socket-proxy/bd691390ce48c264d17db4f8f651393f884747a9/how.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | 7 | "github.com/rs/zerolog" 8 | "github.com/rs/zerolog/log" 9 | ) 10 | 11 | var ( 12 | listenAddr = flag.String("listen", ":4444", "proxy listen address") 13 | upstreamAddr = flag.String("upstream", "", "proxy upstream address") 14 | signalServerAddr = flag.String("signal", "ws://localhost:8000/connection/websocket", "signaling server address") 15 | 16 | secret = flag.String("secret", "", "server secret") 17 | 18 | as = flag.String("as", "", "proxy ID") 19 | to = flag.String("to", "", "proxy target ID") 20 | ) 21 | 22 | func main() { 23 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout}) 24 | flag.Parse() 25 | 26 | if *as != "" { 27 | NewAs(*as, *secret, *upstreamAddr, *signalServerAddr) 28 | 29 | for { 30 | } 31 | } else if *to != "" { 32 | p := NewTo(*to, *secret, *signalServerAddr, *listenAddr) 33 | 34 | log.Print("Proxy listening" + p.Listen + " to target " + p.ID) 35 | p.ListenAndServe() 36 | 37 | return 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/rs/zerolog" 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | func TestSmoke(t *testing.T) { 14 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout}) 15 | go newEchoServer(":9064") 16 | 17 | signalServerAddr := "ws://localhost:8000/connection/websocket" 18 | 19 | go NewAs("smoke", "secret", ":9064", signalServerAddr) 20 | 21 | time.Sleep(1 * time.Second) 22 | 23 | to := NewTo("smoke", "secret", signalServerAddr, ":9063") 24 | go to.ListenAndServe() 25 | 26 | time.Sleep(1 * time.Second) 27 | 28 | receiveCount := 0 29 | 30 | go func() { 31 | addr, err := net.ResolveTCPAddr("tcp", ":9063") 32 | if err != nil { 33 | panic(err) 34 | } 35 | conn, err := net.DialTCP("tcp", nil, addr) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | log.Print("connected") 41 | 42 | // read from socket 43 | go func() { 44 | for { 45 | data := make([]byte, 1024) 46 | n, err := conn.Read(data) 47 | if err != nil { 48 | panic(err) 49 | } 50 | resp := string(data[:n]) 51 | log.Printf("echo received: %#v", resp) 52 | 53 | if resp == "smoke test\n" { 54 | receiveCount++ 55 | } else { 56 | t.Errorf("incorrect echo data %#v", data) 57 | } 58 | 59 | if receiveCount == 3 { 60 | os.Exit(0) 61 | } 62 | } 63 | }() 64 | for i := 0; i < 3; i++ { 65 | if _, err := conn.Write([]byte("smoke test\n")); err != nil { 66 | panic(err) 67 | } 68 | time.Sleep(1 * time.Second) 69 | } 70 | }() 71 | 72 | for { 73 | } 74 | } 75 | 76 | func newEchoServer(port string) { 77 | l, err := net.Listen("tcp", port) 78 | if err != nil { 79 | panic(err) 80 | } 81 | log.Print("Listening to connections on :9064") 82 | defer l.Close() 83 | 84 | for { 85 | conn, err := l.Accept() 86 | if err != nil { 87 | panic(err) 88 | } 89 | 90 | go handleRequest(conn) 91 | } 92 | } 93 | 94 | func handleRequest(conn net.Conn) { 95 | log.Print("Accepted new connection.") 96 | defer conn.Close() 97 | defer log.Print("Closed connection.") 98 | 99 | for { 100 | buf := make([]byte, 1024) 101 | size, err := conn.Read(buf) 102 | if err != nil { 103 | return 104 | } 105 | data := buf[:size] 106 | log.Printf("Read new data from connection %#v", string(data)) 107 | conn.Write(data) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /to.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/rs/zerolog/log" 9 | 10 | centrifuge "github.com/centrifugal/centrifuge-go" 11 | jwt "github.com/dgrijalva/jwt-go" 12 | "github.com/pion/webrtc/v2" 13 | ) 14 | 15 | // To listen to tcp socket and proxy it to webrtc connection 16 | type To struct { 17 | Listen string 18 | SignalServerAddr string 19 | ID string 20 | 21 | DataChannel *webrtc.DataChannel 22 | 23 | tcpConn *net.Conn 24 | } 25 | 26 | // NewTo returns a new "To" proxy 27 | func NewTo(ID string, secret string, signalServerAddr string, listen string) *To { 28 | log.Print("new to") 29 | to := To{ID: ID, SignalServerAddr: signalServerAddr, Listen: listen} 30 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 31 | "sub": ID, 32 | }) 33 | 34 | // Sign and get the complete encoded token as a string using the secret 35 | tokenString, err := token.SignedString([]byte(secret)) 36 | 37 | signalConfig := centrifuge.DefaultConfig() 38 | signalClient := centrifuge.New(to.SignalServerAddr, signalConfig) 39 | signalClient.SetToken(tokenString) 40 | signalClient.Connect() 41 | 42 | config := webrtc.Configuration{ 43 | ICEServers: []webrtc.ICEServer{ 44 | { 45 | URLs: []string{"stun:stun.l.google.com:19302"}, 46 | }, 47 | }, 48 | } 49 | peerConnection, err := webrtc.NewPeerConnection(config) 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | // Set the handler for ICE connection state 55 | // This will notify you when the peer has connected/disconnected 56 | peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { 57 | log.Printf("ICE Connection State has changed: %s", connectionState.String()) 58 | }) 59 | 60 | webRTCConnReady := make(chan struct{}) 61 | 62 | // Register data channel creation handling 63 | peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { 64 | log.Printf("New DataChannel %s %d", d.Label(), d.ID()) 65 | to.DataChannel = d 66 | 67 | // Register channel opening handling 68 | d.OnOpen(func() { 69 | log.Printf("Data channel '%s'-'%d' open. Random messages will now be sent to any connected DataChannels every 5 seconds", d.Label(), d.ID()) 70 | webRTCConnReady <- struct{}{} 71 | }) 72 | 73 | // Register text message handling 74 | d.OnMessage(func(msg webrtc.DataChannelMessage) { 75 | log.Printf("Message from DataChannel '%s': %#v", d.Label(), string(msg.Data)) 76 | 77 | (*to.tcpConn).Write(msg.Data) 78 | }) 79 | }) 80 | offerChan := make(chan []byte) 81 | sub, err := signalClient.NewSubscription(fmt.Sprintf("offer-%s", ID)) 82 | if err != nil { 83 | panic(err) 84 | } 85 | subHandler := &subEventHandler{offerChan} 86 | sub.OnPublish(subHandler) 87 | sub.OnJoin(subHandler) 88 | sub.OnLeave(subHandler) 89 | err = sub.Subscribe() 90 | if err != nil { 91 | panic(err) 92 | } 93 | 94 | history, err := sub.History() 95 | if err != nil { 96 | panic(err) 97 | } 98 | if len(history) > 0 { 99 | log.Printf("%s", history[0].Data) 100 | offerData := fmt.Sprintf("%s", history[0].Data) 101 | go func() { offerChan <- []byte(offerData[:]) }() 102 | } 103 | 104 | log.Print("waiting offer") 105 | offerData := <-offerChan 106 | log.Print("got offer") 107 | var offer webrtc.SessionDescription 108 | err = json.Unmarshal(offerData, &offer) 109 | if err != nil { 110 | panic(err) 111 | } 112 | 113 | err = peerConnection.SetRemoteDescription(offer) 114 | if err != nil { 115 | panic(err) 116 | } 117 | 118 | // Create answer 119 | answer, err := peerConnection.CreateAnswer(nil) 120 | if err != nil { 121 | panic(err) 122 | } 123 | // signal answer 124 | answerData, err := json.Marshal(answer) 125 | if err != nil { 126 | panic(err) 127 | } 128 | err = signalClient.Publish(fmt.Sprintf("answer-%s", ID), answerData) 129 | if err != nil { 130 | panic(err) 131 | } 132 | 133 | // Sets the LocalDescription, and starts our UDP listeners 134 | err = peerConnection.SetLocalDescription(answer) 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | return &to 140 | } 141 | 142 | // ListenAndServe listens on the TCP network address laddr and then handle packets 143 | // on incoming connections. 144 | func (s *To) ListenAndServe() error { 145 | listener, err := net.Listen("tcp", s.Listen) 146 | if err != nil { 147 | return err 148 | } 149 | return s.serve(listener) 150 | } 151 | 152 | func (s *To) serve(ln net.Listener) error { 153 | for { 154 | conn, err := ln.Accept() 155 | if err != nil { 156 | log.Print(err) 157 | continue 158 | } 159 | go s.handleConn(conn) 160 | } 161 | } 162 | 163 | func (s *To) handleConn(conn net.Conn) { 164 | if s.tcpConn != nil { 165 | log.Error().Msg("Only one connection is supported") 166 | conn.Close() 167 | return 168 | } 169 | s.tcpConn = &conn 170 | // write to dst what it reads from src 171 | var pipe = func(src net.Conn) { 172 | defer func() { 173 | conn.Close() 174 | }() 175 | 176 | for { 177 | data := make([]byte, 1024) 178 | n, err := src.Read(data) 179 | if err != nil { 180 | panic(err) 181 | } 182 | log.Printf("proxying %#v", string(data[:n])) 183 | err = s.DataChannel.SendText(string(data[:n])) 184 | if err != nil { 185 | panic(err) 186 | } 187 | } 188 | } 189 | 190 | go pipe(conn) 191 | } 192 | --------------------------------------------------------------------------------