├── .dockerignore ├── .github └── workflows │ ├── deploy-client-binaries.yml │ ├── deploy-client-docker.yml │ ├── deploy-relay-server.yml │ ├── deploy-scripts.yml │ ├── deploy-website.yml │ └── integration-tests.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Cross.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── TODO.md ├── aws ├── README.md ├── docker-compose.yml ├── nginx.conf ├── setup-ec2.sh ├── stack--global-accelerator.yml └── stack.yml ├── scripts ├── init.cmd ├── init.js ├── init.php ├── init.ps1 ├── init.py ├── init.sh ├── java │ ├── Manifest.txt │ ├── build.sh │ └── init.java └── netcore │ ├── build.sh │ ├── init.cs │ └── init.csproj ├── tunshell-client ├── .cargo │ └── config ├── Cargo.toml ├── build │ ├── compile-wasm.sh │ ├── compile.sh │ ├── install-deps-wasm.sh │ └── install-deps.sh ├── docker │ └── prod.Dockerfile └── src │ ├── bin │ └── client.rs │ ├── client.rs │ ├── config.rs │ ├── lib.rs │ ├── p2p │ ├── mod.rs │ ├── tcp.rs │ ├── udp │ │ ├── config.rs │ │ ├── congestion.rs │ │ ├── connection.rs │ │ ├── mod.rs │ │ ├── negotiator.rs │ │ ├── orchestrator.rs │ │ ├── packet.rs │ │ ├── receiver.rs │ │ ├── resender.rs │ │ ├── rtt_estimator.rs │ │ ├── sender.rs │ │ ├── seq_number.rs │ │ └── state.rs │ └── udp_adaptor.rs │ ├── server │ ├── dual_stream.rs │ ├── mod.rs │ ├── openssl_tls_stream.rs │ ├── ring_tls_stream.rs │ ├── tcp_stream.rs │ ├── websocket_stream.rs │ └── websys_websocket_stream.rs │ ├── shell │ ├── client │ │ ├── mod.rs │ │ ├── remote_pty.rs │ │ ├── shell.rs │ │ ├── test.rs │ │ └── xtermjs.rs │ ├── mod.rs │ ├── proto.rs │ └── server │ │ ├── default.rs │ │ ├── fallback │ │ ├── byte_channel.rs │ │ ├── input_stream.rs │ │ ├── interpreter.rs │ │ ├── mod.rs │ │ ├── output_stream.rs │ │ └── shell.rs │ │ ├── mod.rs │ │ ├── pty.rs │ │ ├── remote_pty.rs │ │ └── shell.rs │ ├── stream │ ├── aes_stream.rs │ ├── crypto │ │ ├── mod.rs │ │ ├── openssl.rs │ │ ├── ring.rs │ │ └── web_sys.rs │ ├── mod.rs │ └── relay_stream.rs │ ├── util │ ├── delay.rs │ └── mod.rs │ └── wasm.rs ├── tunshell-server ├── Cargo.lock ├── Cargo.toml ├── certs │ ├── development.cert │ └── development.key ├── docker-compose.yml ├── docker │ ├── dev.Dockerfile │ └── prod.Dockerfile ├── src │ ├── api │ │ ├── cors.rs │ │ ├── mod.rs │ │ ├── register.rs │ │ └── routes │ │ │ ├── create_session.rs │ │ │ ├── info.rs │ │ │ └── mod.rs │ ├── bin │ │ └── server.rs │ ├── db │ │ ├── config.rs │ │ ├── connect.rs │ │ ├── mod.rs │ │ ├── schema.rs │ │ └── session.rs │ ├── lib.rs │ └── relay │ │ ├── config.rs │ │ ├── mod.rs │ │ ├── server │ │ ├── connection.rs │ │ ├── message_stream.rs │ │ ├── mod.rs │ │ ├── relay.rs │ │ ├── session_validation.rs │ │ ├── tests │ │ │ ├── mod.rs │ │ │ └── utils.rs │ │ ├── tls.rs │ │ └── ws.rs │ │ └── start.rs ├── static │ └── .gitkeep └── test.sh ├── tunshell-shared ├── Cargo.toml ├── fuzz.sh ├── fuzz │ └── corpus │ │ ├── crash_1 │ │ ├── crash_2 │ │ ├── crash_3 │ │ ├── crash_4 │ │ ├── example_1 │ │ ├── example_10 │ │ ├── example_11 │ │ ├── example_12 │ │ ├── example_13 │ │ ├── example_14 │ │ ├── example_15 │ │ ├── example_16 │ │ ├── example_17 │ │ ├── example_18 │ │ ├── example_19 │ │ ├── example_2 │ │ ├── example_20 │ │ ├── example_21 │ │ ├── example_22 │ │ ├── example_23 │ │ ├── example_24 │ │ ├── example_25 │ │ ├── example_26 │ │ ├── example_27 │ │ ├── example_28 │ │ ├── example_29 │ │ ├── example_3 │ │ ├── example_30 │ │ ├── example_31 │ │ ├── example_32 │ │ ├── example_33 │ │ ├── example_34 │ │ ├── example_35 │ │ ├── example_36 │ │ ├── example_37 │ │ ├── example_38 │ │ ├── example_39 │ │ ├── example_4 │ │ ├── example_40 │ │ ├── example_41 │ │ ├── example_42 │ │ ├── example_43 │ │ ├── example_44 │ │ ├── example_45 │ │ ├── example_46 │ │ ├── example_47 │ │ ├── example_48 │ │ ├── example_49 │ │ ├── example_5 │ │ ├── example_50 │ │ ├── example_51 │ │ ├── example_6 │ │ ├── example_7 │ │ ├── example_8 │ │ └── example_9 └── src │ ├── bin │ └── fuzzing.rs │ ├── lib.rs │ ├── message.rs │ └── message_stream.rs ├── tunshell-tests ├── Cargo.toml ├── test.sh └── tests │ ├── lib.rs │ ├── utils │ ├── mod.rs │ └── shell.rs │ ├── valid_direct_connection.rs │ └── valid_relay_connection.rs └── website ├── .babelrc ├── .editorconfig ├── components ├── button │ ├── index.tsx │ └── styled.tsx ├── direct-web-term │ ├── index.tsx │ └── styled.tsx ├── donate │ ├── index.tsx │ └── styled.tsx ├── dropdown │ ├── index.tsx │ └── styled.tsx ├── footer │ ├── index.tsx │ └── styled.tsx ├── header │ ├── index.tsx │ └── styled.tsx ├── hero │ ├── index.tsx │ └── styled.tsx ├── layout │ └── index.tsx ├── link │ ├── index.tsx │ └── styled.tsx ├── script │ ├── index.tsx │ └── styled.tsx ├── term │ ├── index.tsx │ └── styled.tsx ├── tunshell-client │ └── index.tsx └── wizard │ ├── index.tsx │ └── styled.tsx ├── next-env.d.ts ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── go.tsx ├── index.tsx └── term.tsx ├── post-export.sh ├── public ├── images │ └── logo.svg └── robots.txt ├── services ├── api-client.ts ├── direct-web-url.ts ├── install-script.ts ├── location.ts ├── session.ts └── tunshell-wasm.ts ├── theme └── colours.tsx ├── tsconfig.json └── typings ├── ionicons.d.ts └── react-typical.d.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | js/ 2 | aws/ 3 | website/ 4 | **/build 5 | **/target -------------------------------------------------------------------------------- /.github/workflows/deploy-client-binaries.yml: -------------------------------------------------------------------------------- 1 | name: Publish Client Libraries 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build_test_deploy: 10 | continue-on-error: true 11 | strategy: 12 | matrix: 13 | include: 14 | # Linux 15 | - platform: ubuntu-latest 16 | target: x86_64-unknown-linux-musl 17 | tests: true 18 | - platform: ubuntu-latest 19 | target: armv7-unknown-linux-musleabihf 20 | - platform: ubuntu-latest 21 | target: aarch64-unknown-linux-musl 22 | tests: true 23 | rustflags: -C link-arg=-lgcc # from https://github.com/rust-embedded/cross/blob/master/docker/Dockerfile.aarch64-unknown-linux-musl 24 | - platform: ubuntu-latest 25 | target: arm-unknown-linux-musleabi 26 | tests: true 27 | - platform: ubuntu-latest 28 | target: arm-linux-androideabi 29 | - platform: ubuntu-latest 30 | target: aarch64-linux-android 31 | - platform: ubuntu-latest 32 | target: i686-unknown-linux-musl 33 | tests: true 34 | - platform: ubuntu-latest 35 | target: i586-unknown-linux-musl 36 | tests: true 37 | - platform: ubuntu-latest 38 | target: mips-unknown-linux-musl 39 | rustflags: --cfg openssl 40 | - platform: ubuntu-latest 41 | target: x86_64-unknown-freebsd 42 | cc: x86_64-unknown-freebsd12-gcc 43 | # Apple 44 | - platform: macos-latest 45 | target: x86_64-apple-darwin 46 | tests: true 47 | - platform: macos-latest 48 | target: x86_64-apple-ios 49 | - platform: macos-latest 50 | target: aarch64-apple-darwin 51 | # Windows 52 | - platform: windows-latest 53 | target: x86_64-pc-windows-msvc 54 | tests: true 55 | rustflags: -C target-feature=+crt-static 56 | - platform: windows-latest 57 | target: i686-pc-windows-msvc 58 | rustflags: -C target-feature=+crt-static 59 | # iSH (https://ish.app/) 60 | - platform: ubuntu-latest 61 | target: i686-unknown-linux-musl 62 | client_binary_name: ish 63 | tests: false 64 | rustflags: -C target-feature=-mmx,-sse,-sse2 --cfg tls_only_aes_gcm 65 | runs-on: ${{ matrix.platform }} 66 | 67 | steps: 68 | - uses: actions/checkout@v2 69 | 70 | - run: ./install-deps.sh 71 | working-directory: tunshell-client/build 72 | shell: bash 73 | env: 74 | TEMPDIR: /tmp 75 | 76 | - run: ./compile.sh ${{ matrix.target }} $PWD/artifacts/client 77 | working-directory: tunshell-client/build 78 | shell: bash 79 | env: 80 | RUN_TESTS: ${{ matrix.tests }} 81 | RUST_TEST_THREADS: 1 82 | CI: true 83 | RUST_LOG: debug 84 | RUSTFLAGS: ${{ matrix.rustflags }} 85 | CC: ${{ matrix.cc }} 86 | 87 | - uses: aws-actions/configure-aws-credentials@v1 88 | with: 89 | aws-access-key-id: ${{ secrets.ARTIFACT_AWS_ACCESS_KEY_ID }} 90 | aws-secret-access-key: ${{ secrets.ARTIFACT_AWS_SECRET_ACCESS_KEY }} 91 | aws-region: us-east-1 92 | 93 | # Deploy artifact to https://artifacts.tunshell.com 94 | - name: Deploy to S3 95 | run: > 96 | aws s3 cp $PWD/artifacts/client s3://tunshell-artifacts/client-${{ matrix.client_binary_name || matrix.target }} 97 | --acl=public-read 98 | --cache-control max-age=31536000 99 | --metadata "sha256=$(openssl sha256 -r $PWD/artifacts/client | awk '{print $1}')" 100 | --metadata-directive REPLACE 101 | working-directory: tunshell-client/build 102 | shell: bash 103 | 104 | - name: Invalidate CloudFront 105 | run: aws cloudfront create-invalidation --distribution-id=E3DF4SGU15BNWT --paths '/*' -------------------------------------------------------------------------------- /.github/workflows/deploy-client-docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Client Docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build_test_deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Run Tests 15 | uses: elgohr/Publish-Docker-Github-Action@v5 16 | env: 17 | RUN_TESTS: true 18 | with: 19 | name: timetoogo/tunshell 20 | dockerfile: tunshell-client/docker/prod.Dockerfile 21 | username: ${{ secrets.DOCKER_USERNAME }} 22 | password: ${{ secrets.DOCKER_PASSWORD }} 23 | tags: "latest" 24 | buildargs: RUN_TESTS -------------------------------------------------------------------------------- /.github/workflows/deploy-relay-server.yml: -------------------------------------------------------------------------------- 1 | name: Publish Server Docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build_test_deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Run Tests 15 | run: ./test.sh 16 | working-directory: tunshell-server 17 | shell: bash 18 | env: 19 | RUST_LOG: debug 20 | 21 | - name: Publish to Docker Hub 22 | uses: elgohr/Publish-Docker-Github-Action@v5 23 | with: 24 | name: timetoogo/tunshell-relay 25 | dockerfile: tunshell-server/docker/prod.Dockerfile 26 | username: ${{ secrets.DOCKER_USERNAME }} 27 | password: ${{ secrets.DOCKER_PASSWORD }} 28 | tags: "latest" -------------------------------------------------------------------------------- /.github/workflows/deploy-scripts.yml: -------------------------------------------------------------------------------- 1 | name: Publish Install Scripts 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - uses: actions/setup-dotnet@v1 15 | with: 16 | dotnet-version: '3.1.x' 17 | - name: Compile .NET Core init DLL 18 | run: ./build.sh 19 | working-directory: scripts/netcore 20 | 21 | - uses: actions/setup-java@v1 22 | with: 23 | java-version: '10' 24 | - name: Compile Java init JAR 25 | run: ./build.sh 26 | working-directory: scripts/java 27 | 28 | - uses: aws-actions/configure-aws-credentials@v1 29 | with: 30 | aws-access-key-id: ${{ secrets.ARTIFACT_AWS_ACCESS_KEY_ID }} 31 | aws-secret-access-key: ${{ secrets.ARTIFACT_AWS_SECRET_ACCESS_KEY }} 32 | aws-region: us-east-1 33 | 34 | - name: Deploy to S3 35 | run: aws s3 sync --delete --exclude="$PWD/*/*" ./ s3://tunshell-init/ --acl=public-read --cache-control max-age=7200 --metadata-directive REPLACE 36 | working-directory: scripts 37 | 38 | - name: Invalidate CloudFront 39 | run: aws cloudfront create-invalidation --distribution-id=E2JEG1NKANF1OH --paths '/*' -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yml: -------------------------------------------------------------------------------- 1 | name: Publish Website 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - feature/website 8 | 9 | jobs: 10 | build_and_deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - run: ./install-deps-wasm.sh 16 | working-directory: tunshell-client/build 17 | shell: bash 18 | 19 | - run: ./compile-wasm.sh 20 | working-directory: tunshell-client/build 21 | shell: bash 22 | 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 12 26 | 27 | - run: npm ci 28 | working-directory: website 29 | 30 | - run: npm run export 31 | working-directory: website 32 | 33 | - uses: aws-actions/configure-aws-credentials@v1 34 | with: 35 | aws-access-key-id: ${{ secrets.WEBSITE_AWS_ACCESS_KEY_ID }} 36 | aws-secret-access-key: ${{ secrets.WEBSITE_AWS_SECRET_ACCESS_KEY }} 37 | aws-region: us-east-1 38 | 39 | - name: Deploy to S3 40 | run: aws s3 sync --delete --acl=public-read --cache-control max-age=7200 --metadata-directive REPLACE out/ s3://tunshell-web/ 41 | working-directory: website 42 | 43 | - name: Deploy to S3 (html) 44 | run: aws s3 cp --recursive --content-type text/html --exclude "*" --include "*.html" --include "go" --include "term" --include "404" --acl=public-read --cache-control max-age=7200 --metadata-directive REPLACE out/ s3://tunshell-web/ 45 | working-directory: website 46 | 47 | - name: Deploy to S3 (wasm) 48 | run: aws s3 cp --recursive --content-type application/wasm --exclude "*" --include "*.wasm" --acl=public-read --cache-control max-age=7200 --metadata-directive REPLACE out/ s3://tunshell-web/ 49 | working-directory: website 50 | 51 | - name: Invalidate CloudFront 52 | run: aws cloudfront create-invalidation --distribution-id=E2BXOYP52RJJQF --paths '/*' 53 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Integration Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | run_tests: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - run: ./test.sh 15 | working-directory: tunshell-tests 16 | shell: bash 17 | env: 18 | RUST_TEST_THREADS: 1 19 | RUST_LOG: debug 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### OSX ### 3 | # General 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | # node 31 | node_modules/ 32 | dist/ 33 | artifacts/ 34 | out/ 35 | .next/ 36 | 37 | # Rust 38 | target/ 39 | 40 | # VS Code 41 | .vscode 42 | 43 | # env 44 | .env 45 | 46 | # build artifacts 47 | tunshell-client/build/tmp 48 | tunshell-client/build/artifacts 49 | tunshell-client/pkg 50 | 51 | # logs 52 | *.log 53 | 54 | # .net core 55 | scripts/netcore/bin/ 56 | obj/ 57 | *.dll 58 | temp/ 59 | 60 | # java 61 | scripts/java/bin/ 62 | *.jar 63 | 64 | # sqlite 65 | *.sqlite -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "tunshell-shared", 5 | "tunshell-client", 6 | "tunshell-server", 7 | "tunshell-tests", 8 | ] -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [build.env] 2 | passthrough = [ 3 | "RUST_LOG", 4 | "CI", 5 | "RUST_TEST_THREADS", 6 | "RUSTFLAGS", 7 | "CC", 8 | ] 9 | 10 | [target."x86_64-unknown-freebsd"] 11 | image = "rustembedded/cross:x86_64-unknown-freebsd" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Elliot Levin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Currently there is no official versioning scheme. 6 | Only the latest revision will be supported. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Please send all vulnerability disclosures to elliotlevin@hotmail.com 11 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | tunshell todo: 2 | 3 | - [x] clean up code / tests 4 | - [x] finalise implementation for reliable-UDP protocol 5 | - [x] replace thrussh with custom protocol over ~~TLS~~ AES 6 | - [x] fix up "early eof" error on client by fixing the handling server-side 7 | - [x] re-write server in Rust 8 | - [x] windows support? 9 | - [x] security enhancements 10 | - [x] website to generate PSK for AEAD stream for relay and direct connections 11 | - [x] script templates to be moved to S3 12 | - [x] move logic to decide on target/client host to client binary 13 | - [x] relay server to generate session specific key which is concat to the client PSK 14 | - [x] basic rust-only shell fallback for limited envs without pty 15 | - [x] support in-browser terminal client 16 | - [x] tunshell-client wasm target 17 | - [x] extend relay server to support websocket connections and implement client in-browser 18 | - [x] init scripts for multiple langs/envs 19 | - [x] sh/bash 20 | - [x] cmd/powershell 21 | - [x] node 22 | - [x] python 23 | - [x] C# 24 | - [x] java 25 | - [x] php 26 | - [x] docker 27 | - [x] improve port binding logic 28 | - [x] integration tests / pipeline tests 29 | - [x] docs / website 30 | - [x] migrate aws account to OU / investigate hosting options 31 | - [ ] fix flakey tests 32 | -------------------------------------------------------------------------------- /aws/README.md: -------------------------------------------------------------------------------- 1 | Relay Server 2 | ============ 3 | 4 | The relay server serves the clients to establish network connectivity between themselves. 5 | Providing them with the address and port information to attempt direct p2p connections and also support the proxying of traffic between clients if direct connections are not possible. 6 | This allows the clients to operate even when behind restrictive firewalls or NAT devices. 7 | 8 | It is composed of: 9 | 10 | - A custom server application, the [tunshell-server](../tunshell-server) 11 | - A SQLite database which persists the session keys 12 | - A [Nginx + Let's Encrypt](https://github.com/linuxserver/docker-letsencrypt) which generates SSL certificates and provides http -> https redirection 13 | 14 | These services are containerised and can be easily spun up by docker compose. 15 | 16 | ## Self-hosting your own relay server 17 | 18 | ### On AWS 19 | 20 | The easiest way to spin up your own relay server is to use the CloudFormation template which is scripted to configure a new relay server in your own AWS environment. 21 | 22 | 1. Go to Cloud Formation console: https://console.aws.amazon.com/cloudformation/home 23 | 2. Create a new stack and select 'Upload a template file' 24 | 3. Upload [this template](./stack.yml) 25 | 4. Fill out the parameters and let the stack initialise 26 | - This stack will configure an EC2 instance 27 | - Create DNS records for the server (for LetsEncrypt validation) 28 | - Bootstrap the instance with docker and the containers 29 | 30 | ### Elsewhere 31 | 32 | If you are hosting your server outside of AWS you will have to provision and configure the relay server yourself. 33 | 34 | You may use the [bootstrap script](./setup-ec2.sh) and [docker-compose.yml](./docker-compose.yml) files as a starting point. 35 | 36 | ## Using your self-hosted relay server 37 | 38 | Unfortunately the current tunshell.com website is hard-coded to use the standard relay.tunshell.com server to generate session keys so these will have to be created manually. 39 | An easy way to create a new pair of session keys is via `curl`: 40 | 41 | ```sh 42 | curl -XPOST https://relay.yourdomain.com/api/sessions 43 | ``` 44 | 45 | Which will generate a pair of session keys: 46 | 47 | ```json 48 | { "peer1_key": "M485rfdQg8h24byfwkEsc", "peer2_key": "FDxbfDDd67fRFd5skflmc" } 49 | ``` 50 | 51 | Additionally you will have to pass an additional argument to the client init scripts to override the default relay server: 52 | 53 | ```sh 54 | # Local 55 | sh <(curl -sSf https://lets.tunshell.com/init.sh) L M485rfdQg8h24byfwkEsc YcO8WqyFIEVv8mxReYugGq relay.yourdomain.com 56 | 57 | # Target 58 | curl -sSf https://lets.tunshell.com/init.sh | sh -s -- T FDxbfDDd67fRFd5skflmc YcO8WqyFIEVv8mxReYugGq relay.yourdomain.com 59 | ``` 60 | -------------------------------------------------------------------------------- /aws/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | relay: 4 | image: timetoogo/tunshell-relay 5 | container_name: relay 6 | restart: unless-stopped 7 | depends_on: 8 | - reverse_proxy 9 | ports: 10 | - "443:3000" 11 | - "5000:3001" 12 | volumes: 13 | - ./config/etc/letsencrypt/:/le-ssl 14 | - ./db.sqlite:/app/db.sqlite 15 | environment: 16 | SQLITE_DB_PATH: /app/db.sqlite 17 | TUNSHELL_API_PORT: 3000 18 | TUNSHELL_RELAY_TLS_PORT: 3001 19 | TLS_RELAY_PRIVATE_KEY: /le-ssl/live/${RELAY_DOMAIN}/privkey.pem 20 | TLS_RELAY_CERT: /le-ssl/live/${RELAY_DOMAIN}/fullchain.pem 21 | DOMAIN_NAME: ${RELAY_DOMAIN} 22 | 23 | reverse_proxy: 24 | image: linuxserver/letsencrypt 25 | container_name: letsencrypt 26 | cap_add: 27 | - NET_ADMIN 28 | environment: 29 | - TZ=UTC 30 | - URL=${RELAY_DOMAIN} 31 | - VALIDATION=http 32 | volumes: 33 | - ./config:/config 34 | ports: 35 | - 80:80 36 | restart: unless-stopped 37 | 38 | watchtower: 39 | image: v2tec/watchtower 40 | container_name: watchtower 41 | volumes: 42 | - /var/run/docker.sock:/var/run/docker.sock 43 | command: watchtower reverse_proxy relay --interval 30 44 | restart: unless-stopped 45 | -------------------------------------------------------------------------------- /aws/nginx.conf: -------------------------------------------------------------------------------- 1 | # redirect all traffic to https 2 | server { 3 | listen 80 default_server; 4 | listen [::]:80 default_server; 5 | server_name _; 6 | return 301 https://$host$request_uri; 7 | } 8 | 9 | # enable subdomain method reverse proxy confs 10 | include /config/nginx/proxy-confs/*.subdomain.conf; 11 | # enable proxy cache for auth 12 | proxy_cache_path cache/ keys_zone=auth_cache:10m; -------------------------------------------------------------------------------- /aws/setup-ec2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "Setting up tunshell server..." 6 | 7 | if [[ ! -x "$(command -v docker)" ]]; 8 | then 9 | sudo apt-get update 10 | sudo apt-get install -y docker.io docker-compose sqlite3 11 | sudo usermod -aG docker ubuntu 12 | fi 13 | 14 | . env.sh 15 | 16 | # Wait until server is reachable via DNS 17 | until nc -zvw5 $RELAY_DOMAIN 22 18 | do 19 | echo "$RELAY_DOMAIN is still not reachable will retry in 30s..." 20 | sleep 30 21 | done 22 | 23 | # Extra time for DNS / routing to propagate 24 | sleep 300 25 | 26 | curl https://raw.githubusercontent.com/TimeToogo/tunshell/master/aws/docker-compose.yml > docker-compose.yml 27 | curl https://raw.githubusercontent.com/TimeToogo/tunshell/master/aws/nginx.conf > nginx.conf 28 | 29 | mkdir -p config/nginx/site-confs/ 30 | mv nginx.conf config/nginx/site-confs/default 31 | touch db.sqlite 32 | 33 | sudo service docker start 34 | sg docker -c "docker-compose pull" 35 | # start services gradually as not to run out of memory on small instances 36 | # and allow extra time for lets encrypt proxy to generate cert 37 | sg docker -c "docker-compose up -d reverse_proxy" 38 | sleep 60 39 | sg docker -c "docker-compose up -d relay" 40 | sleep 30 41 | sg docker -c "docker-compose up -d watchtower" 42 | 43 | echo "done!" 44 | -------------------------------------------------------------------------------- /scripts/init.cmd: -------------------------------------------------------------------------------- 1 | rem === TUNSHELL CMD SCRIPT === 2 | 3 | @echo OFF 4 | 5 | reg Query "HKLM\Hardware\Description\System\CentralProcessor\0" | find /i "x86" > NUL && set OS=32BIT || set OS=64BIT 6 | 7 | if %OS%==32BIT ( 8 | set TARGET="i686-pc-windows-msvc" 9 | ) else if %OS%==64BIT ( 10 | set TARGET="x86_64-pc-windows-msvc" 11 | ) else ( 12 | echo "Unknown CPU architecture: %OS%" 13 | exit 1 14 | ) 15 | 16 | if not exist "%TEMP%\tunshell" mkdir "%TEMP%\tunshell" 17 | set CLIENT_PATH="%TEMP%\tunshell\client.exe" 18 | 19 | echo %CLIENT_PATH% 20 | echo "Installing client..." 21 | powershell -Command "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; (New-Object Net.WebClient).DownloadFile('https://artifacts.tunshell.com/client-%TARGET%.exe', '%CLIENT_PATH%')" 22 | 23 | %CLIENT_PATH% %* 24 | -------------------------------------------------------------------------------- /scripts/init.js: -------------------------------------------------------------------------------- 1 | // === TUNSHELL NODE SCRIPT === 2 | (async () => { 3 | const process = require("process"); 4 | const https = require("https"); 5 | const fs = require("fs"); 6 | const os = require("os"); 7 | const { spawn } = require("child_process"); 8 | 9 | if (!args) { 10 | throw new Error(`args variable must be set`); 11 | } 12 | 13 | const getTarget = () => { 14 | const targets = { 15 | linux: { 16 | x64: "x86_64-unknown-linux-musl", 17 | x32: "i686-unknown-linux-musl", 18 | arm: "armv7-unknown-linux-musleabihf", 19 | arm64: "aarch64-unknown-linux-musl", 20 | }, 21 | darwin: { 22 | x64: "x86_64-apple-darwin", 23 | arm64: "aarch64-apple-darwin", 24 | }, 25 | win32: { 26 | x64: "x86_64-pc-windows-msvc.exe", 27 | x32: "i686-pc-windows-msvc.exe", 28 | }, 29 | }; 30 | 31 | if (!targets[os.platform()]) { 32 | console.error(`Unsupported platform: ${os.platform()}`); 33 | return null; 34 | } 35 | 36 | if (!targets[os.platform()][os.arch()]) { 37 | console.error(`Unsupported CPU architecture: ${os.arch()}`); 38 | return null; 39 | } 40 | 41 | return targets[os.platform()][os.arch()]; 42 | }; 43 | 44 | const downloadClient = (target) => { 45 | const tempPath = os.tmpdir(); 46 | 47 | if (!fs.existsSync(`${tempPath}/tunshell`)) { 48 | fs.mkdirSync(`${tempPath}/tunshell`, { recursive: true }); 49 | } 50 | 51 | const clientPath = `${tempPath}/tunshell/client`; 52 | 53 | return new Promise((resolve, reject) => { 54 | const file = fs.createWriteStream(clientPath); 55 | 56 | const request = https.get( 57 | `https://artifacts.tunshell.com/client-${target}`, 58 | (response) => { 59 | response.pipe(file); 60 | 61 | response.on("end", () => { 62 | file.end(null); 63 | }); 64 | 65 | response.on("error", reject); 66 | } 67 | ); 68 | 69 | file.on("close", () => resolve(clientPath)); 70 | }); 71 | }; 72 | 73 | const execClient = (client, args) => { 74 | fs.chmodSync(client, 0755); 75 | 76 | spawn(client, args, { stdio: "inherit" }); 77 | }; 78 | 79 | const target = getTarget(); 80 | 81 | if (!target) { 82 | process.exit(1); 83 | } 84 | 85 | console.log("Installing client..."); 86 | let clientPath = await downloadClient(target); 87 | 88 | execClient(clientPath, args); 89 | })(); 90 | -------------------------------------------------------------------------------- /scripts/init.php: -------------------------------------------------------------------------------- 1 | // === TUNSHELL PHP SCRIPT === 2 | 3 | return function (array $args) { 4 | echo "Installing client...\n"; 5 | 6 | $targets = [ 7 | 'Linux' => [ 8 | 'x86_64' => 'x86_64-unknown-linux-musl', 9 | 'i686' => 'i686-unknown-linux-musl', 10 | 'armv7' => 'armv7-unknown-linux-musleabihf', 11 | 'aarch64' => 'aarch64-unknown-linux-musl', 12 | ], 13 | 'Darwin' => [ 14 | 'x86_64' => 'x86_64-apple-darwin', 15 | 'arm64' => 'aarch64-apple-darwin', 16 | ], 17 | 'WindowsNT' => [ 18 | 'x86_64' => 'x86_64-pc-windows-msvc.exe', 19 | 'i686' => 'i686-pc-windows-msvc.exe', 20 | ] 21 | ]; 22 | 23 | $os = php_uname('s'); 24 | $arch = php_uname('m'); 25 | 26 | if (!$targets[$os]) { 27 | throw new Exception("Unsupported OS: $os"); 28 | } 29 | 30 | if (!$targets[$os][$arch]) { 31 | throw new Exception("Unsupported CPU architecture: $arch"); 32 | } 33 | 34 | $target = $targets[$os][$arch]; 35 | $clientPath = tempnam(sys_get_temp_dir(), 'tunshell_client_'); 36 | copy("https://artifacts.tunshell.com/client-$target", $clientPath); 37 | chmod($clientPath, 0755); 38 | 39 | $args = implode(' ', $args); 40 | $process = proc_open("$clientPath $args", [ 41 | 0 => ['pipe', 'r'], 42 | 1 => ['pipe', 'w'], 43 | 2 => ['pipe', 'w'], 44 | ], $pipes); 45 | 46 | stream_set_blocking($pipes[1], 0); 47 | stream_set_blocking($pipes[2], 0); 48 | 49 | while (is_resource($pipes[1]) || is_resource($pipes[2])) 50 | { 51 | if (is_resource($pipes[1])) { 52 | if(feof($pipes[1])) 53 | { 54 | fclose($pipes[1]); 55 | } 56 | else 57 | { 58 | echo fgets($pipes[1], 1024); 59 | } 60 | } 61 | 62 | if (is_resource($pipes[2])) { 63 | if(feof($pipes[2])) 64 | { 65 | fclose($pipes[2]); 66 | } 67 | else 68 | { 69 | echo fgets($pipes[2], 1024); 70 | } 71 | } 72 | } 73 | 74 | proc_close($process); 75 | }; 76 | -------------------------------------------------------------------------------- /scripts/init.ps1: -------------------------------------------------------------------------------- 1 | # === TUNSHELL PS SCRIPT === 2 | 3 | if ([System.Environment]::Is64BitOperatingSystem) { 4 | $TARGET="x86_64-pc-windows-msvc" 5 | } else { 6 | $TARGET="i686-pc-windows-msvc" 7 | } 8 | 9 | New-Item -ItemType Directory -Force -Path "$TEMP\tunshell" > $null 10 | $CLIENT_PATH="$TEMP\tunshell\client.exe" 11 | 12 | Write-Host "Installing client..." 13 | [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 14 | (New-Object Net.WebClient).DownloadFile("https://artifacts.tunshell.com/client-$TARGET.exe", "$CLIENT_PATH") 15 | 16 | Invoke-Expression "$CLIENT_PATH $($args -join " ")" 17 | -------------------------------------------------------------------------------- /scripts/init.py: -------------------------------------------------------------------------------- 1 | # === TUNSHELL PYTHON3 SCRIPT === 2 | import platform 3 | import tempfile 4 | import urllib.request 5 | import os 6 | import sys 7 | import subprocess 8 | 9 | def get_target(): 10 | targets = { 11 | 'Linux': { 12 | 'x86_64': 'x86_64-unknown-linux-musl', 13 | 'i686': 'i686-unknown-linux-musl', 14 | 'arm': 'armv7-unknown-linux-musleabihf', 15 | 'arm64': 'aarch64-unknown-linux-musl', 16 | }, 17 | 'Darwin': { 18 | 'x86_64': 'x86_64-apple-darwin', 19 | 'arm64': 'aarch64-apple-darwin', 20 | }, 21 | 'Windows': { 22 | 'x86_64': 'x86_64-pc-windows-msvc', 23 | 'i686': 'i686-pc-windows-msvc', 24 | }, 25 | } 26 | 27 | system = platform.system() 28 | arch = platform.machine() 29 | 30 | if system not in targets: 31 | raise Exception(f'Unsupported platform: {system}') 32 | 33 | if arch not in targets[system]: 34 | raise Exception(f'Unsupported CPU architecture: {arch}') 35 | 36 | return targets[system][arch] 37 | 38 | def run(): 39 | print('Installing client...') 40 | target = get_target() 41 | 42 | with tempfile.NamedTemporaryFile(delete=False) as tmp: 43 | r = urllib.request.urlopen(f'https://artifacts.tunshell.com/client-{target}') 44 | tmp.write(r.read()) 45 | tmp.close() 46 | try: 47 | os.chmod(tmp.name, 0o755) 48 | subprocess.run([tmp.name] + p) 49 | finally: 50 | try: 51 | os.unlink(tmp.name) 52 | except: 53 | pass 54 | 55 | run() 56 | -------------------------------------------------------------------------------- /scripts/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ## === TUNSHELL SHELL SCRIPT === 3 | 4 | set -e 5 | 6 | main() { 7 | case "$(uname -s):$(uname -m):$(uname -v):$(uname -a)" in 8 | Linux:x86_64*) 9 | TARGET="x86_64-unknown-linux-musl" 10 | ;; 11 | Linux:aarch64:*:*Android*) 12 | TARGET="aarch64-linux-android" 13 | ;; 14 | Linux:arm64*|Linux:aarch64*) 15 | TARGET="aarch64-unknown-linux-musl" 16 | ;; 17 | Linux:arm*) 18 | TARGET="armv7-unknown-linux-musleabihf" 19 | ;; 20 | Linux:i686:iSH*) 21 | TARGET="ish" 22 | ;; 23 | Linux:i686*) 24 | TARGET="i686-unknown-linux-musl" 25 | ;; 26 | Linux:i586*) 27 | TARGET="i586-unknown-linux-musl" 28 | ;; 29 | Linux:mips*) 30 | TARGET="mips-unknown-linux-musl" 31 | ;; 32 | FreeBSD:x86_64*) 33 | TARGET="x86_64-unknown-freebsd" 34 | ;; 35 | FreeBSD:amd64*) 36 | TARGET="x86_64-unknown-freebsd" 37 | ;; 38 | FreeBSD:i686*) 39 | TARGET="i686-unknown-freebsd" 40 | ;; 41 | Darwin:x86_64*) 42 | TARGET="x86_64-apple-darwin" 43 | ;; 44 | Darwin:arm64*) 45 | TARGET="aarch64-apple-darwin" 46 | ;; 47 | WindowsNT:x86_64*|MINGW64_NT*:x86_64*) 48 | TARGET="x86_64-pc-windows-msvc.exe" 49 | ;; 50 | WindowsNT:i686*|MINGW32_NT*:i686*) 51 | TARGET="i686-pc-windows-msvc.exe" 52 | ;; 53 | *) 54 | echo "Unsupported system ($(uname -a))" 55 | exit 1 56 | ;; 57 | esac 58 | 59 | if [ -w "$XDG_CACHE_HOME" ] 60 | then 61 | TEMP_PATH="$XDG_CACHE_HOME" 62 | elif mkdir -p $HOME/.cache 63 | then 64 | TEMP_PATH="$HOME/.cache" 65 | elif [ -w "$TMPDIR" ] 66 | then 67 | TEMP_PATH="$TMPDIR" 68 | elif [ -w "/tmp" ] 69 | then 70 | TEMP_PATH="/tmp" 71 | elif [ -x "$(command -v mktemp)" ] 72 | then 73 | TEMP_PATH="$(mktemp -d)" 74 | else 75 | echo "Could not find writeable temp directory" 76 | echo "Run again with TMPDIR=/path/to/writable/dir" 77 | exit 1 78 | fi 79 | 80 | TEMP_PATH="$TEMP_PATH/tunshell" 81 | CLIENT_PATH="$TEMP_PATH/client" 82 | 83 | mkdir -p $TEMP_PATH 84 | 85 | if [ ! -O $TEMP_PATH -a -z "$TUNSHELL_INSECURE_EXEC" ]; 86 | then 87 | echo "Temp path $TEMP_PATH is not owned by current user" 88 | echo "Run again with TUNSHELL_INSECURE_EXEC=1 to ignore this warning" 89 | exit 1 90 | fi 91 | 92 | if [ -x "$(command -v curl)" ] 93 | then 94 | INSTALL_CLIENT=true 95 | 96 | # Check if client is already downloaded and is up-to-date and not tampered with 97 | if [ -x "$(command -v grep)" ] && [ -x "$(command -v cut)" ] && [ -x "$(command -v sha256sum)" ] && [ -f $CLIENT_PATH ] 98 | then 99 | CURRENT_HASH=$(sha256sum $CLIENT_PATH | cut -d' ' -f1 || true) 100 | LATEST_HASH=$(curl -XHEAD -sSfI https://artifacts.tunshell.com/client-${TARGET} | grep -i 'sha256' | cut -d' ' -f2 | cut -d$'\r' -f1 || true) 101 | 102 | if [ ! -z "$CURRENT_HASH" ] && [ ! -z "$LATEST_HASH" ] && [ "$CURRENT_HASH" = "$LATEST_HASH" ] 103 | then 104 | echo "Client already installed..." 105 | INSTALL_CLIENT=false 106 | fi 107 | fi 108 | 109 | if [ "$INSTALL_CLIENT" = true ] 110 | then 111 | echo "Installing client..." 112 | curl -sSf https://artifacts.tunshell.com/client-${TARGET} -o $CLIENT_PATH 113 | fi 114 | elif [ -x "$(command -v wget)" ] 115 | then 116 | wget -q https://artifacts.tunshell.com/client-${TARGET} -O $CLIENT_PATH 117 | else 118 | echo "Could not download client: please install curl or wget..." 119 | exit 1 120 | fi 121 | 122 | chmod +x $CLIENT_PATH 123 | 124 | $CLIENT_PATH "$@" 125 | } 126 | 127 | main "$@" || exit 1 128 | -------------------------------------------------------------------------------- /scripts/java/Manifest.txt: -------------------------------------------------------------------------------- 1 | Main-Class: init 2 | -------------------------------------------------------------------------------- /scripts/java/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | DIR=$(dirname $0) 6 | 7 | javac -d $DIR/bin -source 8 -target 8 init.java 8 | cd $DIR/bin 9 | jar cvfm $DIR/../../init.jar ../Manifest.txt . 10 | cd $DIR -------------------------------------------------------------------------------- /scripts/java/init.java: -------------------------------------------------------------------------------- 1 | import java.io.File; 2 | import java.io.IOException; 3 | import java.io.InputStream; 4 | import java.net.URL; 5 | import java.nio.file.Files; 6 | import java.nio.file.StandardCopyOption; 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.Objects; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Stream; 13 | 14 | public class init { 15 | 16 | public enum Os { 17 | LINUX, 18 | OSX, 19 | WINDOWS; 20 | 21 | public static Os detect() { 22 | return from(System.getProperty("os.name").toLowerCase()); 23 | } 24 | 25 | public static Os from(String name) { 26 | switch (name) { 27 | case "linux": 28 | return Os.LINUX; 29 | case "os x": 30 | case "mac": 31 | case "mac os x": 32 | return Os.OSX; 33 | case "windows": 34 | return Os.WINDOWS; 35 | } 36 | throw new RuntimeException(String.format("Unsupported platform: %s", name)); 37 | } 38 | } 39 | 40 | public enum Arch { 41 | X86_64, 42 | I686, 43 | ARM, 44 | ARM64; 45 | 46 | public static Arch detect() { 47 | return from(System.getProperty("os.arch").toLowerCase()); 48 | } 49 | 50 | public static Arch from(String name) { 51 | switch (name) { 52 | case "x86_64": 53 | case "amd64": 54 | return Arch.X86_64; 55 | case "i686": 56 | return Arch.I686; 57 | case "arm": 58 | return Arch.ARM; 59 | case "arm64": 60 | return Arch.ARM64; 61 | } 62 | throw new RuntimeException(String.format("Unsupported CPU architecture: %s", name)); 63 | } 64 | } 65 | 66 | public static class OsArch { 67 | private final Os os; 68 | private final Arch arch; 69 | 70 | public OsArch(Os os, Arch arch) { 71 | this.os = Objects.requireNonNull(os); 72 | this.arch = Objects.requireNonNull(arch); 73 | } 74 | 75 | @Override 76 | public boolean equals(Object o) { 77 | if (this == o) { 78 | return true; 79 | } 80 | if (o == null || getClass() != o.getClass()) { 81 | return false; 82 | } 83 | OsArch osArch = (OsArch) o; 84 | if (os != osArch.os) { 85 | return false; 86 | } 87 | return arch == osArch.arch; 88 | } 89 | 90 | @Override 91 | public int hashCode() { 92 | int result = os.hashCode(); 93 | result = 31 * result + arch.hashCode(); 94 | return result; 95 | } 96 | } 97 | 98 | private static final Map SUPPORTED_TARGETS; 99 | 100 | static { 101 | final Map targets = new HashMap<>(); 102 | targets.put(new OsArch(Os.LINUX, Arch.X86_64), "x86_64-unknown-linux-musl"); 103 | targets.put(new OsArch(Os.LINUX, Arch.I686), "i686-unknown-linux-musl"); 104 | targets.put(new OsArch(Os.LINUX, Arch.ARM), "armv7-unknown-linux-musleabihf"); 105 | targets.put(new OsArch(Os.LINUX, Arch.ARM64), "aarch64-unknown-linux-musl"); 106 | targets.put(new OsArch(Os.OSX, Arch.X86_64), "x86_64-apple-darwin"); 107 | targets.put(new OsArch(Os.OSX, Arch.ARM64), "aarch64-apple-darwin"); 108 | targets.put(new OsArch(Os.WINDOWS, Arch.X86_64), "x86_64-pc-windows-msvc.exe"); 109 | targets.put(new OsArch(Os.WINDOWS, Arch.I686), "i686-pc-windows-msvc.exe"); 110 | SUPPORTED_TARGETS = Collections.unmodifiableMap(targets); 111 | } 112 | 113 | public static String getTarget(Os os, Arch arch) { 114 | final String target = SUPPORTED_TARGETS.get(new OsArch(os, arch)); 115 | if (target == null) { 116 | throw new RuntimeException( 117 | String.format("Unsupported platform / CPU architecture: %s / %s", os, arch)); 118 | } 119 | return target; 120 | } 121 | 122 | public static String getTarget() { 123 | return getTarget(Os.detect(), Arch.detect()); 124 | } 125 | 126 | public static void main(String[] args) throws IOException, InterruptedException { 127 | System.out.println("Installing client..."); 128 | 129 | final File clientFile = File.createTempFile("tunshell-client", null); 130 | final String url = String.format("https://artifacts.tunshell.com/client-%s", getTarget()); 131 | try (final InputStream in = new URL(url).openStream()) { 132 | Files.copy(in, clientFile.toPath(), StandardCopyOption.REPLACE_EXISTING); 133 | } 134 | if (!clientFile.setExecutable(true)) { 135 | throw new RuntimeException(String.format("Unable to mark '%s' as executable", clientFile)); 136 | } 137 | 138 | new ProcessBuilder( 139 | Stream.concat(Stream.of(clientFile.getAbsolutePath()), Stream.of(args)) 140 | .collect(Collectors.toList())) 141 | .inheritIO() 142 | .start() 143 | .waitFor(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /scripts/netcore/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | DIR=$(dirname $0) 6 | 7 | dotnet build --configuration Release 8 | cp $DIR/bin/Release/netcoreapp3.1/init.dll $DIR/../init.dotnet.dll -------------------------------------------------------------------------------- /scripts/netcore/init.cs: -------------------------------------------------------------------------------- 1 | // === TUNSHELL C# SCRIPT === 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Runtime.InteropServices; 5 | using System.Net; 6 | using System.IO; 7 | using System.Threading.Tasks; 8 | using System.Diagnostics; 9 | using System.Security.AccessControl; 10 | 11 | namespace Tunshell 12 | { 13 | public static class Init 14 | { 15 | private readonly static Dictionary> Targets = new Dictionary>() { 16 | { 17 | OSPlatform.Windows, 18 | new Dictionary{ 19 | {Architecture.X64, "x86_64-pc-windows-msvc.exe"}, 20 | {Architecture.X86, "i686-pc-windows-msvc.exe"}, 21 | } 22 | }, 23 | { 24 | OSPlatform.OSX, 25 | new Dictionary{ 26 | {Architecture.X64, "x86_64-apple-darwin"}, 27 | {Architecture.Arm64, "aarch64-apple-darwin"}, 28 | } 29 | }, 30 | { 31 | OSPlatform.Linux, 32 | new Dictionary{ 33 | {Architecture.X64, "x86_64-unknown-linux-musl"}, 34 | {Architecture.X86, "i686-unknown-linux-musl"}, 35 | {Architecture.Arm, "armv7-unknown-linux-musleabihf"}, 36 | {Architecture.Arm64, "aarch64-unknown-linux-musl"}, 37 | } 38 | }, 39 | }; 40 | 41 | static void Main(string[] args) 42 | { 43 | Console.WriteLine("Installing client..."); 44 | Run(args).Wait(); 45 | } 46 | 47 | private async static Task Run(string[] args) 48 | { 49 | var clientPath = Path.GetTempFileName(); 50 | var webClient = new WebClient(); 51 | 52 | await webClient.DownloadFileTaskAsync($"https://artifacts.tunshell.com/client-{GetTarget()}", clientPath); 53 | 54 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 55 | { 56 | Process.Start(new ProcessStartInfo() 57 | { 58 | FileName = "chmod", 59 | Arguments = "+x " + clientPath 60 | }).WaitForExit(); 61 | } 62 | 63 | var process = Process.Start(new ProcessStartInfo() 64 | { 65 | FileName = clientPath, 66 | Arguments = string.Join(" ", args), 67 | UseShellExecute = false, 68 | RedirectStandardError = true, 69 | RedirectStandardOutput = true 70 | }); 71 | 72 | process.OutputDataReceived += (sender, e) => 73 | { 74 | Console.WriteLine(e.Data); 75 | }; 76 | 77 | process.ErrorDataReceived += (sender, e) => 78 | { 79 | Console.Error.WriteLine(e.Data); 80 | }; 81 | 82 | process.BeginOutputReadLine(); 83 | process.BeginErrorReadLine(); 84 | process.WaitForExit(); 85 | 86 | File.Delete(clientPath); 87 | } 88 | 89 | private static string GetTarget() 90 | { 91 | Dictionary targets = null; 92 | 93 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 94 | { 95 | targets = Targets[OSPlatform.Windows]; 96 | } 97 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 98 | { 99 | targets = Targets[OSPlatform.OSX]; 100 | } 101 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 102 | { 103 | targets = Targets[OSPlatform.Linux]; 104 | } 105 | else 106 | { 107 | throw new Exception($"Unsupported platform"); 108 | } 109 | 110 | var cpu = RuntimeInformation.ProcessArchitecture; 111 | 112 | if (!targets.ContainsKey(cpu)) 113 | { 114 | throw new Exception($"Unsupported CPU architecture: {cpu.ToString()}"); 115 | } 116 | 117 | return targets[cpu]; 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /scripts/netcore/init.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tunshell-client/.cargo/config: -------------------------------------------------------------------------------- 1 | [target.armv7-unknown-linux-musleabihf] 2 | linker = "/tmp/musl-test/musl/bin/armv7-unknown-linux-musleabihf-gcc" 3 | -------------------------------------------------------------------------------- /tunshell-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tunshell-client" 3 | version = "0.1.0" 4 | authors = ["Elliot Levin "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | tunshell-shared = { path = "../tunshell-shared" } 12 | anyhow = "1.0.31" 13 | tokio-util = { version = "0.3.1", features=["compat"] } 14 | futures = "0.3.5" 15 | log = "0.4.8" 16 | env_logger = "0.7.1" 17 | async-trait = "0.1.33" 18 | twox-hash = "1.5.0" 19 | byteorder = "1.3.4" 20 | thiserror = "1.0.19" 21 | rand = "0.7.3" 22 | serde = "1.0.114" 23 | serde_json = "1.0.55" 24 | cfg-if = "0.1.10" 25 | 26 | [target.'cfg(all(not(target_os = "ios"), not(target_os = "android"), not(target_arch = "wasm32")))'.dependencies] 27 | portable-pty = "0.3.1" 28 | 29 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 30 | tokio = { version = "0.2.21", features=["rt-threaded", "blocking", "dns", "time", "io-util", "io-std", "tcp", "udp", "sync", "process", "macros", "signal", "fs", "uds"] } #no-wasm 31 | crossterm = { version = "0.23.2" } 32 | libc = "0.2.71" 33 | async-tungstenite = { version = "0.8.0", features=["tokio-runtime"] } #no-wasm 34 | 35 | [target.'cfg(not(openssl))'.dependencies] 36 | ring = "0.16.15" 37 | webpki = "0.21.2" 38 | webpki-roots = "0.19.0" 39 | tokio-rustls = { version = "0.13.1", features=["dangerous_configuration"] } 40 | 41 | [target.'cfg(openssl)'.dependencies] 42 | native-tls = "0.2.4" 43 | tokio-native-tls = { version = "0.1.0" } 44 | openssl = { version = "0.10.30", features=["vendored"] } 45 | 46 | [target.'cfg(target_arch = "wasm32")'.dependencies] 47 | tokio = { version = "0.2.21", features=["blocking", "time", "io-util", "sync", "macros"] } 48 | wee_alloc = "0.4.5" 49 | wasm-bindgen = "0.2.65" 50 | wasm-bindgen-futures = "0.4.15" 51 | console_error_panic_hook = "0.1.6" 52 | console_log = "0.2.0" 53 | js-sys = "0.3.44" 54 | getrandom = { version = "0.2.6", features=["js"] } 55 | 56 | [target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] 57 | version = "0.3.42" 58 | features = [ 59 | "console", 60 | "Crypto", 61 | "SubtleCrypto", 62 | "CryptoKey", 63 | "Pbkdf2Params", 64 | "AesDerivedKeyParams", 65 | "AesGcmParams", 66 | "BinaryType", 67 | "Blob", 68 | "ErrorEvent", 69 | "FileReader", 70 | "MessageEvent", 71 | "ProgressEvent", 72 | "WebSocket", 73 | ] 74 | 75 | [target.'cfg(unix)'.dependencies] 76 | remote-pty-common = { git = "https://github.com/TimeToogo/remote-pty" } 77 | remote-pty-master = { git = "https://github.com/TimeToogo/remote-pty" } 78 | 79 | [dev-dependencies] 80 | lazy_static = "1.4.0" 81 | -------------------------------------------------------------------------------- /tunshell-client/build/compile-wasm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SCRIPT_DIR=$(dirname "$0") 6 | SCRIPT_DIR=`cd $SCRIPT_DIR;pwd` 7 | 8 | cd $SCRIPT_DIR/../ 9 | 10 | 11 | # Ignore conflicting dependencies which require different 12 | # feature flags based on target. Can be removed once 13 | # https://github.com/rust-lang/cargo/issues/1197 is resolved 14 | cp Cargo.toml Cargo.toml.orig 15 | trap "mv $PWD/Cargo.toml.orig $PWD/Cargo.toml" EXIT 16 | cat Cargo.toml | sed 's/.*\#no-wasm/\#/g' > Cargo.toml.new 17 | mv Cargo.toml.new Cargo.toml 18 | 19 | wasm-pack build 20 | mkdir -p ../website/services/wasm/ 21 | cp -aR pkg/* ../website/services/wasm/ -------------------------------------------------------------------------------- /tunshell-client/build/compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | TARGET=$1 6 | OUTPUT_PATH=$2 7 | 8 | if [[ -z "$TARGET" ]] || [[ -z "$OUTPUT_PATH" ]]; then 9 | echo "usage: compile.sh [target] [output path]" 10 | exit 1 11 | fi 12 | 13 | if [[ -f $HOME/.cargo/env ]]; then 14 | source $HOME/.cargo/env 15 | fi 16 | 17 | 18 | SCRIPT_DIR=$(dirname "$0") 19 | SCRIPT_DIR=`cd $SCRIPT_DIR;pwd` 20 | 21 | mkdir -p $(dirname $OUTPUT_PATH) 22 | 23 | echo "Compiling tunshell-client for $TARGET..." 24 | cd $SCRIPT_DIR/../../ 25 | rustup target add $TARGET 26 | 27 | if [[ ! -z "$RUN_TESTS" ]]; 28 | then 29 | cross test -p tunshell-client --target $TARGET 30 | fi 31 | 32 | cross build -p tunshell-client --release --target $TARGET 33 | cp $SCRIPT_DIR/../../target/$TARGET/release/client $OUTPUT_PATH 34 | 35 | if [[ $TARGET =~ "linux" ]] || [[ $TARGET =~ "freebsd" ]]; 36 | then 37 | case $TARGET in 38 | x86_64-unknown-linux-musl|i686-unknown-linux-musl|i586-unknown-linux-musl) 39 | STRIP="strip" 40 | ;; 41 | aarch64-unknown-linux-musl) 42 | STRIP="aarch64-linux-musl-strip" 43 | ;; 44 | arm-unknown-linux-musleabi) 45 | STRIP="arm-linux-musleabi-strip" 46 | ;; 47 | armv7-unknown-linux-musleabihf) 48 | STRIP="arm-linux-musleabihf-strip" 49 | ;; 50 | arm-linux-androideabi) 51 | STRIP="arm-linux-androideabi-strip" 52 | ;; 53 | aarch64-linux-android) 54 | STRIP="aarch64-linux-android-strip" 55 | ;; 56 | mips-unknown-linux-musl) 57 | STRIP="mips-linux-muslsf-strip" 58 | ;; 59 | x86_64-unknown-freebsd) 60 | STRIP="x86_64-unknown-freebsd12-strip" 61 | ;; 62 | *) 63 | echo "Unknown linux target: $TARGET" 64 | exit 1 65 | ;; 66 | esac 67 | 68 | echo "Stripping binary using cross docker image ($STRIP)..." 69 | docker run --rm -v$(dirname $OUTPUT_PATH):/app/ rustembedded/cross:$TARGET $STRIP /app/$(basename $OUTPUT_PATH) 70 | elif [[ -x "$(command -v strip)" && $TARGET =~ "apple" ]]; 71 | then 72 | echo "Stripping binary..." 73 | strip $OUTPUT_PATH 74 | fi 75 | 76 | echo "Binary saved to: $OUTPUT_PATH" -------------------------------------------------------------------------------- /tunshell-client/build/install-deps-wasm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y 6 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -------------------------------------------------------------------------------- /tunshell-client/build/install-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | TEMPDIR=${TEMPDIR:="$(dirname $0)/tmp"} 6 | cd $TEMPDIR 7 | 8 | SUDO="sudo" 9 | 10 | if [[ ! -x "$(command -v sudo)" ]]; then 11 | SUDO="" 12 | fi 13 | 14 | echo "Installing compile toolchain..." 15 | case "$OSTYPE" in 16 | msys*) 17 | choco install rust-ms 18 | echo '%USERPROFILE%\.cargo\bin' >> $GITHUB_PATH 19 | ;; 20 | 21 | darwin*) 22 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y 23 | ;; 24 | 25 | FreeBSD*) 26 | $SUDO pkg update 27 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y 28 | source $HOME/.cargo/env 29 | ;; 30 | 31 | *) 32 | $SUDO apt update -y 33 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y 34 | ;; 35 | esac 36 | 37 | echo "Installing cross..." 38 | cargo install cross 39 | 40 | -------------------------------------------------------------------------------- /tunshell-client/docker/prod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:alpine AS build 2 | 3 | ARG RUN_TESTS 4 | 5 | ENV RUSTFLAGS="--cfg alpine" 6 | 7 | RUN apk add --no-cache musl-dev 8 | RUN mkdir /app/ 9 | 10 | COPY . /app/ 11 | 12 | WORKDIR /app/tunshell-client 13 | 14 | RUN ([ -n "${RUN_TESTS}" ] && cargo test) || true 15 | RUN cargo build --release 16 | 17 | FROM alpine:latest 18 | RUN mkdir /app/ 19 | 20 | COPY --from=build /app/target/release/client /app/client 21 | RUN chmod +x /app/client 22 | 23 | WORKDIR /app 24 | 25 | ENTRYPOINT [ "/app/client" ] -------------------------------------------------------------------------------- /tunshell-client/src/bin/client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use env_logger; 3 | use log::error; 4 | use std::{process::exit, sync::atomic::Ordering}; 5 | use tokio::signal; 6 | use tunshell_client::{Client, Config, HostShell, STOP_ON_SIGINT}; 7 | 8 | #[tokio::main] 9 | async fn main() -> () { 10 | env_logger::init(); 11 | 12 | let config = Config::new_from_env(); 13 | 14 | let mut client = Client::new(config, HostShell::new().unwrap()); 15 | let mut session = tokio::task::spawn(async move { client.start_session().await }); 16 | 17 | let result = loop { 18 | tokio::select! { 19 | result = &mut session => break result.unwrap_or_else(|_| Err(Error::msg("failed to join task"))), 20 | _ = signal::ctrl_c() => { 21 | if STOP_ON_SIGINT.load(Ordering::SeqCst) { 22 | break Err(Error::msg("interrupt received, terminating")); 23 | } 24 | } 25 | }; 26 | }; 27 | 28 | match result { 29 | Ok(code) => exit(code as i32), 30 | Err(err) => { 31 | error!("Error occurred: {:?}", err); 32 | exit(1) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tunshell-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | 3 | pub use client::*; 4 | 5 | mod config; 6 | pub use config::*; 7 | 8 | mod server; 9 | pub use server::*; 10 | 11 | #[cfg(not(target_arch = "wasm32"))] 12 | mod p2p; 13 | 14 | mod shell; 15 | pub use shell::*; 16 | 17 | mod stream; 18 | pub use stream::*; 19 | 20 | pub mod util; 21 | 22 | cfg_if::cfg_if! { 23 | if #[cfg(target_arch = "wasm32")] { 24 | mod wasm; 25 | pub use wasm::*; 26 | } 27 | } 28 | 29 | cfg_if::cfg_if! { 30 | if #[cfg(not(target_arch = "wasm32"))] { 31 | use std::sync::atomic::AtomicBool; 32 | pub static STOP_ON_SIGINT: AtomicBool = AtomicBool::new(true); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tunshell-client/src/p2p/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::TunnelStream; 2 | use anyhow::Result; 3 | use async_trait::async_trait; 4 | use tunshell_shared::{PeerJoinedPayload}; 5 | 6 | pub mod tcp; 7 | pub mod udp; 8 | pub mod udp_adaptor; 9 | 10 | #[async_trait] 11 | pub trait P2PConnection: TunnelStream { 12 | fn new(peer_info: PeerJoinedPayload) -> Self 13 | where 14 | Self: Sized; 15 | 16 | async fn bind(&mut self) -> Result; 17 | 18 | async fn connect(&mut self, peer_port: u16, master_side: bool) -> Result<()>; 19 | } 20 | -------------------------------------------------------------------------------- /tunshell-client/src/p2p/udp/config.rs: -------------------------------------------------------------------------------- 1 | use super::MAX_PACKET_SIZE; 2 | use std::net::SocketAddr; 3 | use std::time::Duration; 4 | 5 | const DEFAULT_CONNECT_TIMEOUT: u64 = 5000; // ms 6 | const DEFAULT_KEEP_ALIVE_INTERVAL: u64 = 15000; // ms 7 | const DEFAULT_INITIAL_TRANSIT_WINDOW: u32 = 102400; // bytes 8 | const DEFAULT_RECV_WINDOW: u32 = 102400; // bytes 9 | const DEFAULT_PACKET_RESEND_LIMIT: u8 = 10; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct UdpConnectionConfig { 13 | /// How long to allow for the connection to be negotiated 14 | connect_timeout: Duration, 15 | 16 | /// The address on which to bind on 17 | bind_addr: SocketAddr, 18 | 19 | /// How often to send a keep-alive packet 20 | keep_alive_interval: Duration, 21 | 22 | /// Duration to wait for a packet before assuming the connection has dropped. 23 | recv_timeout: Duration, 24 | 25 | /// The initial amount of bytes permitted to be unacknowledged at the start of the connection. 26 | initial_transit_window: u32, 27 | 28 | /// The amount of bytes permitted in the reassembled byte buffer 29 | recv_window: u32, 30 | 31 | /// The amount of resend the connection will tolerate for a single packet 32 | packet_resend_limit: u8, 33 | } 34 | 35 | #[allow(dead_code)] 36 | impl UdpConnectionConfig { 37 | pub fn default() -> Self { 38 | Self { 39 | connect_timeout: Duration::from_millis(DEFAULT_CONNECT_TIMEOUT), 40 | bind_addr: SocketAddr::from(([0, 0, 0, 0], 0)), 41 | keep_alive_interval: Duration::from_millis(DEFAULT_KEEP_ALIVE_INTERVAL), 42 | recv_timeout: Duration::from_millis(DEFAULT_KEEP_ALIVE_INTERVAL * 2), 43 | initial_transit_window: DEFAULT_INITIAL_TRANSIT_WINDOW, 44 | recv_window: DEFAULT_RECV_WINDOW, 45 | packet_resend_limit: DEFAULT_PACKET_RESEND_LIMIT, 46 | } 47 | } 48 | 49 | pub fn connect_timeout(&self) -> Duration { 50 | self.connect_timeout 51 | } 52 | 53 | pub fn with_connect_timeout(mut self, value: Duration) -> Self { 54 | self.connect_timeout = value; 55 | 56 | self 57 | } 58 | 59 | pub fn bind_addr(&self) -> SocketAddr { 60 | self.bind_addr 61 | } 62 | 63 | pub fn with_bind_addr(mut self, value: SocketAddr) -> Self { 64 | self.bind_addr = value; 65 | 66 | self 67 | } 68 | 69 | pub fn keep_alive_interval(&self) -> Duration { 70 | self.keep_alive_interval 71 | } 72 | 73 | pub fn with_keep_alive_interval(mut self, value: Duration) -> Self { 74 | self.keep_alive_interval = value; 75 | 76 | self 77 | } 78 | 79 | pub fn recv_timeout(&self) -> Duration { 80 | self.recv_timeout 81 | } 82 | 83 | pub fn with_recv_timeout(mut self, value: Duration) -> Self { 84 | self.recv_timeout = value; 85 | 86 | self 87 | } 88 | 89 | pub fn initial_transit_window(&self) -> u32 { 90 | self.initial_transit_window 91 | } 92 | 93 | pub fn with_initial_transit_window(mut self, value: u32) -> Self { 94 | self.initial_transit_window = value; 95 | 96 | self 97 | } 98 | 99 | pub fn recv_window(&self) -> u32 { 100 | self.recv_window 101 | } 102 | 103 | pub fn with_recv_window(mut self, value: u32) -> Self { 104 | assert!(value >= MAX_PACKET_SIZE as u32); 105 | self.recv_window = value; 106 | 107 | self 108 | } 109 | 110 | pub fn packet_resend_limit(&self) -> u8 { 111 | self.packet_resend_limit 112 | } 113 | 114 | pub fn with_packet_resend_limit(mut self, value: u8) -> Self { 115 | self.packet_resend_limit = value; 116 | 117 | self 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::*; 124 | 125 | #[test] 126 | fn test_default_config() { 127 | let config = UdpConnectionConfig::default(); 128 | 129 | assert_eq!( 130 | config.connect_timeout.as_millis() as u64, 131 | DEFAULT_CONNECT_TIMEOUT 132 | ); 133 | assert_eq!(config.bind_addr, SocketAddr::from(([0, 0, 0, 0], 0))); 134 | assert_eq!( 135 | config.keep_alive_interval.as_millis() as u64, 136 | DEFAULT_KEEP_ALIVE_INTERVAL 137 | ); 138 | assert_eq!( 139 | config.recv_timeout.as_millis() as u64, 140 | DEFAULT_KEEP_ALIVE_INTERVAL * 2 141 | ); 142 | assert_eq!( 143 | config.initial_transit_window, 144 | DEFAULT_INITIAL_TRANSIT_WINDOW 145 | ); 146 | assert_eq!(config.recv_window, DEFAULT_RECV_WINDOW); 147 | assert_eq!(config.packet_resend_limit, DEFAULT_PACKET_RESEND_LIMIT); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tunshell-client/src/p2p/udp/mod.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod congestion; 3 | mod connection; 4 | mod negotiator; 5 | mod orchestrator; 6 | mod packet; 7 | mod receiver; 8 | mod sender; 9 | mod resender; 10 | mod rtt_estimator; 11 | mod state; 12 | mod seq_number; 13 | 14 | use congestion::*; 15 | use negotiator::*; 16 | use orchestrator::*; 17 | use packet::*; 18 | use sender::*; 19 | use resender::*; 20 | use state::*; 21 | use seq_number::*; 22 | 23 | pub use config::*; 24 | pub use connection::*; 25 | -------------------------------------------------------------------------------- /tunshell-client/src/p2p/udp/rtt_estimator.rs: -------------------------------------------------------------------------------- 1 | use super::{UdpConnectionVars, UdpPacket}; 2 | use std::time::{Duration, Instant}; 3 | 4 | impl UdpConnectionVars { 5 | pub(super) fn store_send_time_of_packet(&mut self, sent_packet: &UdpPacket) { 6 | let corresponding_ack_number = sent_packet.end_sequence_number(); 7 | self.send_times 8 | .insert(corresponding_ack_number, Instant::now()); 9 | } 10 | 11 | pub(super) fn adjust_rtt_estimate(&mut self, recv_packet: &UdpPacket) { 12 | let sent_at = match self.send_times.get(&recv_packet.ack_number) { 13 | Some(sent_at) => sent_at, 14 | None => return, 15 | }; 16 | 17 | let rtt_measurement = Instant::now().duration_since(*sent_at); 18 | 19 | // The rtt estimate is kept by using a moving average. 20 | // This is calculated using the average between the previous value and the new measurement. 21 | if self.rtt_estimate.as_nanos() == 0 { 22 | self.rtt_estimate = rtt_measurement; 23 | } else { 24 | self.rtt_estimate = Duration::from_nanos( 25 | ((self.rtt_estimate.as_nanos() + rtt_measurement.as_nanos()) / 2) as u64, 26 | ); 27 | } 28 | 29 | // We also clear any sent times from sent packets which may have 30 | // not been acknowledged individually. 31 | self.send_times.retain(|k, _| *k > recv_packet.ack_number) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::super::{SequenceNumber, UdpConnectionConfig}; 38 | use super::*; 39 | 40 | #[test] 41 | fn test_store_send_time_of_packet() { 42 | let mut con = UdpConnectionVars::new(UdpConnectionConfig::default()); 43 | 44 | con.store_send_time_of_packet(&UdpPacket::data( 45 | SequenceNumber(50), 46 | SequenceNumber(0), 47 | 0, 48 | &[1], 49 | )); 50 | 51 | assert_eq!(con.send_times.get(&SequenceNumber(51)).is_some(), true); 52 | } 53 | 54 | #[test] 55 | fn test_adjust_rtt_estimate_with_packet_not_in_sent_time() { 56 | let mut con = UdpConnectionVars::new(UdpConnectionConfig::default()); 57 | 58 | con.rtt_estimate = Duration::from_millis(100); 59 | 60 | con.adjust_rtt_estimate(&UdpPacket::data( 61 | SequenceNumber(50), 62 | SequenceNumber(10), 63 | 0, 64 | &[], 65 | )); 66 | 67 | assert_eq!(con.rtt_estimate, Duration::from_millis(100)); 68 | } 69 | 70 | #[test] 71 | fn test_adjust_rtt_estimate_with_sent_packet() { 72 | let mut con = UdpConnectionVars::new(UdpConnectionConfig::default()); 73 | 74 | con.rtt_estimate = Duration::from_millis(0); 75 | 76 | con.send_times.insert( 77 | SequenceNumber(100), 78 | Instant::now() - Duration::from_millis(100), 79 | ); 80 | 81 | con.adjust_rtt_estimate(&UdpPacket::data( 82 | SequenceNumber(50), 83 | SequenceNumber(100), 84 | 0, 85 | &[], 86 | )); 87 | 88 | assert_eq!((con.rtt_estimate.as_millis() as i32 - 100).abs() < 5, true); 89 | } 90 | 91 | #[test] 92 | fn test_adjust_rtt_estimate_with_sent_packet_average() { 93 | let mut con = UdpConnectionVars::new(UdpConnectionConfig::default()); 94 | 95 | con.rtt_estimate = Duration::from_millis(150); 96 | 97 | con.send_times.insert( 98 | SequenceNumber(100), 99 | Instant::now() - Duration::from_millis(100), 100 | ); 101 | 102 | con.adjust_rtt_estimate(&UdpPacket::data( 103 | SequenceNumber(50), 104 | SequenceNumber(100), 105 | 0, 106 | &[], 107 | )); 108 | 109 | assert_eq!((con.rtt_estimate.as_millis() as i32 - 125).abs() < 5, true); 110 | } 111 | 112 | #[test] 113 | fn test_adjust_rtt_estimate_removes_acknowledged_sent_times() { 114 | let mut con = UdpConnectionVars::new(UdpConnectionConfig::default()); 115 | 116 | con.rtt_estimate = Duration::from_millis(150); 117 | 118 | con.send_times.insert(SequenceNumber(50), Instant::now()); 119 | con.send_times.insert(SequenceNumber(100), Instant::now()); 120 | con.send_times.insert(SequenceNumber(150), Instant::now()); 121 | 122 | con.adjust_rtt_estimate(&UdpPacket::data( 123 | SequenceNumber(50), 124 | SequenceNumber(100), 125 | 0, 126 | &[], 127 | )); 128 | 129 | assert_eq!( 130 | con.send_times 131 | .keys() 132 | .copied() 133 | .collect::>(), 134 | vec![SequenceNumber(150)] 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tunshell-client/src/p2p/udp_adaptor.rs: -------------------------------------------------------------------------------- 1 | use super::udp::{UdpConnectParams, UdpConnection, UdpConnectionConfig}; 2 | use crate::p2p::P2PConnection; 3 | use crate::TunnelStream; 4 | use anyhow::Result; 5 | use async_trait::async_trait; 6 | use std::net::{IpAddr, SocketAddr}; 7 | use std::pin::Pin; 8 | use std::task::{Context, Poll}; 9 | use tokio::io::{AsyncRead, AsyncWrite}; 10 | use tunshell_shared::PeerJoinedPayload; 11 | 12 | pub struct UdpConnectionAdaptor { 13 | peer_info: PeerJoinedPayload, 14 | con: UdpConnection, 15 | } 16 | 17 | impl AsyncRead for UdpConnectionAdaptor { 18 | fn poll_read( 19 | mut self: Pin<&mut Self>, 20 | cx: &mut Context<'_>, 21 | buff: &mut [u8], 22 | ) -> Poll> { 23 | Pin::new(&mut self.con).poll_read(cx, buff) 24 | } 25 | } 26 | 27 | impl AsyncWrite for UdpConnectionAdaptor { 28 | fn poll_write( 29 | mut self: Pin<&mut Self>, 30 | cx: &mut Context<'_>, 31 | buff: &[u8], 32 | ) -> Poll> { 33 | Pin::new(&mut self.con).poll_write(cx, buff) 34 | } 35 | 36 | fn poll_flush( 37 | mut self: Pin<&mut Self>, 38 | cx: &mut Context<'_>, 39 | ) -> Poll> { 40 | Pin::new(&mut self.con).poll_flush(cx) 41 | } 42 | 43 | fn poll_shutdown( 44 | mut self: Pin<&mut Self>, 45 | cx: &mut Context<'_>, 46 | ) -> Poll> { 47 | Pin::new(&mut self.con).poll_shutdown(cx) 48 | } 49 | } 50 | 51 | impl TunnelStream for UdpConnectionAdaptor {} 52 | 53 | #[async_trait] 54 | impl P2PConnection for UdpConnectionAdaptor { 55 | fn new(peer_info: PeerJoinedPayload) -> Self { 56 | let config = 57 | UdpConnectionConfig::default().with_bind_addr(SocketAddr::from(([0, 0, 0, 0], 0))); 58 | 59 | Self { 60 | peer_info, 61 | con: UdpConnection::new(config), 62 | } 63 | } 64 | 65 | async fn bind(&mut self) -> Result { 66 | self.con.bind().await 67 | } 68 | 69 | async fn connect(&mut self, peer_port: u16, master_side: bool) -> Result<()> { 70 | let connect_future = self.con.connect(UdpConnectParams { 71 | ip: self.peer_info.peer_ip_address.parse::().unwrap(), 72 | suggested_port: peer_port, 73 | master_side, 74 | }); 75 | 76 | connect_future.await 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use super::*; 83 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 84 | use tokio::{time::delay_for, runtime::Runtime}; 85 | use std::time::Duration; 86 | 87 | #[test] 88 | fn test_connect_simultaneous_open() { 89 | Runtime::new().unwrap().block_on(async { 90 | let peer_info = PeerJoinedPayload { 91 | peer_ip_address: "127.0.0.1".to_owned(), 92 | peer_key: "test".to_owned(), 93 | session_nonce: "nonce".to_owned(), 94 | }; 95 | 96 | let mut connection1 = UdpConnectionAdaptor::new(peer_info.clone()); 97 | 98 | let mut connection2 = UdpConnectionAdaptor::new(peer_info.clone()); 99 | 100 | let port1 = connection1.bind().await.expect("failed to bind"); 101 | let port2 = connection2.bind().await.expect("failed to bind"); 102 | 103 | futures::try_join!( 104 | connection1.connect(port2, true), 105 | connection2.connect(port1, false) 106 | ) 107 | .expect("failed to connect"); 108 | 109 | connection1.write("hello from 1".as_bytes()).await.unwrap(); 110 | connection1.flush().await.unwrap(); 111 | 112 | delay_for(Duration::from_millis(50)).await; 113 | 114 | let mut buff = [0; 1024]; 115 | let read = connection2.read(&mut buff).await.unwrap(); 116 | 117 | assert_eq!( 118 | String::from_utf8(buff[..read].to_vec()).unwrap(), 119 | "hello from 1" 120 | ); 121 | 122 | connection2.write("hello from 2".as_bytes()).await.unwrap(); 123 | connection2.flush().await.unwrap(); 124 | 125 | delay_for(Duration::from_millis(50)).await; 126 | let mut buff = [0; 1024]; 127 | let read = connection1.read(&mut buff).await.unwrap(); 128 | 129 | assert_eq!( 130 | String::from_utf8(buff[..read].to_vec()).unwrap(), 131 | "hello from 2" 132 | ); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tunshell-client/src/server/dual_stream.rs: -------------------------------------------------------------------------------- 1 | use super::tls_stream::TlsServerStream; 2 | use super::websocket_stream::WebsocketServerStream; 3 | use crate::Config; 4 | use anyhow::Result; 5 | use log::*; 6 | use std::{ 7 | io, 8 | pin::Pin, 9 | task::{Context, Poll}, 10 | }; 11 | use tokio::{ 12 | io::{AsyncRead, AsyncWrite}, 13 | time::timeout, 14 | }; 15 | 16 | pub struct ServerStream { 17 | inner: Box, 18 | } 19 | 20 | impl ServerStream { 21 | pub async fn connect(config: &Config) -> Result { 22 | let connection = timeout( 23 | config.server_connection_timeout(), 24 | TlsServerStream::connect(config, config.relay_tls_port()), 25 | ) 26 | .await; 27 | 28 | let connection: Box = match connection { 29 | Ok(Ok(con)) => Box::new(con), 30 | err @ _ => { 31 | error!( 32 | "Failed to connect via TLS, falling back to websocket: {}", 33 | match err { 34 | Err(err) => err.to_string(), 35 | Ok(Err(err)) => err.to_string(), 36 | _ => unreachable!(), 37 | } 38 | ); 39 | 40 | let ws = timeout( 41 | config.server_connection_timeout(), 42 | WebsocketServerStream::connect(config), 43 | ) 44 | .await??; 45 | 46 | Box::new(ws) 47 | } 48 | }; 49 | 50 | Ok(Self { inner: connection }) 51 | } 52 | } 53 | 54 | impl AsyncRead for ServerStream { 55 | fn poll_read( 56 | mut self: Pin<&mut Self>, 57 | cx: &mut Context<'_>, 58 | buf: &mut [u8], 59 | ) -> Poll> { 60 | Pin::new(&mut self.inner).poll_read(cx, buf) 61 | } 62 | } 63 | 64 | impl AsyncWrite for ServerStream { 65 | fn poll_write( 66 | mut self: Pin<&mut Self>, 67 | cx: &mut Context<'_>, 68 | buf: &[u8], 69 | ) -> std::task::Poll> { 70 | Pin::new(&mut self.inner).poll_write(cx, buf) 71 | } 72 | 73 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 74 | Pin::new(&mut self.inner).poll_flush(cx) 75 | } 76 | 77 | fn poll_shutdown( 78 | mut self: Pin<&mut Self>, 79 | cx: &mut Context<'_>, 80 | ) -> Poll> { 81 | Pin::new(&mut self.inner).poll_shutdown(cx) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tunshell-client/src/server/mod.rs: -------------------------------------------------------------------------------- 1 | cfg_if::cfg_if! { 2 | if #[cfg(not(target_arch = "wasm32"))] { 3 | use tokio::io::{AsyncRead, AsyncWrite}; 4 | 5 | pub trait AsyncIO : AsyncRead + AsyncWrite + Send + Unpin {} 6 | 7 | pub mod tcp_stream; 8 | 9 | cfg_if::cfg_if! { 10 | if #[cfg(openssl)] { 11 | mod openssl_tls_stream; 12 | pub mod tls_stream { 13 | pub use super::openssl_tls_stream::*; 14 | } 15 | } else { 16 | mod ring_tls_stream; 17 | pub mod tls_stream { 18 | pub use super::ring_tls_stream::*; 19 | } 20 | } 21 | } 22 | 23 | pub mod websocket_stream; 24 | 25 | mod dual_stream; 26 | pub use dual_stream::*; 27 | } else { 28 | mod websys_websocket_stream; 29 | pub use websys_websocket_stream::*; 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | use crate::{ClientMode, Config}; 37 | use tokio::runtime::Runtime; 38 | 39 | #[test] 40 | fn test_connect_to_relay_server() { 41 | let config = Config::new( 42 | ClientMode::Target, 43 | "test", 44 | "au.relay.tunshell.com", 45 | 5000, 46 | 443, 47 | "test", 48 | true, 49 | false 50 | ); 51 | 52 | let result = Runtime::new() 53 | .unwrap() 54 | .block_on(ServerStream::connect(&config)); 55 | 56 | result.unwrap(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tunshell-client/src/server/openssl_tls_stream.rs: -------------------------------------------------------------------------------- 1 | use super::tcp_stream::TcpServerStream; 2 | use crate::Config; 3 | use anyhow::Result; 4 | use std::{ 5 | io, 6 | pin::Pin, 7 | task::{Context, Poll}, 8 | }; 9 | use tokio::{ 10 | io::{AsyncRead, AsyncWrite}, 11 | net::TcpStream, 12 | }; 13 | use tokio_native_tls::{TlsConnector, TlsStream}; 14 | 15 | pub struct TlsServerStream { 16 | inner: TlsStream, 17 | } 18 | 19 | impl TlsServerStream { 20 | pub async fn connect(config: &Config, port: u16) -> Result { 21 | let connector = TlsConnector::from(native_tls::TlsConnector::builder().build()?); 22 | 23 | let tcp_stream = TcpServerStream::connect(config, port).await?.inner(); 24 | 25 | let transport_stream = connector.connect(config.relay_host(), tcp_stream).await?; 26 | 27 | Ok(Self { 28 | inner: transport_stream, 29 | }) 30 | } 31 | } 32 | 33 | impl AsyncRead for TlsServerStream { 34 | fn poll_read( 35 | mut self: Pin<&mut Self>, 36 | cx: &mut Context<'_>, 37 | buf: &mut [u8], 38 | ) -> Poll> { 39 | Pin::new(&mut self.inner).poll_read(cx, buf) 40 | } 41 | } 42 | 43 | impl AsyncWrite for TlsServerStream { 44 | fn poll_write( 45 | mut self: Pin<&mut Self>, 46 | cx: &mut Context<'_>, 47 | buf: &[u8], 48 | ) -> std::task::Poll> { 49 | Pin::new(&mut self.inner).poll_write(cx, buf) 50 | } 51 | 52 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 53 | Pin::new(&mut self.inner).poll_flush(cx) 54 | } 55 | 56 | fn poll_shutdown( 57 | mut self: Pin<&mut Self>, 58 | cx: &mut Context<'_>, 59 | ) -> Poll> { 60 | Pin::new(&mut self.inner).poll_shutdown(cx) 61 | } 62 | } 63 | 64 | impl super::AsyncIO for TlsServerStream {} 65 | -------------------------------------------------------------------------------- /tunshell-client/src/server/tcp_stream.rs: -------------------------------------------------------------------------------- 1 | use crate::Config; 2 | use anyhow::{bail, Context as AnyhowContext, Result}; 3 | use std::{time::Duration, net::ToSocketAddrs}; 4 | use tokio::{ 5 | io::{AsyncReadExt, AsyncWriteExt}, 6 | net::TcpStream, 7 | }; 8 | 9 | pub struct TcpServerStream { 10 | inner: TcpStream 11 | } 12 | 13 | impl TcpServerStream { 14 | pub fn inner(self) -> TcpStream { 15 | self.inner 16 | } 17 | 18 | pub async fn connect(config: &Config, port: u16) -> Result { 19 | let network_stream = if let Ok(http_proxy) = std::env::var("HTTP_PROXY") { 20 | log::info!("Connecting to relay server via http proxy {}", http_proxy); 21 | 22 | connect_via_http_proxy(config, port, http_proxy).await? 23 | } else { 24 | log::info!("Connecting to relay server over TCP"); 25 | let relay_addr = (config.relay_host(), port) 26 | .to_socket_addrs()? 27 | .next() 28 | .unwrap(); 29 | 30 | TcpStream::connect(relay_addr).await? 31 | }; 32 | 33 | if let Err(err) = network_stream.set_keepalive(Some(Duration::from_secs(30))) { 34 | log::warn!("failed to set tcp keepalive: {}", err); 35 | } 36 | 37 | Ok(Self { 38 | inner: network_stream, 39 | }) 40 | } 41 | } 42 | 43 | async fn connect_via_http_proxy( 44 | config: &Config, 45 | port: u16, 46 | http_proxy: String, 47 | ) -> Result { 48 | let proxy_addr = http_proxy.to_socket_addrs()?.next().unwrap(); 49 | let mut proxy_stream = TcpStream::connect(proxy_addr).await?; 50 | 51 | proxy_stream 52 | .write_all(format!("CONNECT {}:{} HTTP/1.1\n\n", config.relay_host(), port).as_bytes()) 53 | .await?; 54 | let mut read_buff = [0u8; 1024]; 55 | 56 | let read = match proxy_stream.read(&mut read_buff).await? { 57 | 0 => bail!("Failed to read response from http proxy"), 58 | read @ _ => read, 59 | }; 60 | 61 | let response = 62 | String::from_utf8(read_buff[..read].to_vec()).context("failed to parse proxy response")?; 63 | if !response.contains("HTTP/1.1 200") && !response.contains("HTTP/1.0 200") { 64 | bail!(format!( 65 | "invalid response returned from http proxy: {}", 66 | response 67 | )); 68 | } 69 | 70 | Ok(proxy_stream) 71 | } -------------------------------------------------------------------------------- /tunshell-client/src/server/websocket_stream.rs: -------------------------------------------------------------------------------- 1 | use super::tls_stream::TlsServerStream; 2 | use crate::Config; 3 | use anyhow::Result; 4 | use async_tungstenite::{client_async, tungstenite::Message, WebSocketStream}; 5 | use futures::{stream::StreamExt, SinkExt}; 6 | use log::*; 7 | use std::{ 8 | cmp, io, 9 | pin::Pin, 10 | task::{Context, Poll}, 11 | }; 12 | use tokio::io::{AsyncRead, AsyncWrite}; 13 | use tokio_util::compat::*; 14 | 15 | pub struct WebsocketServerStream { 16 | inner: WebSocketStream>, 17 | 18 | read_buff: Vec, 19 | write_buff: Option, 20 | } 21 | 22 | impl WebsocketServerStream { 23 | pub async fn connect(config: &Config) -> Result { 24 | let tls_stream = TlsServerStream::connect(config, config.relay_ws_port()).await?; 25 | 26 | let url = format!( 27 | "wss://{}:{}/ws", 28 | config.relay_host(), 29 | config.relay_ws_port() 30 | ); 31 | let (websocket_stream, _) = client_async(url, tls_stream.compat()).await?; 32 | 33 | Ok(Self { 34 | inner: websocket_stream, 35 | read_buff: vec![], 36 | write_buff: None, 37 | }) 38 | } 39 | } 40 | 41 | impl AsyncRead for WebsocketServerStream { 42 | fn poll_read( 43 | mut self: Pin<&mut Self>, 44 | cx: &mut Context<'_>, 45 | buf: &mut [u8], 46 | ) -> Poll> { 47 | while self.read_buff.is_empty() { 48 | let msg = match self.inner.poll_next_unpin(cx) { 49 | Poll::Ready(Some(Ok(msg))) => msg, 50 | Poll::Ready(Some(Err(err))) => { 51 | error!("error while reading from websocket: {}", err); 52 | return Poll::Ready(Err(io::Error::from(io::ErrorKind::BrokenPipe))); 53 | } 54 | Poll::Ready(None) => return Poll::Ready(Ok(0)), 55 | Poll::Pending => return Poll::Pending, 56 | }; 57 | 58 | if msg.is_binary() { 59 | self.read_buff.extend_from_slice(msg.into_data().as_slice()); 60 | } 61 | } 62 | 63 | let len = cmp::min(buf.len(), self.read_buff.len()); 64 | buf[..len].copy_from_slice(&self.read_buff[..len]); 65 | self.read_buff.drain(..len); 66 | 67 | Poll::Ready(Ok(len)) 68 | } 69 | } 70 | 71 | impl AsyncWrite for WebsocketServerStream { 72 | fn poll_write( 73 | mut self: Pin<&mut Self>, 74 | cx: &mut Context<'_>, 75 | buf: &[u8], 76 | ) -> std::task::Poll> { 77 | assert!(buf.len() > 0); 78 | 79 | let mut written = 0; 80 | let max_size = self 81 | .inner 82 | .get_config() 83 | .max_frame_size 84 | .unwrap_or(1024 * 1024); 85 | 86 | loop { 87 | if self.write_buff.is_none() { 88 | written = cmp::min(max_size, buf.len()); 89 | self.write_buff 90 | .replace(Message::binary(buf[..written].to_vec())); 91 | } 92 | 93 | if let Poll::Pending = self.inner.poll_ready_unpin(cx) { 94 | return Poll::Pending; 95 | } 96 | 97 | let msg = self.write_buff.take().unwrap(); 98 | if let Err(err) = self.inner.start_send_unpin(msg) { 99 | error!("error while writing to websocket: {}", err); 100 | return Poll::Ready(Err(io::Error::from(io::ErrorKind::BrokenPipe))); 101 | } 102 | 103 | if written > 0 { 104 | return Poll::Ready(Ok(written)); 105 | } 106 | } 107 | } 108 | 109 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 110 | if self.write_buff.is_none() { 111 | return Poll::Ready(Ok(())); 112 | } 113 | 114 | if let Poll::Pending = self.inner.poll_ready_unpin(cx) { 115 | return Poll::Pending; 116 | } 117 | 118 | let msg = self.write_buff.take().unwrap(); 119 | if let Err(err) = self.inner.start_send_unpin(msg) { 120 | error!("error while writing to websocket: {}", err); 121 | return Poll::Ready(Err(io::Error::from(io::ErrorKind::BrokenPipe))); 122 | } 123 | 124 | if let Poll::Pending = self.inner.poll_flush_unpin(cx) { 125 | return Poll::Pending; 126 | } 127 | 128 | return Poll::Ready(Ok(())); 129 | } 130 | 131 | fn poll_shutdown( 132 | mut self: Pin<&mut Self>, 133 | cx: &mut Context<'_>, 134 | ) -> Poll> { 135 | self.inner.poll_close_unpin(cx).map_err(|err| { 136 | error!("error while closing to websocket: {}", err); 137 | io::Error::from(io::ErrorKind::BrokenPipe) 138 | }) 139 | } 140 | } 141 | 142 | impl super::AsyncIO for WebsocketServerStream {} 143 | -------------------------------------------------------------------------------- /tunshell-client/src/shell/client/shell.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error, Result}; 2 | use crossterm; 3 | use futures::channel::mpsc; 4 | use futures::channel::mpsc::UnboundedReceiver; 5 | use futures::stream::StreamExt; 6 | use io::{AsyncReadExt, AsyncWriteExt}; 7 | use log::*; 8 | use std::{thread::{self, JoinHandle}, env}; 9 | use tokio::io; 10 | 11 | pub struct HostShellStdin { 12 | stdin: io::Stdin, 13 | } 14 | 15 | pub struct HostShellStdout { 16 | stdout: io::Stdout, 17 | } 18 | 19 | pub struct HostShellResizeWatcher { 20 | receiver: UnboundedReceiver<(u16, u16)>, 21 | } 22 | 23 | pub struct HostShell {} 24 | 25 | impl HostShellStdin { 26 | pub fn new() -> Result { 27 | Ok(Self { stdin: io::stdin() }) 28 | } 29 | 30 | pub async fn read(&mut self, buff: &mut [u8]) -> Result { 31 | self.stdin.read(buff).await.map_err(Error::from) 32 | } 33 | } 34 | 35 | impl HostShellStdout { 36 | pub fn new() -> Result { 37 | Ok(Self { 38 | stdout: io::stdout(), 39 | }) 40 | } 41 | 42 | pub async fn write(&mut self, buff: &[u8]) -> Result<()> { 43 | self.stdout.write_all(buff).await.map_err(Error::from)?; 44 | self.stdout.flush().await.map_err(Error::from)?; 45 | Ok(()) 46 | } 47 | } 48 | 49 | impl HostShellResizeWatcher { 50 | pub fn new() -> Result { 51 | let (receiver, _thread) = Self::create_thread(); 52 | 53 | Ok(Self { receiver }) 54 | } 55 | 56 | fn create_thread() -> (UnboundedReceiver<(u16, u16)>, JoinHandle<()>) { 57 | let (tx, rx) = mpsc::unbounded::<(u16, u16)>(); 58 | let mut prev_size = None; 59 | 60 | let thread = thread::spawn(move || loop { 61 | let size = match crossterm::terminal::size() { 62 | Ok(size) => size, 63 | Err(err) => { 64 | error!("Error while receiving terminal size: {:?}", err); 65 | break; 66 | } 67 | }; 68 | 69 | if prev_size.is_some() && prev_size.unwrap() == size { 70 | std::thread::sleep(std::time::Duration::from_millis(500)); 71 | continue; 72 | } 73 | 74 | prev_size = Some(size); 75 | info!("terminal size changed to {:?}", size); 76 | 77 | match tx.unbounded_send(size) { 78 | Ok(_) => info!("sent terminal size to channel"), 79 | Err(err) => { 80 | error!("Failed to send resize event to channel: {:?}", err); 81 | break; 82 | } 83 | } 84 | }); 85 | 86 | (rx, thread) 87 | } 88 | 89 | pub async fn next(&mut self) -> Result<(u16, u16)> { 90 | match self.receiver.next().await { 91 | Some(size) => Ok(size), 92 | None => Err(Error::msg("Resize watcher has been disposed")), 93 | } 94 | } 95 | } 96 | 97 | impl HostShell { 98 | pub fn new() -> Result { 99 | Ok(Self {}) 100 | } 101 | 102 | pub async fn println(&self, output: &str) { 103 | println!("{}\r", output); 104 | } 105 | 106 | pub fn enable_raw_mode(&mut self) -> Result<()> { 107 | debug!("enabling tty raw mode"); 108 | crossterm::terminal::enable_raw_mode()?; 109 | Ok(()) 110 | } 111 | 112 | pub fn disable_raw_mode(&mut self) -> Result<()> { 113 | debug!("disabling tty raw mode"); 114 | crossterm::terminal::disable_raw_mode()?; 115 | Ok(()) 116 | } 117 | 118 | pub fn stdin(&self) -> Result { 119 | HostShellStdin::new() 120 | } 121 | 122 | pub fn stdout(&self) -> Result { 123 | HostShellStdout::new() 124 | } 125 | 126 | pub fn resize_watcher(&self) -> Result { 127 | HostShellResizeWatcher::new() 128 | } 129 | 130 | pub fn term(&self) -> Result { 131 | match std::env::var("TERM") { 132 | Ok(term) => Ok(term), 133 | Err(err) => Err(Error::new(err)), 134 | } 135 | } 136 | 137 | pub async fn size(&self) -> Result<(u16, u16)> { 138 | crossterm::terminal::size().map_err(Error::new) 139 | } 140 | 141 | pub(crate) fn color(&self) -> Result { 142 | Ok(crossterm::style::available_color_count() > 1 && env::var("NO_COLOR").is_err()) 143 | } 144 | } 145 | 146 | impl Drop for HostShell { 147 | fn drop(&mut self) { 148 | self.disable_raw_mode() 149 | .unwrap_or_else(|err| debug!("Failed to disable terminal raw mode: {:?}", err)); 150 | } 151 | } 152 | 153 | #[cfg(test)] 154 | mod tests { 155 | // use super::*; 156 | // use tokio::runtime::Runtime; 157 | } 158 | -------------------------------------------------------------------------------- /tunshell-client/src/shell/client/test.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error, Result}; 2 | use std::{ 3 | cmp, 4 | sync::{Arc, Mutex}, 5 | task::{Poll, Waker}, 6 | }; 7 | 8 | pub struct HostShellStdin { 9 | state: Arc>, 10 | } 11 | 12 | struct MockStdin { 13 | data: Vec, 14 | closed: bool, 15 | wakers: Vec, 16 | } 17 | 18 | pub struct HostShellStdout { 19 | state: Arc>>, 20 | } 21 | 22 | pub struct HostShellResizeWatcher { 23 | state: Arc>, 24 | } 25 | 26 | struct MockResizeWatcher { 27 | data: Vec<(u16, u16)>, 28 | closed: bool, 29 | wakers: Vec, 30 | } 31 | 32 | pub struct HostShell { 33 | stdin: Arc>, 34 | stdout: Arc>>, 35 | resize: Arc>, 36 | } 37 | 38 | impl HostShellStdin { 39 | pub async fn read(&mut self, buff: &mut [u8]) -> Result { 40 | futures::future::poll_fn(|cx| { 41 | let mut stdin = self.state.lock().unwrap(); 42 | 43 | if stdin.data.len() == 0 { 44 | if stdin.closed { 45 | return Poll::Ready(Ok(0)); 46 | }; 47 | 48 | stdin.wakers.push(cx.waker().clone()); 49 | return Poll::Pending; 50 | } 51 | 52 | let len = cmp::min(buff.len(), stdin.data.len()); 53 | buff[..len].copy_from_slice(&stdin.data[..len]); 54 | stdin.data.drain(..len); 55 | Poll::Ready(Ok(len)) 56 | }) 57 | .await 58 | } 59 | } 60 | 61 | impl HostShellStdout { 62 | pub async fn write(&mut self, buff: &[u8]) -> Result<()> { 63 | let mut stdout = self.state.lock().unwrap(); 64 | stdout.extend_from_slice(buff); 65 | 66 | Ok(()) 67 | } 68 | } 69 | 70 | impl HostShellResizeWatcher { 71 | pub async fn next(&mut self) -> Result<(u16, u16)> { 72 | futures::future::poll_fn(|cx| { 73 | let mut resize = self.state.lock().unwrap(); 74 | 75 | if resize.data.len() == 0 { 76 | if resize.closed { 77 | return Poll::Ready(Err(Error::msg("resize watcher closed"))); 78 | }; 79 | 80 | resize.wakers.push(cx.waker().clone()); 81 | return Poll::Pending; 82 | } 83 | 84 | Poll::Ready(Ok(resize.data.remove(0))) 85 | }) 86 | .await 87 | } 88 | } 89 | 90 | impl HostShell { 91 | pub fn new() -> Result { 92 | Ok(Self { 93 | stdin: Arc::new(Mutex::new(MockStdin { 94 | data: vec![], 95 | closed: false, 96 | wakers: vec![], 97 | })), 98 | stdout: Arc::new(Mutex::new(vec![])), 99 | resize: Arc::new(Mutex::new(MockResizeWatcher { 100 | data: vec![], 101 | closed: false, 102 | wakers: vec![], 103 | })), 104 | }) 105 | } 106 | 107 | pub async fn println(&self, output: &str) { 108 | let mut stdout = self.stdout.lock().unwrap(); 109 | stdout.extend_from_slice(output.as_bytes()); 110 | } 111 | 112 | pub fn enable_raw_mode(&mut self) -> Result<()> { 113 | Ok(()) 114 | } 115 | 116 | pub fn disable_raw_mode(&mut self) -> Result<()> { 117 | Ok(()) 118 | } 119 | 120 | pub fn stdin(&self) -> Result { 121 | Ok(HostShellStdin { 122 | state: Arc::clone(&self.stdin), 123 | }) 124 | } 125 | 126 | pub fn stdout(&self) -> Result { 127 | Ok(HostShellStdout { 128 | state: Arc::clone(&self.stdout), 129 | }) 130 | } 131 | 132 | pub fn resize_watcher(&self) -> Result { 133 | Ok(HostShellResizeWatcher { 134 | state: Arc::clone(&self.resize), 135 | }) 136 | } 137 | 138 | pub fn term(&self) -> Result { 139 | Ok("MOCK".to_owned()) 140 | } 141 | 142 | pub async fn size(&self) -> Result<(u16, u16)> { 143 | Ok((100, 100)) 144 | } 145 | 146 | pub fn write_to_stdin(&self, buf: &[u8]) { 147 | let mut stdin = self.stdin.lock().unwrap(); 148 | stdin.data.extend_from_slice(buf); 149 | stdin.wakers.drain(..).map(|i| i.wake()).for_each(drop); 150 | } 151 | 152 | pub fn drain_stdout(&self) -> Vec { 153 | let mut stdin = self.stdout.lock().unwrap(); 154 | 155 | stdin.drain(..).collect() 156 | } 157 | 158 | pub fn send_resize(&self, size: (u16, u16)) { 159 | let mut resize = self.resize.lock().unwrap(); 160 | resize.data.push(size); 161 | resize.wakers.drain(..).map(|i| i.wake()).for_each(drop); 162 | } 163 | } 164 | 165 | impl Clone for HostShell { 166 | fn clone(&self) -> Self { 167 | Self { 168 | stdin: Arc::clone(&self.stdin), 169 | stdout: Arc::clone(&self.stdout), 170 | resize: Arc::clone(&self.resize), 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tunshell-client/src/shell/mod.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | pub use client::*; 3 | 4 | cfg_if::cfg_if! { 5 | if #[cfg(not(target_arch = "wasm32"))] { 6 | mod server; 7 | pub(crate) use server::*; 8 | } 9 | } 10 | 11 | mod proto; 12 | use proto::*; 13 | 14 | pub struct ShellKey { 15 | key: String, 16 | } 17 | 18 | impl ShellKey { 19 | pub fn new(key: &str) -> Self { 20 | Self { 21 | key: key.to_owned(), 22 | } 23 | } 24 | 25 | pub fn key(&self) -> &str { 26 | &self.key 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tunshell-client/src/shell/server/fallback/byte_channel.rs: -------------------------------------------------------------------------------- 1 | use io::Write; 2 | use std::{ 3 | cmp, io, 4 | pin::Pin, 5 | task::{Context, Poll, Waker}, 6 | }; 7 | use tokio::io::AsyncRead; 8 | 9 | //// An in-memory buffer with a sync write and an async read half 10 | pub(super) struct ByteChannel { 11 | buff: Vec, 12 | read_wakers: Vec, 13 | shutdown: bool, 14 | } 15 | 16 | impl ByteChannel { 17 | pub(super) fn new() -> Self { 18 | Self { 19 | buff: vec![], 20 | read_wakers: vec![], 21 | shutdown: false, 22 | } 23 | } 24 | } 25 | 26 | impl AsyncRead for ByteChannel { 27 | fn poll_read( 28 | mut self: Pin<&mut Self>, 29 | cx: &mut Context<'_>, 30 | buf: &mut [u8], 31 | ) -> Poll> { 32 | if self.buff.len() == 0 { 33 | if self.shutdown { 34 | return Poll::Ready(Ok(0)); 35 | } 36 | 37 | self.read_wakers.push(cx.waker().clone()); 38 | return Poll::Pending; 39 | } 40 | 41 | let len = cmp::min(buf.len(), self.buff.len()); 42 | buf[..len].copy_from_slice(self.buff.drain(..len).collect::>().as_slice()); 43 | Poll::Ready(Ok(len)) 44 | } 45 | } 46 | 47 | impl Write for ByteChannel { 48 | fn write(&mut self, buf: &[u8]) -> io::Result { 49 | if buf.len() == 0 { 50 | return Ok(0); 51 | } 52 | 53 | if self.shutdown { 54 | return Err(io::Error::from(io::ErrorKind::BrokenPipe)); 55 | } 56 | 57 | self.buff.extend_from_slice(buf); 58 | 59 | for i in self.read_wakers.drain(..) { 60 | i.wake(); 61 | } 62 | 63 | Ok(buf.len()) 64 | } 65 | 66 | fn flush(&mut self) -> io::Result<()> { 67 | Ok(()) 68 | } 69 | } 70 | 71 | impl ByteChannel { 72 | pub(super) fn shutdown(&mut self) { 73 | self.shutdown = true; 74 | 75 | for i in self.read_wakers.drain(..) { 76 | i.wake(); 77 | } 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | use std::time::Duration; 85 | use tokio::{io::AsyncReadExt, runtime::Runtime, time::timeout}; 86 | 87 | #[test] 88 | fn test_new() { 89 | let stream = ByteChannel::new(); 90 | 91 | assert_eq!(stream.buff, Vec::::new()); 92 | assert_eq!(stream.read_wakers.len(), 0); 93 | } 94 | 95 | #[test] 96 | fn test_write() { 97 | let mut stream = ByteChannel::new(); 98 | 99 | stream.write(&[1, 2, 3]).unwrap(); 100 | 101 | assert_eq!(stream.buff, vec![1, 2, 3]); 102 | } 103 | 104 | #[test] 105 | fn test_write_then_read() { 106 | let mut stream = ByteChannel::new(); 107 | 108 | let (read, read_buff) = Runtime::new().unwrap().block_on(async { 109 | stream.write(&[1, 2, 3]).unwrap(); 110 | 111 | let mut read_buff = [0u8; 1024]; 112 | let read = stream.read(&mut read_buff).await.unwrap(); 113 | 114 | (read, read_buff) 115 | }); 116 | 117 | assert_eq!(read, 3); 118 | assert_eq!(&read_buff[..3], &[1, 2, 3]); 119 | 120 | assert_eq!(stream.buff, Vec::::new()); 121 | } 122 | 123 | // #[test] 124 | // fn test_read_then_write() { 125 | // let mut stream = Arc::new(Mutex::new(ByteChannel::new())); 126 | 127 | // let (read, read_buff) = Runtime::new().unwrap().block_on(async { 128 | // let mut read_stream = Arc::clone(&stream); 129 | 130 | // let read_task = tokio::spawn(async move { 131 | // let read_stream = read_stream.lock().unwrap(); 132 | // let mut read_buff = [0u8; 1024]; 133 | // let read = read_stream.read(&mut read_buff).await.unwrap(); 134 | 135 | // (read, read_buff) 136 | // }); 137 | 138 | // let stream = stream.lock().unwrap(); 139 | // stream.write(&[1, 2, 3]).unwrap(); 140 | 141 | // read_task.await.unwrap() 142 | // }); 143 | 144 | // assert_eq!(read, 3); 145 | // assert_eq!(&read_buff[..3], &[1, 2, 3]); 146 | 147 | // let stream = stream.lock().unwrap(); 148 | // assert_eq!(stream.buff, Vec::::new()); 149 | // } 150 | 151 | #[test] 152 | fn test_read_empty() { 153 | let mut stream = ByteChannel::new(); 154 | 155 | Runtime::new().unwrap().block_on(async { 156 | let mut read_buff = [0u8; 1024]; 157 | 158 | timeout(Duration::from_millis(100), stream.read(&mut read_buff)) 159 | .await 160 | .expect_err("should await until there is data written"); 161 | }); 162 | } 163 | 164 | #[test] 165 | fn test_read_after_close() { 166 | let mut stream = ByteChannel::new(); 167 | 168 | Runtime::new().unwrap().block_on(async { 169 | stream.shutdown(); 170 | 171 | assert_eq!(stream.read(&mut [0u8; 1024]).await.unwrap(), 0); 172 | }); 173 | } 174 | 175 | #[test] 176 | fn test_write_after_close() { 177 | let mut stream = ByteChannel::new(); 178 | 179 | Runtime::new().unwrap().block_on(async { 180 | stream.shutdown(); 181 | 182 | stream 183 | .write(&mut [0u8; 1024]) 184 | .expect_err("should return error if writing after closing stream"); 185 | }); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /tunshell-client/src/shell/server/fallback/mod.rs: -------------------------------------------------------------------------------- 1 | mod byte_channel; 2 | mod input_stream; 3 | mod interpreter; 4 | mod output_stream; 5 | mod shell; 6 | 7 | pub(self) use byte_channel::*; 8 | pub(self) use input_stream::*; 9 | pub(self) use output_stream::*; 10 | pub(self) use interpreter::*; 11 | 12 | pub(crate) use shell::*; 13 | -------------------------------------------------------------------------------- /tunshell-client/src/shell/server/fallback/output_stream.rs: -------------------------------------------------------------------------------- 1 | use super::ByteChannel; 2 | use io::Write; 3 | use std::{ 4 | io, 5 | pin::Pin, 6 | task::{Context, Poll}, 7 | }; 8 | use tokio::io::AsyncRead; 9 | 10 | const CR: u8 = 0x0D; 11 | const LF: u8 = 0x0A; 12 | 13 | /// Output byte stream for the fallback terminal 14 | /// Performs conversion of LF to CRLF line endings. 15 | pub(super) struct OutputStream { 16 | inner: ByteChannel, 17 | last_byte: Option, 18 | } 19 | 20 | impl OutputStream { 21 | pub(super) fn new() -> Self { 22 | Self { 23 | inner: ByteChannel::new(), 24 | last_byte: None, 25 | } 26 | } 27 | 28 | pub(super) fn shutdown(&mut self) { 29 | self.inner.shutdown(); 30 | } 31 | } 32 | 33 | impl AsyncRead for OutputStream { 34 | fn poll_read( 35 | mut self: Pin<&mut Self>, 36 | cx: &mut Context<'_>, 37 | buf: &mut [u8], 38 | ) -> Poll> { 39 | Pin::new(&mut self.inner).poll_read(cx, buf) 40 | } 41 | } 42 | 43 | impl Write for OutputStream { 44 | fn write(&mut self, buf: &[u8]) -> io::Result { 45 | if buf.len() == 0 { 46 | return Ok(0); 47 | } 48 | 49 | // Convert LF to CRLF 50 | let mut written = 0; 51 | 52 | if buf[0] == LF && self.last_byte.unwrap_or(0) != CR { 53 | self.inner.write_all(&[CR, LF])?; 54 | written += 1; 55 | } 56 | 57 | for i in written..buf.len() { 58 | if i > 0 && buf[i] == LF && buf[i - 1] != CR { 59 | self.inner.write_all(&buf[written..i])?; 60 | self.inner.write_all(&[CR, LF])?; 61 | written = i + 1; 62 | } 63 | } 64 | 65 | self.inner.write_all(&buf[written..])?; 66 | self.last_byte = Some(buf[buf.len() - 1]); 67 | 68 | Ok(buf.len()) 69 | } 70 | 71 | fn flush(&mut self) -> io::Result<()> { 72 | self.inner.flush() 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::*; 79 | use tokio::{io::AsyncReadExt, runtime::Runtime}; 80 | 81 | #[test] 82 | fn test_new() { 83 | let stream = OutputStream::new(); 84 | 85 | assert_eq!(stream.last_byte, None); 86 | } 87 | 88 | #[test] 89 | fn test_write_then_read() { 90 | let mut stream = OutputStream::new(); 91 | 92 | let (read, read_buff) = Runtime::new().unwrap().block_on(async { 93 | stream.write(&[1, 2, 3]).unwrap(); 94 | 95 | let mut read_buff = [0u8; 1024]; 96 | let read = stream.read(&mut read_buff).await.unwrap(); 97 | 98 | (read, read_buff) 99 | }); 100 | 101 | assert_eq!(read, 3); 102 | assert_eq!(&read_buff[..3], &[1, 2, 3]); 103 | } 104 | 105 | #[test] 106 | fn test_write_then_read_replaces_lf_to_crlf() { 107 | let mut stream = OutputStream::new(); 108 | 109 | let (read, read_buff) = Runtime::new().unwrap().block_on(async { 110 | stream.write(&[LF, 2, 3, LF, 5, 6, CR, LF, LF]).unwrap(); 111 | 112 | let mut read_buff = [0u8; 1024]; 113 | let read = stream.read(&mut read_buff).await.unwrap(); 114 | 115 | (read, read_buff) 116 | }); 117 | 118 | assert_eq!( 119 | &read_buff[..read], 120 | &[CR, LF, 2, 3, CR, LF, 5, 6, CR, LF, CR, LF] 121 | ); 122 | } 123 | 124 | #[test] 125 | fn test_write_then_read_replaces_lf_to_crlf_over_multiple_writes() { 126 | let mut stream = OutputStream::new(); 127 | 128 | let (read, read_buff) = Runtime::new().unwrap().block_on(async { 129 | stream.write(&[LF]).unwrap(); 130 | stream.write(&[1, 2, 3]).unwrap(); 131 | stream.write(&[LF, 4, 5, 6, CR]).unwrap(); 132 | stream.write(&[LF, 7, 8, 9]).unwrap(); 133 | 134 | let mut read_buff = [0u8; 1024]; 135 | let read = stream.read(&mut read_buff).await.unwrap(); 136 | 137 | (read, read_buff) 138 | }); 139 | 140 | assert_eq!( 141 | &read_buff[..read], 142 | &[CR, LF, 1, 2, 3, CR, LF, 4, 5, 6, CR, LF, 7, 8, 9] 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /tunshell-client/src/shell/server/fallback/shell.rs: -------------------------------------------------------------------------------- 1 | use super::super::ShellStream; 2 | use super::{InputStream, Interpreter, OutputStream, Token}; 3 | use crate::shell::{proto::WindowSize, server::shell::Shell}; 4 | use anyhow::{Error, Result}; 5 | use async_trait::async_trait; 6 | use futures::{Future, Stream}; 7 | use std::{ 8 | collections::HashMap, 9 | io::Write, 10 | path::PathBuf, 11 | pin::Pin, 12 | sync::{Arc, Mutex}, 13 | }; 14 | use tokio::io::AsyncRead; 15 | use tokio::task::JoinHandle; 16 | 17 | /// In unix environments which do not support pty's we use this 18 | /// bare-bones shell implementation 19 | pub(crate) struct FallbackShell { 20 | _interpreter_task: JoinHandle>, 21 | state: SharedState, 22 | } 23 | 24 | #[derive(Clone)] 25 | pub(super) struct SharedState { 26 | pub(super) inner: Arc>, 27 | } 28 | 29 | pub(super) struct Inner { 30 | pub(super) input: InputStream, 31 | pub(super) output: OutputStream, 32 | pub(super) pwd: PathBuf, 33 | pub(super) env: HashMap, 34 | pub(super) size: WindowSize, 35 | pub(super) exit_code: Option, 36 | } 37 | 38 | impl FallbackShell { 39 | pub(in super::super) fn new(_term: &str, size: WindowSize) -> Self { 40 | let state = SharedState::new(size); 41 | 42 | let mut shell = Self { 43 | _interpreter_task: Interpreter::start(state.clone()), 44 | state, 45 | }; 46 | 47 | shell.write_notice().unwrap(); 48 | 49 | shell 50 | } 51 | 52 | fn write_notice(&mut self) -> Result<()> { 53 | let mut state = self.state.inner.lock().unwrap(); 54 | 55 | state.output.write("\r\n".as_bytes())?; 56 | state.output.write("NOTICE: Tunshell is running in a limited environment and is unable to allocate a pty for a real shell. ".as_bytes())?; 57 | state.output.write( 58 | "Falling back to a built-in pseudo-shell with very limited functionality".as_bytes(), 59 | )?; 60 | state.output.write("\r\n\r\n".as_bytes())?; 61 | 62 | Ok(()) 63 | } 64 | } 65 | 66 | #[async_trait] 67 | impl Shell for FallbackShell { 68 | async fn read(&mut self, buff: &mut [u8]) -> Result { 69 | if self.exit_code().is_ok() { 70 | return Ok(0); 71 | } 72 | 73 | self.state.read_output(buff).await 74 | } 75 | 76 | async fn write(&mut self, buff: &[u8]) -> Result<()> { 77 | if self.exit_code().is_ok() { 78 | return Err(Error::msg("shell has exited")); 79 | } 80 | 81 | // echo chars 82 | let mut state = self.state.inner.lock().unwrap(); 83 | state.input.write_all(buff).map_err(Error::from)?; 84 | Ok(()) 85 | } 86 | 87 | fn resize(&mut self, size: WindowSize) -> Result<()> { 88 | let mut state = self.state.inner.lock().unwrap(); 89 | state.size = size; 90 | Ok(()) 91 | } 92 | 93 | fn exit_code(&self) -> Result { 94 | let state = self.state.inner.lock().unwrap(); 95 | state 96 | .exit_code 97 | .ok_or_else(|| Error::msg("shell has not closed")) 98 | } 99 | 100 | fn custom_io_handling(&self) -> bool { 101 | false 102 | } 103 | 104 | async fn stream_io(&mut self, _stream: &mut ShellStream) -> Result<()> { 105 | unreachable!() 106 | } 107 | } 108 | 109 | impl Drop for FallbackShell { 110 | fn drop(&mut self) {} 111 | } 112 | 113 | impl SharedState { 114 | pub(super) fn new(size: WindowSize) -> Self { 115 | Self { 116 | inner: Arc::new(Mutex::new(Inner { 117 | size, 118 | input: InputStream::new(), 119 | output: OutputStream::new(), 120 | pwd: std::env::current_dir().unwrap(), 121 | env: HashMap::new(), 122 | exit_code: None, 123 | })), 124 | } 125 | } 126 | 127 | pub(super) fn exit_code(&self) -> Option { 128 | self.inner.lock().unwrap().exit_code 129 | } 130 | 131 | pub(super) fn read_input<'a>(&'a mut self) -> impl Future> + 'a { 132 | futures::future::poll_fn(move |cx| { 133 | let mut state = self.inner.lock().unwrap(); 134 | let result = Pin::new(&mut state.input).poll_next(cx); 135 | 136 | result.map(|i| i.ok_or_else(|| Error::msg("input stream ended unexpectedly"))) 137 | }) 138 | } 139 | 140 | pub(super) fn read_output<'a>( 141 | &'a mut self, 142 | buff: &'a mut [u8], 143 | ) -> impl Future> + 'a { 144 | futures::future::poll_fn(move |cx| { 145 | let mut state = self.inner.lock().unwrap(); 146 | let result = Pin::new(&mut state.output).poll_read(cx, buff); 147 | 148 | result.map_err(Error::from) 149 | }) 150 | } 151 | } 152 | 153 | #[cfg(test)] 154 | mod tests { 155 | // use super::*; 156 | // use std::time::Duration; 157 | } 158 | -------------------------------------------------------------------------------- /tunshell-client/src/shell/server/shell.rs: -------------------------------------------------------------------------------- 1 | use super::ShellStream; 2 | use crate::shell::proto::WindowSize; 3 | use anyhow::Result; 4 | use async_trait::async_trait; 5 | 6 | #[async_trait] 7 | pub(super) trait Shell { 8 | async fn read(&mut self, buff: &mut [u8]) -> Result; 9 | 10 | async fn write(&mut self, buff: &[u8]) -> Result<()>; 11 | 12 | fn resize(&mut self, size: WindowSize) -> Result<()>; 13 | 14 | fn exit_code(&self) -> Result; 15 | 16 | fn custom_io_handling(&self) -> bool; 17 | 18 | async fn stream_io(&mut self, stream: &mut ShellStream) -> Result<()>; 19 | } 20 | -------------------------------------------------------------------------------- /tunshell-client/src/stream/crypto/mod.rs: -------------------------------------------------------------------------------- 1 | cfg_if::cfg_if! { 2 | if #[cfg(target_arch = "wasm32")] { 3 | mod web_sys; 4 | pub(super) use self::web_sys::*; 5 | } else if #[cfg(openssl)] { 6 | mod openssl; 7 | pub use self::openssl::*; 8 | } else { 9 | mod ring; 10 | pub use self::ring::*; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tunshell-client/src/stream/crypto/openssl.rs: -------------------------------------------------------------------------------- 1 | use crate::stream::aes_stream::EncryptedMessage; 2 | use anyhow::{bail, Context, Result}; 3 | use log::*; 4 | use openssl::rand::rand_bytes; 5 | use openssl::symm; 6 | use openssl::{hash::MessageDigest, pkcs5::pbkdf2_hmac}; 7 | 8 | pub struct Key(Vec); 9 | 10 | impl Clone for Key { 11 | fn clone(&self) -> Self { 12 | Self(self.0.clone()) 13 | } 14 | } 15 | 16 | pub(in crate::stream) async fn derive_key(salt: &[u8], key: &[u8]) -> Result { 17 | let mut derived_key = [0u8; 32]; 18 | 19 | pbkdf2_hmac( 20 | key, 21 | salt, 22 | 1000, 23 | MessageDigest::sha256(), 24 | &mut derived_key[..], 25 | ) 26 | .context("failed to derive key")?; 27 | 28 | Ok(Key(derived_key.to_vec())) 29 | } 30 | 31 | pub(in crate::stream) async fn encrypt( 32 | plaintext: Vec, 33 | key: Key, 34 | ) -> Result<(EncryptedMessage, Key)> { 35 | let mut nonce = [0u8; 12]; 36 | rand_bytes(&mut nonce[..]).context("failed to generate random bytes")?; 37 | 38 | let mut tag = [0u8; 16]; 39 | let mut ciphertext = symm::encrypt_aead( 40 | symm::Cipher::aes_256_gcm(), 41 | key.0.as_slice(), 42 | Some(&nonce[..]), 43 | &[], 44 | plaintext.as_slice(), 45 | &mut tag[..], 46 | ) 47 | .context("failed to init openssl crypto")?; 48 | 49 | // append tag to end of ciphertext (as per ring) 50 | ciphertext.extend_from_slice(&tag[..]); 51 | 52 | debug!("encrypted {} bytes", plaintext.len()); 53 | Ok(( 54 | EncryptedMessage { 55 | nonce: nonce.to_vec(), 56 | ciphertext, 57 | }, 58 | key, 59 | )) 60 | } 61 | 62 | pub(in crate::stream) async fn decrypt( 63 | message: EncryptedMessage, 64 | key: Key, 65 | ) -> Result<(Vec, Key)> { 66 | // Separate out tag from ciphertext 67 | if message.ciphertext.len() < 16 { 68 | bail!("encrypted message cannot be less than 16 bytes in length"); 69 | } 70 | 71 | let ciphertext = &message.ciphertext[..message.ciphertext.len() - 16]; 72 | let tag = &message.ciphertext[message.ciphertext.len() - 16..]; 73 | 74 | let plaintext = symm::decrypt_aead( 75 | symm::Cipher::aes_256_gcm(), 76 | key.0.as_slice(), 77 | Some(message.nonce.as_slice()), 78 | &[], 79 | ciphertext, 80 | tag, 81 | ) 82 | .context("failed to decrypt data")?; 83 | 84 | debug!("decrypted {} bytes", plaintext.len()); 85 | Ok((plaintext.to_vec(), key)) 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::*; 91 | use tokio::runtime::Runtime; 92 | 93 | #[test] 94 | fn test_derive_key() { 95 | Runtime::new().unwrap().block_on(async { 96 | let key = derive_key(&[1, 2, 3], &[4, 5, 6]).await.unwrap(); 97 | 98 | assert_eq!(key.0.len(), 32); 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tunshell-client/src/stream/crypto/ring.rs: -------------------------------------------------------------------------------- 1 | use crate::stream::aes_stream::EncryptedMessage; 2 | use anyhow::{Error, Result}; 3 | use log::*; 4 | use ring::aead::*; 5 | use ring::pbkdf2::*; 6 | use ring::rand::{SecureRandom, SystemRandom}; 7 | 8 | pub struct Key(LessSafeKey, Vec); 9 | 10 | impl Clone for Key { 11 | fn clone(&self) -> Self { 12 | Self( 13 | LessSafeKey::new(UnboundKey::new(&self.0.algorithm(), self.1.as_slice()).unwrap()), 14 | self.1.clone(), 15 | ) 16 | } 17 | } 18 | 19 | pub(in crate::stream) async fn derive_key(salt: &[u8], key: &[u8]) -> Result { 20 | let mut derived_key = [0u8; 32]; 21 | derive( 22 | PBKDF2_HMAC_SHA256, 23 | std::num::NonZeroU32::new(1000).unwrap(), 24 | salt, 25 | key, 26 | &mut derived_key, 27 | ); 28 | 29 | Ok(Key( 30 | LessSafeKey::new(UnboundKey::new(&AES_256_GCM, &derived_key).unwrap()), 31 | derived_key.to_vec(), 32 | )) 33 | } 34 | 35 | pub(in crate::stream) async fn encrypt( 36 | plaintext: Vec, 37 | key: Key, 38 | ) -> Result<(EncryptedMessage, Key)> { 39 | let mut nonce = [0u8; 12]; 40 | let rand = SystemRandom::new(); 41 | rand.fill(&mut nonce).unwrap(); 42 | 43 | let len = plaintext.len(); 44 | let mut ciphertext = plaintext; 45 | key.0 46 | .seal_in_place_append_tag( 47 | Nonce::assume_unique_for_key(nonce.to_owned()), 48 | Aad::empty(), 49 | &mut ciphertext, 50 | ) 51 | .map_err(|_| Error::msg("failed to encrypt message"))?; 52 | 53 | debug!("encrypted {} bytes", len); 54 | Ok(( 55 | EncryptedMessage { 56 | nonce: nonce.to_vec(), 57 | ciphertext, 58 | }, 59 | key, 60 | )) 61 | } 62 | 63 | pub(in crate::stream) async fn decrypt( 64 | message: EncryptedMessage, 65 | key: Key, 66 | ) -> Result<(Vec, Key)> { 67 | let mut nonce = [0u8; 12]; 68 | nonce.copy_from_slice(&message.nonce[..12]); 69 | let mut plaintext = message.ciphertext.clone(); 70 | 71 | let plaintext = key 72 | .0 73 | .open_in_place( 74 | Nonce::assume_unique_for_key(nonce.to_owned()), 75 | Aad::empty(), 76 | &mut plaintext, 77 | ) 78 | .map_err(|_| Error::msg("failed to decrypt message"))?; 79 | 80 | debug!("decrypted {} bytes", plaintext.len()); 81 | Ok((plaintext.to_vec(), key)) 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use super::*; 87 | use tokio::runtime::Runtime; 88 | 89 | #[test] 90 | fn test_derive_key() { 91 | Runtime::new().unwrap().block_on(async { 92 | let key = derive_key(&[1, 2, 3], &[4, 5, 6]).await.unwrap(); 93 | 94 | assert_eq!(key.0.algorithm(), &AES_256_GCM); 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tunshell-client/src/stream/mod.rs: -------------------------------------------------------------------------------- 1 | use tokio::io::{AsyncRead, AsyncWrite}; 2 | 3 | mod aes_stream; 4 | mod crypto; 5 | mod relay_stream; 6 | 7 | pub use aes_stream::*; 8 | pub use relay_stream::*; 9 | 10 | pub trait TunnelStream: AsyncRead + AsyncWrite + Send + Unpin {} 11 | 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | use super::*; 16 | use futures::io::Cursor; 17 | use tokio_util::compat::Compat; 18 | 19 | impl TunnelStream for Compat>> {} 20 | } 21 | -------------------------------------------------------------------------------- /tunshell-client/src/util/delay.rs: -------------------------------------------------------------------------------- 1 | use cfg_if::cfg_if; 2 | use std::time::Duration; 3 | 4 | cfg_if! { 5 | if #[cfg(target_arch = "wasm32")] { 6 | use std::sync::{Arc, Mutex}; 7 | use std::task::Poll; 8 | use wasm_bindgen::prelude::*; 9 | 10 | #[wasm_bindgen] 11 | extern "C" { 12 | fn setTimeout(closure: &Closure, millis: u32) -> f64; 13 | } 14 | 15 | 16 | pub async fn delay_for(duration: Duration) { 17 | let woken = Arc::new(Mutex::new(false)); 18 | 19 | futures::future::poll_fn(move |cx| { 20 | if *woken.lock().unwrap() { 21 | return Poll::Ready(()); 22 | } 23 | 24 | let callback = { 25 | let waker = cx.waker().clone(); 26 | let woken = Arc::clone(&woken); 27 | 28 | Closure::wrap(Box::new(move || { 29 | let mut woken = woken.lock().unwrap(); 30 | *woken = true; 31 | waker.clone().wake(); 32 | }) as Box) 33 | }; 34 | 35 | setTimeout(&callback, duration.as_millis() as u32); 36 | callback.forget(); 37 | Poll::Pending 38 | }).await 39 | } 40 | } else { 41 | use tokio::time; 42 | 43 | pub async fn delay_for(duration: Duration) { 44 | time::delay_for(duration).await 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tunshell-client/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod delay; 2 | -------------------------------------------------------------------------------- /tunshell-client/src/wasm.rs: -------------------------------------------------------------------------------- 1 | use crate::{Config, Client, ClientMode, HostShell}; 2 | use wasm_bindgen::prelude::*; 3 | use wasm_bindgen::JsValue; 4 | use js_sys::{Uint8Array, Promise}; 5 | use wasm_bindgen_futures::JsFuture; 6 | use std::panic; 7 | use log::*; 8 | 9 | #[global_allocator] 10 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 11 | 12 | #[wasm_bindgen] 13 | pub struct BrowserConfig { 14 | client_key: String, 15 | encryption_key: String, 16 | relay_server: String, 17 | term: TerminalEmulator, 18 | terminate: Promise 19 | } 20 | 21 | #[wasm_bindgen] 22 | impl BrowserConfig { 23 | #[wasm_bindgen(constructor)] 24 | pub fn new(client_key: String, encryption_key: String, relay_server: String, term: TerminalEmulator, terminate: Promise) -> Self{ 25 | Self { 26 | client_key, 27 | encryption_key, 28 | relay_server, 29 | term, 30 | terminate 31 | } 32 | } 33 | } 34 | 35 | 36 | #[wasm_bindgen(typescript_custom_section)] 37 | const TERMINAL_EMULATOR: &'static str = r#" 38 | interface TerminalEmulator { 39 | data: () => Promise; 40 | resize: () => Promise; 41 | write: (data: UInt8Array) => Promise; 42 | size: () => Uint16Array; 43 | clone: () => TerminalEmulator; 44 | } 45 | "#; 46 | 47 | #[wasm_bindgen] 48 | extern "C" { 49 | #[wasm_bindgen(typescript_type = "TerminalEmulator")] 50 | pub type TerminalEmulator; 51 | 52 | #[wasm_bindgen(method, js_name = data)] 53 | pub async fn data( 54 | this: &TerminalEmulator, 55 | ) -> JsValue; 56 | 57 | #[wasm_bindgen(method, js_name = resize)] 58 | pub async fn resize( 59 | this: &TerminalEmulator, 60 | ) -> JsValue; 61 | 62 | #[wasm_bindgen(method, js_name = write)] 63 | pub async fn write( 64 | this: &TerminalEmulator, 65 | data: Uint8Array, 66 | ); 67 | 68 | #[wasm_bindgen(method, js_name = size)] 69 | pub fn size( 70 | this: &TerminalEmulator 71 | ) -> JsValue; 72 | 73 | #[wasm_bindgen(method, js_name = clone)] 74 | pub fn clone( 75 | this: &TerminalEmulator 76 | ) -> TerminalEmulator; 77 | } 78 | 79 | #[wasm_bindgen] 80 | pub async fn tunshell_init_client(config: BrowserConfig) { 81 | panic::set_hook(Box::new(console_error_panic_hook::hook)); 82 | 83 | if let Err(err) = console_log::init_with_level(log::Level::Debug) { 84 | warn!("failed to set log level: {}", err); 85 | } 86 | 87 | let host_shell = HostShell::new(config.term).unwrap(); 88 | let terminate = config.terminate; 89 | let config = Config::new( 90 | ClientMode::Local, 91 | &config.client_key, 92 | &config.relay_server, 93 | 5000, 94 | 443, 95 | &config.encryption_key, 96 | false, 97 | false 98 | ); 99 | 100 | 101 | let mut client = Client::new(config, host_shell); 102 | let terminate = JsFuture::from(terminate); 103 | 104 | tokio::select! { 105 | res = client.start_session() => if let Err(err) = res { 106 | client.println(&format!("\r\nError occurred during session: {:?}", err)).await; 107 | }, 108 | _ = terminate => info!("terminating client...") 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tunshell-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tunshell-server" 3 | version = "0.1.0" 4 | authors = ["Elliot Levin "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | tunshell-shared = { path = "../tunshell-shared" } 9 | tokio = { version = "0.2.21", features=["rt-threaded", "blocking", "time", "io-util", "tcp", "udp", "macros", "sync"] } 10 | tokio-util = { version = "0.3.1", features=["compat"] } 11 | tokio-rustls = { version = "0.14.0", features=["dangerous_configuration"] } 12 | futures = "0.3.5" 13 | anyhow = "1.0.31" 14 | serde = "1.0.114" 15 | serde_json = "1.0.56" 16 | webpki = "0.21.3" 17 | webpki-roots = "0.20.0" 18 | log = "0.4.8" 19 | env_logger = "0.7.1" 20 | warp = { version = "0.3.3", features=["tls"] } 21 | chrono = "0.4.13" 22 | rustls = "0.18.0" 23 | rand = "0.7.3" 24 | rusqlite = { version = "0.23.1", features=["bundled"] } 25 | uuid = { version = "0.8.1", features=["v4"] } 26 | 27 | [dev-dependencies] 28 | lazy_static = "1.4.0" 29 | async-tungstenite = { version = "0.8.0", features=["async-tls", "tokio-runtime"] } 30 | async-tls = "0.9.0" 31 | tungstenite = "0.11.0" 32 | -------------------------------------------------------------------------------- /tunshell-server/certs/development.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC3jCCAcagAwIBAgIJAIOn270PBW51MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV 3 | BAMTEnR1bnNoZWxsLmxvY2FsaG9zdDAeFw0yMDA3MTUxMTUwMTZaFw0zMDA3MTMx 4 | MTUwMTZaMB0xGzAZBgNVBAMTEnR1bnNoZWxsLmxvY2FsaG9zdDCCASIwDQYJKoZI 5 | hvcNAQEBBQADggEPADCCAQoCggEBALQyK9JhvAIspqDvG4kcilvAFhMe4yhMJzAe 6 | aNiJOBJBPVXdwqvnj/vggOOJd3+GKuH+F9dmGsUn9A/kwoQuIm4FXQNCw4O22vBb 7 | l70Sbqpx8w9siZJobdQ7FDlblw49WwcAdbNdhCv+Rgl+7EcVdP6TlrLcs5pFvyZJ 8 | X580NRFRU2dDurpSTKHEYTRk46rTKDncXGGtS7aWbBfvKQ7Eb6JJkpOWkug07NVS 9 | saiCFNkMELkRDBmBKpSeYqidVAg0prAUvzCR0GXHkwv8fs9BC1PH4DSGYj+cu72c 10 | ksmbQE7f75r4JbmyrxKzvA/KvrX6S2uuBS7fAPNPa0ziQR7IGmcCAwEAAaMhMB8w 11 | HQYDVR0RBBYwFIISdHVuc2hlbGwubG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IB 12 | AQCAVeUJQd73SgaggwM1yUlhs6lNyUmmQs3QD+Kya5pMlbUh4DzXPZK1Znr6C8Q1 13 | AFQND5OILmkpq3mTvAYK7RL0+DGMy3zOeq5KV0781nJqKYpDBuXbl5DMjPIYo3GT 14 | EqmN4O9SyN3xLZf6ByFD/2ymoGI1VUvbfsMbzUKFH+oCAro78kzzmtxDlsMa6vLa 15 | s8/TTeuM+eM7RdMC11s8JTMtuNMoq1ECUI1NaH466qKlBrWFIUCmPaX2jmtytDa+ 16 | SMqJuUk5ackdthE3LVn+mn+rDayjtF1eYToAkj4OPYaeSA8G/WFWcRJCjSR4j1+u 17 | fQLIMS28kfpMCNF9Pc6DqH2f 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /tunshell-server/certs/development.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0MivSYbwCLKag 3 | 7xuJHIpbwBYTHuMoTCcwHmjYiTgSQT1V3cKr54/74IDjiXd/hirh/hfXZhrFJ/QP 4 | 5MKELiJuBV0DQsODttrwW5e9Em6qcfMPbImSaG3UOxQ5W5cOPVsHAHWzXYQr/kYJ 5 | fuxHFXT+k5ay3LOaRb8mSV+fNDURUVNnQ7q6UkyhxGE0ZOOq0yg53FxhrUu2lmwX 6 | 7ykOxG+iSZKTlpLoNOzVUrGoghTZDBC5EQwZgSqUnmKonVQINKawFL8wkdBlx5ML 7 | /H7PQQtTx+A0hmI/nLu9nJLJm0BO3++a+CW5sq8Ss7wPyr61+ktrrgUu3wDzT2tM 8 | 4kEeyBpnAgMBAAECggEAFIDU7aZDPzgXaZ5rUqmVsKTlnh1YmYA4wUfkhCbw41rb 9 | AMcv3GGHx5Ae+kTs/OymOnqv+clbaG2IXrqXy3R4ZG5ly/Yvyu/mb4zscg4D248F 10 | qg6ehLMvNAPY5EHgqTUgA2bw/Yy5ekv/ahswBVKSsljWv9lO8lHXUyLsJ3BITYTz 11 | hVV87fdKO79SFbKIau4BhpiHtY9rtb5Us38RWvg2bZfL3GU4Mb9MLXx9yqwmSNmC 12 | QEKrcroTrhUiB1pbAaMkZk/u00vhZ0Zoaacavk+WRYFggK99FWoz7z2BNw5ORWgK 13 | 7L3h/Wrqbr47Hf2qR2KemdhTK+Df8UsKF5LlamsU6QKBgQDb/Dp2MoYIrz0LG4aN 14 | BG0QOZj/tAsszYh6gKwXmrgfoxAjdFzAM2kg4McpN7qMsX5VkQ67y2ourKgbGY3P 15 | RVmpGWhadbWlqZKU18XZ7O68NsHYixKPivpjbv32MwFBXeolrFN9M3UzL1yc6kKR 16 | jC+Or6szAFdB1Q101ycHckOBewKBgQDRslbyoy+TYtaB5tzS8ZE7KzVPfgwWsyeb 17 | fyuOnqqp8tuYGwjqq3nI1rBtqEsWJOIK4PuPhw9QfXnujvHGvnTBDIR2eIKKi5ob 18 | JGdhGiGKS58kZOU5OAF/BC085BEcPBkaaNSyBgUwB7UfWFBP3osxTb6+Zo407vxT 19 | 9EXRBlfJBQKBgQDVHF+CW+WvChzH4u2RNUV232WR6didmatqibat00wmEfEzd6nW 20 | 5Lcmi7tE+eD2JYju2ez9Ds2Z09ezESlpL5TxlVSbtca4azM+kF3yhW6t1RorbmcX 21 | uhphM4hB9x1zNsj7oBrtgmk6odpFhUfh+n2j7Bic+uqNMxmrJDeCJjaxewKBgEtE 22 | 3HjTomwg+iY+m90L4LmAVO5nrCcpv3nNN/FFerTt+2ypp5W0X0574XA7DFiyfICz 23 | KRjnvdOCdpXusVJJYtc4iwOLVjAs0/ASLRlpq8hcRI9nt+/F7qOM+D/3DT05RTl5 24 | j38nMg1/dr/9jzZcB1J3OZRWc40Ei4YHFFhnEnORAoGAAY0BLu/woDBVIxMNMNH0 25 | L0qjJQfIKFt+ZNuKMLcpraMpIo7cSkXK2F9+xzo81pjPpvEkbXfsX0O9Xftr5KjD 26 | I2619f6KrJf9QlG0kd3PDgGbqCRjtOVIxY3Fs8O8Yg9YjECf1aN6p7Q65UzrokMy 27 | ShIMtpM7kJq04MPqEloYAV4= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tunshell-server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Used for local development environment 2 | version: '3.3' 3 | services: 4 | relay: 5 | build: 6 | context: . 7 | dockerfile: docker/dev.Dockerfile 8 | restart: unless-stopped 9 | volumes: 10 | - cargo:/usr/local/cargo:delegated 11 | - .:/app/tunshell-server:delegated 12 | - ../tunshell-shared:/app/tunshell-shared:delegated 13 | - target:/app/target:delegated 14 | - ./db.sqlite:/app/db.sqlite 15 | ports: 16 | - "3000:3000" 17 | - "3001:3001" 18 | - "3002:3002" 19 | environment: 20 | TUNSHELL_API_PORT: 3000 21 | TUNSHELL_RELAY_TLS_PORT: 3001 22 | SQLITE_DB_PATH: /app/db.sqlite 23 | TLS_RELAY_PRIVATE_KEY: /app/tunshell-server/certs/development.key 24 | TLS_RELAY_CERT: /app/tunshell-server/certs/development.cert 25 | DOMAIN_NAME: tunshell.localhost 26 | STATIC_DIR: /app/tunshell-server/static 27 | RUST_LOG: debug 28 | CARGO_TARGET_DIR: /app/target 29 | 30 | volumes: 31 | cargo: 32 | target: -------------------------------------------------------------------------------- /tunshell-server/docker/dev.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:alpine 2 | 3 | ENV RUSTFLAGS="--cfg alpine" 4 | 5 | RUN apk add --no-cache musl-dev openssl-dev sqlite-dev 6 | RUN cargo install cargo-watch 7 | 8 | WORKDIR /app/tunshell-server 9 | 10 | CMD [ "cargo", "watch", "-x", "test", "-x", "run" ] -------------------------------------------------------------------------------- /tunshell-server/docker/prod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:alpine AS build 2 | 3 | ENV RUSTFLAGS="--cfg alpine" 4 | 5 | RUN apk add --no-cache musl-dev sqlite-dev openssl-dev 6 | RUN mkdir /app/ 7 | 8 | COPY . /app/ 9 | 10 | WORKDIR /app/tunshell-server 11 | 12 | RUN cargo build --release 13 | 14 | FROM alpine:latest 15 | 16 | RUN mkdir /app/ 17 | 18 | COPY --from=build /app/target/release/server /app/server 19 | RUN chmod +x /app/server 20 | COPY tunshell-server/static /app/static 21 | 22 | WORKDIR /app 23 | 24 | ENV STATIC_DIR /app/static 25 | 26 | ENTRYPOINT [ "/app/server" ] -------------------------------------------------------------------------------- /tunshell-server/src/api/cors.rs: -------------------------------------------------------------------------------- 1 | use warp::cors::Cors; 2 | 3 | pub fn cors() -> Cors { 4 | warp::cors() 5 | .allow_origins(vec!["https://tunshell.com", "http://localhost:3003"]) 6 | .allow_headers(vec![ 7 | "Host", 8 | "User-Agent", 9 | "Accept", 10 | "Content-Type", 11 | "Sec-Fetch-Mode", 12 | "Referer", 13 | "Origin", 14 | "Authority", 15 | "Access-Control-Request-Method", 16 | "Access-Control-Request-Headers", 17 | ]) 18 | .allow_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE"]) 19 | .build() 20 | } 21 | -------------------------------------------------------------------------------- /tunshell-server/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | mod routes; 2 | mod cors; 3 | 4 | mod register; 5 | pub use register::*; -------------------------------------------------------------------------------- /tunshell-server/src/api/register.rs: -------------------------------------------------------------------------------- 1 | use super::{cors::cors, routes}; 2 | use crate::db; 3 | use anyhow::Result; 4 | use db::SessionStore; 5 | use log::*; 6 | use warp::{filters::BoxedFilter, Filter, Reply}; 7 | 8 | pub async fn register() -> Result> { 9 | info!("registering api server routes"); 10 | 11 | let store = SessionStore::new(db::connect().await?); 12 | 13 | let routes = warp::any() 14 | .and({ 15 | warp::path("api").and( 16 | warp::any().and( 17 | // POST /api/sessions 18 | warp::path("sessions") 19 | .and(warp::post()) 20 | .and_then(move || routes::create_session(store.clone())), 21 | ).or( 22 | // GET /api/info 23 | warp::path("info") 24 | .and(warp::get()) 25 | .and_then(move || routes::get_info()), 26 | ) 27 | ) 28 | }) 29 | .with(cors()); 30 | 31 | Ok(routes.boxed()) 32 | } 33 | -------------------------------------------------------------------------------- /tunshell-server/src/api/routes/create_session.rs: -------------------------------------------------------------------------------- 1 | use crate::db::{Participant, Session, SessionStore}; 2 | use log::*; 3 | use serde::{Deserialize, Serialize}; 4 | use warp::{http::Response, hyper::Body, Rejection, Reply}; 5 | 6 | #[derive(Serialize, Deserialize, Debug)] 7 | struct ResponsePayload<'a> { 8 | peer1_key: &'a str, 9 | peer2_key: &'a str, 10 | } 11 | 12 | pub(crate) async fn create_session(mut store: SessionStore) -> Result, Rejection> { 13 | debug!("creating new session"); 14 | let session = Session::new(Participant::default(), Participant::default()); 15 | 16 | let result = store.save(&session).await; 17 | 18 | if let Err(err) = result { 19 | error!("error while saving session: {}", err); 20 | 21 | return Ok(Box::new( 22 | Response::builder() 23 | .status(500) 24 | .body(Body::from("error occurred while saving session")) 25 | .unwrap(), 26 | )); 27 | } 28 | 29 | Ok(Box::new(warp::reply::json(&ResponsePayload { 30 | peer1_key: &session.peer1.key, 31 | peer2_key: &session.peer2.key, 32 | }))) 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | use crate::db; 39 | use futures::TryStreamExt; 40 | use serde_json; 41 | use tokio::runtime::Runtime; 42 | 43 | #[test] 44 | fn test_create_session() { 45 | Runtime::new().unwrap().block_on(async { 46 | let store = SessionStore::new(db::connect().await.unwrap()); 47 | 48 | let session = create_session(store).await.unwrap(); 49 | 50 | let body = session 51 | .into_response() 52 | .into_body() 53 | .try_fold(Vec::new(), |mut data, chunk| async move { 54 | data.extend_from_slice(&chunk); 55 | Ok(data) 56 | }) 57 | .await 58 | .unwrap(); 59 | 60 | let response = serde_json::from_slice::>(body.as_slice()).unwrap(); 61 | 62 | assert_ne!(response.peer1_key, ""); 63 | assert_ne!(response.peer2_key, ""); 64 | 65 | debug!("response: {:?}", response); 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tunshell-server/src/api/routes/info.rs: -------------------------------------------------------------------------------- 1 | use log::*; 2 | use serde::{Deserialize, Serialize}; 3 | use warp::{reject, Rejection, Reply}; 4 | use std::env; 5 | 6 | #[derive(Serialize, Deserialize, Debug)] 7 | struct ResponsePayload<'a> { 8 | domain_name: &'a str, 9 | } 10 | 11 | #[derive(Debug)] 12 | struct InternalError; 13 | 14 | impl reject::Reject for InternalError {} 15 | 16 | pub(crate) async fn get_info() -> Result, Rejection> { 17 | debug!("retrieving server info new session"); 18 | let domain_name = match env::var("DOMAIN_NAME") { 19 | Ok(domain_name) => domain_name, 20 | Err(err) => { 21 | error!("error while retrieving domain env var: {}", err); 22 | 23 | return Err(reject::custom(InternalError)); 24 | } 25 | }; 26 | 27 | Ok(Box::new(warp::reply::json(&ResponsePayload { 28 | domain_name: &domain_name, 29 | }))) 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::*; 35 | use futures::TryStreamExt; 36 | use serde_json; 37 | use tokio::runtime::Runtime; 38 | 39 | #[test] 40 | fn test_get_info() { 41 | Runtime::new().unwrap().block_on(async { 42 | let result = get_info().await.unwrap(); 43 | 44 | let body = result 45 | .into_response() 46 | .into_body() 47 | .try_fold(Vec::new(), |mut data, chunk| async move { 48 | data.extend_from_slice(&chunk); 49 | Ok(data) 50 | }) 51 | .await 52 | .unwrap(); 53 | 54 | let response = serde_json::from_slice::>(body.as_slice()).unwrap(); 55 | 56 | assert_eq!(response.domain_name, env::var("DOMAIN_NAME").unwrap()); 57 | debug!("response: {:?}", response); 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tunshell-server/src/api/routes/mod.rs: -------------------------------------------------------------------------------- 1 | mod create_session; 2 | mod info; 3 | 4 | pub(crate) use create_session::*; 5 | pub(crate) use info::*; -------------------------------------------------------------------------------- /tunshell-server/src/bin/server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use env_logger; 3 | use tokio; 4 | use tunshell_server::relay; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | env_logger::init(); 9 | tunshell_server::start(relay::Config::from_env()?).await 10 | } 11 | -------------------------------------------------------------------------------- /tunshell-server/src/db/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::env; 3 | 4 | pub(crate) struct Config { 5 | pub(crate) sqlite_db_path: String, 6 | } 7 | 8 | impl Config { 9 | pub(crate) fn from_env() -> Result { 10 | let sqlite_db_path = env::var("SQLITE_DB_PATH")?; 11 | 12 | Ok(Self { sqlite_db_path }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tunshell-server/src/db/connect.rs: -------------------------------------------------------------------------------- 1 | use super::{schema, Config}; 2 | use anyhow::{Context, Error, Result}; 3 | use log::*; 4 | use rusqlite::Connection; 5 | 6 | pub(crate) async fn connect() -> Result { 7 | tokio::task::spawn_blocking(connect_sync) 8 | .await 9 | .context("error while connecting to sqlite")? 10 | } 11 | 12 | fn connect_sync() -> Result { 13 | info!("connecting to sqlite"); 14 | let config = Config::from_env()?; 15 | 16 | let mut con = match Connection::open(config.sqlite_db_path) { 17 | Ok(con) => con, 18 | Err(err) => { 19 | error!("failed to connect to sqlite: {}", err); 20 | return Err(Error::from(err)); 21 | } 22 | }; 23 | 24 | info!("connected to sqlite"); 25 | 26 | schema::init(&mut con).context("error while initialising sqlite schema")?; 27 | 28 | Ok(con) 29 | } 30 | 31 | #[cfg(all(test, integration))] 32 | mod tests { 33 | use super::*; 34 | use tokio::runtime::Runtime; 35 | 36 | #[test] 37 | fn test_connect() { 38 | Runtime::new().unwrap().block_on(async { 39 | let client = connect().await.unwrap(); 40 | let names = client.list_database_names(None, None).await.unwrap(); 41 | 42 | assert_eq!(names, vec!["relay"]); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tunshell-server/src/db/mod.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod connect; 3 | pub(self) mod schema; 4 | mod session; 5 | 6 | pub(crate) use config::*; 7 | pub(crate) use connect::*; 8 | pub(crate) use session::*; 9 | -------------------------------------------------------------------------------- /tunshell-server/src/db/schema.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::*; 3 | use rusqlite::{params, Connection}; 4 | 5 | pub(crate) fn init(con: &mut Connection) -> Result<()> { 6 | info!("initialising sqlite schema"); 7 | 8 | con.execute( 9 | " 10 | CREATE TABLE IF NOT EXISTS sessions ( 11 | id TEXT PRIMARY KEY, 12 | peer1_key TEXT NOT NULL, 13 | peer2_key TEXT NOT NULL, 14 | created_at TEXT NOT NULL 15 | ) 16 | ", 17 | params![], 18 | )?; 19 | 20 | con.execute( 21 | " 22 | CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_peer1_key ON 23 | sessions (peer1_key) 24 | ", 25 | params![], 26 | )?; 27 | 28 | con.execute( 29 | " 30 | CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_peer2_key ON 31 | sessions (peer2_key) 32 | ", 33 | params![], 34 | )?; 35 | 36 | info!("schema initialised"); 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /tunshell-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::*; 3 | 4 | pub mod api; 5 | pub mod relay; 6 | pub mod db; 7 | 8 | pub async fn start(relay_config: relay::Config) -> Result<()> { 9 | info!("starting tunshell server"); 10 | 11 | let routes = match api::register().await { 12 | Ok(r) => r, 13 | Err(err) => { 14 | error!("error while registering api routes: {}", err); 15 | return Err(err); 16 | } 17 | }; 18 | 19 | let result = relay::start(relay_config, routes).await; 20 | info!("tls relay stopped"); 21 | 22 | if let Err(err) = result { 23 | error!("error occurred: {}", err); 24 | return Err(err); 25 | } 26 | 27 | info!("tunshell server exiting"); 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /tunshell-server/src/relay/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error, Result}; 2 | use rustls::{internal::pemfile, Certificate, NoClientAuth, PrivateKey, ServerConfig}; 3 | use std::fs; 4 | use std::io; 5 | use std::{env, sync::Arc, time::Duration}; 6 | 7 | const DEFAULT_CLIENT_KEY_TIMEOUT_MS: u64 = 3000; 8 | const DEFAULT_CLEAN_EXPIRED_CONNECTION_INTERVAL_MS: u64 = 60_000; 9 | const DEFAULT_WAITING_CONNECTION_EXPIRY_MS: u64 = 3600_000; 10 | const DEFAULT_CONNECTED_CONNECTION_EXPIRY_MS: u64 = 36_000_000; 11 | 12 | #[derive(Clone)] 13 | pub struct Config { 14 | pub tls_port: u16, 15 | pub api_port: u16, 16 | pub tls_config: Arc, 17 | pub tls_key_path: String, 18 | pub tls_cert_path: String, 19 | pub client_key_timeout: Duration, 20 | pub expired_connection_clean_interval: Duration, 21 | pub waiting_connection_expiry: Duration, 22 | pub paired_connection_expiry: Duration, 23 | } 24 | 25 | impl Config { 26 | pub fn from_env() -> Result { 27 | let tls_port = env::var("TUNSHELL_RELAY_TLS_PORT")?.parse::()?; 28 | let api_port = env::var("TUNSHELL_API_PORT")?.parse::()?; 29 | 30 | let tls_cert_path = env::var("TLS_RELAY_CERT")?; 31 | let tls_key_path = env::var("TLS_RELAY_PRIVATE_KEY")?; 32 | 33 | let mut tls_config = ServerConfig::new(NoClientAuth::new()); 34 | tls_config.set_single_cert( 35 | Self::parse_tls_cert(tls_cert_path.clone())?, 36 | Self::parse_tls_private_key(tls_key_path.clone())?, 37 | )?; 38 | let tls_config = Arc::new(tls_config); 39 | 40 | Ok(Config { 41 | tls_port, 42 | api_port, 43 | tls_config, 44 | tls_cert_path, 45 | tls_key_path, 46 | client_key_timeout: Duration::from_millis(DEFAULT_CLIENT_KEY_TIMEOUT_MS), 47 | expired_connection_clean_interval: Duration::from_millis( 48 | DEFAULT_CLEAN_EXPIRED_CONNECTION_INTERVAL_MS, 49 | ), 50 | waiting_connection_expiry: Duration::from_millis(DEFAULT_WAITING_CONNECTION_EXPIRY_MS), 51 | paired_connection_expiry: Duration::from_millis(DEFAULT_CONNECTED_CONNECTION_EXPIRY_MS), 52 | }) 53 | } 54 | 55 | pub(super) fn parse_tls_cert(path: String) -> Result> { 56 | let file = fs::File::open(path)?; 57 | let mut reader = io::BufReader::new(file); 58 | 59 | pemfile::certs(&mut reader).map_err(|_| Error::msg("failed to parse tls cert file")) 60 | } 61 | 62 | pub(super) fn parse_tls_private_key(path: String) -> Result { 63 | let file = fs::File::open(path)?; 64 | let mut reader = io::BufReader::new(file); 65 | 66 | let keys = pemfile::pkcs8_private_keys(&mut reader) 67 | .map_err(|_| Error::msg("failed to parse tls private key file"))?; 68 | 69 | Ok(keys.into_iter().next().unwrap()) 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use super::*; 76 | 77 | #[test] 78 | fn test_config_from_env() { 79 | env::remove_var("TUNSHELL_RELAY_TLS_PORT"); 80 | env::remove_var("TUNSHELL_API_PORT"); 81 | env::remove_var("TLS_RELAY_CERT"); 82 | env::remove_var("TLS_RELAY_PRIVATE_KEY"); 83 | 84 | assert!(Config::from_env().is_err()); 85 | 86 | env::set_var("TUNSHELL_RELAY_TLS_PORT", "1234"); 87 | env::set_var("TUNSHELL_API_PORT", "1235"); 88 | env::set_var("TLS_RELAY_CERT", "certs/development.cert"); 89 | env::set_var("TLS_RELAY_PRIVATE_KEY", "certs/development.key"); 90 | 91 | let config = Config::from_env().unwrap(); 92 | 93 | std::io::stdin().lock(); 94 | 95 | assert_eq!(config.tls_port, 1234); 96 | assert_eq!(config.api_port, 1235); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tunshell-server/src/relay/mod.rs: -------------------------------------------------------------------------------- 1 | mod server; 2 | 3 | pub use config::*; 4 | mod config; 5 | 6 | pub use start::*; 7 | mod start; 8 | -------------------------------------------------------------------------------- /tunshell-server/src/relay/server/connection.rs: -------------------------------------------------------------------------------- 1 | use super::ClientMessageStream; 2 | use crate::db::Session; 3 | use anyhow::{Error, Result}; 4 | use futures::{Future, FutureExt, StreamExt}; 5 | use std::net::SocketAddr; 6 | use std::{ 7 | collections::HashMap, 8 | pin::Pin, 9 | task::{Context, Poll}, 10 | time::Instant, 11 | }; 12 | use tokio::{ 13 | io::{AsyncRead, AsyncWrite}, 14 | task::JoinHandle, 15 | }; 16 | use tunshell_shared::ClientMessage; 17 | 18 | pub(super) trait IoStream: AsyncRead + AsyncWrite + Unpin + Send + Sync { 19 | fn get_peer_addr(&self) -> Result; 20 | } 21 | 22 | type ConnectionStream = ClientMessageStream>; 23 | 24 | pub(super) struct Connection { 25 | pub(super) stream: ConnectionStream, 26 | pub(super) key: String, 27 | pub(super) connected_at: Instant, 28 | pub(super) remote_addr: SocketAddr, 29 | } 30 | 31 | pub(super) struct AcceptedConnection { 32 | pub(super) con: Connection, 33 | pub(super) session: Session, 34 | } 35 | 36 | pub(super) struct PairedConnection { 37 | pub(super) task: JoinHandle>, 38 | pub(super) paired_at: Instant, 39 | } 40 | 41 | pub(super) struct Connections { 42 | pub(super) new: NewConnections, 43 | pub(super) waiting: WaitingConnections, 44 | pub(super) paired: PairedConnections, 45 | } 46 | 47 | pub(super) struct NewConnections(pub(super) Vec>>); 48 | pub(super) struct WaitingConnections(pub(super) HashMap); 49 | pub(super) struct PairedConnections(pub(super) Vec); 50 | 51 | impl Connections { 52 | pub(super) fn new() -> Self { 53 | Self { 54 | new: NewConnections(vec![]), 55 | waiting: WaitingConnections(HashMap::new()), 56 | paired: PairedConnections(vec![]), 57 | } 58 | } 59 | } 60 | 61 | impl Future for NewConnections { 62 | type Output = Result; 63 | 64 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 65 | if let Poll::Ready((idx, result)) = poll_all_remove_ready(self.0.iter_mut().enumerate(), cx) 66 | { 67 | self.0.swap_remove(idx); 68 | return Poll::Ready(result); 69 | } 70 | 71 | Poll::Pending 72 | } 73 | } 74 | 75 | impl Future for PairedConnections { 76 | type Output = Result<(Connection, Connection)>; 77 | 78 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 79 | let poll = poll_all_remove_ready( 80 | self.0.iter_mut().enumerate().map(|(k, v)| (k, &mut v.task)), 81 | cx, 82 | ); 83 | 84 | if let Poll::Ready((idx, result)) = poll { 85 | self.0.swap_remove(idx); 86 | return Poll::Ready(result); 87 | } 88 | 89 | Poll::Pending 90 | } 91 | } 92 | 93 | /// If a connection received a message or closed while it is waiting 94 | /// we remove this connection from the pool 95 | impl Future for WaitingConnections { 96 | type Output = (Connection, Result); 97 | 98 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 99 | let mut received = None; 100 | 101 | for (key, con) in &mut self.0 { 102 | let result = con.stream.stream_mut().poll_next_unpin(cx); 103 | 104 | if let Poll::Ready(message) = result { 105 | received = Some((key.to_owned(), message)); 106 | break; 107 | } 108 | } 109 | 110 | if let None = received { 111 | return Poll::Pending; 112 | } 113 | 114 | let (key, message) = received.unwrap(); 115 | let con = self.0.remove(&key).unwrap(); 116 | 117 | Poll::Ready(( 118 | con, 119 | message.unwrap_or_else(|| Err(Error::msg("connection closed by client"))), 120 | )) 121 | } 122 | } 123 | 124 | fn poll_all_remove_ready<'a, T>( 125 | futures: impl Iterator>)>, 126 | cx: &mut Context<'_>, 127 | ) -> Poll<(usize, Result)> 128 | where 129 | T: 'a, 130 | { 131 | for (k, fut) in futures { 132 | let poll = fut.poll_unpin(cx); 133 | 134 | if let Poll::Ready(result) = poll { 135 | return Poll::Ready((k, result.unwrap_or_else(|err| Err(Error::from(err))))); 136 | } 137 | } 138 | 139 | Poll::Pending 140 | } 141 | -------------------------------------------------------------------------------- /tunshell-server/src/relay/server/message_stream.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error, Result}; 2 | use futures::StreamExt; 3 | use log::*; 4 | use std::time::Duration; 5 | use tokio::{ 6 | io::{AsyncRead, AsyncWrite}, 7 | time::timeout, 8 | }; 9 | use tokio_util::compat::*; 10 | use tunshell_shared::{ClientMessage, KeyPayload, MessageStream, ServerMessage}; 11 | 12 | type MessageStreamInner = MessageStream; 13 | 14 | pub(super) struct ClientMessageStream { 15 | stream: Option>>, 16 | closed: bool, 17 | } 18 | 19 | impl ClientMessageStream { 20 | pub(super) fn new(stream: IO) -> Self { 21 | Self { 22 | stream: Some(MessageStreamInner::new(stream.compat())), 23 | closed: false, 24 | } 25 | } 26 | 27 | #[allow(dead_code)] 28 | pub(super) fn stream(&self) -> &MessageStreamInner> { 29 | self.stream.as_ref().unwrap() 30 | } 31 | 32 | pub(super) fn stream_mut(&mut self) -> &mut MessageStreamInner> { 33 | self.stream.as_mut().unwrap() 34 | } 35 | 36 | #[allow(dead_code)] 37 | pub(super) fn inner(&self) -> &IO { 38 | self.stream().inner().get_ref() 39 | } 40 | 41 | pub(super) async fn next(&mut self) -> Result { 42 | match self.stream_mut().next().await { 43 | Some(result @ Ok(ClientMessage::Close)) => { 44 | self.closed = true; 45 | result 46 | } 47 | Some(result) => result, 48 | None => Err(Error::msg("no messages are left in stream")), 49 | } 50 | } 51 | 52 | pub(super) async fn wait_for_key(&mut self, timeout_duration: Duration) -> Result { 53 | let message = timeout(timeout_duration, self.next()).await??; 54 | 55 | match message { 56 | ClientMessage::Key(key) => Ok(key), 57 | message @ _ => Err(Error::msg(format!( 58 | "unexpected message received from client, expecting key, got {:?}", 59 | message 60 | ))), 61 | } 62 | } 63 | 64 | pub(super) async fn write(&mut self, message: ServerMessage) -> Result<()> { 65 | self.stream_mut().write(&message).await 66 | } 67 | } 68 | 69 | impl Drop for ClientMessageStream { 70 | fn drop(&mut self) { 71 | if self.closed { 72 | return; 73 | } 74 | 75 | // Attempt to send a close message to the client 76 | // when the ClientMessageStream is being dropped without being closed 77 | let stream = self.stream.take().unwrap(); 78 | try_send_close(stream); 79 | } 80 | } 81 | 82 | fn try_send_close( 83 | mut stream: MessageStreamInner>, 84 | ) { 85 | // In the case of the client closing the ClientMessageStream early 86 | // there is no need to send a close message 87 | if stream.is_closed() { 88 | return; 89 | } 90 | 91 | tokio::task::spawn(async move { 92 | debug!("sending close"); 93 | stream 94 | .write(&ServerMessage::Close) 95 | .await 96 | .unwrap_or_else(|err| warn!("error while sending close: {}", err)); 97 | // Allow for final messages to be received by waiting before closing connection 98 | tokio::time::delay_for(Duration::from_secs(1)).await; 99 | // TCP connection closed here 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /tunshell-server/src/relay/server/session_validation.rs: -------------------------------------------------------------------------------- 1 | use crate::db::Session; 2 | use chrono::Utc; 3 | 4 | pub(super) fn is_session_valid_to_join(session: &Session, key: &str) -> bool { 5 | // Ensure session is not older than a day 6 | if Utc::now() - session.created_at > chrono::Duration::days(1) { 7 | return false; 8 | } 9 | 10 | let participant = session.participant(key); 11 | 12 | if participant.is_none() { 13 | return false; 14 | } 15 | 16 | return true; 17 | } 18 | -------------------------------------------------------------------------------- /tunshell-server/src/relay/start.rs: -------------------------------------------------------------------------------- 1 | use super::{config::Config, server::Server}; 2 | use crate::db; 3 | use anyhow::Result; 4 | use log::*; 5 | use warp::{filters::BoxedFilter, Reply}; 6 | 7 | pub async fn start(config: Config, routes: BoxedFilter<(impl Reply + 'static,)>) -> Result<()> { 8 | let sessions = db::SessionStore::new(db::connect().await?); 9 | 10 | info!( 11 | "starting relay server on ports (tls: {}, api: {})", 12 | config.tls_port, config.api_port 13 | ); 14 | 15 | Server::new(config, sessions, routes).start(None).await 16 | } 17 | -------------------------------------------------------------------------------- /tunshell-server/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimeToogo/tunshell/90cb3a7babc7dc7df5851148a405f313705830a4/tunshell-server/static/.gitkeep -------------------------------------------------------------------------------- /tunshell-server/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [[ ! -x "$(command -v rustc)" ]]; 6 | then 7 | echo "Installing rust" 8 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y 9 | fi 10 | 11 | export TUNSHELL_API_PORT="3000" 12 | export TUNSHELL_RELAY_TLS_PORT="3001" 13 | export SQLITE_DB_PATH="$PWD/db.sqlite" 14 | export TLS_RELAY_PRIVATE_KEY="$PWD/certs/development.key" 15 | export TLS_RELAY_CERT="$PWD/certs/development.cert" 16 | export STATIC_DIR="$PWD/static" 17 | export DOMAIN_NAME="tunshell.localhost" 18 | 19 | if [[ -z "$SKIP_TESTS" ]]; 20 | then 21 | cargo test $@ 22 | fi -------------------------------------------------------------------------------- /tunshell-shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tunshell-shared" 3 | version = "0.1.0" 4 | authors = ["Elliot Levin "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | serde = { version = "1.0", features = ["derive"] } 11 | serde_json = "1.0" 12 | anyhow = "1.0" 13 | futures = "0.3.5" 14 | futures-test = "0.3.5" 15 | log = "0.4.8" 16 | 17 | [target.'cfg(fuzzing)'.dependencies] 18 | afl = "0.8.0" 19 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export CARGO_TARGET_DIR="$PWD/fuzz/target" 6 | 7 | cargo afl build 8 | cargo afl fuzz -i fuzz/corpus -o fuzz/out $CARGO_TARGET_DIR/debug/fuzzing 9 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/crash_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimeToogo/tunshell/90cb3a7babc7dc7df5851148a405f313705830a4/tunshell-shared/fuzz/corpus/crash_1 -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/crash_2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimeToogo/tunshell/90cb3a7babc7dc7df5851148a405f313705830a4/tunshell-shared/fuzz/corpus/crash_2 -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/crash_3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimeToogo/tunshell/90cb3a7babc7dc7df5851148a405f313705830a4/tunshell-shared/fuzz/corpus/crash_3 -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/crash_4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimeToogo/tunshell/90cb3a7babc7dc7df5851148a405f313705830a4/tunshell-shared/fuzz/corpus/crash_4 -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_1: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_10: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_11: -------------------------------------------------------------------------------- 1 |  {"key":"key"} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_12: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_13: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_14: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_15: -------------------------------------------------------------------------------- 1 | !{"tcp_port":12345,"udp_port":123} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_16: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_17: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_18: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_19: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_2: -------------------------------------------------------------------------------- 1 | !{"tcp_port":null,"udp_port":2222} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_20: -------------------------------------------------------------------------------- 1 | S{"peer_key": "key", "peer_ip_address": "123.123.123.123", "session_nonce": "nonce"} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_21: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_22: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_23: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_24: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_25: -------------------------------------------------------------------------------- 1 | !{"udp_port":2222,"tcp_port":1234} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_26: -------------------------------------------------------------------------------- 1 | !{"udp_port":null,"tcp_port":null} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_27: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_28: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_29: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_3: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_30: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_31: -------------------------------------------------------------------------------- 1 | N{"peer_key":"key","peer_ip_address":"123.123.123.123","session_nonce":"nonce"} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_32: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_33: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_34: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_35: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_36: -------------------------------------------------------------------------------- 1 |  {"key":"key"} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_37: -------------------------------------------------------------------------------- 1 |  {"key":"key"} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_38: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimeToogo/tunshell/90cb3a7babc7dc7df5851148a405f313705830a4/tunshell-shared/fuzz/corpus/example_38 -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_39: -------------------------------------------------------------------------------- 1 |  {"key":"key"} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_4: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_40: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_41: -------------------------------------------------------------------------------- 1 |  {"key":"key"} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_42: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_43: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_44: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_45: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_46: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_47: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_48: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_49: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_5: -------------------------------------------------------------------------------- 1 |  {"key":"key"} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_50: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_51: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_6: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_7: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_8: -------------------------------------------------------------------------------- 1 | !{"udp_port":4444,"tcp_port":5555} -------------------------------------------------------------------------------- /tunshell-shared/fuzz/corpus/example_9: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tunshell-shared/src/bin/fuzzing.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(fuzzing)] 3 | { 4 | use afl::*; 5 | use anyhow::Result; 6 | use futures::executor::block_on_stream; 7 | use futures::io::Cursor; 8 | use tunshell_shared::{ClientMessage, MessageStream, ServerMessage}; 9 | 10 | fuzz!(|data: &[u8]| { 11 | let cursor = Cursor::new(data.to_vec()); 12 | let stream_client: MessageStream>> = 13 | MessageStream::new(cursor.clone()); 14 | let stream_server: MessageStream>> = 15 | MessageStream::new(cursor); 16 | 17 | let _ = block_on_stream(stream_client).collect::>>(); 18 | let _ = block_on_stream(stream_server).collect::>>(); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tunshell-shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod message; 2 | mod message_stream; 3 | 4 | pub use message::*; 5 | pub use message_stream::*; -------------------------------------------------------------------------------- /tunshell-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tunshell-tests" 3 | version = "0.1.0" 4 | authors = ["Elliot Levin "] 5 | edition = "2018" 6 | autotests = false 7 | 8 | [dependencies] 9 | tunshell-client = { path = "../tunshell-client" } 10 | tunshell-server = { path = "../tunshell-server" } 11 | tokio = { version = "0.2.21", features=["rt-threaded", "blocking"] } 12 | reqwest = { version = "0.10", features = ["json"] } 13 | env_logger = "0.7.1" 14 | futures = "0.3.5" 15 | log = "0.4.11" 16 | 17 | [[test]] 18 | name = "tests" 19 | path = "tests/lib.rs" 20 | -------------------------------------------------------------------------------- /tunshell-tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Initialise testing environment 6 | cd ../tunshell-server/ 7 | SKIP_TESTS=true . ./test.sh 8 | cd ../tunshell-tests/ 9 | 10 | export RUSTFLAGS="--cfg integration_test" 11 | export CARGO_TARGET_DIR="$PWD/target" 12 | export RUST_TEST_THREADS="1" 13 | 14 | cargo test $@ 15 | -------------------------------------------------------------------------------- /tunshell-tests/tests/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Once; 2 | 3 | static INIT: Once = Once::new(); 4 | 5 | #[test] 6 | fn setup() { 7 | // TODO: ensure runs before all other tests 8 | INIT.call_once(env_logger::init); 9 | } 10 | 11 | pub mod utils; 12 | 13 | pub mod valid_direct_connection; 14 | pub mod valid_relay_connection; 15 | -------------------------------------------------------------------------------- /tunshell-tests/tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod shell; -------------------------------------------------------------------------------- /tunshell-tests/tests/utils/shell.rs: -------------------------------------------------------------------------------- 1 | // use tunshell_client::HostShell; 2 | 3 | -------------------------------------------------------------------------------- /tunshell-tests/tests/valid_direct_connection.rs: -------------------------------------------------------------------------------- 1 | use reqwest; 2 | use std::collections::HashMap; 3 | use std::time::Duration; 4 | use tokio::runtime::Runtime; 5 | use tokio::time::delay_for; 6 | use tunshell_client as client; 7 | use tunshell_server as server; 8 | 9 | #[test] 10 | fn test() { 11 | Runtime::new().unwrap().block_on(async { 12 | let mut config = server::relay::Config::from_env().unwrap(); 13 | config.tls_port = 20003; 14 | config.api_port = 20004; 15 | 16 | tokio::task::spawn(server::start(config.clone())); 17 | 18 | delay_for(Duration::from_millis(1000)).await; 19 | 20 | let http = reqwest::Client::builder() 21 | .danger_accept_invalid_certs(true) 22 | .build() 23 | .unwrap(); 24 | 25 | let response = http 26 | .post(format!("https://localhost:{}/api/sessions", config.api_port).as_str()) 27 | .send() 28 | .await 29 | .unwrap() 30 | .json::>() 31 | .await 32 | .unwrap(); 33 | 34 | let target_key = response.get("peer1_key").unwrap(); 35 | let client_key = response.get("peer2_key").unwrap(); 36 | 37 | let target_shell = client::HostShell::new().unwrap(); 38 | 39 | let mut target_config = client::Config::new( 40 | client::ClientMode::Target, 41 | target_key, 42 | "localhost.tunshell.com", 43 | config.tls_port, 44 | config.api_port, 45 | "mock_encryption_key", 46 | true, 47 | false, 48 | ); 49 | target_config.set_dangerous_disable_relay_server_verification(true); 50 | let mut target = client::Client::new(target_config, target_shell.clone()); 51 | 52 | let local_shell = client::HostShell::new().unwrap(); 53 | 54 | let mut local_config = client::Config::new( 55 | client::ClientMode::Local, 56 | client_key, 57 | "localhost.tunshell.com", 58 | config.tls_port, 59 | config.api_port, 60 | "mock_encryption_key", 61 | true, 62 | false, 63 | ); 64 | local_config.set_dangerous_disable_relay_server_verification(true); 65 | let mut local = client::Client::new(local_config, local_shell.clone()); 66 | 67 | let session_task = tokio::spawn(async move { 68 | futures::future::join(local.start_session(), target.start_session()).await 69 | }); 70 | 71 | local_shell.write_to_stdin("echo hello\n".as_bytes()); 72 | local_shell.write_to_stdin("exit\n".as_bytes()); 73 | 74 | let result = session_task.await.unwrap(); 75 | assert_eq!((result.0.unwrap(), result.1.unwrap()), (0, 0)); 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /tunshell-tests/tests/valid_relay_connection.rs: -------------------------------------------------------------------------------- 1 | use reqwest; 2 | use std::collections::HashMap; 3 | use std::time::Duration; 4 | use tokio::runtime::Runtime; 5 | use tokio::time::delay_for; 6 | use tunshell_client as client; 7 | use tunshell_server as server; 8 | 9 | #[test] 10 | fn test() { 11 | Runtime::new().unwrap().block_on(async { 12 | let mut config = server::relay::Config::from_env().unwrap(); 13 | config.tls_port = 20001; 14 | config.api_port = 20002; 15 | 16 | tokio::task::spawn(server::start(config.clone())); 17 | 18 | delay_for(Duration::from_millis(1000)).await; 19 | 20 | let http = reqwest::Client::builder() 21 | .danger_accept_invalid_certs(true) 22 | .build() 23 | .unwrap(); 24 | 25 | let response = http 26 | .post(format!("https://localhost:{}/api/sessions", config.api_port).as_str()) 27 | .send() 28 | .await 29 | .unwrap() 30 | .json::>() 31 | .await 32 | .unwrap(); 33 | 34 | let target_key = response.get("peer1_key").unwrap(); 35 | let client_key = response.get("peer2_key").unwrap(); 36 | 37 | let target_shell = client::HostShell::new().unwrap(); 38 | 39 | let mut target_config = client::Config::new( 40 | client::ClientMode::Target, 41 | target_key, 42 | "localhost.tunshell.com", 43 | config.tls_port, 44 | config.api_port, 45 | "mock_encryption_key", 46 | false, 47 | false, 48 | ); 49 | target_config.set_dangerous_disable_relay_server_verification(true); 50 | let mut target = client::Client::new(target_config, target_shell.clone()); 51 | 52 | let local_shell = client::HostShell::new().unwrap(); 53 | 54 | let mut local_config = client::Config::new( 55 | client::ClientMode::Local, 56 | client_key, 57 | "localhost.tunshell.com", 58 | config.tls_port, 59 | config.api_port, 60 | "mock_encryption_key", 61 | false, 62 | false, 63 | ); 64 | local_config.set_dangerous_disable_relay_server_verification(true); 65 | let mut local = client::Client::new(local_config, local_shell.clone()); 66 | 67 | let session_task = tokio::spawn(async move { 68 | futures::future::try_join(local.start_session(), target.start_session()).await 69 | }); 70 | 71 | local_shell.write_to_stdin("echo hello\n".as_bytes()); 72 | local_shell.write_to_stdin("exit\n".as_bytes()); 73 | 74 | let result = session_task.await.unwrap(); 75 | assert_eq!(result.unwrap(), (0, 0)); 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /website/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "plugins": [ 5 | [ 6 | "babel-plugin-styled-components", 7 | { 8 | "ssr": true, 9 | "displayName": true, 10 | "preprocess": false 11 | } 12 | ] 13 | ], 14 | "presets": [ 15 | "next/babel" 16 | ] 17 | }, 18 | "production": { 19 | "plugins": [ 20 | [ 21 | "babel-plugin-styled-components", 22 | { 23 | "ssr": true, 24 | "displayName": true, 25 | "preprocess": false 26 | } 27 | ] 28 | ], 29 | "presets": [ 30 | "next/babel" 31 | ] 32 | } 33 | }, 34 | "plugins": [ 35 | [ 36 | "babel-plugin-styled-components", 37 | { 38 | "ssr": true, 39 | "displayName": true, 40 | "preprocess": false 41 | } 42 | ] 43 | ] 44 | } -------------------------------------------------------------------------------- /website/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | max_line_length = 120 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [{package,bower}.json] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /website/components/button/index.tsx: -------------------------------------------------------------------------------- 1 | import * as Styled from "./styled"; 2 | import { HTMLAttributes } from "react"; 3 | 4 | interface Props { 5 | mode?: "inverted" | "normal"; 6 | } 7 | 8 | export const Button: React.FC & Props> = ({ children, ...props }) => { 9 | return {children}; 10 | }; 11 | -------------------------------------------------------------------------------- /website/components/button/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { COLOURS } from "../../theme/colours"; 3 | 4 | export const Button = styled.button<{ mode?: string }>` 5 | border: none; 6 | padding: 15px; 7 | text-decoration: none; 8 | display: inline-flex; 9 | border-radius: 8px; 10 | border: 1px solid transparent; 11 | box-shadow: 2px 2px 1px 1px rgba(0, 0, 0, 0.5); 12 | box-sizing: border-box; 13 | cursor: pointer; 14 | transition: 0.2s ease-out all; 15 | outline: 0 !important; 16 | color: ${COLOURS.WHITE}; 17 | background-color: ${COLOURS.TAN2}; 18 | font-size: inherit; 19 | 20 | &:hover { 21 | color: ${COLOURS.TAN2}; 22 | background: ${COLOURS.TAN4}; 23 | } 24 | 25 | &:active { 26 | transform: translate(2px, 2px); 27 | box-shadow: none; 28 | } 29 | 30 | &:disabled { 31 | cursor: default; 32 | background: #888 !important; 33 | color: ${COLOURS.OFF_BLACK}!important; 34 | opacity: 0.5; 35 | } 36 | 37 | ${(props) => 38 | props.mode === "inverted" && 39 | css` 40 | color: ${COLOURS.TAN2}; 41 | background: ${COLOURS.TAN4}; 42 | 43 | &:hover { 44 | color: ${COLOURS.WHITE}; 45 | background-color: ${COLOURS.TAN2}; 46 | } 47 | `} 48 | `; 49 | -------------------------------------------------------------------------------- /website/components/direct-web-term/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Head from "next/head"; 3 | import { RELAY_SERVERS } from "../../services/location"; 4 | import { SessionKeys } from "../../services/session"; 5 | import { Message } from "./styled"; 6 | import { TunshellClient } from "../tunshell-client"; 7 | import { Donate } from "../donate"; 8 | import { WebUrlService } from "../../services/direct-web-url"; 9 | 10 | const urlService = new WebUrlService(); 11 | 12 | enum State { 13 | Loading, 14 | Failed, 15 | Success, 16 | Complete, 17 | } 18 | 19 | export const DirectWebSession = () => { 20 | const [state, setState] = useState(State.Loading); 21 | const [sessionKeys, setSessionKeys] = useState(); 22 | 23 | useEffect(() => { 24 | const parsedSession = urlService.parseWebUrl(window.location.href); 25 | 26 | if (!parsedSession) { 27 | setState(State.Failed); 28 | return; 29 | } 30 | 31 | setSessionKeys(parsedSession); 32 | setState(State.Success); 33 | }, []); 34 | 35 | if (state === State.Loading) { 36 | return Loading...; 37 | } 38 | 39 | if (state === State.Failed) { 40 | return Failed to parse parameters from the anchor string, is the URL malformed?; 41 | } 42 | 43 | if (state === State.Complete) { 44 | return ( 45 | <> 46 | Thank you for using tunshell. 47 | 48 | 49 | ); 50 | } 51 | 52 | return ( 53 | setState(State.Complete)}> 54 | 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /website/components/direct-web-term/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Message = styled.div` 4 | margin: 40px 0 20px 0; 5 | text-align: center; 6 | `; 7 | -------------------------------------------------------------------------------- /website/components/donate/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as Styled from "./styled"; 3 | import { Container } from "../layout"; 4 | import { COLOURS } from "../../theme/colours"; 5 | 6 | export const Donate = (props: any) => { 7 | return ( 8 | 9 | 10 |

