mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
fix(xray): migrate the dns outbound off its legacy nonIPQuery and blockTypes (#6519)
* fix(xray): migrate the dns outbound off its legacy nonIPQuery and blockTypes xray-core logs both keys as deprecated on every config load, and refuses them outright next to rules. The panel's own dns outbound card wrote them with defaults until it switched that card to rules, so a panel that ever had one keeps warning at every start, and the card no longer reads them back — saving that outbound from the current UI silently dropped the policy. The seeder converts them into the three rules the core's legacy builder produced, in its order, then drops the keys. * fix(xray): read a dns outbound's null keys and protocol id like the core Two details the seeder got wrong, both found reviewing the diff against the pinned loader. A JSON null is a present key with a nil value here but a nil pointer there, so `nonIPQuery: null` was rewritten into a reject policy the core never built, and `rules: null` hid the legacy pair that the core does still read — dropping the operator's policy on upgrade. And the core lowercases the protocol id before dispatching, so `"protocol": "DNS"` was never migrated and kept warning.
This commit is contained in:
+156
-1
@@ -1254,7 +1254,7 @@ func runSeeders(isUsersEmpty bool) error {
|
||||
}
|
||||
|
||||
if empty && isUsersEmpty {
|
||||
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix2", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "FreedomFinalRulesPrivateEgressBlock", "InboundRealityFinalmaskTcpStrip", "ApiTokensHash", "LegacyProxySettingsCleanup", "OutboundRemovedKeysFix", "FreedomDomainStrategyFix", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted", "ResetIpLimitNoFail2ban"}
|
||||
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix2", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "FreedomFinalRulesPrivateEgressBlock", "InboundRealityFinalmaskTcpStrip", "ApiTokensHash", "LegacyProxySettingsCleanup", "OutboundRemovedKeysFix", "FreedomDomainStrategyFix", "DNSOutboundLegacyKeysFix", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted", "ResetIpLimitNoFail2ban"}
|
||||
for _, name := range seeders {
|
||||
if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
|
||||
return err
|
||||
@@ -1371,6 +1371,12 @@ func runSeeders(isUsersEmpty bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(seedersHistory, "DNSOutboundLegacyKeysFix") {
|
||||
if err := migrateDNSOutboundLegacyKeys(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(seedersHistory, "NodeInboundsAdopted") {
|
||||
if err := seedNodeInboundsAdopted(); err != nil {
|
||||
return err
|
||||
@@ -1771,6 +1777,155 @@ var freedomDomainStrategies = map[string]bool{
|
||||
"forceipv6": true, "forceipv4v6": true, "forceipv6v4": true,
|
||||
}
|
||||
|
||||
// migrateDNSOutboundLegacyKeys rewrites stored dns outbounds once, because the
|
||||
// core logs nonIPQuery/blockTypes as deprecated on every config load.
|
||||
func migrateDNSOutboundLegacyKeys() error {
|
||||
var setting model.Setting
|
||||
err := db.Model(model.Setting{}).Where("key = ?", "xrayTemplateConfig").First(&setting).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return db.Create(&model.HistoryOfSeeders{SeederName: "DNSOutboundLegacyKeysFix"}).Error
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updated, changed, rErr := rewriteDNSOutboundLegacyKeys(setting.Value)
|
||||
if rErr != nil {
|
||||
log.Printf("DNSOutboundLegacyKeysFix: skip (invalid xrayTemplateConfig json): %v", rErr)
|
||||
return db.Create(&model.HistoryOfSeeders{SeederName: "DNSOutboundLegacyKeysFix"}).Error
|
||||
}
|
||||
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if changed {
|
||||
if err := tx.Model(&model.Setting{}).Where("key = ?", "xrayTemplateConfig").
|
||||
Update("value", updated).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Create(&model.HistoryOfSeeders{SeederName: "DNSOutboundLegacyKeysFix"}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// rewriteDNSOutboundLegacyKeys turns a dns outbound's legacy nonIPQuery and
|
||||
// blockTypes into rules, in the order the core's legacy builder used.
|
||||
func rewriteDNSOutboundLegacyKeys(raw string) (string, bool, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return raw, false, nil
|
||||
}
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
return raw, false, err
|
||||
}
|
||||
outbounds, ok := cfg["outbounds"].([]any)
|
||||
if !ok {
|
||||
return raw, false, nil
|
||||
}
|
||||
changed := false
|
||||
for _, ob := range outbounds {
|
||||
obj, ok := ob.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if proto, _ := obj["protocol"].(string); !strings.EqualFold(proto, "dns") {
|
||||
continue
|
||||
}
|
||||
settings, _ := obj["settings"].(map[string]any)
|
||||
if settings == nil {
|
||||
continue
|
||||
}
|
||||
nonIPQuery, hasMode := settings["nonIPQuery"]
|
||||
blockTypes, hasTypes := settings["blockTypes"]
|
||||
// JSON null is absent to the core, which decides on nil pointers.
|
||||
hasMode = hasMode && nonIPQuery != nil
|
||||
hasTypes = hasTypes && blockTypes != nil
|
||||
if !hasMode && !hasTypes {
|
||||
continue
|
||||
}
|
||||
// The core refuses legacy keys next to real rules, so existing rules win.
|
||||
if rules, hasRules := settings["rules"]; !hasRules || rules == nil {
|
||||
settings["rules"] = legacyDNSOutboundRules(dnsNonIPQueryMode(nonIPQuery), legacyDNSBlockTypes(blockTypes))
|
||||
}
|
||||
delete(settings, "nonIPQuery")
|
||||
delete(settings, "blockTypes")
|
||||
changed = true
|
||||
}
|
||||
if !changed {
|
||||
return raw, false, nil
|
||||
}
|
||||
out, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return raw, false, err
|
||||
}
|
||||
return string(out), true, nil
|
||||
}
|
||||
|
||||
// dnsNonIPQueryMode reports the mode the core resolved: everything but drop and
|
||||
// skip meant reject, and any other value never loaded in the first place.
|
||||
func dnsNonIPQueryMode(value any) string {
|
||||
mode, _ := value.(string)
|
||||
mode = strings.ToLower(strings.TrimSpace(mode))
|
||||
if mode == "drop" || mode == "skip" {
|
||||
return mode
|
||||
}
|
||||
return "reject"
|
||||
}
|
||||
|
||||
// legacyDNSBlockTypes accepts every shape the old card could save: a list, a
|
||||
// bare number, or a comma-separated string, minus the qTypes the core rejects.
|
||||
func legacyDNSBlockTypes(value any) []int {
|
||||
items, ok := value.([]any)
|
||||
if !ok && value != nil {
|
||||
items = []any{value}
|
||||
}
|
||||
var out []int
|
||||
for _, item := range items {
|
||||
for _, part := range strings.Split(fmt.Sprint(item), ",") {
|
||||
qType, err := strconv.Atoi(strings.TrimSpace(part))
|
||||
if err != nil || qType < 0 || qType > 65535 {
|
||||
continue
|
||||
}
|
||||
out = append(out, qType)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// legacyDNSOutboundRules mirrors the core's own legacy dns policy: the blocked
|
||||
// qTypes, then the hijack, then the mode's answer for everything else.
|
||||
func legacyDNSOutboundRules(mode string, blockTypes []int) []any {
|
||||
rules := make([]any, 0, 3)
|
||||
if len(blockTypes) > 0 {
|
||||
rule := map[string]any{"action": "drop", "qType": dnsQTypeValue(blockTypes)}
|
||||
if mode == "reject" {
|
||||
rule["action"] = "return"
|
||||
rule["rCode"] = 5
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
rules = append(rules, map[string]any{"action": "hijack", "qType": "1,28"})
|
||||
fallback := map[string]any{"action": "direct"}
|
||||
switch mode {
|
||||
case "reject":
|
||||
fallback["action"] = "return"
|
||||
fallback["rCode"] = 5
|
||||
case "drop":
|
||||
fallback["action"] = "drop"
|
||||
}
|
||||
return append(rules, fallback)
|
||||
}
|
||||
|
||||
// dnsQTypeValue keeps a lone qType a number, the way the core marshals one.
|
||||
func dnsQTypeValue(blockTypes []int) any {
|
||||
if len(blockTypes) == 1 {
|
||||
return blockTypes[0]
|
||||
}
|
||||
parts := make([]string, 0, len(blockTypes))
|
||||
for _, qType := range blockTypes {
|
||||
parts = append(parts, strconv.Itoa(qType))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func normalizeSettingPaths() error {
|
||||
pathKeys := []string{"webBasePath", "subPath", "subJsonPath", "subClashPath"}
|
||||
var rows []model.Setting
|
||||
|
||||
Reference in New Issue
Block a user