Merge pull request #17 from Gouryella/feat/config-file-management

feat(client): Support predefined tunnel configuration and management …
This commit is contained in:
Gouryella
2026-01-15 17:32:21 +08:00
committed by GitHub
29 changed files with 988 additions and 1315 deletions

View File

@@ -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

View File

@@ -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}

292
README.md
View File

@@ -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)
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 <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

View File

@@ -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)
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 TLSCDN 友好,可穿透 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
View 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
}
}

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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"]

View 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"]

View File

@@ -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
```

View 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

View 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

View 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:

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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":

View File

@@ -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)

View File

@@ -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

View 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"
}

View File

@@ -641,7 +641,7 @@ func (h *Handler) serveHomePage(w http.ResponseWriter, r *http.Request) {
<h2>Install</h2>
<div class="code-wrap">
<pre>bash &lt;(curl -fsSL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install.sh)</pre>
<pre>bash &lt;(curl -fsSL https://driptunnel.app/install.sh)</pre>
<button class="copy-btn" onclick="copy(this)">
<svg class="copy" viewBox="0 0 16 16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>
<svg class="check" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg>

View File

@@ -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)

View File

@@ -76,7 +76,6 @@ func (p *BufferPool) Put(buf *[]byte) {
case SizeXLarge:
p.xlarge.Put(buf)
}
// Note: buffers with non-standard sizes are not pooled (let GC handle them)
}
var globalBufferPool = NewBufferPool()

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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"
}
# ============================================================================