From 37d1bfc089eaabeb361a478e411aa784a00f9659 Mon Sep 17 00:00:00 2001 From: Gouryella Date: Thu, 15 Jan 2026 17:18:27 +0800 Subject: [PATCH 1/2] feat(client): Support predefined tunnel configuration and management commands Added predefined tunnel functionality, allowing users to define multiple tunnels in the configuration file and start them by name, including the following improvements: - Added --all flag to start all configured tunnels - Added parameterless start command to list available tunnels - Support configuration of multiple tunnel types (http, https, tcp) - Support advanced configurations such as subdomains, transport protocols, and IP allowlists refactor(deployments): Refactor Docker deployment configuration Removed old Dockerfile and Compose configurations, added new deployment files: - Removed .env.example and old Docker build files - Added Caddy reverse proxy configuration file - Added two deployment modes: standard and Caddy reverse proxy - Added detailed server configuration example files docs: Update documentation to include tunnel configuration and deployment guide Updated Chinese and English README documents: - Added usage instructions and configuration examples for predefined tunnels - Expanded server deployment section to include direct TLS and reverse proxy modes - Added server configuration reference table with detailed configuration item descriptions - Added specific configuration methods for Caddy and Nginx reverse proxies --- .env.example | 49 --- .github/workflows/docker.yml | 42 +-- README.md | 290 ++---------------- README_CN.md | 290 ++---------------- deployments/Caddyfile | 34 ++ deployments/Dockerfile | 38 --- deployments/Dockerfile.client | 33 -- deployments/Dockerfile.release | 58 ---- deployments/Dockerfile.server | 48 +++ deployments/README.md | 249 --------------- deployments/config.caddy.example.yaml | 40 +++ deployments/config.example.yaml | 37 +++ deployments/docker-compose.caddy.yml | 28 ++ deployments/docker-compose.yml | 11 + .../nginx.example.conf | 0 docker-compose.client.yml | 38 --- docker-compose.release.yml | 68 ---- docker-compose.yml | 67 ---- internal/client/cli/config.go | 46 ++- internal/client/cli/http.go | 1 - internal/client/cli/server.go | 229 +++++++++++--- internal/client/cli/server_config.go | 6 +- internal/client/cli/start.go | 250 +++++++++++++++ internal/server/tcp/listener.go | 124 ++++---- pkg/config/client_config.go | 75 ++++- pkg/config/config.go | 103 ++++++- scripts/install-server.sh | 35 +-- 27 files changed, 981 insertions(+), 1308 deletions(-) delete mode 100644 .env.example create mode 100644 deployments/Caddyfile delete mode 100644 deployments/Dockerfile delete mode 100644 deployments/Dockerfile.client delete mode 100644 deployments/Dockerfile.release create mode 100644 deployments/Dockerfile.server delete mode 100644 deployments/README.md create mode 100644 deployments/config.caddy.example.yaml create mode 100644 deployments/config.example.yaml create mode 100644 deployments/docker-compose.caddy.yml create mode 100644 deployments/docker-compose.yml rename nginx.example.conf => deployments/nginx.example.conf (100%) delete mode 100644 docker-compose.client.yml delete mode 100644 docker-compose.release.yml delete mode 100644 docker-compose.yml create mode 100644 internal/client/cli/start.go diff --git a/.env.example b/.env.example deleted file mode 100644 index 4040776..0000000 --- a/.env.example +++ /dev/null @@ -1,49 +0,0 @@ -# =========================================== -# Server Configuration (docker-compose.yml) -# =========================================== - -# Domain for tunnel service -DOMAIN=tunnel.example.com - -# Authentication token -AUTH_TOKEN=your-secret-token-here - -# Server port -PORT=8080 - -# Timezone -TZ=UTC - -# TLS Configuration (choose one) -# Option 1: Auto TLS with Let's Encrypt -# AUTO_TLS=1 - -# Option 2: Manual certificates (place in ./certs/) -# TLS_CERT=1 -# TLS_KEY=1 - -# Build version -VERSION=latest -# GIT_COMMIT= - -# =========================================== -# Client Configuration (docker-compose.client.yml) -# =========================================== - -# Server address -SERVER_ADDR=tunnel.example.com:443 - -# Tunnel type: http, https, or tcp -TUNNEL_TYPE=http - -# Local port to expose -LOCAL_PORT=3000 - -# Local address (default: 127.0.0.1) -# LOCAL_ADDRESS=192.168.1.100 - -# Custom subdomain (optional) -# SUBDOMAIN=myapp - -# Run as daemon -# DAEMON=1 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4bae848..d918952 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -4,63 +4,45 @@ on: push: tags: [ "v*.*.*" ] -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - VERSION: ${{ github.ref_name }} - jobs: build: runs-on: ubuntu-latest - permissions: - contents: read - packages: write - id-token: write steps: - name: Checkout repository uses: actions/checkout@v5 - - name: Install cosign - if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@v4.0.0 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log into registry - if: github.event_name != 'pull_request' + - name: Log into Docker Hub uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ secrets.DOCKERHUB_USERNAME }}/drip-server + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest - name: Build and push Docker image - id: build-and-push uses: docker/build-push-action@v5 with: context: . - file: deployments/Dockerfile.release - push: ${{ github.event_name != 'pull_request' }} + file: deployments/Dockerfile.server + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64,linux/arm64 build-args: | - VERSION=${{ env.VERSION }} + VERSION=${{ github.ref_name }} provenance: false - - - name: Sign the published Docker image - if: github.event_name != 'pull_request' - env: - TAGS: ${{ steps.meta.outputs.tags }} - DIGEST: ${{ steps.build-and-push.outputs.digest }} - run: echo "${TAGS}" | tr ',' '\n' | xargs -I {} cosign sign --yes {}@${DIGEST} diff --git a/README.md b/README.md index 0290730..a769fa5 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ A self-hosted tunneling solution to securely expose your services to the internet.

-

- English +

+ Documentation | - 中文文档 + 中文文档

@@ -23,296 +23,46 @@
-> Drip is a quiet, disciplined tunnel. +> Drip is a quiet, disciplined tunnel. > You light a small lamp on your network, and it carries that light outward—through your own infrastructure, on your own terms. +## Why Drip? -## Why? +- **Control your data** - No third-party servers, traffic stays between your client and server +- **No limits** - Unlimited tunnels, bandwidth, and requests +- **Actually free** - Use your own domain, no paid tiers or feature restrictions +- **Open source** - BSD 3-Clause License -**Control your data.** No third-party servers means your traffic stays between your client and your server. +## Quick Start -**No limits.** Run as many tunnels as you need, use as much bandwidth as your server can handle. - -**Actually free.** Use your own domain, no paid tiers or feature restrictions. - -| Feature | Drip | ngrok Free | -|---------|------|------------| -| Privacy | Your infrastructure | Third-party servers | -| Domain | Your domain | 1 static subdomain | -| Bandwidth | Unlimited | 1 GB/month | -| Active Endpoints | Unlimited | 1 endpoint | -| Tunnels per Agent | Unlimited | Up to 3 | -| Requests | Unlimited | 20,000/month | -| Interstitial Page | None | Yes (removable with header) | -| Open Source | ✓ | ✗ | - -## Quick Install +### Install ```bash bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install.sh) ``` -- Pick a language, then choose to install the **client** (macOS/Linux) or **server** (Linux). -- Non-interactive examples: - - Client: `bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install.sh) --client` - - Server: `bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install.sh) --server` - -### Uninstall -```bash -bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/uninstall.sh) -``` - -## Usage - -### First Time Setup +### Basic Usage ```bash -# Configure server and token (only needed once) +# Configure (first time only) drip config init -``` -### Basic Tunnels - -```bash # Expose local HTTP server drip http 3000 -# Expose local HTTPS server -drip https 443 - -# Pick your subdomain +# With custom subdomain drip http 3000 -n myapp # → https://myapp.your-domain.com - -# Expose TCP service (database, SSH, etc.) -drip tcp 5432 ``` -### Forward to Any Address +## Documentation -Not just localhost - forward to any device on your network: +For complete documentation, visit **[driptunnel.app/en/docs](https://driptunnel.app/en/docs)** -```bash -# Forward to another machine on LAN -drip http 8080 -a 192.168.1.100 - -# Forward to Docker container -drip http 3000 -a 172.17.0.2 - -# Forward to specific interface -drip http 3000 -a 10.0.0.5 -``` - -### Background Mode - -Run tunnels in the background with `-d`: - -```bash -# Start tunnel in background -drip http 3000 -d -drip https 8443 -n api -d - -# List running tunnels -drip list - -# View tunnel logs -drip attach http 3000 - -# Stop tunnels -drip stop http 3000 -drip stop all -``` - -## Server Deployment - -### Prerequisites - -- A domain with DNS pointing to your server (A record) -- Wildcard DNS for subdomains: `*.tunnel.example.com -> YOUR_IP` -- SSL certificate (wildcard recommended) - -### Option 1: Direct (Recommended) - -Drip server handles TLS directly on port 443: - -```bash -# Get wildcard certificate -sudo certbot certonly --manual --preferred-challenges dns \ - -d "*.tunnel.example.com" -d "tunnel.example.com" - -# Start server -drip-server \ - --port 443 \ - --domain tunnel.example.com \ - --tls-cert /etc/letsencrypt/live/tunnel.example.com/fullchain.pem \ - --tls-key /etc/letsencrypt/live/tunnel.example.com/privkey.pem \ - --token YOUR_SECRET_TOKEN -``` - -### Option 2: Behind Nginx - -Run Drip on port 8443, let Nginx handle SSL termination: - -```nginx -server { - listen 443 ssl http2; - server_name *.tunnel.example.com; - - ssl_certificate /etc/letsencrypt/live/tunnel.example.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/tunnel.example.com/privkey.pem; - - location / { - proxy_pass https://127.0.0.1:8443; - proxy_ssl_protocols TLSv1.3; - proxy_ssl_verify off; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_buffering off; - } -} -``` - -### Systemd Service - -The install script creates `/etc/systemd/system/drip-server.service` automatically. Manage with: - -```bash -sudo systemctl start drip-server -sudo systemctl enable drip-server -sudo journalctl -u drip-server -f -``` - -## Features - -**Security** -- TLS 1.3 encryption for all connections -- Token-based authentication -- IP whitelist/blacklist access control -- No legacy protocol support - -**Flexibility** -- HTTP, HTTPS, and TCP tunnels -- Forward to localhost or any LAN address -- Custom subdomains or auto-generated -- Daemon mode for persistent tunnels -- Multiple transport protocols (TCP, WebSocket) - -**Performance** -- Binary protocol with msgpack encoding -- Connection pooling and reuse -- Minimal overhead between client and server - -**Simplicity** -- One-line installation -- Save config once, use everywhere -- Real-time connection stats - -## Architecture - -``` -┌─────────────┐ ┌──────────────┐ ┌─────────────┐ -│ Internet │ ──────> │ Server │ <────── │ Client │ -│ User │ HTTPS │ (Drip) │ TLS 1.3 │ localhost │ -└─────────────┘ └──────────────┘ └─────────────┘ -``` - -## Common Use Cases - -**Development & Testing** -```bash -# Show local dev site to client -drip http 3000 - -# Test webhooks from services like Stripe -drip http 8000 -n webhooks -``` - -**Home Server Access** -```bash -# Access home NAS remotely -drip http 5000 -a 192.168.1.50 - -# Remote into home network via SSH -drip tcp 22 -``` - -**Docker & Containers** -```bash -# Expose containerized app -drip http 8080 -a 172.17.0.3 - -# Database access for debugging -drip tcp 5432 -a db-container -``` - -**IP Access Control** -```bash -# Only allow access from specific networks (CIDR) -drip http 3000 --allow-ip 192.168.0.0/16,10.0.0.0/8 - -# Only allow specific IP addresses -drip http 3000 --allow-ip 192.168.1.100,192.168.1.101 - -# Block specific IP addresses -drip http 3000 --deny-ip 1.2.3.4,5.6.7.8 - -# Combine whitelist and blacklist -drip tcp 5432 --allow-ip 192.168.1.0/24 --deny-ip 192.168.1.100 -``` - -**Transport Protocols** -```bash -# Auto-select transport based on server (default) -drip http 3000 --transport auto - -# Use direct TLS 1.3 connection -drip http 3000 --transport tcp - -# Use WebSocket over TLS (CDN-friendly, works through Cloudflare) -drip http 3000 --transport wss -``` - -## Command Reference - -```bash -# HTTP tunnel -drip http [flags] - -n, --subdomain Custom subdomain - -a, --address Target address (default: 127.0.0.1) - -d, --daemon Run in background - -s, --server Server address - -t, --token Auth token - --allow-ip Allow only these IPs or CIDR ranges - --deny-ip Deny these IPs or CIDR ranges - --transport Transport protocol: auto, tcp, wss (default: auto) - -# HTTPS tunnel (same flags as http) -drip https [flags] - -# TCP tunnel (same flags as http) -drip tcp [flags] - -# Background tunnel management -drip list List running tunnels -drip list -i Interactive mode -drip attach [type] [port] View logs -drip stop Stop tunnel -drip stop all Stop all tunnels - -# Configuration -drip config init Set up server and token -drip config show Show current config -drip config set -``` - -## Acknowledgements - -- [yamux](https://github.com/hashicorp/yamux) - Stream multiplexing library powering Drip's connection multiplexing +- [Installation Guide](https://driptunnel.app/en/docs/installation) +- [Client Usage](https://driptunnel.app/en/docs/client) +- [Server Deployment](https://driptunnel.app/en/docs/server) +- [Configuration Reference](https://driptunnel.app/en/docs/configuration) ## License diff --git a/README_CN.md b/README_CN.md index 29d9017..56e3b7a 100644 --- a/README_CN.md +++ b/README_CN.md @@ -9,10 +9,10 @@ 自建隧道方案,让你的服务安全地暴露到公网。

