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)