修复SS2022订阅下发错误的问题
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

This commit is contained in:
CN-JS-HuiBai
2026-04-17 23:36:46 +08:00
parent a3023fec39
commit 1a03d3d1fc
6 changed files with 226 additions and 121 deletions

View File

@@ -28,7 +28,8 @@ func GenerateClashWithTemplate(templateName string, servers []model.Server, user
proxyNames := make([]string, 0, len(servers))
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
proxy := buildClashProxy(conf, user)
password := service.GenerateServerPassword(&s, &user)
proxy := buildClashProxy(conf, password)
if proxy == nil {
continue
}
@@ -52,7 +53,7 @@ func GenerateClashWithTemplate(templateName string, servers []model.Server, user
return output, nil
}
func buildClashProxy(conf service.NodeServerConfig, user model.User) map[string]any {
func buildClashProxy(conf service.NodeServerConfig, password string) map[string]any {
switch conf.Protocol {
case "shadowsocks":
cipher, _ := conf.Cipher.(string)
@@ -62,7 +63,7 @@ func buildClashProxy(conf service.NodeServerConfig, user model.User) map[string]
"server": conf.RawHost,
"port": conf.ServerPort,
"cipher": cipher,
"password": user.UUID,
"password": password,
}
case "vmess":
return map[string]any{
@@ -70,7 +71,7 @@ func buildClashProxy(conf service.NodeServerConfig, user model.User) map[string]
"type": "vmess",
"server": conf.RawHost,
"port": conf.ServerPort,
"uuid": user.UUID,
"uuid": password,
"alterId": 0,
"cipher": "auto",
"udp": true,
@@ -81,7 +82,7 @@ func buildClashProxy(conf service.NodeServerConfig, user model.User) map[string]
"type": "trojan",
"server": conf.RawHost,
"port": conf.ServerPort,
"password": user.UUID,
"password": password,
"udp": true,
}
default:
@@ -161,7 +162,8 @@ func generateClashFallback(servers []model.Server, user model.User) string {
var proxyNames []string
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
proxy := buildClashProxy(conf, user)
password := service.GenerateServerPassword(&s, &user)
proxy := buildClashProxy(conf, password)
if proxy == nil {
continue
}
@@ -174,7 +176,7 @@ func generateClashFallback(servers []model.Server, user model.User) string {
conf.RawHost,
conf.ServerPort,
fmt.Sprint(proxy["cipher"]),
user.UUID,
fmt.Sprint(proxy["password"]),
))
case "vmess":
builder.WriteString(fmt.Sprintf(
@@ -182,7 +184,7 @@ func generateClashFallback(servers []model.Server, user model.User) string {
conf.Name,
conf.RawHost,
conf.ServerPort,
user.UUID,
fmt.Sprint(proxy["uuid"]),
))
case "trojan":
builder.WriteString(fmt.Sprintf(
@@ -190,7 +192,7 @@ func generateClashFallback(servers []model.Server, user model.User) string {
conf.Name,
conf.RawHost,
conf.ServerPort,
user.UUID,
fmt.Sprint(proxy["password"]),
))
}
proxyNames = append(proxyNames, fmt.Sprintf("\"%s\"", conf.Name))

View File

@@ -14,7 +14,8 @@ func GenerateGeneralLinks(servers []model.Server, user model.User) string {
var links []string
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
link := BuildLink(conf, user)
password := service.GenerateServerPassword(&s, &user)
link := BuildLink(conf, password)
if link != "" {
links = append(links, link)
}
@@ -22,36 +23,35 @@ func GenerateGeneralLinks(servers []model.Server, user model.User) string {
return strings.Join(links, "\n")
}
func BuildLink(c service.NodeServerConfig, user model.User) string {
func BuildLink(c service.NodeServerConfig, password string) string {
switch c.Protocol {
case "shadowsocks":
return buildShadowsocks(c, user)
return buildShadowsocks(c, password)
case "vmess":
return buildVmess(c, user)
return buildVmess(c, password)
case "vless":
return buildVless(c, user)
return buildVless(c, password)
case "trojan":
return buildTrojan(c, user)
return buildTrojan(c, password)
case "hysteria", "hysteria2":
return buildHysteria(c, user)
return buildHysteria(c, password)
case "tuic":
return buildTuic(c, user)
return buildTuic(c, password)
case "anytls":
return buildAnyTLS(c, user)
return buildAnyTLS(c, password)
case "socks":
return buildSocks(c, user)
return buildSocks(c, password)
case "http":
return buildHttp(c, user)
return buildHttp(c, password)
}
return ""
}
func buildShadowsocks(c service.NodeServerConfig, user model.User) string {
func buildShadowsocks(c service.NodeServerConfig, password string) string {
cipher := toString(c.Cipher)
password := user.UUID
userInfo := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", cipher, password)))
userInfo = strings.TrimRight(strings.ReplaceAll(strings.ReplaceAll(userInfo, "+", "-"), "/", "_"), "=")
link := fmt.Sprintf("ss://%s@%s:%d", userInfo, wrapIPv6(c.RawHost), c.ServerPort)
params := url.Values{}
if plugin := toString(c.Plugin); plugin != "" {
@@ -64,13 +64,13 @@ func buildShadowsocks(c service.NodeServerConfig, user model.User) string {
return link + "#" + url.PathEscape(c.Name)
}
func buildVmess(c service.NodeServerConfig, user model.User) string {
func buildVmess(c service.NodeServerConfig, password string) string {
m := map[string]any{
"v": "2",
"ps": c.Name,
"add": c.RawHost,
"port": fmt.Sprintf("%d", c.ServerPort),
"id": user.UUID,
"id": password,
"aid": "0",
"net": toString(c.Network),
"type": "none",
@@ -81,7 +81,7 @@ func buildVmess(c service.NodeServerConfig, user model.User) string {
if c.Tls != nil && toInt(c.Tls) > 0 {
m["tls"] = "tls"
}
if settings, ok := c.NetworkSettings.(map[string]any); ok {
switch toString(c.Network) {
case "ws":
@@ -96,7 +96,7 @@ func buildVmess(c service.NodeServerConfig, user model.User) string {
m["host"] = toString(settings["host"])
}
}
if tlsSettings, ok := c.TlsSettings.(map[string]any); ok {
m["sni"] = toString(tlsSettings["server_name"])
}
@@ -105,13 +105,13 @@ func buildVmess(c service.NodeServerConfig, user model.User) string {
return "vmess://" + base64.StdEncoding.EncodeToString(b)
}
func buildVless(c service.NodeServerConfig, user model.User) string {
func buildVless(c service.NodeServerConfig, password string) string {
params := url.Values{}
params.Set("encryption", "none")
if c.Flow != nil {
params.Set("flow", toString(c.Flow))
}
security := "none"
switch toInt(c.Tls) {
case 1:
@@ -132,7 +132,7 @@ func buildVless(c service.NodeServerConfig, user model.User) string {
}
params.Set("security", security)
params.Set("type", toString(c.Network))
if settings, ok := c.NetworkSettings.(map[string]any); ok {
switch toString(c.Network) {
case "ws":
@@ -145,10 +145,10 @@ func buildVless(c service.NodeServerConfig, user model.User) string {
}
}
return fmt.Sprintf("vless://%s@%s:%d?%s#%s", user.UUID, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name))
return fmt.Sprintf("vless://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name))
}
func buildTrojan(c service.NodeServerConfig, user model.User) string {
func buildTrojan(c service.NodeServerConfig, password string) string {
params := url.Values{}
security := "tls"
if toInt(c.Tls) == 2 {
@@ -164,7 +164,7 @@ func buildTrojan(c service.NodeServerConfig, user model.User) string {
}
}
params.Set("security", security)
if settings, ok := c.NetworkSettings.(map[string]any); ok {
switch toString(c.Network) {
case "ws":
@@ -176,51 +176,55 @@ func buildTrojan(c service.NodeServerConfig, user model.User) string {
}
}
return fmt.Sprintf("trojan://%s@%s:%d?%s#%s", user.UUID, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name))
return fmt.Sprintf("trojan://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name))
}
func buildHysteria(c service.NodeServerConfig, user model.User) string {
func buildHysteria(c service.NodeServerConfig, password string) string {
params := url.Values{}
params.Set("sni", toString(c.ServerName))
if c.Protocol == "hysteria2" || toInt(c.Version) == 2 {
if obfs := toString(c.Obfs); obfs != "" {
params.Set("obfs", "salamander")
params.Set("obfs-password", toString(c.ObfsPassword))
}
return fmt.Sprintf("hysteria2://%s@%s:%d?%s#%s", user.UUID, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name))
return fmt.Sprintf("hysteria2://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name))
}
params.Set("protocol", "udp")
params.Set("auth", user.UUID)
if c.UpMbps != nil { params.Set("upmbps", fmt.Sprintf("%v", c.UpMbps)) }
if c.DownMbps != nil { params.Set("downmbps", fmt.Sprintf("%v", c.DownMbps)) }
params.Set("auth", password)
if c.UpMbps != nil {
params.Set("upmbps", fmt.Sprintf("%v", c.UpMbps))
}
if c.DownMbps != nil {
params.Set("downmbps", fmt.Sprintf("%v", c.DownMbps))
}
return fmt.Sprintf("hysteria://%s:%d?%s#%s", wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name))
}
func buildTuic(c service.NodeServerConfig, user model.User) string {
func buildTuic(c service.NodeServerConfig, password string) string {
params := url.Values{}
params.Set("sni", toString(c.ServerName))
params.Set("congestion_control", toString(c.CongestionControl))
params.Set("udp-relay-mode", "native")
return fmt.Sprintf("tuic://%s:%s@%s:%d?%s#%s", user.UUID, user.UUID, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name))
return fmt.Sprintf("tuic://%s:%s@%s:%d?%s#%s", password, password, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name))
}
func buildAnyTLS(c service.NodeServerConfig, user model.User) string {
func buildAnyTLS(c service.NodeServerConfig, password string) string {
params := url.Values{}
params.Set("sni", toString(c.ServerName))
return fmt.Sprintf("anytls://%s@%s:%d?%s#%s", user.UUID, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name))
return fmt.Sprintf("anytls://%s@%s:%d?%s#%s", password, wrapIPv6(c.RawHost), c.ServerPort, params.Encode(), url.PathEscape(c.Name))
}
func buildSocks(c service.NodeServerConfig, user model.User) string {
auth := base64.StdEncoding.EncodeToString([]byte(user.UUID + ":" + user.UUID))
func buildSocks(c service.NodeServerConfig, password string) string {
auth := base64.StdEncoding.EncodeToString([]byte(password + ":" + password))
return fmt.Sprintf("socks://%s@%s:%d#%s", auth, wrapIPv6(c.RawHost), c.ServerPort, url.PathEscape(c.Name))
}
func buildHttp(c service.NodeServerConfig, user model.User) string {
auth := base64.StdEncoding.EncodeToString([]byte(user.UUID + ":" + user.UUID))
func buildHttp(c service.NodeServerConfig, password string) string {
auth := base64.StdEncoding.EncodeToString([]byte(password + ":" + password))
link := fmt.Sprintf("http://%s@%s:%d", auth, wrapIPv6(c.RawHost), c.ServerPort)
if toInt(c.Tls) > 0 {
params := url.Values{}
@@ -238,16 +242,23 @@ func wrapIPv6(host string) string {
}
func toString(v any) string {
if s, ok := v.(string); ok { return s }
if v == nil { return "" }
if s, ok := v.(string); ok {
return s
}
if v == nil {
return ""
}
return fmt.Sprintf("%v", v)
}
func toInt(v any) int {
switch typed := v.(type) {
case int: return typed
case int64: return int(typed)
case float64: return int(typed)
case int:
return typed
case int64:
return int(typed)
case float64:
return int(typed)
}
return 0
}

View File

@@ -25,7 +25,8 @@ func GenerateSingBox(servers []model.Server, user model.User) (string, error) {
proxyTags := make([]string, 0, len(servers))
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
outbound := buildSingBoxOutbound(conf, user)
password := service.GenerateServerPassword(&s, &user)
outbound := buildSingBoxOutbound(conf, password)
if outbound == nil {
continue
}
@@ -59,7 +60,7 @@ func GenerateSingBox(servers []model.Server, user model.User) (string, error) {
return string(data), nil
}
func buildSingBoxOutbound(conf service.NodeServerConfig, user model.User) map[string]any {
func buildSingBoxOutbound(conf service.NodeServerConfig, password string) map[string]any {
outbound := map[string]any{
"tag": conf.Name,
"server": conf.RawHost,
@@ -70,14 +71,14 @@ func buildSingBoxOutbound(conf service.NodeServerConfig, user model.User) map[st
case "shadowsocks":
outbound["type"] = "shadowsocks"
outbound["method"] = conf.Cipher
outbound["password"] = user.UUID
outbound["password"] = password
case "vmess":
outbound["type"] = "vmess"
outbound["uuid"] = user.UUID
outbound["uuid"] = password
outbound["security"] = "auto"
case "trojan":
outbound["type"] = "trojan"
outbound["password"] = user.UUID
outbound["password"] = password
default:
return nil
}
@@ -91,6 +92,7 @@ func generateSingBoxFallback(servers []model.Server, user model.User) (string, e
for _, s := range servers {
conf := service.BuildNodeConfig(&s)
password := service.GenerateServerPassword(&s, &user)
outbound := map[string]interface{}{
"tag": conf.Name,
"server": conf.RawHost,
@@ -101,14 +103,14 @@ func generateSingBoxFallback(servers []model.Server, user model.User) (string, e
case "shadowsocks":
outbound["type"] = "shadowsocks"
outbound["method"] = conf.Cipher
outbound["password"] = user.UUID
outbound["password"] = password
case "vmess":
outbound["type"] = "vmess"
outbound["uuid"] = user.UUID
outbound["uuid"] = password
outbound["security"] = "auto"
case "trojan":
outbound["type"] = "trojan"
outbound["password"] = user.UUID
outbound["password"] = password
default:
continue
}

View File

@@ -57,11 +57,15 @@ func LoadEmailConfig() EmailConfig {
func (cfg EmailConfig) DebugConfig() map[string]any {
return map[string]any{
"driver": "smtp",
"host": cfg.Host,
"port": cfg.Port,
"encryption": cfg.Encryption,
"username": cfg.Username,
"driver": "smtp",
"host": cfg.Host,
"port": cfg.Port,
"encryption": cfg.Encryption,
"username": cfg.Username,
"from": map[string]any{
"address": cfg.SenderAddress(),
"name": cfg.FromName,
},
"from_address": cfg.SenderAddress(),
"from_name": cfg.FromName,
}

View File

@@ -21,17 +21,35 @@ var serverTypeAliases = map[string]string{
}
var validServerTypes = map[string]struct{}{
"anytls": {},
"http": {},
"hysteria": {},
"mieru": {},
"naive": {},
"anytls": {},
"http": {},
"hysteria": {},
"mieru": {},
"naive": {},
"shadowsocks": {},
"socks": {},
"trojan": {},
"tuic": {},
"vless": {},
"vmess": {},
"socks": {},
"trojan": {},
"tuic": {},
"vless": {},
"vmess": {},
}
var shadowsocks2022CipherConfigs = map[string]struct {
serverKeySize int
userKeySize int
}{
"2022-blake3-aes-128-gcm": {
serverKeySize: 16,
userKeySize: 16,
},
"2022-blake3-aes-256-gcm": {
serverKeySize: 32,
userKeySize: 32,
},
"2022-blake3-chacha20-poly1305": {
serverKeySize: 32,
userKeySize: 32,
},
}
type NodeUser struct {
@@ -47,41 +65,41 @@ type NodeBaseConfig struct {
}
type NodeServerConfig struct {
Name string `json:"-"`
Protocol string `json:"protocol"`
RawHost string `json:"-"`
ListenIP string `json:"listen_ip"`
ServerPort int `json:"server_port"`
Network any `json:"network"`
NetworkSettings any `json:"networkSettings"`
Cipher any `json:"cipher,omitempty"`
Plugin any `json:"plugin,omitempty"`
PluginOpts any `json:"plugin_opts,omitempty"`
ServerKey any `json:"server_key,omitempty"`
Host any `json:"host,omitempty"`
ServerName any `json:"server_name,omitempty"`
Tls any `json:"tls,omitempty"`
TlsSettings any `json:"tls_settings,omitempty"`
Flow any `json:"flow,omitempty"`
Multiplex any `json:"multiplex,omitempty"`
UpMbps any `json:"up_mbps,omitempty"`
DownMbps any `json:"down_mbps,omitempty"`
Version any `json:"version,omitempty"`
Obfs any `json:"obfs,omitempty"`
ObfsPassword any `json:"obfs-password,omitempty"`
CongestionControl any `json:"congestion_control,omitempty"`
AuthTimeout any `json:"auth_timeout,omitempty"`
ZeroRTTHandshake any `json:"zero_rtt_handshake,omitempty"`
Heartbeat any `json:"heartbeat,omitempty"`
PaddingScheme any `json:"padding_scheme,omitempty"`
Transport any `json:"transport,omitempty"`
TrafficPattern any `json:"traffic_pattern,omitempty"`
Decryption any `json:"decryption,omitempty"`
Routes []model.ServerRoute `json:"routes,omitempty"`
CustomOutbounds any `json:"custom_outbounds,omitempty"`
CustomRoutes any `json:"custom_routes,omitempty"`
CertConfig any `json:"cert_config,omitempty"`
BaseConfig NodeBaseConfig `json:"base_config"`
Name string `json:"-"`
Protocol string `json:"protocol"`
RawHost string `json:"-"`
ListenIP string `json:"listen_ip"`
ServerPort int `json:"server_port"`
Network any `json:"network"`
NetworkSettings any `json:"networkSettings"`
Cipher any `json:"cipher,omitempty"`
Plugin any `json:"plugin,omitempty"`
PluginOpts any `json:"plugin_opts,omitempty"`
ServerKey any `json:"server_key,omitempty"`
Host any `json:"host,omitempty"`
ServerName any `json:"server_name,omitempty"`
Tls any `json:"tls,omitempty"`
TlsSettings any `json:"tls_settings,omitempty"`
Flow any `json:"flow,omitempty"`
Multiplex any `json:"multiplex,omitempty"`
UpMbps any `json:"up_mbps,omitempty"`
DownMbps any `json:"down_mbps,omitempty"`
Version any `json:"version,omitempty"`
Obfs any `json:"obfs,omitempty"`
ObfsPassword any `json:"obfs-password,omitempty"`
CongestionControl any `json:"congestion_control,omitempty"`
AuthTimeout any `json:"auth_timeout,omitempty"`
ZeroRTTHandshake any `json:"zero_rtt_handshake,omitempty"`
Heartbeat any `json:"heartbeat,omitempty"`
PaddingScheme any `json:"padding_scheme,omitempty"`
Transport any `json:"transport,omitempty"`
TrafficPattern any `json:"traffic_pattern,omitempty"`
Decryption any `json:"decryption,omitempty"`
Routes []model.ServerRoute `json:"routes,omitempty"`
CustomOutbounds any `json:"custom_outbounds,omitempty"`
CustomRoutes any `json:"custom_routes,omitempty"`
CertConfig any `json:"cert_config,omitempty"`
BaseConfig NodeBaseConfig `json:"base_config"`
}
func NormalizeServerType(serverType string) string {
@@ -165,6 +183,31 @@ func AvailableServersForUser(user *model.User) ([]model.Server, error) {
return filtered, nil
}
func GenerateServerPassword(node *model.Server, user *model.User) string {
if node == nil || user == nil {
return ""
}
if NormalizeServerType(node.Type) != "shadowsocks" {
return user.UUID
}
settings := parseObject(node.ProtocolSettings)
cipher := getMapString(settings, "cipher")
config, ok := shadowsocks2022CipherConfigs[cipher]
if !ok {
return user.UUID
}
createdAt := resolveServerCreatedAt(node)
serverKey := serverKey(createdAt, config.serverKeySize)
if serverKey == "" {
return user.UUID
}
return serverKey + ":" + uuidPrefixBase64(user.UUID, config.userKeySize)
}
func CurrentRate(server *model.Server) float64 {
if !server.RateTimeEnable {
return float64(server.Rate)
@@ -322,7 +365,10 @@ func serverKey(createdAt *time.Time, size int) string {
if createdAt == nil {
return ""
}
sum := md5.Sum([]byte(strconv.FormatInt(createdAt.Unix(), 10)))
// Match XBoard's Helper::getServerKey(created_at, size):
// base64_encode(substr(md5($timestamp), 0, size))
sum := md5.Sum([]byte(createdAt.Format("2006-01-02 15:04:05")))
hex := fmt.Sprintf("%x", sum)
if size > len(hex) {
size = len(hex)
@@ -330,6 +376,38 @@ func serverKey(createdAt *time.Time, size int) string {
return base64.StdEncoding.EncodeToString([]byte(hex[:size]))
}
func resolveServerCreatedAt(node *model.Server) *time.Time {
if node == nil {
return nil
}
if node.ParentID == nil || *node.ParentID <= 0 {
return node.CreatedAt
}
var parent struct {
CreatedAt *time.Time `gorm:"column:created_at"`
}
if err := database.DB.Model(&model.Server{}).
Select("created_at").
Where("id = ?", *node.ParentID).
First(&parent).Error; err == nil && parent.CreatedAt != nil {
return parent.CreatedAt
}
return node.CreatedAt
}
func uuidPrefixBase64(uuid string, size int) string {
if size <= 0 {
return ""
}
if size > len(uuid) {
size = len(uuid)
}
return base64.StdEncoding.EncodeToString([]byte(uuid[:size]))
}
func parseIntSlice(raw *string) []int {
if raw == nil || strings.TrimSpace(*raw) == "" {
return nil