naive_self_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. package main
  2. import (
  3. "crypto/sha256"
  4. "crypto/x509"
  5. "encoding/pem"
  6. "net/netip"
  7. "os"
  8. "strings"
  9. "testing"
  10. "github.com/sagernet/sing-box/common/tls"
  11. C "github.com/sagernet/sing-box/constant"
  12. "github.com/sagernet/sing-box/option"
  13. "github.com/sagernet/sing-box/protocol/naive"
  14. "github.com/sagernet/sing/common"
  15. "github.com/sagernet/sing/common/auth"
  16. "github.com/sagernet/sing/common/json/badoption"
  17. "github.com/sagernet/sing/common/network"
  18. "github.com/stretchr/testify/require"
  19. )
  20. func TestNaiveSelf(t *testing.T) {
  21. caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
  22. caPemContent, err := os.ReadFile(caPem)
  23. require.NoError(t, err)
  24. startInstance(t, option.Options{
  25. Inbounds: []option.Inbound{
  26. {
  27. Type: C.TypeMixed,
  28. Tag: "mixed-in",
  29. Options: &option.HTTPMixedInboundOptions{
  30. ListenOptions: option.ListenOptions{
  31. Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
  32. ListenPort: clientPort,
  33. },
  34. },
  35. },
  36. {
  37. Type: C.TypeNaive,
  38. Tag: "naive-in",
  39. Options: &option.NaiveInboundOptions{
  40. ListenOptions: option.ListenOptions{
  41. Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
  42. ListenPort: serverPort,
  43. },
  44. Users: []auth.User{
  45. {
  46. Username: "sekai",
  47. Password: "password",
  48. },
  49. },
  50. Network: network.NetworkTCP,
  51. InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
  52. TLS: &option.InboundTLSOptions{
  53. Enabled: true,
  54. ServerName: "example.org",
  55. CertificatePath: certPem,
  56. KeyPath: keyPem,
  57. },
  58. },
  59. },
  60. },
  61. },
  62. Outbounds: []option.Outbound{
  63. {
  64. Type: C.TypeDirect,
  65. },
  66. {
  67. Type: C.TypeNaive,
  68. Tag: "naive-out",
  69. Options: &option.NaiveOutboundOptions{
  70. ServerOptions: option.ServerOptions{
  71. Server: "127.0.0.1",
  72. ServerPort: serverPort,
  73. },
  74. Username: "sekai",
  75. Password: "password",
  76. OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
  77. TLS: &option.OutboundTLSOptions{
  78. Enabled: true,
  79. ServerName: "example.org",
  80. Certificate: []string{string(caPemContent)},
  81. },
  82. },
  83. },
  84. },
  85. },
  86. Route: &option.RouteOptions{
  87. Rules: []option.Rule{
  88. {
  89. Type: C.RuleTypeDefault,
  90. DefaultOptions: option.DefaultRule{
  91. RawDefaultRule: option.RawDefaultRule{
  92. Inbound: []string{"mixed-in"},
  93. },
  94. RuleAction: option.RuleAction{
  95. Action: C.RuleActionTypeRoute,
  96. RouteOptions: option.RouteActionOptions{
  97. Outbound: "naive-out",
  98. },
  99. },
  100. },
  101. },
  102. },
  103. },
  104. })
  105. testTCP(t, clientPort, testPort)
  106. }
  107. func TestNaiveSelfPublicKeySHA256(t *testing.T) {
  108. _, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
  109. // Read and parse the server certificate to get its public key SHA256
  110. certPemContent, err := os.ReadFile(certPem)
  111. require.NoError(t, err)
  112. block, _ := pem.Decode(certPemContent)
  113. require.NotNil(t, block)
  114. cert, err := x509.ParseCertificate(block.Bytes)
  115. require.NoError(t, err)
  116. // Calculate SHA256 of SPKI (Subject Public Key Info)
  117. spkiBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
  118. require.NoError(t, err)
  119. pinHash := sha256.Sum256(spkiBytes)
  120. startInstance(t, option.Options{
  121. Inbounds: []option.Inbound{
  122. {
  123. Type: C.TypeMixed,
  124. Tag: "mixed-in",
  125. Options: &option.HTTPMixedInboundOptions{
  126. ListenOptions: option.ListenOptions{
  127. Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
  128. ListenPort: clientPort,
  129. },
  130. },
  131. },
  132. {
  133. Type: C.TypeNaive,
  134. Tag: "naive-in",
  135. Options: &option.NaiveInboundOptions{
  136. ListenOptions: option.ListenOptions{
  137. Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
  138. ListenPort: serverPort,
  139. },
  140. Users: []auth.User{
  141. {
  142. Username: "sekai",
  143. Password: "password",
  144. },
  145. },
  146. Network: network.NetworkTCP,
  147. InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
  148. TLS: &option.InboundTLSOptions{
  149. Enabled: true,
  150. ServerName: "example.org",
  151. CertificatePath: certPem,
  152. KeyPath: keyPem,
  153. },
  154. },
  155. },
  156. },
  157. },
  158. Outbounds: []option.Outbound{
  159. {
  160. Type: C.TypeDirect,
  161. },
  162. {
  163. Type: C.TypeNaive,
  164. Tag: "naive-out",
  165. Options: &option.NaiveOutboundOptions{
  166. ServerOptions: option.ServerOptions{
  167. Server: "127.0.0.1",
  168. ServerPort: serverPort,
  169. },
  170. Username: "sekai",
  171. Password: "password",
  172. OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
  173. TLS: &option.OutboundTLSOptions{
  174. Enabled: true,
  175. ServerName: "example.org",
  176. CertificatePublicKeySHA256: [][]byte{pinHash[:]},
  177. },
  178. },
  179. },
  180. },
  181. },
  182. Route: &option.RouteOptions{
  183. Rules: []option.Rule{
  184. {
  185. Type: C.RuleTypeDefault,
  186. DefaultOptions: option.DefaultRule{
  187. RawDefaultRule: option.RawDefaultRule{
  188. Inbound: []string{"mixed-in"},
  189. },
  190. RuleAction: option.RuleAction{
  191. Action: C.RuleActionTypeRoute,
  192. RouteOptions: option.RouteActionOptions{
  193. Outbound: "naive-out",
  194. },
  195. },
  196. },
  197. },
  198. },
  199. },
  200. })
  201. testTCP(t, clientPort, testPort)
  202. }
  203. func TestNaiveSelfECH(t *testing.T) {
  204. t.Skip("TODO: ECH is not currently supported on naive outbound")
  205. caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
  206. caPemContent, err := os.ReadFile(caPem)
  207. require.NoError(t, err)
  208. echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org"))
  209. instance := startInstance(t, option.Options{
  210. Inbounds: []option.Inbound{
  211. {
  212. Type: C.TypeMixed,
  213. Tag: "mixed-in",
  214. Options: &option.HTTPMixedInboundOptions{
  215. ListenOptions: option.ListenOptions{
  216. Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
  217. ListenPort: clientPort,
  218. },
  219. },
  220. },
  221. {
  222. Type: C.TypeNaive,
  223. Tag: "naive-in",
  224. Options: &option.NaiveInboundOptions{
  225. ListenOptions: option.ListenOptions{
  226. Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
  227. ListenPort: serverPort,
  228. },
  229. Users: []auth.User{
  230. {
  231. Username: "sekai",
  232. Password: "password",
  233. },
  234. },
  235. Network: network.NetworkTCP,
  236. InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
  237. TLS: &option.InboundTLSOptions{
  238. Enabled: true,
  239. ServerName: "example.org",
  240. CertificatePath: certPem,
  241. KeyPath: keyPem,
  242. ECH: &option.InboundECHOptions{
  243. Enabled: true,
  244. Key: []string{echKey},
  245. },
  246. },
  247. },
  248. },
  249. },
  250. },
  251. Outbounds: []option.Outbound{
  252. {
  253. Type: C.TypeDirect,
  254. },
  255. {
  256. Type: C.TypeNaive,
  257. Tag: "naive-out",
  258. Options: &option.NaiveOutboundOptions{
  259. ServerOptions: option.ServerOptions{
  260. Server: "127.0.0.1",
  261. ServerPort: serverPort,
  262. },
  263. Username: "sekai",
  264. Password: "password",
  265. OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
  266. TLS: &option.OutboundTLSOptions{
  267. Enabled: true,
  268. ServerName: "example.org",
  269. Certificate: []string{string(caPemContent)},
  270. ECH: &option.OutboundECHOptions{
  271. Enabled: true,
  272. Config: []string{echConfig},
  273. },
  274. },
  275. },
  276. },
  277. },
  278. },
  279. Route: &option.RouteOptions{
  280. Rules: []option.Rule{
  281. {
  282. Type: C.RuleTypeDefault,
  283. DefaultOptions: option.DefaultRule{
  284. RawDefaultRule: option.RawDefaultRule{
  285. Inbound: []string{"mixed-in"},
  286. },
  287. RuleAction: option.RuleAction{
  288. Action: C.RuleActionTypeRoute,
  289. RouteOptions: option.RouteActionOptions{
  290. Outbound: "naive-out",
  291. },
  292. },
  293. },
  294. },
  295. },
  296. },
  297. })
  298. naiveOut, ok := instance.Outbound().Outbound("naive-out")
  299. require.True(t, ok)
  300. naiveOutbound := naiveOut.(*naive.Outbound)
  301. netLogPath := "/tmp/naive_ech_netlog.json"
  302. require.True(t, naiveOutbound.StartNetLogToFile(netLogPath, true))
  303. defer naiveOutbound.StopNetLog()
  304. testTCP(t, clientPort, testPort)
  305. naiveOutbound.StopNetLog()
  306. logContent, err := os.ReadFile(netLogPath)
  307. require.NoError(t, err)
  308. logStr := string(logContent)
  309. require.True(t, strings.Contains(logStr, `"encrypted_client_hello":true`),
  310. "ECH should be accepted in TLS handshake. NetLog saved to: %s", netLogPath)
  311. }
  312. func TestNaiveSelfInsecureConcurrency(t *testing.T) {
  313. caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
  314. caPemContent, err := os.ReadFile(caPem)
  315. require.NoError(t, err)
  316. instance := startInstance(t, option.Options{
  317. Inbounds: []option.Inbound{
  318. {
  319. Type: C.TypeMixed,
  320. Tag: "mixed-in",
  321. Options: &option.HTTPMixedInboundOptions{
  322. ListenOptions: option.ListenOptions{
  323. Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
  324. ListenPort: clientPort,
  325. },
  326. },
  327. },
  328. {
  329. Type: C.TypeNaive,
  330. Tag: "naive-in",
  331. Options: &option.NaiveInboundOptions{
  332. ListenOptions: option.ListenOptions{
  333. Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())),
  334. ListenPort: serverPort,
  335. },
  336. Users: []auth.User{
  337. {
  338. Username: "sekai",
  339. Password: "password",
  340. },
  341. },
  342. Network: network.NetworkTCP,
  343. InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{
  344. TLS: &option.InboundTLSOptions{
  345. Enabled: true,
  346. ServerName: "example.org",
  347. CertificatePath: certPem,
  348. KeyPath: keyPem,
  349. },
  350. },
  351. },
  352. },
  353. },
  354. Outbounds: []option.Outbound{
  355. {
  356. Type: C.TypeDirect,
  357. },
  358. {
  359. Type: C.TypeNaive,
  360. Tag: "naive-out",
  361. Options: &option.NaiveOutboundOptions{
  362. ServerOptions: option.ServerOptions{
  363. Server: "127.0.0.1",
  364. ServerPort: serverPort,
  365. },
  366. Username: "sekai",
  367. Password: "password",
  368. InsecureConcurrency: 3,
  369. OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{
  370. TLS: &option.OutboundTLSOptions{
  371. Enabled: true,
  372. ServerName: "example.org",
  373. Certificate: []string{string(caPemContent)},
  374. },
  375. },
  376. },
  377. },
  378. },
  379. Route: &option.RouteOptions{
  380. Rules: []option.Rule{
  381. {
  382. Type: C.RuleTypeDefault,
  383. DefaultOptions: option.DefaultRule{
  384. RawDefaultRule: option.RawDefaultRule{
  385. Inbound: []string{"mixed-in"},
  386. },
  387. RuleAction: option.RuleAction{
  388. Action: C.RuleActionTypeRoute,
  389. RouteOptions: option.RouteActionOptions{
  390. Outbound: "naive-out",
  391. },
  392. },
  393. },
  394. },
  395. },
  396. },
  397. })
  398. naiveOut, ok := instance.Outbound().Outbound("naive-out")
  399. require.True(t, ok)
  400. naiveOutbound := naiveOut.(*naive.Outbound)
  401. netLogPath := "/tmp/naive_concurrency_netlog.json"
  402. require.True(t, naiveOutbound.StartNetLogToFile(netLogPath, true))
  403. defer naiveOutbound.StopNetLog()
  404. // Send multiple sequential connections to trigger round-robin
  405. // With insecure_concurrency=3, connections will be distributed to 3 pools
  406. for i := 0; i < 6; i++ {
  407. testTCP(t, clientPort, testPort)
  408. }
  409. naiveOutbound.StopNetLog()
  410. // Verify NetLog contains multiple independent HTTP/2 sessions
  411. logContent, err := os.ReadFile(netLogPath)
  412. require.NoError(t, err)
  413. logStr := string(logContent)
  414. // Count HTTP2_SESSION_INITIALIZED events to verify connection pool isolation
  415. // NetLog stores event types as numeric IDs, HTTP2_SESSION_INITIALIZED = 249
  416. sessionCount := strings.Count(logStr, `"type":249`)
  417. require.GreaterOrEqual(t, sessionCount, 3,
  418. "Expected at least 3 HTTP/2 sessions with insecure_concurrency=3. NetLog: %s", netLogPath)
  419. }