Find this useful?

11 | .bmc-button img{height: 34px !important;width: 35px !important;margin-bottom: 1px !important;box-shadow: none !important;border: none !important;vertical-align: middle !important;}.bmc-button{padding: 7px 15px 7px 10px !important;line-height: 35px !important;height:51px !important;text-decoration: none !important;display:inline-flex !important;color:${COLOURS.WHITE} !important;background-color:${COLOURS.TAN2} !important;border-radius: 8px !important;border: 1px solid transparent !important;font-size: 24px !important;letter-spacing: 0.6px !important;box-shadow: 0px 1px 2px rgba(0,0,0, 0.5) !important;-webkit-box-shadow: 0px 1px 2px 2px rgba(0,0,0, 0.5) !important;margin: 0 auto !important;font-family:'Cookie', cursive !important;-webkit-box-sizing: border-box !important;box-sizing: border-box !important;}.bmc-button:hover, .bmc-button:active, .bmc-button:focus {-webkit-box-shadow: 0px 1px 2px 2px rgba(0, 0, 0, 0.5) !important;text-decoration: none !important;box-shadow: 0px 1px 2px 2px rgba(0,0,0, 0.5) !important;opacity: 0.85 !important;color:${COLOURS.WHITE} !important;}Buy me a coffee`, 14 | }} 15 | > 16 |
17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /website/components/donate/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Container } from "../layout"; 3 | 4 | export const Donate = styled.div` 5 | width: 100%; 6 | padding: 40px 0; 7 | 8 | ${Container} { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | } 13 | 14 | h4 { 15 | font-weight: normal; 16 | } 17 | `; 18 | 19 | export const Button = styled.div` 20 | margin: 0 auto; 21 | `; 22 | -------------------------------------------------------------------------------- /website/components/dropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLAttributes, useRef, useEffect } from "react"; 2 | import { DropdownContainer } from "./styled"; 3 | 4 | interface Props { 5 | onSelect: (string) => void; 6 | inline?: boolean; 7 | disabled?: boolean; 8 | } 9 | 10 | export const Dropdown: React.FC< 11 | React.DetailedHTMLProps, HTMLSelectElement> & Props 12 | > = ({ children, onSelect, inline, disabled, ...props }) => { 13 | const selectRef = useRef(); 14 | 15 | useEffect(() => { 16 | if (!selectRef.current) { 17 | return; 18 | } 19 | 20 | const easydropdown = require("easydropdown").default; 21 | 22 | const edd = easydropdown(selectRef.current, { 23 | callbacks: { 24 | onSelect: (value) => onSelect(value), 25 | }, 26 | }); 27 | 28 | return () => { 29 | edd.destroy(); 30 | }; 31 | }, []); 32 | 33 | return ( 34 | 35 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /website/components/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Container } from "../layout"; 3 | import * as Styled from "./styled"; 4 | 5 | export const Footer = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /website/components/footer/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { COLOURS } from "../../theme/colours"; 3 | 4 | export const Footer = styled.header` 5 | padding: 20px 0; 6 | margin-top: auto; 7 | 8 | &, 9 | a { 10 | color: ${COLOURS.OFF_WHITE}; 11 | } 12 | `; 13 | 14 | export const Contents = styled.div` 15 | display: flex; 16 | flex-direction: row; 17 | justify-content: center; 18 | `; 19 | 20 | export const Credits = styled.div` 21 | display: flex; 22 | flex-direction: row; 23 | align-items: center; 24 | 25 | ion-icon { 26 | margin-left: 5px; 27 | } 28 | 29 | @media (max-width: 900px) { 30 | display: block; 31 | 32 | ion-icon { 33 | margin-top: 5px; 34 | margin-left: 0; 35 | } 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /website/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Container } from "../layout"; 3 | import * as Styled from "./styled"; 4 | import { Link } from "../link"; 5 | 6 | export const Header = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Tunshell 16 | 17 | 18 | 19 | 20 | 33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /website/components/header/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { COLOURS } from "../../theme/colours"; 3 | 4 | export const Header = styled.header` 5 | height: 70px; 6 | color: ${COLOURS.OFF_WHITE}; 7 | 8 | a { 9 | text-decoration: none; 10 | } 11 | 12 | @media (max-width: 768px) { 13 | .hide-on-mobile { 14 | display: none; 15 | } 16 | } 17 | `; 18 | 19 | export const Contents = styled.div` 20 | display: flex; 21 | flex-direction: row; 22 | justify-content: space-between; 23 | height: 100%; 24 | `; 25 | 26 | export const Logo = styled.span` 27 | display: flex; 28 | flex-direction: row; 29 | align-items: center; 30 | height: 100%; 31 | font-size: 24px; 32 | letter-spacing: 1px; 33 | 34 | img { 35 | height: 35px; 36 | margin-right: 10px; 37 | } 38 | `; 39 | 40 | export const Nav = styled.nav` 41 | display: flex; 42 | flex-direction: row; 43 | align-items: center; 44 | height: 100%; 45 | font-size: 16px; 46 | 47 | ul { 48 | padding: 0; 49 | margin: 0; 50 | list-style: none; 51 | display: flex; 52 | flex-direction: row; 53 | align-items: center; 54 | 55 | li:not(:last-child) { 56 | margin-right: 40px; 57 | } 58 | 59 | a { 60 | color: ${COLOURS.WHITE}; 61 | } 62 | 63 | ion-icon { 64 | font-size: 30px; 65 | } 66 | } 67 | `; 68 | -------------------------------------------------------------------------------- /website/components/hero/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Container } from "../layout"; 3 | import * as Styled from "./styled"; 4 | import Typical from "react-typical"; 5 | 6 | export const Hero = () => { 7 | let steps = [ 8 | "remote hosts", 9 | "Unix", 10 | "Windows", 11 | "Jenkins", 12 | "AWS Lambda", 13 | "BitBucket Pipelines", 14 | "Cloud Build", 15 | "GitHub Actions", 16 | "Cloud Functions", 17 | "GitLab CI", 18 | "remote hosts", 19 | ]; 20 | 21 | if (typeof window !== "undefined" && window.innerWidth <= 768) { 22 | steps = steps.filter((i) => i.length <= 14); 23 | } 24 | 25 | return ( 26 | 27 | 28 |

29 | Rapidly shell into{" "} 30 | a.concat([i, 2000]), [])} wrapper="span" /> 31 |

