mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
Merge 06e55727a6 into 756746dbca
This commit is contained in:
commit
47883f5dcd
3 changed files with 180 additions and 4 deletions
|
|
@ -16,6 +16,7 @@ import (
|
||||||
type SubClashService struct {
|
type SubClashService struct {
|
||||||
inboundService service.InboundService
|
inboundService service.InboundService
|
||||||
SubService *SubService
|
SubService *SubService
|
||||||
|
directRules []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClashConfig struct {
|
type ClashConfig struct {
|
||||||
|
|
@ -24,8 +25,11 @@ type ClashConfig struct {
|
||||||
Rules []string `yaml:"rules"`
|
Rules []string `yaml:"rules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSubClashService(subService *SubService) *SubClashService {
|
func NewSubClashService(subService *SubService, rules string) *SubClashService {
|
||||||
return &SubClashService{SubService: subService}
|
return &SubClashService{
|
||||||
|
SubService: subService,
|
||||||
|
directRules: xrayDirectRulesToClash(rules),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubClashService) GetClash(subId string, host string) (string, string, error) {
|
func (s *SubClashService) GetClash(subId string, host string) (string, string, error) {
|
||||||
|
|
@ -76,6 +80,10 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e
|
||||||
}
|
}
|
||||||
proxyNames = append(proxyNames, "DIRECT")
|
proxyNames = append(proxyNames, "DIRECT")
|
||||||
|
|
||||||
|
rules := make([]string, 0, len(s.directRules)+1)
|
||||||
|
rules = append(rules, s.directRules...)
|
||||||
|
rules = append(rules, "MATCH,PROXY")
|
||||||
|
|
||||||
config := ClashConfig{
|
config := ClashConfig{
|
||||||
Proxies: proxies,
|
Proxies: proxies,
|
||||||
ProxyGroups: []map[string]any{{
|
ProxyGroups: []map[string]any{{
|
||||||
|
|
@ -83,7 +91,7 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e
|
||||||
"type": "select",
|
"type": "select",
|
||||||
"proxies": proxyNames,
|
"proxies": proxyNames,
|
||||||
}},
|
}},
|
||||||
Rules: []string{"MATCH,PROXY"},
|
Rules: rules,
|
||||||
}
|
}
|
||||||
|
|
||||||
finalYAML, err := yaml.Marshal(config)
|
finalYAML, err := yaml.Marshal(config)
|
||||||
|
|
@ -127,6 +135,130 @@ func fallbackProxyName(proxy map[string]any, idx int) string {
|
||||||
return fmt.Sprintf("proxy-%d", idx+1)
|
return fmt.Sprintf("proxy-%d", idx+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type xrayDirectRule struct {
|
||||||
|
OutboundTag string `json:"outboundTag"`
|
||||||
|
Domain []string `json:"domain"`
|
||||||
|
IP []string `json:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func xrayDirectRulesToClash(raw string) []string {
|
||||||
|
if strings.TrimSpace(raw) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var xrayRules []xrayDirectRule
|
||||||
|
if err := json.Unmarshal([]byte(raw), &xrayRules); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules []string
|
||||||
|
for _, rule := range xrayRules {
|
||||||
|
if rule.OutboundTag != "direct" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, domain := range rule.Domain {
|
||||||
|
if clashRule := xrayDomainRuleToClash(domain); clashRule != "" {
|
||||||
|
rules = append(rules, clashRule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ip := range rule.IP {
|
||||||
|
rules = append(rules, xrayIPRulesToClash(ip)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dedupeClashRules(rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func xrayDomainRuleToClash(value string) string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(value, "geosite:"):
|
||||||
|
tag := strings.TrimSpace(strings.TrimPrefix(value, "geosite:"))
|
||||||
|
if tag == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("GEOSITE,%s,DIRECT", tag)
|
||||||
|
case strings.HasPrefix(value, "domain:"):
|
||||||
|
domain := strings.TrimSpace(strings.TrimPrefix(value, "domain:"))
|
||||||
|
if domain == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("DOMAIN-SUFFIX,%s,DIRECT", domain)
|
||||||
|
case strings.HasPrefix(value, "full:"):
|
||||||
|
domain := strings.TrimSpace(strings.TrimPrefix(value, "full:"))
|
||||||
|
if domain == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("DOMAIN,%s,DIRECT", domain)
|
||||||
|
case strings.HasPrefix(value, "keyword:"):
|
||||||
|
keyword := strings.TrimSpace(strings.TrimPrefix(value, "keyword:"))
|
||||||
|
if keyword == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("DOMAIN-KEYWORD,%s,DIRECT", keyword)
|
||||||
|
case strings.HasPrefix(value, "regexp:"):
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("DOMAIN-SUFFIX,%s,DIRECT", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func xrayIPRulesToClash(value string) []string {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(value, "geoip:") {
|
||||||
|
tag := strings.TrimSpace(strings.TrimPrefix(value, "geoip:"))
|
||||||
|
if tag == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.EqualFold(tag, "private") {
|
||||||
|
return []string{
|
||||||
|
"IP-CIDR,10.0.0.0/8,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR,172.16.0.0/12,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR,192.168.0.0/16,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR,127.0.0.0/8,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR,169.254.0.0/16,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR6,fc00::/7,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR6,fe80::/10,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR6,::1/128,DIRECT,no-resolve",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []string{fmt.Sprintf("GEOIP,%s,DIRECT", strings.ToUpper(tag))}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(value, "ext:") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleType := "IP-CIDR"
|
||||||
|
if strings.Contains(value, ":") {
|
||||||
|
ruleType = "IP-CIDR6"
|
||||||
|
}
|
||||||
|
return []string{fmt.Sprintf("%s,%s,DIRECT,no-resolve", ruleType, value)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dedupeClashRules(rules []string) []string {
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(rules))
|
||||||
|
deduped := make([]string, 0, len(rules))
|
||||||
|
for _, rule := range rules {
|
||||||
|
if _, ok := seen[rule]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[rule] = struct{}{}
|
||||||
|
deduped = append(deduped, rule)
|
||||||
|
}
|
||||||
|
return deduped
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SubClashService) getProxies(inbound *model.Inbound, client model.Client, host string) []map[string]any {
|
func (s *SubClashService) getProxies(inbound *model.Inbound, client model.Client, host string) []map[string]any {
|
||||||
stream := s.streamData(inbound.StreamSettings)
|
stream := s.streamData(inbound.StreamSettings)
|
||||||
// For node-managed inbounds the Clash proxy "server" must be the
|
// For node-managed inbounds the Clash proxy "server" must be the
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,50 @@ func TestEnsureUniqueProxyNames(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestXrayDirectRulesToClash(t *testing.T) {
|
||||||
|
raw := `[
|
||||||
|
{"type":"field","outboundTag":"direct","domain":["geosite:cn","domain:example.com","full:exact.example","keyword:bank"],"ip":["geoip:cn","geoip:private","1.2.3.0/24","2001:db8::/32"]},
|
||||||
|
{"type":"field","outboundTag":"proxy","domain":["geosite:google"],"ip":["geoip:us"]},
|
||||||
|
{"type":"field","outboundTag":"direct","domain":["geosite:cn"],"ip":["geoip:cn"]}
|
||||||
|
]`
|
||||||
|
|
||||||
|
got := xrayDirectRulesToClash(raw)
|
||||||
|
want := []string{
|
||||||
|
"GEOSITE,cn,DIRECT",
|
||||||
|
"DOMAIN-SUFFIX,example.com,DIRECT",
|
||||||
|
"DOMAIN,exact.example,DIRECT",
|
||||||
|
"DOMAIN-KEYWORD,bank,DIRECT",
|
||||||
|
"GEOIP,CN,DIRECT",
|
||||||
|
"IP-CIDR,10.0.0.0/8,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR,172.16.0.0/12,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR,192.168.0.0/16,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR,127.0.0.0/8,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR,169.254.0.0/16,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR6,fc00::/7,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR6,fe80::/10,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR6,::1/128,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR,1.2.3.0/24,DIRECT,no-resolve",
|
||||||
|
"IP-CIDR6,2001:db8::/32,DIRECT,no-resolve",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("xrayDirectRulesToClash() = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXrayDirectRulesToClashIgnoresInvalidRules(t *testing.T) {
|
||||||
|
cases := []string{
|
||||||
|
"",
|
||||||
|
"not-json",
|
||||||
|
`[{"outboundTag":"direct","domain":["regexp:.*"]},{"outboundTag":"blocked","ip":["geoip:cn"]}]`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, raw := range cases {
|
||||||
|
if got := xrayDirectRulesToClash(raw); len(got) != 0 {
|
||||||
|
t.Fatalf("xrayDirectRulesToClash(%q) = %#v, want empty", raw, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestApplyTransport_XHTTP(t *testing.T) {
|
func TestApplyTransport_XHTTP(t *testing.T) {
|
||||||
svc := &SubClashService{}
|
svc := &SubClashService{}
|
||||||
proxy := map[string]any{}
|
proxy := map[string]any{}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ func NewSUBController(
|
||||||
|
|
||||||
subService: sub,
|
subService: sub,
|
||||||
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
|
||||||
subClashService: NewSubClashService(sub),
|
subClashService: NewSubClashService(sub, jsonRules),
|
||||||
}
|
}
|
||||||
a.initRouter(g)
|
a.initRouter(g)
|
||||||
return a
|
return a
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue