├── .github └── workflows │ ├── build-builder-image.yml │ ├── build-release-image.yml │ ├── build.yml │ ├── codeql-analysis.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── build ├── code ├── client │ ├── app │ │ ├── cmd.go │ │ ├── handler.go │ │ └── program.go │ ├── conn │ │ ├── conn.go │ │ ├── send.go │ │ ├── send_code.go │ │ ├── send_conn.go │ │ ├── send_shell.go │ │ ├── send_vnc.go │ │ └── utils.go │ ├── dashboard │ │ ├── .gitignore │ │ ├── dashboard.go │ │ ├── h_info.go │ │ ├── h_render.go │ │ └── h_rules.go │ ├── global │ │ ├── conf.go │ │ └── port.go │ ├── main.go │ └── rule │ │ ├── bench │ │ ├── bench.go │ │ └── h_http.go │ │ ├── code │ │ ├── .gitignore │ │ ├── code.go │ │ ├── h_forward.go │ │ ├── h_forward_request.go │ │ ├── h_forward_websocket.go │ │ ├── h_info.go │ │ ├── h_new.go │ │ ├── h_render.go │ │ ├── workspace.go │ │ ├── workspace_local_request.go │ │ └── workspace_remote_response.go │ │ ├── mgr.go │ │ ├── shell │ │ ├── .gitignore │ │ ├── exec_windows.go │ │ ├── exec_xx.go │ │ ├── h_new.go │ │ ├── h_render.go │ │ ├── h_resize.go │ │ ├── h_ws.go │ │ ├── link.go │ │ ├── shell.go │ │ └── transform.go │ │ └── vnc │ │ ├── .gitignore │ │ ├── define │ │ ├── kernel32_windows.go │ │ ├── sas_windows.go │ │ └── user32_windows.go │ │ ├── diff.go │ │ ├── events.go │ │ ├── h_clipboard.go │ │ ├── h_ctrl.go │ │ ├── h_new.go │ │ ├── h_render.go │ │ ├── h_ws.go │ │ ├── link.go │ │ ├── process │ │ ├── errors.go │ │ ├── event.go │ │ ├── event_windows.go │ │ ├── event_xx.go │ │ ├── process.go │ │ ├── process_windows.go │ │ └── process_xx.go │ │ ├── vnc.go │ │ ├── vncnetwork │ │ ├── build │ │ ├── vncmsg.pb.go │ │ └── vncmsg.proto │ │ ├── worker.go │ │ └── worker │ │ ├── attach_windows.go │ │ ├── attach_xx.go │ │ ├── capture.go │ │ ├── event.go │ │ └── worker.go ├── hash │ └── hash.go ├── network │ ├── build │ ├── code.pb.go │ ├── code.proto │ ├── connect.pb.go │ ├── connect.proto │ ├── encoding │ │ ├── encoding.go │ │ ├── gzip │ │ │ └── gzip.go │ │ └── proto │ │ │ └── proto.go │ ├── forward.pb.go │ ├── forward.proto │ ├── msg.pb.go │ ├── msg.proto │ ├── network.go │ ├── shell.pb.go │ ├── shell.proto │ ├── vnc.pb.go │ └── vnc.proto ├── server │ ├── app │ │ ├── cmd.go │ │ └── program.go │ ├── global │ │ └── conf.go │ ├── handler │ │ ├── client.go │ │ ├── clients.go │ │ ├── errors.go │ │ └── handler.go │ └── main.go └── utils │ ├── bytes.go │ ├── recover.go │ └── utils.go ├── conf ├── common.yaml ├── local.yaml ├── remote.yaml ├── rule.d │ ├── code-server.yaml │ ├── shell.yaml │ └── vnc.yaml └── server.yaml ├── contrib └── bindata │ └── main.go ├── docker_build ├── docs ├── desc.md ├── imgs │ ├── architecture.drawio │ ├── architecture.jpg │ ├── bench.png │ ├── code_server.png │ ├── dashboard.png │ ├── example.drawio │ ├── example.jpg │ ├── shell.gif │ ├── shell_linux.png │ ├── shell_win.png │ ├── vnc.gif │ ├── vnc_clipboard.png │ ├── vnc_deepin.png │ ├── vnc_fedora.png │ ├── vnc_macos.png │ ├── vnc_ubuntu.png │ ├── vnc_win10.png │ ├── vnc_win11.png │ └── vnc_win2008.png ├── rules.md └── startup.md ├── go.mod ├── go.sum ├── html ├── AdminLTE-3.2.0 │ ├── AdminLTE │ ├── css │ │ ├── adminlte.min.css │ │ └── adminlte.min.css.map │ └── js │ │ ├── adminlte.min.js │ │ └── adminlte.min.js.map ├── bootstrap-4.6.2 │ ├── css │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ └── js │ │ ├── bootstrap.min.js │ │ └── bootstrap.min.js.map ├── code │ ├── common.js │ ├── fontawesome │ ├── index.css │ ├── index.html │ ├── index.js │ └── jquery ├── dashboard │ ├── AdminLTE │ ├── bootstrap │ ├── files.html │ ├── fontawesome │ ├── index.html │ ├── jquery │ ├── js │ │ ├── aside.js │ │ ├── common.js │ │ ├── index.js │ │ └── terminal.js │ ├── templates │ │ ├── aside.html │ │ ├── footer.html │ │ └── header.html │ └── terminal.html ├── fontawesome-free-6.2.1 │ ├── css │ │ └── all.min.css │ ├── js │ │ └── all.min.js │ └── webfonts │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff2 │ │ ├── fa-v4compatibility.ttf │ │ └── fa-v4compatibility.woff2 ├── jquery-mousewheel │ └── jquery.mousewheel.min.js ├── jquery │ ├── jquery-3.6.3.min.js │ └── jquery-3.6.3.min.map ├── js │ └── common.js ├── shell │ ├── index.css │ ├── index.html │ ├── index.js │ ├── jquery │ └── xterm.js ├── vnc │ ├── AdminLTE │ ├── bootstrap │ ├── clipboard_dialog.js │ ├── common.js │ ├── fontawesome │ ├── index.css │ ├── index.html │ ├── index.js │ ├── jquery │ └── jquery-mousewheel └── xterm.js-5.1.0 │ ├── addon │ ├── xterm-addon-attach │ │ ├── xterm-addon-attach.js │ │ └── xterm-addon-attach.js.map │ └── xterm-addon-fit │ │ ├── xterm-addon-fit.js │ │ └── xterm-addon-fit.js.map │ ├── xterm.css │ ├── xterm.js │ └── xterm.js.map └── test ├── .gitignore └── code-server-forward └── main.go /.github/workflows/build-builder-image.yml: -------------------------------------------------------------------------------- 1 | name: build-builder 2 | 3 | on: 4 | push: 5 | branches: [ builder ] 6 | schedule: 7 | - cron: '30 0 * * *' 8 | 9 | env: 10 | GO_VERSION: "1.20.11" 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | ref: builder 20 | 21 | - uses: docker/setup-qemu-action@v3 22 | - uses: docker/setup-buildx-action@v3 23 | 24 | - uses: docker/login-action@v3 25 | with: 26 | username: ${{ secrets.DOCKERHUB_USERNAME }} 27 | password: ${{ secrets.DOCKERHUB_TOKEN }} 28 | 29 | - uses: docker/build-push-action@v5 30 | with: 31 | context: . 32 | pull: true 33 | push: true 34 | tags: lwch/natpass-builder:${{ env.GO_VERSION }} 35 | build-args: | 36 | GO_VERSION=${{ env.GO_VERSION }} -------------------------------------------------------------------------------- /.github/workflows/build-release-image.yml: -------------------------------------------------------------------------------- 1 | name: build-release 2 | 3 | on: 4 | push: 5 | branches: [ release ] 6 | schedule: 7 | - cron: '30 0 * * *' 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | ref: release 17 | 18 | - uses: docker/setup-qemu-action@v3 19 | - uses: docker/setup-buildx-action@v3 20 | 21 | - uses: docker/login-action@v3 22 | with: 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | 26 | - uses: docker/build-push-action@v5 27 | with: 28 | context: . 29 | pull: true 30 | push: true 31 | tags: lwch/natpass-release -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu-20.04 17 | - ubuntu-22.04 18 | - macos-11 19 | - macos-12 20 | - macos-13 21 | go: 22 | - '1.18' 23 | - '1.19' 24 | - '1.20' 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Set up Go 29 | uses: actions/setup-go@v4 30 | with: 31 | go-version: ${{ matrix.go }} 32 | 33 | - name: Lint 34 | run: | 35 | go install golang.org/x/lint/golint@latest 36 | golint -set_exit_status code/... 37 | go install github.com/gordonklaus/ineffassign@latest 38 | ineffassign ./... 39 | 40 | - name: Build Server 41 | run: go build -v code/server/main.go 42 | 43 | - name: Build Client 44 | run: | 45 | go run contrib/bindata/main.go -pkg shell -o code/client/rule/shell/assets.go -prefix html/shell html/shell/... 46 | go run contrib/bindata/main.go -pkg vnc -o code/client/rule/vnc/assets.go -prefix html/vnc html/vnc/... 47 | go run contrib/bindata/main.go -pkg code -o code/client/rule/code/assets.go -prefix html/code html/code/... 48 | go run contrib/bindata/main.go -pkg dashboard -o code/client/dashboard/assets.go -prefix html/dashboard html/dashboard/... 49 | go build -v code/client/main.go 50 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '28 14 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go', 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: [ v*.*.* ] 6 | 7 | jobs: 8 | 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - id: get_version 13 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 14 | 15 | - uses: actions/checkout@v4 16 | 17 | - name: build 18 | uses: lwch/natpass@builder 19 | env: 20 | BUILD_VERSION: ${{ steps.get_version.outputs.VERSION }} 21 | 22 | - name: release 23 | uses: lwch/natpass@release 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | BUILD_VERSION: ${{ steps.get_version.outputs.VERSION }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /build 3 | /release 4 | /tmp 5 | /logs 6 | /run 7 | /*.yaml 8 | /test_conf -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.0.1 4 | 5 | 实现基本功能 6 | 7 | ## v0.1.0 8 | 9 | 1. 修改数据包头,增加crc32校验码 10 | 2. 实现连接池,支持多路IO复用 11 | 3. 新增读写超时时间配置 12 | 4. 减少单次forward数据量,提高吞吐量 13 | 5. 重构部分代码 14 | 15 | **注:由于在包头上增加了crc32校验码,因此v0.0.1版本与v0.1.0版本的程序不能混用** 16 | 17 | ## v0.1.1 18 | 19 | 1. 去除client中的一处超时逻辑,保证收到的每个forward数据被写回本地 20 | 2. 修正client启动时并发设置的idx不正确的BUG 21 | 3. 同一个链接两端idx相同时server端获取错误client对象的问题 22 | 4. 修正connect后返回的第一条数据to_idx设置错误的问题 23 | 24 | ## v0.1.2 25 | 26 | 1. 服务端增加心跳,客户端增加超时次数判断逻辑,用以支持客户端操作系统休眠后的恢复 27 | 2. 修正客户端断开链接后的崩溃问题 28 | 29 | ## v0.2.0 30 | 31 | 1. 新增action参数用于注册系统服务,删除原有init.d启动脚本 32 | 2. 提取link_id为基础字段,修改协议数据包格式 33 | 34 | **注:由于提取link_id作为基础字段,因此v0.2.0版本与旧版本的程序不能混用** 35 | 36 | ## v0.3.0 37 | 38 | 1. 新增shell规则的支持 39 | 40 | ## v0.4.0 41 | 42 | 1. 配置文件支持include语法 43 | 2. 通用的握手方式,支持tcp和shell 44 | 45 | ## v0.5.0 46 | 47 | 1. 新增dashboard和统一的终端管理页面 48 | 49 | ## v0.6.0 50 | 51 | 1. 新增vnc规则支持 52 | 53 | ## v0.6.1 54 | 55 | 1. 修正vnc规则的fps参数上限不起作用的问题 56 | 2. vnc页面增加全屏功能 57 | 58 | ## v0.6.2 59 | 60 | 1. vnc页面支持滚动 61 | 2. go版本升级到1.17.3 62 | 3. 文档补全 63 | 64 | ## v0.7.0 65 | 66 | 1. bootstrap降版到4.6.1 67 | 2. dashboard页面支持规则类型筛选 68 | 3. **为遵守中国法律,移除内网穿透功能**,保留shell和vnc功能不变 69 | 70 | ## v0.7.1 71 | 72 | 1. vnc页面支持远程设置或读取剪贴板(仅支持文本内容) 73 | 74 | ## v0.7.2 75 | 76 | 1. 截屏库统一抽取到https://github.com/lwch/rdesktop 77 | 2. 修正windows下的错误日志显示格式问题 78 | 3. 修正windows下的配置文件include问题 79 | 4. 支持linux远程桌面 80 | 81 | **注:linux受控端需使用np-cli.vnc程序进行启动,且目前无法以systemd等系统服务方式运行** 82 | 83 | ## v0.7.3 84 | 85 | 1. vnc支持\键 86 | 2. 全面迁移到[https://github.com/lwch/rdesktop](https://github.com/lwch/rdesktop),并去除robotgo库的依赖 87 | 3. linux下统一到同一个可执行文件,并支持systemd方式启动 88 | 4. 简化部署流程 89 | 90 | ## v0.8.0 91 | 92 | 1. 支持非tls加密连接 93 | 2. 修改go.mod中的项目名称 94 | 3. 简化部署流程 95 | 96 | ## v0.8.1 97 | 98 | 1. 升级go版本到1.17.6 99 | 2. 简化打包docker镜像 100 | 3. 优化性能 101 | 102 | ## v0.8.2 103 | 104 | 1. 去除连接池的支持 105 | 106 | ## v0.8.3 107 | 108 | 1. 修正连接失败时无法正常运行的问题 109 | 2. 升级go版本到1.17.7 110 | 111 | ## v0.8.4 112 | 113 | 1. 升级go版本到1.18.1 114 | 2. 支持arm环境编译 115 | 116 | ## v0.8.5 117 | 118 | 1. 升级go版本到1.18.3 119 | 2. 修正连接失败时的panic问题 120 | 121 | ## v0.8.6 122 | 123 | 1. 升级rdesktop库,支持libx11库的静态连接 124 | 125 | ## v0.9.0 126 | 127 | 1. 远程桌面支持macos系统 128 | 2. 修改部署文档 129 | 130 | ## v0.9.1 131 | 132 | 1. 修正客户端在断网后会假死的问题 133 | 134 | ## v0.10.0 135 | 136 | 1. go版本升级到1.18.4 137 | 2. 新增code-server支持 138 | 3. 优化disconnect处理逻辑 139 | 140 | ## v0.10.1 141 | 142 | 1. 修改客户端配置文件,新增ssl的insecure支持 143 | 144 | ## v0.10.2 145 | 146 | 1. go版本升级到1.19 147 | 2. 修正windows下服务无法启动的问题 148 | 3. 修正code-server无法全屏问题 149 | 150 | ## v0.10.3 151 | 152 | 1. 修正code-server中剪贴板无法使用的问题 153 | 154 | ## v0.10.4 155 | 156 | 1. 修正windows下连接异常无法正常退出的问题 157 | 158 | ## v0.10.5 159 | 160 | 1. 修改注册系统服务时的配置项 161 | 2. 升级第三方库 162 | 163 | ## v0.11.0 164 | 165 | 1. 修改握手时的签名算法 166 | 2. go版本升级到1.19.1 167 | 3. AdminLTE库升级到3.2.0 168 | 4. bootstrap库升级到4.6.2 169 | 5. jquery库升级到3.6.1 170 | 6. xterm.js库升级到5.0.0 171 | 7. fontawesome库升级到6.2.0 172 | 8. 去除go1.16的支持 173 | 174 | ## v0.11.1 175 | 176 | 1. go版本升级到1.19.2 177 | 2. 实现actions自动打包 178 | 179 | ## v0.11.2 180 | 181 | 1. 修正windows下日志文件无法rotate的问题 182 | 2. 增加未配置项的默认配置 183 | 184 | ## v0.11.3 185 | 186 | 1. go版本升级到1.19.4 187 | 2. 升级第三方库 188 | 189 | ## v0.11.4 190 | 191 | 1. go版本升级到1.19.5 192 | 2. fontawesome升级到6.2.1 193 | 3. jquery升级到3.6.3 194 | 4. xterm.js升级到5.1.0 195 | 5. 升级第三方库 196 | 197 | ## v0.12.0 198 | 199 | 1. 命令行交互切换到cobra库 200 | 2. 新增start、stop、restart、status命令行交互命令 201 | 3. 升级第三方库 202 | 4. 补充代码注释 203 | 204 | ## v0.12.1 205 | 206 | 1. 启动时增加logo输出 207 | 2. 重写网络数据编码逻辑 208 | 3. go版本升级到1.20.1 209 | 4. 补充代码注释 210 | 5. 更新第三方库 211 | 212 | ## v0.12.2 213 | 214 | 1. 修正macos下的鼠标位置无法显示问题 215 | 2. 修正macos下的鼠标位置问题 #34 216 | 3. go版本升级到1.20.2 217 | 4. 更新第三方库 218 | 5. 去除go1.17版本支持 219 | 220 | ## v0.12.3 221 | 222 | 1. go版本升级到1.20.3 223 | 2. 更新第三方库 224 | 225 | ## v0.13.0 226 | 227 | 1. 支持自动生成local_port 228 | 2. 更新第三方库 229 | 3. go版本升级到1.20.11 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 李文超 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # natpass 2 | 3 | [![natpass](https://github.com/lwch/natpass/actions/workflows/build.yml/badge.svg)](https://github.com/lwch/natpass/actions/workflows/build.yml) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/lwch/natpass)](https://goreportcard.com/report/github.com/lwch/natpass) 5 | [![license](https://img.shields.io/github/license/lwch/natpass)](https://opensource.org/licenses/MIT) 6 | [![QQ群711086098](https://img.shields.io/badge/QQ%E7%BE%A4-711086098-success)](https://jq.qq.com/?_wv=1027&k=6Fz2vkVE) 7 | ![downloads](https://img.shields.io/github/downloads/lwch/natpass/total) 8 | 9 | 新一代主机管理工具,支持shell管理,支持远程桌面管理[实现原理](docs/desc.md) 10 | 11 | 1. [如何部署](docs/startup.md) 12 | 2. [规则配置](docs/rules.md) 13 | 3. [开发文档](https://lwch.gitbook.io/natpass/dev) 14 | 15 | 功能与特性: 16 | 17 | 1. 支持私有化部署 18 | 2. 较小的内存占用(约20M左右) 19 | 3. 支持tls安全连接 20 | 4. 支持多路异步IO 21 | 5. 支持虚拟链路层 22 | 6. 支持链路和终端会话监控 23 | 7. protobuf数据编码 24 | 8. 支持web shell 25 | - linux和mac系统支持创建pty设备和颜色输出 26 | - windows系统支持powershell 27 | 9. 支持web vnc 28 | - 支持基本的键盘鼠标操作 29 | - 支持全屏显示 30 | - 支持滚动 31 | - 支持远程剪贴板设置与读取 32 | 10. 支持code-server 33 | 11. 支持多种操作系统 34 | - [x] linux 35 | - [x] windows 36 | - [x] macos 37 | 38 | ## 效果图 39 | 40 | dashboard页面 41 | 42 | ![dashboard](docs/imgs/dashboard.png) 43 | 44 | 命令行 45 | 46 | | platform | 386 | amd64 | arm | arm64 | 47 | | -------- | :-: | :---: | :-: | :---: | 48 | | windows | ✅ | ✅ | ✅ | ✅ | 49 | | macos | | ✅ | | ✅ | 50 | | linux | ✅ | ✅ | ✅ | ✅ | 51 | 52 | ![shell](docs/imgs/shell.gif) 53 | 54 | 远程桌面 55 | 56 | | platform | 386 | amd64 | arm | arm64 | 57 | | -------- | :-: | :---: | :-: | :---: | 58 | | windows | ✅ | ✅ | ❌ | ❌ | 59 | | macos | | ✅ | | ✅ | 60 | | linux | ✅ | ✅ | ❌ | ❌ | 61 | 62 | ![vnc](docs/imgs/vnc.gif) 63 | 64 | windows剪贴板内容 65 | 66 | ![vnc-clipboard](docs/imgs/vnc_clipboard.png) 67 | 68 | code-server支持 69 | 70 | ![code-server](docs/imgs/code_server.png) 71 | 72 | ## 性能 73 | 74 | 在vmware环境下创建4C2G(AMD Ryzen 7 4800U with Radeon Graphics)测试环境,并进行all in one部署server、remote端和local端,使用bench规则进行压测,结果如下: 75 | 76 | ![bench](docs/imgs/bench.png) 77 | 78 | 1. 压测结果仅包含local端发起连接到remote端收到连接并返回成功的整个过程 79 | 2. 实验结果表明,在4C2G环境下可达到上万+的qps,且p99和p100均在60ms以下 80 | 81 | ## TODO 82 | 83 | 1. ~~支持include的yaml配置文件~~ 84 | 2. ~~通用的connect、connect_response、disconnect消息~~ 85 | 3. ~~dashboard页面~~ 86 | 4. 文件传输 87 | 5. ~web远程桌面~ 88 | 6. ~~流量监控统计页面,server还是client?~~ 89 | 7. web端管理规则 90 | 8. 支持录屏 91 | 92 | ## 编译 93 | 94 | 1. 由于html/dashboard等目录下引用第三方库时使用软连接的方式进行处理, 95 | 因此在windows环境下进行编译时需要将这些软连接的目录进行手工替换, 96 | 第三方库的代码都在上级目录下可找到。 97 | 98 | ## stars 99 | 100 | ![stars](https://starchart.cc/lwch/natpass.svg) 101 | 102 | ## 免责声明 103 | 104 | 本软件仅用于个人研究学习,包括但不限于以下条款: 105 | 106 | 1. 严禁用于黑客攻击、远程控制他人计算机等违法违规行为 107 | 2. 软件使用者使用该软件造成的任何损失均与软件作者无关, 108 | 一切后果由使用者自己负责 109 | 3. 严禁用于一切商业用途,包括但不限于提供云桌面、云主机等 110 | 111 | ## 贡献代码 112 | 113 | 为了更好的发展,我们鼓励大家为natpass项目做出贡献及提出建议,项目的地址为[https://github.com/lwch/natpass](https://github.com/lwch/natpass),因此在gitee上提交的pr将不被接受,请大家将pr提交到github的同名项目中。 -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | -------------------------------------------------------------------------------- /build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BUILD_VERSION=${BUILD_VERSION:-0.0.0} 4 | 5 | HASH=`git log -n1 --pretty=format:%h` 6 | REVERSION=`git log --oneline|wc -l|tr -d ' '` 7 | BUILD_TIME=`date +'%Y-%m-%d %H:%M:%S'` 8 | LDFLAGS="-X 'main.gitHash=$HASH' 9 | -X 'main.gitReversion=$REVERSION' 10 | -X 'main.buildTime=$BUILD_TIME' 11 | -X 'main.version=$BUILD_VERSION'" 12 | 13 | if [ "$GOOS" = "windows" ]; then 14 | LDFLAGS="$LDFLAGS 15 | --extldflags '-static -fpic -lssp'" 16 | fi 17 | 18 | go run contrib/bindata/main.go -pkg shell -o code/client/rule/shell/assets.go \ 19 | -prefix html/shell "$@" html/shell/... 20 | go run contrib/bindata/main.go -pkg vnc -o code/client/rule/vnc/assets.go \ 21 | -prefix html/vnc "$@" html/vnc/... 22 | go run contrib/bindata/main.go -pkg code -o code/client/rule/code/assets.go \ 23 | -prefix html/code "$@" html/code/... 24 | go run contrib/bindata/main.go -pkg dashboard -o code/client/dashboard/assets.go \ 25 | -prefix html/dashboard "$@" html/dashboard/... 26 | 27 | go build -ldflags "$LDFLAGS" -o bin/np-svr code/server/*.go 28 | go build -ldflags "$LDFLAGS" -tags "$TAGS" -o bin/np-cli code/client/*.go -------------------------------------------------------------------------------- /code/client/app/handler.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/lwch/logging" 5 | "github.com/lwch/natpass/code/client/conn" 6 | "github.com/lwch/natpass/code/client/global" 7 | "github.com/lwch/natpass/code/client/rule" 8 | "github.com/lwch/natpass/code/client/rule/code" 9 | "github.com/lwch/natpass/code/client/rule/shell" 10 | "github.com/lwch/natpass/code/client/rule/vnc" 11 | "github.com/lwch/natpass/code/network" 12 | ) 13 | 14 | /* 15 | this is the handler function file like http.HandlerFunc. 16 | 17 | for linked rule: 18 | 1. get rule from manager 19 | 2. create rule if not exists by its type 20 | 3. create link call NewLink 21 | 4. do the initialize logic for the link 22 | 5. response connect ok message 23 | 6. loop forward 24 | 25 | no linked rule: 26 | TODO 27 | */ 28 | 29 | func (p *program) shellCreate(mgr *rule.Mgr, conn *conn.Conn, msg *network.Msg) { 30 | create := msg.GetCreq() 31 | tn := mgr.GetLinked(create.GetName(), msg.GetFrom()) 32 | if tn == nil { 33 | tn = shell.New(&global.Rule{ 34 | Name: create.GetName(), 35 | Target: msg.GetFrom(), 36 | Type: "shell", 37 | Exec: create.GetCshell().GetExec(), 38 | Env: create.GetCshell().GetEnv(), 39 | }, p.cfg.ReadTimeout, p.cfg.WriteTimeout) 40 | mgr.Add(tn.(rule.Rule)) 41 | } 42 | lk := tn.NewLink(msg.GetLinkId(), msg.GetFrom(), nil, conn).(*shell.Link) 43 | logging.Info("create link %s for shell rule [%s] from %s to %s", 44 | msg.GetLinkId(), create.GetName(), 45 | msg.GetFrom(), p.cfg.ID) 46 | err := lk.Exec() 47 | if err != nil { 48 | logging.Error("create shell failed: %v", err) 49 | conn.SendConnectError(msg.GetFrom(), msg.GetLinkId(), err.Error()) 50 | return 51 | } 52 | conn.SendConnectOK(msg.GetFrom(), msg.GetLinkId()) 53 | lk.Forward() 54 | } 55 | 56 | func (p *program) vncCreate(confDir string, mgr *rule.Mgr, conn *conn.Conn, msg *network.Msg) { 57 | create := msg.GetCreq() 58 | tn := mgr.GetLinked(create.GetName(), msg.GetFrom()) 59 | if tn == nil { 60 | tn = vnc.New(&global.Rule{ 61 | Name: create.GetName(), 62 | Target: msg.GetFrom(), 63 | Type: "vnc", 64 | Fps: create.GetCvnc().GetFps(), 65 | }, p.cfg.ReadTimeout, p.cfg.WriteTimeout) 66 | mgr.Add(tn.(rule.Rule)) 67 | } 68 | lk := tn.NewLink(msg.GetLinkId(), msg.GetFrom(), nil, conn).(*vnc.Link) 69 | logging.Info("create link %s for vnc rule [%s] from %s to %s", 70 | msg.GetLinkId(), create.GetName(), 71 | msg.GetFrom(), p.cfg.ID) 72 | lk.SetQuality(create.GetCvnc().GetQuality()) 73 | err := lk.Fork(confDir) 74 | if err != nil { 75 | logging.Error("create vnc failed: %v", err) 76 | conn.SendConnectError(msg.GetFrom(), msg.GetLinkId(), err.Error()) 77 | return 78 | } 79 | conn.SendConnectOK(msg.GetFrom(), msg.GetLinkId()) 80 | lk.Forward() 81 | } 82 | 83 | func (p *program) benchCreate(confDir string, mgr *rule.Mgr, conn *conn.Conn, msg *network.Msg) { 84 | create := msg.GetCreq() 85 | logging.Info("create link %s for bench rule [%s] from %s to %s", 86 | msg.GetLinkId(), create.GetName(), 87 | msg.GetFrom(), p.cfg.ID) 88 | conn.SendConnectOK(msg.GetFrom(), msg.GetLinkId()) 89 | } 90 | 91 | func (p *program) codeCreate(confDir string, mgr *rule.Mgr, conn *conn.Conn, msg *network.Msg) { 92 | create := msg.GetCreq() 93 | tn := mgr.GetLinked(create.GetName(), msg.GetFrom()) 94 | if tn == nil { 95 | tn = code.New(&global.Rule{ 96 | Name: create.GetName(), 97 | Target: msg.GetFrom(), 98 | Type: "code-server", 99 | }, p.cfg.ReadTimeout, p.cfg.WriteTimeout) 100 | mgr.Add(tn.(rule.Rule)) 101 | } 102 | workspace := tn.NewLink(msg.GetLinkId(), msg.GetFrom(), nil, conn).(*code.Workspace) 103 | logging.Info("create link %s for code-server rule [%s] from %s to %s", 104 | msg.GetLinkId(), create.GetName(), 105 | msg.GetFrom(), p.cfg.ID) 106 | err := workspace.Exec(p.cfg.CodeDir) 107 | if err != nil { 108 | logging.Error("create vnc failed: %v", err) 109 | conn.SendConnectError(msg.GetFrom(), msg.GetLinkId(), err.Error()) 110 | return 111 | } 112 | conn.SendConnectOK(msg.GetFrom(), msg.GetLinkId()) 113 | workspace.Forward() 114 | } 115 | -------------------------------------------------------------------------------- /code/client/app/program.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "os" 5 | rt "runtime" 6 | 7 | "github.com/common-nighthawk/go-figure" 8 | "github.com/kardianos/service" 9 | "github.com/lwch/logging" 10 | "github.com/lwch/natpass/code/client/conn" 11 | "github.com/lwch/natpass/code/client/dashboard" 12 | "github.com/lwch/natpass/code/client/global" 13 | "github.com/lwch/natpass/code/client/rule" 14 | "github.com/lwch/natpass/code/client/rule/bench" 15 | "github.com/lwch/natpass/code/client/rule/code" 16 | "github.com/lwch/natpass/code/client/rule/shell" 17 | "github.com/lwch/natpass/code/client/rule/vnc" 18 | "github.com/lwch/natpass/code/network" 19 | "github.com/lwch/runtime" 20 | ) 21 | 22 | type program struct { 23 | confDir string 24 | cfg *global.Configure 25 | conn *conn.Conn 26 | } 27 | 28 | func newProgram() *program { 29 | return &program{} 30 | } 31 | 32 | func (p *program) setConfDir(dir string) *program { 33 | p.confDir = dir 34 | return p 35 | } 36 | 37 | func (p *program) setConfigure(cfg *global.Configure) *program { 38 | p.cfg = cfg 39 | return p 40 | } 41 | 42 | // Start main entry for service 43 | func (p *program) Start(s service.Service) error { 44 | go p.run() 45 | return nil 46 | } 47 | 48 | // Stop stop service callback 49 | func (*program) Stop(s service.Service) error { 50 | return nil 51 | } 52 | 53 | func (p *program) run() { 54 | // go func() { 55 | // http.ListenAndServe(":9000", nil) 56 | // }() 57 | 58 | // initialize logging 59 | stdout := true 60 | if rt.GOOS == "windows" { 61 | stdout = false 62 | } 63 | logging.SetSizeRotate(logging.SizeRotateConfig{ 64 | Dir: p.cfg.LogDir, 65 | Name: "np-cli", 66 | Size: int64(p.cfg.LogSize.Bytes()), 67 | Rotate: p.cfg.LogRotate, 68 | WriteStdout: stdout, 69 | WriteFile: true, 70 | }) 71 | defer logging.Flush() 72 | 73 | fg := figure.NewFigure("NatPass", "alligator2", false) 74 | figure.Write(&logging.DefaultLogger, fg) 75 | logging.DefaultLogger.Write(nil) 76 | 77 | // create connection and handshake 78 | p.conn = conn.New(p.cfg) 79 | 80 | // build rule manager 81 | mgr := rule.New() 82 | 83 | // add rules from configure file, wait http request from web browser 84 | for _, t := range p.cfg.Rules { 85 | switch t.Type { 86 | case "shell": 87 | sh := shell.New(t, p.cfg.ReadTimeout, p.cfg.WriteTimeout) 88 | mgr.Add(sh) 89 | go sh.Handle(p.conn) 90 | case "vnc": 91 | v := vnc.New(t, p.cfg.ReadTimeout, p.cfg.WriteTimeout) 92 | mgr.Add(v) 93 | go v.Handle(p.conn) 94 | case "bench": 95 | b := bench.New(t) 96 | mgr.Add(b) 97 | go b.Handle(p.conn) 98 | case "code-server": 99 | cs := code.New(t, p.cfg.ReadTimeout, p.cfg.WriteTimeout) 100 | mgr.Add(cs) 101 | go cs.Handle(p.conn) 102 | } 103 | } 104 | 105 | // handle request from remote node 106 | go func() { 107 | for { 108 | msg := <-p.conn.ChanUnknown() 109 | var linkID string 110 | switch msg.GetXType() { 111 | case network.Msg_connect_req: 112 | switch msg.GetCreq().GetXType() { 113 | case network.ConnectRequest_shell: 114 | // fork /bin/bash command and ack 115 | p.shellCreate(mgr, p.conn, msg) 116 | case network.ConnectRequest_vnc: 117 | // fork np-cli vnc child process and ack 118 | p.vncCreate(p.confDir, mgr, p.conn, msg) 119 | case network.ConnectRequest_bench: 120 | // bench handler response ok directly 121 | p.benchCreate(p.confDir, mgr, p.conn, msg) 122 | case network.ConnectRequest_code: 123 | // fork code-server child process and ack 124 | p.codeCreate(p.confDir, mgr, p.conn, msg) 125 | } 126 | default: 127 | linkID = msg.GetLinkId() 128 | } 129 | // invalid message type, close channel directly 130 | if len(linkID) > 0 { 131 | p.conn.ChanClose(linkID) 132 | logging.Error("link of %s not found, type=%s", 133 | linkID, msg.GetXType().String()) 134 | continue 135 | } 136 | } 137 | }() 138 | 139 | // on disconnect message dispatcher, also to close the forked process 140 | go func() { 141 | for { 142 | id := <-p.conn.ChanDisconnect() 143 | mgr.OnDisconnect(id) 144 | } 145 | }() 146 | 147 | if p.cfg.DashboardEnabled { 148 | // if the dashboard is enabled, wait the connection close async 149 | go func() { 150 | p.conn.Wait() 151 | logging.Flush() 152 | os.Exit(1) 153 | }() 154 | // handle dashboard 155 | db := dashboard.New(p.cfg, p.conn, mgr, Version) 156 | runtime.Assert(db.ListenAndServe(p.cfg.DashboardListen, p.cfg.DashboardPort)) 157 | } else { 158 | // wait the connection close 159 | p.conn.Wait() 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /code/client/conn/send.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lwch/natpass/code/network" 7 | ) 8 | 9 | // SendKeepalive send keepalive message 10 | func (conn *Conn) SendKeepalive() { 11 | var msg network.Msg 12 | msg.To = "server" 13 | msg.XType = network.Msg_keepalive 14 | select { 15 | case conn.write <- &msg: 16 | case <-time.After(conn.cfg.WriteTimeout): 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /code/client/conn/send_conn.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lwch/natpass/code/client/global" 7 | "github.com/lwch/natpass/code/network" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | // SendConnectReq send connect request message 12 | func (conn *Conn) SendConnectReq(id string, cfg *global.Rule) { 13 | var msg network.Msg 14 | msg.To = cfg.Target 15 | msg.XType = network.Msg_connect_req 16 | msg.LinkId = id 17 | switch cfg.Type { 18 | case "shell": 19 | msg.Payload = &network.Msg_Creq{ 20 | Creq: &network.ConnectRequest{ 21 | Name: cfg.Name, 22 | XType: network.ConnectRequest_shell, 23 | Payload: &network.ConnectRequest_Cshell{ 24 | Cshell: &network.ConnectShell{ 25 | Exec: cfg.Exec, 26 | Env: cfg.Env, 27 | }, 28 | }, 29 | }, 30 | } 31 | case "vnc": 32 | fps := cfg.Fps 33 | if fps > 50 { 34 | fps = 50 35 | } else if fps == 0 { 36 | fps = 10 37 | } 38 | msg.Payload = &network.Msg_Creq{ 39 | Creq: &network.ConnectRequest{ 40 | Name: cfg.Name, 41 | XType: network.ConnectRequest_vnc, 42 | Payload: &network.ConnectRequest_Cvnc{ 43 | Cvnc: &network.ConnectVnc{ 44 | Fps: fps, 45 | }, 46 | }, 47 | }, 48 | } 49 | case "bench": 50 | msg.Payload = &network.Msg_Creq{ 51 | Creq: &network.ConnectRequest{ 52 | Name: cfg.Name, 53 | XType: network.ConnectRequest_bench, 54 | }, 55 | } 56 | case "code-server": 57 | msg.Payload = &network.Msg_Creq{ 58 | Creq: &network.ConnectRequest{ 59 | Name: cfg.Name, 60 | XType: network.ConnectRequest_code, 61 | }, 62 | } 63 | } 64 | select { 65 | case conn.write <- &msg: 66 | case <-time.After(conn.cfg.WriteTimeout): 67 | } 68 | } 69 | 70 | // SendConnectVnc send connect vnc request message 71 | func (conn *Conn) SendConnectVnc(id string, cfg *global.Rule, quality uint64, showCursor bool) { 72 | var msg network.Msg 73 | msg.To = cfg.Target 74 | msg.XType = network.Msg_connect_req 75 | msg.LinkId = id 76 | fps := cfg.Fps 77 | if fps > 50 { 78 | fps = 50 79 | } else if fps == 0 { 80 | fps = 10 81 | } 82 | msg.Payload = &network.Msg_Creq{ 83 | Creq: &network.ConnectRequest{ 84 | Name: cfg.Name, 85 | XType: network.ConnectRequest_vnc, 86 | Payload: &network.ConnectRequest_Cvnc{ 87 | Cvnc: &network.ConnectVnc{ 88 | Fps: fps, 89 | Quality: uint32(quality), 90 | Cursor: showCursor, 91 | }, 92 | }, 93 | }, 94 | } 95 | select { 96 | case conn.write <- &msg: 97 | case <-time.After(conn.cfg.WriteTimeout): 98 | } 99 | } 100 | 101 | // SendDisconnect send disconnect message 102 | func (conn *Conn) SendDisconnect(to string, id string) uint64 { 103 | var msg network.Msg 104 | msg.To = to 105 | msg.XType = network.Msg_disconnect 106 | msg.LinkId = id 107 | select { 108 | case conn.write <- &msg: 109 | data, _ := proto.Marshal(&msg) 110 | return uint64(len(data)) 111 | case <-time.After(conn.cfg.WriteTimeout): 112 | return 0 113 | } 114 | } 115 | 116 | // SendConnectError send connect error response message 117 | func (conn *Conn) SendConnectError(to string, id, info string) { 118 | var msg network.Msg 119 | msg.To = to 120 | msg.XType = network.Msg_connect_rep 121 | msg.LinkId = id 122 | msg.Payload = &network.Msg_Crep{ 123 | Crep: &network.ConnectResponse{ 124 | Ok: false, 125 | Msg: info, 126 | }, 127 | } 128 | select { 129 | case conn.write <- &msg: 130 | case <-time.After(conn.cfg.WriteTimeout): 131 | } 132 | } 133 | 134 | // SendConnectOK send connect success response message 135 | func (conn *Conn) SendConnectOK(to string, id string) { 136 | var msg network.Msg 137 | msg.To = to 138 | msg.XType = network.Msg_connect_rep 139 | msg.LinkId = id 140 | msg.Payload = &network.Msg_Crep{ 141 | Crep: &network.ConnectResponse{ 142 | Ok: true, 143 | }, 144 | } 145 | select { 146 | case conn.write <- &msg: 147 | case <-time.After(conn.cfg.WriteTimeout): 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /code/client/conn/send_shell.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lwch/natpass/code/network" 7 | "google.golang.org/protobuf/proto" 8 | ) 9 | 10 | // SendShellData send shell data 11 | func (conn *Conn) SendShellData(to string, id string, data []byte) uint64 { 12 | var msg network.Msg 13 | msg.To = to 14 | msg.XType = network.Msg_shell_data 15 | msg.LinkId = id 16 | msg.Payload = &network.Msg_Sdata{ 17 | Sdata: &network.ShellData{ 18 | Data: dup(data), 19 | }, 20 | } 21 | select { 22 | case conn.write <- &msg: 23 | data, _ := proto.Marshal(&msg) 24 | return uint64(len(data)) 25 | case <-time.After(conn.cfg.WriteTimeout): 26 | return 0 27 | } 28 | } 29 | 30 | // SendShellResize send shell resize 31 | func (conn *Conn) SendShellResize(to string, id string, rows, cols uint32) { 32 | var msg network.Msg 33 | msg.To = to 34 | msg.XType = network.Msg_shell_resize 35 | msg.LinkId = id 36 | msg.Payload = &network.Msg_Sresize{ 37 | Sresize: &network.ShellResize{ 38 | Rows: rows, 39 | Cols: cols, 40 | }, 41 | } 42 | select { 43 | case conn.write <- &msg: 44 | case <-time.After(conn.cfg.WriteTimeout): 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /code/client/conn/send_vnc.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "image" 5 | "time" 6 | 7 | "github.com/lwch/natpass/code/network" 8 | ) 9 | 10 | // SendVNCImage send vnc image data 11 | func (conn *Conn) SendVNCImage(to string, id string, screen, rect image.Rectangle, 12 | encode network.VncImageEncoding, data []byte) { 13 | var msg network.Msg 14 | msg.To = to 15 | msg.XType = network.Msg_vnc_image 16 | msg.LinkId = id 17 | msg.Payload = &network.Msg_Vimg{ 18 | Vimg: &network.VncImage{ 19 | XInfo: &network.VncImageInfo{ 20 | ScreenWidth: uint32(screen.Dx()), 21 | ScreenHeight: uint32(screen.Dy()), 22 | RectX: uint32(rect.Min.X), 23 | RectY: uint32(rect.Min.Y), 24 | RectWidth: uint32(rect.Dx()), 25 | RectHeight: uint32(rect.Dy()), 26 | }, 27 | Encode: encode, 28 | Data: dup(data), 29 | }, 30 | } 31 | select { 32 | case conn.write <- &msg: 33 | case <-time.After(conn.cfg.WriteTimeout): 34 | } 35 | } 36 | 37 | // SendVNCCtrl send vnc config 38 | func (conn *Conn) SendVNCCtrl(to string, id string, quality uint64, showCursor bool) { 39 | var msg network.Msg 40 | msg.To = to 41 | msg.XType = network.Msg_vnc_ctrl 42 | msg.LinkId = id 43 | msg.Payload = &network.Msg_Vctrl{ 44 | Vctrl: &network.VncControl{ 45 | Quality: uint32(quality), 46 | Cursor: showCursor, 47 | }, 48 | } 49 | select { 50 | case conn.write <- &msg: 51 | case <-time.After(conn.cfg.WriteTimeout): 52 | } 53 | } 54 | 55 | // SendVNCMouse send vnc mouse event 56 | func (conn *Conn) SendVNCMouse(to string, id string, 57 | button, status string, x, y int) { 58 | t := network.VncStatus_unset_st 59 | switch status { 60 | case "down": 61 | t = network.VncStatus_down 62 | case "up": 63 | t = network.VncStatus_up 64 | } 65 | btn := network.VncMouse_unset_btn 66 | switch button { 67 | case "left": 68 | btn = network.VncMouse_left 69 | case "middle": 70 | btn = network.VncMouse_middle 71 | case "right": 72 | btn = network.VncMouse_right 73 | } 74 | var msg network.Msg 75 | msg.To = to 76 | msg.XType = network.Msg_vnc_mouse 77 | msg.LinkId = id 78 | msg.Payload = &network.Msg_Vmouse{ 79 | Vmouse: &network.VncMouse{ 80 | Type: t, 81 | Btn: btn, 82 | X: uint32(x), 83 | Y: uint32(y), 84 | }, 85 | } 86 | select { 87 | case conn.write <- &msg: 88 | case <-time.After(conn.cfg.WriteTimeout): 89 | } 90 | } 91 | 92 | // SendVNCKeyboard send vnc keyboard event 93 | func (conn *Conn) SendVNCKeyboard(to string, id string, 94 | status, key string) { 95 | t := network.VncStatus_unset_st 96 | switch status { 97 | case "down": 98 | t = network.VncStatus_down 99 | case "up": 100 | t = network.VncStatus_up 101 | } 102 | var msg network.Msg 103 | msg.To = to 104 | msg.XType = network.Msg_vnc_keyboard 105 | msg.LinkId = id 106 | msg.Payload = &network.Msg_Vkbd{ 107 | Vkbd: &network.VncKeyboard{ 108 | Type: t, 109 | Key: key, 110 | }, 111 | } 112 | select { 113 | case conn.write <- &msg: 114 | case <-time.After(conn.cfg.WriteTimeout): 115 | } 116 | } 117 | 118 | // SendVNCCADEvent send vnc keyboard event 119 | func (conn *Conn) SendVNCCADEvent(to string, id string) { 120 | var msg network.Msg 121 | msg.To = to 122 | msg.XType = network.Msg_vnc_cad 123 | msg.LinkId = id 124 | select { 125 | case conn.write <- &msg: 126 | case <-time.After(conn.cfg.WriteTimeout): 127 | } 128 | } 129 | 130 | // SendVNCScroll send vnc scroll event 131 | func (conn *Conn) SendVNCScroll(to string, id string, x, y int32) { 132 | var msg network.Msg 133 | msg.To = to 134 | msg.XType = network.Msg_vnc_scroll 135 | msg.LinkId = id 136 | msg.Payload = &network.Msg_Vscroll{ 137 | Vscroll: &network.VncScroll{ 138 | X: x, 139 | Y: y, 140 | }, 141 | } 142 | select { 143 | case conn.write <- &msg: 144 | case <-time.After(conn.cfg.WriteTimeout): 145 | } 146 | } 147 | 148 | // SendVNCClipboardData send vnc clipboard data 149 | func (conn *Conn) SendVNCClipboardData(to string, id string, set bool, data string) { 150 | var msg network.Msg 151 | msg.To = to 152 | msg.XType = network.Msg_vnc_clipboard 153 | msg.LinkId = id 154 | msg.Payload = &network.Msg_Vclipboard{ 155 | Vclipboard: &network.VncClipboard{ 156 | Set: set, 157 | XType: network.VncClipboard_text, 158 | Payload: &network.VncClipboard_Data{ 159 | Data: data, 160 | }, 161 | }, 162 | } 163 | select { 164 | case conn.write <- &msg: 165 | case <-time.After(conn.cfg.WriteTimeout): 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /code/client/conn/utils.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | func dup(data []byte) []byte { 4 | ret := make([]byte, len(data)) 5 | copy(ret, data) 6 | return ret 7 | } 8 | -------------------------------------------------------------------------------- /code/client/dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | /assets.go -------------------------------------------------------------------------------- /code/client/dashboard/dashboard.go: -------------------------------------------------------------------------------- 1 | package dashboard 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/pprof" 7 | 8 | "github.com/lwch/natpass/code/client/conn" 9 | "github.com/lwch/natpass/code/client/global" 10 | "github.com/lwch/natpass/code/client/rule" 11 | ) 12 | 13 | // Dashboard dashboard object 14 | type Dashboard struct { 15 | cfg *global.Configure 16 | conn *conn.Conn 17 | mgr *rule.Mgr 18 | Version string 19 | } 20 | 21 | // New create dashboard object 22 | func New(cfg *global.Configure, conn *conn.Conn, mgr *rule.Mgr, version string) *Dashboard { 23 | return &Dashboard{ 24 | cfg: cfg, 25 | conn: conn, 26 | mgr: mgr, 27 | Version: version, 28 | } 29 | } 30 | 31 | // ListenAndServe listen and serve http handler 32 | func (db *Dashboard) ListenAndServe(addr string, port uint16) error { 33 | mux := http.NewServeMux() 34 | mux.HandleFunc("/api/info", db.Info) 35 | mux.HandleFunc("/api/rules", db.Rules) 36 | mux.HandleFunc("/debug/pprof/", pprof.Index) 37 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 38 | mux.HandleFunc("/", db.Render) 39 | svr := &http.Server{ 40 | Addr: fmt.Sprintf("%s:%d", addr, port), 41 | Handler: mux, 42 | } 43 | return svr.ListenAndServe() 44 | } 45 | -------------------------------------------------------------------------------- /code/client/dashboard/h_info.go: -------------------------------------------------------------------------------- 1 | package dashboard 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/lwch/natpass/code/client/rule" 8 | ) 9 | 10 | // Info information data 11 | func (db *Dashboard) Info(w http.ResponseWriter, r *http.Request) { 12 | var ret struct { 13 | Rules int `json:"rules"` 14 | VirtualLinks int `json:"virtual_links"` 15 | Session int `json:"sessions"` 16 | } 17 | ret.Rules = len(db.cfg.Rules) 18 | db.mgr.Range(func(t rule.Rule) { 19 | lr, ok := t.(rule.LinkedRule) 20 | if ok { 21 | n := len(lr.GetLinks()) 22 | ret.VirtualLinks += n 23 | if t.GetTypeName() == "shell" || 24 | t.GetTypeName() == "vnc" { 25 | ret.Session += n 26 | } 27 | } 28 | }) 29 | w.Header().Set("Content-Type", "application/json") 30 | json.NewEncoder(w).Encode(ret) 31 | } 32 | -------------------------------------------------------------------------------- /code/client/dashboard/h_render.go: -------------------------------------------------------------------------------- 1 | package dashboard 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "mime" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "text/template" 12 | ) 13 | 14 | // Render render asset file 15 | func (db *Dashboard) Render(w http.ResponseWriter, r *http.Request) { 16 | dir := strings.TrimPrefix(r.URL.Path, "/") 17 | if filepath.Ext(dir) == ".html" { 18 | db.renderHTML(w, r, dir) 19 | return 20 | } 21 | data, err := Asset(dir) 22 | if err == nil { 23 | ctype := mime.TypeByExtension(filepath.Ext(dir)) 24 | if ctype == "" { 25 | ctype = http.DetectContentType(data) 26 | } 27 | w.Header().Set("Content-Type", ctype) 28 | io.Copy(w, bytes.NewReader(data)) 29 | return 30 | } 31 | db.renderHTML(w, r, "index.html") 32 | } 33 | 34 | func (db *Dashboard) renderHTML(w http.ResponseWriter, r *http.Request, name string) { 35 | data, err := Asset(name) 36 | if err != nil { 37 | if os.IsNotExist(err) { 38 | http.NotFound(w, r) 39 | return 40 | } 41 | http.Error(w, err.Error(), http.StatusInternalServerError) 42 | return 43 | } 44 | 45 | header, _ := Asset("templates/header.html") 46 | aside, _ := Asset("templates/aside.html") 47 | footer, _ := Asset("templates/footer.html") 48 | 49 | tpl := template.New("all") 50 | tpl, err = tpl.Parse(string(header)) 51 | if err != nil { 52 | http.Error(w, err.Error(), http.StatusInternalServerError) 53 | return 54 | } 55 | tpl, err = tpl.Parse(string(aside)) 56 | if err != nil { 57 | http.Error(w, err.Error(), http.StatusInternalServerError) 58 | return 59 | } 60 | tpl, err = tpl.Parse(string(footer)) 61 | if err != nil { 62 | http.Error(w, err.Error(), http.StatusInternalServerError) 63 | return 64 | } 65 | tpl, err = tpl.Parse(string(data)) 66 | if err != nil { 67 | http.Error(w, err.Error(), http.StatusInternalServerError) 68 | return 69 | } 70 | 71 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 72 | tpl.Execute(w, db) 73 | } 74 | -------------------------------------------------------------------------------- /code/client/dashboard/h_rules.go: -------------------------------------------------------------------------------- 1 | package dashboard 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/lwch/natpass/code/client/rule" 8 | ) 9 | 10 | // Rules get rule list 11 | func (db *Dashboard) Rules(w http.ResponseWriter, r *http.Request) { 12 | type link struct { 13 | ID string `json:"id"` 14 | SendBytes uint64 `json:"send_bytes"` 15 | SendPacket uint64 `json:"send_packet"` 16 | RecvBytes uint64 `json:"recv_bytes"` 17 | RecvPacket uint64 `json:"recv_packet"` 18 | } 19 | type item struct { 20 | Name string `json:"name"` 21 | Remote string `json:"remote,omitempty"` 22 | Port uint16 `json:"port"` 23 | Type string `json:"type"` 24 | Links []link `json:"links"` 25 | } 26 | var ret []item 27 | db.mgr.Range(func(t rule.Rule) { 28 | lr, isLR := t.(rule.LinkedRule) 29 | var it item 30 | it.Name = t.GetName() 31 | if isLR { 32 | it.Remote = lr.GetRemote() 33 | } 34 | it.Port = t.GetPort() 35 | it.Type = t.GetTypeName() 36 | if isLR { 37 | for _, l := range lr.GetLinks() { 38 | var lk link 39 | lk.ID = l.GetID() 40 | lk.RecvBytes, lk.SendBytes = l.GetBytes() 41 | lk.RecvPacket, lk.SendPacket = l.GetPackets() 42 | it.Links = append(it.Links, lk) 43 | } 44 | } 45 | ret = append(ret, it) 46 | }) 47 | w.Header().Set("Content-Type", "application/json") 48 | json.NewEncoder(w).Encode(ret) 49 | } 50 | -------------------------------------------------------------------------------- /code/client/global/conf.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/lwch/natpass/code/hash" 10 | "github.com/lwch/natpass/code/utils" 11 | "github.com/lwch/runtime" 12 | "github.com/lwch/yaml" 13 | ) 14 | 15 | // Rule rule config 16 | type Rule struct { 17 | Name string `yaml:"name"` 18 | Target string `yaml:"target"` 19 | Type string `yaml:"type"` 20 | LocalAddr string `yaml:"local_addr"` 21 | LocalPort uint16 `yaml:"local_port"` 22 | // shell 23 | Exec string `yaml:"exec"` 24 | Env []string `yaml:"env"` 25 | // vnc 26 | Fps uint32 `yaml:"fps"` 27 | } 28 | 29 | // Configure client configure 30 | type Configure struct { 31 | ID string 32 | Server string 33 | UseSSL bool 34 | SSLInsecure bool 35 | Hasher *hash.Hasher 36 | Links int 37 | LogDir string 38 | LogSize utils.Bytes 39 | LogRotate int 40 | ReadTimeout time.Duration 41 | WriteTimeout time.Duration 42 | DashboardEnabled bool 43 | DashboardListen string 44 | DashboardPort uint16 45 | Rules []*Rule 46 | CodeDir string 47 | } 48 | 49 | // LoadConf load configure file 50 | func LoadConf(dir string) *Configure { 51 | var cfg struct { 52 | ID string `yaml:"id"` 53 | Server string `yaml:"server"` 54 | Secret string `yaml:"secret"` 55 | SSL struct { 56 | Enabled bool `yaml:"enabled"` 57 | Insecure bool `yaml:"insecure"` 58 | } `yaml:"ssl"` 59 | Link struct { 60 | ReadTimeout time.Duration `yaml:"read_timeout"` 61 | WriteTimeout time.Duration `yaml:"write_timeout"` 62 | } `yaml:"link"` 63 | Log struct { 64 | Dir string `yaml:"dir"` 65 | Size utils.Bytes `yaml:"size"` 66 | Rotate int `yaml:"rotate"` 67 | } `yaml:"log"` 68 | Dashboard struct { 69 | Enabled bool `yaml:"enabled"` 70 | Listen string `yaml:"listen"` 71 | Port uint16 `yaml:"port"` 72 | } `yaml:"dashboard"` 73 | Rules []*Rule `yaml:"rules"` 74 | CodeDir string `yaml:"codedir"` 75 | } 76 | cfg.ID = "unset" 77 | cfg.Server = "127.0.0.1:6154" 78 | cfg.SSL.Enabled = false 79 | cfg.SSL.Insecure = false 80 | cfg.Dashboard.Enabled = true 81 | cfg.Dashboard.Listen = "0.0.0.0" 82 | cfg.Dashboard.Port = 8080 83 | cfg.Secret = "0123456789" 84 | cfg.Link.ReadTimeout = time.Second 85 | cfg.Link.WriteTimeout = time.Second 86 | cfg.Log.Dir = "./logs" 87 | cfg.Log.Size = 50 * 1024 * 1024 88 | cfg.Log.Rotate = 7 89 | cfg.CodeDir = "./code" 90 | runtime.Assert(yaml.Decode(dir, &cfg)) 91 | for i, t := range cfg.Rules { 92 | switch t.Type { 93 | case "shell", "vnc", "bench", "code-server": 94 | default: 95 | panic(fmt.Sprintf("unsupported type: %s", t.Type)) 96 | } 97 | cfg.Rules[i] = t 98 | } 99 | if cfg.Link.ReadTimeout <= 0 { 100 | cfg.Link.ReadTimeout = 5 * time.Second 101 | } 102 | if cfg.Link.WriteTimeout <= 0 { 103 | cfg.Link.WriteTimeout = 5 * time.Second 104 | } 105 | if !filepath.IsAbs(cfg.Log.Dir) { 106 | dir, err := os.Executable() 107 | runtime.Assert(err) 108 | cfg.Log.Dir = filepath.Join(filepath.Dir(dir), cfg.Log.Dir) 109 | } 110 | if !filepath.IsAbs(cfg.CodeDir) { 111 | dir, err := os.Executable() 112 | runtime.Assert(err) 113 | cfg.CodeDir = filepath.Join(filepath.Dir(dir), cfg.CodeDir) 114 | } 115 | return &Configure{ 116 | ID: cfg.ID, 117 | Server: cfg.Server, 118 | UseSSL: cfg.SSL.Enabled, 119 | SSLInsecure: cfg.SSL.Insecure, 120 | Hasher: hash.New(cfg.Secret, 60), 121 | ReadTimeout: cfg.Link.ReadTimeout, 122 | WriteTimeout: cfg.Link.WriteTimeout, 123 | LogDir: cfg.Log.Dir, 124 | LogSize: cfg.Log.Size, 125 | LogRotate: cfg.Log.Rotate, 126 | DashboardEnabled: cfg.Dashboard.Enabled, 127 | DashboardListen: cfg.Dashboard.Listen, 128 | DashboardPort: cfg.Dashboard.Port, 129 | Rules: cfg.Rules, 130 | CodeDir: cfg.CodeDir, 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /code/client/global/port.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/lwch/runtime" 7 | ) 8 | 9 | // GeneratePort generate port for listen 10 | func GeneratePort() uint16 { 11 | l, err := net.ListenTCP("tcp", &net.TCPAddr{}) 12 | runtime.Assert(err) 13 | defer l.Close() 14 | return uint16(l.Addr().(*net.TCPAddr).Port) 15 | } 16 | -------------------------------------------------------------------------------- /code/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/lwch/natpass/code/client/app" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ( 12 | version string = "0.0.0" 13 | gitHash string 14 | gitReversion string 15 | buildTime string 16 | ) 17 | 18 | var a = app.NewApp() 19 | 20 | var rootCmd = &cobra.Command{ 21 | Use: "np-cli", 22 | Short: "natpass client", 23 | Run: a.Run, 24 | } 25 | 26 | var installCmd = &cobra.Command{ 27 | Use: "install", 28 | Short: "register service", 29 | Run: a.Install, 30 | } 31 | 32 | var uninstallCmd = &cobra.Command{ 33 | Use: "uninstall", 34 | Short: "unregister service", 35 | Run: a.Uninstall, 36 | } 37 | 38 | var startCmd = &cobra.Command{ 39 | Use: "start", 40 | Short: "start service", 41 | Run: a.Start, 42 | } 43 | 44 | var stopCmd = &cobra.Command{ 45 | Use: "stop", 46 | Short: "stop service", 47 | Run: a.Stop, 48 | } 49 | 50 | var restartCmd = &cobra.Command{ 51 | Use: "restart", 52 | Short: "restart service", 53 | Run: a.Restart, 54 | } 55 | 56 | var statusCmd = &cobra.Command{ 57 | Use: "status", 58 | Short: "show service status", 59 | Run: a.Status, 60 | } 61 | 62 | var vncCmd = &cobra.Command{ 63 | Use: "vnc", 64 | Short: "vnc child process", 65 | Run: a.Vnc, 66 | } 67 | 68 | var versionCmd = &cobra.Command{ 69 | Use: "version", 70 | Short: "show version info", 71 | Run: func(*cobra.Command, []string) { 72 | fmt.Printf("version: v%s\ntime: %s\ncommit: %s.%s\n", 73 | version, 74 | buildTime, 75 | gitHash, gitReversion) 76 | os.Exit(0) 77 | }, 78 | } 79 | 80 | func main() { 81 | app.Version = version 82 | installCmd.Flags().StringVarP(&app.ConfDir, "conf", "c", "", "configure file path") 83 | installCmd.Flags().StringVarP(&app.User, "user", "u", "", "service user") 84 | installCmd.MarkFlagRequired("conf") 85 | rootCmd.AddCommand(installCmd, uninstallCmd) 86 | rootCmd.AddCommand(startCmd, stopCmd, restartCmd, statusCmd) 87 | rootCmd.AddCommand(versionCmd) 88 | 89 | vncCmd.Flags().StringVarP(&app.ConfDir, "conf", "c", "", "configure file path") 90 | vncCmd.Flags().StringVar(&app.VncName, "name", "", "name for log file") 91 | vncCmd.Flags().Uint16Var(&app.VncPort, "port", 6155, "listen port") 92 | vncCmd.Flags().BoolVar(&app.VncCursor, "cursor", false, "show cursor") 93 | vncCmd.MarkFlagRequired("conf") 94 | vncCmd.MarkFlagRequired("name") 95 | vncCmd.Hidden = true 96 | rootCmd.AddCommand(vncCmd) 97 | 98 | rootCmd.CompletionOptions.DisableDefaultCmd = true 99 | rootCmd.Flags().StringVarP(&app.ConfDir, "conf", "c", "", "configure file path") 100 | rootCmd.MarkFlagRequired("conf") 101 | err := rootCmd.Execute() 102 | if err != nil { 103 | fmt.Println(err.Error()) 104 | os.Exit(1) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /code/client/rule/bench/bench.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | 8 | "github.com/lwch/logging" 9 | "github.com/lwch/natpass/code/client/conn" 10 | "github.com/lwch/natpass/code/client/global" 11 | "github.com/lwch/natpass/code/client/rule" 12 | "github.com/lwch/runtime" 13 | ) 14 | 15 | // Bench benchmark handler 16 | type Bench struct { 17 | Name string 18 | cfg *global.Rule 19 | } 20 | 21 | // Link bench link 22 | type Link struct { 23 | id string 24 | } 25 | 26 | // GetID get link id 27 | func (link *Link) GetID() string { 28 | return link.id 29 | } 30 | 31 | // GetBytes get send and recv bytes 32 | func (link *Link) GetBytes() (uint64, uint64) { 33 | return 0, 0 34 | } 35 | 36 | // GetPackets get send and recv packets 37 | func (link *Link) GetPackets() (uint64, uint64) { 38 | return 0, 0 39 | } 40 | 41 | // New new benchmark handler 42 | func New(cfg *global.Rule) *Bench { 43 | return &Bench{ 44 | Name: cfg.Name, 45 | cfg: cfg, 46 | } 47 | } 48 | 49 | // NewLink new link 50 | func (bench *Bench) NewLink(id, remote string, localConn net.Conn, remoteConn *conn.Conn) rule.Link { 51 | return &Link{id: id} 52 | } 53 | 54 | // GetName get bench rule name 55 | func (bench *Bench) GetName() string { 56 | return bench.Name 57 | } 58 | 59 | // GetTypeName get bench rule type name 60 | func (bench *Bench) GetTypeName() string { 61 | return "bench" 62 | } 63 | 64 | // GetTarget get target of this rule 65 | func (bench *Bench) GetTarget() string { 66 | return bench.cfg.Target 67 | } 68 | 69 | // GetLinks get rule links 70 | func (bench *Bench) GetLinks() []rule.Link { 71 | return nil 72 | } 73 | 74 | // GetRemote get remote target name 75 | func (bench *Bench) GetRemote() string { 76 | return bench.cfg.Target 77 | } 78 | 79 | // GetPort get listen port 80 | func (bench *Bench) GetPort() uint16 { 81 | return bench.cfg.LocalPort 82 | } 83 | 84 | // Handle handle shell 85 | func (bench *Bench) Handle(conn *conn.Conn) { 86 | defer func() { 87 | if err := recover(); err != nil { 88 | logging.Error("close shell: %s, err=%v", bench.Name, err) 89 | } 90 | }() 91 | mux := http.NewServeMux() 92 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 93 | bench.http(conn, w, r) 94 | }) 95 | if bench.cfg.LocalPort == 0 { 96 | bench.cfg.LocalPort = global.GeneratePort() 97 | logging.Info("generate port for %s: %d", bench.Name, bench.cfg.LocalPort) 98 | } 99 | svr := &http.Server{ 100 | Addr: fmt.Sprintf("%s:%d", bench.cfg.LocalAddr, bench.cfg.LocalPort), 101 | Handler: mux, 102 | } 103 | runtime.Assert(svr.ListenAndServe()) 104 | } 105 | -------------------------------------------------------------------------------- /code/client/rule/bench/h_http.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/lwch/logging" 8 | "github.com/lwch/natpass/code/client/conn" 9 | "github.com/lwch/runtime" 10 | ) 11 | 12 | func (bench *Bench) http(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 13 | id, err := runtime.UUID(16, "0123456789abcdef") 14 | if err != nil { 15 | logging.Error("failed to generate link_id for bench: %s, err=%v", 16 | bench.Name, err) 17 | http.Error(w, err.Error(), http.StatusInternalServerError) 18 | return 19 | } 20 | conn.AddLink(id) 21 | conn.SendConnectReq(id, bench.cfg) 22 | ch := conn.ChanRead(id) 23 | <-ch 24 | fmt.Fprint(w, id) 25 | } 26 | -------------------------------------------------------------------------------- /code/client/rule/code/.gitignore: -------------------------------------------------------------------------------- 1 | /assets.go -------------------------------------------------------------------------------- /code/client/rule/code/h_forward.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/lwch/logging" 8 | "github.com/lwch/natpass/code/client/conn" 9 | ) 10 | 11 | // Forward forward code-server requests 12 | func (code *Code) Forward(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 13 | srcPath := r.URL.Path 14 | srcQuery := r.URL.Query() 15 | name := strings.TrimPrefix(r.URL.Path, "/forward/") 16 | name = name[:strings.Index(name, "/")] 17 | 18 | r.URL.Path = strings.TrimPrefix(r.URL.Path, "/forward/"+name) 19 | if len(r.URL.Path) == 0 { 20 | r.URL.Path = "/" 21 | } 22 | 23 | var id string 24 | 25 | const argName = "natpass_connection_id" 26 | 27 | if r.URL.Path == "/" && len(r.FormValue(argName)) == 0 { 28 | var err error 29 | id, err = code.new(conn) 30 | if err != nil { 31 | logging.Error("can not create workspace for [%s]: %v", code.Name, err) 32 | http.Error(w, err.Error(), http.StatusBadGateway) 33 | return 34 | } 35 | http.SetCookie(w, &http.Cookie{ 36 | Name: "__NATPASS_CONNECTION_ID__", 37 | Value: id, 38 | }) 39 | srcQuery.Set(argName, id) 40 | http.Redirect(w, r, srcPath+"?"+srcQuery.Encode(), http.StatusTemporaryRedirect) 41 | return 42 | } 43 | 44 | cookie, err := r.Cookie("__NATPASS_CONNECTION_ID__") 45 | if err != nil { 46 | logging.Error("get connection id: %v", err) 47 | http.Error(w, err.Error(), http.StatusBadRequest) 48 | return 49 | } 50 | id = cookie.Value 51 | 52 | code.RLock() 53 | workspace := code.workspace[id] 54 | code.RUnlock() 55 | 56 | if workspace == nil { 57 | http.NotFound(w, r) 58 | return 59 | } 60 | 61 | if code.isWebsocket(r) { 62 | code.handleWebsocket(workspace, w, r) 63 | } else { 64 | code.handleRequest(conn, workspace, w, r) 65 | } 66 | } 67 | 68 | func (code *Code) isWebsocket(r *http.Request) bool { 69 | upgrade := r.Header.Get("Connection") 70 | return upgrade == "Upgrade" 71 | } 72 | -------------------------------------------------------------------------------- /code/client/rule/code/h_forward_request.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/lwch/logging" 10 | "github.com/lwch/natpass/code/client/conn" 11 | "github.com/lwch/natpass/code/network" 12 | ) 13 | 14 | func (code *Code) handleRequest(conn *conn.Conn, workspace *Workspace, w http.ResponseWriter, r *http.Request) { 15 | reqID, err := workspace.SendRequest(r) 16 | if err != nil { 17 | logging.Error("send_request: %v", err) 18 | http.Error(w, err.Error(), http.StatusBadGateway) 19 | return 20 | } 21 | defer workspace.closeMessage(reqID) 22 | resp := workspace.onResponse(reqID) 23 | if resp == nil { 24 | logging.Error("waiting for [%s] [%s] no response for request, uri=%s, request_id=%d", 25 | workspace.id, workspace.name, r.URL.Path, reqID) 26 | http.Error(w, "no response", http.StatusInternalServerError) 27 | return 28 | } 29 | 30 | if resp.GetXType() != network.Msg_code_response_hdr { 31 | logging.Error("got invalid message type [%s] [%s]: %s", 32 | workspace.id, workspace.name, resp.GetXType().String()) 33 | http.Error(w, "invalid message type", http.StatusServiceUnavailable) 34 | return 35 | } 36 | 37 | hdr := resp.GetCsrepHdr() 38 | for key, values := range hdr.GetHeader() { 39 | for _, v := range values.GetValues() { 40 | w.Header().Add(key, v) 41 | } 42 | } 43 | 44 | var idx uint32 45 | var buf bytes.Buffer 46 | for { 47 | msg := workspace.onResponse(reqID) 48 | if msg == nil { 49 | logging.Error("no response [%s] [%s]", workspace.id, workspace.name) 50 | http.Error(w, "no response", http.StatusBadGateway) 51 | return 52 | } 53 | if msg.GetXType() != network.Msg_code_response_body { 54 | logging.Error("got invalid message type [%s] [%s]: %s", 55 | workspace.id, workspace.name, resp.GetXType().String()) 56 | http.Error(w, "invalid message type", http.StatusServiceUnavailable) 57 | return 58 | } 59 | resp := msg.GetCsrepBody() 60 | if resp.GetIndex() != idx { 61 | logging.Error("loss data [%s] [%s]", workspace.id, workspace.name) 62 | http.Error(w, "loss data", http.StatusResetContent) 63 | return 64 | } 65 | if resp.GetMask()&1 == 0 { 66 | logging.Error("read data [%s] [%s]: %s", workspace.id, workspace.name, string(resp.GetBody())) 67 | http.Error(w, fmt.Sprintf("read error: %s", string(resp.GetBody())), http.StatusResetContent) 68 | return 69 | } 70 | _, err = io.Copy(&buf, bytes.NewReader(resp.GetBody())) 71 | if err != nil { 72 | logging.Error("write body: %v", err) 73 | http.Error(w, fmt.Sprintf("save data: %v", err), http.StatusInternalServerError) 74 | return 75 | } 76 | if resp.GetMask()&2 > 0 { 77 | w.WriteHeader(int(hdr.GetCode())) 78 | io.Copy(w, &buf) 79 | return 80 | } 81 | idx++ 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /code/client/rule/code/h_forward_websocket.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/lwch/logging" 9 | "github.com/lwch/natpass/code/network" 10 | ) 11 | 12 | var upgrader = websocket.Upgrader{ 13 | EnableCompression: true, 14 | } 15 | 16 | func (code *Code) handleWebsocket(workspace *Workspace, w http.ResponseWriter, r *http.Request) { 17 | reqID, err := workspace.SendConnect(r) 18 | if err != nil { 19 | logging.Error("send_connect: %v", err) 20 | http.Error(w, err.Error(), http.StatusBadGateway) 21 | return 22 | } 23 | defer workspace.closeMessage(reqID) 24 | resp := workspace.onResponse(reqID) 25 | if resp == nil { 26 | logging.Error("waiting for [%s] [%s] no response for websocket, request_id=%d", 27 | workspace.id, workspace.name, reqID) 28 | http.Error(w, "no response", http.StatusInternalServerError) 29 | return 30 | } 31 | 32 | if resp.GetXType() != network.Msg_code_connect_response { 33 | logging.Error("got invalid message type [%s] [%s]: %s", 34 | workspace.id, workspace.name, resp.GetXType().String()) 35 | http.Error(w, "invalid message type", http.StatusServiceUnavailable) 36 | return 37 | } 38 | 39 | response := resp.GetCsconnRep() 40 | if !response.GetOk() { 41 | logging.Error("can not create websocket connection [%s] [%s]: %s", 42 | workspace.id, workspace.name, response.GetMsg()) 43 | http.Error(w, response.GetMsg(), http.StatusBadGateway) 44 | return 45 | } 46 | 47 | local, err := upgrader.Upgrade(w, r, nil) 48 | if err != nil { 49 | logging.Error("upgrade websocket connection [%s] [%s]: %v", 50 | workspace.id, workspace.name, err) 51 | http.Error(w, err.Error(), http.StatusBadGateway) 52 | return 53 | } 54 | defer local.Close() 55 | 56 | var wg sync.WaitGroup 57 | 58 | wg.Add(2) 59 | go workspace.ws2remote(&wg, reqID, local) 60 | go workspace.remote2ws(&wg, reqID, local) 61 | wg.Wait() 62 | } 63 | -------------------------------------------------------------------------------- /code/client/rule/code/h_info.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/lwch/logging" 8 | ) 9 | 10 | // Info get workspace info 11 | func (code *Code) Info(w http.ResponseWriter, r *http.Request) { 12 | id := r.FormValue("id") 13 | code.RLock() 14 | workspace := code.workspace[id] 15 | code.RUnlock() 16 | if workspace == nil { 17 | http.NotFound(w, r) 18 | return 19 | } 20 | data, err := json.Marshal(map[string]interface{}{ 21 | "name": code.Name, 22 | "send_bytes": workspace.sendBytes, 23 | "send_packet": workspace.sendPacket, 24 | "recv_bytes": workspace.recvBytes, 25 | "recv_packet": workspace.recvPacket, 26 | }) 27 | if err != nil { 28 | logging.Error("marshal: %v", err) 29 | http.Error(w, err.Error(), http.StatusInternalServerError) 30 | return 31 | } 32 | w.Header().Set("Content-Type", "application/json") 33 | w.Write(data) 34 | } 35 | -------------------------------------------------------------------------------- /code/client/rule/code/h_new.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/lwch/natpass/code/client/conn" 7 | ) 8 | 9 | // New new code-server workspace 10 | func (code *Code) New(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 11 | w.Write([]byte(code.cfg.Name)) 12 | } 13 | -------------------------------------------------------------------------------- /code/client/rule/code/h_render.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "mime" 7 | "net/http" 8 | "path/filepath" 9 | "strings" 10 | "text/template" 11 | 12 | "github.com/lwch/natpass/code/client/conn" 13 | ) 14 | 15 | // Render render code-server 16 | func (code *Code) Render(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 17 | dir := strings.TrimPrefix(r.URL.Path, "/") 18 | data, err := Asset(dir) 19 | if err == nil { 20 | ctype := mime.TypeByExtension(filepath.Ext(dir)) 21 | if ctype == "" { 22 | ctype = http.DetectContentType(data) 23 | } 24 | w.Header().Set("Content-Type", ctype) 25 | io.Copy(w, bytes.NewReader(data)) 26 | return 27 | } 28 | data, _ = Asset("index.html") 29 | tpl, err := template.New("all").Parse(string(data)) 30 | if err != nil { 31 | http.Error(w, err.Error(), http.StatusInternalServerError) 32 | return 33 | } 34 | tpl.Execute(w, code) 35 | } 36 | -------------------------------------------------------------------------------- /code/client/rule/code/workspace_local_request.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "strings" 7 | "sync/atomic" 8 | 9 | "github.com/lwch/logging" 10 | "github.com/lwch/natpass/code/network" 11 | ) 12 | 13 | // SendRequest send request from local node 14 | func (ws *Workspace) SendRequest(r *http.Request) (uint64, error) { 15 | reqID := atomic.AddUint64(&ws.requestID, 1) 16 | body, err := ioutil.ReadAll(r.Body) 17 | if err != nil { 18 | logging.Error("send request for workspace [%s] [%s]: %v", ws.id, ws.name, err) 19 | return 0, err 20 | } 21 | ws.Lock() 22 | ws.onMessage[reqID] = make(chan *network.Msg, 1024) 23 | ws.Unlock() 24 | send := ws.remote.SendCodeRequest(ws.target, ws.id, reqID, 25 | r.Method, r.URL.RequestURI(), body, r.Header) 26 | ws.sendBytes += send 27 | ws.sendPacket++ 28 | return reqID, nil 29 | } 30 | 31 | // SendConnect send websocket connect action from local node 32 | func (ws *Workspace) SendConnect(r *http.Request) (uint64, error) { 33 | reqID := atomic.AddUint64(&ws.requestID, 1) 34 | ws.Lock() 35 | ws.onMessage[reqID] = make(chan *network.Msg, 1024) 36 | ws.Unlock() 37 | 38 | hdr := make(http.Header) 39 | for key, values := range r.Header { 40 | if strings.HasPrefix(key, "Sec-") { 41 | continue 42 | } 43 | for _, value := range values { 44 | hdr.Add(key, value) 45 | } 46 | } 47 | 48 | hdr.Del("Connection") 49 | hdr.Del("Upgrade") 50 | 51 | send := ws.remote.SendCodeConnect(ws.target, ws.id, reqID, 52 | r.URL.RequestURI(), hdr) 53 | ws.sendBytes += send 54 | ws.sendPacket++ 55 | return reqID, nil 56 | } 57 | -------------------------------------------------------------------------------- /code/client/rule/mgr.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | 7 | "github.com/lwch/natpass/code/client/conn" 8 | ) 9 | 10 | // Link link interface 11 | type Link interface { 12 | GetID() string 13 | // GetBytes rx, tx 14 | GetBytes() (uint64, uint64) 15 | // GetPackets rx, tx 16 | GetPackets() (uint64, uint64) 17 | } 18 | 19 | // Rule rule interface 20 | type Rule interface { 21 | GetName() string 22 | GetPort() uint16 23 | GetTypeName() string 24 | } 25 | 26 | // LinkedRule linked rule interface 27 | type LinkedRule interface { 28 | NewLink(id, remote string, localConn net.Conn, remoteConn *conn.Conn) Link 29 | GetRemote() string 30 | GetTarget() string 31 | GetLinks() []Link 32 | OnDisconnect(string) 33 | } 34 | 35 | // Mgr rule manager 36 | type Mgr struct { 37 | sync.RWMutex 38 | rules []Rule 39 | } 40 | 41 | // New new rule manager 42 | func New() *Mgr { 43 | return &Mgr{} 44 | } 45 | 46 | // Add add rule 47 | func (mgr *Mgr) Add(rule Rule) { 48 | mgr.Lock() 49 | defer mgr.Unlock() 50 | mgr.rules = append(mgr.rules, rule) 51 | } 52 | 53 | // GetLinked get rule by name 54 | func (mgr *Mgr) GetLinked(name, remote string) LinkedRule { 55 | mgr.RLock() 56 | defer mgr.RUnlock() 57 | for _, r := range mgr.rules { 58 | lr, ok := r.(LinkedRule) 59 | if !ok { 60 | continue 61 | } 62 | if r.GetName() == name && lr.GetRemote() == remote { 63 | return lr 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | // Range range rules 70 | func (mgr *Mgr) Range(fn func(Rule)) { 71 | mgr.RLock() 72 | defer mgr.RUnlock() 73 | for _, t := range mgr.rules { 74 | fn(t) 75 | } 76 | } 77 | 78 | // OnDisconnect on disconnect message 79 | func (mgr *Mgr) OnDisconnect(id string) { 80 | var links []LinkedRule 81 | mgr.Range(func(r Rule) { 82 | if lr, ok := r.(LinkedRule); ok { 83 | links = append(links, lr) 84 | } 85 | }) 86 | for _, link := range links { 87 | go link.OnDisconnect(id) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /code/client/rule/shell/.gitignore: -------------------------------------------------------------------------------- 1 | /assets.go -------------------------------------------------------------------------------- /code/client/rule/shell/exec_windows.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | // Exec execute shell command 10 | func (link *Link) Exec() error { 11 | var cmd *exec.Cmd 12 | if len(link.parent.cfg.Exec) > 0 { 13 | cmd = exec.Command(link.parent.cfg.Exec) 14 | } 15 | if cmd == nil { 16 | dir, err := exec.LookPath("powershell") 17 | if err == nil { 18 | cmd = exec.Command(dir) 19 | } 20 | } 21 | if cmd == nil { 22 | dir, err := exec.LookPath("cmd") 23 | if err == nil { 24 | cmd = exec.Command(dir) 25 | } 26 | } 27 | if cmd == nil { 28 | return errors.New("no shell command supported") 29 | } 30 | cmd.Env = append(os.Environ(), link.parent.cfg.Env...) 31 | var err error 32 | link.stdin, err = cmd.StdinPipe() 33 | if err != nil { 34 | return err 35 | } 36 | link.stdout, err = cmd.StdoutPipe() 37 | if err != nil { 38 | return err 39 | } 40 | err = cmd.Start() 41 | if err != nil { 42 | return err 43 | } 44 | go cmd.Wait() // defunct process 45 | link.pid = cmd.Process.Pid 46 | return nil 47 | } 48 | 49 | func (link *Link) onClose() { 50 | if link.stdin != nil { 51 | link.stdin.Close() 52 | } 53 | if link.stdout != nil { 54 | link.stdout.Close() 55 | } 56 | } 57 | 58 | func (link *Link) resize(rows, cols uint32) { 59 | } 60 | -------------------------------------------------------------------------------- /code/client/rule/shell/exec_xx.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package shell 5 | 6 | import ( 7 | "errors" 8 | "os" 9 | "os/exec" 10 | 11 | "github.com/creack/pty" 12 | ) 13 | 14 | // Exec execute shell command 15 | func (link *Link) Exec() error { 16 | var cmd *exec.Cmd 17 | if len(link.parent.cfg.Exec) > 0 { 18 | cmd = exec.Command(link.parent.cfg.Exec) 19 | } 20 | if cmd == nil { 21 | dir, err := exec.LookPath("bash") 22 | if err == nil { 23 | cmd = exec.Command(dir) 24 | } 25 | } 26 | if cmd == nil { 27 | dir, err := exec.LookPath("sh") 28 | if err == nil { 29 | cmd = exec.Command(dir) 30 | } 31 | } 32 | if cmd == nil { 33 | return errors.New("no shell command supported") 34 | } 35 | cmd.Env = append(os.Environ(), link.parent.cfg.Env...) 36 | f, err := pty.Start(cmd) 37 | if err != nil { 38 | return err 39 | } 40 | go cmd.Wait() // defunct process 41 | link.stdin = f 42 | link.stdout = f 43 | link.pid = cmd.Process.Pid 44 | return nil 45 | } 46 | 47 | func (link *Link) onClose() { 48 | if link.stdin != nil { 49 | link.stdin.Close() 50 | } 51 | } 52 | 53 | func (link *Link) resize(rows, cols uint32) { 54 | if link.stdin != nil { 55 | pty.Setsize(link.stdin.(*os.File), &pty.Winsize{ 56 | Rows: uint16(rows), 57 | Cols: uint16(cols), 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /code/client/rule/shell/h_new.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/lwch/logging" 9 | "github.com/lwch/natpass/code/client/conn" 10 | "github.com/lwch/natpass/code/network" 11 | "github.com/lwch/runtime" 12 | ) 13 | 14 | // New new shell 15 | func (shell *Shell) New(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 16 | id, err := runtime.UUID(16, "0123456789abcdef") 17 | if err != nil { 18 | logging.Error("failed to generate link_id for shell: %s, err=%v", 19 | shell.Name, err) 20 | http.Error(w, err.Error(), http.StatusInternalServerError) 21 | return 22 | } 23 | link := shell.NewLink(id, shell.cfg.Target, nil, conn).(*Link) 24 | conn.SendConnectReq(id, shell.cfg) 25 | ch := conn.ChanRead(id) 26 | timeout := time.After(shell.readTimeout) 27 | var repMsg *network.Msg 28 | for { 29 | var msg *network.Msg 30 | select { 31 | case msg = <-ch: 32 | case <-timeout: 33 | logging.Error("create shell %s by rule %s failed, timtout", link.id, link.parent.Name) 34 | http.Error(w, "timeout", http.StatusBadGateway) 35 | return 36 | } 37 | if msg.GetXType() != network.Msg_connect_rep { 38 | conn.Requeue(id, msg) 39 | time.Sleep(shell.readTimeout / 10) 40 | continue 41 | } 42 | rep := msg.GetCrep() 43 | if !rep.GetOk() { 44 | logging.Error("create shell %s by rule %s failed, err=%s", 45 | link.id, link.parent.Name, rep.GetMsg()) 46 | http.Error(w, rep.GetMsg(), http.StatusBadGateway) 47 | return 48 | } 49 | repMsg = msg 50 | break 51 | } 52 | logging.Info("create link %s for shell rule [%s] from %s to %s", 53 | link.GetID(), shell.cfg.Name, 54 | repMsg.GetTo(), repMsg.GetFrom()) 55 | fmt.Fprint(w, id) 56 | } 57 | -------------------------------------------------------------------------------- /code/client/rule/shell/h_render.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "io" 7 | "mime" 8 | "net/http" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | // Render render asset file 14 | func (shell *Shell) Render(w http.ResponseWriter, r *http.Request) { 15 | dir := strings.TrimPrefix(r.URL.Path, "/") 16 | data, err := Asset(dir) 17 | if err == nil { 18 | ctype := mime.TypeByExtension(filepath.Ext(dir)) 19 | if ctype == "" { 20 | ctype = http.DetectContentType(data) 21 | } 22 | w.Header().Set("Content-Type", ctype) 23 | io.Copy(w, bytes.NewReader(data)) 24 | return 25 | } 26 | data, _ = Asset("index.html") 27 | tpl, err := template.New("all").Parse(string(data)) 28 | if err != nil { 29 | http.Error(w, err.Error(), http.StatusInternalServerError) 30 | return 31 | } 32 | tpl.Execute(w, shell) 33 | } 34 | -------------------------------------------------------------------------------- /code/client/rule/shell/h_resize.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/lwch/natpass/code/client/conn" 9 | ) 10 | 11 | // Resize resize terminal 12 | func (shell *Shell) Resize(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 13 | id := r.FormValue("id") 14 | rows := r.FormValue("rows") 15 | cols := r.FormValue("cols") 16 | 17 | shell.RLock() 18 | link := shell.links[id] 19 | shell.RUnlock() 20 | 21 | nRows, _ := strconv.ParseUint(rows, 0, 32) 22 | nCols, _ := strconv.ParseUint(cols, 0, 32) 23 | 24 | link.SendResize(uint32(nRows), uint32(nCols)) 25 | 26 | fmt.Fprint(w, "ok") 27 | } 28 | -------------------------------------------------------------------------------- /code/client/rule/shell/h_ws.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/gorilla/websocket" 9 | "github.com/lwch/logging" 10 | "github.com/lwch/natpass/code/client/conn" 11 | "github.com/lwch/natpass/code/network" 12 | "github.com/lwch/natpass/code/utils" 13 | "google.golang.org/protobuf/proto" 14 | ) 15 | 16 | var upgrader = websocket.Upgrader{} 17 | 18 | // WS websocket for forward data 19 | func (shell *Shell) WS(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 20 | id := strings.TrimPrefix(r.URL.Path, "/ws/") 21 | 22 | local, err := upgrader.Upgrade(w, r, nil) 23 | if err != nil { 24 | logging.Error("upgrade websocket failed: %s, err=%v", shell.Name, err) 25 | http.Error(w, err.Error(), http.StatusServiceUnavailable) 26 | return 27 | } 28 | defer local.Close() 29 | 30 | var wg sync.WaitGroup 31 | wg.Add(2) 32 | 33 | go func() { 34 | defer wg.Done() 35 | shell.localForward(id, local) 36 | }() 37 | go func() { 38 | defer wg.Done() 39 | shell.remoteForward(id, local) 40 | }() 41 | wg.Wait() 42 | } 43 | 44 | func (shell *Shell) localForward(id string, local *websocket.Conn) { 45 | defer utils.Recover("localForward") 46 | defer local.Close() 47 | shell.RLock() 48 | link := shell.links[id] 49 | shell.RUnlock() 50 | defer link.Close(true) 51 | for { 52 | _, data, err := local.ReadMessage() 53 | if err != nil { 54 | logging.Error("read local data for %s failed: %v", shell.Name, err) 55 | return 56 | } 57 | link.SendData(data) 58 | logging.Debug("local read %d bytes: name=%s, id=%s", len(data), shell.Name, id) 59 | } 60 | } 61 | 62 | func (shell *Shell) remoteForward(id string, local *websocket.Conn) { 63 | defer utils.Recover("remoteForward") 64 | defer local.Close() 65 | shell.RLock() 66 | link := shell.links[id] 67 | shell.RUnlock() 68 | ch := link.remote.ChanRead(id) 69 | defer link.Close(true) 70 | for { 71 | msg := <-ch 72 | if msg == nil { 73 | return 74 | } 75 | data, _ := proto.Marshal(msg) 76 | link.recvBytes += uint64(len(data)) 77 | link.recvPacket++ 78 | switch msg.GetXType() { 79 | case network.Msg_shell_data: 80 | err := local.WriteMessage(websocket.TextMessage, msg.GetSdata().GetData()) 81 | if err != nil { 82 | logging.Error("write data for %s failed: %v", shell.Name, err) 83 | return 84 | } 85 | logging.Debug("remote read %d bytes: name=%s, id=%s", 86 | len(msg.GetSdata().GetData()), shell.Name, id) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /code/client/rule/shell/link.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/lwch/logging" 8 | "github.com/lwch/natpass/code/client/conn" 9 | "github.com/lwch/natpass/code/network" 10 | "github.com/lwch/natpass/code/utils" 11 | "golang.org/x/text/encoding/simplifiedchinese" 12 | "google.golang.org/protobuf/proto" 13 | ) 14 | 15 | // Link shell link 16 | type Link struct { 17 | parent *Shell 18 | id string // link id 19 | target string // target id 20 | remote *conn.Conn 21 | // in remote 22 | pid int 23 | stdin io.WriteCloser 24 | stdout io.ReadCloser 25 | // runtime 26 | sendBytes uint64 27 | recvBytes uint64 28 | sendPacket uint64 29 | recvPacket uint64 30 | } 31 | 32 | // GetID get link id 33 | func (link *Link) GetID() string { 34 | return link.id 35 | } 36 | 37 | // GetBytes get send and recv bytes 38 | func (link *Link) GetBytes() (uint64, uint64) { 39 | return link.recvBytes, link.sendBytes 40 | } 41 | 42 | // GetPackets get send and recv packets 43 | func (link *Link) GetPackets() (uint64, uint64) { 44 | return link.recvPacket, link.sendPacket 45 | } 46 | 47 | // Close close link 48 | func (link *Link) Close(send bool) { 49 | link.onClose() 50 | p, err := os.FindProcess(link.pid) 51 | if err == nil { 52 | p.Kill() 53 | } 54 | if send { 55 | link.remote.SendDisconnect(link.target, link.id) 56 | } 57 | link.parent.remove(link.id) 58 | link.remote.ChanClose(link.id) 59 | } 60 | 61 | // Forward forward data 62 | func (link *Link) Forward() { 63 | go link.remoteRead() 64 | go link.localRead() 65 | } 66 | 67 | func (link *Link) remoteRead() { 68 | defer utils.Recover("remoteRead") 69 | defer link.Close(true) 70 | ch := link.remote.ChanRead(link.id) 71 | for { 72 | msg := <-ch 73 | if msg == nil { 74 | return 75 | } 76 | data, _ := proto.Marshal(msg) 77 | link.recvBytes += uint64(len(data)) 78 | link.recvPacket++ 79 | switch msg.GetXType() { 80 | case network.Msg_shell_resize: 81 | size := msg.GetSresize() 82 | link.resize(size.GetRows(), size.GetCols()) 83 | case network.Msg_shell_data: 84 | _, err := link.stdin.Write(msg.GetSdata().GetData()) 85 | if err != nil { 86 | logging.Error("write data on shell %s link %s failed, err=%v", 87 | link.parent.Name, link.id, err) 88 | return 89 | } 90 | } 91 | } 92 | } 93 | 94 | func (link *Link) localRead() { 95 | defer utils.Recover("localRead") 96 | defer link.Close(true) 97 | buf := make([]byte, 16*1024) 98 | for { 99 | n, err := link.stdout.Read(buf) 100 | if err != nil { 101 | logging.Error("read data on shell %s link %s failed, err=%v", 102 | link.parent.Name, link.id, err) 103 | return 104 | } 105 | if n == 0 { 106 | continue 107 | } 108 | var data []byte 109 | switch { 110 | case isUtf8(buf[:n]): 111 | data = buf[:n] 112 | case isGBK(buf[:n]): 113 | data, err = simplifiedchinese.GBK.NewDecoder().Bytes(buf[:n]) 114 | if err != nil { 115 | logging.Error("transform gbk to utf8 failed: %v", err) 116 | continue 117 | } 118 | } 119 | logging.Debug("link %s on shell %s read from local %d bytes", 120 | link.id, link.parent.Name, n) 121 | send := link.remote.SendShellData(link.target, link.id, data) 122 | link.sendBytes += send 123 | link.sendPacket++ 124 | } 125 | } 126 | 127 | // SendData send data 128 | func (link *Link) SendData(data []byte) { 129 | send := link.remote.SendShellData(link.target, link.id, data) 130 | link.sendBytes += send 131 | link.sendPacket++ 132 | } 133 | 134 | // SendResize send resize message 135 | func (link *Link) SendResize(rows, cols uint32) { 136 | link.remote.SendShellResize(link.target, link.id, rows, cols) 137 | } 138 | -------------------------------------------------------------------------------- /code/client/rule/shell/shell.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "sync" 8 | "time" 9 | 10 | "github.com/lwch/logging" 11 | "github.com/lwch/natpass/code/client/conn" 12 | "github.com/lwch/natpass/code/client/global" 13 | "github.com/lwch/natpass/code/client/rule" 14 | "github.com/lwch/runtime" 15 | ) 16 | 17 | // Shell shell handler 18 | type Shell struct { 19 | sync.RWMutex 20 | Name string 21 | cfg *global.Rule 22 | links map[string]*Link 23 | readTimeout time.Duration 24 | writeTimeout time.Duration 25 | } 26 | 27 | // New new shell 28 | func New(cfg *global.Rule, readTimeout, writeTimeout time.Duration) *Shell { 29 | return &Shell{ 30 | Name: cfg.Name, 31 | cfg: cfg, 32 | links: make(map[string]*Link), 33 | readTimeout: readTimeout, 34 | writeTimeout: writeTimeout, 35 | } 36 | } 37 | 38 | // NewLink new link 39 | func (shell *Shell) NewLink(id, remote string, localConn net.Conn, remoteConn *conn.Conn) rule.Link { 40 | remoteConn.AddLink(id) 41 | link := &Link{ 42 | parent: shell, 43 | id: id, 44 | target: remote, 45 | remote: remoteConn, 46 | } 47 | shell.Lock() 48 | shell.links[link.id] = link 49 | shell.Unlock() 50 | return link 51 | } 52 | 53 | // GetName get shell rule name 54 | func (shell *Shell) GetName() string { 55 | return shell.Name 56 | } 57 | 58 | // GetTypeName get shell rule type name 59 | func (shell *Shell) GetTypeName() string { 60 | return "shell" 61 | } 62 | 63 | // GetTarget get target of this rule 64 | func (shell *Shell) GetTarget() string { 65 | return shell.cfg.Target 66 | } 67 | 68 | // GetLinks get rule links 69 | func (shell *Shell) GetLinks() []rule.Link { 70 | ret := make([]rule.Link, 0, len(shell.links)) 71 | shell.RLock() 72 | for _, link := range shell.links { 73 | ret = append(ret, link) 74 | } 75 | shell.RUnlock() 76 | return ret 77 | } 78 | 79 | // GetRemote get remote target name 80 | func (shell *Shell) GetRemote() string { 81 | return shell.cfg.Target 82 | } 83 | 84 | // GetPort get listen port 85 | func (shell *Shell) GetPort() uint16 { 86 | return shell.cfg.LocalPort 87 | } 88 | 89 | // OnDisconnect on disconnect message 90 | func (shell *Shell) OnDisconnect(id string) { 91 | shell.RLock() 92 | link := shell.links[id] 93 | shell.RUnlock() 94 | if link != nil { 95 | link.Close(false) 96 | } 97 | } 98 | 99 | // Handle handle shell 100 | func (shell *Shell) Handle(c *conn.Conn) { 101 | defer func() { 102 | if err := recover(); err != nil { 103 | logging.Error("close shell: %s, err=%v", shell.Name, err) 104 | } 105 | }() 106 | pf := func(cb func(*conn.Conn, http.ResponseWriter, *http.Request)) http.HandlerFunc { 107 | return func(w http.ResponseWriter, r *http.Request) { 108 | cb(c, w, r) 109 | } 110 | } 111 | mux := http.NewServeMux() 112 | mux.HandleFunc("/new", pf(shell.New)) 113 | mux.HandleFunc("/ws/", pf(shell.WS)) 114 | mux.HandleFunc("/resize", pf(shell.Resize)) 115 | mux.HandleFunc("/", shell.Render) 116 | if shell.cfg.LocalPort == 0 { 117 | shell.cfg.LocalPort = global.GeneratePort() 118 | logging.Info("generate port for %s: %d", shell.Name, shell.cfg.LocalPort) 119 | } 120 | svr := &http.Server{ 121 | Addr: fmt.Sprintf("%s:%d", shell.cfg.LocalAddr, shell.cfg.LocalPort), 122 | Handler: mux, 123 | } 124 | runtime.Assert(svr.ListenAndServe()) 125 | } 126 | 127 | func (shell *Shell) remove(id string) { 128 | shell.Lock() 129 | delete(shell.links, id) 130 | shell.Unlock() 131 | } 132 | -------------------------------------------------------------------------------- /code/client/rule/shell/transform.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | func isGBK(data []byte) bool { 4 | length := len(data) 5 | var i int = 0 6 | for i < length { 7 | if data[i] <= 0x7f { 8 | i++ 9 | continue 10 | } else { 11 | if data[i] >= 0x81 && 12 | data[i] <= 0xfe && 13 | data[i+1] >= 0x40 && 14 | data[i+1] <= 0xfe && 15 | data[i+1] != 0xf7 { 16 | i += 2 17 | continue 18 | } else { 19 | return false 20 | } 21 | } 22 | } 23 | return true 24 | } 25 | 26 | func preNUm(data byte) int { 27 | var mask byte = 0x80 28 | var num int = 0 29 | for i := 0; i < 8; i++ { 30 | if (data & mask) == mask { 31 | num++ 32 | mask = mask >> 1 33 | } else { 34 | break 35 | } 36 | } 37 | return num 38 | } 39 | 40 | func isUtf8(data []byte) bool { 41 | i := 0 42 | for i < len(data) { 43 | if (data[i] & 0x80) == 0x00 { 44 | i++ 45 | continue 46 | } else if num := preNUm(data[i]); num > 2 { 47 | i++ 48 | for j := 0; j < num-1; j++ { 49 | if (data[i] & 0xc0) != 0x80 { 50 | return false 51 | } 52 | i++ 53 | } 54 | } else { 55 | return false 56 | } 57 | } 58 | return true 59 | } 60 | -------------------------------------------------------------------------------- /code/client/rule/vnc/.gitignore: -------------------------------------------------------------------------------- 1 | /assets.go -------------------------------------------------------------------------------- /code/client/rule/vnc/define/kernel32_windows.go: -------------------------------------------------------------------------------- 1 | package define 2 | 3 | import "syscall" 4 | 5 | var ( 6 | libKernel32, _ = syscall.LoadLibrary("kernel32.dll") 7 | // FuncWTSGetActiveConsoleSessionID https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-wtsgetactiveconsolesessionid 8 | FuncWTSGetActiveConsoleSessionID, _ = syscall.GetProcAddress(syscall.Handle(libKernel32), "WTSGetActiveConsoleSessionId") 9 | ) 10 | 11 | // PROCESSALLACCESS https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights 12 | const PROCESSALLACCESS = 0x1F0FFF 13 | -------------------------------------------------------------------------------- /code/client/rule/vnc/define/sas_windows.go: -------------------------------------------------------------------------------- 1 | package define 2 | 3 | import "syscall" 4 | 5 | var ( 6 | libSas, _ = syscall.LoadLibrary("Sas.dll") 7 | // FuncSendSAS https://docs.microsoft.com/en-us/windows/win32/api/sas/nf-sas-sendsas 8 | FuncSendSAS, _ = syscall.GetProcAddress(syscall.Handle(libSas), "SendSAS") 9 | ) 10 | -------------------------------------------------------------------------------- /code/client/rule/vnc/define/user32_windows.go: -------------------------------------------------------------------------------- 1 | package define 2 | 3 | import "syscall" 4 | 5 | var ( 6 | libUser32, _ = syscall.LoadLibrary("user32.dll") 7 | // FuncGetThreadDesktop https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getthreaddesktop 8 | FuncGetThreadDesktop, _ = syscall.GetProcAddress(syscall.Handle(libUser32), "GetThreadDesktop") 9 | // FuncOpenInputDesktop https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-openinputdesktop 10 | FuncOpenInputDesktop, _ = syscall.GetProcAddress(syscall.Handle(libUser32), "OpenInputDesktop") 11 | // FuncSetThreadDesktop https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setthreaddesktop 12 | FuncSetThreadDesktop, _ = syscall.GetProcAddress(syscall.Handle(libUser32), "SetThreadDesktop") 13 | // FuncCloseDesktop https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-closedesktop 14 | FuncCloseDesktop, _ = syscall.GetProcAddress(syscall.Handle(libUser32), "CloseDesktop") 15 | ) 16 | -------------------------------------------------------------------------------- /code/client/rule/vnc/diff.go: -------------------------------------------------------------------------------- 1 | package vnc 2 | 3 | import ( 4 | "image" 5 | "reflect" 6 | "unsafe" 7 | ) 8 | 9 | func calcDiff(src, dst *image.RGBA) []image.Rectangle { 10 | // 宽度必须为2的倍数 11 | const width = 64 12 | const height = 64 13 | size := dst.Bounds() 14 | ret := make([]image.Rectangle, 0, (size.Max.X*size.Max.Y)/(width*height)) 15 | for y := 0; y < size.Max.Y; y += height { 16 | for x := 0; x < size.Max.X; x += width { 17 | dWidth := size.Max.X - x 18 | dHeight := size.Max.Y - y 19 | if dWidth > width { 20 | dWidth = width 21 | } 22 | if dHeight > height { 23 | dHeight = height 24 | } 25 | rect := image.Rect(x, y, x+dWidth, y+dHeight) 26 | if dWidth%2 == 0 { 27 | if isDiff8(src, dst, rect) { 28 | ret = append(ret, rect) 29 | } 30 | } else { 31 | if isDiff4(src, dst, rect) { 32 | ret = append(ret, rect) 33 | } 34 | } 35 | //////// 36 | } 37 | } 38 | return ret 39 | } 40 | 41 | func isDiff8(src, dst *image.RGBA, rect image.Rectangle) bool { 42 | sx := src.Bounds().Max.X * 4 43 | dx := rect.Min.X * 4 44 | ptr := uintptr(rect.Min.Y*sx + dx) 45 | srcData := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&src.Pix)).Data) 46 | dstData := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&dst.Pix)).Data) 47 | for y := 0; y < rect.Size().Y; y++ { 48 | next := ptr + uintptr(sx) 49 | for x := 0; x < rect.Size().X; x += 2 { 50 | src := (*uint64)(unsafe.Pointer(uintptr(srcData) + ptr)) 51 | dst := (*uint64)(unsafe.Pointer(uintptr(dstData) + ptr)) 52 | if *src != *dst { 53 | return true 54 | } 55 | ptr += 8 56 | } 57 | ptr = next 58 | } 59 | return false 60 | } 61 | 62 | func isDiff4(src, dst *image.RGBA, rect image.Rectangle) bool { 63 | sx := src.Bounds().Max.X * 4 64 | dx := rect.Min.X * 4 65 | ptr := uintptr(rect.Min.Y*sx + dx) 66 | srcData := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&src.Pix)).Data) 67 | dstData := unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&dst.Pix)).Data) 68 | for y := 0; y < rect.Size().Y; y++ { 69 | next := ptr + uintptr(sx) 70 | for x := 0; x < rect.Size().X; x++ { 71 | src := (*uint64)(unsafe.Pointer(uintptr(srcData) + ptr)) 72 | dst := (*uint64)(unsafe.Pointer(uintptr(dstData) + ptr)) 73 | if *src != *dst { 74 | return true 75 | } 76 | ptr += 4 77 | } 78 | ptr = next 79 | } 80 | return false 81 | } 82 | -------------------------------------------------------------------------------- /code/client/rule/vnc/events.go: -------------------------------------------------------------------------------- 1 | package vnc 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/lwch/logging" 7 | "github.com/lwch/natpass/code/client/conn" 8 | ) 9 | 10 | func (v *VNC) mouseEvent(remote *conn.Conn, data []byte) { 11 | var payload struct { 12 | Payload struct { 13 | Button string `json:"button"` 14 | Status string `json:"status"` 15 | X int `json:"x"` 16 | Y int `json:"y"` 17 | } `json:"payload"` 18 | } 19 | err := json.Unmarshal(data, &payload) 20 | if err != nil { 21 | logging.Error("unmarshal: %v", err) 22 | return 23 | } 24 | remote.SendVNCMouse(v.link.target, v.link.id, 25 | payload.Payload.Button, payload.Payload.Status, payload.Payload.X, payload.Payload.Y) 26 | } 27 | 28 | func (v *VNC) keyboardEvent(remote *conn.Conn, data []byte) { 29 | var payload struct { 30 | Payload struct { 31 | Status string `json:"status"` 32 | Key string `json:"key"` 33 | } `json:"payload"` 34 | } 35 | err := json.Unmarshal(data, &payload) 36 | if err != nil { 37 | logging.Error("unmarshal: %v", err) 38 | return 39 | } 40 | remote.SendVNCKeyboard(v.link.target, v.link.id, 41 | payload.Payload.Status, payload.Payload.Key) 42 | } 43 | 44 | func (v *VNC) cadEvent(remote *conn.Conn) { 45 | remote.SendVNCCADEvent(v.link.target, v.link.id) 46 | } 47 | 48 | func (v *VNC) scrollEvent(remote *conn.Conn, data []byte) { 49 | var payload struct { 50 | Payload struct { 51 | X int32 `json:"x"` 52 | Y int32 `json:"y"` 53 | } `json:"payload"` 54 | } 55 | err := json.Unmarshal(data, &payload) 56 | if err != nil { 57 | logging.Error("unmarshal: %v", err) 58 | return 59 | } 60 | remote.SendVNCScroll(v.link.target, v.link.id, 61 | payload.Payload.X, payload.Payload.Y) 62 | } 63 | -------------------------------------------------------------------------------- /code/client/rule/vnc/h_clipboard.go: -------------------------------------------------------------------------------- 1 | package vnc 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/lwch/natpass/code/client/conn" 8 | ) 9 | 10 | // Clipboard get/set clipboard 11 | func (v *VNC) Clipboard(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 12 | if r.Method == http.MethodGet { 13 | v.getClipboard(conn, w, r) 14 | return 15 | } 16 | v.setClipboard(conn, w, r) 17 | } 18 | 19 | func (v *VNC) getClipboard(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 20 | if v.link == nil { 21 | http.NotFound(w, r) 22 | return 23 | } 24 | conn.SendVNCClipboardData(v.link.target, v.link.id, false, "") 25 | data := <-v.chClipboard 26 | fmt.Fprint(w, data.GetData()) 27 | } 28 | 29 | func (v *VNC) setClipboard(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 30 | data := r.FormValue("data") 31 | if v.link == nil { 32 | http.NotFound(w, r) 33 | return 34 | } 35 | conn.SendVNCClipboardData(v.link.target, v.link.id, true, data) 36 | fmt.Fprint(w, "ok") 37 | } 38 | -------------------------------------------------------------------------------- /code/client/rule/vnc/h_ctrl.go: -------------------------------------------------------------------------------- 1 | package vnc 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/lwch/natpass/code/client/conn" 9 | ) 10 | 11 | // Ctrl change vnc rule config 12 | func (v *VNC) Ctrl(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 13 | q := r.FormValue("quality") 14 | s := r.FormValue("show_cursor") 15 | quality, err := strconv.ParseUint(q, 10, 32) 16 | if err != nil { 17 | quality = 50 18 | } 19 | showCursor, err := strconv.ParseBool(s) 20 | if err != nil { 21 | showCursor = false 22 | } 23 | if v.link == nil { 24 | http.NotFound(w, r) 25 | return 26 | } 27 | conn.SendVNCCtrl(v.link.target, v.link.id, quality, showCursor) 28 | fmt.Fprint(w, "ok") 29 | } 30 | -------------------------------------------------------------------------------- /code/client/rule/vnc/h_new.go: -------------------------------------------------------------------------------- 1 | package vnc 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/lwch/logging" 10 | "github.com/lwch/natpass/code/client/conn" 11 | "github.com/lwch/natpass/code/network" 12 | "github.com/lwch/runtime" 13 | ) 14 | 15 | // New new vnc 16 | func (v *VNC) New(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 17 | if v.link != nil { 18 | v.link.Close(true) 19 | } 20 | q := r.FormValue("quality") 21 | s := r.FormValue("show_cursor") 22 | quality, err := strconv.ParseUint(q, 10, 32) 23 | if err != nil { 24 | quality = 50 25 | } 26 | showCursor, err := strconv.ParseBool(s) 27 | if err != nil { 28 | showCursor = false 29 | } 30 | id, err := runtime.UUID(16, "0123456789abcdef") 31 | if err != nil { 32 | logging.Error("failed to generate link_id for vnc: %s, err=%v", 33 | v.Name, err) 34 | http.Error(w, err.Error(), http.StatusInternalServerError) 35 | return 36 | } 37 | if v.link != nil { 38 | conn.SendDisconnect(v.link.target, v.link.id) 39 | } 40 | conn.SendConnectVnc(id, v.cfg, quality, showCursor) 41 | v.link = v.NewLink(id, v.cfg.Target, nil, conn).(*Link) 42 | ch := conn.ChanRead(id) 43 | timeout := time.After(v.readTimeout) 44 | for { 45 | var msg *network.Msg 46 | select { 47 | case msg = <-ch: 48 | case <-timeout: 49 | logging.Error("create vnc %s by rule %s failed, timtout", v.link.id, v.link.parent.Name) 50 | http.Error(w, "timeout", http.StatusBadGateway) 51 | return 52 | } 53 | if msg.GetXType() != network.Msg_connect_rep { 54 | conn.Requeue(id, msg) 55 | time.Sleep(v.readTimeout / 10) 56 | continue 57 | } 58 | rep := msg.GetCrep() 59 | if !rep.GetOk() { 60 | logging.Error("create vnc %s by rule %s failed, err=%s", 61 | v.link.id, v.link.parent.Name, rep.GetMsg()) 62 | http.Error(w, rep.GetMsg(), http.StatusBadGateway) 63 | return 64 | } 65 | break 66 | } 67 | logging.Info("new vnc: name=%s, id=%s", v.Name, id) 68 | fmt.Fprint(w, id) 69 | } 70 | -------------------------------------------------------------------------------- /code/client/rule/vnc/h_render.go: -------------------------------------------------------------------------------- 1 | package vnc 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "io" 7 | "mime" 8 | "net/http" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | // Render render asset file 14 | func (v *VNC) Render(w http.ResponseWriter, r *http.Request) { 15 | dir := strings.TrimPrefix(r.URL.Path, "/") 16 | data, err := Asset(dir) 17 | if err == nil { 18 | ctype := mime.TypeByExtension(filepath.Ext(dir)) 19 | if ctype == "" { 20 | ctype = http.DetectContentType(data) 21 | } 22 | w.Header().Set("Content-Type", ctype) 23 | io.Copy(w, bytes.NewReader(data)) 24 | return 25 | } 26 | data, _ = Asset("index.html") 27 | tpl, err := template.New("all").Parse(string(data)) 28 | if err != nil { 29 | http.Error(w, err.Error(), http.StatusInternalServerError) 30 | return 31 | } 32 | tpl.Execute(w, v) 33 | } 34 | -------------------------------------------------------------------------------- /code/client/rule/vnc/h_ws.go: -------------------------------------------------------------------------------- 1 | package vnc 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "encoding/json" 8 | "errors" 9 | "image" 10 | "image/draw" 11 | "image/jpeg" 12 | "net/http" 13 | "os" 14 | "strings" 15 | "sync" 16 | 17 | "github.com/gorilla/websocket" 18 | "github.com/lwch/logging" 19 | "github.com/lwch/natpass/code/client/conn" 20 | "github.com/lwch/natpass/code/network" 21 | "github.com/lwch/natpass/code/utils" 22 | "github.com/lwch/runtime" 23 | ) 24 | 25 | var upgrader = websocket.Upgrader{} 26 | 27 | // WS websocket handler 28 | func (v *VNC) WS(conn *conn.Conn, w http.ResponseWriter, r *http.Request) { 29 | id := strings.TrimPrefix(r.URL.Path, "/ws/") 30 | local, err := upgrader.Upgrade(w, r, nil) 31 | if err != nil { 32 | http.Error(w, err.Error(), http.StatusInternalServerError) 33 | return 34 | } 35 | defer local.Close() 36 | ch := conn.ChanRead(id) 37 | defer conn.SendDisconnect(v.link.target, v.link.id) 38 | ctx, cancel := context.WithCancel(context.Background()) 39 | defer cancel() 40 | var wg sync.WaitGroup 41 | wg.Add(2) 42 | go func() { 43 | defer cancel() 44 | defer wg.Done() 45 | v.remoteRead(ctx, ch, local) 46 | }() 47 | go func() { 48 | defer cancel() 49 | defer wg.Done() 50 | v.localRead(ctx, local, conn) 51 | }() 52 | wg.Wait() 53 | } 54 | 55 | func (v *VNC) remoteRead(ctx context.Context, ch <-chan *network.Msg, local *websocket.Conn) { 56 | defer utils.Recover("remoteRead") 57 | for { 58 | var msg *network.Msg 59 | select { 60 | case msg = <-ch: 61 | case <-ctx.Done(): 62 | return 63 | } 64 | switch msg.GetXType() { 65 | case network.Msg_vnc_image: 66 | data, err := decodeImage(msg.GetVimg()) 67 | runtime.Assert(err) 68 | replyImage(local, msg.GetVimg(), data, len(msg.GetVimg().GetData())) 69 | case network.Msg_vnc_clipboard: 70 | v.chClipboard <- msg.GetVclipboard() 71 | default: 72 | logging.Error("on message: %s", msg.GetXType().String()) 73 | return 74 | } 75 | } 76 | } 77 | 78 | func (v *VNC) localRead(ctx context.Context, local *websocket.Conn, remote *conn.Conn) { 79 | defer utils.Recover("localRead") 80 | for { 81 | select { 82 | case <-ctx.Done(): 83 | return 84 | default: 85 | } 86 | _, data, err := local.ReadMessage() 87 | if err != nil { 88 | logging.Error("local read: %v", err) 89 | return 90 | } 91 | var msg struct { 92 | Action string `json:"action"` 93 | } 94 | err = json.Unmarshal(data, &msg) 95 | if err != nil { 96 | logging.Error("unmarshal: %v", err) 97 | return 98 | } 99 | switch msg.Action { 100 | case "mouse": 101 | v.mouseEvent(remote, data) 102 | case "keyboard": 103 | v.keyboardEvent(remote, data) 104 | case "cad": 105 | v.cadEvent(remote) 106 | case "scroll": 107 | v.scrollEvent(remote, data) 108 | } 109 | } 110 | } 111 | 112 | func decodeImage(data *network.VncImage) ([]byte, error) { 113 | switch data.GetEncode() { 114 | case network.VncImage_raw: 115 | return data.GetData(), nil 116 | case network.VncImage_jpeg: 117 | img, err := jpeg.Decode(bytes.NewReader(data.GetData())) 118 | if err != nil { 119 | return nil, err 120 | } 121 | // dumpImage(img) 122 | rect := img.Bounds() 123 | raw := image.NewRGBA(rect) 124 | draw.Draw(raw, rect, img, rect.Min, draw.Src) 125 | return raw.Pix, nil 126 | case network.VncImage_png: 127 | // TODO 128 | } 129 | return nil, errors.New("unsupported") 130 | } 131 | 132 | func dumpImage(img image.Image) { 133 | f, err := os.Create(`./debug.jpeg`) 134 | if err != nil { 135 | logging.Error("debug: %v", err) 136 | return 137 | } 138 | defer f.Close() 139 | err = jpeg.Encode(f, img, nil) 140 | if err != nil { 141 | logging.Error("encode: %v", err) 142 | return 143 | } 144 | } 145 | 146 | func replyImage(conn *websocket.Conn, msg *network.VncImage, data []byte, srcSize int) { 147 | info := msg.GetXInfo() 148 | buf := make([]byte, len(data)+28) 149 | binary.BigEndian.PutUint32(buf, info.GetScreenWidth()) 150 | binary.BigEndian.PutUint32(buf[4:], info.GetScreenHeight()) 151 | binary.BigEndian.PutUint32(buf[8:], info.GetRectX()) 152 | binary.BigEndian.PutUint32(buf[12:], info.GetRectY()) 153 | binary.BigEndian.PutUint32(buf[16:], info.GetRectWidth()) 154 | binary.BigEndian.PutUint32(buf[20:], info.GetRectHeight()) 155 | binary.BigEndian.PutUint32(buf[24:], uint32(srcSize)) 156 | copy(buf[28:], data) 157 | conn.WriteMessage(websocket.BinaryMessage, buf) 158 | } 159 | -------------------------------------------------------------------------------- /code/client/rule/vnc/process/errors.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import "errors" 4 | 5 | // ErrNotSupported not supported 6 | var ErrNotSupported = errors.New("not supported") 7 | -------------------------------------------------------------------------------- /code/client/rule/vnc/process/event.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "github.com/lwch/natpass/code/client/rule/vnc/vncnetwork" 5 | "github.com/lwch/natpass/code/network" 6 | ) 7 | 8 | // MouseEvent dispatch mouse event to child process 9 | func (p *Process) MouseEvent(data *network.VncMouse) { 10 | t := vncnetwork.Status_unset_st 11 | switch data.GetType() { 12 | case network.VncStatus_down: 13 | t = vncnetwork.Status_down 14 | case network.VncStatus_up: 15 | t = vncnetwork.Status_up 16 | } 17 | btn := vncnetwork.MouseData_unset_btn 18 | switch data.GetBtn() { 19 | case network.VncMouse_left: 20 | btn = vncnetwork.MouseData_left 21 | case network.VncMouse_middle: 22 | btn = vncnetwork.MouseData_middle 23 | case network.VncMouse_right: 24 | btn = vncnetwork.MouseData_right 25 | } 26 | var msg vncnetwork.VncMsg 27 | msg.XType = vncnetwork.VncMsg_mouse_event 28 | msg.Payload = &vncnetwork.VncMsg_Mouse{ 29 | Mouse: &vncnetwork.MouseData{ 30 | Type: t, 31 | Btn: btn, 32 | X: data.GetX(), 33 | Y: data.GetY(), 34 | }, 35 | } 36 | p.chWrite <- &msg 37 | } 38 | 39 | // KeyboardEvent dispatch keyboard event to child process 40 | func (p *Process) KeyboardEvent(data *network.VncKeyboard) { 41 | t := vncnetwork.Status_unset_st 42 | switch data.GetType() { 43 | case network.VncStatus_down: 44 | t = vncnetwork.Status_down 45 | case network.VncStatus_up: 46 | t = vncnetwork.Status_up 47 | } 48 | var msg vncnetwork.VncMsg 49 | msg.XType = vncnetwork.VncMsg_keyboard_event 50 | msg.Payload = &vncnetwork.VncMsg_Keyboard{ 51 | Keyboard: &vncnetwork.KeyboardData{ 52 | Type: t, 53 | Key: data.GetKey(), 54 | }, 55 | } 56 | p.chWrite <- &msg 57 | } 58 | 59 | // SetCursor dispatch draw cursor to child process 60 | func (p *Process) SetCursor(b bool) { 61 | var msg vncnetwork.VncMsg 62 | msg.XType = vncnetwork.VncMsg_set_cursor 63 | msg.Payload = &vncnetwork.VncMsg_ShowCursor{ 64 | ShowCursor: b, 65 | } 66 | p.chWrite <- &msg 67 | } 68 | 69 | // ScrollEvent dispatch scroll event to child process 70 | func (p *Process) ScrollEvent(data *network.VncScroll) { 71 | var msg vncnetwork.VncMsg 72 | msg.XType = vncnetwork.VncMsg_scroll_event 73 | msg.Payload = &vncnetwork.VncMsg_Scroll{ 74 | Scroll: &vncnetwork.ScrollData{ 75 | X: data.GetX(), 76 | Y: data.GetY(), 77 | }, 78 | } 79 | p.chWrite <- &msg 80 | } 81 | 82 | // SetClipboard set clipboard data to child process 83 | func (p *Process) SetClipboard(data *network.VncClipboard) { 84 | t := vncnetwork.ClipboardData_unset_type 85 | var payload vncnetwork.ClipboardData_Data 86 | switch data.GetXType() { 87 | case network.VncClipboard_file: 88 | t = vncnetwork.ClipboardData_file 89 | case network.VncClipboard_image: 90 | t = vncnetwork.ClipboardData_image 91 | case network.VncClipboard_text: 92 | t = vncnetwork.ClipboardData_text 93 | payload.Data = data.GetData() 94 | } 95 | var msg vncnetwork.VncMsg 96 | msg.XType = vncnetwork.VncMsg_clipboard_event 97 | msg.Payload = &vncnetwork.VncMsg_Clipboard{ 98 | Clipboard: &vncnetwork.ClipboardData{ 99 | Set: data.GetSet(), 100 | XType: t, 101 | Payload: &payload, 102 | }, 103 | } 104 | p.chWrite <- &msg 105 | } 106 | 107 | // GetClipboard get clipboard data from child process 108 | func (p *Process) GetClipboard() string { 109 | var msg vncnetwork.VncMsg 110 | msg.XType = vncnetwork.VncMsg_clipboard_event 111 | msg.Payload = &vncnetwork.VncMsg_Clipboard{ 112 | Clipboard: &vncnetwork.ClipboardData{ 113 | Set: false, 114 | }, 115 | } 116 | p.chWrite <- &msg 117 | data := <-p.chClipboard 118 | return data.GetData() 119 | } 120 | -------------------------------------------------------------------------------- /code/client/rule/vnc/process/event_windows.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "syscall" 5 | 6 | "github.com/lwch/logging" 7 | "github.com/lwch/natpass/code/client/rule/vnc/define" 8 | ) 9 | 10 | // CADEvent handle ctrl+alt+del event 11 | func (p *Process) CADEvent() { 12 | ok, _, err := syscall.Syscall(define.FuncSendSAS, 1, 0, 0, 0) 13 | if ok == 0 { 14 | logging.Error("send sas failed: %v", err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /code/client/rule/vnc/process/event_xx.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package process 5 | 6 | // CADEvent handle ctrl+alt+del event 7 | func (p *Process) CADEvent() { 8 | } 9 | -------------------------------------------------------------------------------- /code/client/rule/vnc/process/process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "image" 7 | "image/jpeg" 8 | "net" 9 | "net/http" 10 | "os" 11 | "sync" 12 | "time" 13 | 14 | "github.com/gorilla/websocket" 15 | "github.com/lwch/logging" 16 | "github.com/lwch/natpass/code/client/rule/vnc/vncnetwork" 17 | "github.com/lwch/natpass/code/utils" 18 | "google.golang.org/protobuf/proto" 19 | ) 20 | 21 | const ( 22 | listenBegin = 6155 23 | listenEnd = 6955 24 | ) 25 | 26 | // Process process 27 | type Process struct { 28 | pid int 29 | srv *http.Server 30 | chWrite chan *vncnetwork.VncMsg 31 | chImage chan *vncnetwork.ImageData 32 | chClipboard chan *vncnetwork.ClipboardData 33 | } 34 | 35 | func (p *Process) listenAndServe() (uint16, error) { 36 | mux := http.NewServeMux() 37 | mux.HandleFunc("/", p.ws) 38 | port := uint16(listenBegin) 39 | for { 40 | if port > listenEnd { 41 | return 0, errors.New("no port available") 42 | } 43 | p.srv = &http.Server{ 44 | Addr: fmt.Sprintf("127.0.0.1:%d", port), 45 | Handler: mux, 46 | } 47 | ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) 48 | if err != nil { 49 | port++ 50 | continue 51 | } 52 | go p.srv.Serve(ln) 53 | return port, nil 54 | } 55 | } 56 | 57 | var upgrader = websocket.Upgrader{EnableCompression: true} 58 | 59 | func (p *Process) ws(w http.ResponseWriter, r *http.Request) { 60 | logging.Info("child process connected") 61 | conn, err := upgrader.Upgrade(w, r, nil) 62 | if err != nil { 63 | http.Error(w, err.Error(), http.StatusInternalServerError) 64 | return 65 | } 66 | defer conn.Close() 67 | defer p.Close() 68 | var wg sync.WaitGroup 69 | wg.Add(2) 70 | go func() { 71 | defer utils.Recover("ws read") 72 | defer wg.Done() 73 | for { 74 | _, data, err := conn.ReadMessage() 75 | if err != nil { 76 | logging.Error("read message: %v", err) 77 | return 78 | } 79 | var msg vncnetwork.VncMsg 80 | err = proto.Unmarshal(data, &msg) 81 | if err != nil { 82 | continue 83 | } 84 | switch msg.GetXType() { 85 | case vncnetwork.VncMsg_capture_data: 86 | p.chImage <- msg.GetData() 87 | case vncnetwork.VncMsg_clipboard_event: 88 | p.chClipboard <- msg.GetClipboard() 89 | default: 90 | } 91 | } 92 | }() 93 | go func() { 94 | defer utils.Recover("ws write") 95 | defer wg.Done() 96 | for { 97 | msg := <-p.chWrite 98 | data, err := proto.Marshal(msg) 99 | if err != nil { 100 | continue 101 | } 102 | err = conn.WriteMessage(websocket.BinaryMessage, data) 103 | if err != nil { 104 | logging.Error("write message: %v", err) 105 | return 106 | } 107 | } 108 | }() 109 | wg.Wait() 110 | } 111 | 112 | func (p *Process) kill() { 113 | ps, _ := os.FindProcess(p.pid) 114 | if ps != nil { 115 | ps.Kill() 116 | } 117 | } 118 | 119 | // Close close process 120 | func (p *Process) Close() { 121 | if p.srv != nil { 122 | p.srv.Close() 123 | } 124 | if p.chImage != nil { 125 | close(p.chImage) 126 | p.chImage = nil 127 | } 128 | if p.chClipboard != nil { 129 | close(p.chClipboard) 130 | p.chClipboard = nil 131 | } 132 | if p.chWrite != nil { 133 | close(p.chWrite) 134 | p.chWrite = nil 135 | } 136 | p.kill() 137 | } 138 | 139 | // Capture capture desktop image 140 | func (p *Process) Capture(timeout time.Duration) (*image.RGBA, error) { 141 | var msg vncnetwork.VncMsg 142 | msg.XType = vncnetwork.VncMsg_capture_req 143 | p.chWrite <- &msg 144 | trans := func(data *vncnetwork.ImageData) *image.RGBA { 145 | img := image.NewRGBA(image.Rect(0, 0, int(data.GetWidth()), int(data.GetHeight()))) 146 | copy(img.Pix, data.GetData()) 147 | // dumpImage(img) 148 | return img 149 | } 150 | if timeout > 0 { 151 | select { 152 | case data := <-p.chImage: 153 | return trans(data), nil 154 | case <-time.After(timeout): 155 | return nil, errors.New("timeout") 156 | } 157 | } else { 158 | data := <-p.chImage 159 | return trans(data), nil 160 | } 161 | } 162 | 163 | func dumpImage(img image.Image) { 164 | f, err := os.Create(`C:\Users\lwch\Pictures\debug.jpeg`) 165 | if err != nil { 166 | logging.Error("debug: %v", err) 167 | return 168 | } 169 | defer f.Close() 170 | err = jpeg.Encode(f, img, nil) 171 | if err != nil { 172 | logging.Error("encode: %v", err) 173 | return 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /code/client/rule/vnc/process/process_windows.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "syscall" 8 | "unicode/utf16" 9 | "unsafe" 10 | 11 | "github.com/lwch/natpass/code/client/rule/vnc/define" 12 | "github.com/lwch/natpass/code/client/rule/vnc/vncnetwork" 13 | "golang.org/x/sys/windows" 14 | ) 15 | 16 | func getLogonPid(sessionID uintptr) uint32 { 17 | snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) 18 | if err != nil { 19 | return 0 20 | } 21 | defer syscall.CloseHandle(snapshot) 22 | var procEntry syscall.ProcessEntry32 23 | procEntry.Size = uint32(unsafe.Sizeof(procEntry)) 24 | err = syscall.Process32First(snapshot, &procEntry) 25 | if err != nil { 26 | return 0 27 | } 28 | var ret uint32 29 | for { 30 | var sid uint32 31 | name := parseProcessName(procEntry.ExeFile) 32 | if strings.ToLower(name) != "winlogon.exe" { 33 | goto next 34 | } 35 | err = windows.ProcessIdToSessionId(procEntry.ProcessID, &sid) 36 | if err != nil { 37 | return ret 38 | } 39 | if sid == uint32(sessionID) { 40 | ret = procEntry.ProcessID 41 | } 42 | next: 43 | err = syscall.Process32Next(snapshot, &procEntry) 44 | if err != nil { 45 | return ret 46 | } 47 | } 48 | } 49 | 50 | func parseProcessName(exeFile [syscall.MAX_PATH]uint16) string { 51 | for i, v := range exeFile { 52 | if v <= 0 { 53 | return string(utf16.Decode(exeFile[:i])) 54 | } 55 | } 56 | return "" 57 | } 58 | 59 | func getSessionID() uintptr { 60 | id, _, _ := syscall.Syscall(define.FuncWTSGetActiveConsoleSessionID, 0, 0, 0, 0) 61 | return id 62 | } 63 | 64 | func getSessionUserTokenWin() windows.Token { 65 | pid := getLogonPid(getSessionID()) 66 | process, err := windows.OpenProcess(define.PROCESSALLACCESS, false, pid) 67 | if err != nil { 68 | return 0 69 | } 70 | defer windows.CloseHandle(process) 71 | var ret windows.Token 72 | windows.OpenProcessToken(process, windows.TOKEN_ALL_ACCESS, &ret) 73 | return ret 74 | } 75 | 76 | // CreateWorker create worker process 77 | func CreateWorker(name, confDir string, showCursor bool) (*Process, error) { 78 | tk := getSessionUserTokenWin() 79 | if tk != 0 { 80 | defer windows.CloseHandle(windows.Handle(tk)) 81 | } 82 | return createWorker(name, confDir, tk, showCursor) 83 | } 84 | 85 | func createWorker(name, confDir string, tk windows.Token, showCursor bool) (*Process, error) { 86 | dir, err := os.Executable() 87 | if err != nil { 88 | return nil, err 89 | } 90 | var p Process 91 | p.chWrite = make(chan *vncnetwork.VncMsg) 92 | p.chImage = make(chan *vncnetwork.ImageData) 93 | p.chClipboard = make(chan *vncnetwork.ClipboardData) 94 | port, err := p.listenAndServe() 95 | if err != nil { 96 | return nil, err 97 | } 98 | var startup windows.StartupInfo 99 | var process windows.ProcessInformation 100 | startup.Cb = uint32(unsafe.Sizeof(startup)) 101 | startup.Desktop = windows.StringToUTF16Ptr("WinSta0\\default") 102 | startup.Flags = windows.STARTF_USESHOWWINDOW 103 | str := dir + fmt.Sprintf(" vnc --conf %s --name %s --port %d", confDir, name, port) 104 | if showCursor { 105 | str += "--cursor" 106 | } 107 | cmd := windows.StringToUTF16Ptr(str) 108 | if tk == 0 { 109 | // only for debug 110 | startup.Flags = 0 111 | err = windows.CreateProcess(nil, cmd, nil, nil, false, windows.CREATE_NEW_CONSOLE, nil, nil, &startup, &process) 112 | } else { 113 | err = windows.CreateProcessAsUser(tk, nil, cmd, nil, nil, false, windows.DETACHED_PROCESS, nil, nil, &startup, &process) 114 | } 115 | if err != nil { 116 | return nil, err 117 | } 118 | p.pid = int(process.ProcessId) 119 | return &p, nil 120 | } 121 | -------------------------------------------------------------------------------- /code/client/rule/vnc/process/process_xx.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package process 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | 11 | "github.com/lwch/natpass/code/client/rule/vnc/vncnetwork" 12 | ) 13 | 14 | // CreateWorker create worker process 15 | func CreateWorker(name, confDir string, showCursor bool) (*Process, error) { 16 | dir, err := os.Executable() 17 | if err != nil { 18 | return nil, err 19 | } 20 | var p Process 21 | p.chWrite = make(chan *vncnetwork.VncMsg) 22 | p.chImage = make(chan *vncnetwork.ImageData) 23 | p.chClipboard = make(chan *vncnetwork.ClipboardData) 24 | port, err := p.listenAndServe() 25 | if err != nil { 26 | return nil, err 27 | } 28 | cmd := exec.Command(dir, "vnc", "--conf", confDir, 29 | "--name", name, 30 | "--port", fmt.Sprintf("%d", port)) 31 | err = cmd.Start() 32 | if err != nil { 33 | p.Close() 34 | return nil, err 35 | } 36 | return &p, nil 37 | } 38 | -------------------------------------------------------------------------------- /code/client/rule/vnc/vnc.go: -------------------------------------------------------------------------------- 1 | package vnc 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "sync" 8 | "time" 9 | 10 | "github.com/lwch/logging" 11 | "github.com/lwch/natpass/code/client/conn" 12 | "github.com/lwch/natpass/code/client/global" 13 | "github.com/lwch/natpass/code/client/rule" 14 | "github.com/lwch/natpass/code/network" 15 | "github.com/lwch/runtime" 16 | ) 17 | 18 | // VNC vnc handler 19 | type VNC struct { 20 | sync.RWMutex 21 | Name string 22 | cfg *global.Rule 23 | link *Link 24 | readTimeout time.Duration 25 | writeTimeout time.Duration 26 | chClipboard chan *network.VncClipboard 27 | } 28 | 29 | // New new vnc 30 | func New(cfg *global.Rule, readTimeout, writeTimeout time.Duration) *VNC { 31 | return &VNC{ 32 | Name: cfg.Name, 33 | cfg: cfg, 34 | readTimeout: readTimeout, 35 | writeTimeout: writeTimeout, 36 | chClipboard: make(chan *network.VncClipboard), 37 | } 38 | } 39 | 40 | // NewLink new link 41 | func (v *VNC) NewLink(id, remote string, localConn net.Conn, remoteConn *conn.Conn) rule.Link { 42 | remoteConn.AddLink(id) 43 | link := &Link{ 44 | parent: v, 45 | id: id, 46 | target: remote, 47 | remote: remoteConn, 48 | } 49 | if v.link != nil { 50 | v.link.Close(true) 51 | } 52 | v.link = link 53 | return link 54 | } 55 | 56 | // GetName get vnc rule name 57 | func (v *VNC) GetName() string { 58 | return v.Name 59 | } 60 | 61 | // GetTypeName get vnc rule type name 62 | func (v *VNC) GetTypeName() string { 63 | return "vnc" 64 | } 65 | 66 | // GetTarget get target of this rule 67 | func (v *VNC) GetTarget() string { 68 | return v.cfg.Target 69 | } 70 | 71 | // GetLinks get rule links 72 | func (v *VNC) GetLinks() []rule.Link { 73 | if v.link != nil { 74 | return []rule.Link{v.link} 75 | } 76 | return nil 77 | } 78 | 79 | // GetRemote get remote target name 80 | func (v *VNC) GetRemote() string { 81 | return v.cfg.Target 82 | } 83 | 84 | // GetPort get listen port 85 | func (v *VNC) GetPort() uint16 { 86 | return v.cfg.LocalPort 87 | } 88 | 89 | // OnDisconnect on disconnect message 90 | func (v *VNC) OnDisconnect(id string) { 91 | // TODO 92 | } 93 | 94 | // Handle handle shell 95 | func (v *VNC) Handle(c *conn.Conn) { 96 | defer func() { 97 | if err := recover(); err != nil { 98 | logging.Error("close shell: %s, err=%v", v.Name, err) 99 | } 100 | }() 101 | pf := func(cb func(*conn.Conn, http.ResponseWriter, *http.Request)) http.HandlerFunc { 102 | return func(w http.ResponseWriter, r *http.Request) { 103 | cb(c, w, r) 104 | } 105 | } 106 | mux := http.NewServeMux() 107 | mux.HandleFunc("/new", pf(v.New)) 108 | mux.HandleFunc("/ctrl", pf(v.Ctrl)) 109 | mux.HandleFunc("/clipboard", pf(v.Clipboard)) 110 | mux.HandleFunc("/ws/", pf(v.WS)) 111 | mux.HandleFunc("/", v.Render) 112 | if v.cfg.LocalPort == 0 { 113 | v.cfg.LocalPort = global.GeneratePort() 114 | logging.Info("generate port for %s: %d", v.Name, v.cfg.LocalPort) 115 | } 116 | svr := &http.Server{ 117 | Addr: fmt.Sprintf("%s:%d", v.cfg.LocalAddr, v.cfg.LocalPort), 118 | Handler: mux, 119 | } 120 | runtime.Assert(svr.ListenAndServe()) 121 | } 122 | 123 | func (v *VNC) remove(id string) { 124 | v.link = nil 125 | } 126 | -------------------------------------------------------------------------------- /code/client/rule/vnc/vncnetwork/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | protoc --go_out=. *.proto -------------------------------------------------------------------------------- /code/client/rule/vnc/vncnetwork/vncmsg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package vncnetwork; 4 | option go_package="./;vncnetwork"; 5 | 6 | message image_data { 7 | bool ok = 1; 8 | string msg = 2; 9 | uint32 bits = 3; 10 | uint32 width = 4; 11 | uint32 height = 5; 12 | bytes data = 6; 13 | } 14 | 15 | enum status { 16 | unset_st = 0; 17 | down = 1; 18 | up = 2; 19 | } 20 | 21 | message mouse_data { 22 | enum button { 23 | unset_btn = 0; 24 | left = 1; 25 | middle = 2; 26 | right = 3; 27 | } 28 | status type = 1; 29 | button btn = 2; 30 | uint32 x = 3; 31 | uint32 y = 4; 32 | } 33 | 34 | message keyboard_data { 35 | status type = 1; 36 | string key = 2; 37 | } 38 | 39 | message scroll_data { 40 | int32 x = 1; 41 | int32 y = 2; 42 | } 43 | 44 | message clipboard_data { 45 | enum type { 46 | unset_type = 0; 47 | text = 1; 48 | image = 2; 49 | file = 3; 50 | } 51 | bool set = 1; 52 | type _type = 2; 53 | oneof payload { 54 | string data = 10; // text data 55 | // TODO 56 | } 57 | } 58 | 59 | message vnc_msg { 60 | enum type { 61 | capture_req = 0; 62 | capture_data = 1; 63 | mouse_event = 2; 64 | keyboard_event = 3; 65 | set_cursor = 4; 66 | scroll_event = 5; 67 | clipboard_event = 6; 68 | } 69 | type _type = 1; 70 | oneof payload { 71 | image_data data = 2; 72 | mouse_data mouse = 3; 73 | keyboard_data keyboard = 4; 74 | bool show_cursor = 5; 75 | scroll_data scroll = 6; 76 | clipboard_data clipboard = 7; 77 | } 78 | } -------------------------------------------------------------------------------- /code/client/rule/vnc/worker.go: -------------------------------------------------------------------------------- 1 | package vnc 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gorilla/websocket" 7 | "github.com/lwch/natpass/code/client/rule/vnc/worker" 8 | "github.com/lwch/runtime" 9 | ) 10 | 11 | // RunWorker run vnc worker 12 | func RunWorker(port uint16, cursor bool) { 13 | worker := worker.NewWorker(cursor) 14 | if worker == nil { 15 | panic("build context failed") 16 | } 17 | conn, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://127.0.0.1:%d", port), nil) 18 | runtime.Assert(err) 19 | worker.Do(conn) 20 | } 21 | -------------------------------------------------------------------------------- /code/client/rule/vnc/worker/attach_windows.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "syscall" 7 | 8 | "github.com/lwch/natpass/code/client/rule/vnc/define" 9 | "golang.org/x/sys/windows" 10 | ) 11 | 12 | func attachDesktop() (func(), error) { 13 | runtime.LockOSThread() 14 | locked := true 15 | oldDesktop, _, err := syscall.Syscall(define.FuncGetThreadDesktop, 1, uintptr(windows.GetCurrentThreadId()), 0, 0) 16 | if oldDesktop == 0 { 17 | runtime.UnlockOSThread() 18 | return nil, fmt.Errorf("get thread desktop: %v", err) 19 | } 20 | desktop, _, err := syscall.Syscall(define.FuncOpenInputDesktop, 3, 0, 0, windows.GENERIC_ALL) 21 | if desktop == 0 { 22 | runtime.UnlockOSThread() 23 | return nil, fmt.Errorf("open input desktop: %v", err) 24 | } 25 | ok, _, err := syscall.Syscall(define.FuncSetThreadDesktop, 1, desktop, 0, 0) 26 | if ok == 0 { 27 | syscall.Syscall(define.FuncCloseDesktop, 1, desktop, 0, 0) 28 | runtime.UnlockOSThread() 29 | return nil, fmt.Errorf("set thread desktop: %v", err) 30 | } 31 | return func() { 32 | syscall.Syscall(define.FuncSetThreadDesktop, 1, oldDesktop, 0, 0) 33 | syscall.Syscall(define.FuncCloseDesktop, 1, desktop, 0, 0) 34 | if locked { 35 | runtime.UnlockOSThread() 36 | locked = false 37 | } 38 | }, nil 39 | } 40 | -------------------------------------------------------------------------------- /code/client/rule/vnc/worker/attach_xx.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package worker 5 | 6 | func attachDesktop() (func(), error) { 7 | return func() {}, nil 8 | } 9 | -------------------------------------------------------------------------------- /code/client/rule/vnc/worker/capture.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lwch/logging" 7 | "github.com/lwch/natpass/code/client/rule/vnc/vncnetwork" 8 | ) 9 | 10 | func (worker *Worker) runCapture() vncnetwork.ImageData { 11 | detach, err := attachDesktop() 12 | if err != nil { 13 | logging.Error("attach desktop: " + err.Error()) 14 | return vncnetwork.ImageData{ 15 | Ok: false, 16 | Msg: fmt.Sprintf("attach desktop: " + err.Error()), 17 | } 18 | } 19 | defer detach() 20 | img, err := worker.cli.Screenshot() 21 | if err != nil { 22 | logging.Error("screenshot: " + err.Error()) 23 | return vncnetwork.ImageData{ 24 | Ok: false, 25 | Msg: fmt.Sprintf("screenshot: " + err.Error()), 26 | } 27 | } 28 | data := make([]byte, len(img.Pix)) 29 | copy(data, img.Pix) 30 | return vncnetwork.ImageData{ 31 | Ok: true, 32 | Bits: 32, 33 | Width: uint32(img.Rect.Max.X), 34 | Height: uint32(img.Rect.Max.Y), 35 | Data: data, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /code/client/rule/vnc/worker/event.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "github.com/lwch/logging" 6 | "github.com/lwch/natpass/code/client/rule/vnc/vncnetwork" 7 | "github.com/lwch/rdesktop" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | func (worker *Worker) runMouse(data *vncnetwork.MouseData) { 12 | detach, err := attachDesktop() 13 | if err != nil { 14 | logging.Error("attach desktop: %v", err) 15 | return 16 | } 17 | defer detach() 18 | worker.cli.MouseMove(int(data.GetX()), int(data.GetY())) 19 | var button rdesktop.MouseButton 20 | switch data.GetBtn() { 21 | case vncnetwork.MouseData_left: 22 | button = rdesktop.MouseLeft 23 | case vncnetwork.MouseData_right: 24 | button = rdesktop.MouseRight 25 | case vncnetwork.MouseData_middle: 26 | button = rdesktop.MouseMiddle 27 | } 28 | switch data.GetType() { 29 | case vncnetwork.Status_down: 30 | worker.cli.ToggleMouse(button, true) 31 | case vncnetwork.Status_up: 32 | worker.cli.ToggleMouse(button, false) 33 | } 34 | } 35 | 36 | func (worker *Worker) runKeyboard(data *vncnetwork.KeyboardData) { 37 | detach, err := attachDesktop() 38 | if err != nil { 39 | logging.Error("attach desktop: %v", err) 40 | return 41 | } 42 | defer detach() 43 | switch data.Type { 44 | case vncnetwork.Status_down: 45 | worker.cli.ToggleKey(data.Key, true) 46 | case vncnetwork.Status_up: 47 | worker.cli.ToggleKey(data.Key, false) 48 | } 49 | } 50 | 51 | func (worker *Worker) runScroll(data *vncnetwork.ScrollData) { 52 | detach, err := attachDesktop() 53 | if err != nil { 54 | logging.Error("attach desktop: %v", err) 55 | return 56 | } 57 | defer detach() 58 | worker.cli.Scroll(int(data.X), int(data.Y)) 59 | } 60 | 61 | func (worker *Worker) runClipboard(conn *websocket.Conn, data *vncnetwork.ClipboardData) { 62 | detach, err := attachDesktop() 63 | if err != nil { 64 | logging.Error("attach desktop: %v", err) 65 | return 66 | } 67 | defer detach() 68 | if data.GetSet() { 69 | worker.setClipboard(data) 70 | } else { 71 | worker.getClipboard(conn) 72 | } 73 | } 74 | 75 | func (worker *Worker) setClipboard(data *vncnetwork.ClipboardData) { 76 | switch data.GetXType() { 77 | case vncnetwork.ClipboardData_file: 78 | case vncnetwork.ClipboardData_image: 79 | case vncnetwork.ClipboardData_text: 80 | worker.cli.ClipboardSet(data.GetData()) 81 | } 82 | } 83 | 84 | func (worker *Worker) getClipboard(conn *websocket.Conn) { 85 | data, _ := worker.cli.ClipboardGet() 86 | var msg vncnetwork.VncMsg 87 | msg.XType = vncnetwork.VncMsg_clipboard_event 88 | msg.Payload = &vncnetwork.VncMsg_Clipboard{ 89 | Clipboard: &vncnetwork.ClipboardData{ 90 | Set: true, 91 | XType: vncnetwork.ClipboardData_text, 92 | Payload: &vncnetwork.ClipboardData_Data{ 93 | Data: data, 94 | }, 95 | }, 96 | } 97 | enc, _ := proto.Marshal(&msg) 98 | conn.WriteMessage(websocket.BinaryMessage, enc) 99 | } 100 | -------------------------------------------------------------------------------- /code/client/rule/vnc/worker/worker.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "image" 5 | "image/jpeg" 6 | "os" 7 | 8 | "github.com/gorilla/websocket" 9 | "github.com/lwch/logging" 10 | "github.com/lwch/natpass/code/client/rule/vnc/vncnetwork" 11 | "github.com/lwch/rdesktop" 12 | "github.com/lwch/runtime" 13 | "google.golang.org/protobuf/proto" 14 | ) 15 | 16 | // Worker worker object 17 | type Worker struct { 18 | cli *rdesktop.Client 19 | } 20 | 21 | // NewWorker create worker 22 | func NewWorker(showCursor bool) *Worker { 23 | worker := &Worker{} 24 | cli, err := rdesktop.New() 25 | if err != nil { 26 | logging.Error("create rdesktop: %v", err) 27 | return nil 28 | } 29 | worker.cli = cli 30 | return worker 31 | } 32 | 33 | // Do handle worker 34 | func (worker *Worker) Do(conn *websocket.Conn) { 35 | defer conn.Close() 36 | for { 37 | _, data, err := conn.ReadMessage() 38 | runtime.Assert(err) 39 | var msg vncnetwork.VncMsg 40 | err = proto.Unmarshal(data, &msg) 41 | if err != nil { 42 | logging.Error("proto unmarshal: %v", err) 43 | continue 44 | } 45 | switch msg.GetXType() { 46 | case vncnetwork.VncMsg_capture_req: 47 | data := worker.runCapture() 48 | // if data.Ok { 49 | // dumpImage(data.Data, int(data.Width), int(data.Height)) 50 | // } 51 | if !data.Ok { 52 | logging.Error("capture: %s", data.Msg) 53 | } 54 | msg.XType = vncnetwork.VncMsg_capture_data 55 | msg.Payload = &vncnetwork.VncMsg_Data{ 56 | Data: &data, 57 | } 58 | enc, _ := proto.Marshal(&msg) 59 | conn.WriteMessage(websocket.BinaryMessage, enc) 60 | case vncnetwork.VncMsg_mouse_event: 61 | worker.runMouse(msg.GetMouse()) 62 | case vncnetwork.VncMsg_keyboard_event: 63 | worker.runKeyboard(msg.GetKeyboard()) 64 | case vncnetwork.VncMsg_set_cursor: 65 | worker.cli.ShowCursor(msg.GetShowCursor()) 66 | case vncnetwork.VncMsg_scroll_event: 67 | worker.runScroll(msg.GetScroll()) 68 | case vncnetwork.VncMsg_clipboard_event: 69 | worker.runClipboard(conn, msg.GetClipboard()) 70 | } 71 | } 72 | } 73 | 74 | // TestCapture test capture 75 | func (worker *Worker) TestCapture() { 76 | msg := worker.runCapture() 77 | dumpImage(msg.Data, int(msg.Width), int(msg.Height)) 78 | } 79 | 80 | func dumpImage(data []byte, width, height int) { 81 | f, err := os.Create(`C:\Users\lwch\Pictures\debug.jpeg`) 82 | if err != nil { 83 | logging.Error("debug: %v", err) 84 | return 85 | } 86 | defer f.Close() 87 | img := image.NewRGBA(image.Rect(0, 0, width, height)) 88 | copy(img.Pix, data) 89 | err = jpeg.Encode(f, img, nil) 90 | if err != nil { 91 | logging.Error("encode: %v", err) 92 | return 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /code/hash/hash.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha512" 6 | "encoding/binary" 7 | "hash" 8 | "math" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // Hasher hasher for handshake 14 | type Hasher struct { 15 | sync.Mutex 16 | period uint 17 | h hash.Hash 18 | } 19 | 20 | // New create hasher 21 | func New(secret string, period uint) *Hasher { 22 | if period == 0 { 23 | period = 30 24 | } 25 | return &Hasher{ 26 | period: period, 27 | h: hmac.New(sha512.New, []byte(secret)), 28 | } 29 | } 30 | 31 | // Hash hash func 32 | func (h *Hasher) Hash() []byte { 33 | now := time.Now() 34 | i := math.Floor(float64(now.Unix()) / float64(h.period)) 35 | var buf [8]byte 36 | binary.BigEndian.PutUint64(buf[:], uint64(i)) 37 | h.Lock() 38 | defer h.Unlock() 39 | h.h.Reset() 40 | h.h.Write(buf[:]) 41 | ret := h.h.Sum(nil) 42 | return ret 43 | } 44 | -------------------------------------------------------------------------------- /code/network/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | protoc --go_out=. *.proto -------------------------------------------------------------------------------- /code/network/code.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package network; 4 | option go_package="./;network"; 5 | 6 | message code_header_values { 7 | repeated string values = 1; 8 | } 9 | 10 | // normal request 11 | message code_request { 12 | uint64 request_id = 1; 13 | string method = 2; 14 | string uri = 3; 15 | bytes body = 4; 16 | map header = 5; 17 | } 18 | 19 | message code_response_header { 20 | uint64 request_id = 1; 21 | uint32 code = 2; 22 | map header = 3; 23 | } 24 | 25 | message code_response_body { 26 | uint64 request_id = 1; 27 | uint32 index = 2; 28 | uint32 mask = 3; 29 | bytes body = 4; 30 | } 31 | 32 | // websocket 33 | message code_connect { 34 | uint64 request_id = 1; 35 | string uri = 2; 36 | map header = 3; 37 | } 38 | 39 | message code_connect_response { 40 | uint64 request_id = 1; 41 | bool ok = 2; 42 | string msg = 3; 43 | map header = 4; 44 | } 45 | 46 | message code_data { 47 | uint64 request_id = 1; 48 | bool ok = 2; 49 | uint32 type = 3; 50 | bytes data = 4; 51 | } -------------------------------------------------------------------------------- /code/network/connect.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package network; 4 | option go_package="./;network"; 5 | 6 | message connect_addr { 7 | string addr = 1; 8 | uint32 port = 2; 9 | } 10 | 11 | message connect_shell { 12 | string exec = 1; 13 | repeated string env = 2; 14 | } 15 | 16 | message connect_vnc { 17 | uint32 fps = 1; 18 | uint32 quality = 2; // image quality, percent 19 | bool cursor = 3; // show cursor 20 | } 21 | 22 | message connect_request { 23 | enum type { 24 | tcp = 0; // tcp reverse proxy 25 | udp = 1; // udp reverse proxy 26 | shell = 2; // shell 27 | vnc = 3; // vnc 28 | bench = 4; // benchmark 29 | code = 5; // code-server 30 | } 31 | string name = 1; // rule name 32 | type _type = 2; // rule type 33 | oneof payload { 34 | connect_addr caddr = 10; // for reverse proxy 35 | connect_shell cshell = 11; // for shell 36 | connect_vnc cvnc = 12; // for vnc 37 | } 38 | } 39 | 40 | message connect_response { 41 | bool ok = 1; 42 | string msg = 2; 43 | } 44 | -------------------------------------------------------------------------------- /code/network/encoding/encoding.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import "io" 4 | 5 | // Codec format data to []byte, decode data from []byte 6 | type Codec interface { 7 | // Marshal format data to []byte 8 | Marshal(interface{}) ([]byte, error) 9 | // Unmarshal decode data from []byte 10 | Unmarshal([]byte, interface{}) error 11 | } 12 | 13 | // Compressor compressor interface 14 | type Compressor interface { 15 | // Compress get compress writer 16 | Compress(io.Writer) (io.WriteCloser, error) 17 | // Decompress get decompress reader 18 | Decompress(io.Reader) (io.ReadCloser, error) 19 | // SetLevel set compress level 20 | SetLevel(int) error 21 | } 22 | -------------------------------------------------------------------------------- /code/network/encoding/gzip/gzip.go: -------------------------------------------------------------------------------- 1 | package gzip 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "io" 7 | "sync" 8 | 9 | "github.com/lwch/natpass/code/network/encoding" 10 | "github.com/lwch/runtime" 11 | ) 12 | 13 | type writer struct { 14 | *gzip.Writer 15 | pool *sync.Pool 16 | } 17 | 18 | // Close close write and put writer to pool 19 | func (w *writer) Close() error { 20 | w.pool.Put(w) 21 | return w.Writer.Close() 22 | } 23 | 24 | type reader struct { 25 | *gzip.Reader 26 | pool *sync.Pool 27 | } 28 | 29 | // Close close reader and put reader to pool 30 | func (r *reader) Close() error { 31 | r.pool.Put(r) 32 | return r.Reader.Close() 33 | } 34 | 35 | type compressor struct { 36 | level int 37 | poolWriter [gzip.BestCompression]sync.Pool 38 | poolReader sync.Pool 39 | } 40 | 41 | // New create compressor 42 | func New(level ...int) (encoding.Compressor, error) { 43 | if len(level) > 0 { 44 | if level[0] < 0 || level[0] > gzip.BestCompression { 45 | return nil, fmt.Errorf("invalid gzip compress level: %d", level[0]) 46 | } 47 | } else { 48 | level = append(level, 6) 49 | } 50 | ret := new(compressor) 51 | ret.level = level[0] 52 | for i := 0; i < gzip.BestCompression; i++ { 53 | ret.poolWriter[i].New = func() interface{} { 54 | w, err := gzip.NewWriterLevel(io.Discard, i) 55 | runtime.Assert(err) 56 | return &writer{Writer: w, pool: &ret.poolWriter[i]} 57 | } 58 | } 59 | ret.poolReader.New = func() interface{} { 60 | r, err := gzip.NewReader(io.NopCloser(nil)) 61 | runtime.Assert(err) 62 | return &reader{Reader: r, pool: &ret.poolReader} 63 | } 64 | return ret, nil 65 | } 66 | 67 | // Compress gzip compress 68 | func (c *compressor) Compress(w io.Writer) (io.WriteCloser, error) { 69 | pw := c.poolWriter[c.level].Get().(*writer) 70 | pw.Writer.Reset(w) 71 | return pw, nil 72 | } 73 | 74 | // Decompress gzip decompress 75 | func (c *compressor) Decompress(r io.Reader) (io.ReadCloser, error) { 76 | pr := c.poolReader.Get().(*reader) 77 | pr.Reader.Reset(r) 78 | return pr, nil 79 | } 80 | 81 | // SetLevel set compress level 82 | func (c *compressor) SetLevel(level int) error { 83 | if level < 0 || level > gzip.BestCompression { 84 | return fmt.Errorf("invalid gzip compress level: %d", level) 85 | } 86 | c.level = level 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /code/network/encoding/proto/proto.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lwch/natpass/code/network/encoding" 7 | "google.golang.org/protobuf/proto" 8 | ) 9 | 10 | type codec struct{} 11 | 12 | // New create protobuf codec 13 | func New() encoding.Codec { 14 | return &codec{} 15 | } 16 | 17 | // Marshal protobuf marshal 18 | func (*codec) Marshal(v interface{}) ([]byte, error) { 19 | vv, ok := v.(proto.Message) 20 | if !ok { 21 | return nil, fmt.Errorf("invalid value type, want proto.Message, got %T", v) 22 | } 23 | return proto.Marshal(vv) 24 | } 25 | 26 | // Unmarshal protobuf unmarshal 27 | func (*codec) Unmarshal(data []byte, v interface{}) error { 28 | vv, ok := v.(proto.Message) 29 | if !ok { 30 | return fmt.Errorf("invalid value type, want proto.Message, got %T", v) 31 | } 32 | return proto.Unmarshal(data, vv) 33 | } 34 | -------------------------------------------------------------------------------- /code/network/forward.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.0 4 | // protoc v3.21.2 5 | // source: forward.proto 6 | 7 | package network 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type Data struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` 29 | } 30 | 31 | func (x *Data) Reset() { 32 | *x = Data{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_forward_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *Data) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*Data) ProtoMessage() {} 45 | 46 | func (x *Data) ProtoReflect() protoreflect.Message { 47 | mi := &file_forward_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use Data.ProtoReflect.Descriptor instead. 59 | func (*Data) Descriptor() ([]byte, []int) { 60 | return file_forward_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *Data) GetData() []byte { 64 | if x != nil { 65 | return x.Data 66 | } 67 | return nil 68 | } 69 | 70 | var File_forward_proto protoreflect.FileDescriptor 71 | 72 | var file_forward_proto_rawDesc = []byte{ 73 | 0x0a, 0x0d, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 74 | 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x1a, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 75 | 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 76 | 0x64, 0x61, 0x74, 0x61, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 0x2f, 0x3b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 77 | 0x72, 0x6b, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 78 | } 79 | 80 | var ( 81 | file_forward_proto_rawDescOnce sync.Once 82 | file_forward_proto_rawDescData = file_forward_proto_rawDesc 83 | ) 84 | 85 | func file_forward_proto_rawDescGZIP() []byte { 86 | file_forward_proto_rawDescOnce.Do(func() { 87 | file_forward_proto_rawDescData = protoimpl.X.CompressGZIP(file_forward_proto_rawDescData) 88 | }) 89 | return file_forward_proto_rawDescData 90 | } 91 | 92 | var file_forward_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 93 | var file_forward_proto_goTypes = []interface{}{ 94 | (*Data)(nil), // 0: network.data 95 | } 96 | var file_forward_proto_depIdxs = []int32{ 97 | 0, // [0:0] is the sub-list for method output_type 98 | 0, // [0:0] is the sub-list for method input_type 99 | 0, // [0:0] is the sub-list for extension type_name 100 | 0, // [0:0] is the sub-list for extension extendee 101 | 0, // [0:0] is the sub-list for field type_name 102 | } 103 | 104 | func init() { file_forward_proto_init() } 105 | func file_forward_proto_init() { 106 | if File_forward_proto != nil { 107 | return 108 | } 109 | if !protoimpl.UnsafeEnabled { 110 | file_forward_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 111 | switch v := v.(*Data); i { 112 | case 0: 113 | return &v.state 114 | case 1: 115 | return &v.sizeCache 116 | case 2: 117 | return &v.unknownFields 118 | default: 119 | return nil 120 | } 121 | } 122 | } 123 | type x struct{} 124 | out := protoimpl.TypeBuilder{ 125 | File: protoimpl.DescBuilder{ 126 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 127 | RawDescriptor: file_forward_proto_rawDesc, 128 | NumEnums: 0, 129 | NumMessages: 1, 130 | NumExtensions: 0, 131 | NumServices: 0, 132 | }, 133 | GoTypes: file_forward_proto_goTypes, 134 | DependencyIndexes: file_forward_proto_depIdxs, 135 | MessageInfos: file_forward_proto_msgTypes, 136 | }.Build() 137 | File_forward_proto = out.File 138 | file_forward_proto_rawDesc = nil 139 | file_forward_proto_goTypes = nil 140 | file_forward_proto_depIdxs = nil 141 | } 142 | -------------------------------------------------------------------------------- /code/network/forward.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package network; 4 | option go_package="./;network"; 5 | 6 | message data { 7 | bytes data = 1; 8 | } -------------------------------------------------------------------------------- /code/network/msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package network; 4 | option go_package="./;network"; 5 | 6 | import "connect.proto"; 7 | import "forward.proto"; 8 | import "shell.proto"; 9 | import "vnc.proto"; 10 | import "code.proto"; 11 | 12 | message handshake_payload { 13 | bytes enc = 1; 14 | } 15 | 16 | message msg { 17 | enum type { 18 | unknown = 0; 19 | handshake = 1; 20 | keepalive = 2; 21 | connect_req = 3; 22 | connect_rep = 4; 23 | disconnect = 5; 24 | forward = 6; 25 | // shell 26 | shell_resize = 10; 27 | shell_data = 11; 28 | // vnc 29 | vnc_ctrl = 20; 30 | vnc_image = 21; 31 | vnc_mouse = 22; 32 | vnc_keyboard = 23; 33 | vnc_cad = 24; // ctrl+alt+del 34 | vnc_scroll = 25; 35 | vnc_clipboard = 26; 36 | // code-server 37 | code_request = 30; 38 | code_response_hdr = 31; 39 | code_response_body = 32; 40 | code_connect = 33; 41 | code_connect_response = 34; 42 | code_data = 35; 43 | } 44 | type _type = 1; 45 | string from = 2; 46 | string to = 4; 47 | string link_id = 6; 48 | oneof payload { 49 | handshake_payload hsp = 10; 50 | connect_request creq = 11; 51 | connect_response crep = 12; 52 | data _data = 13; 53 | // shell 54 | shell_resize sresize = 20; 55 | shell_data sdata = 21; 56 | // vnc 57 | vnc_control vctrl = 30; 58 | vnc_image vimg = 31; 59 | vnc_mouse vmouse = 32; 60 | vnc_keyboard vkbd = 33; 61 | vnc_scroll vscroll = 34; 62 | vnc_clipboard vclipboard = 35; 63 | // code-server 64 | code_request csreq = 40; 65 | code_response_header csrep_hdr = 41; 66 | code_response_body csrep_body = 42; 67 | code_connect csconn = 43; 68 | code_connect_response csconn_rep = 44; 69 | code_data csdata = 45; 70 | } 71 | } -------------------------------------------------------------------------------- /code/network/shell.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package network; 4 | option go_package="./;network"; 5 | 6 | message shell_resize { 7 | uint32 rows = 1; 8 | uint32 cols = 2; 9 | } 10 | 11 | message shell_data { 12 | bytes data = 1; 13 | } -------------------------------------------------------------------------------- /code/network/vnc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package network; 4 | option go_package="./;network"; 5 | 6 | message vnc_control { 7 | uint32 quality = 1; // image quality, percent 8 | bool cursor = 2; // show cursor 9 | } 10 | 11 | message vnc_image { 12 | message info { 13 | uint32 screen_width = 1; 14 | uint32 screen_height = 2; 15 | uint32 rect_x = 3; 16 | uint32 rect_y = 4; 17 | uint32 rect_width = 5; 18 | uint32 rect_height = 6; 19 | } 20 | enum encoding { 21 | raw = 0; 22 | jpeg = 1; 23 | png = 2; 24 | } 25 | info _info = 1; 26 | encoding encode = 2; 27 | bytes data = 3; 28 | } 29 | 30 | enum vnc_status { 31 | unset_st = 0; 32 | down = 1; 33 | up = 2; 34 | } 35 | 36 | message vnc_mouse { 37 | enum button { 38 | unset_btn = 0; 39 | left = 1; 40 | middle = 2; 41 | right = 3; 42 | } 43 | vnc_status type = 1; 44 | button btn = 2; 45 | uint32 x = 3; 46 | uint32 y = 4; 47 | } 48 | 49 | message vnc_keyboard { 50 | vnc_status type = 1; 51 | // https://github.com/go-vgo/robotgo/blob/master/docs/keys.md 52 | string key = 2; 53 | } 54 | 55 | message vnc_scroll { 56 | int32 x = 1; 57 | int32 y = 2; 58 | } 59 | 60 | message vnc_clipboard { 61 | enum type { 62 | unset_type = 0; 63 | text = 1; 64 | image = 2; 65 | file = 3; 66 | } 67 | bool set = 1; 68 | type _type = 2; 69 | oneof payload { 70 | string data = 10; // text data 71 | // TODO 72 | } 73 | } -------------------------------------------------------------------------------- /code/server/app/cmd.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | rt "runtime" 8 | 9 | "github.com/kardianos/service" 10 | "github.com/lwch/natpass/code/server/global" 11 | "github.com/lwch/natpass/code/utils" 12 | "github.com/lwch/runtime" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // User --user param 17 | var User string 18 | 19 | // ConfDir --conf param 20 | var ConfDir string 21 | 22 | // Version application version 23 | var Version string 24 | 25 | // App application 26 | type App struct { 27 | p *program 28 | } 29 | 30 | // NewApp create application 31 | func NewApp() *App { 32 | return &App{ 33 | p: newProgram(), 34 | } 35 | } 36 | 37 | func buildService(p *program) service.Service { 38 | dir, err := filepath.Abs(ConfDir) 39 | runtime.Assert(err) 40 | 41 | var depends []string 42 | if rt.GOOS != "windows" { 43 | depends = append(depends, "After=network.target") 44 | } 45 | var opt service.KeyValue 46 | switch rt.GOOS { 47 | case "windows": 48 | opt = service.KeyValue{ 49 | "StartType": "automatic", 50 | "OnFailure": "restart", 51 | "OnFailureDelayDuration": "5s", 52 | "OnFailureResetPeriod": 10, 53 | } 54 | case "linux": 55 | opt = service.KeyValue{ 56 | "LimitNOFILE": 65000, 57 | } 58 | case "darwin": 59 | opt = service.KeyValue{ 60 | "SessionCreate": true, 61 | } 62 | } 63 | 64 | svc, err := service.New(p, &service.Config{ 65 | Name: "np-svr", 66 | DisplayName: "np-svr", 67 | Description: "natpass server", 68 | UserName: User, 69 | Arguments: []string{"--conf", dir}, 70 | Dependencies: depends, 71 | Option: opt, 72 | }) 73 | runtime.Assert(err) 74 | return svc 75 | } 76 | 77 | // Run run application 78 | func (a *App) Run(*cobra.Command, []string) { 79 | a.p.setConfigure(global.LoadConf(ConfDir)) 80 | 81 | runtime.Assert(buildService(a.p).Run()) 82 | } 83 | 84 | // Install register service 85 | func (a *App) Install(*cobra.Command, []string) { 86 | cfg := global.LoadConf(ConfDir) 87 | 88 | err := buildService(a.p).Install() 89 | if err != nil { 90 | fmt.Println(err.Error()) 91 | os.Exit(1) 92 | } 93 | utils.BuildDir(cfg.LogDir, User) 94 | fmt.Println("register service np-svr success") 95 | } 96 | 97 | // Uninstall unregister service 98 | func (a *App) Uninstall(*cobra.Command, []string) { 99 | err := buildService(a.p).Uninstall() 100 | if err != nil { 101 | fmt.Println(err.Error()) 102 | os.Exit(1) 103 | } 104 | fmt.Println("unregister service np-svr success") 105 | } 106 | 107 | // Start start service 108 | func (a *App) Start(*cobra.Command, []string) { 109 | err := buildService(a.p).Start() 110 | if err != nil { 111 | fmt.Println(err.Error()) 112 | os.Exit(1) 113 | } 114 | fmt.Println("start service np-svr success") 115 | } 116 | 117 | // Stop stop service 118 | func (a *App) Stop(*cobra.Command, []string) { 119 | err := buildService(a.p).Stop() 120 | if err != nil { 121 | fmt.Println(err.Error()) 122 | os.Exit(1) 123 | } 124 | fmt.Println("stop service np-svr success") 125 | } 126 | 127 | // Restart restart service 128 | func (a *App) Restart(*cobra.Command, []string) { 129 | err := buildService(a.p).Restart() 130 | if err != nil { 131 | fmt.Println(err.Error()) 132 | os.Exit(1) 133 | } 134 | fmt.Println("restart service np-svr success") 135 | } 136 | 137 | // Status show service status 138 | func (a *App) Status(*cobra.Command, []string) { 139 | status, err := buildService(a.p).Status() 140 | if err != nil { 141 | fmt.Println(err.Error()) 142 | os.Exit(1) 143 | } 144 | switch status { 145 | case service.StatusRunning: 146 | fmt.Println("service is running") 147 | case service.StatusStopped: 148 | fmt.Println("service is stopped") 149 | case service.StatusUnknown: 150 | fmt.Println("service status is unknown") 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /code/server/app/program.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | rt "runtime" 8 | 9 | "github.com/common-nighthawk/go-figure" 10 | "github.com/kardianos/service" 11 | "github.com/lwch/logging" 12 | "github.com/lwch/natpass/code/server/global" 13 | "github.com/lwch/natpass/code/server/handler" 14 | "github.com/lwch/runtime" 15 | ) 16 | 17 | type program struct { 18 | cfg *global.Configure 19 | } 20 | 21 | func newProgram() *program { 22 | return &program{} 23 | } 24 | 25 | func (p *program) setConfigure(cfg *global.Configure) *program { 26 | p.cfg = cfg 27 | return p 28 | } 29 | 30 | func (p *program) Start(s service.Service) error { 31 | go p.run() 32 | return nil 33 | } 34 | 35 | func (p *program) run() { 36 | // initialize logging 37 | stdout := true 38 | if rt.GOOS == "windows" { 39 | stdout = false 40 | } 41 | logging.SetSizeRotate(logging.SizeRotateConfig{ 42 | Dir: p.cfg.LogDir, 43 | Name: "np-svr", 44 | Size: int64(p.cfg.LogSize.Bytes()), 45 | Rotate: p.cfg.LogRotate, 46 | WriteStdout: stdout, 47 | WriteFile: true, 48 | }) 49 | defer logging.Flush() 50 | 51 | fg := figure.NewFigure("NatPass", "alligator2", false) 52 | figure.Write(&logging.DefaultLogger, fg) 53 | logging.DefaultLogger.Write(nil) 54 | 55 | // go func() { 56 | // http.ListenAndServe(":7878", nil) 57 | // }() 58 | 59 | var l net.Listener 60 | if len(p.cfg.TLSCrt) > 0 && len(p.cfg.TLSKey) > 0 { 61 | cert, err := tls.LoadX509KeyPair(p.cfg.TLSCrt, p.cfg.TLSKey) 62 | runtime.Assert(err) 63 | l, err = tls.Listen("tcp", fmt.Sprintf(":%d", p.cfg.Listen), &tls.Config{ 64 | Certificates: []tls.Certificate{cert}, 65 | }) 66 | runtime.Assert(err) 67 | logging.Info("listen on %d", p.cfg.Listen) 68 | } else { 69 | var err error 70 | l, err = net.Listen("tcp", fmt.Sprintf(":%d", p.cfg.Listen)) 71 | runtime.Assert(err) 72 | } 73 | 74 | p.serve(l) 75 | } 76 | 77 | func (p *program) Stop(s service.Service) error { 78 | return nil 79 | } 80 | 81 | func (p *program) serve(l net.Listener) { 82 | h := handler.New(p.cfg) 83 | for { 84 | conn, err := l.Accept() 85 | if err != nil { 86 | continue 87 | } 88 | go h.Handle(conn) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /code/server/global/conf.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "time" 7 | 8 | "github.com/lwch/natpass/code/hash" 9 | "github.com/lwch/natpass/code/utils" 10 | "github.com/lwch/runtime" 11 | "github.com/lwch/yaml" 12 | ) 13 | 14 | // Configure server configure 15 | type Configure struct { 16 | Listen uint16 17 | Hasher *hash.Hasher 18 | TLSKey string 19 | TLSCrt string 20 | ReadTimeout time.Duration 21 | WriteTimeout time.Duration 22 | LogDir string 23 | LogSize utils.Bytes 24 | LogRotate int 25 | } 26 | 27 | // LoadConf load configure file 28 | func LoadConf(dir string) *Configure { 29 | var cfg struct { 30 | Listen uint16 `yaml:"listen"` 31 | Secret string `yaml:"secret"` 32 | Link struct { 33 | ReadTimeout time.Duration `yaml:"read_timeout"` 34 | WriteTimeout time.Duration `yaml:"write_timeout"` 35 | } `yaml:"link"` 36 | Log struct { 37 | Dir string `yaml:"dir"` 38 | Size utils.Bytes `yaml:"size"` 39 | Rotate int `yaml:"rotate"` 40 | } `yaml:"log"` 41 | TLS struct { 42 | Key string `yaml:"key"` 43 | Crt string `yaml:"crt"` 44 | } `yaml:"tls"` 45 | } 46 | cfg.Listen = 6154 47 | cfg.Secret = "0123456789" 48 | cfg.Link.ReadTimeout = time.Second 49 | cfg.Link.WriteTimeout = time.Second 50 | cfg.Log.Dir = "./logs" 51 | cfg.Log.Size = 50 * 1024 * 1024 52 | cfg.Log.Rotate = 7 53 | runtime.Assert(yaml.Decode(dir, &cfg)) 54 | if !filepath.IsAbs(cfg.Log.Dir) { 55 | dir, err := os.Executable() 56 | runtime.Assert(err) 57 | cfg.Log.Dir = filepath.Join(filepath.Dir(dir), cfg.Log.Dir) 58 | } 59 | return &Configure{ 60 | Listen: cfg.Listen, 61 | Hasher: hash.New(cfg.Secret, 60), 62 | TLSKey: cfg.TLS.Key, 63 | TLSCrt: cfg.TLS.Crt, 64 | ReadTimeout: cfg.Link.ReadTimeout, 65 | WriteTimeout: cfg.Link.WriteTimeout, 66 | LogDir: cfg.Log.Dir, 67 | LogSize: cfg.Log.Size, 68 | LogRotate: cfg.Log.Rotate, 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /code/server/handler/client.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "time" 7 | 8 | "github.com/lwch/logging" 9 | "github.com/lwch/natpass/code/network" 10 | ) 11 | 12 | type client struct { 13 | sync.RWMutex 14 | id string 15 | parent *clients 16 | conn *network.Conn 17 | updated time.Time 18 | links map[string]struct{} // link id => struct{} 19 | } 20 | 21 | func (c *client) close() { 22 | for _, link := range c.getLinks() { 23 | c.parent.parent.closeLink(link) 24 | c.Lock() 25 | delete(c.links, link) 26 | c.Unlock() 27 | } 28 | c.conn.Close() 29 | logging.Info("client %s connection closed", c.id) 30 | } 31 | 32 | func (c *client) run() { 33 | defer c.parent.close(c.id) 34 | for { 35 | if time.Since(c.updated).Seconds() > 600 { 36 | links := make([]string, 0, len(c.links)) 37 | c.RLock() 38 | for id := range c.links { 39 | links = append(links, id) 40 | } 41 | c.RUnlock() 42 | logging.Info("%s is not keepalived, links: %v", c.id, links) 43 | return 44 | } 45 | msg, size, err := c.conn.ReadMessage(c.parent.parent.cfg.ReadTimeout) 46 | if err != nil { 47 | if strings.Contains(err.Error(), "i/o timeout") { 48 | continue 49 | } 50 | logging.Error("read message from %s: %v", c.id, err) 51 | return 52 | } 53 | c.updated = time.Now() 54 | c.parent.parent.onMessage(c, c.conn, msg, size) 55 | } 56 | } 57 | 58 | func (c *client) writeMessage(msg *network.Msg) error { 59 | return c.conn.WriteMessage(msg, c.parent.parent.cfg.WriteTimeout) 60 | } 61 | 62 | func (c *client) addLink(id string) { 63 | c.Lock() 64 | c.links[id] = struct{}{} 65 | c.Unlock() 66 | } 67 | 68 | func (c *client) removeLink(id string) { 69 | c.Lock() 70 | delete(c.links, id) 71 | c.Unlock() 72 | } 73 | 74 | func (c *client) getLinks() []string { 75 | ret := make([]string, 0, len(c.links)) 76 | c.RLock() 77 | for link := range c.links { 78 | ret = append(ret, link) 79 | } 80 | c.RUnlock() 81 | return ret 82 | } 83 | 84 | func (c *client) sendClose(id string) { 85 | var msg network.Msg 86 | msg.From = "server" 87 | msg.To = c.id 88 | msg.XType = network.Msg_disconnect 89 | msg.LinkId = id 90 | c.conn.WriteMessage(&msg, c.parent.parent.cfg.WriteTimeout) 91 | c.Lock() 92 | delete(c.links, id) 93 | c.Unlock() 94 | } 95 | 96 | func (c *client) keepalive() { 97 | var msg network.Msg 98 | msg.From = "server" 99 | msg.To = c.id 100 | msg.XType = network.Msg_keepalive 101 | for { 102 | time.Sleep(10 * time.Second) 103 | err := c.writeMessage(&msg) 104 | if err != nil { 105 | logging.Error("send keepalive: %v", err) 106 | return 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /code/server/handler/clients.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/lwch/natpass/code/network" 8 | ) 9 | 10 | type clients struct { 11 | sync.RWMutex 12 | parent *Handler 13 | data map[string]*client // id => client 14 | } 15 | 16 | func newClients(parent *Handler) *clients { 17 | return &clients{ 18 | parent: parent, 19 | data: make(map[string]*client), 20 | } 21 | } 22 | 23 | func (cs *clients) new(id string, conn *network.Conn) *client { 24 | cli := &client{ 25 | id: id, 26 | parent: cs, 27 | conn: conn, 28 | updated: time.Now(), 29 | links: make(map[string]struct{}), 30 | } 31 | cs.Lock() 32 | if c, ok := cs.data[id]; ok { 33 | c.close() 34 | delete(cs.data, id) 35 | } 36 | cs.data[id] = cli 37 | cs.Unlock() 38 | return cli 39 | } 40 | 41 | func (cs *clients) lookup(id string) *client { 42 | cs.RLock() 43 | defer cs.RUnlock() 44 | return cs.data[id] 45 | } 46 | 47 | func (cs *clients) close(id string) { 48 | cs.Lock() 49 | if c, ok := cs.data[id]; ok { 50 | c.close() 51 | delete(cs.data, id) 52 | } 53 | cs.Unlock() 54 | } 55 | -------------------------------------------------------------------------------- /code/server/handler/errors.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import "errors" 4 | 5 | var errNotHandshake = errors.New("not handshake") 6 | var errInvalidHandshake = errors.New("invalid handshake") 7 | -------------------------------------------------------------------------------- /code/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | _ "net/http/pprof" 8 | 9 | "github.com/lwch/natpass/code/server/app" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | version string = "0.0.0" 15 | gitHash string 16 | gitReversion string 17 | buildTime string 18 | ) 19 | 20 | var a = app.NewApp() 21 | 22 | var rootCmd = &cobra.Command{ 23 | Use: "np-cli", 24 | Short: "natpass client", 25 | Run: a.Run, 26 | } 27 | 28 | var installCmd = &cobra.Command{ 29 | Use: "install", 30 | Short: "register service", 31 | Run: a.Install, 32 | } 33 | 34 | var uninstallCmd = &cobra.Command{ 35 | Use: "uninstall", 36 | Short: "unregister service", 37 | Run: a.Uninstall, 38 | } 39 | 40 | var startCmd = &cobra.Command{ 41 | Use: "start", 42 | Short: "start service", 43 | Run: a.Start, 44 | } 45 | 46 | var stopCmd = &cobra.Command{ 47 | Use: "stop", 48 | Short: "stop service", 49 | Run: a.Stop, 50 | } 51 | 52 | var restartCmd = &cobra.Command{ 53 | Use: "restart", 54 | Short: "restart service", 55 | Run: a.Restart, 56 | } 57 | 58 | var statusCmd = &cobra.Command{ 59 | Use: "status", 60 | Short: "show service status", 61 | Run: a.Status, 62 | } 63 | 64 | var versionCmd = &cobra.Command{ 65 | Use: "version", 66 | Short: "show version info", 67 | Run: func(*cobra.Command, []string) { 68 | fmt.Printf("version: v%s\ntime: %s\ncommit: %s.%s\n", 69 | version, 70 | buildTime, 71 | gitHash, gitReversion) 72 | os.Exit(0) 73 | }, 74 | } 75 | 76 | func main() { 77 | app.Version = version 78 | installCmd.Flags().StringVarP(&app.ConfDir, "conf", "c", "", "configure file path") 79 | installCmd.Flags().StringVarP(&app.User, "user", "u", "", "service user") 80 | installCmd.MarkFlagRequired("conf") 81 | rootCmd.AddCommand(installCmd, uninstallCmd) 82 | rootCmd.AddCommand(startCmd, stopCmd, restartCmd, statusCmd) 83 | rootCmd.AddCommand(versionCmd) 84 | 85 | rootCmd.CompletionOptions.DisableDefaultCmd = true 86 | rootCmd.Flags().StringVarP(&app.ConfDir, "conf", "c", "", "configure file path") 87 | rootCmd.MarkFlagRequired("conf") 88 | err := rootCmd.Execute() 89 | if err != nil { 90 | fmt.Println(err.Error()) 91 | os.Exit(1) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /code/utils/bytes.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/dustin/go-humanize" 4 | 5 | // Bytes bytes for yaml decode 6 | type Bytes uint64 7 | 8 | // UnmarshalYAML custom decode bytes 9 | func (bt *Bytes) UnmarshalYAML(unmarshal func(interface{}) error) error { 10 | var str string 11 | err := unmarshal(&str) 12 | if err != nil { 13 | return err 14 | } 15 | n, err := humanize.ParseBytes(str) 16 | if err != nil { 17 | return err 18 | } 19 | *bt = Bytes(n) 20 | return nil 21 | } 22 | 23 | // Bytes bytes count 24 | func (bt *Bytes) Bytes() uint64 { 25 | return uint64(*bt) 26 | } 27 | -------------------------------------------------------------------------------- /code/utils/recover.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/lwch/logging" 4 | 5 | // Recover default recover 6 | func Recover(name string) { 7 | if err := recover(); err != nil { 8 | logging.Error("%s: %v", name, err) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /code/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "strconv" 7 | 8 | "github.com/lwch/runtime" 9 | ) 10 | 11 | // BuildDir mkdir and chown 12 | func BuildDir(dir, u string) { 13 | runtime.Assert(os.MkdirAll(dir, 0755)) 14 | if len(u) > 0 { 15 | us, err := user.Lookup(u) 16 | runtime.Assert(err) 17 | uid, _ := strconv.ParseInt(us.Uid, 10, 32) 18 | gid, _ := strconv.ParseInt(us.Gid, 10, 32) 19 | runtime.Assert(os.Chown(dir, int(uid), int(gid))) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /conf/common.yaml: -------------------------------------------------------------------------------- 1 | secret: 0123456789 # 预共享密钥,客户端和服务端必须保持一致,否则握手失败 2 | link: 3 | read_timeout: 1s # 读取数据包超时时间 4 | write_timeout: 1s # 发送数据包超时时间 5 | log: 6 | dir: ./logs # 路径,相对于可执行文件所在目录的相对路径 7 | size: 50M # 单个文件大小 8 | rotate: 7 # 保留数量 9 | codedir: ./code # code-server配置保存路径 -------------------------------------------------------------------------------- /conf/local.yaml: -------------------------------------------------------------------------------- 1 | id: local # 客户端ID 2 | server: 127.0.0.1:6154 # 服务器地址 3 | ssl: 4 | enabled: false # 是否使用tls连接 5 | insecure: false # 是否关闭SNI,若使用自签证书可将其设置为true 6 | dashboard: # web面板 7 | enabled: true # 是否开放dashboard 8 | listen: 0.0.0.0 # 监听地址 9 | port: 8080 # 监听端口号 10 | #include common.yaml 11 | rules: # rule列表 12 | #include rule.d/*.yaml -------------------------------------------------------------------------------- /conf/remote.yaml: -------------------------------------------------------------------------------- 1 | id: remote # 客户端ID 2 | server: 127.0.0.1:6154 # 服务器地址 3 | ssl: 4 | enabled: false # 是否使用tls连接 5 | insecure: false # 是否关闭SNI,若使用自签证书可将其设置为true 6 | dashboard: # web面板 7 | enabled: false # 是否开放dashboard 8 | #include common.yaml 9 | rules: # rule列表 -------------------------------------------------------------------------------- /conf/rule.d/code-server.yaml: -------------------------------------------------------------------------------- 1 | # 1. 需先在受控端主机上安装code-server并添加到PATH环境变量 2 | # 下载地址:https://github.com/coder/code-server/releases 3 | # 2. code-server默认使用OpenVSX扩展商店 4 | # 若需切换到微软扩展商店可通过配置系统环境变量EXTENSIONS_GALLERY进行切换 5 | # 详见说明:https://github.com/coder/code-server/blob/10f57bac65f9aa5938df4e495da39c608fbf7798/docs/FAQ.md#how-do-i-use-my-own-extensions-marketplace 6 | - name: code-server # 链路名称 7 | target: remote # 目标客户端ID 8 | type: code-server # code-server 9 | local_addr: 0.0.0.0 # 本地监听地址 10 | # local_port: 8000 # 本地监听端口号 -------------------------------------------------------------------------------- /conf/rule.d/shell.yaml: -------------------------------------------------------------------------------- 1 | - name: shell # 链路名称 2 | target: remote # 目标客户端ID 3 | type: shell # web shell 4 | local_addr: 0.0.0.0 # 本地监听地址 5 | # local_port: 2222 # 本地监听端口号 6 | #exec: /bin/bash # 运行命令 7 | # windows默认powershell或cmd 8 | # 其他系统bash或sh 9 | env: # 环境变量设置 10 | - TERM=xterm -------------------------------------------------------------------------------- /conf/rule.d/vnc.yaml: -------------------------------------------------------------------------------- 1 | # 目前不支持arm架构的windows和linux操作系统 2 | - name: vnc # 链路名称 3 | target: remote # 目标客户端ID 4 | type: vnc # web vnc 5 | local_addr: 0.0.0.0 # 本地监听地址 6 | # local_port: 5900 # 本地监听端口号 7 | fps: 10 # 刷新频率 -------------------------------------------------------------------------------- /conf/server.yaml: -------------------------------------------------------------------------------- 1 | listen: 6154 # 监听端口号 2 | #include common.yaml 3 | #tls: 4 | # key: /dir/to/tls/key/file # tls密钥 5 | # crt: /dir/to/tls/crt/file # tls证书 -------------------------------------------------------------------------------- /contrib/bindata/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | bindata "github.com/go-bindata/go-bindata/v3" 11 | ) 12 | 13 | func main() { 14 | c := bindata.NewConfig() 15 | 16 | flag.Usage = func() { 17 | fmt.Printf("Usage: %s [options] \n\n", os.Args[0]) 18 | flag.PrintDefaults() 19 | } 20 | 21 | flag.BoolVar(&c.Debug, "debug", c.Debug, "Do not embed the assets, but provide the embedding API. Contents will still be loaded from disk.") 22 | flag.StringVar(&c.Package, "pkg", c.Package, "Package name to use in the generated code.") 23 | flag.StringVar(&c.Prefix, "prefix", c.Prefix, "Optional path prefix to strip off asset names.") 24 | flag.StringVar(&c.Output, "o", c.Output, "Optional name of the output file to be generated.") 25 | flag.Parse() 26 | 27 | if flag.NArg() == 0 { 28 | fmt.Fprintf(os.Stderr, "Missing \n\n") 29 | flag.Usage() 30 | os.Exit(1) 31 | } 32 | 33 | c.Input = make([]bindata.InputConfig, flag.NArg()) 34 | for i := range c.Input { 35 | c.Input[i] = parseInput(flag.Arg(i)) 36 | } 37 | 38 | err := bindata.Translate(c) 39 | 40 | if err != nil { 41 | fmt.Fprintf(os.Stderr, "bindata: %v\n", err) 42 | os.Exit(1) 43 | } 44 | } 45 | 46 | func parseInput(path string) bindata.InputConfig { 47 | if strings.HasSuffix(path, "/...") { 48 | return bindata.InputConfig{ 49 | Path: filepath.Clean(path[:len(path)-4]), 50 | Recursive: true, 51 | } 52 | } 53 | return bindata.InputConfig{ 54 | Path: filepath.Clean(path), 55 | Recursive: false, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docker_build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | VERSION=dev 3 | GO_VERSION=1.20.11 4 | docker run -it --rm -e BUILD_VERSION=$VERSION \ 5 | -e GOPROXY=https://goproxy.cn \ 6 | -v `pwd`:/code -w /code lwch/natpass-builder:$GO_VERSION -------------------------------------------------------------------------------- /docs/desc.md: -------------------------------------------------------------------------------- 1 | # 实现原理 2 | 3 | 支持tls链接,protobuf进行数据传输,下面举例远程连接服务器集群内的某台主机 4 | 5 | ![shell](imgs/example.jpg) 6 | 7 | server端配置(10.0.1.1): 8 | 9 | listen: 6154 # 监听端口号 10 | secret: 0123456789 # 预共享密钥 11 | log: 12 | dir: /opt/natpass/logs # 路径 13 | size: 50M # 单个文件大小 14 | rotate: 7 # 保留数量 15 | tls: 16 | key: /dir/to/tls/key/file # tls密钥 17 | crt: /dir/to/tls/crt/file # tls证书 18 | 19 | 服务器client配置(192.168.1.100): 20 | 21 | id: server # 客户端ID 22 | server: 10.0.1.1:6154 # 服务器地址 23 | secret: 0123456789 # 预共享密钥,必须与server端相同,否则握手失败 24 | log: 25 | dir: /opt/natpass/logs # 路径 26 | size: 50M # 单个文件大小 27 | rotate: 7 # 保留数量 28 | 29 | 办公网络client配置(172.16.1.100): 30 | 31 | id: work # 客户端ID 32 | server: 10.0.1.1:6154 # 服务器地址 33 | secret: 0123456789 # 预共享密钥,必须与server端相同,否则握手失败 34 | log: 35 | dir: /opt/natpass/logs # 路径 36 | size: 50M # 单个文件大小 37 | rotate: 7 # 保留数量 38 | rules: # 远端rule列表可为空 39 | - name: rdp # 链路名称 40 | target: server # 目标客户端ID 41 | type: shell # 连接类型tcp或udp 42 | local_addr: 0.0.0.0 # 本地监听地址 43 | local_port: 3389 # 本地监听端口号 44 | 45 | 工作流程如下: 46 | 47 | 1. 办公网络与家庭网络中的np-cli创建tls连接到np-svr 48 | 2. np-cli服务发送握手包,并将配置文件中的secret字段进行md5哈希 49 | 3. np-svr等待握手报文,若等待超时则为非法链接,直接断开 50 | 4. 用户打开办公网络主机172.16.1.100上的终端页面,并连接到服务器集群中的主机server 51 | 5. 172.16.1.100上的np-cli发送connect_request消息,并将连接类型设置为shell 52 | 6. np-svr转发connect_request消息至192.168.1.100上的np-cli 53 | 7. 192.168.1.100上的np-cli接收到connect_request消息,创建/bin/bash进程 54 | 8. 192.168.1.100上的np-cli根据链接创建结果返回connect_response消息 55 | 9. np-svr转发connect_response消息至172.16.1.100上的np-cli 56 | 10. 172.168.1.100上的np-cli接收connect_response消息 57 | 11. 开始转发网页上的输入输出内容 58 | 59 | ## 软件架构 60 | 61 | ![架构图](imgs/architecture.jpg) -------------------------------------------------------------------------------- /docs/imgs/architecture.drawio: -------------------------------------------------------------------------------- 1 | 5V1bc6JIFP41PI5FN7fmERRnandnHyZbNbP7skWUKBUCDpJJsr9++7SAQp94iVxEfZjpNIj4nUufy9eoaOOn18+pv1p+TeZBpFB1/qpoE4VSQqjG/4OZt82MpVqbiUUazvOTthN34X9BPqnms8/hPFhXTsySJMrCVXVylsRxMMsqc36aJi/V0x6SqPqpK38RSBN3Mz+SZ7+H82y5mWXU2s5/CcLFsvhkYtqbI09+cXL+TdZLf5687ExpnqKN0yTJNqOn13EQAXgFLpv3Td85Wt5YGsTZMW94eL1bkufs291P+sM00iT6Ov/xKb/KLz96zr9wfrPZW4FAmjzH8wAuoiqa+7IMs+Bu5c/g6AuXOZ9bZk8R/4vw4UMYReMkSlLxXm0qXnxevtnik4M0C153pvKb/xwkT0GWvvFT8qNajmOuSAWsL1upmHo+t9yViMryWT9XhUV56S1afJADdgJ4lnUYvQWHb3X8ty8V2b8vrqDuRYXVUNFlWEpb2oVFt9tCpZBTczqVxFnuGCjDdGzMX6iO7ZfaYc3rEUSKgGhGWQ5HBU3z53NSHPi0FkA5/ASir163B/loAf+vl/DpnqEwR7GniscUpinupLg4v9nN9TdnS4LjiGVV6aQB/8hcW0GY/nOWbG5CHPajcBHz8YwLJeAScwH2kPtXJz/wFM7n8GZ3lYRxJnA0XMWY1AQdJzGctM7S5DGoTVa1qa4uzatFftSiI6NifLohG5+FqU1bWmOYstZ4puKoimvDgE0Ux4OBrcHY00Hy9ljxLMXlWuCKgcv14sEPo3t/9igUhSmuKt49VpgFM/watgMDm8AF4DKu4uh7rJycZOVEP2klaUicpCpMDVlgCOtUmkcsLzyoWMFwHcaLKHAg4OEYzcOULyFhEgsbSeH2Lx5+u25NlOnyUmbI+Gut4c8Qa+LGMFVsXWg/txQvX67/5W5wlcTrAAzFNcFowGKI4kyvyyxKERwyC6M1sdiSWKKELyh8Kg38+XXBTRAv1CnaphzNpcFTkgVXArd6GG+7U7yPyMiux+nrdadfupK+nL6JBd4n479OngeBP9GsixOAjqy6Igrl6y0f8FDTdYLXYHZ7IalO+g5JTeOWvJNdywisnk3DQnwT6L6u8FCZp258oWKmCFE9yPDAang+51yXERR1sr4CIku7JRMoPMlbFeneLKAR/zPc1bl3/LEC034PxI8aQ/dA6kEX1GmOQDAXdFpxmDKsOCxKg2NRGhQFYoeAUB1XVDGOKxCfJtmqJD0TavnNdIsMKbNQkejJYKWF7UrObEBy3x1zvAj/sb9R6+/Y+/J79vab+Uk/Irtru2MkIcMQYCy1NWDwwFLC5a8/7hTPBi/CfQl4FAu0EBIAojAZtXP0bjx+14Psl+JJHaSytNAJoEfUjs9p7LYIWbn4yVqpm11CKJd/hU5uKsCijcI8vriR84BtD0CjGrthySOmkq0lj3LdFsXzTEVtDc96q713PIsb2AF0NBrxCfFvDcMBNG5bk5yuSr0lxLcgoqOtiU6OA1Bb+PNCbUE3pLphh9aAfwmkW9d1aFX2jAtQLFtCBU8VilWkeSJJ4wyvQ2ycd2P4/WI7jY3TMYjnJ1w4G+dXPLtxLs65SlESTauWZxA5R++UimMhdie1MV6S9JELInf7gn7jcCXgg6nijqG7AWfaQCjYvBeaHRsaDy2pOetgvQ6TWJX0o6MiS2MSHDFL3XnVeh+WOpKZB9Rko6Ia0skqg0mVQfcJklQLjBcIVoIb4ooBt2g2Ff2oMfCwtqUxPtBB2jzH5VKFc7jwJwqzxbumkONCNcYA8tXAJauOTPq+ZDWGS9YeaaxLyR4RP1xAXb8pmWh0xFR7+6pZmya7z7YKzbg05GRxSNyrbiyHR0GXYDlUzkOv2nK0kU7ftRxN79lyqLxEXQI9rin0jf0WoWIW0RZFAsef3pI16NqI7JOHgXqoTu1hGP36buRBzP7lgfG76nsUHlZrJAze8rz4jCWyIrHaO07RFxr8gr/fvfEst3f3NgwGWDfmZBq9WxNOyGCeqCJYIuu0d6oI9RAayVWv3IJ6Nh85y7wIOn1LZTg8HiNtVeLQb3HMftQL4Iw1JQEm9br6j8GwJgRCG7PBMzmikAbOiRWF8Pt0l6B0XRl+zWAuYIXHth6jwsormiJ2Y9YtCMu+uHAM60xJwprBunJz0kFTnW6Fg29lqVuSKfZcOwXn8hbd3gXIahjM8taihAuoDOjYwmOK9EQXAzMvAtSba1Ir7bqMA8lZSLFLrhtqDdKMqUPcOWvZQp500zVrmWAayxTbzotUzBW8jpLFnLO62mExm+aJimm/I5sLYjEf05T9KHuJIOylFjG8GFoz2q47kcxE8d0jomJrE+GIeWxDtg3an8/BOsPCmFN3lZwv45Md+Oky1vtkR+JC//BjBU4Am/YBNrXqYFOKRDIagnaxs7R5tJHAvzkTq1DIln48j4L0Og1JI9rIOmhJne6A/3j/49INSVcv0JCQ9kZjhlQ2QCAhn0hkoqOy8Gu0Ov2o5QvTg9YyEAOLWTrOQIha2wtvy6gY2KJevrH58LjTZux5eliK8PgwypQBxiLlFvHF3E9rRaGu8aX944vtUj3RvaMb2eMgA2493NR6MVqlSZZc0a6K5jWioP0ifh7TiPb2xWG0XxOySngUgQnseObu0H/qCScMHCrK6yVXSAfuw4ZWz1fsvM9IsadSDED09eCgNedQ38qGlC2oiqhGa48e0ZG1blNHsw1opjAvrw43UVnbXwL2DNj+jqJ/+nMqqjBjFojGFKQ1nI9Y8z4cz7YMJmHaITT14qm6HaEpO7SX4H6dzB6DTTnMAK/VJr574ogPPIK/tssO0VbVlPFt7xHJcl4w8+Nf/HPyJ1mzbb9oKCDrRKulXyjMXRY9DKR6uGXr7DBB2ER4Yya8sTUYyGlNr0t0K4i3tKcGRxzjdYgWKGwBFU+3hPiHDxiQBsr9hXRaTLqiLzoR+4kHIgaDHnYv6M8wtCaFd55FagvKuWg7563pCfBqh2sA0oMLUAPoEnpTTg0FNBDgE1g2wf+IfdBDwbjuZBDtxp5q0B7jBf+xiBoXabg6rWvd6TT/c/tDTuLYzs9had7/ -------------------------------------------------------------------------------- /docs/imgs/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/architecture.jpg -------------------------------------------------------------------------------- /docs/imgs/bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/bench.png -------------------------------------------------------------------------------- /docs/imgs/code_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/code_server.png -------------------------------------------------------------------------------- /docs/imgs/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/dashboard.png -------------------------------------------------------------------------------- /docs/imgs/example.drawio: -------------------------------------------------------------------------------- 1 | 7VvbcqM4EP0aPYYChLg8gi/Z2p1JpTZTM7tPLmw0tiYYuYR8yXz9SgZsQPiSBGzPZvLgQNMISd19TncbA9ibb+5ZuJh9phGOgalHGwD7wDSRZYpPKXjJBNAyMsGUkSgTlQRP5CfOhXouXZIIpxVFTmnMyaIqnNAkwRNekYWM0XVV7TuNq09dhFOsCJ4mYaxKv5GIzzKpazp7+R+YTGfFkw3by67Mw0I5X0k6CyO6LongAMAeo5RnR/NND8dy74p9ye4bHri6mxjDCT/nhm9f/Gn/Yf0l+NN07o1R/DSx3Ltim1P+UqwYR2ID8lPK+IxOaRLGg700YHSZRFgOq4uzvc4nShdCaAjhD8z5S27NcMmpEM34PM6vqlPPVyOfXRLkC7nHdI45exEKDMchJ6uqccLcxtOd3u7WR0rEI0w9d0fTy9ebe6NpeNUhUrpkE5zfVd7M2kCwNpAB9epAPGRTzJWBfMbCl5LaQiqkr5/woXnV9Yt55friIJtBcVba3L1o6zivcSJ0DSfCG8L/kbdrTnH6r7yk6badn/c3+fDbk5fSySNmRCwbs1x20CMzhziyeHgZz4Vm1bAQ2Wc5XFs2zpe5CuNlvgTF5rkvi2MUANQXuxqmiwyTv5ONNHXZfGFMpok4nogdl2YIdvgo7RGF6WznHGS+Benif5/Mp2INMRmLzzCOCY5GHMc4JSKQhimeLBnhYiuHf9OlGHr09fFBS1fTY1ZeYcbx5qj9igBzawFZ2GW9ZweENFcv/1mZyqxEFNDT7MM+ULHeq8NRNc0lwjERc8/i0fasQiAj8k7XdLST7GNye/ZSPqtHZTnEnXKMXzC+c9tl4XVEz20bB97nA/CmeP3cXe4aRS1oV+kRdUPbSNcbn9MpDVu/ITozcj1nKoa4HYh2fq3oRJeJTiXZtS+TVENYq5xep99NNJtXQfCCcSVn66hEuYLVnUum1ehM2jVa5923uRSsuoilm8dd6rh+Ny6FVIIYIODawEVg4ICgDzwDDFzgD0AwkJd8C7gCquxYWCAYM3E0lUeGY2qGrRmaoavtg9ugmAmdL7bMEiZSzOZERIG8IDw0lREx6hUaa8JnoyfMBMG0WCY4tuZUQcNWCwXb1KwKC9kqCx3SaT9lNBRb/p9bQZZ1ooNzbisIwXq6YXaTU8JrtHasq7BQqZZ0d4KMhnQDvaeUlMRmWBVic5B3QWKzzyS21vtK7wIHW6GOZHGXrphKDmBgA68H3P6WW3zgG/LA84DvgoEHvEASjmSbgaQXcSDYRuoIFhIHTn574DfQjjCdJJ1fjXIybhn1cfrM6aI9joEerAKCoTKMZWrIK/811DnI6shpvA9FKPWaHyLnjYRSG8g8s0nRWqv3qpWHoekIVvAZwgvCs3smPJu3UXegemnqGsd967h+N0mEq5DHUZKwQOCCINgxwUMDFXiyAnF/lyDH2mDur1eCqGlG7iDeth4VdarfK5WqjnQT4SNHKlQxD1PNYcUW8qoXMGG1n+F4qyBNLQEqzbCq0UmkHcgkjP38wpxE0RYIVef7TuK4R2MqwSmhiVRKOaPPuCasQmgLHmAo31WpjdACDyqNT70r+zqvQwIlXTwGBB/Z0siuW9rSXMXWO6y8iLGLMvhG0r+TzN9RJ1Ft/TnV76Cg21GX+8Bzuu1aX+VVkING79yWtVTdMt7YRFIGOtMpWuvzNORojWkVZoMVzmDPOJorlczTiIFKShaHYxw/CijmhDYC8KeawphyTucNCM2l26hATpc8JomYR/GmoJ4tYCHXN99M5fuL2moVaZOYLqNRlr+RZFoDdiAg07Z9ry2SrkE3dJAC3KgJt83Dzvo+3LY/VAgbzokQPhB5pwdyzsOC1kLYa0iusi9zhttUSrba1ngsj0WWFaCtsA/8riO9FjwWtCwL3TYChJGo1ohYTcjltFuJc8fSCnasg3wp1E1DQ2qw1ztA7QW7oThNsribxKSpsevIdNzLEnRdVGIQut62gxsAf7jN34fAV5tHHyMFd07D+EVrLXhbmdjJ9LujhpraAUM1mNa7Sb/rJVnxnLbSb/RMZv7wr3v+deL8+Dy9Z+jBv7tKyVV58xOC2puf9k7lNV/XNfpCo2OVO7WNe9KNG9lWDcvPzPxbK7R+J+xXStht48YSdniYwz8kE9t1Km5Is6wGA5ktUHEjAl3xXezTCNo+Mppvq4NOD3RmZdYWmaphJYYZk0R+CuBUjJo+Yz6ZFYFUR8t2wbhz7K0DqG64br8BfsOfS4a1BV1jJqYfxyNxIz4QsWd53w5lqy83NNVKlqMG8RsqJXG6/xVm5jz7n7LCwX8= -------------------------------------------------------------------------------- /docs/imgs/example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/example.jpg -------------------------------------------------------------------------------- /docs/imgs/shell.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/shell.gif -------------------------------------------------------------------------------- /docs/imgs/shell_linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/shell_linux.png -------------------------------------------------------------------------------- /docs/imgs/shell_win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/shell_win.png -------------------------------------------------------------------------------- /docs/imgs/vnc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/vnc.gif -------------------------------------------------------------------------------- /docs/imgs/vnc_clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/vnc_clipboard.png -------------------------------------------------------------------------------- /docs/imgs/vnc_deepin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/vnc_deepin.png -------------------------------------------------------------------------------- /docs/imgs/vnc_fedora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/vnc_fedora.png -------------------------------------------------------------------------------- /docs/imgs/vnc_macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/vnc_macos.png -------------------------------------------------------------------------------- /docs/imgs/vnc_ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/vnc_ubuntu.png -------------------------------------------------------------------------------- /docs/imgs/vnc_win10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/vnc_win10.png -------------------------------------------------------------------------------- /docs/imgs/vnc_win11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/vnc_win11.png -------------------------------------------------------------------------------- /docs/imgs/vnc_win2008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/docs/imgs/vnc_win2008.png -------------------------------------------------------------------------------- /docs/rules.md: -------------------------------------------------------------------------------- 1 | # 规则配置 2 | 3 | 所有链接均为正向配置,由连接发起方进行配置 4 | 5 | ## shell规则 6 | 7 | shell规则用于创建一个网页端的命令行操作页面 8 | 9 | - name: shell # 链路名称 10 | target: that # 目标客户端ID 11 | type: shell # web shell 12 | local_addr: 0.0.0.0 # 本地监听地址 13 | #local_port: 8080 # 本地监听端口号 14 | #exec: /bin/bash # 运行命令 15 | # windows默认powershell或cmd 16 | # 其他系统bash或sh 17 | env: # 环境变量设置 18 | - TERM=xterm 19 | 20 | 1. `name`: 该规则名称,必须全局唯一 21 | 2. `target`: 对端客户端ID 22 | 3. `type`: shell 23 | 4. `local_addr`: 本地监听地址,如只允许局域网访问可绑定在局域网IP地址上 24 | 5. `local_port`: 本地监听端口号,可选 25 | 6. `exec`: 连接建立成功后的启动命令 26 | - 指定该参数:直接使用设定的命令运行 27 | - linux系统:优先查找bash命令,若没有则查找sh命令,否则报错 28 | - windows系统:优先查找powershell命令,若没有则查找cmd命令,否则报错 29 | 7. `env`: 进程启动时的环境变量设置 30 | 31 | ## vnc规则 32 | 33 | vnc规则用于创建一个网页端的远程桌面操作页面 34 | 35 | - name: vnc # 链路名称 36 | target: that # 目标客户端ID 37 | type: vnc # web vnc 38 | local_addr: 0.0.0.0 # 本地监听地址 39 | #local_port: 5900 # 本地监听端口号 40 | fps: 10 # 刷新频率 41 | 42 | 1. `name`: 该规则名称,必须全局唯一 43 | 2. `target`: 对端客户端ID 44 | 3. `type`: shell 45 | 4. `local_addr`: 本地监听地址,如只允许局域网访问可绑定在局域网IP地址上 46 | 5. `local_port`: 本地监听端口号,可选 47 | 6. `fps`: 每秒钟截屏多少次,最高50 48 | 49 | 注意: 50 | 51 | 1. 创建vnc连接后远端服务会创建一个子进程进行截屏和键鼠操作, 52 | 主进程会在`6155~6955`之间选一个端口进行监听用于与子进程通信 53 | 2. 使用rdp连接的windows主机,需要将np-cli.exe[注册为系统服务](startup.md#注册系统服务(可选)), 54 | 否则在rdp窗口最小化或者rdp连接关闭后将无法刷新 55 | 3. windows2008系统下需要启用sas策略才可使用ctrl+alt+del按钮进行解锁登录页面,配置方法如下: 56 | 57 | 1. 运行gpedit.msc打开组策略编辑器 58 | 2. 找到计算机配置 => 管理模板 => Windows组件 => Windows登录选项 => 禁用或启用软件安全注意序列 59 | 3. 在详情中设置为已启用,设置允许哪个软件生成软件安全注意序列为*服务* 60 | 61 | ## code-server规则 62 | 63 | vnc规则用于创建一个网页端的code-server页面,主要用于远程开发 64 | 65 | - name: code-server # 链路名称 66 | target: remote # 目标客户端ID 67 | type: code-server # code-server 68 | local_addr: 0.0.0.0 # 本地监听地址 69 | #local_port: 8000 # 本地监听端口号 70 | 71 | 1. `name`: 该规则名称,必须全局唯一 72 | 2. `target`: 对端客户端ID 73 | 3. `type`: code-server 74 | 4. `local_addr`: 本地监听地址,如只允许局域网访问可绑定在局域网IP地址上 75 | 5. `local_port`: 本地监听端口号,可选 -------------------------------------------------------------------------------- /docs/startup.md: -------------------------------------------------------------------------------- 1 | # 开始使用 2 | 3 | 部署过程共分为三部分:服务器端、受控端和控制端,下面以*debian*系统进行举例。 4 | 5 | ## 服务器端部署 6 | 7 | 1. 在服务器上[下载](https://github.com/lwch/natpass/releases)并解压到任意目录 8 | 2. 使用以下命令启动服务器端程序 9 | 10 | sudo ./np-svr --conf server.yaml 11 | 12 | 3. (可选)开放外网防火墙,默认端口6154 13 | 14 | ## 受控端部署 15 | 16 | 1. 在受控端机器上[下载](https://github.com/lwch/natpass/releases)并解压到任意目录 17 | 2. (可选)修改remote.yaml配置文件,修改*server*地址 18 | 3. 使用以下命令启动客户端程序 19 | 20 | sudo ./np-cli --conf remote.yaml --user `whoami` 21 | 22 | ## 控制端部署 23 | 24 | 1. 在本地控制机上[下载](https://github.com/lwch/natpass/releases)并解压到任意目录 25 | 2. (可选)修改local.yaml配置文件,修改*server*地址 26 | 3. (可选)修改rule.d目录下的规则配置文件,[rule配置方法](rules.md) 27 | 4. 使用以下命令启动客户端程序 28 | 29 | sudo ./np-cli --conf local.yaml 30 | 5. 在以上操作成功后即可在浏览器中通过local.yaml中配置的端口号进行访问,默认地址: 31 | 32 | http://127.0.0.1:8080 33 | 34 | ## 安全连接(可选) 35 | 36 | 1. 建议使用tls加密连接,使用方式如下 37 | 38 | - 修改服务器端的server.yaml文件,配置tls相关文件路径,并重启服务 39 | - 修改受控端的remote.yaml配置,配置ssl相关选项,并重启服务 40 | - 修改控制端的local.yaml配置,配置ssl相关选项,并重启服务 41 | 42 | 2. 修改默认连接密钥,修改方式如下 43 | 44 | - 使用以下命令生成一个16位随机串 45 | tr -dc A-Za-z0-9 < /dev/urandom | dd bs=16 count=1 2>/dev/null && echo 46 | - 修改服务器端的common.yaml文件,将secret设置为新的密钥,并重启服务 47 | - 修改受控端的common.yaml文件,将secret设置为新的密钥,并重启服务 48 | - 修改控制端的common.yaml文件,将secret设置为新的密钥,并重启服务 49 | 50 | ## 注册系统服务(可选) 51 | 52 | 1. 在命令行中使用`-action install`参数即可将程序注册为系统服务,使用参数`-user`可设置该服务的启动身份 53 | 2. linux系统使用systemd管理系统服务,windows系统可用services.msc面板启动或停止服务 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lwch/natpass 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be 7 | github.com/creack/pty v1.1.21 8 | github.com/dustin/go-humanize v1.0.1 9 | github.com/go-bindata/go-bindata/v3 v3.1.3 10 | github.com/gorilla/websocket v1.5.1 11 | github.com/kardianos/service v1.2.2 12 | github.com/lwch/logging v1.0.1 13 | github.com/lwch/rdesktop v1.2.2 14 | github.com/lwch/runtime v1.0.1 15 | github.com/lwch/yaml v0.0.0-20220711084242-14c4f5845abe 16 | github.com/spf13/cobra v1.8.0 17 | golang.org/x/sys v0.18.0 18 | golang.org/x/text v0.14.0 19 | google.golang.org/protobuf v1.33.0 20 | ) 21 | 22 | require ( 23 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 24 | github.com/kisielk/errcheck v1.6.3 // indirect 25 | github.com/spf13/pflag v1.0.5 // indirect 26 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect 27 | golang.org/x/mod v0.14.0 // indirect 28 | golang.org/x/net v0.23.0 // indirect 29 | golang.org/x/tools v0.16.0 // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /html/AdminLTE-3.2.0/AdminLTE: -------------------------------------------------------------------------------- 1 | AdminLTE -------------------------------------------------------------------------------- /html/code/common.js: -------------------------------------------------------------------------------- 1 | ../js/common.js -------------------------------------------------------------------------------- /html/code/fontawesome: -------------------------------------------------------------------------------- 1 | ../fontawesome-free-6.2.1 -------------------------------------------------------------------------------- /html/code/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | body { 5 | margin: 0 !important; 6 | padding: 0 !important; 7 | height: 100% !important; 8 | } 9 | .container-fluid { 10 | height: 100%; 11 | overflow-y: hidden; 12 | } 13 | #header { 14 | padding-top: 4px; 15 | } 16 | #fullscreen { 17 | float: right; 18 | margin-right: 5px; 19 | } 20 | #info { 21 | float: right; 22 | margin-right: 5px; 23 | } 24 | #closed { 25 | color: red; 26 | float: right; 27 | margin-right: 5px; 28 | font-weight: bold; 29 | } 30 | label, span { 31 | line-height: 38px; 32 | } 33 | #fullscreen { 34 | cursor: pointer; 35 | font-size: 38px; 36 | } 37 | #code { 38 | width: 100%; 39 | height: calc(100% - 42px); 40 | border: none; 41 | } -------------------------------------------------------------------------------- /html/code/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | code - [{{.Name}}] 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /html/code/index.js: -------------------------------------------------------------------------------- 1 | var page = { 2 | init: function() { 3 | $('#fullscreen').click(page.fullscreen); 4 | $('#code').on('load', function() { 5 | var qry = $('#code')[0].contentWindow.location.search; 6 | var params = new URLSearchParams(qry); 7 | page.id = params.get('natpass_connection_id'); 8 | }); 9 | page.connect(); 10 | setInterval(page.update_info, page.secs*1000); 11 | }, 12 | connect: function() { 13 | $.get('/new', function(ret) { 14 | page.name = ret; 15 | $('#code').attr('src', `/forward/${page.name}/`); 16 | }); 17 | }, 18 | update_info: function() { 19 | if (!page.id) { 20 | return; 21 | } 22 | $.get('/info?id='+page.id, function(ret) { 23 | var send_bytes = ret.send_bytes - page.send; 24 | var recv_bytes = ret.recv_bytes - page.recv; 25 | if (send_bytes < 0) { 26 | send_bytes = 0; 27 | } 28 | if (recv_bytes < 0) { 29 | recv_bytes = 0; 30 | } 31 | page.send = ret.send_bytes; 32 | page.recv = ret.recv_bytes; 33 | var str = 'send: '+humanize.bytes(send_bytes/page.secs)+'/s, '+ 34 | 'recv: '+humanize.bytes(recv_bytes/page.secs)+'/s'; 35 | $('#info').text(str); 36 | }); 37 | }, 38 | fullscreen: function() { 39 | $('#code')[0].requestFullscreen(); 40 | }, 41 | id: '', 42 | name: '', 43 | secs: 2, 44 | send: 0, 45 | recv: 0 46 | }; 47 | $(document).ready(page.init); -------------------------------------------------------------------------------- /html/code/jquery: -------------------------------------------------------------------------------- 1 | ../jquery -------------------------------------------------------------------------------- /html/dashboard/AdminLTE: -------------------------------------------------------------------------------- 1 | ../AdminLTE-3.2.0 -------------------------------------------------------------------------------- /html/dashboard/bootstrap: -------------------------------------------------------------------------------- 1 | ../bootstrap-4.6.2 -------------------------------------------------------------------------------- /html/dashboard/files.html: -------------------------------------------------------------------------------- 1 | {{define "all"}} 2 | {{template "header" .}} 3 | 5 | {{template "aside" .}} 6 | 7 |
8 |
9 |
10 |
11 |
12 |

