起源
由于被监控设备无公网IP, 而且处于不同的内网环境, 为解决无法远程执行命令问题, 最初阶段使用的VPN模式,每个机器运行一个 VPN 容器,容器内部网络拨号的得到 一个VPN IP
外部请求这个VPN IP, VPN 容器再将端口转发到宿主机 (转发到容器NAT网关就是转发到宿主机) ,实现内网穿透
但后来发现, 有些设备处于互联网深处的多层 NAT 网络之下,无法大部分VPN 协议都无法穿透,后面决定使用 FRP 进行内往穿透,就有了本文
思路流程
- 在zabbix 的数据库中新建一个 frp 的数据库, 导入以下 Sql 命令
- 服务端新建一个 PHP 并开放接口, 以便 zabbix agent 请求接口获取一个随机端口, 并将端口绑定到
- 将获取到的端口用 frp 绑定到被监控的 zabbix-agent 客户端端口 也就是zabbix agent 配置中的 ListenPort
- 使用脚本将服务端的端口改成 frp 端口, 如下图,箭头指向的两个值都是脚本自动更新的,左边 frp 代理 zabbix-agent 端口, 右边代理 ssh 端口
- 如果使用脚本对进行修改参考 Zabbix Api https://www.zabbix.com/documentation/5.0/zh/manual/api/reference/configuration

脚本
用于获取 frp 端口的PHP脚本
frp.php
<?phpinclude_once $_SERVER['DOCUMENT_ROOT'] . '/script/php/extend/frp_base.php';class Controller{public function index(){return '您好!这是一个[api]示例应用';}public function Get(){$v['obj'] = new FrpBase();$v['get'] = $_POST;$v['json'] = json_decode($v['get']['json'], true);$v['ret']['hostname'] = $v['json']['hostname'];foreach ($v['json']['port'] as $key => $value) {$v['ret']['port'][$value] = $v['obj']->select_service_port($v['json']['hostname'], $value);}// print_r($v['ret']);echo json_encode($v['ret']);// $v['port'] = $v['obj']->select_service_port();// return json_encode($v['port']);// print_r($v);}function __construct(){if ($_GET['i'] == 'Get') {$this->Get();}}}$use = new Controller();if (false) {$help = <<<USE# view-source:http://zabbix.dcache.kuaicdn.cn/script/php/controller/frp.php?i=Get&json={ "hostname": "00A01FB40CC46036-dcache", "port": ["22","12104"] }USE;}
frp_base.php
<?phpclass Connect_mysql{/* 成员变量 */var $info;var $pdo;function connect(){try {$this->pdo = new PDO("mysql:host=" . $this->info["host"] . ";dbname=" . $this->info["db_name"],$this->info["db_user"],$this->info["db_pwd"]); //创建一个pdo对象$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 设置sql语句查询如果出现问题 就会抛出异常//set_exception_handler("cus_exception_handler");} catch (PDOException $e) {die("connect error:" . $e->getMessage());}$this->pdo->exec("set names 'utf8'");}function init(){$this->info = array('host' => 'localhost','db_name' => 'frp','db_user' => 'root','db_pwd' => 'passwd',);}function __construct(){$this->init();$this->connect();}}class FrpBase{public $v = [];public $pdo;function __construct(){$connect_mysql = new Connect_mysql();$this->pdo = $connect_mysql->pdo;}function new_record($hostname, $port){# 查询并锁行$v['args']['hostname'] = $hostname;$v['args']['port'] = $port;$this->pdo->beginTransaction();$v['sql'] = "SELECT * FROM `port` ORDER BY `port`.`time_last` ASC, `port`.`port_service` ASC limit 1 for UPDATE";$v['stmt'] = $this->pdo->prepare($v['sql']);$v['rs'] = $v['stmt']->execute();$v['select'] = $v['stmt']->fetch(PDO::FETCH_ASSOC);# 更新$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");$v['stmt']->bindValue(":port_app", $v['args']['port'], PDO::PARAM_STR);$v['stmt']->bindValue(":host_sign", $v['args']['hostname'], PDO::PARAM_STR);$v['stmt']->bindValue(":port_service", $v['select']['port_service'], PDO::PARAM_STR);$v['execute'] = $v['stmt']->execute();$this->pdo->commit();return $v['select'];}function select_service_port($hostname, $port){$v['args']['hostname'] = $hostname;$v['args']['port'] = $port;$v['sql'] = "SELECT * FROM `port` WHERE `port_app` = :port_app AND `host_sign` = :host_sign";$v['stmt'] = $this->pdo->prepare($v['sql']);$v['stmt']->bindValue(":port_app", $v['args']['port'], PDO::PARAM_STR);$v['stmt']->bindValue(":host_sign", $v['args']['hostname'], PDO::PARAM_STR);$v['execute'] = $v['stmt']->execute();$v['select'] = $v['stmt']->fetch(PDO::FETCH_ASSOC);if (!isset($v['select']['port_service'])) {$v['select'] = $this->new_record($v['args']['hostname'], $v['args']['port']);$v['select'] = $this->select_service_port($hostname, $port);} else {$v['update'] = $this->pdo->exec("UPDATE `port` SET `time_last` = current_timestamp() WHERE `port`.`port_service` = " . $v['select']["port_service"]);}$this->dev[__METHOD__] = $v;return $v['select'];}}
