~bigbes/shroud

ref: 5afb3bad1be8eb82352dde56e6836a0a8ea4ef7f shroud/internal/config/config.go -rw-r--r-- 6.4 KiB
5afb3bad — Eugene Blikh feat: add optional shadowsocks and outline smart dialer config 2 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package config

import (
	"fmt"
	"os"
	"path/filepath"

	"gopkg.in/yaml.v3"
)

type Config struct {
	// Server identification and display.
	Server ServerConfig `yaml:"server"`
	// REST API settings.
	API APIConfig `yaml:"api"`
	// Prometheus metrics endpoint settings.
	Metrics MetricsConfig `yaml:"metrics"`
	// Shadowsocks proxy settings.
	Shadowsocks ShadowsocksConfig `yaml:"shadowsocks"`
	// AmneziaWG VPN settings.
	AmneziaWG AmneziaWGConfig `yaml:"amneziawg"`
	// ACME (Let's Encrypt) certificate settings.
	ACME ACMEConfig `yaml:"acme"`
	// Path to persistent state file.
	// Defaults to "state.yaml" in the same directory as the config file.
	State string `yaml:"state_file"`

	// configDir is the directory containing the config file, used for resolving relative paths.
	configDir string
}

type ServerConfig struct {
	// Human-readable server name.
	Name string `yaml:"name"`
	// Unique server identifier (auto-generated if empty).
	ID string `yaml:"id"`
	// Hostname or IP used in access key URLs.
	Hostname string `yaml:"hostname"`
}

type APIConfig struct {
	// Listen address for the management API (e.g., ":8081").
	ListenAddr string `yaml:"listen_addr"`
	// Secret prefix for URL-based authentication (e.g., "SecretPath").
	// API will be served at /<secret>/...
	Secret string `yaml:"secret"`
	// Path to TLS certificate file (optional, used for certSha256 in access.yaml).
	CertFile string `yaml:"cert_file"`
}

type MetricsConfig struct {
	// Listen address for the Prometheus /metrics and /healthz endpoint.
	// Defaults to "127.0.0.1:8081".
	ListenAddr string `yaml:"listen_addr"`
	// Node exporter collectors to enable (e.g., [cpu, meminfo, diskstats]).
	// Empty means no node_exporter collectors.
	NodeExporterCollectors []string `yaml:"node_exporter_collectors"`
}

type AmneziaWGConfig struct {
	// Enable AmneziaWG protocol.
	Enabled bool `yaml:"enabled"`
	// UDP listen port for AWG + HTTP/3 mux.
	ListenPort int `yaml:"listen_port"`
	// TUN device name.
	TUNName string `yaml:"tun_name"`
	// Server subnet in CIDR notation (e.g., "10.14.0.0/24").
	// Server takes the first usable IP (.1), peers are allocated from .2 onwards.
	Address string `yaml:"address"`
	// TUN MTU.
	MTU int `yaml:"mtu"`
	// Server interface private key (base64). Auto-generated if empty.
	PrivateKey string `yaml:"private_key"`
	// DNS server for client configs.
	DNS string `yaml:"dns"`
	// Hostname for AWG client endpoint. If empty and muxer is enabled, falls back to server.hostname.
	Hostname string `yaml:"hostname"`

	// HTTP/3 + AWG muxer on the same UDP port.
	// When enabled, AWG and QUIC/HTTP3 share the listen port.
	// nil = auto (enabled when domain is set), true = force on, false = force off.
	MuxEnabled *bool `yaml:"mux_enabled"`

	// HTTP/3 cover for DPI resistance.
	// Domain for Let's Encrypt certificate. Defaults to server.hostname if empty.
	// If still empty after defaulting, HTTP/3 server is disabled.
	Domain string `yaml:"domain"`

	// Obfuscation parameters.
	Jc   int `yaml:"jc"`
	Jmin int `yaml:"jmin"`
	Jmax int `yaml:"jmax"`
	S1   int `yaml:"s1"`
	S2   int `yaml:"s2"`
	S3   int `yaml:"s3"`
	S4   int `yaml:"s4"`
	H1   string `yaml:"h1"` // "min-max" or single value
	H2   string `yaml:"h2"`
	H3   string `yaml:"h3"`
	H4   string `yaml:"h4"`
}

