├── .gitignore ├── 7z ├── 7z.dll └── 7z.exe ├── README.md ├── build.bat ├── build.sh ├── go.mod ├── kazari.png └── tcp2ws.go /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | tcp2ws 3 | tcp2ws.exe 4 | go.sum 5 | .DS_Store -------------------------------------------------------------------------------- /7z/7z.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zanjie1999/tcp-over-websocket/046617973f6849d5f264baa70b9253931c2e90b6/7z/7z.dll -------------------------------------------------------------------------------- /7z/7z.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zanjie1999/tcp-over-websocket/046617973f6849d5f264baa70b9253931c2e90b6/7z/7z.exe -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tcp over WebSocket (TCP to WebSocket, tcp2ws) 2 | 本工具能 使用WebSocket创建隧道,实现TCP端口转发 3 | 在v6.0及以后的版本支持了wss,也就是说现在可以实现https ssl进行更安全的传输 4 | 在v8.3及以后的版本支持了ip优选,会自动选择域名解析中最优的cdn ip进行连接 5 | 在v9.0及以后的版本支持了UDP,也就是说现在可以实现UDP端口转发了 6 | 也就是UDP over WebSocket (UDP to WebSocket, udp2ws) 并没有独立成新程序,写在一起了 7 | 启动时会同时转发指定的端口的TCP和UDP流量 8 | 9 | ## 因为经常修改优化,所以请Star,不要Fork 10 | ### 至于这样脱裤子放屁的操作有什么用? 11 | 举个例子,一个服务器只能通过cdn的http转发(或者https),它也不能联网,这时你就可以利用此工具将需要转发的端口(比如22)转换成ws协议(http)来传输,再去Nginx里面配一个反向代理,那么当本客户端访问Nginx提供的服务的特定路径时将反代到本服务端,实现内网穿透进行端口转发 12 | 当然Nginx不是必须的,直接把监听端口开放到公网上也行 13 | 这时防火墙仅仅发现你连接了一个WebSocket而已 14 | 并且在网络不稳定时,断开的ws会自动重连,保持着转发的tcp连接(断开ws超过2分钟tcp连接将被断开) 15 | 16 | ### 似乎有别的工具也能实类似的功能,写它干嘛? 17 | 最初为了实现将ssh用Nginx转发,写的时候还没找到这种工具,所以先实现了tcp放到ws中传输,但像Nginx就算是ws长链接,根据[官方文档](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_connect_timeout)描述,连接超时时间无论配置多大,通常都不能连接超过75秒,时间一到ssh就会断,于是实现了重连和重发,这是别的工具没有的 18 | 这个工具也不会有太多花里胡哨的功能,干好它的本职工作 19 | 20 | ### 那为什么要支持UDP呢? 21 | UDP是不可靠的,所以在传输过程中可能会丢包,但是这个工具会自动重传丢失的数据包 22 | 例如用做Dns转发,WebSocket会在客户端发送UDP数据包时才创建连接,并且在2分钟超时后自动断开,既保证了连接稳定性,又不会一直挂着连接占用资源 23 | 24 | ## 如何使用 25 | 在右边Releases中选择你使用的平台的程序来运行 26 | 服务端: 27 | `tcp2ws 要代理的ip:端口 本ws服务的监听端口` 28 | `tcp2ws 本地端口 本ws服务的监听端口` 29 | `tcp2ws 本地端口 监听ip:本ws服务的监听端口` 30 | 客户端: 31 | `tcp2ws ws://链接 本地监听端口` 32 | `tcp2ws http://链接 本地监听端口` 33 | 34 | 另外也可以使用wss(https ssl)协议,ssl更为安全,但需要消耗更多流量,需要指定证书路径,另外顺带提一下nginx可以把wss(https)转发到ws(http) 35 | 服务端: 36 | `tcp2ws 要代理的ip:端口 本ws服务的监听端口 证书.crt 证书.key` 37 | `tcp2ws 本地端口 本ws服务的监听端口 证书.crt 证书.key` 38 | `tcp2ws 本地端口 监听ip:本ws服务的监听端口 证书.crt 证书.key` 39 | 使用默认的文件名 server.crt server.key(这里的`wss`也可以是`https`或`ssl`) 40 | `tcp2ws 要代理的ip:端口 本ws服务的监听端口 wss` 41 | 客户端: 42 | `tcp2ws wss://链接 本地监听端口` 43 | `tcp2ws https://链接 本地监听端口` 44 | 45 | 生成自签证书的方法(一路回车即可): 46 | ``` 47 | openssl genrsa -out server.key 2048 48 | openssl ecparam -genkey -name secp384r1 -out server.key 49 | openssl req -new -x509 -sha256 -key server.key -out server.crt -days 36500 50 | ``` 51 | 52 | 举个🌰: 53 | 在服务器运行`tcp2ws 127.0.0.1:22 127.0.0.1:22222` 54 | 然后在nginx中反代了一下 55 | 在客户端运行`tcp2ws ws://yourdomain.com/ssh/ 222` 56 | 那么就可以通过客户端的222来访问服务器的ssh啦 57 | 是不是特别棒呢 58 | 59 | 还可以写一个死循环来守护运行 60 | `while true;do;tcp2ws 127.0.0.1:22 127.0.0.1:22222;done` 61 | 或者用screen来后台运行 62 | `screen -dmS tcp2ws bash -c "tcp2ws 127.0.0.1:22 127.0.0.1:22222"` 63 | 64 | ## 速度 65 | 在乞丐版M1 Pro的macOS下使用本工具来回转换iperf3端口测试得到的数据 66 | ``` 67 | [ ID] Interval Transfer Bitrate 68 | [ 5] 0.00-1.00 sec 988 MBytes 8.28 Gbits/sec 69 | [ 5] 1.00-2.00 sec 977 MBytes 8.19 Gbits/sec 70 | [ 5] 2.00-3.00 sec 982 MBytes 8.23 Gbits/sec 71 | [ 5] 3.00-4.00 sec 994 MBytes 8.34 Gbits/sec 72 | [ 5] 4.00-5.00 sec 966 MBytes 8.10 Gbits/sec 73 | [ 5] 5.00-6.00 sec 982 MBytes 8.24 Gbits/sec 74 | [ 5] 6.00-7.00 sec 989 MBytes 8.30 Gbits/sec 75 | [ 5] 7.00-8.00 sec 935 MBytes 7.84 Gbits/sec 76 | [ 5] 8.00-9.00 sec 1004 MBytes 8.42 Gbits/sec 77 | [ 5] 9.00-10.00 sec 984 MBytes 8.26 Gbits/sec 78 | - - - - - - - - - - - - - - - - - - - - - - - - - 79 | [ ID] Interval Transfer Bitrate 80 | [ 5] 0.00-10.00 sec 9.57 GBytes 8.22 Gbits/sec sender 81 | [ 5] 0.00-10.00 sec 9.56 GBytes 8.21 Gbits/sec receiver 82 | ``` 83 | 走wss,因为ssl,速度肉眼可见下降: 84 | ``` 85 | [ ID] Interval Transfer Bitrate 86 | [ 5] 0.00-10.00 sec 7.38 GBytes 6.34 Gbits/sec sender 87 | [ 5] 0.00-10.00 sec 7.38 GBytes 6.33 Gbits/sec receiver 88 | ``` 89 | 直连 90 | ``` 91 | [ ID] Interval Transfer Bitrate 92 | [ 5] 0.00-10.00 sec 119 GBytes 102 Gbits/sec sender 93 | [ 5] 0.00-10.00 sec 119 GBytes 102 Gbits/sec receiver 94 | ``` 95 | 在i7-8550u的Windows下使用本工具来回转换iperf3端口测试得到的数据 96 | ``` 97 | [ ID] Interval Transfer Bandwidth 98 | [ 4] 0.00-1.00 sec 300 MBytes 2.52 Gbits/sec 99 | [ 4] 1.00-2.00 sec 336 MBytes 2.81 Gbits/sec 100 | [ 4] 2.00-3.00 sec 320 MBytes 2.68 Gbits/sec 101 | [ 4] 3.00-4.00 sec 317 MBytes 2.66 Gbits/sec 102 | [ 4] 4.00-5.00 sec 302 MBytes 2.53 Gbits/sec 103 | [ 4] 5.00-6.00 sec 328 MBytes 2.75 Gbits/sec 104 | [ 4] 6.00-7.00 sec 312 MBytes 2.61 Gbits/sec 105 | [ 4] 7.00-8.00 sec 319 MBytes 2.67 Gbits/sec 106 | [ 4] 8.00-9.00 sec 322 MBytes 2.70 Gbits/sec 107 | [ 4] 9.00-10.00 sec 348 MBytes 2.92 Gbits/sec 108 | - - - - - - - - - - - - - - - - - - - - - - - - - 109 | [ ID] Interval Transfer Bandwidth 110 | [ 4] 0.00-10.00 sec 3.14 GBytes 2.70 Gbits/sec sender 111 | [ 4] 0.00-10.00 sec 3.13 GBytes 2.69 Gbits/sec receiver 112 | ``` 113 | 两个iperf3直连 114 | ``` 115 | [ ID] Interval Transfer Bandwidth 116 | [ 4] 0.00-10.00 sec 9.53 GBytes 8.19 Gbits/sec sender 117 | [ 4] 0.00-10.00 sec 9.53 GBytes 8.19 Gbits/sec receiver 118 | ``` 119 | 120 | ## 伪装 121 | 在直接访问监听端口的任意路径,默认会返回一个空白页面 122 | 可以写一个`index.html`放到运行目录下来代替这个空白页面 123 | 直接访问时就会显示这个文件的内容,伪装成一个非常普通的Web服务 124 | 推荐用一个叫SingleFile的插件可以把页面直接存成一个文件 125 | 126 | 127 | ### 协议 咩License 128 | 使用此项目视为您已阅读并同意遵守 [此LICENSE](https://github.com/zanjie1999/LICENSE) 129 | Using this project is deemed to indicate that you have read and agreed to abide by [this LICENSE](https://github.com/zanjie1999/LICENSE) 130 | 131 | 132 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | rd /s /q build 2 | mkdir build 3 | SET CGO_ENABLED=1 4 | SET GOARCH=amd64 5 | SET GOOS=windows 6 | go build -ldflags="-w -s" 7 | move tcp2ws.exe build\tcp2ws.exe 8 | SET GOARCH=386 9 | go build -ldflags="-w -s" 10 | move tcp2ws.exe build\tcp2ws-i386.exe 11 | SET CGO_ENABLED=0 12 | SET GOOS=linux 13 | go build -ldflags="-w -s" 14 | move tcp2ws build\tcp2ws-linux-i386 15 | SET GOARCH=amd64 16 | go build -ldflags="-w -s" 17 | 7z\7z a tcp2ws-linux.zip tcp2ws 18 | copy /b kazari.png+tcp2ws-linux.zip build\tcp2ws-zip-linux.png 19 | del tcp2ws-linux.zip 20 | move tcp2ws build\tcp2ws-linux 21 | SET GOARCH=arm 22 | go build -ldflags="-w -s" 23 | move tcp2ws build\tcp2ws-linux-arm 24 | SET GOARCH=mips 25 | go build -ldflags="-w -s" 26 | move tcp2ws build\tcp2ws-linux-mips 27 | SET GOARCH=arm64 28 | go build -ldflags="-w -s" 29 | move tcp2ws build\tcp2ws-linux-arm64 30 | SET GOOS=darwin 31 | go build -ldflags="-w -s" 32 | move tcp2ws build\tcp2ws-darwin-arm64 33 | SET GOARCH=amd64 34 | go build -ldflags="-w -s" 35 | move tcp2ws build\tcp2ws-darwin 36 | SET GOOS=freebsd 37 | go build -ldflags="-w -s" 38 | move tcp2ws build/tcp2ws-freebsd -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | cd `dirname $0` 2 | mkdir -p build 3 | rm -rf build/* 4 | export CGO_ENABLED=1 5 | export GOARCH=amd64 6 | export GOOS=windows 7 | go build -ldflags="-w -s" 8 | mv tcp2ws.exe build/tcp2ws.exe 9 | export GOARCH=386 10 | go build -ldflags="-w -s" 11 | mv tcp2ws.exe build/tcp2ws-i386.exe 12 | export CGO_ENABLED=0 13 | export GOOS=linux 14 | go build -ldflags="-w -s" 15 | mv tcp2ws build/tcp2ws-linux-i386 16 | export GOARCH=amd64 17 | go build -ldflags="-w -s" 18 | zip tcp2ws-linux.zip tcp2ws 19 | cp kazari.png build/tcp2ws-zip-linux.png 20 | cat tcp2ws-linux.zip >> build/tcp2ws-zip-linux.png 21 | rm tcp2ws-linux.zip 22 | mv tcp2ws build/tcp2ws-linux 23 | export GOARCH=arm 24 | go build -ldflags="-w -s" 25 | mv tcp2ws build/tcp2ws-linux-arm 26 | export GOARCH=mips 27 | go build -ldflags="-w -s" 28 | mv tcp2ws build/tcp2ws-linux-mips 29 | export GOARCH=arm64 30 | go build -ldflags="-w -s" 31 | mv tcp2ws build/tcp2ws-linux-arm64 32 | export GOOS=darwin 33 | go build -ldflags="-w -s" 34 | mv tcp2ws build/tcp2ws-darwin-arm64 35 | export GOARCH=amd64 36 | go build -ldflags="-w -s" 37 | mv tcp2ws build/tcp2ws-darwin 38 | export GOOS=freebsd 39 | go build -ldflags="-w -s" 40 | mv tcp2ws build/tcp2ws-freebsd -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module cupinkie.com/tcp2ws 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/google/uuid v1.2.0 7 | github.com/gorilla/websocket v1.4.2 8 | github.com/miekg/dns v1.1.54 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /kazari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zanjie1999/tcp-over-websocket/046617973f6849d5f264baa70b9253931c2e90b6/kazari.png -------------------------------------------------------------------------------- /tcp2ws.go: -------------------------------------------------------------------------------- 1 | // Tcp over WebSocket (tcp2ws) 2 | // 基于ws的内网穿透工具 3 | // Sparkle 20210430 4 | // 11.1 5 | 6 | package main 7 | 8 | import ( 9 | "crypto/tls" 10 | "fmt" 11 | "io/ioutil" 12 | "log" 13 | "net" 14 | "net/http" 15 | "net/url" 16 | "os" 17 | "os/signal" 18 | "regexp" 19 | "runtime" 20 | "strings" 21 | "sync" 22 | "time" 23 | 24 | "github.com/google/uuid" 25 | "github.com/gorilla/websocket" 26 | "github.com/miekg/dns" 27 | ) 28 | 29 | type tcp2wsSparkle struct { 30 | isUdp bool 31 | udpConn *net.UDPConn 32 | udpAddr *net.UDPAddr 33 | tcpConn net.Conn 34 | wsConn *websocket.Conn 35 | uuid string 36 | del bool 37 | buf [][]byte 38 | t int64 39 | } 40 | 41 | var ( 42 | tcpAddr string 43 | wsAddr string 44 | wsAddrIp string 45 | wsAddrPort = "" 46 | msgType int = websocket.BinaryMessage 47 | isServer bool 48 | connMap map[string]*tcp2wsSparkle = make(map[string]*tcp2wsSparkle) 49 | // go的map不是线程安全的 读写冲突就会直接exit 50 | connMapLock *sync.RWMutex = new(sync.RWMutex) 51 | ) 52 | 53 | var upgrader = websocket.Upgrader{ 54 | ReadBufferSize: 1024, 55 | WriteBufferSize: 1024, 56 | CheckOrigin: func(r *http.Request) bool { return true }, 57 | } 58 | 59 | func getConn(uuid string) (*tcp2wsSparkle, bool) { 60 | connMapLock.RLock() 61 | defer connMapLock.RUnlock() 62 | conn, haskey := connMap[uuid] 63 | return conn, haskey 64 | } 65 | 66 | func setConn(uuid string, conn *tcp2wsSparkle) { 67 | connMapLock.Lock() 68 | defer connMapLock.Unlock() 69 | connMap[uuid] = conn 70 | } 71 | 72 | func deleteConn(uuid string) { 73 | if conn, haskey := getConn(uuid); haskey && conn != nil && !conn.del { 74 | connMapLock.Lock() 75 | defer connMapLock.Unlock() 76 | conn.del = true 77 | if conn.udpConn != nil { 78 | conn.udpConn.Close() 79 | } 80 | if conn.tcpConn != nil { 81 | conn.tcpConn.Close() 82 | } 83 | if conn.wsConn != nil { 84 | log.Print(uuid, " bye") 85 | conn.wsConn.WriteMessage(websocket.TextMessage, []byte("tcp2wsSparkleClose")) 86 | conn.wsConn.Close() 87 | } 88 | delete(connMap, uuid) 89 | } 90 | } 91 | 92 | func dialNewWs(uuid string) bool { 93 | log.Print("dial ", uuid) 94 | // call ws 95 | dialer := websocket.Dialer{TLSClientConfig: &tls.Config{RootCAs: nil, InsecureSkipVerify: true}, Proxy: http.ProxyFromEnvironment, NetDial: meDial} 96 | // println("tcpAddr ", tcpAddr, " wsAddr ", wsAddr, " wsAddrIp ", wsAddrIp, " wsAddrPort ", wsAddrPort) 97 | wsConn, _, err := dialer.Dial(wsAddr, nil) 98 | if err != nil { 99 | log.Print("connect to ws err: ", err) 100 | return false 101 | } 102 | // send uuid 103 | if err := wsConn.WriteMessage(websocket.TextMessage, []byte(uuid)); err != nil { 104 | log.Print("udp send ws uuid err: ", err) 105 | wsConn.Close() 106 | return false 107 | } 108 | // update 109 | if conn, haskey := getConn(uuid); haskey { 110 | if conn.wsConn != nil { 111 | conn.wsConn.Close() 112 | } 113 | conn.wsConn = wsConn 114 | conn.t = time.Now().Unix() 115 | writeErrorBuf2Ws(conn) 116 | } 117 | return true 118 | } 119 | 120 | // 将tcp或udp的数据转发到ws 121 | func readTcp2Ws(uuid string) bool { 122 | defer func() { 123 | err := recover() 124 | if err != nil { 125 | log.Print(uuid, " tcp -> ws Boom!\n", err) 126 | // readTcp2Ws(uuid) 127 | } 128 | }() 129 | 130 | conn, haskey := getConn(uuid) 131 | if !haskey { 132 | return false 133 | } 134 | buf := make([]byte, 500000) 135 | tcpConn := conn.tcpConn 136 | udpConn := conn.udpConn 137 | isUdp := conn.isUdp 138 | for { 139 | if conn.del || !isUdp && tcpConn == nil || isUdp && udpConn == nil { 140 | return false 141 | } 142 | var length int 143 | var err error 144 | if isUdp { 145 | length, conn.udpAddr, err = udpConn.ReadFromUDP(buf) 146 | // 客户端udp先收到内容再创建ws连接 服务端不可能进入这里 147 | if !isServer && conn.wsConn == nil { 148 | log.Print("try reconnect to ws ", uuid) 149 | if !dialNewWs(uuid) { 150 | // udp ws连接失败 存起来 下次重试 151 | saveErrorBuf(conn, buf, length) 152 | continue 153 | } 154 | go readWs2TcpClient(uuid, true) 155 | } 156 | } else { 157 | length, err = tcpConn.Read(buf) 158 | } 159 | if err != nil { 160 | if conn, haskey := getConn(uuid); haskey && !conn.del { 161 | // tcp中断 关闭所有连接 关过的就不用关了 162 | if err.Error() != "EOF" { 163 | if isUdp { 164 | log.Print(uuid, " udp read err: ", err) 165 | } else { 166 | log.Print(uuid, " tcp read err: ", err) 167 | } 168 | } 169 | deleteConn(uuid) 170 | return false 171 | } 172 | return false 173 | } 174 | // log.Print(uuid, " ws send: ", length) 175 | if length > 0 { 176 | // 因为tcpConn.Read会阻塞 所以要从connMap中获取最新的wsConn 177 | conn, haskey := getConn(uuid) 178 | if !haskey || conn.del { 179 | return false 180 | } 181 | wsConn := conn.wsConn 182 | conn.t = time.Now().Unix() 183 | if wsConn == nil { 184 | if isServer { 185 | // 服务端退出等下次连上来 186 | return false 187 | } 188 | // 客户端 tcp上次重连没有成功 保存并重连 服务端不会设置成nil不会进这里 189 | saveErrorBuf(conn, buf, length) 190 | log.Print("try reconnect to ws ", uuid) 191 | go runClient(nil, uuid) 192 | continue 193 | } 194 | if err = wsConn.WriteMessage(msgType, buf[:length]); err != nil { 195 | log.Print(uuid, " ws write err: ", err) 196 | // tcpConn.Close() 197 | wsConn.Close() 198 | saveErrorBuf(conn, buf, length) 199 | // 此处无需中断 等着新的wsConn 或是被 断开连接 / 回收 即可 200 | } 201 | // if !isServer { 202 | // log.Print(uuid, " send: ", length) 203 | // } 204 | } 205 | } 206 | } 207 | 208 | // 将ws的数据转发到tcp或udp 209 | func readWs2Tcp(uuid string) bool { 210 | defer func() { 211 | err := recover() 212 | if err != nil { 213 | log.Print(uuid, " ws -> tcp Boom!\n", err) 214 | // readWs2Tcp(uuid) 215 | } 216 | }() 217 | 218 | conn, haskey := getConn(uuid) 219 | if !haskey { 220 | return false 221 | } 222 | wsConn := conn.wsConn 223 | tcpConn := conn.tcpConn 224 | udpConn := conn.udpConn 225 | isUdp := conn.isUdp 226 | for { 227 | if conn.del || !isUdp && tcpConn == nil || isUdp && udpConn == nil || wsConn == nil { 228 | return false 229 | } 230 | t, buf, err := wsConn.ReadMessage() 231 | if err != nil || t == -1 { 232 | wsConn.Close() 233 | if conn, haskey := getConn(uuid); haskey && !conn.del { 234 | // 外部干涉导致中断 重连ws 235 | log.Print(uuid, " ws read err: ", err) 236 | return true 237 | } 238 | return false 239 | } 240 | // log.Print(uuid, " ws recv: ", len(buf)) 241 | if len(buf) > 0 { 242 | conn.t = time.Now().Unix() 243 | if t == websocket.TextMessage { 244 | msg := string(buf) 245 | if msg == "tcp2wsSparkle" { 246 | log.Print(uuid, " 咩") 247 | continue 248 | } else if msg == "tcp2wsSparkleClose" { 249 | log.Print(uuid, " say bye") 250 | connMapLock.Lock() 251 | defer connMapLock.Unlock() 252 | wsConn.Close() 253 | if isUdp { 254 | udpConn.Close() 255 | } else { 256 | tcpConn.Close() 257 | } 258 | delete(connMap, uuid) 259 | return false 260 | } 261 | } 262 | msgType = t 263 | if isUdp { 264 | if isServer { 265 | if _, err = udpConn.Write(buf); err != nil { 266 | log.Print(uuid, " udp write err: ", err) 267 | deleteConn(uuid) 268 | return false 269 | } 270 | } else { 271 | // 客户端作为udp服务端回复需要udp客户端发送数据时提供的udpAddr 272 | if _, err = udpConn.WriteToUDP(buf, conn.udpAddr); err != nil { 273 | log.Print(uuid, " udp write err: ", err) 274 | deleteConn(uuid) 275 | return false 276 | } 277 | } 278 | } else { 279 | if _, err = tcpConn.Write(buf); err != nil { 280 | log.Print(uuid, " tcp write err: ", err) 281 | deleteConn(uuid) 282 | return false 283 | } 284 | } 285 | } 286 | } 287 | } 288 | 289 | // 多了一个被动断开后自动重连的功能 290 | func readWs2TcpClient(uuid string, isUdp bool) { 291 | if readWs2Tcp(uuid) { 292 | log.Print(uuid, " ws Boom!") 293 | // error return re call ws 294 | conn, haskey := getConn(uuid) 295 | if haskey { 296 | // 删除wsConn 297 | conn.wsConn = nil 298 | if !isUdp { 299 | // udp的话下次收到数据时会重新建立ws连接 tcp现在重连 300 | runClient(nil, uuid) 301 | } 302 | } 303 | } 304 | } 305 | 306 | // 将没写成的内容写到ws 307 | func writeErrorBuf2Ws(conn *tcp2wsSparkle) { 308 | if conn != nil { 309 | for i := 0; i < len(conn.buf); i++ { 310 | conn.wsConn.WriteMessage(websocket.BinaryMessage, conn.buf[i]) 311 | } 312 | conn.buf = nil 313 | } 314 | } 315 | 316 | // 拷贝当前发生失败内容并保存 317 | func saveErrorBuf(conn *tcp2wsSparkle, buf []byte, length int) { 318 | if conn != nil { 319 | tmp := make([]byte, length) 320 | copy(tmp, buf[:length]) 321 | if conn.buf == nil { 322 | conn.buf = [][]byte{tmp} 323 | } else { 324 | conn.buf = append(conn.buf, tmp) 325 | } 326 | } 327 | } 328 | 329 | // 自定义的Dial连接器,自定义域名解析 330 | func meDial(network, address string) (net.Conn, error) { 331 | // return net.DialTimeout(network, address, 5 * time.Second) 332 | return net.DialTimeout(network, wsAddrIp+wsAddrPort, 5*time.Second) 333 | } 334 | 335 | // 服务端 是tcp还是udp连接是客户端发过来的 336 | func runServer(wsConn *websocket.Conn) { 337 | defer func() { 338 | err := recover() 339 | if err != nil { 340 | log.Print("server Boom!\n", err) 341 | } 342 | }() 343 | 344 | var isUdp bool 345 | var udpConn *net.UDPConn 346 | var tcpConn net.Conn 347 | var uuid string 348 | // read uuid to get from connMap 349 | t, buf, err := wsConn.ReadMessage() 350 | if err != nil || t == -1 || len(buf) == 0 { 351 | log.Print("ws uuid read err: ", err) 352 | wsConn.Close() 353 | return 354 | } 355 | if t == websocket.TextMessage { 356 | uuid = string(buf) 357 | if uuid == "" { 358 | log.Print("ws uuid read empty") 359 | return 360 | } 361 | // U 开头的uuid为udp连接 362 | isUdp = strings.HasPrefix(uuid, "U") 363 | if conn, haskey := getConn(uuid); haskey { 364 | // get 365 | udpConn = conn.udpConn 366 | tcpConn = conn.tcpConn 367 | conn.wsConn.Close() 368 | conn.wsConn = wsConn 369 | writeErrorBuf2Ws(conn) 370 | } 371 | } 372 | 373 | // uuid没有找到 新连接 374 | if isUdp && udpConn == nil { 375 | // call new udp 376 | log.Print("new udp for ", uuid) 377 | udpAddr, err := net.ResolveUDPAddr("udp4", tcpAddr) 378 | if err != nil { 379 | log.Print("resolve udp addr err: ", err) 380 | return 381 | } 382 | udpConn, err = net.DialUDP("udp", nil, udpAddr) 383 | if err != nil { 384 | log.Print("connect to udp err: ", err) 385 | wsConn.WriteMessage(websocket.TextMessage, []byte("tcp2wsSparkleClose")) 386 | wsConn.Close() 387 | return 388 | } 389 | 390 | // save 391 | setConn(uuid, &tcp2wsSparkle{true, udpConn, nil, nil, wsConn, uuid, false, nil, time.Now().Unix()}) 392 | 393 | go readTcp2Ws(uuid) 394 | } else if !isUdp && tcpConn == nil { 395 | // call new tcp 396 | log.Print("new tcp for ", uuid) 397 | tcpConn, err = net.Dial("tcp", tcpAddr) 398 | if err != nil { 399 | log.Print("connect to tcp err: ", err) 400 | wsConn.WriteMessage(websocket.TextMessage, []byte("tcp2wsSparkleClose")) 401 | wsConn.Close() 402 | return 403 | } 404 | 405 | // save 406 | setConn(uuid, &tcp2wsSparkle{false, nil, nil, tcpConn, wsConn, uuid, false, nil, time.Now().Unix()}) 407 | 408 | go readTcp2Ws(uuid) 409 | } else { 410 | log.Print("uuid finded ", uuid) 411 | } 412 | 413 | go readWs2Tcp(uuid) 414 | } 415 | 416 | // tcp客户端 417 | func runClient(tcpConn net.Conn, uuid string) { 418 | defer func() { 419 | err := recover() 420 | if err != nil { 421 | log.Print("client Boom!\n", err) 422 | } 423 | }() 424 | 425 | // is reconnect 426 | if tcpConn == nil { 427 | // conn is close? 428 | if conn, haskey := getConn(uuid); haskey { 429 | if conn.del { 430 | return 431 | } 432 | } else { 433 | return 434 | } 435 | } else { 436 | // save conn 437 | setConn(uuid, &tcp2wsSparkle{false, nil, nil, tcpConn, nil, uuid, false, nil, time.Now().Unix()}) 438 | } 439 | if dialNewWs(uuid) { 440 | // connect ok 441 | go readWs2TcpClient(uuid, false) 442 | if tcpConn != nil { 443 | // 不是重连 444 | go readTcp2Ws(uuid) 445 | } 446 | } else { 447 | log.Print("reconnect to ws fail") 448 | } 449 | } 450 | 451 | // udp客户端 452 | func runClientUdp(listenHostPort string) { 453 | defer func() { 454 | err := recover() 455 | if err != nil { 456 | log.Print("udp client Boom!\n", err) 457 | } 458 | }() 459 | uuid := "U" + uuid.New().String()[32:] 460 | for { 461 | log.Print("Create UDP Listen: ", listenHostPort) 462 | // 开udp监听 463 | udpAddr, err := net.ResolveUDPAddr("udp4", listenHostPort) 464 | if err != nil { 465 | log.Print("UDP Addr Resolve Error: ", err) 466 | return 467 | } 468 | udpConn, err := net.ListenUDP("udp", udpAddr) 469 | if err != nil { 470 | log.Print("UDP Listen Start Error: ", err) 471 | return 472 | } 473 | 474 | // save 475 | setConn(uuid, &tcp2wsSparkle{true, udpConn, nil, nil, nil, uuid, false, nil, time.Now().Unix()}) 476 | 477 | // 收到内容后会开ws连接并拿到UDPAddr 阻塞 478 | readTcp2Ws(uuid) 479 | } 480 | 481 | } 482 | 483 | // 响应ws请求 484 | func wsHandler(w http.ResponseWriter, r *http.Request) { 485 | forwarded := r.Header.Get("X-Forwarded-For") 486 | // 不是ws的请求返回index.html 假装是一个静态服务器 487 | if r.Header.Get("Upgrade") != "websocket" { 488 | if forwarded == "" { 489 | log.Print("not ws: ", r.RemoteAddr) 490 | } else { 491 | log.Print("not ws: ", forwarded) 492 | } 493 | _, err := os.Stat("index.html") 494 | if err == nil { 495 | http.ServeFile(w, r, "index.html") 496 | } 497 | return 498 | } else { 499 | if forwarded == "" { 500 | log.Print("new ws conn: ", r.RemoteAddr) 501 | } else { 502 | log.Print("new ws conn: ", forwarded) 503 | } 504 | } 505 | 506 | // ws协议握手 507 | conn, err := upgrader.Upgrade(w, r, nil) 508 | if err != nil { 509 | log.Print("ws upgrade err: ", err) 510 | return 511 | } 512 | 513 | // 新线程hold住这条连接 514 | go runServer(conn) 515 | } 516 | 517 | // 响应tcp 518 | func tcpHandler(listener net.Listener) { 519 | for { 520 | conn, err := listener.Accept() 521 | if err != nil { 522 | log.Print("tcp accept err: ", err) 523 | return 524 | } 525 | 526 | log.Print("new tcp conn: ") 527 | 528 | // 新线程hold住这条连接 529 | go runClient(conn, uuid.New().String()[31:]) 530 | } 531 | } 532 | 533 | // 启动ws服务 534 | func startWsServer(listenPort string, isSsl bool, sslCrt string, sslKey string) { 535 | var err error = nil 536 | if isSsl { 537 | fmt.Println("use ssl cert: " + sslCrt + " " + sslKey) 538 | err = http.ListenAndServeTLS(listenPort, sslCrt, sslKey, nil) 539 | } else { 540 | err = http.ListenAndServe(listenPort, nil) 541 | } 542 | if err != nil { 543 | log.Fatal("tcp2ws Server Start Error: ", err) 544 | } 545 | } 546 | 547 | // 又造轮子了 发现给v4的ip加个框也能连诶 548 | func tcping(hostname, port string) int64 { 549 | st := time.Now().UnixNano() 550 | c, err := net.DialTimeout("tcp", "["+hostname+"]"+port, 5*time.Second) 551 | if err != nil { 552 | return -1 553 | } 554 | c.Close() 555 | return (time.Now().UnixNano() - st) / 1e6 556 | } 557 | 558 | // 优选ip 559 | func dnsPreferIp(hostname string) (string, uint32) { 560 | // 由正则驱动的hosts解析器 此解析器拥有超咩力 561 | hostsFile := "/etc/hosts" 562 | if runtime.GOOS == "windows" { 563 | hostsFile = os.Getenv("SystemRoot") + `\System32\drivers\etc\hosts` 564 | } 565 | hosts, err := ioutil.ReadFile(hostsFile) 566 | if err == nil { 567 | re := regexp.MustCompile(`(?m)^([0-9.]+).*[ \t](` + hostname + `$|` + hostname + ` .*)`) 568 | matches := re.FindAllStringSubmatch(string(hosts), -1) 569 | if len(matches) > 0 { 570 | log.Print("Use System hosts: ", matches[0][1], " ", hostname) 571 | return matches[0][1], 0 572 | } 573 | } else { 574 | log.Print(`Read System hosts "`, hostsFile, `" error: `, err) 575 | } 576 | 577 | // 从dns获取 578 | log.Print("nslookup " + hostname) 579 | 580 | tc := dns.Client{Net: "tcp", Timeout: 10 * time.Second} 581 | uc := dns.Client{Net: "udp", Timeout: 10 * time.Second} 582 | m := dns.Msg{} 583 | m.SetQuestion(hostname+".", dns.TypeA) 584 | 585 | // 获取系统配置的dns 如果有就用它解析域名 windows咩咩不用不知道怎么写所以不支持 586 | // 由正则驱动的resolv.conf解析器 此解析器拥有超咩力 587 | systemDns := "127.0.0.1" 588 | if runtime.GOOS != "windows" { 589 | resolv, err := ioutil.ReadFile("/etc/resolv.conf") 590 | if err == nil { 591 | re := regexp.MustCompile(`(?m)^nameserver[ \t]+([0-9.]+).*`) 592 | matches := re.FindAllStringSubmatch(string(resolv), -1) 593 | if len(matches) > 0 { 594 | systemDns = matches[0][1] 595 | } 596 | } else { 597 | log.Print(`Read System resolv.conf "/etc/resolv.conf" error: `, err) 598 | } 599 | } 600 | r, _, err := uc.Exchange(&m, systemDns+":53") 601 | if err != nil { 602 | // log.Print("Local DNS Fail: ", err) 603 | r, _, err = tc.Exchange(&m, "208.67.222.222:5353") 604 | if err != nil { 605 | log.Print("OpenDNS Fail: ", err) 606 | return "", 0 607 | } 608 | } else { 609 | log.Print("Use System DNS ", systemDns) 610 | } 611 | if len(r.Answer) == 0 { 612 | log.Print("Could not found NS records") 613 | return "", 0 614 | } 615 | 616 | ip := "" 617 | var ttl uint32 = 60 618 | var lastPing int64 = 5000 619 | for _, ans := range r.Answer { 620 | if a, ok := ans.(*dns.A); ok { 621 | nowPing := tcping(a.A.String(), wsAddrPort) 622 | log.Print("tcping "+a.A.String()+" ", nowPing, "ms") 623 | if nowPing != -1 && nowPing < lastPing { 624 | ip = a.A.String() 625 | ttl = ans.Header().Ttl 626 | lastPing = nowPing 627 | } 628 | } 629 | } 630 | log.Print("Prefer IP " + ip + " for " + hostname) 631 | return ip, ttl 632 | } 633 | 634 | // 根据dns ttl自动更新ip 635 | func dnsPreferIpWithTtl(hostname string, ttl uint32) { 636 | for { 637 | log.Println("DNS TTL: ", ttl, "s") 638 | time.Sleep(time.Duration(ttl) * time.Second) 639 | log.Println("Update IP for " + hostname) 640 | ip, ttlNow := dnsPreferIp(hostname) 641 | if ip != "" { 642 | wsAddrIp = ip 643 | ttl = ttlNow 644 | } else { 645 | log.Println("DNS Fail, Use Last IP: " + wsAddrIp) 646 | } 647 | } 648 | } 649 | 650 | func main() { 651 | arg_num := len(os.Args) 652 | if arg_num < 3 { 653 | fmt.Println("TCP over WebSocket (tcp2ws) with UDP support 11.1\nhttps://github.com/zanjie1999/tcp-over-websocket") 654 | fmt.Println("Client: ws://tcp2wsUrl localPort\nServer: ip:port tcp2wsPort\nUse wss: ip:port tcp2wsPort server.crt server.key") 655 | fmt.Println("Make ssl cert:\nopenssl genrsa -out server.key 2048\nopenssl ecparam -genkey -name secp384r1 -out server.key\nopenssl req -new -x509 -sha256 -key server.key -out server.crt -days 36500") 656 | os.Exit(0) 657 | } 658 | serverUrl := os.Args[1] 659 | listenPort := os.Args[2] 660 | isSsl := false 661 | if arg_num == 4 { 662 | isSsl = os.Args[3] == "wss" || os.Args[3] == "https" || os.Args[3] == "ssl" 663 | } 664 | sslCrt := "server.crt" 665 | sslKey := "server.key" 666 | if arg_num == 5 { 667 | isSsl = true 668 | sslCrt = os.Args[3] 669 | sslKey = os.Args[4] 670 | } 671 | 672 | // 第一个参数是ws 673 | match, _ := regexp.MatchString(`^(ws|wss|http|https)://.*`, serverUrl) 674 | isServer = !match 675 | if isServer { 676 | // 服务端 677 | match, _ := regexp.MatchString(`^\d+$`, serverUrl) 678 | if match { 679 | // 只有端口号默认127.0.0.1 680 | tcpAddr = "127.0.0.1:" + serverUrl 681 | } else { 682 | tcpAddr = serverUrl 683 | } 684 | // ws server 685 | http.HandleFunc("/", wsHandler) 686 | match, _ = regexp.MatchString(`^\d+$`, listenPort) 687 | listenHostPort := listenPort 688 | if match { 689 | // 如果没指定监听ip那就全部监听 省掉不必要的防火墙 690 | listenHostPort = "0.0.0.0:" + listenPort 691 | } 692 | go startWsServer(listenHostPort, isSsl, sslCrt, sslKey) 693 | if isSsl { 694 | log.Print("Server Started wss://" + listenHostPort + " -> " + tcpAddr) 695 | fmt.Print("Proxy with Nginx:\nlocation /" + uuid.New().String()[24:] + "/ {\nproxy_pass https://") 696 | } else { 697 | log.Print("Server Started ws://" + listenHostPort + " -> " + tcpAddr) 698 | fmt.Print("Proxy with Nginx:\nlocation /" + uuid.New().String()[24:] + "/ {\nproxy_pass http://") 699 | } 700 | if match { 701 | fmt.Print("127.0.0.1:" + listenPort) 702 | } else { 703 | fmt.Print(listenPort) 704 | } 705 | fmt.Println("/;\nproxy_read_timeout 3600;\nproxy_http_version 1.1;\nproxy_set_header Upgrade $http_upgrade;\nproxy_set_header Connection \"Upgrade\";\nproxy_set_header X-Forwarded-For $remote_addr;\naccess_log off;\n}") 706 | } else { 707 | // 客户端 708 | if serverUrl[:5] == "https" { 709 | wsAddr = "wss" + serverUrl[5:] 710 | } else if serverUrl[:4] == "http" { 711 | wsAddr = "ws" + serverUrl[4:] 712 | } else { 713 | wsAddr = serverUrl 714 | } 715 | match, _ = regexp.MatchString(`^\d+$`, listenPort) 716 | listenHostPort := listenPort 717 | if match { 718 | // 如果没指定监听ip那就全部监听 省掉不必要的防火墙 719 | listenHostPort = "0.0.0.0:" + listenPort 720 | } 721 | l, err := net.Listen("tcp", listenHostPort) 722 | if err != nil { 723 | log.Fatal("tcp2ws Client Start Error: ", err) 724 | } 725 | // 将ws服务端域名对应的ip缓存起来,避免多次请求dns或dns爆炸导致无法连接 726 | u, err := url.Parse(wsAddr) 727 | if err != nil { 728 | log.Fatal("tcp2ws Client Start Error: ", err) 729 | } 730 | // 确定端口号,下面域名tcping要用 731 | if u.Port() != "" { 732 | wsAddrPort = ":" + u.Port() 733 | } else if wsAddr[:3] == "wss" { 734 | wsAddrPort = ":443" 735 | } else { 736 | wsAddrPort = ":80" 737 | } 738 | if u.Host[0] == '[' { 739 | // ipv6 740 | wsAddrIp = "[" + u.Hostname() + "]" 741 | log.Print("tcping "+u.Hostname()+" ", tcping(u.Hostname(), wsAddrPort), "ms") 742 | } else if match, _ = regexp.MatchString(`^\d+.\d+.\d+.\d+$`, u.Hostname()); match { 743 | // ipv4 744 | wsAddrIp = u.Hostname() 745 | log.Print("tcping "+wsAddrIp+" ", tcping(wsAddrIp, wsAddrPort), "ms") 746 | } else { 747 | // 域名,需要解析,ip优选 748 | var ttl uint32 749 | wsAddrIp, ttl = dnsPreferIp(u.Hostname()) 750 | if wsAddrIp == "" { 751 | log.Fatal("tcp2ws Client Start Error: dns resolve error") 752 | } else if ttl > 0 { 753 | // 根据dns ttl自动更新ip 754 | go dnsPreferIpWithTtl(u.Hostname(), ttl) 755 | } 756 | } 757 | 758 | go tcpHandler(l) 759 | 760 | // 启动一个udp监听用于udp转发 761 | go runClientUdp(listenHostPort) 762 | 763 | log.Print("Client Started " + listenHostPort + " -> " + wsAddr) 764 | } 765 | for { 766 | if isServer { 767 | // 心跳间隔2分钟 768 | time.Sleep(2 * 60 * time.Second) 769 | nowTimeCut := time.Now().Unix() - 2*60 770 | // check ws 771 | for k, i := range connMap { 772 | // 如果超过2分钟没有收到消息,才发心跳,避免读写冲突 773 | if i.t < nowTimeCut { 774 | if i.isUdp { 775 | // udp不需要心跳 超时就关闭 776 | log.Print(i.uuid, " udp timeout close") 777 | deleteConn(k) 778 | } else if err := i.wsConn.WriteMessage(websocket.TextMessage, []byte("tcp2wsSparkle")); err != nil { 779 | log.Print(i.uuid, " tcp timeout close") 780 | i.wsConn.Close() 781 | deleteConn(k) 782 | } 783 | } 784 | } 785 | } else { 786 | // 按 ctrl + c 退出,会阻塞 787 | c := make(chan os.Signal, 1) 788 | signal.Notify(c, os.Interrupt, os.Kill) 789 | <-c 790 | fmt.Println() 791 | log.Print("quit...") 792 | for k, _ := range connMap { 793 | deleteConn(k) 794 | } 795 | os.Exit(0) 796 | } 797 | } 798 | } 799 | --------------------------------------------------------------------------------