├── .github ├── FUNDING.yml └── workflows │ ├── gotest.yml │ └── publish.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── Makefile ├── README.cn.md ├── README.md ├── cmd └── tsshd │ └── main.go ├── debian ├── README ├── README.Debian ├── README.source ├── changelog ├── compat ├── control ├── copyright ├── rules ├── source │ └── format └── tsshd-docs.docs ├── go.mod ├── go.sum └── tsshd ├── bus.go ├── forward.go ├── main.go ├── proto.go ├── server.go ├── service.go ├── session.go ├── utils_darwin.go ├── utils_other.go ├── utils_unix.go └── utils_windows.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: trzsz 2 | -------------------------------------------------------------------------------- /.github/workflows/gotest.yml: -------------------------------------------------------------------------------- 1 | name: Go test tsshd 2 | on: [push] 3 | jobs: 4 | go-test-on-linux: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout tsshd 8 | uses: actions/checkout@v4 9 | - name: Set up Go 10 | uses: actions/setup-go@v5 11 | with: 12 | go-version: "1.21" 13 | - name: go test 14 | run: go test -v -count=1 ./tsshd 15 | go-test-on-macos: 16 | runs-on: macos-latest 17 | steps: 18 | - name: Checkout tsshd 19 | uses: actions/checkout@v4 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: "1.21" 24 | - name: go test 25 | run: go test -v -count=1 ./tsshd 26 | go-test-on-windows: 27 | runs-on: windows-latest 28 | steps: 29 | - name: Checkout tsshd 30 | uses: actions/checkout@v4 31 | - name: Set up Go 32 | uses: actions/setup-go@v5 33 | with: 34 | go-version: "1.21" 35 | - name: go test 36 | run: go test -v -count=1 ./tsshd 37 | go-release-snapshot: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Checkout tsshd 41 | uses: actions/checkout@v4 42 | - name: Set up Go 43 | uses: actions/setup-go@v5 44 | with: 45 | go-version: "1.21" 46 | - name: Run GoReleaser 47 | uses: goreleaser/goreleaser-action@v6 48 | with: 49 | distribution: goreleaser 50 | version: "~> v1" 51 | args: release --clean --snapshot --skip=publish 52 | test-win7-patch: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout trzsz-ssh 56 | uses: actions/checkout@v4 57 | - name: Set up Go 58 | uses: actions/setup-go@v5 59 | with: 60 | go-version: "1.21" 61 | - name: Revert Go1.21 62 | run: | 63 | cd $(go env GOROOT) 64 | curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1 65 | - name: Build tsshd 66 | run: | 67 | go test -v -count=1 ./tsshd 68 | GOOS=windows GOARCH=386 go build -o tsshd_win7_i386/ ./cmd/tsshd/ 69 | GOOS=windows GOARCH=amd64 go build -o tsshd_win7_x86_64/ ./cmd/tsshd/ 70 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release and publish tsshd 2 | on: 3 | release: 4 | types: [released] 5 | jobs: 6 | release-and-publish: 7 | name: Release and publish tsshd 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout tsshd 11 | uses: actions/checkout@v4 12 | - name: Set up Go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: "1.21" 16 | - name: Run GoReleaser 17 | uses: goreleaser/goreleaser-action@v6 18 | with: 19 | distribution: goreleaser 20 | version: "~> v1" 21 | args: release --clean --skip=publish 22 | - name: Upload Release Assets 23 | uses: softprops/action-gh-release@v2 24 | with: 25 | files: | 26 | dist/*.tar.gz 27 | dist/*.zip 28 | dist/*.rpm 29 | dist/*_checksums.txt 30 | - name: Publish rpm to Gemfury 31 | env: 32 | FURY_TOKEN: ${{ secrets.FURY_TOKEN }} 33 | run: | 34 | for filename in dist/tsshd*.rpm; do 35 | curl -F package=@"$filename" https://{$FURY_TOKEN}@push.fury.io/trzsz/ 36 | done 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | bin/ 25 | debian/files 26 | 27 | *.swp 28 | *.swo 29 | 30 | dist/ 31 | vendor/ 32 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: tsshd 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - id: tsshd 7 | main: ./cmd/tsshd 8 | binary: tsshd 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - windows 14 | - darwin 15 | - freebsd 16 | goarch: 17 | - "386" 18 | - amd64 19 | - arm 20 | - arm64 21 | - loong64 22 | goarm: 23 | - "6" 24 | - "7" 25 | ignore: 26 | - goos: windows 27 | goarch: arm 28 | - goos: darwin 29 | goarch: arm 30 | - goos: freebsd 31 | goarch: arm 32 | - goos: freebsd 33 | goarch: "386" 34 | archives: 35 | - id: tsshd 36 | name_template: >- 37 | {{ .ProjectName }}_ 38 | {{- .Version }}_ 39 | {{- if eq .Os "darwin" }}macos_ 40 | {{- else }}{{ .Os }}_{{ end }} 41 | {{- if eq .Arch "amd64" }}x86_64 42 | {{- else if eq .Arch "386" }}i386 43 | {{- else if eq .Arch "arm64" }}aarch64 44 | {{- else if eq .Arch "arm" }}armv{{ .Arm }} 45 | {{- else }}{{ .Arch }}{{ end }} 46 | wrap_in_directory: true 47 | format_overrides: 48 | - goos: windows 49 | format: zip 50 | files: 51 | - none* 52 | nfpms: 53 | - id: tsshd 54 | builds: 55 | - tsshd 56 | file_name_template: >- 57 | {{ .ProjectName }}_ 58 | {{- .Version }}_ 59 | {{- if eq .Os "darwin" }}macos_ 60 | {{- else }}{{ .Os }}_{{ end }} 61 | {{- if eq .Arch "amd64" }}x86_64 62 | {{- else if eq .Arch "386" }}i386 63 | {{- else if eq .Arch "arm64" }}aarch64 64 | {{- else if eq .Arch "arm" }}armv{{ .Arm }} 65 | {{- else }}{{ .Arch }}{{ end }} 66 | homepage: https://trzsz.github.io/ 67 | maintainer: Lonny Wong 68 | description: |- 69 | The `tssh --udp` works like `mosh`, and the `tsshd` works like `mosh-server`. 70 | license: MIT 71 | formats: 72 | - rpm 73 | bindir: /usr/bin 74 | rpm: 75 | group: Unspecified 76 | snapshot: 77 | name_template: "{{ .Version }}.next" 78 | checksum: 79 | name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 The Trzsz SSH Authors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN_DIR := ./bin 2 | BIN_DST := /usr/bin 3 | 4 | ifdef GOOS 5 | ifeq (${GOOS}, windows) 6 | WIN_TARGET := True 7 | endif 8 | else 9 | ifeq (${OS}, Windows_NT) 10 | WIN_TARGET := True 11 | endif 12 | endif 13 | 14 | ifdef WIN_TARGET 15 | TSSHD := tsshd.exe 16 | else 17 | TSSHD := tsshd 18 | endif 19 | 20 | GO_TEST := ${shell basename `which gotest 2>/dev/null` 2>/dev/null || echo go test} 21 | 22 | .PHONY: all clean test install 23 | 24 | all: ${BIN_DIR}/${TSSHD} 25 | 26 | ${BIN_DIR}/${TSSHD}: $(wildcard ./cmd/tsshd/*.go ./tsshd/*.go) go.mod go.sum 27 | go build -o ${BIN_DIR}/ ./cmd/tsshd 28 | 29 | clean: 30 | -rm -f ${BIN_DIR}/tsshd ${BIN_DIR}/tsshd.exe 31 | 32 | test: 33 | ${GO_TEST} -v -count=1 ./tsshd 34 | 35 | install: all 36 | mkdir -p ${DESTDIR}${BIN_DST} 37 | cp ${BIN_DIR}/tsshd ${DESTDIR}${BIN_DST}/ 38 | -------------------------------------------------------------------------------- /README.cn.md: -------------------------------------------------------------------------------- 1 | # tsshd 2 | 3 | [![MIT License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://choosealicense.com/licenses/mit/) 4 | [![GitHub Release](https://img.shields.io/github/v/release/trzsz/tsshd)](https://github.com/trzsz/tsshd/releases) 5 | 6 | `tsshd` 类似于 `mosh-server`,而 [`tssh --udp`](https://github.com/trzsz/trzsz-ssh) 类似于 [`mosh`](https://github.com/mobile-shell/mosh)。 7 | 8 | ## 优点简介 9 | 10 | - 降低延迟( 基于 [QUIC](https://github.com/quic-go/quic-go) / [KCP](https://github.com/xtaci/kcp-go) ) 11 | 12 | - 端口转发( 与 openssh 相同,包括 ssh agent 转发和 X11 转发 ) 13 | 14 | - 连接迁移( 支持网络切换和掉线重连 ) 15 | 16 | ## 如何使用 17 | 18 | 1. 在客户端(本地电脑)上安装 [tssh](https://github.com/trzsz/trzsz-ssh)。 19 | 20 | 2. 在服务端(远程机器)上安装 [tsshd](https://github.com/trzsz/tsshd)。 21 | 22 | 3. 使用 `tssh --udp` 登录服务器。如下配置可省略 `--udp` 参数: 23 | 24 | ``` 25 | Host xxx 26 | #!! UdpMode yes 27 | #!! TsshdPath ~/go/bin/tsshd 28 | ``` 29 | 30 | ## 原理简介 31 | 32 | - `tssh` 在客户端扮演 `ssh` 的角色,`tsshd` 在服务端扮演 `sshd` 的角色。 33 | 34 | - `tssh` 会先作为一个 ssh 客户端正常登录到服务器上,然后在服务器上启动一个新的 `tsshd` 进程。 35 | 36 | - `tsshd` 进程会随机侦听一个 61000 到 62000 之间的 UDP 端口,并将其端口和密钥通过 ssh 通道发回给 `tssh` 进程。登录的 ssh 连接会被关闭,然后 `tssh` 进程通过 UDP 与 `tsshd` 进程通讯。 37 | 38 | - `tsshd` 支持 `QUIC` 协议和 `KCP` 协议(默认是 `QUIC` 协议),可以命令行指定(如 `-oUdpMode=KCP`),或如下配置: 39 | 40 | ``` 41 | Host xxx 42 | #!! UdpMode KCP 43 | ``` 44 | 45 | ## 安装方法 46 | 47 | - Ubuntu 可用 apt 安装 48 | 49 |
sudo apt install tsshd 50 | 51 | ```sh 52 | sudo apt update && sudo apt install software-properties-common 53 | sudo add-apt-repository ppa:trzsz/ppa && sudo apt update 54 | 55 | sudo apt install tsshd 56 | ``` 57 | 58 |
59 | 60 | - Debian 可用 apt 安装 61 | 62 |
sudo apt install tsshd 63 | 64 | ```sh 65 | sudo apt install curl gpg 66 | curl -s 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x7074ce75da7cc691c1ae1a7c7e51d1ad956055ca' \ 67 | | gpg --dearmor -o /usr/share/keyrings/trzsz.gpg 68 | echo 'deb [signed-by=/usr/share/keyrings/trzsz.gpg] https://ppa.launchpadcontent.net/trzsz/ppa/ubuntu jammy main' \ 69 | | sudo tee /etc/apt/sources.list.d/trzsz.list 70 | sudo apt update 71 | 72 | sudo apt install tsshd 73 | ``` 74 | 75 |
76 | 77 | - Linux 可用 yum 安装 78 | 79 |
sudo yum install tsshd 80 | 81 | - 国内推荐使用 [wlnmp](https://www.wlnmp.com/install) 源,安装 tsshd 只需要添加 wlnmp 源( 配置 epel 源不是必须的 ): 82 | 83 | ```sh 84 | curl -fsSL "https://sh.wlnmp.com/wlnmp.sh" | bash 85 | 86 | sudo yum install tsshd 87 | ``` 88 | 89 | - 也可使用 [gemfury](https://gemfury.com/) 源( 只要网络通,所有操作系统通用 ) 90 | 91 | ```sh 92 | echo '[trzsz] 93 | name=Trzsz Repo 94 | baseurl=https://yum.fury.io/trzsz/ 95 | enabled=1 96 | gpgcheck=0' | sudo tee /etc/yum.repos.d/trzsz.repo 97 | 98 | sudo yum install tsshd 99 | ``` 100 | 101 |
102 | 103 | - ArchLinux 可用 [yay](https://github.com/Jguer/yay) 安装 104 | 105 |
yay -S tsshd 106 | 107 | ```sh 108 | yay -Syu 109 | yay -S tsshd 110 | ``` 111 | 112 |
113 | 114 | - 用 Go 直接安装( 要求 go 1.24 以上 ) 115 | 116 |
go install github.com/trzsz/tsshd/cmd/tsshd@latest 117 | 118 | ```sh 119 | go install github.com/trzsz/tsshd/cmd/tsshd@latest 120 | ``` 121 | 122 | 安装后,`tsshd` 程序一般位于 `~/go/bin/` 目录下( Windows 一般在 `C:\Users\your_name\go\bin\` )。 123 | 124 |
125 | 126 | - 用 Go 自己编译( 要求 go 1.24 以上 ) 127 | 128 |
sudo make install 129 | 130 | ```sh 131 | git clone --depth 1 https://github.com/trzsz/tsshd.git 132 | cd tsshd 133 | make 134 | sudo make install 135 | ``` 136 | 137 |
138 | 139 | - 可从 [GitHub Releases](https://github.com/trzsz/tsshd/releases) 中下载,国内可从 [Gitee 发行版](https://gitee.com/trzsz/tsshd/releases) 中下载,解压并加到 `PATH` 环境变量中。 140 | 141 | ## 联系方式 142 | 143 | 有什么问题可以发邮件给作者 ,也可以提 [Issues](https://github.com/trzsz/tsshd/issues) 。欢迎加入 QQ 群:318578930。 144 | 145 | ## 赞助打赏 146 | 147 | [❤️ 赞助 trzsz ❤️](https://github.com/trzsz),请作者喝杯咖啡 ☕ ? 谢谢您们的支持! 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tsshd 2 | 3 | [![MIT License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://choosealicense.com/licenses/mit/) 4 | [![GitHub Release](https://img.shields.io/github/v/release/trzsz/tsshd)](https://github.com/trzsz/tsshd/releases) 5 | [![中文文档](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-%E6%96%87%E6%A1%A3-blue?style=flat)](https://github.com/trzsz/tsshd/blob/main/README.cn.md) 6 | 7 | The `tsshd` works like `mosh-server`, while the [`tssh --udp`](https://github.com/trzsz/trzsz-ssh) works like [`mosh`](https://github.com/mobile-shell/mosh). 8 | 9 | ## Advantages 10 | 11 | - Low Latency ( based on [QUIC](https://github.com/quic-go/quic-go) / [KCP](https://github.com/xtaci/kcp-go) ) 12 | 13 | - Port Forwarding ( same as openssh, includes ssh agent forwarding and X11 forwarding ) 14 | 15 | - Connection Migration ( supports network switching and reconnection ) 16 | 17 | ## How to use 18 | 19 | 1. Install [tssh](https://github.com/trzsz/trzsz-ssh) on the client ( the user's machine ). 20 | 21 | 2. Install [tsshd](https://github.com/trzsz/tsshd) on the server ( the remote host ). 22 | 23 | 3. Use `tssh --udp` to login to the server. Configure as follows to omit `--udp`: 24 | 25 | ``` 26 | Host xxx 27 | #!! UdpMode yes 28 | #!! TsshdPath ~/go/bin/tsshd 29 | ``` 30 | 31 | ## How it works 32 | 33 | - The `tssh` plays the role of `ssh` on the client side, and the `tsshd` plays the role of `sshd` on the server side. 34 | 35 | - The `tssh` will first login to the server normally as an ssh client, and then run a new `tsshd` process on the server. 36 | 37 | - 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. 38 | 39 | - The `tsshd` supports `QUIC` protocol and `KCP` protocol (the default is `QUIC`), which can be specified on the command line (such as `-oUdpMode=KCP`), or configured as follows: 40 | 41 | ``` 42 | Host xxx 43 | #!! UdpMode KCP 44 | ``` 45 | 46 | ## Installation 47 | 48 | - Install with apt on Ubuntu 49 | 50 |
sudo apt install tsshd 51 | 52 | ```sh 53 | sudo apt update && sudo apt install software-properties-common 54 | sudo add-apt-repository ppa:trzsz/ppa && sudo apt update 55 | 56 | sudo apt install tsshd 57 | ``` 58 | 59 |
60 | 61 | - Install with apt on Debian 62 | 63 |
sudo apt install tsshd 64 | 65 | ```sh 66 | sudo apt install curl gpg 67 | curl -s 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x7074ce75da7cc691c1ae1a7c7e51d1ad956055ca' \ 68 | | gpg --dearmor -o /usr/share/keyrings/trzsz.gpg 69 | echo 'deb [signed-by=/usr/share/keyrings/trzsz.gpg] https://ppa.launchpadcontent.net/trzsz/ppa/ubuntu jammy main' \ 70 | | sudo tee /etc/apt/sources.list.d/trzsz.list 71 | sudo apt update 72 | 73 | sudo apt install tsshd 74 | ``` 75 | 76 |
77 | 78 | - Install with yum on Linux 79 | 80 |
sudo yum install tsshd 81 | 82 | - Install with [gemfury](https://gemfury.com/) repository. 83 | 84 | ```sh 85 | echo '[trzsz] 86 | name=Trzsz Repo 87 | baseurl=https://yum.fury.io/trzsz/ 88 | enabled=1 89 | gpgcheck=0' | sudo tee /etc/yum.repos.d/trzsz.repo 90 | 91 | sudo yum install tsshd 92 | ``` 93 | 94 | - Install with [wlnmp](https://www.wlnmp.com/install) repository. It's not necessary to configure the epel repository for tsshd. 95 | 96 | ```sh 97 | curl -fsSL "https://sh.wlnmp.com/wlnmp.sh" | bash 98 | 99 | sudo yum install tsshd 100 | ``` 101 | 102 |
103 | 104 | - Install with [yay](https://github.com/Jguer/yay) on ArchLinux 105 | 106 |
yay -S tsshd 107 | 108 | ```sh 109 | yay -Syu 110 | yay -S tsshd 111 | ``` 112 | 113 |
114 | 115 | - Install with Go ( Requires go 1.24 or later ) 116 | 117 |
go install github.com/trzsz/tsshd/cmd/tsshd@latest 118 | 119 | ```sh 120 | go install github.com/trzsz/tsshd/cmd/tsshd@latest 121 | ``` 122 | 123 | The binaries are usually located in ~/go/bin/ ( C:\Users\your_name\go\bin\ on Windows ). 124 | 125 |
126 | 127 | - Build from source ( Requires go 1.24 or later ) 128 | 129 |
sudo make install 130 | 131 | ```sh 132 | git clone --depth 1 https://github.com/trzsz/tsshd.git 133 | cd tsshd 134 | make 135 | sudo make install 136 | ``` 137 | 138 |
139 | 140 | - Download from the [GitHub Releases](https://github.com/trzsz/tsshd/releases), unzip and add to `PATH` environment. 141 | 142 | ## Contact 143 | 144 | Feel free to email the author , or create an [issue](https://github.com/trzsz/tsshd/issues). Welcome to join the QQ group: 318578930. 145 | 146 | ## Sponsor 147 | 148 | [❤️ Sponsor trzsz ❤️](https://github.com/trzsz), buy the author a drink 🍺 ? Thank you for your support! 149 | -------------------------------------------------------------------------------- /cmd/tsshd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 The Trzsz SSH Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package main 26 | 27 | import ( 28 | "os" 29 | 30 | "github.com/trzsz/tsshd/tsshd" 31 | ) 32 | 33 | func main() { 34 | os.Exit(tsshd.TsshdMain()) 35 | } 36 | -------------------------------------------------------------------------------- /debian/README: -------------------------------------------------------------------------------- 1 | The Debian Package tsshd 2 | ---------------------------- 3 | 4 | The tsshd works like mosh-server, while the 'tssh --udp' works like mosh. 5 | 6 | -- Lonny Wong Sun, 30 Jun 2024 17:38:13 +0800 7 | -------------------------------------------------------------------------------- /debian/README.Debian: -------------------------------------------------------------------------------- 1 | tsshd for Debian 2 | --------------- 3 | 4 | The tsshd works like mosh-server, while the 'tssh --udp' works like mosh. 5 | 6 | -- Lonny Wong Sun, 30 Jun 2024 17:38:13 +0800 7 | -------------------------------------------------------------------------------- /debian/README.source: -------------------------------------------------------------------------------- 1 | tsshd for Debian 2 | --------------- 3 | 4 | tsshd for Debian is developed in Go. 5 | * Install go and make 6 | * make && make install 7 | 8 | -- Lonny Wong Sun, 30 Jun 2024 17:38:13 +0800 9 | 10 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | tsshd (0.1.3) trusty; urgency=medium 2 | 3 | * tsshd v0.1.3 4 | 5 | -- Lonny Wong Sun, 18 Aug 2024 16:55:38 +0800 6 | 7 | tsshd (0.1.2) trusty; urgency=medium 8 | 9 | * tsshd v0.1.2 10 | 11 | -- Lonny Wong Sun, 07 Jul 2024 10:27:08 +0800 12 | 13 | tsshd (0.1.1) trusty; urgency=medium 14 | 15 | * tsshd v0.1.1 16 | 17 | -- Lonny Wong Sun, 30 Jun 2024 17:38:13 +0800 18 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: tsshd 2 | Section: utils 3 | Priority: optional 4 | Maintainer: Lonny Wong 5 | Build-Depends: debhelper (>=9), 6 | golang-1.21-go 7 | Standards-Version: 3.9.6 8 | Homepage: https://trzsz.github.io/ssh 9 | Vcs-Browser: https://github.com/trzsz/tsshd 10 | Vcs-Git: https://github.com/trzsz/tsshd.git 11 | 12 | Package: tsshd 13 | Architecture: any 14 | Depends: ${shlibs:Depends}, ${misc:Depends} 15 | Description: The tsshd works like mosh-server, while the 'tssh --udp' works like mosh. 16 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: tsshd 3 | Upstream-Contact: Lonny Wong 4 | Source: https://github.com/trzsz/tsshd 5 | 6 | Files: * 7 | Copyright: 2024 The Trzsz SSH Authors. 8 | License: MIT 9 | Permission is hereby granted, free of charge, to any person obtaining a 10 | copy of this software and associated documentation files (the "Software"), 11 | to deal in the Software without restriction, including without limitation 12 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 13 | and/or sell copies of the Software, and to permit persons to whom the 14 | Software is furnished to do so, subject to the following conditions: 15 | . 16 | The above copyright notice and this permission notice shall be included 17 | in all copies or substantial portions of the Software. 18 | . 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 22 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 24 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 25 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | export CGO_ENABLED=0 3 | export GOCACHE=/tmp/.cache/go-build 4 | 5 | %: 6 | PATH="/usr/lib/go-1.21/bin:${PATH}" dh $@ 7 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /debian/tsshd-docs.docs: -------------------------------------------------------------------------------- 1 | README.source 2 | README 3 | README.Debian 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/trzsz/tsshd 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/UserExistsError/conpty v0.1.4 7 | github.com/alessio/shellescape v1.4.2 8 | github.com/creack/pty v1.1.24 9 | github.com/quic-go/quic-go v0.50.1-0.20250419021753-b645ce35a210 10 | github.com/trzsz/go-arg v1.5.4 11 | github.com/xtaci/kcp-go/v5 v5.6.20 12 | github.com/xtaci/smux v1.5.34 13 | golang.org/x/crypto v0.37.0 14 | golang.org/x/sys v0.32.0 15 | ) 16 | 17 | require ( 18 | github.com/alexflint/go-scalar v1.2.0 // indirect 19 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 20 | github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 // indirect 21 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 22 | github.com/klauspost/reedsolomon v1.12.4 // indirect 23 | github.com/onsi/ginkgo/v2 v2.23.4 // indirect 24 | github.com/pkg/errors v0.9.1 // indirect 25 | github.com/templexxx/cpu v0.1.1 // indirect 26 | github.com/templexxx/xorsimd v0.4.3 // indirect 27 | github.com/tjfoc/gmsm v1.4.1 // indirect 28 | go.uber.org/automaxprocs v1.6.0 // indirect 29 | go.uber.org/mock v0.5.1 // indirect 30 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect 31 | golang.org/x/mod v0.24.0 // indirect 32 | golang.org/x/net v0.39.0 // indirect 33 | golang.org/x/sync v0.13.0 // indirect 34 | golang.org/x/tools v0.32.0 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/UserExistsError/conpty v0.1.4 h1:+3FhJhiqhyEJa+K5qaK3/w6w+sN3Nh9O9VbJyBS02to= 4 | github.com/UserExistsError/conpty v0.1.4/go.mod h1:PDglKIkX3O/2xVk0MV9a6bCWxRmPVfxqZoTG/5sSd9I= 5 | github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= 6 | github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= 7 | github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= 8 | github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= 9 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 10 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 11 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 12 | github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 13 | github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 17 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 18 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 19 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 20 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 21 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 22 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 23 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 24 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 25 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 28 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 29 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 30 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 31 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 32 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 33 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 34 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 35 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 36 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 37 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 38 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 39 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 40 | github.com/google/pprof v0.0.0-20250418163039-24c5476c6587 h1:b/8HpQhvKLSNzH5oTXN2WkNcMl6YB5K3FRbb+i+Ml34= 41 | github.com/google/pprof v0.0.0-20250418163039-24c5476c6587/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 42 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 43 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 44 | github.com/klauspost/reedsolomon v1.12.4 h1:5aDr3ZGoJbgu/8+j45KtUJxzYm8k08JGtB9Wx1VQ4OA= 45 | github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU= 46 | github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= 47 | github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= 48 | github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= 49 | github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 50 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 51 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 55 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 56 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 57 | github.com/quic-go/quic-go v0.50.1-0.20250419021753-b645ce35a210 h1:5EYqmu8zcwW+WR7pLFJ414szd+LElIZFpcQX0ID76nw= 58 | github.com/quic-go/quic-go v0.50.1-0.20250419021753-b645ce35a210/go.mod h1:xndMnj/3zN3Mjm5+gpFJTVp4oiuROyNIdRsqVdZlfCQ= 59 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 60 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 61 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 62 | github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI= 63 | github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= 64 | github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU= 65 | github.com/templexxx/xorsimd v0.4.3/go.mod h1:oZQcD6RFDisW2Am58dSAGwwL6rHjbzrlu25VDqfWkQg= 66 | github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= 67 | github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= 68 | github.com/trzsz/go-arg v1.5.4 h1:8cuwV8F1UvlKRXJcdLG1cSZNGXkRped+U1rz17M7Kac= 69 | github.com/trzsz/go-arg v1.5.4/go.mod h1:IC6Z/FiVH7uYvcbp1/gJhDYCFPS/GkL0APYakVvgY4I= 70 | github.com/xtaci/kcp-go/v5 v5.6.20 h1:eoZKuVCjU3wVjoiwZyCwXeuO84na/DbBFvpPdPG9NvA= 71 | github.com/xtaci/kcp-go/v5 v5.6.20/go.mod h1:pASZrdycJanBE9aFNhA9UK5cTDc1p27+5s4Dw3RsH1I= 72 | github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= 73 | github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= 74 | github.com/xtaci/smux v1.5.34 h1:OUA9JaDFHJDT8ZT3ebwLWPAgEfE6sWo2LaTy3anXqwg= 75 | github.com/xtaci/smux v1.5.34/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= 76 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 77 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 78 | go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= 79 | go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= 80 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 81 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 82 | golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 83 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 84 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 85 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 86 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= 87 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 88 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 89 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 90 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 91 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 92 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 93 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 94 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 95 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 96 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 97 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 98 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 99 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 100 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 101 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 102 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 103 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 106 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 107 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 108 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 109 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 112 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 113 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 114 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 115 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 116 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 117 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 118 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 119 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 120 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 121 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 122 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 123 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 124 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 125 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 126 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 127 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 128 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 129 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 130 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 131 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 132 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 133 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 134 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 135 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 136 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 137 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 138 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 139 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 140 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 141 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 142 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 143 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 144 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 145 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 146 | -------------------------------------------------------------------------------- /tsshd/bus.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 The Trzsz SSH Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package tsshd 26 | 27 | import ( 28 | "fmt" 29 | "net" 30 | "sync" 31 | "sync/atomic" 32 | "time" 33 | ) 34 | 35 | var serving atomic.Bool 36 | 37 | var busMutex sync.Mutex 38 | 39 | var busStream atomic.Pointer[net.Conn] 40 | 41 | var lastAliveTime atomic.Pointer[time.Time] 42 | 43 | func sendBusCommand(command string) error { 44 | busMutex.Lock() 45 | defer busMutex.Unlock() 46 | stream := busStream.Load() 47 | if stream == nil { 48 | return fmt.Errorf("bus stream is nil") 49 | } 50 | return SendCommand(*stream, command) 51 | } 52 | 53 | func sendBusMessage(command string, msg any) error { 54 | busMutex.Lock() 55 | defer busMutex.Unlock() 56 | stream := busStream.Load() 57 | if stream == nil { 58 | return fmt.Errorf("bus stream is nil") 59 | } 60 | if err := SendCommand(*stream, command); err != nil { 61 | return err 62 | } 63 | return SendMessage(*stream, msg) 64 | } 65 | 66 | func trySendErrorMessage(format string, a ...any) { 67 | _ = sendBusMessage("error", ErrorMessage{fmt.Sprintf(format, a...)}) 68 | } 69 | 70 | func handleBusEvent(stream net.Conn) { 71 | var msg BusMessage 72 | if err := RecvMessage(stream, &msg); err != nil { 73 | SendError(stream, fmt.Errorf("recv bus message failed: %v", err)) 74 | return 75 | } 76 | 77 | busMutex.Lock() 78 | 79 | // only one bus 80 | if !busStream.CompareAndSwap(nil, &stream) { 81 | busMutex.Unlock() 82 | SendError(stream, fmt.Errorf("bus has been initialized")) 83 | return 84 | } 85 | 86 | if err := SendSuccess(stream); err != nil { // ack ok 87 | busMutex.Unlock() 88 | trySendErrorMessage("bus ack ok failed: %v", err) 89 | return 90 | } 91 | 92 | busMutex.Unlock() 93 | 94 | serving.Store(true) 95 | 96 | if msg.Timeout > 0 { 97 | now := time.Now() 98 | lastAliveTime.Store(&now) 99 | go keepAlive(msg.Timeout, msg.Interval) 100 | } 101 | 102 | for { 103 | command, err := RecvCommand(stream) 104 | if err != nil { 105 | trySendErrorMessage("recv bus command failed: %v", err) 106 | return 107 | } 108 | 109 | switch command { 110 | case "resize": 111 | err = handleResizeEvent(stream) 112 | case "close": 113 | exitChan <- 0 114 | return 115 | case "alive": 116 | now := time.Now() 117 | lastAliveTime.Store(&now) 118 | default: 119 | err = handleUnknownEvent(stream) 120 | } 121 | if err != nil { 122 | trySendErrorMessage("handle bus command [%s] failed: %v", command, err) 123 | } 124 | } 125 | } 126 | 127 | func handleUnknownEvent(stream net.Conn) error { 128 | var msg struct{} 129 | if err := RecvMessage(stream, &msg); err != nil { 130 | return fmt.Errorf("recv unknown message failed: %v", err) 131 | } 132 | return fmt.Errorf("unknown command") 133 | } 134 | 135 | func keepAlive(totalTimeout time.Duration, intervalTimeout time.Duration) { 136 | if intervalTimeout == 0 { 137 | intervalTimeout = min(totalTimeout/10, 10*time.Second) 138 | } 139 | go func() { 140 | for { 141 | _ = sendBusCommand("alive") 142 | time.Sleep(intervalTimeout) 143 | } 144 | }() 145 | for { 146 | if t := lastAliveTime.Load(); t != nil && time.Since(*t) > totalTimeout { 147 | trySendErrorMessage("tsshd keep alive timeout") 148 | exitChan <- 2 149 | return 150 | } 151 | time.Sleep(intervalTimeout) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /tsshd/forward.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 The Trzsz SSH Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package tsshd 26 | 27 | import ( 28 | "fmt" 29 | "io" 30 | "net" 31 | "sync" 32 | "sync/atomic" 33 | "time" 34 | ) 35 | 36 | type closeWriter interface { 37 | CloseWrite() error 38 | } 39 | 40 | var acceptMutex sync.Mutex 41 | var acceptID atomic.Uint64 42 | var acceptMap = make(map[uint64]net.Conn) 43 | 44 | func handleDialEvent(stream net.Conn) { 45 | var msg DialMessage 46 | if err := RecvMessage(stream, &msg); err != nil { 47 | SendError(stream, fmt.Errorf("recv dial message failed: %v", err)) 48 | return 49 | } 50 | 51 | var err error 52 | var conn net.Conn 53 | if msg.Timeout > 0 { 54 | conn, err = net.DialTimeout(msg.Network, msg.Addr, msg.Timeout) 55 | } else { 56 | conn, err = net.Dial(msg.Network, msg.Addr) 57 | } 58 | if err != nil { 59 | SendError(stream, fmt.Errorf("dial %s [%s] failed: %v", msg.Network, msg.Addr, err)) 60 | return 61 | } 62 | 63 | defer conn.Close() 64 | 65 | if err := SendSuccess(stream); err != nil { // ack ok 66 | trySendErrorMessage("dial ack ok failed: %v", err) 67 | return 68 | } 69 | 70 | forwardConnection(stream, conn) 71 | } 72 | 73 | func handleListenEvent(stream net.Conn) { 74 | var msg ListenMessage 75 | if err := RecvMessage(stream, &msg); err != nil { 76 | SendError(stream, fmt.Errorf("recv listen message failed: %v", err)) 77 | return 78 | } 79 | 80 | listener, err := net.Listen(msg.Network, msg.Addr) 81 | if err != nil { 82 | SendError(stream, fmt.Errorf("listen on %s [%s] failed: %v", msg.Network, msg.Addr, err)) 83 | return 84 | } 85 | 86 | defer listener.Close() 87 | 88 | if err := SendSuccess(stream); err != nil { // ack ok 89 | trySendErrorMessage("listen ack ok failed: %v", err) 90 | return 91 | } 92 | 93 | for { 94 | conn, err := listener.Accept() 95 | if err == io.EOF { 96 | break 97 | } 98 | if err != nil { 99 | trySendErrorMessage("listener %s [%s] accept failed: %v", msg.Network, msg.Addr, err) 100 | continue 101 | } 102 | id := addAcceptConn(conn) 103 | if err := SendMessage(stream, AcceptMessage{id}); err != nil { 104 | if conn := getAcceptConn(id); conn != nil { 105 | conn.Close() 106 | } 107 | trySendErrorMessage("send accept message failed: %v", err) 108 | return 109 | } 110 | } 111 | } 112 | 113 | func handleAcceptEvent(stream net.Conn) { 114 | var msg AcceptMessage 115 | if err := RecvMessage(stream, &msg); err != nil { 116 | SendError(stream, fmt.Errorf("recv accept message failed: %v", err)) 117 | return 118 | } 119 | 120 | conn := getAcceptConn(msg.ID) 121 | if conn == nil { 122 | SendError(stream, fmt.Errorf("invalid accept id: %d", msg.ID)) 123 | return 124 | } 125 | 126 | defer conn.Close() 127 | 128 | if err := SendSuccess(stream); err != nil { // ack ok 129 | trySendErrorMessage("accept ack ok failed: %v", err) 130 | return 131 | } 132 | 133 | forwardConnection(stream, conn) 134 | } 135 | 136 | func addAcceptConn(conn net.Conn) uint64 { 137 | acceptMutex.Lock() 138 | defer acceptMutex.Unlock() 139 | id := acceptID.Add(1) - 1 140 | acceptMap[id] = conn 141 | return id 142 | } 143 | 144 | func getAcceptConn(id uint64) net.Conn { 145 | acceptMutex.Lock() 146 | defer acceptMutex.Unlock() 147 | if conn, ok := acceptMap[id]; ok { 148 | delete(acceptMap, id) 149 | return conn 150 | } 151 | return nil 152 | } 153 | 154 | func forwardConnection(stream net.Conn, conn net.Conn) { 155 | var wg sync.WaitGroup 156 | wg.Add(2) 157 | go func() { 158 | _, _ = io.Copy(conn, stream) 159 | if cw, ok := conn.(closeWriter); ok { 160 | _ = cw.CloseWrite() 161 | } else { 162 | // close the entire stream since there is no half-close 163 | time.Sleep(200 * time.Millisecond) 164 | _ = conn.Close() 165 | } 166 | wg.Done() 167 | }() 168 | go func() { 169 | _, _ = io.Copy(stream, conn) 170 | if cw, ok := stream.(closeWriter); ok { 171 | _ = cw.CloseWrite() 172 | } else { 173 | // close the entire stream since there is no half-close 174 | time.Sleep(200 * time.Millisecond) 175 | _ = stream.Close() 176 | } 177 | wg.Done() 178 | }() 179 | wg.Wait() 180 | } 181 | -------------------------------------------------------------------------------- /tsshd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 The Trzsz SSH Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package tsshd 26 | 27 | import ( 28 | "fmt" 29 | "io" 30 | "os" 31 | "os/exec" 32 | "os/signal" 33 | "syscall" 34 | "time" 35 | 36 | "github.com/trzsz/go-arg" 37 | ) 38 | 39 | const kTsshdVersion = "0.1.3" 40 | 41 | var exitChan = make(chan int, 1) 42 | 43 | type tsshdArgs struct { 44 | KCP bool `arg:"--kcp" help:"KCP protocol (default is QUIC protocol)"` 45 | Port string `arg:"--port" placeholder:"low-high" help:"UDP port range that the tsshd listens on"` 46 | } 47 | 48 | func (tsshdArgs) Description() string { 49 | return "tsshd works with `tssh --udp`, just like mosh-server.\n" 50 | } 51 | 52 | func (tsshdArgs) Version() string { 53 | return fmt.Sprintf("trzsz sshd %s", kTsshdVersion) 54 | } 55 | 56 | func background() (bool, io.ReadCloser, error) { 57 | if v := os.Getenv("TRZSZ-SSHD-BACKGROUND"); v == "TRUE" { 58 | return false, nil, nil 59 | } 60 | cmd := exec.Command(os.Args[0], os.Args[1:]...) 61 | cmd.Stderr = os.Stderr 62 | cmd.Env = append(os.Environ(), "TRZSZ-SSHD-BACKGROUND=TRUE") 63 | cmd.SysProcAttr = getSysProcAttr() 64 | stdout, err := cmd.StdoutPipe() 65 | if err != nil { 66 | return true, nil, err 67 | } 68 | if err := cmd.Start(); err != nil { 69 | return true, nil, err 70 | } 71 | return true, stdout, nil 72 | } 73 | 74 | var onExitFuncs []func() 75 | 76 | func cleanupOnExit() { 77 | for i := len(onExitFuncs) - 1; i >= 0; i-- { 78 | onExitFuncs[i]() 79 | } 80 | } 81 | 82 | // TsshdMain is the main function of `tsshd` binary. 83 | func TsshdMain() int { 84 | var args tsshdArgs 85 | arg.MustParse(&args) 86 | 87 | parent, stdout, err := background() 88 | if err != nil { 89 | fmt.Fprintf(os.Stderr, "run in background failed: %v\n", err) 90 | return 1 91 | } 92 | 93 | if parent { 94 | defer stdout.Close() 95 | if _, err := io.Copy(os.Stdout, stdout); err != nil { 96 | fmt.Fprintf(os.Stderr, "copy stdout failed: %v\n", err) 97 | return 2 98 | } 99 | return 0 100 | } 101 | 102 | // cleanup on exit 103 | defer cleanupOnExit() 104 | 105 | // handle exit signals 106 | handleExitSignals() 107 | 108 | kcpListener, quicListener, err := initServer(&args) 109 | if err != nil { 110 | fmt.Println(err) 111 | os.Stdout.Close() 112 | return 3 113 | } 114 | 115 | os.Stdout.Close() 116 | 117 | if kcpListener != nil { 118 | defer kcpListener.Close() 119 | go serveKCP(kcpListener) 120 | } 121 | if quicListener != nil { 122 | defer quicListener.Close() 123 | go serveQUIC(quicListener) 124 | } 125 | 126 | go func() { 127 | // should be connected within 20 seconds 128 | time.Sleep(20 * time.Second) 129 | if !serving.Load() { 130 | exitChan <- 1 131 | } 132 | }() 133 | 134 | return <-exitChan 135 | } 136 | 137 | func handleExitSignals() { 138 | sigChan := make(chan os.Signal, 1) 139 | signal.Notify(sigChan, 140 | syscall.SIGTERM, // Default signal for the kill command 141 | syscall.SIGINT, // Ctrl+C signal 142 | syscall.SIGHUP, // Terminal closed (System reboot/shutdown) 143 | ) 144 | 145 | go func() { 146 | <-sigChan 147 | trySendErrorMessage("tsshd has been terminated") 148 | closeAllSessions() 149 | }() 150 | } 151 | -------------------------------------------------------------------------------- /tsshd/proto.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 The Trzsz SSH Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package tsshd 26 | 27 | import ( 28 | "context" 29 | "crypto/sha1" 30 | "crypto/tls" 31 | "crypto/x509" 32 | "encoding/binary" 33 | "encoding/hex" 34 | "encoding/json" 35 | "fmt" 36 | "io" 37 | "net" 38 | "strings" 39 | "time" 40 | 41 | "github.com/quic-go/quic-go" 42 | "github.com/xtaci/kcp-go/v5" 43 | "github.com/xtaci/smux" 44 | "golang.org/x/crypto/pbkdf2" 45 | ) 46 | 47 | const kNoErrorMsg = "_TSSHD_NO_ERROR_" 48 | 49 | type ServerInfo struct { 50 | Ver string 51 | Port int 52 | Mode string 53 | Pass string 54 | Salt string 55 | ServerCert string 56 | ClientCert string 57 | ClientKey string 58 | } 59 | 60 | type ErrorMessage struct { 61 | Msg string 62 | } 63 | 64 | type BusMessage struct { 65 | Timeout time.Duration 66 | Interval time.Duration 67 | } 68 | 69 | type X11Request struct { 70 | ChannelType string 71 | SingleConnection bool 72 | AuthProtocol string 73 | AuthCookie string 74 | ScreenNumber uint32 75 | } 76 | 77 | type AgentRequest struct { 78 | ChannelType string 79 | } 80 | 81 | type StartMessage struct { 82 | ID uint64 83 | Pty bool 84 | Shell bool 85 | Name string 86 | Args []string 87 | Cols int 88 | Rows int 89 | Envs map[string]string 90 | X11 *X11Request 91 | Agent *AgentRequest 92 | } 93 | 94 | type ExitMessage struct { 95 | ID uint64 96 | ExitCode int 97 | } 98 | 99 | type ResizeMessage struct { 100 | ID uint64 101 | Cols int 102 | Rows int 103 | } 104 | 105 | type StderrMessage struct { 106 | ID uint64 107 | } 108 | 109 | type ChannelMessage struct { 110 | ChannelType string 111 | ID uint64 112 | } 113 | 114 | type DialMessage struct { 115 | Network string 116 | Addr string 117 | Timeout time.Duration 118 | } 119 | 120 | type ListenMessage struct { 121 | Network string 122 | Addr string 123 | } 124 | 125 | type AcceptMessage struct { 126 | ID uint64 127 | } 128 | 129 | func writeAll(dst io.Writer, data []byte) error { 130 | m := 0 131 | l := len(data) 132 | for m < l { 133 | n, err := dst.Write(data[m:]) 134 | if err != nil { 135 | return err 136 | } 137 | m += n 138 | } 139 | return nil 140 | } 141 | 142 | func SendCommand(stream net.Conn, command string) error { 143 | if len(command) == 0 { 144 | return fmt.Errorf("send command is empty") 145 | } 146 | if len(command) > 255 { 147 | return fmt.Errorf("send command too long: %s", command) 148 | } 149 | buffer := make([]byte, len(command)+1) 150 | buffer[0] = uint8(len(command)) 151 | copy(buffer[1:], []byte(command)) 152 | if err := writeAll(stream, buffer); err != nil { 153 | return fmt.Errorf("send command write buffer failed: %v", err) 154 | } 155 | return nil 156 | } 157 | 158 | func RecvCommand(stream net.Conn) (string, error) { 159 | length := make([]byte, 1) 160 | if _, err := stream.Read(length); err != nil { 161 | return "", fmt.Errorf("recv command read length failed: %v", err) 162 | } 163 | command := make([]byte, length[0]) 164 | if _, err := io.ReadFull(stream, command); err != nil { 165 | return "", fmt.Errorf("recv command read buffer failed: %v", err) 166 | } 167 | return string(command), nil 168 | } 169 | 170 | func SendMessage(stream net.Conn, msg any) error { 171 | msgBuf, err := json.Marshal(msg) 172 | if err != nil { 173 | return fmt.Errorf("send message marshal failed: %v", err) 174 | } 175 | buffer := make([]byte, len(msgBuf)+4) 176 | binary.BigEndian.PutUint32(buffer, uint32(len(msgBuf))) 177 | copy(buffer[4:], msgBuf) 178 | if err := writeAll(stream, buffer); err != nil { 179 | return fmt.Errorf("send message write buffer failed: %v", err) 180 | } 181 | return nil 182 | } 183 | 184 | func RecvMessage(stream net.Conn, msg any) error { 185 | lenBuf := make([]byte, 4) 186 | if _, err := io.ReadFull(stream, lenBuf); err != nil { 187 | return fmt.Errorf("recv message read length failed: %v", err) 188 | } 189 | msgBuf := make([]byte, binary.BigEndian.Uint32(lenBuf)) 190 | if _, err := io.ReadFull(stream, msgBuf); err != nil { 191 | return fmt.Errorf("recv message read buffer failed: %v", err) 192 | } 193 | if err := json.Unmarshal(msgBuf, msg); err != nil { 194 | return fmt.Errorf("recv message unmarshal failed: %v", err) 195 | } 196 | return nil 197 | } 198 | 199 | func SendError(stream net.Conn, err error) { 200 | if e := SendMessage(stream, ErrorMessage{err.Error()}); e != nil { 201 | trySendErrorMessage("send error [%v] failed: %v", err, e) 202 | } 203 | } 204 | 205 | func SendSuccess(stream net.Conn) error { 206 | return SendMessage(stream, ErrorMessage{kNoErrorMsg}) 207 | } 208 | 209 | func RecvError(stream net.Conn) error { 210 | var errMsg ErrorMessage 211 | if err := RecvMessage(stream, &errMsg); err != nil { 212 | return fmt.Errorf("recv error failed: %v", err) 213 | } 214 | if errMsg.Msg != kNoErrorMsg { 215 | return fmt.Errorf("%s", errMsg.Msg) 216 | } 217 | return nil 218 | } 219 | 220 | type Client interface { 221 | Close() error 222 | Reconnect() error 223 | NewStream() (net.Conn, error) 224 | } 225 | 226 | type kcpClient struct { 227 | session *smux.Session 228 | } 229 | 230 | func (c *kcpClient) Close() error { 231 | return c.session.Close() 232 | } 233 | 234 | func (c *kcpClient) Reconnect() error { 235 | return fmt.Errorf("KCP mode does not support reconnection") 236 | } 237 | 238 | func (c *kcpClient) NewStream() (net.Conn, error) { 239 | stream, err := c.session.OpenStream() 240 | if err != nil { 241 | return nil, fmt.Errorf("kcp smux open stream failed: %v", err) 242 | } 243 | return stream, nil 244 | } 245 | 246 | type quicClient struct { 247 | conn quic.Connection 248 | transport *quic.Transport 249 | } 250 | 251 | func (c *quicClient) Close() error { 252 | err1 := c.conn.CloseWithError(0, "") 253 | err2 := c.transport.Close() 254 | err3 := c.transport.Conn.Close() 255 | if err1 != nil || err2 != nil || err3 != nil { 256 | return fmt.Errorf("close failed: %v, %v, %v", err1, err2, err3) 257 | } 258 | return nil 259 | } 260 | 261 | func (c *quicClient) Reconnect() error { 262 | transport, err := newQuicTransport() 263 | if err != nil { 264 | return fmt.Errorf("new quic transport failed: %v", err) 265 | } 266 | path, err := c.conn.AddPath(transport) 267 | if err != nil { 268 | return fmt.Errorf("quic add path failed: %v", err) 269 | } 270 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 271 | defer cancel() 272 | if err := path.Probe(ctx); err != nil { 273 | return fmt.Errorf("quic path probe failed: %v", err) 274 | } 275 | if err := path.Switch(); err != nil { 276 | return fmt.Errorf("quic path switch failed: %v", err) 277 | } 278 | _ = c.transport.Close() 279 | _ = c.transport.Conn.Close() 280 | c.transport = transport 281 | return nil 282 | } 283 | 284 | func (c *quicClient) NewStream() (net.Conn, error) { 285 | stream, err := c.conn.OpenStreamSync(context.Background()) 286 | if err != nil { 287 | return nil, fmt.Errorf("quic open stream sync failed: %v", err) 288 | } 289 | return &quicStream{stream, c.conn}, err 290 | } 291 | 292 | func NewClient(host string, info *ServerInfo) (Client, error) { 293 | switch info.Mode { 294 | case "": 295 | return nil, fmt.Errorf("Please upgrade tsshd.") 296 | case kModeKCP: 297 | return newKcpClient(host, info) 298 | case kModeQUIC: 299 | return newQuicClient(host, info) 300 | default: 301 | return nil, fmt.Errorf("unknown tsshd mode: %s", info.Mode) 302 | } 303 | } 304 | 305 | func newKcpClient(host string, info *ServerInfo) (Client, error) { 306 | pass, err := hex.DecodeString(info.Pass) 307 | if err != nil { 308 | return nil, fmt.Errorf("decode pass [%s] failed: %v", info.Pass, err) 309 | } 310 | salt, err := hex.DecodeString(info.Salt) 311 | if err != nil { 312 | return nil, fmt.Errorf("decode salt [%s] failed: %v", info.Pass, err) 313 | } 314 | addr := joinHostPort(host, info.Port) 315 | key := pbkdf2.Key(pass, salt, 4096, 32, sha1.New) 316 | block, err := kcp.NewAESBlockCrypt(key) 317 | if err != nil { 318 | return nil, fmt.Errorf("new aes block crypt failed: %v", err) 319 | } 320 | conn, err := kcp.DialWithOptions(addr, block, 10, 3) 321 | if err != nil { 322 | return nil, fmt.Errorf("kcp dial [%s] failed: %v", addr, err) 323 | } 324 | conn.SetNoDelay(1, 10, 2, 1) 325 | session, err := smux.Client(conn, &smuxConfig) 326 | if err != nil { 327 | return nil, fmt.Errorf("kcp smux client failed: %v", err) 328 | } 329 | return &kcpClient{session}, nil 330 | } 331 | 332 | func newQuicClient(host string, info *ServerInfo) (Client, error) { 333 | serverCert, err := hex.DecodeString(info.ServerCert) 334 | if err != nil { 335 | return nil, fmt.Errorf("decode server cert [%s] failed: %v", info.ServerCert, err) 336 | } 337 | clientCert, err := hex.DecodeString(info.ClientCert) 338 | if err != nil { 339 | return nil, fmt.Errorf("decode client cert [%s] failed: %v", info.ClientCert, err) 340 | } 341 | clientKey, err := hex.DecodeString(info.ClientKey) 342 | if err != nil { 343 | return nil, fmt.Errorf("decode client key [%s] failed: %v", info.ClientKey, err) 344 | } 345 | 346 | clientTlsCert, err := tls.X509KeyPair(clientCert, clientKey) 347 | if err != nil { 348 | return nil, fmt.Errorf("x509 key pair failed: %v", err) 349 | } 350 | serverCertPool := x509.NewCertPool() 351 | serverCertPool.AppendCertsFromPEM(serverCert) 352 | tlsConfig := &tls.Config{ 353 | Certificates: []tls.Certificate{clientTlsCert}, 354 | RootCAs: serverCertPool, 355 | ServerName: "tsshd", 356 | } 357 | addr := joinHostPort(host, info.Port) 358 | transport, err := newQuicTransport() 359 | if err != nil { 360 | return nil, fmt.Errorf("new quic transport failed: %v", err) 361 | } 362 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 363 | if err != nil { 364 | return nil, fmt.Errorf("resolve udp addr [%s] failed: %v", addr, err) 365 | } 366 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 367 | defer cancel() 368 | conn, err := transport.Dial(ctx, udpAddr, tlsConfig, &quicConfig) 369 | if err != nil { 370 | return nil, fmt.Errorf("quic transport dail [%s] failed: %v", addr, err) 371 | } 372 | return &quicClient{conn, transport}, nil 373 | } 374 | 375 | func newQuicTransport() (*quic.Transport, error) { 376 | udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) 377 | if err != nil { 378 | return nil, fmt.Errorf("listen udp failed: %v", err) 379 | } 380 | return &quic.Transport{Conn: udpConn}, nil 381 | } 382 | 383 | func joinHostPort(host string, port int) string { 384 | if !strings.HasPrefix(host, "[") && strings.ContainsRune(host, ':') { 385 | return fmt.Sprintf("[%s]:%d", host, port) 386 | } 387 | return fmt.Sprintf("%s:%d", host, port) 388 | } 389 | -------------------------------------------------------------------------------- /tsshd/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 The Trzsz SSH Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package tsshd 26 | 27 | import ( 28 | "crypto/ecdsa" 29 | "crypto/elliptic" 30 | crypto_rand "crypto/rand" 31 | "crypto/sha1" 32 | "crypto/tls" 33 | "crypto/x509" 34 | "encoding/json" 35 | "encoding/pem" 36 | "fmt" 37 | "math/big" 38 | math_rand "math/rand" 39 | "net" 40 | "strconv" 41 | "strings" 42 | "time" 43 | "unicode" 44 | 45 | "github.com/quic-go/quic-go" 46 | "github.com/xtaci/kcp-go/v5" 47 | "golang.org/x/crypto/pbkdf2" 48 | ) 49 | 50 | const ( 51 | kModeKCP = "KCP" 52 | kModeQUIC = "QUIC" 53 | ) 54 | 55 | const ( 56 | kDefaultPortRangeLow = 61001 57 | kDefaultPortRangeHigh = 61999 58 | ) 59 | 60 | var quicConfig = quic.Config{ 61 | HandshakeIdleTimeout: 30 * time.Second, 62 | MaxIdleTimeout: 365 * 24 * time.Hour, 63 | } 64 | 65 | func initServer(args *tsshdArgs) (*kcp.Listener, *quic.Listener, error) { 66 | portRangeLow, portRangeHigh := getPortRange(args) 67 | conn, port, err := listenUdpOnFreePort(portRangeLow, portRangeHigh) 68 | if err != nil { 69 | return nil, nil, err 70 | } 71 | 72 | info := &ServerInfo{ 73 | Ver: kTsshdVersion, 74 | Port: port, 75 | } 76 | 77 | var kcpListener *kcp.Listener 78 | var quicListener *quic.Listener 79 | if args.KCP { 80 | kcpListener, err = listenKCP(conn, info) 81 | } else { 82 | quicListener, err = listenQUIC(conn, info) 83 | } 84 | if err != nil { 85 | return nil, nil, err 86 | } 87 | 88 | infoStr, err := json.Marshal(info) 89 | if err != nil { 90 | if kcpListener != nil { 91 | kcpListener.Close() 92 | } 93 | if quicListener != nil { 94 | quicListener.Close() 95 | } 96 | return nil, nil, fmt.Errorf("json marshal failed: %v\n", err) 97 | } 98 | fmt.Printf("\a%s\r\n", string(infoStr)) 99 | 100 | return kcpListener, quicListener, nil 101 | } 102 | 103 | func getPortRange(args *tsshdArgs) (int, int) { 104 | if args.Port == "" { 105 | return kDefaultPortRangeLow, kDefaultPortRangeHigh 106 | } 107 | ports := strings.FieldsFunc(args.Port, func(c rune) bool { 108 | return unicode.IsSpace(c) || c == ',' || c == '-' 109 | }) 110 | if len(ports) == 1 { 111 | if port, err := strconv.Atoi(ports[0]); err == nil { 112 | return port, port 113 | } 114 | } else if len(ports) == 2 { 115 | port0, err0 := strconv.Atoi(ports[0]) 116 | port1, err1 := strconv.Atoi(ports[1]) 117 | if err0 == nil && err1 == nil { 118 | return port0, port1 119 | } 120 | } 121 | return kDefaultPortRangeLow, kDefaultPortRangeHigh 122 | } 123 | 124 | func listenUdpOnFreePort(low, high int) (*net.UDPConn, int, error) { 125 | if high < low { 126 | return nil, 0, fmt.Errorf("no port in [%d,%d]", low, high) 127 | } 128 | var err error 129 | var conn *net.UDPConn 130 | size := high - low + 1 131 | port := low + math_rand.Intn(size) 132 | for i := 0; i < size; i++ { 133 | if conn, err = listenUdpOnPort(port); err == nil { 134 | return conn, port, nil 135 | } 136 | port++ 137 | if port > high { 138 | port = low 139 | } 140 | } 141 | if err != nil { 142 | return nil, 0, fmt.Errorf("listen udp on [%d,%d] failed: %v", low, high, err) 143 | } 144 | return nil, 0, fmt.Errorf("listen udp on [%d,%d] failed", low, high) 145 | } 146 | 147 | func listenUdpOnPort(port int) (*net.UDPConn, error) { 148 | addr := fmt.Sprintf(":%d", port) 149 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 150 | if err != nil { 151 | return nil, fmt.Errorf("resolve udp addr [%s] failed: %v", addr, err) 152 | } 153 | conn, err := net.ListenUDP("udp", udpAddr) 154 | if err != nil { 155 | return nil, fmt.Errorf("listen udp on [%s] failed: %v", addr, err) 156 | } 157 | return conn, nil 158 | } 159 | 160 | func listenKCP(conn *net.UDPConn, info *ServerInfo) (*kcp.Listener, error) { 161 | pass := make([]byte, 32) 162 | if _, err := crypto_rand.Read(pass); err != nil { 163 | return nil, fmt.Errorf("rand pass failed: %v", err) 164 | } 165 | salt := make([]byte, 32) 166 | if _, err := crypto_rand.Read(salt); err != nil { 167 | return nil, fmt.Errorf("rand salt failed: %v", err) 168 | } 169 | key := pbkdf2.Key(pass, salt, 4096, 32, sha1.New) 170 | 171 | block, err := kcp.NewAESBlockCrypt(key) 172 | if err != nil { 173 | return nil, fmt.Errorf("new aes block crypt failed: %v", err) 174 | } 175 | 176 | listener, err := kcp.ServeConn(block, 10, 3, conn) 177 | if err != nil { 178 | return nil, fmt.Errorf("kcp serve conn failed: %v", err) 179 | } 180 | 181 | info.Mode = kModeKCP 182 | info.Pass = fmt.Sprintf("%x", pass) 183 | info.Salt = fmt.Sprintf("%x", salt) 184 | return listener, nil 185 | } 186 | 187 | func listenQUIC(conn *net.UDPConn, info *ServerInfo) (*quic.Listener, error) { 188 | serverCertPEM, serverKeyPEM, err := generateCertKeyPair() 189 | if err != nil { 190 | return nil, err 191 | } 192 | clientCertPEM, clientKeyPEM, err := generateCertKeyPair() 193 | if err != nil { 194 | return nil, err 195 | } 196 | 197 | serverTlsCert, err := tls.X509KeyPair(serverCertPEM, serverKeyPEM) 198 | if err != nil { 199 | return nil, fmt.Errorf("x509 key pair failed: %v", err) 200 | } 201 | 202 | clientCertPool := x509.NewCertPool() 203 | clientCertPool.AppendCertsFromPEM(clientCertPEM) 204 | tlsConfig := &tls.Config{ 205 | Certificates: []tls.Certificate{serverTlsCert}, 206 | ClientCAs: clientCertPool, 207 | ClientAuth: tls.RequireAndVerifyClientCert, 208 | } 209 | 210 | listener, err := (&quic.Transport{Conn: conn}).Listen(tlsConfig, &quicConfig) 211 | if err != nil { 212 | return nil, fmt.Errorf("quic listen failed: %v", err) 213 | } 214 | 215 | info.Mode = kModeQUIC 216 | info.ServerCert = fmt.Sprintf("%x", serverCertPEM) 217 | info.ClientCert = fmt.Sprintf("%x", clientCertPEM) 218 | info.ClientKey = fmt.Sprintf("%x", clientKeyPEM) 219 | 220 | return listener, nil 221 | } 222 | 223 | func generateCertKeyPair() ([]byte, []byte, error) { 224 | key, err := ecdsa.GenerateKey(elliptic.P256(), crypto_rand.Reader) 225 | if err != nil { 226 | return nil, nil, fmt.Errorf("ecdsa generate key failed: %v", err) 227 | } 228 | now := time.Now() 229 | template := x509.Certificate{ 230 | SerialNumber: big.NewInt(1), 231 | DNSNames: []string{"tsshd"}, 232 | NotBefore: now.AddDate(0, 0, -1), 233 | NotAfter: now.AddDate(1, 0, 0), 234 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 235 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, 236 | } 237 | certDER, err := x509.CreateCertificate(crypto_rand.Reader, &template, &template, &key.PublicKey, key) 238 | if err != nil { 239 | return nil, nil, fmt.Errorf("x509 create certificate failed: %v", err) 240 | } 241 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 242 | keyBytes, err := x509.MarshalECPrivateKey(key) 243 | if err != nil { 244 | return nil, nil, fmt.Errorf("x509 marshal ec private key failed: %v", err) 245 | } 246 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}) 247 | return certPEM, keyPEM, nil 248 | } 249 | -------------------------------------------------------------------------------- /tsshd/service.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 The Trzsz SSH Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package tsshd 26 | 27 | import ( 28 | "context" 29 | "fmt" 30 | "net" 31 | 32 | "github.com/quic-go/quic-go" 33 | "github.com/xtaci/kcp-go/v5" 34 | "github.com/xtaci/smux" 35 | ) 36 | 37 | var smuxConfig = smux.Config{ 38 | Version: 2, 39 | KeepAliveDisabled: true, 40 | MaxFrameSize: 32 * 1024, 41 | MaxStreamBuffer: 64 * 1024, 42 | MaxReceiveBuffer: 4 * 1024 * 1024, 43 | } 44 | 45 | type quicStream struct { 46 | quic.Stream 47 | conn quic.Connection 48 | } 49 | 50 | func (s *quicStream) LocalAddr() net.Addr { 51 | return s.conn.LocalAddr() 52 | } 53 | 54 | func (s *quicStream) RemoteAddr() net.Addr { 55 | return s.conn.RemoteAddr() 56 | } 57 | 58 | func serveKCP(listener *kcp.Listener) { 59 | for { 60 | conn, err := listener.AcceptKCP() 61 | if err != nil { 62 | trySendErrorMessage("kcp accept failed: %v", err) 63 | return 64 | } 65 | go handleKcpConn(conn) 66 | } 67 | } 68 | 69 | func handleKcpConn(conn *kcp.UDPSession) { 70 | defer conn.Close() 71 | 72 | if serving.Load() { 73 | return 74 | } 75 | 76 | conn.SetNoDelay(1, 10, 2, 1) 77 | 78 | session, err := smux.Server(conn, &smuxConfig) 79 | if err != nil { 80 | trySendErrorMessage("kcp smux server failed: %v", err) 81 | return 82 | } 83 | 84 | for { 85 | stream, err := session.AcceptStream() 86 | if err != nil { 87 | trySendErrorMessage("kcp smux accept stream failed: %v", err) 88 | return 89 | } 90 | go handleStream(stream) 91 | } 92 | } 93 | 94 | func serveQUIC(listener *quic.Listener) { 95 | for { 96 | conn, err := listener.Accept(context.Background()) 97 | if err != nil { 98 | trySendErrorMessage("quic accept conn failed: %v", err) 99 | return 100 | } 101 | go handleQuicConn(conn) 102 | } 103 | } 104 | 105 | func handleQuicConn(conn quic.Connection) { 106 | defer func() { 107 | _ = conn.CloseWithError(0, "") 108 | }() 109 | 110 | if serving.Load() { 111 | return 112 | } 113 | 114 | for { 115 | stream, err := conn.AcceptStream(context.Background()) 116 | if err != nil { 117 | trySendErrorMessage("quic accept stream failed: %v", err) 118 | return 119 | } 120 | go handleStream(&quicStream{stream, conn}) 121 | } 122 | } 123 | 124 | func handleStream(stream net.Conn) { 125 | defer stream.Close() 126 | 127 | command, err := RecvCommand(stream) 128 | if err != nil { 129 | SendError(stream, fmt.Errorf("recv stream command failed: %v", err)) 130 | return 131 | } 132 | 133 | var handler func(net.Conn) 134 | 135 | switch command { 136 | case "bus": 137 | handler = handleBusEvent 138 | case "session": 139 | handler = handleSessionEvent 140 | case "stderr": 141 | handler = handleStderrEvent 142 | case "dial": 143 | handler = handleDialEvent 144 | case "listen": 145 | handler = handleListenEvent 146 | case "accept": 147 | handler = handleAcceptEvent 148 | default: 149 | SendError(stream, fmt.Errorf("unknown stream command: %s", command)) 150 | return 151 | } 152 | 153 | if err := SendSuccess(stream); err != nil { // say hello 154 | trySendErrorMessage("tsshd say hello failed: %v", err) 155 | return 156 | } 157 | 158 | handler(stream) 159 | } 160 | -------------------------------------------------------------------------------- /tsshd/session.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 The Trzsz SSH Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package tsshd 26 | 27 | import ( 28 | "fmt" 29 | "io" 30 | "net" 31 | "os" 32 | "os/exec" 33 | "path/filepath" 34 | "regexp" 35 | "runtime" 36 | "strings" 37 | "sync" 38 | "time" 39 | 40 | "github.com/alessio/shellescape" 41 | ) 42 | 43 | type sessionContext struct { 44 | id uint64 45 | cols int 46 | rows int 47 | cmd *exec.Cmd 48 | pty *tsshdPty 49 | wg sync.WaitGroup 50 | stdin io.WriteCloser 51 | stdout io.ReadCloser 52 | stderr io.ReadCloser 53 | started bool 54 | closed bool 55 | } 56 | 57 | type stderrStream struct { 58 | id uint64 59 | wg sync.WaitGroup 60 | stream net.Conn 61 | } 62 | 63 | var sessionMutex sync.Mutex 64 | var sessionMap = make(map[uint64]*sessionContext) 65 | 66 | var stderrMutex sync.Mutex 67 | var stderrMap = make(map[uint64]*stderrStream) 68 | 69 | func (c *sessionContext) StartPty() error { 70 | var err error 71 | c.pty, err = newTsshdPty(c.cmd, c.cols, c.rows) 72 | if err != nil { 73 | return fmt.Errorf("shell pty start failed: %v", err) 74 | } 75 | c.stdin = c.pty.stdin 76 | c.stdout = c.pty.stdout 77 | c.started = true 78 | return nil 79 | } 80 | 81 | func (c *sessionContext) StartCmd() error { 82 | var err error 83 | if c.stdin, err = c.cmd.StdinPipe(); err != nil { 84 | return fmt.Errorf("cmd stdin pipe failed: %v", err) 85 | } 86 | if c.stdout, err = c.cmd.StdoutPipe(); err != nil { 87 | return fmt.Errorf("cmd stdout pipe failed: %v", err) 88 | } 89 | if c.stderr, err = c.cmd.StderrPipe(); err != nil { 90 | return fmt.Errorf("cmd stderr pipe failed: %v", err) 91 | } 92 | if err := c.cmd.Start(); err != nil { 93 | return fmt.Errorf("start cmd %v failed: %v", c.cmd.Args, err) 94 | } 95 | c.started = true 96 | return nil 97 | } 98 | 99 | func (c *sessionContext) forwardIO(stream net.Conn) { 100 | if c.stdin != nil { 101 | go func() { 102 | _, _ = io.Copy(c.stdin, stream) 103 | }() 104 | } 105 | 106 | if c.stdout != nil { 107 | c.wg.Add(1) 108 | go func() { 109 | _, _ = io.Copy(stream, c.stdout) 110 | c.wg.Done() 111 | }() 112 | } 113 | 114 | if c.stderr != nil { 115 | c.wg.Add(1) 116 | go func() { 117 | if stderr, ok := stderrMap[c.id]; ok { 118 | _, _ = io.Copy(stderr.stream, c.stderr) 119 | } else { 120 | _, _ = io.Copy(stream, c.stderr) 121 | } 122 | c.wg.Done() 123 | }() 124 | } 125 | } 126 | 127 | func (c *sessionContext) Wait() { 128 | if c.pty != nil { 129 | _ = c.pty.Wait() 130 | } else { 131 | _ = c.cmd.Wait() 132 | } 133 | c.wg.Wait() 134 | } 135 | 136 | func (c *sessionContext) Close() { 137 | if c.closed { 138 | return 139 | } 140 | c.closed = true 141 | if err := sendBusMessage("exit", ExitMessage{ 142 | ID: c.id, 143 | ExitCode: c.cmd.ProcessState.ExitCode(), 144 | }); err != nil { 145 | trySendErrorMessage("send exit message failed: %v", err) 146 | } 147 | if c.stdin != nil { 148 | c.stdin.Close() 149 | } 150 | if c.stdout != nil { 151 | c.stdout.Close() 152 | } 153 | if c.stderr != nil { 154 | c.stderr.Close() 155 | } 156 | if c.started { 157 | if c.pty != nil { 158 | _ = c.pty.Close() 159 | } else { 160 | _ = c.cmd.Process.Kill() 161 | } 162 | } 163 | sessionMutex.Lock() 164 | defer sessionMutex.Unlock() 165 | delete(sessionMap, c.id) 166 | } 167 | 168 | func (c *sessionContext) SetSize(cols, rows int) error { 169 | if c.pty == nil { 170 | return fmt.Errorf("session %d %v is not pty", c.id, c.cmd.Args) 171 | } 172 | if err := c.pty.Resize(cols, rows); err != nil { 173 | return fmt.Errorf("pty set size failed: %v", err) 174 | } 175 | return nil 176 | } 177 | 178 | func handleSessionEvent(stream net.Conn) { 179 | var msg StartMessage 180 | if err := RecvMessage(stream, &msg); err != nil { 181 | SendError(stream, fmt.Errorf("recv start message failed: %v", err)) 182 | return 183 | } 184 | 185 | handleX11Request(&msg) 186 | 187 | handleAgentRequest(&msg) 188 | 189 | if errStream := getStderrStream(msg.ID); errStream != nil { 190 | defer errStream.Close() 191 | } 192 | 193 | ctx, err := newSessionContext(&msg) 194 | if err != nil { 195 | SendError(stream, err) 196 | return 197 | } 198 | defer ctx.Close() 199 | 200 | if msg.Pty { 201 | err = ctx.StartPty() 202 | } else { 203 | err = ctx.StartCmd() 204 | } 205 | if err != nil { 206 | SendError(stream, err) 207 | return 208 | } 209 | 210 | if err := SendSuccess(stream); err != nil { // ack ok 211 | trySendErrorMessage("session ack ok failed: %v", err) 212 | return 213 | } 214 | 215 | ctx.forwardIO(stream) 216 | 217 | ctx.Wait() 218 | } 219 | 220 | func newSessionContext(msg *StartMessage) (*sessionContext, error) { 221 | cmd, err := getSessionStartCmd(msg) 222 | if err != nil { 223 | return nil, fmt.Errorf("build start command failed: %v", err) 224 | } 225 | 226 | sessionMutex.Lock() 227 | defer sessionMutex.Unlock() 228 | 229 | if ctx, ok := sessionMap[msg.ID]; ok { 230 | return nil, fmt.Errorf("session id %d %v existed", msg.ID, ctx.cmd.Args) 231 | } 232 | 233 | ctx := &sessionContext{ 234 | id: msg.ID, 235 | cmd: cmd, 236 | cols: msg.Cols, 237 | rows: msg.Rows, 238 | } 239 | sessionMap[ctx.id] = ctx 240 | return ctx, nil 241 | } 242 | 243 | func (c *stderrStream) Wait() { 244 | c.wg.Wait() 245 | } 246 | 247 | func (c *stderrStream) Close() { 248 | c.wg.Done() 249 | stderrMutex.Lock() 250 | defer stderrMutex.Unlock() 251 | delete(stderrMap, c.id) 252 | } 253 | 254 | func newStderrStream(id uint64, stream net.Conn) (*stderrStream, error) { 255 | stderrMutex.Lock() 256 | defer stderrMutex.Unlock() 257 | if _, ok := stderrMap[id]; ok { 258 | return nil, fmt.Errorf("session %d stderr already set", id) 259 | } 260 | errStream := &stderrStream{id: id, stream: stream} 261 | errStream.wg.Add(1) 262 | stderrMap[id] = errStream 263 | return errStream, nil 264 | } 265 | 266 | func getStderrStream(id uint64) *stderrStream { 267 | stderrMutex.Lock() 268 | defer stderrMutex.Unlock() 269 | if errStream, ok := stderrMap[id]; ok { 270 | return errStream 271 | } 272 | return nil 273 | } 274 | 275 | func getSessionStartCmd(msg *StartMessage) (*exec.Cmd, error) { 276 | var envs []string 277 | for _, env := range os.Environ() { 278 | pos := strings.IndexRune(env, '=') 279 | if pos <= 0 { 280 | continue 281 | } 282 | name := strings.TrimSpace(env[:pos]) 283 | if _, ok := msg.Envs[name]; !ok { 284 | envs = append(envs, env) 285 | } 286 | } 287 | for key, value := range msg.Envs { 288 | envs = append(envs, fmt.Sprintf("%s=%s", key, value)) 289 | } 290 | 291 | if !msg.Shell { 292 | name := msg.Name 293 | args := msg.Args 294 | wrap := false 295 | if name == "cd" { 296 | wrap = true 297 | } else if _, err := exec.LookPath(name); err != nil { 298 | wrap = true 299 | } else { 300 | for _, arg := range args { 301 | if strings.HasPrefix(arg, "~/") { 302 | wrap = true 303 | break 304 | } 305 | } 306 | } 307 | if wrap { 308 | re := regexp.MustCompile(`\s`) 309 | var buf strings.Builder 310 | buf.WriteString(name) 311 | for _, arg := range args { 312 | buf.WriteByte(' ') 313 | if re.MatchString(arg) { 314 | buf.WriteString(shellescape.Quote(arg)) 315 | } else { 316 | buf.WriteString(arg) 317 | } 318 | } 319 | if runtime.GOOS == "windows" { 320 | name = "cmd" 321 | args = []string{"/c", buf.String()} 322 | } else { 323 | name = "sh" 324 | args = []string{"-c", buf.String()} 325 | } 326 | } 327 | cmd := exec.Command(name, args...) 328 | cmd.Env = envs 329 | return cmd, nil 330 | } 331 | 332 | shell, err := getUserShell() 333 | if err != nil { 334 | return nil, fmt.Errorf("get user shell failed: %v", err) 335 | } 336 | cmd := exec.Command(shell) 337 | if runtime.GOOS != "windows" { 338 | cmd.Args = []string{"-" + filepath.Base(shell)} 339 | } 340 | cmd.Env = envs 341 | return cmd, nil 342 | } 343 | 344 | func handleStderrEvent(stream net.Conn) { 345 | var msg StderrMessage 346 | if err := RecvMessage(stream, &msg); err != nil { 347 | SendError(stream, fmt.Errorf("recv stderr message failed: %v", err)) 348 | return 349 | } 350 | 351 | errStream, err := newStderrStream(msg.ID, stream) 352 | if err != nil { 353 | SendError(stream, err) 354 | return 355 | } 356 | 357 | if err := SendSuccess(stream); err != nil { // ack ok 358 | trySendErrorMessage("stderr ack ok failed: %v", err) 359 | return 360 | } 361 | 362 | errStream.Wait() 363 | } 364 | 365 | func handleResizeEvent(stream net.Conn) error { 366 | var msg ResizeMessage 367 | if err := RecvMessage(stream, &msg); err != nil { 368 | return fmt.Errorf("recv resize message failed: %v", err) 369 | } 370 | if msg.Cols <= 0 || msg.Rows <= 0 { 371 | return fmt.Errorf("resize message invalid: %#v", msg) 372 | } 373 | sessionMutex.Lock() 374 | defer sessionMutex.Unlock() 375 | if ctx, ok := sessionMap[msg.ID]; ok { 376 | return ctx.SetSize(msg.Cols, msg.Rows) 377 | } 378 | return fmt.Errorf("invalid session id: %d", msg.ID) 379 | } 380 | 381 | func handleX11Request(msg *StartMessage) { 382 | if msg.X11 == nil { 383 | return 384 | } 385 | listener, port, err := listenTcpOnFreePort("localhost", 6020, 6999) 386 | if err != nil { 387 | trySendErrorMessage("X11 forwarding listen failed: %v", err) 388 | return 389 | } 390 | onExitFuncs = append(onExitFuncs, func() { 391 | listener.Close() 392 | }) 393 | displayNumber := port - 6000 394 | if msg.X11.AuthProtocol != "" && msg.X11.AuthCookie != "" { 395 | authDisplay := fmt.Sprintf("unix:%d.%d", displayNumber, msg.X11.ScreenNumber) 396 | input := fmt.Sprintf("remove %s\nadd %s %s %s\n", authDisplay, authDisplay, msg.X11.AuthProtocol, msg.X11.AuthCookie) 397 | if err := writeXauthData(input); err == nil { 398 | onExitFuncs = append(onExitFuncs, func() { 399 | _ = writeXauthData(fmt.Sprintf("remove %s\n", authDisplay)) 400 | }) 401 | } 402 | } 403 | go handleChannelAccept(listener, msg.X11.ChannelType) 404 | msg.Envs["DISPLAY"] = fmt.Sprintf("localhost:%d.%d", displayNumber, msg.X11.ScreenNumber) 405 | } 406 | 407 | func listenTcpOnFreePort(host string, low, high int) (net.Listener, int, error) { 408 | var err error 409 | var listener net.Listener 410 | for port := low; port <= high; port++ { 411 | listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", host, port)) 412 | if err == nil { 413 | return listener, port, nil 414 | } 415 | } 416 | if err != nil { 417 | return nil, 0, fmt.Errorf("listen tcp on %s:[%d,%d] failed: %v", host, low, high, err) 418 | } 419 | return nil, 0, fmt.Errorf("listen tcp on %s:[%d,%d] failed", host, low, high) 420 | } 421 | 422 | func writeXauthData(input string) error { 423 | cmd := exec.Command("xauth", "-q", "-") 424 | stdin, err := cmd.StdinPipe() 425 | if err != nil { 426 | return err 427 | } 428 | defer stdin.Close() 429 | if err := cmd.Start(); err != nil { 430 | return err 431 | } 432 | if _, err := stdin.Write([]byte(input)); err != nil { 433 | return err 434 | } 435 | stdin.Close() 436 | done := make(chan struct{}, 1) 437 | go func() { 438 | defer close(done) 439 | _ = cmd.Wait() 440 | done <- struct{}{} 441 | }() 442 | select { 443 | case <-time.After(200 * time.Millisecond): 444 | case <-done: 445 | } 446 | return nil 447 | } 448 | 449 | func handleAgentRequest(msg *StartMessage) { 450 | if msg.Agent == nil { 451 | return 452 | } 453 | tempDir, err := os.MkdirTemp("", "tsshd-") 454 | if err != nil { 455 | trySendErrorMessage("agent forwarding mkdir temp failed: %v", err) 456 | return 457 | } 458 | onExitFuncs = append(onExitFuncs, func() { 459 | _ = os.RemoveAll(tempDir) 460 | }) 461 | agentPath := filepath.Join(tempDir, fmt.Sprintf("agent.%d", os.Getpid())) 462 | listener, err := net.Listen("unix", agentPath) 463 | if err != nil { 464 | trySendErrorMessage("agent forwarding listen on [%s] failed: %v", agentPath, err) 465 | return 466 | } 467 | if err := os.Chmod(agentPath, 0600); err != nil { 468 | trySendErrorMessage("agent forwarding chmod [%s] failed: %v", agentPath, err) 469 | } 470 | onExitFuncs = append(onExitFuncs, func() { 471 | listener.Close() 472 | _ = os.Remove(agentPath) 473 | }) 474 | go handleChannelAccept(listener, msg.Agent.ChannelType) 475 | msg.Envs["SSH_AUTH_SOCK"] = agentPath 476 | } 477 | 478 | func handleChannelAccept(listener net.Listener, channelType string) { 479 | for { 480 | conn, err := listener.Accept() 481 | if err != nil { 482 | trySendErrorMessage("channel accept failed: %v", err) 483 | break 484 | } 485 | go func(conn net.Conn) { 486 | id := addAcceptConn(conn) 487 | if err := sendBusMessage("channel", &ChannelMessage{ChannelType: channelType, ID: id}); err != nil { 488 | trySendErrorMessage("send channel message failed: %v", err) 489 | } 490 | }(conn) 491 | } 492 | } 493 | 494 | func closeAllSessions() { 495 | sessionMutex.Lock() 496 | var sessions []*sessionContext 497 | for _, session := range sessionMap { 498 | sessions = append(sessions, session) 499 | } 500 | sessionMutex.Unlock() 501 | 502 | for _, session := range sessions { 503 | session.Close() 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /tsshd/utils_darwin.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 The Trzsz SSH Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package tsshd 26 | 27 | import ( 28 | "os" 29 | ) 30 | 31 | func getUserShell() (string, error) { 32 | if shell := os.Getenv("SHELL"); shell != "" { 33 | return shell, nil 34 | } 35 | return "/bin/sh", nil 36 | } 37 | -------------------------------------------------------------------------------- /tsshd/utils_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !darwin 2 | 3 | /* 4 | MIT License 5 | 6 | Copyright (c) 2024 The Trzsz SSH Authors. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | package tsshd 28 | 29 | import ( 30 | "os" 31 | ) 32 | 33 | func getUserShell() (string, error) { 34 | if shell := os.Getenv("SHELL"); shell != "" { 35 | return shell, nil 36 | } 37 | // TODO getpwuid(getuid())->pw_shell 38 | return "/bin/sh", nil 39 | } 40 | -------------------------------------------------------------------------------- /tsshd/utils_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | /* 4 | MIT License 5 | 6 | Copyright (c) 2024 The Trzsz SSH Authors. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | package tsshd 28 | 29 | import ( 30 | "io" 31 | "os" 32 | "os/exec" 33 | "syscall" 34 | 35 | "github.com/creack/pty" 36 | ) 37 | 38 | type tsshdPty struct { 39 | cmd *exec.Cmd 40 | ptmx *os.File 41 | stdin io.WriteCloser 42 | stdout io.ReadCloser 43 | } 44 | 45 | func (p *tsshdPty) Wait() error { 46 | return p.cmd.Wait() 47 | } 48 | 49 | func (p *tsshdPty) Close() error { 50 | return p.ptmx.Close() 51 | } 52 | 53 | func (p *tsshdPty) Resize(cols, rows int) error { 54 | return pty.Setsize(p.ptmx, &pty.Winsize{Cols: uint16(cols), Rows: uint16(rows)}) 55 | } 56 | 57 | func newTsshdPty(cmd *exec.Cmd, cols, rows int) (*tsshdPty, error) { 58 | ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Cols: uint16(cols), Rows: uint16(rows)}) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return &tsshdPty{cmd, ptmx, ptmx, ptmx}, nil 63 | } 64 | 65 | func getSysProcAttr() *syscall.SysProcAttr { 66 | return &syscall.SysProcAttr{ 67 | Setsid: true, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tsshd/utils_windows.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2024 The Trzsz SSH Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | package tsshd 26 | 27 | import ( 28 | "context" 29 | "io" 30 | "os/exec" 31 | "strings" 32 | "syscall" 33 | 34 | "github.com/UserExistsError/conpty" 35 | "golang.org/x/sys/windows" 36 | ) 37 | 38 | type tsshdPty struct { 39 | cpty *conpty.ConPty 40 | stdin io.WriteCloser 41 | stdout io.ReadCloser 42 | } 43 | 44 | func (p *tsshdPty) Wait() error { 45 | _, err := p.cpty.Wait(context.Background()) 46 | _ = p.stdout.Close() 47 | return err 48 | } 49 | 50 | func (p *tsshdPty) Close() error { 51 | return p.cpty.Close() 52 | } 53 | 54 | func (p *tsshdPty) Resize(cols, rows int) error { 55 | return p.cpty.Resize(cols, rows) 56 | } 57 | 58 | func newTsshdPty(cmd *exec.Cmd, cols, rows int) (*tsshdPty, error) { 59 | var cmdLine strings.Builder 60 | for _, arg := range cmd.Args { 61 | if cmdLine.Len() > 0 { 62 | cmdLine.WriteString(" ") 63 | } 64 | cmdLine.WriteString(windows.EscapeArg(arg)) 65 | } 66 | cpty, err := conpty.Start(cmdLine.String(), conpty.ConPtyDimensions(cols, rows), conpty.ConPtyEnv(cmd.Env)) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return &tsshdPty{cpty, cpty, cpty}, nil 71 | } 72 | 73 | func getUserShell() (string, error) { 74 | return "PowerShell", nil 75 | } 76 | 77 | func getSysProcAttr() *syscall.SysProcAttr { 78 | return &syscall.SysProcAttr{ 79 | CreationFlags: windows.CREATE_BREAKAWAY_FROM_JOB | windows.DETACHED_PROCESS, 80 | } 81 | } 82 | --------------------------------------------------------------------------------