@@ -10,6 +10,7 @@ import org.junit.Assert.assertEquals
1010import org.junit.Assert.assertFalse
1111import org.junit.Assert.assertNotNull
1212import org.junit.Assert.assertTrue
13+ import org.junit.Assert.fail
1314import org.junit.Test
1415
1516class ConfigBuilderWireGuardTest {
@@ -64,6 +65,68 @@ class ConfigBuilderWireGuardTest {
6465 assertEquals(MAIN_TAG , config.getAsJsonObject(" route" ).get(" final" ).asString)
6566 }
6667
68+ @Test
69+ fun selectorReferencesWireGuardEndpointMemberWithoutLegacyOutbound () {
70+ val options = topologyOptions()
71+ options.outbounds.add(0 , buildSelectorOutbound(MAIN_TAG , listOf (MAIN_TAG , NEXT_TAG )))
72+ options.route.final_ = TAG_PROXY
73+
74+ val config = finalizedTopology(options)
75+ val selector = outbound(config, TAG_PROXY )
76+
77+ assertEquals(MAIN_TAG , selector.get(" default" ).asString)
78+ assertEquals(listOf (MAIN_TAG , NEXT_TAG ), selector
79+ .getAsJsonArray(" outbounds" ).map { it.asString })
80+ assertTopologyReferencesResolve(config)
81+ }
82+
83+ @Test
84+ fun urlTestTopologyUsesWireGuardEndpointAsMainTarget () {
85+ val config = finalizedTopology(topologyOptions())
86+
87+ assertEquals(MAIN_TAG , config.getAsJsonObject(" route" ).get(" final" ).asString)
88+ assertTopologyReferencesResolve(config)
89+ }
90+
91+ @Test
92+ fun applicationFacingWireGuardDetoursToNextOutboundInExistingChainOrder () {
93+ val options = topologyOptions()
94+ options.outbounds.single { it.tag == MAIN_TAG }.detourTo(NEXT_TAG )
95+
96+ val config = finalizedTopology(options)
97+ assertEquals(NEXT_TAG , endpoint(config, MAIN_TAG ).get(" detour" ).asString)
98+ assertEquals(listOf (MAIN_TAG , NEXT_TAG ), chainPath(config))
99+ assertTopologyReferencesResolve(config)
100+ }
101+
102+ @Test
103+ fun egressFacingWireGuardIsReferencedByPreviousOutbound () {
104+ val options = topologyOptions()
105+ options.route.final_ = NEXT_TAG
106+ options.outbounds.single { it.tag == NEXT_TAG }.detourTo(MAIN_TAG )
107+
108+ val config = finalizedTopology(options)
109+ assertEquals(MAIN_TAG , outbound(config, NEXT_TAG ).get(" detour" ).asString)
110+ assertFalse(endpoint(config, MAIN_TAG ).has(" detour" ))
111+ assertEquals(listOf (NEXT_TAG , MAIN_TAG ), chainPath(config))
112+ assertTopologyReferencesResolve(config)
113+ }
114+
115+ @Test
116+ fun wireGuardWithListenPortRejectsPositionRequiringDetour () {
117+ val endpoint = wireGuardEndpoint(listenPort = 51820 )
118+
119+ try {
120+ endpoint.detourTo(NEXT_TAG )
121+ fail(" Expected WireGuard listen_port and detour conflict" )
122+ } catch (error: IllegalArgumentException ) {
123+ assertTrue(error.message.orEmpty().contains(MAIN_TAG ))
124+ assertTrue(error.message.orEmpty().contains(NEXT_TAG ))
125+ assertTrue(error.message.orEmpty().contains(" listen_port" ))
126+ assertFalse(endpoint.asMap().containsKey(" detour" ))
127+ }
128+ }
129+
67130 private fun baseOptions () = MyOptions ().apply {
68131 endpoints = mutableListOf ()
69132 route = RouteOptions ().apply { final_ = MAIN_TAG }
@@ -84,12 +147,94 @@ class ConfigBuilderWireGuardTest {
84147 )
85148 }
86149
150+ private fun topologyOptions () = MyOptions ().apply {
151+ endpoints = mutableListOf ()
152+ route = RouteOptions ().apply { final_ = MAIN_TAG }
153+ outbounds = mutableListOf (
154+ wireGuardEndpoint(),
155+ Outbound ().apply {
156+ type = " socks"
157+ tag = NEXT_TAG
158+ _hack_config_map [" server" ] = " 192.0.2.20"
159+ _hack_config_map [" server_port" ] = 1080
160+ },
161+ Outbound ().apply {
162+ type = " direct"
163+ tag = TAG_DIRECT
164+ },
165+ )
166+ }
167+
168+ private fun wireGuardEndpoint (listenPort : Int = 0) =
169+ buildSingBoxEndpointWireGuardBean(WireGuardBean ().apply {
170+ initializeDefaultValues()
171+ serverAddress = " 198.51.100.10"
172+ serverPort = 51820
173+ localAddress = " 10.0.0.2/32, fd00::2/128"
174+ privateKey = TEST_PRIVATE_KEY
175+ peerPublicKey = TEST_PUBLIC_KEY
176+ this .listenPort = listenPort
177+ }).apply { tag = MAIN_TAG }
178+
179+ private fun finalizedTopology (options : MyOptions ) = gson.toJsonTree(
180+ finalizeRootConfig(options)
181+ ).asJsonObject.also { config ->
182+ val endpoints = config.getAsJsonArray(" endpoints" ).map { it.asJsonObject }
183+ val outbounds = config.getAsJsonArray(" outbounds" ).map { it.asJsonObject }
184+ assertEquals(1 , endpoints.count { it.get(" tag" ).asString == MAIN_TAG })
185+ assertFalse(outbounds.any { it.get(" type" ).asString == " wireguard" })
186+ }
187+
188+ private fun endpoint (config : com.google.gson.JsonObject , tag : String ) =
189+ config.getAsJsonArray(" endpoints" )
190+ .map { it.asJsonObject }
191+ .single { it.get(" tag" ).asString == tag }
192+
193+ private fun outbound (config : com.google.gson.JsonObject , tag : String ) =
194+ config.getAsJsonArray(" outbounds" )
195+ .map { it.asJsonObject }
196+ .single { it.get(" tag" ).asString == tag }
197+
198+ private fun assertTopologyReferencesResolve (config : com.google.gson.JsonObject ) {
199+ val endpoints = config.getAsJsonArray(" endpoints" ).map { it.asJsonObject }
200+ val outbounds = config.getAsJsonArray(" outbounds" ).map { it.asJsonObject }
201+ val availableTags = (endpoints + outbounds).map { it.get(" tag" ).asString }.toSet()
202+ val referencedTags = buildList {
203+ add(config.getAsJsonObject(" route" ).get(" final" ).asString)
204+ endpoints.mapNotNullTo(this ) { it.get(" detour" )?.asString }
205+ outbounds.mapNotNullTo(this ) { it.get(" detour" )?.asString }
206+ outbounds.filter { it.get(" type" ).asString == " selector" }.forEach { selector ->
207+ add(selector.get(" default" ).asString)
208+ selector.getAsJsonArray(" outbounds" ).mapTo(this ) { it.asString }
209+ }
210+ }
211+
212+ assertTrue(" Unresolved topology tags: ${referencedTags - availableTags} " ,
213+ availableTags.containsAll(referencedTags))
214+ }
215+
216+ private fun chainPath (config : com.google.gson.JsonObject ): List <String > {
217+ val nodes = (
218+ config.getAsJsonArray(" endpoints" ).map { it.asJsonObject } +
219+ config.getAsJsonArray(" outbounds" ).map { it.asJsonObject }
220+ ).associateBy { it.get(" tag" ).asString }
221+ val path = mutableListOf<String >()
222+ var currentTag: String? = config.getAsJsonObject(" route" ).get(" final" ).asString
223+ while (currentTag != null ) {
224+ check(path.size <= nodes.size) { " Cycle in topology path: $path " }
225+ path + = currentTag
226+ currentTag = nodes.getValue(currentTag).get(" detour" )?.asString
227+ }
228+ return path
229+ }
230+
87231 private fun endpointOverrideJson (name : String ): String {
88232 return """ {"endpoints":[{"type":"wireguard","tag":"$MAIN_TAG ","name":"$name "}]}"""
89233 }
90234
91235 private companion object {
92236 const val MAIN_TAG = " wireguard-main"
237+ const val NEXT_TAG = " next-hop"
93238 const val CUSTOM_TAG = " custom-wireguard"
94239 const val TEST_PRIVATE_KEY = " AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
95240 const val TEST_PUBLIC_KEY = " BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
0 commit comments