287 lines
6.5 KiB
Go
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
|
|
}
|