Compare commits
252 Commits
55a17bb287
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bffff1d9b | |||
| 61b1dea366 | |||
| c94cbd7f36 | |||
| 618e023996 | |||
| 0104827234 | |||
| 92a97e140f | |||
| b9d573aeac | |||
| 3c438ca093 | |||
| d6bc7128df | |||
| 4b0659bd47 | |||
| 2351ad84bc | |||
| a16b5a232e | |||
| 0ffb0f7256 | |||
| 37ae3a06de | |||
| d0ff3536d2 | |||
| 0104b45331 | |||
| efc3ad00af | |||
| 7ac32e9199 | |||
| cb54689e7c | |||
| 992db16848 | |||
| 16b7a66c01 | |||
| 1def782149 | |||
| 55c11cce90 | |||
| ed21a14f68 | |||
| 004c58b445 | |||
| 8c9ebea489 | |||
| 19cb09f05e | |||
| ae86df8732 | |||
| a2f0b40fa8 | |||
| bf60412640 | |||
| 5f057c50ed | |||
| b2e38b3cb4 | |||
| 47e1790a8b | |||
| b67b8f18a4 | |||
| c6749fe856 | |||
| 4c7ff9d6a0 | |||
| e93edb73af | |||
| 9a6e2f67e6 | |||
| 84b1eb3f5d | |||
| 1bd5a21a94 | |||
| 5c3147536c | |||
| 8656441976 | |||
| 0d52d54eed | |||
| 1b59e304b0 | |||
| fb915487dc | |||
| a3b02b694e | |||
| 9b2a42bdc9 | |||
| 98f3cc95eb | |||
| 12f626b088 | |||
| b9b3f89910 | |||
| 24594915a9 | |||
| 6f6538fa73 | |||
| 239179931c | |||
| f1793baa57 | |||
| 418390fe8d | |||
| 4fd8688685 | |||
| f410510a7f | |||
| 274125e862 | |||
| 837dffddd5 | |||
| 1ef9d21ba4 | |||
| 9307cab1bb | |||
| e462edd99b | |||
| 48d0407c79 | |||
| 3c427af6fe | |||
| 5d54ed6f80 | |||
| 685488c7d4 | |||
| ec40aa2ec1 | |||
| 498926ae5d | |||
| 046552d09a | |||
| 27c46542e8 | |||
| 0c952a2623 | |||
| 50cdd3ea1c | |||
| 7c76016fcf | |||
| 0dca06fbc8 | |||
| 4c067c14d8 | |||
| 529842a411 | |||
| 99847c3ff0 | |||
| 96d4e32672 | |||
| c3581c7ecc | |||
| 657081678f | |||
| 89e8f5cffc | |||
| 5e0004c0d8 | |||
| aa071a1fdb | |||
| 2b9688ce44 | |||
| fa398e8c86 | |||
| c7cf401b0a | |||
| 38530ea0df | |||
| c933d6e6da | |||
| e4b648e447 | |||
| e5d4b4d2e5 | |||
| 35e0d67446 | |||
| 747f71e27c | |||
| 6e5656a00b | |||
| ab004b78eb | |||
| b42bac35bb | |||
| 7b874169cb | |||
| 2f7a51d2b7 | |||
| 6097bcbe8f | |||
| f1fd3572c5 | |||
| 3d715664db | |||
| 31cb8f6db5 | |||
| abe7a7ab08 | |||
| 46fe4f5557 | |||
| 35e4f1f6b7 | |||
| 767526054e | |||
| 69062bd828 | |||
| b3259d2981 | |||
| dc463f4cf0 | |||
| 74f4ab982e | |||
| 070e358463 | |||
| 99fc5a5600 | |||
| ee6cb6c90d | |||
| b52ba03be4 | |||
| 38c4eea2f0 | |||
| 838b33d6c5 | |||
| 470517a00f | |||
| d44a3c1a3b | |||
| 865a96c2ec | |||
| 21bbeef579 | |||
| 8523c79999 | |||
| 96153fa557 | |||
| 4d1bc9dbd0 | |||
| ca826a6229 | |||
| a7691b16f0 | |||
| d87429f98d | |||
| ccd1fbc52f | |||
| b0ba737d0d | |||
| 33aa6d361e | |||
| 1eee23953c | |||
| 1c548281b0 | |||
| 38ebb88ac6 | |||
| e4fa0ba9cd | |||
| cb50b25081 | |||
| 6d8015bdc9 | |||
| 982a4bbff9 | |||
| fa571c9ccd | |||
| ef0058c93f | |||
| e2e7c44bf6 | |||
| 3aadd164f0 | |||
| d6cafc67b2 | |||
| 8b9b6e798a | |||
| b55708721c | |||
| 3e50f5a465 | |||
| e13cc2b851 | |||
| 6e1d7efa6d | |||
| ed48c37706 | |||
| 0d773fba51 | |||
| f1ba1f050d | |||
| 62ca42bb18 | |||
| bc34fad485 | |||
| 500b5f4045 | |||
| 24d28c649c | |||
| 6fd57b0ce2 | |||
| 2dae9f667e | |||
| cb7e309915 | |||
| 55ca6fe620 | |||
| 5478623d19 | |||
| 302c52c784 | |||
| 0020c539ea | |||
| 6c790f84aa | |||
| 0ab6bb934d | |||
| a5871d399b | |||
| 9c2c67fbe1 | |||
| b6a6623406 | |||
| beedccdc29 | |||
| 11aaf00d8d | |||
| 21c1fa5d9a | |||
| 509814f3a8 | |||
| 203e04101b | |||
| 2d98ca843f | |||
| 919f5904f8 | |||
| ea2a1ba94d | |||
| 776666a77a | |||
| 9bde91f047 | |||
| 03c1181186 | |||
| 022c455334 | |||
| 0461ffec7c | |||
| f9c17c644a | |||
| 68a9af9331 | |||
| 016e663eef | |||
| a343aecc0d | |||
| c76afb2380 | |||
| d22818669f | |||
| 5c9609b509 | |||
| 716a5bed7d | |||
| 1fb683ce68 | |||
| c9450ed5fc | |||
| 98e91e46aa | |||
| ab4fbeaef8 | |||
| 57baf4c289 | |||
| 4efb8d205f | |||
| 3a674bc44d | |||
| 25a1985d20 | |||
| dd2d860243 | |||
| ece0198ff5 | |||
| c6903bb2a0 | |||
| b96e3a46bf | |||
| 7b0065f75a | |||
| 2aba0ee03b | |||
| a17fe5843d | |||
| 06662a294b | |||
| a4c3f0bd9a | |||
| 6a04bd911a | |||
| ce2a91e7e3 | |||
| d4acbfa5fc | |||
| b71fe3bf95 | |||
| 51feea00bb | |||
| 3da3578d08 | |||
| cfb6b04563 | |||
| bed2e37610 | |||
| 3214e387ef | |||
| eafbfca68f | |||
| 154b2bdd2c | |||
| e3a14dbaa7 | |||
| 73280f3bbf | |||
| 6b7efbe1da | |||
| be917b8a86 | |||
| 5d35bb88a9 | |||
| 90bce06b34 | |||
| b533c2ec53 | |||
| 3465a489f2 | |||
| 0086594368 | |||
| cfe1080cb0 | |||
| a3d18358db | |||
| 60855d6a85 | |||
| 4b404714f9 | |||
| 2dfe201c82 | |||
| 6abe3aef77 | |||
| e103847796 | |||
| 2fe54fc7f0 | |||
| 971a888fea | |||
| f992425e96 | |||
| 9192c62a9e | |||
| 0aadef4705 | |||
| 27b496893f | |||
| 20b55311f1 | |||
| f878882718 | |||
| e7f8028e83 | |||
| a472778a83 | |||
| 5ab97f3ffa | |||
| 095db04a79 | |||
| 14a0fea9b6 | |||
| 9c7e67aab5 | |||
| 10d1fae351 | |||
| 021402de69 | |||
| 59052ad5ba | |||
| d51e70ba57 | |||
| a79f1e5a07 | |||
| cb53a0ea22 | |||
| 99c276c531 | |||
| dd168f0059 | |||
| 85c1fec4cf |
45
.github/workflows/renovate.yml
vendored
@@ -1,45 +0,0 @@
|
|||||||
name: Renovate
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
log_level:
|
|
||||||
type: choice
|
|
||||||
description: Log level
|
|
||||||
default: INFO
|
|
||||||
options:
|
|
||||||
- DEBUG
|
|
||||||
- INFO
|
|
||||||
- WARN
|
|
||||||
- ERROR
|
|
||||||
- FATAL
|
|
||||||
schedule:
|
|
||||||
- cron: 0 2 * * *
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
renovate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
|
|
||||||
- name: Install bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Cache Bun global packages
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.bun/install/global
|
|
||||||
key: ${{ runner.os }}-bun-global-renovate-40
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-global-
|
|
||||||
|
|
||||||
- name: Install Renovate
|
|
||||||
run: bun install -g renovate@40
|
|
||||||
|
|
||||||
- name: Run renovate
|
|
||||||
run: LOG_LEVEL=${{ github.event.inputs.log_level || 'INFO' }} renovate --token ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }}
|
|
||||||
23
.github/workflows/test.yml
vendored
@@ -1,23 +0,0 @@
|
|||||||
name: Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: bun test
|
|
||||||
3
.gitignore
vendored
@@ -1 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
# Never commit SSH bundles / secrets
|
||||||
|
**/ssh.zip
|
||||||
|
|||||||
42
CLAUDE.md
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
# Runtipi Development Guidelines
|
||||||
|
|
||||||
|
## App-Specific Context Files
|
||||||
|
|
||||||
|
## Deployment Workflow
|
||||||
|
|
||||||
|
**IMPORTANT:** Always follow this workflow when deploying changes to runtipi apps:
|
||||||
|
|
||||||
|
1. Push changes to the repo:
|
||||||
|
```bash
|
||||||
|
cd /etc/runtipi/repos/runtipi
|
||||||
|
git add . && git commit -m "message" && git push
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update the appstore to pull latest changes:
|
||||||
|
```bash
|
||||||
|
sudo runtipi-cli appstore update
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Stop the app:
|
||||||
|
```bash
|
||||||
|
sudo runtipi-cli app stop <APP_NAME>:runtipi
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Start the app:
|
||||||
|
```bash
|
||||||
|
sudo runtipi-cli app start <APP_NAME>:runtipi
|
||||||
|
```
|
||||||
|
|
||||||
|
**NEVER** use `docker compose up/down` or `docker run` directly for runtipi apps unless debugging.
|
||||||
|
|
||||||
|
## App Naming Convention
|
||||||
|
|
||||||
|
Apps require the `:runtipi` namespace suffix when using runtipi-cli commands:
|
||||||
|
- `rego-tunnel-linux:runtipi`
|
||||||
|
- `cistech-tunnel:runtipi`
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
- `/etc/runtipi/repos/runtipi/apps/` - App definitions (config.json, docker-compose.yml, docker-compose.json, source/)
|
||||||
|
- `/etc/runtipi/app-data/` - Runtime app data (managed by runtipi)
|
||||||
|
- `/etc/runtipi/user-config/` - User configuration overrides
|
||||||
65
apps/cfddns/config.json
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../app-info-schema.json",
|
||||||
|
"name": "Cloudflare DDNS",
|
||||||
|
"id": "cfddns",
|
||||||
|
"available": true,
|
||||||
|
"exposable": false,
|
||||||
|
"dynamic_config": true,
|
||||||
|
"no_gui": true,
|
||||||
|
"tipi_version": 7,
|
||||||
|
"version": "latest",
|
||||||
|
"categories": [
|
||||||
|
"network",
|
||||||
|
"utilities"
|
||||||
|
],
|
||||||
|
"description": "Automatically update Cloudflare DNS records with your current public IP. Supports multiple domains, IPv4/IPv6, and proxy toggle.",
|
||||||
|
"short_desc": "Dynamic DNS updater for Cloudflare",
|
||||||
|
"author": "favonia",
|
||||||
|
"source": "https://github.com/favonia/cloudflare-ddns",
|
||||||
|
"form_fields": [
|
||||||
|
{
|
||||||
|
"type": "password",
|
||||||
|
"label": "Cloudflare API Token",
|
||||||
|
"env_variable": "CLOUDFLARE_API_TOKEN",
|
||||||
|
"required": true,
|
||||||
|
"hint": "API token with DNS edit permissions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Domains (comma-separated)",
|
||||||
|
"env_variable": "DOMAINS",
|
||||||
|
"required": true,
|
||||||
|
"hint": "e.g. home.example.com,vpn.example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Proxied (Orange Cloud)",
|
||||||
|
"env_variable": "PROXIED",
|
||||||
|
"required": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Update Schedule",
|
||||||
|
"env_variable": "UPDATE_CRON",
|
||||||
|
"required": false,
|
||||||
|
"default": "@every 5m",
|
||||||
|
"hint": "Cron expression or @every Xm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Timezone",
|
||||||
|
"env_variable": "TZ",
|
||||||
|
"required": false,
|
||||||
|
"default": "UTC"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported_architectures": [
|
||||||
|
"arm64",
|
||||||
|
"amd64"
|
||||||
|
],
|
||||||
|
"created_at": 1736974800000,
|
||||||
|
"updated_at": 1736974800000,
|
||||||
|
"deprecated": false,
|
||||||
|
"min_tipi_version": "4.5.0"
|
||||||
|
}
|
||||||
33
apps/cfddns/docker-compose.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"name": "cfddns",
|
||||||
|
"image": "favonia/cloudflare-ddns:latest",
|
||||||
|
"isMain": true,
|
||||||
|
"environment": [
|
||||||
|
{
|
||||||
|
"key": "CLOUDFLARE_API_TOKEN",
|
||||||
|
"value": "${CLOUDFLARE_API_TOKEN}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "DOMAINS",
|
||||||
|
"value": "${DOMAINS}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "PROXIED",
|
||||||
|
"value": "${PROXIED:-false}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "UPDATE_CRON",
|
||||||
|
"value": "${UPDATE_CRON:-@every 5m}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "TZ",
|
||||||
|
"value": "${TZ:-UTC}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
apps/cfddns/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
@@ -1,11 +0,0 @@
|
|||||||
# Required
|
|
||||||
OC_URL=https://vpn.cistech.net/Employees
|
|
||||||
OC_SERVERCERT=pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0=
|
|
||||||
|
|
||||||
# Optional
|
|
||||||
OC_AUTHGROUP=
|
|
||||||
OC_SSO_ARGS=--browser-display-mode shown
|
|
||||||
VNC_PASSWORD=vpnSSO12
|
|
||||||
NOVNC_PORT=6901
|
|
||||||
PUBLISH_ADDR=0.0.0.0
|
|
||||||
SSH_KEY_PATH=/home/alexz/.ssh/id_ed25519-lenovo
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Cistech Tunnel
|
|
||||||
|
|
||||||
OpenConnect-SSO VPN client running in a container with noVNC for browser-based access.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **OpenConnect-SSO**: Cisco AnyConnect VPN with SSO/SAML authentication
|
|
||||||
- **TOTP Support**: Automatic 2FA via keyring integration
|
|
||||||
- **Auto-reconnect**: Automatically reconnects on disconnection
|
|
||||||
- **noVNC**: Browser-based VNC access on port 6902
|
|
||||||
- **NAT/Masquerade**: Routes traffic through VPN tunnel
|
|
||||||
- **Cloudflared**: Optional Cloudflare tunnel support
|
|
||||||
- **SSH Tunnels**: Optional SSH port forwarding
|
|
||||||
|
|
||||||
## Runtipi Installation
|
|
||||||
|
|
||||||
1. Install from the app store or custom repo
|
|
||||||
2. Configure the required environment variables
|
|
||||||
3. Start the app via Runtipi dashboard
|
|
||||||
|
|
||||||
## First-time SSO Login
|
|
||||||
|
|
||||||
1. Open noVNC at `http://<host>:6902`
|
|
||||||
2. Enter VNC password
|
|
||||||
3. Complete SSO login in the browser window
|
|
||||||
4. VPN will connect and auto-reconnect on disconnect
|
|
||||||
|
|
||||||
## Source Files
|
|
||||||
|
|
||||||
- `source/Dockerfile`: Container build file
|
|
||||||
- `source/entrypoint.sh`: Container entrypoint with auto-reconnect
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
| Variable | Required | Description |
|
|
||||||
|----------|----------|-------------|
|
|
||||||
| OC_URL | Yes | VPN server URL |
|
|
||||||
| OC_SERVERCERT | Yes | Server certificate pin |
|
|
||||||
| OC_USER | No | Username (enables hidden browser mode) |
|
|
||||||
| VNC_PASSWORD | Yes | noVNC access password |
|
|
||||||
| OC_TOTP_SECRET | No | TOTP secret for auto 2FA |
|
|
||||||
| NOVNC_PORT | No | noVNC port (default: 6901) |
|
|
||||||
2
apps/cistech-tunnel/build/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Large binary files - track tar.gz but not 7z
|
||||||
|
*.7z
|
||||||
110
apps/cistech-tunnel/build/Dockerfile
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
LABEL maintainer="alexz"
|
||||||
|
LABEL description="OpenConnect-SSO VPN in Docker with noVNC"
|
||||||
|
LABEL version="1.0.0"
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV container=docker
|
||||||
|
|
||||||
|
# VNC/noVNC settings
|
||||||
|
ENV DISPLAY=:1
|
||||||
|
ENV VNC_PORT=5901
|
||||||
|
ENV NOVNC_PORT=6092
|
||||||
|
|
||||||
|
# Python/Playwright settings
|
||||||
|
ENV VIRTUAL_ENV=/opt/venv
|
||||||
|
ENV PATH=/opt/venv/bin:$PATH
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
# Core tools
|
||||||
|
openconnect \
|
||||||
|
vpnc-scripts \
|
||||||
|
iptables \
|
||||||
|
iproute2 \
|
||||||
|
iputils-ping \
|
||||||
|
net-tools \
|
||||||
|
procps \
|
||||||
|
curl \
|
||||||
|
nano \
|
||||||
|
ca-certificates \
|
||||||
|
# Python
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
# VNC/noVNC (novnc installed from GitHub below)
|
||||||
|
tigervnc-standalone-server \
|
||||||
|
tigervnc-common \
|
||||||
|
websockify \
|
||||||
|
x11vnc \
|
||||||
|
xvfb \
|
||||||
|
# Window manager & terminal
|
||||||
|
openbox \
|
||||||
|
fluxbox \
|
||||||
|
xterm \
|
||||||
|
# Automation tools
|
||||||
|
xdotool \
|
||||||
|
xclip \
|
||||||
|
oathtool \
|
||||||
|
# X11/GUI dependencies for browser
|
||||||
|
dbus \
|
||||||
|
dbus-x11 \
|
||||||
|
libgtk-3-0 \
|
||||||
|
libglib2.0-0 \
|
||||||
|
libnss3 \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libx11-6 \
|
||||||
|
libx11-xcb1 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libxrandr2 \
|
||||||
|
libgbm1 \
|
||||||
|
libxdamage1 \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libxkbcommon-x11-0 \
|
||||||
|
fonts-liberation \
|
||||||
|
# EGL/GL for PyQt6 WebEngine + software rendering
|
||||||
|
libegl1 \
|
||||||
|
libgl1 \
|
||||||
|
libopengl0 \
|
||||||
|
libdbus-1-3 \
|
||||||
|
mesa-utils \
|
||||||
|
libgl1-mesa-dri \
|
||||||
|
# XCB libraries for Qt6 (complete set)
|
||||||
|
libxcb1 \
|
||||||
|
libxcb-cursor0 \
|
||||||
|
libxcb-icccm4 \
|
||||||
|
libxcb-image0 \
|
||||||
|
libxcb-keysyms1 \
|
||||||
|
libxcb-render0 \
|
||||||
|
libxcb-render-util0 \
|
||||||
|
libxcb-shm0 \
|
||||||
|
libxcb-xfixes0 \
|
||||||
|
libxcb-xinerama0 \
|
||||||
|
libxcb-randr0 \
|
||||||
|
libxcb-glx0 \
|
||||||
|
libxcb-shape0 \
|
||||||
|
# sudo needed for openconnect-sso
|
||||||
|
sudo \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install noVNC from GitHub (v1.4.0 - stable release with ES6 modules)
|
||||||
|
RUN curl -fsSL https://github.com/novnc/noVNC/archive/refs/tags/v1.4.0.tar.gz | tar -xz -C /usr/share/ \
|
||||||
|
&& mv /usr/share/noVNC-1.4.0 /usr/share/novnc \
|
||||||
|
&& ln -sf /usr/share/novnc/vnc.html /usr/share/novnc/index.html
|
||||||
|
|
||||||
|
# Create Python venv and install openconnect-sso with all dependencies
|
||||||
|
RUN python3 -m venv "$VIRTUAL_ENV" && \
|
||||||
|
pip install --no-cache-dir --upgrade pip && \
|
||||||
|
pip install --no-cache-dir \
|
||||||
|
'openconnect-sso[full]' \
|
||||||
|
playwright \
|
||||||
|
keyring \
|
||||||
|
keyrings.alt
|
||||||
|
|
||||||
|
# Install Playwright browser (Chromium)
|
||||||
|
RUN python -m playwright install --with-deps chromium
|
||||||
67
apps/cistech-tunnel/build/README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Cistech Tunnel - Build Files
|
||||||
|
|
||||||
|
This directory contains the Dockerfile and scripts to build the OpenConnect-SSO VPN Docker image.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `Dockerfile` - Docker image definition (Ubuntu 22.04 + openconnect-sso + noVNC)
|
||||||
|
- `build.sh` - Build and push script
|
||||||
|
- `scripts/entrypoint.sh` - Container entrypoint
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /etc/runtipi/repos/runtipi/apps/cistech-tunnel/build
|
||||||
|
./build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This builds and pushes to `git.alexzaw.dev/alexz/openconnect-vpn:latest`
|
||||||
|
|
||||||
|
To build without pushing:
|
||||||
|
```bash
|
||||||
|
docker build -t git.alexzaw.dev/alexz/openconnect-vpn:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
## What's in the image
|
||||||
|
|
||||||
|
The Dockerfile creates an image with:
|
||||||
|
- Ubuntu 22.04
|
||||||
|
- openconnect + openconnect-sso[full] (Python)
|
||||||
|
- Playwright Chromium browser (for SSO authentication)
|
||||||
|
- TigerVNC server + noVNC (web-based VNC)
|
||||||
|
- Tools: oathtool (TOTP), openbox, xterm
|
||||||
|
|
||||||
|
### Scripts (baked in)
|
||||||
|
- `/opt/scripts/startup-vnc.sh` - Starts VNC server and noVNC
|
||||||
|
- `/opt/scripts/entrypoint.sh` - Container entrypoint (DNS fix, IP forwarding, config generation)
|
||||||
|
|
||||||
|
## Runtime mounts (from shared/)
|
||||||
|
|
||||||
|
When running as cistech-tunnel app, these are mounted from `shared/`:
|
||||||
|
- `/shared/openconnect-vpn` - Main VPN connection script
|
||||||
|
- `/shared/xstartup` -> `/root/.vnc/xstartup` - VNC session startup
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `VPN_EMAIL` | Email/username for SSO login |
|
||||||
|
| `VPN_PASSWORD` | Password for SSO login |
|
||||||
|
| `VPN_TOTP_SECRET` | TOTP secret for 2FA (base32) |
|
||||||
|
| `VPN_HOST` | VPN server URL (e.g., `https://vpn.example.com/Group`) |
|
||||||
|
| `TARGET_IP` | Target IP for connectivity testing |
|
||||||
|
| `VNC_PASSWORD` | VNC access password |
|
||||||
|
|
||||||
|
## Ports
|
||||||
|
|
||||||
|
- `5901` - VNC server
|
||||||
|
- `6092` - noVNC web interface
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. Container starts, generates openconnect-sso config from env vars
|
||||||
|
2. VNC server starts with noVNC web interface
|
||||||
|
3. xterm launches with the `openconnect-vpn` script
|
||||||
|
4. Script sets up keyring with credentials (password + TOTP)
|
||||||
|
5. openconnect-sso handles SSO authentication via hidden browser
|
||||||
|
6. VPN connects and IP forwarding/NAT is configured
|
||||||
22
apps/cistech-tunnel/build/build.sh
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build and push the OpenConnect-SSO VPN Docker image
|
||||||
|
# Run this from the build directory
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/openconnect-vpn}"
|
||||||
|
IMAGE_TAG="${IMAGE_TAG:-latest}"
|
||||||
|
|
||||||
|
echo "Building ${IMAGE_NAME}:${IMAGE_TAG}..."
|
||||||
|
|
||||||
|
docker build "$@" -t "${IMAGE_NAME}:${IMAGE_TAG}" .
|
||||||
|
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Build complete!"
|
||||||
|
echo ""
|
||||||
|
echo "To test locally:"
|
||||||
|
echo " docker run -d --cap-add=NET_ADMIN --device=/dev/net/tun -p 5901:5901 -p 6092:6092 -e VNC_PASSWORD=changeme -e VPN_HOST=https://vpn.example.com -e VPN_EMAIL=user@example.com ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||||
|
echo ""
|
||||||
|
echo "Then connect via VNC to localhost:5901 or open noVNC at http://localhost:6092/vnc.html"
|
||||||
|
echo ""
|
||||||
BIN
apps/cistech-tunnel/cistech-tunnel.7z
Normal file
@@ -1,53 +1,76 @@
|
|||||||
{
|
{
|
||||||
"name": "Cistech Tunnel",
|
"name": "cistech Tunnel",
|
||||||
"id": "cistech-tunnel",
|
|
||||||
"available": true,
|
"available": true,
|
||||||
"short_desc": "Cistech VPN client container with noVNC.",
|
"port": 6092,
|
||||||
"author": "alexz",
|
|
||||||
"port": 6902,
|
|
||||||
"categories": [
|
|
||||||
"utilities",
|
|
||||||
"network"
|
|
||||||
],
|
|
||||||
"description": "OpenConnect-SSO VPN running in an isolated namespace with noVNC for first-time SSO reconnects.",
|
|
||||||
"tipi_version": 1,
|
|
||||||
"version": "1.0.0",
|
|
||||||
"source": "local",
|
|
||||||
"exposable": true,
|
"exposable": true,
|
||||||
"dynamic_config": true,
|
"dynamic_config": true,
|
||||||
"no_gui": false,
|
"id": "cistech-tunnel",
|
||||||
|
"description": "openconnect-sso in Docker with noVNC web UI for accessing cistech environments. Native Docker - no VM overhead.",
|
||||||
|
"tipi_version": 7,
|
||||||
|
"version": "5.1.14.145",
|
||||||
|
"categories": [
|
||||||
|
"utilities"
|
||||||
|
],
|
||||||
|
"short_desc": "openconnect-sso VPN tunnel to cistech environments (native Docker)",
|
||||||
|
"author": "alexz",
|
||||||
|
"source": "https://git.alexzaw.dev/alexz/runtipi",
|
||||||
"form_fields": [
|
"form_fields": [
|
||||||
{
|
{
|
||||||
"label": "VPN URL",
|
"type": "email",
|
||||||
"type": "text",
|
"label": "VPN Email",
|
||||||
"env_variable": "OC_URL",
|
"hint": "Email address for VPN SSO login (configured in /shared/openconnect-vpn script)",
|
||||||
"required": true,
|
"placeholder": "your-email@company.com",
|
||||||
"default": "https://vpn.cistech.net/Employees"
|
"required": false,
|
||||||
|
"env_variable": "VPN_EMAIL",
|
||||||
|
"default": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "VNC Password",
|
|
||||||
"type": "password",
|
"type": "password",
|
||||||
|
"label": "VPN Password",
|
||||||
|
"hint": "Password for VPN SSO login (configured in /shared/openconnect-vpn script)",
|
||||||
|
"placeholder": "",
|
||||||
|
"required": false,
|
||||||
|
"env_variable": "VPN_PASSWORD",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "TOTP Secret",
|
||||||
|
"hint": "Base32 TOTP secret for 2FA (configured in /shared/openconnect-vpn script)",
|
||||||
|
"placeholder": "",
|
||||||
|
"required": false,
|
||||||
|
"env_variable": "VPN_TOTP_SECRET",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "VPN Host",
|
||||||
|
"hint": "VPN server hostname",
|
||||||
|
"placeholder": "vpn.company.com",
|
||||||
|
"required": false,
|
||||||
|
"env_variable": "VPN_HOST",
|
||||||
|
"default": "vpn.cistech.net/Employees"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Target IP",
|
||||||
|
"hint": "IP address to route through VPN (e.g., IBM i server)",
|
||||||
|
"placeholder": "10.3.1.201",
|
||||||
|
"required": false,
|
||||||
|
"env_variable": "TARGET_IP",
|
||||||
|
"default": "10.3.1.201"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "password",
|
||||||
|
"label": "VNC Password",
|
||||||
|
"hint": "Password for noVNC web interface",
|
||||||
|
"placeholder": "cisco123",
|
||||||
|
"required": false,
|
||||||
"env_variable": "VNC_PASSWORD",
|
"env_variable": "VNC_PASSWORD",
|
||||||
"required": true,
|
"default": ""
|
||||||
"default": "Az@83278327$$@@"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Server Certificate",
|
|
||||||
"type": "text",
|
|
||||||
"env_variable": "OC_SERVERCERT",
|
|
||||||
"required": true,
|
|
||||||
"default": "pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0="
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Username",
|
|
||||||
"type": "text",
|
|
||||||
"env_variable": "OC_USER",
|
|
||||||
"required": true,
|
|
||||||
"default": "alex.zaw@cistech.net"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"supported_architectures": [
|
"supported_architectures": [
|
||||||
"arm64",
|
|
||||||
"amd64"
|
"amd64"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,74 @@
|
|||||||
{
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"name": "cistech-tunnel",
|
"name": "cistech-tunnel",
|
||||||
"image": "cistech-vpn:latest",
|
"image": "git.alexzaw.dev/alexz/openconnect-vpn:latest",
|
||||||
"isMain": true,
|
"environment": [
|
||||||
"internalPort": 6902,
|
{
|
||||||
"privileged": true,
|
"key": "VPN_EMAIL",
|
||||||
"capAdd": ["NET_ADMIN"],
|
"value": "${VPN_EMAIL}"
|
||||||
"devices": ["/dev/net/tun:/dev/net/tun"],
|
|
||||||
"environment": {
|
|
||||||
"OC_URL": "${OC_URL}",
|
|
||||||
"OC_SERVERCERT": "${OC_SERVERCERT}",
|
|
||||||
"OC_USER": "${OC_USER}",
|
|
||||||
"VNC_PASSWORD": "${VNC_PASSWORD}",
|
|
||||||
"NOVNC_PORT": "6902"
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "VPN_PASSWORD",
|
||||||
|
"value": "${VPN_PASSWORD}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "VPN_TOTP_SECRET",
|
||||||
|
"value": "${VPN_TOTP_SECRET}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "VPN_HOST",
|
||||||
|
"value": "${VPN_HOST}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "VNC_PASSWORD",
|
||||||
|
"value": "${VNC_PASSWORD}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "TZ",
|
||||||
|
"value": "${TZ}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "TARGET_IP",
|
||||||
|
"value": "${TARGET_IP}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"internalPort": 6092,
|
||||||
"volumes": [
|
"volumes": [
|
||||||
{ "hostPath": "${APP_DATA_DIR}/data", "containerPath": "/root" }
|
{
|
||||||
]
|
"hostPath": "${APP_DATA_DIR}/config",
|
||||||
|
"containerPath": "/config",
|
||||||
|
"readOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hostPath": "${APP_DATA_DIR}",
|
||||||
|
"containerPath": "/runtime",
|
||||||
|
"readOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hostPath": "/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared",
|
||||||
|
"containerPath": "/shared",
|
||||||
|
"readOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hostPath": "/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/xstartup",
|
||||||
|
"containerPath": "/root/.vnc/xstartup",
|
||||||
|
"readOnly": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stopGracePeriod": "30s",
|
||||||
|
"devices": [
|
||||||
|
"/dev/net/tun"
|
||||||
|
],
|
||||||
|
"privileged": true,
|
||||||
|
"capAdd": [
|
||||||
|
"NET_ADMIN"
|
||||||
|
],
|
||||||
|
"isMain": true,
|
||||||
|
"extraLabels": {
|
||||||
|
"runtipi.managed": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
cistech-tunnel:
|
cistech-tunnel:
|
||||||
image: cistech-vpn:latest
|
image: git.alexzaw.dev/alexz/openconnect-vpn:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
cistech-tunnel_runtipi_network:
|
cistech-tunnel_runtipi_network:
|
||||||
@@ -8,21 +8,26 @@ services:
|
|||||||
tipi_main_network:
|
tipi_main_network:
|
||||||
gw_priority: 1
|
gw_priority: 1
|
||||||
environment:
|
environment:
|
||||||
OC_URL: ${OC_URL}
|
VPN_EMAIL: ${VPN_EMAIL}
|
||||||
OC_SERVERCERT: ${OC_SERVERCERT}
|
VPN_PASSWORD: ${VPN_PASSWORD}
|
||||||
OC_USER: ${OC_USER}
|
VPN_TOTP_SECRET: ${VPN_TOTP_SECRET}
|
||||||
|
VPN_HOST: ${VPN_HOST}
|
||||||
VNC_PASSWORD: ${VNC_PASSWORD}
|
VNC_PASSWORD: ${VNC_PASSWORD}
|
||||||
NOVNC_PORT: "6902"
|
TZ: ${TZ}
|
||||||
|
TARGET_IP: ${TARGET_IP}
|
||||||
ports:
|
ports:
|
||||||
- ${APP_PORT}:6902
|
- ${APP_PORT}:6092
|
||||||
volumes:
|
volumes:
|
||||||
- ${APP_DATA_DIR}/data:/root
|
- ${APP_DATA_DIR}/config:/config
|
||||||
|
- ${APP_DATA_DIR}:/runtime
|
||||||
|
- /etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared:/shared
|
||||||
|
- /etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/xstartup:/root/.vnc/xstartup:ro
|
||||||
labels:
|
labels:
|
||||||
generated: true
|
generated: true
|
||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
traefik.docker.network: runtipi_tipi_main_network
|
traefik.docker.network: runtipi_tipi_main_network
|
||||||
traefik.http.middlewares.cistech-tunnel-runtipi-web-redirect.redirectscheme.scheme: https
|
traefik.http.middlewares.cistech-tunnel-runtipi-web-redirect.redirectscheme.scheme: https
|
||||||
traefik.http.services.cistech-tunnel-runtipi.loadbalancer.server.port: "6902"
|
traefik.http.services.cistech-tunnel-runtipi.loadbalancer.server.port: "6092"
|
||||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
|
traefik.http.routers.cistech-tunnel-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
|
||||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.entrypoints: web
|
traefik.http.routers.cistech-tunnel-runtipi-insecure.entrypoints: web
|
||||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.service: cistech-tunnel-runtipi
|
traefik.http.routers.cistech-tunnel-runtipi-insecure.service: cistech-tunnel-runtipi
|
||||||
@@ -32,3 +37,10 @@ services:
|
|||||||
traefik.http.routers.cistech-tunnel-runtipi.service: cistech-tunnel-runtipi
|
traefik.http.routers.cistech-tunnel-runtipi.service: cistech-tunnel-runtipi
|
||||||
traefik.http.routers.cistech-tunnel-runtipi.tls.certresolver: myresolver
|
traefik.http.routers.cistech-tunnel-runtipi.tls.certresolver: myresolver
|
||||||
runtipi.managed: true
|
runtipi.managed: true
|
||||||
|
runtipi.appurn: cistech-tunnel:runtipi
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun
|
||||||
|
privileged: true
|
||||||
|
stop_grace_period: 30s
|
||||||
|
|||||||
1217
apps/cistech-tunnel/metadata/compose-schema.json
Normal file
@@ -1,20 +1,147 @@
|
|||||||
# Dockerized OpenConnect-SSO with noVNC and Cloudflared
|
# Cistech Tunnel - OpenConnect-SSO VPN
|
||||||
|
|
||||||
## Setup
|
Docker container running OpenConnect-SSO for Cisco AnyConnect VPN with SSO/SAML authentication support via noVNC. Provides transparent VPN access to protected resources from your LAN.
|
||||||
1) Copy `.env.example` to `.env` and fill values (URLs, servercert pins, VNC passwords, cloudflared tokens).
|
|
||||||
|
|
||||||
2) First-time SSO: leave `OC_SSO_ARGS_*=--browser-display-mode visible`.
|
## Features
|
||||||
|
|
||||||
3) Build and start:
|
- **OpenConnect-SSO** - Handles SAML/SSO authentication automatically
|
||||||
docker compose build
|
- **Playwright browser** - Headless Chromium for SSO login
|
||||||
docker compose up -d vpn_a
|
- **Web-based access** via noVNC (port 6092)
|
||||||
# Open http://localhost:6901, complete SSO.
|
- **Auto-login with TOTP** - Credentials stored in keyring
|
||||||
# After success, attach app containers or start cloudflared_a.
|
- **LAN routing** - Other machines on your network can reach VPN targets
|
||||||
|
- **Lightweight** - No systemd, no Cisco bloat
|
||||||
|
|
||||||
4) Optional: switch to headless after first login:
|
## Architecture
|
||||||
Set `OC_SSO_ARGS_*=--browser-display-mode hidden` (or `headless`) and restart the vpn service.
|
|
||||||
|
|
||||||
## Notes
|
```
|
||||||
- Each VPN runs in its own net namespace; routes from one cannot affect the other or the host.
|
LAN Devices ──► Linux Host ──► Container (172.30.0.10) ──► VPN Tunnel ──► Target
|
||||||
- DNS from the VPN applies within its container namespace and attached services only.
|
│ │
|
||||||
- Persisted state lives in the named volumes mounted at `/root` (Playwright cache, configs).
|
│ └── openconnect-sso + openconnect
|
||||||
|
│ └── noVNC web UI (port 6092)
|
||||||
|
│
|
||||||
|
└── Host routing service
|
||||||
|
(routes VPN traffic through container)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. Install the app through Runtipi
|
||||||
|
|
||||||
|
Configure your VPN credentials in app settings:
|
||||||
|
- VPN Email
|
||||||
|
- VPN Password
|
||||||
|
- TOTP Secret (base32)
|
||||||
|
- VPN Host (e.g., `https://vpn.cistech.net/Employees`)
|
||||||
|
- Target IP (for connectivity testing)
|
||||||
|
|
||||||
|
### 2. Install host routing service (required for LAN access)
|
||||||
|
|
||||||
|
**Run this ONCE on the host after app install:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/install-host-services.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates systemd services that route VPN traffic through the container.
|
||||||
|
|
||||||
|
### 3. Access the VPN GUI
|
||||||
|
|
||||||
|
Open `http://<your-server>:6092/vnc.html`
|
||||||
|
|
||||||
|
The VPN will auto-connect using your configured credentials.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Access noVNC
|
||||||
|
|
||||||
|
Navigate to port 6092 on your server. The openconnect-vpn script runs automatically and provides a menu:
|
||||||
|
|
||||||
|
```
|
||||||
|
1 - Connect VPN
|
||||||
|
2 - Disconnect VPN
|
||||||
|
3 - Show VPN status
|
||||||
|
4 - Setup IP forwarding
|
||||||
|
5 - Test connection
|
||||||
|
6 - Show network status
|
||||||
|
7 - Show routing table
|
||||||
|
8 - Setup keyring
|
||||||
|
q - Quit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command line options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Inside container
|
||||||
|
openconnect-vpn -c # Connect and exit
|
||||||
|
openconnect-vpn -d # Disconnect and exit
|
||||||
|
openconnect-vpn -s # Show status
|
||||||
|
openconnect-vpn --help # Show all options
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Inside container
|
||||||
|
cat /var/log/openconnect-vpn/$(date +%Y-%m-%d).log
|
||||||
|
|
||||||
|
# On host
|
||||||
|
cat /var/log/cistech-routing.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## LAN Access
|
||||||
|
|
||||||
|
After the host routing service is installed, any device on your LAN can reach the VPN target:
|
||||||
|
|
||||||
|
1. **From the host:** Works automatically
|
||||||
|
2. **From other LAN devices:** Add a static route pointing to your host
|
||||||
|
|
||||||
|
Example (Windows client):
|
||||||
|
```cmd
|
||||||
|
route add 10.3.1.0 mask 255.255.255.0 192.168.0.150 -p
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `192.168.0.150` is your Linux host IP.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
Before removing the app from Runtipi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/uninstall-host-services.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### VPN not connecting
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker exec cistech-tunnel cat /var/log/openconnect-vpn/$(date +%Y-%m-%d).log
|
||||||
|
|
||||||
|
# Try manual connect
|
||||||
|
docker exec -it cistech-tunnel /shared/openconnect-vpn -c
|
||||||
|
```
|
||||||
|
|
||||||
|
### VPN connects but can't reach target
|
||||||
|
```bash
|
||||||
|
# Check routes inside container
|
||||||
|
docker exec cistech-tunnel ip route
|
||||||
|
|
||||||
|
# Check host routing
|
||||||
|
ip route | grep <target-ip>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Host routing not working
|
||||||
|
```bash
|
||||||
|
# Check watcher service
|
||||||
|
systemctl status cistech-routing-watcher.path
|
||||||
|
|
||||||
|
# Manually trigger routing
|
||||||
|
touch /etc/runtipi/app-data/runtipi/cistech-tunnel/restart-routing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
- **Container IP:** 172.30.0.10 (on br-cistech-vpn bridge)
|
||||||
|
- **Ports:** 6092 (noVNC), 5901 (VNC)
|
||||||
|
- **Capabilities:** `NET_ADMIN`, `/dev/net/tun`
|
||||||
|
- **Log retention:** 7 days (auto-cleanup)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 609 KiB After Width: | Height: | Size: 6.0 KiB |
83
apps/cistech-tunnel/shared/entrypoint.sh
Executable file
@@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Entrypoint: VNC password setup + DNS fix + start VNC
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Force software rendering (no GPU/OpenGL)
|
||||||
|
export QT_QUICK_BACKEND=software
|
||||||
|
export LIBGL_ALWAYS_SOFTWARE=1
|
||||||
|
export GALLIUM_DRIVER=llvmpipe
|
||||||
|
export MESA_GL_VERSION_OVERRIDE=3.3
|
||||||
|
|
||||||
|
# Qt/Chromium flags for running as root
|
||||||
|
export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu --use-gl=swiftshader"
|
||||||
|
export QTWEBENGINE_DISABLE_SANDBOX=1
|
||||||
|
|
||||||
|
# Ensure all shared scripts are executable (permissions may reset after git pull/appstore update)
|
||||||
|
chmod +x /shared/*.sh /shared/openconnect-vpn /root/.vnc/xstartup 2>/dev/null || true
|
||||||
|
|
||||||
|
# Setup TigerVNC password file from env var (passed by runtipi)
|
||||||
|
if [ -n "${VNC_PASSWORD:-}" ]; then
|
||||||
|
mkdir -p /root/.vnc
|
||||||
|
printf '%s\n%s\n' "$VNC_PASSWORD" "$VNC_PASSWORD" | vncpasswd -f > /root/.vnc/passwd
|
||||||
|
chmod 600 /root/.vnc/passwd
|
||||||
|
fi
|
||||||
|
|
||||||
|
# DNS fix - unmount Docker's read-only mounts
|
||||||
|
cp /etc/resolv.conf /tmp/resolv.conf.bak 2>/dev/null || true
|
||||||
|
cp /etc/hosts /tmp/hosts.bak 2>/dev/null || true
|
||||||
|
umount /etc/resolv.conf 2>/dev/null || true
|
||||||
|
umount /etc/hosts 2>/dev/null || true
|
||||||
|
cat /tmp/resolv.conf.bak > /etc/resolv.conf 2>/dev/null || echo "nameserver 8.8.8.8" > /etc/resolv.conf
|
||||||
|
cat /tmp/hosts.bak > /etc/hosts 2>/dev/null || echo "127.0.0.1 localhost" > /etc/hosts
|
||||||
|
|
||||||
|
# Enable IP forwarding
|
||||||
|
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||||
|
echo "[entrypoint] IP forwarding enabled"
|
||||||
|
|
||||||
|
# Generate openconnect-sso config from environment variables
|
||||||
|
mkdir -p /root/.config/openconnect-sso
|
||||||
|
cat > /root/.config/openconnect-sso/config.toml << EOF
|
||||||
|
on_disconnect = ""
|
||||||
|
|
||||||
|
[default_profile]
|
||||||
|
address = "${VPN_HOST:-}"
|
||||||
|
user_group = ""
|
||||||
|
name = ""
|
||||||
|
|
||||||
|
[credentials]
|
||||||
|
username = "${VPN_EMAIL:-}"
|
||||||
|
|
||||||
|
[auto_fill_rules]
|
||||||
|
[[auto_fill_rules."https://*"]]
|
||||||
|
selector = "div[id=passwordError]"
|
||||||
|
action = "stop"
|
||||||
|
|
||||||
|
[[auto_fill_rules."https://*"]]
|
||||||
|
selector = "input[type=email]"
|
||||||
|
fill = "username"
|
||||||
|
|
||||||
|
[[auto_fill_rules."https://*"]]
|
||||||
|
selector = "input[name=passwd]"
|
||||||
|
fill = "password"
|
||||||
|
|
||||||
|
[[auto_fill_rules."https://*"]]
|
||||||
|
selector = "input[data-report-event=Signin_Submit]"
|
||||||
|
action = "click"
|
||||||
|
|
||||||
|
[[auto_fill_rules."https://*"]]
|
||||||
|
selector = "div[data-value=PhoneAppOTP]"
|
||||||
|
action = "click"
|
||||||
|
|
||||||
|
[[auto_fill_rules."https://*"]]
|
||||||
|
selector = "a[id=signInAnotherWay]"
|
||||||
|
action = "click"
|
||||||
|
|
||||||
|
[[auto_fill_rules."https://*"]]
|
||||||
|
selector = "input[id=idTxtBx_SAOTCC_OTC]"
|
||||||
|
fill = "totp"
|
||||||
|
EOF
|
||||||
|
echo "[entrypoint] openconnect-sso config generated"
|
||||||
|
|
||||||
|
# Start VNC server
|
||||||
|
exec /shared/startup-vnc.sh
|
||||||
123
apps/cistech-tunnel/shared/host-routing.sh
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Host routing script for cistech-tunnel
|
||||||
|
# Routes TARGET_IP through the VPN container
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ACTION="${1:-start}"
|
||||||
|
|
||||||
|
# Fixed configuration (we assigned these)
|
||||||
|
CONTAINER_IP="172.30.0.10"
|
||||||
|
BRIDGE_NAME="br-cistech-vpn"
|
||||||
|
TARGET_IP="${TARGET_IP:-10.3.1.0}"
|
||||||
|
TARGET_SUBNET="$(echo "$TARGET_IP" | cut -d. -f1-3).0/24"
|
||||||
|
LAN_SUBNET="192.168.0.0/23"
|
||||||
|
LAN_INTERFACES="eth0 eth1 wlan0"
|
||||||
|
LOG_FILE="/var/log/cistech-routing.log"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [cistech-routing] $*"
|
||||||
|
echo "$msg" | tee -a "$LOG_FILE" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
get_lan_interface() {
|
||||||
|
ip route show default | awk '/default/ {for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_routes() {
|
||||||
|
log "Removing stale routes for $TARGET_SUBNET..."
|
||||||
|
|
||||||
|
# Remove any existing route to TARGET_SUBNET
|
||||||
|
ip route del "$TARGET_SUBNET" 2>/dev/null || true
|
||||||
|
|
||||||
|
log "Stale routes removed"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_routes() {
|
||||||
|
local lan_if
|
||||||
|
lan_if="$(get_lan_interface)"
|
||||||
|
|
||||||
|
log "Applying host routing rules..."
|
||||||
|
log " Container IP: $CONTAINER_IP"
|
||||||
|
log " Bridge: $BRIDGE_NAME"
|
||||||
|
log " Target Subnet: $TARGET_SUBNET"
|
||||||
|
log " LAN interface: ${lan_if:-unknown}"
|
||||||
|
|
||||||
|
# Enable IP forwarding
|
||||||
|
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||||
|
log "IP forwarding enabled"
|
||||||
|
|
||||||
|
# Add route to TARGET_SUBNET via container
|
||||||
|
ip route replace "$TARGET_SUBNET" via "$CONTAINER_IP" dev "$BRIDGE_NAME"
|
||||||
|
log "Route added: $TARGET_SUBNET via $CONTAINER_IP dev $BRIDGE_NAME"
|
||||||
|
|
||||||
|
# Allow forwarding in DOCKER-USER chain for all LAN interfaces
|
||||||
|
for lan_if in $LAN_INTERFACES; do
|
||||||
|
# Check if interface exists
|
||||||
|
if ip link show "$lan_if" &>/dev/null; then
|
||||||
|
# Allow traffic from LAN to container for TARGET_SUBNET
|
||||||
|
iptables -C DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || \
|
||||||
|
iptables -I DOCKER-USER 1 -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_SUBNET" -j ACCEPT
|
||||||
|
|
||||||
|
# Allow return traffic
|
||||||
|
iptables -C DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
|
||||||
|
iptables -I DOCKER-USER 1 -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||||
|
|
||||||
|
log "DOCKER-USER iptables rules added for $lan_if <-> $BRIDGE_NAME"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Masquerade traffic from LAN subnet to VPN bridge (so return traffic routes correctly)
|
||||||
|
# Use nft since iptables-nft backend doesn't support iptables -t nat commands
|
||||||
|
if ! nft list chain ip nat POSTROUTING 2>/dev/null | grep -q "saddr $LAN_SUBNET.*oifname.*$BRIDGE_NAME.*masquerade"; then
|
||||||
|
nft add rule ip nat POSTROUTING ip saddr "$LAN_SUBNET" oifname "$BRIDGE_NAME" counter masquerade
|
||||||
|
log "NAT masquerade rule added for $LAN_SUBNET -> $BRIDGE_NAME"
|
||||||
|
else
|
||||||
|
log "NAT masquerade rule already exists for $LAN_SUBNET -> $BRIDGE_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "OK: Host routing applied - $TARGET_SUBNET via $CONTAINER_IP ($BRIDGE_NAME)"
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_all() {
|
||||||
|
log "Removing all routing rules..."
|
||||||
|
|
||||||
|
# Remove route
|
||||||
|
ip route del "$TARGET_SUBNET" via "$CONTAINER_IP" dev "$BRIDGE_NAME" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Remove iptables rules for all LAN interfaces
|
||||||
|
for lan_if in $LAN_INTERFACES; do
|
||||||
|
iptables -D DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -D DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# Remove masquerade rule (using nft)
|
||||||
|
local handle=""
|
||||||
|
handle=$(nft -a list chain ip nat POSTROUTING 2>/dev/null | grep "saddr $LAN_SUBNET.*oifname.*$BRIDGE_NAME.*masquerade" | grep -oP 'handle \K\d+' | head -1 || true)
|
||||||
|
if [ -n "$handle" ]; then
|
||||||
|
nft delete rule ip nat POSTROUTING handle "$handle" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "All routing rules removed"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
start)
|
||||||
|
remove_routes
|
||||||
|
apply_routes
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
remove_all
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
remove_all
|
||||||
|
sleep 1
|
||||||
|
remove_routes
|
||||||
|
apply_routes
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {start|stop|restart}" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
55
apps/cistech-tunnel/shared/install-host-services.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Install host-side systemd services for cistech-tunnel
|
||||||
|
# Run this ONCE on the host after installing the app in Runtipi
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
APP_DATA_DIR="/etc/runtipi/app-data/runtipi/cistech-tunnel"
|
||||||
|
|
||||||
|
echo "Installing cistech-tunnel host services..."
|
||||||
|
|
||||||
|
# Create the path watcher unit
|
||||||
|
cat << 'EOF' | sudo tee /etc/systemd/system/cistech-routing-watcher.path
|
||||||
|
[Unit]
|
||||||
|
Description=Watch for cistech-tunnel routing trigger
|
||||||
|
|
||||||
|
[Path]
|
||||||
|
PathExists=/etc/runtipi/app-data/runtipi/cistech-tunnel/restart-routing
|
||||||
|
Unit=cistech-routing-watcher.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create the service unit
|
||||||
|
cat << EOF | sudo tee /etc/systemd/system/cistech-routing-watcher.service
|
||||||
|
[Unit]
|
||||||
|
Description=Apply cistech-tunnel routing rules
|
||||||
|
After=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/bin/bash ${SCRIPT_DIR}/host-routing.sh restart
|
||||||
|
ExecStartPost=/bin/rm -f ${APP_DATA_DIR}/restart-routing
|
||||||
|
ExecStartPost=/bin/bash -c 'echo "trigger cleared at \$(date)" >> ${APP_DATA_DIR}/watcher.log'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Make host-routing.sh executable
|
||||||
|
sudo chmod +x "${SCRIPT_DIR}/host-routing.sh"
|
||||||
|
|
||||||
|
# Reload systemd and enable the watcher
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now cistech-routing-watcher.path
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done! Services installed:"
|
||||||
|
echo " - cistech-routing-watcher.path (watches for trigger file)"
|
||||||
|
echo " - cistech-routing-watcher.service (applies routing rules)"
|
||||||
|
echo ""
|
||||||
|
echo "To check status:"
|
||||||
|
echo " systemctl status cistech-routing-watcher.path"
|
||||||
|
echo ""
|
||||||
|
echo "To manually trigger routing:"
|
||||||
|
echo " touch ${APP_DATA_DIR}/restart-routing"
|
||||||
662
apps/cistech-tunnel/shared/openconnect-vpn
Executable file
@@ -0,0 +1,662 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Cistech VPN Connection Script with OpenConnect-SSO
|
||||||
|
# Usage: ./openconnect-vpn [-c|--connect] [-d|--disconnect] [-m|--menu] [-r|--routes] [-s|--status] [--help]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -c, --connect Connect to VPN and exit
|
||||||
|
# -d, --disconnect Disconnect VPN and exit
|
||||||
|
# -m, --menu Skip auto-connect, show menu directly
|
||||||
|
# -r, --routes Show routing table and exit
|
||||||
|
# -s, --status Show VPN status and exit
|
||||||
|
# --help Show this help message
|
||||||
|
|
||||||
|
# Credentials from environment variables (set by runtipi)
|
||||||
|
VPN_EMAIL="${VPN_EMAIL:-}"
|
||||||
|
VPN_PASSWORD="${VPN_PASSWORD:-}"
|
||||||
|
VPN_TOTP_SECRET="${VPN_TOTP_SECRET:-}"
|
||||||
|
VPN_HOST="${VPN_HOST:-}"
|
||||||
|
TARGET_IP="${TARGET_IP:-10.3.1.201}"
|
||||||
|
TARGET_SUBNET="$(echo "$TARGET_IP" | cut -d. -f1-3).0/24"
|
||||||
|
VPN_INTERFACE="${VPN_INTERFACE:-tun0}"
|
||||||
|
CONTAINER_NETWORK="172.30.0.0/24"
|
||||||
|
|
||||||
|
|
||||||
|
# Log directory
|
||||||
|
LOG_DIR="/var/log/openconnect-vpn"
|
||||||
|
LOG_RETENTION_DAYS=7
|
||||||
|
mkdir -p "$LOG_DIR" 2>/dev/null
|
||||||
|
|
||||||
|
get_log_file() {
|
||||||
|
echo "$LOG_DIR/$(date '+%Y-%m-%d').log"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_old_logs() {
|
||||||
|
find "$LOG_DIR" -name "*.log" -type f -mtime +$LOG_RETENTION_DAYS -delete 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
GRAY='\033[0;90m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
print_banner() {
|
||||||
|
echo -e "${CYAN}========================================${NC}"
|
||||||
|
echo -e "${CYAN} Cistech VPN Connection Script ${NC}"
|
||||||
|
echo -e "${CYAN}========================================${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Flags
|
||||||
|
SKIP_AUTO_CONNECT=false
|
||||||
|
DO_CONNECT=false
|
||||||
|
DO_DISCONNECT=false
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log() {
|
||||||
|
local level="$1"
|
||||||
|
local msg="$2"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
local timestamp_short=$(date '+%H:%M:%S')
|
||||||
|
local log_file=$(get_log_file)
|
||||||
|
|
||||||
|
local msg_plain=$(echo -e "$msg" | sed 's/\x1b\[[0-9;]*m//g')
|
||||||
|
echo "[$timestamp] [$level] $msg_plain" >> "$log_file"
|
||||||
|
|
||||||
|
case $level in
|
||||||
|
INFO) echo -e "${GRAY}[$timestamp_short]${NC} ${GREEN}[INFO]${NC} $msg" ;;
|
||||||
|
WARN) echo -e "${GRAY}[$timestamp_short]${NC} ${YELLOW}[WARN]${NC} $msg" ;;
|
||||||
|
ERROR) echo -e "${GRAY}[$timestamp_short]${NC} ${RED}[ERROR]${NC} $msg" ;;
|
||||||
|
DEBUG) echo -e "${GRAY}[$timestamp_short]${NC} ${CYAN}[DEBUG]${NC} $msg" ;;
|
||||||
|
CMD) echo -e "${GRAY}[$timestamp_short]${NC} ${GRAY}[CMD]${NC} $msg" ;;
|
||||||
|
*) echo -e "${GRAY}[$timestamp_short]${NC} $msg" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
run_cmd() {
|
||||||
|
local desc="$1"
|
||||||
|
shift
|
||||||
|
log CMD "$desc: $*"
|
||||||
|
output=$("$@" 2>&1)
|
||||||
|
local rc=$?
|
||||||
|
if [ -n "$output" ]; then
|
||||||
|
echo "$output" | while IFS= read -r line; do
|
||||||
|
echo -e " ${GRAY}│${NC} $line"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
echo -e "${CYAN}Cistech VPN Connection Script${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: $0 [OPTIONS]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " -c, --connect Connect to VPN and exit"
|
||||||
|
echo " -d, --disconnect Disconnect VPN and exit"
|
||||||
|
echo " -m, --menu Skip auto-connect, show menu directly"
|
||||||
|
echo " -r, --routes Show routing table and exit"
|
||||||
|
echo " -s, --status Show VPN status and exit"
|
||||||
|
echo " --help Show this help message"
|
||||||
|
echo ""
|
||||||
|
echo "Menu Options:"
|
||||||
|
echo " 1 - Connect VPN"
|
||||||
|
echo " 2 - Disconnect VPN"
|
||||||
|
echo " 3 - Show VPN status"
|
||||||
|
echo " 4 - Setup IP forwarding"
|
||||||
|
echo " 5 - Test connection"
|
||||||
|
echo " 6 - Show network status"
|
||||||
|
echo " 7 - Show routing table"
|
||||||
|
echo " 8 - Show live TOTP"
|
||||||
|
echo " q - Quit"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fetch server certificate fingerprint
|
||||||
|
get_server_cert() {
|
||||||
|
local host="$1"
|
||||||
|
local server=$(echo "$host" | sed -E 's|^https?://||; s|/.*$||')
|
||||||
|
[[ "$server" != *:* ]] && server="${server}:443"
|
||||||
|
|
||||||
|
log DEBUG "Fetching server certificate from $server..."
|
||||||
|
|
||||||
|
local cert_pin=$(echo | openssl s_client -connect "$server" 2>/dev/null \
|
||||||
|
| openssl x509 -pubkey -noout 2>/dev/null \
|
||||||
|
| openssl pkey -pubin -outform der 2>/dev/null \
|
||||||
|
| openssl dgst -sha256 -binary \
|
||||||
|
| openssl enc -base64)
|
||||||
|
|
||||||
|
if [[ -n "$cert_pin" ]]; then
|
||||||
|
echo "pin-sha256:${cert_pin}"
|
||||||
|
else
|
||||||
|
log WARN "Could not fetch server certificate"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Setup keyring with credentials
|
||||||
|
setup_keyring() {
|
||||||
|
log INFO "Setting up keyring credentials..."
|
||||||
|
|
||||||
|
if [[ -z "$VPN_EMAIL" ]]; then
|
||||||
|
log ERROR "VPN_EMAIL is not set"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 << PYTHON
|
||||||
|
import keyring
|
||||||
|
from keyrings.alt.file import PlaintextKeyring
|
||||||
|
|
||||||
|
keyring.set_keyring(PlaintextKeyring())
|
||||||
|
|
||||||
|
email = "$VPN_EMAIL"
|
||||||
|
password = "$VPN_PASSWORD"
|
||||||
|
totp_secret = "$VPN_TOTP_SECRET"
|
||||||
|
|
||||||
|
if password:
|
||||||
|
keyring.set_password('openconnect-sso', email, password)
|
||||||
|
print(f"Password stored in keyring for {email}")
|
||||||
|
|
||||||
|
if totp_secret:
|
||||||
|
keyring.set_password('openconnect-sso', f'totp/{email}', totp_secret.upper())
|
||||||
|
print(f"TOTP secret stored in keyring for {email}")
|
||||||
|
|
||||||
|
print("Keyring setup complete")
|
||||||
|
PYTHON
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
log INFO "Keyring credentials configured"
|
||||||
|
else
|
||||||
|
log ERROR "Failed to setup keyring"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_totp() {
|
||||||
|
oathtool --totp -b "$VPN_TOTP_SECRET"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test connection to TARGET_IP
|
||||||
|
test_connection() {
|
||||||
|
if [[ -z "$TARGET_IP" ]]; then
|
||||||
|
log WARN "TARGET_IP not set"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log INFO "Testing connection to $TARGET_IP..."
|
||||||
|
if ping -c 3 -W 3 "$TARGET_IP" &>/dev/null; then
|
||||||
|
log INFO "Connection test: ${GREEN}SUCCESS${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log WARN "Connection test: ${RED}FAILED${NC} (may need manual route on host)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_totp() {
|
||||||
|
log INFO "Starting live TOTP display (Ctrl+C to stop)"
|
||||||
|
echo ""
|
||||||
|
while true; do
|
||||||
|
TOTP=$(get_totp)
|
||||||
|
SECONDS_LEFT=$((30 - ($(date +%s) % 30)))
|
||||||
|
echo -ne "\r ${CYAN}Current TOTP:${NC} ${GREEN}$TOTP${NC} (expires in ${YELLOW}${SECONDS_LEFT}s${NC}) "
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
get_vpn_interface() {
|
||||||
|
local iface=$(ip link show | grep -oP 'tun\d+(?=:.*UP)' | head -1)
|
||||||
|
if [ -z "$iface" ]; then
|
||||||
|
iface=$(ip link show | grep -oP 'tun\d+' | head -1)
|
||||||
|
fi
|
||||||
|
echo "$iface"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_container_ip() {
|
||||||
|
ip addr show eth0 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
get_vpn_ip() {
|
||||||
|
local iface=$(get_vpn_interface)
|
||||||
|
if [ -n "$iface" ]; then
|
||||||
|
ip addr show "$iface" 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_vpn_status() {
|
||||||
|
local vpn_iface=$(get_vpn_interface)
|
||||||
|
if [ -n "$vpn_iface" ]; then
|
||||||
|
local vpn_ip=$(get_vpn_ip)
|
||||||
|
log INFO "VPN is ${GREEN}CONNECTED${NC}"
|
||||||
|
log DEBUG " Interface: $vpn_iface"
|
||||||
|
log DEBUG " VPN IP: $vpn_ip"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log WARN "VPN is ${RED}NOT CONNECTED${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_routes() {
|
||||||
|
echo -e "${CYAN}========================================${NC}"
|
||||||
|
echo -e "${CYAN} Current Routing Table ${NC}"
|
||||||
|
echo -e "${CYAN}========================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}IPv4 Routes:${NC}"
|
||||||
|
ip -4 route show | while IFS= read -r line; do
|
||||||
|
if echo "$line" | grep -qE "tun[0-9]"; then
|
||||||
|
echo -e " ${YELLOW}$line${NC}"
|
||||||
|
elif echo "$line" | grep -qE "10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\."; then
|
||||||
|
echo -e " ${CYAN}$line${NC}"
|
||||||
|
else
|
||||||
|
echo " $line"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}VPN Interface:${NC}"
|
||||||
|
local vpn_iface=$(get_vpn_interface)
|
||||||
|
if [ -n "$vpn_iface" ]; then
|
||||||
|
ip addr show "$vpn_iface" 2>/dev/null | grep -E "inet|link" | while IFS= read -r line; do
|
||||||
|
echo " $line"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo -e " ${RED}No VPN interface found${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_network_status() {
|
||||||
|
log INFO "Current network status:"
|
||||||
|
echo ""
|
||||||
|
log DEBUG "Container Network Interfaces:"
|
||||||
|
ip -4 addr show | grep -E "inet |^[0-9]+:" | while IFS= read -r line; do
|
||||||
|
echo -e " ${GRAY}│${NC} $line"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
local vpn_iface=$(get_vpn_interface)
|
||||||
|
if [ -n "$vpn_iface" ]; then
|
||||||
|
local vpn_ip=$(get_vpn_ip)
|
||||||
|
log INFO "VPN Status: ${GREEN}CONNECTED${NC}"
|
||||||
|
log DEBUG " Interface: $vpn_iface"
|
||||||
|
log DEBUG " VPN IP: $vpn_ip"
|
||||||
|
else
|
||||||
|
log WARN "VPN Status: ${RED}NOT CONNECTED${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local container_ip=$(get_container_ip)
|
||||||
|
if [ -n "$container_ip" ]; then
|
||||||
|
log DEBUG "Container IP: $container_ip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log DEBUG "Default gateway:"
|
||||||
|
ip route show default | while IFS= read -r line; do
|
||||||
|
echo -e " ${GRAY}│${NC} $line"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
kill_vpn_processes() {
|
||||||
|
log INFO "Killing VPN processes..."
|
||||||
|
local killed=0
|
||||||
|
|
||||||
|
for pid in $(pgrep -x "openconnect" 2>/dev/null); do
|
||||||
|
log DEBUG "Killing openconnect (PID $pid)"
|
||||||
|
kill -9 "$pid" 2>/dev/null && ((killed++))
|
||||||
|
done
|
||||||
|
|
||||||
|
for pid in $(pgrep -f "openconnect-sso" 2>/dev/null); do
|
||||||
|
log DEBUG "Killing openconnect-sso (PID $pid)"
|
||||||
|
kill -9 "$pid" 2>/dev/null && ((killed++))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $killed -eq 0 ]; then
|
||||||
|
log INFO "No VPN processes were running"
|
||||||
|
else
|
||||||
|
log INFO "Killed $killed process(es)"
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect_vpn() {
|
||||||
|
log INFO "Disconnecting VPN..."
|
||||||
|
kill_vpn_processes
|
||||||
|
|
||||||
|
if check_vpn_status; then
|
||||||
|
log WARN "VPN still appears connected"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log INFO "VPN disconnected"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_forwarding() {
|
||||||
|
log INFO "Setting up IP forwarding rules for $TARGET_SUBNET..."
|
||||||
|
|
||||||
|
local vpn_iface=$(get_vpn_interface)
|
||||||
|
if [ -z "$vpn_iface" ]; then
|
||||||
|
log ERROR "No VPN interface found! Is VPN connected?"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local vpn_ip=$(get_vpn_ip)
|
||||||
|
local container_ip=$(get_container_ip)
|
||||||
|
|
||||||
|
log DEBUG "VPN interface: $vpn_iface"
|
||||||
|
log DEBUG "VPN IP: $vpn_ip"
|
||||||
|
log DEBUG "Container IP: $container_ip"
|
||||||
|
|
||||||
|
# Enable IP forwarding
|
||||||
|
run_cmd "Enabling IP forwarding" sysctl -w net.ipv4.ip_forward=1
|
||||||
|
|
||||||
|
# NAT masquerade for container network going through VPN
|
||||||
|
if ! iptables -t nat -C POSTROUTING -s "$CONTAINER_NETWORK" -o "$vpn_iface" -j MASQUERADE 2>/dev/null; then
|
||||||
|
run_cmd "Adding NAT masquerade for container network -> VPN" iptables -t nat -A POSTROUTING -s "$CONTAINER_NETWORK" -o "$vpn_iface" -j MASQUERADE
|
||||||
|
else
|
||||||
|
log DEBUG "NAT masquerade for container network already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Forward rules at position 1
|
||||||
|
iptables -D FORWARD -d "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -D FORWARD -s "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -D FORWARD -s "$CONTAINER_NETWORK" -j ACCEPT 2>/dev/null || true
|
||||||
|
iptables -D FORWARD -d "$CONTAINER_NETWORK" -j ACCEPT 2>/dev/null || true
|
||||||
|
|
||||||
|
run_cmd "Inserting forward rule (to container network)" iptables -I FORWARD 1 -d "$CONTAINER_NETWORK" -j ACCEPT
|
||||||
|
run_cmd "Inserting forward rule (from container network)" iptables -I FORWARD 1 -s "$CONTAINER_NETWORK" -j ACCEPT
|
||||||
|
run_cmd "Inserting forward rule (from target)" iptables -I FORWARD 1 -s "$TARGET_SUBNET" -j ACCEPT
|
||||||
|
run_cmd "Inserting forward rule (to target)" iptables -I FORWARD 1 -d "$TARGET_SUBNET" -j ACCEPT
|
||||||
|
|
||||||
|
log INFO "Forwarding rules configured"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Trigger host routing service restart
|
||||||
|
log INFO "Triggering host routing service restart..."
|
||||||
|
touch /runtime/restart-routing 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
if [ ! -f /runtime/restart-routing ]; then
|
||||||
|
log INFO "Host routing service restarted"
|
||||||
|
else
|
||||||
|
log WARN "Host watcher may not be running (trigger file still exists)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log INFO "Routing configured for $TARGET_SUBNET through VPN tunnel"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
connect_vpn() {
|
||||||
|
log INFO "=== Starting OpenConnect-SSO VPN ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Kill any existing VPN processes
|
||||||
|
kill_vpn_processes
|
||||||
|
|
||||||
|
# Reset DNS to public servers (VPN script may have overwritten it)
|
||||||
|
log DEBUG "Resetting DNS to public servers..."
|
||||||
|
echo "nameserver 8.8.8.8" > /etc/resolv.conf
|
||||||
|
echo "nameserver 1.1.1.1" >> /etc/resolv.conf
|
||||||
|
|
||||||
|
# Clean up stale tun interface
|
||||||
|
ip link delete tun0 2>/dev/null || true
|
||||||
|
|
||||||
|
# Validate required variables
|
||||||
|
if [[ -z "$VPN_HOST" ]]; then
|
||||||
|
log ERROR "VPN_HOST is not set"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Setup keyring credentials
|
||||||
|
setup_keyring
|
||||||
|
|
||||||
|
# Fetch server certificate
|
||||||
|
log INFO "Fetching server certificate..."
|
||||||
|
local servercert=$(get_server_cert "$VPN_HOST")
|
||||||
|
|
||||||
|
log INFO "Connecting to: $VPN_HOST"
|
||||||
|
log DEBUG "Interface: $VPN_INTERFACE"
|
||||||
|
|
||||||
|
# Build openconnect-sso command
|
||||||
|
local sso_args=()
|
||||||
|
sso_args+=("-s" "$VPN_HOST")
|
||||||
|
|
||||||
|
if [[ -n "$VPN_EMAIL" ]]; then
|
||||||
|
sso_args+=("-u" "$VPN_EMAIL")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use hidden browser for automated login
|
||||||
|
sso_args+=("--browser-display-mode" "hidden")
|
||||||
|
|
||||||
|
# Build openconnect args
|
||||||
|
local oc_args=()
|
||||||
|
oc_args+=("--protocol=anyconnect")
|
||||||
|
oc_args+=("--interface" "$VPN_INTERFACE")
|
||||||
|
oc_args+=("--script" "/usr/share/vpnc-scripts/vpnc-script")
|
||||||
|
|
||||||
|
if [[ -n "$servercert" ]]; then
|
||||||
|
oc_args+=("--servercert" "$servercert")
|
||||||
|
log DEBUG "Server cert: $servercert"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Launch openconnect-sso
|
||||||
|
log INFO "Launching openconnect-sso..."
|
||||||
|
openconnect-sso "${sso_args[@]}" -- /usr/sbin/openconnect "${oc_args[@]}" &
|
||||||
|
OC_PID=$!
|
||||||
|
disown $OC_PID
|
||||||
|
log DEBUG "openconnect-sso started with PID $OC_PID"
|
||||||
|
|
||||||
|
# Wait for VPN to connect
|
||||||
|
log INFO "Waiting for VPN connection..."
|
||||||
|
local wait_count=0
|
||||||
|
local max_wait=300
|
||||||
|
|
||||||
|
while [ -z "$(get_vpn_interface)" ]; do
|
||||||
|
sleep 2
|
||||||
|
((wait_count+=2))
|
||||||
|
if [ $((wait_count % 10)) -eq 0 ]; then
|
||||||
|
log DEBUG "Still waiting for VPN... (${wait_count}s)"
|
||||||
|
fi
|
||||||
|
if [ $wait_count -ge $max_wait ]; then
|
||||||
|
log ERROR "Timeout waiting for VPN connection after ${max_wait}s"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log INFO "VPN connected!"
|
||||||
|
local vpn_iface=$(get_vpn_interface)
|
||||||
|
local vpn_ip=$(get_vpn_ip)
|
||||||
|
log DEBUG " Interface: $vpn_iface"
|
||||||
|
log DEBUG " VPN IP: $vpn_ip"
|
||||||
|
|
||||||
|
# Wait for routes to stabilize
|
||||||
|
log DEBUG "Waiting for routes to stabilize..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Setup forwarding
|
||||||
|
setup_forwarding
|
||||||
|
|
||||||
|
# Test connection to IBMI host
|
||||||
|
test_connection
|
||||||
|
|
||||||
|
# Disable screen blanking
|
||||||
|
xset s off 2>/dev/null || true
|
||||||
|
xset -dpms 2>/dev/null || true
|
||||||
|
xset s noblank 2>/dev/null || true
|
||||||
|
log DEBUG "Screen blanking disabled"
|
||||||
|
|
||||||
|
log INFO "VPN setup complete"
|
||||||
|
|
||||||
|
# Start watchdog in background
|
||||||
|
start_watchdog &
|
||||||
|
WATCHDOG_PID=$!
|
||||||
|
log DEBUG "Watchdog started with PID $WATCHDOG_PID"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Watchdog - monitors VPN and reconnects if dropped
|
||||||
|
start_watchdog() {
|
||||||
|
log INFO "Starting VPN watchdog (check every 60s, keepalive ping every 5min)..."
|
||||||
|
|
||||||
|
local check_interval=60
|
||||||
|
local keepalive_interval=300
|
||||||
|
local last_keepalive=0
|
||||||
|
local reconnect_attempts=0
|
||||||
|
local max_reconnect_attempts=3
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
sleep $check_interval
|
||||||
|
|
||||||
|
local now=$(date +%s)
|
||||||
|
local vpn_iface=$(get_vpn_interface)
|
||||||
|
|
||||||
|
if [ -n "$vpn_iface" ]; then
|
||||||
|
# VPN is up
|
||||||
|
reconnect_attempts=0
|
||||||
|
|
||||||
|
# Keepalive ping every 5 minutes
|
||||||
|
if [ $((now - last_keepalive)) -ge $keepalive_interval ]; then
|
||||||
|
test_connection || log WARN "VPN may be degraded"
|
||||||
|
last_keepalive=$now
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# VPN is down
|
||||||
|
((reconnect_attempts++))
|
||||||
|
log WARN "VPN disconnected! Reconnect attempt $reconnect_attempts/$max_reconnect_attempts..."
|
||||||
|
|
||||||
|
if [ $reconnect_attempts -le $max_reconnect_attempts ]; then
|
||||||
|
connect_vpn
|
||||||
|
else
|
||||||
|
log ERROR "Max reconnect attempts reached. Manual intervention required."
|
||||||
|
sleep 300
|
||||||
|
reconnect_attempts=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main menu
|
||||||
|
main_menu() {
|
||||||
|
echo -e "${GREEN}Options:${NC}"
|
||||||
|
echo -e " ${CYAN}1${NC} - Connect VPN"
|
||||||
|
echo -e " ${CYAN}2${NC} - Disconnect VPN"
|
||||||
|
echo -e " ${CYAN}3${NC} - Show VPN status"
|
||||||
|
echo -e " ${CYAN}4${NC} - Setup IP forwarding only"
|
||||||
|
echo -e " ${CYAN}5${NC} - Test connection to $TARGET_IP"
|
||||||
|
echo -e " ${CYAN}6${NC} - Show network status"
|
||||||
|
echo -e " ${CYAN}7${NC} - Show routing table"
|
||||||
|
echo -e " ${CYAN}8${NC} - Show live TOTP"
|
||||||
|
echo -e " ${CYAN}q${NC} - Quit"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-c|--connect)
|
||||||
|
DO_CONNECT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-d|--disconnect)
|
||||||
|
DO_DISCONNECT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-m|--menu)
|
||||||
|
SKIP_AUTO_CONNECT=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-s|--status)
|
||||||
|
print_banner
|
||||||
|
check_vpn_status
|
||||||
|
echo ""
|
||||||
|
show_network_status
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-r|--routes)
|
||||||
|
show_routes
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
echo "Use --help for usage information"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$DO_CONNECT" = "true" ] && [ "$DO_DISCONNECT" = "true" ]; then
|
||||||
|
echo "Error: --connect and --disconnect are mutually exclusive"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
parse_args "$@"
|
||||||
|
cleanup_old_logs
|
||||||
|
|
||||||
|
echo "" >> "$(get_log_file)"
|
||||||
|
echo "========================================" >> "$(get_log_file)"
|
||||||
|
log INFO "=== Starting OpenConnect-SSO VPN ==="
|
||||||
|
echo ""
|
||||||
|
log DEBUG "VPN_EMAIL=$VPN_EMAIL"
|
||||||
|
log DEBUG "VPN_HOST=$VPN_HOST"
|
||||||
|
log DEBUG "TARGET_IP=$TARGET_IP"
|
||||||
|
log DEBUG "TARGET_SUBNET=$TARGET_SUBNET"
|
||||||
|
log DEBUG "VPN_TOTP_SECRET is $([ -n "$VPN_TOTP_SECRET" ] && echo 'set' || echo 'NOT SET')"
|
||||||
|
|
||||||
|
print_banner
|
||||||
|
|
||||||
|
if [ "$DO_DISCONNECT" = "true" ]; then
|
||||||
|
disconnect_vpn
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DO_CONNECT" = "true" ]; then
|
||||||
|
connect_vpn
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-connect logic (unless -m flag)
|
||||||
|
if [ "$SKIP_AUTO_CONNECT" = "true" ]; then
|
||||||
|
log INFO "Menu mode - skipping auto-connect"
|
||||||
|
elif check_vpn_status; then
|
||||||
|
echo ""
|
||||||
|
log INFO "VPN already connected. Setting up forwarding..."
|
||||||
|
setup_forwarding
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
log INFO "Auto-starting VPN connection..."
|
||||||
|
echo ""
|
||||||
|
connect_vpn
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Interactive menu loop
|
||||||
|
while true; do
|
||||||
|
echo ""
|
||||||
|
main_menu
|
||||||
|
echo -ne "${CYAN}Choice: ${NC}"
|
||||||
|
read -r choice
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
[[ -z "${choice// }" ]] && continue
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1) connect_vpn ;;
|
||||||
|
2) disconnect_vpn ;;
|
||||||
|
3) check_vpn_status ;;
|
||||||
|
4) setup_forwarding ;;
|
||||||
|
5) test_connection ;;
|
||||||
|
6) show_network_status ;;
|
||||||
|
7) show_routes ;;
|
||||||
|
8) show_totp ;;
|
||||||
|
q|Q) log INFO "Goodbye!"; exit 0 ;;
|
||||||
|
*) ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
12
apps/cistech-tunnel/shared/startup-vnc.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
export HOME='/root'
|
||||||
|
export USER='root'
|
||||||
|
rm -f /tmp/.P1-lock /tmp/.X11-unix/X1 2>/dev/null || true
|
||||||
|
rm -rf /tmp/.X*-lock /tmp/.X14-unix/* 2>/dev/null || true
|
||||||
|
echo "Starting TigerVNC server on display :1..."
|
||||||
|
vncserver :1 -geometry 1280x800 -depth 24 -SecurityTypes VncAuth -localhost no
|
||||||
|
sleep 2
|
||||||
|
echo "Starting noVNC on port ${NOVNC_PORT:-6092}..."
|
||||||
|
websockify --web=/usr/share/novnc/ ${NOVNC_PORT:-6092} localhost:${VNC_PORT:-5901} &
|
||||||
|
tail -f /root/.vnc/*.log
|
||||||
24
apps/cistech-tunnel/shared/uninstall-host-services.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Uninstall host-side systemd services for cistech-tunnel
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Removing cistech-tunnel host services..."
|
||||||
|
|
||||||
|
# Stop and disable the watcher
|
||||||
|
sudo systemctl stop cistech-routing-watcher.path 2>/dev/null || true
|
||||||
|
sudo systemctl disable cistech-routing-watcher.path 2>/dev/null || true
|
||||||
|
|
||||||
|
# Remove routing rules
|
||||||
|
/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/host-routing.sh stop 2>/dev/null || true
|
||||||
|
|
||||||
|
# Remove systemd units
|
||||||
|
sudo rm -f /etc/systemd/system/cistech-routing-watcher.path
|
||||||
|
sudo rm -f /etc/systemd/system/cistech-routing-watcher.service
|
||||||
|
|
||||||
|
# Reload systemd
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done! Host services removed."
|
||||||
53
apps/cistech-tunnel/shared/xstartup
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# VNC xstartup - launches terminal with cisco-vpn script
|
||||||
|
|
||||||
|
unset SESSION_MANAGER
|
||||||
|
unset DBUS_SESSION_BUS_ADDRESS
|
||||||
|
|
||||||
|
# Import environment variables from container (PID 1)
|
||||||
|
# Systemd services don't inherit Docker env vars, so we source them here
|
||||||
|
while IFS= read -r -d '' line; do
|
||||||
|
export "$line"
|
||||||
|
done < /proc/1/environ
|
||||||
|
|
||||||
|
export XDG_RUNTIME_DIR=/tmp/runtime-root
|
||||||
|
mkdir -p $XDG_RUNTIME_DIR
|
||||||
|
chmod 700 $XDG_RUNTIME_DIR
|
||||||
|
|
||||||
|
# Force software rendering (no GPU/OpenGL)
|
||||||
|
export QT_QUICK_BACKEND=software
|
||||||
|
export LIBGL_ALWAYS_SOFTWARE=1
|
||||||
|
export GALLIUM_DRIVER=llvmpipe
|
||||||
|
export MESA_GL_VERSION_OVERRIDE=3.3
|
||||||
|
|
||||||
|
# GPU/WebKit workarounds for Cisco UI
|
||||||
|
export GDK_BACKEND=x11
|
||||||
|
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||||
|
|
||||||
|
# Qt/Chromium flags for running as root (no sandbox)
|
||||||
|
export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
|
||||||
|
export QTWEBENGINE_DISABLE_SANDBOX=1
|
||||||
|
|
||||||
|
# Start system dbus daemon (needed for Chromium)
|
||||||
|
mkdir -p /run/dbus
|
||||||
|
dbus-daemon --system --fork 2>/dev/null || true
|
||||||
|
|
||||||
|
# Start dbus session
|
||||||
|
[ -x /usr/bin/dbus-launch ] && eval $(dbus-launch --sh-syntax --exit-with-session)
|
||||||
|
|
||||||
|
# Start window manager
|
||||||
|
openbox &
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Disable screen blanking and power saving
|
||||||
|
xset s off 2>/dev/null || true
|
||||||
|
xset -dpms 2>/dev/null || true
|
||||||
|
xset s noblank 2>/dev/null || true
|
||||||
|
|
||||||
|
# Make script executable and launch in terminal
|
||||||
|
chmod +x /shared/openconnect-vpn 2>/dev/null || true
|
||||||
|
xterm -fn fixed -bg black -fg white -geometry 130x45+10+10 \
|
||||||
|
-title "Cistech VPN Terminal" \
|
||||||
|
-e "bash -c '/shared/openconnect-vpn; exec bash'" &
|
||||||
|
|
||||||
|
wait
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
FROM ubuntu:24.04
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
|
||||||
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
|
|
||||||
VIRTUAL_ENV=/opt/venv \
|
|
||||||
PATH=/opt/venv/bin:$PATH \
|
|
||||||
QTWEBENGINE_DISABLE_SANDBOX=1 \
|
|
||||||
QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu" \
|
|
||||||
OC_URL="https://vpn.cistech.net/Employees" \
|
|
||||||
OC_SERVERCERT="pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0=" \
|
|
||||||
OC_USER="alex.zaw@cistech.net" \
|
|
||||||
OC_TOTP_SECRET="t6ypnjqvyx2yvw2l" \
|
|
||||||
VNC_PASSWORD="Az@83278327\$\$@@"
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
openconnect iproute2 iptables ca-certificates \
|
|
||||||
python3 python3-pip python3-venv \
|
|
||||||
vpnc-scripts curl wget openssh-client \
|
|
||||||
x11vnc xvfb fluxbox novnc websockify xterm nano oathtool \
|
|
||||||
xauth libnss3 libatk1.0-0 libatk-bridge2.0-0 \
|
|
||||||
libx11-6 libx11-xcb1 libxcomposite1 libxrandr2 libgbm1 libxdamage1 \
|
|
||||||
libpango-1.0-0 fonts-liberation \
|
|
||||||
libegl1 libgl1 libopengl0 libdbus-1-3 libglib2.0-0 \
|
|
||||||
libxkbcommon0 libxkbcommon-x11-0 \
|
|
||||||
libxcb1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render0 libxcb-render-util0 libxcb-shm0 libxcb-xfixes0 libxcb-xinerama0 libxcb-randr0 libxcb-glx0 \
|
|
||||||
sudo && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN apt-get update && (apt-get install -y libasound2t64 || apt-get install -y libasound2) && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Python venv + Playwright + openconnect-sso
|
|
||||||
RUN python3 -m venv "$VIRTUAL_ENV"
|
|
||||||
RUN pip install --no-cache-dir openconnect-sso playwright keyring keyrings.alt && \
|
|
||||||
python -m playwright install --with-deps chromium
|
|
||||||
|
|
||||||
# Cloudflared (amd64)
|
|
||||||
RUN arch=$(dpkg --print-architecture) && \
|
|
||||||
if [ "$arch" = "amd64" ]; then \
|
|
||||||
curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o /tmp/cloudflared.deb && \
|
|
||||||
apt-get update && apt-get install -y /tmp/cloudflared.deb && rm -f /tmp/cloudflared.deb ; \
|
|
||||||
else \
|
|
||||||
echo "Install cloudflared manually for arch=$arch" && exit 1 ; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
EXPOSE 6901
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
: "${OC_URL:?OC_URL required}"
|
|
||||||
: "${OC_SERVERCERT:?OC_SERVERCERT required}"
|
|
||||||
|
|
||||||
NOVNC_PORT="${NOVNC_PORT:-6901}"
|
|
||||||
VNC_PASSWORD="${VNC_PASSWORD:-changeme}"
|
|
||||||
DISPLAY_ADDR="${DISPLAY:-:1}"
|
|
||||||
OC_INTERFACE="${OC_INTERFACE:-tun0}"
|
|
||||||
OC_USER="${OC_USER:-}"
|
|
||||||
OC_TOTP_SECRET="${OC_TOTP_SECRET:-}"
|
|
||||||
|
|
||||||
# Default to hidden browser if OC_USER is set
|
|
||||||
if [[ -n "$OC_USER" ]]; then
|
|
||||||
OC_SSO_ARGS_DEFAULT="--browser-display-mode hidden -u $OC_USER"
|
|
||||||
else
|
|
||||||
OC_SSO_ARGS_DEFAULT="--browser-display-mode shown"
|
|
||||||
fi
|
|
||||||
|
|
||||||
CLOUDFLARED_MODE="${CLOUDFLARED_MODE:-off}" # off|token|config
|
|
||||||
CLOUDFLARED_TOKEN="${CLOUDFLARED_TOKEN:-}"
|
|
||||||
SSH_TUNNEL_ENABLE="${SSH_TUNNEL_ENABLE:-0}"
|
|
||||||
SSH_DEST="${SSH_DEST:-zawa@10.3.1.201}"
|
|
||||||
SSH_FORWARDS="${SSH_FORWARDS:-0.0.0.0:8090:localhost:8090}"
|
|
||||||
|
|
||||||
pids=()
|
|
||||||
|
|
||||||
# Setup keyring with TOTP secret if provided
|
|
||||||
setup_keyring() {
|
|
||||||
if [[ -n "$OC_TOTP_SECRET" && -n "$OC_USER" ]]; then
|
|
||||||
python3 -c "
|
|
||||||
import keyring
|
|
||||||
keyring.set_password('openconnect-sso', 'totp/$OC_USER', '$OC_TOTP_SECRET'.upper())
|
|
||||||
print('TOTP secret stored in keyring for $OC_USER')
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create vpn_connect command in PATH and save environment
|
|
||||||
create_vpn_command() {
|
|
||||||
# Save environment variables to a file
|
|
||||||
cat > /etc/vpn.env << ENVFILE
|
|
||||||
export OC_URL="$OC_URL"
|
|
||||||
export OC_SERVERCERT="$OC_SERVERCERT"
|
|
||||||
export OC_INTERFACE="$OC_INTERFACE"
|
|
||||||
export OC_USER="$OC_USER"
|
|
||||||
export OC_SSO_ARGS_DEFAULT="$OC_SSO_ARGS_DEFAULT"
|
|
||||||
export OC_SSO_ARGS="${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT}"
|
|
||||||
export OC_AUTHGROUP="${OC_AUTHGROUP:-}"
|
|
||||||
export OC_USERAGENT="${OC_USERAGENT:-}"
|
|
||||||
export OC_EXTRA_ARGS="${OC_EXTRA_ARGS:-}"
|
|
||||||
export OC_TOTP_SECRET="$OC_TOTP_SECRET"
|
|
||||||
export DISPLAY=":1"
|
|
||||||
ENVFILE
|
|
||||||
|
|
||||||
# Build openconnect command
|
|
||||||
OPENCONNECT_CMD="/usr/sbin/openconnect --protocol=anyconnect --servercert $OC_SERVERCERT --interface $OC_INTERFACE --script /usr/share/vpnc-scripts/vpnc-script"
|
|
||||||
[[ -n "${OC_AUTHGROUP:-}" ]] && OPENCONNECT_CMD+=" --authgroup $OC_AUTHGROUP"
|
|
||||||
[[ -n "${OC_USERAGENT:-}" ]] && OPENCONNECT_CMD+=" --useragent $OC_USERAGENT"
|
|
||||||
[[ -n "${OC_EXTRA_ARGS:-}" ]] && OPENCONNECT_CMD+=" ${OC_EXTRA_ARGS}"
|
|
||||||
echo "export OPENCONNECT_CMD=\"$OPENCONNECT_CMD\"" >> /etc/vpn.env
|
|
||||||
|
|
||||||
cat > /usr/local/bin/vpn_connect << 'VPNCMD'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
source /etc/vpn.env
|
|
||||||
echo "[$(date)] Starting VPN connection..."
|
|
||||||
|
|
||||||
# openconnect-sso reads TOTP from keyring automatically
|
|
||||||
if [[ -n "$OC_USER" ]]; then
|
|
||||||
echo "" | openconnect-sso -s "$OC_URL" ${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT} -- $OPENCONNECT_CMD
|
|
||||||
else
|
|
||||||
openconnect-sso -s "$OC_URL" ${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT} -- $OPENCONNECT_CMD
|
|
||||||
fi
|
|
||||||
VPNCMD
|
|
||||||
chmod +x /usr/local/bin/vpn_connect
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create VPN runner script that keeps shell open
|
|
||||||
create_vpn_script() {
|
|
||||||
cat > /tmp/vpn-runner.sh << 'VPNSCRIPT'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
cd /root
|
|
||||||
|
|
||||||
echo "============================================"
|
|
||||||
echo " Cistech VPN Container"
|
|
||||||
echo "============================================"
|
|
||||||
echo ""
|
|
||||||
echo "Commands:"
|
|
||||||
echo " vpn_connect - Start/restart VPN connection"
|
|
||||||
echo " Ctrl+C - Stop auto-reconnect and drop to shell"
|
|
||||||
echo ""
|
|
||||||
echo "Starting VPN with auto-reconnect..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
vpn_connect
|
|
||||||
echo ""
|
|
||||||
echo "[$(date)] VPN disconnected. Reconnecting in 10 seconds..."
|
|
||||||
echo "(Press Ctrl+C to stop auto-reconnect)"
|
|
||||||
sleep 10
|
|
||||||
done
|
|
||||||
VPNSCRIPT
|
|
||||||
chmod +x /tmp/vpn-runner.sh
|
|
||||||
}
|
|
||||||
|
|
||||||
start_gui() {
|
|
||||||
mkdir -p /root/.vnc
|
|
||||||
x11vnc -storepasswd "$VNC_PASSWORD" /root/.vnc/pass >/dev/null 2>&1 || true
|
|
||||||
rm -f /tmp/.X1-lock /tmp/.X11-unix/X1 2>/dev/null || true
|
|
||||||
Xvfb "$DISPLAY_ADDR" -screen 0 ${XVFB_WxHxD:-1280x800x24} +extension RANDR &
|
|
||||||
pids+=($!)
|
|
||||||
sleep 0.5
|
|
||||||
export DISPLAY="$DISPLAY_ADDR"
|
|
||||||
fluxbox >/tmp/fluxbox.log 2>&1 &
|
|
||||||
pids+=($!)
|
|
||||||
x11vnc -display "$DISPLAY_ADDR" -rfbauth /root/.vnc/pass -forever -shared -rfbport 5900 -quiet &
|
|
||||||
pids+=($!)
|
|
||||||
websockify --web=/usr/share/novnc/ 0.0.0.0:"$NOVNC_PORT" localhost:5900 >/tmp/websockify.log 2>&1 &
|
|
||||||
pids+=($!)
|
|
||||||
}
|
|
||||||
|
|
||||||
start_vpn_terminal() {
|
|
||||||
# Start xterm with VPN script
|
|
||||||
sleep 1
|
|
||||||
xterm -fa 'Monospace' -fs 11 -bg black -fg white -geometry 120x35+50+50 \
|
|
||||||
-T "Cistech VPN" -e /tmp/vpn-runner.sh &
|
|
||||||
pids+=($!)
|
|
||||||
}
|
|
||||||
|
|
||||||
start_cloudflared() {
|
|
||||||
case "$CLOUDFLARED_MODE" in
|
|
||||||
token)
|
|
||||||
[ -n "$CLOUDFLARED_TOKEN" ] && cloudflared tunnel run --token "$CLOUDFLARED_TOKEN" >/tmp/cloudflared.log 2>&1 &
|
|
||||||
pids+=($!)
|
|
||||||
;;
|
|
||||||
config)
|
|
||||||
cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run >/tmp/cloudflared.log 2>&1 &
|
|
||||||
pids+=($!)
|
|
||||||
;;
|
|
||||||
off|*)
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
start_ssh_tunnel() {
|
|
||||||
[ "$SSH_TUNNEL_ENABLE" = "1" ] || return 0
|
|
||||||
IFS=',' read -ra LINES <<< "$SSH_FORWARDS"
|
|
||||||
args=(-N -o StrictHostKeyChecking=no -o ServerAliveInterval=60)
|
|
||||||
for m in "${LINES[@]}"; do args+=(-L "$m"); done
|
|
||||||
ssh "${args[@]}" "$SSH_DEST" &
|
|
||||||
pids+=($!)
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_nat() {
|
|
||||||
(
|
|
||||||
for i in {1..60}; do
|
|
||||||
if ip link show "$OC_INTERFACE" >/dev/null 2>&1; then
|
|
||||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null
|
|
||||||
iptables -t nat -C POSTROUTING -o "$OC_INTERFACE" -j MASQUERADE 2>/dev/null || \
|
|
||||||
iptables -t nat -A POSTROUTING -o "$OC_INTERFACE" -j MASQUERADE
|
|
||||||
echo "NAT enabled on $OC_INTERFACE"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
) &
|
|
||||||
}
|
|
||||||
|
|
||||||
trap 'kill 0' INT TERM
|
|
||||||
|
|
||||||
# Always start GUI now
|
|
||||||
setup_keyring
|
|
||||||
create_vpn_command
|
|
||||||
create_vpn_script
|
|
||||||
start_gui
|
|
||||||
start_vpn_terminal
|
|
||||||
setup_nat
|
|
||||||
start_cloudflared
|
|
||||||
start_ssh_tunnel
|
|
||||||
|
|
||||||
wait
|
|
||||||
54
apps/nas-samba/config.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../app-info-schema.json",
|
||||||
|
"name": "NAS Samba + CloudNAS",
|
||||||
|
"id": "nas-samba",
|
||||||
|
"available": true,
|
||||||
|
"port": 8080,
|
||||||
|
"exposable": true,
|
||||||
|
"dynamic_config": true,
|
||||||
|
"no_gui": false,
|
||||||
|
"tipi_version": 2,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"categories": ["network", "utilities"],
|
||||||
|
"description": "All-in-one NAS container with Samba SMB file sharing and CloudNAS web file manager. Browse files via web UI or connect via SMB at \\\\192.168.0.15\\data with username alexz. No database required — uses filesystem + SQLite.",
|
||||||
|
"short_desc": "NAS with SMB + web file manager",
|
||||||
|
"author": "alexz",
|
||||||
|
"source": "https://gits.alexzaw.dev/alexz/runtipi",
|
||||||
|
"form_fields": [
|
||||||
|
{
|
||||||
|
"type": "password",
|
||||||
|
"label": "SMB Password",
|
||||||
|
"env_variable": "SMB_PASSWORD",
|
||||||
|
"required": true,
|
||||||
|
"hint": "Password for SMB user alexz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "random",
|
||||||
|
"label": "JWT Secret",
|
||||||
|
"env_variable": "JWT_SECRET",
|
||||||
|
"min": 32,
|
||||||
|
"encoding": "hex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "NAS Host Path",
|
||||||
|
"env_variable": "NAS_PATH",
|
||||||
|
"required": true,
|
||||||
|
"default": "/nas",
|
||||||
|
"hint": "Host path to share via SMB and web UI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Bind IP",
|
||||||
|
"env_variable": "BIND_IP",
|
||||||
|
"required": true,
|
||||||
|
"default": "192.168.0.15",
|
||||||
|
"hint": "IP address to bind SMB ports to"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported_architectures": ["arm64", "amd64"],
|
||||||
|
"created_at": 1741536000000,
|
||||||
|
"updated_at": 1742169600000,
|
||||||
|
"deprecated": false,
|
||||||
|
"min_tipi_version": "4.5.0"
|
||||||
|
}
|
||||||
25
apps/nas-samba/docker-compose.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"name": "nas-samba",
|
||||||
|
"image": "git.alexzaw.dev/alexz/nas-samba:1.0.0",
|
||||||
|
"isMain": true,
|
||||||
|
"internalPort": 8080,
|
||||||
|
"addPorts": [
|
||||||
|
{ "hostPort": 445, "containerPort": 445, "interface": "${BIND_IP}" },
|
||||||
|
{ "hostPort": 139, "containerPort": 139, "interface": "${BIND_IP}" }
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
{ "hostPath": "${NAS_PATH:-/nas}", "containerPath": "/nas" },
|
||||||
|
{ "hostPath": "${APP_DATA_DIR}/data/config", "containerPath": "/config" }
|
||||||
|
],
|
||||||
|
"environment": [
|
||||||
|
{ "key": "SMB_PASSWORD", "value": "${SMB_PASSWORD}" },
|
||||||
|
{ "key": "JWT_SECRET", "value": "${JWT_SECRET}" },
|
||||||
|
{ "key": "TZ", "value": "${TZ}" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
apps/nas-samba/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
51
apps/nas-samba/source/Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
LABEL maintainer="alexz"
|
||||||
|
LABEL description="NAS Samba + CloudNAS file manager"
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
# Install runtime: Samba, supervisor, tini, Node.js 20.x, build tools for native modules
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
samba \
|
||||||
|
samba-common-bin \
|
||||||
|
supervisor \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
tini \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy app source
|
||||||
|
COPY app/ /app/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install production dependencies (compiles better-sqlite3 native module)
|
||||||
|
RUN npm ci --omit=dev && npm cache clean --force
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
RUN rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
RUN mkdir -p /nas /config /var/log/supervisor /var/log/samba /run/samba
|
||||||
|
|
||||||
|
# Samba configuration
|
||||||
|
COPY smb.conf /etc/samba/smb.conf
|
||||||
|
|
||||||
|
# Supervisord configuration
|
||||||
|
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
|
# Entrypoint
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 8080 139 445
|
||||||
|
|
||||||
|
ENTRYPOINT ["tini", "--"]
|
||||||
|
CMD ["/entrypoint.sh"]
|
||||||
13
apps/nas-samba/source/app/frontend/build/asset-manifest.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"files": {
|
||||||
|
"main.css": "/static/css/main.da1289f2.css",
|
||||||
|
"main.js": "/static/js/main.fcd0e5c7.js",
|
||||||
|
"index.html": "/index.html",
|
||||||
|
"main.da1289f2.css.map": "/static/css/main.da1289f2.css.map",
|
||||||
|
"main.fcd0e5c7.js.map": "/static/js/main.fcd0e5c7.js.map"
|
||||||
|
},
|
||||||
|
"entrypoints": [
|
||||||
|
"static/css/main.da1289f2.css",
|
||||||
|
"static/js/main.fcd0e5c7.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
apps/nas-samba/source/app/frontend/build/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#0F172A"/><meta name="description" content="CloudSync - Enterprise File Manager"/><title>CloudSync - Enterprise File Manager</title><script defer="defer" src="/static/js/main.fcd0e5c7.js"></script><link href="/static/css/main.da1289f2.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react-dom.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react-jsx-runtime.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* scheduler.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license lucide-react v0.312.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
15860
apps/nas-samba/source/app/frontend/package-lock.json
generated
Normal file
39
apps/nas-samba/source/app/frontend/package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "cloudsync-frontend",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.5",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^3.3.0",
|
||||||
|
"lucide-react": "^0.312.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"tailwind-merge": "^3.5.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
6
apps/nas-samba/source/app/frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
14
apps/nas-samba/source/app/frontend/public/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#0F172A" />
|
||||||
|
<meta name="description" content="CloudSync - Enterprise File Manager" />
|
||||||
|
<title>CloudSync - Enterprise File Manager</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
apps/nas-samba/source/app/frontend/src/App.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/* Additional app-specific styles */
|
||||||
51
apps/nas-samba/source/app/frontend/src/App.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import { Toaster, toast } from 'sonner';
|
||||||
|
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||||
|
import { ThemeProvider, useTheme } from './context/ThemeContext';
|
||||||
|
import LoginPage from './components/LoginPage';
|
||||||
|
import Dashboard from './components/Dashboard';
|
||||||
|
import SharePage from './components/SharePage';
|
||||||
|
import LoadingScreen from './components/ui/LoadingScreen';
|
||||||
|
|
||||||
|
function AppContent() {
|
||||||
|
const { user, loading } = useAuth();
|
||||||
|
|
||||||
|
// Handle /share/:token routes without React Router
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const shareMatch = path.match(/^\/share\/([a-f0-9]+)$/);
|
||||||
|
if (shareMatch) {
|
||||||
|
return <SharePage token={shareMatch[1]} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <LoadingScreen />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <LoginPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Dashboard />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<AuthProvider>
|
||||||
|
<Toaster
|
||||||
|
position="top-right"
|
||||||
|
richColors
|
||||||
|
closeButton
|
||||||
|
toastOptions={{
|
||||||
|
style: {
|
||||||
|
fontFamily: 'Inter, system-ui, sans-serif',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AppContent />
|
||||||
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
1366
apps/nas-samba/source/app/frontend/src/components/Dashboard.js
Normal file
145
apps/nas-samba/source/app/frontend/src/components/LoginPage.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Cloud, Eye, EyeOff, Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
|
function LoginPage() {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { login } = useAuth();
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!username.trim() || !password.trim()) {
|
||||||
|
toast.error('Please enter username and password');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await login(username, password);
|
||||||
|
toast.success(`Welcome back, ${user.name}!`);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error.response?.data?.detail || 'Invalid credentials';
|
||||||
|
toast.error(message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex">
|
||||||
|
{/* Left side - Background image */}
|
||||||
|
<div
|
||||||
|
className="hidden lg:flex lg:w-1/2 xl:w-3/5 relative bg-cover bg-center"
|
||||||
|
style={{
|
||||||
|
backgroundImage: 'url(https://images.pexels.com/photos/28428586/pexels-photo-28428586.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-black/40"></div>
|
||||||
|
<div className="relative z-10 flex flex-col justify-end p-12 text-white">
|
||||||
|
<h1 className="font-secondary text-4xl font-bold mb-4">CloudSync</h1>
|
||||||
|
<p className="text-lg text-white/80 max-w-md">
|
||||||
|
Enterprise-grade file management with secure sharing, storage quotas, and comprehensive activity tracking.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side - Login form */}
|
||||||
|
<div className="flex-1 flex items-center justify-center p-8 bg-background">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
{/* Logo for mobile */}
|
||||||
|
<div className="lg:hidden flex items-center gap-3 mb-8">
|
||||||
|
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
|
||||||
|
<Cloud className="w-5 h-5 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
<span className="font-secondary text-2xl font-bold text-foreground">CloudSync</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="font-secondary text-3xl font-bold text-foreground mb-2">Sign in</h2>
|
||||||
|
<p className="text-muted-foreground">Enter your credentials to access your files</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="username" className="block text-sm font-medium text-foreground mb-2">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
placeholder="Enter your username"
|
||||||
|
disabled={loading}
|
||||||
|
data-testid="login-username-input"
|
||||||
|
className="w-full h-11 px-4 rounded-lg border border-input bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all disabled:opacity-50"
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-foreground mb-2">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="Enter your password"
|
||||||
|
disabled={loading}
|
||||||
|
data-testid="login-password-input"
|
||||||
|
className="w-full h-11 px-4 pr-12 rounded-lg border border-input bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all disabled:opacity-50"
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
data-testid="toggle-password-visibility"
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
data-testid="login-submit-btn"
|
||||||
|
className="w-full h-11 bg-primary text-primary-foreground font-medium rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Signing in...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Sign in'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-8 p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<span className="font-medium text-foreground">Demo credentials:</span>
|
||||||
|
<br />
|
||||||
|
Username: <code className="font-mono bg-background px-1 rounded">admin</code>
|
||||||
|
<br />
|
||||||
|
Password: <code className="font-mono bg-background px-1 rounded">admin123</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
182
apps/nas-samba/source/app/frontend/src/components/SharePage.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Cloud, CheckCircle, Lock, FileText, AlertCircle } from 'lucide-react';
|
||||||
|
import { formatBytes } from '../lib/utils';
|
||||||
|
import * as api from '../lib/api';
|
||||||
|
|
||||||
|
export default function SharePage({ token }) {
|
||||||
|
const [fileInfo, setFileInfo] = useState(null);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [downloaded, setDownloaded] = useState(false);
|
||||||
|
const [downloading, setDownloading] = useState(false);
|
||||||
|
|
||||||
|
const doDownload = useCallback(async (pwd) => {
|
||||||
|
if (!fileInfo || downloading) return;
|
||||||
|
setDownloading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const url = api.downloadSharedFile(token, pwd || null);
|
||||||
|
const res = await fetch(url, { method: 'POST' });
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
throw new Error(data.detail || 'Download failed');
|
||||||
|
}
|
||||||
|
const blob = await res.blob();
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = blobUrl;
|
||||||
|
a.download = fileInfo.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
setDownloaded(true);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
setDownloading(false);
|
||||||
|
}
|
||||||
|
}, [fileInfo, downloading, token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const res = await api.getSharedFileInfo(token);
|
||||||
|
setFileInfo(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.response?.status;
|
||||||
|
if (status === 410) {
|
||||||
|
setError('This share link has expired.');
|
||||||
|
} else if (status === 404) {
|
||||||
|
setError('Share link not found.');
|
||||||
|
} else {
|
||||||
|
setError('Failed to load shared file.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load();
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
// Auto-download if no password required
|
||||||
|
useEffect(() => {
|
||||||
|
if (fileInfo && !fileInfo.requires_password && !downloaded && !downloading) {
|
||||||
|
doDownload(null);
|
||||||
|
}
|
||||||
|
}, [fileInfo, downloaded, downloading, doDownload]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||||
|
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error && !fileInfo) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
|
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
|
||||||
|
<AlertCircle className="w-12 h-12 text-destructive mx-auto mb-4" />
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Unavailable</h2>
|
||||||
|
<p className="text-muted-foreground">{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downloaded state
|
||||||
|
if (downloaded) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
|
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
|
||||||
|
<div className="flex items-center justify-center gap-2 mb-6">
|
||||||
|
<Cloud className="w-6 h-6 text-primary" />
|
||||||
|
<span className="text-lg font-semibold">CloudSync</span>
|
||||||
|
</div>
|
||||||
|
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
|
||||||
|
<h2 className="text-xl font-semibold mb-2">File Downloaded</h2>
|
||||||
|
<div className="flex items-center gap-3 justify-center p-3 bg-accent/50 rounded-lg mt-4">
|
||||||
|
<FileText className="w-6 h-6 text-muted-foreground flex-shrink-0" />
|
||||||
|
<div className="text-left min-w-0">
|
||||||
|
<p className="font-medium text-sm truncate">{fileInfo.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{formatBytes(fileInfo.size)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mt-4">This link has been used and is no longer active.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password required state
|
||||||
|
if (fileInfo.requires_password && !downloaded) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
|
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full">
|
||||||
|
<div className="flex items-center gap-2 mb-6">
|
||||||
|
<Cloud className="w-6 h-6 text-primary" />
|
||||||
|
<span className="text-lg font-semibold">CloudSync</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 mb-6 p-4 bg-accent/50 rounded-lg">
|
||||||
|
<FileText className="w-10 h-10 text-muted-foreground flex-shrink-0" />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-medium truncate">{fileInfo.name}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{formatBytes(fileInfo.size)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 bg-destructive/10 text-destructive text-sm rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium mb-2">
|
||||||
|
<Lock className="w-4 h-4 inline mr-1" />
|
||||||
|
This file is password protected
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => { setPassword(e.target.value); setError(null); }}
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Enter' && password) doDownload(password); }}
|
||||||
|
placeholder="Enter password"
|
||||||
|
className="w-full h-10 px-4 bg-background border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => doDownload(password)}
|
||||||
|
disabled={downloading || !password}
|
||||||
|
className="w-full h-10 bg-primary text-primary-foreground rounded-lg hover:opacity-90 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{downloading ? (
|
||||||
|
<div className="animate-spin w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full"></div>
|
||||||
|
) : (
|
||||||
|
'Download'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downloading state (auto-download in progress, no password)
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
|
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
|
||||||
|
<div className="flex items-center justify-center gap-2 mb-6">
|
||||||
|
<Cloud className="w-6 h-6 text-primary" />
|
||||||
|
<span className="text-lg font-semibold">CloudSync</span>
|
||||||
|
</div>
|
||||||
|
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full mx-auto mb-4"></div>
|
||||||
|
<p className="text-muted-foreground">Downloading {fileInfo.name}...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
|
export function LoadingScreen() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="relative w-12 h-12">
|
||||||
|
<div className="absolute inset-0 rounded-full border-2 border-muted"></div>
|
||||||
|
<div className="absolute inset-0 rounded-full border-2 border-primary border-t-transparent animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground font-medium">Loading CloudSync...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadingScreen;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||||
|
import { login as apiLogin, getMe } from '../lib/api';
|
||||||
|
|
||||||
|
const AuthContext = createContext(null);
|
||||||
|
|
||||||
|
export function AuthProvider({ children }) {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [token, setToken] = useState(localStorage.getItem('token'));
|
||||||
|
|
||||||
|
const checkAuth = useCallback(async () => {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
if (!storedToken) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getMe();
|
||||||
|
setUser(response.data);
|
||||||
|
setToken(storedToken);
|
||||||
|
} catch (error) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
setUser(null);
|
||||||
|
setToken(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuth();
|
||||||
|
}, [checkAuth]);
|
||||||
|
|
||||||
|
const login = async (username, password) => {
|
||||||
|
const response = await apiLogin(username, password);
|
||||||
|
const { token: newToken, user: userData } = response.data;
|
||||||
|
|
||||||
|
localStorage.setItem('token', newToken);
|
||||||
|
setToken(newToken);
|
||||||
|
setUser(userData);
|
||||||
|
|
||||||
|
return userData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
setToken(null);
|
||||||
|
setUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUser = useCallback((userData) => {
|
||||||
|
setUser(prev => ({ ...prev, ...userData }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ user, token, loading, login, logout, updateUser, checkAuth }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const ThemeContext = createContext(null);
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }) {
|
||||||
|
const [theme, setTheme] = useState(() => {
|
||||||
|
const stored = localStorage.getItem('theme');
|
||||||
|
if (stored) return stored;
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement;
|
||||||
|
root.classList.remove('light', 'dark');
|
||||||
|
root.classList.add(theme);
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTheme must be used within a ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
116
apps/nas-samba/source/app/frontend/src/index.css
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: #FFFFFF;
|
||||||
|
--foreground: #0A0A0A;
|
||||||
|
--card: #FFFFFF;
|
||||||
|
--card-foreground: #0A0A0A;
|
||||||
|
--popover: #FFFFFF;
|
||||||
|
--popover-foreground: #0A0A0A;
|
||||||
|
--primary: #0F172A;
|
||||||
|
--primary-foreground: #F8FAFC;
|
||||||
|
--secondary: #F1F5F9;
|
||||||
|
--secondary-foreground: #0F172A;
|
||||||
|
--muted: #F1F5F9;
|
||||||
|
--muted-foreground: #64748B;
|
||||||
|
--accent: #F1F5F9;
|
||||||
|
--accent-foreground: #0F172A;
|
||||||
|
--destructive: #EF4444;
|
||||||
|
--destructive-foreground: #F8FAFC;
|
||||||
|
--border: #E2E8F0;
|
||||||
|
--input: #E2E8F0;
|
||||||
|
--ring: #0F172A;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: #020617;
|
||||||
|
--foreground: #F8FAFC;
|
||||||
|
--card: #0F172A;
|
||||||
|
--card-foreground: #F8FAFC;
|
||||||
|
--popover: #0F172A;
|
||||||
|
--popover-foreground: #F8FAFC;
|
||||||
|
--primary: #F8FAFC;
|
||||||
|
--primary-foreground: #0F172A;
|
||||||
|
--secondary: #1E293B;
|
||||||
|
--secondary-foreground: #F8FAFC;
|
||||||
|
--muted: #1E293B;
|
||||||
|
--muted-foreground: #94A3B8;
|
||||||
|
--accent: #1E293B;
|
||||||
|
--accent-foreground: #F8FAFC;
|
||||||
|
--destructive: #7F1D1D;
|
||||||
|
--destructive-foreground: #F8FAFC;
|
||||||
|
--border: #1E293B;
|
||||||
|
--input: #1E293B;
|
||||||
|
--ring: #D1D5DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--muted-foreground);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skeleton loader animation */
|
||||||
|
@keyframes skeleton-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton {
|
||||||
|
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
||||||
|
background: linear-gradient(90deg, var(--muted) 25%, var(--border) 50%, var(--muted) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File drop zone styles */
|
||||||
|
.drop-zone-active {
|
||||||
|
border-color: var(--ring) !important;
|
||||||
|
background-color: var(--accent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus visible styles */
|
||||||
|
.focus-visible:focus {
|
||||||
|
outline: 2px solid var(--ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition utilities */
|
||||||
|
.transition-default {
|
||||||
|
transition: all 200ms ease-in-out;
|
||||||
|
}
|
||||||
11
apps/nas-samba/source/app/frontend/src/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
136
apps/nas-samba/source/app/frontend/src/lib/api.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_URL = process.env.REACT_APP_BACKEND_URL || '';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: API_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request interceptor to add auth token
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response interceptor to handle auth errors
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
export const login = (username, password) =>
|
||||||
|
api.post('/api/auth/login', { username, password });
|
||||||
|
|
||||||
|
export const getMe = () =>
|
||||||
|
api.get('/api/auth/me');
|
||||||
|
|
||||||
|
// Users
|
||||||
|
export const getUsers = () =>
|
||||||
|
api.get('/api/users');
|
||||||
|
|
||||||
|
export const createUser = (userData) =>
|
||||||
|
api.post('/api/users', userData);
|
||||||
|
|
||||||
|
export const updateUser = (userId, userData) =>
|
||||||
|
api.put(`/api/users/${userId}`, userData);
|
||||||
|
|
||||||
|
export const deleteUser = (userId) =>
|
||||||
|
api.delete(`/api/users/${userId}`);
|
||||||
|
|
||||||
|
// Files
|
||||||
|
export const getFiles = (parentId = null, search = null) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (parentId) params.append('parent_id', parentId);
|
||||||
|
if (search) params.append('search', search);
|
||||||
|
return api.get(`/api/files?${params.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFile = (fileId) =>
|
||||||
|
api.get(`/api/files/${fileId}`);
|
||||||
|
|
||||||
|
export const createFolder = (name, parentId = null) =>
|
||||||
|
api.post('/api/files/folder', { name, parent_id: parentId, is_folder: true });
|
||||||
|
|
||||||
|
export const uploadFile = (file, parentId, onProgress) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
if (parentId) {
|
||||||
|
formData.append('parent_id', parentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.post('/api/files/upload', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
if (onProgress) {
|
||||||
|
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||||
|
onProgress(progress);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renameFile = (fileId, newName) =>
|
||||||
|
api.put(`/api/files/${fileId}/rename`, { new_name: newName });
|
||||||
|
|
||||||
|
export const moveFile = (fileId, destinationId) =>
|
||||||
|
api.put(`/api/files/${fileId}/move`, { destination_id: destinationId });
|
||||||
|
|
||||||
|
export const copyFile = (fileId, destinationId) =>
|
||||||
|
api.post(`/api/files/${fileId}/copy`, { destination_id: destinationId });
|
||||||
|
|
||||||
|
export const deleteFile = (fileId) =>
|
||||||
|
api.delete(`/api/files/${fileId}`);
|
||||||
|
|
||||||
|
export const bulkDeleteFiles = (fileIds) =>
|
||||||
|
api.post('/api/files/bulk-delete', fileIds);
|
||||||
|
|
||||||
|
export const getDownloadUrl = (fileId) =>
|
||||||
|
`${API_URL}/api/files/${fileId}/download`;
|
||||||
|
|
||||||
|
export const getPreviewUrl = (fileId) =>
|
||||||
|
`${API_URL}/api/files/${fileId}/preview`;
|
||||||
|
|
||||||
|
export const getBreadcrumb = (fileId) =>
|
||||||
|
api.get(`/api/files/${fileId}/breadcrumb`);
|
||||||
|
|
||||||
|
// Shares
|
||||||
|
export const getShares = () =>
|
||||||
|
api.get('/api/shares');
|
||||||
|
|
||||||
|
export const createShare = (fileId, expiresInHours = 24, password = null) =>
|
||||||
|
api.post('/api/shares', { file_id: fileId, expires_in_hours: expiresInHours, password });
|
||||||
|
|
||||||
|
export const deleteShare = (shareId) =>
|
||||||
|
api.delete(`/api/shares/${shareId}`);
|
||||||
|
|
||||||
|
export const getSharedFileInfo = (token) =>
|
||||||
|
api.get(`/api/share/${token}`);
|
||||||
|
|
||||||
|
export const downloadSharedFile = (token, password = null) => {
|
||||||
|
const params = password ? `?password=${encodeURIComponent(password)}` : '';
|
||||||
|
return `${API_URL}/api/share/${token}/download${params}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Activity
|
||||||
|
export const getActivity = (limit = 50) =>
|
||||||
|
api.get(`/api/activity?limit=${limit}`);
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
export const getStats = () =>
|
||||||
|
api.get('/api/stats');
|
||||||
|
|
||||||
|
export default api;
|
||||||
33
apps/nas-samba/source/app/frontend/src/lib/utils.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { clsx } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export function cn(...inputs) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBytes(bytes, decimals = 1) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(dateString) {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = Math.floor((now - date) / 1000);
|
||||||
|
|
||||||
|
if (diff < 60) return 'Just now';
|
||||||
|
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
||||||
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
||||||
|
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
|
||||||
|
|
||||||
|
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileExtension(filename) {
|
||||||
|
return filename.includes('.') ? filename.split('.').pop().toLowerCase() : '';
|
||||||
|
}
|
||||||
60
apps/nas-samba/source/app/frontend/tailwind.config.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
background: "var(--background)",
|
||||||
|
foreground: "var(--foreground)",
|
||||||
|
card: "var(--card)",
|
||||||
|
"card-foreground": "var(--card-foreground)",
|
||||||
|
popover: "var(--popover)",
|
||||||
|
"popover-foreground": "var(--popover-foreground)",
|
||||||
|
primary: "var(--primary)",
|
||||||
|
"primary-foreground": "var(--primary-foreground)",
|
||||||
|
secondary: "var(--secondary)",
|
||||||
|
"secondary-foreground": "var(--secondary-foreground)",
|
||||||
|
muted: "var(--muted)",
|
||||||
|
"muted-foreground": "var(--muted-foreground)",
|
||||||
|
accent: "var(--accent)",
|
||||||
|
"accent-foreground": "var(--accent-foreground)",
|
||||||
|
destructive: "var(--destructive)",
|
||||||
|
"destructive-foreground": "var(--destructive-foreground)",
|
||||||
|
border: "var(--border)",
|
||||||
|
input: "var(--input)",
|
||||||
|
ring: "var(--ring)",
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
primary: ['Inter', 'system-ui', 'sans-serif'],
|
||||||
|
secondary: ['Manrope', 'system-ui', 'sans-serif'],
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fadeIn 0.3s ease-out',
|
||||||
|
'slide-up': 'slideUp 0.3s ease-out',
|
||||||
|
'slide-down': 'slideDown 0.3s ease-out',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
||||||
|
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
slideDown: {
|
||||||
|
'0%': { opacity: '0', transform: 'translateY(-10px)' },
|
||||||
|
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
2558
apps/nas-samba/source/app/package-lock.json
generated
Normal file
21
apps/nas-samba/source/app/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "cloudsync",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "CloudSync File Manager - NAS Edition",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node --watch server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"archiver": "^7.0.1",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"better-sqlite3": "^11.7.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"multer": "^1.4.5-lts.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
1499
apps/nas-samba/source/app/server.js
Normal file
33
apps/nas-samba/source/entrypoint.sh
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Create users group if it doesn't exist
|
||||||
|
getent group users >/dev/null 2>&1 || groupadd -g 100 users
|
||||||
|
|
||||||
|
# Create alexz user with UID 1000 if not exists
|
||||||
|
if ! id -u alexz >/dev/null 2>&1; then
|
||||||
|
# Remove any existing user with UID 1000 (e.g. ubuntu)
|
||||||
|
existing_user=$(getent passwd 1000 | cut -d: -f1)
|
||||||
|
if [ -n "$existing_user" ] && [ "$existing_user" != "alexz" ]; then
|
||||||
|
userdel "$existing_user" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
useradd -u 1000 -g users -M -s /usr/sbin/nologin alexz
|
||||||
|
echo "Created user alexz (UID 1000)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set Samba password from environment variable
|
||||||
|
if [ -n "$SMB_PASSWORD" ]; then
|
||||||
|
echo -e "${SMB_PASSWORD}\n${SMB_PASSWORD}" | smbpasswd -a -s alexz
|
||||||
|
echo "Samba password set for alexz"
|
||||||
|
else
|
||||||
|
echo "WARNING: SMB_PASSWORD not set. Samba authentication will fail."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure directories exist
|
||||||
|
mkdir -p /config /var/log/samba /run/samba /nas
|
||||||
|
|
||||||
|
# Ensure correct ownership
|
||||||
|
chown alexz:users /nas 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Starting supervisord..."
|
||||||
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
18
apps/nas-samba/source/smb.conf
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[global]
|
||||||
|
workgroup = WORKGROUP
|
||||||
|
server string = NAS Samba
|
||||||
|
server role = standalone server
|
||||||
|
log file = /var/log/samba/log.%m
|
||||||
|
max log size = 50
|
||||||
|
logging = file
|
||||||
|
map to guest = bad user
|
||||||
|
server min protocol = SMB2
|
||||||
|
server max protocol = SMB3
|
||||||
|
|
||||||
|
[data]
|
||||||
|
path = /nas
|
||||||
|
valid users = alexz
|
||||||
|
browsable = yes
|
||||||
|
writable = yes
|
||||||
|
create mask = 0664
|
||||||
|
directory mask = 0775
|
||||||
34
apps/nas-samba/source/supervisord.conf
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/var/log/supervisor/supervisord.log
|
||||||
|
pidfile=/var/run/supervisord.pid
|
||||||
|
childlogdir=/var/log/supervisor
|
||||||
|
|
||||||
|
[program:smbd]
|
||||||
|
command=/usr/sbin/smbd -F --no-process-group
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
priority=10
|
||||||
|
stdout_logfile=/var/log/supervisor/smbd-stdout.log
|
||||||
|
stderr_logfile=/var/log/supervisor/smbd-stderr.log
|
||||||
|
|
||||||
|
[program:nmbd]
|
||||||
|
command=/usr/sbin/nmbd -F --no-process-group
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
priority=10
|
||||||
|
stdout_logfile=/var/log/supervisor/nmbd-stdout.log
|
||||||
|
stderr_logfile=/var/log/supervisor/nmbd-stderr.log
|
||||||
|
|
||||||
|
[program:cloudnas]
|
||||||
|
command=node /app/server.js
|
||||||
|
directory=/app
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
priority=20
|
||||||
|
environment=PORT="8080",UPLOAD_DIR="/nas",DB_PATH="/config/cloudsync.db",JWT_SECRET="%(ENV_JWT_SECRET)s",SMB_PASSWORD="%(ENV_SMB_PASSWORD)s"
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
services:
|
|
||||||
nginx-proxy-manager:
|
|
||||||
image: jc21/nginx-proxy-manager:2.13.5
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
nginx-proxy-manager_runtipi_network:
|
|
||||||
gw_priority: 0
|
|
||||||
tipi_main_network:
|
|
||||||
gw_priority: 1
|
|
||||||
cistech-macvlan:
|
|
||||||
environment:
|
|
||||||
DISABLE_IPV6: "true"
|
|
||||||
ports:
|
|
||||||
- ${NPM_HTTP_PORT}:80
|
|
||||||
- ${NPM_HTTPS_PORT}:443
|
|
||||||
- ${APP_PORT}:81
|
|
||||||
volumes:
|
|
||||||
- ${APP_DATA_DIR}/data:/data
|
|
||||||
- ${APP_DATA_DIR}/letsencrypt:/etc/letsencrypt
|
|
||||||
labels:
|
|
||||||
generated: true
|
|
||||||
traefik.enable: true
|
|
||||||
traefik.docker.network: runtipi_tipi_main_network
|
|
||||||
traefik.http.middlewares.nginx-proxy-manager-runtipi-web-redirect.redirectscheme.scheme: https
|
|
||||||
traefik.http.services.nginx-proxy-manager-runtipi.loadbalancer.server.port: "81"
|
|
||||||
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
|
|
||||||
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.entrypoints: web
|
|
||||||
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.service: nginx-proxy-manager-runtipi
|
|
||||||
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.middlewares: nginx-proxy-manager-runtipi-web-redirect
|
|
||||||
traefik.http.routers.nginx-proxy-manager-runtipi.rule: Host(`${APP_DOMAIN}`)
|
|
||||||
traefik.http.routers.nginx-proxy-manager-runtipi.entrypoints: websecure
|
|
||||||
traefik.http.routers.nginx-proxy-manager-runtipi.service: nginx-proxy-manager-runtipi
|
|
||||||
traefik.http.routers.nginx-proxy-manager-runtipi.tls.certresolver: myresolver
|
|
||||||
runtipi.managed: true
|
|
||||||
runtipi.appurn: nginx-proxy-manager:runtipi
|
|
||||||
networks:
|
|
||||||
tipi_main_network:
|
|
||||||
name: runtipi_tipi_main_network
|
|
||||||
external: true
|
|
||||||
nginx-proxy-manager_runtipi_network:
|
|
||||||
name: nginx-proxy-manager_runtipi_network
|
|
||||||
external: false
|
|
||||||
cistech-macvlan:
|
|
||||||
name: cistech-macvlan
|
|
||||||
external: true
|
|
||||||
|
|
||||||
20
apps/nginx-proxy-manager/config.json → apps/npm/config.json
Executable file → Normal file
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "Nginx Proxy Manager",
|
"name": "Nginx Proxy Manager",
|
||||||
"id": "nginx-proxy-manager",
|
"id": "npm",
|
||||||
"available": true,
|
"available": true,
|
||||||
"short_desc": "Docker container for managing Nginx proxy hosts with a simple, powerful web interface",
|
"short_desc": "Docker container for managing Nginx proxy hosts with a simple, powerful web interface",
|
||||||
"author": "jc21",
|
"author": "jc21",
|
||||||
"port": 81,
|
"hostname": "nginx.alexzaw.dev",
|
||||||
"categories": [
|
"categories": [
|
||||||
"utilities",
|
"utilities",
|
||||||
"network"
|
"network"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"type": "number",
|
"type": "number",
|
||||||
"label": "HTTP Port",
|
"label": "HTTP Port",
|
||||||
"env_variable": "NPM_HTTP_PORT",
|
"env_variable": "NPM_HTTP_PORT",
|
||||||
"default": "1080",
|
"default": "80",
|
||||||
"required": true,
|
"required": true,
|
||||||
"hint": "Port for HTTP traffic (mapped to container port 80)"
|
"hint": "Port for HTTP traffic (mapped to container port 80)"
|
||||||
},
|
},
|
||||||
@@ -30,7 +30,15 @@
|
|||||||
"type": "number",
|
"type": "number",
|
||||||
"label": "HTTPS Port",
|
"label": "HTTPS Port",
|
||||||
"env_variable": "NPM_HTTPS_PORT",
|
"env_variable": "NPM_HTTPS_PORT",
|
||||||
"default": "10443",
|
"default": "443",
|
||||||
|
"required": true,
|
||||||
|
"hint": "Port for HTTPS traffic (mapped to container port 443)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Web UI Port",
|
||||||
|
"env_variable": "NPM_WEBUI_PORT",
|
||||||
|
"default": "81",
|
||||||
"required": true,
|
"required": true,
|
||||||
"hint": "Port for HTTPS traffic (mapped to container port 443)"
|
"hint": "Port for HTTPS traffic (mapped to container port 443)"
|
||||||
}
|
}
|
||||||
@@ -38,7 +46,5 @@
|
|||||||
"supported_architectures": [
|
"supported_architectures": [
|
||||||
"arm64",
|
"arm64",
|
||||||
"amd64"
|
"amd64"
|
||||||
],
|
]
|
||||||
"created_at": 1731607800000,
|
|
||||||
"updated_at": 1731607800000
|
|
||||||
}
|
}
|
||||||
17
apps/nginx-proxy-manager/docker-compose.json → apps/npm/docker-compose.json
Executable file → Normal file
@@ -1,23 +1,28 @@
|
|||||||
{
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"name": "nginx-proxy-manager",
|
"name": "npm",
|
||||||
"image": "jc21/nginx-proxy-manager:2.13.5",
|
"image": "jc21/nginx-proxy-manager:2.13.5",
|
||||||
"isMain": true,
|
"isMain": true,
|
||||||
"internalPort": 81,
|
"internalPort": 81,
|
||||||
"addPorts": [
|
"addPorts": [
|
||||||
{
|
{
|
||||||
"hostPort": "${NPM_HTTP_PORT}",
|
"hostPort": "192.168.1.150:${NPM_HTTP_PORT}",
|
||||||
"containerPort": 80
|
"containerPort": 80
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"hostPort": "${NPM_HTTPS_PORT}",
|
"hostPort": "192.168.1.150:${NPM_HTTPS_PORT}",
|
||||||
"containerPort": 443
|
"containerPort": 443
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hostPort": "192.168.1.150:${NPM_WEBUI_PORT}",
|
||||||
|
"containerPort": 81
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"environment": {
|
"environment": [
|
||||||
"DISABLE_IPV6": "true"
|
{"key": "DISABLE_IPV6", "value": "true"}
|
||||||
},
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
{
|
{
|
||||||
"hostPath": "${APP_DATA_DIR}/data",
|
"hostPath": "${APP_DATA_DIR}/data",
|
||||||
0
apps/nginx-proxy-manager/metadata/description.md → apps/npm/metadata/description.md
Executable file → Normal file
0
apps/nginx-proxy-manager/metadata/logo.jpg → apps/npm/metadata/logo.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
0
apps/nginx-proxy-manager/metadata/logo.png → apps/npm/metadata/logo.png
Executable file → Normal file
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows 11 Enterprise",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "11e"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows 11 LTSC",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "11l"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows 10 Pro",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "10"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows 10 Enterprise",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "10e"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows 10 LTSC",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "10l"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows 8.1 Enterprise",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "8e"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows 7 Ultimate",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "7u"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows Vista Ultimate",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "vu"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows XP Professional",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "xp"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows 2000 Professional",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "2k"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows Server 2025",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "2025"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows Server 2022",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "2022"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows Server 2019",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "2019"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows Server 2016",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "2016"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows Server 2012 R2",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "2012"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows Server 2008 R2",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "2008"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows Server 2003",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "2003"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Tiny11",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "tiny11"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Tiny11 Core",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "core11"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Tiny11 Nano",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "nano11"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Tiny10",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "tiny10"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "../codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
services:
|
|
||||||
windows:
|
|
||||||
container_name: windows
|
|
||||||
image: ghcr.io/dockur/windows
|
|
||||||
environment:
|
|
||||||
RAM_SIZE: "half"
|
|
||||||
DISK_SIZE: "max"
|
|
||||||
CPU_CORES: "max"
|
|
||||||
devices:
|
|
||||||
- /dev/kvm
|
|
||||||
- /dev/net/tun
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
ports:
|
|
||||||
- 8006:8006
|
|
||||||
- 3389:3389/tcp
|
|
||||||
- 3389:3389/udp
|
|
||||||
volumes:
|
|
||||||
- ./windows:/storage
|
|
||||||
restart: on-failure
|
|
||||||
stop_grace_period: 2m
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Windows 11 Pro",
|
|
||||||
"service": "windows",
|
|
||||||
"containerEnv": {
|
|
||||||
"VERSION": "11"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8006],
|
|
||||||
"portsAttributes": {
|
|
||||||
"8006": {
|
|
||||||
"label": "Web",
|
|
||||||
"onAutoForward": "notify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"otherPortsAttributes": {
|
|
||||||
"onAutoForward": "ignore"
|
|
||||||
},
|
|
||||||
"dockerComposeFile": "codespaces.yml",
|
|
||||||
"initializeCommand": "docker system prune --all --force"
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
.dockerignore
|
|
||||||
.devcontainer
|
|
||||||
.git
|
|
||||||
.github
|
|
||||||
.gitignore
|
|
||||||
.gitlab-ci.yml
|
|
||||||
.gitmodules
|
|
||||||
Dockerfile
|
|
||||||
Dockerfile.archive
|
|
||||||
compose.yml
|
|
||||||
compose.yaml
|
|
||||||
docker-compose.yml
|
|
||||||
docker-compose.yaml
|
|
||||||
*.md
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
name: "\U0001F6A8 Technical issue"
|
|
||||||
description: When you're experiencing problems using the container
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
id: os
|
|
||||||
attributes:
|
|
||||||
label: Operating system
|
|
||||||
description: Your Linux distribution (can be shown by `lsb_release -a`).
|
|
||||||
placeholder: e.g. Ubuntu 24.04
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: summary
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: A clear and concise description of your issue.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: compose
|
|
||||||
attributes:
|
|
||||||
label: Docker compose
|
|
||||||
description: The compose file (or otherwise the `docker run` command used).
|
|
||||||
render: yaml
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: log
|
|
||||||
attributes:
|
|
||||||
label: Docker log
|
|
||||||
description: The logfile of the container (as shown by `docker logs windows`).
|
|
||||||
render: shell
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: screenshot
|
|
||||||
attributes:
|
|
||||||
label: Screenshots (optional)
|
|
||||||
description: Screenshots that might help to make the problem more clear.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
name: "\U0001F680 Feature request"
|
|
||||||
description: Suggest an idea for improving the container
|
|
||||||
title: "[Feature]: "
|
|
||||||
labels: ["enhancement"]
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
id: problem
|
|
||||||
attributes:
|
|
||||||
label: Is your proposal related to a problem?
|
|
||||||
description: |
|
|
||||||
Provide a clear and concise description of what the problem is.
|
|
||||||
For example, "I'm always frustrated when..."
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: solution
|
|
||||||
attributes:
|
|
||||||
label: Describe the solution you'd like.
|
|
||||||
description: |
|
|
||||||
Provide a clear and concise description of what you want to happen.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: alternatives
|
|
||||||
attributes:
|
|
||||||
label: Describe alternatives you've considered.
|
|
||||||
description: |
|
|
||||||
Let us know about other solutions you've tried or researched.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: context
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: |
|
|
||||||
Is there anything else you can add about the proposal?
|
|
||||||
You might want to link to related issues here, if you haven't already.
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
name: "\U0001F41E Bug report"
|
|
||||||
description: Create a report to help us improve the container
|
|
||||||
title: "[Bug]: "
|
|
||||||
labels: ["bug"]
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
id: os
|
|
||||||
attributes:
|
|
||||||
label: Operating system
|
|
||||||
description: Your Linux distribution (can be shown by `lsb_release -a`).
|
|
||||||
placeholder: e.g. Ubuntu 24.04
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: summary
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: Describe the expected behaviour, the actual behaviour, and the steps to reproduce.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: compose
|
|
||||||
attributes:
|
|
||||||
label: Docker compose
|
|
||||||
description: The compose file (or otherwise the `docker run` command used).
|
|
||||||
render: yaml
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: log
|
|
||||||
attributes:
|
|
||||||
label: Docker log
|
|
||||||
description: The logfile of the container (as shown by `docker logs windows`).
|
|
||||||
render: shell
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: screenshot
|
|
||||||
attributes:
|
|
||||||
label: Screenshots (optional)
|
|
||||||
description: Screenshots that might help to make the problem more clear.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
name: "\U00002753 General question"
|
|
||||||
description: Questions about the container not related to an issue
|
|
||||||
title: "[Question]: "
|
|
||||||
labels: ["question"]
|
|
||||||
body:
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Is your question not already answered in the FAQ?
|
|
||||||
description: Please read the [FAQ](https://github.com/dockur/windows/blob/master/readme.md) carefully to avoid asking duplicate questions.
|
|
||||||
options:
|
|
||||||
- label: I made sure the question is not listed in the [FAQ](https://github.com/dockur/windows/blob/master/readme.md).
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
|
||||||
attributes:
|
|
||||||
label: Is this a general question and not a technical issue?
|
|
||||||
description: For questions related to issues you must use the [technical issue](https://github.com/dockur/windows/issues/new?assignees=&labels=&projects=&template=1-issue.yml) form instead. It contains all the right fields (system info, logfiles, etc.) we need in order to be able to help you.
|
|
||||||
options:
|
|
||||||
- label: I am sure my question is not about a technical issue.
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: question
|
|
||||||
attributes:
|
|
||||||
label: Question
|
|
||||||
description: What's the question you have about the container?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
10
apps/rego-tunnel/build/.github/dependabot.yml
vendored
@@ -1,10 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: docker
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
BIN
apps/rego-tunnel/build/.github/logo.png
vendored
|
Before Width: | Height: | Size: 242 KiB |