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,
+ }
+}