mirror of
https://github.com/Gouryella/drip.git
synced 2026-02-23 12:53:43 +00:00
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
This commit is contained in:
49
.env.example
49
.env.example
@@ -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
|
||||
42
.github/workflows/docker.yml
vendored
42
.github/workflows/docker.yml
vendored
@@ -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}
|
||||
|
||||
290
README.md
290
README.md
@@ -9,10 +9,10 @@
|
||||
A self-hosted tunneling solution to securely expose your services to the internet.
|
||||
</p>
|
||||
|
||||
<p align="center ">
|
||||
<a href="README.md">English</a>
|
||||
<p align="center">
|
||||
<a href="https://driptunnel.app/en/docs">Documentation</a>
|
||||
<span> | </span>
|
||||
<a href="README_CN.md">中文文档</a>
|
||||
<a href="https://driptunnel.app/docs">中文文档</a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
@@ -23,296 +23,46 @@
|
||||
|
||||
</div>
|
||||
|
||||
> 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 <port> [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 <port> [flags]
|
||||
|
||||
# TCP tunnel (same flags as http)
|
||||
drip tcp <port> [flags]
|
||||
|
||||
# Background tunnel management
|
||||
drip list List running tunnels
|
||||
drip list -i Interactive mode
|
||||
drip attach [type] [port] View logs
|
||||
drip stop <type> <port> 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 <key> <value>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
290
README_CN.md
290
README_CN.md
@@ -9,10 +9,10 @@
|
||||
自建隧道方案,让你的服务安全地暴露到公网。
|
||||
</p>
|
||||
|
||||
<p align="center ">
|
||||
<a href="README.md">English</a>
|
||||
<p align="center">
|
||||
<a href="https://driptunnel.app/en/docs">English</a>
|
||||
<span> | </span>
|
||||
<a href="README_CN.md">中文文档</a>
|
||||
<a href="https://driptunnel.app/docs">中文文档</a>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
@@ -23,296 +23,46 @@
|
||||
|
||||
</div>
|
||||
|
||||
> 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)
|
||||
|
||||
## 协议
|
||||
|
||||
|
||||
34
deployments/Caddyfile
Normal file
34
deployments/Caddyfile
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
48
deployments/Dockerfile.server
Normal file
48
deployments/Dockerfile.server
Normal file
@@ -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"]
|
||||
@@ -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
|
||||
```
|
||||
40
deployments/config.caddy.example.yaml
Normal file
40
deployments/config.caddy.example.yaml
Normal file
@@ -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
|
||||
37
deployments/config.example.yaml
Normal file
37
deployments/config.example.yaml
Normal file
@@ -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
|
||||
28
deployments/docker-compose.caddy.yml
Normal file
28
deployments/docker-compose.caddy.yml
Normal file
@@ -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:
|
||||
11
deployments/docker-compose.yml
Normal file
11
deployments/docker-compose.yml
Normal file
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
250
internal/client/cli/start.go
Normal file
250
internal/client/cli/start.go
Normal file
@@ -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 <tunnel-name> 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"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user