type ACMEConfig struct {
	// Directory for cached TLS certificates.
	CertCache string `yaml:"cert_cache"`
	// HTTP port for ACME HTTP-01 challenges.
	HTTPPort int `yaml:"http_port"`
}

type ShadowsocksConfig struct {
	// Enable Shadowsocks protocol. Defaults to true.
	Enabled *bool `yaml:"enabled"`
	// Default port for new access keys. 0 means pick a random unused port on first start.
	DefaultPort int `yaml:"default_port"`
	// Default cipher for new access keys.
	DefaultCipher string `yaml:"default_cipher"`
	// UDP NAT timeout as a duration string (e.g., "5m").
	NATTimeout string `yaml:"nat_timeout"`
	// Replay protection history size (0 = disabled).
	ReplayHistory int `yaml:"replay_history"`
	// Path to IP-to-country MaxMind MMDB file.
	IPCountryDB string `yaml:"ip_country_db"`
	// Path to IP-to-ASN MaxMind MMDB file.
	IPASNDB string `yaml:"ip_asn_db"`
}

func Load(filename string) (*Config, error) {
	absPath, err := filepath.Abs(filename)
	if err != nil {
		return nil, fmt.Errorf("resolving config path: %w", err)
	}

	data, err := os.ReadFile(absPath)
	if err != nil {
		return nil, fmt.Errorf("reading config file: %w", err)
	}
	cfg := &Config{
		configDir: filepath.Dir(absPath),
	}
	if err := yaml.Unmarshal(data, cfg); err != nil {
		return nil, fmt.Errorf("parsing config: %w", err)
	}
	cfg.setDefaults()
	return cfg, nil
}

func (c *Config) setDefaults() {
	if c.API.ListenAddr == "" {
		c.API.ListenAddr = ":8081"
	}
	if c.Metrics.ListenAddr == "" {
		c.Metrics.ListenAddr = "127.0.0.1:8081"
	}
	if c.Shadowsocks.Enabled == nil {
		t := true
		c.Shadowsocks.Enabled = &t
	}
	if c.Shadowsocks.DefaultCipher == "" {
		c.Shadowsocks.DefaultCipher = "chacha20-ietf-poly1305"
	}
	if c.Shadowsocks.NATTimeout == "" {
		c.Shadowsocks.NATTimeout = "5m"
	}
	if c.Server.Name == "" {
		c.Server.Name = "Outline Server"
	}
	if c.State == "" {
		c.State = "state.yaml"
	}
	// AmneziaWG defaults.
	if c.AmneziaWG.Enabled {
		if c.AmneziaWG.ListenPort == 0 {
			c.AmneziaWG.ListenPort = 443
		}
		if c.AmneziaWG.TUNName == "" {
			c.AmneziaWG.TUNName = "awg0"
		}
		if c.AmneziaWG.MTU == 0 {
			c.AmneziaWG.MTU = 1420
		}
	}
	// AmneziaWG domain defaults to server hostname.
	if c.AmneziaWG.Enabled && c.AmneziaWG.Domain == "" {
		c.AmneziaWG.Domain = c.Server.Hostname
	}
	// ACME defaults.
	if c.ACME.CertCache == "" {
		c.ACME.CertCache = "/var/lib/shroud/certs"
	}
	if c.ACME.HTTPPort == 0 {
		c.ACME.HTTPPort = 80
	}
}

// AWGMuxEnabled returns whether the AWG+HTTP/3 muxer is enabled.
// If MuxEnabled is explicitly set, that value is used.
// Otherwise, muxer is enabled when a domain is configured.
func (c *AmneziaWGConfig) AWGMuxEnabled() bool {
	if c.MuxEnabled != nil {
		return *c.MuxEnabled
	}
	return c.Domain != ""
}

// AWGHostname returns the hostname for AWG client endpoints.
// Uses awg.hostname if set; falls back to serverHostname when muxer is enabled.
func (c *AmneziaWGConfig) AWGHostname(serverHostname string) string {
	if c.Hostname != "" {
		return c.Hostname
	}
	if c.AWGMuxEnabled() {
		return serverHostname
	}
	return ""
}

// StateFile returns the absolute path to the state file.
func (c *Config) StateFile() string {
	if filepath.IsAbs(c.State) {
		return c.State
	}
	return filepath.Join(c.configDir, c.State)
}