port_conflict_forwarded_test.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. package service
  2. import (
  3. "strings"
  4. "testing"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. )
  8. // checkForwardedPortsConflict only ran from the AmneziaWG save path, so an
  9. // ordinary inbound could take a port a peer forwards on every interface.
  10. func TestAddInboundRefusesAPortAnAmneziaWGPeerForwards(t *testing.T) {
  11. const forwarded = 8443
  12. cases := []struct {
  13. name string
  14. port int
  15. wantErr bool
  16. }{
  17. {"the forwarded port", forwarded, true},
  18. {"a free port", forwarded + 1, false},
  19. }
  20. for _, tc := range cases {
  21. t.Run(tc.name, func(t *testing.T) {
  22. setupConflictDB(t)
  23. seedInboundConflict(t, "awg-forward", "0.0.0.0", 51820, model.AmneziaWG, ``,
  24. awgRelayWindowSettingsWithForward(t, "awg-forward", "8443"))
  25. _, _, err := (&InboundService{}).AddInbound(&model.Inbound{
  26. Tag: "user-inbound", Enable: true, Listen: "0.0.0.0", Port: tc.port,
  27. Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`, Settings: `{"clients":[]}`,
  28. })
  29. if !tc.wantErr {
  30. if err != nil {
  31. t.Fatalf("port %d is free; the create must be allowed: %v", tc.port, err)
  32. }
  33. return
  34. }
  35. if err == nil {
  36. t.Fatalf("port %d is forwarded by a peer of another inbound; the create must be refused", tc.port)
  37. }
  38. if !strings.Contains(err.Error(), "awg-forward@relay-window") {
  39. t.Fatalf("the refusal must name the peer holding the port, got %v", err)
  40. }
  41. })
  42. }
  43. }
  44. // A peer the forward supervisor opens no listener for holds no port: it has no
  45. // email, or no address the tunnel can route to, and Reconcile skips it either way.
  46. func TestAddInboundAllowsAPortNoPeerCanActuallyForward(t *testing.T) {
  47. cases := []struct {
  48. name string
  49. settings func(t *testing.T) string
  50. }{
  51. {
  52. name: "a peer with no email",
  53. settings: func(t *testing.T) string {
  54. t.Helper()
  55. return replaceFirst(t, awgRelayWindowSettingsWithForward(t, "awg-forward", "8443"),
  56. `"email":"awg-forward@relay-window"`, `"email":""`)
  57. },
  58. },
  59. {
  60. name: "an IPv6-only peer on a row without IPv6",
  61. settings: func(t *testing.T) string {
  62. t.Helper()
  63. return replaceFirst(t, awgRelayWindowSettingsWithForward(t, "awg-forward", "8443"),
  64. `"allowedIPs":["10.8.1.2/32"]`, `"allowedIPs":["fd00::2/128"]`)
  65. },
  66. },
  67. }
  68. for _, tc := range cases {
  69. t.Run(tc.name, func(t *testing.T) {
  70. setupConflictDB(t)
  71. seedInboundConflict(t, "awg-forward", "0.0.0.0", 51820, model.AmneziaWG, ``, tc.settings(t))
  72. if _, _, err := (&InboundService{}).AddInbound(&model.Inbound{
  73. Tag: "user-inbound", Enable: true, Listen: "0.0.0.0", Port: 8443,
  74. Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`, Settings: `{"clients":[]}`,
  75. }); err != nil {
  76. t.Fatalf("nothing binds 8443 for this peer; the create must be allowed: %v", err)
  77. }
  78. })
  79. }
  80. }
  81. // The refusal has to point at where the socket really is: the forward listens on
  82. // every interface, so repeating the candidate's requested address asserts a lie.
  83. func TestForwardedPortRefusalNamesTheWildcardBind(t *testing.T) {
  84. setupConflictDB(t)
  85. seedInboundConflict(t, "awg-forward", "0.0.0.0", 51820, model.AmneziaWG, ``,
  86. awgRelayWindowSettingsWithForward(t, "awg-forward", "8443"))
  87. _, _, err := (&InboundService{}).AddInbound(&model.Inbound{
  88. Tag: "user-inbound", Enable: true, Listen: "10.0.0.5", Port: 8443,
  89. Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`, Settings: `{"clients":[]}`,
  90. })
  91. if err == nil {
  92. t.Fatal("the port is forwarded on every interface, including 10.0.0.5; the create must be refused")
  93. }
  94. if !strings.Contains(err.Error(), " on * by its client ") {
  95. t.Fatalf("the refusal must place the forward on every interface, got %v", err)
  96. }
  97. }
  98. func replaceFirst(t *testing.T, s, old, new string) string {
  99. t.Helper()
  100. if !strings.Contains(s, old) {
  101. t.Fatalf("fixture no longer contains %s", old)
  102. }
  103. return strings.Replace(s, old, new, 1)
  104. }
  105. // The forward listener runs where the AmneziaWG row runs, so a node row sharing
  106. // a local peer's port stays legal -- the scoping every other guard here uses.
  107. func TestAddInboundAllowsANodeRowOnALocallyForwardedPort(t *testing.T) {
  108. setupConflictDB(t)
  109. seedInboundConflict(t, "awg-forward", "0.0.0.0", 51820, model.AmneziaWG, ``,
  110. awgRelayWindowSettingsWithForward(t, "awg-forward", "8443"))
  111. node := &model.Node{Name: "n1", Address: "127.0.0.1", Port: 2096, Scheme: "https", Enable: true, Status: "online"}
  112. if err := database.GetDB().Create(node).Error; err != nil {
  113. t.Fatalf("seed node: %v", err)
  114. }
  115. if _, _, err := (&InboundService{}).AddInbound(&model.Inbound{
  116. Tag: "node-inbound", Enable: true, Listen: "0.0.0.0", Port: 8443,
  117. Protocol: model.VLESS, StreamSettings: `{"network":"tcp"}`, Settings: `{"clients":[]}`,
  118. NodeID: &node.Id,
  119. }); err != nil {
  120. t.Fatalf("a node row does not bind here; the create must be allowed: %v", err)
  121. }
  122. }