Merge pull request #13 from Gouryella/feat/prometheus-monitoring

Feat/prometheus monitoring
This commit is contained in:
Gouryella
2026-01-05 14:08:46 +08:00
committed by GitHub
17 changed files with 617 additions and 221 deletions

3
.gitignore vendored
View File

@@ -53,4 +53,5 @@ certs/
.drip-server.env
benchmark-results/
drip-linux-amd64
./drip
./drip
drip

View File

@@ -46,33 +46,6 @@
| Interstitial Page | None | Yes (removable with header) |
| Open Source | ✓ | ✗ |
## What's New in v0.5.0
### 🔄 Switched to Yamux Protocol
Our custom multiplexing protocol had too many edge-case bugs. We replaced it with [yamux](https://github.com/hashicorp/yamux), HashiCorp's battle-tested stream multiplexing library.
**Why Yamux?**
- Production-proven in Consul, Nomad, and other critical infrastructure
- Built-in flow control and keepalive support
- Active maintenance and community support
**What changed:**
- Removed: Custom HPACK compression, flow control, binary framing, HTTP codec
- Added: Yamux-based connection pooling and session management
- Result: ~60% less protocol code, significantly improved stability
### ⚡ Performance Improvements
| Metric | Improvement |
|--------|-------------|
| Connection setup | 3x faster (session reuse) |
| Memory per tunnel | -50% (simplified state) |
| Latency (p99) | -40% (fewer encoding layers) |
| Throughput | +80% (efficient multiplexing) |
> ⚠️ **Breaking Change**: Protocol incompatible with v0.4.x. Upgrade both client and server.
## Quick Install
```bash
@@ -305,6 +278,10 @@ 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
## License
BSD 3-Clause License - see [LICENSE](LICENSE) for details

View File

@@ -46,33 +46,6 @@
| 中间页 | 无 | 有(加请求头可移除) |
| 开源 | ✓ | ✗ |
## v0.5.0 更新内容
### 🔄 切换到 Yamux 协议
自研的多路复用协议存在较多边界情况的 bug难以复现和修复。我们决定暂时放弃自研协议改用 HashiCorp 久经考验的 [yamux](https://github.com/hashicorp/yamux) 库。
**为什么选择 Yamux**
- 生产环境验证,被 Consul、Nomad 等关键基础设施广泛使用
- 内置流控和心跳保活
- 活跃的社区维护
**变更内容:**
- 移除:自研 HPACK 压缩、流控机制、二进制帧协议、HTTP 编解码器
- 新增:基于 Yamux 的连接池和会话管理
- 结果:协议层代码减少约 60%,稳定性显著提升
### ⚡ 性能提升
| 指标 | 提升幅度 |
|------|----------|
| 连接建立 | 快 3 倍(会话复用) |
| 单隧道内存占用 | -50%(状态简化) |
| 延迟 (p99) | -40%(减少编码层) |
| 吞吐量 | +80%(高效多路复用) |
> ⚠️ **破坏性变更**:协议与 v0.4.x 不兼容,客户端和服务端需同时升级。
## 快速安装
```bash
@@ -304,6 +277,10 @@ drip config show 显示当前配置
drip config set <键> <值>
```
## 鸣谢
- [yamux](https://github.com/hashicorp/yamux) - 为 Drip 的连接复用提供支持的流复用库
## 协议
BSD 3-Clause License - 详见 [LICENSE](LICENSE)

35
go.mod
View File

@@ -7,29 +7,40 @@ require (
github.com/goccy/go-json v0.10.5
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/yamux v0.1.2
github.com/spf13/cobra v1.10.1
github.com/prometheus/client_golang v1.23.2
github.com/spf13/cobra v1.10.2
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.45.0
golang.org/x/net v0.47.0
golang.org/x/sys v0.38.0
golang.org/x/crypto v0.46.0
golang.org/x/net v0.48.0
golang.org/x/sys v0.39.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.11.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.6.1 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/text v0.31.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

89
go.sum
View File

@@ -1,42 +1,73 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.6.1 h1:/zMlAezfDzT2xy6acHBzwIfyu2ic0hgkT83UX5EY2gY=
github.com/clipperhouse/displaywidth v0.6.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -50,18 +81,24 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -117,16 +117,17 @@ func runConfigShow(_ *cobra.Command, _ []string) error {
var displayToken string
if cfg.Token != "" {
tokenLen := len(cfg.Token)
if tokenLen <= 3 {
// For very short tokens, just show asterisks
displayToken = "***"
} else if tokenLen > 10 {
// For long tokens, show first 3 and last 3 characters
displayToken = cfg.Token[:3] + "***" + cfg.Token[tokenLen-3:]
if configFull {
displayToken = cfg.Token
} else {
// For medium tokens (4-10 chars), show first 3 characters
displayToken = cfg.Token[:3] + "***"
tokenLen := len(cfg.Token)
if tokenLen <= 3 {
displayToken = "***"
} else if tokenLen > 10 {
displayToken = cfg.Token[:3] + "***" + cfg.Token[tokenLen-3:]
} else {
displayToken = cfg.Token[:3] + "***"
}
}
} else {
displayToken = ""

View File

@@ -21,16 +21,17 @@ import (
)
var (
serverPort int
serverPublicPort int
serverDomain string
serverAuthToken string
serverDebug bool
serverTCPPortMin int
serverTCPPortMax int
serverTLSCert string
serverTLSKey string
serverPprofPort int
serverPort int
serverPublicPort int
serverDomain string
serverAuthToken string
serverMetricsToken string
serverDebug bool
serverTCPPortMin int
serverTCPPortMax int
serverTLSCert string
serverTLSKey string
serverPprofPort int
)
var serverCmd = &cobra.Command{
@@ -48,6 +49,7 @@ func init() {
serverCmd.Flags().IntVar(&serverPublicPort, "public-port", getEnvInt("DRIP_PUBLIC_PORT", 0), "Public port to display in URLs (env: DRIP_PUBLIC_PORT)")
serverCmd.Flags().StringVarP(&serverDomain, "domain", "d", getEnvString("DRIP_DOMAIN", constants.DefaultDomain), "Server domain (env: DRIP_DOMAIN)")
serverCmd.Flags().StringVarP(&serverAuthToken, "token", "t", getEnvString("DRIP_TOKEN", ""), "Authentication token (env: DRIP_TOKEN)")
serverCmd.Flags().StringVar(&serverMetricsToken, "metrics-token", getEnvString("DRIP_METRICS_TOKEN", ""), "Metrics and stats token (env: DRIP_METRICS_TOKEN)")
serverCmd.Flags().BoolVar(&serverDebug, "debug", false, "Enable debug logging")
serverCmd.Flags().IntVar(&serverTCPPortMin, "tcp-port-min", getEnvInt("DRIP_TCP_PORT_MIN", constants.DefaultTCPPortMin), "Minimum TCP tunnel port (env: DRIP_TCP_PORT_MIN)")
serverCmd.Flags().IntVar(&serverTCPPortMax, "tcp-port-max", getEnvInt("DRIP_TCP_PORT_MAX", constants.DefaultTCPPortMax), "Maximum TCP tunnel port (env: DRIP_TCP_PORT_MAX)")
@@ -130,7 +132,7 @@ func runServer(_ *cobra.Command, _ []string) error {
listenAddr := fmt.Sprintf("0.0.0.0:%d", serverPort)
httpHandler := proxy.NewHandler(tunnelManager, logger, serverDomain, serverAuthToken)
httpHandler := proxy.NewHandler(tunnelManager, logger, serverDomain, serverAuthToken, serverMetricsToken)
listener := tcp.NewListener(listenAddr, tlsConfig, serverAuthToken, tunnelManager, logger, portAllocator, serverDomain, displayPort, httpHandler)

View File

@@ -0,0 +1,176 @@
package cli
import (
"fmt"
"os"
"drip/internal/shared/constants"
"github.com/spf13/cobra"
)
var (
serverConfigFull bool
)
var serverConfigCmd = &cobra.Command{
Use: "config",
Short: "Manage server configuration",
Long: "Display and manage Drip server configuration",
}
var serverConfigShowCmd = &cobra.Command{
Use: "show",
Short: "Show server configuration",
Long: "Display the current Drip server configuration from environment variables and flags",
RunE: runServerConfigShow,
}
func init() {
serverConfigCmd.AddCommand(serverConfigShowCmd)
serverConfigShowCmd.Flags().BoolVar(&serverConfigFull, "full", false, "Show full tokens (not masked)")
serverCmd.AddCommand(serverConfigCmd)
}
func runServerConfigShow(_ *cobra.Command, _ []string) error {
// Read configuration from environment variables and defaults
port := getEnvInt("DRIP_PORT", 8443)
publicPort := getEnvInt("DRIP_PUBLIC_PORT", 0)
domain := getEnvString("DRIP_DOMAIN", constants.DefaultDomain)
token := getEnvString("DRIP_TOKEN", "")
metricsToken := getEnvString("DRIP_METRICS_TOKEN", "")
tlsCert := getEnvString("DRIP_TLS_CERT", "")
tlsKey := getEnvString("DRIP_TLS_KEY", "")
tcpPortMin := getEnvInt("DRIP_TCP_PORT_MIN", constants.DefaultTCPPortMin)
tcpPortMax := getEnvInt("DRIP_TCP_PORT_MAX", constants.DefaultTCPPortMax)
pprofPort := getEnvInt("DRIP_PPROF_PORT", 0)
if publicPort == 0 {
publicPort = port
}
// Mask tokens if not showing full
displayToken := maskToken(token, serverConfigFull)
displayMetricsToken := maskToken(metricsToken, serverConfigFull)
// Print configuration
fmt.Println()
fmt.Println("╔══════════════════════════════════════════════════════════════╗")
fmt.Println("║ Drip Server Configuration ║")
fmt.Println("╚══════════════════════════════════════════════════════════════╝")
fmt.Println()
// Server settings
fmt.Println("📡 Server Settings:")
fmt.Printf(" Domain: %s\n", colorValue(domain))
fmt.Printf(" Port: %s\n", colorValue(fmt.Sprintf("%d", port)))
if publicPort != port {
fmt.Printf(" Public Port: %s\n", colorValue(fmt.Sprintf("%d", publicPort)))
}
fmt.Println()
// Authentication
fmt.Println("🔐 Authentication:")
if token != "" {
fmt.Printf(" Auth Token: %s\n", colorValue(displayToken))
} else {
fmt.Printf(" Auth Token: %s\n", colorWarning("(not set)"))
}
if metricsToken != "" {
fmt.Printf(" Metrics Token: %s\n", colorValue(displayMetricsToken))
} else {
fmt.Printf(" Metrics Token: %s\n", colorWarning("(not set)"))
}
fmt.Println()
// TLS Configuration
fmt.Println("🔒 TLS Configuration:")
if tlsCert != "" {
certStatus := checkFileExists(tlsCert)
fmt.Printf(" Certificate: %s %s\n", colorValue(tlsCert), certStatus)
} else {
fmt.Printf(" Certificate: %s\n", colorError("(not set)"))
}
if tlsKey != "" {
keyStatus := checkFileExists(tlsKey)
fmt.Printf(" Private Key: %s %s\n", colorValue(tlsKey), keyStatus)
} else {
fmt.Printf(" Private Key: %s\n", colorError("(not set)"))
}
fmt.Println()
// TCP Tunnel Ports
fmt.Println("🌐 TCP Tunnel Port Range:")
fmt.Printf(" Min Port: %s\n", colorValue(fmt.Sprintf("%d", tcpPortMin)))
fmt.Printf(" Max Port: %s\n", colorValue(fmt.Sprintf("%d", tcpPortMax)))
fmt.Printf(" Available Ports: %s\n", colorValue(fmt.Sprintf("%d", tcpPortMax-tcpPortMin+1)))
fmt.Println()
// Performance
if pprofPort > 0 {
fmt.Println("⚡ Performance:")
fmt.Printf(" Pprof Port: %s\n", colorValue(fmt.Sprintf("%d", pprofPort)))
fmt.Println()
}
// Configuration sources
fmt.Println("📋 Configuration Sources:")
fmt.Println(" Environment variables (DRIP_*)")
fmt.Println(" Command-line flags")
fmt.Println(" Config file: /etc/drip/server.env")
fmt.Println()
// Endpoints
fmt.Println("🔗 Server Endpoints:")
fmt.Printf(" Main: https://%s:%d\n", domain, publicPort)
fmt.Printf(" Health: https://%s:%d/health\n", domain, publicPort)
fmt.Printf(" Stats: https://%s:%d/stats\n", domain, publicPort)
fmt.Printf(" Metrics: https://%s:%d/metrics\n", domain, publicPort)
fmt.Println()
if !serverConfigFull && (token != "" || metricsToken != "") {
fmt.Println("💡 Tip: Use --full flag to show complete tokens")
fmt.Println()
}
return nil
}
func maskToken(token string, showFull bool) string {
if token == "" {
return ""
}
if showFull {
return token
}
tokenLen := len(token)
if tokenLen <= 8 {
return "***"
}
// Show first 4 and last 4 characters
return token[:4] + "***" + token[tokenLen-4:]
}
func checkFileExists(path string) string {
if _, err := os.Stat(path); err == nil {
return colorSuccess("✓")
}
return colorError("✗ (not found)")
}
func colorValue(s string) string {
return fmt.Sprintf("\033[36m%s\033[0m", s) // Cyan
}
func colorSuccess(s string) string {
return fmt.Sprintf("\033[32m%s\033[0m", s) // Green
}
func colorWarning(s string) string {
return fmt.Sprintf("\033[33m%s\033[0m", s) // Yellow
}
func colorError(s string) string {
return fmt.Sprintf("\033[31m%s\033[0m", s) // Red
}

View File

@@ -0,0 +1,106 @@
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// Tunnel metrics
TunnelCount = promauto.NewGauge(prometheus.GaugeOpts{
Name: "drip_tunnel_count",
Help: "Current number of active tunnels",
})
TunnelRegistrations = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_tunnel_registrations_total",
Help: "Total number of tunnel registrations",
})
TunnelRegistrationFailures = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "drip_tunnel_registration_failures_total",
Help: "Total number of failed tunnel registrations",
}, []string{"reason"})
TunnelsByIP = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "drip_tunnels_by_ip",
Help: "Number of tunnels per client IP",
}, []string{"ip"})
// Connection metrics
ActiveConnections = promauto.NewGauge(prometheus.GaugeOpts{
Name: "drip_active_connections",
Help: "Current number of active TCP connections",
})
TotalConnections = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_connections_total",
Help: "Total number of connections handled",
})
// Traffic metrics
BytesReceived = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_bytes_received_total",
Help: "Total bytes received",
})
BytesSent = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_bytes_sent_total",
Help: "Total bytes sent",
})
RequestsTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_requests_total",
Help: "Total number of HTTP requests handled",
})
// Per-tunnel metrics
TunnelBytesReceived = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "drip_tunnel_bytes_received_total",
Help: "Total bytes received per tunnel",
}, []string{"tunnel_id", "subdomain", "type"})
TunnelBytesSent = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "drip_tunnel_bytes_sent_total",
Help: "Total bytes sent per tunnel",
}, []string{"tunnel_id", "subdomain", "type"})
TunnelActiveConnections = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "drip_tunnel_active_connections",
Help: "Current number of active connections per tunnel",
}, []string{"tunnel_id", "subdomain", "type"})
// Rate limiting metrics
RateLimitRejections = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "drip_rate_limit_rejections_total",
Help: "Total number of rate limit rejections",
}, []string{"type", "ip"})
// System metrics
PanicTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "drip_panic_total",
Help: "Total number of panics recovered",
})
WorkerPoolSize = promauto.NewGauge(prometheus.GaugeOpts{
Name: "drip_worker_pool_size",
Help: "Current worker pool size",
})
WorkerPoolActiveWorkers = promauto.NewGauge(prometheus.GaugeOpts{
Name: "drip_worker_pool_active_workers",
Help: "Current number of active workers",
})
// HTTP proxy metrics
HTTPRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "drip_http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
}, []string{"method", "status"})
HTTPRequestsInFlight = promauto.NewGauge(prometheus.GaugeOpts{
Name: "drip_http_requests_in_flight",
Help: "Current number of HTTP requests being processed",
})
)

View File

@@ -20,6 +20,7 @@ import (
"drip/internal/shared/pool"
"drip/internal/shared/protocol"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap"
)
@@ -33,18 +34,20 @@ var bufioReaderPool = sync.Pool{
const openStreamTimeout = 3 * time.Second
type Handler struct {
manager *tunnel.Manager
logger *zap.Logger
domain string
authToken string
manager *tunnel.Manager
logger *zap.Logger
domain string
authToken string
metricsToken string
}
func NewHandler(manager *tunnel.Manager, logger *zap.Logger, domain string, authToken string) *Handler {
func NewHandler(manager *tunnel.Manager, logger *zap.Logger, domain string, authToken string, metricsToken string) *Handler {
return &Handler{
manager: manager,
logger: logger,
domain: domain,
authToken: authToken,
manager: manager,
logger: logger,
domain: domain,
authToken: authToken,
metricsToken: metricsToken,
}
}
@@ -57,6 +60,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveStats(w, r)
return
}
if r.URL.Path == "/metrics" {
h.serveMetrics(w, r)
return
}
subdomain := h.extractSubdomain(r.Host)
if subdomain == "" {
@@ -323,30 +330,129 @@ func (h *Handler) extractSubdomain(host string) string {
func (h *Handler) serveHomePage(w http.ResponseWriter, r *http.Request) {
html := `<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Drip - Your Tunnel, Your Domain, Anywhere</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
h1 { color: #333; }
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
.stats { background: #f9f9f9; padding: 15px; border-radius: 5px; margin: 20px 0; }
</style>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Drip - Your Tunnel, Your Domain, Anywhere</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #fff;
color: #24292f;
line-height: 1.6;
}
.container { max-width: 720px; margin: 0 auto; padding: 48px 24px; }
header { margin-bottom: 48px; }
h1 { font-size: 28px; font-weight: 600; margin-bottom: 8px; }
h1 span { margin-right: 8px; }
.desc { color: #57606a; font-size: 16px; }
h2 { font-size: 18px; font-weight: 600; margin: 32px 0 12px; }
.code-wrap {
position: relative;
background: #f6f8fa;
border: 1px solid #d0d7de;
border-radius: 6px;
margin-bottom: 12px;
}
.code-wrap pre {
margin: 0;
padding: 12px 16px;
padding-right: 60px;
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
font-size: 14px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.copy-btn {
position: absolute;
top: 8px;
right: 8px;
background: #fff;
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 4px 6px;
cursor: pointer;
color: #57606a;
display: flex;
align-items: center;
justify-content: center;
}
.copy-btn:hover { background: #f3f4f6; }
.copy-btn svg { width: 16px; height: 16px; }
.copy-btn .check { display: none; color: #1a7f37; }
.copy-btn.copied .copy { display: none; }
.copy-btn.copied .check { display: block; }
.links { margin-top: 32px; display: flex; gap: 24px; flex-wrap: wrap; }
.links a { color: #0969da; text-decoration: none; font-size: 14px; }
.links a:hover { text-decoration: underline; }
footer { margin-top: 48px; padding-top: 24px; border-top: 1px solid #d0d7de; }
footer a { color: #57606a; text-decoration: none; font-size: 14px; }
footer a:hover { color: #0969da; }
</style>
</head>
<body>
<h1>💧 Drip - Your Tunnel, Your Domain, Anywhere</h1>
<p>A self-hosted tunneling solution to securely expose your services to the internet.</p>
<div class="container">
<header>
<h1><span>💧</span>Drip</h1>
<p class="desc">Your Tunnel, Your Domain, Anywhere</p>
</header>
<h2>Quick Start</h2>
<p>Install the client:</p>
<code>bash <(curl -fsSL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install.sh)</code>
<p>A self-hosted tunneling solution to securely expose your services to the internet.</p>
<p>Start a tunnel:</p>
<code>drip http 3000</code><br><br>
<code>drip https 443</code><br><br>
<code>drip tcp 5432</code>
<p><a href="/health">Health Check</a> | <a href="/stats">Statistics</a></p>
<h2>Install</h2>
<div class="code-wrap">
<pre>bash &lt;(curl -fsSL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/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>
</button>
</div>
<h2>Usage</h2>
<div class="code-wrap">
<pre>drip http 3000</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>
</button>
</div>
<div class="code-wrap">
<pre>drip https 443</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>
</button>
</div>
<div class="code-wrap">
<pre>drip tcp 5432</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>
</button>
</div>
<div class="links">
<a href="/health">Health Check</a>
<a href="/stats">Statistics</a>
<a href="/metrics">Prometheus Metrics</a>
</div>
<footer>
<a href="https://github.com/Gouryella/drip" target="_blank">GitHub</a>
</footer>
</div>
<script>
function copy(btn) {
const text = btn.previousElementSibling.textContent;
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('copied');
setTimeout(() => { btn.classList.remove('copied'); }, 2000);
});
}
</script>
</body>
</html>`
@@ -375,7 +481,7 @@ func (h *Handler) serveHealth(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) serveStats(w http.ResponseWriter, r *http.Request) {
if h.authToken != "" {
if h.metricsToken != "" {
// Only accept token via Authorization header (Bearer token)
// URL query parameters are insecure (logged, cached, visible in browser history)
var token string
@@ -384,9 +490,9 @@ func (h *Handler) serveStats(w http.ResponseWriter, r *http.Request) {
token = strings.TrimPrefix(authHeader, "Bearer ")
}
if token != h.authToken {
if token != h.metricsToken {
w.Header().Set("WWW-Authenticate", `Bearer realm="stats"`)
http.Error(w, "Unauthorized: provide token via 'Authorization: Bearer <token>' header", http.StatusUnauthorized)
http.Error(w, "Unauthorized: provide metrics token via 'Authorization: Bearer <token>' header", http.StatusUnauthorized)
return
}
}
@@ -426,6 +532,26 @@ func (h *Handler) serveStats(w http.ResponseWriter, r *http.Request) {
w.Write(data)
}
func (h *Handler) serveMetrics(w http.ResponseWriter, r *http.Request) {
if h.metricsToken != "" {
// Only accept token via Authorization header (Bearer token)
var token string
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
token = strings.TrimPrefix(authHeader, "Bearer ")
}
if token != h.metricsToken {
w.Header().Set("WWW-Authenticate", `Bearer realm="metrics"`)
http.Error(w, "Unauthorized: provide metrics token via 'Authorization: Bearer <token>' header", http.StatusUnauthorized)
return
}
}
// Serve Prometheus metrics
promhttp.Handler().ServeHTTP(w, r)
}
type bufferedReadWriteCloser struct {
*bufio.Reader
net.Conn

View File

@@ -372,51 +372,6 @@ func (g *ConnectionGroup) buildSessionHeap(includePrimary bool) *sessionHeap {
return h
}
func (g *ConnectionGroup) selectSession() *yamux.Session {
h := g.buildSessionHeap(false)
if h.Len() == 0 {
sessionHeapPool.Put(h)
h = g.buildSessionHeap(true)
}
if h.Len() == 0 {
sessionHeapPool.Put(h)
return nil
}
entry := heap.Pop(h).(*sessionEntry)
session := entry.session
*h = (*h)[:0]
sessionHeapPool.Put(h)
if session == nil || session.IsClosed() {
return nil
}
return session
}
func (g *ConnectionGroup) sessionsSnapshot(includePrimary bool) []*yamux.Session {
g.mu.RLock()
defer g.mu.RUnlock()
if len(g.Sessions) == 0 {
return nil
}
sessions := make([]*yamux.Session, 0, len(g.Sessions))
for id, session := range g.Sessions {
if session == nil || session.IsClosed() {
continue
}
if id == "primary" && !includePrimary {
continue
}
sessions = append(sessions, session)
}
return sessions
}
func (g *ConnectionGroup) deleteClosedSessions() {
g.mu.Lock()
for id, session := range g.Sessions {

View File

@@ -10,6 +10,7 @@ import (
"sync"
"time"
"drip/internal/server/metrics"
"drip/internal/server/tunnel"
"drip/internal/shared/pool"
"drip/internal/shared/recovery"
@@ -56,6 +57,9 @@ func NewListener(address string, tlsConfig *tls.Config, authToken string, manage
panicMetrics := recovery.NewPanicMetrics(logger, nil)
recoverer := recovery.NewRecoverer(logger, panicMetrics)
// Initialize worker pool metrics
metrics.WorkerPoolSize.Set(float64(workers))
return &Listener{
address: address,
tlsConfig: tlsConfig,
@@ -237,11 +241,17 @@ func (l *Listener) handleConnection(netConn net.Conn) {
l.connections[connID] = conn
l.connMu.Unlock()
// Update connection metrics
metrics.TotalConnections.Inc()
metrics.ActiveConnections.Inc()
defer func() {
l.connMu.Lock()
delete(l.connections, connID)
l.connMu.Unlock()
metrics.ActiveConnections.Dec()
if !conn.IsHandedOff() {
netConn.Close()
}

View File

@@ -6,6 +6,7 @@ import (
"sync/atomic"
"time"
"drip/internal/server/metrics"
"drip/internal/shared/protocol"
"github.com/gorilla/websocket"
"go.uber.org/zap"
@@ -144,6 +145,8 @@ func (c *Connection) AddBytesIn(n int64) {
return
}
c.bytesIn.Add(n)
metrics.BytesReceived.Add(float64(n))
metrics.TunnelBytesReceived.WithLabelValues(c.Subdomain, c.Subdomain, c.GetTunnelType().String()).Add(float64(n))
}
func (c *Connection) AddBytesOut(n int64) {
@@ -151,6 +154,8 @@ func (c *Connection) AddBytesOut(n int64) {
return
}
c.bytesOut.Add(n)
metrics.BytesSent.Add(float64(n))
metrics.TunnelBytesSent.WithLabelValues(c.Subdomain, c.Subdomain, c.GetTunnelType().String()).Add(float64(n))
}
func (c *Connection) GetBytesIn() int64 {
@@ -163,12 +168,14 @@ func (c *Connection) GetBytesOut() int64 {
func (c *Connection) IncActiveConnections() {
c.activeConnections.Add(1)
metrics.TunnelActiveConnections.WithLabelValues(c.Subdomain, c.Subdomain, c.GetTunnelType().String()).Inc()
}
func (c *Connection) DecActiveConnections() {
if v := c.activeConnections.Add(-1); v < 0 {
c.activeConnections.Store(0)
}
metrics.TunnelActiveConnections.WithLabelValues(c.Subdomain, c.Subdomain, c.GetTunnelType().String()).Dec()
}
func (c *Connection) GetActiveConnections() int64 {

View File

@@ -7,6 +7,7 @@ import (
"sync/atomic"
"time"
"drip/internal/server/metrics"
"drip/internal/shared/utils"
"github.com/gorilla/websocket"
"go.uber.org/zap"
@@ -176,6 +177,7 @@ func (m *Manager) RegisterWithIP(conn *websocket.Conn, customSubdomain string, r
zap.Int64("current", current),
zap.Int("max", m.maxTunnels),
)
metrics.TunnelRegistrationFailures.WithLabelValues("max_tunnels").Inc()
return "", ErrTooManyTunnels
}
if m.tunnelCount.CompareAndSwap(current, current+1) {
@@ -199,6 +201,8 @@ func (m *Manager) RegisterWithIP(conn *websocket.Conn, customSubdomain string, r
zap.String("ip", remoteIP),
zap.Int("limit", m.rateLimit),
)
metrics.RateLimitRejections.WithLabelValues("registration", remoteIP).Inc()
metrics.TunnelRegistrationFailures.WithLabelValues("rate_limit").Inc()
return "", ErrRateLimitExceeded
}
@@ -211,11 +215,13 @@ func (m *Manager) RegisterWithIP(conn *websocket.Conn, customSubdomain string, r
zap.Int("current", currentPerIP),
zap.Int("max", m.maxTunnelsPerIP),
)
metrics.TunnelRegistrationFailures.WithLabelValues("max_per_ip").Inc()
return "", ErrTooManyPerIP
}
// Reserve per-IP slot while still holding the lock
m.tunnelsByIP[remoteIP]++
metrics.TunnelsByIP.WithLabelValues(remoteIP).Set(float64(m.tunnelsByIP[remoteIP]))
m.ipMu.Unlock()
}
@@ -227,6 +233,9 @@ func (m *Manager) RegisterWithIP(conn *websocket.Conn, customSubdomain string, r
m.tunnelsByIP[remoteIP]--
if m.tunnelsByIP[remoteIP] == 0 {
delete(m.tunnelsByIP, remoteIP)
metrics.TunnelsByIP.DeleteLabelValues(remoteIP)
} else {
metrics.TunnelsByIP.WithLabelValues(remoteIP).Set(float64(m.tunnelsByIP[remoteIP]))
}
}
m.ipMu.Unlock()
@@ -293,6 +302,10 @@ func (m *Manager) RegisterWithIP(conn *websocket.Conn, customSubdomain string, r
zap.Int64("total_tunnels", m.tunnelCount.Load()),
)
// Update Prometheus metrics
metrics.TunnelRegistrations.Inc()
metrics.TunnelCount.Set(float64(m.tunnelCount.Load()))
return subdomain, nil
}
@@ -321,6 +334,9 @@ func (m *Manager) Unregister(subdomain string) {
m.tunnelsByIP[remoteIP]--
if m.tunnelsByIP[remoteIP] == 0 {
delete(m.tunnelsByIP, remoteIP)
metrics.TunnelsByIP.DeleteLabelValues(remoteIP)
} else {
metrics.TunnelsByIP.WithLabelValues(remoteIP).Set(float64(m.tunnelsByIP[remoteIP]))
}
}
m.ipMu.Unlock()
@@ -330,6 +346,9 @@ func (m *Manager) Unregister(subdomain string) {
zap.String("subdomain", subdomain),
zap.Int64("total_tunnels", m.tunnelCount.Load()),
)
// Update Prometheus metrics
metrics.TunnelCount.Set(float64(m.tunnelCount.Load()))
}
// Get retrieves a tunnel connection by subdomain
@@ -394,6 +413,9 @@ func (m *Manager) CleanupStale(timeout time.Duration) int {
m.tunnelsByIP[remoteIP]--
if m.tunnelsByIP[remoteIP] == 0 {
delete(m.tunnelsByIP, remoteIP)
metrics.TunnelsByIP.DeleteLabelValues(remoteIP)
} else {
metrics.TunnelsByIP.WithLabelValues(remoteIP).Set(float64(m.tunnelsByIP[remoteIP]))
}
}
m.ipMu.Unlock()

View File

@@ -79,20 +79,6 @@ func (p *BufferPool) Put(buf *[]byte) {
// Note: buffers with non-standard sizes are not pooled (let GC handle them)
}
// GetXLarge returns a 1MB buffer for bulk data transfers
func (p *BufferPool) GetXLarge() *[]byte {
return p.xlarge.Get().(*[]byte)
}
// PutXLarge returns a 1MB buffer to the pool
func (p *BufferPool) PutXLarge(buf *[]byte) {
if buf == nil || cap(*buf) != SizeXLarge {
return
}
*buf = (*buf)[:cap(*buf)]
p.xlarge.Put(buf)
}
var globalBufferPool = NewBufferPool()
func GetBuffer(size int) *[]byte {
@@ -102,13 +88,3 @@ func GetBuffer(size int) *[]byte {
func PutBuffer(buf *[]byte) {
globalBufferPool.Put(buf)
}
// GetXLargeBuffer returns a 1MB buffer from the global pool
func GetXLargeBuffer() *[]byte {
return globalBufferPool.GetXLarge()
}
// PutXLargeBuffer returns a 1MB buffer to the global pool
func PutXLargeBuffer(buf *[]byte) {
globalBufferPool.PutXLarge(buf)
}

View File

@@ -7,6 +7,7 @@ import (
"sync/atomic"
"time"
"drip/internal/server/metrics"
"go.uber.org/zap"
)
@@ -39,6 +40,7 @@ func NewPanicMetrics(logger *zap.Logger, alerter Alerter) *PanicMetrics {
func (pm *PanicMetrics) RecordPanic(location string, panicValue interface{}) {
atomic.AddUint64(&pm.totalPanics, 1)
metrics.PanicTotal.Inc()
pm.mu.Lock()

View File

@@ -59,8 +59,9 @@ MSG_EN=(
["enter_domain"]="Enter your domain (e.g., tunnel.example.com)"
["domain_required"]="Domain is required"
["enter_port"]="Enter server port"
["enter_token"]="Enter authentication token (leave empty to generate)"
["token_generated"]="Token generated"
["enter_token"]="Enter authentication token (leave empty to auto-generate)"
["token_generated"]="Authentication token generated"
["metrics_token_generated"]="Metrics token generated"
["enter_cert_path"]="Enter TLS certificate path (public key)"
["enter_key_path"]="Enter TLS private key path"
["cert_not_found"]="Certificate file not found"
@@ -94,7 +95,8 @@ MSG_EN=(
["install_complete"]="Installation completed!"
["client_info"]="Client connection info"
["server_addr"]="Server"
["token_label"]="Token"
["token_label"]="Auth Token"
["metrics_token_label"]="Metrics Token"
["service_commands"]="Service management commands"
["cmd_start"]="Start service"
["cmd_stop"]="Stop service"
@@ -149,7 +151,8 @@ MSG_ZH=(
["domain_required"]="域名是必填项"
["enter_port"]="输入服务器端口"
["enter_token"]="输入认证令牌(留空自动生成)"
["token_generated"]="令牌已生成"
["token_generated"]="认证令牌已生成"
["metrics_token_generated"]="监控令牌已生成"
["enter_cert_path"]="输入 TLS 证书路径(公钥)"
["enter_key_path"]="输入 TLS 私钥路径"
["cert_not_found"]="证书文件未找到"
@@ -183,7 +186,8 @@ MSG_ZH=(
["install_complete"]="安装完成!"
["client_info"]="客户端连接信息"
["server_addr"]="服务器"
["token_label"]="令牌"
["token_label"]="认证令牌"
["metrics_token_label"]="监控令牌"
["service_commands"]="服务管理命令"
["cmd_start"]="启动服务"
["cmd_stop"]="停止服务"
@@ -546,7 +550,7 @@ install_binary() {
# Configuration
# ============================================================================
generate_token() {
# Generate a random 32-character token
# Generate a random 32-character token (16 bytes = 32 hex chars)
if command -v openssl &> /dev/null; then
openssl rand -hex 16
else
@@ -583,7 +587,7 @@ configure_server() {
read -p "$(msg enter_tcp_max) [$DEFAULT_TCP_PORT_MAX]: " TCP_PORT_MAX < /dev/tty
TCP_PORT_MAX="${TCP_PORT_MAX:-$DEFAULT_TCP_PORT_MAX}"
# Token
# Authentication token (user can provide or auto-generate)
echo ""
read -p "$(msg enter_token): " TOKEN < /dev/tty
if [[ -z "$TOKEN" ]]; then
@@ -591,6 +595,10 @@ configure_server() {
print_success "$(msg token_generated): $TOKEN"
fi
# Metrics token (always auto-generated)
METRICS_TOKEN=$(generate_token)
print_success "$(msg metrics_token_generated): $METRICS_TOKEN"
# TLS certificate selection
print_panel "$(msg cert_option_title)" \
"${GREEN}1)${NC} $(msg cert_option_certbot)" \
@@ -946,6 +954,7 @@ DRIP_PORT=${PORT}
DRIP_PUBLIC_PORT=${PUBLIC_PORT}
DRIP_DOMAIN=${DOMAIN}
DRIP_TOKEN=${TOKEN}
DRIP_METRICS_TOKEN=${METRICS_TOKEN}
# TLS certificate paths
DRIP_TLS_CERT=${CERT_PATH}
@@ -995,8 +1004,9 @@ show_completion() {
print_panel "$(msg install_complete)"
echo -e "${CYAN}$(msg client_info):${NC}"
echo -e " ${BOLD}$(msg server_addr):${NC} ${DOMAIN}:${PORT}"
echo -e " ${BOLD}$(msg token_label):${NC} ${TOKEN}"
echo -e " ${BOLD}$(msg server_addr):${NC} ${DOMAIN}:${PORT}"
echo -e " ${BOLD}$(msg token_label):${NC} ${TOKEN}"
echo -e " ${BOLD}$(msg metrics_token_label):${NC} ${METRICS_TOKEN}"
echo ""
echo -e "${CYAN}$(msg service_commands):${NC}"