feat(client): Add bandwidth limit function support

- Implement client bandwidth limitation parameter --bandwidth, supporting 1M, 1MB, 1G and other formats
- Added parseBandwidth function to parse bandwidth values and verify them
- Added bandwidth limit option in HTTP, HTTPS, TCP commands
- Pass bandwidth configuration to the server through protocol
- Add relevant test cases to verify the bandwidth analysis function

feat(server): implements server-side bandwidth limitation function

- Add bandwidth limitation logic in connection processing, using token bucket algorithm
- Implement an effective rate limiting strategy that minimizes the bandwidth of the client and server
- Added QoS limiter and restricted connection wrapper
- Integrated bandwidth throttling in HTTP and WebSocket proxies
- Added global bandwidth limit and burst multiplier settings in server configuration

docs: Updated documentation to describe bandwidth limiting functionality

- Add 2025-02-14 version update instructions in README and README_CN
- Add bandwidth limit function description and usage examples
- Provide client and server configuration examples and parameter descriptions
This commit is contained in:
Gouryella
2026-02-15 02:39:50 +08:00
parent 8edb792f13
commit 89f67ab145
30 changed files with 1132 additions and 5 deletions

View File

@@ -21,6 +21,7 @@ var (
authPass string
authBearer string
transport string
bandwidth string
)
var httpCmd = &cobra.Command{
@@ -37,6 +38,7 @@ Example:
drip http 3000 --auth secret Enable proxy authentication with password
drip http 3000 --auth-bearer sk-xxx Enable proxy authentication with bearer token
drip http 3000 --transport wss Use WebSocket over TLS (CDN-friendly)
drip http 3000 --bandwidth 1M Limit bandwidth to 1 MB/s
Configuration:
First time: Run 'drip config init' to save server and token
@@ -45,7 +47,13 @@ Configuration:
Transport options:
auto - Automatically select based on server address (default)
tcp - Direct TLS 1.3 connection
wss - WebSocket over TLS (works through CDN like Cloudflare)`,
wss - WebSocket over TLS (works through CDN like Cloudflare)
Bandwidth format:
1K, 1KB - 1 kilobyte per second (1024 bytes/s)
1M, 1MB - 1 megabyte per second (1048576 bytes/s)
1G, 1GB - 1 gigabyte per second
1024 - 1024 bytes per second (raw number)`,
Args: cobra.ExactArgs(1),
RunE: runHTTP,
SilenceUsage: true,
@@ -61,6 +69,7 @@ func init() {
httpCmd.Flags().StringVar(&authPass, "auth", "", "Password for proxy authentication")
httpCmd.Flags().StringVar(&authBearer, "auth-bearer", "", "Bearer token for proxy authentication")
httpCmd.Flags().StringVar(&transport, "transport", "auto", "Transport protocol: auto, tcp, wss (WebSocket over TLS)")
httpCmd.Flags().StringVar(&bandwidth, "bandwidth", "", "Bandwidth limit (e.g., 1M, 500K, 1G)")
httpCmd.Flags().BoolVar(&daemonMarker, "daemon-child", false, "Internal flag for daemon child process")
httpCmd.Flags().MarkHidden("daemon-child")
rootCmd.AddCommand(httpCmd)
@@ -85,6 +94,11 @@ func runHTTP(_ *cobra.Command, args []string) error {
return err
}
bw, err := parseBandwidth(bandwidth)
if err != nil {
return err
}
connConfig := &tcp.ConnectorConfig{
ServerAddr: serverAddr,
Token: token,
@@ -98,6 +112,7 @@ func runHTTP(_ *cobra.Command, args []string) error {
AuthPass: authPass,
AuthBearer: authBearer,
Transport: parseTransport(transport),
Bandwidth: bw,
}
var daemon *DaemonInfo
@@ -118,3 +133,41 @@ func parseTransport(s string) tcp.TransportType {
return tcp.TransportAuto
}
}
func parseBandwidth(s string) (int64, error) {
if s == "" {
return 0, nil
}
s = strings.TrimSpace(strings.ToUpper(s))
if s == "" {
return 0, nil
}
var multiplier int64 = 1
switch {
case strings.HasSuffix(s, "GB") || strings.HasSuffix(s, "G"):
multiplier = 1024 * 1024 * 1024
s = strings.TrimSuffix(strings.TrimSuffix(s, "GB"), "G")
case strings.HasSuffix(s, "MB") || strings.HasSuffix(s, "M"):
multiplier = 1024 * 1024
s = strings.TrimSuffix(strings.TrimSuffix(s, "MB"), "M")
case strings.HasSuffix(s, "KB") || strings.HasSuffix(s, "K"):
multiplier = 1024
s = strings.TrimSuffix(strings.TrimSuffix(s, "KB"), "K")
case strings.HasSuffix(s, "B"):
s = strings.TrimSuffix(s, "B")
}
val, err := strconv.ParseInt(s, 10, 64)
if err != nil || val < 0 {
return 0, fmt.Errorf("invalid bandwidth value: %q (use format like 1M, 500K, 1G)", s)
}
result := val * multiplier
if val > 0 && result/multiplier != val {
return 0, fmt.Errorf("bandwidth value overflow: %q", s)
}
return result, nil
}

View File

@@ -0,0 +1,59 @@
package cli
import (
"testing"
)
func TestParseBandwidth(t *testing.T) {
tests := []struct {
input string
want int64
wantErr bool
}{
{"", 0, false},
{"0", 0, false},
{"1024", 1024, false},
{"1K", 1024, false},
{"1KB", 1024, false},
{"1k", 1024, false},
{"1M", 1024 * 1024, false},
{"1MB", 1024 * 1024, false},
{"1m", 1024 * 1024, false},
{"10M", 10 * 1024 * 1024, false},
{"1G", 1024 * 1024 * 1024, false},
{"1GB", 1024 * 1024 * 1024, false},
{"500K", 500 * 1024, false},
{"100M", 100 * 1024 * 1024, false},
{" 1M ", 1024 * 1024, false},
{"1B", 1, false},
{"100B", 100, false},
{"invalid", 0, true},
{"abc", 0, true},
{"-1M", 0, true},
{"-100", 0, true},
{"1.5M", 0, true},
{"M", 0, true},
{"K", 0, true},
{"9223372036854775807K", 0, true},
{"9999999999999999999G", 0, true},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got, err := parseBandwidth(tt.input)
if tt.wantErr {
if err == nil {
t.Errorf("parseBandwidth(%q) = %d, want error", tt.input, got)
}
return
}
if err != nil {
t.Errorf("parseBandwidth(%q) unexpected error: %v", tt.input, err)
return
}
if got != tt.want {
t.Errorf("parseBandwidth(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}
}

View File

@@ -24,6 +24,7 @@ Example:
drip https 443 --auth secret Enable proxy authentication with password
drip https 443 --auth-bearer sk-xxx Enable proxy authentication with bearer token
drip https 443 --transport wss Use WebSocket over TLS (CDN-friendly)
drip https 443 --bandwidth 1M Limit bandwidth to 1 MB/s
Configuration:
First time: Run 'drip config init' to save server and token
@@ -32,7 +33,13 @@ Configuration:
Transport options:
auto - Automatically select based on server address (default)
tcp - Direct TLS 1.3 connection
wss - WebSocket over TLS (works through CDN like Cloudflare)`,
wss - WebSocket over TLS (works through CDN like Cloudflare)
Bandwidth format:
1K, 1KB - 1 kilobyte per second (1024 bytes/s)
1M, 1MB - 1 megabyte per second (1048576 bytes/s)
1G, 1GB - 1 gigabyte per second
1024 - 1024 bytes per second (raw number)`,
Args: cobra.ExactArgs(1),
RunE: runHTTPS,
SilenceUsage: true,
@@ -48,6 +55,7 @@ func init() {
httpsCmd.Flags().StringVar(&authPass, "auth", "", "Password for proxy authentication")
httpsCmd.Flags().StringVar(&authBearer, "auth-bearer", "", "Bearer token for proxy authentication")
httpsCmd.Flags().StringVar(&transport, "transport", "auto", "Transport protocol: auto, tcp, wss (WebSocket over TLS)")
httpsCmd.Flags().StringVar(&bandwidth, "bandwidth", "", "Bandwidth limit (e.g., 1M, 500K, 1G)")
httpsCmd.Flags().BoolVar(&daemonMarker, "daemon-child", false, "Internal flag for daemon child process")
httpsCmd.Flags().MarkHidden("daemon-child")
rootCmd.AddCommand(httpsCmd)
@@ -72,6 +80,11 @@ func runHTTPS(_ *cobra.Command, args []string) error {
return err
}
bw, err := parseBandwidth(bandwidth)
if err != nil {
return err
}
connConfig := &tcp.ConnectorConfig{
ServerAddr: serverAddr,
Token: token,
@@ -85,6 +98,7 @@ func runHTTPS(_ *cobra.Command, args []string) error {
AuthPass: authPass,
AuthBearer: authBearer,
Transport: parseTransport(transport),
Bandwidth: bw,
}
var daemon *DaemonInfo

View File

@@ -313,6 +313,24 @@ func runServer(cmd *cobra.Command, _ []string) error {
listener.SetAllowedTransports(cfg.AllowedTransports)
listener.SetAllowedTunnelTypes(cfg.AllowedTunnelTypes)
bandwidth, err := parseBandwidth(cfg.Bandwidth)
if err != nil {
logger.Fatal("Invalid bandwidth configuration", zap.Error(err))
}
burstMultiplier := cfg.BurstMultiplier
if burstMultiplier <= 0 {
burstMultiplier = 2.0
}
listener.SetBandwidth(bandwidth)
listener.SetBurstMultiplier(burstMultiplier)
if bandwidth > 0 {
logger.Info("Bandwidth limit configured",
zap.String("bandwidth", cfg.Bandwidth),
zap.Int64("bandwidth_bytes_sec", bandwidth),
zap.Float64("burst_multiplier", burstMultiplier),
)
}
if err := listener.Start(); err != nil {
logger.Fatal("Failed to start TCP listener", zap.Error(err))
}

View File

@@ -242,6 +242,7 @@ func buildConnectorConfig(cfg *config.ClientConfig, t *config.TunnelConfig) *tcp
AuthPass: t.Auth,
AuthBearer: t.AuthBearer,
Transport: transport,
Bandwidth: parseBandwidthOrZero(t.Bandwidth),
}
}
@@ -251,3 +252,8 @@ func getAddress(t *config.TunnelConfig) string {
}
return "127.0.0.1"
}
func parseBandwidthOrZero(s string) int64 {
bw, _ := parseBandwidth(s)
return bw
}

View File

@@ -24,6 +24,7 @@ Example:
drip tcp 22 --allow-ip 10.0.0.1 Allow single IP
drip tcp 22 --deny-ip 1.2.3.4 Block specific IP
drip tcp 22 --transport wss Use WebSocket over TLS (CDN-friendly)
drip tcp 22 --bandwidth 1M Limit bandwidth to 1 MB/s
Supported Services:
- Databases: PostgreSQL (5432), MySQL (3306), Redis (6379), MongoDB (27017)
@@ -54,6 +55,7 @@ func init() {
tcpCmd.Flags().StringSliceVar(&allowIPs, "allow-ip", nil, "Allow only these IPs or CIDR ranges (e.g., 192.168.1.1,10.0.0.0/8)")
tcpCmd.Flags().StringSliceVar(&denyIPs, "deny-ip", nil, "Deny these IPs or CIDR ranges (e.g., 1.2.3.4,192.168.1.0/24)")
tcpCmd.Flags().StringVar(&transport, "transport", "auto", "Transport protocol: auto, tcp, wss (WebSocket over TLS)")
tcpCmd.Flags().StringVar(&bandwidth, "bandwidth", "", "Bandwidth limit (e.g., 1M, 500K, 1G)")
tcpCmd.Flags().BoolVar(&daemonMarker, "daemon-child", false, "Internal flag for daemon child process")
tcpCmd.Flags().MarkHidden("daemon-child")
rootCmd.AddCommand(tcpCmd)
@@ -74,6 +76,11 @@ func runTCP(_ *cobra.Command, args []string) error {
return err
}
bw, err := parseBandwidth(bandwidth)
if err != nil {
return err
}
connConfig := &tcp.ConnectorConfig{
ServerAddr: serverAddr,
Token: token,
@@ -85,6 +92,7 @@ func runTCP(_ *cobra.Command, args []string) error {
AllowIPs: allowIPs,
DenyIPs: denyIPs,
Transport: parseTransport(transport),
Bandwidth: bw,
}
var daemon *DaemonInfo

View File

@@ -30,6 +30,9 @@ func buildDaemonArgs(tunnelType string, args []string, subdomain string, localAd
if authBearer != "" {
daemonArgs = append(daemonArgs, "--auth-bearer", authBearer)
}
if bandwidth != "" {
daemonArgs = append(daemonArgs, "--bandwidth", bandwidth)
}
if insecure {
daemonArgs = append(daemonArgs, "--insecure")
}