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)
-