package xray import ( "encoding/json" "fmt" "os" "strconv" "strings" "vpnclient/internal/config" ) // writeRuntimeConfig builds an Xray config.json for SOCKS+HTTP inbounds and one proxy outbound. func writeRuntimeConfig(path string, profile config.Profile) error { link, err := Parse(profile.Proxy) if err != nil { return err } socksHost, socksPort := "127.0.0.1", 1080 httpHost, httpPort := "127.0.0.1", 1081 if hp, ok := profile.SOCKSListenHostPort(); ok { h, p, err := splitHostPort(hp, 1080) if err == nil { socksHost, socksPort = h, p } } if hp, ok := profile.HTTPListenHostPort(); ok { h, p, err := splitHostPort(hp, 1081) if err == nil { httpHost, httpPort = h, p } } outbound, err := buildOutbound(link) if err != nil { return err } cfg := map[string]any{ "log": map[string]any{"loglevel": "warning"}, "inbounds": []any{ map[string]any{ "tag": "socks-in", "listen": socksHost, "port": socksPort, "protocol": "socks", "settings": map[string]any{"udp": true, "auth": "noauth"}, }, map[string]any{ "tag": "http-in", "listen": httpHost, "port": httpPort, "protocol": "http", }, }, "outbounds": []any{ outbound, map[string]any{"protocol": "freedom", "tag": "direct"}, map[string]any{"protocol": "blackhole", "tag": "block"}, }, } data, err := json.MarshalIndent(cfg, "", " ") if err != nil { return err } return os.WriteFile(path, append(data, '\n'), 0o600) } func buildOutbound(link Link) (map[string]any, error) { stream, err := buildStreamSettings(link) if err != nil { return nil, err } switch link.Protocol { case config.ProtocolVLESS: user := map[string]any{ "id": link.UUID, "encryption": firstNonEmpty(link.Security, "none"), } if link.Flow != "" { user["flow"] = link.Flow } settings := map[string]any{ "vnext": []any{ map[string]any{ "address": link.Address, "port": link.Port, "users": []any{user}, }, }, } if link.PacketEncoding != "" { settings["packetEncoding"] = link.PacketEncoding } return map[string]any{ "tag": "proxy", "protocol": "vless", "settings": settings, "streamSettings": stream, }, nil case config.ProtocolVMess: user := map[string]any{ "id": link.UUID, "alterId": link.AlterID, "security": firstNonEmpty(link.Security, "auto"), } return map[string]any{ "tag": "proxy", "protocol": "vmess", "settings": map[string]any{ "vnext": []any{ map[string]any{ "address": link.Address, "port": link.Port, "users": []any{user}, }, }, }, "streamSettings": stream, }, nil case config.ProtocolTrojan: return map[string]any{ "tag": "proxy", "protocol": "trojan", "settings": map[string]any{ "servers": []any{ map[string]any{ "address": link.Address, "port": link.Port, "password": link.UUID, }, }, }, "streamSettings": stream, }, nil default: return nil, fmt.Errorf("unsupported xray protocol %q", link.Protocol) } } func buildStreamSettings(link Link) (map[string]any, error) { network := strings.ToLower(firstNonEmpty(link.Network, "tcp")) stream := map[string]any{"network": network} switch network { case "ws", "websocket": stream["network"] = "ws" ws := map[string]any{} if link.Path != "" { ws["path"] = link.Path } if link.Host != "" { ws["headers"] = map[string]any{"Host": link.Host} } stream["wsSettings"] = ws case "grpc", "gun": stream["network"] = "grpc" grpc := map[string]any{"serviceName": firstNonEmpty(link.ServiceName, link.Path)} if link.Mode != "" { grpc["multiMode"] = strings.EqualFold(link.Mode, "multi") } stream["grpcSettings"] = grpc case "h2", "http": stream["network"] = "h2" h2 := map[string]any{} if link.Path != "" { h2["path"] = link.Path } if link.Host != "" { h2["host"] = strings.Split(link.Host, ",") } stream["httpSettings"] = h2 case "httpupgrade": stream["network"] = "httpupgrade" hu := map[string]any{} if link.Path != "" { hu["path"] = link.Path } if link.Host != "" { hu["host"] = link.Host } stream["httpupgradeSettings"] = hu case "xhttp", "splithttp": stream["network"] = "xhttp" xh := map[string]any{} if link.Path != "" { xh["path"] = link.Path } if link.Host != "" { xh["host"] = link.Host } stream["xhttpSettings"] = xh default: // tcp stream["network"] = "tcp" if link.Type != "" && link.Type != "none" { stream["tcpSettings"] = map[string]any{ "header": map[string]any{"type": link.Type}, } } } sec := strings.ToLower(firstNonEmpty(link.TLS, "none")) switch sec { case "", "none", "0", "false": stream["security"] = "none" case "tls": stream["security"] = "tls" tls := map[string]any{ "serverName": firstNonEmpty(link.SNI, link.Host, link.Address), "allowInsecure": link.AllowInsecure, } if link.FP != "" { tls["fingerprint"] = link.FP } if link.ALPN != "" { tls["alpn"] = splitCSV(link.ALPN) } stream["tlsSettings"] = tls case "reality": stream["security"] = "reality" reality := map[string]any{ "serverName": firstNonEmpty(link.SNI, link.Host, link.Address), "fingerprint": firstNonEmpty(link.FP, "chrome"), "publicKey": link.PBK, "shortId": link.SID, "spiderX": firstNonEmpty(link.SPX, ""), } if link.PBK == "" { return nil, fmt.Errorf("reality: нужен pbk (publicKey)") } stream["realitySettings"] = reality default: stream["security"] = sec } return stream, nil } func splitHostPort(hp string, defPort int) (string, int, error) { host, portStr, err := netSplitHostPort(hp) if err != nil { return "", 0, err } if portStr == "" { return host, defPort, nil } p, err := strconv.Atoi(portStr) if err != nil { return "", 0, err } return host, p, nil } func netSplitHostPort(hp string) (host, port string, err error) { if strings.HasPrefix(hp, "[") { return splitBracket(hp) } if i := strings.LastIndex(hp, ":"); i >= 0 { return hp[:i], hp[i+1:], nil } return hp, "", nil } func splitBracket(hp string) (string, string, error) { end := strings.Index(hp, "]") if end < 0 { return "", "", fmt.Errorf("bad host:port") } host := hp[1:end] rest := hp[end+1:] if strings.HasPrefix(rest, ":") { return host, rest[1:], nil } return host, "", nil } func splitCSV(s string) []string { parts := strings.Split(s, ",") out := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { out = append(out, p) } } return out }