起源

由于被监控设备无公网IP, 而且处于不同的内网环境, 为解决无法远程执行命令问题, 最初阶段使用的VPN模式,每个机器运行一个 VPN 容器,容器内部网络拨号的得到 一个VPN IP
外部请求这个VPN IP, VPN 容器再将端口转发到宿主机 (转发到容器NAT网关就是转发到宿主机) ,实现内网穿透
但后来发现, 有些设备处于互联网深处的多层 NAT 网络之下,无法大部分VPN 协议都无法穿透,后面决定使用 FRP 进行内往穿透,就有了本文

Zabbix 配合 FRP 解决 zabbix-agent 无固定IP,监控困难问题 - 图1

思路流程

  1. 在zabbix 的数据库中新建一个 frp 的数据库, 导入以下 Sql 命令

frp-3w-port.zip

  1. 服务端新建一个 PHP 并开放接口, 以便 zabbix agent 请求接口获取一个随机端口, 并将端口绑定到
  2. 将获取到的端口用 frp 绑定到被监控的 zabbix-agent 客户端端口 也就是zabbix agent 配置中的 ListenPort
  3. 使用脚本将服务端的端口改成 frp 端口, 如下图,箭头指向的两个值都是脚本自动更新的,左边 frp 代理 zabbix-agent 端口, 右边代理 ssh 端口
  4. 如果使用脚本对进行修改参考 Zabbix Api https://www.zabbix.com/documentation/5.0/zh/manual/api/reference/configuration

image.png

脚本

用于获取 frp 端口的PHP脚本

frp.php

  1. <?php
  2. include_once $_SERVER['DOCUMENT_ROOT'] . '/script/php/extend/frp_base.php';
  3. class Controller
  4. {
  5. public function index()
  6. {
  7. return '您好!这是一个[api]示例应用';
  8. }
  9. public function Get()
  10. {
  11. $v['obj'] = new FrpBase();
  12. $v['get'] = $_POST;
  13. $v['json'] = json_decode($v['get']['json'], true);
  14. $v['ret']['hostname'] = $v['json']['hostname'];
  15. foreach ($v['json']['port'] as $key => $value) {
  16. $v['ret']['port'][$value] = $v['obj']->select_service_port($v['json']['hostname'], $value);
  17. }
  18. // print_r($v['ret']);
  19. echo json_encode($v['ret']);
  20. // $v['port'] = $v['obj']->select_service_port();
  21. // return json_encode($v['port']);
  22. // print_r($v);
  23. }
  24. function __construct()
  25. {
  26. if ($_GET['i'] == 'Get') {
  27. $this->Get();
  28. }
  29. }
  30. }
  31. $use = new Controller();
  32. if (false) {
  33. $help = <<<USE
  34. # view-source:http://zabbix.dcache.kuaicdn.cn/script/php/controller/frp.php?i=Get&json={ "hostname": "00A01FB40CC46036-dcache", "port": ["22","12104"] }
  35. USE;
  36. }

frp_base.php

  1. <?php
  2. class Connect_mysql
  3. {
  4. /* 成员变量 */
  5. var $info;
  6. var $pdo;
  7. function connect()
  8. {
  9. try {
  10. $this->pdo = new PDO(
  11. "mysql:host=" . $this->info["host"] . ";dbname=" . $this->info["db_name"],
  12. $this->info["db_user"],
  13. $this->info["db_pwd"]
  14. ); //创建一个pdo对象
  15. $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 设置sql语句查询如果出现问题 就会抛出异常
  16. //set_exception_handler("cus_exception_handler");
  17. } catch (PDOException $e) {
  18. die("connect error:" . $e->getMessage());
  19. }
  20. $this->pdo->exec("set names 'utf8'");
  21. }
  22. function init()
  23. {
  24. $this->info = array(
  25. 'host' => 'localhost',
  26. 'db_name' => 'frp',
  27. 'db_user' => 'root',
  28. 'db_pwd' => 'passwd',
  29. );
  30. }
  31. function __construct()
  32. {
  33. $this->init();
  34. $this->connect();
  35. }
  36. }
  37. class FrpBase
  38. {
  39. public $v = [];
  40. public $pdo;
  41. function __construct()
  42. {
  43. $connect_mysql = new Connect_mysql();
  44. $this->pdo = $connect_mysql->pdo;
  45. }
  46. function new_record($hostname, $port)
  47. {
  48. # 查询并锁行
  49. $v['args']['hostname'] = $hostname;
  50. $v['args']['port'] = $port;
  51. $this->pdo->beginTransaction();
  52. $v['sql'] = "SELECT * FROM `port` ORDER BY `port`.`time_last` ASC, `port`.`port_service` ASC limit 1 for UPDATE";
  53. $v['stmt'] = $this->pdo->prepare($v['sql']);
  54. $v['rs'] = $v['stmt']->execute();
  55. $v['select'] = $v['stmt']->fetch(PDO::FETCH_ASSOC);
  56. # 更新
  57. $v['stmt'] = $this->pdo->prepare("UPDATE `port` SET `time_last` = current_timestamp(), `host_sign` = :host_sign, `port_app` = :port_app WHERE `port`.`port_service` = :port_service");
  58. $v['stmt']->bindValue(":port_app", $v['args']['port'], PDO::PARAM_STR);
  59. $v['stmt']->bindValue(":host_sign", $v['args']['hostname'], PDO::PARAM_STR);
  60. $v['stmt']->bindValue(":port_service", $v['select']['port_service'], PDO::PARAM_STR);
  61. $v['execute'] = $v['stmt']->execute();
  62. $this->pdo->commit();
  63. return $v['select'];
  64. }
  65. function select_service_port($hostname, $port)
  66. {
  67. $v['args']['hostname'] = $hostname;
  68. $v['args']['port'] = $port;
  69. $v['sql'] = "SELECT * FROM `port` WHERE `port_app` = :port_app AND `host_sign` = :host_sign";
  70. $v['stmt'] = $this->pdo->prepare($v['sql']);
  71. $v['stmt']->bindValue(":port_app", $v['args']['port'], PDO::PARAM_STR);
  72. $v['stmt']->bindValue(":host_sign", $v['args']['hostname'], PDO::PARAM_STR);
  73. $v['execute'] = $v['stmt']->execute();
  74. $v['select'] = $v['stmt']->fetch(PDO::FETCH_ASSOC);
  75. if (!isset($v['select']['port_service'])) {
  76. $v['select'] = $this->new_record($v['args']['hostname'], $v['args']['port']);
  77. $v['select'] = $this->select_service_port($hostname, $port);
  78. } else {
  79. $v['update'] = $this->pdo->exec("UPDATE `port` SET `time_last` = current_timestamp() WHERE `port`.`port_service` = " . $v['select']["port_service"]);
  80. }
  81. $this->dev[__METHOD__] = $v;
  82. return $v['select'];
  83. }
  84. }