├── .eslintrc.json ├── Dockerfile ├── styles ├── Home.module.css ├── VNC.module.css └── globals.css ├── public ├── favicon.ico ├── vercel.svg ├── thirteen.svg └── next.svg ├── .gitmodules ├── pages ├── _app.tsx ├── _document.tsx └── index.tsx ├── .gitignore ├── tsconfig.json ├── next.config.js ├── package.json ├── LICENSE ├── .github └── workflows │ ├── deploy.yaml │ └── preview.yaml ├── README.md └── components └── vnc.tsx /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM catthehacker/ubuntu:act-latest 2 | RUN npm install -g yarn@latest -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100vh; 3 | height: 100vh; 4 | } 5 | -------------------------------------------------------------------------------- /styles/VNC.module.css: -------------------------------------------------------------------------------- 1 | .vnc { 2 | width: 100vw; 3 | height: 100vh; 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conblem/tailvnc/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tailscale"] 2 | path = tailscale 3 | url = https://github.com/conblem/tailscale.git 4 | branch = working 5 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | /public/main.wasm 39 | 40 | .env -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules", "tailscale"] 23 | } 24 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const CopyPlugin = require('copy-webpack-plugin') 4 | const path = require('path'); 5 | 6 | const nextConfig = { 7 | reactStrictMode: true, 8 | compiler: { 9 | styledComponents: true, 10 | }, 11 | webpack: (config, {isServer}) => { 12 | if (isServer) { 13 | return config 14 | } 15 | config.plugins.push(new CopyPlugin({ 16 | patterns: [ 17 | { 18 | from: path.resolve(__dirname, "tailscale/cmd/tsconnect/pkg/main.wasm"), 19 | to: path.resolve(__dirname, "public/main.wasm")}, 20 | ] 21 | })); 22 | return config 23 | } 24 | } 25 | 26 | module.exports = nextConfig 27 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { Inter } from 'next/font/google' 3 | import dynamic from "next/dynamic"; 4 | import styles from '@/styles/Home.module.css' 5 | 6 | const inter = Inter({ subsets: ['latin'] }) 7 | 8 | // we load this with ssr false as it does not support server side rendering 9 | const VNC = dynamic(() => import('@/components/vnc').then(mod => mod.VNC), {ssr: false}); 10 | 11 | export default function Home() { 12 | return ( 13 | <> 14 | 15 | tailvnc 16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailvnc", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "act": "act --secret-file .env -P ubuntu-latest=conblem/act --container-architecture linux/amd64" 11 | }, 12 | "dependencies": { 13 | "@novnc/novnc": "^1.4.0", 14 | "@types/node": "18.15.3", 15 | "@types/react": "18.0.28", 16 | "@types/react-dom": "18.0.11", 17 | "eslint": "8.36.0", 18 | "eslint-config-next": "13.2.4", 19 | "next": "13.2.4", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "styled-components": "^5.3.10", 23 | "tsconnect": "file:./tailscale/cmd/tsconnect/pkg", 24 | "typescript": "5.0.2" 25 | }, 26 | "devDependencies": { 27 | "@types/styled-components": "^5.1.26", 28 | "copy-webpack-plugin": "^11.0.0", 29 | "vercel": "^29.1.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023 conblem. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Vercel Production Deployment 2 | 3 | env: 4 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 5 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | Deploy-Production: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: 'recursive' 19 | - uses: actions/cache@v3 20 | id: tailscale-go-cache 21 | with: 22 | path: ~/.cache 23 | key: ${{ runner.os }}-tailscale-go-2-${{ hashFiles('tailscale/go.mod', 'tailscale/tool/yarn.rev', 'tailscale/tool/node.rev', 'tailscale/tool/binaryen.rev') }} 24 | - name: Build tsconnect package 25 | run: | 26 | cd tailscale 27 | ./tool/go run ./cmd/tsconnect build-pkg 28 | - name: Get yarn cache directory 29 | id: yarn-cache-dir 30 | shell: bash 31 | run: echo "dir=$(yarn cache dir)" >> ${GITHUB_OUTPUT} 32 | - uses: actions/cache@v3 33 | id: yarn-cache # use this to check for `cache-hit` ==> if: steps.yarn-cache.outputs.cache-hit != 'true' 34 | with: 35 | path: ${{ steps.yarn-cache-dir.outputs.dir }} 36 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-yarn- 39 | - name: Install deps 40 | run: yarn install 41 | - name: Pull Vercel Environment Information 42 | run: yarn vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} 43 | - name: Build Project Artifacts 44 | run: yarn vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} 45 | - name: Deploy Project Artifacts to Vercel 46 | id: vercel-deploy 47 | run: yarn -s vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | ### This is very much work in progress 3 | 4 | This is a prototype VNC client (based on [noVNC](https://novnc.com/info.html)) that fully runs in the browser and uses tailscale wasm to connect to the remote computer. 5 | The work is based on [my PR](https://github.com/tailscale/tailscale/pull/8047) to tailscale that adds TCP support to the tsconnect package. 6 | Even tho [the PR](https://github.com/tailscale/tailscale/pull/8047) links to the [vnc branch](https://github.com/conblem/tailscale/tree/vnc) this uses the [working branch](https://github.com/conblem/tailscale/tree/working). 7 | This is because the [vnc branch](https://github.com/conblem/tailscale/tree/vnc) is based on the [main develop branch](https://github.com/tailscale/tailscale) of tailscale which doesn't seem super stable. 8 | I backported my changes to a stable tag of tailscale in the [working branch](https://github.com/conblem/tailscale/tree/working). 9 | 10 | I am not ascoiated with tailscale in any way. 11 | 12 | # Getting Started 13 | First make sure to clone the tailscale submodule 14 | ```bash 15 | git pull --recurse-submodules 16 | ``` 17 | 18 | Afterwards build the tsconnect package, for this you have to cd into the tailscale folder 19 | ```bash 20 | cd tailscale 21 | ./tool/go run ./cmd/tsconnect build-pkg 22 | ``` 23 | 24 | Now you can start the server just like any other next.js project 25 | ```bash 26 | npm install 27 | npm run dev 28 | ``` 29 | 30 | To change the ip of your vnc server and the password edit the [index.tsx file](https://github.com/conblem/tailvnc/blob/main/pages/index.tsx) in the pages folder. 31 | Once done open the browser and go to [localhost:3000](http://localhost:3000/) and open your console. 32 | There should be a message saying "needsLogin" or something similiar, copy the url into a seperate tab and sign in with your tailscale account. 33 | If everything goes as planned you should now see your remote desktop in the browser. -------------------------------------------------------------------------------- /.github/workflows/preview.yaml: -------------------------------------------------------------------------------- 1 | name: Vercel Preview Deployment 2 | 3 | env: 4 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 5 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} 6 | 7 | on: 8 | pull_request: 9 | 10 | jobs: 11 | Deploy-Preview: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | pull-requests: write 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: 'recursive' 19 | - uses: actions/cache@v3 20 | id: tailscale-go-cache 21 | with: 22 | path: ~/.cache 23 | key: ${{ runner.os }}-tailscale-go-2-${{ hashFiles('tailscale/go.mod', 'tailscale/tool/yarn.rev', 'tailscale/tool/node.rev', 'tailscale/tool/binaryen.rev') }} 24 | - name: Build tsconnect package 25 | run: | 26 | cd tailscale 27 | ./tool/go run ./cmd/tsconnect build-pkg 28 | - name: Get yarn cache directory 29 | id: yarn-cache-dir 30 | shell: bash 31 | run: echo "dir=$(yarn cache dir)" >> ${GITHUB_OUTPUT} 32 | - uses: actions/cache@v3 33 | id: yarn-cache # use this to check for `cache-hit` ==> if: steps.yarn-cache.outputs.cache-hit != 'true' 34 | with: 35 | path: ${{ steps.yarn-cache-dir.outputs.dir }} 36 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-yarn- 39 | - name: Install deps 40 | run: yarn install 41 | - name: Pull Vercel Environment Information 42 | run: yarn vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} 43 | - name: Build Project Artifacts 44 | run: yarn vercel build --token=${{ secrets.VERCEL_TOKEN }} 45 | - name: Deploy Project Artifacts to Vercel 46 | id: vercel-deploy 47 | run: | 48 | yarn -s vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} > deployment-url.txt 49 | echo "deployment-url=$(cat deployment-url.txt)" >> ${GITHUB_OUTPUT} 50 | - name: Add deployment url to PR 51 | uses: mshick/add-pr-comment@v2 52 | with: 53 | message: | 54 | The current preview can be found at 🌍: 55 | ${{ steps.vercel-deploy.outputs.deployment-url }} 56 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | body { 89 | color: rgb(var(--foreground-rgb)); 90 | background: linear-gradient( 91 | to bottom, 92 | transparent, 93 | rgb(var(--background-end-rgb)) 94 | ) 95 | rgb(var(--background-start-rgb)); 96 | } 97 | 98 | a { 99 | color: inherit; 100 | text-decoration: none; 101 | } 102 | 103 | @media (prefers-color-scheme: dark) { 104 | html { 105 | color-scheme: dark; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /components/vnc.tsx: -------------------------------------------------------------------------------- 1 | import {createIPN} from "tsconnect"; 2 | import {useCallback, useEffect, useRef, useState} from "react"; 3 | // @ts-ignore 4 | import * as Log from "@novnc/novnc/core/util/logging"; 5 | // @ts-ignore 6 | import RFB from "@novnc/novnc/core/rfb"; 7 | import styles from '@/styles/VNC.module.css' 8 | 9 | export const localStorage: IPNStateStorage = { 10 | setState(id, value) { 11 | window.localStorage.setItem(`ipn-state-${id}`, value) 12 | }, 13 | getState(id) { 14 | return window.localStorage.getItem(`ipn-state-${id}`) || "" 15 | }, 16 | } 17 | 18 | interface Starting { 19 | state: "starting"; 20 | } 21 | 22 | interface RunningState { 23 | state: "running"; 24 | ipn: IPN 25 | } 26 | 27 | interface RunningWithNetMap { 28 | state: "runningWithNetMap"; 29 | ipn: IPN; 30 | netMap: string; 31 | } 32 | 33 | interface WaitingForLogin { 34 | state: "waitingForLogin"; 35 | url: string 36 | } 37 | 38 | interface Error { 39 | state: "error"; 40 | error: string; 41 | } 42 | 43 | type State = Starting | WaitingForLogin | RunningState | RunningWithNetMap | Error; 44 | 45 | let didIPNInit = false; 46 | 47 | function useIPN(): State { 48 | const [state, setState] = useState({state: "starting"}); 49 | const [ipnState, setIpnState] = useState(); 50 | const [ipn, setIpn] = useState(); 51 | 52 | useEffect(() => { 53 | if(didIPNInit) return; 54 | 55 | didIPNInit = true; 56 | (async () => { 57 | 58 | // authKey should probably be optional 59 | // @ts-ignore 60 | const emptyAuthKey: string = undefined as string; 61 | const ipn = await createIPN({ 62 | stateStorage: localStorage, 63 | routeAll: true, 64 | authKey: emptyAuthKey, 65 | panicHandler: (error: string) => setState({ state: "error", error }), 66 | }); 67 | ipn.run({ 68 | notifyNetMap: (netMap: string) => { 69 | if(state.state !== "running") { 70 | console.log("invalid state to set netmap") 71 | return; 72 | }; 73 | setState({ state: "runningWithNetMap", ipn: state.ipn, netMap }); 74 | }, 75 | notifyPanicRecover: (error: string) => setState({ state: "error", error }), 76 | notifyState: (ipnState: IPNState) => { 77 | setIpnState(ipnState) 78 | if(state.state !== "running" && ipnState === "Running") { 79 | setState({ state: "running", ipn }); 80 | } 81 | }, 82 | notifyBrowseToURL: (url: string) => { 83 | if(ipnState === "Running") return; 84 | setState({ state: "waitingForLogin", url }); 85 | }, 86 | }); 87 | 88 | setIpn(ipn); 89 | })(); 90 | 91 | }); 92 | 93 | useEffect(() => { 94 | if (ipnState === "NeedsLogin") { 95 | ipn?.login(); 96 | } 97 | }, [ipnState, ipn]); 98 | 99 | return state; 100 | } 101 | const DEFAULT_VNC_PORT = 5900; 102 | 103 | function useRFB(host: string, port: number, password: string, ipn?: IPN, div?: HTMLDivElement) { 104 | useEffect(() => { 105 | if(ipn === undefined || div === undefined) { 106 | return 107 | } 108 | 109 | let rawChannel: TailscaleRawChannel | undefined; 110 | let rfb: RFB | undefined; 111 | (async () => { 112 | rawChannel = await TailscaleRawChannel.connect(ipn, host, port || DEFAULT_VNC_PORT); 113 | Log.initLogging('debug'); 114 | rfb = new RFB(div, rawChannel, { credentials: { password }}); 115 | rfb.scaleViewport = true; 116 | })(); 117 | 118 | return () => { 119 | if(rfb != undefined) { 120 | rfb.disconnect(); 121 | return; 122 | } 123 | rawChannel?.close(); 124 | } 125 | }, [host, port, password, ipn, div]) 126 | } 127 | 128 | export function VNC({host, port, password}: {host: string, port?: number, password: string}) { 129 | const ipnState = useIPN(); 130 | const [ipn, setIPN] = useState(); 131 | 132 | const [div, setDiv] = useState(); 133 | const ref = useCallback((node: HTMLDivElement) => setDiv(node), []); 134 | 135 | // only update ipn if it changes 136 | useEffect(() => { 137 | if(ipnState.state === "waitingForLogin") { 138 | console.log("login", ipnState.url) 139 | window.open(ipnState.url, '_blank')?.focus(); 140 | return 141 | } 142 | if(!(ipnState.state == "running" || ipnState.state == "runningWithNetMap")) { 143 | console.log(ipnState); 144 | return; 145 | } 146 | if(ipnState.ipn === ipn) { 147 | return 148 | } 149 | setIPN(ipnState.ipn); 150 | }, [ipnState, ipn]); 151 | 152 | useRFB(host, port || DEFAULT_VNC_PORT, password, ipn, div) 153 | 154 | return
155 | } 156 | 157 | class TailscaleRawChannel { 158 | static async connect(ipn: IPN, hostname: string, port: number): Promise { 159 | let readCallback = (_: Uint8Array) => {}; 160 | 161 | // we connect first 162 | const tcp = await ipn.tcp({ 163 | hostname, 164 | port, 165 | readCallback: data => readCallback(data), 166 | readBufferSizeInBytes: 4 * 1024 * 1024, 167 | }); 168 | 169 | const wrapper = new TailscaleRawChannel(tcp); 170 | // then reassign the onmessage handler 171 | // this is okay because vnc is a client first protocol 172 | readCallback = (data: Uint8Array) => wrapper.onmessage({ data }); 173 | 174 | return wrapper; 175 | } 176 | 177 | onopen: () => void = () => {}; 178 | onclose: (e: CloseEvent) => void = () => {}; 179 | onerror: (e: Event) => void = () => {}; 180 | onmessage: (e: { data: ArrayBuffer}) => void = () => {}; 181 | binaryType: "arraybuffer" = "arraybuffer"; 182 | protocol: "wss" = "wss"; 183 | readyState: "open" = "open"; 184 | 185 | private constructor(private readonly tcp: IPNTCPSession) { 186 | } 187 | 188 | send(data: Uint8Array) { 189 | this.tcp.write(data); 190 | } 191 | 192 | close() { 193 | this.tcp.close(); 194 | } 195 | } 196 | --------------------------------------------------------------------------------