NetworkDetection.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. <?php
  2. namespace App\Utils;
  3. use Exception;
  4. use Http;
  5. use Illuminate\Http\Client\PendingRequest;
  6. use Log;
  7. class NetworkDetection
  8. {
  9. private const PROTOCOLS = ['icmp', 'tcp'];
  10. private const STATUS_OK = 1; // 正常
  11. private const STATUS_ABROAD = 2; // 国外访问异常
  12. private const STATUS_BLOCKED = 3; // 被墙
  13. private const STATUS_DOWN = 4; // 宕机
  14. private static array $apis = ['selfHost', 'vps234', 'idcoffer', 'ip112', 'upx8', 'rss', 'vps1352'];
  15. private static ?PendingRequest $basicRequest;
  16. public static function networkStatus(string $ip, int $port = 22, ?string $source = null): ?array
  17. {
  18. $status = self::checkNetworkStatus($ip, $port, $source);
  19. if (! $status) {
  20. return null;
  21. }
  22. $result = [];
  23. foreach (self::PROTOCOLS as $protocol) {
  24. [$in, $out] = [$status['in'][$protocol], $status['out'][$protocol]];
  25. $result[$protocol] = match (true) {
  26. $in && $out => self::STATUS_OK,
  27. $in && ! $out => self::STATUS_ABROAD,
  28. ! $in && $out => self::STATUS_BLOCKED,
  29. default => self::STATUS_DOWN,
  30. };
  31. }
  32. return $result;
  33. }
  34. private static function checkNetworkStatus(string $ip, int $port, ?string $source = null): ?array
  35. { // 通过众多API进行节点阻断检测.
  36. self::$basicRequest = Http::timeout(5)->retry(2)->withOptions(['http_errors' => false])->withoutVerifying()->withUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36');
  37. foreach ($source ? [$source] : self::$apis as $api) {
  38. if (! method_exists(self::class, $api)) {
  39. continue;
  40. }
  41. try {
  42. $result = self::$api($ip, $port);
  43. if ($result !== null) {
  44. return $result;
  45. }
  46. } catch (Exception $e) {
  47. Log::error("[$api] 网络阻断测试报错: ".$e->getMessage());
  48. }
  49. }
  50. return null;
  51. }
  52. private static function toolsdaquan(string $ip, int $port): ?array
  53. { // deprecated, 开发依据: https://www.toolsdaquan.com/ipcheck/
  54. $data = self::fetchJson(static fn () => self::$basicRequest->withHeader('Referer', 'https://www.toolsdaquan.com/ipcheck/')
  55. ->get("https://www.toolsdaquan.com/toolapi/public/ipchecking?ip=$ip&port=$port"), $ip, 'toolsdaquan');
  56. if (! $data || $data['success'] !== 1) {
  57. return null;
  58. }
  59. $data = $data['data'];
  60. return [
  61. 'in' => ['icmp' => $data['icmp'] === 'success', 'tcp' => $data['tcp'] === 'success'],
  62. 'out' => ['icmp' => $data['outside_icmp'] === 'success', 'tcp' => $data['outside_tcp'] === 'success'],
  63. ];
  64. }
  65. private static function fetchJson(callable $callback, string $ip, string $apiName): ?array
  66. {
  67. try {
  68. $res = $callback();
  69. if ($res->ok()) {
  70. return $res->json();
  71. }
  72. } catch (Exception $e) {
  73. Log::warning("【阻断检测】检测{$ip}时, [$apiName]接口异常: ".$e->getMessage());
  74. }
  75. return null;
  76. }
  77. private static function gd(string $ip, int $port): ?array
  78. { // deprecated, 开发依据: https://ping.gd/
  79. $data = self::fetchJson(static fn () => self::$basicRequest->get("https://ping.gd/api/ip-test/$ip:$port"), $ip, 'gd');
  80. if (! $data) {
  81. return null;
  82. }
  83. return [
  84. 'in' => [
  85. 'icmp' => $data[0]['result']['ping_alive'],
  86. 'tcp' => $data[0]['result']['telnet_alive'],
  87. ],
  88. 'out' => [
  89. 'icmp' => $data[1]['result']['ping_alive'],
  90. 'tcp' => $data[1]['result']['telnet_alive'],
  91. ],
  92. ];
  93. }
  94. private static function vps234(string $ip, int $port): ?array
  95. { // 开发依据: https://www.vps234.com/ipchecker/
  96. $data = self::fetchJson(static fn () => self::$basicRequest
  97. ->withHeaders(['Origin' => 'https://www.vps234.com', 'Referer' => 'https://www.vps234.com/ipchecker/'])
  98. ->asForm()->post('https://www.vps234.com/ipcheck/getdata/', ['ip' => $ip]), $ip, 'vps234');
  99. if (! $data || $data['error'] || ! ($data['data']['success'] ?? false)) {
  100. return null;
  101. }
  102. $r = $data['data']['data'];
  103. return [
  104. 'in' => ['icmp' => $r['innerICMP'], 'tcp' => $r['innerTCP']],
  105. 'out' => ['icmp' => $r['outICMP'], 'tcp' => $r['outTCP']],
  106. ];
  107. }
  108. private static function flyzy2005(string $ip, int $port): ?array
  109. { // deprecated, 开发依据: https://www.flyzy2005.cn/tech/ip-check/
  110. $inner = self::fetchJson(static fn () => self::$basicRequest->get("https://mini.flyzy2005.cn/ip_check.php?ip=$ip&port=$port"), $ip, 'flyzy2005');
  111. $outer = self::fetchJson(static fn () => self::$basicRequest->get("https://mini.flyzy2005.cn/ip_check_outside.php?ip=$ip&port=$port"), $ip, 'flyzy2005');
  112. if (! $inner || ! $outer) {
  113. return null;
  114. }
  115. return [
  116. 'in' => ['icmp' => $inner['icmp'] === 'success', 'tcp' => $inner['tcp'] === 'success'],
  117. 'out' => ['icmp' => $outer['outside_icmp'] === 'success', 'tcp' => $outer['outside_tcp'] === 'success'],
  118. ];
  119. }
  120. private static function idcoffer(string $ip, int $port): ?array
  121. { // 开发依据: https://www.idcoffer.com/ipcheck
  122. $inner = self::fetchJson(static fn () => self::$basicRequest->get("https://api.24kplus.com/ipcheck?host=$ip&port=$port"), $ip, 'idcoffer');
  123. $outer = self::fetchJson(static fn () => self::$basicRequest->get("https://api.idcoffer.com/ipcheck?host=$ip&port=$port"), $ip, 'idcoffer');
  124. if (! $inner || ! $outer || ! $inner['code'] || ! $outer['code']) {
  125. return null;
  126. }
  127. return [
  128. 'in' => ['icmp' => $inner['data']['ping'], 'tcp' => $inner['data']['tcp']],
  129. 'out' => ['icmp' => $outer['data']['ping'], 'tcp' => $outer['data']['tcp']],
  130. ];
  131. }
  132. private static function ip112(string $ip, int $port): ?array
  133. { // 开发依据: https://ip112.cn/
  134. $inner = self::fetchJson(static fn () => self::$basicRequest->asForm()->post('https://api.ycwxgzs.com/ipcheck/index.php', ['ip' => $ip, 'port' => $port]), $ip, 'ip112');
  135. $outer = self::fetchJson(static fn () => self::$basicRequest->asForm()->post('https://api.52bwg.com/ipcheck/ipcheck.php', ['ip' => $ip, 'port' => $port]), $ip, 'ip112');
  136. if (! $inner || ! $outer) {
  137. return null;
  138. }
  139. return [
  140. 'in' => ['icmp' => str_contains($inner['icmp'], 'green'), 'tcp' => str_contains($inner['tcp'], 'green')],
  141. 'out' => ['icmp' => str_contains($outer['icmp'], 'green'), 'tcp' => str_contains($outer['tcp'], 'green')],
  142. ];
  143. }
  144. private static function upx8(string $ip, int $port): ?array
  145. { // 开发依据: https://blog.upx8.com/ipcha.html
  146. $inner = self::fetchJson(static fn () => self::$basicRequest->asForm()->post('https://api.sm171.com/check-cn.php', ['ip' => $ip, 'port' => $port]), $ip, 'upx8');
  147. $outer = self::fetchJson(static fn () => self::$basicRequest->asForm()->post('https://ip.upx8.com/api/check-us.php', ['ip' => $ip, 'port' => $port]), $ip, 'upx8');
  148. if (! $inner || ! $outer) {
  149. return null;
  150. }
  151. return [
  152. 'in' => ['icmp' => $inner['icmp'] === '正常', 'tcp' => $inner['tcp'] === '正常'],
  153. 'out' => ['icmp' => $outer['icmp'] === '正常', 'tcp' => $outer['tcp'] === '正常'],
  154. ];
  155. }
  156. private static function vps1352(string $ip, int $port): ?array
  157. { // 开发依据: https://www.51vps.info/ipcheck.html https://www.vps1352.com/ipcheck.html 有缺陷api,查不了海外做判断 备用
  158. try {
  159. $response = self::$basicRequest->asForm()->withHeader('Referer', 'https://www.51vps.info')
  160. ->post('https://www.vps1352.com/check.php', ['ip' => $ip, 'port' => $port]);
  161. if ($response->ok()) {
  162. // 检查响应内容是否包含PHP错误信息
  163. $body = $response->body();
  164. if (str_contains($body, 'Warning') || str_contains($body, 'Fatal error')) {
  165. // 如果响应中包含PHP错误信息,则提取JSON部分
  166. $jsonStart = strpos($body, '{');
  167. $jsonEnd = strrpos($body, '}');
  168. if ($jsonStart !== false && $jsonEnd !== false) {
  169. $jsonStr = substr($body, $jsonStart, $jsonEnd - $jsonStart + 1);
  170. $data = json_decode($jsonStr, true, 512, JSON_THROW_ON_ERROR);
  171. } else {
  172. Log::warning("【阻断检测】检测{$ip}时,[vps1352]接口返回内容包含错误信息: ".$body);
  173. return null;
  174. }
  175. } else {
  176. $data = $response->json();
  177. }
  178. if (! empty($data)) {
  179. return [
  180. 'in' => ['icmp' => $data['icmp'] === '开放', 'tcp' => $data['tcp'] === '开放'],
  181. 'out' => ['icmp' => true, 'tcp' => true],
  182. ];
  183. }
  184. }
  185. } catch (Exception $e) {
  186. Log::warning("【阻断检测】检测{$ip}时,[vps1352]接口异常: ".$e->getMessage());
  187. }
  188. return null;
  189. }
  190. private static function rss(string $ip, int $port): ?array
  191. { // 开发依据: https://ip.rss.ink/index/check
  192. $inner = self::fetchJson(static fn () => self::$basicRequest->get("https://ip.rss.ink/api/scan?ip=$ip&port=$port"), $ip, 'rss');
  193. $outer = self::fetchJson(static fn () => self::$basicRequest->get("https://tcp.mk/api/scan?ip=$ip&port=$port"), $ip, 'rss');
  194. if (! $inner || ! $outer || $inner['code'] !== 200 || $outer['code'] !== 200) {
  195. return null;
  196. }
  197. return [
  198. 'in' => ['icmp' => $inner['msg'] === 'Ok', 'tcp' => $inner['msg'] === 'Ok'],
  199. 'out' => ['icmp' => $outer['msg'] === 'Ok', 'tcp' => $outer['msg'] === 'Ok'],
  200. ];
  201. }
  202. private static function selfHost(string $ip, int $port): ?array
  203. { // 开发依据: https://github.com/ProxyPanel/PingAgent
  204. // 从环境变量获取探针服务器配置
  205. $domestic = config('services.probe.domestic');
  206. $foreign = config('services.probe.foreign');
  207. if (empty($domestic)) {
  208. return null;
  209. }
  210. $probe = static fn ($entry) => (static function ($entry) use ($ip, $port) {
  211. [$url,$token] = array_pad(explode('|', $entry), 2, null);
  212. $req = self::$basicRequest;
  213. if ($token) {
  214. $req = $req->withToken($token);
  215. }
  216. $res = $req->asJson()->post("$url/probe", ['target' => $ip, 'port' => $port]);
  217. $d = $res->ok() && ! empty($res->json()[0]) ? $res->json()[0] : [];
  218. return ['icmp' => ! empty($d['icmp']), 'tcp' => ! empty($d['tcp'])];
  219. })($entry);
  220. return ['in' => $probe($domestic), 'out' => empty($foreign) ? ['icmp' => true, 'tcp' => true] : $probe($foreign)];
  221. }
  222. }