Files
drip/pkg/config/client_config.go
Gouryella 37d1bfc089 feat(client): Support predefined tunnel configuration and management commands
Added predefined tunnel functionality, allowing users to define multiple tunnels in the configuration file and start them by name, including the following improvements:
- Added --all flag to start all configured tunnels
- Added parameterless start command to list available tunnels
- Support configuration of multiple tunnel types (http, https, tcp)
- Support advanced configurations such as subdomains, transport protocols, and IP allowlists

refactor(deployments): Refactor Docker deployment configuration

Removed old Dockerfile and Compose configurations, added new deployment files:
- Removed .env.example and old Docker build files
- Added Caddy reverse proxy configuration file
- Added two deployment modes: standard and Caddy reverse proxy
- Added detailed server configuration example files

docs: Update documentation to include tunnel configuration and deployment guide

Updated Chinese and English README documents:
- Added usage instructions and configuration examples for predefined tunnels
- Expanded server deployment section to include direct TLS and reverse proxy modes
- Added server configuration reference table with detailed configuration item descriptions
- Added specific configuration methods for Caddy and Nginx reverse proxies
2026-01-15 17:18:27 +08:00

181 lines
5.1 KiB
Go

package config
import (
"fmt"
"net"
"os"
"path/filepath"
"strings"
"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)
Tunnels []*TunnelConfig `yaml:"tunnels,omitempty"` // Predefined tunnels
}
// Validate checks if the client configuration is valid
func (c *ClientConfig) Validate() error {
if c.Server == "" {
return fmt.Errorf("server address is required")
}
host, port, err := net.SplitHostPort(c.Server)
if err != nil {
if strings.Contains(err.Error(), "missing port") {
return fmt.Errorf("server address must include port (e.g., example.com:443), got: %s", c.Server)
}
return fmt.Errorf("invalid server address format: %s (expected host:port)", c.Server)
}
if host == "" {
return fmt.Errorf("server host is required")
}
if port == "" {
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()
if err != nil {
return ".drip/config.yaml"
}
return filepath.Join(home, ".drip", "config.yaml")
}
// LoadClientConfig loads configuration from file
func LoadClientConfig(path string) (*ClientConfig, error) {
if path == "" {
path = DefaultClientConfigPath()
}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("config file not found at %s, please run 'drip config init' first", path)
}
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var config ClientConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return &config, nil
}
// SaveClientConfig saves configuration to file
func SaveClientConfig(config *ClientConfig, path string) error {
if path == "" {
path = DefaultClientConfigPath()
}
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)
}
// Write to file with secure permissions
if err := os.WriteFile(path, data, 0600); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
// ConfigExists checks if config file exists
func ConfigExists(path string) bool {
if path == "" {
path = DefaultClientConfigPath()
}
_, err := os.Stat(path)
return err == nil
}