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..cdfa64f 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)
+bash <(curl -sL https://driptunnel.app/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..8234e9d 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)
+bash <(curl -sL https://driptunnel.app/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..4a518cb 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,172 @@ 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 {
+ // Port
+ if cmd.Flags().Changed("port") {
+ cfg.Port = serverPort
+ } else if os.Getenv("DRIP_PORT") != "" {
+ cfg.Port = serverPort
+ } else if cfg.Port == 0 {
+ cfg.Port = serverPort
+ }
+
+ // PublicPort
+ if cmd.Flags().Changed("public-port") {
+ cfg.PublicPort = serverPublicPort
+ } else if os.Getenv("DRIP_PUBLIC_PORT") != "" {
+ cfg.PublicPort = serverPublicPort
+ }
+
+ // Domain
+ 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
+ if cmd.Flags().Changed("tunnel-domain") {
+ cfg.TunnelDomain = serverTunnelDomain
+ } else if os.Getenv("DRIP_TUNNEL_DOMAIN") != "" {
+ cfg.TunnelDomain = serverTunnelDomain
+ }
+
+ // AuthToken
+ if cmd.Flags().Changed("token") {
+ cfg.AuthToken = serverAuthToken
+ } else if os.Getenv("DRIP_TOKEN") != "" {
+ cfg.AuthToken = serverAuthToken
+ }
+
+ // MetricsToken
+ if cmd.Flags().Changed("metrics-token") {
+ cfg.MetricsToken = serverMetricsToken
+ } else if os.Getenv("DRIP_METRICS_TOKEN") != "" {
+ cfg.MetricsToken = serverMetricsToken
+ }
+
+ // Debug
+ if cmd.Flags().Changed("debug") {
+ cfg.Debug = serverDebug
+ }
+
+ // TCPPortMin
+ 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
+ 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
+ if cmd.Flags().Changed("tls-cert") {
+ cfg.TLSCertFile = serverTLSCert
+ } else if os.Getenv("DRIP_TLS_CERT") != "" {
+ cfg.TLSCertFile = serverTLSCert
+ }
+
+ // TLSKeyFile
+ if cmd.Flags().Changed("tls-key") {
+ cfg.TLSKeyFile = serverTLSKey
+ } else if os.Getenv("DRIP_TLS_KEY") != "" {
+ cfg.TLSKeyFile = serverTLSKey
+ }
+
+ // PprofPort
+ if cmd.Flags().Changed("pprof") {
+ cfg.PprofPort = serverPprofPort
+ } else if os.Getenv("DRIP_PPROF_PORT") != "" {
+ cfg.PprofPort = serverPprofPort
+ }
+
+ // AllowedTransports
+ 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
+ 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)
+ }
+
+ // 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.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 {
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,75 +248,67 @@ 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),
- )
+ 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)
- 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))
}
+ protocol := "TCP (plain)"
+ if cfg.TLSEnabled {
+ protocol = "TCP over TLS 1.3"
+ }
+
logger.Info("Drip Server started",
zap.String("address", listenAddr),
- zap.String("domain", serverDomain),
- zap.String("tunnel_domain", tunnelDomain),
- zap.String("protocol", "TCP over TLS 1.3"),
- zap.Strings("transports", serverConfig.AllowedTransports),
- zap.Strings("tunnel_types", serverConfig.AllowedTunnelTypes),
+ zap.String("domain", cfg.Domain),
+ zap.String("tunnel_domain", cfg.TunnelDomain),
+ zap.String("protocol", protocol),
+ 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/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)