port_conflict.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. package service
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net"
  6. "strings"
  7. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  8. "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
  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/tuic"
  12. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  13. "gorm.io/gorm"
  14. )
  15. type transportBits uint8
  16. const (
  17. transportTCP transportBits = 1 << iota
  18. transportUDP
  19. )
  20. func inboundTransports(protocol model.Protocol, streamSettings, settings string) transportBits {
  21. // protocols that ignore streamSettings entirely.
  22. switch protocol {
  23. case model.Hysteria, model.WireGuard, model.AmneziaWG, model.TUIC:
  24. return transportUDP
  25. case model.MTProto:
  26. return transportTCP
  27. }
  28. var bits transportBits
  29. // peek at streamSettings.network to spot udp-based transports.
  30. // parse errors are non-fatal: missing or weird streamSettings just
  31. // keeps the default tcp bit below.
  32. network := ""
  33. if streamSettings != "" {
  34. var ss map[string]any
  35. if json.Unmarshal([]byte(streamSettings), &ss) == nil {
  36. if n, _ := ss["network"].(string); n != "" {
  37. network = n
  38. }
  39. }
  40. }
  41. switch network {
  42. case "kcp", "quic":
  43. bits |= transportUDP
  44. default:
  45. bits |= transportTCP
  46. }
  47. // a few protocols carry their L4 choice in settings instead of (or in
  48. // addition to) streamSettings: SS / Tunnel via a CSV field that wins
  49. // outright, Mixed via an additive udp boolean.
  50. if settings != "" {
  51. var st map[string]any
  52. if json.Unmarshal([]byte(settings), &st) == nil {
  53. switch protocol {
  54. case model.Shadowsocks, model.Tunnel:
  55. key := "network"
  56. if protocol == model.Tunnel {
  57. key = "allowedNetwork"
  58. }
  59. if n, ok := st[key].(string); ok && n != "" {
  60. bits = 0
  61. for part := range strings.SplitSeq(n, ",") {
  62. switch strings.TrimSpace(part) {
  63. case "tcp":
  64. bits |= transportTCP
  65. case "udp":
  66. bits |= transportUDP
  67. }
  68. }
  69. }
  70. case model.Mixed:
  71. // socks/http "mixed" inbound: settings.udp=true means it
  72. // also relays udp on the same port (socks5 udp associate).
  73. if udpOn, _ := st["udp"].(bool); udpOn {
  74. bits |= transportUDP
  75. }
  76. }
  77. }
  78. }
  79. // safety net: never return zero, even if every parse failed.
  80. if bits == 0 {
  81. bits = transportTCP
  82. }
  83. return bits
  84. }
  85. // bindAddr is a listen address plus sockopt.v6only. xray listens on "tcp"/"udp",
  86. // so Go opens every wildcard, 0.0.0.0 included, dual-stack unless v6only is set.
  87. type bindAddr struct {
  88. listen string
  89. v6only bool
  90. }
  91. var loopbackBind = bindAddr{listen: "127.0.0.1"}
  92. func inboundBindAddr(ib *model.Inbound) bindAddr {
  93. return bindAddr{listen: ib.Listen, v6only: streamV6Only(ib.StreamSettings)}
  94. }
  95. func streamV6Only(streamSettings string) bool {
  96. if !strings.Contains(streamSettings, "v6only") {
  97. return false
  98. }
  99. var stream struct {
  100. Sockopt struct {
  101. V6Only bool `json:"v6only"`
  102. } `json:"sockopt"`
  103. }
  104. _ = json.Unmarshal([]byte(streamSettings), &stream)
  105. return stream.Sockopt.V6Only
  106. }
  107. func listenOverlaps(a, b bindAddr) bool {
  108. if a.listen == b.listen {
  109. return true
  110. }
  111. familiesA, wildcardA, okA := bindFamilies(a)
  112. familiesB, wildcardB, okB := bindFamilies(b)
  113. if !okA || !okB {
  114. return wildcardA || wildcardB
  115. }
  116. return (wildcardA || wildcardB) && familiesA&familiesB != 0
  117. }
  118. type addrFamily uint8
  119. const (
  120. familyIPv4 addrFamily = 1 << iota
  121. familyIPv6
  122. )
  123. // bindFamilies reports the address families a listen claims; ok is false for a
  124. // listen that is not an IP, such as a unix socket path.
  125. func bindFamilies(a bindAddr) (families addrFamily, wildcard, ok bool) {
  126. if isAnyListen(a.listen) {
  127. if a.v6only {
  128. return familyIPv6, true, true
  129. }
  130. return familyIPv4 | familyIPv6, true, true
  131. }
  132. ip := net.ParseIP(a.listen)
  133. if ip == nil {
  134. return 0, false, false
  135. }
  136. if ip.To4() != nil {
  137. return familyIPv4, false, true
  138. }
  139. return familyIPv6, false, true
  140. }
  141. func isAnyListen(s string) bool {
  142. return s == "" || s == "0.0.0.0" || s == "::" || s == "::0"
  143. }
  144. type portConflictDetail struct {
  145. InboundID int
  146. Remark string
  147. Tag string
  148. Listen string
  149. Port int
  150. // Relay marks Port as an automatic loopback relay port, not a configured one.
  151. Relay bool
  152. // ForwardedBy is the peer whose port forward holds Port, when that is the
  153. // reason for the conflict.
  154. ForwardedBy string
  155. Transports transportBits
  156. }
  157. // String renders the detail as a single-line, user-facing summary.
  158. func (d *portConflictDetail) String() string {
  159. name := d.Remark
  160. if name == "" {
  161. name = d.Tag
  162. }
  163. if name == "" {
  164. name = fmt.Sprintf("#%d", d.InboundID)
  165. } else if d.InboundID > 0 {
  166. name = fmt.Sprintf("'%s' (#%d)", name, d.InboundID)
  167. } else {
  168. // reserved/system inbounds (e.g. the Xray API) have no DB id.
  169. name = fmt.Sprintf("'%s'", name)
  170. }
  171. listen := d.Listen
  172. if isAnyListen(listen) {
  173. listen = "*"
  174. }
  175. port := fmt.Sprintf("port %d", d.Port)
  176. if d.Relay {
  177. port = fmt.Sprintf("relay port %d", d.Port)
  178. }
  179. if d.ForwardedBy != "" {
  180. return fmt.Sprintf("%s (%s) already forwarded on inbound %s on %s by its client %s",
  181. port, transportTagSuffix(d.Transports), name, listen, d.ForwardedBy)
  182. }
  183. return fmt.Sprintf("%s (%s) already used by inbound %s on %s",
  184. port, transportTagSuffix(d.Transports), name, listen)
  185. }
  186. // defaultXrayAPIPort is the loopback port of the internal Xray API inbound
  187. // (tag "api") seeded into the config template. Used as a fallback when the
  188. // template can't be parsed.
  189. const defaultXrayAPIPort = 62789
  190. // reservedAPIPort returns the port of the internal Xray API inbound declared
  191. // in the config template, falling back to defaultXrayAPIPort.
  192. func reservedAPIPort() int {
  193. tmpl, err := (&SettingService{}).GetXrayConfigTemplate()
  194. if err != nil || tmpl == "" {
  195. return defaultXrayAPIPort
  196. }
  197. var parsed struct {
  198. Inbounds []struct {
  199. Port int `json:"port"`
  200. Tag string `json:"tag"`
  201. } `json:"inbounds"`
  202. }
  203. if json.Unmarshal([]byte(tmpl), &parsed) != nil {
  204. return defaultXrayAPIPort
  205. }
  206. for _, in := range parsed.Inbounds {
  207. if in.Tag == "api" && in.Port > 0 {
  208. return in.Port
  209. }
  210. }
  211. return defaultXrayAPIPort
  212. }
  213. // checkPortConflict reads outside any transaction; callers that must not race a
  214. // concurrent create use checkPortConflictTx inside their own transaction.
  215. func (s *InboundService) checkPortConflict(inbound *model.Inbound, ignoreId int) (*portConflictDetail, error) {
  216. return checkPortConflictTx(database.GetDB(), inbound, ignoreId)
  217. }
  218. func checkPortConflictTx(db *gorm.DB, inbound *model.Inbound, ignoreId int) (*portConflictDetail, error) {
  219. newBits := inboundTransports(inbound.Protocol, inbound.StreamSettings, inbound.Settings)
  220. // The internal Xray API inbound (tag "api", loopback TCP) isn't a DB row,
  221. // so a local user inbound reusing its port would leave Xray binding the
  222. // port twice (#5304). Nodes run their own Xray, so this only applies to
  223. // the local panel.
  224. if inbound.NodeID == nil && inbound.Port == reservedAPIPort() &&
  225. newBits&transportTCP != 0 && listenOverlaps(loopbackBind, inboundBindAddr(inbound)) {
  226. return &portConflictDetail{
  227. Tag: "api",
  228. Listen: "127.0.0.1",
  229. Port: inbound.Port,
  230. Transports: transportTCP,
  231. }, nil
  232. }
  233. // Egress SOCKS server holds loopback EgressPort when AWG outbounds are
  234. // active; conflict check prevents inbounds from colliding with it.
  235. if inbound.NodeID == nil && inbound.Port == amneziawgnet.EgressPort() &&
  236. newBits&transportTCP != 0 && listenOverlaps(loopbackBind, inboundBindAddr(inbound)) {
  237. return &portConflictDetail{
  238. Tag: "amneziawg-egress",
  239. Listen: "127.0.0.1",
  240. Port: inbound.Port,
  241. Transports: transportTCP,
  242. }, nil
  243. }
  244. // Every enabled local AmneziaWG inbound gets its own automatic Xray
  245. // SOCKS5 relay inbound (see injectAmneziawgnetSocks) on 127.0.0.1 at a
  246. // port derived purely from its id (amneziawgnet.SOCKSPortForInbound) --
  247. // like the internal Xray API inbound above, that relay inbound is not
  248. // itself a database row, so the ordinary DB-backed query below can never
  249. // see it. Without this check, an unrelated inbound saved onto that exact
  250. // port silently fails at the next Xray start, taking every other
  251. // protocol down with it, not just AmneziaWG.
  252. if inbound.NodeID == nil && listenOverlaps(loopbackBind, inboundBindAddr(inbound)) {
  253. conflict, err := checkAmneziawgnetSocksConflict(db, inbound, ignoreId, newBits)
  254. if err != nil {
  255. return nil, err
  256. }
  257. if conflict != nil {
  258. return conflict, nil
  259. }
  260. conflict, err = checkTuicSocksConflict(db, inbound, ignoreId, newBits)
  261. if err != nil {
  262. return nil, err
  263. }
  264. if conflict != nil {
  265. return conflict, nil
  266. }
  267. }
  268. // The reverse direction, only meaningful once the id is known -- AddInbound
  269. // runs it after Save. Only a local row owns a relay slot (#6537 review).
  270. if inbound.NodeID == nil && inbound.Protocol == model.AmneziaWG && ignoreId > 0 {
  271. if self := amneziawgnetSocksSelfConflict(inbound, ignoreId); self != "" {
  272. return nil, common.NewError(self)
  273. }
  274. conflict, err := checkAmneziawgnetSocksRelayCollision(db, ignoreId)
  275. if err != nil {
  276. return nil, err
  277. }
  278. if conflict != nil {
  279. return conflict, nil
  280. }
  281. conflict, err = checkAmneziawgnetSocksReverseConflict(db, ignoreId)
  282. if err != nil {
  283. return nil, err
  284. }
  285. if conflict != nil {
  286. return conflict, nil
  287. }
  288. }
  289. // A forwarded port is not a column and not a relay slot, so the query below
  290. // cannot see it either: the port is bound by the peer's forward listener.
  291. forwardedBy, err := amneziawgForwardedPortOwner(db, inbound, ignoreId)
  292. if err != nil {
  293. return nil, err
  294. }
  295. if forwardedBy != nil {
  296. forwardedBy.Transports = newBits
  297. return forwardedBy, nil
  298. }
  299. if inbound.NodeID == nil && inbound.Protocol == model.TUIC && ignoreId > 0 {
  300. if self := tuicSocksSelfConflict(inbound, ignoreId); self != "" {
  301. return nil, common.NewError(self)
  302. }
  303. conflict, err := checkTuicSocksRelayCollision(db, ignoreId)
  304. if err != nil {
  305. return nil, err
  306. }
  307. if conflict != nil {
  308. return conflict, nil
  309. }
  310. conflict, err = checkTuicSocksReverseConflict(db, ignoreId)
  311. if err != nil {
  312. return nil, err
  313. }
  314. if conflict != nil {
  315. return conflict, nil
  316. }
  317. }
  318. var candidates []*model.Inbound
  319. q := db.Model(model.Inbound{}).Where("port = ?", inbound.Port)
  320. if ignoreId > 0 {
  321. q = q.Where("id != ?", ignoreId)
  322. }
  323. if err := q.Find(&candidates).Error; err != nil {
  324. return nil, err
  325. }
  326. for _, c := range candidates {
  327. if !sameNode(c.NodeID, inbound.NodeID) {
  328. continue
  329. }
  330. if !listenOverlaps(inboundBindAddr(c), inboundBindAddr(inbound)) {
  331. continue
  332. }
  333. existingBits := inboundTransports(c.Protocol, c.StreamSettings, c.Settings)
  334. shared := existingBits & newBits
  335. if shared == 0 {
  336. continue
  337. }
  338. return &portConflictDetail{
  339. InboundID: c.Id,
  340. Remark: c.Remark,
  341. Tag: c.Tag,
  342. Listen: c.Listen,
  343. Port: c.Port,
  344. Transports: shared,
  345. }, nil
  346. }
  347. return nil, nil
  348. }
  349. // amneziawgForwardedPortOwner names the AmneziaWG row on the same host whose peer
  350. // forwards inbound's port -- a bind on every interface that only the AWG side checked.
  351. func amneziawgForwardedPortOwner(db *gorm.DB, inbound *model.Inbound, ignoreId int) (*portConflictDetail, error) {
  352. var rows []*model.Inbound
  353. q := db.Model(model.Inbound{}).Where("protocol = ?", model.AmneziaWG)
  354. if ignoreId > 0 {
  355. q = q.Where("id != ?", ignoreId)
  356. }
  357. if err := q.Find(&rows).Error; err != nil {
  358. return nil, err
  359. }
  360. for _, row := range rows {
  361. if !sameNode(row.NodeID, inbound.NodeID) {
  362. continue
  363. }
  364. instance, ok := amneziawg.InstanceFromInbound(row)
  365. if !ok {
  366. continue
  367. }
  368. email, forwards := amneziawgnet.ForwardedPortOwner(instance, inbound.Port)
  369. if !forwards {
  370. continue
  371. }
  372. return &portConflictDetail{
  373. InboundID: row.Id,
  374. Remark: row.Remark,
  375. Tag: row.Tag,
  376. // the forward binds :port on every interface, wherever the
  377. // candidate asked to listen.
  378. Listen: "",
  379. Port: inbound.Port,
  380. ForwardedBy: email,
  381. }, nil
  382. }
  383. return nil, nil
  384. }
  385. // checkAmneziawgnetSocksConflict: inbound's port vs the relay port every matching
  386. // local row reserves, emitted or not; db keeps it in the caller's transaction (#6225).
  387. func checkAmneziawgnetSocksConflict(db *gorm.DB, inbound *model.Inbound, ignoreId int, newBits transportBits) (*portConflictDetail, error) {
  388. // A disabled row still owns the slot its id derives: SetInboundEnable flips
  389. // the column with no port check, so enabling it later must not collide.
  390. var candidates []*model.Inbound
  391. q := db.Model(model.Inbound{}).Where("protocol = ? AND node_id IS NULL", model.AmneziaWG)
  392. if ignoreId > 0 {
  393. q = q.Where("id != ?", ignoreId)
  394. }
  395. if err := q.Find(&candidates).Error; err != nil {
  396. return nil, err
  397. }
  398. // Ownership does not depend on the peers: the relay appears when the first
  399. // client is added, and the client paths run no port check at all.
  400. for _, c := range candidates {
  401. if amneziawgnet.SOCKSPortForInbound(c.Id) != inbound.Port {
  402. continue
  403. }
  404. return &portConflictDetail{
  405. InboundID: c.Id,
  406. Remark: c.Remark,
  407. Tag: c.Tag,
  408. Listen: "127.0.0.1",
  409. Port: inbound.Port,
  410. Transports: newBits,
  411. }, nil
  412. }
  413. return nil, nil
  414. }
  415. // checkAmneziawgnetSocksRelayCollision reports whether id's derived relay port
  416. // is already claimed by another local AmneziaWG inbound, disabled rows included.
  417. func checkAmneziawgnetSocksRelayCollision(db *gorm.DB, id int) (*portConflictDetail, error) {
  418. relayPort := amneziawgnet.SOCKSPortForInbound(id)
  419. var candidates []*model.Inbound
  420. if err := db.Model(model.Inbound{}).
  421. Where("protocol = ? AND node_id IS NULL AND id != ?", model.AmneziaWG, id).
  422. Find(&candidates).Error; err != nil {
  423. return nil, err
  424. }
  425. for _, c := range candidates {
  426. if amneziawgnet.SOCKSPortForInbound(c.Id) != relayPort {
  427. continue
  428. }
  429. return &portConflictDetail{
  430. InboundID: c.Id,
  431. Remark: c.Remark,
  432. Tag: c.Tag,
  433. Listen: "127.0.0.1",
  434. Port: relayPort,
  435. Relay: true,
  436. Transports: transportTCP,
  437. }, nil
  438. }
  439. return nil, nil
  440. }
  441. // amneziawgnetSocksSelfConflict: a row's own WireGuard port vs the relay port its
  442. // own id derives -- all three checks below exclude that id, so nothing else does.
  443. func amneziawgnetSocksSelfConflict(inbound *model.Inbound, id int) string {
  444. if id <= 0 || inbound.NodeID != nil || !listenOverlaps(loopbackBind, inboundBindAddr(inbound)) {
  445. return ""
  446. }
  447. relayPort := amneziawgnet.SOCKSPortForInbound(id)
  448. if inbound.Port != relayPort {
  449. return ""
  450. }
  451. return fmt.Sprintf("WireGuard port %d is inbound #%d's own SOCKS5 relay port on 127.0.0.1; choose a different WireGuard port",
  452. relayPort, id)
  453. }
  454. // checkAmneziawgnetSocksReverseConflict mirrors checkAmneziawgnetSocksConflict:
  455. // does id's own derived relay port collide with some other inbound's port.
  456. func checkAmneziawgnetSocksReverseConflict(db *gorm.DB, id int) (*portConflictDetail, error) {
  457. relayPort := amneziawgnet.SOCKSPortForInbound(id)
  458. var candidates []*model.Inbound
  459. if err := db.Model(model.Inbound{}).
  460. Where("port = ? AND node_id IS NULL AND id != ?", relayPort, id).
  461. Find(&candidates).Error; err != nil {
  462. return nil, err
  463. }
  464. for _, c := range candidates {
  465. if !listenOverlaps(loopbackBind, inboundBindAddr(c)) {
  466. continue
  467. }
  468. return &portConflictDetail{
  469. InboundID: c.Id,
  470. Remark: c.Remark,
  471. Tag: c.Tag,
  472. Listen: c.Listen,
  473. Port: relayPort,
  474. Relay: true,
  475. Transports: transportTCP,
  476. }, nil
  477. }
  478. return nil, nil
  479. }
  480. func checkTuicSocksConflict(db *gorm.DB, inbound *model.Inbound, ignoreId int, newBits transportBits) (*portConflictDetail, error) {
  481. var candidates []*model.Inbound
  482. q := db.Model(model.Inbound{}).Where("protocol = ? AND node_id IS NULL", model.TUIC)
  483. if ignoreId > 0 {
  484. q = q.Where("id != ?", ignoreId)
  485. }
  486. if err := q.Find(&candidates).Error; err != nil {
  487. return nil, err
  488. }
  489. for _, c := range candidates {
  490. if _, ok := tuic.InstanceFromInbound(c); !ok {
  491. continue
  492. }
  493. if tuic.SOCKSPortForInbound(c.Id) != inbound.Port {
  494. continue
  495. }
  496. return &portConflictDetail{
  497. InboundID: c.Id,
  498. Remark: c.Remark,
  499. Tag: c.Tag,
  500. Listen: "127.0.0.1",
  501. Port: inbound.Port,
  502. Transports: newBits,
  503. }, nil
  504. }
  505. return nil, nil
  506. }
  507. func checkTuicSocksRelayCollision(db *gorm.DB, id int) (*portConflictDetail, error) {
  508. relayPort := tuic.SOCKSPortForInbound(id)
  509. var candidates []*model.Inbound
  510. if err := db.Model(model.Inbound{}).
  511. Where("protocol = ? AND node_id IS NULL AND id != ?", model.TUIC, id).
  512. Find(&candidates).Error; err != nil {
  513. return nil, err
  514. }
  515. for _, c := range candidates {
  516. if tuic.SOCKSPortForInbound(c.Id) != relayPort {
  517. continue
  518. }
  519. return &portConflictDetail{
  520. InboundID: c.Id,
  521. Remark: c.Remark,
  522. Tag: c.Tag,
  523. Listen: "127.0.0.1",
  524. Port: relayPort,
  525. Relay: true,
  526. Transports: transportTCP,
  527. }, nil
  528. }
  529. return nil, nil
  530. }
  531. func tuicSocksSelfConflict(inbound *model.Inbound, id int) string {
  532. if id <= 0 || inbound.NodeID != nil || !listenOverlaps(loopbackBind, inboundBindAddr(inbound)) {
  533. return ""
  534. }
  535. relayPort := tuic.SOCKSPortForInbound(id)
  536. if inbound.Port != relayPort {
  537. return ""
  538. }
  539. return fmt.Sprintf("TUIC port %d is inbound #%d's own SOCKS5 relay port on 127.0.0.1; choose a different TUIC port",
  540. relayPort, id)
  541. }
  542. func checkTuicSocksReverseConflict(db *gorm.DB, id int) (*portConflictDetail, error) {
  543. relayPort := tuic.SOCKSPortForInbound(id)
  544. var candidates []*model.Inbound
  545. if err := db.Model(model.Inbound{}).
  546. Where("port = ? AND node_id IS NULL AND id != ?", relayPort, id).
  547. Find(&candidates).Error; err != nil {
  548. return nil, err
  549. }
  550. for _, c := range candidates {
  551. if !listenOverlaps(loopbackBind, inboundBindAddr(c)) {
  552. continue
  553. }
  554. return &portConflictDetail{
  555. InboundID: c.Id,
  556. Remark: c.Remark,
  557. Tag: c.Tag,
  558. Listen: c.Listen,
  559. Port: relayPort,
  560. Relay: true,
  561. Transports: transportTCP,
  562. }, nil
  563. }
  564. return nil, nil
  565. }
  566. func sameNode(a, b *int) bool {
  567. if a == nil && b == nil {
  568. return true
  569. }
  570. if a == nil || b == nil {
  571. return false
  572. }
  573. return *a == *b
  574. }
  575. func baseInboundTag(port int) string {
  576. return fmt.Sprintf("in-%v", port)
  577. }
  578. func transportTagSuffix(b transportBits) string {
  579. switch b {
  580. case transportTCP:
  581. return "tcp"
  582. case transportUDP:
  583. return "udp"
  584. case transportTCP | transportUDP:
  585. return "tcpudp"
  586. }
  587. return "any"
  588. }
  589. // nodeTagPrefix scopes a tag to one remote node so the same listen+port
  590. // can live on the central panel and on a node without bumping the global
  591. // UNIQUE(inbounds.tag) constraint. nil → "" (local panel).
  592. func nodeTagPrefix(nodeID *int) string {
  593. if nodeID == nil {
  594. return ""
  595. }
  596. return fmt.Sprintf("n%d-", *nodeID)
  597. }
  598. func composeInboundTag(port int, nodeID *int, bits transportBits) string {
  599. return nodeTagPrefix(nodeID) + baseInboundTag(port) + "-" + transportTagSuffix(bits)
  600. }
  601. func isAutoGeneratedTag(tag string, port int, nodeID *int, bits transportBits) bool {
  602. base := composeInboundTag(port, nodeID, bits)
  603. if tag == base {
  604. return true
  605. }
  606. suffix, ok := strings.CutPrefix(tag, base+"-")
  607. if !ok || suffix == "" {
  608. return false
  609. }
  610. for _, r := range suffix {
  611. if r < '0' || r > '9' {
  612. return false
  613. }
  614. }
  615. return true
  616. }
  617. func (s *InboundService) generateInboundTag(inbound *model.Inbound, ignoreId int) (string, error) {
  618. bits := inboundTransports(inbound.Protocol, inbound.StreamSettings, inbound.Settings)
  619. candidate := composeInboundTag(inbound.Port, inbound.NodeID, bits)
  620. exists, err := s.tagExists(candidate, ignoreId)
  621. if err != nil {
  622. return "", err
  623. }
  624. if !exists {
  625. return candidate, nil
  626. }
  627. for i := 2; i < 100; i++ {
  628. c := fmt.Sprintf("%s-%d", candidate, i)
  629. exists, err = s.tagExists(c, ignoreId)
  630. if err != nil {
  631. return "", err
  632. }
  633. if !exists {
  634. return c, nil
  635. }
  636. }
  637. return "", common.NewError("could not pick a unique inbound tag for port:", inbound.Port)
  638. }
  639. func (s *InboundService) resolveInboundTag(inbound *model.Inbound, ignoreId int) (string, error) {
  640. if inbound.Tag != "" {
  641. taken, err := s.tagExists(inbound.Tag, ignoreId)
  642. if err != nil {
  643. return "", err
  644. }
  645. if !taken {
  646. return inbound.Tag, nil
  647. }
  648. }
  649. return s.generateInboundTag(inbound, ignoreId)
  650. }
  651. func (s *InboundService) tagExists(tag string, ignoreId int) (bool, error) {
  652. db := database.GetDB()
  653. q := db.Model(model.Inbound{}).Where("tag = ?", tag)
  654. if ignoreId > 0 {
  655. q = q.Where("id != ?", ignoreId)
  656. }
  657. var count int64
  658. if err := q.Count(&count).Error; err != nil {
  659. return false, err
  660. }
  661. return count > 0, nil
  662. }