32 |

33 | Tunshell is a simple and secure method to remote shell into ephemeral environments such as deployment 34 | pipelines or serverless functions 35 |

36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /website/components/hero/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Hero = styled.div` 4 | padding: 15vh 0; 5 | 6 | h1 { 7 | font-weight: normal; 8 | font-size: 40px; 9 | line-height: 45px; 10 | } 11 | 12 | .typing { 13 | &:after { 14 | content: "_"; 15 | animation: blink 1s infinite; 16 | } 17 | } 18 | 19 | p { 20 | font-size: 20px; 21 | line-height: 35px; 22 | } 23 | 24 | @keyframes blink { 25 | 50% { 26 | opacity: 0; 27 | } 28 | } 29 | 30 | @media (max-width: 768px) { 31 | padding: 30px 0; 32 | 33 | h1 { 34 | height: 135px; 35 | } 36 | } 37 | 38 | @media (max-width: 350px) { 39 | padding: 20px 0; 40 | 41 | h1 { 42 | height: 175px; 43 | } 44 | } 45 | `; 46 | -------------------------------------------------------------------------------- /website/components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | export const Container = styled.div` 5 | width: 1200px; 6 | max-width: 100%; 7 | height: 100%; 8 | margin: 0 auto; 9 | box-sizing: border-box; 10 | 11 | @media (max-width: 1200px) { 12 | width: 1000px; 13 | } 14 | 15 | @media (max-width: 1000px) { 16 | padding: 0 15px; 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /website/components/link/index.tsx: -------------------------------------------------------------------------------- 1 | import NextLink, { LinkProps } from "next/link"; 2 | import * as Styled from "./styled"; 3 | 4 | interface Props extends LinkProps { 5 | a?: React.AnchorHTMLAttributes; 6 | } 7 | 8 | export const Link: React.FC = ({ children, a = {}, ...props }) => { 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /website/components/link/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { COLOURS } from "../../theme/colours"; 3 | 4 | export const Link = styled.span` 5 | a { 6 | color: ${COLOURS.OFF_WHITE}; 7 | text-decoration: none; 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /website/components/script/index.tsx: -------------------------------------------------------------------------------- 1 | import * as Styled from "./styled"; 2 | import { useState, useRef, useEffect } from "react"; 3 | import hjs from "highlight.js"; 4 | 5 | interface Props { 6 | script: string; 7 | lang: string; 8 | } 9 | 10 | export const Script: React.FC = ({ script, lang }) => { 11 | const scriptRef = useRef(); 12 | const [copied, setCopied] = useState(false); 13 | 14 | useEffect(() => { 15 | if (!scriptRef.current) { 16 | return; 17 | } 18 | 19 | hjs.highlightBlock(scriptRef.current); 20 | }, [scriptRef.current, lang, script]); 21 | 22 | const copyToClipboard = () => { 23 | navigator.clipboard 24 | .writeText(script) 25 | .then(() => setCopied(true)) 26 | .then(() => new Promise((r) => setTimeout(r, 2000))) 27 | .then(() => setCopied(false)); 28 | }; 29 | 30 | return ( 31 | 32 | 33 | {script} 34 | 35 | 36 | 37 | 38 | Copied 39 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /website/components/script/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { COLOURS } from "../../theme/colours"; 3 | 4 | export const Wrapper = styled.pre` 5 | width: 100%; 6 | height: 100%; 7 | color: ${COLOURS.OFF_WHITE}; 8 | border-radius: 5px; 9 | box-shadow: 0 0 2px 1px #444 inset; 10 | position: relative; 11 | white-space: normal; 12 | word-break: break-word; 13 | line-height: 24px; 14 | font-size: 16px; 15 | padding: 15px 35px 15px 15px; 16 | box-sizing: border-box; 17 | margin: 0; 18 | 19 | &, 20 | & > span { 21 | background: ${COLOURS.TAN2}; 22 | } 23 | `; 24 | 25 | export const Copy = styled.button` 26 | position: absolute; 27 | top: 5px; 28 | right: 5px; 29 | width: 30px; 30 | height: 30px; 31 | border-radius: 5px; 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | background: none; 36 | border: none; 37 | font-size: 20px; 38 | color: ${COLOURS.OFF_WHITE}; 39 | cursor: pointer; 40 | transition: 0.2s ease-out all; 41 | outline: 0 !important; 42 | padding: 0; 43 | 44 | &:hover { 45 | background: ${COLOURS.TAN4}; 46 | } 47 | `; 48 | 49 | export const Copied = styled.div<{ active: boolean }>` 50 | position: absolute; 51 | background: ${COLOURS.TAN4}; 52 | bottom: calc(100% + 5px); 53 | border-radius: 5px; 54 | font-size: 16px; 55 | color: ${COLOURS.TAN2}; 56 | padding: 5px; 57 | pointer-events: none; 58 | transition: 0.2s ease-out all; 59 | opacity: 0; 60 | white-space: nowrap; 61 | 62 | ${({ active }) => 63 | active && 64 | css` 65 | opacity: 1; 66 | `} 67 | `; 68 | -------------------------------------------------------------------------------- /website/components/term/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | import { TerminalEmulator as TerminalEmulatorInterface } from "../../services/wasm/tunshell_client"; 3 | import * as Styled from "./styled"; 4 | 5 | export interface TerminalEmulatorProps { 6 | fullScreen?: boolean; 7 | onEmulatorInitialised?: (emulator: TerminalEmulatorInterface, term: import("xterm").Terminal) => void; 8 | onClose?: () => void; 9 | children?: any 10 | } 11 | 12 | export const TerminalEmulator: React.SFC = ({ 13 | fullScreen, 14 | onClose = () => {}, 15 | onEmulatorInitialised = () => {}, 16 | children 17 | }) => { 18 | const viewportRef = useRef(); 19 | 20 | const [term, setTerm] = useState(); 21 | const [fitAddon, setFitAddon] = useState(); 22 | 23 | useEffect(() => { 24 | if (!viewportRef.current || term) { 25 | return; 26 | } 27 | 28 | initialiseTerminal(); 29 | }, [viewportRef.current]); 30 | 31 | useEffect(() => { 32 | if (!term) { 33 | return; 34 | } 35 | 36 | initialiseEmulatorWasmInterface(); 37 | }, [term]); 38 | 39 | useEffect(() => { 40 | if (!fitAddon || !viewportRef.current) { 41 | return; 42 | } 43 | 44 | // Handle terminal resizing 45 | fitAddon.fit(); 46 | window.addEventListener("resize", fitAddon.fit); 47 | 48 | return () => window.removeEventListener("resize", fitAddon.fit); 49 | }, [fitAddon, viewportRef.current]); 50 | 51 | const initialiseTerminal = async () => { 52 | console.log(`Creating terminal...`); 53 | 54 | const [xterm, fit] = await Promise.all([import("xterm").then((i) => i), import("xterm-addon-fit").then((i) => i)]); 55 | 56 | const initialisedTerm = new xterm.Terminal({ logLevel: "debug" }); 57 | const fitAddon = new fit.FitAddon(); 58 | initialisedTerm.loadAddon(fitAddon); 59 | initialisedTerm.open(viewportRef.current); 60 | 61 | await new Promise((r) => initialisedTerm.writeln("Welcome to the tunshell web terminal\r\n", r)); 62 | 63 | setTerm(initialisedTerm); 64 | setFitAddon(fitAddon); 65 | 66 | initialisedTerm.focus(); 67 | }; 68 | 69 | const initialiseEmulatorWasmInterface = () => { 70 | const emulator: TerminalEmulatorInterface = { 71 | data: () => 72 | new Promise((resolve) => { 73 | const stop = term.onData((data) => { 74 | resolve(data); 75 | stop.dispose(); 76 | }); 77 | }), 78 | resize: () => 79 | new Promise((resolve) => { 80 | const stop = term.onResize((size) => { 81 | resolve(new Uint16Array([size.cols, size.rows])); 82 | stop.dispose(); 83 | }); 84 | }), 85 | write: (data) => new Promise((r) => term.write(data, r)), 86 | size: () => new Uint16Array([term.cols, term.rows]), 87 | clone: () => ({ ...emulator }), 88 | }; 89 | 90 | onEmulatorInitialised(emulator, term); 91 | }; 92 | 93 | if (fullScreen) { 94 | return ( 95 | 96 | 97 | 98 | 99 | onClose()}> 100 | 101 | 102 | 103 | 104 | {children} 105 | 106 | ); 107 | } else { 108 | return ; 109 | } 110 | }; 111 | -------------------------------------------------------------------------------- /website/components/term/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import { COLOURS } from "../../theme/colours"; 3 | 4 | export const TermViewport = styled.div` 5 | width: 100%; 6 | height: 100%; 7 | box-sizing: border-box; 8 | `; 9 | 10 | export const Term = styled.div` 11 | width: 100%; 12 | height: 100%; 13 | background: ${COLOURS.BLACK}; 14 | box-shadow: 0 0 5px #222; 15 | z-index: 11; 16 | border-radius: 10px; 17 | padding: 20px; 18 | position: relative; 19 | box-sizing: border-box; 20 | `; 21 | 22 | export const FullScreenWrapper = styled.div` 23 | position: fixed; 24 | top: 0; 25 | left: 0; 26 | right: 0; 27 | bottom: 0; 28 | z-index: 10; 29 | box-sizing: border-box; 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: center; 33 | align-items: center; 34 | padding: 20px; 35 | 36 | ${Term} { 37 | height: 400px; 38 | width: 900px; 39 | min-height: 380px; 40 | max-height: 100%; 41 | max-width: 100%; 42 | margin: 20px; 43 | } 44 | `; 45 | 46 | export const Overlay = styled.div` 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | right: 0; 51 | bottom: 0; 52 | z-index: 9; 53 | background: rgba(51, 47, 45, 0.75); 54 | `; 55 | 56 | export const Close = styled.button` 57 | position: absolute; 58 | top: -25px; 59 | right: -20px; 60 | font-size: 40px; 61 | padding: 0; 62 | border-radius: 50px; 63 | border: none; 64 | background: ${COLOURS.TAN4}; 65 | color: ${COLOURS.TAN2}; 66 | cursor: pointer; 67 | z-index: 10; 68 | display: flex; 69 | justify-content: center; 70 | align-items: center; 71 | outline: 0 !important; 72 | `; 73 | -------------------------------------------------------------------------------- /website/components/tunshell-client/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import { TunshellWasm } from "../../services/tunshell-wasm"; 3 | import { useEffect, useState } from "react"; 4 | import { TerminalEmulator as TerminalEmulatorInterface } from "../../services/wasm/tunshell_client"; 5 | import { TerminalEmulator } from "../term"; 6 | import { SessionKeys } from "../../services/session"; 7 | 8 | interface TunshellClientProps { 9 | session: SessionKeys; 10 | onClose: () => void; 11 | children?: any; 12 | } 13 | 14 | export const TunshellClient = dynamic({ 15 | loader: async () => { 16 | const tunshellWasm = await new TunshellWasm().init(); 17 | 18 | return ({ session, onClose, children }) => { 19 | const [emulatorInterface, setEmulatorInterface] = useState(); 20 | const [term, setTerm] = useState(); 21 | 22 | useEffect(() => { 23 | if (!emulatorInterface || !term) { 24 | return; 25 | } 26 | 27 | tunshellWasm 28 | .connect(session, emulatorInterface) 29 | .then(() => term.writeln("\nThe client has exited")) 30 | .catch(() => term.writeln("\nAn error occurred during your session")); 31 | 32 | return () => { 33 | tunshellWasm.terminate(); 34 | }; 35 | }, [session, emulatorInterface, term]); 36 | 37 | const onEmulatorInitialised = (emulatorInterface: TerminalEmulatorInterface, term: import("xterm").Terminal) => { 38 | setEmulatorInterface(emulatorInterface); 39 | setTerm(term); 40 | }; 41 | 42 | return ( 43 | 44 | {children} 45 | 46 | ); 47 | }; 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /website/components/wizard/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { COLOURS } from "../../theme/colours"; 3 | import { DropdownContainer } from "../dropdown/styled"; 4 | 5 | export const Wizard = styled.div` 6 | margin: 20px 0; 7 | `; 8 | 9 | export const Dialog = styled.div` 10 | padding: 20px 30px 30px 30px; 11 | background: ${COLOURS.TAN5}; 12 | box-shadow: 0 0 5px #242424; 13 | border-radius: 5px; 14 | margin-bottom: 40px; 15 | position: relative; 16 | 17 | @media (max-width: 768px) { 18 | padding: 20px; 19 | } 20 | `; 21 | 22 | export const StepHeader = styled.header` 23 | font-size: 18px; 24 | display: flex; 25 | flex-direction: column; 26 | justify-content: center; 27 | align-items: center; 28 | margin-bottom: 40px; 29 | 30 | &:last-child { 31 | margin-bottom: 0; 32 | } 33 | `; 34 | 35 | export const StepNumber = styled.span` 36 | display: inline-block; 37 | width: 18px; 38 | height: 18px; 39 | font-size: 12px; 40 | padding-left: 1px; 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | border-radius: 50px; 45 | border: 1px solid #aaa; 46 | margin-bottom: 20px; 47 | `; 48 | 49 | export const Environments = styled.div` 50 | display: flex; 51 | flex-direction: row; 52 | 53 | @media (max-width: 900px) { 54 | flex-direction: column; 55 | } 56 | `; 57 | 58 | export const Environment = styled.div` 59 | flex: 0 0 calc(50% - 1px); 60 | padding: 0 20px; 61 | display: flex; 62 | flex-direction: column; 63 | align-items: center; 64 | box-sizing: border-box; 65 | 66 | h3 { 67 | font-size: 24px; 68 | font-weight: normal; 69 | text-align: center; 70 | margin: 0 0 5px 0; 71 | display: none; 72 | } 73 | 74 | p { 75 | margin: 0 0 20px 0; 76 | text-align: center; 77 | } 78 | 79 | @media (max-width: 900px) { 80 | &:not(:last-child) { 81 | margin-bottom: 30px; 82 | } 83 | } 84 | 85 | @media (max-width: 768px) { 86 | padding: 0; 87 | } 88 | `; 89 | 90 | export const Dropdown = styled.div` 91 | width: 350px; 92 | max-width: 100%; 93 | font-size: 24px; 94 | text-align: center; 95 | `; 96 | 97 | export const RelayLocation = styled.div` 98 | display: flex; 99 | margin-top: 15px; 100 | font-size: 16px; 101 | align-items: center; 102 | position: absolute; 103 | top: 0; 104 | right: 0; 105 | 106 | label { 107 | margin-right: 15px; 108 | } 109 | 110 | ${DropdownContainer} { 111 | min-width: 50px; 112 | } 113 | 114 | @media (max-width: 900px) { 115 | position: static; 116 | justify-content: center; 117 | } 118 | `; 119 | 120 | export const Separator = styled.hr` 121 | width: 1px; 122 | height: auto; 123 | background: #555; 124 | border: none; 125 | box-shadow: none; 126 | 127 | @media (max-width: 900px) { 128 | display: none; 129 | } 130 | `; 131 | 132 | export const Error = styled.p` 133 | margin-bottom: 0; 134 | 135 | &, 136 | & a { 137 | color: ${COLOURS.RED}; 138 | } 139 | 140 | a { 141 | text-decoration: underline; 142 | } 143 | `; 144 | 145 | export const LaunchShell = styled.div` 146 | margin: auto 0; 147 | display: flex; 148 | flex-direction: column; 149 | justify-content: center; 150 | `; 151 | 152 | export const LaunchShellLink = styled.a` 153 | text-align: center; 154 | color: white; 155 | margin-top: 10px; 156 | font-size: 12px; 157 | `; 158 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@timetoogo/tunshell--website", 3 | "scripts": { 4 | "dev": "next dev -p 3003", 5 | "export": "next build && next export && ./post-export.sh" 6 | }, 7 | "dependencies": { 8 | "base-x": "^3.0.8", 9 | "classnames": "^2.2.6", 10 | "easydropdown": "^4.2.0", 11 | "highlight.js": "^10.4.1", 12 | "next": "13.0.2", 13 | "react": "16.13.1", 14 | "react-dom": "16.13.1", 15 | "react-typical": "^0.1.3", 16 | "styled-components": "^5.1.1", 17 | "xterm": "^4.8.1", 18 | "xterm-addon-fit": "^0.4.0" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^13.13.5", 22 | "@types/react": "^16.9.34", 23 | "@types/styled-components": "^5.1.2", 24 | "babel-plugin-styled-components": "^1.11.1", 25 | "typescript": "^3.8.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /website/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | import "xterm/css/xterm.css"; 3 | import "highlight.js/styles/obsidian.css"; 4 | import { COLOURS } from "../theme/colours"; 5 | 6 | const Reset = createGlobalStyle` 7 | html, body { 8 | padding: 0; 9 | margin: 0; 10 | background: ${COLOURS.TAN3}; 11 | color: ${COLOURS.OFF_WHITE}; 12 | } 13 | 14 | html, body, #__next { 15 | height: 100%; 16 | } 17 | `; 18 | 19 | const Typography = createGlobalStyle` 20 | html, body, button { 21 | font-family: 'Courier Prime', monospace; 22 | } 23 | `; 24 | 25 | export default function MyApp({ Component, pageProps }) { 26 | return ( 27 | <> 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /website/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | import { ServerStyleSheet } from "styled-components"; 3 | 4 | export default class MyDocument extends Document { 5 | static async getInitialProps(ctx) { 6 | const sheet = new ServerStyleSheet(); 7 | const originalRenderPage = ctx.renderPage; 8 | 9 | try { 10 | ctx.renderPage = () => 11 | originalRenderPage({ 12 | enhanceApp: (App) => (props) => sheet.collectStyles(), 13 | }); 14 | 15 | const initialProps = await Document.getInitialProps(ctx); 16 | return { 17 | ...initialProps, 18 | styles: ( 19 | <> 20 | {initialProps.styles} 21 | {sheet.getStyleElement()} 22 | 23 | ), 24 | }; 25 | } finally { 26 | sheet.seal(); 27 | } 28 | } 29 | 30 | render() { 31 | return ( 32 | 33 | 34 | 35 | 39 | 50 | 51 | 52 | 53 |
54 | 55 | 56 | 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /website/pages/go.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import { Header } from "../components/header"; 4 | import { Wizard } from "../components/wizard"; 5 | import { Donate } from "../components/donate"; 6 | 7 | export default function Go() { 8 | return ( 9 |
10 | 11 | Create a session - Tunshell 12 | 13 | 14 |
15 | 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /website/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Head from "next/head"; 3 | import { Header } from "../components/header"; 4 | import { Hero } from "../components/hero"; 5 | import styled from "styled-components"; 6 | import { Link } from "../components/link"; 7 | import { Button } from "../components/button"; 8 | import { Footer } from "../components/footer"; 9 | 10 | const Main = styled.main` 11 | height: 100%; 12 | min-height: min-content; 13 | display: flex; 14 | flex-direction: column; 15 | `; 16 | 17 | const CTA = styled.div` 18 | width: 100%; 19 | display: flex; 20 | justify-content: center; 21 | margin-bottom: 30px; 22 | 23 | > *:first-child { 24 | margin-right: 15px; 25 | } 26 | 27 | button { 28 | font-size: 18px; 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | } 34 | `; 35 | 36 | export default function Home() { 37 | return ( 38 |
39 | 40 | Tunshell - Remote shell into ephemeral environments 41 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /website/pages/term.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Head from "next/head"; 3 | import { Header } from "../components/header"; 4 | import { DirectWebSession } from "../components/direct-web-term"; 5 | 6 | export default function Web() { 7 | return ( 8 |
9 | 10 | Direct Web Session - Tunshell 11 | 12 | 13 |
14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /website/post-export.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # We move the static HTML files to the exact path we want to serve them 4 | # when loading the website via CloudFront. 5 | # Typically this is handled by using an S3 website origin which does this sort of mapping. 6 | # However, since we are asking people to run scripts on their machines, 7 | # we want to enforce TLS to the origin server for E2E security. 8 | # S3 websites do not support this: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-https-cloudfront-to-s3-origin.html 9 | # So we use a standard S3 origin and ensure the S3 paths are exactly what is reflected in the final URL. 10 | 11 | mv out/go.html out/go 12 | mv out/term.html out/term 13 | mv out/404.html out/404 -------------------------------------------------------------------------------- /website/public/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/public/robots.txt: -------------------------------------------------------------------------------- 1 | Disallow: / -------------------------------------------------------------------------------- /website/services/api-client.ts: -------------------------------------------------------------------------------- 1 | import { RelayServer } from "./location"; 2 | 3 | export interface CreateSessionResponse { 4 | peer1Key: string; 5 | peer2Key: string; 6 | } 7 | 8 | export class ApiClient { 9 | createSession = async (relayServer: RelayServer): Promise => { 10 | const response = await fetch(`https://${relayServer.domain}/api/sessions`, { 11 | method: "POST", 12 | }).then((i) => i.json()); 13 | 14 | return { 15 | peer1Key: response.peer1_key, 16 | peer2Key: response.peer2_key, 17 | }; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /website/services/direct-web-url.ts: -------------------------------------------------------------------------------- 1 | import { RELAY_SERVERS } from "./location"; 2 | import { SessionKeys } from "./session"; 3 | 4 | export class WebUrlService { 5 | public createWebUrl = (session: SessionKeys): string => { 6 | return `${window.location.origin}/term#${session.localKey},${session.encryptionSecret},${session.relayServer.domain}`; 7 | }; 8 | 9 | public parseWebUrl = (urlString: string): SessionKeys | null => { 10 | const url = new URL(urlString); 11 | 12 | if (!url.hash) { 13 | return null; 14 | } 15 | 16 | const parts = url.hash.substring(1).split(","); 17 | 18 | if (parts.length < 3) { 19 | return null; 20 | } 21 | 22 | const [localKey, encryptionSecret, relayServerDomain] = parts; 23 | const relayServer = RELAY_SERVERS.find((i) => i.domain === relayServerDomain); 24 | 25 | if (!relayServer) { 26 | return null; 27 | } 28 | 29 | return { localKey, targetKey: "", encryptionSecret, relayServer }; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /website/services/location.ts: -------------------------------------------------------------------------------- 1 | export interface RelayServer { 2 | label: string; 3 | domain: string; 4 | default: boolean; 5 | } 6 | 7 | export const RELAY_SERVERS: RelayServer[] = [ 8 | { label: "US", domain: "relay.tunshell.com", default: true }, 9 | { label: "AU", domain: "au.relay.tunshell.com", default: false }, 10 | { label: "UK", domain: "eu.relay.tunshell.com", default: false }, 11 | ]; 12 | 13 | export class LocationService { 14 | public findNearestRelayServer = async (): Promise => { 15 | const response = await fetch("https://nearest.relay.tunshell.com/api/info").then((i) => i.json()); 16 | 17 | return RELAY_SERVERS.find((i) => i.domain === response.domain_name); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /website/services/session.ts: -------------------------------------------------------------------------------- 1 | import { ApiClient, CreateSessionResponse } from "./api-client"; 2 | import { RelayServer } from "./location"; 3 | 4 | export interface SessionKeys { 5 | targetKey: string; 6 | localKey: string; 7 | encryptionSecret: string; 8 | relayServer: RelayServer; 9 | } 10 | 11 | export class SessionService { 12 | private readonly api = new ApiClient(); 13 | 14 | public createSessionKeys = async (relayServer: RelayServer): Promise => { 15 | let keys = await this.api.createSession(relayServer); 16 | keys = this.randomizeSessionKeys(keys); 17 | 18 | return { 19 | targetKey: keys.peer1Key, 20 | localKey: keys.peer2Key, 21 | encryptionSecret: this.generateEncryptionSecret(), 22 | relayServer, 23 | }; 24 | }; 25 | 26 | // To ensure the relay server does not know which 27 | // key is assigned to the local/target hosts we 28 | // randomise them here 29 | private randomizeSessionKeys = (keys: CreateSessionResponse): CreateSessionResponse => { 30 | const flip = Math.random() >= 0.5; 31 | 32 | return flip 33 | ? { 34 | peer1Key: keys.peer2Key, 35 | peer2Key: keys.peer1Key, 36 | } 37 | : { 38 | peer1Key: keys.peer1Key, 39 | peer2Key: keys.peer2Key, 40 | }; 41 | }; 42 | 43 | // Generates a secure secret for each of the clients 44 | // Key: 22 alphanumeric chars (131 bits of entropy) 45 | private generateEncryptionSecret = (): string => { 46 | const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 47 | 48 | const gen = (len: number): string => { 49 | const buff = new Uint8Array(len); 50 | window.crypto.getRandomValues(buff); 51 | let out = ""; 52 | 53 | for (let i = 0; i < len; i++) { 54 | out += alphanumeric[buff[i] % alphanumeric.length]; 55 | } 56 | 57 | return out; 58 | }; 59 | 60 | return gen(22); 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /website/services/tunshell-wasm.ts: -------------------------------------------------------------------------------- 1 | import { TerminalEmulator } from "./wasm/tunshell_client"; 2 | import { SessionKeys } from "./session"; 3 | 4 | type ClientModule = typeof import("./wasm/tunshell_client"); 5 | 6 | export class TunshellWasm { 7 | private module: ClientModule; 8 | private terminateCallback: () => void | undefined; 9 | 10 | constructor() {} 11 | 12 | init = async () => { 13 | let module = await import("./wasm/tunshell_client").catch((e) => { 14 | console.error(e); 15 | throw e; 16 | }); 17 | this.module = module; 18 | return this; 19 | }; 20 | 21 | connect = async (session: SessionKeys, emulator: TerminalEmulator) => { 22 | let terminatePromise = new Promise((resolve) => { 23 | this.terminateCallback = resolve; 24 | }); 25 | 26 | const config = new this.module.BrowserConfig( 27 | session.localKey, 28 | session.encryptionSecret, 29 | session.relayServer.domain, 30 | emulator, 31 | terminatePromise 32 | ); 33 | 34 | console.log(`Initialising client...`); 35 | await this.module.tunshell_init_client(config); 36 | console.log(`Client session finished...`); 37 | }; 38 | 39 | terminate = () => { 40 | if (this.terminateCallback) { 41 | this.terminateCallback(); 42 | } 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /website/theme/colours.tsx: -------------------------------------------------------------------------------- 1 | export const COLOURS = { 2 | BLACK: "#000", 3 | OFF_BLACK: "#333", 4 | WHITE: "#fff", 5 | OFF_WHITE: "#eee", 6 | 7 | TAN2: "#231c1c", 8 | TAN3: "#332a25", 9 | TAN4: "#bd9898", 10 | TAN5: "#4c423a", 11 | 12 | RED: "#ff7d7d", 13 | }; 14 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ], 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /website/typings/ionicons.d.ts: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | declare global { 4 | namespace JSX { 5 | interface IntrinsicElements { 6 | "ion-icon": HTMLAttributes & { name: string }; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /website/typings/react-typical.d.ts: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | declare module "react-typical" { 4 | const i: any; 5 | exports = i; 6 | } 7 | --------------------------------------------------------------------------------