Files
SingBox-Gopanel/internal/protocol/singbox.go
CN-JS-HuiBai cafab67dcc
Some checks failed
build / build (api, amd64, linux) (push) Failing after -51s
build / build (api, arm64, linux) (push) Failing after -51s
build / build (api.exe, amd64, windows) (push) Failing after -51s
持续集成修复订阅错误的问题
2026-04-18 00:08:19 +08:00

388 lines
9.3 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
}
if multiplex := buildSingBoxMultiplex(conf.Multiplex); multiplex != nil {
outbound["multiplex"] = multiplex
}
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
}
if multiplex := buildSingBoxMultiplex(conf.Multiplex); multiplex != nil {
outbound["multiplex"] = multiplex
}
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 {
utlsConfig := buildSingBoxUTLS(conf.UTLS)
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
}
}
if utlsConfig != nil {
tls["utls"] = utlsConfig
}
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"]),
}
}
if utlsConfig != nil {
tls["utls"] = utlsConfig
}
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 "h2":
transport := map[string]any{
"type": "http",
}
if settings != nil {
if path := singBoxString(settings["path"]); path != "" {
transport["path"] = path
}
if host := singBoxHostValue(settings["host"]); len(host) > 0 {
transport["host"] = host
}
}
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
case "quic":
return map[string]any{
"type": "quic",
}
default:
return nil
}
}
func buildSingBoxUTLS(value any) map[string]any {
settings, ok := value.(map[string]any)
if !ok || !singBoxBool(settings["enabled"]) {
return nil
}
utls := map[string]any{
"enabled": true,
}
if fingerprint := singBoxString(settings["fingerprint"]); fingerprint != "" {
utls["fingerprint"] = fingerprint
}
return utls
}
func buildSingBoxMultiplex(value any) map[string]any {
settings, ok := value.(map[string]any)
if !ok || !singBoxBool(settings["enabled"]) {
return nil
}
multiplex := map[string]any{
"enabled": true,
}
if protocol := singBoxString(settings["protocol"]); protocol != "" {
multiplex["protocol"] = protocol
}
if maxConnections := singBoxInt(settings["max_connections"]); maxConnections > 0 {
multiplex["max_connections"] = maxConnections
}
if minStreams := singBoxInt(settings["min_streams"]); minStreams > 0 {
multiplex["min_streams"] = minStreams
}
if maxStreams := singBoxInt(settings["max_streams"]); maxStreams > 0 {
multiplex["max_streams"] = maxStreams
}
if singBoxBool(settings["padding"]) {
multiplex["padding"] = true
}
if brutal, ok := settings["brutal"].(map[string]any); ok && singBoxBool(brutal["enabled"]) {
multiplex["brutal"] = map[string]any{
"enabled": true,
"up_mbps": singBoxInt(brutal["up_mbps"]),
"down_mbps": singBoxInt(brutal["down_mbps"]),
}
}
return multiplex
}
func singBoxHostValue(value any) []string {
switch typed := value.(type) {
case string:
if typed == "" {
return nil
}
return []string{typed}
case []string:
return append([]string{}, typed...)
case []any:
result := make([]string, 0, len(typed))
for _, item := range typed {
if text := singBoxString(item); text != "" {
result = append(result, text)
}
}
return result
default:
return nil
}
}
func singBoxString(value any) string {
switch typed := value.(type) {
case string:
return typed
case nil:
return ""
default:
return toString(value)
}
}
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
}
}