diff --git a/.goreleaser.yaml b/.goreleaser.yaml index d3eaaaa..af4154b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -17,7 +17,6 @@ builds: - amd64 - arm - arm64 - - loong64 goarm: - "6" - "7" diff --git a/Makefile b/Makefile index 37ca645..750aabb 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ ${BIN_DIR}/${TSSHD}: $(wildcard ./cmd/tsshd/*.go ./tsshd/*.go) go.mod go.sum go build -o ${BIN_DIR}/ ./cmd/tsshd clean: - -rm -f ${BIN_DIR}/tsshd{,.exe} + -rm -f ${BIN_DIR}/tsshd ${BIN_DIR}/tsshd.exe test: ${GO_TEST} -v -count=1 ./tsshd diff --git a/README.md b/README.md index 40012d4..0958422 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,68 @@ # tsshd The [`tssh --udp`](https://github.com/trzsz/trzsz-ssh) works like [`mosh`](https://github.com/mobile-shell/mosh), and the `tsshd` works like `mosh-server`. + +## Advanced Features + +- Low latency ( based on kcp ) + +- Port forwarding ( same as ssh ) + +## How to use + +1. Install [tssh](https://github.com/trzsz/trzsz-ssh) on the client ( the user's machine ). + +2. Install [tsshd](https://github.com/trzsz/tsshd) on the server ( the remote host ). + +3. Use `tssh --udp xxx` to login to the server. Configure as follows to omit `--udp`: + + ``` + Host xxx + #!! UdpMode yes + #!! TsshdPath ~/go/bin/tsshd + ``` + +## How it works + +The `tssh` plays the role of `ssh` on the client side, and the `tsshd` plays the role of `sshd` on the server side. + +The `tssh` will first login to the server normally as an ssh client, and then run a new `tsshd` process on the server. + +The `tsshd` process listens on a random udp port between 61000 and 62000, and sends its port number and a secret key back to the `tssh` process over the ssh channel. The ssh connection is then shut down, and the `tssh` process communicates with the `tsshd` process over udp. + +## Installation + +- Install with Go ( Requires go 1.20 or later ) + +
go install github.com/trzsz/tsshd/cmd/tsshd@latest + + ```sh + go install github.com/trzsz/tsshd/cmd/tsshd@latest + ``` + + The binaries are usually located in ~/go/bin/ ( C:\Users\your_name\go\bin\ on Windows ). + +
+ +- Build from source ( Requires go 1.20 or later ) + +
sudo make install + + ```sh + git clone --depth 1 https://github.com/trzsz/tsshd.git + cd tsshd + make + sudo make install + ``` + +
+ +- Download from the [GitHub Releases](https://github.com/trzsz/tsshd/releases), unzip and add to `PATH` environment. + +## Contact + +Feel free to email the author , or create an [issue](https://github.com/trzsz/tsshd/issues). Welcome to join the QQ group: 318578930. + +## Sponsor + +[❤️ Sponsor trzsz ❤️](https://github.com/trzsz), buy the author a drink 🍺 ? Thank you for your support! diff --git a/go.mod b/go.mod index f9f63cc..17d9811 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,23 @@ module github.com/trzsz/tsshd go 1.20 -require github.com/trzsz/go-arg v1.5.3 +require ( + github.com/UserExistsError/conpty v0.1.3 + github.com/creack/pty v1.1.21 + github.com/trzsz/go-arg v1.5.3 + github.com/xtaci/kcp-go/v5 v5.6.1 + golang.org/x/crypto v0.24.0 + golang.org/x/sys v0.21.0 +) require ( github.com/alexflint/go-scalar v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/reedsolomon v1.12.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/stretchr/testify v1.8.4 // indirect + github.com/templexxx/cpu v0.1.0 // indirect + github.com/templexxx/xorsimd v0.4.2 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect + golang.org/x/net v0.26.0 // indirect ) diff --git a/go.sum b/go.sum index fd85adb..84dabbc 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,145 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/UserExistsError/conpty v0.1.3 h1:YzGQkHAiBBkAihOCO5J2cAnahzb8ePvje2YxG7et1E0= +github.com/UserExistsError/conpty v0.1.3/go.mod h1:PDglKIkX3O/2xVk0MV9a6bCWxRmPVfxqZoTG/5sSd9I= github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/klauspost/cpuid v1.2.4/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/reedsolomon v1.9.9/go.mod h1:O7yFFHiQwDR6b2t63KPUpccPtNdp5ADgh1gg4fd12wo= +github.com/klauspost/reedsolomon v1.12.1 h1:NhWgum1efX1x58daOBGCFWcxtEhOhXKKl1HAPQUp03Q= +github.com/klauspost/reedsolomon v1.12.1/go.mod h1:nEi5Kjb6QqtbofI6s+cbG/j1da11c96IBYBSnVGtuBs= +github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104/go.mod h1:wqKykBG2QzQDJEzvRkcS8x6MiSJkF52hXZsXcjaB3ls= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/cpu v0.0.7/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/cpu v0.1.0 h1:wVM+WIJP2nYaxVxqgHPD4wGA2aJ9rvrQRV8CvFzNb40= +github.com/templexxx/cpu v0.1.0/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= +github.com/templexxx/xorsimd v0.4.1/go.mod h1:W+ffZz8jJMH2SXwuKu9WhygqBMbFnp14G2fqEr8qaNo= +github.com/templexxx/xorsimd v0.4.2 h1:ocZZ+Nvu65LGHmCLZ7OoCtg8Fx8jnHKK37SjvngUoVI= +github.com/templexxx/xorsimd v0.4.2/go.mod h1:HgwaPoDREdi6OnULpSfxhzaiiSUY4Fi3JPn1wpt28NI= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/trzsz/go-arg v1.5.3 h1:eIDwDEmvSahtr5HpQOLrSa+YMqWQQ0H20xx60XgXQJw= github.com/trzsz/go-arg v1.5.3/go.mod h1:IC6Z/FiVH7uYvcbp1/gJhDYCFPS/GkL0APYakVvgY4I= +github.com/xtaci/kcp-go/v5 v5.6.1 h1:Pwn0aoeNSPF9dTS7IgiPXn0HEtaIlVb6y5UKWPsx8bI= +github.com/xtaci/kcp-go/v5 v5.6.1/go.mod h1:W3kVPyNYwZ06p79dNwFWQOVFrdcBpDBsdyvK8moQrYo= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/arch v0.0.0-20190909030613-46d78d1859ac/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200808120158-1030fc2bf1d9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200425043458-8463f397d07c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200808161706-5bf02b21f123/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/tsshd/bus.go b/tsshd/bus.go new file mode 100644 index 0000000..dc3e2af --- /dev/null +++ b/tsshd/bus.go @@ -0,0 +1,144 @@ +/* +MIT License + +Copyright (c) 2024 The Trzsz SSH Authors. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. +*/ + +package tsshd + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/xtaci/kcp-go/v5" +) + +var busMutex sync.Mutex + +var busSession atomic.Pointer[kcp.UDPSession] + +var lastAliveTime atomic.Pointer[time.Time] + +func sendBusCommand(command string) error { + busMutex.Lock() + defer busMutex.Unlock() + session := busSession.Load() + if session == nil { + return fmt.Errorf("bus session is nil") + } + return SendCommand(session, command) +} + +func sendBusMessage(command string, msg any) error { + busMutex.Lock() + defer busMutex.Unlock() + session := busSession.Load() + if session == nil { + return fmt.Errorf("bus session is nil") + } + if err := SendCommand(session, command); err != nil { + return err + } + return SendMessage(session, msg) +} + +func trySendErrorMessage(format string, a ...any) { + _ = sendBusMessage("error", ErrorMessage{fmt.Sprintf(format, a...)}) +} + +func handleBusEvent(session *kcp.UDPSession) { + var msg BusMessage + if err := RecvMessage(session, &msg); err != nil { + SendError(session, fmt.Errorf("recv bus message failed: %v", err)) + return + } + + busMutex.Lock() + + // only one bus + if !busSession.CompareAndSwap(nil, session) { + busMutex.Unlock() + SendError(session, fmt.Errorf("bus has been initialized")) + return + } + + if err := SendSuccess(session); err != nil { // ack ok + busMutex.Unlock() + trySendErrorMessage("bus ack ok failed: %v", err) + return + } + + busMutex.Unlock() + + serving.Store(true) + + if msg.Timeout > 0 { + now := time.Now() + lastAliveTime.Store(&now) + go keepAlive(msg.Timeout) + } + + for { + command, err := RecvCommand(session) + if err != nil { + trySendErrorMessage("recv bus command failed: %v", err) + return + } + + switch command { + case "resize": + err = handleResizeEvent(session) + case "close": + exitChan <- true + return + case "alive": + now := time.Now() + lastAliveTime.Store(&now) + default: + err = handleUnknownEvent(session) + } + if err != nil { + trySendErrorMessage("handle bus command [%s] failed: %v", command, err) + } + } +} + +func handleUnknownEvent(session *kcp.UDPSession) error { + var msg struct{} + if err := RecvMessage(session, &msg); err != nil { + return fmt.Errorf("recv unknown message failed: %v", err) + } + return fmt.Errorf("unknown command") +} + +func keepAlive(timeout time.Duration) { + for { + _ = sendBusCommand("alive") + if t := lastAliveTime.Load(); t != nil && time.Since(*t) > timeout { + trySendErrorMessage("tsshd keep alive timeout") + exitChan <- true + return + } + time.Sleep(timeout / 10) + } +} diff --git a/tsshd/forward.go b/tsshd/forward.go new file mode 100644 index 0000000..055c80c --- /dev/null +++ b/tsshd/forward.go @@ -0,0 +1,150 @@ +/* +MIT License + +Copyright (c) 2024 The Trzsz SSH Authors. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. +*/ + +package tsshd + +import ( + "fmt" + "io" + "net" + "sync" + "sync/atomic" + + "github.com/xtaci/kcp-go/v5" +) + +var acceptMutex sync.Mutex +var acceptID atomic.Uint64 +var acceptMap = make(map[uint64]net.Conn) + +func handleDialEvent(session *kcp.UDPSession) { + var msg DialMessage + if err := RecvMessage(session, &msg); err != nil { + SendError(session, fmt.Errorf("recv dial message failed: %v", err)) + return + } + + var err error + var conn net.Conn + if msg.Timeout > 0 { + conn, err = net.DialTimeout(msg.Network, msg.Addr, msg.Timeout) + } else { + conn, err = net.Dial(msg.Network, msg.Addr) + } + if err != nil { + SendError(session, fmt.Errorf("dial %s [%s] failed: %v", msg.Network, msg.Addr, err)) + return + } + + defer conn.Close() + + if err := SendSuccess(session); err != nil { // ack ok + trySendErrorMessage("dial ack ok failed: %v", err) + return + } + + forwardConnection(session, conn) +} + +func handleListenEvent(session *kcp.UDPSession) { + var msg ListenMessage + if err := RecvMessage(session, &msg); err != nil { + SendError(session, fmt.Errorf("recv listen message failed: %v", err)) + return + } + + listener, err := net.Listen(msg.Network, msg.Addr) + if err != nil { + SendError(session, fmt.Errorf("listen on %s [%s] failed: %v", msg.Network, msg.Addr, err)) + return + } + + defer listener.Close() + + if err := SendSuccess(session); err != nil { // ack ok + trySendErrorMessage("listen ack ok failed: %v", err) + return + } + + for { + conn, err := listener.Accept() + if err == io.EOF { + break + } + if err != nil { + trySendErrorMessage("listener %s [%s] accept failed: %v", msg.Network, msg.Addr, err) + continue + } + acceptMutex.Lock() + id := acceptID.Add(1) - 1 + acceptMap[id] = conn + if err := SendMessage(session, AcceptMessage{id}); err != nil { + acceptMutex.Unlock() + trySendErrorMessage("send accept message failed: %v", err) + return + } + acceptMutex.Unlock() + } +} + +func handleAcceptEvent(session *kcp.UDPSession) { + var msg AcceptMessage + if err := RecvMessage(session, &msg); err != nil { + SendError(session, fmt.Errorf("recv accept message failed: %v", err)) + return + } + + acceptMutex.Lock() + defer acceptMutex.Unlock() + + conn, ok := acceptMap[msg.ID] + if !ok { + SendError(session, fmt.Errorf("invalid accept id: %d", msg.ID)) + return + } + + delete(acceptMap, msg.ID) + defer conn.Close() + + if err := SendSuccess(session); err != nil { // ack ok + trySendErrorMessage("accept ack ok failed: %v", err) + return + } + + forwardConnection(session, conn) +} + +func forwardConnection(session *kcp.UDPSession, conn net.Conn) { + var wg sync.WaitGroup + wg.Add(2) + go func() { + _, _ = io.Copy(conn, session) + wg.Done() + }() + go func() { + _, _ = io.Copy(session, conn) + wg.Done() + }() + wg.Wait() +} diff --git a/tsshd/main.go b/tsshd/main.go index 38761e9..f56555d 100644 --- a/tsshd/main.go +++ b/tsshd/main.go @@ -26,6 +26,9 @@ package tsshd import ( "fmt" + "io" + "os" + "os/exec" "github.com/trzsz/go-arg" ) @@ -43,9 +46,54 @@ func (tsshdArgs) Version() string { return fmt.Sprintf("trzsz sshd %s", kTsshdVersion) } +func background() (bool, io.ReadCloser, error) { + if v := os.Getenv("TRZSZ-SSHD-BACKGROUND"); v == "TRUE" { + return false, nil, nil + } + cmd := exec.Command(os.Args[0], os.Args[1:]...) + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), "TRZSZ-SSHD-BACKGROUND=TRUE") + cmd.SysProcAttr = getSysProcAttr() + stdout, err := cmd.StdoutPipe() + if err != nil { + return true, nil, err + } + if err := cmd.Start(); err != nil { + return true, nil, err + } + return true, stdout, nil +} + // TsshdMain is the main function of `tsshd` binary. func TsshdMain() int { var args tsshdArgs arg.MustParse(&args) + + parent, stdout, err := background() + if err != nil { + fmt.Fprintf(os.Stderr, "run in background failed: %v\n", err) + return 1 + } + + if parent { + defer stdout.Close() + if _, err := io.Copy(os.Stdout, stdout); err != nil { + fmt.Fprintf(os.Stderr, "copy stdout failed: %v\n", err) + return 2 + } + return 0 + } + + listener, err := initServer(&args) + if err != nil { + fmt.Println(err) + os.Stdout.Close() + return 3 + } + + os.Stdout.Close() + + serve(listener) + return 0 } diff --git a/tsshd/proto.go b/tsshd/proto.go new file mode 100644 index 0000000..11a75b1 --- /dev/null +++ b/tsshd/proto.go @@ -0,0 +1,184 @@ +/* +MIT License + +Copyright (c) 2024 The Trzsz SSH Authors. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. +*/ + +package tsshd + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "io" + "time" + + "github.com/xtaci/kcp-go/v5" +) + +const kNoErrorMsg = "_TSSHD_NO_ERROR_" + +type ServerInfo struct { + Ver string + Pass string + Salt string + Port int +} + +type ErrorMessage struct { + Msg string +} + +type BusMessage struct { + Timeout time.Duration +} + +type StartMessage struct { + ID uint64 + Pty bool + Shell bool + Name string + Args []string + Cols int + Rows int + Envs map[string]string +} + +type ExitMessage struct { + ID uint64 + ExitCode int +} + +type ResizeMessage struct { + ID uint64 + Cols int + Rows int +} + +type StderrMessage struct { + ID uint64 +} + +type DialMessage struct { + Network string + Addr string + Timeout time.Duration +} + +type ListenMessage struct { + Network string + Addr string +} + +type AcceptMessage struct { + ID uint64 +} + +func writeAll(dst io.Writer, data []byte) error { + m := 0 + l := len(data) + for m < l { + n, err := dst.Write(data[m:]) + if err != nil { + return err + } + m += n + } + return nil +} + +func SendCommand(session *kcp.UDPSession, command string) error { + if len(command) == 0 { + return fmt.Errorf("send command is empty") + } + if len(command) > 255 { + return fmt.Errorf("send command too long: %s", command) + } + buffer := make([]byte, len(command)+1) + buffer[0] = uint8(len(command)) + copy(buffer[1:], []byte(command)) + if err := writeAll(session, buffer); err != nil { + return fmt.Errorf("send command write buffer failed: %v", err) + } + return nil +} + +func RecvCommand(session *kcp.UDPSession) (string, error) { + length := make([]byte, 1) + if _, err := session.Read(length); err != nil { + return "", fmt.Errorf("recv command read length failed: %v", err) + } + command := make([]byte, length[0]) + if _, err := io.ReadFull(session, command); err != nil { + return "", fmt.Errorf("recv command read buffer failed: %v", err) + } + return string(command), nil +} + +func SendMessage(session *kcp.UDPSession, msg any) error { + msgBuf, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("send message marshal failed: %v", err) + } + buffer := make([]byte, len(msgBuf)+4) + binary.BigEndian.PutUint32(buffer, uint32(len(msgBuf))) + copy(buffer[4:], msgBuf) + if err := writeAll(session, buffer); err != nil { + return fmt.Errorf("send message write buffer failed: %v", err) + } + return nil +} + +func RecvMessage(session *kcp.UDPSession, msg any) error { + lenBuf := make([]byte, 4) + if _, err := io.ReadFull(session, lenBuf); err != nil { + return fmt.Errorf("recv message read length failed: %v", err) + } + msgBuf := make([]byte, binary.BigEndian.Uint32(lenBuf)) + if _, err := io.ReadFull(session, msgBuf); err != nil { + return fmt.Errorf("recv message read buffer failed: %v", err) + } + if err := json.Unmarshal(msgBuf, msg); err != nil { + return fmt.Errorf("recv message unmarshal failed: %v", err) + } + return nil +} + +func SendError(session *kcp.UDPSession, err error) { + if e := SendMessage(session, ErrorMessage{err.Error()}); e != nil { + trySendErrorMessage("send error [%v] failed: %v", err, e) + } +} + +func SendSuccess(session *kcp.UDPSession) error { + return SendMessage(session, ErrorMessage{kNoErrorMsg}) +} + +func RecvError(session *kcp.UDPSession) error { + var errMsg ErrorMessage + if err := RecvMessage(session, &errMsg); err != nil { + return fmt.Errorf("recv error failed: %v", err) + } + if errMsg.Msg != kNoErrorMsg { + return fmt.Errorf(errMsg.Msg) + } + return nil +} diff --git a/tsshd/server.go b/tsshd/server.go new file mode 100644 index 0000000..1c6a7f8 --- /dev/null +++ b/tsshd/server.go @@ -0,0 +1,115 @@ +/* +MIT License + +Copyright (c) 2024 The Trzsz SSH Authors. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. +*/ + +package tsshd + +import ( + crypto_rand "crypto/rand" + "crypto/sha1" + "encoding/json" + "fmt" + math_rand "math/rand" + "net" + + "github.com/xtaci/kcp-go/v5" + "golang.org/x/crypto/pbkdf2" +) + +const kDefaultPortRangeLow = 61001 + +const kDefaultPortRangeHigh = 61999 + +func initServer(args *tsshdArgs) (*kcp.Listener, error) { + portRangeLow := kDefaultPortRangeLow + portRangeHigh := kDefaultPortRangeHigh + conn, port := listenOnFreePort(portRangeLow, portRangeHigh) + if conn == nil { + return nil, fmt.Errorf("no free udp port in [%d, %d]", portRangeLow, portRangeHigh) + } + + pass := make([]byte, 32) + if _, err := crypto_rand.Read(pass); err != nil { + return nil, fmt.Errorf("rand pass failed: %v", err) + } + salt := make([]byte, 32) + if _, err := crypto_rand.Read(salt); err != nil { + return nil, fmt.Errorf("rand salt failed: %v", err) + } + key := pbkdf2.Key(pass, salt, 4096, 32, sha1.New) + + block, err := kcp.NewAESBlockCrypt(key) + if err != nil { + return nil, fmt.Errorf("new aes block crypt failed: %v", err) + } + + listener, err := kcp.ServeConn(block, 10, 3, conn) + if err != nil { + return nil, fmt.Errorf("kcp serve conn failed: %v", err) + } + + svrInfo := ServerInfo{ + Ver: kTsshdVersion, + Pass: fmt.Sprintf("%x", pass), + Salt: fmt.Sprintf("%x", salt), + Port: port, + } + info, err := json.Marshal(svrInfo) + if err != nil { + listener.Close() + return nil, fmt.Errorf("json marshal failed: %v\n", err) + } + fmt.Printf("\a%s\r\n", string(info)) + + return listener, nil +} + +func listenOnFreePort(low, high int) (*net.UDPConn, int) { + if high < low { + return nil, -1 + } + size := high - low + 1 + port := low + math_rand.Intn(size) + for i := 0; i < size; i++ { + if conn := listenOnPort(port); conn != nil { + return conn, port + } + port++ + if port > high { + port = low + } + } + return nil, -1 +} + +func listenOnPort(port int) *net.UDPConn { + addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", port)) + if err != nil { + return nil + } + conn, err := net.ListenUDP("udp", addr) + if err != nil { + return nil + } + return conn +} diff --git a/tsshd/service.go b/tsshd/service.go new file mode 100644 index 0000000..805c7dc --- /dev/null +++ b/tsshd/service.go @@ -0,0 +1,99 @@ +/* +MIT License + +Copyright (c) 2024 The Trzsz SSH Authors. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. +*/ + +package tsshd + +import ( + "fmt" + "sync/atomic" + "time" + + "github.com/xtaci/kcp-go/v5" +) + +var serving atomic.Bool + +var exitChan = make(chan bool, 1) + +func serve(listener *kcp.Listener) { + defer listener.Close() + + go func() { + // should be connected within 10 seconds + time.Sleep(10 * time.Second) + if !serving.Load() { + exitChan <- true + } + }() + + go func() { + for { + session, err := listener.AcceptKCP() + if err != nil { + trySendErrorMessage("kcp accept failed: %v", err) + return + } + go handleSession(session) + } + }() + + <-exitChan +} + +func handleSession(session *kcp.UDPSession) { + defer session.Close() + + command, err := RecvCommand(session) + if err != nil { + SendError(session, fmt.Errorf("recv session command failed: %v", err)) + return + } + + var handler func(*kcp.UDPSession) + + switch command { + case "bus": + handler = handleBusEvent + case "session": + handler = handleSessionEvent + case "stderr": + handler = handleStderrEvent + case "dial": + handler = handleDialEvent + case "listen": + handler = handleListenEvent + case "accept": + handler = handleAcceptEvent + default: + SendError(session, fmt.Errorf("unknown session command: %s", command)) + return + } + + if err := SendSuccess(session); err != nil { // say hello + trySendErrorMessage("tsshd say hello failed: %v", err) + return + } + + handler(session) +} diff --git a/tsshd/session.go b/tsshd/session.go new file mode 100644 index 0000000..ed3bf05 --- /dev/null +++ b/tsshd/session.go @@ -0,0 +1,332 @@ +/* +MIT License + +Copyright (c) 2024 The Trzsz SSH Authors. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. +*/ + +package tsshd + +import ( + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + + "github.com/xtaci/kcp-go/v5" +) + +type sessionContext struct { + id uint64 + cols int + rows int + cmd *exec.Cmd + pty *tsshdPty + wg sync.WaitGroup + stdin io.WriteCloser + stdout io.ReadCloser + stderr io.ReadCloser + started bool +} + +type stderrContext struct { + id uint64 + wg sync.WaitGroup + session *kcp.UDPSession +} + +var sessionMutex sync.Mutex +var sessionMap = make(map[uint64]*sessionContext) + +var stderrMutex sync.Mutex +var stderrMap = make(map[uint64]*stderrContext) + +func (c *sessionContext) StartPty() error { + var err error + c.pty, err = newTsshdPty(c.cmd, c.cols, c.rows) + if err != nil { + return fmt.Errorf("shell pty start failed: %v", err) + } + c.stdin = c.pty.stdin + c.stdout = c.pty.stdout + c.started = true + return nil +} + +func (c *sessionContext) StartCmd() error { + var err error + if c.stdin, err = c.cmd.StdinPipe(); err != nil { + return fmt.Errorf("cmd stdin pipe failed: %v", err) + } + if c.stdout, err = c.cmd.StdoutPipe(); err != nil { + return fmt.Errorf("cmd stdout pipe failed: %v", err) + } + if c.stderr, err = c.cmd.StderrPipe(); err != nil { + return fmt.Errorf("cmd stderr pipe failed: %v", err) + } + if err := c.cmd.Start(); err != nil { + return fmt.Errorf("start cmd %v failed: %v", c.cmd.Args, err) + } + c.started = true + return nil +} + +func (c *sessionContext) forwardIO(session *kcp.UDPSession) { + if c.stdin != nil { + go func() { + _, _ = io.Copy(c.stdin, session) + }() + } + + if c.stdout != nil { + c.wg.Add(1) + go func() { + _, _ = io.Copy(session, c.stdout) + c.wg.Done() + }() + } + + if c.stderr != nil { + c.wg.Add(1) + go func() { + if stderr, ok := stderrMap[c.id]; ok { + _, _ = io.Copy(stderr.session, c.stderr) + } else { + _, _ = io.Copy(session, c.stderr) + } + c.wg.Done() + }() + } +} + +func (c *sessionContext) Wait() { + if c.pty != nil { + _ = c.pty.Wait() + } else { + _ = c.cmd.Wait() + } + c.wg.Wait() +} + +func (c *sessionContext) Close() { + if err := sendBusMessage("exit", ExitMessage{ + ID: c.id, + ExitCode: c.cmd.ProcessState.ExitCode(), + }); err != nil { + trySendErrorMessage("send exit message failed: %v", err) + } + if c.stdin != nil { + c.stdin.Close() + } + if c.stdout != nil { + c.stdout.Close() + } + if c.stderr != nil { + c.stderr.Close() + } + if c.started { + if c.pty != nil { + _ = c.pty.Close() + } else { + _ = c.cmd.Process.Kill() + } + } + sessionMutex.Lock() + defer sessionMutex.Unlock() + delete(sessionMap, c.id) +} + +func (c *sessionContext) SetSize(cols, rows int) error { + if c.pty == nil { + return fmt.Errorf("session %d %v is not pty", c.id, c.cmd.Args) + } + if err := c.pty.Resize(cols, rows); err != nil { + return fmt.Errorf("pty set size failed: %v", err) + } + return nil +} + +func handleSessionEvent(session *kcp.UDPSession) { + var msg StartMessage + if err := RecvMessage(session, &msg); err != nil { + SendError(session, fmt.Errorf("recv start message failed: %v", err)) + return + } + + if errCtx := getStderrSession(msg.ID); errCtx != nil { + defer errCtx.Close() + } + + ctx, err := newSession(&msg) + if err != nil { + SendError(session, err) + return + } + defer ctx.Close() + + if msg.Pty { + err = ctx.StartPty() + } else { + err = ctx.StartCmd() + } + if err != nil { + SendError(session, err) + return + } + + if err := SendSuccess(session); err != nil { // ack ok + trySendErrorMessage("session ack ok failed: %v", err) + return + } + + ctx.forwardIO(session) + + ctx.Wait() +} + +func newSession(msg *StartMessage) (*sessionContext, error) { + cmd, err := getSessionStartCmd(msg) + if err != nil { + return nil, fmt.Errorf("build start command failed: %v", err) + } + + sessionMutex.Lock() + defer sessionMutex.Unlock() + + if ctx, ok := sessionMap[msg.ID]; ok { + return nil, fmt.Errorf("session id %d %v existed", msg.ID, ctx.cmd.Args) + } + + ctx := &sessionContext{ + id: msg.ID, + cmd: cmd, + cols: msg.Cols, + rows: msg.Rows, + } + sessionMap[ctx.id] = ctx + return ctx, nil +} + +func (c *stderrContext) Wait() { + c.wg.Wait() +} + +func (c *stderrContext) Close() { + c.wg.Done() + stderrMutex.Lock() + defer stderrMutex.Unlock() + delete(stderrMap, c.id) +} + +func newStderrSession(id uint64, session *kcp.UDPSession) (*stderrContext, error) { + stderrMutex.Lock() + defer stderrMutex.Unlock() + if _, ok := stderrMap[id]; ok { + return nil, fmt.Errorf("session %d stderr already set", id) + } + ctx := &stderrContext{id: id, session: session} + ctx.wg.Add(1) + stderrMap[id] = ctx + return ctx, nil +} + +func getStderrSession(id uint64) *stderrContext { + stderrMutex.Lock() + defer stderrMutex.Unlock() + if ctx, ok := stderrMap[id]; ok { + return ctx + } + return nil +} + +func getSessionStartCmd(msg *StartMessage) (*exec.Cmd, error) { + var envs []string + for _, env := range os.Environ() { + pos := strings.IndexRune(env, '=') + if pos <= 0 { + continue + } + name := strings.TrimSpace(env[:pos]) + if _, ok := msg.Envs[name]; !ok { + envs = append(envs, env) + } + } + for key, value := range msg.Envs { + envs = append(envs, fmt.Sprintf("%s=%s", key, value)) + } + + if !msg.Shell { + cmd := exec.Command(msg.Name, msg.Args...) + cmd.Env = envs + return cmd, nil + } + + shell, err := getUserShell() + if err != nil { + return nil, fmt.Errorf("get user shell failed: %v", err) + } + cmd := exec.Command(shell) + if runtime.GOOS != "windows" { + cmd.Args = []string{"-" + filepath.Base(shell)} + } + cmd.Env = envs + return cmd, nil +} + +func handleStderrEvent(session *kcp.UDPSession) { + var msg StderrMessage + if err := RecvMessage(session, &msg); err != nil { + SendError(session, fmt.Errorf("recv stderr message failed: %v", err)) + return + } + + ctx, err := newStderrSession(msg.ID, session) + if err != nil { + SendError(session, err) + return + } + + if err := SendSuccess(session); err != nil { // ack ok + trySendErrorMessage("stderr ack ok failed: %v", err) + return + } + + ctx.Wait() +} + +func handleResizeEvent(session *kcp.UDPSession) error { + var msg ResizeMessage + if err := RecvMessage(session, &msg); err != nil { + return fmt.Errorf("recv resize message failed: %v", err) + } + if msg.Cols <= 0 || msg.Rows <= 0 { + return fmt.Errorf("resize message invalid: %#v", msg) + } + sessionMutex.Lock() + defer sessionMutex.Unlock() + if ctx, ok := sessionMap[msg.ID]; ok { + return ctx.SetSize(msg.Cols, msg.Rows) + } + return fmt.Errorf("invalid session id: %d", msg.ID) +} diff --git a/tsshd/utils_darwin.go b/tsshd/utils_darwin.go new file mode 100644 index 0000000..8e202f2 --- /dev/null +++ b/tsshd/utils_darwin.go @@ -0,0 +1,36 @@ +/* +MIT License + +Copyright (c) 2024 The Trzsz SSH Authors. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. +*/ + +package tsshd + +import ( + "os" +) + +func getUserShell() (string, error) { + if shell := os.Getenv("SHELL"); shell != "" { + return shell, nil + } + return "/bin/sh", nil +} diff --git a/tsshd/utils_linux.go b/tsshd/utils_linux.go new file mode 100644 index 0000000..2493389 --- /dev/null +++ b/tsshd/utils_linux.go @@ -0,0 +1,39 @@ +//go:build !windows && !darwin + +/* +MIT License + +Copyright (c) 2024 The Trzsz SSH Authors. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. +*/ + +package tsshd + +import ( + "os" +) + +func getUserShell() (string, error) { + if shell := os.Getenv("SHELL"); shell != "" { + return shell, nil + } + // TODO getpwuid(getuid())->pw_shell + return "/bin/sh", nil +} diff --git a/tsshd/utils_unix.go b/tsshd/utils_unix.go new file mode 100644 index 0000000..a80ca88 --- /dev/null +++ b/tsshd/utils_unix.go @@ -0,0 +1,69 @@ +//go:build !windows + +/* +MIT License + +Copyright (c) 2024 The Trzsz SSH Authors. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. +*/ + +package tsshd + +import ( + "io" + "os" + "os/exec" + "syscall" + + "github.com/creack/pty" +) + +type tsshdPty struct { + cmd *exec.Cmd + ptmx *os.File + stdin io.WriteCloser + stdout io.ReadCloser +} + +func (p *tsshdPty) Wait() error { + return p.cmd.Wait() +} + +func (p *tsshdPty) Close() error { + return p.ptmx.Close() +} + +func (p *tsshdPty) Resize(cols, rows int) error { + return pty.Setsize(p.ptmx, &pty.Winsize{Cols: uint16(cols), Rows: uint16(rows)}) +} + +func newTsshdPty(cmd *exec.Cmd, cols, rows int) (*tsshdPty, error) { + ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Cols: uint16(cols), Rows: uint16(rows)}) + if err != nil { + return nil, err + } + return &tsshdPty{cmd, ptmx, ptmx, ptmx}, nil +} + +func getSysProcAttr() *syscall.SysProcAttr { + return &syscall.SysProcAttr{ + Setsid: true, + } +} diff --git a/tsshd/utils_windows.go b/tsshd/utils_windows.go new file mode 100644 index 0000000..af84057 --- /dev/null +++ b/tsshd/utils_windows.go @@ -0,0 +1,81 @@ +/* +MIT License + +Copyright (c) 2024 The Trzsz SSH Authors. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. +*/ + +package tsshd + +import ( + "context" + "io" + "os/exec" + "strings" + "syscall" + + "github.com/UserExistsError/conpty" + "golang.org/x/sys/windows" +) + +type tsshdPty struct { + cpty *conpty.ConPty + stdin io.WriteCloser + stdout io.ReadCloser +} + +func (p *tsshdPty) Wait() error { + _, err := p.cpty.Wait(context.Background()) + _ = p.stdout.Close() + return err +} + +func (p *tsshdPty) Close() error { + return p.cpty.Close() +} + +func (p *tsshdPty) Resize(cols, rows int) error { + return p.cpty.Resize(cols, rows) +} + +func newTsshdPty(cmd *exec.Cmd, cols, rows int) (*tsshdPty, error) { + var cmdLine strings.Builder + for _, arg := range cmd.Args { + if cmdLine.Len() > 0 { + cmdLine.WriteString(" ") + } + cmdLine.WriteString(windows.EscapeArg(arg)) + } + cpty, err := conpty.Start(cmdLine.String(), conpty.ConPtyDimensions(cols, rows)) + if err != nil { + return nil, err + } + return &tsshdPty{cpty, cpty, cpty}, nil +} + +func getUserShell() (string, error) { + return "PowerShell", nil +} + +func getSysProcAttr() *syscall.SysProcAttr { + return &syscall.SysProcAttr{ + CreationFlags: windows.CREATE_BREAKAWAY_FROM_JOB | windows.DETACHED_PROCESS, + } +}