diff --git a/Makefile b/Makefile index fb7674a..6f0f434 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ ${BIN_DIR}/${TSSH}: $(wildcard ./cmd/tssh/*.go ./tssh/*.go) go.mod go.sum go build -o ${BIN_DIR}/ ./cmd/tssh clean: - -rm -f ${BIN_DIR}/tssh{,.exe} + -rm -f ${BIN_DIR}/tssh ${BIN_DIR}/tssh.exe test: ${GO_TEST} -v -count=1 ./tssh diff --git a/README.cn.md b/README.cn.md index c4fe59d..d302dc3 100644 --- a/README.cn.md +++ b/README.cn.md @@ -663,6 +663,22 @@ trzsz-ssh ( tssh ) 设计为 ssh 客户端的直接替代品,提供与 openssh - 如果在 `~/.tssh.conf` 中设置了 `SetTerminalTitle = Yes`,则会在登录后自动设置终端标题,但是服务器上的 `PROMPT_COMMAND` 会覆盖 `tssh` 设置的标题。 - 在 `tssh` 退出后不会重置为原来的标题,你需要在本地 shell 中设置 `PROMPT_COMMAND`,让它覆盖 `tssh` 设置的标题。 +## UDP 模式 + +- 在服务器上安装 [tsshd](https://github.com/trzsz/tsshd),使用 `tssh --udp xxx` 登录服务器,或者如下配置以省略 `--udp` 参数: + + ``` + Host xxx + #!! UdpMode yes + #!! TsshdPath ~/go/bin/tsshd + ``` + +- `tssh` 在客户端扮演 `ssh` 的角色,`tsshd` 在服务端扮演 `sshd` 的角色。 + +- `tssh` 会先作为一个 ssh 客户端正常登录到服务器上,然后在服务器上启动一个新的 `tsshd` 进程。 + +- `tsshd` 进程会随机侦听一个 61000 到 62000 之间的 UDP 端口,并将其端口和密钥通过 ssh 通道发回给 `tssh` 进程。登录的 ssh 连接会被关闭,然后 `tssh` 进程通过 UDP 与 `tsshd` 进程通讯。 + ## 故障排除 - 在 Warp 终端,分块 Blocks 的功能需要将 `tssh` 重命名为 `ssh`,推荐建个软链接( 对更新友好 ): diff --git a/README.en.md b/README.en.md index 208316a..c7b1564 100644 --- a/README.en.md +++ b/README.en.md @@ -665,6 +665,22 @@ trzsz-ssh ( tssh ) is an ssh client designed as a drop-in replacement for the op - If `SetTerminalTitle = Yes` is set in `~/.tssh.conf`, the terminal title is automatically set after login, but `PROMPT_COMMAND` on the server overrides the title set by `tssh`. - `tssh` does not reset to the original title after exiting, you need to set `PROMPT_COMMAND` in the local shell so that it overrides the title set by `tssh`. +## UDP Mode + +- Install [tsshd](https://github.com/trzsz/tsshd) on the server, use `tssh --udp xxx` to login to the server, or configure as follows to omit `--udp`: + + ``` + Host xxx + #!! UdpMode yes + #!! TsshdPath ~/go/bin/tsshd + ``` + +- 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. + ## Trouble shooting - In the Warp terminal, the features like Blocks requires renaming `tssh` to `ssh`. It is recommended to create a soft link (friendly for updates): diff --git a/README.md b/README.md index 5e932d7..6880879 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ trzsz-ssh ( tssh ) offers additional useful features: | [Custom Configuration](README.en.md#custom-configuration) | [个性配置](README.cn.md#%E4%B8%AA%E6%80%A7%E9%85%8D%E7%BD%AE) | | [Comments of Config](README.en.md#comments-of-config) | [配置注释](README.cn.md#%E9%85%8D%E7%BD%AE%E6%B3%A8%E9%87%8A) | | [Other Features](README.en.md#other-features) | [其他功能](README.cn.md#%E5%85%B6%E4%BB%96%E5%8A%9F%E8%83%BD) | +| [UDP Mode](README.en.md#udp-mode) | [UDP 模式](README.cn.md#udp-%E6%A8%A1%E5%BC%8F) | ## Installation diff --git a/go.mod b/go.mod index b3fe692..d4e9a20 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,8 @@ go 1.20 require ( github.com/Microsoft/go-winio v0.6.2 github.com/alessio/shellescape v1.4.2 - github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.26.3 + github.com/charmbracelet/bubbletea v0.26.5 github.com/charmbracelet/lipgloss v0.11.0 github.com/chzyer/readline v1.5.1 github.com/creack/pty v1.1.21 @@ -19,14 +18,17 @@ require ( github.com/skeema/knownhosts v1.2.2 github.com/stretchr/testify v1.8.4 github.com/trzsz/go-arg v1.5.3 + github.com/trzsz/go-socks5 v0.1.0 github.com/trzsz/iterm2 v0.1.2 github.com/trzsz/pageant v0.1.0 github.com/trzsz/promptui v0.10.7 github.com/trzsz/ssh_config v1.3.6 github.com/trzsz/trzsz-go v1.1.8-0.20240525015006-6424386a6738 - golang.org/x/crypto v0.23.0 - golang.org/x/sys v0.20.0 - golang.org/x/term v0.20.0 + github.com/trzsz/tsshd v0.1.0 + github.com/xtaci/kcp-go/v5 v5.6.1 + golang.org/x/crypto v0.24.0 + golang.org/x/sys v0.21.0 + golang.org/x/term v0.21.0 ) require ( @@ -37,30 +39,36 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect - github.com/charmbracelet/x/ansi v0.1.1 // indirect - github.com/charmbracelet/x/input v0.1.1 // indirect + github.com/charmbracelet/x/ansi v0.1.2 // indirect + github.com/charmbracelet/x/input v0.1.2 // indirect github.com/charmbracelet/x/term v0.1.1 // indirect github.com/charmbracelet/x/windows v0.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/gorilla/websocket v1.5.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/josephspurrier/goversioninfo v1.4.0 // indirect - github.com/klauspost/compress v1.17.8 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/klauspost/reedsolomon v1.12.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/ncruces/zenity v0.10.12 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect github.com/rivo/uniseg v0.4.7 // 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 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/image v0.16.0 // indirect - golang.org/x/net v0.25.0 // indirect + golang.org/x/image v0.17.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/text v0.15.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 65760a2..fda8328 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/UserExistsError/conpty v0.1.3 h1:YzGQkHAiBBkAihOCO5J2cAnahzb8ePvje2YxG7et1E0= @@ -10,8 +12,6 @@ github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+W github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= github.com/andybrewer/mack v0.0.0-20220307193339-22e922cc18af h1:PNE0xdyuLeOTujftqZs8DlhDoi+T54ONZhiGOxH5t2A= github.com/andybrewer/mack v0.0.0-20220307193339-22e922cc18af/go.mod h1:oUO968BJnuljnB5tntrY3w3zDfI5/PqnQ+RuiZ8aFhk= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -19,16 +19,17 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.26.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg= -github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q= +github.com/charmbracelet/bubbletea v0.26.5 h1:90pqTPElAReb/qQUgSMUresTkfwVr0Wx+zczeHHOgxk= +github.com/charmbracelet/bubbletea v0.26.5/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= -github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= -github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/input v0.1.1 h1:YDOJaTUKCqtGnq9PHzx3pkkl4pXDOANUHmhH3DqMtM4= -github.com/charmbracelet/x/input v0.1.1/go.mod h1:jvdTVUnNWj/RD6hjC4FsoB0SeZCJ2ZBkiuFP9zXvZI0= +github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= +github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/input v0.1.2 h1:QJAZr33eOhDowkkEQ24rsJy4Llxlm+fRDf/cQrmqJa0= +github.com/charmbracelet/x/input v0.1.2/go.mod h1:LGBim0maUY4Pitjn/4fHnuXb4KirU3DODsyuHuXdOyA= github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= @@ -39,6 +40,8 @@ github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +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= @@ -46,17 +49,42 @@ 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/dchest/jsmin v0.0.0-20220218165748-59f39799265f h1:OGqDDftRTwrvUoL6pOG7rYTmWsTCvyEWFsMjg+HcOaA= github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f/go.mod h1:Dv9D0NUlAsaQcGQZa5kc5mqR9ua72SmA8VXi4cd+cBw= +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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +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/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -67,6 +95,7 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104/go.mod h1:wqKykBG2QzQDJEzvRkcS8x6MiSJkF52hXZsXcjaB3ls= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -75,10 +104,13 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/ncruces/zenity v0.10.12 h1:o4SErDa0kQijlqG6W4OYYzO6kA0fGu34uegvJGcMLBI= github.com/ncruces/zenity v0.10.12/go.mod h1:5OZIERViRR2fN0FcJCcisqxI+lYMDGzEDCEwB/+8iao= +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/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 h1:GranzK4hv1/pqTIhMTXt2X8MmMOuH3hMeUR0o9SP5yc= github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844/go.mod h1:T1TLSfyWVBRXVGzWd0o9BI4kfoO9InEgfQe4NV3mLz8= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -93,8 +125,20 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV 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/trzsz/go-socks5 v0.1.0 h1:R5gbAkGf4EOuwYG3aYZF2lh72e/paFgNkBJqIohHqyE= +github.com/trzsz/go-socks5 v0.1.0/go.mod h1:BN+xFP3tb8oKl4hQTFDQIoL+tdCaJ0QhJKLwpmyjVik= github.com/trzsz/iterm2 v0.1.2 h1:VwfLzr2fKeaLf+p4tS0ms+kqdiQQxVLbTJUoyuQXmK8= github.com/trzsz/iterm2 v0.1.2/go.mod h1:PMI+3JcT7J9D0T6e3mOWv8ICYdrrNZwuge3Tm7zDLws= github.com/trzsz/pageant v0.1.0 h1:Ux5VSA6IMwahrMQ+8BeCnDKh3lO/OPkCQgBEpsQ379s= @@ -105,34 +149,109 @@ github.com/trzsz/ssh_config v1.3.6 h1:+LBCg2uzhAgw2s19yqeUdD4YwW2z4kvlsXtKB6zDjm github.com/trzsz/ssh_config v1.3.6/go.mod h1:uSVHpGOTpBwE1FwyUrtnanlFuxZKt4dvdKFVKe41h58= github.com/trzsz/trzsz-go v1.1.8-0.20240525015006-6424386a6738 h1:yPNjsEKiQAkBtbPBHWKg8sZcx2NhEvK1WmfmVMs3Moc= github.com/trzsz/trzsz-go v1.1.8-0.20240525015006-6424386a6738/go.mod h1:fBEGNZiKmGYodYENKATNuz/U3Wzt6MWWJs3ox0CzPhM= +github.com/trzsz/tsshd v0.1.0 h1:1zrcv40kALWBVAitVio+7LHE8O7YqLe1G0rlOVEHx9Y= +github.com/trzsz/tsshd v0.1.0/go.mod h1:jX9fIJwOIUfx3FdpUj485/rwdEwPNgXlAfMMihN9dRw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +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= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +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/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw= -golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/image v0.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco= +golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +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/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +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/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +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/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +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= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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/tssh/agent.go b/tssh/agent.go index 7906d8c..6e651ba 100644 --- a/tssh/agent.go +++ b/tssh/agent.go @@ -87,7 +87,7 @@ func getAgentClient(args *sshArgs, param *sshParam) agent.ExtendedAgent { const channelType = "auth-agent@openssh.com" -func forwardToRemote(client *ssh.Client, addr string) error { +func forwardToRemote(client sshClient, addr string) error { channels := client.HandleChannelOpen(channelType) if channels == nil { return fmt.Errorf("agent: already have handler for %s", channelType) @@ -120,3 +120,14 @@ func forwardAgentRequest(channel ssh.Channel, addr string) { forwardChannel(channel, conn) } + +func requestAgentForwarding(session sshSession) error { + ok, err := session.SendRequest("auth-agent-req@openssh.com", true, nil) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("forwarding request denied") + } + return nil +} diff --git a/tssh/args.go b/tssh/args.go index 8039dba..d2884d6 100644 --- a/tssh/args.go +++ b/tssh/args.go @@ -80,6 +80,8 @@ type sshArgs struct { Relay bool `arg:"--relay" help:"force trzsz run as a relay on the jump server"` Debug bool `arg:"--debug" help:"verbose mode for debugging, same as ssh's -vvv"` Zmodem bool `arg:"--zmodem" help:"enable zmodem lrzsz ( rz / sz ) feature"` + UdpMode bool `arg:"--udp" help:"ssh over UDP protocol like mosh"` + TsshdPath string `arg:"--tsshd-path" placeholder:"path" help:"[udp] tsshd absolute path on the server"` NewHost bool `arg:"--new-host" help:"[tools] add new host to configuration"` EncSecret bool `arg:"--enc-secret" help:"[tools] encode secret for configuration"` InstallTrzsz bool `arg:"--install-trzsz" help:"[tools] install trzsz to the remote server"` diff --git a/tssh/args_test.go b/tssh/args_test.go index faea7db..177bab7 100644 --- a/tssh/args_test.go +++ b/tssh/args_test.go @@ -88,6 +88,9 @@ func TestSshArgs(t *testing.T) { assertArgsEqual("--debug", sshArgs{Debug: true}) assertArgsEqual("--zmodem", sshArgs{Zmodem: true}) + assertArgsEqual("--udp", sshArgs{UdpMode: true}) + assertArgsEqual("--tsshd-path /usr/bin/tsshd", sshArgs{TsshdPath: "/usr/bin/tsshd"}) + assertArgsEqual("--new-host", sshArgs{NewHost: true}) assertArgsEqual("--enc-secret", sshArgs{EncSecret: true}) assertArgsEqual("--install-trzsz", sshArgs{InstallTrzsz: true}) diff --git a/tssh/ctrl_unix.go b/tssh/ctrl_unix.go index 449415b..8309524 100644 --- a/tssh/ctrl_unix.go +++ b/tssh/ctrl_unix.go @@ -45,7 +45,6 @@ import ( "time" "github.com/creack/pty" - "golang.org/x/crypto/ssh" ) type controlMaster struct { @@ -319,7 +318,7 @@ func startControlMaster(args *sshArgs, sshPath string) error { return nil } -func connectViaControl(args *sshArgs, param *sshParam) *ssh.Client { +func connectViaControl(args *sshArgs, param *sshParam) sshClient { ctrlMaster := getOptionConfig(args, "ControlMaster") ctrlPath := getOptionConfig(args, "ControlPath") @@ -377,5 +376,5 @@ func connectViaControl(args *sshArgs, param *sshParam) *ssh.Client { } debug("login to [%s] success", args.Destination) - return ssh.NewClient(ncc, chans, reqs) + return sshNewClient(ncc, chans, reqs) } diff --git a/tssh/ctrl_windows.go b/tssh/ctrl_windows.go index e5c6e6a..7171e90 100644 --- a/tssh/ctrl_windows.go +++ b/tssh/ctrl_windows.go @@ -26,11 +26,9 @@ package tssh import ( "strings" - - "golang.org/x/crypto/ssh" ) -func connectViaControl(args *sshArgs, param *sshParam) *ssh.Client { +func connectViaControl(args *sshArgs, param *sshParam) sshClient { ctrlMaster := getOptionConfig(args, "ControlMaster") ctrlPath := getOptionConfig(args, "ControlPath") diff --git a/tssh/env.go b/tssh/env.go index 71a3157..b07a4e2 100644 --- a/tssh/env.go +++ b/tssh/env.go @@ -29,8 +29,6 @@ import ( "os" "regexp" "strings" - - "golang.org/x/crypto/ssh" ) type sshEnv struct { @@ -120,7 +118,7 @@ func getSetEnvs(args *sshArgs) ([]*sshEnv, error) { return envs, nil } -func sendAndSetEnv(args *sshArgs, session *ssh.Session) (string, error) { +func sendAndSetEnv(args *sshArgs, session sshSession) (string, error) { sendEnvs, err := getSendEnvs(args) if err != nil { return "", err diff --git a/tssh/expect.go b/tssh/expect.go index 879be74..b518b52 100644 --- a/tssh/expect.go +++ b/tssh/expect.go @@ -310,6 +310,9 @@ func (e *sshExpect) captureOutput(reader io.Reader, ch chan<- []byte) ([]byte, e } func (e *sshExpect) wrapOutput(reader io.Reader, writer io.Writer, ch chan []byte) { + if reader == nil { + return + } buf, err := e.captureOutput(reader, ch) if err != nil { return @@ -474,7 +477,7 @@ func getExpectTimeout(args *sshArgs, prefix string) int { return int(count) } -func execExpectInteractions(args *sshArgs, ss *sshSession) { +func execExpectInteractions(args *sshArgs, ss *sshClientSession) { expectCount := getExpectCount(args, "") if expectCount <= 0 { return diff --git a/tssh/forward.go b/tssh/forward.go index ac9ba8f..3bce8ca 100644 --- a/tssh/forward.go +++ b/tssh/forward.go @@ -37,7 +37,7 @@ import ( "sync" "time" - "github.com/armon/go-socks5" + "github.com/trzsz/go-socks5" "golang.org/x/crypto/ssh" ) @@ -203,9 +203,9 @@ func listenOnLocal(args *sshArgs, addr *string, port string) (listeners []net.Li listen := func(network, address string) { listener, err := net.Listen(network, address) if err != nil { - debug("forward listen on local '%s' failed: %v", address, err) + debug("forward listen on local %s '%s' failed: %v", network, address, err) } else { - debug("forward listen on local '%s' success", address) + debug("forward listen on local %s '%s' success", network, address) listeners = append(listeners, listener) } } @@ -223,13 +223,13 @@ func listenOnLocal(args *sshArgs, addr *string, port string) (listeners []net.Li return } -func listenOnRemote(args *sshArgs, client *ssh.Client, addr *string, port string) (listeners []net.Listener) { +func listenOnRemote(args *sshArgs, client sshClient, addr *string, port string) (listeners []net.Listener) { listen := func(network, address string) { listener, err := client.Listen(network, address) if err != nil { - debug("forward listen on remote '%s' failed: %v", address, err) + debug("forward listen on remote %s '%s' failed: %v", network, address, err) } else { - debug("forward listen on remote '%s' success", address) + debug("forward listen on remote %s '%s' success", network, address) listeners = append(listeners, listener) } } @@ -247,8 +247,8 @@ func listenOnRemote(args *sshArgs, client *ssh.Client, addr *string, port string return } -func stdioForward(client *ssh.Client, addr string) (*sync.WaitGroup, error) { - conn, err := dialWithTimeout(client, "tcp", addr, 10*time.Second) +func stdioForward(client sshClient, addr string) (*sync.WaitGroup, error) { + conn, err := client.DialTimeout("tcp", addr, 10*time.Second) if err != nil { return nil, fmt.Errorf("stdio forward failed: %v", err) } @@ -279,11 +279,11 @@ func (d sshResolver) Resolve(ctx context.Context, name string) (context.Context, return ctx, []byte{}, nil } -func dynamicForward(client *ssh.Client, b *bindCfg, args *sshArgs) { +func dynamicForward(client sshClient, b *bindCfg, args *sshArgs) { server, err := socks5.New(&socks5.Config{ Resolver: &sshResolver{}, Dial: func(ctx context.Context, network, addr string) (net.Conn, error) { - return dialWithTimeout(client, network, addr, 10*time.Second) + return client.DialTimeout(network, addr, 10*time.Second) }, Logger: log.New(io.Discard, "", log.LstdFlags), }) @@ -330,7 +330,7 @@ func netForward(local, remote net.Conn) { <-done } -func localForward(client *ssh.Client, f *forwardCfg, args *sshArgs) { +func localForward(client sshClient, f *forwardCfg, args *sshArgs) { remoteAddr := joinHostPort(f.destHost, strconv.Itoa(f.destPort)) for _, listener := range listenOnLocal(args, f.bindAddr, strconv.Itoa(f.bindPort)) { go func(listener net.Listener) { @@ -344,7 +344,7 @@ func localForward(client *ssh.Client, f *forwardCfg, args *sshArgs) { debug("local forward accept failed: %v", err) continue } - remote, err := dialWithTimeout(client, "tcp", remoteAddr, 10*time.Second) + remote, err := client.DialTimeout("tcp", remoteAddr, 10*time.Second) if err != nil { debug("local forward dial [%s] failed: %v", remoteAddr, err) local.Close() @@ -356,7 +356,7 @@ func localForward(client *ssh.Client, f *forwardCfg, args *sshArgs) { } } -func remoteForward(client *ssh.Client, f *forwardCfg, args *sshArgs) { +func remoteForward(client sshClient, f *forwardCfg, args *sshArgs) { localAddr := joinHostPort(f.destHost, strconv.Itoa(f.destPort)) for _, listener := range listenOnRemote(args, client, f.bindAddr, strconv.Itoa(f.bindPort)) { go func(listener net.Listener) { @@ -382,7 +382,7 @@ func remoteForward(client *ssh.Client, f *forwardCfg, args *sshArgs) { } } -func sshForward(client *ssh.Client, args *sshArgs, param *sshParam) error { +func sshForward(client sshClient, args *sshArgs, param *sshParam) error { // clear all forwardings if strings.ToLower(getOptionConfig(args, "ClearAllForwardings")) == "yes" { return nil @@ -447,7 +447,7 @@ type x11Request struct { ScreenNumber uint32 } -func sshX11Forward(args *sshArgs, client *ssh.Client, session *ssh.Session) { +func sshX11Forward(args *sshArgs, client sshClient, session sshSession) { if args.NoX11Forward || !args.X11Untrusted && !args.X11Trusted && strings.ToLower(getOptionConfig(args, "ForwardX11")) != "yes" { return } diff --git a/tssh/login.go b/tssh/login.go index 4af99c7..8bd515a 100644 --- a/tssh/login.go +++ b/tssh/login.go @@ -44,7 +44,6 @@ import ( "github.com/alessio/shellescape" "github.com/skeema/knownhosts" "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" "golang.org/x/term" ) @@ -74,28 +73,6 @@ type sshParam struct { command string } -type sshSession struct { - client *ssh.Client - session *ssh.Session - serverIn io.WriteCloser - serverOut io.Reader - serverErr io.Reader - cmd string - tty bool -} - -func (s *sshSession) Close() { - if s.serverIn != nil { - s.serverIn.Close() - } - if s.session != nil { - s.session.Close() - } - if s.client != nil { - s.client.Close() - } -} - func joinHostPort(host, port string) string { if !strings.HasPrefix(host, "[") && strings.ContainsRune(host, ':') { return fmt.Sprintf("[%s]:%s", host, port) @@ -937,21 +914,6 @@ func parseCmdAndTTY(args *sshArgs, param *sshParam) (cmd string, tty bool, err e return } -func dialWithTimeout(client *ssh.Client, network, addr string, timeout time.Duration) (conn net.Conn, err error) { - done := make(chan struct{}, 1) - go func() { - defer close(done) - conn, err = client.Dial(network, addr) - done <- struct{}{} - }() - select { - case <-time.After(timeout): - err = fmt.Errorf("dial [%s] timeout", addr) - case <-done: - } - return -} - var lastServerAliveTime atomic.Pointer[time.Time] type connWithTimeout struct { @@ -1030,7 +992,7 @@ func getNetworkAddressFamily(args *sshArgs) string { } } -func sshConnect(args *sshArgs, client *ssh.Client, proxy string) (*ssh.Client, *sshParam, bool, error) { +func sshConnect(args *sshArgs, client sshClient, proxy string) (sshClient, *sshParam, bool, error) { param, err := getSshParam(args) if err != nil { return nil, nil, false, err @@ -1066,9 +1028,9 @@ func sshConnect(args *sshArgs, client *ssh.Client, proxy string) (*ssh.Client, * network := getNetworkAddressFamily(args) - proxyConnect := func(client *ssh.Client, proxy string) (*ssh.Client, *sshParam, bool, error) { + proxyConnect := func(client sshClient, proxy string) (sshClient, *sshParam, bool, error) { debug("login to [%s], addr: %s", args.Destination, param.addr) - conn, err := dialWithTimeout(client, network, param.addr, 10*time.Second) + conn, err := client.DialTimeout(network, param.addr, 10*time.Second) if err != nil { return nil, param, false, fmt.Errorf("proxy [%s] dial tcp [%s] failed: %v", proxy, param.addr, err) } @@ -1077,7 +1039,7 @@ func sshConnect(args *sshArgs, client *ssh.Client, proxy string) (*ssh.Client, * return nil, param, false, fmt.Errorf("proxy [%s] new conn [%s] failed: %v", proxy, param.addr, err) } debug("login to [%s] success", args.Destination) - return ssh.NewClient(ncc, chans, reqs), param, false, nil + return sshNewClient(ncc, chans, reqs), param, false, nil } // has parent client @@ -1097,7 +1059,7 @@ func sshConnect(args *sshArgs, client *ssh.Client, proxy string) (*ssh.Client, * return nil, param, false, fmt.Errorf("proxy command [%s] new conn [%s] failed: %v", cmd, param.addr, err) } debug("login to [%s] success", args.Destination) - return ssh.NewClient(ncc, chans, reqs), param, false, nil + return sshNewClient(ncc, chans, reqs), param, false, nil } // no proxy @@ -1112,11 +1074,11 @@ func sshConnect(args *sshArgs, client *ssh.Client, proxy string) (*ssh.Client, * return nil, param, false, fmt.Errorf("new conn [%s] failed: %v", param.addr, err) } debug("login to [%s] success", args.Destination) - return ssh.NewClient(ncc, chans, reqs), param, false, nil + return sshNewClient(ncc, chans, reqs), param, false, nil } // has proxies - var proxyClient *ssh.Client + var proxyClient sshClient for _, proxy = range param.proxy { proxyClient, _, _, err = sshConnect(&sshArgs{Destination: proxy}, proxyClient, proxy) if err != nil { @@ -1126,7 +1088,7 @@ func sshConnect(args *sshArgs, client *ssh.Client, proxy string) (*ssh.Client, * return proxyConnect(proxyClient, proxy) } -func keepAlive(client *ssh.Client, args *sshArgs) { +func keepAlive(client sshClient, args *sshArgs) { getOptionValue := func(option string) int { value, err := strconv.Atoi(getOptionConfig(args, option)) if err != nil { @@ -1167,7 +1129,7 @@ func keepAlive(client *ssh.Client, args *sshArgs) { }() } -func sshAgentForward(args *sshArgs, param *sshParam, client *ssh.Client, session *ssh.Session) { +func sshAgentForward(args *sshArgs, param *sshParam, client sshClient, session sshSession) { if args.NoForwardAgent || !args.ForwardAgent && strings.ToLower(getOptionConfig(args, "ForwardAgent")) != "yes" { return } @@ -1184,16 +1146,15 @@ func sshAgentForward(args *sshArgs, param *sshParam, client *ssh.Client, session warning("forward to agent [%s] failed: %v", addr, err) return } - if err := agent.RequestAgentForwarding(session); err != nil { + if err := requestAgentForwarding(session); err != nil { warning("request agent forwarding failed: %v", err) return } debug("request ssh agent forwarding success") } -func sshLogin(args *sshArgs) (ss *sshSession, err error) { - ss = &sshSession{} - var param *sshParam +func sshTcpLogin(args *sshArgs) (ss *sshClientSession, param *sshParam, udpMode bool, err error) { + ss = &sshClientSession{} defer func() { if err != nil { ss.Close() @@ -1211,6 +1172,9 @@ func sshLogin(args *sshArgs) (ss *sshSession, err error) { return } + // udp mode ? + udpMode = args.UdpMode || strings.ToLower(getOptionConfig(args, "UdpMode")) == "yes" + // parse cmd and tty ss.cmd, ss.tty, err = parseCmdAndTTY(args, param) if err != nil { @@ -1218,24 +1182,26 @@ func sshLogin(args *sshArgs) (ss *sshSession, err error) { } // keep alive - if !control { + if !control && !udpMode { keepAlive(ss.client, args) } - // stdio forward - if args.StdioForward != "" { + // stdio forward runs as a proxy without port forwarding. + // but udp mode requires a new session to start tsshd. + if args.StdioForward != "" && !udpMode { return } - // ssh forward - if !control { + // ssh port forwarding + if !control && !udpMode { if err = sshForward(ss.client, args, param); err != nil { return } } - // no command - if args.NoCommand { + // session is useless without executing remote command. + // but udp mode requires a new session to start tsshd. + if args.NoCommand && !udpMode { return } @@ -1246,13 +1212,6 @@ func sshLogin(args *sshArgs) (ss *sshSession, err error) { return } - // send and set env - var term string - term, err = sendAndSetEnv(args, ss.session) - if err != nil { - return - } - // session input and output ss.serverIn, err = ss.session.StdinPipe() if err != nil { @@ -1270,7 +1229,7 @@ func sshLogin(args *sshArgs) (ss *sshSession, err error) { return } - if !control { + if !control && !udpMode { // ssh agent forward sshAgentForward(args, param, ss.client, ss.session) @@ -1278,16 +1237,53 @@ func sshLogin(args *sshArgs) (ss *sshSession, err error) { sshX11Forward(args, ss.client, ss.session) } + return +} + +func sshLogin(args *sshArgs) (*sshClientSession, error) { + ss, param, udpMode, err := sshTcpLogin(args) + if err != nil { + return nil, err + } + + if udpMode { + ss, err = sshUdpLogin(args, param, ss) + if err != nil { + return nil, err + } + + // ssh port forwarding if not running as a proxy ( aka: not stdio forward ). + if args.StdioForward == "" { + if err := sshForward(ss.client, args, param); err != nil { + ss.Close() + return nil, err + } + } + } + + // if running as a proxy ( aka: stdio forward ), or if not executing remote command, + // then there is no need to initialize the session, so we return early here. + if args.StdioForward != "" || args.NoCommand { + return ss, nil + } + + // send and set env + term, err := sendAndSetEnv(args, ss.session) + if err != nil { + ss.Close() + return nil, err + } + // not terminal or not tty if !isTerminal || !ss.tty { - return + return ss, nil } // request pty session width, height, err := getTerminalSize() if err != nil { - err = fmt.Errorf("get terminal size failed: %v", err) - return + ss.Close() + return nil, fmt.Errorf("get terminal size failed: %v", err) } if term == "" { term = os.Getenv("TERM") @@ -1296,9 +1292,9 @@ func sshLogin(args *sshArgs) (ss *sshSession, err error) { } } if err = ss.session.RequestPty(term, height, width, ssh.TerminalModes{}); err != nil { - err = fmt.Errorf("request pty failed: %v", err) - return + ss.Close() + return nil, fmt.Errorf("request pty failed: %v", err) } - return + return ss, nil } diff --git a/tssh/main.go b/tssh/main.go index 5d3af08..cb1ed67 100644 --- a/tssh/main.go +++ b/tssh/main.go @@ -74,12 +74,9 @@ func background(args *sshArgs, dest string) (bool, error) { sleepTime := time.Duration(0) for { - cmd := exec.Cmd{ - Path: os.Args[0], - Args: newArgs, - Env: env, - Stderr: os.Stderr, - } + cmd := exec.Command(newArgs[0], newArgs[1:]...) + cmd.Env = env + cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { return true, fmt.Errorf("run in background failed: %v", err) @@ -217,7 +214,7 @@ func sshStart(args *sshArgs) error { return nil } - // no command + // not executing remote command if args.NoCommand { cleanupAfterLogin() _ = ss.client.Wait() diff --git a/tssh/ssh.go b/tssh/ssh.go new file mode 100644 index 0000000..55553f4 --- /dev/null +++ b/tssh/ssh.go @@ -0,0 +1,131 @@ +/* +MIT License + +Copyright (c) 2023-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 tssh + +import ( + "fmt" + "io" + "net" + "time" + + "golang.org/x/crypto/ssh" +) + +type sshClient interface { + Wait() error + Close() error + NewSession() (sshSession, error) + DialTimeout(network, addr string, timeout time.Duration) (net.Conn, error) + Listen(network, addr string) (net.Listener, error) + HandleChannelOpen(channelType string) <-chan ssh.NewChannel + SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error) +} + +type sshSession interface { + Wait() error + Close() error + Shell() error + Run(cmd string) error + Start(cmd string) error + WindowChange(height, width int) error + Setenv(name, value string) error + StdinPipe() (io.WriteCloser, error) + StdoutPipe() (io.Reader, error) + StderrPipe() (io.Reader, error) + Output(cmd string) ([]byte, error) + CombinedOutput(cmd string) ([]byte, error) + RequestPty(term string, height, width int, termmodes ssh.TerminalModes) error + SendRequest(name string, wantReply bool, payload []byte) (bool, error) +} + +type sshClientSession struct { + client sshClient + session sshSession + serverIn io.WriteCloser + serverOut io.Reader + serverErr io.Reader + cmd string + tty bool +} + +func (s *sshClientSession) Close() { + if s.serverIn != nil { + s.serverIn.Close() + } + if s.session != nil { + s.session.Close() + } + if s.client != nil { + s.client.Close() + } +} + +type sshClientWrapper struct { + client *ssh.Client +} + +func (c *sshClientWrapper) Wait() error { + return c.client.Wait() +} + +func (c *sshClientWrapper) Close() error { + return c.client.Close() +} + +func (c *sshClientWrapper) NewSession() (sshSession, error) { + return c.client.NewSession() +} + +func (c *sshClientWrapper) DialTimeout(network, addr string, timeout time.Duration) (conn net.Conn, err error) { + done := make(chan struct{}, 1) + go func() { + defer close(done) + conn, err = c.client.Dial(network, addr) + done <- struct{}{} + }() + select { + case <-time.After(timeout): + err = fmt.Errorf("dial [%s] timeout", addr) + case <-done: + } + return +} + +func (c *sshClientWrapper) Listen(network, addr string) (net.Listener, error) { + return c.client.Listen(network, addr) +} + +func (c *sshClientWrapper) HandleChannelOpen(channelType string) <-chan ssh.NewChannel { + return c.client.HandleChannelOpen(channelType) +} + +func (c *sshClientWrapper) SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error) { + return c.client.SendRequest(name, wantReply, payload) +} + +func sshNewClient(c ssh.Conn, chans <-chan ssh.NewChannel, reqs <-chan *ssh.Request) sshClient { + client := ssh.NewClient(c, chans, reqs) + return &sshClientWrapper{client} +} diff --git a/tssh/tools.go b/tssh/tools.go index 8743c81..6bee9de 100644 --- a/tssh/tools.go +++ b/tssh/tools.go @@ -35,7 +35,6 @@ import ( "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "golang.org/x/crypto/ssh" ) const ( @@ -464,7 +463,7 @@ func execLocalTools(args *sshArgs) (int, bool) { } // execRemoteTools execute remote tools if necessary -func execRemoteTools(args *sshArgs, client *ssh.Client) { +func execRemoteTools(args *sshArgs, client sshClient) { switch { case args.InstallTrzsz: execInstallTrzsz(args, client) diff --git a/tssh/tools_install_trzsz.go b/tssh/tools_install_trzsz.go index 8e6ccd2..11442ba 100644 --- a/tssh/tools_install_trzsz.go +++ b/tssh/tools_install_trzsz.go @@ -36,8 +36,6 @@ import ( "os" "path/filepath" "strings" - - "golang.org/x/crypto/ssh" ) const kMaxBufferSize = 32 * 1024 @@ -72,7 +70,7 @@ func getLatestTrzszVersion() (string, error) { return release.TagName[1:], nil } -func checkTrzszVersion(client *ssh.Client, cmd, name, version string) bool { +func checkTrzszVersion(client sshClient, cmd, name, version string) bool { session, err := client.NewSession() if err != nil { return false @@ -85,16 +83,16 @@ func checkTrzszVersion(client *ssh.Client, cmd, name, version string) bool { return strings.TrimSpace(string(output)) == fmt.Sprintf("%s (trzsz) go %s", name, version) } -func checkInstalledVersion(client *ssh.Client, path, name, version string) bool { +func checkInstalledVersion(client sshClient, path, name, version string) bool { // local may be Windows, remote may be Linux, so filepath.Join is not suitable here. return checkTrzszVersion(client, fmt.Sprintf("%s/%s -v", path, name), name, version) } -func checkTrzszExecutable(client *ssh.Client, name, version string) bool { +func checkTrzszExecutable(client sshClient, name, version string) bool { return checkTrzszVersion(client, fmt.Sprintf("$SHELL -l -c '%s -v'", name), name, version) } -func checkTrzszPathEnv(client *ssh.Client, version, path string) { +func checkTrzszPathEnv(client sshClient, version, path string) { trzExecutable := checkTrzszExecutable(client, "trz", version) tszExecutable := checkTrzszExecutable(client, "tsz", version) if !trzExecutable || !tszExecutable { @@ -102,7 +100,34 @@ func checkTrzszPathEnv(client *ssh.Client, version, path string) { } } -func getRemoteServerOS(client *ssh.Client) (string, error) { +func getRemoteUserHome(client sshClient) (string, error) { + session, err := client.NewSession() + if err != nil { + return "", err + } + defer session.Close() + output, err := session.Output("env") + if err != nil { + return "", err + } + scanner := bufio.NewScanner(strings.NewReader(string(output))) + for scanner.Scan() { + env := scanner.Text() + pos := strings.IndexRune(env, '=') + if pos <= 0 { + continue + } + if env[:pos] == "HOME" { + if home := strings.TrimSpace(env[pos+1:]); home != "" { + return home, nil + } + break + } + } + return "~", nil +} + +func getRemoteServerOS(client sshClient) (string, error) { session, err := client.NewSession() if err != nil { return "", err @@ -123,7 +148,7 @@ func getRemoteServerOS(client *ssh.Client) (string, error) { } } -func getRemoteServerArch(client *ssh.Client) (string, error) { +func getRemoteServerArch(client sshClient) (string, error) { session, err := client.NewSession() if err != nil { return "", err @@ -150,7 +175,7 @@ func getRemoteServerArch(client *ssh.Client) (string, error) { } } -func mkdirInstallPath(client *ssh.Client, path string) error { +func mkdirInstallPath(client sshClient, path string) error { session, err := client.NewSession() if err != nil { return err @@ -273,7 +298,7 @@ func readTrzszBinary(path, version, svrOS, arch string) ([]byte, []byte, error) return extractTrzszBinary(gzr, version, svrOS, arch) } -func uploadTrzszBinary(client *ssh.Client, path string, trz, tsz []byte) error { +func uploadTrzszBinary(client sshClient, path string, trz, tsz []byte) error { session, err := client.NewSession() if err != nil { return err @@ -334,7 +359,6 @@ func uploadTrzszBinary(client *ssh.Client, path string, trz, tsz []byte) error { } go func() { - defer writer.Close() defer progress.stopProgress() if !checkTransferResponse() { return @@ -351,11 +375,15 @@ func uploadTrzszBinary(client *ssh.Client, path string, trz, tsz []byte) error { if !writeBinaryContent(tsz) { return } + _ = writeTransferCommand("E\n") }() - output, err := session.CombinedOutput(fmt.Sprintf("scp -tqr %s", path)) + stderr, err := session.StderrPipe() if err != nil { - msg := strings.TrimSpace(string(output)) + return err + } + if err := session.Run(fmt.Sprintf("scp -tqr %s", path)); err != nil { + msg := readFromStream(stderr) if msg != "" { errMsg = append(errMsg, msg) } @@ -367,7 +395,7 @@ func uploadTrzszBinary(client *ssh.Client, path string, trz, tsz []byte) error { return nil } -func execInstallTrzsz(args *sshArgs, client *ssh.Client) { +func execInstallTrzsz(args *sshArgs, client sshClient) { version := args.TrzszVersion if version == "" { var err error @@ -383,6 +411,14 @@ func execInstallTrzsz(args *sshArgs, client *ssh.Client) { installPath = "~/.local/bin/" } + if strings.HasPrefix(installPath, "~/") { + home, err := getRemoteUserHome(client) + if err != nil { + toolsWarn("InstallTrzsz", "get remote user home path failed: %v", err) + } + installPath = filepath.Join(home, installPath[2:]) + } + trzInstalled := checkInstalledVersion(client, installPath, "trz", version) tszInstalled := checkInstalledVersion(client, installPath, "tsz", version) if trzInstalled && tszInstalled { diff --git a/tssh/trzsz.go b/tssh/trzsz.go index 6564e81..752da88 100644 --- a/tssh/trzsz.go +++ b/tssh/trzsz.go @@ -82,13 +82,12 @@ func wrapStdIO(serverIn io.WriteCloser, serverOut io.Reader, serverErr io.Reader // delay and close for { time.Sleep(time.Second) - if lastTime := lastServerAliveTime.Load(); lastTime != nil && time.Since(*lastTime) >= time.Second { + if lastTime := lastServerAliveTime.Load(); lastTime != nil && time.Since(*lastTime) > 2*time.Second { return } } } if err != nil { - warning("wrap stdio read failed: %v", err) return } } @@ -104,7 +103,7 @@ func wrapStdIO(serverIn io.WriteCloser, serverOut io.Reader, serverErr io.Reader } } -func enableTrzsz(args *sshArgs, ss *sshSession) error { +func enableTrzsz(args *sshArgs, ss *sshClientSession) error { // not terminal or not tty if !isTerminal || !ss.tty { wrapStdIO(ss.serverIn, ss.serverOut, ss.serverErr, ss.tty) @@ -137,7 +136,7 @@ func enableTrzsz(args *sshArgs, ss *sshSession) error { onTerminalResize(func(width, height int) { _ = ss.session.WindowChange(height, width) }) // setup tunnel connect trzszRelay.SetTunnelConnector(func(port int) net.Conn { - conn, _ := dialWithTimeout(ss.client, "tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Second) + conn, _ := ss.client.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Second) return conn }) return nil @@ -176,7 +175,7 @@ func enableTrzsz(args *sshArgs, ss *sshSession) error { // setup tunnel connect trzszFilter.SetTunnelConnector(func(port int) net.Conn { - conn, _ := dialWithTimeout(ss.client, "tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Second) + conn, _ := ss.client.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Second) return conn }) diff --git a/tssh/udp.go b/tssh/udp.go new file mode 100644 index 0000000..b4ca8aa --- /dev/null +++ b/tssh/udp.go @@ -0,0 +1,679 @@ +/* +MIT License + +Copyright (c) 2023-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 tssh + +import ( + "bytes" + "crypto/sha1" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net" + "os" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/google/shlex" + "github.com/trzsz/tsshd/tsshd" + "github.com/xtaci/kcp-go/v5" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/ssh" +) + +const kDefaultUdpAliveTimeout = 100 * time.Second + +type sshUdpClient struct { + key []byte + addr string + wg sync.WaitGroup + busMutex sync.Mutex + busSession *kcp.UDPSession + sessionMutex sync.Mutex + sessionID atomic.Uint64 + sessionMap map[uint64]*sshUdpSession + lastAliveTime atomic.Pointer[time.Time] + closed atomic.Bool +} + +func (c *sshUdpClient) Wait() error { + c.wg.Wait() + return nil +} + +func (c *sshUdpClient) Close() error { + c.busMutex.Lock() + defer c.busMutex.Unlock() + if err := tsshd.SendCommand(c.busSession, "close"); err != nil { + warning("send close command failed: %v", err) + } + c.closed.Store(true) + return c.busSession.Close() +} + +func (c *sshUdpClient) NewSession() (sshSession, error) { + kcpSession, err := newKcpSession(c.addr, c.key, "session") + if err != nil { + return nil, err + } + c.wg.Add(1) + udpSession := &sshUdpSession{client: c, session: kcpSession, envs: make(map[string]string)} + udpSession.wg.Add(1) + c.sessionMutex.Lock() + defer c.sessionMutex.Unlock() + udpSession.id = c.sessionID.Add(1) - 1 + c.sessionMap[udpSession.id] = udpSession + return udpSession, nil +} + +func (c *sshUdpClient) DialTimeout(network, addr string, timeout time.Duration) (net.Conn, error) { + session, err := newKcpSession(c.addr, c.key, "dial") + if err != nil { + return nil, err + } + msg := tsshd.DialMessage{ + Network: network, + Addr: addr, + Timeout: timeout, + } + if err := tsshd.SendMessage(session, &msg); err != nil { + session.Close() + return nil, fmt.Errorf("send dial message failed: %v", err) + } + if err := tsshd.RecvError(session); err != nil { + session.Close() + return nil, err + } + c.wg.Add(1) + return &sshUdpConn{session, c}, nil +} + +func (c *sshUdpClient) Listen(network, addr string) (net.Listener, error) { + session, err := newKcpSession(c.addr, c.key, "listen") + if err != nil { + return nil, err + } + msg := tsshd.ListenMessage{ + Network: network, + Addr: addr, + } + if err := tsshd.SendMessage(session, &msg); err != nil { + session.Close() + return nil, fmt.Errorf("send listen message failed: %v", err) + } + if err := tsshd.RecvError(session); err != nil { + session.Close() + return nil, err + } + c.wg.Add(1) + return &sshUdpListener{client: c, session: session}, nil +} + +func (c *sshUdpClient) HandleChannelOpen(channelType string) <-chan ssh.NewChannel { + return nil +} + +func (c *sshUdpClient) SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error) { + return false, nil, fmt.Errorf("ssh udp client SendRequest is not supported yet") +} + +func (c *sshUdpClient) sendBusCommand(command string) error { + c.busMutex.Lock() + defer c.busMutex.Unlock() + return tsshd.SendCommand(c.busSession, command) +} + +func (c *sshUdpClient) sendBusMessage(command string, msg any) error { + c.busMutex.Lock() + defer c.busMutex.Unlock() + if err := tsshd.SendCommand(c.busSession, command); err != nil { + return err + } + return tsshd.SendMessage(c.busSession, msg) +} + +func (c *sshUdpClient) udpKeepAlive(timeout time.Duration) { + for { + if err := c.sendBusCommand("alive"); err != nil { + warning("udp keep alive failed: %v", err) + } + if t := c.lastAliveTime.Load(); t != nil && time.Since(*t) > timeout { + warning("udp keep alive timeout") + os.Exit(125) + } + time.Sleep(timeout / 10) + if c.closed.Load() { + return + } + } +} + +func (c *sshUdpClient) handleBusEvent() { + for { + command, err := tsshd.RecvCommand(c.busSession) + if c.closed.Load() { + return + } + if err != nil { + warning("recv bus command failed: %v", err) + return + } + switch command { + case "exit": + c.handleExitEvent() + case "error": + c.handleErrorEvent() + case "alive": + now := time.Now() + c.lastAliveTime.Store(&now) + default: + warning("unknown command bus command: %s", command) + } + } +} + +func (c *sshUdpClient) handleExitEvent() { + var exitMsg tsshd.ExitMessage + if err := tsshd.RecvMessage(c.busSession, &exitMsg); err != nil { + warning("recv exit message failed: %v", err) + return + } + + c.sessionMutex.Lock() + defer c.sessionMutex.Unlock() + + udpSession, ok := c.sessionMap[exitMsg.ID] + if !ok { + warning("invalid or exited session id: %d", exitMsg.ID) + return + } + udpSession.code = exitMsg.ExitCode + udpSession.wg.Done() + // the kcp server does not send io.EOF, we trigger it ourselves. + if udpSession.stdout != nil { + udpSession.stdout.Close() + } + if udpSession.stderr != nil { + udpSession.stderr.Close() + } + + delete(c.sessionMap, exitMsg.ID) + c.wg.Done() +} + +func (c *sshUdpClient) handleErrorEvent() { + var errMsg tsshd.ErrorMessage + if err := tsshd.RecvMessage(c.busSession, &errMsg); err != nil { + warning("recv error message failed: %v", err) + return + } + warning("udp error: %s", errMsg.Msg) +} + +type sshUdpSession struct { + id uint64 + wg sync.WaitGroup + client *sshUdpClient + session *kcp.UDPSession + pty bool + height int + width int + envs map[string]string + started bool + stdin io.Reader + stdout io.WriteCloser + stderr *kcp.UDPSession + code int +} + +func (s *sshUdpSession) Wait() error { + s.wg.Wait() + if s.code != 0 { + return fmt.Errorf("udp session exit with %d", s.code) + } + return nil +} + +func (s *sshUdpSession) Close() error { + if s.stdout != nil { + _ = s.stdout.Close() + } + if s.stderr != nil { + _ = s.stderr.Close() + } + return s.session.Close() +} + +func (s *sshUdpSession) Shell() error { + msg := tsshd.StartMessage{ + ID: s.id, + Pty: s.pty, + Shell: true, + Cols: s.width, + Rows: s.height, + Envs: s.envs, + } + return s.startSession(&msg) +} + +func (s *sshUdpSession) Run(cmd string) error { + if err := s.Start(cmd); err != nil { + return err + } + return s.Wait() +} + +func (s *sshUdpSession) Start(cmd string) error { + args, err := shlex.Split(cmd) + if err != nil { + return fmt.Errorf("split cmd [%s] failed: %v", cmd, err) + } + if len(args) == 0 { + return fmt.Errorf("cmd [%s] is empty", cmd) + } + msg := tsshd.StartMessage{ + ID: s.id, + Pty: s.pty, + Shell: false, + Name: args[0], + Args: args[1:], + Envs: s.envs, + } + return s.startSession(&msg) +} + +func (s *sshUdpSession) startSession(msg *tsshd.StartMessage) error { + if s.started { + return fmt.Errorf("session already started") + } + s.started = true + if err := tsshd.SendMessage(s.session, msg); err != nil { + return fmt.Errorf("send session message failed: %v", err) + } + if err := tsshd.RecvError(s.session); err != nil { + return err + } + if s.stdin != nil { + go func() { + _, _ = io.Copy(s.session, s.stdin) + }() + } + if s.stdout != nil { + go func() { + defer s.stdout.Close() + _, _ = io.Copy(s.stdout, s.session) + }() + } + return nil +} + +func (s *sshUdpSession) WindowChange(height, width int) error { + return s.client.sendBusMessage("resize", tsshd.ResizeMessage{ + ID: s.id, + Cols: width, + Rows: height, + }) +} + +func (s *sshUdpSession) Setenv(name, value string) error { + s.envs[name] = value + return nil +} + +func (s *sshUdpSession) StdinPipe() (io.WriteCloser, error) { + if s.stdin != nil { + return nil, fmt.Errorf("stdin already set") + } + reader, writer := io.Pipe() + s.stdin = reader + return writer, nil +} + +func (s *sshUdpSession) StdoutPipe() (io.Reader, error) { + if s.stdout != nil { + return nil, fmt.Errorf("stdout already set") + } + reader, writer := io.Pipe() + s.stdout = writer + return reader, nil +} + +func (s *sshUdpSession) StderrPipe() (io.Reader, error) { + if s.stderr != nil { + return nil, fmt.Errorf("stderr already set") + } + session, err := newKcpSession(s.client.addr, s.client.key, "stderr") + if err != nil { + return nil, err + } + if err := tsshd.SendMessage(session, tsshd.StderrMessage{ID: s.id}); err != nil { + session.Close() + return nil, fmt.Errorf("send stderr message failed: %v", err) + } + if err := tsshd.RecvError(session); err != nil { + session.Close() + return nil, err + } + s.stderr = session + return s.stderr, nil +} + +func (s *sshUdpSession) Output(cmd string) ([]byte, error) { + stdout, err := s.StdoutPipe() + if err != nil { + return nil, err + } + if err := s.Start(cmd); err != nil { + return nil, err + } + var buf bytes.Buffer + var wg sync.WaitGroup + wg.Add(1) + go func() { + _, _ = buf.ReadFrom(stdout) + wg.Done() + }() + if err := s.Wait(); err != nil { + return nil, err + } + wg.Wait() + return buf.Bytes(), nil +} + +func (s *sshUdpSession) CombinedOutput(cmd string) ([]byte, error) { + output, err := s.Output(cmd) + if err != nil || s.stderr == nil { + return output, err + } + var buf bytes.Buffer + buf.Write(output) + _, _ = buf.ReadFrom(s.stderr) + return buf.Bytes(), nil +} + +func (s *sshUdpSession) RequestPty(term string, height, width int, termmodes ssh.TerminalModes) error { + s.pty = true + s.envs["TERM"] = term + s.height = height + s.width = width + return nil +} + +func (s *sshUdpSession) SendRequest(name string, wantReply bool, payload []byte) (bool, error) { + return false, fmt.Errorf("ssh udp session SendRequest is not supported yet") +} + +type sshUdpListener struct { + client *sshUdpClient + session *kcp.UDPSession +} + +func (l *sshUdpListener) Accept() (net.Conn, error) { + var msg tsshd.AcceptMessage + if err := tsshd.RecvMessage(l.session, &msg); err != nil { + return nil, fmt.Errorf("recv accept message failed: %v", err) + } + session, err := newKcpSession(l.client.addr, l.client.key, "accept") + if err != nil { + return nil, err + } + if err := tsshd.SendMessage(session, &msg); err != nil { + session.Close() + return nil, fmt.Errorf("send accept message failed: %v", err) + } + if err := tsshd.RecvError(session); err != nil { + session.Close() + return nil, err + } + l.client.wg.Add(1) + return &sshUdpConn{session, l.client}, nil +} + +func (l *sshUdpListener) Close() error { + l.client.wg.Done() + return l.session.Close() +} + +func (l *sshUdpListener) Addr() net.Addr { + return nil +} + +type sshUdpConn struct { + *kcp.UDPSession + client *sshUdpClient +} + +func (c *sshUdpConn) Close() error { + c.client.wg.Done() + return c.UDPSession.Close() +} + +func sshUdpLogin(args *sshArgs, param *sshParam, ss *sshClientSession) (*sshClientSession, error) { + defer ss.Close() + + svrInfo, err := startTsshdServer(args, ss) + if err != nil { + return nil, err + } + pass, err := hex.DecodeString(svrInfo.Pass) + if err != nil { + return nil, fmt.Errorf("decode pass [%s] failed: %v", svrInfo.Pass, err) + } + salt, err := hex.DecodeString(svrInfo.Salt) + if err != nil { + return nil, fmt.Errorf("decode salt [%s] failed: %v", svrInfo.Pass, err) + } + key := pbkdf2.Key(pass, salt, 4096, 32, sha1.New) + addr := joinHostPort(param.host, strconv.Itoa(svrInfo.Port)) + + busSession, err := newKcpSession(addr, key, "bus") + if err != nil { + return nil, err + } + + udpAliveTimeout := getUdpAliveTimeout(args) + if err := tsshd.SendMessage(busSession, tsshd.BusMessage{Timeout: udpAliveTimeout}); err != nil { + busSession.Close() + return nil, fmt.Errorf("send bus message failed: %v", err) + } + if err := tsshd.RecvError(busSession); err != nil { + busSession.Close() + return nil, err + } + + debug("udp login [%s] success", args.Destination) + udpClient := sshUdpClient{ + key: key, + addr: addr, + busSession: busSession, + sessionMap: make(map[uint64]*sshUdpSession), + } + + // keep alive + if udpAliveTimeout > 0 { + now := time.Now() + udpClient.lastAliveTime.Store(&now) + go udpClient.udpKeepAlive(udpAliveTimeout) + } + + go udpClient.handleBusEvent() + + // no exit while not executing remote command or running in background + if args.NoCommand || args.Background { + udpClient.wg.Add(1) + } + + // if running as a proxy ( aka: stdio forward ), or if not executing remote command, + // then there is no need to make a new session, so we return early here. + if args.StdioForward != "" || args.NoCommand { + return &sshClientSession{ + client: &udpClient, + cmd: ss.cmd, + tty: ss.tty, + }, nil + } + + udpSession, err := udpClient.NewSession() + if err != nil { + busSession.Close() + return nil, fmt.Errorf("new session failed: %v", err) + } + + serverIn, _ := udpSession.StdinPipe() + serverOut, _ := udpSession.StdoutPipe() + return &sshClientSession{ + client: &udpClient, + session: udpSession, + serverIn: serverIn, + serverOut: serverOut, + serverErr: nil, + cmd: ss.cmd, + tty: ss.tty, + }, nil +} + +func startTsshdServer(args *sshArgs, ss *sshClientSession) (*tsshd.ServerInfo, error) { + cmd := getTsshdCommand(args) + debug("tsshd command: %s", cmd) + + if err := ss.session.RequestPty("xterm-256color", 20, 80, ssh.TerminalModes{}); err != nil { + return nil, fmt.Errorf("request pty for tsshd failed: %v", err) + } + + if err := ss.session.Start(cmd); err != nil { + return nil, fmt.Errorf("start tsshd failed: %v", err) + } + if err := ss.session.Wait(); err != nil { + var builder strings.Builder + if outMsg := readFromStream(ss.serverOut); outMsg != "" { + builder.WriteString(outMsg) + } + if errMsg := readFromStream(ss.serverErr); errMsg != "" { + if builder.Len() > 0 { + builder.WriteString("\n") + } + builder.WriteString(errMsg) + } + if builder.Len() == 0 { + builder.WriteString(err.Error()) + } + return nil, fmt.Errorf("(Have you installed tsshd on your server? You may need to specify the path to tsshd.)\r\n"+ + "run tsshd failed: %s", builder.String()) + } + + output := readFromStream(ss.serverOut) + if output == "" { + if errMsg := readFromStream(ss.serverErr); errMsg != "" { + return nil, fmt.Errorf("run tsshd failed: %s", errMsg) + } + return nil, fmt.Errorf("run tsshd failed: the output is empty") + } + pos := strings.LastIndex(output, "\a{") + if pos >= 0 { + output = output[pos+1:] + } + if !strings.HasPrefix(output, "{") || !strings.HasSuffix(output, "}") { + return nil, fmt.Errorf("run tsshd failed: %s", output) + } + + var svrInfo tsshd.ServerInfo + if err := json.Unmarshal([]byte(output), &svrInfo); err != nil { + return nil, fmt.Errorf("json unmarshal [%s] failed: %v", output, err) + } + + return &svrInfo, nil +} + +func getTsshdCommand(args *sshArgs) string { + var buf strings.Builder + if args.TsshdPath != "" { + buf.WriteString(args.TsshdPath) + } else if tsshdPath := getExOptionConfig(args, "TsshdPath"); tsshdPath != "" { + buf.WriteString(tsshdPath) + } else { + buf.WriteString("tsshd") + } + return buf.String() +} + +func readFromStream(stream io.Reader) string { + var buf bytes.Buffer + _, _ = buf.ReadFrom(stream) + return strings.TrimSpace(buf.String()) +} + +func newKcpSession(addr string, key []byte, cmd string) (session *kcp.UDPSession, err error) { + block, err := kcp.NewAESBlockCrypt(key) + if err != nil { + return nil, fmt.Errorf("new aes block crypt failed: %v", err) + } + + done := make(chan struct{}, 1) + go func() { + defer func() { + if err != nil && session != nil { + session.Close() + } + done <- struct{}{} + close(done) + }() + session, err = kcp.DialWithOptions(addr, block, 10, 3) + if err != nil { + err = fmt.Errorf("kcp dial [%s] [%s] failed: %v", addr, cmd, err) + return + } + if err = tsshd.SendCommand(session, cmd); err != nil { + err = fmt.Errorf("kcp send command [%s] [%s] failed: %v", addr, cmd, err) + return + } + if err = tsshd.RecvError(session); err != nil { + err = fmt.Errorf("kcp new session [%s] [%s] failed: %v", addr, cmd, err) + return + } + }() + + select { + case <-time.After(10 * time.Second): + err = fmt.Errorf("kcp new session [%s] [%s] timeout", addr, cmd) + case <-done: + } + return +} + +func getUdpAliveTimeout(args *sshArgs) time.Duration { + udpAliveTimeout := getExOptionConfig(args, "UdpAliveTimeout") + if udpAliveTimeout == "" { + return kDefaultUdpAliveTimeout + } + timeoutSeconds, err := strconv.Atoi(udpAliveTimeout) + if err != nil { + warning("UdpAliveTimeout [%s] invalid: %v", udpAliveTimeout, err) + return kDefaultUdpAliveTimeout + } + return time.Duration(timeoutSeconds) * time.Second +}