├── .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 |
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 |
--------------------------------------------------------------------------------