Files

287 lines
6.5 KiB
Go

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
}