package hysteria2 import ( "fmt" "os" "strings" "vpnclient/internal/config" ) func writeRuntimeConfig(path string, profile config.Profile) error { ep, err := ParseEndpoint(profile.Proxy) if err != nil { return err } var insecureOverride *bool if profile.Hy2Insecure { v := true insecureOverride = &v } ep.ApplyOverrides(profile.Hy2SNI, profile.Hy2Obfs, profile.Hy2ObfsPassword, profile.Hy2PinSHA256, insecureOverride) socks := "127.0.0.1:1080" httpListen := "127.0.0.1:1081" if hp, ok := profile.SOCKSListenHostPort(); ok { socks = hp } if hp, ok := profile.HTTPListenHostPort(); ok { httpListen = hp } var b strings.Builder b.WriteString("# generated by Navis — do not edit\n") // Expanded form so congestion/obfs/tls can coexist with auth. b.WriteString("server: " + yamlQuote(ep.Server) + "\n") if ep.Auth != "" { b.WriteString("auth: " + yamlQuote(ep.Auth) + "\n") } if ep.SNI != "" || ep.Insecure || ep.PinSHA256 != "" { b.WriteString("tls:\n") if ep.SNI != "" { b.WriteString(" sni: " + yamlQuote(ep.SNI) + "\n") } if ep.Insecure { b.WriteString(" insecure: true\n") } if ep.PinSHA256 != "" { b.WriteString(" pinSHA256: " + yamlQuote(ep.PinSHA256) + "\n") } } obfs := strings.ToLower(strings.TrimSpace(ep.Obfs)) if obfs == "" { obfs = strings.ToLower(strings.TrimSpace(profile.Hy2Obfs)) } obfsPass := ep.ObfsPassword if obfsPass == "" { obfsPass = profile.Hy2ObfsPassword } switch obfs { case "salamander": if obfsPass == "" { return fmt.Errorf("hysteria2: salamander требует пароль (obfs-password)") } b.WriteString("obfs:\n") b.WriteString(" type: salamander\n") b.WriteString(" salamander:\n") b.WriteString(" password: " + yamlQuote(obfsPass) + "\n") case "gecko": if obfsPass == "" { return fmt.Errorf("hysteria2: gecko требует пароль (obfs-password)") } b.WriteString("obfs:\n") b.WriteString(" type: gecko\n") b.WriteString(" gecko:\n") b.WriteString(" password: " + yamlQuote(obfsPass) + "\n") } // Congestion: BBR (default) or reno. Bandwidth enables Brutal instead. up := ParseBandwidthMbps(profile.Hy2BandwidthUp) down := ParseBandwidthMbps(profile.Hy2BandwidthDown) cong := strings.ToLower(strings.TrimSpace(profile.Hy2Congestion)) if cong == "" { cong = "bbr" } bbrProfile := strings.ToLower(strings.TrimSpace(profile.Hy2BBRProfile)) if bbrProfile == "" { bbrProfile = "standard" } if up != "" || down != "" { b.WriteString("bandwidth:\n") if up != "" { b.WriteString(" up: " + yamlQuote(up) + "\n") } if down != "" { b.WriteString(" down: " + yamlQuote(down) + "\n") } } else { // No Brutal → use BBR/reno b.WriteString("congestion:\n") b.WriteString(" type: " + yamlQuote(cong) + "\n") if cong == "bbr" { b.WriteString(" bbrProfile: " + yamlQuote(bbrProfile) + "\n") } } if hop := strings.TrimSpace(profile.Hy2HopInterval); hop != "" { b.WriteString("transport:\n") b.WriteString(" type: udp\n") b.WriteString(" udp:\n") b.WriteString(" hopInterval: " + yamlQuote(hop) + "\n") } if profile.Hy2FastOpen { b.WriteString("fastOpen: true\n") } if profile.Hy2Lazy { b.WriteString("lazy: true\n") } // "Masquerade extras" on client = look like normal HTTPS via SNI already above; // optional ALPN hint kept for compatibility with some panels. if alpn := strings.TrimSpace(profile.Hy2ALPN); alpn != "" { // Official client uses TLS defaults; keep as comment for operators. b.WriteString("# alpn-hint: " + yamlQuote(alpn) + "\n") } b.WriteString("socks5:\n") b.WriteString(" listen: " + yamlQuote(socks) + "\n") b.WriteString("http:\n") b.WriteString(" listen: " + yamlQuote(httpListen) + "\n") return writeFile(path, b.String()) } func writeFile(path, content string) error { return os.WriteFile(path, []byte(content), 0o600) }