mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
* fix(win32): harden the cmd.exe shell sinks on develop (GHSA-rjvj-r52f-8v5q) Forward-port of the 1.2.x win32 command-injection remediation. On Windows the SNMP/ping/data-query/trap commands run through cmd.exe, which ignores the \" escape and toggles quoting on every ", so cacti_escapeshellarg cannot stop & | ^ < > ( ). - add cacti_escapeshellarg_cmd(), stripping those operators on win32 before quoting (a Unix pass-through) - snmp_escape_string(): strip the operators in the win32 branch - route the SNMP binary hostname, the data-query device values (get_script_query_path), the data-input arg builders, the Windows ping hostname, and the SNMP trap notification arguments through the strip get_script_query_path is device-reachable (a rogue monitored device supplies an SNMP index/system field that executes on the Windows poller); the rest are post-auth. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(win32): strip %VAR% from the hostname on Windows (GHSA-rjvj-r52f-8v5q) cmd.exe expands %VAR% even inside quotes, so a device hostname of %USERNAME% would expand into the SNMP/ping command line. Add an opt-in $strip_env flag to cacti_escapeshellarg_cmd() and set it for the SNMP binary and ping hostnames, which never legitimately contain a percent. Community and credential values are left untouched because they can legitimately contain %. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(changelog): note win32 cmd.exe hardening for develop * security: require Package Management permission to trust new package signers package_accept_key() (reachable via the repository-based package import flow) inserted new keys into package_public_keys, the global trust store consulted by import_validate_signature() for every future import by every user, without checking for the Package Management (realm 29) permission that gates package_keys.php/package.php/package_repos.php for the same table. An Import Templates (realm 17) user could therefore add themselves as a trusted signer for the whole install. Also add the 'accept' action to the CSRF-protected bad_actions list so the acceptance step requires POST plus a valid token like every other state-changing action. GHSA-gxgm-8rhv-r7jv * docs(changelog): note package-signer trust authorization fix * security: close tab-based open redirect bypass in CSRF timeout handler sanitize_uri()'s guard (20298c6cd) models the WHATWG rule that browsers strip leading C0 controls/whitespace before resolving a redirect, but Chromium separately strips an embedded tab from anywhere in the target. A request target of /%09/evil.example passes the guard (no leading control char, not two leading slashes) but Chromium reduces the resulting 'Location: /<TAB>/evil.example' header to a network-path reference and navigates off-site. 1.2.x already uses validate_redirect_url() for this same sink (3338a63e1), which resolves the URL via parse_url() and checks host/port against the server-configured values, so it isn't bypassable this way. Switch develop's CSRF timeout handler to match. GHSA-c725-4jm5-9fx5 * security: authorize single-item Report actions against ownership reports.php sets \ = true, so include/auth.php grants an implicit guest session (when the Guest User feature is enabled) without ever evaluating the realm 22 gate declared for this page. The single-item action handlers reports_item_remove()/_moveup()/_movedown() relied solely on that page-level gate and performed no independent ownership check, so an unauthenticated guest-bound request could delete or reorder reports_items rows belonging to any report, including ones owned by an administrator. reports_form_actions() already gained an equivalent \ check; add the same authorization (Reports admin realm 21, or the requesting user owns the item's report) to the three single-item handlers. Same underlying chain as the 1.2.31 fix for reports_user.php (guest bind removed + cacti_authorize_resource() added to reports_item_remove()); the rewritten develop reports.php/lib/html_reports.php did not carry either half of that fix forward. GHSA-j5jh-fhwg-r56x * security: authorize tree node CRUD actions against per-tree access tree.php dispatches create_node/delete_node/move_node/rename_node/copy_node directly to api_tree_*() with no is_tree_allowed() check, even though the normal tree-view path already enforces that boundary. A non-admin user with the Trees realm but no explicit grant on a given tree_id could still create, delete, move, rename or copy nodes on that tree by calling these actions directly, bypassing the authorization boundary enforced elsewhere. GHSA-prxj-84qq-v5jq * security: remove shell from Realtime RRD create and validate DS bounds poller_realtime.php built the Realtime-graph RRD 'create' command line (including the SNMP-sourced ifSpeed/ifHighSpeed interface speed, substituted in verbatim as the DS maximum) and ran it through shell_exec(). A hostile or spoofed monitored device could return an ifSpeed value such as '1000000000\uid=197608(ladams) gid=197121 groups=197121' and get arbitrary OS command execution as the web/poller user the next time any user with the Realtime Graphs permission (or an unauthenticated guest, where configured) viewed a realtime graph for an affected data source. - lib/rrd.php: rrdtool_function_interface_speed() now casts ifHighSpeed/ ifSpeed to int before they become the DS maximum. - lib/rrd.php: rrdtool_function_create() now rejects any non-numeric rrd_minimum/rrd_maximum right before they are concatenated onto the rrdtool command line, forcing them to 'U' and logging instead. - poller_realtime.php: the RRD create is now routed through the same shell-free rrdtool_execute()/proc_open(bypass_shell) path every other RRDtool invocation already uses, matching the pattern 1.2.x already uses for this exact call site, instead of shell_exec(). GHSA-f88m-q789-g22j * security: authorize Report save/edit against report ownership reports_form_save()'s update path fetched the existing report's owner from the database but never compared it to the session user before calling sql_save(), so any user holding only the Reports realm could overwrite another user's report by POSTing its id. reports_edit() had the same gap on the read side: it fetched a report by id with no ownership check, so the same user could view any other user's report configuration (recipients, schedule, etc.). 1.2.x already guards both call sites with cacti_authorize_resource(..., 'reports'); develop had lost that check. GHSA-8f5c-3h59-q7gc * security: reject ORDER BY sort_column subquery bypass sanitize_sql_column()'s allowlist regex kept parentheses to support the INET_ATON(col)/NATURAL_SORT_KEY(col) sort wrappers, but that also let MySQL subquery syntax through unchanged, since a subquery like (SELECT(0)FROM(SELECT(SLEEP(5)))a) needs only alphanumerics and parentheses. get_order_string() then detects the '(' and disables its backtick quoting, so the payload reaches ORDER BY verbatim, giving any user with realm 2-5 a blind, time-based SQL injection via sort_column. sanitize_sql_column() now only allows '(' through when the whole value is exactly one of the two known safe wrapper forms around a plain identifier; anything else containing '(' -- including subquery syntax -- now falls back to the default. Added regression tests covering the reported subquery and REGEXP-exfiltration payloads. GHSA-8j73-cvf6-jrvx * security: confine Reports format_file to a bare filename reports_form_save() wrote the POST-supplied format_file value verbatim into reports.format_file, and reports_load_format_file() concatenated it onto CACTI_PATH_FORMATS with no directory-component check, so a value such as ../../../../etc/passwd escaped the formats directory and any Reports-realm user could read arbitrary files readable by the web server. 1.2.x already fixed the same sink (GHSA-g37j-39f4-6r4j / CVE-2026-41884, GHSA-mjvw-mhj5-9jcj / CVE-2026-40084); develop had lost the guard. format_file is now confined to basename() at both the save site and the load site, with the load site additionally rejecting a bare '.'/'..'/empty result before any file IO. Added regression tests. GHSA-5x27-wqpm-m24v * security: authorize Data Source enable/disable against owning device ds_enable()/ds_disable() and the bulk drp_action enable/disable loops in form_actions() passed a user-controlled local_data_id straight into api_data_source_enable()/api_data_source_disable() with no object-level authorization check, so any user holding the Sites/Devices/Data realm could toggle the enabled state of a data source belonging to a device outside their assigned Device Permissions. Added data_source_authorized(\), which allows host-independent data sources (data_local.host_id = 0) and otherwise defers to is_device_allowed(\), and applied it to both the single-item and bulk action paths. GHSA-2fhv-c794-v433 * security: authorize bulk Change Data Source Profile against owning device The bulk 'Change Data Source Profile' action (drp_action == '6') iterated selected_items and updated data_template_data/data_template_rrd/poller_item for each supplied local_data_id with no object-level authorization check, so any Sites/Devices/Data realm user could retarget the RRD step/heartbeat of a data source belonging to a device outside their assigned Device Permissions -- the same class of gap as GHSA-2fhv, in a different bulk action. Guarded with the data_source_authorized() helper added for that fix. This action does not exist on 1.2.x (develop-only feature). GHSA-fc82-w6xq-4jg7 * security: grant AllowOverride All in docker/apache.conf The packaged docker/apache.conf granted only 'AllowOverride FileInfo Options=FollowSymLinks' for the Cacti document root. Cacti's shipped .htaccess deny rules (log/, cache/*, scripts/, cli/, bin/, mibs/, rra/, contrib/, tests/) rely on 'Require all denied' (Apache 2.4, override type AuthConfig) and/or legacy 'Order/Deny' (override type Limit), neither of which FileInfo/Options covers, so Apache silently ignored every one of those deny rules and served the directories over HTTP to any unauthenticated client -- most notably log/cacti.log, which can contain usernames, client IPs, SQL backtraces, and any secrets Cacti logs (SNMP community strings, LDAP bind details). AllowOverride All lets every shipped .htaccess apply as intended. Added a regression test. docker/ does not exist on 1.2.x (develop-only). GHSA-fxfg-j995-j69j * docs: add security advisory / CVE policy to Copilot instructions Documents the policy established for this remediation effort: develop is unreleased, so a develop-only vulnerability does not get a CVE requested; 1.2.x is the released/LTS branch, so a CVE is requested when the same vulnerable code is also reachable there. Also notes to verify against both branches' actual source rather than trusting a reporter's claimed affected-version range. * fix(security): update stale test harnesses and address review findings CI fixes: - CsrfCheckContractProbe.php: stub validate_redirect_url() so the fatal-fail probe doesn't fatal on the new open-redirect dependency that csrf_error_callback() picked up. - InstallerCsrfRecoveryTest.php: update the stale sanitize_uri()-based assertion to match the new validate_redirect_url() redirect code. - psalm-baseline.xml: drop the TaintedHeader suppression for include/csrf.php now that validate_redirect_url() resolves it. Copilot review findings on GHSA-rjvj-r52f-8v5q hardening: - lib/data_query.php: only strip '%' for the host_hostname/ host_management_ip substitutions in get_script_query_path(), not indiscriminately for credential substitutions. - lib/functions.php: opt into percent stripping for the hostname host column in get_full_test_script_path() and for type_code='hostname' fields in get_full_script_path(). - lib/snmpagent.php: sanitize the trap receiver hostname against percent expansion before it reaches the escaping helper. - tests/security/baselines/*.tsv: refresh the sink inventory and architectural hotspot baselines for the updated command sinks. - tests/Unit/DataCollection/Snmp/SnmpCoverageTest.php: add the missing cacti_escapeshellarg_cmd stub, and update the snmp_escape_string contract test for the intentional win32 quote-stripping hardening. - tests/Unit/Security/Shell/CmdMetacharHelperTest.php + tests/fixtures/CmdMetacharHelperWin32Probe.php: add a real win32 subprocess execution test for cacti_escapeshellarg_cmd instead of only asserting on source text. - CHANGELOG: add the pull-request reference to the new entry. * fix(snmp): remove now-unreachable quote-escaping branch in snmp_escape_string The metachar strip on the line above already removes the double quote on win32, so the substr_count(SNMP_ESCAPE_CHARACTER) branch below it could never execute; drop it so lib/snmp.php reaches 100% coverage again. * style: fix pre-existing php-cs-fixer blank-line drift blocking CI data_sources.php and lib/html_reports.php had a pre-existing blank-line style drift (unrelated to this PR's security fixes) that php-cs-fixer's --dry-run check flags as fixable. Since "PHP 8.3 locked integration" is part of this PR's CI / required gate, correct the two spots so the whole PR is green: - data_sources.php: collapse a double blank line before form_actions(). - lib/html_reports.php: insert the blank line php-cs-fixer expects between the input-validation gfrv() calls and the following "====" separator comment in reports_item_moveup() and reports_item_remove(). No behavior change; purely CI/style formatting. * style: refresh phpstan baseline for pre-existing, unrelated findings Three PHPStan findings were masked behind the earlier php-cs-fixer failure and only surfaced once that step passed: - lib/html_reports.php: a second, already-guarded 'user_id' offset access from an unrelated, previously-merged security commit (92b8d4e6f) was never added to the baseline count. - cli/splice_rrd.php, poller_realtime.php: two other pre-existing, unrelated findings had no baseline entry at all. None of these touch code from this PR; added narrowly-scoped baseline entries (not a full regeneration) purely to unblock the required PHP 8.3 locked integration check. * fix(security): address follow-up review findings on consolidated PR Closes the four remaining review findings from the follow-up review round: - package_import.php: trustSigner() now POSTs with a CSRF token instead of $.getJSON, since 'accept' is enforced as a POST-only action in include/global.php's bad_actions list (GHSA-gxgm-8rhv-r7jv). - lib/html_reports.php: reports_item_authorized() gained an optional $report_id parameter that, when given, requires it to match the item's actual current report - reports_item_movedown()/reports_item_moveup() now pass the request's report id through it, closing a cross-report sequence-swap variant where an item you own could be reordered against a report you don't. The item_edit render path and the save_component_report_item save path (reachable via reports.php's guest bind) previously had no ownership check at all; both now verify the target report is owned by the caller, and (when editing an existing item) that the item's current report is too, before rendering or calling sql_save() (GHSA-j5jh-fhwg-r56x). - tree.php + include/global.php: create_node/rename_node/move_node/copy_node now submit as POST with a CSRF token (matching the existing delete_node pattern) and are added to the POST-only bad_actions list, since the per-tree authorization check added earlier didn't make these mutations CSRF-safe on its own (GHSA-prxj-84qq-v5jq). Also updates the PR description to list every GHSA fix actually included in this consolidated branch (previously only GHSA-rjvj-r52f-8v5q was mentioned), per review feedback that the description didn't match the actual scope. * fix: resolve CI failures on PR #8056 (docblock alignment, sink baseline) - lib/html_reports.php: fix php-cs-fixer docblock indentation drift on reports_item_authorized()'s @param $report_id continuation lines (one extra space of alignment) introduced by the prior ownership-check fix. - tests/security/baselines/sink_inventory.baseline.tsv: refresh to track the 4 new header('Location: ' . get_reports_page()) redirect sinks added by the new report/report-item ownership checks in lib/html_reports.php. Verified via tests/security/verify_sink_inventory.sh and confirmed tests/security/verify_architectural_hotspots.sh still reports no new hotspots. * fix: track new PHPStan offset findings from extended reports_item_authorized() phpstan-baseline.neon: the reports_item_authorized($item_id, $report_id) extension added a new array offset access ('report_id') and a third occurrence of the existing 'user_id' offset access on the array|bool return of db_fetch_row_prepared() in lib/html_reports.php. Add a new baseline entry for 'report_id' (count: 1) and bump the existing 'user_id' entry from count: 2 to count: 3 to match. Verified locally with `include/vendor/bin/phpstan analyse --memory-limit=1G` (0 errors). --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <copilot@users.noreply.github.com>
806 lines
27 KiB
PHP
806 lines
27 KiB
PHP
<?php
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
| |
|
|
| This program is distributed in the hope that it will be useful, |
|
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
| GNU General Public License for more details. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
class Net_Ping {
|
|
public socket $socket;
|
|
public array $host;
|
|
public int $port;
|
|
public mixed $ping_status;
|
|
public string $ping_response;
|
|
public mixed $snmp_status;
|
|
public string $snmp_response;
|
|
public string $request;
|
|
public int $request_len;
|
|
public mixed $reply;
|
|
public int $timeout;
|
|
public int $retries;
|
|
public int $precision;
|
|
public int $time;
|
|
public float $timer_start_time;
|
|
public string $sqn;
|
|
public int $avail_method;
|
|
public int $ping_type;
|
|
|
|
function __construct() {
|
|
$this->port = 33439;
|
|
}
|
|
|
|
function __destruct() {
|
|
}
|
|
|
|
function close_socket() : void {
|
|
@socket_shutdown($this->socket, 2);
|
|
socket_close($this->socket);
|
|
}
|
|
|
|
function start_time() : void {
|
|
$this->timer_start_time = microtime(true);
|
|
}
|
|
|
|
function get_time(int $acc = 2) : mixed {
|
|
// format start time
|
|
$start_time = $this->timer_start_time;
|
|
// get and format end time
|
|
$end_time = microtime(true);
|
|
|
|
return number_format($end_time - $start_time, $acc);
|
|
}
|
|
|
|
function build_udp_packet() : void {
|
|
$data = 'cacti-monitoring-system'; // the actual test data
|
|
|
|
// now lets build the actual UDP packet
|
|
$this->request = chr(0) . chr(1) . chr(0) . $data . chr(0);
|
|
$this->request_len = strlen($this->request);
|
|
}
|
|
|
|
function ping_error_handler(int $errno, string $errmsg, string $filename, int $linenum, array $vars = []) : bool {
|
|
return true;
|
|
}
|
|
|
|
function set_ping_error_handler() : void {
|
|
set_error_handler([$this, 'ping_error_handler']);
|
|
}
|
|
|
|
function restore_cacti_error_handler() : void {
|
|
restore_error_handler();
|
|
}
|
|
|
|
function build_icmp_packet() : void {
|
|
$seq_low = random_int(0,255);
|
|
$seq_high = random_int(0,255);
|
|
|
|
$data = 'cacti-monitoring-system'; // the actual test data
|
|
$type = "\x08"; // 8 echo message; 0 echo reply message
|
|
$code = "\x00"; // always 0 for this program
|
|
$chksm = "\x00\x00"; // generate checksum for icmp request
|
|
$id = chr($seq_high) . chr($seq_low);
|
|
$sqn = chr($seq_high) . chr($seq_low);
|
|
$this->sqn = $sqn;
|
|
|
|
// now lets build the actual icmp packet
|
|
$this->request = $type . $code . $chksm . $id . $sqn . $data;
|
|
$chksm = $this->get_checksum($this->request);
|
|
|
|
$this->request = $type . $code . $chksm . $id . $sqn . $data;
|
|
$this->request_len = strlen($this->request);
|
|
}
|
|
|
|
function get_checksum(string $data) : string {
|
|
if (strlen($data) % 2) {
|
|
$data .= "\x00";
|
|
}
|
|
|
|
$bit = unpack('n*', $data);
|
|
$sum = array_sum($bit);
|
|
|
|
while ($sum >> 16) {
|
|
$sum = ($sum >> 16) + ($sum & 0xffff);
|
|
}
|
|
|
|
return pack('n*', ~$sum);
|
|
}
|
|
|
|
function ping_icmp() : bool {
|
|
// ping me
|
|
if ($this->host['hostname']) {
|
|
// initialize variables
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('ICMP Ping timed out');
|
|
|
|
// establish timeout variables
|
|
$to_sec = floor($this->timeout / 1000);
|
|
$to_usec = ($this->timeout % 1000) * 1000;
|
|
|
|
// clean up hostname if specifying snmp_transport
|
|
$this->host['hostname'] = $this->strip_ip_address($this->host['hostname']);
|
|
|
|
/* determine the host's ip address
|
|
* this prevents from command injection as well*/
|
|
if ($this->is_ipaddress($this->host['hostname'])) {
|
|
$host_ip = $this->host['hostname'];
|
|
} else {
|
|
// again, as a side effect, prevention from command injection
|
|
$host_ip = cacti_gethostbyname($this->host['hostname']);
|
|
|
|
if (!$this->is_ipaddress($host_ip)) {
|
|
cacti_log('WARNING: ICMP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname']);
|
|
$this->ping_response = 'ICMP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname'];
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// we have to use the real ping, in cases where windows failed or while using UNIX/Linux
|
|
$pattern = bin2hex('cacti-monitoring-system'); // the actual test data
|
|
|
|
$fping = read_config_option('path_fping');
|
|
|
|
if ($fping != '' && file_exists($fping) && is_executable($fping)) {
|
|
$using_fping = true;
|
|
|
|
if (filter_var($this->host['hostname'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
if (file_exists('/usr/sbin/fping6')) {
|
|
$fping = '/usr/sbin/fping6';
|
|
} elseif (file_exists('/usr/bin/fping6')) {
|
|
$fping = '/usr/bin/fping6';
|
|
} elseif (file_exists('/usr/local/sbin/fping6')) {
|
|
$fping = '/usr/local/sbin/fping6';
|
|
} elseif (file_exists('/usr/local/bin/fping6')) {
|
|
$fping = '/usr/local/bin/fping6';
|
|
}
|
|
}
|
|
|
|
$result = shell_exec(cacti_escapeshellarg($fping) . ' -q -t ' . $this->timeout . ' -c 1 -r ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true) . ' 2>&1');
|
|
} else {
|
|
$using_fping = false;
|
|
|
|
/**
|
|
* The host timeout is given in ms; recalculate to seconds.
|
|
* All numeric fields are safe. The hostname is quoted via
|
|
* cacti_escapeshellarg() as defense-in-depth alongside the
|
|
* DNS resolution check above.
|
|
*/
|
|
if (substr_count(cacti_strtolower(PHP_OS), 'sun')) {
|
|
$result = shell_exec('ping ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'hpux')) {
|
|
$result = shell_exec('ping -m ' . ceil($this->timeout / 1000) . ' -n ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'mac')) {
|
|
$result = shell_exec('ping -t ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'freebsd')) {
|
|
if (filter_var($host_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
$result = shell_exec('ping6 -t ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} else {
|
|
$result = shell_exec('ping -t ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
}
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'darwin')) {
|
|
$result = shell_exec('ping -t ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'bsd')) {
|
|
$result = shell_exec('ping -w ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (substr_count(cacti_strtolower(PHP_OS), 'aix')) {
|
|
$result = shell_exec('ping -i ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} elseif (cacti_strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
|
$result = shell_exec('chcp 437 && ping -w ' . $this->timeout . ' -n ' . $this->retries . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} else {
|
|
/**
|
|
* Please know, that when running SELinux, httpd will throw
|
|
* ping: cap_set_proc: Permission denied
|
|
* as it now tries to open an ICMP socket and fails
|
|
* $result will be empty, then.
|
|
*/
|
|
if (filter_var($host_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
$result = shell_exec('ping -6 -W ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' -p ' . $pattern . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true));
|
|
} else {
|
|
$result = shell_exec('ping -W ' . ceil($this->timeout / 1000) . ' -c ' . $this->retries . ' -p ' . $pattern . ' ' . cacti_escapeshellarg_cmd($this->host['hostname'], true, true) . ' 2>&1');
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($using_fping) {
|
|
/**
|
|
* Prototype fping output below:
|
|
* www.google.com : xmt/rcv/%loss = 1/1/0%, min/avg/max = 25.6/25.6/25.6
|
|
* 172.24.254.2 : xmt/rcv/%loss = 1/1/0%, min/avg/max = 7.01/7.01/7.01
|
|
*/
|
|
$position = strpos($result, 'min/avg/max =');
|
|
|
|
if ($position > 0) {
|
|
$output = substr($result, $position);
|
|
$results = explode('/', $output);
|
|
$this->ping_status = $results[3]; // avg
|
|
$this->ping_response = __('ICMP Ping Success (fping.exe) (%s ms)', $results[1]);
|
|
|
|
return true;
|
|
} else {
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('ICMP Ping Timed Out (fping.exe) (' . $this->host['hostname'] . '), Result [' . $result . ']');
|
|
|
|
return false;
|
|
}
|
|
} elseif (str_starts_with(PHP_OS, 'WIN')) {
|
|
/**
|
|
* The native ping command was used for windows platform has output
|
|
* as follows.
|
|
*
|
|
* ping -w 500 -n 3 abc
|
|
*
|
|
* Pinging abc [216.239.38.120] with 32 bytes of data:
|
|
* Reply from 216.239.38.120: bytes=32 time=30ms TTL=53
|
|
* Reply from 216.239.38.120: bytes=32 time=22ms TTL=53
|
|
* Reply from 216.239.38.120: bytes=32 time=25ms TTL=53
|
|
*
|
|
* Ping statistics for 216.239.38.120:
|
|
* Packets: Sent = 3, Received = 3, Lost = 0 (0% loss),
|
|
* Approximate round trip times in milli-seconds:
|
|
* Minimum = 22ms, Maximum = 30ms, Average = 25ms
|
|
*/
|
|
$position = strpos($result, 'Minimum');
|
|
|
|
if ($position > 0) {
|
|
$output = trim(substr($result, $position));
|
|
$pieces = explode(',', $output);
|
|
$results = explode('=', $pieces[2]); // Average
|
|
|
|
$this->ping_status = trim(str_replace('ms', '', $output));
|
|
$this->ping_response = __('ICMP Ping Success (ping.exe) (%s ms)', $this->ping_status);
|
|
|
|
return true;
|
|
} else {
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('ICMP Ping Timed Out (ping.exe) (' . $this->host['hostname'] . '), Result [' . $result . ']');
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
$position = strpos($result, 'min/avg/max');
|
|
|
|
if ($position > 0) {
|
|
$output = trim(str_replace(' ms', '', substr($result, $position)));
|
|
$pieces = explode('=', $output);
|
|
$results = explode('/', $pieces[1]);
|
|
|
|
$this->ping_status = $results[1];
|
|
$this->ping_response = __('ICMP Ping Success (%s ms)', $results[1]);
|
|
|
|
return true;
|
|
} else {
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('ICMP ping Timed out');
|
|
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('Destination address not specified');
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function seteuid() : int {
|
|
$cacti_user = '';
|
|
|
|
// if we are unix, set the effective userid to root and then create
|
|
if ((CACTI_SERVER_OS == 'unix') &&
|
|
(function_exists('posix_getuid'))) {
|
|
$cacti_user = posix_getuid();
|
|
posix_seteuid(0);
|
|
}
|
|
|
|
return $cacti_user;
|
|
}
|
|
|
|
function setuid(int $cacti_poller_account) : void {
|
|
// if we are unix, set the effective userid to root and then create
|
|
if ((CACTI_SERVER_OS == 'unix') &&
|
|
(function_exists('posix_getuid'))) {
|
|
posix_seteuid($cacti_poller_account);
|
|
}
|
|
}
|
|
|
|
function ping_snmp() : mixed {
|
|
// initialize variables
|
|
$this->snmp_status = 'down';
|
|
$this->snmp_response = 'Device did not respond to SNMP';
|
|
$output = '';
|
|
|
|
// get start time
|
|
$this->start_time();
|
|
|
|
// by default, we look at sysUptime
|
|
if ($this->avail_method == AVAIL_SNMP_GET_NEXT) {
|
|
$oid = '.1.3.6.1.2.1.1.3.0';
|
|
} elseif ($this->avail_method == AVAIL_SNMP_GET_SYSDESC) {
|
|
$oid = '.1.3.6.1.2.1.1.1.0';
|
|
} else {
|
|
$oid = '.1.3.6.1.2.1.1.3.0';
|
|
}
|
|
|
|
$session = cacti_snmp_session($this->host['hostname'], $this->host['snmp_community'],
|
|
$this->host['snmp_version'], $this->host['snmp_username'],
|
|
$this->host['snmp_password'], $this->host['snmp_auth_protocol'],
|
|
$this->host['snmp_priv_passphrase'], $this->host['snmp_priv_protocol'],
|
|
$this->host['snmp_context'], $this->host['snmp_engine_id'],
|
|
$this->host['snmp_port'], $this->host['snmp_timeout'],
|
|
$this->retries, read_config_option('max_get_size'));
|
|
|
|
if ($session === false) {
|
|
$this->snmp_status = 'down';
|
|
$this->snmp_response = 'Failed to make SNMP session';
|
|
|
|
return false;
|
|
}
|
|
|
|
$result = $this->get_snmp_result($session, $oid);
|
|
|
|
if (!$result && $oid == '.1.3.6.1.2.1.1.3.0') {
|
|
$result = $this->get_snmp_result($session, '.1.3.6.1.6.3.10.2.1.3.0');
|
|
}
|
|
|
|
$session->close();
|
|
|
|
return $result;
|
|
}
|
|
|
|
function get_snmp_result(object $session, string $oid) : bool {
|
|
// getnext does not work in php versions less than 5
|
|
if (($this->avail_method == AVAIL_SNMP_GET_NEXT) &&
|
|
(version_compare('5', phpversion(), '<'))) {
|
|
$output = cacti_snmp_session_getnext($session, $oid);
|
|
} else {
|
|
$output = cacti_snmp_session_get($session, $oid);
|
|
}
|
|
|
|
// determine total time +- ~10%
|
|
$this->time = $this->get_time($this->precision);
|
|
|
|
// check result for uptime
|
|
if ($output !== false && $output != 'U' && strlen($output)) {
|
|
// calculate total time
|
|
$this->snmp_status = $this->time * 1000;
|
|
$this->snmp_response = 'Device responded to SNMP';
|
|
|
|
return true;
|
|
} else {
|
|
$this->snmp_status = 'down';
|
|
$this->snmp_response = 'Device did not respond to SNMP';
|
|
|
|
return false;
|
|
}
|
|
} // ping_snmp
|
|
|
|
function ping_udp() : bool {
|
|
$this->set_ping_error_handler();
|
|
|
|
// hostname must be nonblank
|
|
if ($this->host['hostname'] != '') {
|
|
// initialize variables
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('default');
|
|
|
|
// establish timeout variables
|
|
$to_sec = intval(floor($this->timeout / 1000));
|
|
$to_usec = intval(($this->timeout % 1000) * 1000);
|
|
|
|
// clean up hostname if specifying snmp_transport
|
|
$this->host['hostname'] = $this->strip_ip_address($this->host['hostname']);
|
|
|
|
// determine the host's ip address
|
|
if ($this->is_ipaddress($this->host['hostname'])) {
|
|
$host_ip = $this->host['hostname'];
|
|
} else {
|
|
$host_ip = cacti_gethostbyname($this->host['hostname']);
|
|
|
|
if (!$this->is_ipaddress($host_ip)) {
|
|
cacti_log('WARNING: UDP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname']);
|
|
$this->ping_response = 'UDP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname'];
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// initialize the socket
|
|
if (filter_var($host_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
if (defined('AF_INET6')) {
|
|
$this->socket = socket_create(AF_INET6, SOCK_DGRAM, SOL_UDP);
|
|
} else {
|
|
$this->ping_response = __('IPv6 support seems to be missing!');
|
|
$this->ping_status = 'down';
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
$this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
|
|
}
|
|
|
|
socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $to_sec, 'usec' => $to_usec]);
|
|
socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, ['sec' => $to_sec, 'usec' => $to_usec]);
|
|
|
|
socket_connect($this->socket, $host_ip, $this->port);
|
|
|
|
// format packet
|
|
$this->build_udp_packet();
|
|
|
|
$error = '';
|
|
$retry_count = 0;
|
|
|
|
while (true) {
|
|
if ($retry_count >= $this->retries) {
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('UDP ping error: %s', $error);
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
|
|
// get start time
|
|
$this->start_time();
|
|
|
|
// write to the socket
|
|
socket_write($this->socket, $this->request, $this->request_len);
|
|
|
|
// get the socket response
|
|
$r = [$this->socket];
|
|
$w = [$this->socket];
|
|
$f = [$this->socket];
|
|
|
|
$num_changed_sockets = socket_select($r, $w, $f, $to_sec, $to_usec);
|
|
|
|
if ($num_changed_sockets === false) {
|
|
$error = 'UDP ping: socket_select(), reason: ' . socket_strerror(socket_last_error($this->socket));
|
|
} else {
|
|
switch($num_changed_sockets) {
|
|
case 2: // response received, so host is available
|
|
case 1:
|
|
// get packet response
|
|
// $code = socket_recv($this->socket, $this->reply, 256, 0);
|
|
$code = socket_recv($this->socket, $this->reply, 256, 0);
|
|
|
|
// get the end time after the packet was received
|
|
$this->time = $this->get_time($this->precision);
|
|
|
|
$errno = socket_last_error($this->socket);
|
|
socket_clear_error($this->socket);
|
|
|
|
if (($code == -1 || empty($code)) &&
|
|
($errno == EHOSTUNREACH || $errno == ECONNRESET || $errno == ECONNREFUSED)) {
|
|
// set the return message
|
|
$this->ping_status = $this->time * 1000;
|
|
$this->ping_response = __('UDP Ping Success (%s ms)', $this->time * 1000);
|
|
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return true;
|
|
} else {
|
|
$error = socket_strerror($errno);
|
|
}
|
|
|
|
break;
|
|
case 0:
|
|
// timeout
|
|
$error = 'timeout';
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
$retry_count++;
|
|
}
|
|
} else {
|
|
$this->ping_response = __('Destination address not specified');
|
|
$this->ping_status = 'down';
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
} // end ping_udp
|
|
|
|
function ping_tcp() : bool {
|
|
$this->set_ping_error_handler();
|
|
|
|
// hostname must be nonblank
|
|
if ($this->host['hostname'] != '') {
|
|
// initialize variables
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('default');
|
|
|
|
// establish timeout variables
|
|
$to_sec = intval(floor($this->timeout / 1000));
|
|
$to_usec = intval(($this->timeout % 1000) * 1000);
|
|
|
|
// clean up hostname if specifying snmp_transport
|
|
$this->host['hostname'] = $this->strip_ip_address($this->host['hostname']);
|
|
|
|
// determine the host's ip address
|
|
if ($this->is_ipaddress($this->host['hostname'])) {
|
|
$host_ip = $this->host['hostname'];
|
|
} else {
|
|
$host_ip = cacti_gethostbyname($this->host['hostname']);
|
|
|
|
if (!$this->is_ipaddress($host_ip)) {
|
|
cacti_log('WARNING: TCP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname']);
|
|
$this->ping_response = 'TCP Ping Error: cacti_gethostbyname failed for ' . $this->host['hostname'];
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// initialize the socket
|
|
if (filter_var($host_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
if (defined('AF_INET6')) {
|
|
$this->socket = socket_create(AF_INET6, SOCK_STREAM, SOL_TCP);
|
|
} else {
|
|
$this->ping_response = __('IPv6 support appears to be missing!');
|
|
$this->ping_status = 'down';
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
|
|
}
|
|
|
|
while (1) {
|
|
// set start time
|
|
$this->start_time();
|
|
|
|
socket_set_block($this->socket);
|
|
socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $to_sec, 'usec' => $to_usec]);
|
|
socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, ['sec' => $to_sec, 'usec' => $to_usec]);
|
|
|
|
socket_connect($this->socket, $host_ip, $this->port);
|
|
|
|
$errno = socket_last_error($this->socket);
|
|
|
|
if ($errno > 0) {
|
|
if ($errno == 111 && $this->ping_type == PING_TCP_CLOSED) {
|
|
$this->time = $this->get_time($this->precision);
|
|
$this->ping_status = 'up';
|
|
$this->ping_response = __('TCP Ping Success Connection Refused (%s ms)', $this->time * 1000);
|
|
$this->ping_status = $this->time * 1000;
|
|
} else {
|
|
$this->ping_response = __('TCP Ping Failed: socket_connect(), reason: %s', socket_strerror($errno));
|
|
$this->ping_status = 'down';
|
|
}
|
|
|
|
socket_clear_error($this->socket);
|
|
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
if ($this->ping_status == 'down') {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$r = [$this->socket];
|
|
$w = [$this->socket];
|
|
$f = [$this->socket];
|
|
|
|
$num_changed_sockets = socket_select($r, $w, $f, $to_sec, $to_usec);
|
|
|
|
if ($num_changed_sockets === false) {
|
|
$this->ping_response = __('TCP Ping Failed: socket_select() failed, reason: %s', socket_strerror(socket_last_error()));
|
|
$this->ping_status = 'down';
|
|
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
} else {
|
|
switch($num_changed_sockets) {
|
|
case 2: // response received, so host is available
|
|
case 1:
|
|
// connected, so calculate the total time and return
|
|
$this->time = $this->get_time($this->precision);
|
|
|
|
if (($this->time * 1000) <= $this->timeout) {
|
|
$this->ping_response = __('TCP Ping Success (%s ms)', $this->time * 1000);
|
|
$this->ping_status = $this->time * 1000;
|
|
}
|
|
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return true;
|
|
case 0:
|
|
// timeout
|
|
$this->ping_response = __('TCP ping timed out');
|
|
$this->ping_status = 'down';
|
|
|
|
$this->close_socket();
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$this->ping_response = __('Destination address not specified');
|
|
$this->ping_status = 'down';
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return false;
|
|
}
|
|
} // end ping_tcp
|
|
|
|
function ping(int $avail_method = AVAIL_SNMP_AND_PING, int $ping_type = PING_ICMP, int $timeout = 500, int $retries = 3) : bool {
|
|
$this->set_ping_error_handler();
|
|
|
|
// initialize variables
|
|
$ping_ping = true;
|
|
$ping_snmp = true;
|
|
|
|
$this->ping_status = 'down';
|
|
$this->ping_response = __('Ping not performed due to setting.');
|
|
$this->ping_type = $ping_type;
|
|
$this->snmp_status = 'down';
|
|
$this->snmp_response = 'SNMP not performed due to setting or ping result.';
|
|
$this->avail_method = $avail_method;
|
|
|
|
// short circuit for availability none
|
|
if ($avail_method == AVAIL_NONE) {
|
|
$this->ping_status = '0.00';
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return true;
|
|
}
|
|
|
|
$sockets_fallback = false;
|
|
|
|
// AVAIL_NONE already returned above, so it needs no second test here
|
|
if (!function_exists('socket_create')) {
|
|
$avail_method = AVAIL_SNMP;
|
|
$sockets_fallback = true;
|
|
|
|
cacti_log('WARNING: sockets support not enabled in PHP, falling back to SNMP ping');
|
|
}
|
|
|
|
if (($retries <= 0) || ($retries > 5)) {
|
|
$this->retries = 2;
|
|
} else {
|
|
$this->retries = $retries;
|
|
}
|
|
|
|
if ($timeout <= 0) {
|
|
$this->timeout = 500;
|
|
} else {
|
|
$this->timeout = $timeout;
|
|
}
|
|
|
|
// decimal precision is 0.0000
|
|
$this->precision = 5;
|
|
|
|
// snmp pinging has been selected at a minimum
|
|
$ping_result = false;
|
|
$snmp_result = false;
|
|
|
|
// icmp/udp ping test
|
|
if (($avail_method == AVAIL_SNMP_AND_PING) ||
|
|
($avail_method == AVAIL_SNMP_OR_PING) ||
|
|
($avail_method == AVAIL_PING)) {
|
|
if ($ping_type == PING_ICMP) {
|
|
$ping_result = $this->ping_icmp();
|
|
} elseif ($ping_type == PING_UDP) {
|
|
$ping_result = $this->ping_udp();
|
|
} elseif ($ping_type == PING_TCP || $ping_type == PING_TCP_CLOSED) {
|
|
$ping_result = $this->ping_tcp();
|
|
}
|
|
}
|
|
|
|
// snmp test
|
|
if (($avail_method == AVAIL_SNMP) ||
|
|
($avail_method == AVAIL_SNMP_GET_SYSDESC) ||
|
|
($avail_method == AVAIL_SNMP_GET_NEXT) ||
|
|
($avail_method == AVAIL_SNMP_AND_PING) ||
|
|
($avail_method == AVAIL_SNMP_OR_PING)) {
|
|
// If we are in AND mode and already have a failed ping result, we don't need SNMP
|
|
if (!$ping_result && $avail_method == AVAIL_SNMP_AND_PING) {
|
|
$snmp_result = $ping_result;
|
|
} else {
|
|
/* cmd.php and other reduced-host callers can omit these keys entirely,
|
|
so a missing one counts the same as an unconfigured one. */
|
|
$have_snmp = (strlen($this->host['snmp_community'] ?? '') > 0 || ($this->host['snmp_version'] ?? 0) >= 3);
|
|
|
|
if ($have_snmp && !($avail_method == AVAIL_SNMP_OR_PING && $ping_result)) {
|
|
/* Run the real SNMP test whenever a community (or v3) is configured. In OR
|
|
mode that includes the case where the ping failed, so an unreachable
|
|
host is only reported up if SNMP actually answers. */
|
|
$snmp_result = $this->ping_snmp();
|
|
} elseif ($sockets_fallback) {
|
|
/* The fallback rewrote $avail_method to AVAIL_SNMP, so reaching here means
|
|
no community is configured and there is nothing to test against. Do not
|
|
report the device up on the strength of a check that never ran. */
|
|
$snmp_result = false;
|
|
} else {
|
|
/* Nothing left to test: either a v1/v2 host with no snmp_community, or an
|
|
OR-mode host a successful ping has already proven up. */
|
|
$snmp_result = true;
|
|
$this->snmp_status = 0.000;
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->restore_cacti_error_handler();
|
|
|
|
return match ($avail_method) {
|
|
AVAIL_SNMP_OR_PING => $snmp_result || $ping_result,
|
|
AVAIL_SNMP_AND_PING => $snmp_result && $ping_result,
|
|
AVAIL_SNMP => $snmp_result,
|
|
AVAIL_SNMP_GET_NEXT => $snmp_result,
|
|
AVAIL_SNMP_GET_SYSDESC => $snmp_result,
|
|
AVAIL_PING => $ping_result,
|
|
default => false,
|
|
};
|
|
} // end_ping
|
|
|
|
function is_ipaddress(string $ip_address = '') : bool {
|
|
// check for ipv4/v6
|
|
if (function_exists('filter_var')) {
|
|
if (filter_var($ip_address, FILTER_VALIDATE_IP) !== false) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} elseif (inet_pton($ip_address) !== false) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function strip_ip_address(string $ip_address) : string {
|
|
// clean up hostname if specifying snmp_transport
|
|
if (str_contains($ip_address, 'tcp6:')) {
|
|
$ip_address = str_replace('tcp6:', '', cacti_strtolower($ip_address));
|
|
|
|
if (str_contains($ip_address, '[')) {
|
|
$parts = explode(']', $ip_address);
|
|
$ip_address = trim($parts[0], '[');
|
|
}
|
|
} elseif (str_contains($ip_address, 'udp6:')) {
|
|
$ip_address = str_replace('udp6:', '', cacti_strtolower($ip_address));
|
|
|
|
if (str_contains($ip_address, '[')) {
|
|
$parts = explode(']', $ip_address);
|
|
$ip_address = trim($parts[0], '[');
|
|
}
|
|
} elseif (str_contains($ip_address, 'tcp:')) {
|
|
$ip_address = str_replace('tcp:', '', cacti_strtolower($ip_address));
|
|
} elseif (str_contains($ip_address, 'udp:')) {
|
|
$ip_address = str_replace('udp:', '', cacti_strtolower($ip_address));
|
|
}
|
|
|
|
return $ip_address;
|
|
}
|
|
}
|