AutoCheckNodeTCP.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. <?php
  2. namespace App\Console\Commands;
  3. use App\Components\Helpers;
  4. use App\Components\ServerChan;
  5. use App\Http\Models\SsNode;
  6. use App\Mail\nodeCrashWarning;
  7. use Cache;
  8. use Exception;
  9. use GuzzleHttp\Exception\GuzzleException;
  10. use Illuminate\Console\Command;
  11. use Log;
  12. use Mail;
  13. class AutoCheckNodeTCP extends Command
  14. {
  15. protected $signature = 'autoCheckNodeTCP';
  16. protected $description = '自动检测节点是否被TCP阻断';
  17. protected static $systemConfig;
  18. public function __construct()
  19. {
  20. parent::__construct();
  21. self::$systemConfig = Helpers::systemConfig();
  22. }
  23. public function handle()
  24. {
  25. $jobStartTime = microtime(TRUE);
  26. if(self::$systemConfig['is_tcp_check']){
  27. if(!Cache::has('tcp_check_time')){
  28. $this->checkNodes();
  29. }elseif(Cache::get('tcp_check_time') <= time()){
  30. $this->checkNodes();
  31. }else{
  32. Log::info('下次节点TCP阻断检测时间:'.date('Y-m-d H:i:s', Cache::get('tcp_check_time')));
  33. }
  34. }
  35. $jobEndTime = microtime(TRUE);
  36. $jobUsedTime = round(($jobEndTime-$jobStartTime), 4);
  37. Log::info('执行定时任务【'.$this->description.'】,耗时'.$jobUsedTime.'秒');
  38. }
  39. // 监测节点状态
  40. private function checkNodes()
  41. {
  42. $title = "节点异常警告";
  43. $nodeList = SsNode::query()->where('is_transit', 0)->where('is_nat', 0)->where('status', 1)->where('is_tcp_check', 1)->get();
  44. foreach($nodeList as $node){
  45. $tcpCheck = $this->tcpCheck($node->ip);
  46. if(FALSE !== $tcpCheck){
  47. switch($tcpCheck){
  48. case 1:
  49. $text = '服务器宕机';
  50. break;
  51. case 2:
  52. $text = '海外不通';
  53. break;
  54. case 3:
  55. $text = 'TCP阻断';
  56. break;
  57. case 0:
  58. default:
  59. $text = '正常';
  60. }
  61. // 异常才发通知消息
  62. if($tcpCheck){
  63. if(self::$systemConfig['tcp_check_warning_times']){
  64. // 已通知次数
  65. $cacheKey = 'tcp_check_warning_times_'.$node->id;
  66. if(Cache::has($cacheKey)){
  67. $times = Cache::get($cacheKey);
  68. }else{
  69. Cache::put($cacheKey, 1, 725); // 最多设置提醒12次,12*60=720分钟缓存时效,多5分钟防止异常
  70. $times = 1;
  71. }
  72. if($times < self::$systemConfig['tcp_check_warning_times']){
  73. Cache::increment($cacheKey);
  74. $this->notifyMaster($title, "节点**{$node->name}【{$node->ip}】**:**".$text."**", $node->name, $node->server);
  75. }elseif($times >= self::$systemConfig['tcp_check_warning_times']){
  76. Cache::forget($cacheKey);
  77. SsNode::query()->where('id', $node->id)->update(['status' => 0]);
  78. $this->notifyMaster($title, "节点**{$node->name}【{$node->ip}】**:**".$text."**,节点自动进入维护状态", $node->name, $node->server);
  79. }
  80. }else{
  81. $this->notifyMaster($title, "节点**{$node->name}【{$node->ip}】**:**".$text."**", $node->name, $node->server);
  82. }
  83. }
  84. Log::info("【TCP阻断检测】".$node->name.' - '.$node->ip.' - '.$text);
  85. }
  86. }
  87. // 随机生成下次检测时间
  88. $nextCheckTime = time()+mt_rand(1800, 3600);
  89. Cache::put('tcp_check_time', $nextCheckTime, 60);
  90. }
  91. /**
  92. * 用ipcheck.need.sh进行TCP阻断检测
  93. *
  94. * @param string $ip 被检测的IP
  95. *
  96. * @return bool|int
  97. */
  98. private function tcpCheck($ip)
  99. {
  100. try{
  101. $url = 'https://ipcheck.need.sh/api_v2.php?ip='.$ip;
  102. $ret = $this->curlRequest($url);
  103. $ret = json_decode($ret);
  104. if(!$ret || $ret->result != 'success'){
  105. Log::warning("【TCP阻断检测】检测".$ip."时,接口返回异常");
  106. return FALSE;
  107. }
  108. } catch(Exception $e){
  109. Log::warning("【TCP阻断检测】检测".$ip."时,接口请求超时");
  110. return FALSE;
  111. }
  112. if(!$ret->data->inside_gfw->tcp->alive && !$ret->data->outside_gfw->tcp->alive){
  113. return 1; // 服务器宕机或者检测接口挂了
  114. }elseif($ret->data->inside_gfw->tcp->alive && !$ret->data->outside_gfw->tcp->alive){
  115. return 2; // 国外访问异常
  116. }elseif(!$ret->data->inside_gfw->tcp->alive && $ret->data->outside_gfw->tcp->alive){
  117. return 3; // 被墙
  118. }else{
  119. return 0; // 正常
  120. }
  121. }
  122. /**
  123. * 通知管理员
  124. *
  125. * @param string $title 消息标题
  126. * @param string $content 消息内容
  127. * @param string $nodeName 节点名称
  128. * @param string $nodeServer 节点域名
  129. *
  130. * @throws GuzzleException
  131. */
  132. private function notifyMaster($title, $content, $nodeName, $nodeServer)
  133. {
  134. $this->notifyMasterByEmail($title, $content, $nodeName, $nodeServer);
  135. ServerChan::send($title, $content);
  136. }
  137. /**
  138. * 发邮件通知管理员
  139. *
  140. * @param string $title 消息标题
  141. * @param string $content 消息内容
  142. * @param string $nodeName 节点名称
  143. * @param string $nodeServer 节点域名
  144. */
  145. private function notifyMasterByEmail($title, $content, $nodeName, $nodeServer)
  146. {
  147. if(self::$systemConfig['crash_warning_email']){
  148. $logId = Helpers::addEmailLog(self::$systemConfig['crash_warning_email'], $title, $content);
  149. Mail::to(self::$systemConfig['crash_warning_email'])->send(new nodeCrashWarning($logId, $nodeName, $nodeServer));
  150. }
  151. }
  152. /**
  153. * 发起一个CURL请求
  154. *
  155. * @param string $url 请求地址
  156. * @param array $data POST数据,留空则为GET
  157. *
  158. * @return mixed
  159. */
  160. private function curlRequest($url, $data = [])
  161. {
  162. $data = json_encode($data, JSON_UNESCAPED_UNICODE);
  163. $ch = curl_init();
  164. curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
  165. curl_setopt($ch, CURLOPT_TIMEOUT, 10);
  166. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
  167. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
  168. curl_setopt($ch, CURLOPT_URL, $url);
  169. // curl_setopt($ch, CURLOPT_HTTPHEADER, [
  170. // 'Accept: application/json', // 请求报头
  171. // 'Content-Type: application/json', // 实体报头
  172. // 'Content-Length: ' . strlen($data)
  173. // ]);
  174. // 如果data有数据,则用POST请求
  175. if($data){
  176. curl_setopt($ch, CURLOPT_POST, 1);
  177. curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  178. }
  179. $result = curl_exec($ch);
  180. curl_close($ch);
  181. return $result;
  182. }
  183. }