| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- package service
- import (
- "cmp"
- "encoding/json"
- "fmt"
- "slices"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
- "github.com/mhsanaei/3x-ui/v3/internal/xray"
- )
- // bindConflict names two generated inbounds whose listens cannot coexist.
- type bindConflict struct {
- tagA string
- tagB string
- listen string
- listenB string
- port int
- shared transportBits
- }
- func (c bindConflict) String() string {
- return fmt.Sprintf("inbounds %q and %q both bind %s:%d (%s)",
- c.tagA, c.tagB, displayListen(c.listen), c.port, transportTagSuffix(c.shared))
- }
- // bindConflicts reports inbounds of newCfg whose sockets collide. A collision the
- // running config already serves is excused: it is demonstration, not a guess.
- func bindConflicts(newCfg, runningCfg *xray.Config) []bindConflict {
- conflicts := rawBindConflicts(newCfg)
- if len(conflicts) == 0 {
- return nil
- }
- excused := runningBindPairs(runningCfg)
- if len(excused) == 0 {
- return conflicts
- }
- kept := make([]bindConflict, 0, len(conflicts))
- for _, c := range conflicts {
- if _, ok := excused[bindPairKey(c)]; !ok {
- kept = append(kept, c)
- }
- }
- return kept
- }
- // rawBindConflicts groups inbounds by port first: only a port two inbounds share
- // is worth parsing transports for, which keeps the probe free on clean configs.
- func rawBindConflicts(cfg *xray.Config) []bindConflict {
- if cfg == nil {
- return nil
- }
- byPort := make(map[int][]*xray.InboundConfig, len(cfg.InboundConfigs))
- for i := range cfg.InboundConfigs {
- ib := &cfg.InboundConfigs[i]
- if ib.Port > 0 {
- byPort[ib.Port] = append(byPort[ib.Port], ib)
- }
- }
- var conflicts []bindConflict
- for port, group := range byPort {
- for i := range group {
- for j := i + 1; j < len(group); j++ {
- left, right := group[i], group[j]
- listenLeft, listenRight := configListen(left.Listen), configListen(right.Listen)
- if !listenOverlaps(listenLeft, listenRight) {
- continue
- }
- // One port carrying tcp on one inbound and udp on another is a
- // supported deployment (vless/tcp + hysteria2/udp), never a clash.
- shared := configTransports(left) & configTransports(right)
- if shared == 0 {
- continue
- }
- conflicts = append(conflicts, bindConflict{
- tagA: left.Tag,
- tagB: right.Tag,
- listen: listenLeft,
- listenB: listenRight,
- port: port,
- shared: shared,
- })
- }
- }
- }
- // Port grouping iterates a map: order the report so the first conflict the
- // caller shows is the same on every restart.
- slices.SortFunc(conflicts, func(a, b bindConflict) int {
- return cmp.Or(cmp.Compare(a.port, b.port), cmp.Compare(a.tagA, b.tagA), cmp.Compare(a.tagB, b.tagB))
- })
- return conflicts
- }
- // runningBindPairs is what the core is demonstrably binding right now, keyed the
- // way a new config's conflicts are, so only the identical one is excused.
- func runningBindPairs(cfg *xray.Config) map[string]struct{} {
- conflicts := rawBindConflicts(cfg)
- if len(conflicts) == 0 {
- return nil
- }
- pairs := make(map[string]struct{}, len(conflicts))
- for _, c := range conflicts {
- pairs[bindPairKey(c)] = struct{}{}
- }
- return pairs
- }
- // bindPairKey is the socket set an excuse was granted for: the two listens, the
- // port and the shared transports, never the tags, which the generator reorders.
- func bindPairKey(c bindConflict) string {
- left, right := c.listen, c.listenB
- if left > right {
- left, right = right, left
- }
- return fmt.Sprintf("%d\x00%s\x00%s\x00%d", c.port, left, right, c.shared)
- }
- func displayListen(listen string) string {
- if isAnyListen(listen) {
- return "*"
- }
- return listen
- }
- // configListen decodes a generated inbound's listen field: absent or empty means
- // every address, which is what the core does with an empty listen too.
- func configListen(raw json_util.RawMessage) string {
- var listen string
- if len(raw) == 0 || json.Unmarshal(raw, &listen) != nil {
- return ""
- }
- return listen
- }
- // configTransports reads a generated inbound's transports through the same rule
- // the save-time guards use, so the two cannot drift apart.
- func configTransports(ib *xray.InboundConfig) transportBits {
- // "socks" is the panel's own bridge shape (injectAmneziawgnetSocks and
- // friends): its udp flag lives in settings like a mixed inbound's.
- if ib.Protocol == "socks" {
- return inboundTransports(model.Mixed, string(ib.StreamSettings), string(ib.Settings))
- }
- return inboundTransports(model.Protocol(ib.Protocol), string(ib.StreamSettings), string(ib.Settings))
- }
|