-

- English +

+ English | - 中文文档 + 中文文档

@@ -23,296 +23,46 @@
-> Drip 是一条安静、自律的隧道。 +> Drip 是一条安静、自律的隧道。 > 你在自己的网络里点亮一盏小灯,它便把光带出去——经过你自己的基础设施,按你自己的方式。 +## 为什么选择 Drip? -## 为什么? +- **掌控数据** - 没有第三方服务器,流量只在你的客户端与服务器之间传输 +- **没有限制** - 无限隧道、带宽和请求数 +- **真的免费** - 用你自己的域名,没有付费档位或功能阉割 +- **开源** - BSD 3-Clause 协议 -**掌控数据。** 没有第三方服务器,流量只在你的客户端与服务器之间传输。 +## 快速开始 -**没有限制。** 想开多少隧道就开多少,带宽只受你的服务器性能限制。 - -**真的免费。** 用你自己的域名,没有付费档位或功能阉割。 - -| 特性 | Drip | ngrok 免费 | -|------|------|-----------| -| 隐私 | 自己的基础设施 | 第三方服务器 | -| 域名 | 你的域名 | 1 个固定子域名 | -| 带宽 | 无限制 | 1 GB/月 | -| 活跃端点 | 无限制 | 1 个端点 | -| 每个 Agent 的隧道数 | 无限制 | 最多 3 条 | -| 请求数 | 无限制 | 20,000 次/月 | -| 中间页 | 无 | 有(加请求头可移除) | -| 开源 | ✓ | ✗ | - -## 快速安装 +### 安装 ```bash bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install.sh) ``` -- 先选择语言,再选择安装 **客户端**(macOS/Linux)或 **服务器**(Linux)。 -- 非交互示例: - - 客户端:`bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install.sh) --client` - - 服务器:`bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install.sh) --server` - -### 卸载 -```bash -bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/uninstall.sh) -``` - -## 使用 - -### 首次配置 +### 基本使用 ```bash -# 配置服务器地址和 token(只需一次) +# 配置(仅首次需要) drip config init -``` -### 基础隧道 - -```bash # 暴露本地 HTTP 服务 drip http 3000 -# 暴露本地 HTTPS 服务 -drip https 443 - -# 选择你的子域名 +# 使用自定义子域名 drip http 3000 -n myapp # → https://myapp.your-domain.com - -# 暴露 TCP 服务(数据库、SSH 等) -drip tcp 5432 ``` -### 转发到任意地址 +## 文档 -不只是 localhost,可以转发到网络里的任何设备: +完整文档请访问 **[driptunnel.app/docs](https://driptunnel.app/docs)** -```bash -# 转发到局域网其他机器 -drip http 8080 -a 192.168.1.100 - -# 转发到 Docker 容器 -drip http 3000 -a 172.17.0.2 - -# 转发到特定网卡 -drip http 3000 -a 10.0.0.5 -``` - -### 后台模式 - -使用 `-d` 让隧道在后台运行: - -```bash -# 后台启动隧道 -drip http 3000 -d -drip https 8443 -n api -d - -# 列出运行中的隧道 -drip list - -# 查看隧道日志 -drip attach http 3000 - -# 停止隧道 -drip stop http 3000 -drip stop all -``` - -## 服务端部署 - -### 前置条件 - -- 域名 A 记录已指向服务器 -- 子域名的泛解析:`*.tunnel.example.com -> 你的 IP` -- SSL 证书(推荐通配符) - -### 方案一:直接部署(推荐) - -Drip 服务端直接在 443 端口处理 TLS: - -```bash -# 获取通配符证书 -sudo certbot certonly --manual --preferred-challenges dns \ - -d "*.tunnel.example.com" -d "tunnel.example.com" - -# 启动服务 -drip-server \ - --port 443 \ - --domain tunnel.example.com \ - --tls-cert /etc/letsencrypt/live/tunnel.example.com/fullchain.pem \ - --tls-key /etc/letsencrypt/live/tunnel.example.com/privkey.pem \ - --token 你的密钥 -``` - -### 方案二:Nginx 反向代理 - -Drip 监听 8443 端口,由 Nginx 负责 SSL 终止: - -```nginx -server { - listen 443 ssl http2; - server_name *.tunnel.example.com; - - ssl_certificate /etc/letsencrypt/live/tunnel.example.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/tunnel.example.com/privkey.pem; - - location / { - proxy_pass https://127.0.0.1:8443; - proxy_ssl_protocols TLSv1.3; - proxy_ssl_verify off; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_buffering off; - } -} -``` - -### Systemd 服务 - -安装脚本会自动创建 `/etc/systemd/system/drip-server.service`。管理方式: - -```bash -sudo systemctl start drip-server -sudo systemctl enable drip-server -sudo journalctl -u drip-server -f -``` - -## 特性 - -**安全性** -- 所有连接使用 TLS 1.3 加密 -- 基于 Token 的身份验证 -- IP 白名单/黑名单访问控制 -- 不支持任何遗留协议 - -**灵活性** -- 支持 HTTP、HTTPS 和 TCP 隧道 -- 可以转发到 localhost 或任何局域网地址 -- 自定义子域名或自动生成 -- 守护模式保持隧道持久运行 -- 多种传输协议(TCP、WebSocket) - -**性能** -- 二进制协议 + msgpack 编码 -- 连接池复用 -- 客户端与服务器之间的额外开销极小 - -**简单** -- 一行命令完成安装 -- 配置一次,到处可用 -- 实时查看连接统计 - -## 架构 - -``` -┌─────────────┐ ┌──────────────┐ ┌─────────────┐ -│ 互联网用户 │ ──────> │ 服务器 │ <────── │ 客户端 │ -│ │ HTTPS │ (Drip) │ TLS 1.3 │ localhost │ -└─────────────┘ └──────────────┘ └─────────────┘ -``` - -## 常见场景 - -**开发与测试** -```bash -# 把本地开发站点给客户预览 -drip http 3000 - -# 测试第三方 webhook(如 Stripe) -drip http 8000 -n webhooks -``` - -**家庭服务器访问** -```bash -# 远程访问家里的 NAS -drip http 5000 -a 192.168.1.50 - -# 通过 SSH 远程进入家庭网络 -drip tcp 22 -``` - -**Docker 与容器** -```bash -# 暴露容器化应用 -drip http 8080 -a 172.17.0.3 - -# 数据库调试 -drip tcp 5432 -a db-container -``` - -**IP 访问控制** -```bash -# 只允许特定网段访问(CIDR) -drip http 3000 --allow-ip 192.168.0.0/16,10.0.0.0/8 - -# 只允许特定 IP 访问 -drip http 3000 --allow-ip 192.168.1.100,192.168.1.101 - -# 拒绝特定 IP -drip http 3000 --deny-ip 1.2.3.4,5.6.7.8 - -# 组合白名单和黑名单 -drip tcp 5432 --allow-ip 192.168.1.0/24 --deny-ip 192.168.1.100 -``` - -**传输协议** -```bash -# 根据服务器自动选择传输协议(默认) -drip http 3000 --transport auto - -# 使用直接 TLS 1.3 连接 -drip http 3000 --transport tcp - -# 使用 WebSocket over TLS(CDN 友好,可穿透 Cloudflare) -drip http 3000 --transport wss -``` - -## 命令参考 - -```bash -# HTTP 隧道 -drip http <端口> [参数] - -n, --subdomain 自定义子域名 - -a, --address 目标地址(默认:127.0.0.1) - -d, --daemon 后台运行 - -s, --server 服务器地址 - -t, --token 认证 token - --allow-ip 只允许这些 IP 或 CIDR 访问 - --deny-ip 拒绝这些 IP 或 CIDR 访问 - --transport 传输协议:auto, tcp, wss(默认:auto) - -# HTTPS 隧道(参数同 http) -drip https <端口> [参数] - -# TCP 隧道(参数同 http) -drip tcp <端口> [参数] - -# 后台隧道管理 -drip list 列出运行中的隧道 -drip list -i 交互模式 -drip attach [类型] [端口] 查看日志 -drip stop <类型> <端口> 停止隧道 -drip stop all 停止所有隧道 - -# 配置 -drip config init 设置服务器和 token -drip config show 显示当前配置 -drip config set <键> <值> -``` - -## 鸣谢 - -- [yamux](https://github.com/hashicorp/yamux) - 为 Drip 的连接复用提供支持的流复用库 +- [安装指南](https://driptunnel.app/docs/installation) +- [客户端使用](https://driptunnel.app/docs/client) +- [服务端部署](https://driptunnel.app/docs/server) +- [配置参考](https://driptunnel.app/docs/configuration) ## 协议 diff --git a/deployments/Caddyfile b/deployments/Caddyfile new file mode 100644 index 0000000..f6b50ba --- /dev/null +++ b/deployments/Caddyfile @@ -0,0 +1,34 @@ +# Caddyfile for drip-server reverse proxy +# +# This configuration: +# - Obtains wildcard certificate via DNS challenge (Cloudflare) +# - Reverse proxies HTTPS/WSS traffic to drip-server +# - Handles all subdomains for tunnel routing +# - Supports WebSocket connections for WSS transport + +# Global options +{ + email {$ACME_EMAIL} +} + +# Main domain and all subdomains +{$DOMAIN}, *.{$DOMAIN} { + # Use DNS challenge for wildcard certificate + # Force TLS 1.3 only + tls { + dns cloudflare {$CF_API_TOKEN} + protocols tls1.3 tls1.3 + } + + # Reverse proxy to drip-server (plain TCP mode) + reverse_proxy drip-server:8443 { + # Pass original host header + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} + + # Flush immediately for streaming/WebSocket + flush_interval -1 + } +} diff --git a/deployments/Dockerfile b/deployments/Dockerfile deleted file mode 100644 index 23bf4e9..0000000 --- a/deployments/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -FROM golang:1.23-alpine AS builder - -RUN apk add --no-cache git ca-certificates tzdata - -WORKDIR /app - -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . - -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ - -ldflags="-s -w -X main.Version=${VERSION:-dev} -X main.GitCommit=${GIT_COMMIT:-unknown} -X main.BuildTime=$(date -u '+%Y-%m-%d_%H:%M:%S')" \ - -o drip \ - ./cmd/drip - -FROM alpine:latest - -RUN apk add --no-cache ca-certificates tzdata - -RUN addgroup -S drip && adduser -S -G drip drip - -WORKDIR /app - -RUN mkdir -p /app/data/certs && \ - chown -R drip:drip /app - -COPY --from=builder /app/drip /app/drip - -USER drip - -EXPOSE 80 443 8080 20000-40000 - -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 - -ENTRYPOINT ["/app/drip"] -CMD ["server", "--port", "8080"] diff --git a/deployments/Dockerfile.client b/deployments/Dockerfile.client deleted file mode 100644 index 0d09c9b..0000000 --- a/deployments/Dockerfile.client +++ /dev/null @@ -1,33 +0,0 @@ -FROM golang:1.25-alpine AS builder - -RUN apk add --no-cache git ca-certificates - -WORKDIR /app - -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . - -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ - -ldflags="-s -w -X main.Version=${VERSION:-dev} -X main.GitCommit=${GIT_COMMIT:-unknown} -X main.BuildTime=$(date -u '+%Y-%m-%d_%H:%M:%S')" \ - -o drip \ - ./cmd/drip - -FROM alpine:latest - -RUN apk add --no-cache ca-certificates - -RUN addgroup -S drip && adduser -S -G drip drip - -WORKDIR /app - -RUN mkdir -p /app/data && \ - chown -R drip:drip /app - -COPY --from=builder /app/drip /app/drip - -USER drip - -ENTRYPOINT ["/app/drip"] -CMD ["--help"] diff --git a/deployments/Dockerfile.release b/deployments/Dockerfile.release deleted file mode 100644 index 8524b39..0000000 --- a/deployments/Dockerfile.release +++ /dev/null @@ -1,58 +0,0 @@ -# ========================= -# Builder stage (Alpine) -# ========================= -FROM golang:1.25-alpine AS builder - -RUN apk add --no-cache ca-certificates tzdata - -# All project files under /app -WORKDIR /app -ADD . . - -# Git tag/version passed from build args, e.g. v1.0.0 -ARG VERSION=dev - -# Buildx injects these automatically for multi-arch builds -ARG TARGETOS -ARG TARGETARCH - -# Adjust this to your real main package directory -# e.g. /app/cmd/drip-server if different -WORKDIR /app/cmd/drip - -# main.version should match the version variable in your Go code -RUN env CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ - go build -v -trimpath \ - -ldflags "-s -w -X main.version=${VERSION}" \ - -o /app/bin/drip - -# ========================= -# Runtime stage (Alpine) -# ========================= -FROM alpine:3.22 - -RUN apk add --no-cache ca-certificates tzdata curl && \ - update-ca-certificates - -# Everything lives under /app in runtime image -WORKDIR /app - -# Optional but nice to have: certs + timezone data -COPY --from=builder /etc/ssl/certs /etc/ssl/certs -COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo - -# Copy the built binary into /app -COPY --from=builder /app/bin/drip /app/drip - -# Non-root user -RUN addgroup -S drip && adduser -S -G drip drip && \ - chown -R drip:drip /app -USER drip - -EXPOSE 80 443 8080 20000-20100 - -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -fsS "http://localhost:${PORT:-8080}/health" >/dev/null || exit 1 - -ENTRYPOINT ["/app/drip"] -CMD ["server", "--port", "8080"] diff --git a/deployments/Dockerfile.server b/deployments/Dockerfile.server new file mode 100644 index 0000000..b45ddcc --- /dev/null +++ b/deployments/Dockerfile.server @@ -0,0 +1,48 @@ +# ========================= +# Builder stage +# ========================= +FROM golang:1.25-alpine AS builder + +RUN apk add --no-cache ca-certificates tzdata + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +# Version passed from build args +ARG VERSION=dev + +# Buildx injects these automatically for multi-arch builds +ARG TARGETOS +ARG TARGETARCH + +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ + go build -trimpath \ + -ldflags "-s -w -X main.version=${VERSION}" \ + -o /app/bin/drip \ + ./cmd/drip + +# ========================= +# Runtime stage +# ========================= +FROM alpine:latest + +RUN apk add --no-cache ca-certificates tzdata && \ + update-ca-certificates + +WORKDIR /app + +COPY --from=builder /etc/ssl/certs /etc/ssl/certs +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=builder /app/bin/drip /app/drip + +RUN addgroup -S drip && adduser -S -G drip drip && \ + mkdir -p /app/data/certs && \ + chown -R drip:drip /app + +USER drip + +ENTRYPOINT ["/app/drip"] +CMD ["server", "-c", "/app/config.yaml"] diff --git a/deployments/README.md b/deployments/README.md deleted file mode 100644 index d823f12..0000000 --- a/deployments/README.md +++ /dev/null @@ -1,249 +0,0 @@ -# Docker Deployment - -## Quick Start (Recommended) - -Deploy drip-server using pre-built images from GitHub Container Registry: - -```bash -# Pull the latest image -docker pull ghcr.io/gouryella/drip:latest - -# Or use docker compose -curl -fsSL https://raw.githubusercontent.com/Gouryella/drip/main/docker-compose.release.yml -o docker-compose.yml - -# Create .env file -cat > .env << EOF -DOMAIN=tunnel.example.com -AUTH_TOKEN=your-secret-token -VERSION=latest -EOF - -# Place your TLS certificates -mkdir -p certs -cp /path/to/fullchain.pem certs/ -cp /path/to/privkey.pem certs/ - -# Start server -docker compose up -d -``` - -## Build from Source - -If you prefer to build locally: - -### Server (Production) - -```bash -# Copy and configure environment -cp .env.example .env -nano .env - -# Edit server configuration -DOMAIN=tunnel.example.com -AUTH_TOKEN=your-secret-token -TLS_CERT=1 -TLS_KEY=1 - -# Place certificates -mkdir -p certs -cp /path/to/fullchain.pem certs/ -cp /path/to/privkey.pem certs/ - -# Uncomment volume mount in docker-compose.yml -# - ./certs:/app/data/certs:ro - -# Start server -docker compose up -d - -# View logs -docker compose logs -f -``` - -### Client (Development/Testing) - -```bash -# Copy and configure client environment -cp .env.example .env.client -nano .env.client - -# Edit client configuration -SERVER_ADDR=tunnel.example.com:443 -AUTH_TOKEN=your-secret-token -TUNNEL_TYPE=http -LOCAL_PORT=3000 - -# Start client -docker compose -f docker-compose.client.yml --env-file .env.client up -d - -# View logs -docker compose -f docker-compose.client.yml logs -f -``` - -## Configuration - -### Environment Variables - -Create `.env` from `.env.example`: - -```bash -DOMAIN=tunnel.example.com -AUTH_TOKEN=your-secret-token -``` - -### TLS Certificates - -**Option 1: Auto TLS (Let's Encrypt)** - -```bash -# Enable in .env -AUTO_TLS=1 - -# Ensure port 80 is accessible for ACME challenges -``` - -**Option 2: Manual Certificates** - -```bash -# Place certificates in ./certs/ -mkdir -p certs -cp fullchain.pem certs/cert.pem -cp privkey.pem certs/key.pem - -# Uncomment in docker-compose.yml -# - ./certs:/app/data/certs:ro - -# Enable in .env -TLS_CERT=1 -TLS_KEY=1 -``` - -## Data Persistence - -All data is stored in Docker volumes: - -- `drip-data`: Server data and certificates at `/app/data` -- `client-data`: Client configuration at `/app/data` - -### Backup - -```bash -# Backup server data -docker run --rm -v drip-data:/data -v $(pwd):/backup alpine tar czf /backup/drip-backup.tar.gz -C /data . - -# Restore -docker run --rm -v drip-data:/data -v $(pwd):/backup alpine tar xzf /backup/drip-backup.tar.gz -C /data -``` - -## Port Mapping - -| Container Port | Host Port | Purpose | -|---------------|-----------|---------| -| 80 | 80 | HTTP (ACME challenges) | -| 443 | 443 | HTTPS (main service) | -| 8080 | 8080 | HTTP (no TLS) | -| 20000-20100 | 20000-20100 | TCP tunnels | - -## Management - -### Server - -```bash -# Start -docker compose up -d - -# Stop -docker compose down - -# Restart -docker compose restart - -# View logs -docker compose logs -f - -# Shell access -docker compose exec server sh - -# Update -docker compose pull -docker compose up -d -``` - -### Client - -```bash -# Start -docker compose -f docker-compose.client.yml up -d - -# Stop -docker compose -f docker-compose.client.yml down - -# View logs -docker compose -f docker-compose.client.yml logs -f - -# Different tunnel types -TUNNEL_TYPE=http LOCAL_PORT=3000 docker compose -f docker-compose.client.yml up -d -TUNNEL_TYPE=https LOCAL_PORT=8443 docker compose -f docker-compose.client.yml up -d -TUNNEL_TYPE=tcp LOCAL_PORT=5432 docker compose -f docker-compose.client.yml up -d -``` - -## Production Deployment - -### With Reverse Proxy - -If using Nginx/Traefik in front: - -```yaml -services: - server: - ports: - - "127.0.0.1:8080:8080" # Only expose to localhost - command: > - server - --domain tunnel.example.com - --port 8080 - --token ${AUTH_TOKEN} -``` - -### Resource Limits - -Adjust in `docker-compose.yml`: - -```yaml -deploy: - resources: - limits: - cpus: '2' - memory: 512M -``` - -## Troubleshooting - -**Certificate errors** - -```bash -# Check certificate files -docker compose exec server ls -la /app/data/certs - -# Check server logs -docker compose logs server | grep -i tls -``` - -**Connection issues** - -```bash -# Verify port accessibility -curl -I https://tunnel.example.com - -# Check server status -docker compose exec server /app/drip server --help -``` - -**Reset everything** - -```bash -# Stop and remove everything -docker compose down -v - -# Start fresh -docker compose up -d -``` diff --git a/deployments/config.caddy.example.yaml b/deployments/config.caddy.example.yaml new file mode 100644 index 0000000..39cf431 --- /dev/null +++ b/deployments/config.caddy.example.yaml @@ -0,0 +1,40 @@ +# Drip Server Configuration (Caddy reverse proxy mode) +# Use with: docker-compose.caddy.yml +# +# Architecture: +# Client --[HTTPS/WSS]--> Caddy:443 --[HTTP/WS]--> drip-server:8443 +# Client --[TCP tunnel]--> drip-server:20000-20100 (direct, no proxy) + +# Server port - Caddy will proxy to this port (internal only) +port: 8443 + +# Domain for client connections (required) +domain: tunnel.example.com + +# Domain for tunnel URLs (optional, defaults to domain) +# tunnel_domain: example.com + +# Authentication token (optional, but recommended) +token: your-secret-token + +# TLS disabled - Caddy handles TLS termination +tls_enabled: false + +# Public port for URLs - set to 443 since Caddy serves on 443 +public_port: 443 + +# TCP tunnel port range (exposed directly to clients, not through Caddy) +tcp_port_min: 20000 +tcp_port_max: 20100 + +# Optional settings +# metrics_token: secret # Token for /metrics endpoint +# debug: false # Enable debug logging +# pprof_port: 6060 # Enable pprof profiling +# transports: # Allowed transports (default: tcp,wss) +# - tcp +# - wss +# tunnel_types: # Allowed tunnel types (default: http,https,tcp) +# - http +# - https +# - tcp diff --git a/deployments/config.example.yaml b/deployments/config.example.yaml new file mode 100644 index 0000000..0fe7075 --- /dev/null +++ b/deployments/config.example.yaml @@ -0,0 +1,37 @@ +# Drip Server Configuration (Direct TLS mode) +# Use with: docker-compose.yml + +# Server port (required) +port: 443 + +# Domain for client connections (required) +domain: tunnel.example.com + +# Domain for tunnel URLs (optional, defaults to domain) +# tunnel_domain: example.com + +# Authentication token (optional, but recommended) +token: your-secret-token + +# TLS settings +# drip-server handles TLS directly, requires certificate files +tls_enabled: true +tls_cert: /app/certs/fullchain.pem +tls_key: /app/certs/privkey.pem + +# TCP tunnel port range +tcp_port_min: 20000 +tcp_port_max: 20100 + +# Optional settings +# public_port: 443 # Port to display in URLs (for reverse proxy) +# metrics_token: secret # Token for /metrics endpoint +# debug: false # Enable debug logging +# pprof_port: 6060 # Enable pprof profiling +# transports: # Allowed transports (default: tcp,wss) +# - tcp +# - wss +# tunnel_types: # Allowed tunnel types (default: http,https,tcp) +# - http +# - https +# - tcp diff --git a/deployments/docker-compose.caddy.yml b/deployments/docker-compose.caddy.yml new file mode 100644 index 0000000..8b6ec49 --- /dev/null +++ b/deployments/docker-compose.caddy.yml @@ -0,0 +1,28 @@ +services: + caddy: + image: slothcroissant/caddy-cloudflaredns:latest + container_name: drip-caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + environment: + DOMAIN: ${DOMAIN} + ACME_EMAIL: ${ACME_EMAIL:-} + CF_API_TOKEN: ${CF_API_TOKEN} + + drip-server: + image: driptunnel/drip-server:${VERSION:-latest} + container_name: drip-server + restart: unless-stopped + ports: + - "20000-20100:20000-20100" + volumes: + - ./config.yaml:/app/config.yaml:ro + +volumes: + caddy-data: diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml new file mode 100644 index 0000000..b1a78b1 --- /dev/null +++ b/deployments/docker-compose.yml @@ -0,0 +1,11 @@ +services: + drip-server: + image: driptunnel/drip-server:${VERSION:-latest} + container_name: drip-server + restart: unless-stopped + ports: + - "443:443" + - "20000-20100:20000-20100" + volumes: + - ./config.yaml:/app/config.yaml:ro + - ./certs:/app/certs:ro diff --git a/nginx.example.conf b/deployments/nginx.example.conf similarity index 100% rename from nginx.example.conf rename to deployments/nginx.example.conf diff --git a/docker-compose.client.yml b/docker-compose.client.yml deleted file mode 100644 index 7d1f500..0000000 --- a/docker-compose.client.yml +++ /dev/null @@ -1,38 +0,0 @@ -services: - client: - build: - context: . - dockerfile: deployments/Dockerfile.client - args: - VERSION: ${VERSION:-dev} - GIT_COMMIT: ${GIT_COMMIT:-unknown} - image: drip-client:${VERSION:-latest} - container_name: drip-client - restart: unless-stopped - network_mode: host - - volumes: - - drip-client-data:/app/data - # Optional: mount config file - # - ./client-config.yaml:/app/data/config.yaml:ro - - environment: - TZ: ${TZ:-UTC} - - command: > - ${TUNNEL_TYPE:-http} ${LOCAL_PORT:-3000} - --server ${SERVER_ADDR} - ${AUTH_TOKEN:+--token ${AUTH_TOKEN}} - ${SUBDOMAIN:+--subdomain ${SUBDOMAIN}} - ${LOCAL_ADDRESS:+--address ${LOCAL_ADDRESS}} - ${DAEMON:+--daemon} - - logging: - driver: json-file - options: - max-size: 10m - max-file: "3" - -volumes: - drip-client-data: - driver: local diff --git a/docker-compose.release.yml b/docker-compose.release.yml deleted file mode 100644 index 8bf9504..0000000 --- a/docker-compose.release.yml +++ /dev/null @@ -1,68 +0,0 @@ -# Docker Compose for deploying drip-server from GitHub Release -# -# Usage: -# 1. Copy this file to your server -# 2. Create .env file with your settings (see .env.example below) -# 3. Run: docker compose -f docker-compose.release.yml up -d -# -# Environment variables (.env.example): -# DOMAIN=tunnel.example.com -# AUTH_TOKEN=your-secret-token -# VERSION=latest -# TZ=UTC - -services: - drip-server: - image: ghcr.io/gouryella/drip:${VERSION:-latest} - container_name: drip-server - restart: unless-stopped - - ports: - - "443:443" - - "20000-20100:20000-20100" # TCP tunnel ports - - volumes: - - ./certs:/app/data/certs:ro - - ./data:/app/data - - environment: - TZ: ${TZ:-UTC} - - command: > - server - --domain ${DOMAIN:-tunnel.localhost} - --port 443 - --tls-cert /app/data/certs/fullchain.pem - --tls-key /app/data/certs/privkey.pem - --token ${AUTH_TOKEN:-} - --tcp-port-min 20000 - --tcp-port-max 20100 - - networks: - - drip-net - - logging: - driver: json-file - options: - max-size: 10m - max-file: "3" - - deploy: - resources: - limits: - cpus: '2' - memory: 512M - reservations: - cpus: '0.25' - memory: 64M - - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:443/health"] - interval: 30s - timeout: 3s - retries: 3 - start_period: 10s - -networks: - drip-net: - driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 2a47dd3..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,67 +0,0 @@ -services: - server: - build: - context: . - dockerfile: deployments/Dockerfile - args: - VERSION: ${VERSION:-dev} - GIT_COMMIT: ${GIT_COMMIT:-unknown} - image: drip-server:${VERSION:-latest} - container_name: drip-server - restart: unless-stopped - - ports: - - "80:80" - - "443:443" - - "8080:8080" - - "20000-20100:20000-20100" - - volumes: - - drip-data:/app/data - # Mount TLS certificates if not using auto-TLS - # - ./certs:/app/data/certs:ro - - environment: - TZ: ${TZ:-UTC} - - command: > - server - --domain ${DOMAIN:-tunnel.localhost} - --port ${PORT:-8080} - ${TLS_CERT:+--tls-cert /app/data/certs/fullchain.pem} - ${TLS_KEY:+--tls-key /app/data/certs/privkey.pem} - ${AUTO_TLS:+--auto-tls} - ${AUTH_TOKEN:+--token ${AUTH_TOKEN}} - - networks: - - drip-net - - logging: - driver: json-file - options: - max-size: 10m - max-file: "3" - - deploy: - resources: - limits: - cpus: '2' - memory: 512M - reservations: - cpus: '0.5' - memory: 128M - - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:${PORT:-8080}/health"] - interval: 30s - timeout: 3s - retries: 3 - start_period: 5s - -volumes: - drip-data: - driver: local - -networks: - drip-net: - driver: bridge diff --git a/internal/client/cli/config.go b/internal/client/cli/config.go index fb9af69..668ed42 100644 --- a/internal/client/cli/config.go +++ b/internal/client/cli/config.go @@ -15,7 +15,7 @@ import ( var configCmd = &cobra.Command{ Use: "config", Short: "Manage configuration", - Long: "Manage Drip client configuration (server, token, etc.)", + Long: "Manage Drip client configuration (server, token, tunnels)", } var configInitCmd = &cobra.Command{ @@ -135,6 +135,32 @@ func runConfigShow(_ *cobra.Command, _ []string) error { fmt.Println(ui.RenderConfigShow(cfg.Server, displayToken, !configFull, cfg.TLS, config.DefaultClientConfigPath())) + // Show tunnels if configured + if len(cfg.Tunnels) > 0 { + fmt.Println() + fmt.Println(ui.Title("Configured Tunnels")) + for _, t := range cfg.Tunnels { + addr := t.Address + if addr == "" { + addr = "127.0.0.1" + } + fmt.Printf(" %-12s %-6s %s:%d", t.Name, t.Type, addr, t.Port) + if t.Subdomain != "" { + fmt.Printf(" subdomain=%s", t.Subdomain) + } + if t.Transport != "" { + fmt.Printf(" transport=%s", t.Transport) + } + if len(t.AllowIPs) > 0 { + fmt.Printf(" allow=%s", strings.Join(t.AllowIPs, ",")) + } + if len(t.DenyIPs) > 0 { + fmt.Printf(" deny=%s", strings.Join(t.DenyIPs, ",")) + } + fmt.Println() + } + } + return nil } @@ -221,6 +247,24 @@ func runConfigValidate(_ *cobra.Command, _ []string) error { fmt.Println(ui.RenderConfigValidation(serverValid, serverMsg, tokenSet, tokenMsg, tlsEnabled)) + // Validate tunnels + if len(cfg.Tunnels) > 0 { + fmt.Println() + fmt.Println(ui.Title("Tunnel Validation")) + allValid := true + for _, t := range cfg.Tunnels { + if err := t.Validate(); err != nil { + fmt.Printf(" ✗ %s: %v\n", t.Name, err) + allValid = false + } else { + fmt.Printf(" ✓ %s: valid\n", t.Name) + } + } + if !allValid { + return fmt.Errorf("some tunnels have invalid configuration") + } + } + if !serverValid { return fmt.Errorf("invalid configuration: %s", serverMsg) } diff --git a/internal/client/cli/http.go b/internal/client/cli/http.go index 305e7f9..c3c2156 100644 --- a/internal/client/cli/http.go +++ b/internal/client/cli/http.go @@ -98,7 +98,6 @@ func runHTTP(_ *cobra.Command, args []string) error { return runTunnelWithUI(connConfig, daemon) } -// parseTransport converts a string to TransportType func parseTransport(s string) tcp.TransportType { switch strings.ToLower(s) { case "wss": diff --git a/internal/client/cli/server.go b/internal/client/cli/server.go index 73ec9ac..4ccc4cb 100644 --- a/internal/client/cli/server.go +++ b/internal/client/cli/server.go @@ -36,6 +36,7 @@ var ( serverPprofPort int serverTransports string serverTunnelTypes string + serverConfigFile string ) var serverCmd = &cobra.Command{ @@ -48,6 +49,9 @@ var serverCmd = &cobra.Command{ func init() { rootCmd.AddCommand(serverCmd) + // Config file flag + serverCmd.Flags().StringVarP(&serverConfigFile, "config", "c", "", "Path to config file (default: /etc/drip/config.yaml or ~/.drip/server.yaml)") + // Command line flags with environment variable defaults serverCmd.Flags().IntVarP(&serverPort, "port", "p", getEnvInt("DRIP_PORT", 8443), "Server port (env: DRIP_PORT)") serverCmd.Flags().IntVar(&serverPublicPort, "public-port", getEnvInt("DRIP_PUBLIC_PORT", 0), "Public port to display in URLs (env: DRIP_PUBLIC_PORT)") @@ -71,32 +75,180 @@ func init() { serverCmd.Flags().StringVar(&serverTunnelTypes, "tunnel-types", getEnvString("DRIP_TUNNEL_TYPES", "http,https,tcp"), "Allowed tunnel types: http,https,tcp (env: DRIP_TUNNEL_TYPES)") } -func runServer(_ *cobra.Command, _ []string) error { +func runServer(cmd *cobra.Command, _ []string) error { // Apply server-mode GC tuning (high throughput, more memory) tuning.ApplyMode(tuning.ModeServer) - if serverTLSCert == "" { - return fmt.Errorf("TLS certificate path is required (use --tls-cert flag or DRIP_TLS_CERT environment variable)") + // Load config file if specified or if default exists + var cfg *config.ServerConfig + configPath := serverConfigFile + if configPath == "" && config.ServerConfigExists("") { + configPath = config.DefaultServerConfigPath() } - if serverTLSKey == "" { - return fmt.Errorf("TLS private key path is required (use --tls-key flag or DRIP_TLS_KEY environment variable)") + if configPath != "" { + var err error + cfg, err = config.LoadServerConfig(configPath) + if err != nil { + return fmt.Errorf("failed to load config file: %w", err) + } + } + if cfg == nil { + cfg = &config.ServerConfig{} } - if err := utils.InitServerLogger(serverDebug); err != nil { + // Configuration priority: flag > env > config file > default + // Note: flag variables already contain env defaults from init() + // We need to check if flag was explicitly set, or if env var exists + + // Port: flag > env > config > default(8443) + if cmd.Flags().Changed("port") { + cfg.Port = serverPort + } else if os.Getenv("DRIP_PORT") != "" { + cfg.Port = serverPort // serverPort already has env value + } else if cfg.Port == 0 { + cfg.Port = serverPort // use default + } + + // PublicPort: flag > env > config > default(0) + // Note: 0 is a valid value meaning "same as port" + if cmd.Flags().Changed("public-port") { + cfg.PublicPort = serverPublicPort + } else if os.Getenv("DRIP_PUBLIC_PORT") != "" { + cfg.PublicPort = serverPublicPort + } + // else keep config file value (including 0) + + // Domain: flag > env > config > default + if cmd.Flags().Changed("domain") { + cfg.Domain = serverDomain + } else if os.Getenv("DRIP_DOMAIN") != "" { + cfg.Domain = serverDomain + } else if cfg.Domain == "" { + cfg.Domain = serverDomain + } + + // TunnelDomain: flag > env > config > default("") + if cmd.Flags().Changed("tunnel-domain") { + cfg.TunnelDomain = serverTunnelDomain + } else if os.Getenv("DRIP_TUNNEL_DOMAIN") != "" { + cfg.TunnelDomain = serverTunnelDomain + } + // else keep config file value + + // AuthToken: flag > env > config > default("") + if cmd.Flags().Changed("token") { + cfg.AuthToken = serverAuthToken + } else if os.Getenv("DRIP_TOKEN") != "" { + cfg.AuthToken = serverAuthToken + } + // else keep config file value + + // MetricsToken: flag > env > config > default("") + if cmd.Flags().Changed("metrics-token") { + cfg.MetricsToken = serverMetricsToken + } else if os.Getenv("DRIP_METRICS_TOKEN") != "" { + cfg.MetricsToken = serverMetricsToken + } + // else keep config file value + + // Debug: flag > config > default(false) + // Note: debug has no env var + if cmd.Flags().Changed("debug") { + cfg.Debug = serverDebug + } + // else keep config file value + + // TCPPortMin: flag > env > config > default + if cmd.Flags().Changed("tcp-port-min") { + cfg.TCPPortMin = serverTCPPortMin + } else if os.Getenv("DRIP_TCP_PORT_MIN") != "" { + cfg.TCPPortMin = serverTCPPortMin + } else if cfg.TCPPortMin == 0 { + cfg.TCPPortMin = serverTCPPortMin + } + + // TCPPortMax: flag > env > config > default + if cmd.Flags().Changed("tcp-port-max") { + cfg.TCPPortMax = serverTCPPortMax + } else if os.Getenv("DRIP_TCP_PORT_MAX") != "" { + cfg.TCPPortMax = serverTCPPortMax + } else if cfg.TCPPortMax == 0 { + cfg.TCPPortMax = serverTCPPortMax + } + + // TLSCertFile: flag > env > config > default("") + if cmd.Flags().Changed("tls-cert") { + cfg.TLSCertFile = serverTLSCert + } else if os.Getenv("DRIP_TLS_CERT") != "" { + cfg.TLSCertFile = serverTLSCert + } + // else keep config file value + + // TLSKeyFile: flag > env > config > default("") + if cmd.Flags().Changed("tls-key") { + cfg.TLSKeyFile = serverTLSKey + } else if os.Getenv("DRIP_TLS_KEY") != "" { + cfg.TLSKeyFile = serverTLSKey + } + // else keep config file value + + // PprofPort: flag > env > config > default(0) + // Note: 0 is valid meaning "disabled" + if cmd.Flags().Changed("pprof") { + cfg.PprofPort = serverPprofPort + } else if os.Getenv("DRIP_PPROF_PORT") != "" { + cfg.PprofPort = serverPprofPort + } + // else keep config file value + + // AllowedTransports: flag > env > config > default + if cmd.Flags().Changed("transports") { + cfg.AllowedTransports = parseCommaSeparated(serverTransports) + } else if os.Getenv("DRIP_TRANSPORTS") != "" { + cfg.AllowedTransports = parseCommaSeparated(serverTransports) + } else if len(cfg.AllowedTransports) == 0 { + cfg.AllowedTransports = parseCommaSeparated(serverTransports) + } + + // AllowedTunnelTypes: flag > env > config > default + if cmd.Flags().Changed("tunnel-types") { + cfg.AllowedTunnelTypes = parseCommaSeparated(serverTunnelTypes) + } else if os.Getenv("DRIP_TUNNEL_TYPES") != "" { + cfg.AllowedTunnelTypes = parseCommaSeparated(serverTunnelTypes) + } else if len(cfg.AllowedTunnelTypes) == 0 { + cfg.AllowedTunnelTypes = parseCommaSeparated(serverTunnelTypes) + } + + // TLS is always enabled for server + cfg.TLSEnabled = true + + // Validate required fields + if cfg.TLSCertFile == "" { + return fmt.Errorf("TLS certificate path is required (use --tls-cert flag, DRIP_TLS_CERT environment variable, or config file)") + } + if cfg.TLSKeyFile == "" { + return fmt.Errorf("TLS private key path is required (use --tls-key flag, DRIP_TLS_KEY environment variable, or config file)") + } + + if err := utils.InitServerLogger(cfg.Debug); err != nil { return fmt.Errorf("failed to initialize logger: %w", err) } defer utils.Sync() logger := utils.GetLogger() + if configPath != "" { + logger.Info("Loaded configuration from file", zap.String("path", configPath)) + } + logger.Info("Starting Drip Server", zap.String("version", Version), zap.String("commit", GitCommit), ) - if serverPprofPort > 0 { + if cfg.PprofPort > 0 { go func() { - pprofAddr := fmt.Sprintf("localhost:%d", serverPprofPort) + pprofAddr := fmt.Sprintf("localhost:%d", cfg.PprofPort) logger.Info("Starting pprof server", zap.String("address", pprofAddr)) if err := http.ListenAndServe(pprofAddr, nil); err != nil { logger.Error("pprof server failed", zap.Error(err)) @@ -104,63 +256,46 @@ func runServer(_ *cobra.Command, _ []string) error { }() } - displayPort := serverPublicPort - if displayPort == 0 { - displayPort = serverPort + // Set public port for display if not specified + if cfg.PublicPort == 0 { + cfg.PublicPort = cfg.Port } - // Use tunnel domain if set, otherwise fall back to domain - tunnelDomain := serverTunnelDomain - if tunnelDomain == "" { - tunnelDomain = serverDomain + // Use tunnel domain if not set, fall back to domain + if cfg.TunnelDomain == "" { + cfg.TunnelDomain = cfg.Domain } - serverConfig := &config.ServerConfig{ - Port: serverPort, - PublicPort: displayPort, - Domain: serverDomain, - TunnelDomain: tunnelDomain, - TCPPortMin: serverTCPPortMin, - TCPPortMax: serverTCPPortMax, - TLSEnabled: true, - TLSCertFile: serverTLSCert, - TLSKeyFile: serverTLSKey, - AuthToken: serverAuthToken, - Debug: serverDebug, - AllowedTransports: parseCommaSeparated(serverTransports), - AllowedTunnelTypes: parseCommaSeparated(serverTunnelTypes), - } - - if err := serverConfig.Validate(); err != nil { + if err := cfg.Validate(); err != nil { logger.Fatal("Invalid server configuration", zap.Error(err)) } - tlsConfig, err := serverConfig.LoadTLSConfig() + tlsConfig, err := cfg.LoadTLSConfig() if err != nil { logger.Fatal("Failed to load TLS configuration", zap.Error(err)) } logger.Info("TLS 1.3 configuration loaded", - zap.String("cert", serverTLSCert), - zap.String("key", serverTLSKey), + zap.String("cert", cfg.TLSCertFile), + zap.String("key", cfg.TLSKeyFile), ) tunnelManager := tunnel.NewManager(logger) - portAllocator, err := tcp.NewPortAllocator(serverTCPPortMin, serverTCPPortMax) + portAllocator, err := tcp.NewPortAllocator(cfg.TCPPortMin, cfg.TCPPortMax) if err != nil { logger.Fatal("Invalid TCP port range", zap.Error(err)) } - listenAddr := fmt.Sprintf("0.0.0.0:%d", serverPort) + listenAddr := fmt.Sprintf("0.0.0.0:%d", cfg.Port) - httpHandler := proxy.NewHandler(tunnelManager, logger, tunnelDomain, serverAuthToken, serverMetricsToken) - httpHandler.SetAllowedTransports(serverConfig.AllowedTransports) - httpHandler.SetAllowedTunnelTypes(serverConfig.AllowedTunnelTypes) + httpHandler := proxy.NewHandler(tunnelManager, logger, cfg.TunnelDomain, cfg.AuthToken, cfg.MetricsToken) + httpHandler.SetAllowedTransports(cfg.AllowedTransports) + httpHandler.SetAllowedTunnelTypes(cfg.AllowedTunnelTypes) - listener := tcp.NewListener(listenAddr, tlsConfig, serverAuthToken, tunnelManager, logger, portAllocator, serverDomain, tunnelDomain, displayPort, httpHandler) - listener.SetAllowedTransports(serverConfig.AllowedTransports) - listener.SetAllowedTunnelTypes(serverConfig.AllowedTunnelTypes) + listener := tcp.NewListener(listenAddr, tlsConfig, cfg.AuthToken, tunnelManager, logger, portAllocator, cfg.Domain, cfg.TunnelDomain, cfg.PublicPort, httpHandler) + listener.SetAllowedTransports(cfg.AllowedTransports) + listener.SetAllowedTunnelTypes(cfg.AllowedTunnelTypes) if err := listener.Start(); err != nil { logger.Fatal("Failed to start TCP listener", zap.Error(err)) @@ -168,11 +303,11 @@ func runServer(_ *cobra.Command, _ []string) error { logger.Info("Drip Server started", zap.String("address", listenAddr), - zap.String("domain", serverDomain), - zap.String("tunnel_domain", tunnelDomain), + zap.String("domain", cfg.Domain), + zap.String("tunnel_domain", cfg.TunnelDomain), zap.String("protocol", "TCP over TLS 1.3"), - zap.Strings("transports", serverConfig.AllowedTransports), - zap.Strings("tunnel_types", serverConfig.AllowedTunnelTypes), + zap.Strings("transports", cfg.AllowedTransports), + zap.Strings("tunnel_types", cfg.AllowedTunnelTypes), ) quit := make(chan os.Signal, 1) diff --git a/internal/client/cli/server_config.go b/internal/client/cli/server_config.go index 58e23ac..c4adbee 100644 --- a/internal/client/cli/server_config.go +++ b/internal/client/cli/server_config.go @@ -114,10 +114,10 @@ func runServerConfigShow(_ *cobra.Command, _ []string) error { } // Configuration sources - fmt.Println("📋 Configuration Sources:") + fmt.Println("Configuration Sources:") + fmt.Println(" Command-line flags (highest priority)") fmt.Println(" Environment variables (DRIP_*)") - fmt.Println(" Command-line flags") - fmt.Println(" Config file: /etc/drip/server.env") + fmt.Println(" Config file: /etc/drip/config.yaml or ~/.drip/server.yaml") fmt.Println() // Endpoints diff --git a/internal/client/cli/start.go b/internal/client/cli/start.go new file mode 100644 index 0000000..4de5df2 --- /dev/null +++ b/internal/client/cli/start.go @@ -0,0 +1,250 @@ +package cli + +import ( + "fmt" + "os" + "os/signal" + "strings" + "sync" + "syscall" + + "drip/internal/client/tcp" + "drip/internal/shared/protocol" + "drip/internal/shared/ui" + "drip/internal/shared/utils" + "drip/pkg/config" + + "github.com/spf13/cobra" +) + +var ( + startAll bool +) + +var startCmd = &cobra.Command{ + Use: "start [tunnel-names...]", + Short: "Start predefined tunnels from config", + Long: `Start one or more predefined tunnels from your configuration file. + +Examples: + drip start web Start the tunnel named "web" + drip start web api Start multiple tunnels + drip start --all Start all configured tunnels + +Configuration file example (~/.drip/config.yaml): + server: tunnel.example.com:443 + token: your-token + tls: true + tunnels: + - name: web + type: http + port: 3000 + subdomain: myapp + + - name: api + type: http + port: 8080 + subdomain: api + transport: wss + + - name: db + type: tcp + port: 5432 + subdomain: postgres + allow_ips: + - 192.168.0.0/16 + - 10.0.0.0/8`, + RunE: runStart, +} + +func init() { + startCmd.Flags().BoolVar(&startAll, "all", false, "Start all configured tunnels") + rootCmd.AddCommand(startCmd) +} + +func runStart(_ *cobra.Command, args []string) error { + cfg, err := config.LoadClientConfig("") + if err != nil { + return err + } + + if len(cfg.Tunnels) == 0 { + return fmt.Errorf("no tunnels configured in %s", config.DefaultClientConfigPath()) + } + + var tunnelsToStart []*config.TunnelConfig + + if startAll { + tunnelsToStart = cfg.Tunnels + } else if len(args) == 0 { + // No args and no --all flag, show available tunnels + fmt.Println(ui.Title("Available Tunnels")) + fmt.Println() + for _, t := range cfg.Tunnels { + fmt.Printf(" %s\n", formatTunnelInfo(t)) + } + fmt.Println() + fmt.Println("Usage:") + fmt.Println(" drip start Start a specific tunnel") + fmt.Println(" drip start --all Start all tunnels") + return nil + } else { + // Start specific tunnels by name + for _, name := range args { + t := cfg.GetTunnel(name) + if t == nil { + availableNames := cfg.GetTunnelNames() + return fmt.Errorf("tunnel '%s' not found. Available tunnels: %s", name, strings.Join(availableNames, ", ")) + } + tunnelsToStart = append(tunnelsToStart, t) + } + } + + if len(tunnelsToStart) == 0 { + return fmt.Errorf("no tunnels to start") + } + + // Start tunnels + if len(tunnelsToStart) == 1 { + return startSingleTunnel(cfg, tunnelsToStart[0]) + } + + return startMultipleTunnels(cfg, tunnelsToStart) +} + +func formatTunnelInfo(t *config.TunnelConfig) string { + addr := t.Address + if addr == "" { + addr = "127.0.0.1" + } + info := fmt.Sprintf("%-12s %s %s:%d", t.Name, t.Type, addr, t.Port) + if t.Subdomain != "" { + info += fmt.Sprintf(" (subdomain: %s)", t.Subdomain) + } + return info +} + +func startSingleTunnel(cfg *config.ClientConfig, t *config.TunnelConfig) error { + connConfig := buildConnectorConfig(cfg, t) + + fmt.Printf("Starting tunnel '%s' (%s %s:%d)\n", t.Name, t.Type, getAddress(t), t.Port) + + return runTunnelWithUI(connConfig, nil) +} + +func startMultipleTunnels(cfg *config.ClientConfig, tunnels []*config.TunnelConfig) error { + if err := utils.InitLogger(verbose); err != nil { + return fmt.Errorf("failed to initialize logger: %w", err) + } + defer utils.Sync() + + logger := utils.GetLogger() + + fmt.Println(ui.Title("Starting Tunnels")) + fmt.Println() + + var wg sync.WaitGroup + errChan := make(chan error, len(tunnels)) + stopChan := make(chan struct{}) + + // Handle interrupt signal + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigChan + fmt.Println("\nShutting down tunnels...") + close(stopChan) + }() + + for _, t := range tunnels { + wg.Add(1) + go func(tunnel *config.TunnelConfig) { + defer wg.Done() + + connConfig := buildConnectorConfig(cfg, tunnel) + fmt.Printf(" Starting %s (%s %s:%d)...\n", tunnel.Name, tunnel.Type, getAddress(tunnel), tunnel.Port) + + client := tcp.NewTunnelClient(connConfig, logger) + + // Connect + if err := client.Connect(); err != nil { + errChan <- fmt.Errorf("%s: %w", tunnel.Name, err) + return + } + + fmt.Printf(" ✓ %s: %s\n", tunnel.Name, client.GetURL()) + + // Run until stopped + select { + case <-stopChan: + client.Close() + } + }(t) + } + + // Wait for interrupt or error + go func() { + wg.Wait() + close(errChan) + }() + + // Collect errors + var errors []error + for err := range errChan { + errors = append(errors, err) + fmt.Printf(" ✗ %v\n", err) + } + + // Wait for signal if no errors + if len(errors) == 0 { + <-stopChan + } + + wg.Wait() + + if len(errors) > 0 { + return fmt.Errorf("%d tunnel(s) failed to start", len(errors)) + } + + return nil +} + +func buildConnectorConfig(cfg *config.ClientConfig, t *config.TunnelConfig) *tcp.ConnectorConfig { + tunnelType := protocol.TunnelTypeHTTP + switch t.Type { + case "https": + tunnelType = protocol.TunnelTypeHTTPS + case "tcp": + tunnelType = protocol.TunnelTypeTCP + } + + transport := tcp.TransportAuto + switch strings.ToLower(t.Transport) { + case "tcp", "tls": + transport = tcp.TransportTCP + case "wss": + transport = tcp.TransportWebSocket + } + + return &tcp.ConnectorConfig{ + ServerAddr: cfg.Server, + Token: cfg.Token, + TunnelType: tunnelType, + LocalHost: getAddress(t), + LocalPort: t.Port, + Subdomain: t.Subdomain, + Insecure: insecure, + AllowIPs: t.AllowIPs, + DenyIPs: t.DenyIPs, + AuthPass: t.Auth, + Transport: transport, + } +} + +func getAddress(t *config.TunnelConfig) string { + if t.Address != "" { + return t.Address + } + return "127.0.0.1" +} diff --git a/internal/server/tcp/listener.go b/internal/server/tcp/listener.go index 78e71d1..2553163 100644 --- a/internal/server/tcp/listener.go +++ b/internal/server/tcp/listener.go @@ -97,16 +97,26 @@ func NewListener(address string, tlsConfig *tls.Config, authToken string, manage func (l *Listener) Start() error { var err error - l.listener, err = tls.Listen("tcp", l.address, l.tlsConfig) - if err != nil { - return fmt.Errorf("failed to start TLS listener: %w", err) + // Support both TLS and plain TCP modes + if l.tlsConfig != nil { + l.listener, err = tls.Listen("tcp", l.address, l.tlsConfig) + if err != nil { + return fmt.Errorf("failed to start TLS listener: %w", err) + } + l.logger.Info("TCP listener started (TLS mode)", + zap.String("address", l.address), + zap.String("tls_version", "TLS 1.3"), + ) + } else { + l.listener, err = net.Listen("tcp", l.address) + if err != nil { + return fmt.Errorf("failed to start TCP listener: %w", err) + } + l.logger.Info("TCP listener started (plain mode - for reverse proxy)", + zap.String("address", l.address), + ) } - l.logger.Info("TCP listener started", - zap.String("address", l.address), - zap.String("tls_version", "TLS 1.3"), - ) - l.httpListener = newConnQueueListener(l.listener.Addr(), 4096) l.httpServer = &http.Server{ @@ -205,56 +215,66 @@ func (l *Listener) handleConnection(netConn net.Conn) { return } - tlsConn, ok := netConn.(*tls.Conn) - if !ok { - l.logger.Error("Connection is not TLS") - return - } + // Handle TLS connections + if tlsConn, ok := netConn.(*tls.Conn); ok { + if err := tlsConn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil { + l.logger.Warn("Failed to set read deadline", + zap.String("remote_addr", netConn.RemoteAddr().String()), + zap.Error(err), + ) + return + } - if err := tlsConn.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil { - l.logger.Warn("Failed to set read deadline", + if err := tlsConn.Handshake(); err != nil { + l.logger.Warn("TLS handshake failed", + zap.String("remote_addr", netConn.RemoteAddr().String()), + zap.Error(err), + ) + return + } + + if err := tlsConn.SetReadDeadline(time.Time{}); err != nil { + l.logger.Warn("Failed to clear read deadline", + zap.String("remote_addr", netConn.RemoteAddr().String()), + zap.Error(err), + ) + return + } + + if tcpConn, ok := tlsConn.NetConn().(*net.TCPConn); ok { + tcpConn.SetNoDelay(true) + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(30 * time.Second) + tcpConn.SetReadBuffer(256 * 1024) + tcpConn.SetWriteBuffer(256 * 1024) + } + + state := tlsConn.ConnectionState() + l.logger.Info("New TLS connection", zap.String("remote_addr", netConn.RemoteAddr().String()), - zap.Error(err), + zap.Uint16("tls_version", state.Version), + zap.String("cipher_suite", tls.CipherSuiteName(state.CipherSuite)), ) - return - } - if err := tlsConn.Handshake(); err != nil { - l.logger.Warn("TLS handshake failed", + if state.Version != tls.VersionTLS13 { + l.logger.Warn("Connection not using TLS 1.3", + zap.Uint16("version", state.Version), + ) + return + } + } else { + // Handle plain TCP connections (reverse proxy mode) + if tcpConn, ok := netConn.(*net.TCPConn); ok { + tcpConn.SetNoDelay(true) + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(30 * time.Second) + tcpConn.SetReadBuffer(256 * 1024) + tcpConn.SetWriteBuffer(256 * 1024) + } + + l.logger.Info("New plain TCP connection (reverse proxy mode)", zap.String("remote_addr", netConn.RemoteAddr().String()), - zap.Error(err), ) - return - } - - if err := tlsConn.SetReadDeadline(time.Time{}); err != nil { - l.logger.Warn("Failed to clear read deadline", - zap.String("remote_addr", netConn.RemoteAddr().String()), - zap.Error(err), - ) - return - } - - if tcpConn, ok := tlsConn.NetConn().(*net.TCPConn); ok { - tcpConn.SetNoDelay(true) - tcpConn.SetKeepAlive(true) - tcpConn.SetKeepAlivePeriod(30 * time.Second) - tcpConn.SetReadBuffer(256 * 1024) - tcpConn.SetWriteBuffer(256 * 1024) - } - - state := tlsConn.ConnectionState() - l.logger.Info("New connection", - zap.String("remote_addr", netConn.RemoteAddr().String()), - zap.Uint16("tls_version", state.Version), - zap.String("cipher_suite", tls.CipherSuiteName(state.CipherSuite)), - ) - - if state.Version != tls.VersionTLS13 { - l.logger.Warn("Connection not using TLS 1.3", - zap.Uint16("version", state.Version), - ) - return } conn := NewConnection(netConn, l.authToken, l.manager, l.logger, l.portAlloc, l.domain, l.tunnelDomain, l.publicPort, l.httpHandler, l.groupManager, l.httpListener) diff --git a/pkg/config/client_config.go b/pkg/config/client_config.go index a883a4c..7cde8e5 100644 --- a/pkg/config/client_config.go +++ b/pkg/config/client_config.go @@ -10,11 +10,49 @@ import ( "gopkg.in/yaml.v3" ) +// TunnelConfig holds configuration for a predefined tunnel +type TunnelConfig struct { + Name string `yaml:"name"` // Tunnel name (required, unique identifier) + Type string `yaml:"type"` // Tunnel type: http, https, tcp (required) + Port int `yaml:"port"` // Local port to forward (required) + Address string `yaml:"address,omitempty"` // Local address (default: 127.0.0.1) + Subdomain string `yaml:"subdomain,omitempty"` // Custom subdomain + Transport string `yaml:"transport,omitempty"` // Transport: auto, tcp, wss + AllowIPs []string `yaml:"allow_ips,omitempty"` // Allowed IPs/CIDRs + DenyIPs []string `yaml:"deny_ips,omitempty"` // Denied IPs/CIDRs + Auth string `yaml:"auth,omitempty"` // Proxy authentication password (http/https only) +} + +// Validate checks if the tunnel configuration is valid +func (t *TunnelConfig) Validate() error { + if t.Name == "" { + return fmt.Errorf("tunnel name is required") + } + if t.Type == "" { + return fmt.Errorf("tunnel type is required for '%s'", t.Name) + } + t.Type = strings.ToLower(t.Type) + if t.Type != "http" && t.Type != "https" && t.Type != "tcp" { + return fmt.Errorf("invalid tunnel type '%s' for '%s': must be http, https, or tcp", t.Type, t.Name) + } + if t.Port < 1 || t.Port > 65535 { + return fmt.Errorf("invalid port %d for '%s': must be between 1 and 65535", t.Port, t.Name) + } + if t.Transport != "" { + t.Transport = strings.ToLower(t.Transport) + if t.Transport != "auto" && t.Transport != "tcp" && t.Transport != "wss" { + return fmt.Errorf("invalid transport '%s' for '%s': must be auto, tcp, or wss", t.Transport, t.Name) + } + } + return nil +} + // ClientConfig represents the client configuration type ClientConfig struct { - Server string `yaml:"server"` // Server address (e.g., tunnel.example.com:443) - Token string `yaml:"token"` // Authentication token - TLS bool `yaml:"tls"` // Use TLS (always true for production) + Server string `yaml:"server"` // Server address (e.g., tunnel.example.com:443) + Token string `yaml:"token"` // Authentication token + TLS bool `yaml:"tls"` // Use TLS (always true for production) + Tunnels []*TunnelConfig `yaml:"tunnels,omitempty"` // Predefined tunnels } // Validate checks if the client configuration is valid @@ -39,9 +77,40 @@ func (c *ClientConfig) Validate() error { return fmt.Errorf("server port is required") } + // Validate tunnels and check for duplicate names + names := make(map[string]bool) + for _, t := range c.Tunnels { + if err := t.Validate(); err != nil { + return err + } + if names[t.Name] { + return fmt.Errorf("duplicate tunnel name: %s", t.Name) + } + names[t.Name] = true + } + return nil } +// GetTunnel returns a tunnel by name +func (c *ClientConfig) GetTunnel(name string) *TunnelConfig { + for _, t := range c.Tunnels { + if t.Name == name { + return t + } + } + return nil +} + +// GetTunnelNames returns all tunnel names +func (c *ClientConfig) GetTunnelNames() []string { + names := make([]string, len(c.Tunnels)) + for i, t := range c.Tunnels { + names[i] = t.Name + } + return names +} + // DefaultClientConfig returns the default configuration path func DefaultClientConfigPath() string { home, err := os.UserHomeDir() diff --git a/pkg/config/config.go b/pkg/config/config.go index 06af9a6..45a6664 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,36 +4,43 @@ import ( "crypto/tls" "fmt" "os" + "path/filepath" "strings" + + "gopkg.in/yaml.v3" ) // ServerConfig holds the server configuration type ServerConfig struct { - Port int - PublicPort int // Port to display in URLs (for reverse proxy scenarios) - Domain string // Domain for client connections (e.g., connect.example.com) - TunnelDomain string // Domain for tunnel URLs (e.g., example.com for *.example.com) + Port int `yaml:"port"` + PublicPort int `yaml:"public_port"` // Port to display in URLs (for reverse proxy scenarios) + Domain string `yaml:"domain"` // Domain for client connections (e.g., connect.example.com) + TunnelDomain string `yaml:"tunnel_domain"` // Domain for tunnel URLs (e.g., example.com for *.example.com) // TCP tunnel dynamic port allocation - TCPPortMin int - TCPPortMax int + TCPPortMin int `yaml:"tcp_port_min"` + TCPPortMax int `yaml:"tcp_port_max"` // TLS settings - TLSEnabled bool - TLSCertFile string - TLSKeyFile string + TLSEnabled bool `yaml:"tls_enabled"` + TLSCertFile string `yaml:"tls_cert"` + TLSKeyFile string `yaml:"tls_key"` // Security - AuthToken string + AuthToken string `yaml:"token"` + MetricsToken string `yaml:"metrics_token"` // Logging - Debug bool + Debug bool `yaml:"debug"` + + // Performance + PprofPort int `yaml:"pprof_port"` // Allowed transports: "tcp", "wss", or "tcp,wss" (default: "tcp,wss") - AllowedTransports []string + AllowedTransports []string `yaml:"transports"` // Allowed tunnel types: "http", "https", "tcp" (default: all) - AllowedTunnelTypes []string + AllowedTunnelTypes []string `yaml:"tunnel_types"` } // Validate checks if the server configuration is valid @@ -156,3 +163,73 @@ func GetClientTLSConfigInsecure() *tls.Config { }, } } + +// DefaultServerConfigPath returns the default server configuration path +func DefaultServerConfigPath() string { + // Check /etc/drip/config.yaml first (system-wide) + systemPath := "/etc/drip/config.yaml" + if _, err := os.Stat(systemPath); err == nil { + return systemPath + } + + // Fall back to user home directory + home, err := os.UserHomeDir() + if err != nil { + return ".drip/server.yaml" + } + return filepath.Join(home, ".drip", "server.yaml") +} + +// LoadServerConfig loads server configuration from file +func LoadServerConfig(path string) (*ServerConfig, error) { + if path == "" { + path = DefaultServerConfigPath() + } + + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("config file not found at %s", path) + } + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config ServerConfig + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %w", err) + } + + return &config, nil +} + +// SaveServerConfig saves server configuration to file +func SaveServerConfig(config *ServerConfig, path string) error { + if path == "" { + path = DefaultServerConfigPath() + } + + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0700); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + data, err := yaml.Marshal(config) + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := os.WriteFile(path, data, 0600); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + return nil +} + +// ServerConfigExists checks if server config file exists +func ServerConfigExists(path string) bool { + if path == "" { + path = DefaultServerConfigPath() + } + _, err := os.Stat(path) + return err == nil +} diff --git a/scripts/install-server.sh b/scripts/install-server.sh index 332b22a..cb6f34d 100755 --- a/scripts/install-server.sh +++ b/scripts/install-server.sh @@ -929,11 +929,8 @@ LimitNPROC=4096 # Working directory WorkingDirectory=${WORK_DIR} -# Load environment variables from file -EnvironmentFile=${CONFIG_DIR}/server.env - -# Start command (uses environment variables) -ExecStart=${INSTALL_DIR}/drip server +# Start command (uses config file) +ExecStart=${INSTALL_DIR}/drip server --config ${CONFIG_DIR}/config.yaml # Logging StandardOutput=journal @@ -972,30 +969,32 @@ configure_firewall() { save_config() { print_step "$(msg saving_config)" - cat > "$CONFIG_DIR/server.env" << EOF + cat > "$CONFIG_DIR/config.yaml" << EOF # Drip Server Configuration # Generated: $(date) # DO NOT SHARE THIS FILE - Contains sensitive information # Server settings -DRIP_PORT=${PORT} -DRIP_PUBLIC_PORT=${PUBLIC_PORT} -DRIP_DOMAIN=${DOMAIN} -DRIP_TOKEN=${TOKEN} -DRIP_METRICS_TOKEN=${METRICS_TOKEN} +port: ${PORT} +public_port: ${PUBLIC_PORT} +domain: ${DOMAIN} + +# Authentication +token: ${TOKEN} +metrics_token: ${METRICS_TOKEN} # TLS certificate paths -DRIP_TLS_CERT=${CERT_PATH} -DRIP_TLS_KEY=${KEY_PATH} +tls_cert: ${CERT_PATH} +tls_key: ${KEY_PATH} # TCP tunnel port range -DRIP_TCP_PORT_MIN=${TCP_PORT_MIN} -DRIP_TCP_PORT_MAX=${TCP_PORT_MAX} +tcp_port_min: ${TCP_PORT_MIN} +tcp_port_max: ${TCP_PORT_MAX} EOF - chmod 640 "$CONFIG_DIR/server.env" - chown root:"$SERVICE_USER" "$CONFIG_DIR/server.env" - print_success "$(msg config_saved): $CONFIG_DIR/server.env" + chmod 640 "$CONFIG_DIR/config.yaml" + chown root:"$SERVICE_USER" "$CONFIG_DIR/config.yaml" + print_success "$(msg config_saved): $CONFIG_DIR/config.yaml" } # ============================================================================ From 5459d952becdba1061747076c17a6291e14039d6 Mon Sep 17 00:00:00 2001 From: Gouryella Date: Thu, 15 Jan 2026 17:32:08 +0800 Subject: [PATCH 2/2] feat(server): Optimize configuration priority processing logic --- README.md | 2 +- README_CN.md | 2 +- internal/client/cli/server.go | 89 +++++++++++++++-------------- internal/server/proxy/handler.go | 2 +- internal/shared/pool/buffer_pool.go | 1 - 5 files changed, 48 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index a769fa5..cdfa64f 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ ### Install ```bash -bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install.sh) +bash <(curl -sL https://driptunnel.app/install.sh) ``` ### Basic Usage diff --git a/README_CN.md b/README_CN.md index 56e3b7a..8234e9d 100644 --- a/README_CN.md +++ b/README_CN.md @@ -38,7 +38,7 @@ ### 安装 ```bash -bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install.sh) +bash <(curl -sL https://driptunnel.app/install.sh) ``` ### 基本使用 diff --git a/internal/client/cli/server.go b/internal/client/cli/server.go index 4ccc4cb..4a518cb 100644 --- a/internal/client/cli/server.go +++ b/internal/client/cli/server.go @@ -96,29 +96,23 @@ func runServer(cmd *cobra.Command, _ []string) error { cfg = &config.ServerConfig{} } - // Configuration priority: flag > env > config file > default - // Note: flag variables already contain env defaults from init() - // We need to check if flag was explicitly set, or if env var exists - - // Port: flag > env > config > default(8443) + // Port if cmd.Flags().Changed("port") { cfg.Port = serverPort } else if os.Getenv("DRIP_PORT") != "" { - cfg.Port = serverPort // serverPort already has env value + cfg.Port = serverPort } else if cfg.Port == 0 { - cfg.Port = serverPort // use default + cfg.Port = serverPort } - // PublicPort: flag > env > config > default(0) - // Note: 0 is a valid value meaning "same as port" + // PublicPort if cmd.Flags().Changed("public-port") { cfg.PublicPort = serverPublicPort } else if os.Getenv("DRIP_PUBLIC_PORT") != "" { cfg.PublicPort = serverPublicPort } - // else keep config file value (including 0) - // Domain: flag > env > config > default + // Domain if cmd.Flags().Changed("domain") { cfg.Domain = serverDomain } else if os.Getenv("DRIP_DOMAIN") != "" { @@ -127,38 +121,33 @@ func runServer(cmd *cobra.Command, _ []string) error { cfg.Domain = serverDomain } - // TunnelDomain: flag > env > config > default("") + // TunnelDomain if cmd.Flags().Changed("tunnel-domain") { cfg.TunnelDomain = serverTunnelDomain } else if os.Getenv("DRIP_TUNNEL_DOMAIN") != "" { cfg.TunnelDomain = serverTunnelDomain } - // else keep config file value - // AuthToken: flag > env > config > default("") + // AuthToken if cmd.Flags().Changed("token") { cfg.AuthToken = serverAuthToken } else if os.Getenv("DRIP_TOKEN") != "" { cfg.AuthToken = serverAuthToken } - // else keep config file value - // MetricsToken: flag > env > config > default("") + // MetricsToken if cmd.Flags().Changed("metrics-token") { cfg.MetricsToken = serverMetricsToken } else if os.Getenv("DRIP_METRICS_TOKEN") != "" { cfg.MetricsToken = serverMetricsToken } - // else keep config file value - // Debug: flag > config > default(false) - // Note: debug has no env var + // Debug if cmd.Flags().Changed("debug") { cfg.Debug = serverDebug } - // else keep config file value - // TCPPortMin: flag > env > config > default + // TCPPortMin if cmd.Flags().Changed("tcp-port-min") { cfg.TCPPortMin = serverTCPPortMin } else if os.Getenv("DRIP_TCP_PORT_MIN") != "" { @@ -167,7 +156,7 @@ func runServer(cmd *cobra.Command, _ []string) error { cfg.TCPPortMin = serverTCPPortMin } - // TCPPortMax: flag > env > config > default + // TCPPortMax if cmd.Flags().Changed("tcp-port-max") { cfg.TCPPortMax = serverTCPPortMax } else if os.Getenv("DRIP_TCP_PORT_MAX") != "" { @@ -176,32 +165,28 @@ func runServer(cmd *cobra.Command, _ []string) error { cfg.TCPPortMax = serverTCPPortMax } - // TLSCertFile: flag > env > config > default("") + // TLSCertFile if cmd.Flags().Changed("tls-cert") { cfg.TLSCertFile = serverTLSCert } else if os.Getenv("DRIP_TLS_CERT") != "" { cfg.TLSCertFile = serverTLSCert } - // else keep config file value - // TLSKeyFile: flag > env > config > default("") + // TLSKeyFile if cmd.Flags().Changed("tls-key") { cfg.TLSKeyFile = serverTLSKey } else if os.Getenv("DRIP_TLS_KEY") != "" { cfg.TLSKeyFile = serverTLSKey } - // else keep config file value - // PprofPort: flag > env > config > default(0) - // Note: 0 is valid meaning "disabled" + // PprofPort if cmd.Flags().Changed("pprof") { cfg.PprofPort = serverPprofPort } else if os.Getenv("DRIP_PPROF_PORT") != "" { cfg.PprofPort = serverPprofPort } - // else keep config file value - // AllowedTransports: flag > env > config > default + // AllowedTransports if cmd.Flags().Changed("transports") { cfg.AllowedTransports = parseCommaSeparated(serverTransports) } else if os.Getenv("DRIP_TRANSPORTS") != "" { @@ -210,7 +195,7 @@ func runServer(cmd *cobra.Command, _ []string) error { cfg.AllowedTransports = parseCommaSeparated(serverTransports) } - // AllowedTunnelTypes: flag > env > config > default + // AllowedTunnelTypes if cmd.Flags().Changed("tunnel-types") { cfg.AllowedTunnelTypes = parseCommaSeparated(serverTunnelTypes) } else if os.Getenv("DRIP_TUNNEL_TYPES") != "" { @@ -219,15 +204,22 @@ func runServer(cmd *cobra.Command, _ []string) error { cfg.AllowedTunnelTypes = parseCommaSeparated(serverTunnelTypes) } - // TLS is always enabled for server - cfg.TLSEnabled = true - - // Validate required fields - if cfg.TLSCertFile == "" { - return fmt.Errorf("TLS certificate path is required (use --tls-cert flag, DRIP_TLS_CERT environment variable, or config file)") + // TLSEnabled + if os.Getenv("DRIP_TLS_ENABLED") != "" { + cfg.TLSEnabled = os.Getenv("DRIP_TLS_ENABLED") == "true" || os.Getenv("DRIP_TLS_ENABLED") == "1" + } else if cfg.TLSCertFile != "" && cfg.TLSKeyFile != "" { + if !cfg.TLSEnabled { + cfg.TLSEnabled = true + } } - if cfg.TLSKeyFile == "" { - return fmt.Errorf("TLS private key path is required (use --tls-key flag, DRIP_TLS_KEY environment variable, or config file)") + + if cfg.TLSEnabled { + if cfg.TLSCertFile == "" { + return fmt.Errorf("TLS certificate path is required when TLS is enabled (use --tls-cert flag, DRIP_TLS_CERT environment variable, or config file)") + } + if cfg.TLSKeyFile == "" { + return fmt.Errorf("TLS private key path is required when TLS is enabled (use --tls-key flag, DRIP_TLS_KEY environment variable, or config file)") + } } if err := utils.InitServerLogger(cfg.Debug); err != nil { @@ -275,10 +267,14 @@ func runServer(cmd *cobra.Command, _ []string) error { logger.Fatal("Failed to load TLS configuration", zap.Error(err)) } - logger.Info("TLS 1.3 configuration loaded", - zap.String("cert", cfg.TLSCertFile), - zap.String("key", cfg.TLSKeyFile), - ) + if cfg.TLSEnabled { + logger.Info("TLS 1.3 configuration loaded", + zap.String("cert", cfg.TLSCertFile), + zap.String("key", cfg.TLSKeyFile), + ) + } else { + logger.Info("TLS disabled - running in plain TCP mode (for reverse proxy)") + } tunnelManager := tunnel.NewManager(logger) @@ -301,11 +297,16 @@ func runServer(cmd *cobra.Command, _ []string) error { logger.Fatal("Failed to start TCP listener", zap.Error(err)) } + protocol := "TCP (plain)" + if cfg.TLSEnabled { + protocol = "TCP over TLS 1.3" + } + logger.Info("Drip Server started", zap.String("address", listenAddr), zap.String("domain", cfg.Domain), zap.String("tunnel_domain", cfg.TunnelDomain), - zap.String("protocol", "TCP over TLS 1.3"), + zap.String("protocol", protocol), zap.Strings("transports", cfg.AllowedTransports), zap.Strings("tunnel_types", cfg.AllowedTunnelTypes), ) diff --git a/internal/server/proxy/handler.go b/internal/server/proxy/handler.go index 38e8258..acdb0b5 100644 --- a/internal/server/proxy/handler.go +++ b/internal/server/proxy/handler.go @@ -641,7 +641,7 @@ func (h *Handler) serveHomePage(w http.ResponseWriter, r *http.Request) {

Install

-
bash <(curl -fsSL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install.sh)
+
bash <(curl -fsSL https://driptunnel.app/install.sh)