LoadClient reads and validates a client config file.
(path string)
| 289 | |
| 290 | // LoadClient reads and validates a client config file. |
| 291 | func LoadClient(path string) (*Client, error) { |
| 292 | b, err := os.ReadFile(path) |
| 293 | if err != nil { |
| 294 | if errors.Is(err, fs.ErrNotExist) { |
| 295 | return nil, fmt.Errorf("config file %q not found.\n Fix: copy the example and edit it:\n cp client_config.example.json %s", path, path) |
| 296 | } |
| 297 | return nil, fmt.Errorf("cannot read config %q: %w", path, err) |
| 298 | } |
| 299 | |
| 300 | var f clientFile |
| 301 | if err := json.Unmarshal(b, &f); err != nil { |
| 302 | return nil, fmt.Errorf("config %q is not valid JSON: %v\n Common causes: missing comma between fields, trailing comma after the last field, unclosed quote, or a typo in a field name", path, err) |
| 303 | } |
| 304 | |
| 305 | listenHost := firstNonEmpty(f.SocksHost, "127.0.0.1") |
| 306 | listenPort := firstPositive(f.SocksPort) |
| 307 | if listenPort == 0 { |
| 308 | listenPort = 1080 |
| 309 | } |
| 310 | if listenPort < 1 || listenPort > 65535 { |
| 311 | return nil, fmt.Errorf("socks_port %d is out of range (must be 1-65535)", listenPort) |
| 312 | } |
| 313 | |
| 314 | relayURLs := make([]string, 0, len(f.RelayURLs)) |
| 315 | for _, raw := range f.RelayURLs { |
| 316 | normalized, nerr := normalizeRelayURL(raw) |
| 317 | if nerr != nil { |
| 318 | return nil, nerr |
| 319 | } |
| 320 | if normalized != "" { |
| 321 | relayURLs = append(relayURLs, normalized) |
| 322 | } |
| 323 | } |
| 324 | relayURLs = dedupeStrings(relayURLs) |
| 325 | |
| 326 | key := strings.TrimSpace(f.TunnelKey) |
| 327 | if key == "" || key == "REPLACE_WITH_64_HEX_CHARACTER_RANDOM_KEY" { |
| 328 | return nil, fmt.Errorf("tunnel_key is empty or still the placeholder text in %s.\n Fix: generate a key with 'openssl rand -hex 32' and paste the 64-character output into the tunnel_key field. The same value must be in server_config.json", path) |
| 329 | } |
| 330 | if len(key) != 64 { |
| 331 | return nil, fmt.Errorf("tunnel_key must be exactly 64 hex characters (got %d) in %s.\n Fix: generate a fresh key with 'openssl rand -hex 32' and paste the full output. Use the SAME value in client_config.json and server_config.json", len(key), path) |
| 332 | } |
| 333 | raw, err := hex.DecodeString(key) |
| 334 | if err != nil || len(raw) != 32 { |
| 335 | return nil, fmt.Errorf("tunnel_key in %s contains non-hex characters.\n Valid characters are 0-9 and a-f. Generate a fresh key with 'openssl rand -hex 32' and copy it carefully — no spaces, quotes, or extra newlines", path) |
| 336 | } |
| 337 | |
| 338 | useFronting := len(relayURLs) == 0 |
| 339 | scriptURLs := relayURLs |
| 340 | scriptAccounts := make([]string, len(relayURLs)) // direct relay_urls have no account labels |
| 341 | googleIP := "" |
| 342 | var sniHosts []string |
| 343 | |
| 344 | if useFronting { |
| 345 | googleHost := firstNonEmpty(f.GoogleHost, "216.239.38.120") |
| 346 | googlePort := 443 |
| 347 | googleIP = net.JoinHostPort(googleHost, strconv.Itoa(googlePort)) |
| 348 | sniHosts = parseSNIHosts(f.SNI) |
no test coverage detected