287 lines
6.8 KiB
Go
287 lines
6.8 KiB
Go
package protocol
|
|
|
|
import (
|
|
"encoding/json"
|
|
"xboard-go/internal/model"
|
|
"xboard-go/internal/service"
|
|
)
|
|
|
|
type SingBoxConfig struct {
|
|
Outbounds []map[string]interface{} `json:"outbounds"`
|
|
}
|
|
|
|
func GenerateSingBox(servers []model.Server, user model.User) (string, error) {
|
|
template := service.GetSubscribeTemplate("singbox")
|
|
if template == "" {
|
|
return generateSingBoxFallback(servers, user)
|
|
}
|
|
|
|
var config map[string]any
|
|
if err := json.Unmarshal([]byte(template), &config); err != nil {
|
|
return generateSingBoxFallback(servers, user)
|
|
}
|
|
|
|
outbounds := make([]any, 0, len(servers))
|
|
proxyTags := make([]string, 0, len(servers))
|
|
for _, s := range servers {
|
|
conf := service.BuildNodeConfig(&s)
|
|
password := service.GenerateServerPassword(&s, &user)
|
|
outbound := buildSingBoxOutbound(conf, password)
|
|
if outbound == nil {
|
|
continue
|
|
}
|
|
|
|
outbounds = append(outbounds, outbound)
|
|
proxyTags = append(proxyTags, conf.Name)
|
|
}
|
|
|
|
existingOutbounds := anySlice(config["outbounds"])
|
|
for index, item := range existingOutbounds {
|
|
outbound, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
outboundType, _ := outbound["type"].(string)
|
|
if outboundType != "selector" && outboundType != "urltest" {
|
|
continue
|
|
}
|
|
|
|
outbound["outbounds"] = appendUniqueAny(anySlice(outbound["outbounds"]), stringsToAny(proxyTags)...)
|
|
existingOutbounds[index] = outbound
|
|
}
|
|
|
|
config["outbounds"] = append(existingOutbounds, outbounds...)
|
|
|
|
data, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
func buildSingBoxOutbound(conf service.NodeServerConfig, password string) map[string]any {
|
|
outbound := map[string]any{
|
|
"tag": conf.Name,
|
|
"server": conf.RawHost,
|
|
"server_port": conf.Port,
|
|
}
|
|
|
|
switch conf.Protocol {
|
|
case "shadowsocks":
|
|
outbound["type"] = "shadowsocks"
|
|
outbound["method"] = conf.Cipher
|
|
outbound["password"] = password
|
|
case "vmess":
|
|
outbound["type"] = "vmess"
|
|
outbound["uuid"] = password
|
|
outbound["security"] = "auto"
|
|
case "vless":
|
|
outbound["type"] = "vless"
|
|
outbound["uuid"] = password
|
|
outbound["packet_encoding"] = "xudp"
|
|
if flow := singBoxString(conf.Flow); flow != "" {
|
|
outbound["flow"] = flow
|
|
}
|
|
if tls := buildSingBoxTLS(conf); tls != nil {
|
|
outbound["tls"] = tls
|
|
}
|
|
if transport := buildSingBoxTransport(conf); transport != nil {
|
|
outbound["transport"] = transport
|
|
}
|
|
case "trojan":
|
|
outbound["type"] = "trojan"
|
|
outbound["password"] = password
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
return outbound
|
|
}
|
|
|
|
func generateSingBoxFallback(servers []model.Server, user model.User) (string, error) {
|
|
outbounds := []map[string]interface{}{}
|
|
proxyTags := []string{}
|
|
|
|
for _, s := range servers {
|
|
conf := service.BuildNodeConfig(&s)
|
|
password := service.GenerateServerPassword(&s, &user)
|
|
outbound := map[string]interface{}{
|
|
"tag": conf.Name,
|
|
"server": conf.RawHost,
|
|
"server_port": conf.Port,
|
|
}
|
|
|
|
switch conf.Protocol {
|
|
case "shadowsocks":
|
|
outbound["type"] = "shadowsocks"
|
|
outbound["method"] = conf.Cipher
|
|
outbound["password"] = password
|
|
case "vmess":
|
|
outbound["type"] = "vmess"
|
|
outbound["uuid"] = password
|
|
outbound["security"] = "auto"
|
|
case "vless":
|
|
outbound["type"] = "vless"
|
|
outbound["uuid"] = password
|
|
outbound["packet_encoding"] = "xudp"
|
|
if flow := singBoxString(conf.Flow); flow != "" {
|
|
outbound["flow"] = flow
|
|
}
|
|
if tls := buildSingBoxTLS(conf); tls != nil {
|
|
outbound["tls"] = tls
|
|
}
|
|
if transport := buildSingBoxTransport(conf); transport != nil {
|
|
outbound["transport"] = transport
|
|
}
|
|
case "trojan":
|
|
outbound["type"] = "trojan"
|
|
outbound["password"] = password
|
|
default:
|
|
continue
|
|
}
|
|
|
|
outbounds = append(outbounds, outbound)
|
|
proxyTags = append(proxyTags, conf.Name)
|
|
}
|
|
|
|
selector := map[string]interface{}{
|
|
"type": "selector",
|
|
"tag": "Proxy",
|
|
"outbounds": proxyTags,
|
|
}
|
|
outbounds = append(outbounds, selector)
|
|
|
|
config := SingBoxConfig{
|
|
Outbounds: outbounds,
|
|
}
|
|
|
|
data, err := json.MarshalIndent(config, "", " ")
|
|
return string(data), err
|
|
}
|
|
|
|
func buildSingBoxTLS(conf service.NodeServerConfig) map[string]any {
|
|
switch singBoxInt(conf.Tls) {
|
|
case 1:
|
|
tls := map[string]any{
|
|
"enabled": true,
|
|
}
|
|
if tlsSettings, ok := conf.TlsSettings.(map[string]any); ok {
|
|
tls["insecure"] = singBoxBool(tlsSettings["allow_insecure"])
|
|
if serverName := singBoxString(tlsSettings["server_name"]); serverName != "" {
|
|
tls["server_name"] = serverName
|
|
}
|
|
}
|
|
return tls
|
|
case 2:
|
|
tls := map[string]any{
|
|
"enabled": true,
|
|
}
|
|
if tlsSettings, ok := conf.TlsSettings.(map[string]any); ok {
|
|
tls["insecure"] = singBoxBool(tlsSettings["allow_insecure"])
|
|
if serverName := singBoxString(tlsSettings["server_name"]); serverName != "" {
|
|
tls["server_name"] = serverName
|
|
}
|
|
tls["reality"] = map[string]any{
|
|
"enabled": true,
|
|
"public_key": singBoxString(tlsSettings["public_key"]),
|
|
"short_id": singBoxString(tlsSettings["short_id"]),
|
|
}
|
|
}
|
|
return tls
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func buildSingBoxTransport(conf service.NodeServerConfig) map[string]any {
|
|
network := singBoxString(conf.Network)
|
|
settings, _ := conf.NetworkSettings.(map[string]any)
|
|
switch network {
|
|
case "ws":
|
|
transport := map[string]any{
|
|
"type": "ws",
|
|
}
|
|
if settings != nil {
|
|
if path := singBoxString(settings["path"]); path != "" {
|
|
transport["path"] = path
|
|
}
|
|
if headers, ok := settings["headers"].(map[string]any); ok {
|
|
if host := singBoxString(headers["Host"]); host != "" {
|
|
transport["headers"] = map[string]any{"Host": host}
|
|
}
|
|
}
|
|
}
|
|
return transport
|
|
case "grpc":
|
|
transport := map[string]any{
|
|
"type": "grpc",
|
|
}
|
|
if settings != nil {
|
|
if serviceName := singBoxString(settings["serviceName"]); serviceName != "" {
|
|
transport["service_name"] = serviceName
|
|
}
|
|
}
|
|
return transport
|
|
case "httpupgrade":
|
|
transport := map[string]any{
|
|
"type": "httpupgrade",
|
|
}
|
|
if settings != nil {
|
|
if path := singBoxString(settings["path"]); path != "" {
|
|
transport["path"] = path
|
|
}
|
|
if host := singBoxString(settings["host"]); host != "" {
|
|
transport["host"] = host
|
|
}
|
|
if headers, ok := settings["headers"].(map[string]any); ok && len(headers) > 0 {
|
|
transport["headers"] = headers
|
|
}
|
|
}
|
|
return transport
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func singBoxString(value any) string {
|
|
switch typed := value.(type) {
|
|
case string:
|
|
return typed
|
|
case nil:
|
|
return ""
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func singBoxInt(value any) int {
|
|
switch typed := value.(type) {
|
|
case int:
|
|
return typed
|
|
case int64:
|
|
return int(typed)
|
|
case float64:
|
|
return int(typed)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func singBoxBool(value any) bool {
|
|
switch typed := value.(type) {
|
|
case bool:
|
|
return typed
|
|
case int:
|
|
return typed != 0
|
|
case int64:
|
|
return typed != 0
|
|
case float64:
|
|
return typed != 0
|
|
case string:
|
|
return typed == "1" || typed == "true"
|
|
default:
|
|
return false
|
|
}
|
|
}
|