文件

13 |
14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 | 暂未实现 23 |
24 | 25 |
26 | 27 |
28 | {{template "footer"}} 29 | {{end}} -------------------------------------------------------------------------------- /html/dashboard/fontawesome: -------------------------------------------------------------------------------- 1 | ../fontawesome-free-6.2.1 -------------------------------------------------------------------------------- /html/dashboard/index.html: -------------------------------------------------------------------------------- 1 | {{define "all"}} 2 | {{template "header" .}} 3 | 4 | {{template "aside" .}} 5 | 6 | 15 |
16 |
17 |
18 |
19 |
20 |

总览

21 |
22 |
23 | 24 |
25 | 26 |
27 | 28 |
29 |
30 |
31 | 32 |
33 |
34 |
35 |

终端列表

36 |
37 |
38 |
39 | 40 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
名称对端类型虚拟连接数接收/发送(字节数)接收/发送(数据包)操作
58 |
59 | 60 |
61 | 62 |
63 | 64 |
65 | 66 |
67 | 68 |
69 | {{template "footer"}} 70 | {{end}} -------------------------------------------------------------------------------- /html/dashboard/jquery: -------------------------------------------------------------------------------- 1 | ../jquery -------------------------------------------------------------------------------- /html/dashboard/js/aside.js: -------------------------------------------------------------------------------- 1 | var aside = { 2 | active_dashboard: function() { 3 | $('.sidebar #dashboard').addClass('active'); 4 | }, 5 | active_terminal: function() { 6 | $('.sidebar #terminal').addClass('active'); 7 | }, 8 | active_files: function() { 9 | $('.sidebar #files').addClass('active'); 10 | } 11 | }; -------------------------------------------------------------------------------- /html/dashboard/js/common.js: -------------------------------------------------------------------------------- 1 | ../../js/common.js -------------------------------------------------------------------------------- /html/dashboard/js/index.js: -------------------------------------------------------------------------------- 1 | var page = { 2 | init: function() { 3 | $('#rule-type').change(page.render); 4 | page.render(); 5 | setInterval(page.render, 5000); 6 | }, 7 | render: function() { 8 | page.render_cards(); 9 | page.render_rules(); 10 | }, 11 | render_cards: function() { 12 | $.get('/api/info', function(ret) { 13 | $('#cards').empty(); 14 | page.add_card('规则总数', ret.rules); 15 | page.add_card('虚拟连接数', ret.virtual_links); 16 | page.add_card('终端会话', ret.sessions); 17 | }); 18 | }, 19 | render_rules: function() { 20 | $.get('/api/rules', function(ret) { 21 | $('#rules tbody').empty(); 22 | var type = $('#rule-type').val(); 23 | $.each(ret, function(_, rule) { 24 | if (type != 'all' && rule.type != type) { 25 | return; 26 | } 27 | var send_bytes = 0; 28 | var send_packet = 0; 29 | var recv_bytes = 0; 30 | var recv_packet = 0; 31 | $.each(rule.links, function(_, link) { 32 | send_bytes += link.send_bytes; 33 | send_packet += link.send_packet; 34 | recv_bytes += link.recv_bytes; 35 | recv_packet += link.recv_packet; 36 | }); 37 | var op = ''; 38 | switch (rule.type) { 39 | case 'shell': 40 | case 'vnc': 41 | case 'code-server': 42 | op = `连接`; 43 | break; 44 | } 45 | var str = ` 46 | 47 | ${rule.name} 48 | ${rule.remote} 49 | ${rule.type} 50 | ${rule.links?rule.links.length:0} 51 | ${humanize.bytes(recv_bytes)}/${humanize.bytes(send_bytes)} 52 | ${recv_packet}/${send_packet} 53 | ${op} 54 | `; 55 | $('#rules tbody').append($(str)); 56 | }); 57 | }); 58 | }, 59 | add_card: function(title, count) { 60 | var str = ` 61 |
62 |
63 |
64 |
65 |

${title}

66 |
67 |
68 |
69 |
70 |

71 | ${count} 72 |

73 |
74 |
75 |
76 |
`; 77 | $('#cards').append($(str)); 78 | } 79 | }; 80 | $(document).ready(page.init); -------------------------------------------------------------------------------- /html/dashboard/js/terminal.js: -------------------------------------------------------------------------------- 1 | var page = { 2 | init: function() { 3 | page.load(function() { 4 | var name = arg('name'); 5 | if (name != 'null') { 6 | $('#terms').find(`option:contains(${name})`).prop('selected', true); 7 | page.connect(); 8 | } 9 | }); 10 | $('#connect').click(page.connect); 11 | }, 12 | load: function(cb) { 13 | if (!cb) cb = function(){}; 14 | $.get('/api/rules', function(ret) { 15 | $('#terms').empty(); 16 | $.each(ret, function(_, rule) { 17 | if (rule.type != 'shell' && 18 | rule.type != 'vnc' && 19 | rule.type != 'code-server') { 20 | return; 21 | } 22 | $('#terms').append($(``)); 23 | }); 24 | cb(); 25 | }); 26 | }, 27 | connect: function() { 28 | $('#tabs>.nav-item>.active').removeClass('active'); 29 | $('#tab-content>.active').removeClass('show').removeClass('active'); 30 | var idx = page.idx; 31 | var str = ` 32 | `; 35 | var obj = $(str); 36 | obj.find('button').text('shell - ['+$('#terms option:selected').text()+']'); 37 | obj.click(function() { 38 | var $this = $(this); 39 | $('#tabs>.nav-item>.active').removeClass('active'); 40 | $this.find('button').addClass('active'); 41 | $('#tab-content>.active').removeClass('show').removeClass('active'); 42 | $('#tab-'+idx).addClass('show').addClass('active'); 43 | }); 44 | $('#tabs').append(obj); 45 | var str = ` 46 |
47 | 48 |
`; 49 | var obj = $(str); 50 | obj.attr('id', 'tab-'+idx); 51 | obj.find('iframe').attr('src', 'http://'+location.hostname+':'+escape($('#terms').val())); 52 | $('#tab-content').append(obj); 53 | page.idx++; 54 | }, 55 | idx: 0 56 | }; 57 | $(document).ready(page.init); -------------------------------------------------------------------------------- /html/dashboard/templates/aside.html: -------------------------------------------------------------------------------- 1 | {{define "aside"}} 2 | 3 | 37 | {{end}} -------------------------------------------------------------------------------- /html/dashboard/templates/footer.html: -------------------------------------------------------------------------------- 1 | {{define "footer"}} 2 | 3 | 4 | 5 | {{end}} -------------------------------------------------------------------------------- /html/dashboard/templates/header.html: -------------------------------------------------------------------------------- 1 | {{define "header"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | NatPass v{{.Version}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | {{end}} -------------------------------------------------------------------------------- /html/dashboard/terminal.html: -------------------------------------------------------------------------------- 1 | {{define "all"}} 2 | {{template "header" .}} 3 | 4 | {{template "aside" .}} 5 | 6 |
7 |
8 |
9 |
10 |
11 |

终端

12 |
13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 | 37 |
38 | 39 |
40 | {{template "footer"}} 41 | {{end}} -------------------------------------------------------------------------------- /html/fontawesome-free-6.2.1/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/html/fontawesome-free-6.2.1/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /html/fontawesome-free-6.2.1/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/html/fontawesome-free-6.2.1/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /html/fontawesome-free-6.2.1/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/html/fontawesome-free-6.2.1/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /html/fontawesome-free-6.2.1/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/html/fontawesome-free-6.2.1/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /html/fontawesome-free-6.2.1/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/html/fontawesome-free-6.2.1/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /html/fontawesome-free-6.2.1/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/html/fontawesome-free-6.2.1/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /html/fontawesome-free-6.2.1/webfonts/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/html/fontawesome-free-6.2.1/webfonts/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /html/fontawesome-free-6.2.1/webfonts/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwch/natpass/0e5b71ae8469d7a88fd08eb9fdd6deec4ce42c5e/html/fontawesome-free-6.2.1/webfonts/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /html/jquery-mousewheel/jquery.mousewheel.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Mousewheel 3.1.13 3 | * Copyright OpenJS Foundation and other contributors 4 | */ 5 | !function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e:e(jQuery)}(function(a){var u,r,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in window.document||9<=window.document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],f=Array.prototype.slice;if(a.event.fixHooks)for(var n=e.length;n;)a.event.fixHooks[e[--n]]=a.event.mouseHooks;var d=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],i,!1);else this.onmousewheel=i;a.data(this,"mousewheel-line-height",d.getLineHeight(this)),a.data(this,"mousewheel-page-height",d.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],i,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=a(e),e=t["offsetParent"in a.fn?"offsetParent":"parent"]();return e.length||(e=a("body")),parseInt(e.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return a(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function i(e){var t,n=e||window.event,i=f.call(arguments,1),o=0,l=0,s=0,h=0;if((e=a.event.fix(n)).type="mousewheel","detail"in n&&(s=-1*n.detail),"wheelDelta"in n&&(s=n.wheelDelta),"wheelDeltaY"in n&&(s=n.wheelDeltaY),"wheelDeltaX"in n&&(l=-1*n.wheelDeltaX),"axis"in n&&n.axis===n.HORIZONTAL_AXIS&&(l=-1*s,s=0),o=0===s?l:s,"deltaY"in n&&(o=s=-1*n.deltaY),"deltaX"in n&&(l=n.deltaX,0===s&&(o=-1*l)),0!==s||0!==l)return 1===n.deltaMode?(o*=t=a.data(this,"mousewheel-line-height"),s*=t,l*=t):2===n.deltaMode&&(o*=t=a.data(this,"mousewheel-page-height"),s*=t,l*=t),h=Math.max(Math.abs(s),Math.abs(l)),(!r||h/g, ">") 9 | .replace(/"/g, """) 10 | .replace(/'/g, "'"); 11 | } 12 | var humanize = { 13 | bytes: function(n) { 14 | return humanize.humanate_bytes(n, 1024, ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']) 15 | }, 16 | humanate_bytes: function(n, base, sizes) { 17 | if (n < 10) { 18 | return n+'B'; 19 | } 20 | var e = Math.floor(humanize.logn(n, base)); 21 | var suffix = sizes[e]; 22 | var val = Math.floor(n/Math.pow(base, e)*10+0.5) / 10 23 | return val.toFixed(1)+suffix; 24 | }, 25 | logn: function(n, base) { 26 | return Math.log(n) / Math.log(base) 27 | } 28 | }; -------------------------------------------------------------------------------- /html/shell/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | .xterm .xterm-viewport { 6 | width: 100% !important; 7 | } -------------------------------------------------------------------------------- /html/shell/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | shell - [{{.Name}}] 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /html/shell/index.js: -------------------------------------------------------------------------------- 1 | var page = { 2 | init: function() { 3 | page.terminal = new Terminal({ 4 | renderType: 'canvas' 5 | }); 6 | page.terminal.open(document.getElementById('terminal')); 7 | page.terminal.writeln('正在连接...'); 8 | $.get('/new', function(ret) { 9 | page.id = ret; 10 | page.websocket = new WebSocket('ws://'+location.host+'/ws/'+ret); 11 | page.websocket.onclose = page.onclose; 12 | page.terminal.reset(); 13 | page.terminal.loadAddon(new AttachAddon.AttachAddon(page.websocket)); 14 | document.getElementById('terminal').style.height = (window.innerHeight-1) + 'px'; 15 | var fit = new FitAddon.FitAddon(); 16 | page.terminal.loadAddon(fit); 17 | fit.fit(); 18 | page.resize(); 19 | }).fail(function(xhr) { 20 | page.terminal.writeln('连接失败:'+xhr.responseText); 21 | }); 22 | }, 23 | resize: function() { 24 | $.post('/resize', { 25 | id: page.id, 26 | rows: page.terminal.rows, 27 | cols: page.terminal.cols 28 | }); 29 | }, 30 | onclose: function() { 31 | page.terminal.writeln(''); 32 | page.terminal.writeln("\033[0;31m连接已断开!"); 33 | }, 34 | id: undefined, 35 | terminal: undefined, 36 | websocket: undefined 37 | }; 38 | $(document).ready(page.init); -------------------------------------------------------------------------------- /html/shell/jquery: -------------------------------------------------------------------------------- 1 | ../jquery -------------------------------------------------------------------------------- /html/shell/xterm.js: -------------------------------------------------------------------------------- 1 | ../xterm.js-5.1.0 -------------------------------------------------------------------------------- /html/vnc/AdminLTE: -------------------------------------------------------------------------------- 1 | ../AdminLTE-3.2.0 -------------------------------------------------------------------------------- /html/vnc/bootstrap: -------------------------------------------------------------------------------- 1 | ../bootstrap-4.6.2 -------------------------------------------------------------------------------- /html/vnc/clipboard_dialog.js: -------------------------------------------------------------------------------- 1 | var clipboard_dialog = { 2 | init: function() { 3 | $('#dlg-clipboard #set-clipboard').click(clipboard_dialog.set); 4 | $('#dlg-clipboard #get-clipboard').click(clipboard_dialog.get); 5 | }, 6 | modal: function() { 7 | $('#dlg-clipboard textarea').val(''); 8 | $('#dlg-clipboard').modal(); 9 | }, 10 | set: function() { 11 | $.post('/clipboard', {data: $('#dlg-clipboard textarea').val()}); 12 | }, 13 | get: function() { 14 | $.get('/clipboard', function(ret) { 15 | $('#dlg-clipboard textarea').val(ret); 16 | }); 17 | } 18 | }; 19 | $(document).ready(clipboard_dialog.init); -------------------------------------------------------------------------------- /html/vnc/common.js: -------------------------------------------------------------------------------- 1 | ../js/common.js -------------------------------------------------------------------------------- /html/vnc/fontawesome: -------------------------------------------------------------------------------- 1 | ../fontawesome-free-6.2.1 -------------------------------------------------------------------------------- /html/vnc/index.css: -------------------------------------------------------------------------------- 1 | .container-fluid { 2 | margin-top: 10px; 3 | } 4 | #quality { 5 | width: 120px; 6 | display: inline-block; 7 | } 8 | #vnc { 9 | margin-top: 10px; 10 | object-fit: none; 11 | } 12 | #fullscreen { 13 | float: right; 14 | } 15 | #cad, 16 | #clipboard, 17 | #info { 18 | float: right; 19 | margin-right: 5px; 20 | } 21 | #closed { 22 | color: red; 23 | float: right; 24 | margin-right: 5px; 25 | font-weight: bold; 26 | } 27 | label, span { 28 | line-height: 38px; 29 | } 30 | #fullscreen { 31 | cursor: pointer; 32 | font-size: 38px; 33 | } 34 | 35 | #dlg-clipboard textarea { 36 | resize: vertical; 37 | min-height: 300px; 38 | } 39 | #dlg-clipboard #row-data { 40 | margin-top: 10px; 41 | } -------------------------------------------------------------------------------- /html/vnc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vnc - [{{.Name}}] 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 |    32 |
33 | 34 | 35 |
   36 | 37 | 38 | 39 | fps: 0, bandwidth: 0 B/s 40 | 41 |
42 | 43 | 44 |
45 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /html/vnc/jquery: -------------------------------------------------------------------------------- 1 | ../jquery -------------------------------------------------------------------------------- /html/vnc/jquery-mousewheel: -------------------------------------------------------------------------------- 1 | ../jquery-mousewheel -------------------------------------------------------------------------------- /html/xterm.js-5.1.0/addon/xterm-addon-attach/xterm-addon-attach.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.AttachAddon=t():e.AttachAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;function s(e,t,s){return e.addEventListener(t,s),{dispose:()=>{s&&e.removeEventListener(t,s)}}}Object.defineProperty(t,"__esModule",{value:!0}),t.AttachAddon=void 0,t.AttachAddon=class{constructor(e,t){this._disposables=[],this._socket=e,this._socket.binaryType="arraybuffer",this._bidirectional=!(t&&!1===t.bidirectional)}activate(e){this._disposables.push(s(this._socket,"message",(t=>{const s=t.data;e.write("string"==typeof s?s:new Uint8Array(s))}))),this._bidirectional&&(this._disposables.push(e.onData((e=>this._sendData(e)))),this._disposables.push(e.onBinary((e=>this._sendBinary(e))))),this._disposables.push(s(this._socket,"close",(()=>this.dispose()))),this._disposables.push(s(this._socket,"error",(()=>this.dispose())))}dispose(){for(const e of this._disposables)e.dispose()}_sendData(e){this._checkOpenSocket()&&this._socket.send(e)}_sendBinary(e){if(!this._checkOpenSocket())return;const t=new Uint8Array(e.length);for(let s=0;s(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{constructor(){}activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})())); 2 | //# sourceMappingURL=xterm-addon-fit.js.map -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | /env -------------------------------------------------------------------------------- /test/code-server-forward/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/gorilla/websocket" 16 | "github.com/lwch/runtime" 17 | ) 18 | 19 | var cli = &http.Client{ 20 | Transport: &http.Transport{ 21 | Dial: func(network, addr string) (net.Conn, error) { 22 | return net.Dial("unix", "./code-server/code-server.sock") 23 | }, 24 | }, 25 | } 26 | var upgrader = websocket.Upgrader{} 27 | var dialer = websocket.Dialer{ 28 | NetDial: func(network, addr string) (net.Conn, error) { 29 | return net.Dial("unix", "./code-server/code-server.sock") 30 | }, 31 | } 32 | 33 | func main() { 34 | dir := "/home/lwch/src/natpass/code-server" 35 | exec := exec.Command("code-server", "--disable-update-check", 36 | "--auth", "none", 37 | "--socket", filepath.Join(dir, "code-server.sock"), 38 | "--user-data-dir", filepath.Join(dir, "data"), 39 | "--extensions-dir", filepath.Join(dir, "extensions"), ".") 40 | exec.Stdout = os.Stdout 41 | exec.Stderr = os.Stderr 42 | runtime.Assert(exec.Start()) 43 | time.Sleep(time.Second) 44 | 45 | go exec.Wait() 46 | 47 | conn, err := net.Dial("unix", "./code-server/code-server.sock") 48 | runtime.Assert(err) 49 | conn.Close() 50 | 51 | http.HandleFunc("/", next) 52 | http.ListenAndServe(":8001", nil) 53 | } 54 | 55 | func normal(w http.ResponseWriter, r *http.Request) { 56 | u := r.URL 57 | u.Scheme = "http" 58 | u.Host = "unix" 59 | req, err := http.NewRequest(r.Method, u.String(), r.Body) 60 | runtime.Assert(err) 61 | 62 | for key, values := range r.Header { 63 | for _, v := range values { 64 | req.Header.Add(key, v) 65 | } 66 | } 67 | 68 | rep, err := cli.Do(req) 69 | runtime.Assert(err) 70 | defer rep.Body.Close() 71 | 72 | for key, values := range rep.Header { 73 | for _, v := range values { 74 | w.Header().Add(key, v) 75 | } 76 | } 77 | 78 | w.WriteHeader(rep.StatusCode) 79 | 80 | _, err = io.Copy(w, rep.Body) 81 | runtime.Assert(err) 82 | } 83 | 84 | func ws(w http.ResponseWriter, r *http.Request) { 85 | u := r.URL 86 | u.Scheme = "ws" 87 | u.Host = "unix" 88 | 89 | hdr := make(http.Header) 90 | for key, values := range r.Header { 91 | if strings.HasPrefix(key, "Sec-") { 92 | continue 93 | } 94 | for _, value := range values { 95 | hdr.Add(key, value) 96 | } 97 | } 98 | 99 | hdr.Del("Connection") 100 | hdr.Del("Upgrade") 101 | 102 | remote, resp, err := dialer.Dial(u.String(), hdr) 103 | runtime.Assert(err) 104 | defer resp.Body.Close() 105 | defer remote.Close() 106 | 107 | local, err := upgrader.Upgrade(w, r, nil) 108 | runtime.Assert(err) 109 | defer local.Close() 110 | 111 | cp := func(wg *sync.WaitGroup, dst, src *websocket.Conn) { 112 | defer wg.Done() 113 | defer dst.Close() 114 | defer src.Close() 115 | for { 116 | t, data, err := src.ReadMessage() 117 | if err != nil { 118 | fmt.Println(err) 119 | return 120 | } 121 | err = dst.WriteMessage(t, data) 122 | if err != nil { 123 | fmt.Println(err) 124 | return 125 | } 126 | } 127 | } 128 | 129 | var wg sync.WaitGroup 130 | 131 | wg.Add(2) 132 | go cp(&wg, local, remote) 133 | go cp(&wg, remote, local) 134 | 135 | wg.Wait() 136 | } 137 | 138 | func next(w http.ResponseWriter, r *http.Request) { 139 | upgrade := r.Header.Get("Connection") 140 | 141 | if upgrade == "Upgrade" { 142 | ws(w, r) 143 | return 144 | } 145 | normal(w, r) 146 | } 147 | --------------------------------------------------------------------------------