diff --git a/httpdocs/dist b/httpdocs/dist index c3958e30a3..ce9538786a 160000 --- a/httpdocs/dist +++ b/httpdocs/dist @@ -1 +1 @@ -Subproject commit c3958e30a3061e09a876e5d042d867d32113f6f7 +Subproject commit ce9538786a95b2dd5914b7c4934aa6d372e59f13 diff --git a/scripts/locales/en.lua b/scripts/locales/en.lua index 4775900562..74218237d5 100644 --- a/scripts/locales/en.lua +++ b/scripts/locales/en.lua @@ -9477,6 +9477,7 @@ local lang = { ["storage_utilization"] = "Storage Utilization", ["storage_utilization_pcap"] = "PCAP Storage Utilization", ["traffic_extraction_jobs"] = "Traffic Extraction Jobs", + ["view_extraction_note"] = "Traffic recording is enabled on the following viewed interfaces: %{interfaces}. Packet extractions will extract traffic from all of them.", ["traffic_extractions"] = "Extractions", ["traffic_on_disk"] = "Traffic On Disk", ["traffic_recording"] = "Traffic Recording", diff --git a/scripts/lua/if_stats.lua b/scripts/lua/if_stats.lua index 98f613b7f6..cb5aa8c0d1 100644 --- a/scripts/lua/if_stats.lua +++ b/scripts/lua/if_stats.lua @@ -297,10 +297,17 @@ if _SERVER["REQUEST_METHOD"] == "POST" and not isEmptyString(_POST["traffic_reco recording_utils.setCurrentTrafficRecordingProvider(ifstats.id, _POST["traffic_recording_provider"]) end -local has_traffic_recording_page = (recording_utils.isAvailable() and not interface.isView() and - (is_packet_interface - or ((recording_utils.isSupportedZMQInterface(ifid) and not table.empty(ext_interfaces))) or - (recording_utils.getCurrentTrafficRecordingProvider(ifid) ~= "ntopng"))) +local viewed_ifaces_with_recording = {} +if interface.isView() then + viewed_ifaces_with_recording = recording_utils.getViewedInterfacesWithRecording(ifid) +end + +local has_traffic_recording_page = (recording_utils.isAvailable() and + ((not interface.isView() and + (is_packet_interface + or ((recording_utils.isSupportedZMQInterface(ifid) and not table.empty(ext_interfaces))) + or (recording_utils.getCurrentTrafficRecordingProvider(ifid) ~= "ntopng"))) + or (interface.isView() and not table.empty(viewed_ifaces_with_recording)))) local dismiss_recording_providers_reminder = recording_utils.isExternalProvidersReminderDismissed(ifstats.id) @@ -1628,6 +1635,28 @@ elseif (page == "trafficprofiles") then elseif (page == "traffic_recording" and has_traffic_recording_page) then local master_ifid = interface.getMasterInterfaceId() + if interface.isView() then + -- View: we cannot enable recording here, but we can extract traffic + -- from all viewed interfaces (if they have recording enabled) + if ntop.isEnterpriseM() then + local iface_names = {} + for _, iface in ipairs(viewed_ifaces_with_recording) do + table.insert(iface_names, iface.ifname) + end + print('
' .. + i18n('traffic_recording.view_extraction_note', + { interfaces = table.concat(iface_names, ", ") }) .. + '
') + + print('') + print('
') + dofile(dirs.installdir .. "/scripts/lua/inc/traffic_recording_jobs.lua") + print('
') + end + else if not dismiss_recording_providers_reminder then print('
' .. i18n('traffic_recording.msg_external_providers_detected', { @@ -1716,6 +1745,7 @@ elseif (page == "traffic_recording" and has_traffic_recording_page) then end print('
') + end -- not interface.isView() elseif (page == "config") then if (not isAdministrator()) then return @@ -2113,7 +2143,7 @@ function toggle_mirrored_traffic_function_off(){ end end - if has_traffic_recording_page then + if has_traffic_recording_page and not interface.isView() then local cur_provider = recording_utils.getCurrentTrafficRecordingProvider(ifstats.id) local providers = recording_utils.getAvailableTrafficRecordingProviders() diff --git a/scripts/lua/inc/traffic_recording_jobs.lua b/scripts/lua/inc/traffic_recording_jobs.lua index 5fc2d6ec27..74c34142da 100644 --- a/scripts/lua/inc/traffic_recording_jobs.lua +++ b/scripts/lua/inc/traffic_recording_jobs.lua @@ -96,7 +96,7 @@ print[[ $("#extractionjobs").datatable({ title: "", - url: "/lua/rest/v2/get/pcap/extraction/tasks.lua",]] + url: "/lua/rest/v2/get/pcap/extraction/tasks.lua?ifid=]] print(tostring(master_ifid)) print[[",]] -- Sort by column_id if a specific job_id is set to show it in the first table page if not isEmptyString(_GET["job_id"]) then diff --git a/scripts/lua/modules/graph_utils.lua b/scripts/lua/modules/graph_utils.lua index 8aa2dbeace..832bb324bb 100644 --- a/scripts/lua/modules/graph_utils.lua +++ b/scripts/lua/modules/graph_utils.lua @@ -303,10 +303,8 @@ function graph_utils.drawNewGraphs(source_value_object) local ifstats = interface.getStats() local ifid = (source_value_object.ifid) or (ifstats.id) - -- Check extraction permissions - local traffic_extraction_permitted = - recording_utils.isActive(ifid) or - recording_utils.isExtractionActive(ifid) + -- Check extraction permissions (handles View interfaces transparently) + local _, traffic_extraction_permitted = recording_utils.getStats(ifid) if source_value_object == nil then source_value_object = {} end diff --git a/scripts/lua/modules/recording_utils.lua b/scripts/lua/modules/recording_utils.lua index 9dfb078ad2..f1dec21db0 100644 --- a/scripts/lua/modules/recording_utils.lua +++ b/scripts/lua/modules/recording_utils.lua @@ -774,9 +774,28 @@ function recording_utils.isSmartEnabled(ifid) return false end +--! @brief Return the list of viewed interfaces that have traffic recording enabled. +--! @return array of { ifid, ifname } +function recording_utils.getViewedInterfacesWithRecording(view_ifid) + local result = {} + interface.select(tostring(view_ifid)) + local view_id = interface.getId() + + for other_ifid, other_ifname in pairs(interface.getIfNames() or {}) do + interface.select(tostring(other_ifid)) + if interface.viewedBy() == view_id then + local sub_ifid = interface.getId() + if recording_utils.isEnabled(sub_ifid) then + table.insert(result, { ifid = sub_ifid, ifname = other_ifname }) + end + end + end + + interface.select(tostring(view_ifid)) + return result +end + --! @brief Check if traffic extraction is available and recording is enabled on an interface ---! @param ifid the interface identifier ---! @return true if extraction is available and recording is enabled, false otherwise function recording_utils.isExtractionEnabled(ifid) if recording_utils.isExtractionAvailable() then return isRecordingEnabled(ifid) @@ -889,6 +908,34 @@ function recording_utils.stats(ifid) return parse_proc_stats(proc_stats) end +--! @brief Return dump window stats using recording_utils.stats() +--! In case of View interface stats are aggregated from all virwed interfaces +--! @return stats with FirstDumpedEpoch/LastDumpedEpoch, is_active boolean +function recording_utils.getStats(ifid) + interface.select(ifid) + if interface.isView() then + local viewed_ifaces = recording_utils.getViewedInterfacesWithRecording(ifid) + local view_first_epoch = nil + local view_last_epoch = 0 + local any_active = false + + for _, iface in ipairs(viewed_ifaces) do + if recording_utils.isActive(iface.ifid) or recording_utils.isExtractionActive(iface.ifid) then + any_active = true + local iface_stats = recording_utils.stats(iface.ifid) + local fe = tonumber(iface_stats['FirstDumpedEpoch'] or 0) + local le = tonumber(iface_stats['LastDumpedEpoch'] or 0) + if fe > 0 and (view_first_epoch == nil or fe < view_first_epoch) then view_first_epoch = fe end + if le > 0 and (view_last_epoch == nil or le > view_last_epoch) then view_last_epoch = le end + end + end + + return { FirstDumpedEpoch = view_first_epoch or 0, LastDumpedEpoch = view_last_epoch }, any_active + else + return recording_utils.stats(ifid), (recording_utils.isActive(ifid) or recording_utils.isExtractionActive(ifid)) + end +end + --! @brief Return statistics from the traffic recording service (n2disk) --! @param ifid the interface identifier --! @return the statistics @@ -1109,8 +1156,8 @@ function recording_utils.recommendedSpace(ifid, storage_info) return math.floor(recommended) end ---! @brief Check if there is pcap data for a specified time interval (fully included in the dump window) ---! @param ifid the interface identifier +--! @brief Check if there is pcap data for a specified time interval (fully included in the dump window) +--! @param ifid the interface identifier --! @param epoch_begin the begin time (epoch) --! @param epoch_end the end time (epoch) --! @return a table with 'available' = true if the specified interval is included in the dump window, 'epoch_begin'/'epoch_end' are also returned with the actual available window. @@ -1118,6 +1165,25 @@ function recording_utils.isDataAvailable(ifid, epoch_begin, epoch_end) local info = {} info.available = false + interface.select(ifid) + if interface.isView() then + -- View interface: aggregate availability from all viewed interfaces + local viewed_ifaces = recording_utils.getViewedInterfacesWithRecording(ifid) + for _, iface in ipairs(viewed_ifaces) do + local iface_info = recording_utils.isDataAvailable(iface.ifid, epoch_begin, epoch_end) + if iface_info.available then + if not info.available then + info = iface_info + else + -- Extend the window to cover all available constituent data + if iface_info.epoch_begin < info.epoch_begin then info.epoch_begin = iface_info.epoch_begin end + if iface_info.epoch_end > info.epoch_end then info.epoch_end = iface_info.epoch_end end + end + end + end + return info + end + if recording_utils.isExtractionEnabled(ifid) then local stats = recording_utils.stats(ifid) info = is_data_in_window(stats, epoch_begin, epoch_end) diff --git a/scripts/lua/rest/v2/create/pcap/extraction/task.lua b/scripts/lua/rest/v2/create/pcap/extraction/task.lua index 2a2f1f210c..3d8061e815 100644 --- a/scripts/lua/rest/v2/create/pcap/extraction/task.lua +++ b/scripts/lua/rest/v2/create/pcap/extraction/task.lua @@ -27,7 +27,7 @@ local filter = _POST["bpf_filter"] or _GET["bpf_filter"] local chart_url = _POST["url"] or _GET["url"] if not recording_utils.isAvailable() then - -- local error = i18n("traffic_recording.not_granted") + -- local error = i18n("traffic_recording.not_granted") rest_utils.answer(rest_utils.consts.err.not_granted) return end @@ -51,7 +51,35 @@ local ifstats = interface.getStats() time_from = tonumber(time_from) time_to = tonumber(time_to) -local timeline_path = recording_utils.getTimelineByInterval(ifstats.id, time_from, time_to) +local timeline_path + +if ifstats.isView then + -- Build comma-separated list of timelines from all viewed interfaces that have + -- recording enabled and data in the requested interval. + local viewed_ifaces = recording_utils.getViewedInterfacesWithRecording(ifstats.id) + + if table.empty(viewed_ifaces) then + rest_utils.answer(rest_utils.consts.err.not_granted) + return + end + + local paths = {} + for _, iface in ipairs(viewed_ifaces) do + local tl = recording_utils.getTimelineByInterval(iface.ifid, time_from, time_to) + if tl then + table.insert(paths, tl) + end + end + + if #paths == 0 then + rest_utils.answer(rest_utils.consts.err.bad_content) + return + end + + timeline_path = table.concat(paths, ",") +else + timeline_path = recording_utils.getTimelineByInterval(ifstats.id, time_from, time_to) +end local params = { time_from = time_from, diff --git a/scripts/lua/rest/v2/get/pcap/extraction/tasks.lua b/scripts/lua/rest/v2/get/pcap/extraction/tasks.lua index 6e3caf9e83..735f053808 100644 --- a/scripts/lua/rest/v2/get/pcap/extraction/tasks.lua +++ b/scripts/lua/rest/v2/get/pcap/extraction/tasks.lua @@ -116,7 +116,7 @@ for id, _ in pairsByValues(sorter, sOrder) do job_files = #job_files end - local status_desc = i18n("traffic_recording."..job.status) + local status_desc = i18n("traffic_recording."..job.status) if job.status == "failure" then local error_desc if job.error_code == 2 or job.error_code == 3 then error_desc = i18n("traffic_recording.err_alloc") @@ -143,8 +143,8 @@ for id, _ in pairsByValues(sorter, sOrder) do end end - res[#res + 1] = { - column_id = job.id, + res[#res + 1] = { + column_id = job.id, column_job_time = format_utils.formatEpoch(job.time), column_job_files = job_files, column_status = status_desc, diff --git a/scripts/lua/rest/v2/get/pcap/live_extraction.lua b/scripts/lua/rest/v2/get/pcap/live_extraction.lua index c7ac98ae9d..97f781fb36 100644 --- a/scripts/lua/rest/v2/get/pcap/live_extraction.lua +++ b/scripts/lua/rest/v2/get/pcap/live_extraction.lua @@ -49,10 +49,38 @@ if filter == nil then filter = "" end -local timeline_path = recording_utils.getTimelineByInterval(ifid, time_from, time_to) +local ifstats = interface.getStats() +local timeline_path + +if ifstats.isView then + -- View: return a comma-separated list of timelines from all viewed interfaces + -- that have recording enabled and data in the interval + local viewed_ifaces = recording_utils.getViewedInterfacesWithRecording(ifstats.id) + + if table.empty(viewed_ifaces) then + rest_utils.answer(rest_utils.consts.err.not_granted) + return + end + + local paths = {} + for _, iface in ipairs(viewed_ifaces) do + local tl = recording_utils.getTimelineByInterval(iface.ifid, time_from, time_to) + if tl then + table.insert(paths, tl) + end + end + + if #paths == 0 then + rest_utils.answer(rest_utils.consts.err.bad_content) + return + end + + timeline_path = table.concat(paths, ",") +else + timeline_path = recording_utils.getTimelineByInterval(ifid, time_from, time_to) +end local fname = time_from.."-"..time_to..".pcap" sendHTTPContentTypeHeader('application/vnd.tcpdump.pcap', 'attachment; filename="'..fname..'"') ntop.runLiveExtraction(ifid, time_from, time_to, filter, timeline_path) -