Compare commits
1492 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39d2513df1 | |||
| 3cb4df474b | |||
| b64bb92c8c | |||
| 7f0e2f801d | |||
| 0f7cee0613 | |||
| b2e10fc60b | |||
| aa3f17fb51 | |||
| d64fc627bb | |||
| 6934ab3e5d | |||
| 028d13aef8 | |||
| 4a78c165bc | |||
| c1033a006f | |||
| e8a42013c0 | |||
| 94e5263250 | |||
| 7c68aa9d83 | |||
| ef13e336da | |||
| d16cb21059 | |||
| 237837e7be | |||
| 2a6edd98be | |||
| 9238b80675 | |||
| bb5e6f1959 | |||
| 1cc0865411 | |||
| cb8d979f23 | |||
| 5932b0db29 | |||
| dbe4fe78b5 | |||
| b5bcd81ae4 | |||
| d45ccd7953 | |||
| 6815563902 | |||
| 7c38ad7c57 | |||
| 885aeb384e | |||
| 57f01412e7 | |||
| fd84ea4114 | |||
| a4e2e6d6a2 | |||
| b8cfe9bc82 | |||
| b945f4a3c7 | |||
| cfb6776e8f | |||
| ebaa5088b6 | |||
| 209679f098 | |||
| 19ad248cfe | |||
| 35f1f58476 | |||
| 4c86e7c26e | |||
| 5d0897cd94 | |||
| fc4ab733c8 | |||
| d606453b83 | |||
| c6c8eccd51 | |||
| 03074fba61 | |||
| ac02d23083 | |||
| 5c6aaf285b | |||
| d3f9809486 | |||
| a12909e33d | |||
| 3e823a7440 | |||
| dc1d3a27dd | |||
| d0f1e21f49 | |||
| 3b8615fad0 | |||
| d9d00f4137 | |||
| 2aefbd43eb | |||
| cf33391785 | |||
| a35ba5dfbd | |||
| 3b4d895c1d | |||
| e8585a82c6 | |||
| 087c08f95a | |||
| 6612ca8256 | |||
| 487692deb7 | |||
| e9332168bf | |||
| 15fcfec4c8 | |||
| a4baeeb0fa | |||
| 093e51186d | |||
| 14a29884e7 | |||
| 30b0bd8d0d | |||
| ae84df7367 | |||
| b673167731 | |||
| 659f2ded09 | |||
| 585d64b0be | |||
| 2ae2e747e2 | |||
| 4199cee4e5 | |||
| 8be8a912c4 | |||
| 851f22680a | |||
| 12abc99bfc | |||
| 07e50637a2 | |||
| c1824c7598 | |||
| 18eebf999d | |||
| e37d6b7523 | |||
| 8c5375ec62 | |||
| 30792299e8 | |||
| e6db0f6f9e | |||
| 04478ab490 | |||
| f371da490a | |||
| 0a0ef6e6b5 | |||
| fd57019e73 | |||
| 04ac6cb2d7 | |||
| 29a0be9629 | |||
| 7aa27fd3de | |||
| fcc478ff63 | |||
| 54d5ad2342 | |||
| ba25e05536 | |||
| 5a5a81fa0b | |||
| 322c4b305e | |||
| 590f11e671 | |||
| 872ec80292 | |||
| f19f153f9c | |||
| d7abb8f49e | |||
| e54e713db7 | |||
| de91fb27db | |||
| db8f46eae5 | |||
| 42e7f95e4f | |||
| 0a52a79fa0 | |||
| 9dc1b1f2cf | |||
| b61cad5a5f | |||
| 66cd746ad2 | |||
| ae4a92b66b | |||
| 55c0ccf1b0 | |||
| deedd26fac | |||
| e6c8307b96 | |||
| d6d5753283 | |||
| 014d510774 | |||
| 42f89e69a7 | |||
| d5a0997775 | |||
| af7db6f86b | |||
| 7b7f43e12e | |||
| 8dd1df1451 | |||
| d4a73493b5 | |||
| 9fce03d0ed | |||
| a12ca3ea04 | |||
| 4e9df84d3e | |||
| d3118b53f2 | |||
| cd45b51487 | |||
| 6aa26d2621 | |||
| 0b452707c3 | |||
| 303b642ade | |||
| 80eec036dd | |||
| f9a85366ea | |||
| 378c891399 | |||
| d04b5eee37 | |||
| 405cd7f55e | |||
| 850096789b | |||
| 3d42ac9dba | |||
| d0265265f3 | |||
| 2d6dd33a7b | |||
| 3a32373abe | |||
| befb2937d1 | |||
| 0a375bec67 | |||
| 029f8b67e7 | |||
| 7475d1757b | |||
| 5a16614a7b | |||
| edb06b466a | |||
| 30b5391895 | |||
| 6cd04b831f | |||
| 1e616198f0 | |||
| a710146e4f | |||
| 9cd530d223 | |||
| fc89f25591 | |||
| 84ab54fea2 | |||
| d642d09844 | |||
| 6ae46c50e2 | |||
| fe4b451988 | |||
| 5c9b634039 | |||
| 09461663cb | |||
| 50c1145815 | |||
| edc4856cb8 | |||
| 4dd7eee8fe | |||
| 516637954c | |||
| 9e714bfcb3 | |||
| 858dc5237a | |||
| 2b4a4f7ecc | |||
| f9f0c905a4 | |||
| f997335113 | |||
| ed344cd281 | |||
| 94775f120a | |||
| abd23b3227 | |||
| 7d3e002140 | |||
| 1adb22944a | |||
| f48253371d | |||
| 74f701096e | |||
| 8eca5fe8af | |||
| ff40b53e2f | |||
| 5080d94bb7 | |||
| 24539be29f | |||
| e951e3d913 | |||
| 83e5f1d10a | |||
| 47a6623e62 | |||
| d6e3fc5c91 | |||
| 685d23f2d1 | |||
| d46bbdc219 | |||
| be95e6a88e | |||
| 2435708628 | |||
| 6cf6057dfa | |||
| af92cf5a7d | |||
| 2277de95c0 | |||
| 3c90ee0206 | |||
| 41fd237a87 | |||
| f6bded88dd | |||
| 8600fdba35 | |||
| 2c47abc219 | |||
| 10b0f8c9de | |||
| 53572877fa | |||
| 69e000b23d | |||
| a8e09611eb | |||
| 4fdc46b8ca | |||
| ac043da8a2 | |||
| a192fec127 | |||
| b0b09273cf | |||
| 01a943b7ea | |||
| 06a3738b59 | |||
| fa4432584e | |||
| bb1c3d915f | |||
| d8983a4761 | |||
| 9815041c77 | |||
| 41b0983582 | |||
| bb1f677d45 | |||
| de1c8bcfcc | |||
| ca6e963575 | |||
| bfb6813dc7 | |||
| cc2b4eec65 | |||
| dd11569969 | |||
| 11c3dd97c3 | |||
| 92f15150cd | |||
| f8e809da8b | |||
| 12b9349e62 | |||
| a580494bde | |||
| 66f100ebb3 | |||
| b0213ffdf6 | |||
| 538b2eb0f8 | |||
| f2093c4c52 | |||
| 02ac57a4c6 | |||
| 379266f205 | |||
| 23dbb35da7 | |||
| 22a605ecda | |||
| c7dd45f4f2 | |||
| 609e688d87 | |||
| a2a4ea79a9 | |||
| c08147c362 | |||
| c99fda9839 | |||
| 587d103518 | |||
| 192a4afe6d | |||
| ee69364f2b | |||
| 6be0bd7b21 | |||
| a3972b98f3 | |||
| a2b6e669d8 | |||
| 55c42fbf2a | |||
| ab4af6bd82 | |||
| c3a8ee59dc | |||
| 18f23d9d3b | |||
| 4a3686fb32 | |||
| 68ed08d36e | |||
| d0612619fd | |||
| 3ddd6ee699 | |||
| 485ea2e28d | |||
| 2fd57baa37 | |||
| 73d1dce4db | |||
| bd60224577 | |||
| 3f1b741d9d | |||
| b71614a9cd | |||
| 31d29d97c0 | |||
| 183391b6e3 | |||
| 3ad6464595 | |||
| e684bef35d | |||
| a87d5d375b | |||
| 545683c9ba | |||
| 1a8631257b | |||
| ebebd3277f | |||
| 5242827baf | |||
| c6799d5bf3 | |||
| 054a3bfa5e | |||
| 02a9d64188 | |||
| 29b67c29f5 | |||
| 79d5cad738 | |||
| de5b714a28 | |||
| 416ee28e80 | |||
| 71f0d98e68 | |||
| 37bd57ffc5 | |||
| b4ac79e4fa | |||
| 8aba43d448 | |||
| e564083352 | |||
| 08bf66350a | |||
| 66d4c21b77 | |||
| 359b0af7ae | |||
| 4b1fa29ade | |||
| e287ac88c8 | |||
| 4812f067db | |||
| 47be68c52f | |||
| 418477267b | |||
| 74ad37d329 | |||
| fd83f88ec2 | |||
| 214ab76459 | |||
| f67bee04ad | |||
| 2cfef7a614 | |||
| bd6b67cbfc | |||
| bb374f2c7e | |||
| f85c382dbd | |||
| 0b76ff0409 | |||
| e3bbd47c81 | |||
| 0672820718 | |||
| e0e58dff45 | |||
| cfae051109 | |||
| 60ddf5ba6b | |||
| 861b1746e3 | |||
| 4dec554b42 | |||
| d7ad91fe8b | |||
| fb51683981 | |||
| e5d14bada9 | |||
| 80fb265960 | |||
| f5493b08ae | |||
| 4f7d6db361 | |||
| 636b20290a | |||
| 7ead97079c | |||
| e42cc3a60a | |||
| d231490917 | |||
| 521d61acb3 | |||
| ed9c26b012 | |||
| e782da7c71 | |||
| 6fd0f7f0db | |||
| 7c14a0c200 | |||
| 2789db2c70 | |||
| 8aee96b5eb | |||
| 02d58dde9a | |||
| 623f5446f2 | |||
| b961c86066 | |||
| 3e4f0e9b5b | |||
| c0141225d3 | |||
| 026bf6a37b | |||
| c117a334ce | |||
| 051520de37 | |||
| b5146e4712 | |||
| 4457b6b2c9 | |||
| 12edb051c3 | |||
| 923e3f32d1 | |||
| 5490db90c0 | |||
| cb68d42291 | |||
| ec3a8e80d6 | |||
| eb0960125b | |||
| 12b70e2088 | |||
| 0c473dea19 | |||
| e71dd7342a | |||
| a765215dda | |||
| edea3027f7 | |||
| 305219c3bc | |||
| 9c4188fba7 | |||
| 0f3124bece | |||
| 2614f72a36 | |||
| 480c7fc8a1 | |||
| 302e7f148e | |||
| 98d6da983b | |||
| f6f60f77d3 | |||
| 7be0fb3ecd | |||
| d53a9f9ecd | |||
| 9eb03eece2 | |||
| 6632ba3b6e | |||
| bd582f1d90 | |||
| f5f1e12565 | |||
| 207d6f4f8c | |||
| 52e0a19826 | |||
| fc0e60a097 | |||
| 65d6b4aaa8 | |||
| 7b11755e7f | |||
| 2d0de4860c | |||
| 7406ef16ff | |||
| a084b21d50 | |||
| bf01fe2bdb | |||
| 519cc2a4b5 | |||
| c21653d40f | |||
| 1919c5e05f | |||
| 8a17d93882 | |||
| 21ed008c7b | |||
| d83cfa28c2 | |||
| d27e2091a7 | |||
| c545678e47 | |||
| 05cba9b0fe | |||
| b472aa0e6b | |||
| 1ad840851a | |||
| ffc74f479c | |||
| 81c0087ac1 | |||
| 97327a0101 | |||
| 6318dd554b | |||
| 785aa7eb8a | |||
| 872e926132 | |||
| b278180eb5 | |||
| ce64ffbf3b | |||
| 5ad8605a41 | |||
| 95f7e99752 | |||
| f703a41778 | |||
| 2251c59c0b | |||
| edd0ce96b1 | |||
| 562283b3cc | |||
| bb04953845 | |||
| 208a93a917 | |||
| 92f33ee3f8 | |||
| 084346b348 | |||
| 15de18b736 | |||
| b60423d1c0 | |||
| 86e38c6963 | |||
| 9ba4d7e691 | |||
| 16e72787c9 | |||
| 3ffac8b29f | |||
| 10df1b44ea | |||
| fa12878258 | |||
| 0f1ea1e119 | |||
| 1959608f24 | |||
| 0e041a6e9a | |||
| c78ef380cc | |||
| 57362a4bde | |||
| 7f24ad534f | |||
| 680832614b | |||
| 4357abbff4 | |||
| 905be66c3f | |||
| 318a7b54a5 | |||
| 5db2df77a0 | |||
| d039cb9edf | |||
| 9a1654bae9 | |||
| 3bf911da9c | |||
| 3f778a1ea2 | |||
| 8e03de8055 | |||
| 1f42d7fc07 | |||
| 0700e834f1 | |||
| 777190e861 | |||
| 33572477fc | |||
| 2fb6e62e13 | |||
| 94cc72d2b9 | |||
| f68c353715 | |||
| e077c18108 | |||
| 1a5e105212 | |||
| e0881caab4 | |||
| 7219425258 | |||
| 51eabe5440 | |||
| 6f0b3ce990 | |||
| 69e27ed3bb | |||
| fff6ab30e6 | |||
| fdb67a86f4 | |||
| ea088376ac | |||
| 52332d960e | |||
| 3ead542e2b | |||
| 9d1f98ff34 | |||
| f305e26a39 | |||
| aa47fba20d | |||
| 69c5bbfd3d | |||
| 90ed02804c | |||
| 822c1de79c | |||
| d910b93525 | |||
| 7e6b1c247b | |||
| f3f2b7fab5 | |||
| e6f260da76 | |||
| 55bc2bf934 | |||
| f22454da5d | |||
| 4a87549fa7 | |||
| d447adc97f | |||
| 3773962b64 | |||
| be0a2506ce | |||
| 7f9cb8dfdd | |||
| 71a5b6e480 | |||
| 02e53ced50 | |||
| 42c27a5e7e | |||
| af04bbcf87 | |||
| 9bedfe8a7b | |||
| 2fdf684ee7 | |||
| 5b79951da7 | |||
| 06aa680d45 | |||
| cdb9b1811c | |||
| 0fc1f2f5d3 | |||
| ef1bb3dd34 | |||
| 1bca321d3f | |||
| 247e2b3ba3 | |||
| 41fd2b0cfb | |||
| 72da42ee40 | |||
| c130d55e8f | |||
| 5ae84f7eac | |||
| df5ea251e1 | |||
| 8890d9f004 | |||
| 4fcb3f9d06 | |||
| 5bf7c98cd3 | |||
| 46bc1a49df | |||
| 21175f41ec | |||
| 864c63987e | |||
| 4ac0547e22 | |||
| 12a9ee262c | |||
| cfa9c19c94 | |||
| 56e33e6cdd | |||
| 02421072c1 | |||
| b862a0dc65 | |||
| 1f25d6a000 | |||
| e1def0616a | |||
| 83fd6efc17 | |||
| f0c0e2e83a | |||
| 6ca3eb769e | |||
| 963d24ab66 | |||
| cfd81441fa | |||
| 4084ae2938 | |||
| 3f9bc098ec | |||
| 9cb28ed969 | |||
| 773ddc5373 | |||
| 38193b5621 | |||
| 358713a2a3 | |||
| 5b9f24c1f0 | |||
| c47c2c3666 | |||
| 49f7c3e7d7 | |||
| 423e5de2c6 | |||
| 3e3387e63e | |||
| debddace8b | |||
| 160b412e0a | |||
| 0f3e0a0ea2 | |||
| c4cf90e807 | |||
| 5db46e81b7 | |||
| 1ef80a3a96 | |||
| a46d9d0d2a | |||
| 7b80536e1e | |||
| 5733ecf20e | |||
| eae33b61cf | |||
| 9e55b525f1 | |||
| 678b3cb505 | |||
| b4c833b039 | |||
| 597bd021b8 | |||
| ba03118a43 | |||
| 82148408b0 | |||
| 042900e065 | |||
| 874fccc351 | |||
| 14f36872e7 | |||
| 3b6ad3052a | |||
| 194fc6b6ed | |||
| 0275ad54ac | |||
| 7ca4044467 | |||
| 1672494ee9 | |||
| bbbbc72d22 | |||
| 1e7f49b756 | |||
| ac4c0f7ee1 | |||
| 6cc91b1a89 | |||
| 45facff41d | |||
| ee703e6c95 | |||
| 87213c34a6 | |||
| 73a7c76183 | |||
| ed5282f2b3 | |||
| 390c657047 | |||
| 7071072862 | |||
| d111328541 | |||
| 76cb2aaf46 | |||
| 7ff1397163 | |||
| bcf5d49a3d | |||
| 4fffb17283 | |||
| 83b8bdfdf4 | |||
| cc1538a24d | |||
| eb19199d18 | |||
| 441e5ef8d5 | |||
| d768774aad | |||
| c3d83907a5 | |||
| b52a98ae5e | |||
| a70e4089e3 | |||
| 397989769c | |||
| 796676abdc | |||
| 3c93ccb86c | |||
| fc8c74184a | |||
| 1939e6b5cf | |||
| 1c2ac9385d | |||
| 7513f1fe07 | |||
| 63e2c02daa | |||
| fec76385e1 | |||
| bf21428746 | |||
| 01ab8dad17 | |||
| 8974f73841 | |||
| d3a2a2413c | |||
| 9b52863270 | |||
| 733914a7a7 | |||
| 7b0ab1ea4d | |||
| 51ff95f071 | |||
| fc7aca46ce | |||
| 78e34caff3 | |||
| d2aecd4dee | |||
| 9be1083495 | |||
| 38eb9ee13f | |||
| 15b36dfc57 | |||
| 40a83f7cac | |||
| fd9f912c18 | |||
| eef6e60dbb | |||
| f0de5275b9 | |||
| 4eb5c0263c | |||
| 7a0d997a81 | |||
| 17b7c6d357 | |||
| 206f2cb306 | |||
| 1bc433097b | |||
| 9bee5fbe99 | |||
| e819798d80 | |||
| 14ff9eb527 | |||
| a60f45ce31 | |||
| 0b9a96209f | |||
| 3747e58e4e | |||
| 28b1788dc1 | |||
| 12ab2954b0 | |||
| d0e8937f03 | |||
| 1a7ab97a3a | |||
| ef4145787b | |||
| 899e4c1b14 | |||
| faa4385087 | |||
| 172d9fd093 | |||
| 093716baaa | |||
| 0165ad54b3 | |||
| b52dd33102 | |||
| 00aed90f2f | |||
| 5f3d7c0213 | |||
| 401f051774 | |||
| e14b48f3eb | |||
| 1972f83b86 | |||
| 55a11bbeee | |||
| ca32ee3a0e | |||
| 159a370286 | |||
| d1904d52d9 | |||
| 5e993fd91b | |||
| bada7c93d7 | |||
| 38850597f3 | |||
| da347492d3 | |||
| 2ec5d8db3c | |||
| fd9c5040bf | |||
| aa328f0add | |||
| 9743d7b87b | |||
| 98fb0c433e | |||
| 7c9fcd9f43 | |||
| 54c76d9968 | |||
| 40b3f0fedc | |||
| dcfcf83430 | |||
| e46b354643 | |||
| f497e4e301 | |||
| b65e4b3819 | |||
| d166b036fc | |||
| ddf5f22037 | |||
| 7d8a9f2b6d | |||
| 0a1695e3d7 | |||
| 4a653d4935 | |||
| 2bc31a10c5 | |||
| e8d2c6214b | |||
| 3a0f2687e9 | |||
| 04c98326b2 | |||
| eb22c7f303 | |||
| d51a4d7a7e | |||
| 0fb705e1e2 | |||
| 10b849ef09 | |||
| d7d3b23cea | |||
| c3786d434e | |||
| 9e3b92014a | |||
| f4e088131b | |||
| e55e069fe3 | |||
| d8d3767798 | |||
| 7e99b1ac78 | |||
| 6ff3a73bf2 | |||
| 2a43b52344 | |||
| abff80ec23 | |||
| a4edf86195 | |||
| 0d0da6bfec | |||
| e0c8ece9b5 | |||
| 4d875bc3d4 | |||
| 3a6e23bcef | |||
| efd0716707 | |||
| c94a5fb743 | |||
| 047011f60b | |||
| a54ed3a51a | |||
| c37f09bfcd | |||
| 1c7042463d | |||
| dcb003f9ab | |||
| 7dbda3cee7 | |||
| 26bee229a1 | |||
| 5bf2beb179 | |||
| 4a5c551678 | |||
| 277894215d | |||
| 684e08a3a1 | |||
| 19dbc2f9b9 | |||
| 833a1e06f0 | |||
| daca0831a4 | |||
| 337889c5f1 | |||
| 244d2d3866 | |||
| c0fed0ba4f | |||
| affb107b9d | |||
| f96073af99 | |||
| 496a0483d2 | |||
| e11dca00bb | |||
| fde39bf34e | |||
| 4f11bae238 | |||
| f6282ba71f | |||
| 3edf1f4e1b | |||
| 41bc064083 | |||
| eb8562e6b0 | |||
| 68fbdd92c3 | |||
| 02038a5d93 | |||
| 4fb8c2f4b2 | |||
| 7afffa60c3 | |||
| 0e6c860360 | |||
| ebfbbfa08b | |||
| b5f182dfec | |||
| 4cf28d0ad0 | |||
| 149bb049a5 | |||
| 124702f0a2 | |||
| c1cebe578b | |||
| e46c1ee849 | |||
| 70f1743114 | |||
| 54d520727e | |||
| 1f1e4db486 | |||
| e536236634 | |||
| 140c236da5 | |||
| 69ede34274 | |||
| fcf6e22132 | |||
| 7438ee8308 | |||
| f01cf7fcb5 | |||
| 7a852f78e4 | |||
| 0923659a49 | |||
| f9feb08607 | |||
| 3b43fe39e5 | |||
| de30fa15b3 | |||
| 25a4d7c14d | |||
| c8d3607efe | |||
| a0e73a9aa9 | |||
| eaccf237a4 | |||
| 5124266346 | |||
| 5cf2ea5a1e | |||
| 7a1af5914e | |||
| e61f5eeb76 | |||
| 6d92106f9d | |||
| 8b06745e86 | |||
| 85ad999975 | |||
| 4c0f2d84cc | |||
| 6fc9803431 | |||
| 59a710bae5 | |||
| 98c642e1a8 | |||
| e91f4470fb | |||
| b33cc5284f | |||
| eea6db6814 | |||
| ba622c7edf | |||
| b52e89d614 | |||
| 49cd3a0494 | |||
| 75cc16c5e0 | |||
| e674d22ecd | |||
| dca5011eb1 | |||
| 4d8e38b704 | |||
| 5ba4352641 | |||
| 1b2cc11a97 | |||
| a3591e4bbb | |||
| aa0f5639b1 | |||
| f3abd0d9fc | |||
| 4a62aff7d2 | |||
| c78e624eaf | |||
| 934cf5d21c | |||
| f252d1395a | |||
| 2dc0472c69 | |||
| 3e09adc4d1 | |||
| 11750b9382 | |||
| 30a4c2199a | |||
| 406a9f996e | |||
| 5373579bd5 | |||
| 9b4cc201e7 | |||
| 6f2c96c2b6 | |||
| 1f6104de8b | |||
| e078a2ab27 | |||
| 5695c17908 | |||
| e2c1081d5a | |||
| bcbcbc91c7 | |||
| 5eb3566e8d | |||
| d75eca8dd4 | |||
| 640c16d8dc | |||
| 1ba5c5a7a6 | |||
| f67af69dda | |||
| 5d47777307 | |||
| ab22bb9804 | |||
| 2626462e49 | |||
| 41893d79c0 | |||
| 834c1ba63d | |||
| 633ee63891 | |||
| eb5627c0d0 | |||
| 6db38f6e3d | |||
| c69a758429 | |||
| cee3a0ffec | |||
| 18c0143186 | |||
| bbf0b05b49 | |||
| 44723c56ad | |||
| e53c36b53b | |||
| 80f26cd4b8 | |||
| b023414cd0 | |||
| 2f56104565 | |||
| 9cd5fefdca | |||
| 25c42c475f | |||
| 96e66da071 | |||
| 6914b9ee1b | |||
| cfc6546c97 | |||
| 0880313659 | |||
| 875ca02126 | |||
| c2d5925053 | |||
| 547bbf8e95 | |||
| 2218251b03 | |||
| 4da3a23162 | |||
| 28a90baf88 | |||
| 7bbdda2f2f | |||
| 884b444a41 | |||
| e1ff2df36e | |||
| 61c0111778 | |||
| bfc9a64e07 | |||
| 2ba92045cc | |||
| aeca9f51c8 | |||
| b107c0ac1d | |||
| 65a04b4784 | |||
| eab9f50cfd | |||
| b8bb83b524 | |||
| 93eb9fe3b9 | |||
| 5f167512f5 | |||
| a727b81263 | |||
| f27c9192d1 | |||
| 90153fa17f | |||
| cdfaa01852 | |||
| 33d2c3b00d | |||
| b7f992cdc0 | |||
| 0a6a24e309 | |||
| 153b4cffef | |||
| f09a413232 | |||
| cba58f6ae2 | |||
| 2bf4b91488 | |||
| b60b7f4307 | |||
| e4ca04a096 | |||
| d0f7ecec44 | |||
| f488811f01 | |||
| d212cda1e1 | |||
| ba760eac59 | |||
| 8549b5ea46 | |||
| 0b3c106c6f | |||
| 84e7ee4ef3 | |||
| e5d498ea6e | |||
| 6da835a2ca | |||
| 1f9a71e6ac | |||
| 9c92fdc257 | |||
| da219228fa | |||
| c0a6455d08 | |||
| 709e2a9ed4 | |||
| c3ac9f01d2 | |||
| 65eba3795f | |||
| 341cdb5dbe | |||
| 4f43c2ce45 | |||
| 2ec691fc6b | |||
| 81ed321654 | |||
| 6ad37c70f1 | |||
| c7ffd6d82d | |||
| ae4b0fd8d3 | |||
| beceaba44d | |||
| f5987d9767 | |||
| 616712b338 | |||
| 076a968476 | |||
| e60eb703c6 | |||
| cdede639f2 | |||
| 23264d71f0 | |||
| 9caafc4303 | |||
| 48a3690a39 | |||
| b66a8ca44d | |||
| 82087e1187 | |||
| fdfd0438c3 | |||
| 28027b5288 | |||
| ba54005753 | |||
| c6758b11b5 | |||
| 6c29e5e9a4 | |||
| 17e0db2ffc | |||
| 490ea59499 | |||
| f4db6bcf63 | |||
| 90f89de957 | |||
| 9612b868f2 | |||
| 5f8ea93f36 | |||
| fc132f7282 | |||
| 6f9bb6caa7 | |||
| 0e5b88de8f | |||
| 5b4f51981e | |||
| 3dcee45e9f | |||
| 3573a3bec3 | |||
| 796bad1c1c | |||
| 77042f6fae | |||
| 013ac308f7 | |||
| d703582f19 | |||
| 1db80f740d | |||
| a0d2740280 | |||
| 297083f3c4 | |||
| 15a4ad978a | |||
| 63f4cfac83 | |||
| ca849fb19e | |||
| 3de3070ab7 | |||
| cbea4bab7c | |||
| fe8b825c34 | |||
| daa0394960 | |||
| e5aba5d99b | |||
| c4847eb3de | |||
| 5a5f911453 | |||
| 22cef29c27 | |||
| ef41641680 | |||
| 69135e8707 | |||
| 5daef71147 | |||
| 5ffc5ec502 | |||
| 35063db3e6 | |||
| 868c24bb8b | |||
| 7367baffb8 | |||
| 819ff2995a | |||
| 3b5d04b717 | |||
| b673cd73ac | |||
| 649c1a022b | |||
| 034e58bc9d | |||
| a95f280102 | |||
| df8da05f32 | |||
| 635581719b | |||
| 77d5e203e8 | |||
| 370d002b25 | |||
| 18f0fe47ff | |||
| cccd6139fc | |||
| 1fadca8524 | |||
| af01e2ac06 | |||
| de22e16cd4 | |||
| e073b19343 | |||
| a81a05cd45 | |||
| fc67281d2a | |||
| ece35b9a1c | |||
| ed8fb7fa82 | |||
| 3688dd4634 | |||
| 7ff445ef55 | |||
| e4847b4c76 | |||
| 5f5d8f1d2f | |||
| a45aa489e8 | |||
| 9b86ba9e35 | |||
| f5b74ecd78 | |||
| 2d0ab355d6 | |||
| e41235f76b | |||
| 3045f7000e | |||
| c0c2dfb657 | |||
| 622cafbfd6 | |||
| d8a1f66af9 | |||
| c34cce63b0 | |||
| f8f17b5d38 | |||
| 1d3a194d89 | |||
| 056384fdab | |||
| 7c40d074f3 | |||
| c61595eb0d | |||
| 7192c970fa | |||
| cfa709c651 | |||
| 97c467af41 | |||
| 6039426bac | |||
| 862fb90de9 | |||
| 842c32f29f | |||
| 0501de1658 | |||
| dbe26847c7 | |||
| 806290f0a5 | |||
| 325c643314 | |||
| 4adc0affbe | |||
| b5026095a0 | |||
| fa7da3be10 | |||
| 35b44f1955 | |||
| 4708ee8823 | |||
| 52471f2ace | |||
| 0b065c745d | |||
| 9ce8244065 | |||
| b7fafa1bf9 | |||
| 114c974ce5 | |||
| e035925d25 | |||
| c0fda6fcba | |||
| 75c90e3c45 | |||
| 9960f49698 | |||
| 1a2c4cc9a1 | |||
| ee4f05b07d | |||
| 17ef476ede | |||
| 141b98631c | |||
| 845562bca3 | |||
| 105a41eeea | |||
| 9a9d315e62 | |||
| c42aa93bf7 | |||
| a7664f03aa | |||
| fa341c9a5a | |||
| a15ab4759e | |||
| b8939763d4 | |||
| 51adca8568 | |||
| f646eff048 | |||
| fee0a016d8 | |||
| f040fa5c08 | |||
| 7f3c6b4665 | |||
| 8b806fe0be | |||
| b37d8c2369 | |||
| b5451e9d3d | |||
| b2235d4c38 | |||
| c8ba5d727e | |||
| a3de44cd0a | |||
| 214d9e1c53 | |||
| 92900c3f74 | |||
| e17e566daa | |||
| df4e232087 | |||
| 828a39331b | |||
| a0223a3eee | |||
| 7f6a526b25 | |||
| 9983ea25d2 | |||
| 47166b937f | |||
| 0a09966e81 | |||
| 7ec34e934e | |||
| f2b03e7492 | |||
| 5208bd62c5 | |||
| d9f0854c27 | |||
| 6be125b5cb | |||
| c884c098fd | |||
| f77fe05c92 | |||
| f3bfa8ceba | |||
| ae7d9d87d2 | |||
| 1652040c1c | |||
| 818b7cdff4 | |||
| 48ce359d2d | |||
| e115bf0c6d | |||
| 0415b60ba5 | |||
| 4570fdb05f | |||
| 9d109e7ca9 | |||
| 2574553180 | |||
| 66e77d50bd | |||
| 253bd793d7 | |||
| be30de6728 | |||
| a1455bbb1c | |||
| 1ac19ae3e9 | |||
| 6e6ca209df | |||
| 52699967cd | |||
| 146d20ce86 | |||
| b6959b5990 | |||
| 06649df8b1 | |||
| 0174ed9082 | |||
| adabb281b1 | |||
| 63e710d1ab | |||
| 509a568446 | |||
| 6fd94b53f0 | |||
| 164412fa34 | |||
| 514ca0810e | |||
| 7582f86482 | |||
| 4b4c46e5ae | |||
| 162e156b33 | |||
| bc7d1971ef | |||
| bbdee92f37 | |||
| cf9e830cc7 | |||
| 804e425a87 | |||
| cc21383928 | |||
| 413f4efd69 | |||
| de69605eff | |||
| 9338ba3525 | |||
| 322b6ec615 | |||
| 304232d029 | |||
| f80c3bfe07 | |||
| bb0a62fc8b | |||
| 5bfdca6cd9 | |||
| 447e712a9d | |||
| 8bb03f189d | |||
| 3b0554cd9b | |||
| 858101b0d9 | |||
| a7cf8bee28 | |||
| af1ec7bea9 | |||
| 002bf7ef22 | |||
| 6919e2336d | |||
| 5a5bd22073 | |||
| a726f00f35 | |||
| 79297f8a42 | |||
| e3f70ac253 | |||
| 838b346fcc | |||
| eba9545ccf | |||
| 890ade9495 | |||
| 518ef1e0ec | |||
| bdcecfca72 | |||
| 1363846ac4 | |||
| 30347546a2 | |||
| ac7eb28e91 | |||
| 748405473b | |||
| 1080390bed | |||
| c48725c7dd | |||
| a5287dbadc | |||
| ee5a3b0dd9 | |||
| 3d001541e5 | |||
| b376b229b9 | |||
| 33b6203978 | |||
| 2d803e009c | |||
| 2ddbe38781 | |||
| 96181a2b8d | |||
| 8308b8eaf2 | |||
| 00f26ff529 | |||
| 49dcdf3ae5 | |||
| 409b431d1c | |||
| 6da988e3db | |||
| fd8f8306ee | |||
| 74b342f5c6 | |||
| 2504ec79ee | |||
| 3e7b211b17 | |||
| 13d5514a4c | |||
| f0f9da0f1b | |||
| f6d2c5f473 | |||
| 6e8dd5b250 | |||
| f4779bc50c | |||
| 432baf262d | |||
| a1d68fcde3 | |||
| e053db3dff | |||
| f624bd651e | |||
| 84964c7f91 | |||
| 96f56b468e | |||
| bdc3212f38 | |||
| 508ddf6df2 | |||
| 17af24d179 | |||
| c260e447ea | |||
| 0eb40ae993 | |||
| 18f3e39346 | |||
| 311af02726 | |||
| b799969de8 | |||
| 93541883bb | |||
| 33430bff8d | |||
| 3bc2081540 | |||
| a3b1feabff | |||
| 7af8d3843e | |||
| 2235805800 | |||
| 364b521ec3 | |||
| 3cdaf4a8ee | |||
| b2fc6dcdd0 | |||
| 70e9320463 | |||
| 304c7ed068 | |||
| ddb908f937 | |||
| accd17afd4 | |||
| 9f598b77b4 | |||
| d82fa974b1 | |||
| fcbd4a0d48 | |||
| 5cd2b8845e | |||
| 723ab70170 | |||
| a26bf3eeda | |||
| c33b6463c6 | |||
| df995a3ab2 | |||
| 817f844212 | |||
| fa8113b8d7 | |||
| 99b95d8369 | |||
| fc2e4ff210 | |||
| 1063bf71d6 | |||
| f2af5c45e9 | |||
| b132b0d2f0 | |||
| 7869f99fc8 | |||
| 122f2eb400 | |||
| 3c0f6eeb21 | |||
| 19a109355b | |||
| 703965a0dd | |||
| 66f92c6c60 | |||
| a1cdf6b7a5 | |||
| 554c7b5687 | |||
| 2d987313a7 | |||
| 9eebe32bdf | |||
| 8e9da0ad6f | |||
| 2f20dea611 | |||
| 167baf64a9 | |||
| bd7a214f7f | |||
| f16d2d9a74 | |||
| e984d2c274 | |||
| 8720d087ea | |||
| a1e19b9fcd | |||
| 6b9728dc84 | |||
| 1ce9b7c0c8 | |||
| ff75d3fdc2 | |||
| 8154812570 | |||
| 1dcd2478fc | |||
| 0515806e92 | |||
| f286506ba4 | |||
| 48be736275 | |||
| af0faeab16 | |||
| fb6edee842 | |||
| 1209c4b92a | |||
| e4e668b492 | |||
| 92d3136a23 | |||
| f6e74ddb45 | |||
| c34f332771 | |||
| eb1d71bda6 | |||
| 3cdd5822cb | |||
| 8f924b5ce1 | |||
| 2ae645dce5 | |||
| f848a7119f | |||
| 2fa41db32f | |||
| 9af4516761 | |||
| 189b07124d | |||
| e6e5cdcdcd | |||
| a31e4dab85 | |||
| b2a9397dc7 | |||
| a536df1a02 | |||
| 1a2751563d | |||
| 2dc82b7e83 | |||
| b02d81ae53 | |||
| 773bcc5658 | |||
| 9142b9cfb4 | |||
| 376882d975 | |||
| f330c32ccc | |||
| 58dbb71b53 | |||
| ebd0257f6e | |||
| 945c584fc6 | |||
| 19cd24c37a | |||
| 3994810c4e | |||
| d86e68c77a | |||
| cc7bdefe54 | |||
| 51b32a030a | |||
| 6c76ddd145 | |||
| 0d12cc5dc8 | |||
| cfb756723c | |||
| 0e9f198341 | |||
| 1fa1325630 | |||
| dc79d3a897 | |||
| 486f3ffc96 | |||
| bf030e12f5 | |||
| e80cce9696 | |||
| 5bb9ecce47 | |||
| 5eb09aa54e | |||
| ff261d9939 | |||
| 1624ec87b2 | |||
| f1062f2f45 | |||
| 39341c27bc | |||
| 31a90cec2b | |||
| 617fc63393 | |||
| 802f2cf3eb | |||
| c7efcde868 | |||
| e858179204 | |||
| 6871b0b950 | |||
| e9a27a1585 | |||
| 976b765629 | |||
| 126b9b6516 | |||
| 94dab02b54 | |||
| a893b87730 | |||
| 7db2ddd1f7 | |||
| 748980aa1a | |||
| 96d416066e | |||
| 3955bb16bc | |||
| de9bbf842f | |||
| 336b673746 | |||
| f1b6b1e871 | |||
| fba4c03bb5 | |||
| a32ae5b53f | |||
| 589e0f38fd | |||
| e21116680e | |||
| e1960f5aff | |||
| 327ba57088 | |||
| 04e1b024e2 | |||
| b59fe9b57b | |||
| fab0b756de | |||
| aa2727d0d0 | |||
| ce3dd73a81 | |||
| 2eab209fea | |||
| 02476657bf | |||
| 35602120e8 | |||
| 9e800e08ab | |||
| 09fc20794e | |||
| 3cd95fbfdb | |||
| c243fadacf | |||
| 9c06412ceb | |||
| 8d1f0d5df9 | |||
| a035f42008 | |||
| 838fb041aa | |||
| 7fbab63227 | |||
| ad871723fe | |||
| 8887b44bf7 | |||
| 176beced3a | |||
| a3561ddc6c | |||
| 097bd06021 | |||
| dfdd1efcc8 | |||
| 1ac5a410d4 | |||
| 8c44e849c9 | |||
| 2684bd2af4 | |||
| 01a860aab5 | |||
| e1faf4e54e | |||
| 14dd4d6b99 | |||
| bab21bc8a5 | |||
| 8393b3ce86 | |||
| e15eec9cff | |||
| 32741ed7ab | |||
| 0d77a65bbb | |||
| 6eaac2d7e9 | |||
| 496a0ec92c | |||
| 826329e996 | |||
| 4f57da4a38 | |||
| b3f49d0a34 | |||
| b78b370408 | |||
| 82afcdddd0 | |||
| a33a698f38 | |||
| 3aff1800cd | |||
| ea816ca981 | |||
| a2bace4ede | |||
| 5589d1058b | |||
| a12fc32ff0 | |||
| ce2d1c5e0d | |||
| eb75666c85 | |||
| 4b970cedcc | |||
| fbd9d92f5e | |||
| cdaff4da06 | |||
| fbb17390f2 | |||
| 52273db482 | |||
| a6af25ae88 | |||
| 3ac89d68fb | |||
| 267a43fd97 | |||
| 6d0384b6f1 | |||
| 7ae4be402f | |||
| 46f0b7be5b | |||
| b253f2d947 | |||
| 444ade8afe | |||
| 522dbdd170 | |||
| d89238e1aa | |||
| 52710020ea | |||
| a18b8f4f2b | |||
| 01e0a6570d | |||
| cd6ef8f062 | |||
| ea383c43bd | |||
| 68e08e3866 | |||
| 329ed26e85 | |||
| c830a4cea4 | |||
| e7e088ec83 | |||
| cf6c814eb4 | |||
| 8f62e42ae8 | |||
| 89b4060ba2 | |||
| e4d4b329a0 | |||
| 09c5f41995 | |||
| 02b25787c1 | |||
| cdb050b6c5 | |||
| 21f47b536d | |||
| f14484c986 | |||
| 358dd78dc3 | |||
| d79e072316 | |||
| c24dee567a | |||
| 9f75bafcea | |||
| 2150452629 | |||
| 507a32a08e | |||
| 44dee8e850 | |||
| 5e4d9246c2 | |||
| 9fe7419467 | |||
| 41b2251dfe | |||
| 73fad43573 | |||
| 4676717582 | |||
| 834766e6e7 | |||
| e304dce347 | |||
| 61654aefeb | |||
| 1664aaa25b | |||
| 68f1f64f3d | |||
| 7bad57ca52 | |||
| 1c8e1f0993 | |||
| 6037ae6fc4 | |||
| dc1c5400b8 | |||
| 9ae4688171 | |||
| e3f39234b2 | |||
| 0df0b2d6ac | |||
| 9ce96d0591 | |||
| a7ef0618ba | |||
| cc9b083e5d | |||
| 5af322552c | |||
| fc852281dd | |||
| 0b2f036a22 | |||
| cf2becb5e9 | |||
| 82d8eba1b9 | |||
| 286ad34d94 | |||
| ff0bc6594d | |||
| 7b8113aef1 | |||
| 81d2ef5db5 | |||
| bdea3ef88c | |||
| c62c86fc29 | |||
| 59f698f755 | |||
| 9ac979006e | |||
| da6291a965 | |||
| bf6555e57c | |||
| 35b114220e | |||
| 800bb6a4e9 | |||
| 2c80521f5b | |||
| 6351ce5991 | |||
| 683362f0ee | |||
| 84fc909339 | |||
| 74171e26db | |||
| f25c0cc890 | |||
| 29848053a4 | |||
| 0d9856919e | |||
| 0ae7f2f7b3 | |||
| 3dde6b0ca3 | |||
| 6b28208044 | |||
| 13f855e3c4 | |||
| c870595e98 | |||
| ae19a3f68d | |||
| 545afc41b3 | |||
| 7177d88144 | |||
| 294ed50afd | |||
| 0105fe48f7 | |||
| c7ff23e3d5 | |||
| 23e9d7fde5 | |||
| c93edd8875 | |||
| bde37e38a7 | |||
| c401d63d2f | |||
| 52416dd43d | |||
| 59bd7128ae | |||
| 87f16467bb | |||
| bed0fd00bd | |||
| 0672af98f8 | |||
| 2341eceb65 | |||
| ec5f7245bf | |||
| caa2edcf05 | |||
| dbe78d0aa5 | |||
| 71f2f590a7 | |||
| ed26120581 | |||
| 71bd684b46 | |||
| 02ae19f0c7 | |||
| a46b0d58eb | |||
| d1262d169b | |||
| 4a85c95b02 | |||
| a368927b9f | |||
| 80feb69af5 | |||
| c09f0d5787 | |||
| ff667546b8 | |||
| 6a255cdfa4 | |||
| 2b5784df6f | |||
| a9665dd2d8 | |||
| 1e52877e93 | |||
| 062c0d8ddb | |||
| a7a70b448f | |||
| 3e52aeb804 | |||
| 4b4f5d145b | |||
| 775fa5ea62 | |||
| 34d8329c8a | |||
| 2e7ae732aa | |||
| 424287e258 | |||
| 40d03bbb96 | |||
| d768694445 | |||
| 56c7f2ef69 | |||
| 3839b0c59c | |||
| 77cab14ae8 | |||
| b0dbd4c7ca | |||
| 50ca2e0e69 | |||
| 0bf8beda94 | |||
| 4d334929c9 | |||
| ccfdf096f9 | |||
| b4f2af2778 | |||
| 32b9e4855c | |||
| bdbce5147e | |||
| a4833506ae | |||
| d4e8072248 | |||
| 207ca93735 | |||
| 99307ab8f0 | |||
| a5bb39ac8a | |||
| 073c7c0410 | |||
| 7e88e3ba4f | |||
| 1ec23a7b39 | |||
| 685f9e220c | |||
| e77b7eb52e | |||
| 29014704e0 | |||
| b3570d9c0b | |||
| 6e427cee82 | |||
| 0a1b6d00d9 | |||
| 00ffe66f36 | |||
| 63661fbdaa | |||
| 4d4a4543c5 | |||
| 2349805968 | |||
| bdbd0b154e | |||
| 653137ec58 | |||
| 39f65850be | |||
| 2d8db97417 | |||
| 17a6f80798 | |||
| 381fe859ff | |||
| e2bdf17b82 | |||
| d20afc6801 | |||
| 54046a27e6 | |||
| 36c000d18a | |||
| 2f3c2cf4d5 | |||
| 892358d5d8 | |||
| 6dced903cd | |||
| f8a98a426e | |||
| 011506e99f | |||
| 966151d3fe | |||
| 247f4db77e | |||
| 9a661bc401 | |||
| f5f1b3816c | |||
| 3971c9badc | |||
| 390fbf046b | |||
| ec7ba59528 | |||
| 838941c6a4 | |||
| a0ec764b64 | |||
| 26f1f7099b | |||
| 613e9ac8bf | |||
| ea979f02b5 | |||
| 383f55f809 | |||
| ce61f177dc | |||
| 6a64157537 | |||
| de83302c8a | |||
| 68553d3807 | |||
| 4cfe3394b1 | |||
| ae9aa75ba0 | |||
| 3d7ed12d4b | |||
| f70be5bce9 | |||
| 405667697e | |||
| 3aeda7de81 | |||
| 8a775d662a | |||
| ec0ccbca76 | |||
| 6cdcbb0096 | |||
| 79e3881704 | |||
| 4cf2d429f0 | |||
| fa51952bfa | |||
| b2c4c0a67e | |||
| cf2c58637b | |||
| a2c262441e | |||
| 237be79680 | |||
| 441a64b8c6 | |||
| 2128488658 | |||
| d9dc8317f5 | |||
| 6eda2180e4 | |||
| 57a2f0bebf | |||
| 1fd3ccaa0c | |||
| 528597d0c7 | |||
| 37411e3e6b | |||
| 6d681b92aa | |||
| 73fae63905 | |||
| 216710974a | |||
| f3bbef7412 | |||
| fa74d9c598 | |||
| 113364af26 | |||
| 2990db305e | |||
| db8942fe5a | |||
| 402d63bd9a |
@@ -1,3 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: v2rayNG程序问题
|
||||||
|
about: 创建一个报告来帮助我们改进
|
||||||
|
---
|
||||||
|
|
||||||
在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。
|
在提出问题前请先自行排除服务器端问题,同时也请通过搜索确认是否有人提出过相同问题。
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +19,8 @@
|
|||||||
|
|
||||||
### 日志信息
|
### 日志信息
|
||||||
<details>
|
<details>
|
||||||
通过 `adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main` 获取日志。请自行删减日志中可能出现的敏感信息。
|
|
||||||
|
通过`adb logcat -s com.v2ray.ang GoLog V2rayConfigUtilGoLog Main`获取日志。请自行删减日志中可能出现的敏感信息。
|
||||||
|
|
||||||
如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。
|
如果问题可重现,建议先执行`adb logcat -c`清空系统日志再执行上述命令,再操作重现问题。
|
||||||
```
|
```
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: V2Ray程序问题
|
||||||
|
url: https://github.com/v2fly/v2ray-core/
|
||||||
|
about: 如果您有V2Ray而非v2rayNG的问题,请至这个链接讨论。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
name: Build APK
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
fetch-depth: '0'
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v4.0.1
|
||||||
|
with:
|
||||||
|
log-accepted-android-sdk-licenses: false
|
||||||
|
cmdline-tools-version: '13114758'
|
||||||
|
packages: 'platforms;android-36.1 build-tools;36.1.0 platform-tools'
|
||||||
|
|
||||||
|
- name: Install NDK
|
||||||
|
run: |
|
||||||
|
echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
|
||||||
|
--channel=0 \
|
||||||
|
--install "ndk;28.2.13676358"
|
||||||
|
echo "NDK_HOME=$ANDROID_HOME/ndk/28.2.13676358" >> $GITHUB_ENV
|
||||||
|
sed -i '10i\
|
||||||
|
\
|
||||||
|
ndkVersion = "28.2.13676358"' ${{ github.workspace }}/V2rayNG/app/build.gradle.kts
|
||||||
|
|
||||||
|
- name: Restore cached libhevtun
|
||||||
|
id: cache-libhevtun-restore
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/libs
|
||||||
|
key: libhevtun-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hev-socks5-tunnel/HEAD') }}-${{ hashFiles('compile-hevtun.sh') }}
|
||||||
|
|
||||||
|
- name: Build libhevtun
|
||||||
|
if: steps.cache-libhevtun-restore.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
bash compile-hevtun.sh
|
||||||
|
|
||||||
|
- name: Save libhevtun
|
||||||
|
if: steps.cache-libhevtun-restore.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/libs
|
||||||
|
key: libhevtun-${{ runner.os }}-${{ env.NDK_HOME }}-${{ hashFiles('.git/modules/hev-socks5-tunnel/HEAD') }}-${{ hashFiles('compile-hevtun.sh') }}
|
||||||
|
|
||||||
|
- name: Copy libhevtun
|
||||||
|
run: |
|
||||||
|
cp -r ${{ github.workspace }}/libs ${{ github.workspace }}/V2rayNG/app
|
||||||
|
|
||||||
|
- name: Fetch AndroidLibXrayLite tag
|
||||||
|
run: |
|
||||||
|
pushd AndroidLibXrayLite
|
||||||
|
CURRENT_TAG=$(git describe --tags --abbrev=0)
|
||||||
|
echo "Current tag in this repo: $CURRENT_TAG"
|
||||||
|
echo "CURRENT_TAG=$CURRENT_TAG" >> $GITHUB_ENV
|
||||||
|
popd
|
||||||
|
|
||||||
|
- name: Download libv2ray
|
||||||
|
uses: robinraju/release-downloader@v1.13
|
||||||
|
with:
|
||||||
|
repository: '2dust/AndroidLibXrayLite'
|
||||||
|
tag: ${{ env.CURRENT_TAG }}
|
||||||
|
fileName: 'libv2ray.aar'
|
||||||
|
out-file-path: V2rayNG/app/libs/
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v5
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '21'
|
||||||
|
|
||||||
|
- name: Decode Keystore
|
||||||
|
uses: timheuer/base64-to-file@v2.0.0
|
||||||
|
id: android_keystore
|
||||||
|
with:
|
||||||
|
fileName: "android_keystore.jks"
|
||||||
|
encodedString: ${{ secrets.APP_KEYSTORE_BASE64 }}
|
||||||
|
|
||||||
|
- name: Build APK
|
||||||
|
run: |
|
||||||
|
cd ${{ github.workspace }}/V2rayNG
|
||||||
|
echo "sdk.dir=${ANDROID_HOME}" > local.properties
|
||||||
|
chmod 755 gradlew
|
||||||
|
./gradlew licenseFdroidReleaseReport
|
||||||
|
./gradlew assembleRelease -Pandroid.injected.signing.store.file=${{ steps.android_keystore.outputs.filePath }} -Pandroid.injected.signing.store.password=${{ secrets.APP_KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.APP_KEYSTORE_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.APP_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Upload arm64-v8a APK
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: ${{ success() }}
|
||||||
|
with:
|
||||||
|
name: arm64-v8a
|
||||||
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*arm64-v8a*.apk
|
||||||
|
|
||||||
|
- name: Upload armeabi-v7a APK
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: ${{ success() }}
|
||||||
|
with:
|
||||||
|
name: armeabi-v7a
|
||||||
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*armeabi-v7a*.apk
|
||||||
|
|
||||||
|
- name: Upload x86 APK
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
if: ${{ success() }}
|
||||||
|
with:
|
||||||
|
name: x86-apk
|
||||||
|
path: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*x86*.apk
|
||||||
|
|
||||||
|
- name: Upload to release
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
if: github.event.inputs.release_tag != ''
|
||||||
|
with:
|
||||||
|
file: ${{ github.workspace }}/V2rayNG/app/build/outputs/apk/*/release/*.apk
|
||||||
|
tag: ${{ github.event.inputs.release_tag }}
|
||||||
|
file_glob: true
|
||||||
|
prerelease: true
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
name: Validate Fastlane metadata
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
go:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Validate Fastlane Supply Metadata
|
||||||
|
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2.1.0
|
||||||
+64
-9
@@ -1,11 +1,66 @@
|
|||||||
V2rayNG/app/src/main/res/layout/activity_inapp_buy.xml
|
# Ignore data and key store files
|
||||||
V2rayNG/app/src/main/assets/geoip.dat
|
|
||||||
V2rayNG/app/src/main/assets/geosite.dat
|
|
||||||
V2rayNG/app/src/main/java/com/v2ray/ang/InappBuyActivity.java
|
|
||||||
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
|
||||||
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
|
||||||
*.dat
|
*.dat
|
||||||
*.jks
|
*.jks
|
||||||
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
|
||||||
V2rayNG/gradle/wrapper/gradle-wrapper.properties
|
# Ignore output JSON file
|
||||||
V2rayNG/app/release/output.json
|
V2rayNG/app/release/output.json
|
||||||
|
|
||||||
|
# Ignore IDE and build system directories
|
||||||
|
.idea/
|
||||||
|
.gradle/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Ignore local properties and DS_Store files
|
||||||
|
/local.properties
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Ignore build directories and captures
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
V2rayNG/app/build
|
||||||
|
V2rayNG/build
|
||||||
|
V2rayNG/local.properties
|
||||||
|
|
||||||
|
# Ignore APK and AAR files
|
||||||
|
*.apk
|
||||||
|
*.aar
|
||||||
|
|
||||||
|
# Ignore signing properties
|
||||||
|
signing.properties
|
||||||
|
|
||||||
|
# Ignore shared object files
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Ignore Google services JSON
|
||||||
|
V2rayNG/app/google-services.json
|
||||||
|
|
||||||
|
# Additional common Android/Java ignores
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*.orig
|
||||||
|
*.class
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
|
||||||
|
# Ignore executable files
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.obj
|
||||||
|
*.o
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# Ignore files from other IDEs
|
||||||
|
.vscode/
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
|
||||||
|
# Ignore OS-specific files
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[submodule "AndroidLibXrayLite"]
|
||||||
|
path = AndroidLibXrayLite
|
||||||
|
url = https://github.com/2dust/AndroidLibXrayLite
|
||||||
|
[submodule "hev-socks5-tunnel"]
|
||||||
|
path = hev-socks5-tunnel
|
||||||
|
url = https://github.com/heiher/hev-socks5-tunnel
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
sudo: required
|
|
||||||
language: go
|
|
||||||
go:
|
|
||||||
- "1.12"
|
|
||||||
go_import_path: github.com/2dust/AndroidLibV2rayLite
|
|
||||||
git:
|
|
||||||
depth: 5
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
update: true
|
|
||||||
before_script:
|
|
||||||
- sudo ntpdate -u time.google.com
|
|
||||||
- date
|
|
||||||
- make all
|
|
||||||
- make downloadGoMobile
|
|
||||||
script:
|
|
||||||
- make BuildMobile
|
|
||||||
after_success:
|
|
||||||
deploy:
|
|
||||||
provider: releases
|
|
||||||
api_key: ${GH_TOKEN}
|
|
||||||
file:
|
|
||||||
- libv2ray.aar
|
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package CoreI
|
|
||||||
|
|
||||||
import (
|
|
||||||
v2core "v2ray.com/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Status struct {
|
|
||||||
IsRunning bool
|
|
||||||
PackageName string
|
|
||||||
|
|
||||||
Vpoint v2core.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckVersion() int {
|
|
||||||
return 20
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Status) GetDataDir() string {
|
|
||||||
return v.PackageName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Status) GetApp(name string) string {
|
|
||||||
return v.PackageName + name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Status) GetTun2socksArgs(localDNS bool, enableIPv6 bool) (ret []string) {
|
|
||||||
ret = []string{"--netif-ipaddr",
|
|
||||||
"26.26.26.2",
|
|
||||||
"--netif-netmask",
|
|
||||||
"255.255.255.252",
|
|
||||||
"--socks-server-addr",
|
|
||||||
"127.0.0.1:10808",
|
|
||||||
"--tunmtu",
|
|
||||||
"1500",
|
|
||||||
"--loglevel",
|
|
||||||
"notice",
|
|
||||||
"--enable-udprelay",
|
|
||||||
"--sock-path",
|
|
||||||
v.GetDataDir() + "sock_path",
|
|
||||||
}
|
|
||||||
|
|
||||||
if enableIPv6 {
|
|
||||||
ret = append(ret, "--netif-ip6addr", "da26:2626::2")
|
|
||||||
}
|
|
||||||
|
|
||||||
if localDNS {
|
|
||||||
ret = append(ret, "--dnsgw", "127.0.0.1:10807")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Status) GetVPNSetupArg(localDNS bool, enableIPv6 bool) (ret string) {
|
|
||||||
ret = "m,1500 a,26.26.26.1,30 r,0.0.0.0,0"
|
|
||||||
|
|
||||||
if enableIPv6 {
|
|
||||||
ret += " a,da26:2626::1,126 r,::,0"
|
|
||||||
}
|
|
||||||
if localDNS {
|
|
||||||
ret += " d,26.26.26.2"
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
pb:
|
|
||||||
go get -u github.com/golang/protobuf/protoc-gen-go
|
|
||||||
@echo "pb Start"
|
|
||||||
asset:
|
|
||||||
bash gen_assets.sh download
|
|
||||||
mkdir assets
|
|
||||||
cp -v data/*.dat assets/
|
|
||||||
# cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geosite.dat > geosite.dat
|
|
||||||
# cd assets;curl https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/data/geoip.dat > geoip.dat
|
|
||||||
|
|
||||||
shippedBinary:
|
|
||||||
cd shippedBinarys; $(MAKE) shippedBinary
|
|
||||||
|
|
||||||
fetchDep:
|
|
||||||
-go get github.com/2dust/AndroidLibV2rayLite
|
|
||||||
go get github.com/2dust/AndroidLibV2rayLite
|
|
||||||
|
|
||||||
ANDROID_HOME=$(HOME)/android-sdk-linux
|
|
||||||
export ANDROID_HOME
|
|
||||||
PATH:=$(PATH):$(GOPATH)/bin
|
|
||||||
export PATH
|
|
||||||
downloadGoMobile:
|
|
||||||
go get golang.org/x/mobile/cmd/...
|
|
||||||
sudo apt-get install -qq libstdc++6:i386 lib32z1 expect
|
|
||||||
cd ~ ;curl -L https://raw.githubusercontent.com/2dust/AndroidLibV2rayLite/master/ubuntu-cli-install-android-sdk.sh | sudo bash - > /dev/null
|
|
||||||
ls ~
|
|
||||||
ls ~/android-sdk-linux/
|
|
||||||
gomobile init ;gomobile bind -v -tags json github.com/2dust/AndroidLibV2rayLite
|
|
||||||
|
|
||||||
BuildMobile:
|
|
||||||
@echo Stub
|
|
||||||
|
|
||||||
all: asset pb shippedBinary fetchDep
|
|
||||||
@echo DONE
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package Escort
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/CoreI"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (v *Escorting) EscortRun(proc string, pt []string, additionalEnv string, sendFd func() int) {
|
|
||||||
log.Println(proc, pt)
|
|
||||||
count := 0
|
|
||||||
for count <= 42 {
|
|
||||||
cmd := exec.Command(proc, pt...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
if len(additionalEnv) > 0 {
|
|
||||||
//additionalEnv := "FOO=bar"
|
|
||||||
newEnv := append(os.Environ(), additionalEnv)
|
|
||||||
cmd.Env = newEnv
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
log.Println("EscortRun cmd.Start err", err)
|
|
||||||
goto CMDERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.escortProcess == nil {
|
|
||||||
log.Println("EscortRun v.escortProcess nil")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
*v.escortProcess = append(*v.escortProcess, cmd.Process)
|
|
||||||
log.Println("EscortRun Waiting....")
|
|
||||||
|
|
||||||
if count > 0 {
|
|
||||||
go func() {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
sendFd()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
log.Println("EscortRun cmd.Wait err:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
CMDERROR:
|
|
||||||
if v.Status.IsRunning {
|
|
||||||
log.Println("EscortRun Unexpected Exit, Restart now.")
|
|
||||||
count++
|
|
||||||
} else {
|
|
||||||
log.Println("EscortRun Exit")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Escorting) EscortingUp() {
|
|
||||||
if v.escortProcess != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
v.escortProcess = new([](*os.Process))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Escorting) EscortingDown() {
|
|
||||||
if v.escortProcess == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("EscortingDown() Killing all escorted process ")
|
|
||||||
for _, pr := range *v.escortProcess {
|
|
||||||
pr.Kill()
|
|
||||||
if _, err := pr.Wait(); err != nil {
|
|
||||||
log.Println("EscortingDown pr.Wait err:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v.escortProcess = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Escorting struct {
|
|
||||||
escortProcess *[](*os.Process)
|
|
||||||
Status *CoreI.Status
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# AndroidLibV2rayLite
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
package VPN
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
v2net "v2ray.com/core/common/net"
|
|
||||||
v2internet "v2ray.com/core/transport/internet"
|
|
||||||
)
|
|
||||||
|
|
||||||
type protectSet interface {
|
|
||||||
Protect(int) int
|
|
||||||
}
|
|
||||||
|
|
||||||
type resolved struct {
|
|
||||||
domain string
|
|
||||||
IPs []net.IP
|
|
||||||
Port int
|
|
||||||
ipIdx uint8
|
|
||||||
ipLock sync.Mutex
|
|
||||||
lastSwitched time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextIP switch to another resolved result.
|
|
||||||
// there still be race-condition here if multiple err concurently occured
|
|
||||||
// may cause idx keep switching,
|
|
||||||
// but that's an outside error can hardly handled here
|
|
||||||
func (r *resolved) NextIP() {
|
|
||||||
r.ipLock.Lock()
|
|
||||||
defer r.ipLock.Unlock()
|
|
||||||
|
|
||||||
if len(r.IPs) > 1 {
|
|
||||||
|
|
||||||
// throttle, don't switch too quickly
|
|
||||||
now := time.Now()
|
|
||||||
if now.Sub(r.lastSwitched) < time.Second*5 {
|
|
||||||
log.Println("switch too quickly")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.lastSwitched = now
|
|
||||||
r.ipIdx++
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.ipIdx >= uint8(len(r.IPs)) {
|
|
||||||
r.ipIdx = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
cur := r.currentIP()
|
|
||||||
log.Printf("switched to next IP: %s", cur)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resolved) currentIP() net.IP {
|
|
||||||
if len(r.IPs) > 0 {
|
|
||||||
return r.IPs[r.ipIdx]
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPreotectedDialer ...
|
|
||||||
func NewPreotectedDialer(p protectSet) *ProtectedDialer {
|
|
||||||
d := &ProtectedDialer{
|
|
||||||
// prefer native lookup on Android
|
|
||||||
resolver: &net.Resolver{PreferGo: false},
|
|
||||||
protectSet: p,
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProtectedDialer ...
|
|
||||||
type ProtectedDialer struct {
|
|
||||||
currentServer string
|
|
||||||
resolveChan chan struct{}
|
|
||||||
|
|
||||||
vServer *resolved
|
|
||||||
resolver *net.Resolver
|
|
||||||
|
|
||||||
protectSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ProtectedDialer) IsVServerReady() bool {
|
|
||||||
return (d.vServer != nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ProtectedDialer) PrepareResolveChan() {
|
|
||||||
d.resolveChan = make(chan struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ProtectedDialer) ResolveChan() <-chan struct{} {
|
|
||||||
return d.resolveChan
|
|
||||||
}
|
|
||||||
|
|
||||||
// simplicated version of golang: internetAddrList in src/net/ipsock.go
|
|
||||||
func (d *ProtectedDialer) lookupAddr(addr string) (*resolved, error) {
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
host, port string
|
|
||||||
portnum int
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if host, port, err = net.SplitHostPort(addr); err != nil {
|
|
||||||
log.Printf("PrepareDomain SplitHostPort Err: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if portnum, err = d.resolver.LookupPort(ctx, "tcp", port); err != nil {
|
|
||||||
log.Printf("PrepareDomain LookupPort Err: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := d.resolver.LookupIPAddr(ctx, host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(addrs) == 0 {
|
|
||||||
return nil, fmt.Errorf("domain %s Failed to resolve", addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
IPs := make([]net.IP, len(addrs))
|
|
||||||
for i, ia := range addrs {
|
|
||||||
IPs[i] = ia.IP
|
|
||||||
}
|
|
||||||
|
|
||||||
rs := &resolved{
|
|
||||||
domain: host,
|
|
||||||
IPs: IPs,
|
|
||||||
Port: portnum,
|
|
||||||
}
|
|
||||||
|
|
||||||
return rs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareDomain caches direct v2ray server host
|
|
||||||
func (d *ProtectedDialer) PrepareDomain(domainName string, closeCh <-chan struct{}) {
|
|
||||||
log.Printf("Preparing Domain: %s", domainName)
|
|
||||||
d.currentServer = domainName
|
|
||||||
|
|
||||||
defer close(d.resolveChan)
|
|
||||||
maxRetry := 10
|
|
||||||
for {
|
|
||||||
if maxRetry == 0 {
|
|
||||||
log.Println("PrepareDomain maxRetry reached. exiting.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved, err := d.lookupAddr(domainName)
|
|
||||||
if err != nil {
|
|
||||||
maxRetry--
|
|
||||||
log.Printf("PrepareDomain err: %v\n", err)
|
|
||||||
select {
|
|
||||||
case <-closeCh:
|
|
||||||
log.Printf("PrepareDomain exit due to v2ray closed")
|
|
||||||
return
|
|
||||||
case <-time.After(time.Second * 2):
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
d.vServer = resolved
|
|
||||||
log.Printf("Prepare Result:\n Domain: %s\n Port: %d\n IPs: %v\n",
|
|
||||||
resolved.domain, resolved.Port, resolved.IPs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ProtectedDialer) getFd(network v2net.Network) (fd int, err error) {
|
|
||||||
switch network {
|
|
||||||
case v2net.Network_TCP:
|
|
||||||
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP)
|
|
||||||
case v2net.Network_UDP:
|
|
||||||
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("unknow network")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial exported as the protected dial method
|
|
||||||
func (d *ProtectedDialer) Dial(ctx context.Context,
|
|
||||||
src v2net.Address, dest v2net.Destination, sockopt *v2internet.SocketConfig) (net.Conn, error) {
|
|
||||||
|
|
||||||
network := dest.Network.SystemString()
|
|
||||||
Address := dest.NetAddr()
|
|
||||||
|
|
||||||
// v2ray server address,
|
|
||||||
// try to connect fixed IP if multiple IP parsed from domain,
|
|
||||||
// and switch to next IP if error occurred
|
|
||||||
if strings.Compare(Address, d.currentServer) == 0 {
|
|
||||||
if d.vServer == nil {
|
|
||||||
log.Println("Dial pending prepare ...", Address)
|
|
||||||
<-d.resolveChan
|
|
||||||
|
|
||||||
// user may close connection during PrepareDomain,
|
|
||||||
// fast return release resources.
|
|
||||||
if d.vServer == nil {
|
|
||||||
return nil, fmt.Errorf("fail to prepare domain %s", d.currentServer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fd, err := d.getFd(dest.Network)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
curIP := d.vServer.currentIP()
|
|
||||||
conn, err := d.fdConn(ctx, curIP, d.vServer.Port, fd)
|
|
||||||
if err != nil {
|
|
||||||
d.vServer.NextIP()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Printf("Using Prepared: %s", curIP)
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// v2ray connecting to "domestic" servers, no caching results
|
|
||||||
log.Printf("Not Using Prepared: %s,%s", network, Address)
|
|
||||||
resolved, err := d.lookupAddr(Address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fd, err := d.getFd(dest.Network)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// use the first resolved address.
|
|
||||||
// the result IP may vary, eg: IPv6 addrs comes first if client has ipv6 address
|
|
||||||
return d.fdConn(ctx, resolved.IPs[0], resolved.Port, fd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *ProtectedDialer) fdConn(ctx context.Context, ip net.IP, port int, fd int) (net.Conn, error) {
|
|
||||||
|
|
||||||
defer unix.Close(fd)
|
|
||||||
|
|
||||||
// call android VPN service to "protect" the fd connecting straight out
|
|
||||||
d.Protect(fd)
|
|
||||||
|
|
||||||
sa := &unix.SockaddrInet6{
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
copy(sa.Addr[:], ip)
|
|
||||||
|
|
||||||
if err := unix.Connect(fd, sa); err != nil {
|
|
||||||
log.Printf("fdConn unix.Connect err, Close Fd: %d Err: %v", fd, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
file := os.NewFile(uintptr(fd), "Socket")
|
|
||||||
if file == nil {
|
|
||||||
// returned value will be nil if fd is not a valid file descriptor
|
|
||||||
return nil, errors.New("fdConn fd invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
defer file.Close()
|
|
||||||
//Closing conn does not affect file, and closing file does not affect conn.
|
|
||||||
conn, err := net.FileConn(file)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("fdConn FileConn Close Fd: %d Err: %v", fd, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
package VPN
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
v2net "v2ray.com/core/common/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeSupportSet struct{}
|
|
||||||
|
|
||||||
func (f fakeSupportSet) Protect(int) int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProtectedDialer_PrepareDomain(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
domainName string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
{"", args{"baidu.com:80"}},
|
|
||||||
// {"", args{"cloudflare.com:443"}},
|
|
||||||
// {"", args{"apple.com:443"}},
|
|
||||||
// {"", args{"110.110.110.110:443"}},
|
|
||||||
// {"", args{"[2002:1234::1]:443"}},
|
|
||||||
}
|
|
||||||
d := NewPreotectedDialer(fakeSupportSet{})
|
|
||||||
for _, tt := range tests {
|
|
||||||
ch := make(chan struct{})
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
go d.PrepareDomain(tt.args.domainName, ch)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
go d.vServer.NextIP()
|
|
||||||
t.Log(d.vServer.currentIP())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProtectedDialer_Dial(t *testing.T) {
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
{"baidu.com:80", false},
|
|
||||||
{"cloudflare.com:80", false},
|
|
||||||
{"172.16.192.11:80", true},
|
|
||||||
// {"172.16.192.10:80", true},
|
|
||||||
// {"[2fff:4322::1]:443", true},
|
|
||||||
// {"[fc00::1]:443", true},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
ch := make(chan struct{})
|
|
||||||
|
|
||||||
d := NewPreotectedDialer(fakeSupportSet{})
|
|
||||||
d.currentServer = tt.name
|
|
||||||
|
|
||||||
go d.PrepareDomain(tt.name, ch)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
dial := func() {
|
|
||||||
defer wg.Done()
|
|
||||||
dest, _ := v2net.ParseDestination("tcp:" + tt.name)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
conn, err := d.Dial(ctx, nil, dest, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Log(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_host, _, _ := net.SplitHostPort(tt.name)
|
|
||||||
fmt.Fprintf(conn, fmt.Sprintf("GET / HTTP/1.1\r\nHost: %s\r\n\r\n", _host))
|
|
||||||
status, err := bufio.NewReader(conn).ReadString('\n')
|
|
||||||
t.Logf("%#v, %#v\n", status, err)
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
for n := 0; n < 3; n++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go dial()
|
|
||||||
// time.Sleep(time.Millisecond * 10)
|
|
||||||
// d.pendingMap[tt.name] = make(chan struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_resolved_NextIP(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
domain string
|
|
||||||
IPs []net.IP
|
|
||||||
Port int
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
}{
|
|
||||||
// TODO: Add test cases.
|
|
||||||
{"test1",
|
|
||||||
fields{
|
|
||||||
domain: "www.baidu.com",
|
|
||||||
IPs: []net.IP{
|
|
||||||
net.ParseIP("1.2.3.4"),
|
|
||||||
net.ParseIP("4.3.2.1"),
|
|
||||||
net.ParseIP("1234::1"),
|
|
||||||
net.ParseIP("4321::1"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
r := &resolved{
|
|
||||||
domain: tt.fields.domain,
|
|
||||||
IPs: tt.fields.IPs,
|
|
||||||
Port: tt.fields.Port,
|
|
||||||
}
|
|
||||||
t.Logf("%v", r.IPs)
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("%v", r.currentIP())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
||||||
# Set magic variables for current file & dir
|
|
||||||
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
|
|
||||||
__base="$(basename ${__file} .sh)"
|
|
||||||
|
|
||||||
if [[ ! -d $NDK_HOME ]]; then
|
|
||||||
echo "Android NDK: NDK_HOME not found. please set env \$NDK_HOME"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TMPDIR=$(mktemp -d)
|
|
||||||
clear_tmp () {
|
|
||||||
rm -rf $TMPDIR
|
|
||||||
}
|
|
||||||
|
|
||||||
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; clear_tmp; exit 1' ERR INT
|
|
||||||
install -m644 $__dir/tun2socks.mk $TMPDIR/
|
|
||||||
|
|
||||||
pushd $TMPDIR
|
|
||||||
git clone --depth=1 https://github.com/shadowsocks/badvpn.git
|
|
||||||
git clone --depth=1 https://github.com/shadowsocks/libancillary.git
|
|
||||||
$NDK_HOME/ndk-build \
|
|
||||||
NDK_PROJECT_PATH=. \
|
|
||||||
APP_BUILD_SCRIPT=./tun2socks.mk \
|
|
||||||
APP_ABI=all \
|
|
||||||
APP_PLATFORM=android-19 \
|
|
||||||
NDK_LIBS_OUT=$TMPDIR/libs \
|
|
||||||
NDK_OUT=$TMPDIR/tmp \
|
|
||||||
APP_SHORT_COMMANDS=false LOCAL_SHORT_COMMANDS=false -B -j4
|
|
||||||
|
|
||||||
install -v -m755 libs/armeabi-v7a/tun2socks $__dir/shippedBinarys/ArchDep/arm/
|
|
||||||
install -v -m755 libs/arm64-v8a/tun2socks $__dir/shippedBinarys/ArchDep/arm64/
|
|
||||||
install -v -m755 libs/x86/tun2socks $__dir/shippedBinarys/ArchDep/386/
|
|
||||||
install -v -m755 libs/x86_64/tun2socks $__dir/shippedBinarys/ArchDep/amd64/
|
|
||||||
popd
|
|
||||||
|
|
||||||
pushd $__dir/shippedBinarys
|
|
||||||
make clean && make shippedBinary
|
|
||||||
popd
|
|
||||||
|
|
||||||
rm -rf $TMPDIR
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
||||||
# Set magic variables for current file & dir
|
|
||||||
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
|
|
||||||
__base="$(basename ${__file} .sh)"
|
|
||||||
|
|
||||||
|
|
||||||
DATADIR=${__dir}/data
|
|
||||||
|
|
||||||
compile_dat () {
|
|
||||||
local TMPDIR=$(mktemp -d)
|
|
||||||
|
|
||||||
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; rm -rf $TMPDIR; trap ERR; exit 1' ERR
|
|
||||||
|
|
||||||
local GEOSITE=${GOPATH}/src/github.com/v2ray/domain-list-community
|
|
||||||
if [[ -d ${GEOSITE} ]]; then
|
|
||||||
cd ${GEOSITE} && git pull
|
|
||||||
else
|
|
||||||
mkdir -p ${GEOSITE}
|
|
||||||
cd ${GEOSITE} && git clone https://github.com/v2ray/domain-list-community.git .
|
|
||||||
fi
|
|
||||||
go run main.go
|
|
||||||
|
|
||||||
if [[ -e dlc.dat ]]; then
|
|
||||||
rm -f $DATADIR/geosite.dat
|
|
||||||
mv dlc.dat $DATADIR/geosite.dat
|
|
||||||
echo "----------> geosite.dat updated."
|
|
||||||
else
|
|
||||||
echo "----------> geosite.dat failed to update."
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [[ ! -x $GOPATH/bin/geoip ]]; then
|
|
||||||
go get -v -u github.com/v2ray/geoip
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd $TMPDIR
|
|
||||||
curl -L -O http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country-CSV.zip
|
|
||||||
unzip -q GeoLite2-Country-CSV.zip
|
|
||||||
mkdir geoip && find . -name '*.csv' -exec mv -t ./geoip {} +
|
|
||||||
$GOPATH/bin/geoip \
|
|
||||||
--country=./geoip/GeoLite2-Country-Locations-en.csv \
|
|
||||||
--ipv4=./geoip/GeoLite2-Country-Blocks-IPv4.csv \
|
|
||||||
--ipv6=./geoip/GeoLite2-Country-Blocks-IPv6.csv
|
|
||||||
|
|
||||||
if [[ -e geoip.dat ]]; then
|
|
||||||
rm -f $DATADIR/geoip.dat
|
|
||||||
mv ./geoip.dat $DATADIR/geoip.dat
|
|
||||||
echo "----------> geoip.dat updated."
|
|
||||||
else
|
|
||||||
echo "----------> geoip.dat failed to update."
|
|
||||||
fi
|
|
||||||
trap ERR
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
download_dat () {
|
|
||||||
wget -qO - https://api.github.com/repos/v2ray/geoip/releases/latest \
|
|
||||||
| grep browser_download_url | cut -d '"' -f 4 \
|
|
||||||
| wget -i - -O $DATADIR/geoip.dat
|
|
||||||
|
|
||||||
wget -qO - https://api.github.com/repos/v2ray/domain-list-community/releases/latest \
|
|
||||||
| grep browser_download_url | cut -d '"' -f 4 \
|
|
||||||
| wget -i - -O $DATADIR/geosite.dat
|
|
||||||
}
|
|
||||||
|
|
||||||
ACTION="${1:-}"
|
|
||||||
if [[ -z $ACTION ]]; then
|
|
||||||
ACTION=download
|
|
||||||
fi
|
|
||||||
|
|
||||||
case $ACTION in
|
|
||||||
"download") download_dat;;
|
|
||||||
"compile") compile_dat;;
|
|
||||||
esac
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
package libv2ray
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/CoreI"
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/Process/Escort"
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/VPN"
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/shippedBinarys"
|
|
||||||
mobasset "golang.org/x/mobile/asset"
|
|
||||||
|
|
||||||
v2core "v2ray.com/core"
|
|
||||||
v2filesystem "v2ray.com/core/common/platform/filesystem"
|
|
||||||
v2stats "v2ray.com/core/features/stats"
|
|
||||||
v2serial "v2ray.com/core/infra/conf/serial"
|
|
||||||
_ "v2ray.com/core/main/distro/all"
|
|
||||||
v2internet "v2ray.com/core/transport/internet"
|
|
||||||
|
|
||||||
v2applog "v2ray.com/core/app/log"
|
|
||||||
v2commlog "v2ray.com/core/common/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
v2Assert = "v2ray.location.asset"
|
|
||||||
assetperfix = "/dev/libv2rayfs0/asset"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*V2RayPoint V2Ray Point Server
|
|
||||||
This is territory of Go, so no getter and setters!
|
|
||||||
*/
|
|
||||||
type V2RayPoint struct {
|
|
||||||
SupportSet V2RayVPNServiceSupportsSet
|
|
||||||
statsManager v2stats.Manager
|
|
||||||
|
|
||||||
dialer *VPN.ProtectedDialer
|
|
||||||
status *CoreI.Status
|
|
||||||
escorter *Escort.Escorting
|
|
||||||
v2rayOP *sync.Mutex
|
|
||||||
closeChan chan struct{}
|
|
||||||
|
|
||||||
PackageName string
|
|
||||||
DomainName string
|
|
||||||
ConfigureFileContent string
|
|
||||||
EnableLocalDNS bool
|
|
||||||
ForwardIpv6 bool
|
|
||||||
}
|
|
||||||
|
|
||||||
/*V2RayVPNServiceSupportsSet To support Android VPN mode*/
|
|
||||||
type V2RayVPNServiceSupportsSet interface {
|
|
||||||
Setup(Conf string) int
|
|
||||||
Prepare() int
|
|
||||||
Shutdown() int
|
|
||||||
Protect(int) int
|
|
||||||
OnEmitStatus(int, string) int
|
|
||||||
SendFd() int
|
|
||||||
}
|
|
||||||
|
|
||||||
/*RunLoop Run V2Ray main loop
|
|
||||||
*/
|
|
||||||
func (v *V2RayPoint) RunLoop() (err error) {
|
|
||||||
v.v2rayOP.Lock()
|
|
||||||
defer v.v2rayOP.Unlock()
|
|
||||||
//Construct Context
|
|
||||||
v.status.PackageName = v.PackageName
|
|
||||||
|
|
||||||
if !v.status.IsRunning {
|
|
||||||
v.closeChan = make(chan struct{})
|
|
||||||
v.dialer.PrepareResolveChan()
|
|
||||||
go v.dialer.PrepareDomain(v.DomainName, v.closeChan)
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
// wait until resolved
|
|
||||||
case <-v.dialer.ResolveChan():
|
|
||||||
// shutdown VPNService if server name can not reolved
|
|
||||||
if !v.dialer.IsVServerReady() {
|
|
||||||
log.Println("vServer cannot resolved, shutdown")
|
|
||||||
v.StopLoop()
|
|
||||||
v.SupportSet.Shutdown()
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop waiting if manually closed
|
|
||||||
case <-v.closeChan:
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = v.pointloop()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/*StopLoop Stop V2Ray main loop
|
|
||||||
*/
|
|
||||||
func (v *V2RayPoint) StopLoop() (err error) {
|
|
||||||
v.v2rayOP.Lock()
|
|
||||||
defer v.v2rayOP.Unlock()
|
|
||||||
if v.status.IsRunning {
|
|
||||||
close(v.closeChan)
|
|
||||||
v.shutdownInit()
|
|
||||||
v.SupportSet.OnEmitStatus(0, "Closed")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//Delegate Funcation
|
|
||||||
func (v *V2RayPoint) GetIsRunning() bool {
|
|
||||||
return v.status.IsRunning
|
|
||||||
}
|
|
||||||
|
|
||||||
//Delegate Funcation
|
|
||||||
func (v V2RayPoint) QueryStats(tag string, direct string) int64 {
|
|
||||||
if v.statsManager == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
counter := v.statsManager.GetCounter(fmt.Sprintf("inbound>>>%s>>>traffic>>>%s", tag, direct))
|
|
||||||
if counter == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return counter.Set(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *V2RayPoint) shutdownInit() {
|
|
||||||
v.status.IsRunning = false
|
|
||||||
v.status.Vpoint.Close()
|
|
||||||
v.status.Vpoint = nil
|
|
||||||
v.statsManager = nil
|
|
||||||
v.escorter.EscortingDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *V2RayPoint) pointloop() error {
|
|
||||||
if err := v.runTun2socks(); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("EnableLocalDNS: %v\nForwardIpv6: %v\nDomainName: %s",
|
|
||||||
v.EnableLocalDNS,
|
|
||||||
v.ForwardIpv6,
|
|
||||||
v.DomainName)
|
|
||||||
|
|
||||||
log.Println("loading v2ray config")
|
|
||||||
config, err := v2serial.LoadJSONConfig(strings.NewReader(v.ConfigureFileContent))
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("new v2ray core")
|
|
||||||
inst, err := v2core.New(config)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.status.Vpoint = inst
|
|
||||||
v.statsManager = inst.GetFeature(v2stats.ManagerType()).(v2stats.Manager)
|
|
||||||
|
|
||||||
log.Println("start v2ray core")
|
|
||||||
v.status.IsRunning = true
|
|
||||||
if err := v.status.Vpoint.Start(); err != nil {
|
|
||||||
v.status.IsRunning = false
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
v.SupportSet.Prepare()
|
|
||||||
v.SupportSet.Setup(v.status.GetVPNSetupArg(v.EnableLocalDNS, v.ForwardIpv6))
|
|
||||||
v.SupportSet.OnEmitStatus(0, "Running")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initV2Env() {
|
|
||||||
if os.Getenv(v2Assert) != "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//Initialize asset API, Since Raymond Will not let notify the asset location inside Process,
|
|
||||||
//We need to set location outside V2Ray
|
|
||||||
os.Setenv(v2Assert, assetperfix)
|
|
||||||
//Now we handle read
|
|
||||||
v2filesystem.NewFileReader = func(path string) (io.ReadCloser, error) {
|
|
||||||
if strings.HasPrefix(path, assetperfix) {
|
|
||||||
p := path[len(assetperfix)+1:]
|
|
||||||
//is it overridden?
|
|
||||||
//by, ok := overridedAssets[p]
|
|
||||||
//if ok {
|
|
||||||
// return os.Open(by)
|
|
||||||
//}
|
|
||||||
return mobasset.Open(p)
|
|
||||||
}
|
|
||||||
return os.Open(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Delegate Funcation
|
|
||||||
func TestConfig(ConfigureFileContent string) error {
|
|
||||||
initV2Env()
|
|
||||||
_, err := v2serial.LoadJSONConfig(strings.NewReader(ConfigureFileContent))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
/*NewV2RayPoint new V2RayPoint*/
|
|
||||||
func NewV2RayPoint(s V2RayVPNServiceSupportsSet) *V2RayPoint {
|
|
||||||
initV2Env()
|
|
||||||
|
|
||||||
// inject our own log writer
|
|
||||||
v2applog.RegisterHandlerCreator(v2applog.LogType_Console,
|
|
||||||
func(lt v2applog.LogType,
|
|
||||||
options v2applog.HandlerCreatorOptions) (v2commlog.Handler, error) {
|
|
||||||
return v2commlog.NewLogger(createStdoutLogWriter()), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
dialer := VPN.NewPreotectedDialer(s)
|
|
||||||
v2internet.UseAlternativeSystemDialer(dialer)
|
|
||||||
status := &CoreI.Status{}
|
|
||||||
return &V2RayPoint{
|
|
||||||
SupportSet: s,
|
|
||||||
v2rayOP: new(sync.Mutex),
|
|
||||||
status: status,
|
|
||||||
dialer: dialer,
|
|
||||||
escorter: &Escort.Escorting{Status: status},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v V2RayPoint) runTun2socks() error {
|
|
||||||
shipb := shippedBinarys.FirstRun{Status: v.status}
|
|
||||||
if err := shipb.CheckAndExport(); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
v.escorter.EscortingUp()
|
|
||||||
go v.escorter.EscortRun(
|
|
||||||
v.status.GetApp("tun2socks"),
|
|
||||||
v.status.GetTun2socksArgs(v.EnableLocalDNS, v.ForwardIpv6), "",
|
|
||||||
v.SupportSet.SendFd)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*CheckVersion int
|
|
||||||
This func will return libv2ray binding version.
|
|
||||||
*/
|
|
||||||
func CheckVersion() int {
|
|
||||||
return CoreI.CheckVersion()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*CheckVersionX string
|
|
||||||
This func will return libv2ray binding version and V2Ray version used.
|
|
||||||
*/
|
|
||||||
func CheckVersionX() string {
|
|
||||||
return fmt.Sprintf("Libv2rayLite V%d, Core V%s", CheckVersion(), v2core.Version())
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package libv2ray
|
|
||||||
|
|
||||||
//go:generate make all
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
readme.txt
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,13 +0,0 @@
|
|||||||
Platdep=shippedBinary.386 shippedBinary.amd64 shippedBinary.arm64 shippedBinary.arm
|
|
||||||
|
|
||||||
shippedBinaryDep:
|
|
||||||
go get -u github.com/jteeuwen/go-bindata/...
|
|
||||||
|
|
||||||
shippedBinary.%:
|
|
||||||
go-bindata -nometadata -nomemcopy -pkg shippedBinarys -o ./binary_$*.go -tags $* ArchIndep/ ArchDep/$*/
|
|
||||||
|
|
||||||
shippedBinary:shippedBinaryDep $(Platdep)
|
|
||||||
@echo "Done"
|
|
||||||
|
|
||||||
clean:
|
|
||||||
-rm binary*
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package shippedBinarys
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/2dust/AndroidLibV2rayLite/CoreI"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FirstRun struct {
|
|
||||||
Status *CoreI.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FirstRun) checkIfRcExist() error {
|
|
||||||
datadir := v.Status.GetDataDir()
|
|
||||||
if _, err := os.Stat(datadir + strconv.Itoa(CoreI.CheckVersion())); !os.IsNotExist(err) {
|
|
||||||
log.Println("file exists")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
IndepDir, err := AssetDir("ArchIndep")
|
|
||||||
log.Println(IndepDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, fn := range IndepDir {
|
|
||||||
log.Println(datadir+"ArchIndep/"+fn)
|
|
||||||
|
|
||||||
err := RestoreAsset(datadir, "ArchIndep/"+fn)
|
|
||||||
log.Println(err)
|
|
||||||
|
|
||||||
//GrantPremission
|
|
||||||
os.Chmod(datadir+"ArchIndep/"+fn, 0700)
|
|
||||||
log.Println(os.Remove(datadir + fn))
|
|
||||||
log.Println(os.Symlink(datadir+"ArchIndep/"+fn, datadir + fn))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
DepDir, err := AssetDir("ArchDep")
|
|
||||||
log.Println(DepDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, fn := range DepDir {
|
|
||||||
DepDir2, err := AssetDir("ArchDep/" + fn)
|
|
||||||
log.Println("ArchDep/" + fn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, FND := range DepDir2 {
|
|
||||||
log.Println(datadir+"ArchDep/"+fn+"/"+FND)
|
|
||||||
|
|
||||||
RestoreAsset(datadir, "ArchDep/"+fn+"/"+FND)
|
|
||||||
os.Chmod(datadir+"ArchDep/"+fn+"/"+FND, 0700)
|
|
||||||
log.Println(os.Remove(datadir + FND))
|
|
||||||
log.Println(os.Symlink(datadir+"ArchDep/"+fn+"/"+FND, datadir+FND))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s, _ := os.Create(datadir + strconv.Itoa(CoreI.CheckVersion()))
|
|
||||||
s.Close()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *FirstRun) CheckAndExport() error {
|
|
||||||
return v.checkIfRcExist()
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# Copyright (C) 2009 The Android Open Source Project
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
LOCAL_PATH := $(call my-dir)
|
|
||||||
ROOT_PATH := $(LOCAL_PATH)
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
## libancillary
|
|
||||||
########################################################
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
|
|
||||||
ANCILLARY_SOURCE := fd_recv.c fd_send.c
|
|
||||||
|
|
||||||
LOCAL_MODULE := libancillary
|
|
||||||
#LOCAL_CFLAGS += -I$(LOCAL_PATH)/libancillary
|
|
||||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/libancillary
|
|
||||||
LOCAL_SRC_FILES := $(addprefix libancillary/, $(ANCILLARY_SOURCE))
|
|
||||||
|
|
||||||
include $(BUILD_STATIC_LIBRARY)
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
## tun2socks
|
|
||||||
########################################################
|
|
||||||
|
|
||||||
include $(CLEAR_VARS)
|
|
||||||
|
|
||||||
LOCAL_CFLAGS := -std=gnu99
|
|
||||||
LOCAL_CFLAGS += -DBADVPN_THREADWORK_USE_PTHREAD -DBADVPN_LINUX -DBADVPN_BREACTOR_BADVPN -D_GNU_SOURCE
|
|
||||||
LOCAL_CFLAGS += -DBADVPN_USE_SIGNALFD -DBADVPN_USE_EPOLL
|
|
||||||
LOCAL_CFLAGS += -DBADVPN_LITTLE_ENDIAN -DBADVPN_THREAD_SAFE
|
|
||||||
LOCAL_CFLAGS += -DNDEBUG -DANDROID
|
|
||||||
LOCAL_CFLAGS += -I
|
|
||||||
|
|
||||||
LOCAL_STATIC_LIBRARIES := libancillary
|
|
||||||
|
|
||||||
LOCAL_C_INCLUDES := \
|
|
||||||
$(LOCAL_PATH)/badvpn/libancillary \
|
|
||||||
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv4 \
|
|
||||||
$(LOCAL_PATH)/badvpn/lwip/src/include/ipv6 \
|
|
||||||
$(LOCAL_PATH)/badvpn/lwip/src/include \
|
|
||||||
$(LOCAL_PATH)/badvpn/lwip/custom \
|
|
||||||
$(LOCAL_PATH)/badvpn \
|
|
||||||
$(LOCAL_PATH)/libancillary
|
|
||||||
|
|
||||||
TUN2SOCKS_SOURCES := \
|
|
||||||
base/BLog_syslog.c \
|
|
||||||
system/BReactor_badvpn.c \
|
|
||||||
system/BSignal.c \
|
|
||||||
system/BConnection_common.c \
|
|
||||||
system/BConnection_unix.c \
|
|
||||||
system/BTime.c \
|
|
||||||
system/BUnixSignal.c \
|
|
||||||
system/BNetwork.c \
|
|
||||||
flow/StreamRecvInterface.c \
|
|
||||||
flow/PacketRecvInterface.c \
|
|
||||||
flow/PacketPassInterface.c \
|
|
||||||
flow/StreamPassInterface.c \
|
|
||||||
flow/SinglePacketBuffer.c \
|
|
||||||
flow/BufferWriter.c \
|
|
||||||
flow/PacketBuffer.c \
|
|
||||||
flow/PacketStreamSender.c \
|
|
||||||
flow/PacketPassConnector.c \
|
|
||||||
flow/PacketProtoFlow.c \
|
|
||||||
flow/PacketPassFairQueue.c \
|
|
||||||
flow/PacketProtoEncoder.c \
|
|
||||||
flow/PacketProtoDecoder.c \
|
|
||||||
socksclient/BSocksClient.c \
|
|
||||||
tuntap/BTap.c \
|
|
||||||
lwip/src/core/udp.c \
|
|
||||||
lwip/src/core/memp.c \
|
|
||||||
lwip/src/core/init.c \
|
|
||||||
lwip/src/core/pbuf.c \
|
|
||||||
lwip/src/core/tcp.c \
|
|
||||||
lwip/src/core/tcp_out.c \
|
|
||||||
lwip/src/core/netif.c \
|
|
||||||
lwip/src/core/def.c \
|
|
||||||
lwip/src/core/ip.c \
|
|
||||||
lwip/src/core/mem.c \
|
|
||||||
lwip/src/core/tcp_in.c \
|
|
||||||
lwip/src/core/stats.c \
|
|
||||||
lwip/src/core/inet_chksum.c \
|
|
||||||
lwip/src/core/timeouts.c \
|
|
||||||
lwip/src/core/ipv4/icmp.c \
|
|
||||||
lwip/src/core/ipv4/igmp.c \
|
|
||||||
lwip/src/core/ipv4/ip4_addr.c \
|
|
||||||
lwip/src/core/ipv4/ip4_frag.c \
|
|
||||||
lwip/src/core/ipv4/ip4.c \
|
|
||||||
lwip/src/core/ipv4/autoip.c \
|
|
||||||
lwip/src/core/ipv6/ethip6.c \
|
|
||||||
lwip/src/core/ipv6/inet6.c \
|
|
||||||
lwip/src/core/ipv6/ip6_addr.c \
|
|
||||||
lwip/src/core/ipv6/mld6.c \
|
|
||||||
lwip/src/core/ipv6/dhcp6.c \
|
|
||||||
lwip/src/core/ipv6/icmp6.c \
|
|
||||||
lwip/src/core/ipv6/ip6.c \
|
|
||||||
lwip/src/core/ipv6/ip6_frag.c \
|
|
||||||
lwip/src/core/ipv6/nd6.c \
|
|
||||||
lwip/custom/sys.c \
|
|
||||||
tun2socks/tun2socks.c \
|
|
||||||
base/DebugObject.c \
|
|
||||||
base/BLog.c \
|
|
||||||
base/BPending.c \
|
|
||||||
system/BDatagram_unix.c \
|
|
||||||
flowextra/PacketPassInactivityMonitor.c \
|
|
||||||
tun2socks/SocksUdpGwClient.c \
|
|
||||||
udpgw_client/UdpGwClient.c
|
|
||||||
|
|
||||||
LOCAL_MODULE := tun2socks
|
|
||||||
|
|
||||||
LOCAL_LDLIBS := -ldl -llog
|
|
||||||
|
|
||||||
LOCAL_SRC_FILES := $(addprefix badvpn/, $(TUN2SOCKS_SOURCES))
|
|
||||||
|
|
||||||
include $(BUILD_SYSTEM)/build-executable.mk
|
|
||||||
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Thanks to https://gist.github.com/wenzhixin/43cf3ce909c24948c6e7
|
|
||||||
# Execute this script in your home directory. Lines 17 and 21 will prompt you for a y/n
|
|
||||||
|
|
||||||
# Install Oracle JDK 8
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y openjdk-8-jdk
|
|
||||||
apt-get install -y unzip make expect # NDK stuff
|
|
||||||
|
|
||||||
# Get SDK tools (link from https://developer.android.com/studio/index.html#downloads)
|
|
||||||
wget -q https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
|
|
||||||
mkdir android-sdk-linux
|
|
||||||
unzip sdk*.zip -d android-sdk-linux
|
|
||||||
|
|
||||||
# Get NDK (https://developer.android.com/ndk/downloads/index.html)
|
|
||||||
# wget -q https://dl.google.com/android/repository/android-ndk-r15c-linux-x86_64.zip
|
|
||||||
# unzip android-ndk*.zip >> /dev/null
|
|
||||||
|
|
||||||
ACCEPT_LICENSES_URL=https://gist.githubusercontent.com/xiaokangwang/1489fd223d26581bfec92adb3cb0088e/raw/328eb6925099df5aae3e76790f8232f0fc378f8b/accept-licenses
|
|
||||||
|
|
||||||
ACCEPT_LICENSES_ITEM="android-sdk-license-bcbbd656|intel-android-sysimage-license-1ea702d1|android-sdk-license-2742d1c5"
|
|
||||||
|
|
||||||
# Let it update itself and install some stuff
|
|
||||||
cd android-sdk-linux/tools
|
|
||||||
|
|
||||||
curl -L -o accept-licenses $ACCEPT_LICENSES_URL
|
|
||||||
|
|
||||||
chmod +x accept-licenses
|
|
||||||
|
|
||||||
./accept-licenses "./android update sdk --use-sdk-wrapper --all --no-ui" $ACCEPT_LICENSES_ITEM >/dev/null
|
|
||||||
|
|
||||||
# Download every build-tools version that has ever existed
|
|
||||||
# This will save you time! Thank me later for this
|
|
||||||
|
|
||||||
#./accept-licenses "./android update sdk --use-sdk-wrapper --all --no-ui --filter 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27" $ACCEPT_LICENSES_ITEM
|
|
||||||
|
|
||||||
PACKAGE_PARSE_URL=https://gist.githubusercontent.com/xiaokangwang/06268fb23034ed94bc301880e862da09/raw/afd95cbbe2f8c1d9e7b0277b7c5ef39af756a6ee/parse.awk
|
|
||||||
|
|
||||||
reduceout=https://gist.githubusercontent.com/xiaokangwang/4684bdb5c3415b943f52aa4803386480/raw/b46dab1cc60f02c0d87f88f01e27157034218faa/out.awk
|
|
||||||
|
|
||||||
cd bin
|
|
||||||
|
|
||||||
curl -L -o parse.awk $PACKAGE_PARSE_URL
|
|
||||||
|
|
||||||
curl -L -o reduce.awk $reduceout
|
|
||||||
|
|
||||||
sudo apt-get install gawk
|
|
||||||
|
|
||||||
./sdkmanager --verbose --list |awk -f parse.awk > ~/package_to_install
|
|
||||||
|
|
||||||
readarray -t filenames < $HOME/package_to_install
|
|
||||||
|
|
||||||
cat $HOME/package_to_install
|
|
||||||
|
|
||||||
yes|./sdkmanager --verbose "${filenames[@]}" |awk -f reduce.awk
|
|
||||||
|
|
||||||
# If you need additional packages for your app, check available packages with:
|
|
||||||
# ./android list sdk --all
|
|
||||||
|
|
||||||
# install certain packages with:
|
|
||||||
# ./android update sdk --no-ui --all --filter 1,2,3,<...>,N
|
|
||||||
# where N is the number of the package in the list (see previous command)
|
|
||||||
|
|
||||||
./sdkmanager "ndk-bundle"
|
|
||||||
|
|
||||||
# Add the directory containing executables in PATH so that they can be found
|
|
||||||
echo 'export ANDROID_HOME=$HOME/android-sdk-linux' >> ~/.bashrc
|
|
||||||
echo 'export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools' >> ~/.bashrc
|
|
||||||
# echo 'export NDK_HOME=$HOME/android-ndk-r15c' >> ~/.bashrc
|
|
||||||
# echo 'export ANDROID_NDK_HOME=$NDK_HOME' >> ~/.bashrc
|
|
||||||
|
|
||||||
|
|
||||||
source ~/.bashrc
|
|
||||||
|
|
||||||
# Make sure you can execute 32 bit executables if this is 64 bit machine, otherwise skip this
|
|
||||||
dpkg --add-architecture i386
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y libc6:i386 libstdc++6:i386 zlib1g:i386
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package libv2ray
|
|
||||||
|
|
||||||
// This struct creates our own log writer without datatime stamp
|
|
||||||
// As Android adds time stamps on each line
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
v2commlog "v2ray.com/core/common/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type consoleLogWriter struct {
|
|
||||||
logger *log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *consoleLogWriter) Write(s string) error {
|
|
||||||
w.logger.Print(s)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *consoleLogWriter) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This logger won't print data/time stamps
|
|
||||||
func createStdoutLogWriter() v2commlog.WriterCreator {
|
|
||||||
return func() v2commlog.Writer {
|
|
||||||
return &consoleLogWriter{
|
|
||||||
logger: log.New(os.Stdout, "", 0)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Submodule
+1
Submodule AndroidLibXrayLite added at 4c3a3cd051
@@ -1,29 +1,40 @@
|
|||||||
v2rayNG 隐私条款
|
**v2rayNG 隐私权政策**
|
||||||
|
|
||||||
最后更新 2017-11-22
|
本政策自2023年11月17日起施行
|
||||||
|
|
||||||
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私条款。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
|
2dust 将 v2rayNG 应用程序构建为开源应用程序。 本服务由 2dust 免费提供,并且旨在按原样使用。
|
||||||
|
|
||||||
1. 信息收集
|
v2rayNG 尊重并保护所有用户的个人隐私权,为此我们向大众公开这份隐私权政策。**您使用 v2rayNG 即代表您以阅读并同意了这份条款,如果您不同意这份条款请立即停止使用并卸载 v2rayNG。**
|
||||||
|
|
||||||
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私条款。
|
**信息收集**
|
||||||
|
|
||||||
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
|
v2rayNG 软件自身不会发送任何信息到开发者,但是您下载软件的应用市场(如 Google Play)可能会收集关于应用运行状态的相关信息并提供给 v2rayNG 开发者。有关这些信息,请阅读您使用的应用市场所提供的隐私权政策。
|
||||||
|
|
||||||
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
|
v2rayNG 软件中可能包含需要通过 IAP 支付解锁的功能,您的支付信息将由相关的 IAP 渠道进行处理,而我们对支付信息没有访问权。
|
||||||
|
|
||||||
2. 信息共享
|
当您向 v2rayNG 开发者反馈软件运行中的错误时,开发者可能会要求您提供软件以及系统的日志以帮助确认问题的原因。因日志中可能包括敏感信息,此类信息只能由您自己操作发送。**我们不对任何传输服务的安全性和隐私性做任何明示或暗示的担保,请您在传送相关信息时选择可以您自身可以接受的方式。**
|
||||||
|
|
||||||
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
|
**信息共享**
|
||||||
|
|
||||||
3. 信息存留
|
我们不会向任何第三方出售收集到的用户数据。我们可能向外部开发者提供信息以协助软件的开发,但是在提供信息之前我们会传达相关保密义务并确定其可以遵守。
|
||||||
|
|
||||||
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
|
**信息存留**
|
||||||
|
|
||||||
4. 信息泄露
|
除非有相关法律规定,我们会在 30 天内清除不需要继续使用的用户数据,或将统计数据整合为无法识别单个用户的综合报告。
|
||||||
|
|
||||||
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
|
**信息泄露**
|
||||||
|
|
||||||
5. 条款修改
|
我们会使用合理的技术和安全手段尽力保护用户的数据,但是无法保证数据的绝对安全。如果我们确认数据发生了泄露,我们会在 7 天内通过可用的渠道通知用户。**您同意不向我们追责任何因不可抗力而造成的损失。**
|
||||||
|
|
||||||
|
**儿童隐私**
|
||||||
|
|
||||||
|
这些服务不针对 13 岁以下的任何人。我不会故意收集 13 岁以下儿童的个人身份信息。 如果我发现 13 岁以下的儿童向我提供了个人信息,我会立即从我们的服务器中删除该信息。 如果您是父母或监护人,并且您知道您的孩子向我们提供了个人信息,请与我联系,以便我能够采取必要的行动。
|
||||||
|
|
||||||
|
**条款修改**
|
||||||
|
|
||||||
|
我们保留修改这份隐私权政策的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私权政策。**
|
||||||
|
|
||||||
|
**联系我们**
|
||||||
|
|
||||||
|
如果您对我的隐私政策有任何疑问或建议,请随时通过 CaptainIronng@protonmail.com 与我联系。
|
||||||
|
|
||||||
我们保留修改这份隐私条款的权利,但是会确保在更新条款前至少 30 天通过我们的可用渠道和应用内提示来通知用户。**在新条款生效后继续使用软件即表示您同意修改后的隐私条款。**
|
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
# v2rayNG
|
# v2rayNG
|
||||||
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=com.v2ray.ang">
|
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
||||||
<img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" width="165" height="64" />
|
|
||||||
</a>
|
[](https://developer.android.com/about/versions/lollipop)
|
||||||
|
[](https://kotlinlang.org)
|
||||||
|
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||||
|
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||||
|
[](https://github.com/2dust/v2rayNG/releases)
|
||||||
|
[](https://t.me/v2rayn)
|
||||||
|
|
||||||
|
### Telegram Channel
|
||||||
|
[github_2dust](https://t.me/github_2dust)
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
#### Geoip and Geosite
|
||||||
|
- geoip.dat and geosite.dat files are in `Android/data/com.v2ray.ang/files/assets` (path may differ on some Android device)
|
||||||
|
- download feature will get enhanced version in this [repo](https://github.com/Loyalsoldier/v2ray-rules-dat) (Note it need a working proxy)
|
||||||
|
- latest official [domain list](https://github.com/Loyalsoldier/v2ray-rules-dat) and [ip list](https://github.com/Loyalsoldier/geoip) can be imported manually
|
||||||
|
- possible to use third party dat file in the same folder, like [h2y](https://guide.v2fly.org/routing/sitedata.html#%E5%A4%96%E7%BD%AE%E7%9A%84%E5%9F%9F%E5%90%8D%E6%96%87%E4%BB%B6)
|
||||||
|
|
||||||
|
### More in our [wiki](https://github.com/2dust/v2rayNG/wiki)
|
||||||
|
|
||||||
|
### Development guide
|
||||||
|
|
||||||
|
Android project under V2rayNG folder can be compiled directly in Android Studio, or using Gradle wrapper. But the v2ray core inside the aar is (probably) outdated.
|
||||||
|
The aar can be compiled from the Golang project [AndroidLibV2rayLite](https://github.com/2dust/AndroidLibV2rayLite) or [AndroidLibXrayLite](https://github.com/2dust/AndroidLibXrayLite).
|
||||||
|
For a quick start, read guide for [Go Mobile](https://github.com/golang/go/wiki/Mobile) and [Makefiles for Go Developers](https://tutorialedge.net/golang/makefiles-for-go-developers/)
|
||||||
|
|
||||||
|
v2rayNG can run on Android Emulators. For WSA, VPN permission need to be granted via
|
||||||
|
`appops set [package name] ACTIVATE_VPN allow`
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
*.iml
|
|
||||||
.gradle
|
|
||||||
/local.properties
|
|
||||||
/.idea
|
|
||||||
.DS_Store
|
|
||||||
/build
|
|
||||||
/captures
|
|
||||||
*.apk
|
|
||||||
signing.properties
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
/build
|
|
||||||
/google-services.json
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 28
|
|
||||||
buildToolsVersion '28.0.3'
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
targetCompatibility = "8"
|
|
||||||
sourceCompatibility = "8"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.v2ray.ang"
|
|
||||||
minSdkVersion 17
|
|
||||||
targetSdkVersion Integer.parseInt("$targetSdkVer")
|
|
||||||
multiDexEnabled true
|
|
||||||
versionCode 212
|
|
||||||
versionName "1.0.2"
|
|
||||||
}
|
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
release {
|
|
||||||
storeFile file("../key.jks")
|
|
||||||
keyAlias 'ang'
|
|
||||||
keyPassword '123456'
|
|
||||||
storePassword '123456'
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
storeFile file("../key.jks")
|
|
||||||
keyAlias 'ang'
|
|
||||||
keyPassword '123456'
|
|
||||||
storePassword '123456'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
zipAlignEnabled false
|
|
||||||
shrinkResources false
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
minifyEnabled false
|
|
||||||
zipAlignEnabled false
|
|
||||||
shrinkResources false
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
|
||||||
}
|
|
||||||
|
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
enable true
|
|
||||||
reset()
|
|
||||||
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
|
|
||||||
universalApk true //generate an additional APK that contains all the ABIs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// map for the version code
|
|
||||||
project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
|
|
||||||
|
|
||||||
android.applicationVariants.all { variant ->
|
|
||||||
// assign different version code for each output
|
|
||||||
variant.outputs.each { output ->
|
|
||||||
output.versionCodeOverride =
|
|
||||||
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) *
|
|
||||||
1000000 + android.defaultConfig.versionCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
|
||||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
|
||||||
testImplementation 'junit:junit:4.12'
|
|
||||||
implementation project(':dpreference')
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
|
||||||
// Android support library
|
|
||||||
implementation "com.android.support:support-v4:$supportLibVersion"
|
|
||||||
implementation "com.android.support:appcompat-v7:$supportLibVersion"
|
|
||||||
implementation "com.android.support:design:$supportLibVersion"
|
|
||||||
implementation "com.android.support:cardview-v7:$supportLibVersion"
|
|
||||||
implementation "com.android.support:preference-v7:$supportLibVersion"
|
|
||||||
implementation "com.android.support:recyclerview-v7:$supportLibVersion"
|
|
||||||
implementation "com.android.support:multidex:1.0.3"
|
|
||||||
// DSL
|
|
||||||
implementation "org.jetbrains.anko:anko-sdk15:$ankoVersion"
|
|
||||||
implementation "org.jetbrains.anko:anko-support-v4:$ankoVersion"
|
|
||||||
implementation "org.jetbrains.anko:anko-appcompat-v7:$ankoVersion"
|
|
||||||
implementation "org.jetbrains.anko:anko-design:$ankoVersion"
|
|
||||||
implementation 'com.google.code.gson:gson:2.8.5'
|
|
||||||
implementation 'io.reactivex:rxjava:1.3.4'
|
|
||||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
|
||||||
implementation 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'
|
|
||||||
implementation 'com.dinuscxj:recycleritemdecoration:1.0.0'
|
|
||||||
implementation 'io.reactivex:rxkotlin:0.60.0'
|
|
||||||
implementation 'me.dm7.barcodescanner:core:1.9.8'
|
|
||||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
|
||||||
implementation 'com.github.jorgecastilloprz:fabprogresscircle:1.01@aar'
|
|
||||||
implementation 'com.beust:klaxon:3.0.1'
|
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
|
||||||
|
|
||||||
implementation(name: 'libv2ray', ext: 'aar')
|
|
||||||
//implementation(name: 'tun2socks', ext: 'aar')
|
|
||||||
}
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
jcenter()
|
|
||||||
maven { url 'https://maven.google.com' }
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlinVersion"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
repositories {
|
|
||||||
flatDir {
|
|
||||||
dirs 'libs'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
id("com.jaredsburrows.license")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.v2ray.ang"
|
||||||
|
compileSdk = 37
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.v2ray.ang"
|
||||||
|
minSdk = 24
|
||||||
|
targetSdk = 37
|
||||||
|
versionCode = 731
|
||||||
|
versionName = "2.2.1"
|
||||||
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
isEnable = true
|
||||||
|
reset()
|
||||||
|
if (abiFilterList != null && abiFilterList.isNotEmpty()) {
|
||||||
|
include(*abiFilterList.toTypedArray())
|
||||||
|
} else {
|
||||||
|
include(
|
||||||
|
"arm64-v8a",
|
||||||
|
"armeabi-v7a",
|
||||||
|
"x86_64",
|
||||||
|
"x86"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isUniversalApk = abiFilterList.isNullOrEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions.add("distribution")
|
||||||
|
productFlavors {
|
||||||
|
create("fdroid") {
|
||||||
|
dimension = "distribution"
|
||||||
|
applicationIdSuffix = ".fdroid"
|
||||||
|
buildConfigField("String", "DISTRIBUTION", "\"F-Droid\"")
|
||||||
|
}
|
||||||
|
create("playstore") {
|
||||||
|
dimension = "distribution"
|
||||||
|
buildConfigField("String", "DISTRIBUTION", "\"Play Store\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("main") {
|
||||||
|
jniLibs.srcDirs("libs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applicationVariants.all {
|
||||||
|
val variant = this
|
||||||
|
val isFdroid = variant.productFlavors.any { it.name == "fdroid" }
|
||||||
|
if (isFdroid) {
|
||||||
|
val versionCodes =
|
||||||
|
mapOf(
|
||||||
|
"armeabi-v7a" to 2, "arm64-v8a" to 1, "x86" to 4, "x86_64" to 3, "universal" to 0
|
||||||
|
)
|
||||||
|
|
||||||
|
variant.outputs
|
||||||
|
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
||||||
|
.forEach { output ->
|
||||||
|
val abi = output.getFilter("ABI") ?: "universal"
|
||||||
|
output.outputFileName = "v2rayNG_${variant.versionName}-fdroid_${abi}.apk"
|
||||||
|
if (versionCodes.containsKey(abi)) {
|
||||||
|
output.versionCodeOverride =
|
||||||
|
(100 * variant.versionCode + versionCodes[abi]!!).plus(5000000)
|
||||||
|
} else {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val versionCodes =
|
||||||
|
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
|
||||||
|
|
||||||
|
variant.outputs
|
||||||
|
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
||||||
|
.forEach { output ->
|
||||||
|
val abi = if (output.getFilter("ABI") != null)
|
||||||
|
output.getFilter("ABI")
|
||||||
|
else
|
||||||
|
"universal"
|
||||||
|
|
||||||
|
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
|
||||||
|
if (versionCodes.containsKey(abi)) {
|
||||||
|
output.versionCodeOverride =
|
||||||
|
(1000000 * versionCodes[abi]!!).plus(variant.versionCode)
|
||||||
|
} else {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Core Libraries
|
||||||
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar", "*.jar"))))
|
||||||
|
|
||||||
|
// AndroidX Core Libraries
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(libs.androidx.activity)
|
||||||
|
implementation(libs.androidx.constraintlayout)
|
||||||
|
implementation(libs.preference.ktx)
|
||||||
|
implementation(libs.recyclerview)
|
||||||
|
implementation(libs.androidx.swiperefreshlayout)
|
||||||
|
implementation(libs.androidx.viewpager2)
|
||||||
|
implementation(libs.androidx.fragment)
|
||||||
|
|
||||||
|
// UI Libraries
|
||||||
|
implementation(libs.material)
|
||||||
|
implementation(libs.toasty)
|
||||||
|
implementation(libs.editorkit)
|
||||||
|
implementation(libs.flexbox)
|
||||||
|
|
||||||
|
// Data and Storage Libraries
|
||||||
|
implementation(libs.mmkv.static)
|
||||||
|
implementation(libs.gson)
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
|
||||||
|
// Reactive and Utility Libraries
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
|
// Language and Processing Libraries
|
||||||
|
implementation(libs.language.base)
|
||||||
|
implementation(libs.language.json)
|
||||||
|
|
||||||
|
// Intent and Utility Libraries
|
||||||
|
implementation(libs.quickie.foss)
|
||||||
|
implementation(libs.core)
|
||||||
|
|
||||||
|
// AndroidX Lifecycle and Architecture Components
|
||||||
|
implementation(libs.lifecycle.viewmodel.ktx)
|
||||||
|
implementation(libs.lifecycle.livedata.ktx)
|
||||||
|
implementation(libs.lifecycle.runtime.ktx)
|
||||||
|
|
||||||
|
// Background Task Libraries
|
||||||
|
implementation(libs.work.runtime.ktx)
|
||||||
|
implementation(libs.work.multiprocess)
|
||||||
|
|
||||||
|
// Multidex Support
|
||||||
|
implementation(libs.multidex)
|
||||||
|
|
||||||
|
// Testing Libraries
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
androidTestImplementation(libs.androidx.junit)
|
||||||
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
testImplementation(libs.org.mockito.mockito.inline)
|
||||||
|
testImplementation(libs.mockito.kotlin)
|
||||||
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
https://github.com/2dust/v2rayNG/tree/master/AndroidLibV2rayLite
|
|
||||||
Binary file not shown.
Vendored
+8
-48
@@ -1,14 +1,10 @@
|
|||||||
# Add project specific ProGuard rules here.
|
# Add project specific ProGuard rules here.
|
||||||
# By default, the flags in this file are appended to flags specified
|
# You can control the set of applied configuration files using the
|
||||||
# in G:\android-sdk/tools/proguard/proguard-android.txt
|
# proguardFiles setting in build.gradle.
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
#
|
||||||
# For more details, see
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
# If your project uses WebView with JS, uncomment the following
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
# class:
|
# class:
|
||||||
@@ -16,46 +12,10 @@
|
|||||||
# public *;
|
# public *;
|
||||||
#}
|
#}
|
||||||
|
|
||||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
# Uncomment this to preserve the line number information for
|
||||||
# removes such information by default, so configure it to keep all of it.
|
# debugging stack traces.
|
||||||
-keepattributes Signature
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
# For using GSON @Expose annotation
|
# If you keep the line number information, uncomment this to
|
||||||
-keepattributes *Annotation*
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
# Gson specific classes
|
|
||||||
-keep class sun.misc.Unsafe { *; }
|
|
||||||
|
|
||||||
-dontwarn org.apache.commons.**
|
|
||||||
-keep class org.apache.commons.** { *;}
|
|
||||||
|
|
||||||
# Disable debug info output
|
|
||||||
-assumenosideeffects class android.util.Log {
|
|
||||||
public static boolean isLoggable(java.lang.String,int);
|
|
||||||
public static int v(...);
|
|
||||||
public static int i(...);
|
|
||||||
public static int w(...);
|
|
||||||
public static int d(...);
|
|
||||||
public static int e(...);
|
|
||||||
}
|
|
||||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
|
||||||
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
|
||||||
static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
|
|
||||||
static void throwUninitializedPropertyAccessException(java.lang.String);
|
|
||||||
}
|
|
||||||
|
|
||||||
-dontwarn org.jetbrains.anko.internals.**
|
|
||||||
-keep class org.jetbrains.anko.internals.** { *;}
|
|
||||||
|
|
||||||
-dontwarn rx.internal.util.unsafe.**
|
|
||||||
-keep class rx.internal.util.unsafe.** { *;}
|
|
||||||
|
|
||||||
-dontwarn app.dinus.**
|
|
||||||
-keep class app.dinus.** { *;}
|
|
||||||
|
|
||||||
-keepclassmembers class ** {
|
|
||||||
@com.hwangjr.rxbus.annotation.Subscribe public *;
|
|
||||||
@com.hwangjr.rxbus.annotation.Produce public *;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep class libv2ray.** { *;}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.v2ray.ang;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.test.ApplicationTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
|
||||||
*/
|
|
||||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
|
||||||
public ApplicationTest() {
|
|
||||||
super(Application.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<item name="app_name" type="string">v2rayNG (DEV)</item>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name" translatable="false">v2rayNG (F-Droid)</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_qu_switch_24dp"
|
||||||
|
android:shortcutDisabledMessage="@string/app_widget_name"
|
||||||
|
android:shortcutId="shortcuts_switch"
|
||||||
|
android:shortcutLongLabel="@string/app_widget_name"
|
||||||
|
android:shortcutShortLabel="@string/app_widget_name">
|
||||||
|
|
||||||
|
<!--suppress AndroidDomInspection -->
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetClass="com.v2ray.ang.ui.ScSwitchActivity"
|
||||||
|
android:targetPackage="com.v2ray.ang.fdroid" />
|
||||||
|
<categories android:name="android.shortcut.conversation" />
|
||||||
|
</shortcut>
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_qu_scan_24dp"
|
||||||
|
android:shortcutDisabledMessage="@string/menu_item_import_config_qrcode"
|
||||||
|
android:shortcutId="shortcuts_scan"
|
||||||
|
android:shortcutLongLabel="@string/menu_item_import_config_qrcode"
|
||||||
|
android:shortcutShortLabel="@string/menu_item_import_config_qrcode">
|
||||||
|
|
||||||
|
<!--suppress AndroidDomInspection -->
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetClass="com.v2ray.ang.ui.ScScannerActivity"
|
||||||
|
android:targetPackage="com.v2ray.ang.fdroid" />
|
||||||
|
<categories android:name="android.shortcut.conversation" />
|
||||||
|
</shortcut>
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_qu_start_24dp"
|
||||||
|
android:shortcutDisabledMessage="@string/toast_services_start"
|
||||||
|
android:shortcutId="shortcuts_start"
|
||||||
|
android:shortcutLongLabel="@string/toast_services_start"
|
||||||
|
android:shortcutShortLabel="@string/toast_services_start">
|
||||||
|
|
||||||
|
<!--suppress AndroidDomInspection -->
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetClass="com.v2ray.ang.ui.ScStartActivity"
|
||||||
|
android:targetPackage="com.v2ray.ang.fdroid" />
|
||||||
|
<categories android:name="android.shortcut.conversation" />
|
||||||
|
</shortcut>
|
||||||
|
<shortcut
|
||||||
|
android:enabled="true"
|
||||||
|
android:icon="@drawable/ic_qu_stop_24dp"
|
||||||
|
android:shortcutDisabledMessage="@string/toast_services_stop"
|
||||||
|
android:shortcutId="shortcuts_stop"
|
||||||
|
android:shortcutLongLabel="@string/toast_services_stop"
|
||||||
|
android:shortcutShortLabel="@string/toast_services_stop">
|
||||||
|
|
||||||
|
<!--suppress AndroidDomInspection -->
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:targetClass="com.v2ray.ang.ui.ScStopActivity"
|
||||||
|
android:targetPackage="com.v2ray.ang.fdroid" />
|
||||||
|
<categories android:name="android.shortcut.conversation" />
|
||||||
|
</shortcut>
|
||||||
|
</shortcuts>
|
||||||
@@ -1,38 +1,65 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.v2ray.ang">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:ignore="MissingLeanbackLauncher">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<supports-screens
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
|
android:anyDensity="true"
|
||||||
|
android:largeScreens="true"
|
||||||
|
android:normalScreens="true"
|
||||||
|
android:smallScreens="true"
|
||||||
|
android:xlargeScreens="true" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.camera.autofocus"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.touchscreen"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<!-- https://developer.android.com/about/versions/11/privacy/package-visibility -->
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="PackageVisibilityPolicy,QueryAllPackagesPermission" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<!-- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> -->
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="com.android.vending.BILLING" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
tools:ignore="ForegroundServicesPolicy" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||||
|
android:minSdkVersion="34" />
|
||||||
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".AngApplication"
|
android:name=".AngApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:banner="@mipmap/ic_banner"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppThemeDayNight.NoActionBar"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:theme="@style/AppTheme.NoActionBar"
|
android:exported="true"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
|
|
||||||
<data android:mimeType="text/plain" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||||
@@ -44,82 +71,231 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ServerActivity"
|
android:name=".ui.ServerActivity"
|
||||||
|
android:exported="false"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.Server2Activity"
|
android:name=".ui.ServerCustomConfigActivity"
|
||||||
|
android:exported="false"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.Server3Activity"
|
android:name=".ui.ServerGroupActivity"
|
||||||
|
android:exported="false"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.Server4Activity"
|
android:name=".ui.ServerProxyChainActivity"
|
||||||
|
android:exported="false"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:windowSoftInputMode="stateUnchanged" />
|
||||||
<activity android:name=".ui.SettingsActivity" />
|
|
||||||
<activity android:name=".ui.PerAppProxyActivity" />
|
|
||||||
<activity android:name=".ui.ScannerActivity" />
|
|
||||||
<!-- <activity android:name=".InappBuyActivity" />-->
|
|
||||||
<activity android:name=".ui.LogcatActivity" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.RoutingSettingsActivity"
|
android:name=".ui.SettingsActivity"
|
||||||
android:windowSoftInputMode="stateUnchanged" />
|
android:exported="false" />
|
||||||
<activity android:name=".ui.SubSettingActivity" />
|
<activity
|
||||||
|
android:name=".ui.PerAppProxyActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.AppPickerActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.ScannerActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.LogcatActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.RoutingSettingActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.RoutingEditActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.SubSettingActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.UserAssetActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.UserAssetUrlActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity android:name=".ui.SubEditActivity" />
|
<activity
|
||||||
<activity android:name=".ui.ScScannerActivity" />
|
android:name=".ui.SubEditActivity"
|
||||||
<activity android:name=".ui.ScSwitchActivity" />
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.ScScannerActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.ScSwitchActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
|
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.ScStartActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
|
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.ScStopActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
|
android:theme="@style/AppThemeDayNight.NoActionBar.Translucent" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.UrlSchemeActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<data android:scheme="v2rayng" />
|
||||||
|
<data android:host="install-config" />
|
||||||
|
<data android:host="install-sub" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.CheckUpdateActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.BackupActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.AboutActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.V2RayVpnService"
|
android:name=".service.CoreVpnService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:permission="android.permission.BIND_VPN_SERVICE"
|
android:permission="android.permission.BIND_VPN_SERVICE"
|
||||||
android:process=":RunSoLibV2RayDaemon">
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
|
tools:ignore="VpnServicePolicy">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.net.VpnService" />
|
<action android:name="android.net.VpnService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="vpn" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<!--<receiver android:name=".receiver.WidgetProvider">-->
|
<service
|
||||||
<!--<meta-data-->
|
android:name=".service.CoreProxyOnlyService"
|
||||||
<!--android:name="android.appwidget.provider"-->
|
android:exported="false"
|
||||||
<!--android:resource="@xml/app_widget_provider" />-->
|
android:foregroundServiceType="specialUse"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="proxy" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<!--<intent-filter>-->
|
<service
|
||||||
<!--<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
|
android:name=".service.CoreTestService"
|
||||||
<!--<action android:name="com.v2ray.ang.action.widget.click" />-->
|
android:exported="false"
|
||||||
<!--</intent-filter>-->
|
android:foregroundServiceType="specialUse"
|
||||||
<!--</receiver>-->
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="test" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.WidgetProvider"
|
||||||
|
android:exported="true"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/app_widget_provider" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
<action android:name="${applicationId}.action.widget.click" />
|
||||||
|
<action android:name="${applicationId}.action.activity" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
<receiver
|
||||||
|
android:name=".receiver.BootReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="BootReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.QSTileService"
|
android:name=".service.QSTileService"
|
||||||
android:icon="@drawable/ic_v"
|
android:exported="true"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
|
android:icon="@drawable/ic_stat_name"
|
||||||
android:label="@string/app_tile_name"
|
android:label="@string/app_tile_name"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
|
android:process=":RunSoLibV2RayDaemon">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="tile" />
|
||||||
</service>
|
</service>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.TaskerActivity"
|
android:name=".ui.TaskerActivity"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:exported="true"
|
||||||
android:label="@string/app_name">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver android:name=".receiver.TaskerReceiver">
|
<receiver
|
||||||
|
android:name=".receiver.TaskerReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
android:process=":RunSoLibV2RayDaemon"
|
||||||
|
tools:ignore="ExportedReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
<!-- =====================Tasker===================== -->
|
<!-- =====================Tasker===================== -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.work.WorkManagerInitializer"
|
||||||
|
android:value="androidx.startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.work.multiprocess.RemoteWorkManagerService"
|
||||||
|
android:exported="false"
|
||||||
|
android:process=":bg" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.cache"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/cache_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2012 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.android.vending.billing;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
|
|
||||||
* This service provides the following features:
|
|
||||||
* 1. Provides a new API to get details of in-app items published for the app including
|
|
||||||
* price, type, title and description.
|
|
||||||
* 2. The purchase flow is synchronous and purchase information is available immediately
|
|
||||||
* after it completes.
|
|
||||||
* 3. Purchase information of in-app purchases is maintained within the Google Play system
|
|
||||||
* till the purchase is consumed.
|
|
||||||
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
|
|
||||||
* in-app items are consumable and thereafter can be purchased again.
|
|
||||||
* 5. An API to get current purchases of the user immediately. This will not contain any
|
|
||||||
* consumed purchases.
|
|
||||||
*
|
|
||||||
* All calls will give a response code with the following possible values
|
|
||||||
* RESULT_OK = 0 - success
|
|
||||||
* RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
|
|
||||||
* RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
|
|
||||||
* RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
|
|
||||||
* RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
|
|
||||||
* RESULT_ERROR = 6 - Fatal error during the API action
|
|
||||||
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
|
|
||||||
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
|
|
||||||
*/
|
|
||||||
interface IInAppBillingService {
|
|
||||||
/**
|
|
||||||
* Checks support for the requested billing API version, package and in-app type.
|
|
||||||
* Minimum API version supported by this interface is 3.
|
|
||||||
* @param apiVersion the billing version which the app is using
|
|
||||||
* @param packageName the package name of the calling app
|
|
||||||
* @param type type of the in-app item being purchased "inapp" for one-time purchases
|
|
||||||
* and "subs" for subscription.
|
|
||||||
* @return RESULT_OK(0) on success, corresponding result code on failures
|
|
||||||
*/
|
|
||||||
int isBillingSupported(int apiVersion, String packageName, String type);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides details of a list of SKUs
|
|
||||||
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
|
|
||||||
* with a list JSON strings containing the productId, price, title and description.
|
|
||||||
* This API can be called with a maximum of 20 SKUs.
|
|
||||||
* @param apiVersion billing API version that the Third-party is using
|
|
||||||
* @param packageName the package name of the calling app
|
|
||||||
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
|
|
||||||
* @return Bundle containing the following key-value pairs
|
|
||||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
|
||||||
* failure as listed above.
|
|
||||||
* "DETAILS_LIST" with a StringArrayList containing purchase information
|
|
||||||
* in JSON format similar to:
|
|
||||||
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
|
|
||||||
* "title : "Example Title", "description" : "This is an example description" }'
|
|
||||||
*/
|
|
||||||
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
|
|
||||||
* the type, a unique purchase token and an optional developer payload.
|
|
||||||
* @param apiVersion billing API version that the app is using
|
|
||||||
* @param packageName package name of the calling app
|
|
||||||
* @param sku the SKU of the in-app item as published in the developer console
|
|
||||||
* @param type the type of the in-app item ("inapp" for one-time purchases
|
|
||||||
* and "subs" for subscription).
|
|
||||||
* @param developerPayload optional argument to be sent back with the purchase information
|
|
||||||
* @return Bundle containing the following key-value pairs
|
|
||||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
|
||||||
* failure as listed above.
|
|
||||||
* "BUY_INTENT" - PendingIntent to start the purchase flow
|
|
||||||
*
|
|
||||||
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
|
|
||||||
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
|
|
||||||
* If the purchase is successful, the result data will contain the following key-value pairs
|
|
||||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
|
||||||
* failure as listed above.
|
|
||||||
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
|
|
||||||
* '{"orderId":"12999763169054705758.1371079406387615",
|
|
||||||
* "packageName":"com.example.app",
|
|
||||||
* "productId":"exampleSku",
|
|
||||||
* "purchaseTime":1345678900000,
|
|
||||||
* "purchaseToken" : "122333444455555",
|
|
||||||
* "developerPayload":"example developer payload" }'
|
|
||||||
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
|
|
||||||
* was signed with the private key of the developer
|
|
||||||
* TODO: change this to app-specific keys.
|
|
||||||
*/
|
|
||||||
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
|
|
||||||
String developerPayload);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current SKUs owned by the user of the type and package name specified along with
|
|
||||||
* purchase information and a signature of the data to be validated.
|
|
||||||
* This will return all SKUs that have been purchased in V3 and managed items purchased using
|
|
||||||
* V1 and V2 that have not been consumed.
|
|
||||||
* @param apiVersion billing API version that the app is using
|
|
||||||
* @param packageName package name of the calling app
|
|
||||||
* @param type the type of the in-app items being requested
|
|
||||||
* ("inapp" for one-time purchases and "subs" for subscription).
|
|
||||||
* @param continuationToken to be set as null for the first call, if the number of owned
|
|
||||||
* skus are too many, a continuationToken is returned in the response bundle.
|
|
||||||
* This method can be called again with the continuation token to get the next set of
|
|
||||||
* owned skus.
|
|
||||||
* @return Bundle containing the following key-value pairs
|
|
||||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
|
||||||
* failure as listed above.
|
|
||||||
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
|
|
||||||
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
|
|
||||||
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
|
|
||||||
* of the purchase information
|
|
||||||
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
|
|
||||||
* next set of in-app purchases. Only set if the
|
|
||||||
* user has more owned skus than the current list.
|
|
||||||
*/
|
|
||||||
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consume the last purchase of the given SKU. This will result in this item being removed
|
|
||||||
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
|
|
||||||
* @param apiVersion billing API version that the app is using
|
|
||||||
* @param packageName package name of the calling app
|
|
||||||
* @param purchaseToken token in the purchase information JSON that identifies the purchase
|
|
||||||
* to be consumed
|
|
||||||
* @return 0 if consumption succeeded. Appropriate error values for failures.
|
|
||||||
*/
|
|
||||||
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"remarks": "绕过bittorrent",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"protocol": [
|
||||||
|
"bittorrent"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "阻断udp443",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"port": "443",
|
||||||
|
"network": "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理Google",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"domain": [
|
||||||
|
"geosite:google"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理海外公共DNSIP",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"ip": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"1.0.0.1",
|
||||||
|
"2606:4700:4700::1111",
|
||||||
|
"2606:4700:4700::1001",
|
||||||
|
"1.1.1.2",
|
||||||
|
"1.0.0.2",
|
||||||
|
"2606:4700:4700::1112",
|
||||||
|
"2606:4700:4700::1002",
|
||||||
|
"1.1.1.3",
|
||||||
|
"1.0.0.3",
|
||||||
|
"2606:4700:4700::1113",
|
||||||
|
"2606:4700:4700::1003",
|
||||||
|
"8.8.8.8",
|
||||||
|
"8.8.4.4",
|
||||||
|
"2001:4860:4860::8888",
|
||||||
|
"2001:4860:4860::8844",
|
||||||
|
"94.140.14.14",
|
||||||
|
"94.140.15.15",
|
||||||
|
"2a10:50c0::ad1:ff",
|
||||||
|
"2a10:50c0::ad2:ff",
|
||||||
|
"94.140.14.15",
|
||||||
|
"94.140.15.16",
|
||||||
|
"2a10:50c0::bad1:ff",
|
||||||
|
"2a10:50c0::bad2:ff",
|
||||||
|
"94.140.14.140",
|
||||||
|
"94.140.14.141",
|
||||||
|
"2a10:50c0::1:ff",
|
||||||
|
"2a10:50c0::2:ff",
|
||||||
|
"208.67.222.222",
|
||||||
|
"208.67.220.220",
|
||||||
|
"2620:119:35::35",
|
||||||
|
"2620:119:53::53",
|
||||||
|
"208.67.222.123",
|
||||||
|
"208.67.220.123",
|
||||||
|
"2620:119:35::123",
|
||||||
|
"2620:119:53::123",
|
||||||
|
"9.9.9.9",
|
||||||
|
"149.112.112.112",
|
||||||
|
"2620:fe::9",
|
||||||
|
"2620:fe::fe",
|
||||||
|
"9.9.9.11",
|
||||||
|
"149.112.112.11",
|
||||||
|
"2620:fe::11",
|
||||||
|
"2620:fe::fe:11",
|
||||||
|
"9.9.9.10",
|
||||||
|
"149.112.112.10",
|
||||||
|
"2620:fe::10",
|
||||||
|
"2620:fe::fe:10",
|
||||||
|
"77.88.8.8",
|
||||||
|
"77.88.8.1",
|
||||||
|
"2a02:6b8::feed:0ff",
|
||||||
|
"2a02:6b8:0:1::feed:0ff",
|
||||||
|
"77.88.8.88",
|
||||||
|
"77.88.8.2",
|
||||||
|
"2a02:6b8::feed:bad",
|
||||||
|
"2a02:6b8:0:1::feed:bad",
|
||||||
|
"77.88.8.7",
|
||||||
|
"77.88.8.3",
|
||||||
|
"2a02:6b8::feed:a11",
|
||||||
|
"2a02:6b8:0:1::feed:a11"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理海外公共DNS域名",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"domain": [
|
||||||
|
"domain:cloudflare-dns.com",
|
||||||
|
"domain:one.one.one.one",
|
||||||
|
"domain:dns.google",
|
||||||
|
"domain:adguard-dns.com",
|
||||||
|
"domain:opendns.com",
|
||||||
|
"domain:umbrella.com",
|
||||||
|
"domain:quad9.net",
|
||||||
|
"domain:yandex.net"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理IP",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"ip": [
|
||||||
|
"geoip:facebook",
|
||||||
|
"geoip:fastly",
|
||||||
|
"geoip:google",
|
||||||
|
"geoip:netflix",
|
||||||
|
"geoip:telegram",
|
||||||
|
"geoip:twitter"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理GFW",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"domain": [
|
||||||
|
"geosite:gfw",
|
||||||
|
"geosite:greatfire"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "最终直连",
|
||||||
|
"port": "0-65535",
|
||||||
|
"outboundTag": "direct"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"remarks": "阻断udp443",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"port": "443",
|
||||||
|
"network": "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "最终代理",
|
||||||
|
"port": "0-65535",
|
||||||
|
"outboundTag": "proxy"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"remarks": "阻断udp443",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"port": "443",
|
||||||
|
"network": "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "代理Google",
|
||||||
|
"outboundTag": "proxy",
|
||||||
|
"domain": [
|
||||||
|
"geosite:google"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过局域网域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过中国公共DNSIP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"223.5.5.5",
|
||||||
|
"223.6.6.6",
|
||||||
|
"2400:3200::1",
|
||||||
|
"2400:3200:baba::1",
|
||||||
|
"119.29.29.29",
|
||||||
|
"1.12.12.12",
|
||||||
|
"120.53.53.53",
|
||||||
|
"2402:4e00::",
|
||||||
|
"2402:4e00:1::",
|
||||||
|
"180.76.76.76",
|
||||||
|
"2400:da00::6666",
|
||||||
|
"114.114.114.114",
|
||||||
|
"114.114.115.115",
|
||||||
|
"114.114.114.119",
|
||||||
|
"114.114.115.119",
|
||||||
|
"114.114.114.110",
|
||||||
|
"114.114.115.110",
|
||||||
|
"180.184.1.1",
|
||||||
|
"180.184.2.2",
|
||||||
|
"101.226.4.6",
|
||||||
|
"218.30.118.6",
|
||||||
|
"123.125.81.6",
|
||||||
|
"140.207.198.6",
|
||||||
|
"1.2.4.8",
|
||||||
|
"210.2.4.8",
|
||||||
|
"52.80.66.66",
|
||||||
|
"117.50.22.22",
|
||||||
|
"2400:7fc0:849e:200::4",
|
||||||
|
"2404:c2c0:85d8:901::4",
|
||||||
|
"117.50.10.10",
|
||||||
|
"52.80.52.52",
|
||||||
|
"2400:7fc0:849e:200::8",
|
||||||
|
"2404:c2c0:85d8:901::8",
|
||||||
|
"117.50.60.30",
|
||||||
|
"52.80.60.30"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过中国公共DNS域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"domain:alidns.com",
|
||||||
|
"domain:doh.pub",
|
||||||
|
"domain:dot.pub",
|
||||||
|
"domain:360.cn",
|
||||||
|
"domain:onedns.net"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过中国IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:cn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "绕过中国域名",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:cn"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"remarks": "Block udp443",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"port": "443",
|
||||||
|
"network": "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Direct LAN IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Direct LAN domains",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Bypass Iran domains",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"domain:ir",
|
||||||
|
"geosite:category-ir"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Bypass Iran IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:ir"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"remarks": "Bypass bittorrent",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"protocol": [
|
||||||
|
"bittorrent"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Block udp443",
|
||||||
|
"outboundTag": "block",
|
||||||
|
"port": "443",
|
||||||
|
"network": "udp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Direct LAN IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Direct LAN domains",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:private"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Bypass Russia domains",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"domain": [
|
||||||
|
"geosite:category-ru"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remarks": "Bypass Russia IP",
|
||||||
|
"outboundTag": "direct",
|
||||||
|
"ip": [
|
||||||
|
"geoip:ru"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,413 @@
|
|||||||
|
amanita_design.samorost3.gp
|
||||||
|
android
|
||||||
|
au.com.shiftyjelly.pocketcasts
|
||||||
|
bbc.mobile.news.ww
|
||||||
|
be.mygod.vpnhotspot
|
||||||
|
ch.protonmail.android
|
||||||
|
cm.aptoide.pt
|
||||||
|
co.wanqu.android
|
||||||
|
com.alphainventor.filemanager
|
||||||
|
com.amazon.kindle
|
||||||
|
com.amazon.mshop.android.shopping
|
||||||
|
com.android.chrome
|
||||||
|
com.android.providers.downloads
|
||||||
|
com.android.providers.downloads.ui
|
||||||
|
com.android.providers.telephony
|
||||||
|
com.android.settings
|
||||||
|
com.android.vending
|
||||||
|
com.android6park.m6park
|
||||||
|
com.apkpure.aegon
|
||||||
|
com.apkupdater
|
||||||
|
com.app.pornhub
|
||||||
|
com.arthurivanets.owly
|
||||||
|
com.asahi.tida.tablet
|
||||||
|
com.authy.authy
|
||||||
|
com.avmovie
|
||||||
|
com.ballistiq.artstation
|
||||||
|
com.binance.dev
|
||||||
|
com.bitly.app
|
||||||
|
com.brave.browser
|
||||||
|
com.brave.browser_beta
|
||||||
|
com.breel.wallpapers18
|
||||||
|
com.bvanced.android.youtube
|
||||||
|
com.chrome.beta
|
||||||
|
com.chrome.canary
|
||||||
|
com.chrome.dev
|
||||||
|
com.cl.newt66y
|
||||||
|
com.cradle.iitc_mobile
|
||||||
|
org.exarhteam.iitc_mobile
|
||||||
|
com.cygames.shadowverse
|
||||||
|
com.dcard.freedom
|
||||||
|
com.devhd.feedly
|
||||||
|
com.devolver.reigns2
|
||||||
|
com.discord
|
||||||
|
com.downloader.video.tumblr
|
||||||
|
com.driverbrowser
|
||||||
|
com.dropbox.android
|
||||||
|
com.duolingo
|
||||||
|
com.duckduckgo.mobile.android
|
||||||
|
com.dv.adm
|
||||||
|
com.estrongs.android.pop
|
||||||
|
com.estrongs.android.pop.pro
|
||||||
|
com.evernote
|
||||||
|
com.facebook.katana
|
||||||
|
com.facebook.lite
|
||||||
|
com.facebook.mlite
|
||||||
|
com.facebook.orca
|
||||||
|
com.facebook.services
|
||||||
|
com.facebook.system
|
||||||
|
com.fastaccess.github
|
||||||
|
com.felixfilip.scpae
|
||||||
|
com.fireproofstudios.theroom4
|
||||||
|
com.firstrowria.pushnotificationtester
|
||||||
|
com.flyersoft.moonreaderp
|
||||||
|
com.fooview.android.fooview
|
||||||
|
com.fvd.eversync
|
||||||
|
com.gameloft.android.anmp.glofta8hm
|
||||||
|
com.gameloft.android.anmp.glofta9hm
|
||||||
|
com.gianlu.aria2app
|
||||||
|
com.github.yeriomin.yalpstore
|
||||||
|
com.google.android.apps.adm
|
||||||
|
com.google.android.apps.books
|
||||||
|
com.google.android.apps.docs
|
||||||
|
com.google.android.apps.docs.editors.sheets
|
||||||
|
com.google.android.apps.fitness
|
||||||
|
com.google.android.apps.googleassistant
|
||||||
|
com.google.android.apps.googlevoice
|
||||||
|
com.google.android.apps.hangoutsdialer
|
||||||
|
com.google.android.apps.inbox
|
||||||
|
com.google.android.apps.magazines
|
||||||
|
com.google.android.apps.maps
|
||||||
|
com.google.android.apps.nbu.files
|
||||||
|
com.google.android.apps.paidtasks
|
||||||
|
com.google.android.apps.pdfviewer
|
||||||
|
com.google.android.apps.photos
|
||||||
|
com.google.android.apps.plus
|
||||||
|
com.google.android.apps.translate
|
||||||
|
com.google.android.gm
|
||||||
|
com.google.android.gms
|
||||||
|
com.google.android.gms.setup
|
||||||
|
com.google.android.googlequicksearchbox
|
||||||
|
com.google.android.gsf
|
||||||
|
com.google.android.gsf.login
|
||||||
|
com.google.android.ims
|
||||||
|
com.google.android.inputmethod.latin
|
||||||
|
com.google.android.instantapps.supervisor
|
||||||
|
com.google.android.keep
|
||||||
|
com.google.android.music
|
||||||
|
com.google.android.ogyoutube
|
||||||
|
com.google.android.partnersetup
|
||||||
|
com.google.android.play.games
|
||||||
|
com.google.android.street
|
||||||
|
com.google.android.syncadapters.calendar
|
||||||
|
com.google.android.syncadapters.contacts
|
||||||
|
com.google.android.talk
|
||||||
|
com.google.android.tts
|
||||||
|
com.google.android.videos
|
||||||
|
com.google.android.youtube
|
||||||
|
com.google.ar.lens
|
||||||
|
com.hochan.coldsoup
|
||||||
|
com.ifttt.ifttt
|
||||||
|
com.imgur.mobile
|
||||||
|
com.innologica.inoreader
|
||||||
|
com.instagram.android
|
||||||
|
com.instagram.lite
|
||||||
|
com.instapaper.android
|
||||||
|
com.jarvanh.vpntether
|
||||||
|
com.kapp.youtube.final
|
||||||
|
com.klinker.android.twitter_l
|
||||||
|
com.lastpass.lpandroid
|
||||||
|
com.linecorp.linelite
|
||||||
|
com.lingodeer
|
||||||
|
com.ltnnews.news
|
||||||
|
com.mediapods.tumbpods
|
||||||
|
com.mgoogle.android.gms
|
||||||
|
com.microsoft.emmx
|
||||||
|
com.microsoft.office.powerpoint
|
||||||
|
com.microsoft.skydrive
|
||||||
|
com.mixplorer
|
||||||
|
com.msd.consumerchinese
|
||||||
|
com.msd.professionalchinese
|
||||||
|
com.mss2011c.sharehelper
|
||||||
|
com.netflix.mediaclient
|
||||||
|
com.newin.nplayer.pro
|
||||||
|
com.nianticlabs.ingress.prime.qa
|
||||||
|
com.nianticproject.ingress
|
||||||
|
com.ninefolders.hd3
|
||||||
|
com.ninegag.android.app
|
||||||
|
com.nintendo.zara
|
||||||
|
com.nytimes.cn
|
||||||
|
com.oasisfeng.island
|
||||||
|
com.ocnt.liveapp.hw
|
||||||
|
com.orekie.search
|
||||||
|
com.patreon.android
|
||||||
|
com.paypal.android.p2pmobile
|
||||||
|
com.perol.asdpl.pixivez
|
||||||
|
com.pinterest
|
||||||
|
com.popularapp.periodcalendar
|
||||||
|
com.popularapp.videodownloaderforinstagram
|
||||||
|
com.pushbullet.android
|
||||||
|
com.quoord.tapatalkpro.activity
|
||||||
|
com.quora.android
|
||||||
|
com.rayark.cytus2
|
||||||
|
com.rayark.implosion
|
||||||
|
com.rayark.pluto
|
||||||
|
com.reddit.frontpage
|
||||||
|
com.resilio.sync
|
||||||
|
com.rhmsoft.edit
|
||||||
|
com.rubenmayayo.reddit
|
||||||
|
com.sec.android.app.sbrowser
|
||||||
|
com.sec.android.app.sbrowser.beta
|
||||||
|
com.shanga.walli
|
||||||
|
com.simplehabit.simplehabitapp
|
||||||
|
com.slack
|
||||||
|
com.snaptube.premium
|
||||||
|
com.sololearn
|
||||||
|
com.sonelli.juicessh
|
||||||
|
com.sparkslab.dcardreader
|
||||||
|
com.spotify.music
|
||||||
|
com.tencent.huatuo
|
||||||
|
com.termux
|
||||||
|
com.teslacoilsw.launcher
|
||||||
|
com.theinitium.news
|
||||||
|
com.thomsonreuters.reuters
|
||||||
|
com.thunkable.android.hritvik00.freenom
|
||||||
|
com.topjohnwu.magisk
|
||||||
|
com.tripadvisor.tripadvisor
|
||||||
|
com.tumblr
|
||||||
|
com.twitter.android
|
||||||
|
com.u91porn
|
||||||
|
com.u9porn
|
||||||
|
com.ubisoft.dance.justdance2015companion
|
||||||
|
com.udn.news
|
||||||
|
com.utopia.pxview
|
||||||
|
com.valvesoftware.android.steam.community
|
||||||
|
com.vanced.manager
|
||||||
|
com.vanced.android.youtube
|
||||||
|
com.vanced.android.apps.youtube.music
|
||||||
|
com.mgoogle.android.gms
|
||||||
|
com.vimeo.android.videoapp
|
||||||
|
com.vivaldi.browser
|
||||||
|
com.vivaldi.browser.snapshot
|
||||||
|
com.vkontakte.android
|
||||||
|
com.whatsapp
|
||||||
|
com.wire
|
||||||
|
com.wuxiangai.refactor
|
||||||
|
com.xda.labs
|
||||||
|
com.xvideos.app
|
||||||
|
com.yahoo.mobile.client.android.superapp
|
||||||
|
com.yandex.browser
|
||||||
|
com.yandex.browser.beta
|
||||||
|
com.yandex.browser.alpha
|
||||||
|
com.z28j.feel
|
||||||
|
com.zhiliaoapp.musically
|
||||||
|
con.medium.reader
|
||||||
|
de.apkgrabber
|
||||||
|
de.robv.android.xposed.installer
|
||||||
|
dk.tacit.android.foldersync.full
|
||||||
|
es.rafalense.telegram.themes
|
||||||
|
es.rafalense.themes
|
||||||
|
flipboard.app
|
||||||
|
fm.moon.app
|
||||||
|
fr.gouv.etalab.mastodon
|
||||||
|
github.tornaco.xposedmoduletest
|
||||||
|
idm.internet.download.manager
|
||||||
|
idm.internet.download.manager.plus
|
||||||
|
io.github.javiewer
|
||||||
|
io.github.skyhacker2.magnetsearch
|
||||||
|
io.va.exposed
|
||||||
|
it.mvilla.android.fenix2
|
||||||
|
jp.bokete.app.android
|
||||||
|
jp.naver.line.android
|
||||||
|
jp.pxv.android
|
||||||
|
luo.speedometergpspro
|
||||||
|
m.cna.com.tw.App
|
||||||
|
mark.via.gp
|
||||||
|
me.tshine.easymark
|
||||||
|
net.teeha.android.url_shortener
|
||||||
|
net.tsapps.appsales
|
||||||
|
onion.fire
|
||||||
|
org.fdroid.fdroid
|
||||||
|
org.freedownloadmanager.fdm
|
||||||
|
org.kustom.widget
|
||||||
|
org.mozilla.fennec_aurora
|
||||||
|
org.mozilla.fenix
|
||||||
|
org.mozilla.fenix.nightly
|
||||||
|
org.mozilla.firefox
|
||||||
|
org.mozilla.firefox_beta
|
||||||
|
org.mozilla.focus
|
||||||
|
org.schabi.newpipe
|
||||||
|
org.telegram.messenger
|
||||||
|
org.telegram.messenger.web
|
||||||
|
org.telegram.multi
|
||||||
|
org.telegram.plus
|
||||||
|
org.thunderdog.challegram
|
||||||
|
org.torproject.android
|
||||||
|
org.torproject.torbrowser_alpha
|
||||||
|
org.wikipedia
|
||||||
|
org.xbmc.kodi
|
||||||
|
pl.zdunex25.updater
|
||||||
|
tv.twitch.android.app
|
||||||
|
tw.com.gamer.android.activecenter
|
||||||
|
videodownloader.downloadvideo.downloader
|
||||||
|
uk.co.bbc.learningenglish
|
||||||
|
com.ted.android
|
||||||
|
de.danoeh.antennapod
|
||||||
|
com.kiwibrowser.browser
|
||||||
|
nekox.messenger
|
||||||
|
com.nextcloud.client
|
||||||
|
com.aurora.store
|
||||||
|
com.aurora.adroid
|
||||||
|
chat.simplex.app
|
||||||
|
im.vector.app
|
||||||
|
network.loki.messenger
|
||||||
|
eu.siacs.conversations
|
||||||
|
xyz.nextalone.nagram
|
||||||
|
net.programmierecke.radiodroid2
|
||||||
|
im.fdx.v2ex
|
||||||
|
ml.docilealligator.infinityforreddit
|
||||||
|
com.bytemyth.ama
|
||||||
|
app.vanadium.browser
|
||||||
|
com.cakewallet.cake_wallet
|
||||||
|
org.purplei2p.i2pd
|
||||||
|
dk.tacit.android.foldersync.lite
|
||||||
|
com.nononsenseapps.feeder
|
||||||
|
com.m2049r.xmrwallet
|
||||||
|
com.paypal.android.p2pmobile
|
||||||
|
com.google.android.apps.googlevoice
|
||||||
|
com.readdle.spark
|
||||||
|
org.torproject.torbrowser
|
||||||
|
com.deepl.mobiletranslator
|
||||||
|
com.microsoft.bing
|
||||||
|
com.keylesspalace.tusky
|
||||||
|
com.ottplay.ottplay
|
||||||
|
ru.iptvremote.android.iptv.pro
|
||||||
|
jp.naver.line.android
|
||||||
|
com.xmflsct.app.tooot
|
||||||
|
com.forem.android
|
||||||
|
app.revanced.android.youtube
|
||||||
|
com.mgoogle.android.gms
|
||||||
|
com.pionex.client
|
||||||
|
vip.mytokenpocket
|
||||||
|
im.token.app
|
||||||
|
com.linekong.mars24
|
||||||
|
com.feixiaohao
|
||||||
|
com.aicoin.appandroid
|
||||||
|
com.binance.dev
|
||||||
|
com.kraken.trade
|
||||||
|
com.okinc.okex.gp
|
||||||
|
com.authy.authy
|
||||||
|
air.com.rosettastone.mobile.CoursePlayer
|
||||||
|
com.blizzard.bma
|
||||||
|
com.amazon.kindle
|
||||||
|
com.google.android.apps.fitness
|
||||||
|
net.tsapps.appsales
|
||||||
|
com.wemesh.android
|
||||||
|
com.google.android.apps.googleassistant
|
||||||
|
allen.town.focus.reader
|
||||||
|
me.hyliu.fluent_reader_lite
|
||||||
|
com.aljazeera.mobile
|
||||||
|
com.ft.news
|
||||||
|
de.marmaro.krt.ffupdater
|
||||||
|
myradio.radio.fmradio.liveradio.radiostation
|
||||||
|
com.google.earth
|
||||||
|
eu.kanade.tachiyomi.j2k
|
||||||
|
com.audials
|
||||||
|
com.microsoft.skydrive
|
||||||
|
com.mb.android.tg
|
||||||
|
com.melodis.midomiMusicIdentifier.freemium
|
||||||
|
com.foxnews.android
|
||||||
|
ch.threema.app
|
||||||
|
com.briarproject.briar.android
|
||||||
|
foundation.e.apps
|
||||||
|
com.valvesoftware.android.steam.friendsui
|
||||||
|
com.imback.yeetalk
|
||||||
|
so.onekey.app.wallet
|
||||||
|
com.xc3fff0e.xmanager
|
||||||
|
meditofoundation.medito
|
||||||
|
com.picol.client
|
||||||
|
com.streetwriters.notesnook
|
||||||
|
shanghai.panewsApp.com
|
||||||
|
org.coursera.android
|
||||||
|
com.positron_it.zlib
|
||||||
|
com.blizzard.messenger
|
||||||
|
com.javdb.javrocket
|
||||||
|
com.picacomic.fregata
|
||||||
|
com.fxl.chacha
|
||||||
|
me.proton.android.drive
|
||||||
|
com.lastpass.lpandroid
|
||||||
|
com.tradingview.tradingviewapp
|
||||||
|
com.deviantart.android.damobile
|
||||||
|
com.fusionmedia.investing
|
||||||
|
com.ewa.ewaapp
|
||||||
|
com.duolingo
|
||||||
|
com.hellotalk
|
||||||
|
io.github.huskydg.magisk
|
||||||
|
com.jsy.xpgbox
|
||||||
|
com.hostloc.app.hostloc
|
||||||
|
com.dena.pokota
|
||||||
|
com.vitorpamplona.amethyst
|
||||||
|
com.zhiliaoapp.musically
|
||||||
|
us.spotco.fennec_dos
|
||||||
|
com.fongmi.android.tv
|
||||||
|
com.pocketprep.android.itcybersecurity
|
||||||
|
com.cloudtv
|
||||||
|
com.glassdoor.app
|
||||||
|
com.indeed.android.jobsearch
|
||||||
|
com.linkedin.android
|
||||||
|
com.github.tvbox.osc.bh
|
||||||
|
com.example.douban
|
||||||
|
com.sipnetic.app
|
||||||
|
com.microsoft.rdc.androidx
|
||||||
|
org.zwanoo.android.speedtest
|
||||||
|
com.sonelli.juicessh
|
||||||
|
com.scmp.newspulse
|
||||||
|
org.lsposed.manager
|
||||||
|
mnn.Android
|
||||||
|
com.thomsonretuers.reuters
|
||||||
|
com.guardian
|
||||||
|
com.ttxapps.onesyncv2
|
||||||
|
org.fcitx.fcitx5.android.updater
|
||||||
|
com.tailscale.ipn
|
||||||
|
tw.nekomimi.nekogram
|
||||||
|
com.nexon.kartdrift
|
||||||
|
io.syncapps.lemmy_sync
|
||||||
|
com.seazon.feedme
|
||||||
|
com.readwise
|
||||||
|
de.spiritcroc.riotx
|
||||||
|
com.openai.chatgpt
|
||||||
|
io.changenow.changenow
|
||||||
|
com.poe.android
|
||||||
|
com.twingate
|
||||||
|
com.blinkslabs.blinkist.android
|
||||||
|
com.ichi2.anki
|
||||||
|
md.obsidian
|
||||||
|
com.musixmatch.android.lyrify
|
||||||
|
com.cyber.turbo
|
||||||
|
com.offsec.nethunter
|
||||||
|
me.ghui.v2er
|
||||||
|
com.samruston.twitter
|
||||||
|
org.adaway
|
||||||
|
org.swiftapps.swiftbackup
|
||||||
|
com.zerotier.one
|
||||||
|
com.quietmobile
|
||||||
|
com.instagram.barcelona
|
||||||
|
im.molly.app
|
||||||
|
com.rvx.android.youtube
|
||||||
|
com.deepl.mobiletranslator
|
||||||
|
com.qingsong.yingmi
|
||||||
|
com.lemurbrowser.exts
|
||||||
|
com.silverdev.dnartdroid
|
||||||
|
me.ash.reader
|
||||||
|
de.tutao.tutanota
|
||||||
|
dev.imranr.obtainium
|
||||||
|
com.getsomeheadspace.android
|
||||||
|
org.cromite.cromite
|
||||||
|
com.nutomic.syncthingandroid
|
||||||
|
com.bumble.app
|
||||||
|
com.cnn.mobile.android.phone
|
||||||
|
com.google.android.apps.authenticator2
|
||||||
|
com.microsoft.copilot
|
||||||
|
com.netflix.NGP.Storyteller
|
||||||
|
com.Slack
|
||||||
|
com.server.auditor.ssh.client
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
com.android.chrome
|
|
||||||
com.google.android.googlequicksearchbox
|
|
||||||
com.google.android.apps.photos
|
|
||||||
com.google.android.youtube
|
|
||||||
com.google.android.gm
|
|
||||||
com.google.android.apps.plus
|
|
||||||
com.android.vending
|
|
||||||
com.google.android.inputmethod.latin
|
|
||||||
com.google.android.apps.paidtasks
|
|
||||||
com.google.android.keep
|
|
||||||
com.google.android.gms.setup
|
|
||||||
com. google.android. apps.magazines
|
|
||||||
com.google.android.videos
|
|
||||||
com. google.android.gms
|
|
||||||
com.google.android.apps.books
|
|
||||||
com.google.android.music
|
|
||||||
com.google.android.play.games
|
|
||||||
com.google.android.gsf
|
|
||||||
com.google.android.gsf.login
|
|
||||||
com.app.pornhub
|
|
||||||
com.spotify.music
|
|
||||||
org.thunderdog.challegram
|
|
||||||
com.tumblr
|
|
||||||
com.twitter.android
|
|
||||||
com.xda.labs
|
|
||||||
com.kapp.youtube.final
|
|
||||||
com.google.android.ims
|
|
||||||
com.wire
|
|
||||||
mark.via.gp
|
|
||||||
com.downloader.video.tumblr
|
|
||||||
com.sololearn
|
|
||||||
com.cygames.shadowverse
|
|
||||||
com.felixfilip.scpae
|
|
||||||
amanita_design.samorost3.gp
|
|
||||||
com.devolver.reigns2
|
|
||||||
com.utopia.pxview
|
|
||||||
ch.protonmail.android
|
|
||||||
com.perol.asdpl.pixivez
|
|
||||||
com.pinterest
|
|
||||||
com.paypal.android.p2pmobile
|
|
||||||
com.arthurivanets.owly
|
|
||||||
com.rubenmayayo.reddit
|
|
||||||
com.rayark.cytus2
|
|
||||||
com.rayark.pluto
|
|
||||||
com.rayark.implosion
|
|
||||||
com.fireproofstudios.theroom4
|
|
||||||
com.netflix.mediaclient
|
|
||||||
com.instagram.android
|
|
||||||
com.google.android.apps.hangoutsdialer
|
|
||||||
com.google.android.talk
|
|
||||||
com.google.android.apps.plus
|
|
||||||
com.google.android.apps.pdfviewer
|
|
||||||
com.google.android.apps.magazines
|
|
||||||
com.google.android.apps.nbu.files
|
|
||||||
com.evernote
|
|
||||||
net.tsapps.appsales
|
|
||||||
com.google.android.apps.translate
|
|
||||||
com.google.ar.lens
|
|
||||||
com.google.android.apps.adm
|
|
||||||
com.google.android.apps.googleassistant
|
|
||||||
tw.com.gamer.android.activecenter
|
|
||||||
org.telegram.plus
|
|
||||||
com.brave.browser
|
|
||||||
com.breel.wallpapers18
|
|
||||||
com.teslacoilsw.launcher
|
|
||||||
com.lastpass.lpandroid
|
|
||||||
org.kustom.widget
|
|
||||||
com.fooview.android.fooview
|
|
||||||
com.google.android.apps.docs
|
|
||||||
com.google.android.apps.maps
|
|
||||||
com.facebook.services
|
|
||||||
com.facebook.system
|
|
||||||
com.facebook.katana
|
|
||||||
com.nianticlabs.ingress.prime.qa
|
|
||||||
com.vanced.android.youtube
|
|
||||||
com.nianticproject.ingress
|
|
||||||
com.quoord.tapatalkpro.activity
|
|
||||||
org.mozilla.firefox
|
|
||||||
com.reddit.frontpage
|
|
||||||
com.google.android.apps.fitness
|
|
||||||
au.com.shiftyjelly.pocketcasts
|
|
||||||
com.google.android.gms
|
|
||||||
com.android.providers.telephony
|
|
||||||
com.resilio.sync
|
|
||||||
com.google.android.apps.googlevoice
|
|
||||||
com.discord
|
|
||||||
com.cradle.iitc_mobile
|
|
||||||
be.mygod.vpnhotspot
|
|
||||||
com.alphainventor.filemanager
|
|
||||||
com.android.providers.downloads
|
|
||||||
com.apkpure.aegon
|
|
||||||
com.ballistiq.artstation
|
|
||||||
com.bitly.app
|
|
||||||
com.chrome.canary
|
|
||||||
com.chrome.dev
|
|
||||||
com.devhd.feedly
|
|
||||||
com.dropbox.android
|
|
||||||
com.estrongs.android.pop
|
|
||||||
com.estrongs.android.pop.pro
|
|
||||||
com.fastaccess.github
|
|
||||||
com.firstrowria.pushnotificationtester
|
|
||||||
com.fvd.eversync
|
|
||||||
com.gianlu.aria2app
|
|
||||||
com.github.yeriomin.yalpstore
|
|
||||||
com.google.android.apps.docs.editors.sheets
|
|
||||||
com.google.android.instantapps.supervisor
|
|
||||||
com.google.android.ogyoutube
|
|
||||||
com.google.android.partnersetup
|
|
||||||
com.google.android.syncadapters.calendar
|
|
||||||
com.google.android.syncadapters.contacts
|
|
||||||
com.google.android.tts
|
|
||||||
com.hochan.coldsoup
|
|
||||||
com.ifttt.ifttt
|
|
||||||
com.imgur.mobile
|
|
||||||
com.innologica.inoreader
|
|
||||||
com.instapaper.android
|
|
||||||
com.jarvanh.vpntether
|
|
||||||
com.mediapods.tumbpods
|
|
||||||
com.mgoogle.android.gms
|
|
||||||
com.microsoft.office.powerpoint
|
|
||||||
com.mixplorer
|
|
||||||
com.msd.consumerchinese
|
|
||||||
com.msd.professionalchinese
|
|
||||||
com.mss2011c.sharehelper
|
|
||||||
com.newin.nplayer.pro
|
|
||||||
com.oasisfeng.island
|
|
||||||
com.orekie.search
|
|
||||||
com.popularapp.videodownloaderforinstagram
|
|
||||||
com.pushbullet.android
|
|
||||||
com.rhmsoft.edit
|
|
||||||
com.slack
|
|
||||||
com.tencent.huatuo
|
|
||||||
com.termux
|
|
||||||
com.thunkable.android.hritvik00.freenom
|
|
||||||
com.topjohnwu.magisk
|
|
||||||
com.u91porn
|
|
||||||
com.u9porn
|
|
||||||
com.vimeo.android.videoapp
|
|
||||||
com.wuxiangai.refactor
|
|
||||||
com.yandex.browser
|
|
||||||
com.z28j.feel
|
|
||||||
de.robv.android.xposed.installer
|
|
||||||
dk.tacit.android.foldersync.full
|
|
||||||
es.rafalense.telegram.themes
|
|
||||||
es.rafalense.themes
|
|
||||||
flipboard.app
|
|
||||||
github.tornaco.xposedmoduletest
|
|
||||||
io.va.exposed
|
|
||||||
jp.pxv.android
|
|
||||||
me.tshine.easymark
|
|
||||||
net.teeha.android.url_shortener
|
|
||||||
onion.fire
|
|
||||||
org.fdroid.fdroid
|
|
||||||
org.mozilla.fennec_aurora
|
|
||||||
org.schabi.newpipe
|
|
||||||
org.telegram.messenger
|
|
||||||
org.torproject.android
|
|
||||||
org.xbmc.kodi
|
|
||||||
pl.zdunex25.updater
|
|
||||||
videodownloader.downloadvideo.downloader
|
|
||||||
com.quora.android
|
|
||||||
com.lingodeer
|
|
||||||
org.wikipedia
|
|
||||||
com.ninegag.android.app
|
|
||||||
com.duolingo
|
|
||||||
com.patreon.android
|
|
||||||
com.valvesoftware.android.steam.communimunity
|
|
||||||
co.wanqu.android
|
|
||||||
jp.bokete.app.android
|
|
||||||
com.vkontakte.android
|
|
||||||
com.amazon.mshop.android.shopping
|
|
||||||
com.ubisoft.dance.justdance2015companion
|
|
||||||
com.gameloft.android.anmp.glofta8hm
|
|
||||||
com.gameloft.android.anmp.glofta9hm
|
|
||||||
com.binance.dev
|
|
||||||
com.asahi.tida.tablet
|
|
||||||
com.theinitium.news
|
|
||||||
com.driverbrowser
|
|
||||||
com.thomsonreuters.reuters
|
|
||||||
com.nytimes.cn
|
|
||||||
com.android.providers.downloads.ui
|
|
||||||
com.avmovie
|
|
||||||
bbc.mobile.news.ww
|
|
||||||
org.mozilla.focus
|
|
||||||
io.github.javiewer
|
|
||||||
com.sonelli.juicessh
|
|
||||||
con.medium.reader
|
|
||||||
com.microsoft.skydrive
|
|
||||||
com.valvesoftware.android.steam.community
|
|
||||||
com.nintendo.zara
|
|
||||||
org.torproject.torbrowser_alpha
|
|
||||||
tv.twitch.android.app
|
|
||||||
com.shanga.walli
|
|
||||||
com.whatsapp
|
|
||||||
com.wire
|
|
||||||
com.simplehabit.simplehabitapp
|
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"statsInboundUplink": true,
|
"statsOutboundUplink": true,
|
||||||
"statsInboundDownlink": true
|
"statsOutboundDownlink": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"inbounds": [{
|
"inbounds": [{
|
||||||
@@ -30,17 +30,10 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"destOverride": [
|
"destOverride": [
|
||||||
"http",
|
"http",
|
||||||
"tls"
|
"tls",
|
||||||
|
"quic"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"tag": "http",
|
|
||||||
"port": 10809,
|
|
||||||
"protocol": "http",
|
|
||||||
"settings": {
|
|
||||||
"userLevel": 8
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outbounds": [{
|
"outbounds": [{
|
||||||
@@ -54,7 +47,7 @@
|
|||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||||
"alterId": 64,
|
"alterId": 0,
|
||||||
"security": "auto",
|
"security": "auto",
|
||||||
"level": 8
|
"level": 8
|
||||||
}
|
}
|
||||||
@@ -81,7 +74,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"protocol": "freedom",
|
"protocol": "freedom",
|
||||||
"settings": {},
|
"streamSettings": {
|
||||||
|
"sockopt": {
|
||||||
|
"domainStrategy": "UseIP"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tag": "direct"
|
"tag": "direct"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -95,7 +92,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"routing": {
|
"routing": {
|
||||||
"domainStrategy": "IPIfNonMatch",
|
"domainStrategy": "AsIs",
|
||||||
"rules": []
|
"rules": []
|
||||||
},
|
},
|
||||||
"dns": {
|
"dns": {
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
"stats":{},
|
||||||
|
"log": {
|
||||||
|
"loglevel": "warning"
|
||||||
|
},
|
||||||
|
"policy":{
|
||||||
|
"levels": {
|
||||||
|
"8": {
|
||||||
|
"handshake": 4,
|
||||||
|
"connIdle": 300,
|
||||||
|
"uplinkOnly": 1,
|
||||||
|
"downlinkOnly": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"statsOutboundUplink": true,
|
||||||
|
"statsOutboundDownlink": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inbounds": [{
|
||||||
|
"tag": "socks",
|
||||||
|
"port": 10808,
|
||||||
|
"protocol": "socks",
|
||||||
|
"settings": {
|
||||||
|
"auth": "noauth",
|
||||||
|
"udp": true,
|
||||||
|
"userLevel": 8
|
||||||
|
},
|
||||||
|
"sniffing": {
|
||||||
|
"enabled": true,
|
||||||
|
"destOverride": [
|
||||||
|
"http",
|
||||||
|
"tls",
|
||||||
|
"quic"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "tun",
|
||||||
|
"protocol": "tun",
|
||||||
|
"settings": {
|
||||||
|
"name": "xray0",
|
||||||
|
"MTU": 1500,
|
||||||
|
"userLevel": 8
|
||||||
|
},
|
||||||
|
"sniffing": {
|
||||||
|
"enabled": true,
|
||||||
|
"destOverride": [
|
||||||
|
"http",
|
||||||
|
"tls",
|
||||||
|
"quic"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [{
|
||||||
|
"tag": "proxy",
|
||||||
|
"protocol": "vmess",
|
||||||
|
"settings": {
|
||||||
|
"vnext": [
|
||||||
|
{
|
||||||
|
"address": "v2ray.cool",
|
||||||
|
"port": 10086,
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "a3482e88-686a-4a58-8126-99c9df64b7bf",
|
||||||
|
"alterId": 0,
|
||||||
|
"security": "auto",
|
||||||
|
"level": 8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"address": "v2ray.cool",
|
||||||
|
"method": "chacha20",
|
||||||
|
"ota": false,
|
||||||
|
"password": "123456",
|
||||||
|
"port": 10086,
|
||||||
|
"level": 8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"streamSettings": {
|
||||||
|
"network": "tcp"
|
||||||
|
},
|
||||||
|
"mux": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocol": "freedom",
|
||||||
|
"streamSettings": {
|
||||||
|
"sockopt": {
|
||||||
|
"domainStrategy": "UseIP"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocol": "blackhole",
|
||||||
|
"tag": "block",
|
||||||
|
"settings": {
|
||||||
|
"response": {
|
||||||
|
"type": "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routing": {
|
||||||
|
"domainStrategy": "AsIs",
|
||||||
|
"rules": []
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"hosts": {},
|
||||||
|
"servers": []
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,48 @@
|
|||||||
|
package com.v2ray.ang
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.multidex.MultiDexApplication
|
||||||
|
import androidx.work.Configuration
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import com.tencent.mmkv.MMKV
|
||||||
|
import com.v2ray.ang.AppConfig.ANG_PACKAGE
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
|
||||||
|
class AngApplication : MultiDexApplication() {
|
||||||
|
companion object {
|
||||||
|
lateinit var application: AngApplication
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the base context to the application.
|
||||||
|
* @param base The base context.
|
||||||
|
*/
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
application = this
|
||||||
|
}
|
||||||
|
|
||||||
|
private val workManagerConfiguration: Configuration = Configuration.Builder()
|
||||||
|
.setDefaultProcessName("${ANG_PACKAGE}:bg")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the application.
|
||||||
|
*/
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
MMKV.initialize(this)
|
||||||
|
|
||||||
|
// Initialize WorkManager with the custom configuration
|
||||||
|
WorkManager.initialize(this, workManagerConfiguration)
|
||||||
|
|
||||||
|
// Ensure critical preference defaults are present in MMKV early
|
||||||
|
SettingsManager.initApp(this)
|
||||||
|
SettingsManager.setNightMode()
|
||||||
|
|
||||||
|
es.dmoral.toasty.Toasty.Config.getInstance()
|
||||||
|
.setGravity(android.view.Gravity.BOTTOM, 0, 300)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
package com.v2ray.ang
|
||||||
|
|
||||||
|
|
||||||
|
object AppConfig {
|
||||||
|
|
||||||
|
/** The application's package name. */
|
||||||
|
const val ANG_PACKAGE = BuildConfig.APPLICATION_ID
|
||||||
|
const val TAG = BuildConfig.APPLICATION_ID
|
||||||
|
|
||||||
|
/** Directory names used in the app's file system. */
|
||||||
|
const val DIR_ASSETS = "assets"
|
||||||
|
|
||||||
|
const val WEBDAV_BACKUP_DIR = "backups"
|
||||||
|
const val WEBDAV_BACKUP_FILE_NAME = "backup_ng.zip"
|
||||||
|
|
||||||
|
/** Legacy configuration keys. */
|
||||||
|
const val ANG_CONFIG = "ang_config"
|
||||||
|
|
||||||
|
// Default subscription ID for ungrouped servers
|
||||||
|
const val DEFAULT_SUBSCRIPTION_ID = "__default_subscription__"
|
||||||
|
|
||||||
|
/** Preferences mapped to MMKV storage. */
|
||||||
|
const val PREF_SNIFFING_ENABLED = "pref_sniffing_enabled"
|
||||||
|
const val PREF_ROUTE_ONLY_ENABLED = "pref_route_only_enabled"
|
||||||
|
const val PREF_PER_APP_PROXY = "pref_per_app_proxy"
|
||||||
|
const val PREF_PER_APP_PROXY_SET = "pref_per_app_proxy_set"
|
||||||
|
const val PREF_BYPASS_APPS = "pref_bypass_apps"
|
||||||
|
const val PREF_LOCAL_DNS_ENABLED = "pref_local_dns_enabled"
|
||||||
|
const val PREF_FAKE_DNS_ENABLED = "pref_fake_dns_enabled"
|
||||||
|
const val PREF_APPEND_HTTP_PROXY = "pref_append_http_proxy"
|
||||||
|
const val PREF_LOCAL_DNS_PORT = "pref_local_dns_port"
|
||||||
|
const val PREF_VPN_DNS = "pref_vpn_dns"
|
||||||
|
const val PREF_VPN_BYPASS_LAN = "pref_vpn_bypass_lan"
|
||||||
|
const val PREF_VPN_INTERFACE_ADDRESS_CONFIG_INDEX = "pref_vpn_interface_address_config_index"
|
||||||
|
const val PREF_VPN_MTU = "pref_vpn_mtu"
|
||||||
|
const val PREF_ROUTING_DOMAIN_STRATEGY = "pref_routing_domain_strategy"
|
||||||
|
const val PREF_ROUTING_RULESET = "pref_routing_ruleset"
|
||||||
|
const val PREF_MUX_ENABLED = "pref_mux_enabled"
|
||||||
|
const val PREF_MUX_CONCURRENCY = "pref_mux_concurrency"
|
||||||
|
const val PREF_MUX_XUDP_CONCURRENCY = "pref_mux_xudp_concurrency"
|
||||||
|
const val PREF_MUX_XUDP_QUIC = "pref_mux_xudp_quic"
|
||||||
|
const val PREF_FRAGMENT_ENABLED = "pref_fragment_enabled"
|
||||||
|
const val PREF_FRAGMENT_PACKETS = "pref_fragment_packets"
|
||||||
|
const val PREF_FRAGMENT_LENGTH = "pref_fragment_length"
|
||||||
|
const val PREF_FRAGMENT_INTERVAL = "pref_fragment_interval"
|
||||||
|
const val SUBSCRIPTION_UPDATE_TASK_NAME = "subscription_updater"
|
||||||
|
const val SUBSCRIPTION_MIN_INTERVAL_MINUTES = 15L
|
||||||
|
const val PREF_SPEED_ENABLED = "pref_speed_enabled"
|
||||||
|
const val PREF_CONFIRM_REMOVE = "pref_confirm_remove"
|
||||||
|
const val PREF_START_SCAN_IMMEDIATE = "pref_start_scan_immediate"
|
||||||
|
const val PREF_DOUBLE_COLUMN_DISPLAY = "pref_double_column_display"
|
||||||
|
const val PREF_GROUP_ALL_DISPLAY = "pref_group_all_display"
|
||||||
|
const val PREF_LANGUAGE = "pref_language"
|
||||||
|
const val PREF_UI_MODE_NIGHT = "pref_ui_mode_night"
|
||||||
|
const val PREF_IPV6_ENABLED = "pref_ipv6_enabled"
|
||||||
|
const val PREF_PREFER_IPV6 = "pref_prefer_ipv6"
|
||||||
|
const val PREF_PROXY_SHARING = "pref_proxy_sharing_enabled"
|
||||||
|
const val PREF_ALLOW_INSECURE = "pref_allow_insecure"
|
||||||
|
const val PREF_ENABLE_LOCAL_PROXY = "pref_enable_local_proxy"
|
||||||
|
const val PREF_SOCKS_PORT = "pref_socks_port"
|
||||||
|
const val PREF_DYNAMIC_SOCKS_PORT = "pref_dynamic_socks_port"
|
||||||
|
const val PREF_SOCKS_USERNAME = "pref_socks_username"
|
||||||
|
const val PREF_SOCKS_PASSWORD = "pref_socks_password"
|
||||||
|
const val PREF_SOCKS_ENABLE_UDP = "pref_socks_enable_udp"
|
||||||
|
const val PREF_REMOTE_DNS = "pref_remote_dns"
|
||||||
|
const val PREF_DOMESTIC_DNS = "pref_domestic_dns"
|
||||||
|
const val PREF_DNS_HOSTS = "pref_dns_hosts"
|
||||||
|
const val PREF_DELAY_TEST_URL = "pref_delay_test_url"
|
||||||
|
const val PREF_IP_API_URL = "pref_ip_api_url"
|
||||||
|
const val PREF_LOGLEVEL = "pref_core_loglevel"
|
||||||
|
const val PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD = "pref_outbound_domain_resolve_method"
|
||||||
|
const val PREF_MODE = "pref_mode"
|
||||||
|
const val PREF_IS_BOOTED = "pref_is_booted"
|
||||||
|
const val PREF_CHECK_UPDATE_PRE_RELEASE = "pref_check_update_pre_release"
|
||||||
|
const val PREF_GEO_FILES_SOURCES = "pref_geo_files_sources"
|
||||||
|
const val PREF_USE_HEV_TUNNEL = "pref_use_hev_tunnel_v2"
|
||||||
|
const val PREF_HEV_TUNNEL_LOGLEVEL = "pref_hev_tunnel_loglevel"
|
||||||
|
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
|
||||||
|
const val PREF_AUTO_REMOVE_INVALID_AFTER_TEST = "pref_auto_remove_invalid_after_test"
|
||||||
|
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
|
||||||
|
const val PREF_REAL_PING_CONCURRENCY = "pref_real_ping_concurrency"
|
||||||
|
|
||||||
|
/** Cache keys. */
|
||||||
|
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
||||||
|
|
||||||
|
/** Protocol identifiers. */
|
||||||
|
const val PROTOCOL_FREEDOM = "freedom"
|
||||||
|
|
||||||
|
/** Broadcast actions. */
|
||||||
|
const val BROADCAST_ACTION_SERVICE = "$ANG_PACKAGE.action.service"
|
||||||
|
const val BROADCAST_ACTION_ACTIVITY = "$ANG_PACKAGE.action.activity"
|
||||||
|
const val BROADCAST_ACTION_WIDGET_CLICK = "$ANG_PACKAGE.action.widget.click"
|
||||||
|
|
||||||
|
/** Tasker extras. */
|
||||||
|
const val TASKER_EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"
|
||||||
|
const val TASKER_EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"
|
||||||
|
const val TASKER_EXTRA_BUNDLE_SWITCH = "tasker_extra_bundle_switch"
|
||||||
|
const val TASKER_EXTRA_BUNDLE_GUID = "tasker_extra_bundle_guid"
|
||||||
|
const val TASKER_DEFAULT_GUID = "Default"
|
||||||
|
|
||||||
|
/** Tags for different proxy modes. */
|
||||||
|
const val TAG_PROXY = "proxy"
|
||||||
|
const val TAG_DIRECT = "direct"
|
||||||
|
const val TAG_BLOCKED = "block"
|
||||||
|
const val TAG_FRAGMENT = "fragment"
|
||||||
|
const val TAG_DNS = "dns-module"
|
||||||
|
const val TAG_DOMESTIC_DNS = "domestic-dns"
|
||||||
|
const val TAG_BALANCER = "balancer-main"
|
||||||
|
const val TAG_BALANCER_PRE = "balancer"
|
||||||
|
|
||||||
|
/** Network-related constants. */
|
||||||
|
const val UPLINK = "uplink"
|
||||||
|
const val DOWNLINK = "downlink"
|
||||||
|
|
||||||
|
/** URLs for various resources. */
|
||||||
|
const val GITHUB_URL = "https://github.com"
|
||||||
|
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com"
|
||||||
|
const val GITHUB_DOWNLOAD_URL = "$GITHUB_URL/%s/releases/latest/download"
|
||||||
|
const val ANDROID_PACKAGE_NAME_LIST_URL = "$GITHUB_RAW_URL/2dust/androidpackagenamelist/master/proxy.txt"
|
||||||
|
const val APP_URL = "$GITHUB_URL/2dust/v2rayNG"
|
||||||
|
const val APP_API_URL = "https://api.github.com/repos/2dust/v2rayNG/releases"
|
||||||
|
const val APP_ISSUES_URL = "$APP_URL/issues"
|
||||||
|
const val APP_WIKI_MODE = "$APP_URL/wiki/Mode"
|
||||||
|
const val APP_PRIVACY_POLICY = "$GITHUB_RAW_URL/2dust/v2rayNG/master/CR.md"
|
||||||
|
const val APP_PROMOTION_URL = "aHR0cHM6Ly85LjIzNDQ1Ni54eXovYWJjLmh0bWw="
|
||||||
|
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
||||||
|
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
||||||
|
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
||||||
|
|
||||||
|
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
||||||
|
const val IP_API_URL = "https://api.ip.sb/geoip"
|
||||||
|
|
||||||
|
/** DNS server addresses. */
|
||||||
|
const val DNS_PROXY = "1.1.1.1"
|
||||||
|
const val DNS_DIRECT = "223.5.5.5"
|
||||||
|
const val DNS_VPN = "1.1.1.1"
|
||||||
|
const val GEOSITE_PRIVATE = "geosite:private"
|
||||||
|
const val GEOSITE_CN = "geosite:cn"
|
||||||
|
const val GEOIP_PRIVATE = "geoip:private"
|
||||||
|
const val GEOIP_CN = "geoip:cn"
|
||||||
|
|
||||||
|
/** Geo data file names. */
|
||||||
|
const val GEOSITE_DAT = "geosite.dat"
|
||||||
|
const val GEOIP_DAT = "geoip.dat"
|
||||||
|
const val GEOIP_ONLY_CN_PRIVATE_DAT = "geoip-only-cn-private.dat"
|
||||||
|
const val GEOIP_ONLY_CN_PRIVATE_URL = "$GITHUB_RAW_URL/Loyalsoldier/geoip/release/$GEOIP_ONLY_CN_PRIVATE_DAT"
|
||||||
|
|
||||||
|
/** Ports and addresses for various services. */
|
||||||
|
const val PORT_LOCAL_DNS = "10853"
|
||||||
|
const val PORT_SOCKS = "10808"
|
||||||
|
const val WIREGUARD_LOCAL_ADDRESS_V4 = "172.16.0.2/32"
|
||||||
|
const val WIREGUARD_LOCAL_ADDRESS_V6 = "2606:4700:110:8f81:d551:a0:532e:a2b3/128"
|
||||||
|
const val WIREGUARD_LOCAL_MTU = "1420"
|
||||||
|
const val LOOPBACK = "127.0.0.1"
|
||||||
|
|
||||||
|
/** Message constants for communication. */
|
||||||
|
const val MSG_REGISTER_CLIENT = 1
|
||||||
|
const val MSG_STATE_RUNNING = 11
|
||||||
|
const val MSG_STATE_NOT_RUNNING = 12
|
||||||
|
const val MSG_UNREGISTER_CLIENT = 2
|
||||||
|
const val MSG_STATE_START = 3
|
||||||
|
const val MSG_STATE_START_SUCCESS = 31
|
||||||
|
const val MSG_STATE_START_FAILURE = 32
|
||||||
|
const val MSG_STATE_STOP = 4
|
||||||
|
const val MSG_STATE_STOP_SUCCESS = 41
|
||||||
|
const val MSG_STATE_RESTART = 5
|
||||||
|
const val MSG_MEASURE_DELAY = 6
|
||||||
|
const val MSG_MEASURE_DELAY_SUCCESS = 61
|
||||||
|
const val MSG_MEASURE_CONFIG_START = 7
|
||||||
|
const val MSG_MEASURE_CONFIG_CANCEL = 71
|
||||||
|
const val MSG_MEASURE_CONFIG_SUCCESS = 72
|
||||||
|
const val MSG_MEASURE_CONFIG_NOTIFY = 73
|
||||||
|
const val MSG_MEASURE_CONFIG_FINISH = 74
|
||||||
|
|
||||||
|
/** Notification channel IDs and names. */
|
||||||
|
const val RAY_NG_CHANNEL_ID = "RAY_NG_M_CH_ID"
|
||||||
|
const val RAY_NG_CHANNEL_NAME = "v2rayNG Background Service"
|
||||||
|
|
||||||
|
/** Protocols Scheme **/
|
||||||
|
const val VMESS = "vmess://"
|
||||||
|
const val CUSTOM = ""
|
||||||
|
const val SHADOWSOCKS = "ss://"
|
||||||
|
const val SOCKS = "socks://"
|
||||||
|
const val SOCKS4 = "socks4://"
|
||||||
|
const val SOCKS5 = "socks5://"
|
||||||
|
const val HTTP = "http://"
|
||||||
|
const val VLESS = "vless://"
|
||||||
|
const val TROJAN = "trojan://"
|
||||||
|
const val WIREGUARD = "wireguard://"
|
||||||
|
const val TUIC = "tuic://"
|
||||||
|
const val HYSTERIA = "hysteria://"
|
||||||
|
const val HYSTERIA2 = "hysteria2://"
|
||||||
|
const val HY2 = "hy2://"
|
||||||
|
|
||||||
|
/** Give a good name to this, IDK*/
|
||||||
|
const val VPN = "VPN"
|
||||||
|
const val VPN_MTU = 1500
|
||||||
|
|
||||||
|
/** hev-sock5-tunnel read-write-timeout value */
|
||||||
|
const val HEVTUN_RW_TIMEOUT = "300,60"
|
||||||
|
|
||||||
|
// Google API rule constants
|
||||||
|
const val GOOGLEAPIS_CN_DOMAIN = "domain:googleapis.cn"
|
||||||
|
const val GOOGLEAPIS_COM_DOMAIN = "googleapis.com"
|
||||||
|
|
||||||
|
// Android Private DNS constants
|
||||||
|
const val DNS_DNSPOD_DOMAIN = "dot.pub"
|
||||||
|
const val DNS_ALIDNS_DOMAIN = "dns.alidns.com"
|
||||||
|
const val DNS_CLOUDFLARE_ONE_DOMAIN = "one.one.one.one"
|
||||||
|
const val DNS_CLOUDFLARE_DNS_COM_DOMAIN = "dns.cloudflare.com"
|
||||||
|
const val DNS_CLOUDFLARE_DNS_DOMAIN = "cloudflare-dns.com"
|
||||||
|
const val DNS_GOOGLE_DOMAIN = "dns.google"
|
||||||
|
const val DNS_QUAD9_DOMAIN = "dns.quad9.net"
|
||||||
|
const val DNS_YANDEX_DOMAIN = "common.dot.dns.yandex.net"
|
||||||
|
|
||||||
|
const val DEFAULT_PORT = 443
|
||||||
|
const val DEFAULT_SECURITY = "auto"
|
||||||
|
const val DEFAULT_LEVEL = 8
|
||||||
|
const val DEFAULT_NETWORK = "tcp"
|
||||||
|
const val TLS = "tls"
|
||||||
|
const val REALITY = "reality"
|
||||||
|
const val HEADER_TYPE_HTTP = "http"
|
||||||
|
|
||||||
|
const val UNIDENTIFIED_PACKAGE = "__unknown_app__"
|
||||||
|
|
||||||
|
val DNS_ALIDNS_ADDRESSES = arrayListOf("223.5.5.5", "223.6.6.6", "2400:3200::1", "2400:3200:baba::1")
|
||||||
|
val DNS_CLOUDFLARE_ONE_ADDRESSES = arrayListOf("1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001")
|
||||||
|
val DNS_CLOUDFLARE_DNS_COM_ADDRESSES = arrayListOf("104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5")
|
||||||
|
val DNS_CLOUDFLARE_DNS_ADDRESSES = arrayListOf("104.16.248.249", "104.16.249.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9")
|
||||||
|
val DNS_DNSPOD_ADDRESSES = arrayListOf("1.12.12.12", "120.53.53.53")
|
||||||
|
val DNS_GOOGLE_ADDRESSES = arrayListOf("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844")
|
||||||
|
val DNS_QUAD9_ADDRESSES = arrayListOf("9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9")
|
||||||
|
val DNS_YANDEX_ADDRESSES = arrayListOf("77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff")
|
||||||
|
|
||||||
|
//minimum list https://serverfault.com/a/304791
|
||||||
|
val ROUTED_IP_LIST = arrayListOf(
|
||||||
|
"0.0.0.0/5",
|
||||||
|
"8.0.0.0/7",
|
||||||
|
"11.0.0.0/8",
|
||||||
|
"12.0.0.0/6",
|
||||||
|
"16.0.0.0/4",
|
||||||
|
"32.0.0.0/3",
|
||||||
|
"64.0.0.0/2",
|
||||||
|
"128.0.0.0/3",
|
||||||
|
"160.0.0.0/5",
|
||||||
|
"168.0.0.0/6",
|
||||||
|
"172.0.0.0/12",
|
||||||
|
"172.32.0.0/11",
|
||||||
|
"172.64.0.0/10",
|
||||||
|
"172.128.0.0/9",
|
||||||
|
"173.0.0.0/8",
|
||||||
|
"174.0.0.0/7",
|
||||||
|
"176.0.0.0/4",
|
||||||
|
"192.0.0.0/9",
|
||||||
|
"192.128.0.0/11",
|
||||||
|
"192.160.0.0/13",
|
||||||
|
"192.169.0.0/16",
|
||||||
|
"192.170.0.0/15",
|
||||||
|
"192.172.0.0/14",
|
||||||
|
"192.176.0.0/12",
|
||||||
|
"192.192.0.0/10",
|
||||||
|
"193.0.0.0/8",
|
||||||
|
"194.0.0.0/7",
|
||||||
|
"196.0.0.0/6",
|
||||||
|
"200.0.0.0/5",
|
||||||
|
"208.0.0.0/4",
|
||||||
|
"240.0.0.0/4"
|
||||||
|
)
|
||||||
|
|
||||||
|
val PRIVATE_IP_LIST = arrayListOf(
|
||||||
|
"0.0.0.0/8",
|
||||||
|
"10.0.0.0/8",
|
||||||
|
"127.0.0.0/8",
|
||||||
|
"172.16.0.0/12",
|
||||||
|
"192.168.0.0/16",
|
||||||
|
"169.254.0.0/16",
|
||||||
|
"224.0.0.0/4"
|
||||||
|
)
|
||||||
|
|
||||||
|
val GEO_FILES_SOURCES = arrayListOf(
|
||||||
|
"Loyalsoldier/v2ray-rules-dat",
|
||||||
|
"runetfreedom/russia-v2ray-rules-dat",
|
||||||
|
"Chocolate4U/Iran-v2ray-rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
val BUILTIN_OUTBOUND_TAGS = setOf(
|
||||||
|
TAG_PROXY,
|
||||||
|
TAG_DIRECT,
|
||||||
|
TAG_BLOCKED,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.v2ray.ang.contracts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common Adapter -> host callback interface that includes common actions: edit, remove and refresh.
|
||||||
|
* Extend this interface or define more specific interfaces for different adapters as needed.
|
||||||
|
*/
|
||||||
|
interface BaseAdapterListener {
|
||||||
|
/**
|
||||||
|
* Request the host to edit the specified item.
|
||||||
|
* @param guid Unique identifier (GUID) of the item
|
||||||
|
* @param position Current position in the adapter (optional; host should validate it)
|
||||||
|
*/
|
||||||
|
fun onEdit(guid: String, position: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the host to remove the specified item. Position is provided for optional animation or validation.
|
||||||
|
* @param guid Unique identifier (GUID) of the item
|
||||||
|
* @param position Current position in the adapter (optional; host should validate it)
|
||||||
|
*/
|
||||||
|
fun onRemove(guid: String, position: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the host to share the specified URL.
|
||||||
|
* @param url The URL to be shared
|
||||||
|
*/
|
||||||
|
fun onShare(url: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the host to refresh data (for example, reload from the ViewModel or call notifyDataSetChanged).
|
||||||
|
*/
|
||||||
|
fun onRefreshData()
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.v2ray.ang.contracts
|
||||||
|
|
||||||
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
|
|
||||||
|
interface MainAdapterListener : BaseAdapterListener {
|
||||||
|
|
||||||
|
fun onEdit(guid: String, position: Int, profile: ProfileItem)
|
||||||
|
|
||||||
|
fun onSelectServer(guid: String)
|
||||||
|
|
||||||
|
fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.v2ray.ang.contracts
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
|
||||||
|
interface ServiceControl {
|
||||||
|
/**
|
||||||
|
* Gets the service instance.
|
||||||
|
* @return The service instance.
|
||||||
|
*/
|
||||||
|
fun getService(): Service
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the service.
|
||||||
|
*/
|
||||||
|
fun startService()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service.
|
||||||
|
*/
|
||||||
|
fun stopService()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protects the VPN socket.
|
||||||
|
* @param socket The socket to protect.
|
||||||
|
* @return True if the socket is protected, false otherwise.
|
||||||
|
*/
|
||||||
|
fun vpnProtect(socket: Int): Boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.v2ray.ang.contracts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that defines the control operations for tun2socks implementations.
|
||||||
|
*
|
||||||
|
* This interface is implemented by different tunnel solutions like:
|
||||||
|
*/
|
||||||
|
interface Tun2SocksControl {
|
||||||
|
/**
|
||||||
|
* Starts the tun2socks process with the appropriate parameters.
|
||||||
|
* This initializes the VPN tunnel and connects it to the SOCKS proxy.
|
||||||
|
*/
|
||||||
|
fun startTun2Socks()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the tun2socks process and cleans up resources.
|
||||||
|
*/
|
||||||
|
fun stopTun2Socks()
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package com.v2ray.ang.core
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.CoreConfigContext
|
||||||
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
|
import com.v2ray.ang.enums.CoreResolvedType
|
||||||
|
import com.v2ray.ang.enums.EConfigType
|
||||||
|
import com.v2ray.ang.extension.isComplexType
|
||||||
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build runtime context from the selected profile.
|
||||||
|
*
|
||||||
|
* All outbound type analysis is completed here for both the selected profile
|
||||||
|
* and routing targets. Custom profiles are returned immediately without
|
||||||
|
* entering the normal analysis flow.
|
||||||
|
*/
|
||||||
|
object CoreConfigContextBuilder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load one profile and produce a fully analyzed context.
|
||||||
|
*
|
||||||
|
* Null is returned only when the selected profile cannot be loaded.
|
||||||
|
*/
|
||||||
|
fun build(context: Context, guid: String): CoreConfigContext? {
|
||||||
|
val config = MmkvManager.decodeServerConfig(guid) ?: return null
|
||||||
|
|
||||||
|
// CUSTOM: return immediately — CoreConfigManager handles this path on its own.
|
||||||
|
if (config.configType == EConfigType.CUSTOM) {
|
||||||
|
return CoreConfigContext(context = context, guid = guid, isCustom = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Resolve the main outbound (always tag = TAG_PROXY).
|
||||||
|
val primaryResolvedOutbound = resolveOutbound(AppConfig.TAG_PROXY, config) ?: run {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to resolve main outbound for '${config.remarks}'")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Resolve all non-builtin routing outbound tags.
|
||||||
|
val routingResolvedOutbounds = resolveRoutingOutbounds()
|
||||||
|
|
||||||
|
return CoreConfigContext(
|
||||||
|
context = context,
|
||||||
|
guid = guid,
|
||||||
|
resolvedOutbounds = listOf(primaryResolvedOutbound) + routingResolvedOutbounds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve one outbound target into a normalized outbound entry.
|
||||||
|
*
|
||||||
|
* Custom profiles are ignored at this stage and produce no entry.
|
||||||
|
*/
|
||||||
|
private fun resolveOutbound(tag: String, profile: ProfileItem): CoreConfigContext.ResolvedOutbound? {
|
||||||
|
if (profile.configType == EConfigType.CUSTOM) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val (resolvedProfiles, resolvedType) = when (profile.configType) {
|
||||||
|
EConfigType.POLICYGROUP -> Pair(
|
||||||
|
resolvePolicyGroupProfiles(profile),
|
||||||
|
CoreResolvedType.POLICYGROUP,
|
||||||
|
)
|
||||||
|
|
||||||
|
EConfigType.PROXYCHAIN -> {
|
||||||
|
val chainProfiles = resolveProxyChainProfiles(profile)
|
||||||
|
val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN
|
||||||
|
Pair(chainProfiles, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val chainProfiles = resolveProxyChainProfilesFromGroup(profile)
|
||||||
|
val type = if (chainProfiles.size <= 1) CoreResolvedType.NORMAL else CoreResolvedType.PROXYCHAIN
|
||||||
|
Pair(chainProfiles, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreConfigContext.ResolvedOutbound(
|
||||||
|
tag = tag,
|
||||||
|
profile = profile,
|
||||||
|
resolvedProfiles = resolvedProfiles,
|
||||||
|
resolvedType = resolvedType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect and resolve non-builtin routing targets from enabled rules.
|
||||||
|
*
|
||||||
|
* Invalid or empty targets are skipped and handled by fallback logic later.
|
||||||
|
*/
|
||||||
|
private fun resolveRoutingOutbounds(): List<CoreConfigContext.ResolvedOutbound> {
|
||||||
|
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return emptyList()
|
||||||
|
val resolvedOutbounds = mutableListOf<CoreConfigContext.ResolvedOutbound>()
|
||||||
|
val processedTags = mutableSetOf<String>()
|
||||||
|
|
||||||
|
try {
|
||||||
|
rulesetItems
|
||||||
|
.filter { it.enabled }
|
||||||
|
.mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } }
|
||||||
|
.filter { tag -> tag !in AppConfig.BUILTIN_OUTBOUND_TAGS }
|
||||||
|
.distinct()
|
||||||
|
.forEach { tag ->
|
||||||
|
if (tag in processedTags) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
processedTags.add(tag)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val profile = SettingsManager.getServerViaRemarks(tag) ?: run {
|
||||||
|
LogUtil.w(AppConfig.TAG, "Routing tag '$tag' has no matching profile — will fall back to proxy at routing time")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
val resolvedOutbound = resolveOutbound(tag, profile) ?: run {
|
||||||
|
LogUtil.w(AppConfig.TAG, "Cannot use CUSTOM profile as routing outbound for tag '$tag', skipping")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
if (resolvedOutbound.resolvedProfiles.isEmpty()) {
|
||||||
|
LogUtil.w(AppConfig.TAG, "Routing outbound '$tag' resolved to empty list, skipping")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
resolvedOutbounds.add(resolvedOutbound)
|
||||||
|
LogUtil.d(AppConfig.TAG, "Resolved routing outbound: tag='$tag', type='${resolvedOutbound.resolvedType}', profiles=${resolvedOutbound.resolvedProfiles.size}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to resolve routing outbound for tag '$tag', skipping", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to resolve routing outbounds from rulesets", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedOutbounds
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolvePolicyGroupProfiles(config: ProfileItem): List<ProfileItem> {
|
||||||
|
try {
|
||||||
|
val serverList = MmkvManager.decodeAllServerList()
|
||||||
|
return serverList
|
||||||
|
.asSequence()
|
||||||
|
.mapNotNull { id -> MmkvManager.decodeServerConfig(id) }
|
||||||
|
.filter { profile ->
|
||||||
|
val subscriptionId = config.policyGroupSubscriptionId
|
||||||
|
if (subscriptionId.isNullOrBlank()) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
profile.subscriptionId == subscriptionId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter { profile ->
|
||||||
|
val filter = config.policyGroupFilter
|
||||||
|
if (filter.isNullOrBlank()) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Regex(filter).containsMatchIn(profile.remarks)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
profile.remarks.contains(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter { it.server.isNotNullEmpty() }
|
||||||
|
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
||||||
|
.filter { !it.configType.isComplexType() }
|
||||||
|
.toList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to resolve policy group profiles for '${config.remarks}'", e)
|
||||||
|
return listOf(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveProxyChainProfiles(config: ProfileItem): List<ProfileItem> {
|
||||||
|
if (config.proxyChainProfiles.isNullOrBlank()) {
|
||||||
|
return listOf(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return config.proxyChainProfiles.orEmpty().split(",")
|
||||||
|
.asSequence()
|
||||||
|
.mapNotNull { remark -> SettingsManager.getServerViaRemarks(remark) }
|
||||||
|
.filter { it.server.isNotNullEmpty() }
|
||||||
|
.filter { !Utils.isPureIpAddress(it.server!!) || Utils.isValidUrl(it.server!!) }
|
||||||
|
.filter { !it.configType.isComplexType() }
|
||||||
|
.toList()
|
||||||
|
.reversed()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain profiles for '${config.remarks}'", e)
|
||||||
|
return listOf(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve chain nodes from subscription neighbors in order: next, current, prev.
|
||||||
|
*
|
||||||
|
* When no chain is available, return a single-node result.
|
||||||
|
*/
|
||||||
|
private fun resolveProxyChainProfilesFromGroup(config: ProfileItem): List<ProfileItem> {
|
||||||
|
if (config.subscriptionId.isEmpty()) {
|
||||||
|
return listOf(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val subItem = MmkvManager.decodeSubscription(config.subscriptionId) ?: return listOf(config)
|
||||||
|
val resolved = mutableListOf<ProfileItem>()
|
||||||
|
SettingsManager.getServerViaRemarks(subItem.nextProfile)?.let { resolved.add(it) }
|
||||||
|
resolved.add(config)
|
||||||
|
SettingsManager.getServerViaRemarks(subItem.prevProfile)?.let { resolved.add(it) }
|
||||||
|
return resolved
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to resolve proxy chain from group for '${config.remarks}'", e)
|
||||||
|
return listOf(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,997 @@
|
|||||||
|
package com.v2ray.ang.core
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.TextUtils
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.ConfigResult
|
||||||
|
import com.v2ray.ang.dto.CoreConfigContext
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig
|
||||||
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
|
import com.v2ray.ang.dto.entities.RulesetItem
|
||||||
|
import com.v2ray.ang.enums.BalancerStrategyType
|
||||||
|
import com.v2ray.ang.enums.CoreResolvedType
|
||||||
|
import com.v2ray.ang.enums.EConfigType
|
||||||
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
import com.v2ray.ang.util.PackageUidResolver
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
object CoreConfigManager {
|
||||||
|
private var initConfigCache: String? = null
|
||||||
|
private var initConfigCacheWithTun: String? = null
|
||||||
|
|
||||||
|
//region get config function
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the runtime configuration for normal startup.
|
||||||
|
*/
|
||||||
|
fun getV2rayConfig(context: Context, guid: String): ConfigResult {
|
||||||
|
try {
|
||||||
|
val configContext = CoreConfigContextBuilder.build(context, guid)
|
||||||
|
?: return ConfigResult(status = false, guid = guid, errorMessage = "Failed to build config context")
|
||||||
|
if (configContext.isCustom) {
|
||||||
|
return buildV2rayCustomConfig(configContext)
|
||||||
|
}
|
||||||
|
return toConfigResult(configContext, buildUnifiedConfig(configContext))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to get V2ray config", e)
|
||||||
|
return ConfigResult(
|
||||||
|
status = false,
|
||||||
|
guid = guid,
|
||||||
|
errorMessage = "Failed to get V2ray config: ${e.message ?: e.javaClass.simpleName}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a lightweight configuration for latency testing.
|
||||||
|
*
|
||||||
|
* The core flow is reused, then non-essential sections are removed.
|
||||||
|
*/
|
||||||
|
fun getV2rayConfig4Speedtest(context: Context, guid: String): ConfigResult {
|
||||||
|
try {
|
||||||
|
val configContext = CoreConfigContextBuilder.build(context, guid)
|
||||||
|
?: return ConfigResult(status = false, guid = guid, errorMessage = "Failed to build config context")
|
||||||
|
if (configContext.isCustom) {
|
||||||
|
return buildV2rayCustomConfig(configContext)
|
||||||
|
}
|
||||||
|
val v2rayConfig = buildUnifiedConfig(configContext)
|
||||||
|
postProcessForSpeedtest(v2rayConfig)
|
||||||
|
|
||||||
|
return toConfigResult(configContext, v2rayConfig)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to get V2ray config for speedtest", e)
|
||||||
|
return ConfigResult(
|
||||||
|
status = false,
|
||||||
|
guid = guid,
|
||||||
|
errorMessage = "Failed to get V2ray config for speedtest: ${e.message ?: e.javaClass.simpleName}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build configuration for custom profiles.
|
||||||
|
*/
|
||||||
|
private fun buildV2rayCustomConfig(configContext: CoreConfigContext): ConfigResult {
|
||||||
|
val context = configContext.context
|
||||||
|
val raw = MmkvManager.decodeServerRaw(configContext.guid)
|
||||||
|
?: return ConfigResult(status = false, guid = configContext.guid, errorMessage = "Custom config is empty")
|
||||||
|
val result = ConfigResult(true, configContext.guid, raw)
|
||||||
|
if (!needTun()) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = JsonUtil.parseString(raw)?.takeIf { it.isJsonObject }?.asJsonObject ?: return result
|
||||||
|
|
||||||
|
// Check whether package names need to be replaced with UIDs
|
||||||
|
if (SettingsManager.canUseProcessRouting()) {
|
||||||
|
val rulesJson = json.get("routing")?.takeIf { it.isJsonObject }?.asJsonObject
|
||||||
|
?.get("rules")?.takeIf { it.isJsonArray }?.asJsonArray
|
||||||
|
?: JsonArray()
|
||||||
|
|
||||||
|
for (elem in rulesJson) {
|
||||||
|
val rule = elem.takeIf { it.isJsonObject }?.asJsonObject ?: continue
|
||||||
|
val process = rule.get("process")?.takeIf { it.isJsonArray }?.asJsonArray ?: continue
|
||||||
|
val packages = process.mapNotNull {
|
||||||
|
it.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString }?.asString
|
||||||
|
}.takeIf { it.isNotEmpty() } ?: continue
|
||||||
|
val uids = PackageUidResolver.packageNamesToUids(context, packages).takeIf { it.isNotEmpty() } ?: continue
|
||||||
|
|
||||||
|
rule.add("process", JsonArray().apply { uids.forEach { add(it) } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if tun inbound exists
|
||||||
|
val inboundsJson = json.get("inbounds")?.takeIf { it.isJsonArray }?.asJsonArray
|
||||||
|
?: JsonArray().also { json.add("inbounds", it) }
|
||||||
|
val tunNotExists = inboundsJson.none { elem ->
|
||||||
|
elem.isJsonObject && elem.asJsonObject.get("protocol")
|
||||||
|
?.takeIf { it.isJsonPrimitive && it.asJsonPrimitive.isString }
|
||||||
|
?.asString == "tun"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tunNotExists) {
|
||||||
|
// add tun inbound from template
|
||||||
|
val templateConfig = initV2rayConfig(configContext)
|
||||||
|
templateConfig.inbounds.firstOrNull { it.tag == "tun" }?.let { inboundTun ->
|
||||||
|
inboundTun.settings?.mtu = SettingsManager.getVpnMtu()
|
||||||
|
inboundsJson.add(JsonUtil.parseString(JsonUtil.toJson(inboundTun)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonUtil.toJsonPretty(json)?.let { ConfigResult(true, configContext.guid, it) } ?: result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build one unified configuration for every non-custom profile type.
|
||||||
|
*
|
||||||
|
* The analyzed outbound plan is consumed in order and converted to concrete
|
||||||
|
* outbounds before routing, DNS, and runtime extras are assembled.
|
||||||
|
*/
|
||||||
|
private fun buildUnifiedConfig(configContext: CoreConfigContext): V2rayConfig {
|
||||||
|
require(configContext.resolvedOutbounds.isNotEmpty()) { "resolvedOutbounds must not be empty for a non-CUSTOM context" }
|
||||||
|
val primaryResolvedOutbound = configContext.resolvedOutbounds.first()
|
||||||
|
|
||||||
|
val v2rayConfig = initV2rayConfig(configContext)
|
||||||
|
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
||||||
|
v2rayConfig.remarks = primaryResolvedOutbound.profile.remarks
|
||||||
|
|
||||||
|
configureInbounds(v2rayConfig)
|
||||||
|
|
||||||
|
if (v2rayConfig.outbounds.isNotEmpty()) {
|
||||||
|
v2rayConfig.outbounds.removeAt(0)
|
||||||
|
}
|
||||||
|
val existingTags = v2rayConfig.outbounds.mapTo(mutableSetOf()) { it.tag }
|
||||||
|
val policyGroupBalancerTags = mutableMapOf<String, String>()
|
||||||
|
val balancerStrategies = mutableListOf<BalancerStrategy>()
|
||||||
|
|
||||||
|
// resolvedOutbounds is a single ordered plan: index 0 is primary and must be prepended,
|
||||||
|
// the rest are routing outbounds and can be appended.
|
||||||
|
configContext.resolvedOutbounds.forEachIndexed { index, spec ->
|
||||||
|
buildOutbounds(
|
||||||
|
resolvedOutbound = spec,
|
||||||
|
prepend = index == 0,
|
||||||
|
existingTags = existingTags,
|
||||||
|
v2rayConfig = v2rayConfig,
|
||||||
|
policyGroupBalancerTags = policyGroupBalancerTags,
|
||||||
|
balancerStrategies = balancerStrategies,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User routing rules (policyGroupBalancerTags rewrites TAG_PROXY→balancer when main is POLICYGROUP).
|
||||||
|
configureRouting(configContext, v2rayConfig, policyGroupBalancerTags)
|
||||||
|
configureFakeDns(v2rayConfig)
|
||||||
|
configureDns(v2rayConfig, policyGroupBalancerTags)
|
||||||
|
configureLocalDns(v2rayConfig)
|
||||||
|
|
||||||
|
// (added by getDns / getCustomLocalDns) to use the balancer, then add
|
||||||
|
// the catch-all balancer rule.
|
||||||
|
if (primaryResolvedOutbound.resolvedType == CoreResolvedType.POLICYGROUP) {
|
||||||
|
if (v2rayConfig.routing.domainStrategy == "IPIfNonMatch") {
|
||||||
|
v2rayConfig.routing.rules.add(
|
||||||
|
V2rayConfig.RoutingBean.RulesBean(
|
||||||
|
ip = arrayListOf("0.0.0.0/0", "::/0"),
|
||||||
|
balancerTag = AppConfig.TAG_BALANCER,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
v2rayConfig.routing.rules.add(
|
||||||
|
V2rayConfig.RoutingBean.RulesBean(
|
||||||
|
network = "tcp,udp",
|
||||||
|
balancerTag = AppConfig.TAG_BALANCER,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyObservability(v2rayConfig, balancerStrategies)
|
||||||
|
applySpeedDisabled(v2rayConfig)
|
||||||
|
resolveOutboundDomainsToHosts(v2rayConfig)
|
||||||
|
|
||||||
|
return v2rayConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert one analyzed outbound entry into concrete outbounds and register
|
||||||
|
* them to the runtime configuration.
|
||||||
|
*/
|
||||||
|
private fun buildOutbounds(
|
||||||
|
resolvedOutbound: CoreConfigContext.ResolvedOutbound,
|
||||||
|
prepend: Boolean,
|
||||||
|
existingTags: MutableSet<String>,
|
||||||
|
v2rayConfig: V2rayConfig,
|
||||||
|
policyGroupBalancerTags: MutableMap<String, String>,
|
||||||
|
balancerStrategies: MutableList<BalancerStrategy>,
|
||||||
|
) {
|
||||||
|
if (resolvedOutbound.tag in existingTags) {
|
||||||
|
LogUtil.w(AppConfig.TAG, "Resolved outbound tag '${resolvedOutbound.tag}' already exists, skipping duplicated entry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (resolvedOutbound.resolvedType) {
|
||||||
|
CoreResolvedType.NORMAL -> handleNormalResolvedOutbound(
|
||||||
|
resolvedOutbound = resolvedOutbound,
|
||||||
|
prepend = prepend,
|
||||||
|
existingTags = existingTags,
|
||||||
|
v2rayConfig = v2rayConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
CoreResolvedType.PROXYCHAIN -> handleProxyChainResolvedOutbound(
|
||||||
|
resolvedOutbound = resolvedOutbound,
|
||||||
|
prepend = prepend,
|
||||||
|
existingTags = existingTags,
|
||||||
|
v2rayConfig = v2rayConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
CoreResolvedType.POLICYGROUP -> handlePolicyGroupResolvedOutbound(
|
||||||
|
resolvedOutbound = resolvedOutbound,
|
||||||
|
prepend = prepend,
|
||||||
|
existingTags = existingTags,
|
||||||
|
v2rayConfig = v2rayConfig,
|
||||||
|
policyGroupBalancerTags = policyGroupBalancerTags,
|
||||||
|
balancerStrategies = balancerStrategies,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and insert a single-node outbound entry.
|
||||||
|
*/
|
||||||
|
private fun handleNormalResolvedOutbound(
|
||||||
|
resolvedOutbound: CoreConfigContext.ResolvedOutbound,
|
||||||
|
prepend: Boolean,
|
||||||
|
existingTags: MutableSet<String>,
|
||||||
|
v2rayConfig: V2rayConfig,
|
||||||
|
) {
|
||||||
|
val profile = resolvedOutbound.resolvedProfiles.firstOrNull() ?: run {
|
||||||
|
LogUtil.w(AppConfig.TAG, "NORMAL resolved outbound '${resolvedOutbound.tag}' has empty resolvedProfiles, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val outbound = convertProfile2Outbound(profile) ?: run {
|
||||||
|
LogUtil.w(AppConfig.TAG, "Could not convert NORMAL resolved outbound '${resolvedOutbound.tag}' profile to outbound, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
outbound.tag = resolvedOutbound.tag
|
||||||
|
if (prepend) {
|
||||||
|
v2rayConfig.outbounds.add(0, outbound)
|
||||||
|
} else {
|
||||||
|
v2rayConfig.outbounds.add(outbound)
|
||||||
|
}
|
||||||
|
existingTags.add(resolvedOutbound.tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and insert a multi-hop chain entry.
|
||||||
|
*/
|
||||||
|
private fun handleProxyChainResolvedOutbound(
|
||||||
|
resolvedOutbound: CoreConfigContext.ResolvedOutbound,
|
||||||
|
prepend: Boolean,
|
||||||
|
existingTags: MutableSet<String>,
|
||||||
|
v2rayConfig: V2rayConfig,
|
||||||
|
) {
|
||||||
|
val chainOutbounds = resolvedOutbound.resolvedProfiles
|
||||||
|
.mapNotNull { convertProfile2Outbound(it) }
|
||||||
|
.toMutableList()
|
||||||
|
if (chainOutbounds.isEmpty()) {
|
||||||
|
LogUtil.w(AppConfig.TAG, "PROXYCHAIN resolved outbound '${resolvedOutbound.tag}' has no valid profiles, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (chainOutbounds.size == 1) {
|
||||||
|
val outbound = chainOutbounds.first()
|
||||||
|
outbound.tag = resolvedOutbound.tag
|
||||||
|
if (prepend) {
|
||||||
|
v2rayConfig.outbounds.add(0, outbound)
|
||||||
|
} else {
|
||||||
|
v2rayConfig.outbounds.add(outbound)
|
||||||
|
}
|
||||||
|
existingTags.add(resolvedOutbound.tag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val chainTags = chainOutbounds.mapIndexed { index, _ ->
|
||||||
|
if (index == 0) {
|
||||||
|
resolvedOutbound.tag
|
||||||
|
} else {
|
||||||
|
"${AppConfig.TAG_PROXY}-${resolvedOutbound.tag}-$index"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chainTags.any { it in existingTags }) {
|
||||||
|
LogUtil.w(
|
||||||
|
AppConfig.TAG,
|
||||||
|
"PROXYCHAIN resolved outbound '${resolvedOutbound.tag}' has colliding hop tags, skipping"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chainOutbounds.forEachIndexed { index, outbound ->
|
||||||
|
outbound.tag = chainTags[index]
|
||||||
|
}
|
||||||
|
for (i in 0 until chainOutbounds.size - 1) {
|
||||||
|
chainOutbounds[i].ensureSockopt().dialerProxy = chainOutbounds[i + 1].tag
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prepend) {
|
||||||
|
v2rayConfig.outbounds.addAll(0, chainOutbounds)
|
||||||
|
} else {
|
||||||
|
v2rayConfig.outbounds.addAll(chainOutbounds)
|
||||||
|
}
|
||||||
|
chainOutbounds.forEach { existingTags.add(it.tag) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and insert a policy-group entry and its balancer metadata.
|
||||||
|
*/
|
||||||
|
private fun handlePolicyGroupResolvedOutbound(
|
||||||
|
resolvedOutbound: CoreConfigContext.ResolvedOutbound,
|
||||||
|
prepend: Boolean,
|
||||||
|
existingTags: MutableSet<String>,
|
||||||
|
v2rayConfig: V2rayConfig,
|
||||||
|
policyGroupBalancerTags: MutableMap<String, String>,
|
||||||
|
balancerStrategies: MutableList<BalancerStrategy>,
|
||||||
|
) {
|
||||||
|
val memberPairs = resolvedOutbound.resolvedProfiles.mapNotNull { profile ->
|
||||||
|
convertProfile2Outbound(profile)?.let { ob -> ob to profile }
|
||||||
|
}
|
||||||
|
if (memberPairs.isEmpty()) {
|
||||||
|
LogUtil.w(AppConfig.TAG, "POLICYGROUP resolved outbound '${resolvedOutbound.tag}' has no valid member outbounds, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val memberTagPrefix = "${AppConfig.TAG_PROXY}-${resolvedOutbound.tag}-"
|
||||||
|
val membersToAdd = mutableListOf<V2rayConfig.OutboundBean>()
|
||||||
|
memberPairs.forEachIndexed { index, (outbound, profile) ->
|
||||||
|
val memberTag = "$memberTagPrefix${index + 1}-${profile.remarks.trim()}"
|
||||||
|
if (memberTag in existingTags) {
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
outbound.tag = memberTag
|
||||||
|
membersToAdd.add(outbound)
|
||||||
|
existingTags.add(memberTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membersToAdd.isEmpty()) {
|
||||||
|
LogUtil.w(
|
||||||
|
AppConfig.TAG,
|
||||||
|
"POLICYGROUP resolved outbound '${resolvedOutbound.tag}' produced no unique member tags, skipping"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prepend) {
|
||||||
|
v2rayConfig.outbounds.addAll(0, membersToAdd)
|
||||||
|
} else {
|
||||||
|
v2rayConfig.outbounds.addAll(membersToAdd)
|
||||||
|
}
|
||||||
|
|
||||||
|
val balancerTag = if (resolvedOutbound.tag == AppConfig.TAG_PROXY) {
|
||||||
|
AppConfig.TAG_BALANCER
|
||||||
|
} else {
|
||||||
|
"${AppConfig.TAG_BALANCER_PRE}-${resolvedOutbound.tag}"
|
||||||
|
}
|
||||||
|
val strategy = buildBalancerStrategy(
|
||||||
|
policyGroupType = resolvedOutbound.profile.policyGroupType,
|
||||||
|
selector = listOf(memberTagPrefix),
|
||||||
|
balancerTag = balancerTag,
|
||||||
|
)
|
||||||
|
val existingBalancers = v2rayConfig.routing.balancers?.toMutableList() ?: mutableListOf()
|
||||||
|
if (existingBalancers.none { it.tag == balancerTag }) {
|
||||||
|
existingBalancers.add(strategy.balancer)
|
||||||
|
v2rayConfig.routing.balancers = existingBalancers
|
||||||
|
}
|
||||||
|
balancerStrategies.add(strategy)
|
||||||
|
policyGroupBalancerTags[resolvedOutbound.tag] = balancerTag
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trim runtime sections that are not needed for latency testing.
|
||||||
|
*/
|
||||||
|
private fun postProcessForSpeedtest(v2rayConfig: V2rayConfig) {
|
||||||
|
v2rayConfig.log.loglevel = MmkvManager.decodeSettingsString(AppConfig.PREF_LOGLEVEL) ?: "warning"
|
||||||
|
v2rayConfig.inbounds.clear()
|
||||||
|
v2rayConfig.routing.rules.clear()
|
||||||
|
v2rayConfig.dns = null
|
||||||
|
v2rayConfig.fakedns = null
|
||||||
|
v2rayConfig.stats = null
|
||||||
|
v2rayConfig.policy = null
|
||||||
|
v2rayConfig.outbounds.forEach { key -> key.mux = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize a runtime configuration into a standard result object.
|
||||||
|
*/
|
||||||
|
private fun toConfigResult(configContext: CoreConfigContext, v2rayConfig: V2rayConfig): ConfigResult {
|
||||||
|
return ConfigResult(
|
||||||
|
status = true,
|
||||||
|
guid = configContext.guid,
|
||||||
|
content = JsonUtil.toJsonPretty(v2rayConfig) ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the base template from cache or assets and parse it.
|
||||||
|
*/
|
||||||
|
private fun initV2rayConfig(configContext: CoreConfigContext): V2rayConfig {
|
||||||
|
val context = configContext.context
|
||||||
|
val assets: String
|
||||||
|
if (needTun()) {
|
||||||
|
assets = initConfigCacheWithTun ?: Utils.readTextFromAssets(context, "v2ray_config_with_tun.json")
|
||||||
|
if (TextUtils.isEmpty(assets)) {
|
||||||
|
error("Missing asset: v2ray_config_with_tun.json")
|
||||||
|
}
|
||||||
|
initConfigCacheWithTun = assets
|
||||||
|
} else {
|
||||||
|
assets = initConfigCache ?: Utils.readTextFromAssets(context, "v2ray_config.json")
|
||||||
|
if (TextUtils.isEmpty(assets)) {
|
||||||
|
error("Missing asset: v2ray_config.json")
|
||||||
|
}
|
||||||
|
initConfigCache = assets
|
||||||
|
}
|
||||||
|
return JsonUtil.fromJson(assets, V2rayConfig::class.java)
|
||||||
|
?: error("Failed to parse config template")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region some sub function
|
||||||
|
|
||||||
|
private fun needTun(): Boolean {
|
||||||
|
return SettingsManager.isVpnMode() && !SettingsManager.isUsingHevTun()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure inbound listeners and related runtime options.
|
||||||
|
*/
|
||||||
|
private fun configureInbounds(v2rayConfig: V2rayConfig) {
|
||||||
|
val vpn = SettingsManager.isVpnMode()
|
||||||
|
val useHev = SettingsManager.isUsingHevTun()
|
||||||
|
val forcedByHev = vpn && useHev
|
||||||
|
|
||||||
|
val enableLocalProxy = forcedByHev || MmkvManager.decodeSettingsBool(AppConfig.PREF_ENABLE_LOCAL_PROXY, true)
|
||||||
|
|
||||||
|
val socksPort = SettingsManager.getSocksPort()
|
||||||
|
val socksUsername = SettingsManager.getSocksUsername()
|
||||||
|
val socksPassword = SettingsManager.getSocksPassword()
|
||||||
|
val inbound1 = v2rayConfig.inbounds[0]
|
||||||
|
if (inbound1.settings == null) {
|
||||||
|
inbound1.settings = V2rayConfig.InboundBean.InSettingsBean()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) != true) {
|
||||||
|
inbound1.listen = AppConfig.LOOPBACK
|
||||||
|
}
|
||||||
|
inbound1.port = socksPort
|
||||||
|
inbound1.settings?.udp = MmkvManager.decodeSettingsBool(AppConfig.PREF_SOCKS_ENABLE_UDP, true)
|
||||||
|
if (socksUsername != null && socksPassword != null) {
|
||||||
|
inbound1.settings?.auth = "password"
|
||||||
|
inbound1.settings?.accounts = listOf(
|
||||||
|
V2rayConfig.InboundBean.InSettingsBean.SocksAccountBean(
|
||||||
|
user = socksUsername,
|
||||||
|
pass = socksPassword
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
inbound1.settings?.auth = "noauth"
|
||||||
|
inbound1.settings?.accounts = null
|
||||||
|
}
|
||||||
|
val fakedns = MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
||||||
|
val sniffAllTlsAndHttp =
|
||||||
|
MmkvManager.decodeSettingsBool(AppConfig.PREF_SNIFFING_ENABLED, true) != false
|
||||||
|
inbound1.sniffing?.enabled = fakedns || sniffAllTlsAndHttp
|
||||||
|
inbound1.sniffing?.routeOnly =
|
||||||
|
MmkvManager.decodeSettingsBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
|
||||||
|
if (!sniffAllTlsAndHttp) {
|
||||||
|
inbound1.sniffing?.destOverride?.clear()
|
||||||
|
}
|
||||||
|
if (fakedns) {
|
||||||
|
inbound1.sniffing?.destOverride?.add("fakedns")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Utils.isXray()) {
|
||||||
|
val inbound2 = JsonUtil.fromJson(JsonUtil.toJson(inbound1), V2rayConfig.InboundBean::class.java)
|
||||||
|
?: error("Failed to clone inbound template")
|
||||||
|
inbound2.tag = EConfigType.HTTP.name.lowercase()
|
||||||
|
inbound2.port = SettingsManager.getHttpPort()
|
||||||
|
inbound2.protocol = EConfigType.HTTP.name.lowercase()
|
||||||
|
inbound2.settings?.auth = null
|
||||||
|
inbound2.settings?.udp = null
|
||||||
|
v2rayConfig.inbounds.add(inbound2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enableLocalProxy) {
|
||||||
|
v2rayConfig.inbounds.removeIf { it.protocol == "socks" || it.protocol == "http" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needTun()) {
|
||||||
|
val inboundTun = v2rayConfig.inbounds.firstOrNull { e -> e.tag == "tun" }
|
||||||
|
inboundTun?.settings?.mtu = SettingsManager.getVpnMtu()
|
||||||
|
inboundTun?.sniffing = inbound1.sniffing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable fake DNS when local DNS and fake DNS are both enabled.
|
||||||
|
*/
|
||||||
|
private fun configureFakeDns(v2rayConfig: V2rayConfig) {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) == true
|
||||||
|
&& MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
|
||||||
|
) {
|
||||||
|
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect domain rules that target one outbound tag.
|
||||||
|
*/
|
||||||
|
private fun collectUserRuleDomainsByTag(tag: String): ArrayList<String> {
|
||||||
|
val domain = ArrayList<String>()
|
||||||
|
|
||||||
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
|
rulesetItems?.forEach { key ->
|
||||||
|
if (key.enabled && key.outboundTag == tag && !key.domain.isNullOrEmpty()) {
|
||||||
|
key.domain?.forEach {
|
||||||
|
domain.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect domain rules that target non-builtin outbound tags.
|
||||||
|
*/
|
||||||
|
private fun collectCustomOutboundDomains(): ArrayList<String> {
|
||||||
|
val domain = ArrayList<String>()
|
||||||
|
|
||||||
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
|
rulesetItems?.forEach { key ->
|
||||||
|
if (key.enabled && !AppConfig.BUILTIN_OUTBOUND_TAGS.contains(key.outboundTag)
|
||||||
|
&& !key.domain.isNullOrEmpty()
|
||||||
|
) {
|
||||||
|
key.domain?.forEach {
|
||||||
|
domain.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure local DNS inbounds, outbounds, and routing rules.
|
||||||
|
*/
|
||||||
|
private fun configureLocalDns(v2rayConfig: V2rayConfig) {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_LOCAL_DNS_ENABLED) != true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true) {
|
||||||
|
val geositeCn = arrayListOf(AppConfig.GEOSITE_CN)
|
||||||
|
val proxyDomain = collectUserRuleDomainsByTag(AppConfig.TAG_PROXY)
|
||||||
|
val directDomain = collectUserRuleDomainsByTag(AppConfig.TAG_DIRECT)
|
||||||
|
val finalDomain = geositeCn.plus(proxyDomain).plus(directDomain).distinct()
|
||||||
|
// fakedns with all domains to make it always top priority
|
||||||
|
v2rayConfig.dns?.servers?.add(
|
||||||
|
0,
|
||||||
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
|
address = "fakedns",
|
||||||
|
domains = finalDomain
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsManager.isVpnMode()) {
|
||||||
|
if (SettingsManager.isUsingHevTun()) {
|
||||||
|
//hev-socks5-tunnel dns routing
|
||||||
|
v2rayConfig.routing.rules.add(
|
||||||
|
0, V2rayConfig.RoutingBean.RulesBean(
|
||||||
|
inboundTag = arrayListOf("socks"),
|
||||||
|
outboundTag = "dns-out",
|
||||||
|
port = "53",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
v2rayConfig.routing.rules.add(
|
||||||
|
0, V2rayConfig.RoutingBean.RulesBean(
|
||||||
|
inboundTag = arrayListOf("tun"),
|
||||||
|
outboundTag = "dns-out",
|
||||||
|
port = "53",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS outbound
|
||||||
|
if (v2rayConfig.outbounds.none { e -> e.protocol == "dns" && e.tag == "dns-out" }) {
|
||||||
|
v2rayConfig.outbounds.add(
|
||||||
|
V2rayConfig.OutboundBean(
|
||||||
|
protocol = "dns",
|
||||||
|
tag = "dns-out",
|
||||||
|
settings = null,
|
||||||
|
streamSettings = null,
|
||||||
|
mux = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove speed-test runtime sections when the feature is disabled.
|
||||||
|
*/
|
||||||
|
private fun applySpeedDisabled(v2rayConfig: V2rayConfig) {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_SPEED_ENABLED) != true) {
|
||||||
|
v2rayConfig.stats = null
|
||||||
|
v2rayConfig.policy = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure DNS servers, hosts, and DNS routing rules.
|
||||||
|
*/
|
||||||
|
private fun configureDns(
|
||||||
|
v2rayConfig: V2rayConfig,
|
||||||
|
policyGroupBalancerTags: Map<String, String>,
|
||||||
|
) {
|
||||||
|
val hosts = mutableMapOf<String, Any>()
|
||||||
|
val servers = ArrayList<Any>()
|
||||||
|
|
||||||
|
//remote Dns
|
||||||
|
val remoteDns = SettingsManager.getRemoteDnsServers()
|
||||||
|
val proxyDomain = (collectUserRuleDomainsByTag(AppConfig.TAG_PROXY) + collectCustomOutboundDomains()).distinct()
|
||||||
|
remoteDns.forEach {
|
||||||
|
servers.add(it)
|
||||||
|
}
|
||||||
|
if (proxyDomain.isNotEmpty()) {
|
||||||
|
servers.add(
|
||||||
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
|
address = remoteDns.first(),
|
||||||
|
domains = proxyDomain,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// domestic DNS
|
||||||
|
val domesticDns = SettingsManager.getDomesticDnsServers()
|
||||||
|
val directDomain = collectUserRuleDomainsByTag(AppConfig.TAG_DIRECT)
|
||||||
|
val isCnRoutingMode = directDomain.contains(AppConfig.GEOSITE_CN)
|
||||||
|
val cnRegionFilter = { domain: String ->
|
||||||
|
domain.startsWith("geosite:") && (domain.endsWith("-cn") || domain.endsWith("@cn"))
|
||||||
|
|| domain == AppConfig.GEOSITE_CN
|
||||||
|
}
|
||||||
|
val finalDirectDomain = if (isCnRoutingMode) directDomain.filterNot {
|
||||||
|
cnRegionFilter(it)
|
||||||
|
} else directDomain
|
||||||
|
val domesticDnsTags = mutableListOf<String>()
|
||||||
|
domesticDns.forEachIndexed { index, element ->
|
||||||
|
val tag = AppConfig.TAG_DOMESTIC_DNS + index
|
||||||
|
servers.add(
|
||||||
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
|
address = element,
|
||||||
|
domains = finalDirectDomain,
|
||||||
|
skipFallback = true,
|
||||||
|
tag = tag
|
||||||
|
)
|
||||||
|
)
|
||||||
|
domesticDnsTags.add(tag)
|
||||||
|
}
|
||||||
|
if (isCnRoutingMode) {
|
||||||
|
val geoipCn = arrayListOf(AppConfig.GEOIP_CN)
|
||||||
|
val cnRegionDomain = directDomain.filter { cnRegionFilter(it) }
|
||||||
|
domesticDns.forEachIndexed { index, element ->
|
||||||
|
val geositeCnDnsTag = AppConfig.TAG_DOMESTIC_DNS + index + "_cn_expect"
|
||||||
|
servers.add(
|
||||||
|
V2rayConfig.DnsBean.ServersBean(
|
||||||
|
address = element,
|
||||||
|
domains = cnRegionDomain,
|
||||||
|
expectIPs = geoipCn,
|
||||||
|
skipFallback = true,
|
||||||
|
tag = geositeCnDnsTag
|
||||||
|
)
|
||||||
|
)
|
||||||
|
domesticDnsTags.add(geositeCnDnsTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//block dns
|
||||||
|
val blkDomain = collectUserRuleDomainsByTag(AppConfig.TAG_BLOCKED)
|
||||||
|
if (blkDomain.isNotEmpty()) {
|
||||||
|
hosts.putAll(blkDomain.map { it to AppConfig.LOOPBACK })
|
||||||
|
}
|
||||||
|
|
||||||
|
// hardcode googleapi rule to fix play store problems
|
||||||
|
hosts[AppConfig.GOOGLEAPIS_CN_DOMAIN] = AppConfig.GOOGLEAPIS_COM_DOMAIN
|
||||||
|
|
||||||
|
// hardcode popular Android Private DNS rule to fix localhost DNS problem
|
||||||
|
hosts[AppConfig.DNS_ALIDNS_DOMAIN] = AppConfig.DNS_ALIDNS_ADDRESSES
|
||||||
|
hosts[AppConfig.DNS_CLOUDFLARE_ONE_DOMAIN] = AppConfig.DNS_CLOUDFLARE_ONE_ADDRESSES
|
||||||
|
hosts[AppConfig.DNS_CLOUDFLARE_DNS_COM_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_COM_ADDRESSES
|
||||||
|
hosts[AppConfig.DNS_CLOUDFLARE_DNS_DOMAIN] = AppConfig.DNS_CLOUDFLARE_DNS_ADDRESSES
|
||||||
|
hosts[AppConfig.DNS_DNSPOD_DOMAIN] = AppConfig.DNS_DNSPOD_ADDRESSES
|
||||||
|
hosts[AppConfig.DNS_GOOGLE_DOMAIN] = AppConfig.DNS_GOOGLE_ADDRESSES
|
||||||
|
hosts[AppConfig.DNS_QUAD9_DOMAIN] = AppConfig.DNS_QUAD9_ADDRESSES
|
||||||
|
hosts[AppConfig.DNS_YANDEX_DOMAIN] = AppConfig.DNS_YANDEX_ADDRESSES
|
||||||
|
|
||||||
|
//User DNS hosts
|
||||||
|
val userHosts = MmkvManager.decodeSettingsString(AppConfig.PREF_DNS_HOSTS)
|
||||||
|
if (userHosts.isNotNullEmpty()) {
|
||||||
|
val userHostsMap = userHosts?.split(",")
|
||||||
|
?.filter { it.isNotEmpty() }
|
||||||
|
?.filter { it.contains(":") }
|
||||||
|
?.associate { it.split(":").let { (k, v) -> k to v } }
|
||||||
|
if (userHostsMap != null) {
|
||||||
|
hosts.putAll(userHostsMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS dns
|
||||||
|
v2rayConfig.dns = V2rayConfig.DnsBean(
|
||||||
|
servers = servers,
|
||||||
|
hosts = hosts,
|
||||||
|
tag = AppConfig.TAG_DNS,
|
||||||
|
enableParallelQuery = if ((domesticDns.size + remoteDns.size) > 2) true else null
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNS routing
|
||||||
|
v2rayConfig.routing.rules.add(
|
||||||
|
V2rayConfig.RoutingBean.RulesBean(
|
||||||
|
outboundTag = AppConfig.TAG_DIRECT,
|
||||||
|
inboundTag = domesticDnsTags,
|
||||||
|
domain = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val dnsProxyBalancerTag = policyGroupBalancerTags[AppConfig.TAG_PROXY]
|
||||||
|
if (dnsProxyBalancerTag != null) {
|
||||||
|
v2rayConfig.routing.rules.add(
|
||||||
|
V2rayConfig.RoutingBean.RulesBean(
|
||||||
|
balancerTag = dnsProxyBalancerTag,
|
||||||
|
inboundTag = arrayListOf(AppConfig.TAG_DNS),
|
||||||
|
domain = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
v2rayConfig.routing.rules.add(
|
||||||
|
V2rayConfig.RoutingBean.RulesBean(
|
||||||
|
outboundTag = AppConfig.TAG_PROXY,
|
||||||
|
inboundTag = arrayListOf(AppConfig.TAG_DNS),
|
||||||
|
domain = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region outbound related functions
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve outbound domains to IPs and write resolved hosts to DNS map.
|
||||||
|
*/
|
||||||
|
private fun resolveOutboundDomainsToHosts(v2rayConfig: V2rayConfig) {
|
||||||
|
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "1") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val proxyOutboundList = v2rayConfig.getAllProxyOutbound()
|
||||||
|
val dns = v2rayConfig.dns ?: return
|
||||||
|
val newHosts = dns.hosts?.toMutableMap() ?: mutableMapOf()
|
||||||
|
val preferIpv6 = MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6) == true
|
||||||
|
|
||||||
|
for (item in proxyOutboundList) {
|
||||||
|
val domain = item.getServerAddress()
|
||||||
|
if (domain.isNullOrEmpty()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newHosts.containsKey(domain)) {
|
||||||
|
item.ensureSockopt().domainStrategy = "UseIP"
|
||||||
|
item.ensureSockopt().happyEyeballs = V2rayConfig.OutboundBean.StreamSettingsBean.HappyEyeballsBean(
|
||||||
|
prioritizeIPv6 = preferIpv6,
|
||||||
|
interleave = 2
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val resolvedIps = HttpUtil.resolveHostToIP(domain, preferIpv6)
|
||||||
|
if (resolvedIps.isNullOrEmpty()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
item.ensureSockopt().domainStrategy = "UseIP"
|
||||||
|
item.ensureSockopt().happyEyeballs = V2rayConfig.OutboundBean.StreamSettingsBean.HappyEyeballsBean(
|
||||||
|
prioritizeIPv6 = preferIpv6,
|
||||||
|
interleave = 2
|
||||||
|
)
|
||||||
|
newHosts[domain] = if (resolvedIps.size == 1) {
|
||||||
|
resolvedIps[0]
|
||||||
|
} else {
|
||||||
|
resolvedIps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns.hosts = newHosts
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert one profile object into one outbound object.
|
||||||
|
*/
|
||||||
|
private fun convertProfile2Outbound(profileItem: ProfileItem): V2rayConfig.OutboundBean? {
|
||||||
|
return CoreOutboundBuilder.convert(profileItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
|
||||||
|
//region routing related functions
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge probe settings from all balancer strategies into the runtime config.
|
||||||
|
*/
|
||||||
|
private fun applyObservability(v2rayConfig: V2rayConfig, strategies: List<BalancerStrategy>) {
|
||||||
|
val allObsSelectors = strategies
|
||||||
|
.mapNotNull { it.observatory?.subjectSelector }
|
||||||
|
.flatten()
|
||||||
|
.distinct()
|
||||||
|
val obsTemplate = strategies.firstNotNullOfOrNull { it.observatory }
|
||||||
|
if (obsTemplate != null && allObsSelectors.isNotEmpty()) {
|
||||||
|
v2rayConfig.observatory = V2rayConfig.ObservatoryObject(
|
||||||
|
subjectSelector = allObsSelectors,
|
||||||
|
probeUrl = obsTemplate.probeUrl,
|
||||||
|
probeInterval = obsTemplate.probeInterval,
|
||||||
|
enableConcurrency = obsTemplate.enableConcurrency
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val allBurstSelectors = strategies
|
||||||
|
.mapNotNull { it.burstObservatory?.subjectSelector }
|
||||||
|
.flatten()
|
||||||
|
.distinct()
|
||||||
|
val burstTemplate = strategies.firstNotNullOfOrNull { it.burstObservatory }
|
||||||
|
if (burstTemplate != null && allBurstSelectors.isNotEmpty()) {
|
||||||
|
v2rayConfig.burstObservatory = V2rayConfig.BurstObservatoryObject(
|
||||||
|
subjectSelector = allBurstSelectors,
|
||||||
|
pingConfig = burstTemplate.pingConfig
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure routing domain strategy and append enabled user rules.
|
||||||
|
*/
|
||||||
|
private fun configureRouting(
|
||||||
|
configContext: CoreConfigContext,
|
||||||
|
v2rayConfig: V2rayConfig,
|
||||||
|
policyGroupBalancerTags: Map<String, String>
|
||||||
|
) {
|
||||||
|
|
||||||
|
v2rayConfig.routing.domainStrategy =
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
||||||
|
?: "AsIs"
|
||||||
|
|
||||||
|
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||||
|
rulesetItems?.forEach { key ->
|
||||||
|
appendRoutingUserRule(configContext, key, v2rayConfig, policyGroupBalancerTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert one rule item and append it to routing rules.
|
||||||
|
*/
|
||||||
|
private fun appendRoutingUserRule(
|
||||||
|
configContext: CoreConfigContext,
|
||||||
|
item: RulesetItem?,
|
||||||
|
v2rayConfig: V2rayConfig,
|
||||||
|
policyGroupBalancerTags: Map<String, String>
|
||||||
|
) {
|
||||||
|
val context = configContext.context
|
||||||
|
if (item == null || !item.enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), V2rayConfig.RoutingBean.RulesBean::class.java) ?: return
|
||||||
|
|
||||||
|
// Replace specific geoip rules with ext versions
|
||||||
|
rule.ip?.let { ipList ->
|
||||||
|
val updatedIpList = ArrayList<String>()
|
||||||
|
ipList.forEach { ip ->
|
||||||
|
when (ip) {
|
||||||
|
AppConfig.GEOIP_CN -> updatedIpList.add("ext:${AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT}:cn")
|
||||||
|
AppConfig.GEOIP_PRIVATE -> updatedIpList.add("ext:${AppConfig.GEOIP_ONLY_CN_PRIVATE_DAT}:private")
|
||||||
|
else -> updatedIpList.add(ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule.ip = updatedIpList
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsManager.canUseProcessRouting()) {
|
||||||
|
// Convert process package names to UIDs
|
||||||
|
rule.process?.let { processList ->
|
||||||
|
if (processList.isNotEmpty()) {
|
||||||
|
val uids = PackageUidResolver.packageNamesToUids(context, processList)
|
||||||
|
rule.process = uids.ifEmpty { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rule.process = null
|
||||||
|
}
|
||||||
|
|
||||||
|
val outboundTag = rule.outboundTag
|
||||||
|
|
||||||
|
// Route rules targeting a custom policy-group tag should hit its balancer.
|
||||||
|
policyGroupBalancerTags[outboundTag]?.let { balancerTag ->
|
||||||
|
rule.outboundTag = null
|
||||||
|
rule.balancerTag = balancerTag
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the outbound tag is a custom one that failed to inject, fall back to proxy
|
||||||
|
if (!outboundTag.isNullOrBlank()
|
||||||
|
&& outboundTag !in policyGroupBalancerTags
|
||||||
|
&& outboundTag !in AppConfig.BUILTIN_OUTBOUND_TAGS
|
||||||
|
&& v2rayConfig.outbounds.none { it.tag == outboundTag }
|
||||||
|
) {
|
||||||
|
LogUtil.w(AppConfig.TAG, "Outbound tag '$outboundTag' not found, falling back to '${AppConfig.TAG_PROXY}'")
|
||||||
|
rule.outboundTag = AppConfig.TAG_PROXY
|
||||||
|
}
|
||||||
|
|
||||||
|
v2rayConfig.routing.rules.add(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build balancer and probe settings from one policy-group strategy value.
|
||||||
|
*/
|
||||||
|
private fun buildBalancerStrategy(
|
||||||
|
policyGroupType: String?,
|
||||||
|
selector: List<String>,
|
||||||
|
balancerTag: String = AppConfig.TAG_BALANCER,
|
||||||
|
): BalancerStrategy {
|
||||||
|
val probeUrl = MmkvManager.decodeSettingsString(AppConfig.PREF_DELAY_TEST_URL) ?: AppConfig.DELAY_TEST_URL
|
||||||
|
val strategyType = BalancerStrategyType.from(policyGroupType)
|
||||||
|
val balancer = V2rayConfig.RoutingBean.BalancerBean(
|
||||||
|
tag = balancerTag,
|
||||||
|
selector = selector,
|
||||||
|
strategy = V2rayConfig.RoutingBean.StrategyObject(type = strategyType.policyGroupType)
|
||||||
|
)
|
||||||
|
val observatory = if (strategyType.requiresObservatory) {
|
||||||
|
V2rayConfig.ObservatoryObject(
|
||||||
|
subjectSelector = selector,
|
||||||
|
probeUrl = probeUrl,
|
||||||
|
probeInterval = "3m",
|
||||||
|
enableConcurrency = true
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
val burstObservatory = if (strategyType.requiresBurstObservatory) {
|
||||||
|
V2rayConfig.BurstObservatoryObject(
|
||||||
|
subjectSelector = selector,
|
||||||
|
pingConfig = V2rayConfig.BurstObservatoryObject.PingConfigObject(
|
||||||
|
destination = probeUrl,
|
||||||
|
interval = "5m",
|
||||||
|
sampling = 2,
|
||||||
|
timeout = "30s"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
return BalancerStrategy(balancer, observatory, burstObservatory)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carry balancer data plus optional probe settings for later merge.
|
||||||
|
*/
|
||||||
|
private data class BalancerStrategy(
|
||||||
|
val balancer: V2rayConfig.RoutingBean.BalancerBean,
|
||||||
|
val observatory: V2rayConfig.ObservatoryObject? = null,
|
||||||
|
val burstObservatory: V2rayConfig.BurstObservatoryObject? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.v2ray.ang.core
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import go.Seq
|
||||||
|
import libv2ray.CoreCallbackHandler
|
||||||
|
import libv2ray.CoreController
|
||||||
|
import libv2ray.Libv2ray
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2Ray Native Library Manager
|
||||||
|
*
|
||||||
|
* Thread-safe singleton wrapper for Libv2ray native methods.
|
||||||
|
* Provides initialization protection and unified API for V2Ray core operations.
|
||||||
|
*/
|
||||||
|
object CoreNativeManager {
|
||||||
|
private val initialized = AtomicBoolean(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize V2Ray core environment.
|
||||||
|
* This method is thread-safe and ensures initialization happens only once.
|
||||||
|
* Subsequent calls will be ignored silently.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun initCoreEnv(context: Context?) {
|
||||||
|
if (initialized.compareAndSet(false, true)) {
|
||||||
|
try {
|
||||||
|
Seq.setContext(context?.applicationContext)
|
||||||
|
val assetPath = Utils.userAssetPath(context)
|
||||||
|
val deviceId = Utils.getDeviceIdForXUDPBaseKey()
|
||||||
|
Libv2ray.initCoreEnv(assetPath, deviceId)
|
||||||
|
LogUtil.i(AppConfig.TAG, "V2Ray core environment initialized successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to initialize V2Ray core environment", e)
|
||||||
|
initialized.set(false)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LogUtil.d(AppConfig.TAG, "V2Ray core environment already initialized, skipping")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reconcileBrowserDialer(dialerAddr: String) {
|
||||||
|
try {
|
||||||
|
Libv2ray.reconcileBrowserDialer(dialerAddr)
|
||||||
|
LogUtil.i(AppConfig.TAG, "Browser dialer reconciled successfully with address: $dialerAddr")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to reconcile browser dialer with address: $dialerAddr", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get V2Ray core version.
|
||||||
|
*
|
||||||
|
* @return Version string of the V2Ray core
|
||||||
|
*/
|
||||||
|
fun getLibVersion(): String {
|
||||||
|
return try {
|
||||||
|
Libv2ray.checkVersionX()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to check V2Ray version", e)
|
||||||
|
"Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measure outbound connection delay.
|
||||||
|
*
|
||||||
|
* @param config The configuration JSON string
|
||||||
|
* @param testUrl The URL to test against
|
||||||
|
* @return Delay in milliseconds, or -1 if test failed
|
||||||
|
*/
|
||||||
|
fun measureOutboundDelay(config: String, testUrl: String): Long {
|
||||||
|
return try {
|
||||||
|
Libv2ray.measureOutboundDelay(config, testUrl)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to measure outbound delay", e)
|
||||||
|
-1L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new core controller instance.
|
||||||
|
*
|
||||||
|
* @param handler The callback handler for core events
|
||||||
|
* @return A new CoreController instance
|
||||||
|
*/
|
||||||
|
fun newCoreController(handler: CoreCallbackHandler): CoreController {
|
||||||
|
return try {
|
||||||
|
Libv2ray.newCoreController(handler)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to create core controller", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,688 @@
|
|||||||
|
package com.v2ray.ang.core
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.dto.V2rayConfig.OutboundBean
|
||||||
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
|
import com.v2ray.ang.enums.EConfigType
|
||||||
|
import com.v2ray.ang.enums.NetworkType
|
||||||
|
import com.v2ray.ang.extension.isNotNullEmpty
|
||||||
|
import com.v2ray.ang.extension.nullIfBlank
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.util.HttpUtil
|
||||||
|
import com.v2ray.ang.util.JsonUtil
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralizes ProfileItem -> OutboundBean conversion.
|
||||||
|
* Most protocol builders mirror the previous *Fmt.toOutbound behavior.
|
||||||
|
*/
|
||||||
|
object CoreOutboundBuilder {
|
||||||
|
|
||||||
|
/** Dispatches a profile to protocol-specific outbound builder. */
|
||||||
|
fun convert(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outbound = when (profileItem.configType) {
|
||||||
|
EConfigType.VMESS -> toOutboundVmess(profileItem)
|
||||||
|
EConfigType.SHADOWSOCKS -> toOutboundShadowsocks(profileItem)
|
||||||
|
EConfigType.SOCKS -> toOutboundSocks(profileItem)
|
||||||
|
EConfigType.VLESS -> toOutboundVless(profileItem)
|
||||||
|
EConfigType.TROJAN -> toOutboundTrojan(profileItem)
|
||||||
|
EConfigType.WIREGUARD -> toOutboundWireguard(profileItem)
|
||||||
|
EConfigType.HYSTERIA2 -> toOutboundHysteria2(profileItem)
|
||||||
|
EConfigType.HTTP -> toOutboundHttp(profileItem)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
outbound ?: return null
|
||||||
|
val ret = updateOutboundWithGlobalSettings(outbound)
|
||||||
|
if (!ret) return null
|
||||||
|
return outbound
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Applies global outbound options (mux, protocol-specific tweaks, etc.). */
|
||||||
|
private fun updateOutboundWithGlobalSettings(outbound: OutboundBean): Boolean {
|
||||||
|
try {
|
||||||
|
var muxEnabled = MmkvManager.decodeSettingsBool(AppConfig.PREF_MUX_ENABLED, false)
|
||||||
|
val protocol = outbound.protocol
|
||||||
|
if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|
|| protocol.equals(EConfigType.WIREGUARD.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA.name, true)
|
||||||
|
) {
|
||||||
|
muxEnabled = false
|
||||||
|
} else if (outbound.streamSettings?.network == NetworkType.XHTTP.type) {
|
||||||
|
muxEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (muxEnabled) {
|
||||||
|
outbound.mux?.enabled = true
|
||||||
|
outbound.mux?.concurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_CONCURRENCY, "8").orEmpty().toInt()
|
||||||
|
outbound.mux?.xudpConcurrency = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_CONCURRENCY, "16").orEmpty().toInt()
|
||||||
|
outbound.mux?.xudpProxyUDP443 = MmkvManager.decodeSettingsString(AppConfig.PREF_MUX_XUDP_QUIC, "reject")
|
||||||
|
if (protocol.equals(EConfigType.VLESS.name, true) && outbound.settings?.vnext?.first()?.users?.first()?.flow?.isNotEmpty() == true) {
|
||||||
|
outbound.mux?.concurrency = -1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outbound.mux?.enabled = false
|
||||||
|
outbound.mux?.concurrency = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to update outbound with global settings", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates an initial outbound template for a protocol type. */
|
||||||
|
fun createInitOutbound(configType: EConfigType): OutboundBean? {
|
||||||
|
return when (configType) {
|
||||||
|
EConfigType.VMESS,
|
||||||
|
EConfigType.VLESS ->
|
||||||
|
return OutboundBean(
|
||||||
|
protocol = configType.name.lowercase(),
|
||||||
|
settings = OutboundBean.OutSettingsBean(
|
||||||
|
vnext = listOf(
|
||||||
|
OutboundBean.OutSettingsBean.VnextBean(
|
||||||
|
users = listOf(OutboundBean.OutSettingsBean.VnextBean.UsersBean())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
streamSettings = OutboundBean.StreamSettingsBean()
|
||||||
|
)
|
||||||
|
|
||||||
|
EConfigType.SHADOWSOCKS,
|
||||||
|
EConfigType.SOCKS,
|
||||||
|
EConfigType.HTTP,
|
||||||
|
EConfigType.TROJAN ->
|
||||||
|
return OutboundBean(
|
||||||
|
protocol = configType.name.lowercase(),
|
||||||
|
settings = OutboundBean.OutSettingsBean(
|
||||||
|
servers = listOf(OutboundBean.OutSettingsBean.ServersBean())
|
||||||
|
),
|
||||||
|
streamSettings = OutboundBean.StreamSettingsBean()
|
||||||
|
)
|
||||||
|
|
||||||
|
EConfigType.WIREGUARD ->
|
||||||
|
return OutboundBean(
|
||||||
|
protocol = configType.name.lowercase(),
|
||||||
|
settings = OutboundBean.OutSettingsBean(
|
||||||
|
secretKey = "",
|
||||||
|
peers = listOf(OutboundBean.OutSettingsBean.WireGuardBean())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
EConfigType.HYSTERIA,
|
||||||
|
EConfigType.HYSTERIA2 ->
|
||||||
|
return OutboundBean(
|
||||||
|
protocol = EConfigType.HYSTERIA.name.lowercase(),
|
||||||
|
settings = OutboundBean.OutSettingsBean(
|
||||||
|
servers = null
|
||||||
|
),
|
||||||
|
streamSettings = OutboundBean.StreamSettingsBean()
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Per-protocol builders — implementations are identical to each *Fmt.toOutbound ──
|
||||||
|
|
||||||
|
private fun toOutboundVmess(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = createInitOutbound(EConfigType.VMESS)
|
||||||
|
|
||||||
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
|
vnext.address = getServerAddress(profileItem)
|
||||||
|
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
vnext.users[0].id = profileItem.password.orEmpty()
|
||||||
|
vnext.users[0].security = profileItem.method
|
||||||
|
}
|
||||||
|
|
||||||
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
|
populateTransportSettings(it, profileItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundBean?.streamSettings?.let {
|
||||||
|
populateTlsSettings(it, profileItem, sni)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toOutboundVless(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = createInitOutbound(EConfigType.VLESS)
|
||||||
|
|
||||||
|
outboundBean?.settings?.vnext?.first()?.let { vnext ->
|
||||||
|
vnext.address = getServerAddress(profileItem)
|
||||||
|
vnext.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
vnext.users[0].id = profileItem.password.orEmpty()
|
||||||
|
vnext.users[0].encryption = profileItem.method
|
||||||
|
vnext.users[0].flow = profileItem.flow
|
||||||
|
}
|
||||||
|
|
||||||
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
|
populateTransportSettings(it, profileItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundBean?.streamSettings?.let {
|
||||||
|
populateTlsSettings(it, profileItem, sni)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toOutboundShadowsocks(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = createInitOutbound(EConfigType.SHADOWSOCKS)
|
||||||
|
|
||||||
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
|
server.address = getServerAddress(profileItem)
|
||||||
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
server.password = profileItem.password
|
||||||
|
server.method = profileItem.method
|
||||||
|
}
|
||||||
|
|
||||||
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
|
populateTransportSettings(it, profileItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundBean?.streamSettings?.let {
|
||||||
|
populateTlsSettings(it, profileItem, sni)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toOutboundTrojan(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = createInitOutbound(EConfigType.TROJAN)
|
||||||
|
|
||||||
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
|
server.address = getServerAddress(profileItem)
|
||||||
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
server.password = profileItem.password
|
||||||
|
server.flow = profileItem.flow
|
||||||
|
}
|
||||||
|
|
||||||
|
val sni = outboundBean?.streamSettings?.let {
|
||||||
|
populateTransportSettings(it, profileItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundBean?.streamSettings?.let {
|
||||||
|
populateTlsSettings(it, profileItem, sni)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toOutboundSocks(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = createInitOutbound(EConfigType.SOCKS)
|
||||||
|
|
||||||
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
|
server.address = getServerAddress(profileItem)
|
||||||
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
if (profileItem.username.isNotNullEmpty()) {
|
||||||
|
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||||
|
socksUsersBean.user = profileItem.username.orEmpty()
|
||||||
|
socksUsersBean.pass = profileItem.password.orEmpty()
|
||||||
|
server.users = listOf(socksUsersBean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toOutboundHttp(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = createInitOutbound(EConfigType.HTTP)
|
||||||
|
|
||||||
|
outboundBean?.settings?.servers?.first()?.let { server ->
|
||||||
|
server.address = getServerAddress(profileItem)
|
||||||
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
if (profileItem.username.isNotNullEmpty()) {
|
||||||
|
val socksUsersBean = OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||||
|
socksUsersBean.user = profileItem.username.orEmpty()
|
||||||
|
socksUsersBean.pass = profileItem.password.orEmpty()
|
||||||
|
server.users = listOf(socksUsersBean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toOutboundWireguard(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = createInitOutbound(EConfigType.WIREGUARD)
|
||||||
|
|
||||||
|
val rawAddresses = profileItem.localAddress
|
||||||
|
?.split(",")
|
||||||
|
?.map { it.trim() }
|
||||||
|
?.filter { it.isNotEmpty() }
|
||||||
|
?.ifEmpty { null }
|
||||||
|
?: listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4)
|
||||||
|
|
||||||
|
val addresses = if (MmkvManager.decodeSettingsBool(AppConfig.PREF_IPV6_ENABLED) == true) {
|
||||||
|
rawAddresses
|
||||||
|
} else {
|
||||||
|
val ipv4Addresses = rawAddresses.filter { !it.contains(":") }
|
||||||
|
ipv4Addresses.ifEmpty { listOf(AppConfig.WIREGUARD_LOCAL_ADDRESS_V4) }
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundBean?.settings?.let { wireguard ->
|
||||||
|
wireguard.secretKey = profileItem.secretKey
|
||||||
|
wireguard.address = addresses
|
||||||
|
wireguard.peers?.firstOrNull()?.let { peer ->
|
||||||
|
peer.publicKey = profileItem.publicKey.orEmpty()
|
||||||
|
peer.preSharedKey = profileItem.preSharedKey?.nullIfBlank()
|
||||||
|
peer.endpoint = Utils.getIpv6Address(profileItem.server) + ":${profileItem.serverPort}"
|
||||||
|
}
|
||||||
|
wireguard.mtu = profileItem.mtu
|
||||||
|
wireguard.reserved = profileItem.reserved?.takeIf { it.isNotBlank() }?.split(",")?.filter { it.isNotBlank() }?.map { it.trim().toInt() }
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toOutboundHysteria2(profileItem: ProfileItem): OutboundBean? {
|
||||||
|
val outboundBean = createInitOutbound(EConfigType.HYSTERIA2) ?: return null
|
||||||
|
profileItem.network = NetworkType.HYSTERIA.type
|
||||||
|
profileItem.alpn = "h3"
|
||||||
|
|
||||||
|
outboundBean.settings?.let { server ->
|
||||||
|
server.address = getServerAddress(profileItem)
|
||||||
|
server.port = profileItem.serverPort.orEmpty().toInt()
|
||||||
|
server.version = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
val sni = outboundBean.streamSettings?.let {
|
||||||
|
populateTransportSettings(it, profileItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundBean.streamSettings?.let {
|
||||||
|
populateTlsSettings(it, profileItem, sni)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outboundBean
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTcpHttpRequest(
|
||||||
|
host: String?,
|
||||||
|
path: String?
|
||||||
|
): OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean {
|
||||||
|
val requestString =
|
||||||
|
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.122 Mobile Safari/537.36"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
|
||||||
|
val request = JsonUtil.fromJson(
|
||||||
|
requestString,
|
||||||
|
OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
|
||||||
|
) ?: OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean()
|
||||||
|
|
||||||
|
val parsedHost = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
request.headers.Host = parsedHost.ifEmpty { null }
|
||||||
|
request.path = path.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }.ifEmpty { listOf("/") }
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures transport settings for an outbound connection.
|
||||||
|
*
|
||||||
|
* Sets up protocol-specific transport options based on the profile settings.
|
||||||
|
*
|
||||||
|
* @param streamSettings The stream settings to configure
|
||||||
|
* @param profileItem The profile containing transport configuration
|
||||||
|
* @return The Server Name Indication (SNI) value to use, or null if not applicable
|
||||||
|
*/
|
||||||
|
fun populateTransportSettings(streamSettings: OutboundBean.StreamSettingsBean, profileItem: ProfileItem): String? {
|
||||||
|
val transport = profileItem.network.orEmpty()
|
||||||
|
val headerType = profileItem.headerType
|
||||||
|
val host = profileItem.host
|
||||||
|
val path = profileItem.path
|
||||||
|
val seed = profileItem.seed
|
||||||
|
// val quicSecurity = profileItem.quicSecurity
|
||||||
|
// val key = profileItem.quicKey
|
||||||
|
val mode = profileItem.mode
|
||||||
|
val serviceName = profileItem.serviceName
|
||||||
|
val authority = profileItem.authority
|
||||||
|
val xhttpMode = profileItem.xhttpMode
|
||||||
|
val xhttpExtra = profileItem.xhttpExtra
|
||||||
|
val finalMask = profileItem.finalMask
|
||||||
|
var sni: String? = null
|
||||||
|
streamSettings.network = transport.ifEmpty { NetworkType.TCP.type }
|
||||||
|
when (streamSettings.network) {
|
||||||
|
NetworkType.TCP.type -> {
|
||||||
|
val tcpSetting = OutboundBean.StreamSettingsBean.TcpSettingsBean()
|
||||||
|
if (headerType == AppConfig.HEADER_TYPE_HTTP) {
|
||||||
|
tcpSetting.header.type = AppConfig.HEADER_TYPE_HTTP
|
||||||
|
val requestObj = createTcpHttpRequest(host, path)
|
||||||
|
tcpSetting.header.request = requestObj
|
||||||
|
sni = requestObj.headers.Host?.getOrNull(0)
|
||||||
|
} else {
|
||||||
|
tcpSetting.header.type = "none"
|
||||||
|
sni = host
|
||||||
|
}
|
||||||
|
streamSettings.tcpSettings = tcpSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.KCP.type -> {
|
||||||
|
val kcpSetting = OutboundBean.StreamSettingsBean.KcpSettingsBean()
|
||||||
|
profileItem.kcpMtu?.let { kcpSetting.mtu = it }
|
||||||
|
profileItem.kcpTti?.let { kcpSetting.tti = it }
|
||||||
|
streamSettings.kcpSettings = kcpSetting
|
||||||
|
val udpMaskList = mutableListOf<OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean>()
|
||||||
|
if (!headerType.isNullOrEmpty() && headerType != "none") {
|
||||||
|
val kcpHeaderType = when {
|
||||||
|
headerType == "wechat-video" -> "header-wechat"
|
||||||
|
else -> "header-$headerType"
|
||||||
|
}
|
||||||
|
udpMaskList.add(
|
||||||
|
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
|
type = kcpHeaderType,
|
||||||
|
settings = if (headerType == "dns" && !host.isNullOrEmpty()) {
|
||||||
|
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
|
domain = host
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (seed.isNullOrEmpty()) {
|
||||||
|
udpMaskList.add(
|
||||||
|
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
|
type = "mkcp-original"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
udpMaskList.add(
|
||||||
|
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
|
type = "mkcp-aes128gcm",
|
||||||
|
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
|
password = seed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
streamSettings.finalmask = OutboundBean.StreamSettingsBean.FinalMaskBean(
|
||||||
|
udp = udpMaskList.toList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.WS.type -> {
|
||||||
|
val wssetting = OutboundBean.StreamSettingsBean.WsSettingsBean()
|
||||||
|
wssetting.host = host.orEmpty()
|
||||||
|
sni = host
|
||||||
|
wssetting.path = path ?: "/"
|
||||||
|
streamSettings.wsSettings = wssetting
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.HTTP_UPGRADE.type -> {
|
||||||
|
val httpupgradeSetting = OutboundBean.StreamSettingsBean.HttpupgradeSettingsBean()
|
||||||
|
httpupgradeSetting.host = host.orEmpty()
|
||||||
|
sni = host
|
||||||
|
httpupgradeSetting.path = path ?: "/"
|
||||||
|
streamSettings.httpupgradeSettings = httpupgradeSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.XHTTP.type -> {
|
||||||
|
val xhttpSetting = OutboundBean.StreamSettingsBean.XhttpSettingsBean()
|
||||||
|
xhttpSetting.host = host.orEmpty()
|
||||||
|
sni = host
|
||||||
|
xhttpSetting.path = path ?: "/"
|
||||||
|
xhttpSetting.mode = xhttpMode
|
||||||
|
xhttpSetting.extra = JsonUtil.parseString(xhttpExtra)
|
||||||
|
streamSettings.xhttpSettings = xhttpSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.H2.type, NetworkType.HTTP.type -> {
|
||||||
|
streamSettings.network = NetworkType.H2.type
|
||||||
|
val h2Setting = OutboundBean.StreamSettingsBean.HttpSettingsBean()
|
||||||
|
h2Setting.host = host.orEmpty().split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
sni = h2Setting.host.getOrNull(0)
|
||||||
|
h2Setting.path = path ?: "/"
|
||||||
|
streamSettings.httpSettings = h2Setting
|
||||||
|
}
|
||||||
|
|
||||||
|
// "quic" -> {
|
||||||
|
// val quicsetting = QuicSettingBean()
|
||||||
|
// quicsetting.security = quicSecurity ?: "none"
|
||||||
|
// quicsetting.key = key.orEmpty()
|
||||||
|
// quicsetting.header.type = headerType ?: "none"
|
||||||
|
// quicSettings = quicsetting
|
||||||
|
// }
|
||||||
|
|
||||||
|
NetworkType.GRPC.type -> {
|
||||||
|
val grpcSetting = OutboundBean.StreamSettingsBean.GrpcSettingsBean()
|
||||||
|
grpcSetting.multiMode = mode == "multi"
|
||||||
|
grpcSetting.serviceName = serviceName.orEmpty()
|
||||||
|
grpcSetting.authority = authority.orEmpty()
|
||||||
|
grpcSetting.idle_timeout = 60
|
||||||
|
grpcSetting.health_check_timeout = 20
|
||||||
|
sni = authority
|
||||||
|
streamSettings.grpcSettings = grpcSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkType.HYSTERIA.type -> {
|
||||||
|
val hysteriaSetting = OutboundBean.StreamSettingsBean.HysteriaSettingsBean(
|
||||||
|
version = 2,
|
||||||
|
auth = profileItem.password.orEmpty(),
|
||||||
|
)
|
||||||
|
val quicParams = OutboundBean.StreamSettingsBean.FinalMaskBean.QuicParamsBean(
|
||||||
|
brutalUp = profileItem.bandwidthUp?.nullIfBlank(),
|
||||||
|
brutalDown = profileItem.bandwidthDown?.nullIfBlank(),
|
||||||
|
)
|
||||||
|
quicParams.congestion = if (quicParams.brutalUp != null || quicParams.brutalDown != null) "brutal" else null
|
||||||
|
if (profileItem.portHopping.isNotNullEmpty()) {
|
||||||
|
val rawInterval = profileItem.portHoppingInterval?.trim().nullIfBlank()
|
||||||
|
val interval = if (rawInterval == null) {
|
||||||
|
"30"
|
||||||
|
} else {
|
||||||
|
val singleValue = rawInterval.toIntOrNull()
|
||||||
|
if (singleValue != null) {
|
||||||
|
if (singleValue < 5) {
|
||||||
|
"30"
|
||||||
|
} else {
|
||||||
|
rawInterval
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val parts = rawInterval.split('-')
|
||||||
|
if (parts.size == 2) {
|
||||||
|
val start = parts[0].trim().toIntOrNull()
|
||||||
|
val end = parts[1].trim().toIntOrNull()
|
||||||
|
if (start != null && end != null) {
|
||||||
|
val minStart = maxOf(5, start)
|
||||||
|
val minEnd = maxOf(minStart, end)
|
||||||
|
"$minStart-$minEnd"
|
||||||
|
} else {
|
||||||
|
"30"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"30"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quicParams.udpHop = OutboundBean.StreamSettingsBean.FinalMaskBean.QuicParamsBean.UdpHopBean(
|
||||||
|
ports = profileItem.portHopping,
|
||||||
|
interval = interval
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val finalmask = OutboundBean.StreamSettingsBean.FinalMaskBean(
|
||||||
|
quicParams = quicParams
|
||||||
|
)
|
||||||
|
if (profileItem.obfsPassword.isNotNullEmpty()) {
|
||||||
|
finalmask.udp = listOf(
|
||||||
|
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
|
type = "salamander",
|
||||||
|
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
|
password = profileItem.obfsPassword.orEmpty()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
streamSettings.hysteriaSettings = hysteriaSetting
|
||||||
|
streamSettings.finalmask = finalmask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalMask?.let {
|
||||||
|
val parsedFinalMask = JsonUtil.parseString(finalMask)
|
||||||
|
if (parsedFinalMask != null) {
|
||||||
|
streamSettings.finalmask = parsedFinalMask
|
||||||
|
} else {
|
||||||
|
LogUtil.w("V2rayConfigManager", "Invalid finalMask JSON, keeping previously generated finalmask")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sni
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures TLS or REALITY security settings for an outbound connection.
|
||||||
|
*
|
||||||
|
* Sets up security-related parameters like certificates, fingerprints, and SNI.
|
||||||
|
*
|
||||||
|
* @param streamSettings The stream settings to configure
|
||||||
|
* @param profileItem The profile containing security configuration
|
||||||
|
* @param sniExt An external SNI value to use if the profile doesn't specify one
|
||||||
|
*/
|
||||||
|
fun populateTlsSettings(streamSettings: OutboundBean.StreamSettingsBean, profileItem: ProfileItem, sniExt: String?) {
|
||||||
|
val streamSecurity = profileItem.security.orEmpty()
|
||||||
|
val allowInsecure = profileItem.insecure == true
|
||||||
|
val sni = if (profileItem.sni.isNullOrEmpty()) {
|
||||||
|
when {
|
||||||
|
sniExt.isNotNullEmpty() && Utils.isDomainName(sniExt) -> sniExt
|
||||||
|
profileItem.server.isNotNullEmpty() && Utils.isDomainName(profileItem.server) -> profileItem.server
|
||||||
|
else -> sniExt
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
profileItem.sni
|
||||||
|
}
|
||||||
|
|
||||||
|
streamSettings.security = streamSecurity.nullIfBlank()
|
||||||
|
if (streamSettings.security == null) return
|
||||||
|
val tlsSetting = OutboundBean.StreamSettingsBean.TlsSettingsBean(
|
||||||
|
allowInsecure = allowInsecure,
|
||||||
|
serverName = sni.nullIfBlank(),
|
||||||
|
fingerprint = profileItem.fingerPrint.nullIfBlank(),
|
||||||
|
alpn = profileItem.alpn?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }.takeIf { !it.isNullOrEmpty() },
|
||||||
|
echConfigList = profileItem.echConfigList.nullIfBlank(),
|
||||||
|
pinnedPeerCertSha256 = profileItem.pinnedCA256.nullIfBlank(),
|
||||||
|
publicKey = profileItem.publicKey.nullIfBlank(),
|
||||||
|
shortId = profileItem.shortId.nullIfBlank(),
|
||||||
|
spiderX = profileItem.spiderX.nullIfBlank(),
|
||||||
|
mldsa65Verify = profileItem.mldsa65Verify.nullIfBlank(),
|
||||||
|
)
|
||||||
|
if (streamSettings.security == AppConfig.TLS) {
|
||||||
|
streamSettings.tlsSettings = tlsSetting
|
||||||
|
streamSettings.realitySettings = null
|
||||||
|
} else if (streamSettings.security == AppConfig.REALITY) {
|
||||||
|
streamSettings.tlsSettings = null
|
||||||
|
streamSettings.realitySettings = tlsSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileItem.finalMask.isNullOrEmpty()) {
|
||||||
|
updateOutboundFragment(streamSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the outbound with fragment settings for traffic optimization.
|
||||||
|
*
|
||||||
|
* Configures packet fragmentation for TLS and REALITY protocols if enabled.
|
||||||
|
*
|
||||||
|
* @param streamSettings The streamSettings object to be modified
|
||||||
|
* @return true if fragment configuration was successful, false otherwise
|
||||||
|
*/
|
||||||
|
private fun updateOutboundFragment(streamSettings: OutboundBean.StreamSettingsBean): Boolean {
|
||||||
|
try {
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_FRAGMENT_ENABLED, false) == false) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (streamSettings.security != AppConfig.TLS
|
||||||
|
&& streamSettings.security != AppConfig.REALITY
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (streamSettings.sockopt?.dialerProxy.isNotNullEmpty()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var packets =
|
||||||
|
MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
|
||||||
|
if (streamSettings.security == AppConfig.REALITY
|
||||||
|
&& packets == "tlshello"
|
||||||
|
) {
|
||||||
|
packets = "1-3"
|
||||||
|
} else if (streamSettings.security == AppConfig.TLS
|
||||||
|
&& packets != "tlshello"
|
||||||
|
) {
|
||||||
|
packets = "tlshello"
|
||||||
|
}
|
||||||
|
|
||||||
|
val fragmentMask = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
|
type = "fragment",
|
||||||
|
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
|
packets = packets,
|
||||||
|
length = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_LENGTH)
|
||||||
|
?: "50-100",
|
||||||
|
delay = MmkvManager.decodeSettingsString(AppConfig.PREF_FRAGMENT_INTERVAL)
|
||||||
|
?: "10-20"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val noiseMask = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean(
|
||||||
|
type = "noise",
|
||||||
|
settings = OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean(
|
||||||
|
noise = listOf(
|
||||||
|
OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean.MaskSettingsBean.NoiseMaskBean(
|
||||||
|
rand = "10-20",
|
||||||
|
delay = "10-16",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val finalMaskObj = streamSettings.finalmask?.let { existingFinalMask ->
|
||||||
|
JsonUtil.parseString(JsonUtil.toJson(existingFinalMask))
|
||||||
|
} ?: JsonObject()
|
||||||
|
|
||||||
|
// finalmask.tcp / finalmask.udp are arrays; prepend mask at index 0.
|
||||||
|
fun prependMask(scope: String, mask: OutboundBean.StreamSettingsBean.FinalMaskBean.MaskBean) {
|
||||||
|
val current = finalMaskObj.get(scope)
|
||||||
|
if (current != null && current.isJsonArray && current.asJsonArray.size() > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val newArray = JsonArray()
|
||||||
|
newArray.add(JsonUtil.parseString(JsonUtil.toJson(mask)))
|
||||||
|
|
||||||
|
if (current != null && current.isJsonArray) {
|
||||||
|
current.asJsonArray.forEach { newArray.add(it) }
|
||||||
|
}
|
||||||
|
finalMaskObj.add(scope, newArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
prependMask("tcp", fragmentMask)
|
||||||
|
prependMask("udp", noiseMask)
|
||||||
|
streamSettings.finalmask = finalMaskObj
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "Failed to update outbound fragment", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getServerAddress(profileItem: ProfileItem): String {
|
||||||
|
if (Utils.isPureIpAddress(profileItem.server.orEmpty())) {
|
||||||
|
return profileItem.server.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
val domain = HttpUtil.toIdnDomain(profileItem.server.orEmpty())
|
||||||
|
if (MmkvManager.decodeSettingsString(AppConfig.PREF_OUTBOUND_DOMAIN_RESOLVE_METHOD, "1") != "2") {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
//Resolve and replace domain
|
||||||
|
val resolvedIps = HttpUtil.resolveHostToIP(domain, MmkvManager.decodeSettingsBool(AppConfig.PREF_PREFER_IPV6))
|
||||||
|
if (resolvedIps.isNullOrEmpty()) {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
return resolvedIps.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
package com.v2ray.ang.core
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.system.OsConstants
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.R
|
||||||
|
import com.v2ray.ang.contracts.ServiceControl
|
||||||
|
import com.v2ray.ang.dto.OutboundTrafficStat
|
||||||
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
|
import com.v2ray.ang.enums.EConfigType
|
||||||
|
import com.v2ray.ang.extension.isComplexType
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
|
import com.v2ray.ang.handler.NotificationManager
|
||||||
|
import com.v2ray.ang.handler.SettingsManager
|
||||||
|
import com.v2ray.ang.handler.SpeedtestManager
|
||||||
|
import com.v2ray.ang.service.CoreProxyOnlyService
|
||||||
|
import com.v2ray.ang.service.CoreVpnService
|
||||||
|
import com.v2ray.ang.service.DialerNativeService
|
||||||
|
import com.v2ray.ang.service.DialerWebviewService
|
||||||
|
import com.v2ray.ang.service.IDialerService
|
||||||
|
import com.v2ray.ang.util.LogUtil
|
||||||
|
import com.v2ray.ang.util.MessageUtil
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import libv2ray.CoreCallbackHandler
|
||||||
|
import libv2ray.CoreController
|
||||||
|
import libv2ray.ProcessFinder
|
||||||
|
import java.lang.ref.SoftReference
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
|
||||||
|
object CoreServiceManager {
|
||||||
|
|
||||||
|
private val coreController: CoreController = CoreNativeManager.newCoreController(CoreCallback())
|
||||||
|
private val mMsgReceive = ReceiveMessageHandler()
|
||||||
|
private var currentConfig: ProfileItem? = null
|
||||||
|
private var processFinder: XrayProcessFinder? = null
|
||||||
|
private var browserDialer: IDialerService? = null
|
||||||
|
|
||||||
|
var serviceControl: SoftReference<ServiceControl>? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
val service = value?.get()?.getService()
|
||||||
|
CoreNativeManager.initCoreEnv(service)
|
||||||
|
if (service != null && processFinder == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
processFinder = XrayProcessFinder(service)
|
||||||
|
coreController.registerProcessFinder(processFinder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the V2Ray service from a toggle action.
|
||||||
|
* @param context The context from which the service is started.
|
||||||
|
* @return True if the service was started successfully, false otherwise.
|
||||||
|
*/
|
||||||
|
fun startVServiceFromToggle(context: Context): Boolean {
|
||||||
|
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||||
|
context.toast(R.string.app_tile_first_use)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
startContextService(context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: ${e.message}", e)
|
||||||
|
context.toast(e.message ?: e.javaClass.simpleName)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the V2Ray service.
|
||||||
|
* @param context The context from which the service is started.
|
||||||
|
* @param guid The GUID of the server configuration to use (optional).
|
||||||
|
*/
|
||||||
|
fun startVService(context: Context, guid: String? = null) {
|
||||||
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
|
||||||
|
|
||||||
|
if (guid != null) {
|
||||||
|
MmkvManager.setSelectServer(guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startContextService(context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: ${e.message}", e)
|
||||||
|
context.toast(e.message ?: e.javaClass.simpleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the V2Ray service.
|
||||||
|
* @param context The context from which the service is stopped.
|
||||||
|
*/
|
||||||
|
fun stopVService(context: Context) {
|
||||||
|
//context.toast(R.string.toast_services_stop)
|
||||||
|
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the V2Ray service is running.
|
||||||
|
* @return True if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isRunning() = coreController.isRunning
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the currently running server.
|
||||||
|
* @return The name of the running server.
|
||||||
|
*/
|
||||||
|
fun getRunningServerName() = currentConfig?.remarks.orEmpty()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the context service for V2Ray.
|
||||||
|
* Chooses between VPN service or Proxy-only service based on user settings.
|
||||||
|
* @param context The context from which the service is started.
|
||||||
|
* @throws IllegalStateException if the core is already running, no server is selected,
|
||||||
|
* server config cannot be decoded, or server configuration is invalid.
|
||||||
|
* @throws Exception if the foreground service fails to start.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
private fun startContextService(context: Context) {
|
||||||
|
if (coreController.isRunning) {
|
||||||
|
LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val guid = MmkvManager.getSelectServer()
|
||||||
|
?: run {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: No server selected")
|
||||||
|
error(context.getString(R.string.app_tile_first_use))
|
||||||
|
}
|
||||||
|
|
||||||
|
val config = MmkvManager.decodeServerConfig(guid)
|
||||||
|
?: run {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
|
||||||
|
error(context.getString(R.string.toast_config_file_invalid))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.configType.isComplexType()
|
||||||
|
&& !Utils.isValidUrl(config.server)
|
||||||
|
&& !Utils.isPureIpAddress(config.server.orEmpty())
|
||||||
|
) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Invalid server configuration")
|
||||||
|
error(context.getString(R.string.toast_config_file_invalid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh socks port when enabled dynamic socks port
|
||||||
|
SettingsManager.refreshRuntimeSocksPort()
|
||||||
|
|
||||||
|
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
|
||||||
|
// if (!result.status) error(result.errorMessage.ifBlank { "Failed to get V2Ray config" })
|
||||||
|
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING)) {
|
||||||
|
context.toast(R.string.toast_warning_pref_proxysharing_short)
|
||||||
|
} else {
|
||||||
|
context.toast(R.string.toast_services_start)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isVpnMode = SettingsManager.isVpnMode()
|
||||||
|
val intent = if (isVpnMode) {
|
||||||
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting VPN service")
|
||||||
|
Intent(context.applicationContext, CoreVpnService::class.java)
|
||||||
|
} else {
|
||||||
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting Proxy service")
|
||||||
|
Intent(context.applicationContext, CoreProxyOnlyService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Missing permission to start foreground service", e)
|
||||||
|
throw IllegalStateException(e.message ?: e.javaClass.simpleName, e)
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||||
|
e.javaClass.name == "android.app.ForegroundServiceStartNotAllowedException"
|
||||||
|
) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Foreground service start not allowed", e)
|
||||||
|
throw IllegalStateException(e.message ?: e.javaClass.simpleName, e)
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
|
||||||
|
* `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
|
||||||
|
* Starts the V2Ray core service.
|
||||||
|
*/
|
||||||
|
fun startCoreLoop(vpnInterface: ParcelFileDescriptor?): Boolean {
|
||||||
|
if (coreController.isRunning) {
|
||||||
|
LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val service = getService()
|
||||||
|
if (service == null) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Service is null")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
doStartCoreLoop(service, vpnInterface)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val message = e.message?.takeUnless { it.isBlank() } ?: e.javaClass.simpleName
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: $message", e)
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, message)
|
||||||
|
NotificationManager.cancelNotification()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
private fun doStartCoreLoop(service: Service, vpnInterface: ParcelFileDescriptor?) {
|
||||||
|
val guid = MmkvManager.getSelectServer() ?: error("No server selected")
|
||||||
|
val config = MmkvManager.decodeServerConfig(guid) ?: error("Failed to decode server config")
|
||||||
|
|
||||||
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting core loop for ${config.remarks}")
|
||||||
|
val result = CoreConfigManager.getV2rayConfig(service, guid)
|
||||||
|
LogUtil.d(AppConfig.TAG, result.content)
|
||||||
|
if (!result.status) {
|
||||||
|
error(result.errorMessage.ifBlank { "Failed to get V2Ray config" })
|
||||||
|
}
|
||||||
|
|
||||||
|
val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
|
||||||
|
mFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||||
|
mFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||||
|
mFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||||
|
ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
|
||||||
|
|
||||||
|
currentConfig = config
|
||||||
|
var tunFd = vpnInterface?.fd ?: 0
|
||||||
|
val dialerAddr = if (currentConfig?.browserDialerMode.isNullOrEmpty()) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"127.0.0.1:${Utils.findRandomFreePort()}"
|
||||||
|
}
|
||||||
|
if (SettingsManager.isUsingHevTun()) {
|
||||||
|
tunFd = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationManager.showNotification(currentConfig)
|
||||||
|
CoreNativeManager.reconcileBrowserDialer(dialerAddr)
|
||||||
|
coreController.startLoop(result.content, tunFd)
|
||||||
|
|
||||||
|
if (!coreController.isRunning) {
|
||||||
|
error("Core failed to start")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browserDialer != null) {
|
||||||
|
browserDialer!!.stop()
|
||||||
|
browserDialer = null
|
||||||
|
}
|
||||||
|
if (config.browserDialerMode == "OkHttp") {
|
||||||
|
browserDialer = DialerNativeService()
|
||||||
|
browserDialer!!.start(service, dialerAddr)
|
||||||
|
} else if (config.browserDialerMode == "WebView") {
|
||||||
|
browserDialer = DialerWebviewService()
|
||||||
|
browserDialer!!.start(service, dialerAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
|
||||||
|
NotificationManager.startSpeedNotification()
|
||||||
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Core started successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the V2Ray core service.
|
||||||
|
* Unregisters broadcast receivers, stops notifications, and shuts down plugins.
|
||||||
|
* @return True if the core was stopped successfully, false otherwise.
|
||||||
|
*/
|
||||||
|
fun stopCoreLoop(): Boolean {
|
||||||
|
val service = getService() ?: return false
|
||||||
|
|
||||||
|
if (coreController.isRunning) {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
coreController.stopLoop()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to stop V2Ray loop", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close existing browser dialer
|
||||||
|
CoreNativeManager.reconcileBrowserDialer("")
|
||||||
|
if (browserDialer != null) {
|
||||||
|
browserDialer!!.stop()
|
||||||
|
browserDialer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
|
||||||
|
NotificationManager.cancelNotification()
|
||||||
|
|
||||||
|
try {
|
||||||
|
service.unregisterReceiver(mMsgReceive)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to unregister receiver", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries and resets all outbound traffic counters in one core call.
|
||||||
|
* Go side format: tag,direction,value;tag,direction,value;
|
||||||
|
*/
|
||||||
|
fun queryAllOutboundTrafficStats(): List<OutboundTrafficStat> {
|
||||||
|
val payload = coreController.queryAllOutboundTrafficStats()
|
||||||
|
|
||||||
|
val result = ArrayList<OutboundTrafficStat>()
|
||||||
|
|
||||||
|
payload.split(';').forEach { entry ->
|
||||||
|
if (entry.isBlank()) return@forEach
|
||||||
|
|
||||||
|
val parts = entry.split(',', limit = 3)
|
||||||
|
if (parts.size != 3) return@forEach
|
||||||
|
|
||||||
|
val value = parts[2].toLongOrNull() ?: return@forEach
|
||||||
|
|
||||||
|
result.add(
|
||||||
|
OutboundTrafficStat(
|
||||||
|
tag = parts[0],
|
||||||
|
direction = parts[1],
|
||||||
|
value = value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// LogUtil.d(AppConfig.TAG, "Queried outbound traffic stats: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the connection delay for the current V2Ray configuration.
|
||||||
|
* Tests with primary URL first, then falls back to alternative URL if needed.
|
||||||
|
* Also fetches remote IP information if the delay test was successful.
|
||||||
|
*/
|
||||||
|
private fun measureV2rayDelay() {
|
||||||
|
if (coreController.isRunning == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val service = getService() ?: return@launch
|
||||||
|
var time = -1L
|
||||||
|
var errorStr = ""
|
||||||
|
|
||||||
|
try {
|
||||||
|
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
|
||||||
|
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
|
}
|
||||||
|
if (time == -1L) {
|
||||||
|
try {
|
||||||
|
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
|
||||||
|
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = if (time >= 0) {
|
||||||
|
service.getString(R.string.connection_test_available, time)
|
||||||
|
} else {
|
||||||
|
service.getString(R.string.connection_test_error, errorStr)
|
||||||
|
}
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
|
||||||
|
|
||||||
|
// Only fetch IP info if the delay test was successful
|
||||||
|
if (time >= 0) {
|
||||||
|
SpeedtestManager.getRemoteIPInfo()?.let { ip ->
|
||||||
|
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current service instance.
|
||||||
|
* @return The current service instance, or null if not available.
|
||||||
|
*/
|
||||||
|
private fun getService(): Service? {
|
||||||
|
return serviceControl?.get()?.getService()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core callback handler implementation for handling V2Ray core events.
|
||||||
|
* Handles startup, shutdown, socket protection, and status emission.
|
||||||
|
*/
|
||||||
|
private class CoreCallback : CoreCallbackHandler {
|
||||||
|
/**
|
||||||
|
* Called when V2Ray core starts up.
|
||||||
|
* @return 0 for success, any other value for failure.
|
||||||
|
*/
|
||||||
|
override fun startup(): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when V2Ray core shuts down.
|
||||||
|
* @return 0 for success, any other value for failure.
|
||||||
|
*/
|
||||||
|
override fun shutdown(): Long {
|
||||||
|
val serviceControl = serviceControl?.get() ?: return -1
|
||||||
|
return try {
|
||||||
|
serviceControl.stopService()
|
||||||
|
0
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to stop service", e)
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when V2Ray core emits status information.
|
||||||
|
* @param l Status code.
|
||||||
|
* @param s Status message.
|
||||||
|
* @return Always returns 0.
|
||||||
|
*/
|
||||||
|
override fun onEmitStatus(l: Long, s: String?): Long {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process finder implementation for Xray core.
|
||||||
|
* Uses ConnectivityManager to find the owning UID of a connection based on network parameters.
|
||||||
|
*/
|
||||||
|
private class XrayProcessFinder(context: Context) : ProcessFinder {
|
||||||
|
private val cm: ConnectivityManager? = context.getSystemService(ConnectivityManager::class.java)
|
||||||
|
|
||||||
|
override fun findProcessByConnection(network: String, srcIP: String, srcPort: Long, destIP: String, destPort: Long): Long {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return -1L
|
||||||
|
if (cm == null) return -1L
|
||||||
|
val proto = when (network) {
|
||||||
|
"tcp" -> OsConstants.IPPROTO_TCP
|
||||||
|
"udp" -> OsConstants.IPPROTO_UDP
|
||||||
|
else -> return -1L
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destIP.isBlank() || destPort == 0L) {
|
||||||
|
LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to :$destPort, (no dest)")
|
||||||
|
return -1L
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val uid = cm.getConnectionOwnerUid(
|
||||||
|
proto,
|
||||||
|
InetSocketAddress(srcIP, srcPort.toInt()),
|
||||||
|
InetSocketAddress(destIP, destPort.toInt())
|
||||||
|
).toLong()
|
||||||
|
LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid")
|
||||||
|
//LogUtil.d(AppConfig.TAG, "ProcessFinder: Find $network connection from $srcIP:$srcPort to $destIP:$destPort, uid=$uid,${PackageUidResolver.uidToPackageName(uid.toString())}")
|
||||||
|
|
||||||
|
uid
|
||||||
|
} catch (_: Exception) {
|
||||||
|
-1L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast receiver for handling messages sent to the service.
|
||||||
|
* Handles registration, service control, and screen events.
|
||||||
|
*/
|
||||||
|
private class ReceiveMessageHandler : BroadcastReceiver() {
|
||||||
|
/**
|
||||||
|
* Handles received broadcast messages.
|
||||||
|
* Processes service control messages and screen state changes.
|
||||||
|
* @param ctx The context in which the receiver is running.
|
||||||
|
* @param intent The intent being received.
|
||||||
|
*/
|
||||||
|
override fun onReceive(ctx: Context?, intent: Intent?) {
|
||||||
|
val serviceControl = serviceControl?.get() ?: return
|
||||||
|
when (intent?.getIntExtra("key", 0)) {
|
||||||
|
AppConfig.MSG_REGISTER_CLIENT -> {
|
||||||
|
if (coreController.isRunning) {
|
||||||
|
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
|
||||||
|
} else {
|
||||||
|
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppConfig.MSG_UNREGISTER_CLIENT -> {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
AppConfig.MSG_STATE_START -> {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
AppConfig.MSG_STATE_STOP -> {
|
||||||
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Stop service")
|
||||||
|
serviceControl.stopService()
|
||||||
|
}
|
||||||
|
|
||||||
|
AppConfig.MSG_STATE_RESTART -> {
|
||||||
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Restart service")
|
||||||
|
serviceControl.stopService()
|
||||||
|
Thread.sleep(500L)
|
||||||
|
startVService(serviceControl.getService())
|
||||||
|
}
|
||||||
|
|
||||||
|
AppConfig.MSG_MEASURE_DELAY -> {
|
||||||
|
measureV2rayDelay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (intent?.action) {
|
||||||
|
Intent.ACTION_SCREEN_OFF -> {
|
||||||
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen off")
|
||||||
|
NotificationManager.stopSpeedNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent.ACTION_SCREEN_ON -> {
|
||||||
|
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Screen on")
|
||||||
|
NotificationManager.startSpeedNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
|
||||||
|
data class AppInfo(
|
||||||
|
val appName: String,
|
||||||
|
val packageName: String,
|
||||||
|
val appIcon: Drawable,
|
||||||
|
val isSystemApp: Boolean,
|
||||||
|
var isSelected: Int
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class CheckUpdateResult(
|
||||||
|
val hasUpdate: Boolean,
|
||||||
|
val latestVersion: String? = null,
|
||||||
|
val releaseNotes: String? = null,
|
||||||
|
val downloadUrl: String? = null,
|
||||||
|
val error: String? = null,
|
||||||
|
val isPreRelease: Boolean = false
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class ConfigResult(
|
||||||
|
var status: Boolean,
|
||||||
|
var guid: String? = null,
|
||||||
|
var content: String = "",
|
||||||
|
var errorMessage: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.v2ray.ang.dto.entities.ProfileItem
|
||||||
|
import com.v2ray.ang.enums.CoreResolvedType
|
||||||
|
|
||||||
|
data class CoreConfigContext(
|
||||||
|
val context: Context,
|
||||||
|
val guid: String,
|
||||||
|
val isCustom: Boolean = false,
|
||||||
|
val resolvedOutbounds: List<ResolvedOutbound> = emptyList(),
|
||||||
|
) {
|
||||||
|
data class ResolvedOutbound(
|
||||||
|
val tag: String,
|
||||||
|
val profile: ProfileItem,
|
||||||
|
val resolvedProfiles: List<ProfileItem>,
|
||||||
|
val resolvedType: CoreResolvedType,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class GitHubRelease(
|
||||||
|
@SerializedName("tag_name")
|
||||||
|
val tagName: String,
|
||||||
|
@SerializedName("body")
|
||||||
|
val body: String,
|
||||||
|
@SerializedName("assets")
|
||||||
|
val assets: List<Asset>,
|
||||||
|
@SerializedName("prerelease")
|
||||||
|
val prerelease: Boolean = false,
|
||||||
|
@SerializedName("published_at")
|
||||||
|
val publishedAt: String = ""
|
||||||
|
) {
|
||||||
|
data class Asset(
|
||||||
|
@SerializedName("name")
|
||||||
|
val name: String,
|
||||||
|
@SerializedName("browser_download_url")
|
||||||
|
val browserDownloadUrl: String
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class GroupMapItem(
|
||||||
|
var id: String,
|
||||||
|
var remarks: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class IPAPIInfo(
|
||||||
|
var ip: String? = null,
|
||||||
|
var clientIp: String? = null,
|
||||||
|
var ip_addr: String? = null,
|
||||||
|
var query: String? = null,
|
||||||
|
var country: String? = null,
|
||||||
|
var country_name: String? = null,
|
||||||
|
var country_code: String? = null,
|
||||||
|
var countryCode: String? = null,
|
||||||
|
var location: LocationBean? = null
|
||||||
|
) {
|
||||||
|
data class LocationBean(
|
||||||
|
var country_code: String? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class OutboundTrafficStat(
|
||||||
|
val tag: String,
|
||||||
|
val direction: String,
|
||||||
|
val value: Long,
|
||||||
|
)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
sealed class RealPingEvent {
|
||||||
|
|
||||||
|
/** Periodic progress update while the batch is still running. */
|
||||||
|
data class Progress(val text: String) : RealPingEvent()
|
||||||
|
|
||||||
|
/** A single server result is available. */
|
||||||
|
data class Result(val guid: String, val delayMillis: Long) : RealPingEvent()
|
||||||
|
|
||||||
|
/** The entire batch has finished or been cancelled. */
|
||||||
|
data class Finish(val status: String) : RealPingEvent()
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of subscription update operation
|
||||||
|
*/
|
||||||
|
data class SubscriptionUpdateResult(
|
||||||
|
val configCount: Int = 0, // Total configs updated
|
||||||
|
val successCount: Int = 0, // Subscriptions updated successfully
|
||||||
|
val failureCount: Int = 0, // Subscriptions failed to update
|
||||||
|
val skipCount: Int = 0 // Subscriptions skipped (disabled)
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Combine two results by adding their counts
|
||||||
|
*/
|
||||||
|
operator fun plus(other: SubscriptionUpdateResult): SubscriptionUpdateResult {
|
||||||
|
return SubscriptionUpdateResult(
|
||||||
|
configCount = this.configCount + other.configCount,
|
||||||
|
successCount = this.successCount + other.successCount,
|
||||||
|
failureCount = this.failureCount + other.failureCount,
|
||||||
|
skipCount = this.skipCount + other.skipCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
data class TestServiceMessage(
|
||||||
|
val key: Int,
|
||||||
|
val subscriptionId: String = "",
|
||||||
|
val serverGuids: List<String> = emptyList()
|
||||||
|
) : Serializable
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class UrlContentRequest(
|
||||||
|
val url: String?,
|
||||||
|
val timeout: Int = 15000,
|
||||||
|
val httpPort: Int = 0,
|
||||||
|
val proxyUsername: String? = null,
|
||||||
|
val proxyPassword: String? = null,
|
||||||
|
val userAgent: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.enums.EConfigType
|
||||||
|
|
||||||
|
data class V2rayConfig(
|
||||||
|
var remarks: String? = null,
|
||||||
|
var stats: Any? = null,
|
||||||
|
val log: LogBean,
|
||||||
|
var policy: PolicyBean? = null,
|
||||||
|
val inbounds: ArrayList<InboundBean>,
|
||||||
|
var outbounds: ArrayList<OutboundBean>,
|
||||||
|
var dns: DnsBean? = null,
|
||||||
|
val routing: RoutingBean,
|
||||||
|
val api: Any? = null,
|
||||||
|
val transport: Any? = null,
|
||||||
|
val reverse: Any? = null,
|
||||||
|
var fakedns: Any? = null,
|
||||||
|
val browserForwarder: Any? = null,
|
||||||
|
var observatory: Any? = null,
|
||||||
|
var burstObservatory: Any? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class LogBean(
|
||||||
|
val access: String? = null,
|
||||||
|
val error: String? = null,
|
||||||
|
var loglevel: String? = null,
|
||||||
|
val dnsLog: Boolean? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class InboundBean(
|
||||||
|
var tag: String,
|
||||||
|
var port: Int?,
|
||||||
|
var protocol: String,
|
||||||
|
var listen: String? = null,
|
||||||
|
var settings: InSettingsBean? = null,
|
||||||
|
var sniffing: SniffingBean? = null,
|
||||||
|
val streamSettings: Any? = null,
|
||||||
|
val allocate: Any? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class InSettingsBean(
|
||||||
|
var auth: String? = null,
|
||||||
|
var udp: Boolean? = null,
|
||||||
|
var userLevel: Int? = null,
|
||||||
|
var accounts: List<SocksAccountBean>? = null,
|
||||||
|
var name: String? = null,
|
||||||
|
|
||||||
|
var mtu: Int? = null
|
||||||
|
) {
|
||||||
|
data class SocksAccountBean(
|
||||||
|
var user: String = "",
|
||||||
|
var pass: String = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SniffingBean(
|
||||||
|
var enabled: Boolean,
|
||||||
|
val destOverride: ArrayList<String>,
|
||||||
|
val metadataOnly: Boolean? = null,
|
||||||
|
var routeOnly: Boolean? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class OutboundBean(
|
||||||
|
var tag: String = "proxy",
|
||||||
|
var protocol: String,
|
||||||
|
var settings: OutSettingsBean? = null,
|
||||||
|
var streamSettings: StreamSettingsBean? = null,
|
||||||
|
val sendThrough: String? = null,
|
||||||
|
var mux: MuxBean? = MuxBean(false)
|
||||||
|
) {
|
||||||
|
data class OutSettingsBean(
|
||||||
|
var vnext: List<VnextBean>? = null,
|
||||||
|
var servers: List<ServersBean>? = null,
|
||||||
|
/*Blackhole*/
|
||||||
|
var response: Response? = null,
|
||||||
|
/*DNS*/
|
||||||
|
val network: String? = null,
|
||||||
|
var address: Any? = null,
|
||||||
|
var port: Int? = null,
|
||||||
|
/*Freedom*/
|
||||||
|
val redirect: String? = null,
|
||||||
|
val userLevel: Int? = null,
|
||||||
|
/*Loopback*/
|
||||||
|
val inboundTag: String? = null,
|
||||||
|
/*Wireguard*/
|
||||||
|
var secretKey: String? = null,
|
||||||
|
val peers: List<WireGuardBean>? = null,
|
||||||
|
var reserved: List<Int>? = null,
|
||||||
|
var mtu: Int? = null,
|
||||||
|
var obfsPassword: String? = null,
|
||||||
|
var version: Int? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class VnextBean(
|
||||||
|
var address: String = "",
|
||||||
|
var port: Int = AppConfig.DEFAULT_PORT,
|
||||||
|
var users: List<UsersBean>
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class UsersBean(
|
||||||
|
var id: String = "",
|
||||||
|
var alterId: Int? = null,
|
||||||
|
var security: String? = null,
|
||||||
|
var level: Int = AppConfig.DEFAULT_LEVEL,
|
||||||
|
var encryption: String? = null,
|
||||||
|
var flow: String? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ServersBean(
|
||||||
|
var address: String = "",
|
||||||
|
var method: String? = null,
|
||||||
|
var ota: Boolean = false,
|
||||||
|
var password: String? = null,
|
||||||
|
var port: Int = AppConfig.DEFAULT_PORT,
|
||||||
|
var level: Int = AppConfig.DEFAULT_LEVEL,
|
||||||
|
val email: String? = null,
|
||||||
|
var flow: String? = null,
|
||||||
|
val ivCheck: Boolean? = null,
|
||||||
|
var users: List<SocksUsersBean>? = null
|
||||||
|
) {
|
||||||
|
data class SocksUsersBean(
|
||||||
|
var user: String = "",
|
||||||
|
var pass: String = "",
|
||||||
|
var level: Int = AppConfig.DEFAULT_LEVEL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Response(var type: String)
|
||||||
|
|
||||||
|
data class WireGuardBean(
|
||||||
|
var publicKey: String = "",
|
||||||
|
var preSharedKey: String? = null,
|
||||||
|
var endpoint: String = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class StreamSettingsBean(
|
||||||
|
var network: String = AppConfig.DEFAULT_NETWORK,
|
||||||
|
var security: String? = null,
|
||||||
|
var tcpSettings: TcpSettingsBean? = null,
|
||||||
|
var kcpSettings: KcpSettingsBean? = null,
|
||||||
|
var wsSettings: WsSettingsBean? = null,
|
||||||
|
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
|
||||||
|
var xhttpSettings: XhttpSettingsBean? = null,
|
||||||
|
var httpSettings: HttpSettingsBean? = null,
|
||||||
|
var tlsSettings: TlsSettingsBean? = null,
|
||||||
|
var quicSettings: QuicSettingBean? = null,
|
||||||
|
var realitySettings: TlsSettingsBean? = null,
|
||||||
|
var grpcSettings: GrpcSettingsBean? = null,
|
||||||
|
var hysteriaSettings: HysteriaSettingsBean? = null,
|
||||||
|
var finalmask: Any? = null,
|
||||||
|
val dsSettings: Any? = null,
|
||||||
|
var sockopt: SockoptBean? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class TcpSettingsBean(
|
||||||
|
var header: HeaderBean = HeaderBean(),
|
||||||
|
val acceptProxyProtocol: Boolean? = null
|
||||||
|
) {
|
||||||
|
data class HeaderBean(
|
||||||
|
var type: String = "none",
|
||||||
|
var request: RequestBean? = null,
|
||||||
|
var response: Any? = null
|
||||||
|
) {
|
||||||
|
data class RequestBean(
|
||||||
|
var path: List<String> = ArrayList(),
|
||||||
|
var headers: HeadersBean = HeadersBean(),
|
||||||
|
val version: String? = null,
|
||||||
|
val method: String? = null
|
||||||
|
) {
|
||||||
|
data class HeadersBean(
|
||||||
|
var Host: List<String>? = ArrayList(),
|
||||||
|
@SerializedName("User-Agent")
|
||||||
|
val userAgent: List<String>? = null,
|
||||||
|
@SerializedName("Accept-Encoding")
|
||||||
|
val acceptEncoding: List<String>? = null,
|
||||||
|
val Connection: List<String>? = null,
|
||||||
|
val Pragma: Any? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class KcpSettingsBean(
|
||||||
|
var mtu: Int = 1350,
|
||||||
|
var tti: Int = 50,
|
||||||
|
var uplinkCapacity: Int = 12,
|
||||||
|
var downlinkCapacity: Int = 100,
|
||||||
|
var congestion: Boolean = false,
|
||||||
|
var readBufferSize: Int = 1,
|
||||||
|
var writeBufferSize: Int = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
data class WsSettingsBean(
|
||||||
|
var path: String? = null,
|
||||||
|
var host: String? = null,
|
||||||
|
var headers: Map<String, String>? = null,
|
||||||
|
val maxEarlyData: Int? = null,
|
||||||
|
val useBrowserForwarding: Boolean? = null,
|
||||||
|
val acceptProxyProtocol: Boolean? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class HttpupgradeSettingsBean(
|
||||||
|
var path: String? = null,
|
||||||
|
var host: String? = null,
|
||||||
|
val acceptProxyProtocol: Boolean? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class XhttpSettingsBean(
|
||||||
|
var path: String? = null,
|
||||||
|
var host: String? = null,
|
||||||
|
var mode: String? = null,
|
||||||
|
var extra: Any? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class HttpSettingsBean(
|
||||||
|
var host: List<String> = ArrayList(),
|
||||||
|
var path: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SockoptBean(
|
||||||
|
var TcpNoDelay: Boolean? = null,
|
||||||
|
var tcpKeepAliveIdle: Int? = null,
|
||||||
|
var tcpFastOpen: Boolean? = null,
|
||||||
|
var tproxy: String? = null,
|
||||||
|
var mark: Int? = null,
|
||||||
|
var dialerProxy: String? = null,
|
||||||
|
var domainStrategy: String? = null,
|
||||||
|
var happyEyeballs: HappyEyeballsBean? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class HappyEyeballsBean(
|
||||||
|
var prioritizeIPv6: Boolean? = null,
|
||||||
|
var maxConcurrentTry: Int? = 4,
|
||||||
|
var tryDelayMs: Int? = 250, // ms
|
||||||
|
var interleave: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TlsSettingsBean(
|
||||||
|
var allowInsecure: Boolean = false,
|
||||||
|
var serverName: String? = null,
|
||||||
|
val alpn: List<String>? = null,
|
||||||
|
val minVersion: String? = null,
|
||||||
|
val maxVersion: String? = null,
|
||||||
|
val preferServerCipherSuites: Boolean? = null,
|
||||||
|
val cipherSuites: String? = null,
|
||||||
|
val fingerprint: String? = null,
|
||||||
|
val certificates: List<Any>? = null,
|
||||||
|
val disableSystemRoot: Boolean? = null,
|
||||||
|
val enableSessionResumption: Boolean? = null,
|
||||||
|
var echConfigList: String? = null,
|
||||||
|
var pinnedPeerCertSha256: String? = null,
|
||||||
|
// REALITY settings
|
||||||
|
val show: Boolean = false,
|
||||||
|
var publicKey: String? = null,
|
||||||
|
var shortId: String? = null,
|
||||||
|
var spiderX: String? = null,
|
||||||
|
var mldsa65Verify: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class QuicSettingBean(
|
||||||
|
var security: String = "none",
|
||||||
|
var key: String = "",
|
||||||
|
var header: HeaderBean = HeaderBean()
|
||||||
|
) {
|
||||||
|
data class HeaderBean(var type: String = "none")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class GrpcSettingsBean(
|
||||||
|
var serviceName: String = "",
|
||||||
|
var authority: String? = null,
|
||||||
|
var multiMode: Boolean? = null,
|
||||||
|
var idle_timeout: Int? = null,
|
||||||
|
var health_check_timeout: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class HysteriaSettingsBean(
|
||||||
|
var version: Int,
|
||||||
|
var auth: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
//https://xtls.github.io/config/transport.html#finalmaskobject
|
||||||
|
data class FinalMaskBean(
|
||||||
|
var tcp: List<MaskBean>? = null,
|
||||||
|
var udp: List<MaskBean>? = null,
|
||||||
|
var quicParams: QuicParamsBean? = null
|
||||||
|
) {
|
||||||
|
data class MaskBean(
|
||||||
|
var type: String,
|
||||||
|
var settings: MaskSettingsBean? = null
|
||||||
|
) {
|
||||||
|
data class MaskSettingsBean(
|
||||||
|
val password: String? = null,
|
||||||
|
val domain: String? = null,
|
||||||
|
// fragment
|
||||||
|
val packets: String? = null,
|
||||||
|
val length: String? = null,
|
||||||
|
val delay: String? = null,
|
||||||
|
// val maxSplit: String? = null,
|
||||||
|
// noise
|
||||||
|
val reset: Int? = null,
|
||||||
|
val noise: List<NoiseMaskBean>? = null
|
||||||
|
) {
|
||||||
|
data class NoiseMaskBean(
|
||||||
|
val rand: String? = null,
|
||||||
|
// val randRange: String? = null,
|
||||||
|
// val type: String? = null,
|
||||||
|
// val packet: String? = null,
|
||||||
|
val delay: String? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class QuicParamsBean(
|
||||||
|
var congestion: String? = null,
|
||||||
|
var brutalUp: String? = null,
|
||||||
|
var brutalDown: String? = null,
|
||||||
|
var udpHop: UdpHopBean? = null,
|
||||||
|
) {
|
||||||
|
// Nested data class for the udpHop JSON object
|
||||||
|
data class UdpHopBean(
|
||||||
|
var ports: String? = null,
|
||||||
|
var interval: String? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MuxBean(
|
||||||
|
var enabled: Boolean,
|
||||||
|
var concurrency: Int? = null,
|
||||||
|
var xudpConcurrency: Int? = null,
|
||||||
|
var xudpProxyUDP443: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getServerAddress(): String? {
|
||||||
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||||
|
) {
|
||||||
|
return settings?.vnext?.firstOrNull()?.address ?: settings?.address as? String
|
||||||
|
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|
) {
|
||||||
|
return settings?.servers?.firstOrNull()?.address
|
||||||
|
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
|
return settings?.peers?.firstOrNull()?.endpoint?.substringBeforeLast(":")
|
||||||
|
} else if (protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA.name, true)
|
||||||
|
) {
|
||||||
|
return settings?.address as? String
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getServerPort(): Int? {
|
||||||
|
if (protocol.equals(EConfigType.VMESS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.VLESS.name, true)
|
||||||
|
) {
|
||||||
|
return settings?.vnext?.firstOrNull()?.port ?: settings?.port
|
||||||
|
} else if (protocol.equals(EConfigType.SHADOWSOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.SOCKS.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HTTP.name, true)
|
||||||
|
|| protocol.equals(EConfigType.TROJAN.name, true)
|
||||||
|
) {
|
||||||
|
return settings?.servers?.firstOrNull()?.port
|
||||||
|
} else if (protocol.equals(EConfigType.WIREGUARD.name, true)) {
|
||||||
|
return settings?.peers?.firstOrNull()?.endpoint?.substringAfterLast(":")?.toInt()
|
||||||
|
} else if (protocol.equals(EConfigType.HYSTERIA2.name, true)
|
||||||
|
|| protocol.equals(EConfigType.HYSTERIA.name, true)
|
||||||
|
) {
|
||||||
|
return settings?.port
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ensureSockopt(): StreamSettingsBean.SockoptBean {
|
||||||
|
val stream = streamSettings ?: StreamSettingsBean().also {
|
||||||
|
streamSettings = it
|
||||||
|
}
|
||||||
|
|
||||||
|
val sockopt = stream.sockopt ?: StreamSettingsBean.SockoptBean().also {
|
||||||
|
stream.sockopt = it
|
||||||
|
}
|
||||||
|
|
||||||
|
return sockopt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DnsBean(
|
||||||
|
var servers: ArrayList<Any>? = null,
|
||||||
|
var hosts: Map<String, Any>? = null,
|
||||||
|
val clientIp: String? = null,
|
||||||
|
val disableCache: Boolean? = null,
|
||||||
|
val queryStrategy: String? = null,
|
||||||
|
val enableParallelQuery: Boolean? = null,
|
||||||
|
val tag: String? = null
|
||||||
|
) {
|
||||||
|
data class ServersBean(
|
||||||
|
var address: String = "",
|
||||||
|
var port: Int? = null,
|
||||||
|
var domains: List<String>? = null,
|
||||||
|
var expectIPs: List<String>? = null,
|
||||||
|
val clientIp: String? = null,
|
||||||
|
val skipFallback: Boolean? = null,
|
||||||
|
val tag: String? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RoutingBean(
|
||||||
|
var domainStrategy: String,
|
||||||
|
var domainMatcher: String? = null,
|
||||||
|
var rules: ArrayList<RulesBean>,
|
||||||
|
var balancers: List<BalancerBean>? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class RulesBean(
|
||||||
|
var type: String = "field",
|
||||||
|
var ip: List<String>? = null,
|
||||||
|
var domain: List<String>? = null,
|
||||||
|
var process: List<String>? = null,
|
||||||
|
var outboundTag: String? = null,
|
||||||
|
var balancerTag: String? = null,
|
||||||
|
var port: String? = null,
|
||||||
|
val sourcePort: String? = null,
|
||||||
|
val network: String? = null,
|
||||||
|
val source: List<String>? = null,
|
||||||
|
val user: List<String>? = null,
|
||||||
|
var inboundTag: List<String>? = null,
|
||||||
|
val protocol: List<String>? = null,
|
||||||
|
val attrs: String? = null,
|
||||||
|
val domainMatcher: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BalancerBean(
|
||||||
|
val tag: String,
|
||||||
|
val selector: List<String>,
|
||||||
|
val fallbackTag: String? = null,
|
||||||
|
val strategy: StrategyObject? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StrategyObject(
|
||||||
|
val type: String = "random", // "random" | "roundRobin" | "leastPing" | "leastLoad"
|
||||||
|
val settings: StrategySettingsObject? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StrategySettingsObject(
|
||||||
|
val expected: Int? = null,
|
||||||
|
val maxRTT: String? = null,
|
||||||
|
val tolerance: Double? = null,
|
||||||
|
val baselines: List<String>? = null,
|
||||||
|
val costs: List<CostObject>? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CostObject(
|
||||||
|
val regexp: Boolean = false,
|
||||||
|
val match: String,
|
||||||
|
val value: Double
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PolicyBean(
|
||||||
|
var levels: Map<String, LevelBean>,
|
||||||
|
var system: Any? = null
|
||||||
|
) {
|
||||||
|
data class LevelBean(
|
||||||
|
var handshake: Int? = null,
|
||||||
|
var connIdle: Int? = null,
|
||||||
|
var uplinkOnly: Int? = null,
|
||||||
|
var downlinkOnly: Int? = null,
|
||||||
|
val statsUserUplink: Boolean? = null,
|
||||||
|
val statsUserDownlink: Boolean? = null,
|
||||||
|
var bufferSize: Int? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ObservatoryObject(
|
||||||
|
val subjectSelector: List<String>,
|
||||||
|
val probeUrl: String,
|
||||||
|
val probeInterval: String,
|
||||||
|
val enableConcurrency: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BurstObservatoryObject(
|
||||||
|
val subjectSelector: List<String>,
|
||||||
|
val pingConfig: PingConfigObject
|
||||||
|
) {
|
||||||
|
data class PingConfigObject(
|
||||||
|
val destination: String,
|
||||||
|
val connectivity: String? = null,
|
||||||
|
val interval: String,
|
||||||
|
val sampling: Int,
|
||||||
|
val timeout: String? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FakednsBean(
|
||||||
|
var ipPool: String = "198.18.0.0/15",
|
||||||
|
var poolSize: Int = 10000
|
||||||
|
) // roughly 10 times smaller than total ip pool
|
||||||
|
|
||||||
|
fun getProxyOutbound(): OutboundBean? {
|
||||||
|
outbounds.forEach { outbound ->
|
||||||
|
EConfigType.entries.forEach {
|
||||||
|
if (outbound.protocol.equals(it.name, true)) {
|
||||||
|
return outbound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllProxyOutbound(): List<OutboundBean> {
|
||||||
|
return outbounds.filter { outbound ->
|
||||||
|
EConfigType.entries.any { it.name.equals(outbound.protocol, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.v2ray.ang.dto
|
||||||
|
|
||||||
|
data class VmessQRCode(
|
||||||
|
var v: String = "",
|
||||||
|
var ps: String = "",
|
||||||
|
var add: String = "",
|
||||||
|
var port: String = "",
|
||||||
|
var id: String = "",
|
||||||
|
var aid: String = "0",
|
||||||
|
var scy: String = "",
|
||||||
|
var net: String = "",
|
||||||
|
var type: String = "",
|
||||||
|
var host: String = "",
|
||||||
|
var path: String = "",
|
||||||
|
var tls: String = "",
|
||||||
|
var sni: String = "",
|
||||||
|
var alpn: String = "",
|
||||||
|
var fp: String = "",
|
||||||
|
var insecure: String = ""
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.v2ray.ang.dto.entities
|
||||||
|
|
||||||
|
data class AssetUrlCache(
|
||||||
|
val guid: String,
|
||||||
|
val assetUrl: AssetUrlItem
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.v2ray.ang.dto.entities
|
||||||
|
|
||||||
|
data class AssetUrlItem(
|
||||||
|
var remarks: String = "",
|
||||||
|
var url: String = "",
|
||||||
|
val addedTime: Long = System.currentTimeMillis(),
|
||||||
|
var lastUpdated: Long = -1,
|
||||||
|
var locked: Boolean? = false,
|
||||||
|
)
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package com.v2ray.ang.dto.entities
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
import com.v2ray.ang.enums.EConfigType
|
||||||
|
import com.v2ray.ang.util.Utils
|
||||||
|
|
||||||
|
data class ProfileItem(
|
||||||
|
val configVersion: Int = 4,
|
||||||
|
val configType: EConfigType,
|
||||||
|
var subscriptionId: String = "",
|
||||||
|
var addedTime: Long = System.currentTimeMillis(),
|
||||||
|
|
||||||
|
var remarks: String = "",
|
||||||
|
var description: String? = null,
|
||||||
|
var server: String? = null,
|
||||||
|
var serverPort: String? = null,
|
||||||
|
|
||||||
|
var password: String? = null,
|
||||||
|
var method: String? = null,
|
||||||
|
var flow: String? = null,
|
||||||
|
var username: String? = null,
|
||||||
|
|
||||||
|
var network: String? = null,
|
||||||
|
var headerType: String? = null,
|
||||||
|
var host: String? = null,
|
||||||
|
var path: String? = null,
|
||||||
|
var seed: String? = null,
|
||||||
|
var kcpMtu: Int? = null,
|
||||||
|
var kcpTti: Int? = null,
|
||||||
|
|
||||||
|
var quicSecurity: String? = null,
|
||||||
|
var quicKey: String? = null,
|
||||||
|
var mode: String? = null,
|
||||||
|
var serviceName: String? = null,
|
||||||
|
var authority: String? = null,
|
||||||
|
var xhttpMode: String? = null,
|
||||||
|
var xhttpExtra: String? = null,
|
||||||
|
var finalMask: String? = null,
|
||||||
|
var security: String? = null,
|
||||||
|
var sni: String? = null,
|
||||||
|
var alpn: String? = null,
|
||||||
|
var fingerPrint: String? = null,
|
||||||
|
var insecure: Boolean? = null,
|
||||||
|
var echConfigList: String? = null,
|
||||||
|
var pinnedCA256: String? = null,
|
||||||
|
|
||||||
|
var publicKey: String? = null,
|
||||||
|
var shortId: String? = null,
|
||||||
|
var spiderX: String? = null,
|
||||||
|
var mldsa65Verify: String? = null,
|
||||||
|
|
||||||
|
var secretKey: String? = null,
|
||||||
|
var preSharedKey: String? = null,
|
||||||
|
var localAddress: String? = null,
|
||||||
|
var reserved: String? = null,
|
||||||
|
var mtu: Int? = null,
|
||||||
|
|
||||||
|
var obfsPassword: String? = null,
|
||||||
|
var portHopping: String? = null,
|
||||||
|
var portHoppingInterval: String? = null,
|
||||||
|
var pinSHA256: String? = null,
|
||||||
|
var bandwidthDown: String? = null,
|
||||||
|
var bandwidthUp: String? = null,
|
||||||
|
|
||||||
|
var policyGroupType: String? = null,
|
||||||
|
var policyGroupSubscriptionId: String? = null,
|
||||||
|
var policyGroupFilter: String? = null,
|
||||||
|
var proxyChainProfiles: String? = null,
|
||||||
|
|
||||||
|
var browserDialerMode: String? = null,
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun create(configType: EConfigType): ProfileItem {
|
||||||
|
return ProfileItem(configType = configType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getServerAddressAndPort(): String {
|
||||||
|
if (server.isNullOrEmpty() && configType == EConfigType.CUSTOM) {
|
||||||
|
return "${AppConfig.LOOPBACK}:${AppConfig.PORT_SOCKS}"
|
||||||
|
}
|
||||||
|
return Utils.getIpv6Address(server) + ":" + serverPort
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other == null) return false
|
||||||
|
val obj = other as ProfileItem
|
||||||
|
|
||||||
|
return (this.server == obj.server
|
||||||
|
&& this.serverPort == obj.serverPort
|
||||||
|
&& this.password == obj.password
|
||||||
|
&& this.method == obj.method
|
||||||
|
&& this.flow == obj.flow
|
||||||
|
&& this.username == obj.username
|
||||||
|
|
||||||
|
&& this.network == obj.network
|
||||||
|
&& this.headerType == obj.headerType
|
||||||
|
&& this.host == obj.host
|
||||||
|
&& this.path == obj.path
|
||||||
|
&& this.seed == obj.seed
|
||||||
|
&& this.kcpMtu == obj.kcpMtu
|
||||||
|
&& this.kcpTti == obj.kcpTti
|
||||||
|
&& this.quicSecurity == obj.quicSecurity
|
||||||
|
&& this.quicKey == obj.quicKey
|
||||||
|
&& this.mode == obj.mode
|
||||||
|
&& this.serviceName == obj.serviceName
|
||||||
|
&& this.authority == obj.authority
|
||||||
|
&& this.xhttpMode == obj.xhttpMode
|
||||||
|
|
||||||
|
&& this.security == obj.security
|
||||||
|
&& this.sni == obj.sni
|
||||||
|
&& this.alpn == obj.alpn
|
||||||
|
&& this.fingerPrint == obj.fingerPrint
|
||||||
|
&& this.publicKey == obj.publicKey
|
||||||
|
&& this.shortId == obj.shortId
|
||||||
|
|
||||||
|
&& this.secretKey == obj.secretKey
|
||||||
|
&& this.localAddress == obj.localAddress
|
||||||
|
&& this.reserved == obj.reserved
|
||||||
|
&& this.mtu == obj.mtu
|
||||||
|
|
||||||
|
&& this.obfsPassword == obj.obfsPassword
|
||||||
|
&& this.portHopping == obj.portHopping
|
||||||
|
&& this.portHoppingInterval == obj.portHoppingInterval
|
||||||
|
&& this.pinnedCA256 == obj.pinnedCA256
|
||||||
|
&& this.proxyChainProfiles == obj.proxyChainProfiles
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.v2ray.ang.dto.entities
|
||||||
|
|
||||||
|
data class RulesetItem(
|
||||||
|
var remarks: String? = "",
|
||||||
|
var ip: List<String>? = null,
|
||||||
|
var domain: List<String>? = null,
|
||||||
|
var process: List<String>? = null,
|
||||||
|
var outboundTag: String = "",
|
||||||
|
var port: String? = null,
|
||||||
|
var network: String? = null,
|
||||||
|
var protocol: List<String>? = null,
|
||||||
|
var enabled: Boolean = true,
|
||||||
|
var locked: Boolean? = false,
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.v2ray.ang.dto.entities
|
||||||
|
|
||||||
|
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
||||||
|
fun getTestDelayString(): String {
|
||||||
|
if (testDelayMillis == 0L) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return testDelayMillis.toString() + "ms"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.v2ray.ang.dto.entities
|
||||||
|
|
||||||
|
data class ServersCache(
|
||||||
|
val guid: String,
|
||||||
|
val profile: ProfileItem
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.v2ray.ang.dto.entities
|
||||||
|
|
||||||
|
data class SubscriptionCache(
|
||||||
|
val guid: String,
|
||||||
|
val subscription: SubscriptionItem
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.v2ray.ang.dto.entities
|
||||||
|
|
||||||
|
data class SubscriptionItem(
|
||||||
|
var remarks: String = "",
|
||||||
|
var url: String = "",
|
||||||
|
var enabled: Boolean = true,
|
||||||
|
val addedTime: Long = System.currentTimeMillis(),
|
||||||
|
var lastUpdated: Long = -1,
|
||||||
|
var autoUpdate: Boolean = false,
|
||||||
|
var updateInterval: Long = 1440, // in minutes, default to 24 hours
|
||||||
|
var prevProfile: String? = null,
|
||||||
|
var nextProfile: String? = null,
|
||||||
|
var filter: String? = null,
|
||||||
|
var allowInsecureUrl: Boolean = false,
|
||||||
|
var userAgent: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.v2ray.ang.dto.entities
|
||||||
|
|
||||||
|
data class WebDavConfig(
|
||||||
|
val baseUrl: String,
|
||||||
|
val username: String? = null,
|
||||||
|
val password: String? = null,
|
||||||
|
val remoteBasePath: String = "/",
|
||||||
|
val timeoutSeconds: Long = 30
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.v2ray.ang.enums
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load-balancing strategy types supported by xray balancer.
|
||||||
|
*
|
||||||
|
* @param policyGroupType The string value written into the xray config JSON.
|
||||||
|
* @param policyGroupTypeValue The numeric string stored in [com.v2ray.ang.dto.entities.ProfileItem.policyGroupType].
|
||||||
|
* @param requiresBurstObservatory Whether this strategy needs a burstObservatory (leastLoad).
|
||||||
|
* @param requiresObservatory Whether this strategy needs an observatory (leastPing).
|
||||||
|
*/
|
||||||
|
enum class BalancerStrategyType(
|
||||||
|
val policyGroupType: String,
|
||||||
|
val policyGroupTypeValue: String,
|
||||||
|
val requiresBurstObservatory: Boolean = false,
|
||||||
|
val requiresObservatory: Boolean = false,
|
||||||
|
) {
|
||||||
|
LEAST_LOAD("leastLoad", "1", requiresBurstObservatory = true),
|
||||||
|
RANDOM("random", "2"),
|
||||||
|
ROUND_ROBIN("roundRobin", "3"),
|
||||||
|
LEAST_PING("leastPing", "", requiresObservatory = true); // default / else
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(policyGroupType: String?): BalancerStrategyType =
|
||||||
|
entries.firstOrNull { it.policyGroupTypeValue == policyGroupType } ?: LEAST_PING
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.v2ray.ang.enums
|
||||||
|
|
||||||
|
/** Runtime type used during config assembly only. */
|
||||||
|
enum class CoreResolvedType {
|
||||||
|
NORMAL,
|
||||||
|
POLICYGROUP,
|
||||||
|
PROXYCHAIN,
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.v2ray.ang.enums
|
||||||
|
|
||||||
|
import com.v2ray.ang.AppConfig
|
||||||
|
|
||||||
|
enum class EConfigType(val value: Int, val protocolScheme: String) {
|
||||||
|
VMESS(1, AppConfig.VMESS),
|
||||||
|
CUSTOM(2, AppConfig.CUSTOM),
|
||||||
|
SHADOWSOCKS(3, AppConfig.SHADOWSOCKS),
|
||||||
|
SOCKS(4, AppConfig.SOCKS),
|
||||||
|
VLESS(5, AppConfig.VLESS),
|
||||||
|
TROJAN(6, AppConfig.TROJAN),
|
||||||
|
WIREGUARD(7, AppConfig.WIREGUARD),
|
||||||
|
|
||||||
|
// TUIC(8, AppConfig.TUIC),
|
||||||
|
HYSTERIA2(9, AppConfig.HYSTERIA2),
|
||||||
|
HYSTERIA(900, AppConfig.HYSTERIA),
|
||||||
|
HTTP(10, AppConfig.HTTP),
|
||||||
|
POLICYGROUP(101, AppConfig.CUSTOM),
|
||||||
|
PROXYCHAIN(102, AppConfig.CUSTOM);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromInt(value: Int) = entries.firstOrNull { it.value == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.v2ray.ang.enums
|
||||||
|
|
||||||
|
enum class Language(val code: String) {
|
||||||
|
AUTO("auto"),
|
||||||
|
ENGLISH("en"),
|
||||||
|
CHINA("zh-rCN"),
|
||||||
|
TRADITIONAL_CHINESE("zh-rTW"),
|
||||||
|
VIETNAMESE("vi"),
|
||||||
|
RUSSIAN("ru"),
|
||||||
|
PERSIAN("fa"),
|
||||||
|
ARABIC("ar"),
|
||||||
|
BANGLA("bn"),
|
||||||
|
BAKHTIARI("bqi-rIR");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromCode(code: String): Language {
|
||||||
|
return entries.find { it.code == code } ?: AUTO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.v2ray.ang.enums
|
||||||
|
|
||||||
|
enum class NetworkType(val type: String) {
|
||||||
|
TCP("tcp"),
|
||||||
|
KCP("kcp"),
|
||||||
|
WS("ws"),
|
||||||
|
HTTP_UPGRADE("httpupgrade"),
|
||||||
|
XHTTP("xhttp"),
|
||||||
|
HTTP("http"),
|
||||||
|
H2("h2"),
|
||||||
|
|
||||||
|
//QUIC("quic"),
|
||||||
|
GRPC("grpc"),
|
||||||
|
HYSTERIA("hysteria");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(type: String?) = entries.find { it.type == type } ?: TCP
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.v2ray.ang.enums
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum defining different notification channels.
|
||||||
|
* Each channel has a unique channelId, notificationId, and display name.
|
||||||
|
*/
|
||||||
|
enum class NotificationChannelType(
|
||||||
|
val channelId: String,
|
||||||
|
val channelName: String,
|
||||||
|
val notificationId: Int
|
||||||
|
) {
|
||||||
|
SUBSCRIPTION_UPDATE(
|
||||||
|
channelId = "subscription_update_channel",
|
||||||
|
channelName = "Subscription Update Service",
|
||||||
|
notificationId = 13
|
||||||
|
),
|
||||||
|
CORE_TEST(
|
||||||
|
channelId = "core_test_channel",
|
||||||
|
channelName = "Core Test Service",
|
||||||
|
notificationId = 12
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.v2ray.ang.enums
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission types used in the app, handling API level differences.
|
||||||
|
*/
|
||||||
|
enum class PermissionType {
|
||||||
|
/** Camera permission (used for scanning QR codes) */
|
||||||
|
CAMERA {
|
||||||
|
override fun getPermission(): String = Manifest.permission.CAMERA
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Notification permission (Android 13+) */
|
||||||
|
POST_NOTIFICATIONS {
|
||||||
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
|
override fun getPermission(): String = Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Return the actual Android permission string */
|
||||||
|
abstract fun getPermission(): String
|
||||||
|
|
||||||
|
/** Return a human-readable label for the permission */
|
||||||
|
fun getLabel(): String {
|
||||||
|
return when (this) {
|
||||||
|
CAMERA -> "Camera"
|
||||||
|
POST_NOTIFICATIONS -> "Notification"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.v2ray.ang.enums
|
||||||
|
|
||||||
|
enum class RoutingType(val fileName: String) {
|
||||||
|
WHITE("custom_routing_white"),
|
||||||
|
BLACK("custom_routing_black"),
|
||||||
|
GLOBAL("custom_routing_global"),
|
||||||
|
WHITE_IRAN("custom_routing_white_iran"),
|
||||||
|
WHITE_RUSSIA("custom_routing_white_russia");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromIndex(index: Int): RoutingType {
|
||||||
|
return when (index) {
|
||||||
|
0 -> WHITE
|
||||||
|
1 -> BLACK
|
||||||
|
2 -> GLOBAL
|
||||||
|
3 -> WHITE_IRAN
|
||||||
|
4 -> WHITE_RUSSIA
|
||||||
|
else -> WHITE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.v2ray.ang.enums
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VPN interface address configuration enum class
|
||||||
|
* Defines predefined IPv4 and IPv6 address pairs for VPN TUN interface configuration.
|
||||||
|
* Each option provides client and router addresses to establish point-to-point VPN tunnels.
|
||||||
|
*/
|
||||||
|
enum class VpnInterfaceAddressConfig(
|
||||||
|
val displayName: String,
|
||||||
|
val ipv4Client: String,
|
||||||
|
val ipv4Router: String,
|
||||||
|
val ipv6Client: String,
|
||||||
|
val ipv6Router: String
|
||||||
|
) {
|
||||||
|
OPTION_1("10.10.14.x", "10.10.14.1", "10.10.14.2", "fc00::10:10:14:1", "fc00::10:10:14:2"),
|
||||||
|
OPTION_2("10.1.0.x", "10.1.0.1", "10.1.0.2", "fc00::10:1:0:1", "fc00::10:1:0:2"),
|
||||||
|
OPTION_3("10.0.0.x", "10.0.0.1", "10.0.0.2", "fc00::10:0:0:1", "fc00::10:0:0:2"),
|
||||||
|
OPTION_4("172.31.0.x", "172.31.0.1", "172.31.0.2", "fc00::172:31:0:1", "fc00::172:31:0:2"),
|
||||||
|
OPTION_5("172.20.0.x", "172.20.0.1", "172.20.0.2", "fc00::172:20:0:1", "fc00::172:20:0:2"),
|
||||||
|
OPTION_6("172.16.0.x", "172.16.0.1", "172.16.0.2", "fc00::172:16:0:1", "fc00::172:16:0:2"),
|
||||||
|
OPTION_7("192.168.100.x", "192.168.100.1", "192.168.100.2", "fc00::192:168:100:1", "fc00::192:168:100:2");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Retrieves the VPN interface address configuration based on the specified index.
|
||||||
|
*
|
||||||
|
* @param index The configuration index (0-based) corresponding to user selection
|
||||||
|
* @return The VpnInterfaceAddressConfig instance at the specified index,
|
||||||
|
* or OPTION_1 (default) if the index is out of bounds
|
||||||
|
*/
|
||||||
|
fun getConfigByIndex(index: Int): VpnInterfaceAddressConfig {
|
||||||
|
return if (index in entries.toTypedArray().indices) {
|
||||||
|
entries[index]
|
||||||
|
} else {
|
||||||
|
OPTION_1 // Default to the first configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package com.v2ray.ang.extension
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.v2ray.ang.AngApplication
|
||||||
|
import com.v2ray.ang.enums.EConfigType
|
||||||
|
import es.dmoral.toasty.Toasty
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.net.URI
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
val Context.v2RayApplication: AngApplication?
|
||||||
|
get() = applicationContext as? AngApplication
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given resource ID.
|
||||||
|
*
|
||||||
|
* @param message The resource ID of the message to show.
|
||||||
|
*/
|
||||||
|
fun Context.toast(message: Int) {
|
||||||
|
Toasty.normal(this, message).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given text.
|
||||||
|
*
|
||||||
|
* @param message The text of the message to show.
|
||||||
|
*/
|
||||||
|
fun Context.toast(message: CharSequence) {
|
||||||
|
Toasty.normal(this, message).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given resource ID.
|
||||||
|
*
|
||||||
|
* @param message The resource ID of the message to show.
|
||||||
|
*/
|
||||||
|
fun Context.toastSuccess(message: Int) {
|
||||||
|
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given text.
|
||||||
|
*
|
||||||
|
* @param message The text of the message to show.
|
||||||
|
*/
|
||||||
|
fun Context.toastSuccess(message: CharSequence) {
|
||||||
|
Toasty.success(this, message, Toast.LENGTH_SHORT, true).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given resource ID.
|
||||||
|
*
|
||||||
|
* @param message The resource ID of the message to show.
|
||||||
|
*/
|
||||||
|
fun Context.toastError(message: Int) {
|
||||||
|
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast message with the given text.
|
||||||
|
*
|
||||||
|
* @param message The text of the message to show.
|
||||||
|
*/
|
||||||
|
fun Context.toastError(message: CharSequence) {
|
||||||
|
Toasty.error(this, message, Toast.LENGTH_SHORT, true).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const val THRESHOLD = 1000L
|
||||||
|
const val DIVISOR = 1024.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Long value to a speed string.
|
||||||
|
*
|
||||||
|
* @return The speed string.
|
||||||
|
*/
|
||||||
|
fun Long.toSpeedString(): String = this.toTrafficString() + "/s"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Long value to a traffic string.
|
||||||
|
*
|
||||||
|
* @return The traffic string.
|
||||||
|
*/
|
||||||
|
fun Long.toTrafficString(): String {
|
||||||
|
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB")
|
||||||
|
var size = this.toDouble()
|
||||||
|
var unitIndex = 0
|
||||||
|
while (size >= THRESHOLD && unitIndex < units.size - 1) {
|
||||||
|
size /= DIVISOR
|
||||||
|
unitIndex++
|
||||||
|
}
|
||||||
|
return String.format(Locale.getDefault(), "%.1f %s", size, units[unitIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
val URI.idnHost: String
|
||||||
|
get() = host?.replace("[", "")?.replace("]", "").orEmpty()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all whitespace from the string.
|
||||||
|
*
|
||||||
|
* @return The string without whitespace.
|
||||||
|
*/
|
||||||
|
fun String?.removeWhiteSpace(): String? = this?.replace(" ", "")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns null if the string is null or blank, otherwise returns the string itself.
|
||||||
|
*
|
||||||
|
* @return The string or null.
|
||||||
|
*/
|
||||||
|
fun String?.nullIfBlank(): String? = this?.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the string to a Long value, or returns 0 if the conversion fails.
|
||||||
|
*
|
||||||
|
* @return The Long value.
|
||||||
|
*/
|
||||||
|
fun String.toLongEx(): Long = toLongOrNull() ?: 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for package changes and executes a callback when a change occurs.
|
||||||
|
*
|
||||||
|
* @param onetime Whether to unregister the receiver after the first callback.
|
||||||
|
* @param callback The callback to execute when a package change occurs.
|
||||||
|
* @return The BroadcastReceiver that was registered.
|
||||||
|
*/
|
||||||
|
fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit) =
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
callback()
|
||||||
|
if (onetime) context.unregisterReceiver(this)
|
||||||
|
}
|
||||||
|
}.apply {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
registerReceiver(this, IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
|
addDataScheme("package")
|
||||||
|
}, Context.RECEIVER_EXPORTED)
|
||||||
|
} else {
|
||||||
|
registerReceiver(this, IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
|
addDataScheme("package")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a serializable object from the Bundle.
|
||||||
|
*
|
||||||
|
* @param key The key of the serializable object.
|
||||||
|
* @return The serializable object, or null if not found.
|
||||||
|
*/
|
||||||
|
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
|
||||||
|
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a serializable object from the Intent.
|
||||||
|
*
|
||||||
|
* @param key The key of the serializable object.
|
||||||
|
* @return The serializable object, or null if not found.
|
||||||
|
*/
|
||||||
|
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
|
||||||
|
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the CharSequence is not null and not empty.
|
||||||
|
*
|
||||||
|
* @return True if the CharSequence is not null and not empty, false otherwise.
|
||||||
|
*/
|
||||||
|
fun CharSequence?.isNotNullEmpty(): Boolean = !this.isNullOrBlank()
|
||||||
|
|
||||||
|
fun String.concatUrl(vararg paths: String): String {
|
||||||
|
val builder = StringBuilder(this.trimEnd('/'))
|
||||||
|
|
||||||
|
paths.forEach { path ->
|
||||||
|
val trimmedPath = path.trim('/')
|
||||||
|
if (trimmedPath.isNotEmpty()) {
|
||||||
|
builder.append('/').append(trimmedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to match text either by Regex or literal string.
|
||||||
|
*/
|
||||||
|
fun String.matchesPattern(regex: Regex?, keyword: String?, ignoreCase: Boolean = true): Boolean {
|
||||||
|
if (keyword.isNullOrEmpty()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return regex?.containsMatchIn(this)
|
||||||
|
?: this.contains(keyword, ignoreCase = ignoreCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the config type is a group type (PolicyGroup or ProxyChain).
|
||||||
|
*
|
||||||
|
* @return True if the config type is PolicyGroup or ProxyChain, false otherwise.
|
||||||
|
*/
|
||||||
|
fun EConfigType.isGroupType(): Boolean {
|
||||||
|
return this == EConfigType.POLICYGROUP || this == EConfigType.PROXYCHAIN
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the config type is a complex type (Custom, PolicyGroup, or ProxyChain).
|
||||||
|
*
|
||||||
|
* @return True if the config type is Custom, PolicyGroup, or ProxyChain, false otherwise.
|
||||||
|
*/
|
||||||
|
fun EConfigType.isComplexType(): Boolean {
|
||||||
|
return this == EConfigType.CUSTOM || this == EConfigType.POLICYGROUP || this == EConfigType.PROXYCHAIN
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user