1
0

inbound_migration.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. package service
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  11. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  12. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  13. "gorm.io/gorm"
  14. )
  15. func (s *InboundService) MigrationRemoveOrphanedTraffics() {
  16. db := database.GetDB()
  17. query := fmt.Sprintf(
  18. "DELETE FROM client_traffics WHERE email NOT IN (SELECT email FROM clients) AND email NOT IN (SELECT %s %s)",
  19. database.JSONFieldText("client.value", "email"),
  20. database.JSONClientsFromInbound(),
  21. )
  22. result := db.Exec(query)
  23. if result.Error != nil {
  24. logger.Warning("MigrationRemoveOrphanedTraffics failed:", result.Error)
  25. return
  26. }
  27. if result.RowsAffected > 0 {
  28. logger.Infof("MigrationRemoveOrphanedTraffics: removed %d orphaned client_traffics row(s)", result.RowsAffected)
  29. }
  30. }
  31. func (s *InboundService) MigrationRequirements() {
  32. db := database.GetDB()
  33. tx := db.Begin()
  34. var err error
  35. defer func() {
  36. if err == nil {
  37. tx.Commit()
  38. if !database.IsPostgres() {
  39. if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil {
  40. logger.Warningf("VACUUM failed: %v", dbErr)
  41. }
  42. }
  43. } else {
  44. tx.Rollback()
  45. }
  46. }()
  47. if tx.Migrator().HasColumn(&model.Inbound{}, "all_time") {
  48. if err = tx.Migrator().DropColumn(&model.Inbound{}, "all_time"); err != nil {
  49. return
  50. }
  51. }
  52. if tx.Migrator().HasColumn(&xray.ClientTraffic{}, "all_time") {
  53. if err = tx.Migrator().DropColumn(&xray.ClientTraffic{}, "all_time"); err != nil {
  54. return
  55. }
  56. }
  57. if err = normalizeInboundShareAddressColumns(tx); err != nil {
  58. return
  59. }
  60. // Normalize "enable" columns to boolean on Postgres. Legacy SQLite data
  61. // (0/1 integers), partial migrations, or mixed write paths (public API
  62. // inbound updates that flow through UpdateClientStat + client syncs, plus
  63. // node traffic merge deltas) can leave the column as integer or with mixed
  64. // interpretation. This (combined with the dialect-aware
  65. // ClientTrafficEnableMergeExpr) prevents type problems in the node traffic
  66. // sync merge (SetRemoteTraffic) and makes the sync robust even when
  67. // inbounds are updated via the public API (incl. ones carrying
  68. // externalProxy in streamSettings). The same expression is also safe on
  69. // SQLite (no PG :: casts).
  70. if database.IsPostgres() {
  71. // Use DO block so it is idempotent and doesn't fail if already boolean.
  72. normalizeBool := func(table, col string) {
  73. tx.Exec(fmt.Sprintf(`
  74. DO $$
  75. BEGIN
  76. IF EXISTS (
  77. SELECT 1 FROM information_schema.columns
  78. WHERE table_name = '%s' AND column_name = '%s'
  79. AND data_type <> 'boolean'
  80. ) THEN
  81. ALTER TABLE %s ALTER COLUMN %s
  82. TYPE boolean USING (CASE WHEN %s::text IN ('1','true','t','yes') THEN true ELSE false END);
  83. END IF;
  84. END $$;`, table, col, table, col, col))
  85. }
  86. normalizeBool("inbounds", "enable")
  87. normalizeBool("client_traffics", "enable")
  88. normalizeBool("nodes", "enable")
  89. normalizeBool("clients", "enable")
  90. normalizeBool("api_tokens", "enabled")
  91. normalizeBool("outbound_subscriptions", "enabled")
  92. }
  93. // Fix inbounds based problems
  94. var inbounds []*model.Inbound
  95. err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan", "shadowsocks", "hysteria"}).Find(&inbounds).Error
  96. if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
  97. return
  98. }
  99. for inbound_index := range inbounds {
  100. settings := map[string]any{}
  101. _ = json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
  102. if raw, exists := settings["clients"]; exists && raw == nil {
  103. settings["clients"] = []any{}
  104. }
  105. clients, ok := settings["clients"].([]any)
  106. if ok {
  107. // Fix Client configuration problems
  108. newClients := make([]any, 0, len(clients))
  109. hasVisionFlow := false
  110. for client_index := range clients {
  111. c := clients[client_index].(map[string]any)
  112. // Add email='' if it is not exists
  113. if _, ok := c["email"]; !ok {
  114. c["email"] = ""
  115. }
  116. // Convert string tgId to int64
  117. if _, ok := c["tgId"]; ok {
  118. tgId := c["tgId"]
  119. if tgIdStr, ok2 := tgId.(string); ok2 {
  120. tgIdInt64, err := strconv.ParseInt(strings.ReplaceAll(tgIdStr, " ", ""), 10, 64)
  121. if err == nil {
  122. c["tgId"] = tgIdInt64
  123. }
  124. }
  125. }
  126. // Remove "flow": "xtls-rprx-direct"
  127. if _, ok := c["flow"]; ok {
  128. if c["flow"] == "xtls-rprx-direct" {
  129. c["flow"] = ""
  130. }
  131. }
  132. if flow, _ := c["flow"].(string); flow == "xtls-rprx-vision" {
  133. hasVisionFlow = true
  134. }
  135. // Backfill created_at and updated_at
  136. if _, ok := c["created_at"]; !ok {
  137. c["created_at"] = time.Now().Unix() * 1000
  138. }
  139. c["updated_at"] = time.Now().Unix() * 1000
  140. newClients = append(newClients, any(c))
  141. }
  142. settings["clients"] = newClients
  143. // Drop orphaned testseed: VLESS-only field, only meaningful when at least
  144. // one client uses the exact xtls-rprx-vision flow. Older versions saved it
  145. // for any non-empty flow (including the UDP variant) or kept it after the
  146. // flow was cleared from the client modal — clean those up here.
  147. if inbounds[inbound_index].Protocol == model.VLESS && !hasVisionFlow {
  148. delete(settings, "testseed")
  149. }
  150. modifiedSettings, err := json.MarshalIndent(settings, "", " ")
  151. if err != nil {
  152. return
  153. }
  154. inbounds[inbound_index].Settings = string(modifiedSettings)
  155. }
  156. // Add client traffic row for all clients which has email
  157. modelClients, err := s.GetClients(inbounds[inbound_index])
  158. if err != nil {
  159. return
  160. }
  161. for _, modelClient := range modelClients {
  162. if len(modelClient.Email) > 0 {
  163. var count int64
  164. tx.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
  165. if count == 0 {
  166. _ = s.AddClientStat(tx, inbounds[inbound_index].Id, &modelClient)
  167. }
  168. }
  169. }
  170. // Heal clients table for installs where the one-shot seeder
  171. // skipped clients due to a tgId-string unmarshal error.
  172. if syncErr := s.clientService.SyncInbound(tx, inbounds[inbound_index].Id, modelClients); syncErr != nil {
  173. logger.Warning("MigrationRequirements sync clients failed:", syncErr)
  174. }
  175. }
  176. tx.Save(inbounds)
  177. // Remove orphaned traffics
  178. tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
  179. // Migrate old MultiDomain to External Proxy
  180. var externalProxy []struct {
  181. Id int
  182. Port int
  183. StreamSettings string // text column on both DBs; safer than []byte for cross-DB scan
  184. }
  185. externalProxyQuery := `select id, port, stream_settings
  186. from inbounds
  187. WHERE protocol in ('vmess','vless','trojan')
  188. AND json_extract(stream_settings, '$.security') = 'tls'
  189. AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`
  190. if database.IsPostgres() {
  191. externalProxyQuery = `select id, port, stream_settings
  192. from inbounds
  193. WHERE protocol in ('vmess','vless','trojan')
  194. AND NULLIF(stream_settings, '')::jsonb #>> '{security}' = 'tls'
  195. AND NULLIF(stream_settings, '')::jsonb #> '{tlsSettings,settings,domains}' IS NOT NULL`
  196. }
  197. err = tx.Raw(externalProxyQuery).Scan(&externalProxy).Error
  198. if err != nil || len(externalProxy) == 0 {
  199. return
  200. }
  201. for _, ep := range externalProxy {
  202. var reverses any
  203. var stream map[string]any
  204. _ = json.Unmarshal([]byte(ep.StreamSettings), &stream)
  205. if tlsSettings, ok := stream["tlsSettings"].(map[string]any); ok {
  206. if settings, ok := tlsSettings["settings"].(map[string]any); ok {
  207. if domains, ok := settings["domains"].([]any); ok {
  208. for _, domain := range domains {
  209. if domainMap, ok := domain.(map[string]any); ok {
  210. domainMap["forceTls"] = "same"
  211. domainMap["port"] = ep.Port
  212. domainMap["dest"] = domainMap["domain"].(string)
  213. delete(domainMap, "domain")
  214. }
  215. }
  216. }
  217. reverses = settings["domains"]
  218. delete(settings, "domains")
  219. }
  220. }
  221. stream["externalProxy"] = reverses
  222. newStream, _ := json.MarshalIndent(stream, " ", " ")
  223. tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream)
  224. }
  225. // Legacy tag cleanup for old auto-generated tags (e.g. "0.0.0.0:443-...").
  226. // Must be cross-DB: INSTR/REPLACE work on SQLite; Postgres needs position().
  227. tagCleanup := `UPDATE inbounds
  228. SET tag = REPLACE(tag, '0.0.0.0:', '')
  229. WHERE INSTR(tag, '0.0.0.0:') > 0;`
  230. if database.IsPostgres() {
  231. tagCleanup = `UPDATE inbounds
  232. SET tag = REPLACE(tag, '0.0.0.0:', '')
  233. WHERE position('0.0.0.0:' in tag) > 0;`
  234. }
  235. err = tx.Exec(tagCleanup).Error
  236. if err != nil {
  237. return
  238. }
  239. }
  240. func (s *InboundService) MigrateDB() {
  241. s.MigrationRequirements()
  242. s.MigrationRemoveOrphanedTraffics()
  243. s.MigrationRestoreVisionFlow()
  244. }
  245. // MigrationRestoreVisionFlow repairs VLESS inbounds whose clients lost their
  246. // XTLS Vision flow because the inbound was not flow-eligible when the client was
  247. // written (e.g. an XHTTP inbound whose vlessenc encryption was enabled only
  248. // later). For each now-eligible inbound it restores flow=xtls-rprx-vision on
  249. // clients whose intended flow (their flow_override on a sibling inbound) is
  250. // Vision. Idempotent: once a client carries the flow it is skipped, so this is a
  251. // no-op on healthy installs and on subsequent boots.
  252. func (s *InboundService) MigrationRestoreVisionFlow() {
  253. db := database.GetDB()
  254. var inbounds []*model.Inbound
  255. if err := db.Model(&model.Inbound{}).
  256. Where("protocol = ?", model.VLESS).
  257. Find(&inbounds).Error; err != nil {
  258. logger.Warning("MigrationRestoreVisionFlow: load inbounds failed:", err)
  259. return
  260. }
  261. for _, ib := range inbounds {
  262. restored, changed := s.restoreVisionFlowForEligibleInbound(nil, ib.Settings, ib.StreamSettings, ib.Protocol)
  263. if !changed {
  264. continue
  265. }
  266. clients, err := s.GetClients(&model.Inbound{Settings: restored})
  267. if err != nil {
  268. logger.Warning("MigrationRestoreVisionFlow: parse clients for inbound", ib.Id, "failed:", err)
  269. continue
  270. }
  271. err = db.Transaction(func(tx *gorm.DB) error {
  272. if e := tx.Model(&model.Inbound{}).Where("id = ?", ib.Id).Update("settings", restored).Error; e != nil {
  273. return e
  274. }
  275. return s.clientService.SyncInbound(tx, ib.Id, clients)
  276. })
  277. if err != nil {
  278. logger.Warning("MigrationRestoreVisionFlow: update inbound", ib.Id, "failed:", err)
  279. continue
  280. }
  281. logger.Info("MigrationRestoreVisionFlow: restored XTLS Vision flow on inbound", ib.Id)
  282. }
  283. }