inbound_tls_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. package service
  2. import (
  3. "reflect"
  4. "strings"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. )
  9. func TestValidateInboundTLSCertificates(t *testing.T) {
  10. tests := []struct {
  11. name string
  12. streamSettings string
  13. wantErr bool
  14. }{
  15. {"empty stream", "", false},
  16. {"whitespace stream", " \t\n", false},
  17. {"none ignores stale TLS settings", `{"security":"none","tlsSettings":{"certificates":[{}]}}`, false},
  18. {"reality needs no TLS certificate", `{"security":"reality","realitySettings":{}}`, false},
  19. {"missing TLS settings", `{"security":"tls"}`, true},
  20. {"uppercase TLS security", `{"security":"TLS","tlsSettings":{}}`, true},
  21. {"mixed-case TLS security", `{"security":"Tls","tlsSettings":{}}`, true},
  22. {"null TLS settings", `{"security":"tls","tlsSettings":null}`, true},
  23. {"missing certificates", `{"security":"tls","tlsSettings":{}}`, true},
  24. {"null certificates", `{"security":"tls","tlsSettings":{"certificates":null}}`, true},
  25. {"empty certificates", `{"security":"tls","tlsSettings":{"certificates":[]}}`, true},
  26. {"null certificate row", `{"security":"tls","tlsSettings":{"certificates":[null]}}`, true},
  27. {"empty default file fields", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"","keyFile":""}]}}`, true},
  28. {"empty default inline fields", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":[],"key":[]}]}}`, true},
  29. {"whitespace file fields", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":" \t","keyFile":" \n"}]}}`, true},
  30. {"whitespace inline certificate", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":[" ","\t"],"key":["private key"]}]}}`, true},
  31. {"whitespace inline key", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":["certificate"],"key":[" ","\n"]}]}}`, true},
  32. {"missing private key", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem"}]}}`, true},
  33. {"missing certificate", `{"security":"tls","tlsSettings":{"certificates":[{"keyFile":"/node/key.pem"}]}}`, true},
  34. {"verify only", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"verify","certificateFile":"/node/ca.pem"}]}}`, true},
  35. {"verify with private key still needs server certificate", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"verify","certificateFile":"/node/ca.pem","keyFile":"/node/key.pem"}]}}`, true},
  36. {"issue needs private key", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"issue","certificateFile":"/node/ca.pem"}]}}`, true},
  37. {"file credentials with default usage", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, false},
  38. {"inline credentials", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":["certificate"],"key":["private key"]}]}}`, false},
  39. {"certificate file and inline key", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","key":["private key"]}]}}`, false},
  40. {"inline certificate and key file", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":["certificate"],"keyFile":"/node/key.pem"}]}}`, false},
  41. {"encipherment usage", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"encipherment","certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, false},
  42. {"issue usage", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"issue","certificateFile":"/node/ca.pem","keyFile":"/node/ca-key.pem"}]}}`, false},
  43. {"unknown usage defaults to encipherment like Xray", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"custom","certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, false},
  44. {"verify and server certificates", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"verify","certificateFile":"/node/ca.pem"},{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, false},
  45. {"verify usage is case insensitive", `{"security":"tls","tlsSettings":{"certificates":[{"usage":"VERIFY","certificateFile":"/node/ca.pem"},{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, false},
  46. {"empty extra certificate row", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"},{}]}}`, true},
  47. {"empty extra verify certificate", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"},{"usage":"verify"}]}}`, true},
  48. {"whitespace certificate file overrides inline content", `{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":" ","certificate":["certificate"],"key":["private key"]}]}}`, true},
  49. {"whitespace key file overrides inline content", `{"security":"tls","tlsSettings":{"certificates":[{"certificate":["certificate"],"keyFile":" ","key":["private key"]}]}}`, true},
  50. {"malformed stream", `{"security":"tls"`, true},
  51. {"malformed TLS settings", `{"security":"tls","tlsSettings":"invalid"}`, true},
  52. {"malformed certificate list", `{"security":"tls","tlsSettings":{"certificates":{}}}`, true},
  53. }
  54. for _, tt := range tests {
  55. t.Run(tt.name, func(t *testing.T) {
  56. err := validateInboundTLSCertificates(tt.streamSettings)
  57. if (err != nil) != tt.wantErr {
  58. t.Fatalf("validateInboundTLSCertificates() error = %v, wantErr %v", err, tt.wantErr)
  59. }
  60. })
  61. }
  62. }
  63. func TestValidateInboundTLSCertificatesIdentifiesIncompleteRow(t *testing.T) {
  64. err := validateInboundTLSCertificates(`{"security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"},{"certificateFile":"/node/other.pem"}]}}`)
  65. if err == nil || !strings.Contains(err.Error(), "TLS certificate 2") || !strings.Contains(err.Error(), "private key") {
  66. t.Fatalf("expected actionable error for the second certificate's private key, got %v", err)
  67. }
  68. }
  69. func TestAddInboundRejectsMissingTLSCertificates(t *testing.T) {
  70. setupConflictDB(t)
  71. mgr := useTestRuntimeManager(t)
  72. fake := &fakeNodeRuntime{}
  73. mgr.SetLocalRuntimeOverride(fake)
  74. inbound := &model.Inbound{
  75. Tag: "tls-missing-44310",
  76. Enable: true,
  77. Listen: "0.0.0.0",
  78. Port: 44310,
  79. Protocol: model.VLESS,
  80. StreamSettings: `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"","keyFile":""}]}}`,
  81. Settings: `{"clients":[]}`,
  82. }
  83. _, needRestart, err := (&InboundService{}).AddInbound(inbound)
  84. if err == nil || !strings.Contains(err.Error(), "TLS") {
  85. t.Fatalf("AddInbound: expected TLS validation error, got %v", err)
  86. }
  87. if needRestart {
  88. t.Fatal("AddInbound: rejected TLS configuration requested a restart")
  89. }
  90. var count int64
  91. if err := database.GetDB().Model(&model.Inbound{}).Count(&count).Error; err != nil {
  92. t.Fatalf("count inbounds: %v", err)
  93. }
  94. if count != 0 {
  95. t.Fatalf("AddInbound: rejected TLS configuration created %d rows", count)
  96. }
  97. if fake.addInbound.Load() != 0 || fake.updateInbound.Load() != 0 || fake.delInbound.Load() != 0 {
  98. t.Fatal("AddInbound: rejected TLS configuration reached the runtime")
  99. }
  100. }
  101. func TestUpdateInboundRejectsMissingTLSCertificates(t *testing.T) {
  102. setupConflictDB(t)
  103. mgr := useTestRuntimeManager(t)
  104. fake := &fakeNodeRuntime{}
  105. mgr.SetLocalRuntimeOverride(fake)
  106. seedInboundConflict(t, "tls-existing-44311", "0.0.0.0", 44311, model.VLESS,
  107. `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`, `{"clients":[]}`)
  108. var existing model.Inbound
  109. if err := database.GetDB().Where("tag = ?", "tls-existing-44311").First(&existing).Error; err != nil {
  110. t.Fatalf("load existing inbound: %v", err)
  111. }
  112. update := existing
  113. update.Remark = "must not be saved"
  114. update.Port = 44312
  115. update.StreamSettings = `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem"}]}}`
  116. _, needRestart, err := (&InboundService{}).UpdateInbound(&update)
  117. if err == nil || !strings.Contains(err.Error(), "TLS") {
  118. t.Fatalf("UpdateInbound: expected TLS validation error, got %v", err)
  119. }
  120. if needRestart {
  121. t.Fatal("UpdateInbound: rejected TLS configuration requested a restart")
  122. }
  123. var reloaded model.Inbound
  124. if err := database.GetDB().First(&reloaded, existing.Id).Error; err != nil {
  125. t.Fatalf("reload existing inbound: %v", err)
  126. }
  127. if !reflect.DeepEqual(reloaded, existing) {
  128. t.Fatal("UpdateInbound: rejected TLS configuration changed the stored inbound")
  129. }
  130. if fake.addInbound.Load() != 0 || fake.updateInbound.Load() != 0 || fake.delInbound.Load() != 0 {
  131. t.Fatal("UpdateInbound: rejected TLS configuration reached the runtime")
  132. }
  133. }
  134. // The panel used to seed a TLS inbound with an all-empty certificate, so rows in
  135. // that shape predate the guard and must stay editable — see UpdateInbound.
  136. func TestUpdateInboundAllowsUntouchedLegacyTLSCertificates(t *testing.T) {
  137. const legacyStream = `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"","keyFile":"","certificate":[],"key":[]}]}}`
  138. tests := []struct {
  139. name string
  140. streamSettings string
  141. }{
  142. {"remark-only edit resends the stored block", legacyStream},
  143. {"node push re-encodes the same block", `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"key":[],"certificate":[],"keyFile":"","certificateFile":""}]}}`},
  144. {"a partial fix to the stored credentials is tolerated", `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem"}]}}`},
  145. {"completing the credentials is accepted", `{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{"certificateFile":"/node/cert.pem","keyFile":"/node/key.pem"}]}}`},
  146. }
  147. for _, tt := range tests {
  148. t.Run(tt.name, func(t *testing.T) {
  149. setupConflictDB(t)
  150. mgr := useTestRuntimeManager(t)
  151. mgr.SetLocalRuntimeOverride(&fakeNodeRuntime{})
  152. seedInboundConflict(t, "tls-legacy-44321", "0.0.0.0", 44321, model.VLESS, legacyStream, `{"clients":[]}`)
  153. var existing model.Inbound
  154. if err := database.GetDB().Where("tag = ?", "tls-legacy-44321").First(&existing).Error; err != nil {
  155. t.Fatalf("load legacy inbound: %v", err)
  156. }
  157. update := existing
  158. update.Remark = "renamed"
  159. update.StreamSettings = tt.streamSettings
  160. if _, _, err := (&InboundService{}).UpdateInbound(&update); err != nil {
  161. t.Fatalf("UpdateInbound: %v", err)
  162. }
  163. var reloaded model.Inbound
  164. if err := database.GetDB().First(&reloaded, existing.Id).Error; err != nil {
  165. t.Fatalf("reload inbound: %v", err)
  166. }
  167. if reloaded.Remark != "renamed" {
  168. t.Fatalf("UpdateInbound: remark = %q, want %q", reloaded.Remark, "renamed")
  169. }
  170. })
  171. }
  172. }