Skip to content

Commit

Permalink
Added bore and ZeroTier tunnels
Browse files Browse the repository at this point in the history
  • Loading branch information
doki-nordic committed Dec 16, 2024
1 parent 9aed06f commit 493d7f6
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 18 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/playground.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,41 @@ on:
- bash
- pwsh
- cmd
term:
type: choice
description: HTTP terminal
options:
- bore
- zrok
- ZeroTier
- srv.us
- localhost.run
- ngrok
files:
type: choice
description: File browser
options:
- bore
- zrok
- ZeroTier
- srv.us
- localhost.run
- ngrok
ssh:
type: choice
description: SSH
options:
- bore
- zrok
- ZeroTier
- SSH-J.com
rdp:
type: choice
description: Remote Desktop
options:
- bore
- zrok
- ZeroTier
jobs:
action:
runs-on: ${{ github.event.inputs.os }}
Expand Down
24 changes: 19 additions & 5 deletions src/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,24 @@ ssh | 22 | 9922 | SSH + SFTP
rdp | 3389 | 9989 | Remote Desktop (Window only)


Secrets:
Secrets only:
* `PASSWORD`
* `ZROK_TOKEN`
* `NETWORK_ID`
* `ACCESS_TOKEN`
* Zrok
* `ZROK_TOKEN`
* ZeroTier
* `ZEROTIER_NETWORK_ID`
* `ZEROTIER_ACCESS_TOKEN`

Configuration (in `CONF` )

* `IP`
* `TERM_ENDPOINT`
* `TERM_PORT`
* `TERM_CLIENT_PORT`
* `FILES_ENDPOINT`
* `FILES_PORT`
* `FILES_CLIENT_PORT`
* `SSH_ENDPOINT`
* `SSH_CLIENT_PORT`
* `RDP_ENDPOINT`
* `RDP_CLIENT_PORT`
* `SSH_KEYS`
97 changes: 97 additions & 0 deletions src/lib/bore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@

import re
import time
import subprocess

import lib.conf as conf
from lib.proc import ProcessHandler
from lib.utils import CallOnce, download, untar, unzip
from lib.tunnel import ConnectionType, Tunnel

arch_file = conf.temp_dir / ('bore.zip' if conf.is_windows else 'bore.tar.gz')
exe_file = conf.temp_dir / ('bore.exe' if conf.is_windows else 'bore')

global_prepare_once = CallOnce()

def global_prepare(_checked=False):
if not _checked:
return global_prepare_once.call(global_prepare, True)
# Download if does not exist
if not exe_file.exists():
download(conf.bore_url, arch_file)
if conf.is_windows:
unzip(arch_file, exe_file.parent)
else:
untar(arch_file, exe_file.parent, 'gz')
if not exe_file.exists():
raise FileNotFoundError(f'Bore executable not found at "{exe_file}"')


class Bore(Tunnel):

_process: ProcessHandler

def __init__(self):
super().__init__()
self._process = None
self._buffer = b''
self._info = {}
self._info_ready = False
self._use_client_port = True
self.is_stopped = lambda: False

def setup(self, name: str, type: ConnectionType, port: int, endpoint: 'str|None', client_port: int):
super().setup(name, type, port, endpoint, client_port)
global_prepare()

def start(self):
self._buffer = b''
self._connect()
self._process.on_exit = self._process_on_exit
self._process.on_stdout = self._process_on_stdout

def stop(self):
if self._process:
self._process.terminate(10)

