From d7c31e0ae366749ab4648668f7ac01eb7eb6ff3d Mon Sep 17 00:00:00 2001 From: bnerickson Date: Thu, 7 Mar 2024 10:27:26 -0800 Subject: [PATCH] Wireguard application graph cleanup and new wireguard interface/global metrics. (#15847) --- .../application/wireguard-common.inc.php | 6 + .../graphs/application/wireguard_time.inc.php | 81 +++++++---- .../application/wireguard_traffic.inc.php | 69 +++++---- .../html/pages/device/apps/wireguard.inc.php | 126 ++++++++++++---- .../polling/applications/wireguard.inc.php | 135 +++++++++++++++--- tests/data/linux_wireguard-v1.json | 60 +++++--- tests/snmpsim/linux_wireguard-v1.snmprec | 2 +- 7 files changed, 356 insertions(+), 123 deletions(-) create mode 100644 includes/html/graphs/application/wireguard-common.inc.php diff --git a/includes/html/graphs/application/wireguard-common.inc.php b/includes/html/graphs/application/wireguard-common.inc.php new file mode 100644 index 0000000000..28d880b028 --- /dev/null +++ b/includes/html/graphs/application/wireguard-common.inc.php @@ -0,0 +1,6 @@ +app_id, $name); - $interface_client = $interface_client_list[0] ?? ''; -} +require 'wireguard-common.inc.php'; $unit_text = 'Minutes'; +$interface_client_map = $app->data['mappings'] ?? []; $colours = 'psychedelic'; +$metric_desc = 'Last Handshake'; +$metric_name = 'minutes_since_last_handshake'; +$rrd_list = []; +$rrdArray = []; +$scale_min = 0; +$unitlen = 7; -$rrdArray = [ - 'minutes_since_last_handshake' => ['descr' => 'Last Handshake'], -]; - -$rrd_filename = Rrd::name($device['hostname'], [ - $polling_type, - $name, - $app->app_id, - $interface_client, -]); - -if (Rrd::checkRrdExists($rrd_filename)) { - foreach ($rrdArray as $rrdVar => $rrdValues) { - $rrd_list[] = [ - 'filename' => $rrd_filename, - 'descr' => $rrdValues['descr'], - 'ds' => $rrdVar, - ]; +if (isset($vars['interface']) && isset($vars['client'])) { + // This section draws the individual graphs in the device application page + // displaying the SPECIFIED wireguard interface and client metric. + $wg_intf_client = $vars['interface'] . '-' . $vars['client']; + $rrdArray[$wg_intf_client] = [ + $metric_name => ['descr' => $metric_desc], + ]; +} elseif (! isset($vars['interface']) && ! isset($vars['client'])) { + // This section draws the graph for the application-specific pages + // displaying ALL wireguard interfaces' clients' metrics. + foreach ($interface_client_map as $wg_intf => $wg_client_list) { + foreach ($wg_client_list as $wg_client) { + $wg_intf_client = $wg_intf . '-' . $wg_client; + $rrdArray[$wg_intf_client] = [ + $metric_name => [ + 'descr' => $wg_intf_client . ' ' . $metric_desc, + ], + ]; + } + } +} + +if (! $rrdArray) { + graph_error('No Data to Display', 'No Data'); +} + +$i = 0; +foreach ($rrdArray as $wg_intf_client => $wg_metric) { + $rrd_filename = Rrd::name($device['hostname'], [ + $polling_type, + $name, + $app->app_id, + $wg_intf_client, + ]); + + if (Rrd::checkRrdExists($rrd_filename)) { + $rrd_list[$i]['filename'] = $rrd_filename; + $rrd_list[$i]['descr'] = $wg_metric[$metric_name]['descr']; + $rrd_list[$i]['ds'] = $metric_name; + $i++; + } else { + graph_error('No Data file ' . basename($rrd_filename), 'No Data'); } -} else { - d_echo('RRD ' . $rrd_filename . ' not found'); } require 'includes/html/graphs/generic_multi_line_exact_numbers.inc.php'; diff --git a/includes/html/graphs/application/wireguard_traffic.inc.php b/includes/html/graphs/application/wireguard_traffic.inc.php index a5d0b39d84..4cc4a919d3 100644 --- a/includes/html/graphs/application/wireguard_traffic.inc.php +++ b/includes/html/graphs/application/wireguard_traffic.inc.php @@ -1,44 +1,59 @@ app_id, $name); - $interface_client = $interface_client_list[0] ?? ''; -} - -$unit_text = 'Bytes'; - -$ds_in = 'bytes_rcvd'; -$in_text = 'Rcvd'; -$ds_out = 'bytes_sent'; -$out_text = 'Sent'; +require 'wireguard-common.inc.php'; +$unit_text = 'Bytes/s'; $format = 'bytes'; $print_total = true; - +$in_text = 'In'; +$out_text = 'Out'; $colour_area_in = 'FF3300'; $colour_line_in = 'FF0000'; $colour_area_out = 'FF6633'; $colour_line_out = 'CC3300'; - $colour_area_in_max = 'FF6633'; $colour_area_out_max = 'FF9966'; -$rrd_filename = Rrd::name($device['hostname'], [ - $polling_type, - $name, - $app->app_id, - $interface_client, -]); +if (! isset($vars['interface']) && ! isset($vars['client'])) { + // This section is called if we're being asked to graph + // the host's wireguard global metrics. + $ds_in = 'bytes_rcvd_total'; + $ds_out = 'bytes_sent_total'; +} elseif (isset($vars['interface']) && isset($vars['client'])) { + // This section is called if we're being asked to graph + // a wireguard interface's client metrics. + $flattened_name = $vars['interface'] . '-' . $vars['client']; + $ds_in = 'bytes_rcvd'; + $ds_out = 'bytes_sent'; +} elseif (isset($vars['interface'])) { + // This section is called if we're being asked to graph + // a wireguard interface's metrics. + $flattened_name = $vars['interface']; + $ds_in = 'bytes_rcvd_total_intf'; + $ds_out = 'bytes_sent_total_intf'; +} + +if (! isset($vars['interface']) && ! isset($vars['client'])) { + $rrd_filename = Rrd::name($device['hostname'], [ + $polling_type, + $name, + $app->app_id, + ]); +} elseif (isset($vars['interface'])) { + $rrd_filename = Rrd::name($device['hostname'], [ + $polling_type, + $name, + $app->app_id, + $flattened_name, + ]); +} + +if (! isset($rrd_filename)) { + graph_error('No Data to Display', 'No Data'); +} if (! Rrd::checkRrdExists($rrd_filename)) { - d_echo('RRD ' . $rrd_filename . ' not found'); + graph_error('No Data file ' . basename($rrd_filename), 'No Data'); } require 'includes/html/graphs/generic_duplex.inc.php'; diff --git a/includes/html/pages/device/apps/wireguard.inc.php b/includes/html/pages/device/apps/wireguard.inc.php index 4f115f0a13..d564deea5e 100644 --- a/includes/html/pages/device/apps/wireguard.inc.php +++ b/includes/html/pages/device/apps/wireguard.inc.php @@ -1,5 +1,42 @@ +
+

' . + $gtext . + '

+
+
+
'; + include 'includes/html/print-graphrow.inc.php'; + echo '
'; + echo '
'; + echo ''; +} + $link_array = [ 'page' => 'device', 'device' => $device['device_id'], @@ -7,14 +44,21 @@ $link_array = [ 'app' => 'wireguard', ]; +$interface_client_map = $app->data['mappings'] ?? []; +$graph_map = [ + 'interface' => [ + 'clients' => [], + 'total' => [], + ], + 'total' => [], +]; + print_optionbar_start(); echo generate_link('All Interfaces', $link_array); echo ' | Interfaces: '; -$interface_client_map = $app->data['mappings'] ?? []; - -// generate interface links +// generate interface links on the host application page $i = 0; foreach ($interface_client_map as $interface => $client_list) { $label = @@ -32,14 +76,25 @@ foreach ($interface_client_map as $interface => $client_list) { print_optionbar_end(); -// build the interface/client -> graph map +// generates the global wireguard graph mapping +if (! isset($vars['interface'])) { + $graph_map['total'] = [ + 'wireguard_traffic' => 'Wireguard Total Traffic', + ]; +} + foreach ($interface_client_map as $interface => $client_list) { if ( ! isset($vars['interface']) || (isset($vars['interface']) && $interface == $vars['interface']) ) { + // generates the interface graph mapping + $graph_map['interface']['total'][$interface] = [ + 'wireguard_traffic' => $interface . ' ' . 'Total Traffic', + ]; foreach ($client_list as $client) { - $interface_client_map[$interface][$client] = [ + // generates the interface+client graph mapping + $graph_map['interface']['clients'][$interface][$client] = [ 'wireguard_traffic' => $interface . ' ' . $client . ' Traffic', 'wireguard_time' => $interface . ' ' . @@ -50,29 +105,44 @@ foreach ($interface_client_map as $interface => $client_list) { } } -// generate graphs on a per-interface, per-client basis -foreach ($interface_client_map as $interface => $client_list) { - foreach ($client_list as $client => $graphs) { - foreach ($graphs as $gtype => $gtext) { - $graph_type = $gtype; - $graph_array['height'] = '100'; - $graph_array['width'] = '215'; - $graph_array['to'] = time(); - $graph_array['id'] = $app['app_id']; - $graph_array['type'] = 'application_' . $gtype; - $graph_array['interface'] = $interface; - $graph_array['client'] = $client; - - echo '
-
-

' . $gtext . '

-
-
-
'; - include 'includes/html/print-graphrow.inc.php'; - echo '
'; - echo '
'; - echo '
'; +// print graphs +foreach ($graph_map as $category => $category_map) { + foreach ($category_map as $subcategory => $subcategory_map) { + if ($category === 'total') { + // print graphs for global wireguard metrics + wireguard_graph_printer( + $subcategory, + $app['app_id'], + null, + null, + $subcategory_map + ); + } elseif ($category === 'interface') { + foreach ($subcategory_map as $interface => $interface_map) { + foreach ($interface_map as $client => $client_map) { + if ($subcategory === 'total') { + // print graphs for wireguard interface metrics + wireguard_graph_printer( + $client, + $app['app_id'], + $interface, + null, + $client_map + ); + } elseif ($subcategory === 'clients') { + foreach ($client_map as $gtype => $gtext) { + // print graphs for wireguard interface+client metrics + wireguard_graph_printer( + $gtype, + $app['app_id'], + $interface, + $client, + $gtext + ); + } + } + } + } } } } diff --git a/includes/polling/applications/wireguard.inc.php b/includes/polling/applications/wireguard.inc.php index 6c6b1ea4f6..fccdb64be6 100644 --- a/includes/polling/applications/wireguard.inc.php +++ b/includes/polling/applications/wireguard.inc.php @@ -13,37 +13,72 @@ try { } catch (JsonAppMissingKeysException $e) { $interface_client_map = $e->getParsedJson(); } catch (JsonAppException $e) { - echo PHP_EOL . $name . ':' . $e->getCode() . ':' . $e->getMessage() . PHP_EOL; + echo PHP_EOL . + $name . + ':' . + $e->getCode() . + ':' . + $e->getMessage() . + PHP_EOL; update_application($app, $e->getCode() . ':' . $e->getMessage(), []); return; } -$rrd_name = [$polling_type, $name, $app->app_id]; -$rrd_def = RrdDefinition::make() +// RRD definition for interface+client metrics. +$rrd_def_intfclient = RrdDefinition::make() ->addDataset('bytes_rcvd', 'DERIVE', 0) ->addDataset('bytes_sent', 'DERIVE', 0) ->addDataset('minutes_since_last_handshake', 'GAUGE', 0); +// RRD definition for interface metrics. +$rrd_def_intf = RrdDefinition::make() + ->addDataset('bytes_rcvd_total_intf', 'DERIVE', 0) + ->addDataset('bytes_sent_total_intf', 'DERIVE', 0); + +// RRD definition for global wireguard metrics. +$rrd_def_total = RrdDefinition::make() + ->addDataset('bytes_rcvd_total', 'DERIVE', 0) + ->addDataset('bytes_sent_total', 'DERIVE', 0); + $metrics = []; $mappings = []; +$bytes_rcvd_total = null; +$bytes_sent_total = null; + // Parse json data for interfaces and their respective clients' metrics. +// Add any relevant data to the interface and global metrics within. foreach ($interface_client_map as $interface => $client_list) { - $finterface = is_string($interface) ? filter_var($interface, FILTER_SANITIZE_STRING) : null; + $bytes_rcvd_total_intf = null; + $bytes_sent_total_intf = null; + + $finterface = is_string($interface) + ? filter_var($interface, FILTER_SANITIZE_STRING) + : null; if (is_null($finterface)) { - echo PHP_EOL . $name . ':' . ' Invalid or no interface found.' . PHP_EOL; + echo PHP_EOL . + $name . + ':' . + ' Invalid or no interface found.' . + PHP_EOL; continue; } $mappings[$finterface] = []; foreach ($client_list as $client => $client_data) { - $fclient = is_string($client) ? filter_var($client, FILTER_SANITIZE_STRING) : null; + $fclient = is_string($client) + ? filter_var($client, FILTER_SANITIZE_STRING) + : null; if (is_null($fclient)) { - echo PHP_EOL . $name . ':' . ' Invalid or no client found.' . PHP_EOL; + echo PHP_EOL . + $name . + ':' . + ' Invalid or no client found.' . + PHP_EOL; continue; } @@ -55,28 +90,80 @@ foreach ($interface_client_map as $interface => $client_list) { $bytes_sent = is_int($client_data['bytes_sent']) ? $client_data['bytes_sent'] : null; - $minutes_since_last_handshake = is_int($client_data['minutes_since_last_handshake']) + $minutes_since_last_handshake = is_int( + $client_data['minutes_since_last_handshake'] + ) ? $client_data['minutes_since_last_handshake'] : null; - $fields = [ + if (is_int($bytes_rcvd)) { + $bytes_rcvd_total_intf += $bytes_rcvd; + $bytes_rcvd_total += $bytes_rcvd; + } + + if (is_int($bytes_sent)) { + $bytes_sent_total_intf += $bytes_sent; + $bytes_sent_total += $bytes_sent; + } + + $fields_intfclient = [ 'bytes_rcvd' => $bytes_rcvd, 'bytes_sent' => $bytes_sent, 'minutes_since_last_handshake' => $minutes_since_last_handshake, ]; // create flattened metrics - $metrics[$finterface . '_' . $fclient] = $fields; - $tags = [ + $metrics['intf_' . $finterface . '_client_' . $fclient] = $fields_intfclient; + $tags_intfclient = [ 'name' => $name, 'app_id' => $app->app_id, - 'rrd_def' => $rrd_def, - 'rrd_name' => [$polling_type, $name, $app->app_id, $finterface, $fclient], + 'rrd_def' => $rrd_def_intfclient, + 'rrd_name' => [ + $polling_type, + $name, + $app->app_id, + $finterface, + $fclient, + ], ]; - data_update($device, $polling_type, $tags, $fields); + data_update($device, $polling_type, $tags_intfclient, $fields_intfclient); } + + // create interface fields + $fields_intf = [ + 'bytes_rcvd_total_intf' => $bytes_rcvd_total_intf, + 'bytes_sent_total_intf' => $bytes_sent_total_intf, + ]; + + // create interface metrics + $metrics['intf_' . $finterface] = $fields_intf; + + $tags_intf = [ + 'name' => $name, + 'app_id' => $app->app_id, + 'rrd_def' => $rrd_def_intf, + 'rrd_name' => [$polling_type, $name, $app->app_id, $finterface], + ]; + data_update($device, $polling_type, $tags_intf, $fields_intf); } +// create total fields +$fields_all = [ + 'bytes_rcvd_total' => $bytes_rcvd_total, + 'bytes_sent_total' => $bytes_sent_total, +]; + +// create total metrics +$metrics['global'] = $fields_all; + +$tags_all = [ + 'name' => $name, + 'app_id' => $app->app_id, + 'rrd_def' => $rrd_def_total, + 'rrd_name' => [$polling_type, $name, $app->app_id], +]; +data_update($device, $polling_type, $tags_all, $fields_all); + // variable tracks whether we updated mappings so it only happens once $mappings_updated = false; @@ -90,8 +177,14 @@ if (count($added_interfaces) > 0 || count($removed_interfaces) > 0) { $app->data = ['mappings' => $mappings]; $mappings_updated = true; $log_message = 'Wireguard Interfaces Change:'; - $log_message .= count($added_interfaces) > 0 ? ' Added ' . implode(',', $added_interfaces) : ''; - $log_message .= count($removed_interfaces) > 0 ? ' Removed ' . implode(',', $removed_interfaces) : ''; + $log_message .= + count($added_interfaces) > 0 + ? ' Added ' . implode(',', $added_interfaces) + : ''; + $log_message .= + count($removed_interfaces) > 0 + ? ' Removed ' . implode(',', $removed_interfaces) + : ''; log_event($log_message, $device, 'application'); } @@ -107,8 +200,14 @@ foreach ($mappings as $interface => $client_list) { $mappings_updated = true; } $log_message = 'Wireguard Interface ' . $interface . ' Clients Change:'; - $log_message .= count($added_clients) > 0 ? ' Added ' . implode(',', $added_clients) : ''; - $log_message .= count($removed_clients) > 0 ? ' Removed ' . implode(',', $removed_clients) : ''; + $log_message .= + count($added_clients) > 0 + ? ' Added ' . implode(',', $added_clients) + : ''; + $log_message .= + count($removed_clients) > 0 + ? ' Removed ' . implode(',', $removed_clients) + : ''; log_event($log_message, $device, 'application'); } } diff --git a/tests/data/linux_wireguard-v1.json b/tests/data/linux_wireguard-v1.json index 54b42fff60..f969fd45d7 100644 --- a/tests/data/linux_wireguard-v1.json +++ b/tests/data/linux_wireguard-v1.json @@ -50,91 +50,115 @@ ], "application_metrics": [ { - "metric": "wg0_client1.domain.com_bytes_rcvd", - "value": 1534068, + "metric": "global_bytes_rcvd_total", + "value": 14891148, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_client1.domain.com_bytes_sent", - "value": 97772, + "metric": "global_bytes_sent_total", + "value": 105501824, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_client1.domain.com_minutes_since_last_handshake", - "value": 1, + "metric": "intf_wg0_bytes_rcvd_total_intf", + "value": 14891148, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_client2_bytes_rcvd", + "metric": "intf_wg0_bytes_sent_total_intf", + "value": 105501824, + "value_prev": null, + "app_type": "wireguard" + }, + { + "metric": "intf_wg0_client_client1.domain.com_bytes_rcvd", "value": 0, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_client2_bytes_sent", + "metric": "intf_wg0_client_client1.domain.com_bytes_sent", "value": 0, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_client2_minutes_since_last_handshake", + "metric": "intf_wg0_client_client1.domain.com_minutes_since_last_handshake", "value": 0, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_computer_bytes_rcvd", + "metric": "intf_wg0_client_client2_bytes_rcvd", "value": 0, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_computer_bytes_sent", + "metric": "intf_wg0_client_client2_bytes_sent", "value": 0, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_computer_minutes_since_last_handshake", + "metric": "intf_wg0_client_client2_minutes_since_last_handshake", "value": 0, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_it_admin.domain.org_bytes_rcvd", + "metric": "intf_wg0_client_computer_bytes_rcvd", + "value": 14891148, + "value_prev": null, + "app_type": "wireguard" + }, + { + "metric": "intf_wg0_client_computer_bytes_sent", + "value": 105501824, + "value_prev": null, + "app_type": "wireguard" + }, + { + "metric": "intf_wg0_client_computer_minutes_since_last_handshake", + "value": 2, + "value_prev": null, + "app_type": "wireguard" + }, + { + "metric": "intf_wg0_client_it_admin.domain.org_bytes_rcvd", "value": 0, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_it_admin.domain.org_bytes_sent", + "metric": "intf_wg0_client_it_admin.domain.org_bytes_sent", "value": 0, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_it_admin.domain.org_minutes_since_last_handshake", + "metric": "intf_wg0_client_it_admin.domain.org_minutes_since_last_handshake", "value": 0, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_my_phone_bytes_rcvd", + "metric": "intf_wg0_client_my_phone_bytes_rcvd", "value": 0, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_my_phone_bytes_sent", + "metric": "intf_wg0_client_my_phone_bytes_sent", "value": 0, "value_prev": null, "app_type": "wireguard" }, { - "metric": "wg0_my_phone_minutes_since_last_handshake", + "metric": "intf_wg0_client_my_phone_minutes_since_last_handshake", "value": 0, "value_prev": null, "app_type": "wireguard" diff --git a/tests/snmpsim/linux_wireguard-v1.snmprec b/tests/snmpsim/linux_wireguard-v1.snmprec index 4a7e183b53..081bb5091e 100644 --- a/tests/snmpsim/linux_wireguard-v1.snmprec +++ b/tests/snmpsim/linux_wireguard-v1.snmprec @@ -7,4 +7,4 @@ 1.3.6.1.2.1.25.1.1.0|67|77552962 1.3.6.1.4.1.8072.1.3.2.2.1.21.6.100.105.115.116.114.111|2|1 1.3.6.1.4.1.8072.1.3.2.2.1.21.9.119.105.114.101.103.117.97.114.100|2|1 -1.3.6.1.4.1.8072.1.3.2.3.1.2.9.119.105.114.101.103.117.97.114.100|4x|7b226572726f72537472696e67223a2022222c20226572726f72223a20302c202276657273696f6e223a20312c202264617461223a207b22776730223a207b22636c69656e74312e646f6d61696e2e636f6d223a207b226d696e757465735f73696e63655f6c6173745f68616e647368616b65223a20312c202262797465735f72637664223a20313533343036382c202262797465735f73656e74223a2039373737327d2c2022636c69656e7432223a207b226d696e757465735f73696e63655f6c6173745f68616e647368616b65223a206e756c6c2c202262797465735f72637664223a20302c202262797465735f73656e74223a20307d2c20226d795f70686f6e65223a207b226d696e757465735f73696e63655f6c6173745f68616e647368616b65223a206e756c6c2c202262797465735f72637664223a20302c202262797465735f73656e74223a20307d2c202269745f61646d696e2e646f6d61696e2e6f7267223a207b226d696e757465735f73696e63655f6c6173745f68616e647368616b65223a206e756c6c2c202262797465735f72637664223a20302c202262797465735f73656e74223a20307d2c2022636f6d7075746572223a207b226d696e757465735f73696e63655f6c6173745f68616e647368616b65223a206e756c6c2c202262797465735f72637664223a20302c202262797465735f73656e74223a20307d7d7d7d0a +1.3.6.1.4.1.8072.1.3.2.3.1.2.9.119.105.114.101.103.117.97.114.100|4x|7b226572726f72537472696e67223a2022222c20226572726f72223a20302c202276657273696f6e223a20312c202264617461223a207b22776730223a207b22636c69656e74312e646f6d61696e2e636f6d223a207b226d696e757465735f73696e63655f6c6173745f68616e647368616b65223a206e756c6c2c202262797465735f72637664223a20302c202262797465735f73656e74223a20307d2c2022636c69656e7432223a207b226d696e757465735f73696e63655f6c6173745f68616e647368616b65223a206e756c6c2c202262797465735f72637664223a20302c202262797465735f73656e74223a20307d2c20226d795f70686f6e65223a207b226d696e757465735f73696e63655f6c6173745f68616e647368616b65223a206e756c6c2c202262797465735f72637664223a20302c202262797465735f73656e74223a20307d2c202269745f61646d696e2e646f6d61696e2e6f7267223a207b226d696e757465735f73696e63655f6c6173745f68616e647368616b65223a206e756c6c2c202262797465735f72637664223a20302c202262797465735f73656e74223a20307d2c2022636f6d7075746572223a207b226d696e757465735f73696e63655f6c6173745f68616e647368616b65223a20322c202262797465735f72637664223a2031343839313134382c202262797465735f73656e74223a203130353530313832347d7d7d7d0a