mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-15 19:02:18 +03:00
* fix(sub): gate external Clash shadowsocks links like the inbound path clashProxyFromExternal returned as soon as it had built the ss proxy, so an ss:// link skipped applyTransport/applySecurity: a node whose tcp/http obfuscation Clash cannot express was emitted anyway (mihomo then opens a plain shadowsocks stream at a server that requires the header, and the node silently never connects), and security=tls was silently stripped. The inbound path runs both helpers for every protocol, so the two Clash importers disagreed about the same node. * fix(sub): count a dropped external link in the quota header The client email that feeds AggregateTrafficByEmails was recorded only when a proxy came out of the link, so a node Clash cannot represent also vanished from the Subscription-Userinfo header of every other node in the same subscription — the header reported another client's numbers as the whole subscription's. The inactive-link branch already counted an email without a proxy; make that unconditional so the header describes the subscribers, not the representable subset of their nodes. * docs(sub): describe clashProxyFromExternal by what it does, not by protocol The protocol list in the doc comment went stale the moment the shadowsocks branch stopped returning early, and it restated what the switch already says.
237 lines
5.5 KiB
Go
237 lines
5.5 KiB
Go
package sub
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// clashProxyFromExternal converts a pasted share link into a mihomo/Clash proxy
|
|
// entry, or nil when Clash can't represent it — the same gate getProxies runs.
|
|
func (s *SubClashService) clashProxyFromExternal(rawLink, name string) map[string]any {
|
|
ob := parseExternalLink(rawLink)
|
|
if ob == nil {
|
|
return nil
|
|
}
|
|
protocol, _ := ob["protocol"].(string)
|
|
settings, _ := ob["settings"].(map[string]any)
|
|
stream, _ := ob["streamSettings"].(map[string]any)
|
|
if stream == nil {
|
|
stream = map[string]any{}
|
|
}
|
|
if settings == nil {
|
|
return nil
|
|
}
|
|
|
|
proxy := map[string]any{"name": name, "udp": true}
|
|
|
|
switch protocol {
|
|
case "vmess":
|
|
vnext, _ := settings["vnext"].([]any)
|
|
if len(vnext) == 0 {
|
|
return nil
|
|
}
|
|
vn, _ := vnext[0].(map[string]any)
|
|
users, _ := vn["users"].([]any)
|
|
if vn == nil || len(users) == 0 {
|
|
return nil
|
|
}
|
|
user, _ := users[0].(map[string]any)
|
|
proxy["type"] = "vmess"
|
|
proxy["server"] = fmt.Sprint(vn["address"])
|
|
proxy["port"] = clashInt(vn["port"])
|
|
proxy["uuid"] = fmt.Sprint(user["id"])
|
|
proxy["alterId"] = 0
|
|
cipher, _ := user["security"].(string)
|
|
if cipher == "" {
|
|
cipher = "auto"
|
|
}
|
|
proxy["cipher"] = cipher
|
|
case "vless":
|
|
proxy["type"] = "vless"
|
|
proxy["server"] = fmt.Sprint(settings["address"])
|
|
proxy["port"] = clashInt(settings["port"])
|
|
proxy["uuid"] = fmt.Sprint(settings["id"])
|
|
if flow, _ := settings["flow"].(string); flow != "" {
|
|
proxy["flow"] = flow
|
|
}
|
|
case "trojan":
|
|
server := firstServer(settings)
|
|
if server == nil {
|
|
return nil
|
|
}
|
|
proxy["type"] = "trojan"
|
|
proxy["server"] = fmt.Sprint(server["address"])
|
|
proxy["port"] = clashInt(server["port"])
|
|
proxy["password"] = fmt.Sprint(server["password"])
|
|
case "shadowsocks":
|
|
server := firstServer(settings)
|
|
if server == nil {
|
|
server = settings
|
|
}
|
|
method, _ := server["method"].(string)
|
|
if method == "" {
|
|
return nil
|
|
}
|
|
proxy["type"] = "ss"
|
|
proxy["server"] = fmt.Sprint(server["address"])
|
|
proxy["port"] = clashInt(server["port"])
|
|
proxy["cipher"] = method
|
|
proxy["password"] = fmt.Sprint(server["password"])
|
|
// No early return: the shared transport/security tail is what drops an
|
|
// obfs node Clash cannot express, exactly as buildProxy does for inbounds.
|
|
case "hysteria":
|
|
return clashHysteriaFromExternal(settings, stream, name)
|
|
case "wireguard":
|
|
return clashWireguardFromExternal(settings, name)
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
network, _ := stream["network"].(string)
|
|
if !s.applyTransport(proxy, network, stream) {
|
|
return nil
|
|
}
|
|
security, _ := stream["security"].(string)
|
|
if !s.applySecurity(proxy, security, stream) {
|
|
return nil
|
|
}
|
|
return proxy
|
|
}
|
|
|
|
func firstServer(settings map[string]any) map[string]any {
|
|
servers, _ := settings["servers"].([]any)
|
|
if len(servers) == 0 {
|
|
return nil
|
|
}
|
|
server, _ := servers[0].(map[string]any)
|
|
return server
|
|
}
|
|
|
|
func clashHysteriaFromExternal(settings, stream map[string]any, name string) map[string]any {
|
|
hy, _ := stream["hysteriaSettings"].(map[string]any)
|
|
auth := ""
|
|
if hy != nil {
|
|
auth, _ = hy["auth"].(string)
|
|
}
|
|
if auth == "" {
|
|
return nil
|
|
}
|
|
proxy := map[string]any{
|
|
"name": name,
|
|
"type": "hysteria2",
|
|
"server": fmt.Sprint(settings["address"]),
|
|
"port": clashInt(settings["port"]),
|
|
"password": auth,
|
|
"udp": true,
|
|
}
|
|
if tls, _ := stream["tlsSettings"].(map[string]any); tls != nil {
|
|
if sni, _ := tls["serverName"].(string); sni != "" {
|
|
proxy["sni"] = sni
|
|
}
|
|
if alpn := clashStringList(tls["alpn"]); len(alpn) > 0 {
|
|
proxy["alpn"] = alpn
|
|
}
|
|
if fp, _ := tls["fingerprint"].(string); fp != "" {
|
|
proxy["client-fingerprint"] = fp
|
|
}
|
|
}
|
|
return proxy
|
|
}
|
|
|
|
func clashWireguardFromExternal(settings map[string]any, name string) map[string]any {
|
|
peers, _ := settings["peers"].([]any)
|
|
if len(peers) == 0 {
|
|
return nil
|
|
}
|
|
peer, _ := peers[0].(map[string]any)
|
|
if peer == nil {
|
|
return nil
|
|
}
|
|
host, port := splitClashHostPort(fmt.Sprint(peer["endpoint"]))
|
|
if host == "" || port == 0 {
|
|
return nil
|
|
}
|
|
proxy := map[string]any{
|
|
"name": name,
|
|
"type": "wireguard",
|
|
"server": host,
|
|
"port": port,
|
|
"udp": true,
|
|
}
|
|
if sk, _ := settings["secretKey"].(string); sk != "" {
|
|
proxy["private-key"] = sk
|
|
}
|
|
if pk, _ := peer["publicKey"].(string); pk != "" {
|
|
proxy["public-key"] = pk
|
|
}
|
|
if psk, _ := peer["preSharedKey"].(string); psk != "" {
|
|
proxy["pre-shared-key"] = psk
|
|
}
|
|
for _, addr := range clashStringList(settings["address"]) {
|
|
ip := stripCIDR(addr)
|
|
if strings.Contains(ip, ":") {
|
|
proxy["ipv6"] = ip
|
|
} else {
|
|
proxy["ip"] = ip
|
|
}
|
|
}
|
|
return proxy
|
|
}
|
|
|
|
func clashInt(v any) int {
|
|
switch x := v.(type) {
|
|
case int:
|
|
return x
|
|
case int64:
|
|
return int(x)
|
|
case float64:
|
|
return int(x)
|
|
case string:
|
|
n, _ := strconv.Atoi(x)
|
|
return n
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func clashStringList(v any) []string {
|
|
switch x := v.(type) {
|
|
case []any:
|
|
out := make([]string, 0, len(x))
|
|
for _, item := range x {
|
|
if s, ok := item.(string); ok && s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
case []string:
|
|
return x
|
|
case string:
|
|
if x == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(x, ",")
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func stripCIDR(addr string) string {
|
|
if before, _, ok := strings.Cut(addr, "/"); ok {
|
|
return before
|
|
}
|
|
return addr
|
|
}
|
|
|
|
func splitClashHostPort(endpoint string) (string, int) {
|
|
endpoint = strings.TrimSpace(endpoint)
|
|
i := strings.LastIndex(endpoint, ":")
|
|
if i < 0 {
|
|
return endpoint, 0
|
|
}
|
|
host := strings.Trim(endpoint[:i], "[]")
|
|
port, _ := strconv.Atoi(endpoint[i+1:])
|
|
return host, port
|
|
}
|