def _connect(self):
if self._use_client_port:
self._process = ProcessHandler([exe_file, 'local', '-t', 'bore.pub', '-p', str(self.client_port), str(self.port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE)
else:
self._process = ProcessHandler([exe_file, 'local', '-t', 'bore.pub', str(self.port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE)

def _process_on_exit(self, process: subprocess.Popen, forced: bool):
if forced:
self.is_stopped = lambda: True
else:
if self._use_client_port:
print(f'Bore exited with code {process.returncode} - retrying with random port.')
self._use_client_port = False
self.start()
else:
print(f'Bore exited unexpectedly with code {process.returncode}')
time.sleep(1)
self.start()

def is_started(self):
return self._info_ready

def _process_on_stdout(self, data: bytes):
if self._info_ready or (len(data) == 0):
return
self._buffer += data
text = str(self._buffer, 'utf-8')
port = None
for m in re.finditer(r'remote_port=([0-9]+)|bore\.pub:([0-9]+)', text):
port = m.group(1) or m.group(2)
if port is not None:
self._info_ready = True
self._info = {
'host': 'bore.pub',
'port': port,
}
else:
self._info = { }

def get_info(self) -> dict[str, str]:
return self._info
7 changes: 7 additions & 0 deletions src/lib/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
'darwin': 'https://github.com/openziti/zrok/releases/download/v0.4.44/zrok_0.4.44_darwin_amd64.tar.gz',
}

bore_urls = {
'windows': 'https://github.com/ekzhang/bore/releases/download/v0.5.2/bore-v0.5.2-x86_64-pc-windows-msvc.zip',
'linux': 'https://github.com/ekzhang/bore/releases/download/v0.5.2/bore-v0.5.2-x86_64-unknown-linux-musl.tar.gz',
'darwin': 'https://github.com/ekzhang/bore/releases/download/v0.5.2/bore-v0.5.2-x86_64-apple-darwin.tar.gz',
}

ttyd_urls = {
'windows': 'https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.win32.exe',
'linux': 'https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64',
Expand Down Expand Up @@ -49,6 +55,7 @@
temp_dir = root_dir.parent

zrok_url = zrok_urls[system]
bore_url = bore_urls[system]
ttyd_url = ttyd_urls[system]

if not is_windows and not is_linux and not is_macos:
Expand Down
25 changes: 16 additions & 9 deletions src/lib/service_ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
from lib.tunnel import ConnectionType, Tunnel


is_docker = Path('/.dockerenv').exists()


def replace_host_keys(ssh_keys_dir: Path):
for dst_pub_file in ssh_keys_dir.glob('ssh_host_*_key.pub'):
dst_prv_file = dst_pub_file.with_suffix('')
Expand Down Expand Up @@ -53,12 +56,13 @@ def get_ssh_keys():

def stop_ssh_service(stop_cmd, check_cmd, check_re):
subprocess.run(stop_cmd)
for _ in range(15):
time.sleep(1)
result = subprocess.run(check_cmd, stdout=subprocess.PIPE, check=True)
port_list = str(result.stdout, 'utf-8')
if re.search(check_re, port_list) is None:
break
if not is_docker:
for _ in range(15):
time.sleep(1)
result = subprocess.run(check_cmd, stdout=subprocess.PIPE, check=True)
port_list = str(result.stdout, 'utf-8')
if re.search(check_re, port_list) is None:
break


class ServiceSSH(Service):
Expand All @@ -73,7 +77,7 @@ def setup(self, tunnel: Tunnel):
elif conf.is_linux:
subprocess.run([conf.sudo, 'systemctl', 'disable', '--now', 'ssh.socket'])
stop_ssh_service(
[conf.sudo, 'systemctl', 'stop', 'ssh'],
[conf.sudo, 'systemctl', 'stop', 'ssh'] if not is_docker else [conf.sudo, 'service', 'ssh', 'stop'],
['netstat', '-lt'],
r':(22|ssh)\s'
)
Expand Down Expand Up @@ -118,8 +122,11 @@ def start(self):
if conf.is_windows:
subprocess.run(['net', 'start', 'sshd'], check=True)
elif conf.is_linux:
subprocess.run([conf.sudo, 'systemctl', 'enable', '--now', 'ssh.socket'])
subprocess.run([conf.sudo, 'systemctl', 'start', 'ssh'], check=True)
if not is_docker:
subprocess.run([conf.sudo, 'systemctl', 'enable', '--now', 'ssh.socket'])
subprocess.run([conf.sudo, 'systemctl', 'start', 'ssh'], check=True)
else:
subprocess.run([conf.sudo, 'service', 'ssh', 'start'], check=True)
elif conf.is_macos:
subprocess.run([conf.sudo, 'launchctl', 'load', '/System/Library/LaunchDaemons/ssh.plist'])

Expand Down
5 changes: 5 additions & 0 deletions src/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pickle
import random
import tarfile
import zipfile
import threading
import subprocess
import urllib.request
Expand All @@ -24,6 +25,10 @@ def untar(file: Path, output: Path, compression: 'Literal["gz", "xz", "bz2"]|Non
with tarfile.open(file, 'r' if compression is None else 'r:' + compression) as tar:
tar.extractall(path=output)

def unzip(file: Path, output: Path) -> None:
with zipfile.ZipFile(file, 'r') as zf:
zf.extractall(output)


polling_objects = set()

Expand Down
31 changes: 31 additions & 0 deletions src/lib/zerotier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

from os import environ

import lib.conf as conf
from lib.tunnel import ConnectionType, Tunnel

class ZeroTier(Tunnel):

def __init__(self):
super().__init__()

def setup(self, name: str, type: ConnectionType, port: int, endpoint: 'str|None', client_port: int):
super().setup(name, type, port, endpoint, client_port)

def start(self):
pass

def stop(self):
pass

def is_started(self):
return True

def is_stopped(self):
return True

def get_info(self) -> dict[str, str]:
return {
'host': environ['_PLAYGROUND_IGNORE_IP'] if '_PLAYGROUND_IGNORE_IP' in environ else conf.get_value('IP', 'unknown.host'),
'port': self.port,
}
10 changes: 6 additions & 4 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,28 @@
from lib.utils import poll_objects
from lib.tunnel import ConnectionType
from lib.zrok import Zrok
from lib.bore import Bore
from lib.zerotier import ZeroTier
from lib.service import Service
from lib.service_rdp import ServiceRDP
from lib.service_ssh import ServiceSSH

services: 'list[Service]' = []

t = ServiceTerm()
t.setup(Zrok())
t.setup(ZeroTier())
services.append(t)

f = ServiceFiles()
f.setup(Zrok())
f.setup(ZeroTier())
services.append(f)

rdp = ServiceRDP()
rdp.setup(Zrok())
rdp.setup(ZeroTier())
services.append(rdp)

ssh = ServiceSSH()
ssh.setup(Zrok())
ssh.setup(ZeroTier())
services.append(ssh)


Expand Down

0 comments on commit 493d7f6

Please sign in to comment.