%PDF- %PDF-
Direktori : /backups/router/usr/local/opnsense/mvc/app/views/OPNsense/Diagnostics/ |
Current File : //backups/router/usr/local/opnsense/mvc/app/views/OPNsense/Diagnostics/fw_log.volt |
{# # Copyright (c) 2014-2021 Deciso B.V. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. #} <script> 'use strict'; $( document ).ready(function() { var field_type_icons = { 'binat': 'fa-exchange', 'block': 'fa-ban', 'in': 'fa-arrow-right', 'nat': 'fa-exchange', 'out': 'fa-arrow-left', 'pass': 'fa-play', 'rdr': 'fa-exchange' }; var interface_descriptions = {}; let hostnameMap = {}; /** * reverse lookup address fields (replace address part for hostname if found) */ function reverse_lookup() { let to_fetch = []; let unfinshed_lookup = false; $(".address").each(function(){ let address = $(this).data('address'); if (!hostnameMap.hasOwnProperty(address) && !to_fetch.includes(address)) { // limit dns fetches to 50 per cycle if (to_fetch.length >= 50) { unfinshed_lookup = true; return; } to_fetch.push(address); } }); let update_grid = function() { $(".address").each(function(){ if (hostnameMap.hasOwnProperty($(this).data('address'))) { $(this).text($(this).text().replace( $(this).data('address'), hostnameMap[$(this).data('address')] )); $(this).removeClass('address'); } }); }; if (to_fetch.length > 0) { ajaxGet('/api/diagnostics/dns/reverse_lookup', { 'address': to_fetch }, function(data, status) { $.each(to_fetch, function(index, address) { if (!data.hasOwnProperty(address) || data[address] === undefined) { hostnameMap[address] = address; } else { hostnameMap[address] = data[address]; } }); if (unfinshed_lookup) { // schedule next fetch reverse_lookup(); } update_grid(); }); } else { update_grid(); } } /** * set new selection * @param items list of lexical expressions * @param operator enable or disable global OR operator */ function set_selection(items, operator) { // remove old selection $("#filters > span.badge").click(); // collect valid condition types let conditions = []; $("#filter_condition > option").each(function(){ conditions.push($(this).val()); }); items.forEach(function(value) { let parts = value.split(new RegExp("("+conditions.join("|")+")(.+)$")); if (parts.length >= 3 && $("#filter_tag").val(parts[0]).val() === parts[0] ) { $("#filter_tag").val(parts[0]); $("#filter_condition").val(parts[1]); $("#filter_value").val(parts[2]); $("#add_filter_condition").click(); } else if (value.toLowerCase() == "or=1") { operator = "1"; } }); $("#filter_or_type").prop('checked', operator === "1" ? true : false); $(".selectpicker").selectpicker('refresh'); $("#filter_tag").change(); } /** * add new filters template * @param t_data template's parameters */ function addTemplate(t_data) { ajaxCall('/api/diagnostics/lvtemplate/addItem/', t_data, function(data, status) { if (data.result == "saved") { fetchTemplates(data.uuid); } else { BootstrapDialog.show({ type: BootstrapDialog.TYPE_DANGER, title: "{{ lang._('Add filters template') }}", message: "{{ lang._('Template save failed. Message: ') }}" + data.result, buttons: [{ label: "{{ lang._('Close') }}", action: function (dialogRef) { dialogRef.close(); } }] }); fetchTemplates("00000"); } }) } /** * set template new values * @param t_id template uuid * @param t_data template's parameters */ function editTemplate(t_id, t_data) { ajaxCall('/api/diagnostics/lvtemplate/setItem/' + t_id, t_data, function(data, status) { if (data.result == "saved") { fetchTemplates(t_id); } else { BootstrapDialog.show({ type: BootstrapDialog.TYPE_DANGER, title: "{{ lang._('Filters template edit') }}", message: "{{ lang._('Template edit failed. Message: ') }}" + data.result, buttons: [{ label: "{{ lang._('Close') }}", action: function (dialogRef) { dialogRef.close(); } }] }); fetchTemplates(t_id); } }) } /** * delete filters template * @param t_id template uuid */ function delTemplate(t_id) { ajaxCall('/api/diagnostics/lvtemplate/delItem/' + t_id, {}, function(data, status) { if (data.result == "deleted") { //don't reset current filters so template can be restored right after delete $("#templates option[value=" + t_id + "]").remove(); $("#templates").val("").selectpicker('refresh'); } else { BootstrapDialog.show({ type: BootstrapDialog.TYPE_DANGER, title: "{{ lang._('Filters template delete') }}", message: "{{ lang._('Template delete failed. Result: ') }}" + data.result, buttons: [{ label: "{{ lang._('Close') }}", action: function (dialogRef) { dialogRef.close(); } }] }); } }) } /** * fetch templates from config * @param opt select value to make :selected and apply */ function fetchTemplates(opt) { const t_fetched = new $.Deferred(); opt = opt || "00000"; //apply = apply || true; $('#templ_name').val(""); $('#templates').empty(); $('#templates').append($('<option/>', {value: "00000", text: "None"}).data('template', {'filters': "0", 'or': "0"}).addClass("disp_none_opt templates")); $('#templates').append($('<option/>', {value: "00001", text: "New"}).data('template', {'filters': "0", 'or': "0"}).attr('data-icon','fa-solid fa-file').addClass("add_new_opt templ_save")); $('#templates').selectpicker('refresh'); $('.templates').show(); $('.templ_save').hide(); ajaxGet('/api/diagnostics/lvtemplate/searchItem/', {}, function(data, status) { let templates = data.rows; $.each(templates, function(i, template) { $('#templates').append(template.uuid == opt ? $('<option/>', {value:template.uuid, text:template.name, selected: "selected" }).data('template', template) : $('<option/>', {value:template.uuid, text:template.name }).data('template', template)); }); $('#templates').selectpicker('refresh'); $('.badge').click(); $("#templates").change(); t_fetched.resolve(); }); return t_fetched; } function fetch_log() { let record_spec = []; // Overfetch for limited display scope to increase the chance of being able to find matches. // As we fetch once per second, we would be limited to 25 records/sec of log data when 25 is selected. let max_rows = Math.max(1000, parseInt($("#limit").val())); // read heading, contains field specs $("#grid-log > thead > tr > th ").each(function () { record_spec.push({ 'column-id': $(this).data('column-id'), 'type': $(this).data('type'), 'class': $(this).attr('class') }); }); // read last digest (record hash) from top data row var last_digest = $("#grid-log > tbody > tr:first > td:first").text(); // fetch new log lines and add on top of grid-log ajaxGet('/api/diagnostics/firewall/log/', {'digest': last_digest, 'limit': max_rows}, function(data, status) { if (status == 'error') { // stop poller on failure $("#auto_refresh").prop('checked', false); } else if (last_digest != '' && $("#grid-log > tbody > tr").length == 1){ // $("#limit").change(); called, this request should be discarted (data grid reset) return; } else if (data !== undefined && data.length > 0) { let record; let trs = []; while ((record = data.pop()) != null) { if (record['__digest__'] != last_digest) { var log_tr = $("<tr>"); if (record.interface !== undefined && interface_descriptions[record.interface] !== undefined) { record['interface_name'] = interface_descriptions[record.interface]; } else { record['interface_name'] = record.interface; } log_tr.data('details', record); log_tr.hide(); $.each(record_spec, function(idx, field){ var log_td = $('<td>').addClass(field['class']); var column_name = field['column-id']; var content = null; switch (field['type']) { case 'icon': var icon = field_type_icons[record[column_name]]; if (icon != undefined) { log_td.html('<i class="fa '+icon+'" aria-hidden="true"></i><span style="display:none">'+record[column_name]+'</span>'); } break; case 'address': log_td.text(record[column_name]); log_td.addClass('address'); log_td.data('address', record[column_name]); if (record[column_name+'port'] !== undefined) { if (record['ipversion'] == 6) { log_td.text('['+log_td.text()+']:'+record[column_name+'port']); } else { log_td.text(log_td.text()+':'+record[column_name+'port']); } } break; case 'info': log_td.html('<button class="act_info btn btn-xs fa fa-info-circle" aria-hidden="true"></i>'); break; case 'label': // record data is always html-escaped. no special protection required log_td.html(record[column_name]); break; default: if (record[column_name] != undefined) { log_td.text(record[column_name]); } } log_tr.append(log_td); }); if (record['action'] == 'pass') { log_tr.addClass('fw_pass'); } else if (record['action'] == 'block') { log_tr.addClass('fw_block'); } else if (record['action'] == 'rdr' || record['action'] == 'nat' || record['action'] == 'binat') { log_tr.addClass('fw_nat'); } trs.unshift(log_tr); } } $("#grid-log > tbody > tr:first").before(trs); // apply filter after load apply_filter(); /** * Limit output, try to keep max X records on screen. * By removing the invisible items first, we should be able to keep the filtered ones * longer in scope. In theory the number of items in memory can grow to 2 x max_rows, but * in practice that's not really an issue. */ $("#grid-log > tbody > tr:not(:visible):gt("+max_rows+")").remove(); $("#grid-log > tbody > tr:visible:gt("+max_rows+")").remove(); // bind info buttons $(".act_info").unbind('click').click(function(){ var sender_tr = $(this).parent().parent(); var sender_details = sender_tr.data('details'); var hidden_columns = ['__spec__', '__host__', '__digest__']; var map_icon = ['dir', 'action']; var sorted_keys = Object.keys(sender_details).sort(); var tbl = $('<table class="table table-condensed table-hover"/>'); var tbl_tbody = $("<tbody/>"); for (let i=0 ; i < sorted_keys.length; i++) { if (hidden_columns.indexOf(sorted_keys[i]) === -1 ) { var row = $("<tr/>"); var icon = null; if (map_icon.indexOf(sorted_keys[i]) !== -1) { if (field_type_icons[sender_details[sorted_keys[i]]] !== undefined) { icon = $("<i/>"); icon.addClass("fa fa-fw").addClass(field_type_icons[sender_details[sorted_keys[i]]]); } } row.append($("<td/>").text(sorted_keys[i])); if (sorted_keys[i] == 'rid') { // rid field, links to rule origin var rid = sender_details[sorted_keys[i]]; var rid_td = $("<td/>").addClass("act_info_fld_"+sorted_keys[i]); if (rid.length >= 32) { var rid_link = $("<a target='_blank' href='/firewall_rule_lookup.php?rid=" + rid + "'/>"); rid_link.text(rid); rid_td.append($("<i/>").addClass('fa fa-fw fa-search')); rid_td.append(rid_link); } row.append(rid_td); } else if (icon === null) { row.append($("<td/>").addClass("act_info_fld_"+sorted_keys[i]).html( sender_details[sorted_keys[i]] )); } else { row.append($("<td/>") .append(icon) .append($("<span/>").addClass("act_info_fld_"+sorted_keys[i]).text( " [" + sender_details[sorted_keys[i]] + "]" )) ); } tbl_tbody.append(row); } } tbl.append(tbl_tbody); BootstrapDialog.show({ title: "{{ lang._('Detailed rule info') }}", message: tbl, type: BootstrapDialog.TYPE_INFO, draggable: true, buttons: [{ label: '<i class="fa fa-search" aria-hidden="true"></i>', action: function(){ $(this).unbind('click'); $(".act_info_fld_src, .act_info_fld_dst").each(function(){ var target_field = $(this); ajaxGet('/api/diagnostics/dns/reverse_lookup', {'address': target_field.text()}, function(data, status) { if (data[target_field.text()] != undefined) { var resolv_output = data[target_field.text()]; if (target_field.text() != resolv_output) { target_field.text(target_field.text() + ' [' + resolv_output + ']'); } } target_field.prepend('<i class="fa fa-search" aria-hidden="true"></i> '); }); }); } },{ label: "{{ lang._('Close') }}", action: function(dialogItself){ dialogItself.close(); } }] }); }); // reverse lookup when selected if ($('#dolookup').is(':checked')) { reverse_lookup(); } } }); } // matcher function match_filter(value, condition, data) { if (data === undefined) { return false; } data = data.toLowerCase(); return (condition === '=' && data == value) || (condition === '~' && data.match(value)) || (condition === '!=' && data != value) || (condition === '!~' && !data.match(value)); } // live filter function apply_filter() { let filters = []; let visible_rows = 1; let max_rows = parseInt($("#limit").val()); $("#filters > span.badge").each(function(){ filters.push($(this).data('filter')); }); let filter_or_type = $("#filter_or_type").is(':checked'); $("#grid-log > tbody > tr").each(function(){ let selected_tr = $(this); let this_data = $(this).data('details'); if (this_data === undefined) { return; } let is_matched = (filters.length > 0) ? !filter_or_type : true; for (let i=0; i < filters.length; i++) { let filter_value = filters[i].value.toLowerCase(); let filter_condition = filters[i].condition; let this_condition_match = false; let filter_tag = filters[i].tag; if (filter_tag === '__addr__') { let src_match = match_filter(filter_value, filter_condition, this_data['src']); let dst_match = match_filter(filter_value, filter_condition, this_data['dst']); if (!filter_condition.match('!')) { this_condition_match = src_match || dst_match; } else { this_condition_match = src_match && dst_match; } } else if (filter_tag === '__port__') { let srcport_match = match_filter(filter_value, filter_condition, this_data['srcport']); let dstport_match = match_filter(filter_value, filter_condition, this_data['dstport']); if (!filter_condition.match('!')) { this_condition_match = srcport_match || dstport_match; } else { this_condition_match = srcport_match && dstport_match; } } else { this_condition_match = match_filter(filter_value, filter_condition, this_data[filter_tag]); } if (!this_condition_match && !filter_or_type) { // normal AND condition, exit when one of the criteria is not met is_matched = this_condition_match; break; } else if (filter_or_type) { // or condition is deselected by default is_matched = is_matched || this_condition_match; } } if (is_matched && visible_rows <= max_rows) { selected_tr.show(); visible_rows += 1; } else { selected_tr.hide(); } }); } $("#add_filter_condition").click(function(){ if ($("#filter_value").val() === "") { return; } let $new_filter = $("<span/>").addClass("badge"); $new_filter.data('filter', { tag:$("#filter_tag").val(), condition:$("#filter_condition").val(), value:$("#filter_value").val(), } ); $new_filter.text($("#filter_tag").val() + $("#filter_condition").val() + $("#filter_value").val()); $new_filter.click(function(){ $("#filter_tag").val($(this).data('filter').tag); $("#filter_condition").val($(this).data('filter').condition); $("#filter_value").val($(this).data('filter').value); $(this).remove(); if ($("#filters > span.badge").length == 0) { $("#filters_help").hide(); } $('.selectpicker').selectpicker('refresh'); $("#filter_tag").change(); apply_filter(); }); $("#filters").append($new_filter); $("#filter_value").val(""); $("#filters_help").show(); apply_filter(); }); $("#filter_or_type").click(function(){ apply_filter(); }); // reset log content on limit change, forces a reload $("#limit").change(function(){ $('#grid-log > tbody').html("<tr></tr>"); }); function poller() { if ($("#auto_refresh").is(':checked')) { fetch_log(); } setTimeout(poller, 1000); } // manual refresh $("#refresh").click(function(){ fetch_log(); }); // templates actions $("#templates").change(function () { if ($('#templ_save_start').is(':visible')) { //apply chosen template let t_data = $(this).find('option:selected').data('template') ? $(this).find('option:selected').data('template') : {'filters': "0", 'or': "0"}; set_selection(t_data.filters.split(','), t_data.or); } else { //choose template to modify or create new one. Show Name input if New option clicked if ($('#templates').val() === "00001") { $('#templates').selectpicker('hide'); $('#templ_name').show().focus(); } } }); $('#templ_save_start').click(function () { if ($(".badge").text() == '') { BootstrapDialog.show({ type: BootstrapDialog.TYPE_DANGER, title: "{{ lang._('Save filters template') }}", message: "{{ lang._('Filters not set') }}", buttons: [{ label: "{{ lang._('Close') }}", action: function (dialogRef) { dialogRef.close(); } }] }); } else { $('.templates').hide(); $('.templ_save').show(); $('#templ_name').focus(); if ($("#templates option").length == 3){ //no stored templates. skip to new template name $('#templates').val("00001").selectpicker('refresh').change(); } } }); $("#templ_save_cancel").click(function () { $('#templ_name').val("").hide(); $('.templ_save').hide(); $('.templates').show(); $('#templates').val('').selectpicker('refresh').selectpicker('show'); }); $("#templ_name").on('keyup', function (e) { if (e.key === 'Enter' || e.keyCode === 13) { $('#templ_save_apply').click(); } else if (e.keyCode === 27) { $('#templ_name').val("").hide(); $('#templates').val('').selectpicker('refresh').selectpicker('show'); } }); $("#templ_save_apply").click(function () { let fltrs = ""; $('.badge').each(function () { fltrs += $(this).text() + ","; }); fltrs = fltrs.slice(0, -1); let or = $('#filter_or_type').prop("checked") ? "1" : "0"; let t_data = { 'template': { 'filters': fltrs, 'or': or } }; $('#templates').selectpicker('refresh').selectpicker('show'); if ($("#templ_name").val().length >= 1 && $("#templ_name").is(':visible')) { //new template t_data.template.name = $("#templ_name").val(); $('#templ_name').val("").hide(); addTemplate(t_data); } else if ($("#templ_name").val().length == 0 && $("#templ_name").is(':hidden') && $("#templates").val().length == 36) { //edit template let t_id = $("#templates").val(); t_data.template.name = $("#templates option:selected").text(); editTemplate(t_id, t_data); } }); $("#template_delete").click(function () { let t_id = $('#templates').val(); if (t_id.length == 36) { delTemplate(t_id); } }); // fetch interface mappings on load ajaxGet('/api/diagnostics/interface/getInterfaceNames', {}, function(data, status) { interface_descriptions = data; /** * fetch log "static" dropdown filters and add logic */ ajaxGet('/api/diagnostics/firewall/log_filters', {}, function(data, status) { if (data.action !== undefined) { let filter_value_items = $("#filter_value_items"); let filter_value = $("#filter_value"); filter_value_items.data('filters', data); filter_value_items.change(function(){ filter_value.val($(this).val()) }); $("#filter_tag").change(function(){ let filters = filter_value_items.data('filters'); let filter = $("#filter_tag").val(); filter_value.hide(); filter_value_items.parent().hide(); if (filters[filter] !== undefined) { filter_value_items.parent().show(); filter_value_items.empty(); for (let i = 0 ; i < filters[filter].length ; ++i) { let filter_opt = $("<option/>").text(filters[filter][i]); if (filter_value.val() == filters[filter][i]) { filter_opt.prop('selected', true); } filter_value_items.append(filter_opt); } filter_value_items.selectpicker('refresh'); filter_value_items.change(); } else { filter_value.show(); } }).change(); fetchTemplates("00000").done(function() { // get and apply url params. ie11 compat set_selection(window.location.search.substring(1).split("&"), "0"); }); } }); }); // startup poller poller(); }); </script> <style> .data-center { text-align: center !important; } .table > tbody > tr > td { padding-top: 1px !important; padding-bottom: 1px !important; } .act_info { cursor: pointer; } .fw_pass { background: rgba(5, 142, 73, 0.3); } .fw_block { background: rgba(235, 9, 9, 0.3); } .fw_nat { background: rgba(73, 173, 255, 0.3); } </style> <div class="content-box"> <div class="content-box-main"> <div class="table-responsive"> <div class="col-xs-12"> <div class="col-lg-7 col-sm-12"> <table class="table table-condensed"> <tbody> <tr> <td style="width:125px;"> <select id="filter_tag" class="selectpicker" data-width="120px"> <option value="action">{{ lang._('action') }}</option> <option value="interface_name">{{ lang._('interface') }}</option> <option value="dir">{{ lang._('dir') }}</option> <option value="__timestamp__">{{ lang._('Time') }}</option> <option value="src">{{ lang._('src') }}</option> <option value="srcport">{{ lang._('src_port') }}</option> <option value="dst">{{ lang._('dst') }}</option> <option value="dstport">{{ lang._('dst_port') }}</option> <option value="__addr__">{{ lang._('address') }}</option> <option value="__port__">{{ lang._('port') }}</option> <option value="protoname">{{ lang._('protoname') }}</option> <option value="label">{{ lang._('label') }}</option> <option value="rid">{{ lang._('rule id') }}</option> <option value="tcpflags">{{ lang._('tcpflags') }}</option> </select> </td> <td style="width:125px;"> <select id="filter_condition" class="selectpicker" data-width="120px"> <option value="~" selected=selected>{{ lang._('contains') }}</option> <option value="=">{{ lang._('is') }}</option> <option value="!~">{{ lang._('does not contain') }}</option> <option value="!=">{{ lang._('is not') }}</option> </select> </td> <td style="width:200px;"> <input type="text" id="filter_value"></input> <div> <select id="filter_value_items" class="selectpicker" data-width="200px"></select> </div> </td> <td> <button class="btn" id="add_filter_condition" aria-hidden="true"> <i class="fa fa-plus"></i> </button> </td> </tr> <tr> <td colspan="4"> <div id="filters"> </div> <div id="filters_help" style="display:none; padding-top:5px"> <small>{{ lang._('click on badge to remove filter') }}</small> </div> </td> </tr> </tbody> <tfoot> <tr> <td colspan="4"> <label> <input id="filter_or_type" type="checkbox"> {{ lang._('Select any of given criteria (or)') }} </label> </td> </tr> </tfoot> </table> </div> <div class="col-lg-5 col-sm-12"> <div class="pull-left" style="padding-bottom: 5px;padding-right: 5px;"> <button type="button" class="btn btn-default templates" title="Save the current set of filters" id="templ_save_start"> <span class="fa fa-angle-double-right"></span> </button> <button type="button" class="btn btn-default templ_save" title="Cancel" id="templ_save_cancel"> <span class="fa fa-times"></span> </button> <div style="display: inline-block;vertical-align: top;"> <select id="templates" class="selectpicker" title="Choose template" data-width="200"></select> <input type="text" id="templ_name" placeholder="Template name" style="width:200px;vertical-align:middle;display:none;"> </div> <button type="button" class="btn btn-default templ_save" title="Save template" id="templ_save_apply"> <span class="fa fa-save"></span> </button> <span class="templates"> <button id="template_delete" type="button" class="btn btn-default" title="Delete selected template"> <span class="fa fa-trash"></span> </button> </span> </div> <div class="pull-right"> <div class="checkbox-inline"> <label> <input id="auto_refresh" type="checkbox" checked="checked"> <span class="fa fa-refresh"></span> {{ lang._('Auto refresh') }} </label> <br/> <label> <input id="dolookup" type="checkbox"> <span class="fa fa-search"></span> {{ lang._('Lookup hostnames') }} </label> </div><br/> <select id="limit" class="selectpicker" data-width="150" > <option value="25" selected="selected">25</option> <option value="50">50</option> <option value="100">100</option> <option value="250">250</option> <option value="500">500</option> <option value="1000">1000</option> <option value="2500">2500</option> <option value="5000">5000</option> <option value="10000">10000</option> <option value="20000">20000</option> </select> <button id="refresh" type="button" class="btn btn-default"> <span class="fa fa-refresh"></span> </button> </div> </div> </div> <div class="col-xs-12"> <hr/> <div class="table-responsive"> <table id="grid-log" class="table table-condensed table-responsive"> <thead> <tr> <th class="hidden" data-column-id="__digest__" data-type="string">{{ lang._('Hash') }}</th> <th class="data-center" data-column-id="action" data-type="icon"></th> <th data-column-id="interface_name" data-type="interface">{{ lang._('Interface') }}</th> <th data-column-id="dir" data-type="icon"></th> <th data-column-id="__timestamp__" data-type="string">{{ lang._('Time') }}</th> <th data-column-id="src" data-type="address">{{ lang._('Source') }}</th> <th data-column-id="dst" data-type="address">{{ lang._('Destination') }}</th> <th data-column-id="protoname" data-type="string">{{ lang._('Proto') }}</th> <th data-column-id="label" data-type="label">{{ lang._('Label') }}</th> <th data-column-id="" data-type="info" style="width:20px;"></th> </tr> </thead> <tbody> <tr></tr> </tbody> </table> <br/> </div> </div> </div> </div> </div>