├── .gitignore ├── benchmarks ├── noop.pdf ├── noop.png ├── flow-scaling.pdf ├── flow-scaling.png ├── flow-scaling.txt ├── noop.txt ├── noop.tex ├── flow-scaling.tex └── tumcolors.sty ├── path_of_a_packet.png ├── .gitmodules ├── lua ├── event.lua ├── apiUtils.lua ├── flowscope.lua ├── hmap.lua ├── qq.lua ├── tuple.lua ├── restApi.lua └── flowtracker.lua ├── LICENSE ├── CMakeLists.txt ├── examples ├── noop.lua ├── liveStatistician.lua ├── quicDetect.lua └── exampleUserModule.lua ├── src ├── qq_wrapper.cpp ├── var_hashmap.cpp └── QQ.hpp └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.swo 2 | *.swp 3 | *.pcap 4 | -------------------------------------------------------------------------------- /benchmarks/noop.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmericp/FlowScope/HEAD/benchmarks/noop.pdf -------------------------------------------------------------------------------- /benchmarks/noop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmericp/FlowScope/HEAD/benchmarks/noop.png -------------------------------------------------------------------------------- /path_of_a_packet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmericp/FlowScope/HEAD/path_of_a_packet.png -------------------------------------------------------------------------------- /benchmarks/flow-scaling.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmericp/FlowScope/HEAD/benchmarks/flow-scaling.pdf -------------------------------------------------------------------------------- /benchmarks/flow-scaling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmericp/FlowScope/HEAD/benchmarks/flow-scaling.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libmoon"] 2 | path = libmoon 3 | url = https://github.com/libmoon/libmoon 4 | shallow = true 5 | [submodule "tbb"] 6 | path = tbb 7 | url = https://github.com/01org/tbb.git 8 | shallow = true 9 | -------------------------------------------------------------------------------- /benchmarks/flow-scaling.txt: -------------------------------------------------------------------------------- 1 | # Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz, Intel XL710, noop module, direct mode, send rate 17.55 Mpps, 4 rx-threads 2 | # flows, total Mpps, std. der. Mpps, checker time [sec] 3 | 1000, 13.29, 0.05, 0.00049 4 | 10000, 14.10, 0.2, 0.004 5 | 100000, 14.5, 0.34, 0.43 6 | 1000000, 9.05, 0.25, 0.56 7 | 10000000, 8.62, 0.20, 6.2 -------------------------------------------------------------------------------- /lua/event.lua: -------------------------------------------------------------------------------- 1 | local log = require "log" 2 | 3 | local mod = {} 4 | 5 | mod.create = 1 6 | mod.delete = 2 7 | 8 | function mod.newEvent(filterString, action, id, timestamp) 9 | local id = id or filterString 10 | if action ~= mod.create and action ~= mod.delete then 11 | log:error("Invalid event action: %i", action) 12 | return nil 13 | end 14 | return {action = action, filter = filterString, id = id, timestamp = timestamp} 15 | end 16 | 17 | return mod 18 | -------------------------------------------------------------------------------- /benchmarks/noop.txt: -------------------------------------------------------------------------------- 1 | # Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz, Intel X540-AT2, 10^6 IPv4 UDP flows, direct mode, send rate 14.88 Mpps 2 | # threads, total Mpps, checker time [sec] 3 | 1, 2.40, 0.42 4 | 2, 4.70, 0.54 5 | 4, 9.36, 0.575 6 | 6, 13.61, 0.91 7 | 8, 14.17, 0.72 8 | 10, 13.1, 0.68 9 | 10 | # Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz, Intel XL710, 10^6 IPv4 UDP flows, direct mode, send rate 17.6 Mpps 11 | # threads, total Mpps, checker time [sec] 12 | 1, 2.30, 0.40 13 | 2, 4.56, 0.58 14 | 4, 9.09, 0.58 15 | 6, 13.5, 0.71 16 | 8, 15.37, 0.73 17 | 10, 17.2, 0.73 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Paul Emmerich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | CMAKE_MINIMUM_REQUIRED(VERSION 2.8.10) 2 | 3 | SET(CMAKE_CXX_FLAGS "-fno-stack-protector -Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -g -O3 -std=gnu++11 -march=native") 4 | SET(CMAKE_C_FLAGS "-fno-stack-protector -Wall -Wextra -Wno-unused-parameter -Wno-unused-variable -Wno-unused-function -g -O3 -std=gnu11 -march=native") 5 | 6 | SET(PRJ qq) 7 | 8 | include(${CMAKE_CURRENT_SOURCE_DIR}/tbb/cmake/TBBBuild.cmake) 9 | tbb_build(TBB_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/tbb CONFIG_DIR TBB_DIR) 10 | find_package(TBB) 11 | 12 | SET(QQ_SRC src/qq_wrapper.cpp) 13 | 14 | SET(LIBMOON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libmoon) 15 | 16 | INCLUDE_DIRECTORIES( 17 | ${CMAKE_CURRENT_SOURCE_DIR}/src 18 | ${LIBMOON_DIR}/deps/dpdk/x86_64-native-linuxapp-gcc/include 19 | ${LIBMOON_DIR}/deps/luajit/src 20 | ${LIBMOON_DIR}/lib 21 | ${LIBMOON_DIR}/src 22 | ) 23 | 24 | ADD_DEFINITIONS(-DNDEBUG) 25 | ADD_LIBRARY(${PRJ} SHARED ${QQ_SRC}) 26 | 27 | 28 | SET(FLOWTRACKER_SRC 29 | src/var_hashmap.cpp 30 | ) 31 | ADD_LIBRARY(flowtracker SHARED ${FLOWTRACKER_SRC}) 32 | TARGET_LINK_LIBRARIES(flowtracker ${TBB_IMPORTED_TARGETS}) 33 | -------------------------------------------------------------------------------- /examples/noop.lua: -------------------------------------------------------------------------------- 1 | local ffi = require "ffi" 2 | local lm = require "libmoon" 3 | local pktLib = require "packet" 4 | local eth = require "proto.ethernet" 5 | local ip = require "proto.ip4" 6 | local tuple = require "tuple" 7 | 8 | local module = {} 9 | 10 | 11 | -- IPv4 5-Tuples are not really no-ops, but doing exactly nothing would result in 0 flows in the hash tables. Not exactly a very good benchmark 12 | module.flowKeys = tuple.flowKeys 13 | module.extractFlowKey = tuple.extractIP5Tuple 14 | 15 | ffi.cdef [[ 16 | struct flow_state { 17 | }; 18 | ]] 19 | module.stateType = "struct flow_state" 20 | module.defaultState = {} 21 | 22 | function module.handlePacket(flowKey, state, buf, isFirstPacket) 23 | local t = lm.getTime() * 10^6 24 | end 25 | 26 | module.checkInterval = 5 27 | 28 | function module.checkInitializer(checkState) 29 | checkState.start_time = lm.getTime() * 10^6 30 | end 31 | 32 | function module.checkExpiry(flowKey, flowState, checkState) 33 | local t = lm.getTime() * 10^6 34 | 35 | return false 36 | end 37 | 38 | function module.checkFinalizer(checkState) 39 | local t = lm.getTime() * 10^6 40 | end 41 | 42 | return module 43 | -------------------------------------------------------------------------------- /benchmarks/noop.tex: -------------------------------------------------------------------------------- 1 | \documentclass[border=0pt]{standalone} 2 | 3 | \usepackage{tumcolors} 4 | \usepackage{pgfplots} 5 | \pgfplotsset{compat=newest} 6 | \pgfplotsset{grid style={dotted}} 7 | \usetikzlibrary{plotmarks} 8 | 9 | \begin{document} 10 | 11 | \pgfplotsset{compat=newest} 12 | \pgfplotsset{grid style={dotted}} 13 | \tikzset{every mark/.append style={scale=0.9}} 14 | \begin{tikzpicture} 15 | \begin{axis}[ 16 | footnotesize, 17 | scale only axis, 18 | width=7cm, 19 | height=3cm, 20 | xlabel=Threads, 21 | ymin=0, 22 | ymax=20, 23 | symbolic x coords={0, 1, 2, 4, 6, 8, 10, 12}, 24 | xtick=data, 25 | ytick={0,10, 20, 30}, 26 | ylabel=Packet Rate {[}Mpps{]}, 27 | grid=major, 28 | legend entries={XL710, X540-AT2}, 29 | legend style={at={(0.98,0.06)},anchor=south east, font=\footnotesize}, 30 | every axis legend/.append style={nodes={right}} 31 | ] 32 | \addplot[mark=square*,color=TUMBlue] coordinates { 33 | (1, 2.30) 34 | (2, 4.56) 35 | (4, 9.09) 36 | (6, 13.5) 37 | (8, 15.37) 38 | (10, 17.2) 39 | }; 40 | \addplot[mark=triangle*,color=TUMOrange] coordinates { 41 | (1, 2.40) 42 | (2, 4.70) 43 | (4, 9.36) 44 | (6, 13.61) 45 | (8, 14.17) 46 | (10, 13.1) 47 | }; 48 | \draw[dashed, line width=0.2mm, TUMOrange] (0,14.88) -- (12,14.88); 49 | \draw[dashed, line width=0.2mm, TUMBlue] (0,17.6) -- (12,17.6); 50 | \end{axis} 51 | \end{tikzpicture} 52 | \end{document} -------------------------------------------------------------------------------- /benchmarks/flow-scaling.tex: -------------------------------------------------------------------------------- 1 | \documentclass[border=0pt]{standalone} 2 | 3 | \usepackage{tumcolors} 4 | \usepackage{pgfplots} 5 | \pgfplotsset{compat=newest} 6 | \pgfplotsset{grid style={dotted}} 7 | \usetikzlibrary{plotmarks} 8 | 9 | \begin{document} 10 | 11 | \pgfplotsset{compat=newest} 12 | \pgfplotsset{grid style={dotted}} 13 | \tikzset{every mark/.append style={scale=0.9}} 14 | \begin{tikzpicture} 15 | 16 | \begin{axis}[ 17 | axis y line*=right, 18 | axis x line=none, 19 | ylabel={Checker time [s]}, 20 | footnotesize, 21 | scale only axis, 22 | width=7cm, 23 | height=3cm, 24 | grid=none, 25 | ymin=0, 26 | symbolic x coords={1 K, 10 K, 100 K, 1 M, 10 M}, 27 | xtick=data, 28 | every axis legend/.append style={nodes={right}}, 29 | ybar, 30 | ] 31 | \addplot[color=TUMOrange,fill=TUMOrange] coordinates { 32 | (1 K, 0.00049) 33 | (10 K, 0.004) 34 | (100 K, 0.43) 35 | (1 M, 0.56) 36 | (10 M, 6.2) 37 | }; 38 | \end{axis} 39 | 40 | \begin{axis}[ 41 | footnotesize, 42 | scale only axis, 43 | width=7cm, 44 | height=3cm, 45 | xlabel=Flows, 46 | ymin=0, 47 | ymax=20, 48 | symbolic x coords={1 K, 10 K, 100 K, 1 M, 10 M}, 49 | xtick=data, 50 | ytick={0,10, 20, 30}, 51 | ylabel=Packet Rate {[}Mpps{]}, 52 | grid=major, 53 | legend entries={XL710, X540-AT2}, 54 | legend style={at={(0.28,0.80)},anchor=south east, font=\footnotesize}, 55 | every axis legend/.append style={nodes={right}} 56 | ] 57 | \addplot[mark=square*,color=TUMBlue] coordinates { 58 | (1 K, 13.29) 59 | (10 K, 14.10) 60 | (100 K, 14.5) 61 | (1 M, 9.05) 62 | (10 M, 8.62) 63 | }; 64 | %\addplot[only marks,mark=square,color=TUMBlue, error bars/.cd,y dir=both,y explicit] coordinates { 65 | %(1 K, 13.29) +- (0, 0.05) 66 | %(10 K, 14.10) +- (0, 0.2) 67 | %(100 K, 14.5) +- (0, 0.34) 68 | %(1 M, 9.05) +- (0, 0.25) 69 | %(10 M, 8.62) +- (0, 0.20) 70 | %}; 71 | \end{axis} 72 | 73 | \end{tikzpicture} 74 | \end{document} -------------------------------------------------------------------------------- /benchmarks/tumcolors.sty: -------------------------------------------------------------------------------- 1 | %---------------------------------------------------------------------- 2 | % Identify package 3 | % 4 | \NeedsTeXFormat{LaTeX2e} 5 | \ProvidesPackage{tumcolors}[2009/06/29 Color definitions of official TUM colors] 6 | 7 | %---------------------------------------------------------------------- 8 | % Load color system 9 | % 10 | \RequirePackage{xcolor} 11 | 12 | %---------------------------------------------------------------------- 13 | % Color definitions 14 | % 15 | % Official TUM Black/White colors 16 | \definecolor{TUMBlack}{cmyk}{0,0,0,1} % Black 17 | \definecolor{TUMWhite}{cmyk}{0,0,0,0} % White 18 | 19 | % Official TUMBlue color (official TUM logo color) 20 | \definecolor{TUMBlue} {cmyk}{1,0.43,0,0} % Pantone 300 C 21 | 22 | % Additional TUM blue colors 23 | \definecolor{TUMDarkBlue} {cmyk}{1,0.57,0.12,0.7} % Pantone 540 C 24 | \definecolor{TUMDarkerBlue} {cmyk}{1,0.54,0.04,0.19} % Pantone 301 C 25 | \definecolor{TUMMediumBlue} {cmyk}{0.9,0.48,0,0} % Pantone 285 C 26 | \definecolor{TUMLighterBlue}{cmyk}{0.65,0.19,0.01,0.04} % Pantone 542 C 27 | \definecolor{TUMLightBlue} {cmyk}{0.42,0.09,0,0} % Pantone 283 C 28 | 29 | % Additional TUM gray colors 30 | \definecolor{TUMDarkGray} {cmyk}{0,0,0,0.8} % DarkGray 80% Black 31 | \definecolor{TUMMediumGray}{cmyk}{0,0,0,0.5} % MediumGray 50% Black 32 | \definecolor{TUMLightGray} {cmyk}{0,0,0,0.2} % LightGray 20% Black 33 | 34 | % TUM Highlight colors 35 | \definecolor{TUMGreen} {cmyk}{0.35,0,1,0.2} % Pantone 383 C 36 | \definecolor{TUMOrange}{cmyk}{0,0.65,0.95,0} % Pantone 158 C 37 | \definecolor{TUMIvony} {cmyk}{0.03,0.04,0.14,0.08} % Pantone 7527 C 38 | 39 | % TUM Presentation colors 40 | \definecolor{TUMBeamerYellow} {rgb}{1.00,0.71,0.00} % RGB 255,180,000 41 | \definecolor{TUMBeamerOrange} {rgb}{1.00,0.50,0.00} % RGB 255,128,000 42 | \definecolor{TUMBeamerRed} {rgb}{0.90,0.20,0.09} % RGB 229,052,024 43 | \definecolor{TUMBeamerDarkRed} {rgb}{0.79,0.13,0.25} % RGB 202,033,063 44 | \definecolor{TUMBeamerBlue} {rgb}{0.00,0.60,1.00} % RGB 000,153,255 45 | \definecolor{TUMBeamerLightBlue} {rgb}{0.25,0.75,1.00} % RGB 065,190,255 46 | \definecolor{TUMBeamerGreen} {rgb}{0.57,0.67,0.42} % RGB 145,172,107 47 | \definecolor{TUMBeamerLightGreen}{rgb}{0.71,0.79,0.51} % RGB 181,202,130 48 | 49 | 50 | % Colors for chair I8 51 | \definecolor{I8LogoRed} {rgb}{0.51,0,0.08} 52 | \definecolor{I8LightBlue} {rgb}{0.725,0.812,0.882} 53 | \definecolor{I8DarkBlue} {rgb}{0.490,0.573,0.667} 54 | \definecolor{I8Blue} {rgb}{0.576,0.624,0.718} 55 | 56 | \endinput 57 | 58 | -------------------------------------------------------------------------------- /lua/apiUtils.lua: -------------------------------------------------------------------------------- 1 | local turbo = require 'turbo' 2 | local event = require 'event' 3 | 4 | local module = {} 5 | 6 | module.filterIdRegex = "([a-zA-Z0-9.:-_]+)" 7 | 8 | function module.checkForToken(requestHandler, allowedToken) 9 | local token = requestHandler.request.headers:get("X-Authorization-Token", true) 10 | if token ~= nil then 11 | for _, v in pairs(allowedToken) do 12 | if v == token then 13 | return true 14 | end 15 | end 16 | end 17 | return false 18 | end 19 | 20 | function module.ifAuthorized(requestHandler, allowedToken, fct) 21 | -- check for the X-Authorization-Token header and compare its value to the ones present in allowedToken 22 | if module.checkForToken(requestHandler, allowedToken) then 23 | fct() 24 | else 25 | error(turbo.web.HTTPError(401, { message = "Unauthorized." })) 26 | end 27 | end 28 | 29 | function module.checkFilterAttributes(filterObject) 30 | local requiredKeys = filterObject['id'] ~= nil and filterObject['filter'] ~= nil 31 | requiredKeys = requiredKeys and (string.match(filterObject['id'], module.filterIdRegex) ~= nil) 32 | -- pipes present => it should be an array of numbers (logical consequence) 33 | local pipesCorrect = not (filterObject['pipes'] ~= nil) or (type(filterObject['pipes']) == 'table') 34 | return requiredKeys and pipesCorrect 35 | end 36 | 37 | function module.prepareFilter(json, allowed_pipes) 38 | local filter = { 39 | id = tostring(json.id), 40 | filter = json.filter, 41 | timestamp = json.timestamp, 42 | action = event.create, 43 | pipes = {} 44 | } 45 | 46 | -- formatting of the pipes is already checked in apiUtils.checkFilterAttributes 47 | if json['pipes'] ~= nil then 48 | for i, pipe_number in ipairs(json['pipes']) do 49 | if (allowed_pipes[pipe_number] ~= nil) then 50 | filter.pipes[i] = pipe_number 51 | else 52 | error(turbo.web.HTTPError(400, { error = "Pipe number " .. tostring(pipe_number) .. " not allowed." })) 53 | end 54 | end 55 | else 56 | filter.pipes = allowed_pipes 57 | end 58 | return filter 59 | end 60 | 61 | function module.applyFilter(filter, filters, pipes) 62 | if filters[filter.id] ~= nil then 63 | error(turbo.web.HTTPError(400, { error = "Filter id is already in use. Choose another one." })) 64 | else 65 | filters[filter.id] = filter 66 | -- add filter to pipe (what if pipe argument is not present? => add to all) 67 | for i, pipe in ipairs(pipes) do 68 | if filter.pipes[i] ~= nil then 69 | pipe:send(event.newEvent(filter.filter, event.create, filter.id, filter.timestamp)) 70 | end 71 | end 72 | end 73 | end 74 | 75 | return module 76 | -------------------------------------------------------------------------------- /lua/flowscope.lua: -------------------------------------------------------------------------------- 1 | local lm = require "libmoon" 2 | local device = require "device" 3 | local ffi = require "ffi" 4 | local log = require "log" 5 | local flowtracker = require "flowtracker" 6 | local qq = require "qq" 7 | 8 | local jit = require "jit" 9 | jit.opt.start("maxrecord=20000", "maxirconst=20000", "loopunroll=4000") 10 | 11 | function configure(parser) 12 | parser:argument("module", "Path to user-defined analysis module") 13 | parser:argument("dev", "Devices to use."):args("+"):convert(tonumber) 14 | parser:option("--size", "Storage capacity of the in-memory ring buffer in GiB."):convert(tonumber):default("8") 15 | parser:option("--rate", "Rate of the generated traffic in buckets/s."):convert(tonumber):default("10") 16 | parser:option("--rx-threads", "Number of rx threads per device. If --generate is given, then number of traffic generator threads."):convert(tonumber):default("1"):target("rxThreads") 17 | parser:option("--analyze-threads", "Number of analyzer threads. No effect if mode=qq"):convert(tonumber):default("1"):target("analyzeThreads") 18 | parser:option("--dump-threads", "Number of dump threads."):convert(tonumber):default("1"):target("dumperThreads") 19 | parser:option("--path", "Path for output pcaps."):default(".") 20 | parser:option("--log-level", "Log level"):default("WARN"):target("logLevel") 21 | parser:option("--max-rules", "Maximum number of rules"):convert(tonumber):default("100"):target("maxRules") 22 | parser:flag("--generate", "Generate traffic instead of reading from a device"):default(False) 23 | parser:option("-p --api-port", "Port for the HTTP REST api."):convert(tonumber):default("8000"):target("apiPort") 24 | parser:option("-b --api-bind", "Bind to a specific IP address. (for example 127.0.0.1)"):target("apiAddress") 25 | parser:option("-t --api-token", "Token for authorization to api."):default("hardToGuess"):target("apiToken"):count("*") 26 | local args = parser:parse() 27 | return args 28 | end 29 | 30 | 31 | function master(args) 32 | local f, err = loadfile(args.module) 33 | if f == nil then 34 | log:error(err) 35 | end 36 | local userModule = f() 37 | -- TODO: pass more/all CLI flags to module 38 | userModule.logLevel = args.logLevel 39 | local tracker = flowtracker.new(userModule) 40 | 41 | -- this part should be wrapped by flowscope and exposed via CLI arguments 42 | for i, dev in ipairs(args.dev) do 43 | args.dev[i] = device.config{ 44 | port = dev, 45 | rxQueues = args.rxThreads, 46 | rssQueues = args.rxThreads 47 | } 48 | -- Create analyzers 49 | for threadId = 0, args.rxThreads - 1 do 50 | -- get from QQ or from a device queue 51 | if userModule.mode == "qq" then 52 | local q = qq.createQQ(args.size) 53 | table.insert(tracker.qq, q) 54 | tracker:startNewInserter(args.dev[i]:getRxQueue(threadId), q) 55 | tracker:startNewDumper(args.path, q) 56 | tracker:startNewAnalyzer(args.module, q) 57 | else 58 | tracker:startNewAnalyzer(args.module, args.dev[i]:getRxQueue(threadId)) 59 | end 60 | end 61 | -- Start checker, has to done after the analyzers/pipes are created 62 | tracker:startChecker(args.module) 63 | end 64 | device.waitForLinks() 65 | -- end wrapped part 66 | lm.waitForTasks() 67 | tracker:delete() 68 | log:info("[master]: Shutdown") 69 | end 70 | -------------------------------------------------------------------------------- /examples/liveStatistician.lua: -------------------------------------------------------------------------------- 1 | local ffi = require "ffi" 2 | local lm = require "libmoon" 3 | local pktLib = require "packet" 4 | local eth = require "proto.ethernet" 5 | local ip = require "proto.ip4" 6 | local tuple = require "tuple" 7 | 8 | local module = {} 9 | 10 | local inactiveFlowExpiry = 30 -- seconds 11 | 12 | ffi.cdef [[ 13 | struct live_flow_state { 14 | uint64_t packets_interval; 15 | uint64_t bytes_interval; 16 | uint64_t packets_total; 17 | uint64_t bytes_total; 18 | uint64_t last_seen; 19 | uint64_t interval_start; 20 | uint64_t first_seen; 21 | }; 22 | ]] 23 | 24 | module.flowKeys = tuple.flowKeys 25 | module.stateType = "struct live_flow_state" 26 | module.defaultState = {} 27 | module.extractFlowKey = tuple.extractIP5Tuple 28 | 29 | function module.handlePacket(flowKey, state, buf, isFirstPacket) 30 | local t = lm.getTime() * 10^6 31 | state.packets_interval = state.packets_interval + 1 32 | state.bytes_interval = state.bytes_interval + buf:getSize() 33 | state.packets_total = state.packets_total + 1 34 | state.bytes_total = state.bytes_total + buf:getSize() 35 | if isFirstPacket then 36 | state.first_seen = t 37 | state.interval_start = t 38 | end 39 | state.last_seen = t 40 | end 41 | 42 | module.checkInterval = 5 43 | 44 | function module.checkInitializer(checkState) 45 | checkState.start_time = lm.getTime() * 10^6 46 | checkState.active_flows = 0ull 47 | checkState.cumulative_packets = 0ull 48 | checkState.cumulative_bytes = 0ull 49 | checkState.tops = {} 50 | end 51 | 52 | local function sortedInsert(t, max, entry, cmpFn) 53 | if #t < max then 54 | table.insert(t, entry) 55 | table.sort(t, cmpFn) 56 | else 57 | for i=1, #t do 58 | if cmpFn(entry, t[i]) then 59 | table.insert(t, i, entry) 60 | table.remove(t) 61 | break 62 | end 63 | end 64 | end 65 | end 66 | 67 | function module.checkExpiry(flowKey, flowState, checkState) 68 | local t = lm.getTime() * 10^6 69 | 70 | local d = tonumber(t - flowState.interval_start) / 10^6 71 | local bps = tonumber(flowState.bytes_interval * 8) / d 72 | local pps = tonumber(flowState.packets_interval) / d 73 | local e = {bps, pps, flowKey, flowState.interval_start, t, flowState.bytes_interval, flowState.packets_interval} 74 | local cmpFn = function(a, b) return a[1] > b[1] end 75 | sortedInsert(checkState.tops, 10, e, cmpFn) 76 | 77 | checkState.cumulative_packets = checkState.cumulative_packets + flowState.packets_interval 78 | checkState.cumulative_bytes = checkState.cumulative_bytes + flowState.bytes_interval 79 | 80 | -- Reset interval counter 81 | flowState.bytes_interval = 0 82 | flowState.packets_interval = 0 83 | flowState.interval_start = t 84 | 85 | if flowState.last_seen + inactiveFlowExpiry * 10^6 < t then 86 | return true, t / 10^6 87 | else 88 | checkState.active_flows = checkState.active_flows + 1 89 | return false 90 | end 91 | end 92 | 93 | function module.checkFinalizer(checkState) 94 | local t = lm.getTime() * 10^6 95 | print("Top flows over sliding " .. module.checkInterval .. "s window:") 96 | print("#", "bps", "pps", "Flow") 97 | for k,v in pairs(checkState.tops) do 98 | print(string.format("%i %.2f %.2f %s", k, v[1], v[2], v[3])) 99 | end 100 | print(string.format("Active flows %i, cumulative packets %i [%.2f/s], cumulative bytes %i [%.2f/s], took %.2fs", tonumber(checkState.active_flows), tonumber(checkState.cumulative_packets), tonumber(checkState.cumulative_packets) / module.checkInterval, tonumber(checkState.cumulative_bytes), tonumber(checkState.cumulative_bytes) / module.checkInterval, (t - tonumber(checkState.start_time)) / 10^6)) 101 | end 102 | 103 | return module 104 | -------------------------------------------------------------------------------- /lua/hmap.lua: -------------------------------------------------------------------------------- 1 | local ffi = require "ffi" 2 | local log = require "log" 3 | local flowtrackerlib = ffi.load("../build/flowtracker") 4 | local C = ffi.C 5 | 6 | local hmapTemplate = [[ 7 | typedef struct hmapk{key_size}v{value_size} hmapk{key_size}v{value_size}; 8 | typedef struct hmapk{key_size}v{value_size}_accessor hmapk{key_size}v{value_size}_accessor; 9 | hmapk{key_size}v{value_size}* hmapk{key_size}v{value_size}_create(); 10 | void hmapk{key_size}v{value_size}_delete(hmapk{key_size}v{value_size}* map); 11 | void hmapk{key_size}v{value_size}_clear(hmapk{key_size}v{value_size}* map); 12 | hmapk{key_size}v{value_size}_accessor* hmapk{key_size}v{value_size}_new_accessor(); 13 | void hmapk{key_size}v{value_size}_accessor_free(hmapk{key_size}v{value_size}_accessor* a); 14 | void hmapk{key_size}v{value_size}_accessor_release(hmapk{key_size}v{value_size}_accessor* a); 15 | bool hmapk{key_size}v{value_size}_access(hmapk{key_size}v{value_size}* map, hmapk{key_size}v{value_size}_accessor* a, const void* key); 16 | bool hmapk{key_size}v{value_size}_find(hmapk{key_size}v{value_size}* map, hmapk{key_size}v{value_size}_accessor* a, const void* key); 17 | bool hmapk{key_size}v{value_size}_erase(hmapk{key_size}v{value_size}* map, hmapk{key_size}v{value_size}_accessor* a); 18 | uint8_t* hmapk{key_size}v{value_size}_accessor_get_value(hmapk{key_size}v{value_size}_accessor* a); 19 | ]] 20 | 21 | local module = {} 22 | 23 | local keySizes = { 8, 16, 32, 64 } 24 | local valueSizes = { 8, 16, 32, 64, 128 } 25 | 26 | -- Get tbb hash map with fitting key and value size 27 | function module.createHashmap(keySize, valueSize) 28 | local realKeySize, realValueSize = 0, 0 29 | if keySize <= 8 then 30 | realKeySize = 8 31 | elseif keySize <= 16 then 32 | realKeySize = 16 33 | elseif keySize <= 32 then 34 | realKeySize = 32 35 | elseif keySize <= 64 then 36 | realKeySize = 64 37 | else 38 | log:error("Keys of size %d are not supported", keySize) 39 | return nil 40 | end 41 | if valueSize <= 8 then 42 | realValueSize = 8 43 | elseif valueSize <= 16 then 44 | realValueSize = 16 45 | elseif valueSize <= 32 then 46 | realValueSize = 32 47 | elseif valueSize <= 64 then 48 | realValueSize = 64 49 | elseif valueSize <= 128 then 50 | realValueSize = 128 51 | else 52 | log:error("Values of size %d are not supported", valueSize) 53 | return nil 54 | end 55 | 56 | return flowtrackerlib["hmapk" .. realKeySize .. "v" .. realValueSize .. "_create"]() 57 | end 58 | 59 | function makeHashmapFor(keySize, valueSize) 60 | local map = {} 61 | function map:clear() 62 | flowtrackerlib["hmapk" .. keySize .. "v" .. valueSize .. "_clear"](self) 63 | end 64 | function map:delete() 65 | flowtrackerlib["hmapk" .. keySize .. "v" .. valueSize .. "_delete"](self) 66 | end 67 | function map:access(a, tpl) 68 | return flowtrackerlib["hmapk" .. keySize .. "v" .. valueSize .. "_access"](self, a, tpl) 69 | end 70 | function map:find(a, tpl) 71 | return flowtrackerlib["hmapk" .. keySize .. "v" .. valueSize .. "_find"](self, a, tpl) 72 | end 73 | function map.newAccessor() 74 | return flowtrackerlib["hmapk" .. keySize .. "v" .. valueSize .. "_new_accessor"]() 75 | end 76 | function map:erase(a) 77 | return flowtrackerlib["hmapk" .. keySize .. "v" .. valueSize .. "_erase"](self, a) 78 | end 79 | function map.keyBufSize() 80 | return keySize 81 | end 82 | function map.valueSize() 83 | return valueSize 84 | end 85 | local accessor = {} 86 | function accessor:get() 87 | return flowtrackerlib["hmapk" .. keySize .. "v" .. valueSize .. "_accessor_get_value"](self) 88 | end 89 | function accessor:free() 90 | return flowtrackerlib["hmapk" .. keySize .. "v" .. valueSize .. "_accessor_free"](self) 91 | end 92 | function accessor:release() 93 | return flowtrackerlib["hmapk" .. keySize .. "v" .. valueSize .. "_accessor_release"](self) 94 | end 95 | map.__index = map 96 | accessor.__index = accessor 97 | ffi.metatype("hmapk" .. keySize .. "v" .. valueSize, map) 98 | ffi.metatype("hmapk" .. keySize .. "v" .. valueSize .. "_accessor", accessor) 99 | end 100 | 101 | for _, k in pairs(keySizes) do 102 | for _, v in pairs(valueSizes) do 103 | local definition, _ = hmapTemplate:gsub("{value_size}", v) 104 | definition, _ = definition:gsub("{key_size}", k) 105 | ffi.cdef(definition) 106 | makeHashmapFor(k, v) 107 | end 108 | end 109 | 110 | -- Helper function to get the size of the largest flow key 111 | -- args is a table of hash maps 112 | function module.getLargestKeyBufSize(args) 113 | local sz = {} 114 | for _, v in ipairs(args) do 115 | table.insert(sz, v.keyBufSize()) 116 | end 117 | table.sort(sz) 118 | return sz[#sz] 119 | end 120 | 121 | return module 122 | -------------------------------------------------------------------------------- /lua/qq.lua: -------------------------------------------------------------------------------- 1 | local ffi = require "ffi" 2 | local packetLib = require "packet" 3 | local memory = require "memory" 4 | 5 | local qqlib = ffi.load("build/qq") 6 | 7 | ffi.cdef [[ 8 | typedef struct packet_header { 9 | uint64_t ts_vlan; 10 | uint16_t pkt_len; 11 | uint8_t data[]; 12 | } packet_header_t; 13 | 14 | typedef struct qq { } qq_t; 15 | 16 | void qq_init(); 17 | 18 | qq_t* qq_create(uint32_t storage_capacity); 19 | 20 | void qq_delete(qq_t*); 21 | 22 | size_t qq_size(qq_t*); 23 | 24 | size_t qq_capacity(qq_t*); 25 | 26 | typedef struct storage { } storage_t; 27 | 28 | storage_t* qq_storage_dequeue(qq_t*); 29 | 30 | storage_t* qq_storage_try_dequeue(qq_t*); 31 | 32 | storage_t* qq_storage_enqueue(qq_t*); 33 | 34 | storage_t* qq_storage_peek(qq_t*); 35 | 36 | storage_t* qq_storage_try_peek(qq_t*); 37 | 38 | size_t qq_get_enqueue_counter(qq_t*); 39 | 40 | size_t qq_get_dequeue_counter(qq_t*); 41 | 42 | size_t qq_get_enqueue_overflow_counter(qq_t*); 43 | 44 | void qq_set_priority(qq_t* q, const uint8_t new_priority); 45 | 46 | void qq_storage_release(storage_t*); 47 | 48 | size_t qq_storage_size(storage_t*); 49 | 50 | bool qq_storage_store(storage_t*, uint64_t ts, uint16_t vlan, uint16_t len, const uint8_t* data); 51 | 52 | const packet_header_t& qq_storage_get_packet(storage_t*, const size_t); 53 | 54 | uint64_t qq_packet_header_get_timestamp(const packet_header_t&); 55 | 56 | uint64_t qq_packet_header_get_vlan(const packet_header_t& h); 57 | 58 | uint16_t qq_packet_header_get_len(const packet_header_t& h); 59 | 60 | void* qq_packet_header_get_data(const packet_header_t& h); 61 | 62 | void dummy_enqueue(qq_t* q); 63 | 64 | void qq_inserter_loop(uint8_t device, uint16_t queue_id, qq_t* qq); 65 | ]] 66 | 67 | local C = ffi.C 68 | 69 | local mod = {} 70 | 71 | --- @param storageSize desired storage capacity in GiB 72 | function mod.createQQ(storageSize) 73 | qqlib.qq_init() -- idempotent 74 | return qqlib.qq_create(storageSize * 1024) 75 | end 76 | 77 | local qq = {} 78 | qq.__index = qq 79 | local storage = {} 80 | storage.__index = storage 81 | local packetHeader = {} 82 | packetHeader.__index = packetHeader 83 | 84 | 85 | function qq:delete() 86 | qqlib.qq_delete(self) 87 | end 88 | 89 | function qq:size() 90 | return tonumber(qqlib.qq_size(self)) 91 | end 92 | 93 | function qq:capacity() 94 | return tonumber(qqlib.qq_capacity(self)) 95 | end 96 | 97 | function qq:dequeue() 98 | return qqlib.qq_storage_dequeue(self) 99 | end 100 | 101 | function qq:tryDequeue() 102 | return qqlib.qq_storage_try_dequeue(self) 103 | end 104 | 105 | function qq:tryPeek() 106 | return qqlib.qq_storage_try_peek(self) 107 | end 108 | 109 | function qq:enqueue() 110 | return qqlib.qq_storage_enqueue(self) 111 | end 112 | 113 | function qq:peek() 114 | return qqlib.qq_storage_peek(self) 115 | end 116 | 117 | function qq:getEnqueueCounter() 118 | return tonumber(qqlib.qq_get_enqueue_counter(self)) 119 | end 120 | 121 | function qq:getEnqueueOverflowCounter() 122 | return tonumber(qqlib.qq_get_enqueue_overflow_counter(self)) 123 | end 124 | 125 | function qq:getDequeueCounter() 126 | return tonumber(qqlib.qq_get_dequeue_counter(self)) 127 | end 128 | 129 | function qq:setPriority(new_priority) 130 | return qqlib.qq_set_priority(self, new_priority) 131 | end 132 | 133 | function qq:dummy() 134 | qqlib.dummy_enqueue(self) 135 | end 136 | 137 | function storage:release() 138 | qqlib.qq_storage_release(self) 139 | end 140 | 141 | function storage:size() 142 | return tonumber(qqlib.qq_storage_size(self)) 143 | end 144 | 145 | function storage:store(ts, vlan, len, data) 146 | return qqlib.qq_storage_store(self, ts, vlan, len, data) 147 | end 148 | 149 | function storage:getPacket(idx) 150 | return qqlib.qq_storage_get_packet(self, idx) 151 | end 152 | 153 | local band, rshift, lshift = bit.band, bit.rshift, bit.lshift 154 | 155 | function packetHeader:getTimestamp() 156 | -- timestamp relative to libmoon.getTime(), i.e. the TSC in seconds 157 | return tonumber(band(self.ts_vlan, 0xffffffffffffULL)) / 10^6 158 | end 159 | 160 | function packetHeader:getVlan() 161 | return rshift(self.ts_vlan, 48) 162 | end 163 | 164 | function packetHeader:getSize() 165 | return self.pkt_len 166 | end 167 | 168 | function packetHeader:getData() 169 | return ffi.cast("void*", self.data) 170 | end 171 | 172 | function packetHeader:getBytes() 173 | return self.data 174 | end 175 | 176 | function packetHeader:dump() 177 | return packetLib.getEthernetPacket(self):resolveLastHeader():dump() 178 | end 179 | 180 | function packetHeader:clone() 181 | local pkt = memory.alloc("packet_header_t*", ffi.sizeof("packet_header_t") + self.pkt_len) 182 | pkt.ts_vlan = self.ts_vlan 183 | pkt.pkt_len = self.pkt_len 184 | ffi.copy(pkt.data, self.data, self.pkt_len) 185 | return pkt 186 | end 187 | 188 | function qq:inserterLoop(queue) 189 | qqlib.qq_inserter_loop(queue.id, queue.qid, self) 190 | end 191 | 192 | ffi.metatype("qq_t", qq) 193 | ffi.metatype("storage_t", storage) 194 | ffi.metatype("packet_header_t", packetHeader) 195 | 196 | return mod 197 | 198 | -------------------------------------------------------------------------------- /lua/tuple.lua: -------------------------------------------------------------------------------- 1 | local ffi = require "ffi" 2 | local pktLib = require "packet" 3 | local eth = require "proto.ethernet" 4 | local ip4 = require "proto.ip4" 5 | local ip6 = require "proto.ip6" 6 | 7 | local module = {} 8 | 9 | ffi.cdef [[ 10 | struct ipv4_5tuple { 11 | union ip4_address ip_a; 12 | union ip4_address ip_b; 13 | uint16_t port_a; 14 | uint16_t port_b; 15 | uint8_t proto; 16 | } __attribute__((__packed__)); 17 | 18 | struct ipv6_5tuple { 19 | union ip6_address ip_a; 20 | union ip6_address ip_b; 21 | uint16_t port_a; 22 | uint16_t port_b; 23 | uint8_t proto; 24 | } __attribute__((__packed__)); 25 | ]] 26 | 27 | assert(ffi.sizeof("struct ipv4_5tuple") == 13) 28 | 29 | module.flowKeys = { 30 | "struct ipv4_5tuple", 31 | "struct ipv6_5tuple", 32 | } 33 | 34 | local ip4Tuple = {} 35 | 36 | local stringTemplate = "ipv4_5tuple{ip_a: %s, ip_b: %s, port_a: %i, port_b: %i, proto: %s}" 37 | function ip4Tuple:__tostring() 38 | local proto 39 | if self.proto == ip4.PROTO_UDP then 40 | proto = "udp" 41 | elseif self.proto == ip4.PROTO_TCP then 42 | proto = "tcp" 43 | else 44 | proto = tostring(tonumber(self.proto)) 45 | end 46 | return stringTemplate:format(self.ip_a:getString(), self.ip_b:getString(), self.port_a, self.port_b, proto) 47 | end 48 | 49 | -- TODO: Rearrange expressions to generate better lua code in pflua 50 | local pflangTemplateUni = "src host %s src port %i dst host %s dst port %i %s" 51 | function ip4Tuple:getPflangUni() 52 | local proto 53 | if self.proto == ip4.PROTO_UDP then 54 | proto = "udp" 55 | elseif self.proto == ip4.PROTO_TCP then 56 | proto = "tcp" 57 | else 58 | proto = tostring(tonumber(self.proto)) 59 | end 60 | return pflangTemplateUni:format(self.ip_a:getString(), self.port_a, self.ip_b:getString(), self.port_b, proto) 61 | end 62 | 63 | local pflangTemplate = "ip proto %i and host %s and host %s and port %i and port %i" 64 | function ip4Tuple:getPflang() 65 | return pflangTemplate:format(self.proto, self.ip_a:getString(), self.ip_b:getString(), self.port_a, self.port_b) 66 | end 67 | 68 | ip4Tuple.__index = ip4Tuple 69 | ffi.metatype("struct ipv4_5tuple", ip4Tuple) 70 | 71 | local ip6Tuple = {} 72 | 73 | local pflangTemplate6 = "ip6 proto %i and host %s and host %s and port %i and port %i" 74 | function ip6Tuple:getPflang() 75 | return pflangTemplate6:format(self.proto, self.ip_a:getString(), self.ip_b:getString(), self.port_a, self.port_b) 76 | end 77 | 78 | ip6Tuple.__index = ip6Tuple 79 | ffi.metatype("struct ipv6_5tuple", ip6Tuple) 80 | 81 | -- Uni-directional 82 | function module.extractIP5TupleUni(buf, keyBuf) 83 | local ethPkt = pktLib.getEthernetPacket(buf) 84 | if ethPkt.eth:getType() == eth.TYPE_IP then 85 | -- actual L4 type doesn't matter 86 | keyBuf = ffi.cast("struct ipv4_5tuple&", keyBuf) 87 | local parsedPkt = pktLib.getUdp4Packet(buf) 88 | -- IPs are copied in network byte order so that the getString() functions work 89 | keyBuf.ip_a.uint32 = parsedPkt.ip4.src.uint32 90 | keyBuf.ip_b.uint32 = parsedPkt.ip4.dst.uint32 91 | -- port is always at the same position as UDP 92 | keyBuf.port_a = parsedPkt.udp:getSrcPort() 93 | keyBuf.port_b = parsedPkt.udp:getDstPort() 94 | local proto = parsedPkt.ip4:getProtocol() 95 | if proto == ip4.PROTO_UDP or proto == ip4.PROTO_TCP or proto == ip4.PROTO_SCTP then 96 | keyBuf.proto = proto 97 | return true, 1 98 | end 99 | elseif ethPkt.eth:getType() == eth.TYPE_IP6 then 100 | keyBuf = ffi.cast("struct ipv6_5tuple&", keyBuf) 101 | local parsedPkt = pktLib.getUdp6Packet(buf) 102 | keyBuf.ip_a.uint64[0] = parsedPkt.ip6.src.uint64[0] 103 | keyBuf.ip_a.uint64[1] = parsedPkt.ip6.src.uint64[1] 104 | keyBuf.ip_b.uint64[0] = parsedPkt.ip6.dst.uint64[0] 105 | keyBuf.ip_b.uint64[1] = parsedPkt.ip6.dst.uint64[1] 106 | keyBuf.port_a = parsedPkt.udp:getSrcPort() 107 | keyBuf.port_b = parsedPkt.udp:getDstPort() 108 | local proto = parsedPkt.ip6:getProtocol() 109 | if proto == ip6.PROTO_UDP or proto == ip6.PROTO_TCP or proto == ip6.PROTO_SCTP then 110 | keyBuf.proto = proto 111 | return true, 2 112 | end 113 | end 114 | return false 115 | end 116 | 117 | -- Bi-directional 118 | function module.extractIP5Tuple(buf, keyBuf) 119 | local ok, idx = module.extractIP5TupleUni(buf, keyBuf) 120 | if ok and idx == 1 then 121 | keyBuf = ffi.cast("struct ipv4_5tuple&", keyBuf) 122 | if keyBuf.ip_a.uint32 > keyBuf.ip_b.uint32 then 123 | keyBuf.ip_a.uint32, keyBuf.ip_b.uint32 = keyBuf.ip_b.uint32, keyBuf.ip_a.uint32 124 | keyBuf.port_a, keyBuf.port_b = keyBuf.port_b, keyBuf.port_a 125 | end 126 | return ok, idx 127 | end 128 | if ok and idx == 2 then 129 | keyBuf = ffi.cast("struct ipv6_5tuple&", keyBuf) 130 | if keyBuf.ip_a < keyBuf.ip_b then 131 | keyBuf.ip_a, keyBuf.ip_b = keyBuf.ip_b:get(), keyBuf.ip_a:get() 132 | keyBuf.port_a, keyBuf.port_b = keyBuf.port_b, keyBuf.port_a 133 | end 134 | return ok, idx 135 | end 136 | return false 137 | end 138 | 139 | return module 140 | -------------------------------------------------------------------------------- /examples/quicDetect.lua: -------------------------------------------------------------------------------- 1 | local ffi = require "ffi" 2 | local lm = require "libmoon" 3 | local pktLib = require "packet" 4 | local eth = require "proto.ethernet" 5 | local ip4 = require "proto.ip4" 6 | local ip6 = require "proto.ip6" 7 | local tuple = require "tuple" 8 | local log = require "log" 9 | local hmap = require "hmap" 10 | local namespace = require "namespaces" 11 | local band = bit.band 12 | 13 | -- A protocol detector/tracker for quic as of RFC version 01 14 | -- https://tools.ietf.org/html/draft-ietf-quic-transport-01 15 | 16 | ffi.cdef [[ 17 | int memcmp(const void *s1, const void *s2, size_t n); 18 | ]] 19 | 20 | local module = {} 21 | 22 | ffi.cdef [[ 23 | struct conn_id { 24 | uint64_t id; 25 | }; 26 | ]] 27 | module.flowKeys = { 28 | "struct ipv4_5tuple", 29 | --"struct conn_id" 30 | } 31 | 32 | ffi.cdef [[ 33 | struct quic_flow_state { 34 | uint64_t last_seen; 35 | uint64_t first_seen; 36 | uint64_t connection_id; 37 | uint8_t cid_set; 38 | uint8_t tracked; 39 | }; 40 | ]] 41 | module.stateType = "struct quic_flow_state" 42 | module.defaultState = {} 43 | 44 | module.mode = "qq" 45 | 46 | 47 | local shared = namespace:get() 48 | local IDtable = nil 49 | local acc = nil 50 | local IDkeyBuf = nl 51 | 52 | shared.lock(function() 53 | if shared.tbl == nil then 54 | shared.tbl = hmap.createHashmap(8, ffi.sizeof("struct ipv4_5tuple")) 55 | end 56 | IDtable = shared.tbl 57 | acc = IDtable.newAccessor() 58 | ffi.gc(acc, acc.free) 59 | IDkeyBuf = ffi.new("uint8_t[?]", IDtable.keyBufSize()) 60 | end) 61 | 62 | 63 | function module.extractFlowKey(buf, keyBuf) 64 | local success, idx = tuple.extractIP5Tuple(buf, keyBuf) -- Reuse 5-tuple extractor 65 | if success and idx == 1 then 66 | keyBuf = ffi.cast("struct ipv4_5tuple&", keyBuf) 67 | if keyBuf.proto == ip4.PROTO_UDP and (keyBuf.port_a == 443 or keyBuf.port_b == 443) then 68 | return success, idx 69 | end 70 | end 71 | return false 72 | end 73 | 74 | log:setLevel("INFO") 75 | 76 | function module.handlePacket(flowKey, state, buf, isFirstPacket) 77 | local t = buf:getTimestamp() * 10^6 78 | state.last_seen = t 79 | if isFirstPacket then 80 | state.first_seen = t 81 | end 82 | 83 | local udpPkt = pktLib.getUdp4Packet(buf) 84 | local flags = udpPkt.payload.uint8[0] 85 | if band(flags, 0x80) ~= 0 then 86 | return false -- Reserved bit set, probably not quic 87 | end 88 | if band(flags, 0x08) ~= 0 then 89 | local cid = ffi.cast("uint64_t*", udpPkt.payload.uint8 + 1)[0] 90 | log:debug("[Analyzer]: Found CID %s", tostring(cid)) 91 | if not isFirstPacket and cid ~= state.connection_id then 92 | log:info("Connection ID changed for flow %s: %s -> %s", flowKey, tonumber(state.connection_id), tonumber(cid)) 93 | end 94 | state.connection_id = cid 95 | -- Check ID -> 5-Tuple map 96 | ffi.fill(IDkeyBuf, IDtable.keyBufSize()) 97 | ffi.copy(IDkeyBuf, udpPkt.payload.uint8 + 1, 8) 98 | local new = IDtable:access(acc, IDkeyBuf) 99 | local tpl = ffi.cast("struct ipv4_5tuple&", acc:get()) 100 | if new then 101 | ffi.copy(tpl, flowKey, ffi.sizeof("struct ipv4_5tuple")) 102 | end 103 | local dump = false 104 | if ffi.C.memcmp(tpl, flowKey, ffi.sizeof("struct ipv4_5tuple")) ~= 0 then 105 | log:warn("Connection migration of id %i from %s to %s", tonumber(cid), tpl, flowKey) 106 | state.tracked = 1 107 | dump = true 108 | end 109 | acc:release() 110 | return dump 111 | else 112 | log:debug("[Analyzer]: No CID") 113 | if isFirstPacket then 114 | log:info("First packet in flow and no connection ID in %s", flowKey) 115 | else 116 | ffi.fill(IDkeyBuf, IDtable.keyBufSize()) 117 | ffi.cast("uint64_t*", IDkeyBuf)[0] = state.connection_id 118 | local exists = IDtable:find(acc, IDkeyBuf) 119 | local dump = false 120 | if exits then 121 | local tpl = ffi.cast("struct ipv4_5tuple&", acc:get()) 122 | if ffi.C.memcmp(tpl, flowKey, ffi.sizeof("struct ipv4_5tuple")) ~= 0 then 123 | log:warn("Connection migration of id %i from %s to %s", tonumber(state.connection_id), tpl, flowKey) 124 | state.tracked = 1 125 | dump = true 126 | end 127 | end 128 | acc:release() 129 | return dump 130 | end 131 | end 132 | return false 133 | end 134 | 135 | 136 | -- #### Checker configuration #### 137 | 138 | module.checkInterval = 5 139 | 140 | function module.checkExpiry(flowKey, state, checkState) 141 | local t = lm.getTime() * 10^6 142 | if state.tracked == 1 and tonumber(state.last_seen) + 30 * 10^6 < t then 143 | ffi.fill(IDkeyBuf, IDtable.keyBufSize()) 144 | ffi.cast("uint64_t*", IDkeyBuf)[0] = state.connection_id 145 | --ffi.copy(IDkeyBuf, state.connection_id, 8) 146 | local new = IDtable:access(acc, IDkeyBuf) 147 | assert(new == false) -- Must hold or we have missed an ID before 148 | IDtable:erase(acc) 149 | acc:release() 150 | return true, tonumber(state.last_seen) / 10^6 151 | end 152 | return false 153 | end 154 | 155 | -- #### Dumper configuration #### 156 | -- Only applicable if mode is set to "qq" 157 | 158 | module.maxDumperRules = 100 159 | 160 | -- Function that returns a packet filter string in pcap syntax from a given flow key 161 | function module.buildPacketFilter(flowKey) 162 | return flowKey:getPflang() 163 | end 164 | 165 | return module 166 | -------------------------------------------------------------------------------- /src/qq_wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "QQ.hpp" 8 | #include "lifecycle.hpp" 9 | 10 | constexpr size_t bucket_size = 8; // bucket size in 2 MiB pages, pick something between 4 and 32 (8 MiB - 64 MiB) 11 | 12 | namespace QQ { 13 | static inline void inserter_loop(uint8_t port_id, uint16_t queue_id, QQ* qq) { 14 | constexpr size_t batchsize = 64; 15 | 16 | const uint64_t tsc_hz = rte_get_tsc_hz(); //!< cycles per second 17 | const double tsc_hz_usec = tsc_hz / (1000.0 * 1000.0); //!< cycles per microsecond 18 | 19 | auto enq_ptr = qq->enqueue(); 20 | struct rte_mbuf* bufs[batchsize] __rte_cache_aligned; 21 | 22 | while (libmoon::is_running(0)) { 23 | uint16_t rx_cnt = rte_eth_rx_burst(port_id, queue_id, bufs, batchsize); 24 | 25 | if (rx_cnt == 0) { 26 | rte_delay_us(2); // taken from dpdk example bond/main 27 | } 28 | 29 | // we need the floating point op, tsc / (tsc_hz / 10^6) is too imprecise 30 | // (and tsc * 10^6 overflows after a few hours) 31 | uint64_t timestamp_batch = (uint64_t) (rte_rdtsc() / tsc_hz_usec); 32 | 33 | for (uint16_t i = 0; i < rx_cnt; ++i) { 34 | while (!enq_ptr.store( 35 | timestamp_batch, 36 | // vlan 37 | bufs[i]->ol_flags & PKT_RX_VLAN_PKT ? bufs[i]->vlan_tci & 0xFFF : 0, 38 | rte_pktmbuf_pkt_len(bufs[i]), // packet length 39 | rte_pktmbuf_mtod(bufs[i], const uint8_t*) // packet data 40 | )) { 41 | enq_ptr.release(); // Unlock the old one 42 | enq_ptr = qq->enqueue(); // Get a new one 43 | } 44 | rte_pktmbuf_free(bufs[i]); 45 | } 46 | } 47 | enq_ptr.release(); 48 | // Attempt to make stray packets eligible for dequeuing after shutdown 49 | // FIXME: Loop counter should depend on actual number of buckets blocked 50 | for (int i = 0; i < 30; ++i) { 51 | auto t = qq->enqueue(); 52 | t.release(); 53 | rte_delay_ms(10); 54 | } 55 | } 56 | } 57 | 58 | extern "C" { 59 | void qq_init() { 60 | QQ::init(); 61 | } 62 | 63 | QQ::QQ* qq_create(uint32_t storage_capacity) { 64 | const size_t num_buckets = (storage_capacity / 2) / bucket_size; 65 | return new QQ::QQ(num_buckets); 66 | } 67 | 68 | void qq_delete(QQ::QQ* q) { 69 | delete q; 70 | } 71 | 72 | size_t qq_size(const QQ::QQ* q) { 73 | return q->size(); 74 | } 75 | 76 | 77 | const QQ::Ptr* qq_storage_peek(QQ::QQ* q) { 78 | auto temp = new QQ::Ptr(std::move(q->peek())); 79 | return temp; 80 | } 81 | 82 | const QQ::Ptr* qq_storage_dequeue(QQ::QQ* q) { 83 | auto c = new QQ::Ptr(std::move(q->dequeue())); 84 | return c; 85 | } 86 | 87 | const QQ::Ptr* qq_storage_try_dequeue(QQ::QQ* q) { 88 | return q->try_dequeue(); 89 | } 90 | 91 | const QQ::Ptr* qq_storage_try_peek(QQ::QQ* q) { 92 | return q->try_peek(); 93 | } 94 | 95 | const QQ::Ptr* qq_storage_enqueue(QQ::QQ* q) { 96 | auto c = new QQ::Ptr(std::move(q->enqueue())); 97 | return c; 98 | } 99 | 100 | void qq_storage_release(QQ::Ptr* ptr) { 101 | delete ptr; // dtor unlocks the storage mutex 102 | } 103 | 104 | size_t qq_storage_size(QQ::Ptr* ptr) { 105 | return ptr->size(); 106 | } 107 | 108 | size_t qq_get_enqueue_counter(QQ::QQ* q) { 109 | return q->get_enqueue_counter(); 110 | } 111 | 112 | size_t qq_get_dequeue_counter(QQ::QQ* q) { 113 | return q->get_dequeue_counter(); 114 | } 115 | 116 | size_t qq_get_enqueue_overflow_counter(QQ::QQ* q) { 117 | return q->get_enqueue_overflow_counter(); 118 | } 119 | 120 | void qq_set_priority(QQ::QQ* q, const uint8_t new_priority) { 121 | return q->set_priority(new_priority); 122 | } 123 | 124 | const QQ::packet_header& qq_storage_get_packet(QQ::Ptr* ptr, const size_t idx) { 125 | return (const QQ::packet_header&) *ptr->operator[](idx); 126 | } 127 | 128 | bool qq_storage_store(QQ::Ptr* ptr, 129 | uint64_t ts, 130 | uint16_t vlan, 131 | uint16_t len, 132 | const uint8_t* data) { 133 | return ptr->store(ts, vlan, len, data); 134 | } 135 | 136 | void dummy_enqueue(QQ::QQ* q) { 137 | auto ptr = q->enqueue(); 138 | uint8_t data[64] = {55}; 139 | memset(data, 55, 64); 140 | ptr.store(123456789, 4095, 64, data); 141 | } 142 | 143 | 144 | uint64_t qq_packet_header_get_timestamp(QQ::packet_header* h) { 145 | return h->timestamp; 146 | } 147 | 148 | uint64_t qq_packet_header_get_vlan(QQ::packet_header* h) { 149 | return h->vlan; 150 | } 151 | 152 | uint16_t qq_packet_header_get_len(QQ::packet_header* h) { 153 | return h->len; 154 | } 155 | 156 | const uint8_t* qq_packet_header_get_data(QQ::packet_header* h) { 157 | return h->data; 158 | } 159 | 160 | // implemented in C++ for better timestamp precision 161 | void qq_inserter_loop(uint8_t device, uint16_t queue_id, QQ::QQ* qq) { 162 | QQ::inserter_loop(device, queue_id, qq); 163 | } 164 | 165 | size_t qq_capacity(const QQ::QQ* q) noexcept { 166 | return q->capacity(); 167 | } 168 | 169 | } 170 | 171 | -------------------------------------------------------------------------------- /examples/exampleUserModule.lua: -------------------------------------------------------------------------------- 1 | local ffi = require "ffi" 2 | local lm = require "libmoon" 3 | local pktLib = require "packet" 4 | local eth = require "proto.ethernet" 5 | local ip = require "proto.ip4" 6 | local log = require "log" 7 | 8 | local module = {} 9 | 10 | -- Define wanted flow keys and flow state 11 | -- must be done on top level to be available/defined in all threads 12 | ffi.cdef [[ 13 | struct my_flow_state { 14 | uint64_t packet_counter; 15 | uint64_t byte_counter; 16 | uint64_t first_seen; 17 | uint64_t last_seen; 18 | uint8_t tracked; 19 | uint16_t some_fancy_data[20]; 20 | }; 21 | 22 | struct my_primary_flow_key { 23 | uint32_t ip_dst; 24 | uint32_t ip_src; 25 | uint16_t port_dst; 26 | uint16_t port_src; 27 | uint8_t proto; 28 | } __attribute__((__packed__)); 29 | 30 | struct my_secondary_flow_key { 31 | uint8_t ip_dst[16]; 32 | uint8_t ip_src[16]; 33 | uint16_t port_dst; 34 | uint16_t port_src; 35 | uint8_t proto; 36 | } __attribute__((__packed__)); 37 | ]] 38 | 39 | -- Set buffer mode 40 | -- "direct" for direct access to the NIC without additional buffering or dumping 41 | -- "qq" for the QQ ringbuffer 42 | module.mode = "qq" 43 | 44 | -- Export flow keys 45 | -- Position in the array corresponds to the index returned by extractFlowKey() 46 | module.flowKeys = { 47 | "struct my_primary_flow_key", 48 | "struct my_secondary_flow_key", 49 | } 50 | 51 | -- Export flow state type 52 | module.stateType = "struct my_flow_state" 53 | 54 | -- Custom default state for new flows 55 | -- See ffi.new() for table initializer rules 56 | module.defaultState = {packet_counter = 0, some_fancy_data = {0xab}} 57 | 58 | -- Function that builds the appropriate flow key for the packet given in buf 59 | -- return true and the hash map index for successful extraction, false if a packet should be ignored 60 | -- Use libmoons packet library to access common protocol fields 61 | -- See tuple.lua for classic IPv4/v6 5-tuple keys 62 | function module.extractFlowKey(buf, keyBuf) 63 | local ethPkt = pktLib.getEthernetPacket(buf) 64 | if ethPkt.eth:getType() == eth.TYPE_IP then 65 | -- actual L4 type doesn't matter 66 | keyBuf = ffi.cast("struct my_primary_flow_key&", keyBuf) 67 | local parsedPkt = pktLib.getUdp4Packet(buf) 68 | keyBuf.ip_dst = parsedPkt.ip4:getDst() 69 | keyBuf.ip_src = parsedPkt.ip4:getSrc() 70 | local TTL = parsedPkt.ip4:getTTL() 71 | -- port is always at the same position as UDP 72 | keyBuf.port_dst = parsedPkt.udp:getDstPort() 73 | keyBuf.port_src = parsedPkt.udp:getSrcPort() 74 | local proto = parsedPkt.ip4:getProtocol() 75 | if proto == ip.PROTO_UDP or proto == ip.PROTO_TCP or proto == ip.PROTO_SCTP then 76 | keyBuf.proto = proto 77 | return true, 1 78 | end 79 | else 80 | log:info("Packet not IP") 81 | end 82 | return false 83 | end 84 | 85 | -- state starts out empty if it doesn't exist yet; buf is whatever the device queue or QQ gives us 86 | -- flowKey will be a ctype of one of the above defined flow keys 87 | function module.handlePacket(flowKey, state, buf, isFirstPacket) 88 | -- qq bufs (mode == "qq") always hold their timestamp of arival at the NIC in seconds 89 | -- lm.getTime() is sourced from the same clock (TSC) and can be directly compared to these 90 | local ts = buf:getTimestamp() * 10^6 -- Shift float to get more digits to store in a uint 91 | state.packet_counter = state.packet_counter + 1 92 | state.byte_counter = state.byte_counter + buf:getSize() 93 | if isFirstPacket then 94 | state.first_seen = ts 95 | end 96 | state.last_seen = ts 97 | 98 | if math.random(0, 10000) == 0 then 99 | state.tracked = 1 100 | return true 101 | else 102 | return false 103 | end 104 | end 105 | 106 | 107 | -- #### Checker configuration #### 108 | 109 | -- Set the interval in which the checkExpiry function should be called. 110 | -- Don't define it or set it to nil to disable the checker task 111 | -- float in seconds 112 | module.checkInterval = 30 113 | 114 | -- Per checker run persistent state, e.g., to track overall flow changes 115 | module.checkState = {start_time = 0} 116 | 117 | -- Function that gets called in regular intervals to decide if a flow is still active. 118 | -- Returns false for active flows. 119 | -- Returns true and a timestamp in seconds for flows that are expired. 120 | function module.checkExpiry(flowKey, state, checkState) 121 | local t = lm.getTime() 122 | local last = tonumber(state.last_seen) / 10^6 -- Convert back to seconds 123 | if state.tracked == 1 and last + 30 < t then 124 | return true, last 125 | end 126 | return false 127 | end 128 | 129 | -- Function that gets called once per checker run at very beginning, before any flow is touched 130 | function module.checkInitializer(checkState) 131 | checkState.start_time = lm.getTime() * 10^6 132 | end 133 | 134 | -- Function that gets called once per checker run at very end, after all flows have been processed 135 | function module.checkFinalizer(checkState, keptFlows, purgedFlows) 136 | local t = lm.getTime() * 10^6 137 | log:info("[Checker]: Done, took %fs, flows %i/%i/%i [purged/kept/total]", (t - tonumber(checkState.start_time)) / 10^6, purgedFlows, keptFlows, purgedFlows+keptFlows) 138 | end 139 | 140 | 141 | -- #### Dumper configuration #### 142 | -- Only applicable if mode is set to "qq" 143 | 144 | module.maxDumperRules = 50 145 | 146 | -- Function that returns a packet filter string in pcap syntax from a given flow key 147 | function module.buildPacketFilter(flowKey) 148 | local ip4AddrDst = ffi.new("union ip4_address") 149 | local ip4AddrSrc = ffi.new("union ip4_address") 150 | ip4AddrDst:set(flowKey.ip_dst) 151 | ip4AddrSrc:set(flowKey.ip_src) 152 | return ("ip proto %i and host %s and host %s and port %i and port %i"):format(flowKey.proto, ip4AddrSrc:getString(), ip4AddrDst:getString(), flowKey.port_dst, flowKey.port_src) 153 | end 154 | 155 | return module 156 | -------------------------------------------------------------------------------- /src/var_hashmap.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace var_hash_map { 6 | /* Secret hash cookie */ 7 | constexpr uint32_t secret = 0xF00BA; 8 | 9 | template::value>::type* = nullptr> 10 | struct var_crc_hash { 11 | var_crc_hash() = default; 12 | var_crc_hash(const var_crc_hash& h) = default; 13 | 14 | inline bool equal(const K& j, const K& k) const noexcept { 15 | return j == k; 16 | } 17 | 18 | // Safety check 19 | static_assert(sizeof(K) == K::size, "sizeof(K) != K::size"); 20 | 21 | /* Specialized hash functions for known key_buf sizes */ 22 | template 23 | inline size_t hash(const U& k, typename std::enable_if::type* = 0) const noexcept { 24 | size_t hash = secret; 25 | return _mm_crc32_u64(hash, *reinterpret_cast(k.data + 0)); 26 | } 27 | 28 | template 29 | inline size_t hash(const U& k, typename std::enable_if::type* = 0) const noexcept { 30 | size_t hash = secret; 31 | hash = _mm_crc32_u64(hash, *reinterpret_cast(k.data + 0)); 32 | return _mm_crc32_u64(hash, *reinterpret_cast(k.data + 8)); 33 | } 34 | 35 | template 36 | inline size_t hash(const U& k, typename std::enable_if::type* = 0) const noexcept { 37 | size_t hash = secret; 38 | hash = _mm_crc32_u64(hash, *reinterpret_cast(k.data + 0)); 39 | hash = _mm_crc32_u64(hash, *reinterpret_cast(k.data + 8)); 40 | hash = _mm_crc32_u64(hash, *reinterpret_cast(k.data + 16)); 41 | return _mm_crc32_u64(hash, *reinterpret_cast(k.data + 24));; 42 | } 43 | 44 | template 45 | inline size_t hash(const U& k, typename std::enable_if::type* = 0) const noexcept { 46 | size_t hash = secret; 47 | hash = _mm_crc32_u64(hash, *reinterpret_cast(k.data + 0)); 48 | hash = _mm_crc32_u64(hash, *reinterpret_cast(k.data + 8)); 49 | hash = _mm_crc32_u64(hash, *reinterpret_cast(k.data + 16)); 50 | hash = _mm_crc32_u64(hash, *reinterpret_cast(k.data + 24)); 51 | hash = _mm_crc32_u64(hash, *reinterpret_cast(k.data + 32)); 52 | hash = _mm_crc32_u64(hash, *reinterpret_cast(k.data + 40)); 53 | hash = _mm_crc32_u64(hash, *reinterpret_cast(k.data + 48)); 54 | return _mm_crc32_u64(hash, *reinterpret_cast(k.data + 56));; 55 | } 56 | 57 | /* Generic version for key_bufs of any length 58 | * TODO: Maybe do something fancy for size mod 4 = 0 59 | */ 60 | /* 61 | inline size_t hash(const K& k) const noexcept { 62 | uint64_t hash = 0; 63 | for (size_t i = 0; i < K::size; ++i) { 64 | hash = _mm_crc32_u8(hash, k.data[i]); 65 | } 66 | return hash; 67 | } 68 | */ 69 | }; 70 | 71 | template 72 | struct key_buf { 73 | static constexpr size_t size = key_size; 74 | uint8_t data[key_size]; 75 | } __attribute__((__packed__)); 76 | 77 | template 78 | inline bool operator==(const key_buf& lhs, const key_buf& rhs) noexcept { 79 | return std::memcmp(lhs.data, rhs.data, key_size) == 0; 80 | } 81 | 82 | template using K = key_buf; 83 | template using V = std::array; 84 | } 85 | 86 | extern "C" { 87 | using namespace var_hash_map; 88 | 89 | #define MAP_IMPL(key_size, value_size) \ 90 | template class tbb::concurrent_hash_map, V, var_crc_hash>>; \ 91 | using hmapk##key_size##v##value_size = tbb::concurrent_hash_map, V, var_crc_hash>>; \ 92 | hmapk##key_size##v##value_size* hmapk##key_size##v##value_size##_create() { \ 93 | return new hmapk##key_size##v##value_size; \ 94 | } \ 95 | void hmapk##key_size##v##value_size##_delete(hmapk##key_size##v##value_size* map) { \ 96 | delete map; \ 97 | } \ 98 | void hmapk##key_size##v##value_size##_clear(hmapk##key_size##v##value_size* map) { \ 99 | map->clear(); \ 100 | } \ 101 | hmapk##key_size##v##value_size::accessor* hmapk##key_size##v##value_size##_new_accessor() { \ 102 | return new hmapk##key_size##v##value_size::accessor; \ 103 | } \ 104 | void hmapk##key_size##v##value_size##_accessor_free(hmapk##key_size##v##value_size::accessor* a) { \ 105 | a->release(); \ 106 | delete a; \ 107 | } \ 108 | void hmapk##key_size##v##value_size##_accessor_release(hmapk##key_size##v##value_size::accessor* a) { \ 109 | a->release(); \ 110 | } \ 111 | bool hmapk##key_size##v##value_size##_access(hmapk##key_size##v##value_size* map, hmapk##key_size##v##value_size::accessor* a, const void* key) { \ 112 | return map->insert(*a, *static_cast*>(key)); \ 113 | } \ 114 | std::uint8_t* hmapk##key_size##v##value_size##_accessor_get_value(hmapk##key_size##v##value_size::accessor* a) { \ 115 | return (*a)->second.data(); \ 116 | } \ 117 | bool hmapk##key_size##v##value_size##_erase(hmapk##key_size##v##value_size* map, hmapk##key_size##v##value_size::accessor* a) { \ 118 | if (a->empty()) std::terminate();\ 119 | return map->erase(*a); \ 120 | } \ 121 | bool hmapk##key_size##v##value_size##_find(hmapk##key_size##v##value_size* map, hmapk##key_size##v##value_size::accessor* a, const void* key) { \ 122 | return map->find(*a, *static_cast*>(key)); \ 123 | } 124 | 125 | #define MAP_VALUES(value_size) \ 126 | MAP_IMPL(8, value_size) \ 127 | MAP_IMPL(16, value_size) \ 128 | MAP_IMPL(32, value_size) \ 129 | MAP_IMPL(64, value_size) 130 | 131 | MAP_VALUES(8) 132 | MAP_VALUES(16) 133 | MAP_VALUES(32) 134 | MAP_VALUES(64) 135 | MAP_VALUES(128) 136 | } 137 | -------------------------------------------------------------------------------- /lua/restApi.lua: -------------------------------------------------------------------------------- 1 | local apiUtils = require 'apiUtils' 2 | local event = require 'event' 3 | 4 | local mod = {} 5 | 6 | -- args need to contain: args.apiToken and api.dumperThreads and we need access to the pipes 7 | -- to interface the dumperThreads and apply new filters 8 | function mod.start(turbo, args, pipes) 9 | -- this webhandler modifies specific filter (i.e given by an id) 10 | local SpecificFilterWebHandler = class("SpecificFilterWebHandler", turbo.web.RequestHandler) 11 | -- this Webhandler handles only creation of a filter and showing all (under the namespace /filter/) 12 | local FilterWebHandler = class("FilterWebHandler", turbo.web.RequestHandler) 13 | 14 | -- RegExes to check on the filter ids/paths 15 | local SPECIFIC_FILTER_REGEX = string.format("^/filter/%s$", apiUtils.filterIdRegex) 16 | 17 | -- create the array of pipes where we want to apply the filter 18 | local ALLOWED_PIPES = {} 19 | for i = 1, args.dumperThreads do 20 | ALLOWED_PIPES[i] = i 21 | end 22 | 23 | local filters = {} 24 | 25 | function SpecificFilterWebHandler:get() 26 | local function showFilter() 27 | local filterId = string.match(self.request.path, SPECIFIC_FILTER_REGEX) 28 | if filters[filterId] ~= nil then 29 | self:write(filters[filterId]) 30 | self:finish() 31 | else 32 | error(turbo.web.HTTPError(404, { error = "Not found" })) 33 | end 34 | end 35 | 36 | apiUtils.ifAuthorized(self, args.apiToken, showFilter) 37 | end 38 | 39 | function SpecificFilterWebHandler:put() 40 | local function notImplemented() 41 | error(turbo.web.HTTPError(501, { error = "Update not implemented." })) 42 | end 43 | 44 | apiUtils.ifAuthorized(self, args.apiToken, notImplemented) 45 | end 46 | 47 | function SpecificFilterWebHandler:delete() 48 | local function deleteFilter() 49 | local filterId = string.match(self.request.path, SPECIFIC_FILTER_REGEX) 50 | if filters[filterId] ~= nil then 51 | local filter = filters[filterId] 52 | filters[filterId] = nil 53 | filter.action = event.delete 54 | -- Remove filter from flowscope 55 | for i, _ in ipairs(filter.pipes) do 56 | pipes[i]:send(event.newEvent(filter.filter, event.delete, filter.id, filter.timestamp)) 57 | end 58 | self:write(filter) 59 | self:finish() 60 | else 61 | error(turbo.web.HTTPError(404, { error = "Not found." })) 62 | end 63 | end 64 | 65 | apiUtils.ifAuthorized(self, args.apiToken, deleteFilter) 66 | end 67 | 68 | function FilterWebHandler:post() 69 | local function createFilter() 70 | -- this will raise an error if json is not present 71 | local jsonBody = self:get_json(true) 72 | 73 | -- decide if we have an array of filters or only one filter 74 | -- specific to one filter are the keys 'filter' and 'id' - but should not have more than 'filter', 'id', 75 | -- 'timestamp' and 'pipes' - else we are ignoring them 76 | -- whereas the array does not have any of these 77 | if jsonBody == nil then 78 | return error(turbo.web.HTTPError(400, { error = "Filter json malformed." })) 79 | end 80 | 81 | local singleFilter = jsonBody['filter'] ~= nil or jsonBody['id'] ~= nil 82 | 83 | if singleFilter then 84 | if apiUtils.checkFilterAttributes(jsonBody) then 85 | local filter = apiUtils.prepareFilter(jsonBody, ALLOWED_PIPES) 86 | apiUtils.applyFilter(filter, filters, pipes) 87 | self:write(filter) 88 | self:finish() 89 | return 90 | else 91 | return error(turbo.web.HTTPError(400, { error = "Filter json malformed." })) 92 | end 93 | end 94 | 95 | -- now we handle the pipelined filter application 96 | local appliedFilter = {} 97 | local filter_error = {} 98 | for _, jsonFilter in ipairs(jsonBody) do 99 | if apiUtils.checkFilterAttributes(jsonFilter) then 100 | local filter = apiUtils.prepareFilter(jsonFilter, ALLOWED_PIPES) 101 | apiUtils.applyFilter(filter, filters, pipes) 102 | appliedFilter[#appliedFilter + 1] = filter 103 | else 104 | filter_error[#filter_error + 1] = filter 105 | end 106 | end 107 | if #filter_error ~= 0 then 108 | return error(turbo.web.HTTPError(400, { 109 | error = "Filter json malformed.", 110 | error_ids = filter_error, 111 | applied_filter = appliedFilter 112 | })) 113 | else 114 | self:write(appliedFilter) 115 | end 116 | self:finish() 117 | end 118 | 119 | apiUtils.ifAuthorized(self, args.apiToken, createFilter) 120 | end 121 | 122 | function FilterWebHandler:get() 123 | local function showFilters() 124 | self:write(filters) 125 | self:finish() 126 | end 127 | 128 | apiUtils.ifAuthorized(self, args.apiToken, showFilters) 129 | end 130 | 131 | function FilterWebHandler:delete() 132 | local function showFilters() 133 | local arguments = self.request.arguments 134 | local filter_ids = {} 135 | 136 | if arguments['filter_id'] ~= nil then 137 | if type(arguments['filter_id']) == 'table' then 138 | filter_ids = arguments['filter_id'] 139 | else 140 | filter_ids[#filter_ids + 1] = arguments['filter_id'] 141 | end 142 | else 143 | return error(turbo.web.HTTPError(400, { error = "URL parameter malformed." })) 144 | end 145 | -- arrays to keep track which filter_ids we have deleted and which we couldn't find 146 | local removed_filter = {} 147 | local filter_not_found = {} 148 | for _, filterId in pairs(filter_ids) do 149 | if filters[filterId] ~= nil then 150 | local filter = filters[filterId] 151 | filters[filterId] = nil 152 | filter.action = event.delete 153 | -- Remove filter from flowscope 154 | for i, _ in ipairs(filter.pipes) do 155 | pipes[i]:send(event.newEvent(filter.filter, event.delete, filter.id, filter.timestamp)) 156 | end 157 | removed_filter[#removed_filter + 1] = filter 158 | else 159 | filter_not_found[#filter_not_found + 1] = filterId 160 | end 161 | end 162 | 163 | if #filter_not_found ~= 0 then 164 | return error(turbo.web.HTTPError(404, { error = "Filter not found", not_found_ids = filter_not_found, removed_filter = removed_filter })) 165 | end 166 | 167 | self:write(removed_filter) 168 | self:finish() 169 | end 170 | 171 | apiUtils.ifAuthorized(self, args.apiToken, showFilters) 172 | end 173 | 174 | return { 175 | { SPECIFIC_FILTER_REGEX, SpecificFilterWebHandler }, 176 | { "^/filter/$", FilterWebHandler } 177 | } 178 | end 179 | 180 | return mod 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FlowScope is an oscilloscope for your network traffic. It records all traffic continuously in a ring buffer and dumps specific flows to disk on trigger events. 2 | Triggers can be packets matching a user-defined filter. 3 | 4 | [Read our new and updated paper](https://www.net.in.tum.de/fileadmin/bibtex/publications/papers/FlowScope-flow-tracking.pdf) (CloudNet 2018, [BibTeX](https://www.net.in.tum.de/publications/bibtex/FLowScopeCloudNet18.bib)) about FlowScope to learn more about FlowScope's architecture and flow tracking capabilities. 5 | 6 | Our [older publication](https://www.net.in.tum.de/fileadmin/bibtex/publications/papers/FlowScope.pdf) (IFIP Networking 2017, [BibTeX](https://www.net.in.tum.de/publications/bibtex/FlowScope17.bib)) focuses on our QQ data structure used for time-traveling network debugging. 7 | 8 | 9 | # Architecture 10 | 11 | ![path_of_a_packet](path_of_a_packet.png) 12 | 13 | FlowScope consists of 3 different task that classify, analyze and dump packets. 14 | These can be programmed and modified by user modules written in Lua. 15 | 16 | Further it uses the QQ ring buffer which allows non-destructive early dequeuing (peeking) at the head and the ``concurrent_hash_map`` from [TBB](https://www.threadingbuildingblocks.org/docs/help/index.htm). 17 | 18 | Due to their nature as C++ template classes, ``concurrent_hash_map``s need to be instantiated with known key and value types at compile time. But this would require a module author to write their own bindings for their wanted flow keys and values. As this is bothersome, we already define and bind multiple versions of the hash maps with type agnostic byte arrays of common sizes. As long as a key/value fits in one of the byte arrays, a hash map can be created dynamically at run time with any type. 19 | 20 | 21 | # QQ (Queue-in-Queue) Ringbuffer 22 | 23 | FlowScope can use the QQ ringbuffer to keep packets in memory instead of discarding them immediately. 24 | QQ consists of 2 levels of nested queues; the outer queue holding references to the inner ones, and the inner queues actually storing the data. This layered design improves multi-threaded performance by reducing the contention for the mutex guarding access. Instead of acquiring the lock on every insert/dequeue, threads get a complete inner queue exclusively for a short time. 25 | This model works nicely with the common receive-side scaling (RSS) optimization in NICs, because QQ perseveres intra-flow packet ordering. 26 | The inner queues store variably sized objects (like packets) tightly packed without losing useful properties like random access. 27 | 28 | Further, QQ allows access to elements a the queue head without dequeuing them, so that analyzers see new packets as soon as possible. Contrary to other buffers QQ aims to be as filled as possible. It does not allow dequeuing packets until the fill level reaches the high water mark, so that as many as possible packets are still available in case of detected anomalies. 29 | 30 | 31 | ## Analyzer 32 | 33 | The Analyzer(s) dequeues packets either directly from a NIC or through an intermediary ring buffer (QQ) as soon as they arrive (``rte_eth_rx_burst()``/``QQ_peek()``). With the ``extractFlowKey()`` function each packet is classified into one of the N flow tables by extracting its identifying flow feature (e.g. 5-Tuple, IP ID, etc.). This process is idempotent and does not yet involve any state. Basic pre-filtering can be performed very cheaply here, e.g., by discarding IPv4 traffic in an IPv6-only measurement. The function therefore returns if a packet is interesting at all and to which hash table it belongs. The Checker is informed about every interesting packet. 34 | 35 | From the flow key the flow state is looked up in the corresponding hash table. This locks the cell for exclusive read-write access until the user module function ``handlePacket()`` returns. 36 | ``handlePacket()`` can perform arbitrary flow analysis based on the flows previous state and the current packet and updates the flow state with new data. Should a threshold be passed or an anomaly be identified, the function can request the archival of this flow by returning ``true`` (For efficiency reasons this should only happen once per flow. A simple bool flag in the flow state usually suffices). 37 | 38 | For such flows a PCAP filter expression has then to be build by the module in the ``buildPacketFilter()`` function. 39 | 40 | 41 | ## Checker 42 | 43 | Since the modules ``handlePacket()`` function is only called for arriving packets, there would be no way to detect and delete inactive flows. Therefore a checker iterates over all flows in the hash tables in regular intervals and passes them to the ``checkExpiry()`` module function. Here the user can decide if a flow is still active, by, e.g. keeping the timestamp of the last seen packet or protocol specific flags (e.g. TCP fin). Should a flow deemed inactive, it is purged from the hash map and the dumpers are instructed to forget its matching filter rule. 44 | 45 | Due to technical limitations hash tables generally are not concurrently iterable and write-able. As a workaround we store every key in an array and just iterate of this array instead. 46 | 47 | 48 | ## Dumper 49 | 50 | Dumpers dequeue packets from the QQ ring buffer as late as possible to maximize the amount of information available in case of a detected anomaly. 51 | 52 | The [pflua](https://github.com/Igalia/pflua) framework is used to facilitate high performance packet matching with the familiar PCAP filter syntax. More precisely the [pfmatch](https://github.com/Igalia/pflua/blob/master/doc/pfmatch.md) dialect, also seen in Snabb, is used. It is more powerful then normal pflang filters as it directly attaches functions to matches instead of just returning a yes/no filter decision. Together with the Lua-JIT compiler this allows better optimization and direct dumping to the appropriate per-flow pcap file. 53 | 54 | Due to their (possibly immensely) delayed processing of the packets, rules can not be immediately discarded once a flow is inactive or the capture of interesting flows could end early, leading to missing packets. If the Checkers requests deletion of of a flow rule, the timestamp of the last seen packet is also included. With this Dumpers know exactly when it is safe to finally forget about a rule; if the other dequeued packets in QQ are older than the timestamp. 55 | 56 | 57 | ## Simple Example - Flow Statistics 58 | 59 | The following describes the inner workings off FlowScope and the stages a packets passes through on the example of the flow statistics user module [examples/liveStatistician.lua](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua) which calculates traffic metrics for IP flows. 60 | 61 | To get access to utility functions for time, protocol parsing and libmoon itself, the module import them in [lines 1-6](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L1-L6). 62 | 63 | Next the modules defines local variables that are not exposed to the FlowScope driver. This can be configuration or values required for calculations, like here. flowStatistician defines a flow as expired once no packet has arrived in [30 seconds](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L10). 64 | 65 | ### Analyzer 66 | 67 | Next come the required module definitions so that FlowScope knows what to initialize and which functions to call. Lines [12 to 22](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L12-L22) define a C struct which encapsulates the state of a flow. In [module.stateType](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L25) this type is exposed, so that FlowScope can instantiate the hash tables with this as the value type. It is possible to give an alternative default state to all-zeros with [module.defaultState](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L26). 68 | Lastly the flow key types are defined. Since FlowScope already comes with a IP 5-Tuple key and extraction function, it is reused from the tuple lib. 69 | 70 | With the flowkey extracted the associated flow state is looked up from the hash table and the [handlePacket](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L29) function is called. 71 | 72 | ### Checker 73 | 74 | Next is the checker configured. [module.checkInterval](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L42) sets how often it should run. Here it is set to observe 5 seconds intervals of traffic. 75 | With the [checkInitializer](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L44) a module can perform setup actions for the checker state. It is run once per checker-run, at the very beginning, before any flow is accessed. Here the module sets some counters and an empty list for the top X flows ([L45-49](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L45-L49)). 76 | 77 | The [checkExpiry](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L67) function is then called for every flow currently present in the hash tables. In its arguments it holds the flow key, flow state and checker state. 78 | The module calculates various [traffic metrics](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L70-L72) such as bits per second and packets per second and then [compares](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L52-L65) this flow to the others if it contains more traffic. 79 | For the next checker run, the counters of a flow are [reset](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L80-L83). 80 | Further it adds this flows metric to a [global counter](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L77-L78) to also get data about the total amount traffic. 81 | 82 | Lastly the checker decides if a flow is still active by comparing its last seen value (filled in by the analyzers) to the current time ([L85-90](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L85-L90)). 83 | 84 | At the end of a run, the collected data is printed out in the [checkFinalizer](https://github.com/pudelkoM/FlowScope/blob/master/examples/liveStatistician.lua#L93-L101) function. 85 | 86 | ### Path of a Packet 87 | 88 | First the flow key has to be extracted from the packet. This is done in the [extractIP5Tuple](https://github.com/pudelkoM/FlowScope/blob/master/lua/tuple.lua#L96) and [extractIP5TupleUni](https://github.com/pudelkoM/FlowScope/blob/master/lua/tuple.lua#L72) functions. By using libmoon's packet library stack the packet is layer-for-layer unwrapped and the key buffer is [filled](https://github.com/pudelkoM/FlowScope/blob/master/lua/tuple.lua#L76-L88) with the information. 89 | 90 | ## Advanced Example - TTL Analysis 91 | 92 | # Installation 93 | 94 | 1. `git submodule update --init --recursive` 95 | 2. Compile libmoon in the `libmoon` submodule. Follow instructions [there](https://github.com/libmoon/libmoon#installation). 96 | 3. `cd build ; cmake .. ; make ; cd ..` 97 | 4. `./libmoon/build/libmoon lua/flowscope.lua --help` 98 | 99 | FlowScope requires gcc 5 or later. You can use 100 | 101 | CC=gcc-5 CXX=g++-5 cmake .. 102 | 103 | to set the compiler if gcc 5 is not your default. 104 | 105 | 106 | # Usage 107 | 108 | ## Immediate mode without buffering/dumping 109 | 110 | A simple test setup with synthetic traffic for quick testing can be built with two directly connected machines. 111 | 112 | * Install FlowScope on host A und [libmoon](https://github.com/emmericp/libmoon) on host B 113 | * Start monitoring on host A: ```./libmoon/build/libmoon lua/flowscope.lua examples/liveStatistician.lua ``` 114 | * Run ```./build/libmoon examples/pktgen.lua --rate=5000 ``` on host B 115 | 116 | The `pktgen.lua` MoonGen script generates 1000 UDP flows in the subnet 10.0.0.0/16 on random ports in the range 1234 to 2234. 117 | 118 | For a 40 Gbit XL710 NIC you should see similar output like this on the monitor host: 119 | ``` 120 | Top flows over sliding 5s window: 121 | # bps pps Flow 122 | 1 649586.24 10826.44 ipv4_5tuple{ip_a: 10.0.0.10, ip_b: 10.1.0.10, port_a: 2143, port_b: 1234, proto: udp} 123 | 2 648500.83 10808.35 ipv4_5tuple{ip_a: 10.0.0.10, ip_b: 10.1.0.10, port_a: 1950, port_b: 1234, proto: udp} 124 | 3 647902.81 10798.38 ipv4_5tuple{ip_a: 10.0.0.10, ip_b: 10.1.0.10, port_a: 2164, port_b: 1234, proto: udp} 125 | [...] 126 | Active flows 1000, cumulative packets 53329880 [10665976.00/s], cumulative bytes 3199792800 [639958560.00/s], took 0.00s 127 | ``` 128 | 129 | # Hardware Requirements 130 | 131 | 1. A CPU with a constant and invariant TSC. All recent Intel CPUs (Nehalem or newer) have this feature. 132 | 2. See [libmoon](https://github.com/emmericp/libmoon) 133 | 134 | # Mini Benchmark 135 | 136 | The following mini benchmark show the processing rate of FlowScope analyzer threads when doing no analysis on packets. 137 | To minimize influence of module functions, the benchmark was created with the noop.lua module which does only extract IP 5-Tuples and accesses the hash tables, but does not calculate anything. It more or less measures the upper performance bound. 138 | 139 | Packet rates for 1 M flows: 140 | ![benchmarks/noop.pdf](benchmarks/noop.png) 141 | 142 | Packet rates for 4 Threads: 143 | ![benchmarks/flow-scaling.pdf](benchmarks/flow-scaling.png) 144 | -------------------------------------------------------------------------------- /lua/flowtracker.lua: -------------------------------------------------------------------------------- 1 | local ffi = require "ffi" 2 | local C = ffi.C 3 | local memory = require "memory" 4 | local flowtrackerlib = ffi.load("../build/flowtracker") 5 | local hmap = require "hmap" 6 | local lm = require "libmoon" 7 | local log = require "log" 8 | local stats = require "stats" 9 | local pktLib = require "packet" 10 | local eth = require "proto.ethernet" 11 | local ip = require "proto.ip4" 12 | local pipe = require "pipe" 13 | local timer = require "timer" 14 | local pcap = require "pcap" 15 | local ev = require "event" 16 | local qqLib = require "qq" 17 | local pf = require "pf" 18 | local match = require "pf.match" 19 | 20 | 21 | local mod = {} 22 | 23 | ffi.cdef[[ 24 | struct new_flow_info { 25 | uint8_t index; 26 | void* flow_key; 27 | }; 28 | ]] 29 | 30 | local flowtracker = {} 31 | 32 | function mod.new(args) 33 | -- Check parameters 34 | for k, v in pairs(args) do 35 | log:info("%s: %s", k, v) 36 | end 37 | if args.stateType == nil then 38 | log:error("Module has no stateType") 39 | return nil 40 | end 41 | if type(args.flowKeys) ~= "table" then 42 | log:error("Module has no flow keys table") 43 | return nil 44 | end 45 | if #args.flowKeys < 1 then 46 | log:error("Flow key array must contain at least one entry") 47 | return nil 48 | end 49 | if args.defaultState == nil then 50 | log:info("Module has no default flow state, using {}") 51 | args.defaultState = {} 52 | end 53 | if type(args.extractFlowKey) ~= "function" then 54 | log:error("Module has no extractFlowKey function") 55 | return nil 56 | end 57 | local obj = setmetatable(args, flowtracker) 58 | 59 | -- Create hash maps 60 | obj.maps = {} 61 | for _, v in ipairs(args.flowKeys) do 62 | local m = hmap.createHashmap(ffi.sizeof(v), ffi.sizeof(obj.stateType)) 63 | log:info("{%s -> %s}: %s", v, obj.stateType, m) 64 | table.insert(obj.maps, m) 65 | end 66 | 67 | -- Create temporary object with zero bytes or user-defined initializers 68 | local tmp = ffi.new(obj.stateType, obj.defaultState) 69 | -- Allocate persistent (non-GC) memory 70 | obj.defaultState = memory.alloc("void*", ffi.sizeof(obj.stateType)) 71 | -- Make temporary object persistent 72 | ffi.copy(obj.defaultState, tmp, ffi.sizeof(obj.stateType)) 73 | 74 | -- Setup expiry checker pipes 75 | obj.pipes = {} 76 | 77 | -- Setup filter pipes for dumpers 78 | obj.filterPipes = {} 79 | 80 | -- Setup table for QQs 81 | obj.qq = {} 82 | 83 | -- Shutdown delay to catch packets hanging in QQ. In ms 84 | obj.shutdownDelay = 3000 85 | 86 | return obj 87 | end 88 | 89 | -- Starts a new analyzer 90 | function flowtracker:startNewAnalyzer(userModule, queue) 91 | local p = pipe.newFastPipe() 92 | table.insert(self.pipes, p) -- Store pipes so the checker can access them 93 | if ffi.istype("qq_t", queue) then 94 | log:info("QQ mode") 95 | lm.startTask("__FLOWTRACKER_ANALYZER_QQ", self, userModule, queue, p) 96 | else 97 | log:info("direct mode") 98 | lm.startTask("__FLOWTRACKER_ANALYZER", self, userModule, queue, p) 99 | end 100 | end 101 | 102 | -- Starts the flow expiry checker 103 | -- Must only be called after all analyzers are set up 104 | function flowtracker:startChecker(userModule) 105 | lm.startTask("__FLOWTRACKER_CHECKER", self, userModule) 106 | end 107 | 108 | -- Starts a new dumper 109 | -- Must be started before any analyzer 110 | function flowtracker:startNewDumper(path, qq) 111 | local p = pipe.newSlowPipe() 112 | table.insert(self.filterPipes, p) 113 | lm.startTask("__FLOWTRACKER_DUMPER", self, #self.filterPipes, qq, path, p) 114 | end 115 | 116 | -- Starts a new task that inserts packets from a NIC queue into a QQ 117 | function flowtracker:startNewInserter(rxQueue, qq) 118 | lm.startTask("__FLOWTRACKER_INSERTER", rxQueue, qq) 119 | end 120 | 121 | function flowtracker:analyzer(userModule, queue, flowPipe) 122 | userModule = loadfile(userModule)() 123 | 124 | -- Cast flow state + default back to correct type 125 | local stateType = ffi.typeof(userModule.stateType .. "*") 126 | self.defaultState = ffi.cast(stateType, self.defaultState) 127 | 128 | -- Cache functions 129 | local handler = userModule.handlePacket 130 | local extractFlowKey = userModule.extractFlowKey 131 | 132 | -- Allocate hash map accessors 133 | local accs = {} 134 | for _, v in ipairs(self.maps) do 135 | table.insert(accs, v.newAccessor()) 136 | end 137 | 138 | -- Allocate flow key buffer 139 | local sz = hmap.getLargestKeyBufSize(self.maps) 140 | local keyBuf = ffi.new("uint8_t[?]", sz) 141 | log:info("Key buffer size: %i", sz) 142 | 143 | local bufs = memory.bufArray() 144 | local rxCtr = stats:newPktRxCounter("Analyzer") 145 | 146 | --require("jit.p").start("a") 147 | while lm.running(self.shutdownDelay) do 148 | local rx = queue:tryRecv(bufs, 10) 149 | for i = 1, rx do 150 | local buf = bufs[i] 151 | rxCtr:countPacket(buf) 152 | ffi.fill(keyBuf, sz) -- Clear shared key buffer 153 | local success, index = extractFlowKey(buf, keyBuf) 154 | if success then 155 | local flowKey = ffi.cast(userModule.flowKeys[index] .. "*", keyBuf) -- Correctly cast alias to the key buffer 156 | local isNew = self.maps[index]:access(accs[index], keyBuf) 157 | local t = accs[index]:get() 158 | local valuePtr = ffi.cast(stateType, t) 159 | if isNew then 160 | -- Copy-construct default state 161 | ffi.copy(valuePtr, self.defaultState, ffi.sizeof(self.defaultState)) 162 | 163 | -- Copy keyBuf and inform checker about new flow 164 | if userModule.checkInterval then -- Only bother if there are dumpers 165 | local t = memory.alloc("void*", sz) 166 | ffi.fill(t, sz) 167 | ffi.copy(t, keyBuf, sz) 168 | local info = memory.alloc("struct new_flow_info*", ffi.sizeof("struct new_flow_info")) 169 | info.index = index 170 | info.flow_key = t 171 | -- we use send here since we know a checker exists and deques/frees our flow keys 172 | flowPipe:send(info) 173 | end 174 | end 175 | -- direct mode has no dumpers, so we can ignore dump requests of the handler 176 | handler(flowKey, valuePtr, buf, isNew) 177 | accs[index]:release() 178 | end 179 | end 180 | bufs:free(rx) 181 | rxCtr:update() 182 | end 183 | --require("jit.p").stop() 184 | for _, v in ipairs(accs) do 185 | v:free() 186 | end 187 | rxCtr:finalize() 188 | end 189 | 190 | function flowtracker:analyzerQQ(userModule, queue, flowPipe) 191 | userModule = loadfile(userModule)() 192 | 193 | -- Cast flow state + default back to correct type 194 | local stateType = ffi.typeof(userModule.stateType .. "*") 195 | self.defaultState = ffi.cast(stateType, self.defaultState) 196 | 197 | -- Cache functions 198 | local handler = userModule.handlePacket 199 | local extractFlowKey = userModule.extractFlowKey 200 | local buildPacketFilter = userModule.buildPacketFilter 201 | 202 | -- Allocate hash map accessors 203 | local accs = {} 204 | for _, v in ipairs(self.maps) do 205 | table.insert(accs, v.newAccessor()) 206 | end 207 | 208 | -- Allocate flow key buffer 209 | local sz = hmap.getLargestKeyBufSize(self.maps) 210 | local keyBuf = ffi.new("uint8_t[?]", sz) 211 | log:info("Key buffer size: %i", sz) 212 | 213 | local rxCtr = stats:newPktRxCounter("Analyzer") 214 | 215 | --require("jit.p").start("a") 216 | while lm.running(self.shutdownDelay) do 217 | local storage = queue:tryPeek() 218 | if storage ~= nil then 219 | for i = 0, storage:size() - 1 do 220 | local buf = storage:getPacket(i) 221 | rxCtr:countPacket(buf) 222 | ffi.fill(keyBuf, sz) -- Clear shared key buffer 223 | local success, index = extractFlowKey(buf, keyBuf) 224 | if success then 225 | local flowKey = ffi.cast(userModule.flowKeys[index] .. "*", keyBuf) -- Correctly cast alias to the key buffer 226 | local isNew = self.maps[index]:access(accs[index], keyBuf) 227 | local t = accs[index]:get() 228 | local valuePtr = ffi.cast(stateType, t) 229 | if isNew then 230 | -- Copy-construct default state 231 | ffi.copy(valuePtr, self.defaultState, ffi.sizeof(self.defaultState)) 232 | 233 | -- Copy keyBuf and inform checker about new flow 234 | if userModule.checkInterval then -- Only bother if there are dumpers 235 | local t = memory.alloc("void*", sz) 236 | ffi.fill(t, sz) 237 | ffi.copy(t, keyBuf, sz) 238 | local info = memory.alloc("struct new_flow_info*", ffi.sizeof("struct new_flow_info")) 239 | info.index = index 240 | info.flow_key = t 241 | -- we use send here since we know a checker exists and deques/frees our flow keys 242 | flowPipe:send(info) 243 | end 244 | end 245 | if handler(flowKey, valuePtr, buf, isNew) then 246 | local event = ev.newEvent(buildPacketFilter(flowKey), ev.create) 247 | log:debug("[Analyzer]: Handler requested dump of flow %s", flowKey) 248 | for _, pipe in ipairs(self.filterPipes) do 249 | pipe:send(event) 250 | end 251 | end 252 | accs[index]:release() 253 | end 254 | end 255 | storage:release() 256 | end 257 | rxCtr:update() 258 | end 259 | --require("jit.p").stop() 260 | for _, v in ipairs(accs) do 261 | v:free() 262 | end 263 | rxCtr:finalize() 264 | end 265 | 266 | function flowtracker:checker(userModule) 267 | userModule = loadfile(userModule)() 268 | if not userModule.checkInterval then 269 | log:info("[Checker]: Disabled by user module") 270 | return 271 | end 272 | local stateType = ffi.typeof(userModule.stateType .. "*") 273 | local checkTimer = timer:new(self.checkInterval) 274 | local initializer = userModule.checkInitializer or function() end 275 | local finalizer = userModule.checkFinalizer or function() end 276 | local buildPacketFilter = userModule.buildPacketFilter or function() end 277 | local checkState = userModule.checkState or {} 278 | 279 | -- Flow list 280 | local flows = {} 281 | local addToList = function(l, flow) 282 | l[#l + 1] = flow 283 | end 284 | local deleteFlow = function(flow) 285 | memory.free(flow.flow_key) 286 | memory.free(flow) 287 | end 288 | 289 | -- Allocate hash map accessors 290 | local accs = {} 291 | for _, v in ipairs(self.maps) do 292 | table.insert(accs, v.newAccessor()) 293 | end 294 | 295 | -- require("jit.p").start("a") 296 | while lm.running(self.shutdownDelay) do 297 | for _, pipe in ipairs(self.pipes) do 298 | local newFlow = pipe:tryRecv(10) 299 | if newFlow ~= nil then 300 | newFlow = ffi.cast("struct new_flow_info&", newFlow) 301 | --print("checker", newFlow) 302 | addToList(flows, newFlow) 303 | end 304 | end 305 | if checkTimer:expired() then 306 | log:info("[Checker]: Started") 307 | checkTimer:reset() -- Reseting the timer first makes the checker self-clocking 308 | -- require("jit.p").start("a") 309 | local t1 = time() 310 | local purged, keep = 0, 0 311 | local keepList = {} 312 | initializer(checkState) 313 | for i = #flows, 1, -1 do 314 | local index, keyBuf = flows[i].index, flows[i].flow_key 315 | local isNew = self.maps[index]:access(accs[index], keyBuf) 316 | assert(isNew == false) -- Must hold or we have an error 317 | local valuePtr = ffi.cast(stateType, accs[index]:get()) 318 | local flowKey = ffi.cast(userModule.flowKeys[index] .. "*", keyBuf) 319 | local expired, ts = userModule.checkExpiry(flowKey, valuePtr, checkState) 320 | if expired then 321 | assert(ts) 322 | self.maps[index]:erase(accs[index]) 323 | local event = ev.newEvent(buildPacketFilter(flowKey), ev.delete, nil, ts) 324 | for _, pipe in ipairs(self.filterPipes) do 325 | pipe:send(event) 326 | end 327 | deleteFlow(flows[i]) 328 | purged = purged + 1 329 | else 330 | addToList(keepList, flows[i]) 331 | keep = keep + 1 332 | end 333 | accs[index]:release() 334 | end 335 | flows = keepList 336 | finalizer(checkState, keep, purged) 337 | local t2 = time() 338 | log:info("[Checker]: Done, took %fs, flows %i/%i/%i [purged/kept/total]", t2 - t1, purged, keep, purged+keep) 339 | -- require("jit.p").stop() 340 | end 341 | end 342 | -- require("jit.p").stop() 343 | for _, v in ipairs(accs) do 344 | v:free() 345 | end 346 | log:info("[Checker]: Shutdown") 347 | end 348 | 349 | function flowtracker:dumper(id, qq, path, filterPipe) 350 | pcap:setInitialFilesize(2^19) -- 0.5 MiB 351 | local ruleSet = {} -- Used to maintain the filter strings and pcap handles 352 | local handlers = {} -- Holds handle functions for the matcher 353 | local matcher = nil 354 | local currentTS = 0 -- Timestamp of the current packet. Used to expire rules and to pass a ts to the pcap writer 355 | local ruleCtr = 0 356 | local maxRules = self.maxDumperRules 357 | local needRebuild = true 358 | local rxCtr = stats:newManualRxCounter("Dumper", "plain") 359 | 360 | log:setLevel("INFO") 361 | 362 | require("jit.p").start("a") 363 | local handleEvent = function(event) 364 | if event == nil then 365 | return 366 | end 367 | log:debug("[Dumper]: Got event %i, %s, %i", event.action, event.filter, event.timestamp or 0) 368 | if event.action == ev.create and ruleSet[event.id] == nil and ruleCtr < maxRules then 369 | local triggerWallTime = wallTime() 370 | local pcapFileName = path .. "/" .. ("FlowScope-dump " .. os.date("%Y-%m-%d %H-%M-%S", triggerWallTime) .. " " .. event.id .. " part " .. id .. ".pcap"):gsub("[ /\\]", "_") 371 | local pcapWriter = pcap:newWriter(pcapFileName, triggerWallTime) 372 | ruleSet[event.id] = {filter = event.filter, pcap = pcapWriter} 373 | ruleCtr = ruleCtr + 1 374 | needRebuild = true 375 | elseif event.action == ev.delete and ruleSet[event.id] ~= nil then 376 | ruleSet[event.id].timestamp = event.timestamp 377 | log:info("[Dumper]: Marked rule %s as expired at %f, now %f", event.id, event.timestamp, currentTS) 378 | end 379 | end 380 | 381 | while lm.running(self.shutdownDelay) do 382 | -- Get new filters 383 | local event 384 | repeat 385 | event = filterPipe:tryRecv(10) 386 | handleEvent(event) 387 | until event == nil 388 | 389 | -- Check for expired rules 390 | for k, _ in pairs(ruleSet) do 391 | if ruleSet[k].timestamp and currentTS > ruleSet[k].timestamp then 392 | ruleSet[k].pcap:close() 393 | log:info("[Dumper #%i]: Expired rule %s, %f > %f", id, k, currentTS, ruleSet[k].timestamp) 394 | ruleSet[k] = nil 395 | ruleCtr = ruleCtr - 1 396 | needRebuild = true 397 | end 398 | end 399 | 400 | -- Rebuild matcher from ruleSet 401 | if needRebuild then 402 | handlers = {} 403 | local lines = {} 404 | local idx = 0 405 | for _, v in pairs(ruleSet) do 406 | idx = idx + 1 407 | handlers["h" .. idx] = function(data, l) v.pcap:write(currentTS, data, l) end -- We can't pass a timestamp through the pflua matcher directly, so we keep it in a local variable before calling it 408 | table.insert(lines, v.filter .. " => " .. "h" .. idx .. "()") -- Build line in pfmatch syntax 409 | end 410 | log:info("[Dumper]: total number of rules: %i", idx) 411 | local allLines = table.concat(lines, "\n") 412 | log:debug("[Dumper]: all rules:\n%s", allLines) 413 | --print(match.compile("match {" .. allLines .. "}", {source = true})) 414 | matcher = match.compile("match {" .. allLines .. "}") 415 | needRebuild = false 416 | end 417 | 418 | -- Filter packets 419 | local storage = qq:tryDequeue() 420 | if storage ~= nil then 421 | rxCtr:updateWithSize(storage:size(), 0) 422 | for i = 0, storage:size() - 1 do 423 | local pkt = storage:getPacket(i) 424 | local timestamp = pkt:getTimestamp() 425 | local data = pkt:getBytes() 426 | local len = pkt:getSize() 427 | currentTS = timestamp 428 | matcher(handlers, data, len) 429 | end 430 | storage:release() 431 | else 432 | lm.sleepMicrosIdle(10) 433 | end 434 | rxCtr:update(0, 0) 435 | end 436 | require("jit.p").stop() 437 | rxCtr:finalize() 438 | for _, rule in pairs(ruleSet) do 439 | rule.pcap:close() 440 | end 441 | log:info("[Dumper]: Shutdown") 442 | end 443 | 444 | function flowtracker.inserter(rxQueue, qq) 445 | qq:inserterLoop(rxQueue) 446 | log:info("[Inserter]: Shutdown") 447 | end 448 | 449 | function flowtracker:delete() 450 | memory.free(self.defaultState) 451 | for _, v in ipairs(self.maps) do 452 | v:delete() 453 | end 454 | for _, v in ipairs(self.pipes) do 455 | v:delete() 456 | end 457 | for _, v in ipairs(self.filterPipes) do 458 | v:delete() 459 | end 460 | for _, v in ipairs(self.qq) do 461 | v:delete() 462 | end 463 | end 464 | 465 | flowtracker.__index = flowtracker 466 | 467 | -- usual libmoon threading magic 468 | __FLOWTRACKER_ANALYZER = flowtracker.analyzer 469 | mod.analyzerTask = "__FLOWTRACKER_ANALYZER" 470 | 471 | __FLOWTRACKER_ANALYZER_QQ = flowtracker.analyzerQQ 472 | mod.analyzerQQTask = "__FLOWTRACKER_ANALYZER_QQ" 473 | 474 | __FLOWTRACKER_CHECKER = flowtracker.checker 475 | mod.checkerTask = "__FLOWTRACKER_CHECKER" 476 | 477 | __FLOWTRACKER_DUMPER = flowtracker.dumper 478 | mod.dumperTask = "__FLOWTRACKER_DUMPER" 479 | 480 | __FLOWTRACKER_INSERTER = flowtracker.inserter 481 | mod.inserterTask = "__FLOWTRACKER_INSERTER" 482 | 483 | -- don't forget the usual magic in __serialize for thread-stuff 484 | 485 | return mod 486 | -------------------------------------------------------------------------------- /src/QQ.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | 20 | #define PP_STR2(str) #str 21 | #define PP_STR(str) PP_STR2(str) 22 | #define handle_error(msg) \ 23 | do { perror(__FILE__ ":" PP_STR(__LINE__) ":\n\t" msg); exit(EXIT_FAILURE); } while (0) 24 | 25 | 26 | #if __GNUC__ < 5 && !defined(__clang__) 27 | /* Taken from LLVM libcxx - MIT Licence */ 28 | namespace std { 29 | inline void* 30 | align(size_t alignment, size_t size, void*& ptr, size_t& space) noexcept 31 | { 32 | void* r = nullptr; 33 | if (size <= space) 34 | { 35 | char* p1 = static_cast(ptr); 36 | char* p2 = reinterpret_cast(reinterpret_cast(p1 + (alignment - 1)) & -alignment); 37 | size_t d = static_cast(p2 - p1); 38 | if (d <= space - size) 39 | { 40 | r = p2; 41 | ptr = r; 42 | space -= d; 43 | } 44 | } 45 | return r; 46 | } 47 | } // namespace std 48 | #endif 49 | 50 | namespace QQ { 51 | template 52 | std::string format_bytes(T bytes, int precision = 2) { 53 | std::stringstream s; 54 | s << std::setprecision(precision) << std::fixed; 55 | if (bytes >= 1ULL << 40) 56 | s << (1. * bytes / (1ULL << 40)) << " TiB"; 57 | else if (bytes >= 1ULL << 30) 58 | s << (1. * bytes / (1ULL << 30)) << " GiB"; 59 | else if (bytes >= 1ULL << 20) 60 | s << (1. * bytes / (1ULL << 20)) << " MiB"; 61 | else if (bytes >= 1ULL << 10) 62 | s << (1. * bytes / (1ULL << 10)) << " KiB"; 63 | else 64 | s << bytes << " B"; 65 | 66 | return s.str(); 67 | } 68 | 69 | template 70 | std::string format_bits(T bytes, int precision = 2) { 71 | std::stringstream s; 72 | s << std::setprecision(precision) << std::fixed; 73 | if (bytes >= 100000000000ULL) 74 | s << (8. * bytes / 1000000000000ULL) << " Tbit"; 75 | else if (bytes >= 100000000ULL) 76 | s << (8. * bytes / 1000000000ULL) << " Gbit"; 77 | else if (bytes >= 100000ULL) 78 | s << (8. * bytes / 1000000ULL) << " Mbit"; 79 | else if (bytes >= 100ULL) 80 | s << (8. * bytes / 1000ULL) << " Kbit"; 81 | else 82 | s << 8. * bytes << " bit"; 83 | 84 | return s.str(); 85 | } 86 | 87 | template 88 | std::string format_SI(T num, int precision = 2) { 89 | std::stringstream s; 90 | s << std::setprecision(precision) << std::fixed; 91 | if (num >= 1000000000000ULL) 92 | s << (1. * num / 1000000000000ULL) << " T"; 93 | else if (num >= 1000000000ULL) 94 | s << (1. * num / 1000000000ULL) << " G"; 95 | else if (num >= 1000000ULL) 96 | s << (1. * num / 1000000ULL) << " M"; 97 | else if (num >= 1000ULL) 98 | s << (1. * num / 1000ULL) << " K"; 99 | else 100 | s << num; 101 | 102 | return s.str(); 103 | } 104 | } 105 | 106 | 107 | namespace QQ { 108 | namespace literals { 109 | constexpr unsigned long long int operator 110 | "" 111 | 112 | _KiB(unsigned long long int value) { return value * 1024ULL; } 113 | 114 | constexpr unsigned long long int operator 115 | "" 116 | 117 | _MiB(unsigned long long int value) { return value * 1024_KiB; } 118 | 119 | constexpr unsigned long long int operator 120 | "" 121 | 122 | _GiB(unsigned long long int value) { return value * 1024_MiB; } 123 | 124 | constexpr unsigned long long int operator 125 | "" 126 | 127 | _TiB(unsigned long long int value) { return value * 1024_GiB; } 128 | } 129 | 130 | constexpr size_t KiB(const size_t value) { return value * 1024ULL; } 131 | 132 | constexpr size_t MiB(const size_t value) { return value * KiB(1024ULL); } 133 | 134 | constexpr size_t GiB(const size_t value) { return value * MiB(1024ULL); } 135 | 136 | constexpr size_t TiB(const size_t value) { return value * GiB(1024ULL); } 137 | } 138 | 139 | 140 | namespace QQ { 141 | double CYCLES_PER_SECOND; 142 | constexpr size_t huge_page_size = 1024ULL * 1024 * 2; //!< 2 MiB 143 | 144 | void init() { 145 | CYCLES_PER_SECOND = (double) rte_get_tsc_hz(); 146 | } 147 | 148 | //! The struct used to store packets internally. 149 | /*! 150 | * More of a metadata header than a real storage struct for packets. 151 | */ 152 | struct packet_header { 153 | packet_header() { } 154 | 155 | explicit packet_header(const packet_header& r) : timestamp(r.timestamp), vlan(r.vlan), len(r.len) { } 156 | 157 | explicit packet_header(const uint64_t ts, const uint64_t vlan, const uint16_t len) 158 | : timestamp(ts), vlan(vlan), len(len) { } 159 | 160 | uint64_t timestamp:48; //!< Stores a timestamp. Unit is microseconds. 161 | uint64_t vlan:12; //!< Field to store the VLAN tag. Prevents messy Ethernet header. 162 | uint16_t len; //!< Holds the length of the data array. 163 | uint8_t data[]; //!< Flexible array member. Valid since C99, not really in C++. 164 | }; 165 | 166 | static_assert(sizeof(packet_header) == 16, "packet_header size mismatch"); 167 | static_assert(alignof(packet_header) == 8, "packet_header alignment mismatch"); 168 | static_assert(offsetof(packet_header, len) == 8, "expected len field at 8th byte"); 169 | static_assert(offsetof(packet_header, data) == 10, "expected len field at 10th byte"); 170 | 171 | template 172 | struct Storage { 173 | explicit Storage(uint8_t* data) : backend(data), current(data) { 174 | refs.reserve(storage_cap / 64); 175 | } 176 | 177 | Storage& operator=(Storage&& other) noexcept { 178 | std::swap(m_, other.m_); 179 | backend = other.backend; 180 | current = other.current; 181 | refs = std::move(other.refs); 182 | 183 | return *this; 184 | } 185 | 186 | [[deprecated]] inline bool store(const packet_header& p) noexcept { 187 | return store(p.timestamp, p.vlan, p.len, p.data); 188 | } 189 | 190 | inline bool store(const uint64_t timestamp, const uint64_t vlan, const uint16_t length, 191 | const uint8_t* data) noexcept { 192 | // TODO: make the actual value depend on the storage cap and/or expected speed 193 | auto now = _rdtsc(); 194 | auto diff = now - acquisition; 195 | if (diff > timeout * CYCLES_PER_SECOND) { 196 | #ifndef NDEBUG 197 | std::cout << "Hold time too long: " << std::fixed << diff / CYCLES_PER_SECOND << " s , max: " << 198 | timeout << " s. Returning false" << std::endl; 199 | #endif 200 | return false; 201 | } 202 | 203 | // TODO: Alignment on current and/or new_pkt->data 204 | size_t space = storage_cap - (current - backend); 205 | if (!std::align(alignof(packet_header), sizeof(packet_header) + length, (void*&) current, space)) 206 | return false; 207 | auto new_pkt = new(current) packet_header(timestamp, vlan, length); 208 | std::memcpy(new_pkt->data, data, length); 209 | refs.push_back(new_pkt); 210 | current += sizeof(packet_header) + length; 211 | return true; 212 | } 213 | 214 | inline const packet_header* operator[](const size_t idx) const { 215 | return refs.at(idx); 216 | } 217 | 218 | // TODO: test if works 219 | inline void pop_back() noexcept { 220 | current = (uint8_t*) refs.back(); 221 | refs.pop_back(); 222 | } 223 | 224 | inline void clear() noexcept { 225 | refs.clear(); 226 | current = (uint8_t*) backend; 227 | } 228 | 229 | inline std::vector::const_iterator cbegin() const noexcept { 230 | return refs.cbegin(); 231 | } 232 | 233 | inline std::vector::const_iterator cend() const noexcept { 234 | return refs.cend(); 235 | } 236 | 237 | inline size_t size() const noexcept { 238 | return refs.size(); 239 | } 240 | 241 | 242 | template 243 | static void perf_test() { 244 | constexpr uint64_t runs = (1024 * 8) / storage_size; 245 | constexpr int num_threads = 4; 246 | 247 | std::vector all_counter(num_threads); 248 | 249 | auto task = [&all_counter](unsigned int thread_id) { 250 | uint64_t counter = 0; 251 | uint8_t* bytes; 252 | if ((bytes = (uint8_t*) mmap(NULL, storage_size * huge_page_size, 253 | PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)) == MAP_FAILED) 254 | handle_error("mmap"); 255 | if (madvise(bytes, storage_size * huge_page_size, MADV_HUGEPAGE)) 256 | handle_error("madvise"); 257 | if (mlock(bytes, storage_size * huge_page_size)) 258 | handle_error("mlock"); 259 | auto packet_data = new uint8_t[packet_len]; 260 | 261 | 262 | Storage s(bytes); 263 | for (auto i = 0ULL; i < runs; ++i) { 264 | s.acquisition = _rdtsc(); 265 | counter = 0; 266 | while (s.store(123, 1, packet_len, packet_data)) { 267 | ++counter; 268 | continue; 269 | } 270 | if (counter != s.size()) 271 | std::cerr << "Counter mismatch! Is: " << counter << ", stored: " << s.size() << std::endl; 272 | s.clear(); 273 | } 274 | 275 | if (munmap(bytes, storage_size * huge_page_size)) 276 | handle_error("munmap"); 277 | delete[] packet_data; 278 | 279 | all_counter.at(thread_id) = counter; 280 | }; 281 | 282 | auto start = std::chrono::high_resolution_clock::now(); 283 | 284 | std::vector threads(0); 285 | for (unsigned int i = 0; i < num_threads; ++i) { 286 | threads.emplace_back(task, i); 287 | } 288 | 289 | for (auto &e : threads) 290 | e.join(); 291 | 292 | double diff = std::chrono::duration(std::chrono::high_resolution_clock::now() - start).count(); 293 | 294 | 295 | #ifndef NDEBUG 296 | if (!std::equal(all_counter.begin() + 1, all_counter.end(), all_counter.cbegin())) { 297 | std::cerr << "Counter mismatch over the threads!" << std::endl; 298 | std::exit(2); 299 | } 300 | #endif 301 | const uint64_t total_packets = all_counter.at(0); 302 | const uint64_t total_data = (sizeof(packet_header) + packet_len) * total_packets * runs; 303 | 304 | if (verbose) { 305 | std::cout << "#### QQ::Storage ####" << std::endl; 306 | std::cout << "Sizeof packet: " << sizeof(packet_header) + packet_len << " B" << std::endl; 307 | std::cout << "Storage size: " << storage_size << " huge pages, " << 308 | format_bytes(storage_size * huge_page_size) << std::endl; 309 | std::cout << "Parallel threads: " << num_threads << std::endl; 310 | std::cout << "Took: " << diff << " s" << std::endl; 311 | std::cout << format_SI(total_packets * runs) << " packets; " << format_SI((total_packets * runs) / diff) << 312 | " packets/s" << std::endl; 313 | std::cout << "Throughput: " << format_bytes(total_data) << "; " << format_bits(total_data / diff) << 314 | "/s" << std::endl << std::endl; 315 | } else { 316 | // bucket_size packet_length packets/s 317 | std::cout << storage_size * 2 << " " << packet_len << " " << std::fixed << 318 | ((total_packets * runs) / diff) / (1000. * 1000.) << std::endl; 319 | } 320 | 321 | 322 | } 323 | 324 | 325 | template 326 | static void variable_packet_size_perf_test() { 327 | constexpr uint64_t runs = (1024 * 12) / storage_size; 328 | constexpr size_t max_packet_len = 512; 329 | uint64_t counter = 0; 330 | 331 | uint8_t* bytes; 332 | if ((bytes = (uint8_t*) mmap(NULL, storage_size * huge_page_size, 333 | PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)) == MAP_FAILED) 334 | handle_error("mmap"); 335 | if (madvise(bytes, storage_size * huge_page_size, MADV_HUGEPAGE)) 336 | handle_error("madvise"); 337 | auto packet_data = new uint8_t[max_packet_len]; 338 | 339 | auto start = std::chrono::high_resolution_clock::now(); 340 | 341 | Storage s(bytes); 342 | for (auto i = 0ULL; i < runs; ++i) { 343 | s.acquisition = _rdtsc(); 344 | counter = 0; 345 | const uint16_t real_pkt_len = (uint16_t) ((counter + 1) % 128); 346 | while (s.store(123, 1, real_pkt_len, packet_data)) { 347 | ++counter; 348 | continue; 349 | } 350 | if (counter != s.size()) 351 | std::cerr << "Counter mismatch! Is: " << counter << ", stored: " << s.size() << std::endl; 352 | s.clear(); 353 | } 354 | 355 | double diff = std::chrono::duration(std::chrono::high_resolution_clock::now() - start).count(); 356 | 357 | const uint64_t total_data = (sizeof(packet_header) + 64) * counter * runs; 358 | 359 | std::cout << "#### QQ::Storage ####" << std::endl; 360 | std::cout << "Sizeof packet: " << sizeof(packet_header) + 64 << " B" << std::endl; 361 | std::cout << "Storage size: " << storage_size << " huge pages, " << 362 | format_bytes(storage_size * huge_page_size) << std::endl; 363 | std::cout << "Took: " << diff << " s" << std::endl; 364 | std::cout << format_SI(counter * runs) << " packets; " << format_SI((counter * runs) / diff) << 365 | " packets/s" << std::endl; 366 | std::cout << "Throughput: " << format_bytes(total_data) << "; " << format_bits(total_data / diff) << 367 | "/s" << std::endl << std::endl; 368 | if (munmap(bytes, storage_size * huge_page_size)) 369 | handle_error("munmap"); 370 | delete[] packet_data; 371 | } 372 | 373 | static void timeout_test() { 374 | constexpr size_t packet_len = 64; 375 | constexpr size_t storage_size = 8; 376 | 377 | auto bytes = new uint8_t[huge_page_size * storage_size]; 378 | auto packet_data = new uint8_t[packet_len]; 379 | 380 | Storage s(bytes); 381 | s.acquisition = _rdtsc(); 382 | 383 | if (!s.store(213, 1, packet_len, packet_data)) { 384 | std::cerr << "Store failed when it should have not!" << std::endl; 385 | std::cout << "Timeout test failed!" << std::endl; 386 | return; 387 | } 388 | uint64_t t = static_cast(timeout * 1.01 * 1000); 389 | std::this_thread::sleep_for(std::chrono::milliseconds(t)); 390 | 391 | if (s.store(213, 1, packet_len, packet_data)) { 392 | std::cerr << "Store succeeded when it should have not!" << std::endl; 393 | std::cout << "Timeout test failed!" << std::endl; 394 | return; 395 | } 396 | 397 | std::cout << "Timeout test passed" << std::endl; 398 | delete[] bytes; 399 | delete[] packet_data; 400 | } 401 | 402 | public: 403 | std::mutex m_; 404 | uint64_t acquisition = 0; 405 | private: 406 | const uint8_t* backend; 407 | uint8_t* current; 408 | // TODO: can be optimized to store offsets instead of pointers, saving 4 byte/packet 409 | // (a variant without random access could save even more memory) 410 | std::vector refs; 411 | constexpr static double timeout = 0.3; // time in seconds 412 | }; 413 | 414 | template 415 | struct Ptr { 416 | Ptr() { } 417 | 418 | Ptr(Storage& s) : storage(&s) { 419 | lock_ = std::unique_lock(storage->m_); 420 | storage->acquisition = _rdtsc(); 421 | } 422 | 423 | Ptr(const Ptr& other) = delete; 424 | 425 | Ptr(Ptr&& other) : storage(other.storage), lock_(std::move(other.lock_)) { } 426 | 427 | Ptr& operator=(Ptr&& other) { 428 | storage = other.storage; 429 | lock_ = std::move(other.lock_); 430 | return *this; 431 | } 432 | 433 | ~Ptr() { 434 | release(); 435 | } 436 | 437 | inline void release() noexcept { 438 | if (lock_) 439 | lock_.unlock(); 440 | } 441 | 442 | template 443 | inline bool store(Args&& ...args) { 444 | return storage->store(std::forward(args)...); 445 | } 446 | 447 | inline const packet_header* operator[](const size_t idx) const noexcept { 448 | return storage->operator[](idx); 449 | } 450 | 451 | inline void pop_back() { 452 | storage->pop_back(); 453 | } 454 | 455 | inline void clear() { 456 | storage->clear(); 457 | } 458 | 459 | inline size_t size() const noexcept { 460 | return storage->size(); 461 | } 462 | 463 | inline std::vector::const_iterator cbegin() const noexcept { 464 | return storage->cbegin(); 465 | } 466 | 467 | inline std::vector::const_iterator cend() const noexcept { 468 | return storage->cend(); 469 | } 470 | 471 | private: 472 | Storage* storage; //!< Handle of the managed storage element 473 | std::unique_lock lock_; //!< To lock the QQ::Storage::m_ mutex 474 | }; 475 | 476 | template 477 | class QQ { 478 | 479 | public: 480 | QQ(size_t num_buckets): num_buckets(num_buckets) { 481 | if ((backend_ = (uint8_t*) mmap(NULL, pages_per_bucket * num_buckets * huge_page_size, 482 | PROT_READ | PROT_WRITE, 483 | MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)) == MAP_FAILED) { 484 | handle_error("mmap"); 485 | } 486 | if (madvise(backend_, pages_per_bucket * num_buckets * huge_page_size, MADV_HUGEPAGE)) 487 | handle_error("madvise"); 488 | 489 | memset(backend_, 0, pages_per_bucket * num_buckets * huge_page_size); 490 | head = tail = peek_pos = 0; 491 | priority = 1; 492 | 493 | for (unsigned int i = 0; i < num_buckets; ++i) { 494 | storage_in_use.emplace_back(new Storage( 495 | backend_ + i * pages_per_bucket * huge_page_size)); 496 | } 497 | } 498 | 499 | QQ(const QQ&) = delete; 500 | 501 | QQ(QQ&&) = delete; 502 | 503 | ~QQ() { 504 | if (munmap(backend_, pages_per_bucket * num_buckets * huge_page_size)) 505 | handle_error("munmap"); 506 | for (auto& e : storage_in_use) 507 | delete e; 508 | } 509 | 510 | QQ& operator=(const QQ&) = delete; 511 | 512 | QQ& operator=(QQ&&) = delete; 513 | 514 | Ptr waiting_enqueue(const uint8_t call_priority = 1) { 515 | Storage* s = nullptr; 516 | //{ 517 | std::unique_lock lk(mutex_); 518 | //cv_prio.wait(lk, [&] { return check_priority_no_lock(call_priority); }); // wait until true/while false 519 | 520 | while (full_no_lock()) { 521 | #ifndef NDEBUG 522 | std::cout << "waiting_enqueue(" << (uint32_t) call_priority << "): full == true, head: " << head << ", tail: " << tail << std::endl; 523 | #endif 524 | not_full.wait(lk); 525 | } 526 | //not_full.wait(lk, [&] { return !full_no_lock(); }); 527 | #ifndef NDEBUG 528 | if (!lk.owns_lock()) { 529 | std::cerr << "Lock not owned" << std::endl; 530 | std::exit(2); 531 | } 532 | #endif 533 | s = storage_in_use.at(head); 534 | #ifndef NDEBUG 535 | std::cout << "waiting_enqueue(" << (uint32_t) call_priority << "): head " << head << " -> " << wrap(head+1) << std::endl; 536 | #endif 537 | 538 | if (wrap(head + 1) == tail) { 539 | std::cerr << "Enqueuing into full queue" << std::endl; 540 | std::exit(2); 541 | //throw new std::logic_error("Enqueuing into full queue"); 542 | } 543 | 544 | head = wrap(head + 1); 545 | 546 | #ifndef NDEBUG 547 | if (head == tail) { 548 | std::cerr << "Enqueued into full queue" << std::endl; 549 | std::exit(2); 550 | } 551 | #endif 552 | 553 | ++waiting_enqueue_call_counter; 554 | 555 | Ptr p{*s}; 556 | p.clear(); 557 | lk.unlock(); 558 | non_empty.notify_one(); 559 | return p; 560 | //} 561 | 562 | //non_empty.notify_all(); 563 | //not_full.notify_one(); 564 | //Ptr p{*s}; 565 | //p.clear(); 566 | //return p; 567 | } 568 | 569 | 570 | Ptr enqueue(const uint8_t call_priority = 1) { 571 | Storage* s = nullptr; 572 | std::unique_lock lk(mutex_); 573 | not_full.wait(lk, [&] { return check_priority_no_lock(call_priority) || !full_no_lock(); }); 574 | s = storage_in_use.at(head); 575 | head = wrap(head + 1); 576 | ++enqueue_call_counter; 577 | if (head == tail) { 578 | tail = wrap(tail + 16); 579 | ++enqueue_overflow_counter; 580 | #ifndef NDEBUG 581 | std::cerr << "[QQ] Enqueue overflow occurred, dropping last 16 buckets!" << std::endl; 582 | #endif 583 | } 584 | Ptr p{*s}; // The ctor locks the Storage mutex, potentially blocking if another Ptr hold still holds it 585 | lk.unlock(); 586 | non_empty.notify_one(); 587 | p.clear(); 588 | return std::move(p); 589 | } 590 | 591 | Ptr* try_dequeue(const uint8_t call_priority = 1) { 592 | Storage* s = nullptr; 593 | std::unique_lock lk(mutex_); 594 | if (!non_empty.wait_for(lk, std::chrono::milliseconds(10), [&] { 595 | return distance(head, tail) < 8 // Wait until head is less than 8 buckets behind tail 596 | && distance(tail, head) > 8; // Wait until tail is more than 8 buckets behind head 597 | })) 598 | return nullptr; 599 | s = storage_in_use.at(tail); 600 | tail = wrap(tail + 1); 601 | ++dequeue_call_counter; 602 | Ptr p{*s}; 603 | lk.unlock(); 604 | not_full.notify_one(); 605 | return new Ptr(std::move(p)); 606 | } 607 | 608 | Ptr dequeue(const uint8_t call_priority = 1) { 609 | Storage* s = nullptr; 610 | std::unique_lock lk(mutex_); 611 | //non_empty.wait(lk, [&] { return distance(tail, head) > 8; }); //This waits as short as possible 612 | non_empty.wait(lk, [&] { 613 | return distance(head, tail) < 8 // Wait until head is less than 8 buckets behind tail 614 | && distance(tail, head) > 8; // Wait until tail is more than 8 buckets behind head 615 | }); // This waits until QQ is full 616 | s = storage_in_use.at(tail); 617 | tail = wrap(tail + 1); 618 | ++dequeue_call_counter; 619 | Ptr p{*s}; 620 | lk.unlock(); 621 | not_full.notify_one(); 622 | return p; 623 | } 624 | 625 | Ptr peek(const uint8_t call_priority = 1) { 626 | Storage* s = nullptr; 627 | std::unique_lock lk(mutex_); 628 | if (distance(peek_pos, head) > num_buckets / 2) { 629 | peek_pos = wrap(peek_pos + 16); 630 | #ifndef NDEBUG 631 | std::cerr << "[QQ::peek()]: peek pointer is lacking more than 50% behind head!" << std::endl; 632 | #endif 633 | } 634 | //cv_prio.wait(lk, [&] { return check_priority_no_lock(call_priority); }); 635 | non_empty.wait(lk, [&] { return distance(peek_pos, head) > 8; }); 636 | s = storage_in_use.at(peek_pos); 637 | peek_pos = wrap(peek_pos + 1); 638 | Ptr p{*s}; 639 | lk.unlock(); 640 | non_empty.notify_one(); 641 | return p; 642 | } 643 | 644 | Ptr* try_peek(const uint8_t call_priority = 1) { 645 | Storage* s = nullptr; 646 | std::unique_lock lk(mutex_); 647 | if (distance(peek_pos, head) > num_buckets / 2) { 648 | peek_pos = wrap(peek_pos + 16); 649 | } 650 | if (!non_empty.wait_for(lk, std::chrono::milliseconds(10), [&] { 651 | return distance(peek_pos, head) > 8; // Wait until peek_pos is more than 8 buckets behind head 652 | })) 653 | return nullptr; 654 | s = storage_in_use.at(peek_pos); 655 | peek_pos = wrap(peek_pos + 1); 656 | auto p = new Ptr(*s); 657 | lk.unlock(); 658 | non_empty.notify_one(); 659 | return p; 660 | } 661 | 662 | inline bool empty() { 663 | std::lock_guard lg(mutex_); 664 | return head == tail; 665 | } 666 | 667 | inline bool empty_no_lock() const noexcept { 668 | return head == tail; 669 | } 670 | 671 | inline bool full_no_lock() const noexcept { 672 | return wrap(head + 1) == tail; 673 | } 674 | 675 | inline bool check_priority_no_lock(const uint8_t prio_to_check) const noexcept { 676 | return prio_to_check >= priority; 677 | } 678 | 679 | inline void set_priority(const uint8_t new_priority) noexcept { 680 | std::unique_lock lk(mutex_); 681 | priority = new_priority; 682 | lk.unlock(); // unlock early to prevent instant re-blocking in other thread. 683 | cv_prio.notify_all(); 684 | } 685 | 686 | inline void set_priority_no_lock(const uint8_t new_priority) noexcept { 687 | priority = new_priority; 688 | } 689 | 690 | inline size_t size() const noexcept { 691 | if (head >= tail) 692 | return head - tail; 693 | else 694 | return num_buckets - (tail - head); 695 | } 696 | 697 | inline size_t capacity() const noexcept { 698 | return num_buckets; 699 | } 700 | 701 | inline size_t distance(const size_t a, const size_t b) const noexcept { 702 | if (b >= a) 703 | return b - a; 704 | else 705 | return num_buckets - (a - b); 706 | } 707 | 708 | inline size_t wrap(const size_t value) const noexcept { 709 | if (value >= num_buckets) 710 | return value - num_buckets; 711 | else 712 | return value; 713 | } 714 | 715 | inline size_t get_enqueue_counter() const noexcept { 716 | return enqueue_call_counter; 717 | } 718 | 719 | inline size_t get_enqueue_overflow_counter() const noexcept { 720 | return enqueue_overflow_counter; 721 | } 722 | 723 | inline size_t get_dequeue_counter() const noexcept { 724 | return dequeue_call_counter; 725 | } 726 | 727 | void print_storages() const noexcept { 728 | std::cout << "Storages: "; 729 | for (auto& e : storage_in_use) { 730 | std::cout << " | " << e->size(); 731 | } 732 | std::cout << " |" << std::endl; 733 | } 734 | 735 | void debug() const { 736 | std::cout << "Number of buckets: " << num_buckets << std::endl; 737 | std::cout << "Bucket size: " << format_bytes(pages_per_bucket * huge_page_size) << std::endl; 738 | std::cout << "Total size: " << format_bytes(num_buckets * pages_per_bucket * huge_page_size) << std::endl; 739 | std::cout << "Minimal look back time at 10 Gbit/s: " << 740 | (num_buckets * pages_per_bucket * huge_page_size) / (1024 * 1024 * 1024 * 10. / 8) << " s" << std::endl; 741 | std::cout << "head: " << head << std::endl; 742 | std::cout << "tail: " << tail << std::endl; 743 | std::cout << "dequeue call counter: " << dequeue_call_counter << ", " << format_SI(dequeue_call_counter) << 744 | std::endl; 745 | std::cout << "enqueue call counter: " << enqueue_call_counter << ", " << format_SI(enqueue_call_counter) << 746 | std::endl; 747 | std::cout << "waiting_enqueue call counter: " << waiting_enqueue_call_counter << ", " << 748 | format_SI(waiting_enqueue_call_counter) << std::endl; 749 | std::cout << "enqueue overflow counter: " << enqueue_overflow_counter << ", " << 750 | format_SI(enqueue_overflow_counter) << std::endl; 751 | } 752 | 753 | 754 | template 755 | static void pure_perf_test() { 756 | //constexpr uint64_t runs = 1024 * 1024 * 1; 757 | constexpr uint64_t runs = 1024 * 16; 758 | 759 | std::vector producer(0); 760 | std::vector consumer(0); 761 | 762 | QQ<1> qq(num_buckets); 763 | 764 | auto start = std::chrono::high_resolution_clock::now(); 765 | 766 | for (unsigned int i = 0; i < num_producer; ++i) { 767 | producer.emplace_back([&, i]() { 768 | for (auto j = 0ULL; j < runs; ++j) { 769 | //auto enq_ptr = qq.waiting_enqueue((uint8_t) i); 770 | auto enq_ptr = qq.waiting_enqueue(); 771 | if (enq_ptr.size() != 0) { 772 | std::cerr << i << ": enqueue storage was not empty" << std::endl; 773 | std::exit(2); 774 | //throw new std::logic_error("enqueue storage was not empty"); 775 | } 776 | if (!enq_ptr.store(1ULL, 1, 4, (const uint8_t*) &i)) { 777 | std::cerr << i << ": store failed where it should not" << std::endl; 778 | std::exit(2); 779 | //throw new std::logic_error("store failed where it should not"); 780 | } 781 | if (enq_ptr.size() != 1) { 782 | std::cerr << i << ": storage must hold one element" << std::endl; 783 | std::exit(2); 784 | } 785 | #ifndef NDEBUG 786 | //std::cout << "producer " << i << " enqueue'd. j: " << j << std::endl; 787 | #endif 788 | } 789 | { 790 | #ifndef NDEBUG 791 | std::cout << "producer " << i << " trying to send termination value... "; 792 | #endif 793 | auto enq_ptr = qq.waiting_enqueue(); 794 | if (!enq_ptr.store(0ULL, 1, 4, (const uint8_t*) &i)) { 795 | std::cerr << i << ": store failed where it should not" << std::endl; 796 | std::exit(2); 797 | //throw new std::logic_error("store failed where it should not"); 798 | } 799 | if (enq_ptr.size() != 1) { 800 | std::cerr << i << ": storage must hold one element" << std::endl; 801 | std::exit(2); 802 | } 803 | } 804 | #ifndef NDEBUG 805 | std::cout << " Done. value: " << i << ". QQ size: " << qq.size() 806 | << ", head: " << qq.head << ", tail: " << qq.tail << std::endl; 807 | #endif 808 | }); 809 | } 810 | 811 | for (unsigned int i = 0; i < num_consumer; ++i) { 812 | consumer.emplace_back([&, i]() { 813 | while (true) { 814 | #ifndef NDEBUG 815 | //std::cout << "consumer " << i << " trying to dequeue..." << std::endl; 816 | #endif 817 | //auto deq_prt = qq.dequeue((uint8_t) i); 818 | auto deq_prt = qq.dequeue(); 819 | #ifndef NDEBUG 820 | //std::cout << "consumer " << i << " got it." << std::endl; 821 | #endif 822 | if (deq_prt.size() != 1) { 823 | std::cerr << i << ": dequeued storage was empty or over filled: " << deq_prt.size() << std::endl; 824 | std::exit(2); 825 | //throw new std::logic_error("dequeued storage was empty or over filled"); 826 | } 827 | 828 | if (deq_prt[0]->timestamp == 0) { 829 | #ifndef NDEBUG 830 | uint8_t val = deq_prt[0]->data[0]; 831 | std::cout << "consumer " << i << " got termination value: " << (uint32_t) val 832 | << ". QQ size: " << qq.size() << ", head: " << qq.head << ", tail: " << qq.tail << std::endl; 833 | #endif 834 | break; 835 | } else if (deq_prt[0]->timestamp != 1) { 836 | std::cerr << i << ": Timestamp invalid" << std::endl; 837 | std::exit(2); 838 | //throw new std::logic_error("timestamp invalid"); 839 | } 840 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 841 | deq_prt.clear(); 842 | } 843 | }); 844 | } 845 | 846 | for (auto& e : producer) { 847 | e.join(); 848 | } 849 | for (auto& e : consumer) { 850 | e.join(); 851 | } 852 | 853 | double diff = std::chrono::duration(std::chrono::high_resolution_clock::now() - start).count(); 854 | 855 | if (verbose) { 856 | std::cout << "#### New QQ (pure perf) ####" << std::endl; 857 | std::cout << "Debug" << std::endl; 858 | qq.debug(); 859 | std::cout << "END Debug" << std::endl; 860 | std::cout << "Producer: " << num_producer << std::endl; 861 | std::cout << "Consumer: " << num_consumer << std::endl; 862 | std::cout << "Bucket size: " << 1 << " huge pages" << std::endl; 863 | std::cout << "Number of buckets: " << num_buckets << std::endl; 864 | std::cout << "Took: " << diff << " s" << std::endl; 865 | std::cout << format_SI(runs) << " enqueue calls; " << format_SI(runs / diff) << 866 | " enqueues/s" << std::endl; 867 | } else { 868 | std::cout << num_buckets << " " << std::fixed << runs / diff << std::endl; 869 | } 870 | } 871 | 872 | private: 873 | uint8_t* backend_; 874 | size_t head; 875 | size_t tail; 876 | size_t peek_pos; 877 | uint8_t priority; //!< stores the current priority level of the queue. 878 | const size_t num_buckets; 879 | std::mutex mutex_; 880 | std::vector*> storage_in_use; 881 | 882 | std::condition_variable non_empty; 883 | std::condition_variable not_full; 884 | std::condition_variable cv_prio; 885 | 886 | // Performance counter 887 | uint64_t dequeue_call_counter = 0; 888 | uint64_t enqueue_call_counter = 0; 889 | uint64_t waiting_enqueue_call_counter = 0; 890 | uint64_t enqueue_overflow_counter = 0; 891 | }; 892 | 893 | 894 | static void perf_test() { 895 | constexpr uint64_t runs = 1024 * 1024 * 256; 896 | constexpr size_t packet_len = 32; 897 | constexpr int num_producer = 4; 898 | constexpr int num_consumer = 4; 899 | constexpr size_t bucket_size = 64; 900 | constexpr size_t num_buckets = 128; 901 | 902 | std::vector producer(0); 903 | std::vector consumer(0); 904 | std::vector producer_counter(num_producer, 0); 905 | std::vector consumer_counter(num_consumer, 0); 906 | 907 | QQ myq(num_buckets); 908 | 909 | auto start = std::chrono::high_resolution_clock::now(); 910 | 911 | for (unsigned int i = 0; i < num_producer; ++i) { 912 | producer.emplace_back([&, i]() { 913 | uint64_t counter = 0; 914 | while (counter < runs) { 915 | { 916 | auto enq_ptr = myq.enqueue(); 917 | while (enq_ptr.store(i, 1, packet_len, (uint8_t*) &counter)) { 918 | ++counter; 919 | continue; 920 | } 921 | } 922 | } 923 | { 924 | auto enq_ptr = myq.enqueue(); 925 | enq_ptr.clear(); 926 | } 927 | producer_counter[i] = counter; 928 | }); 929 | } 930 | 931 | 932 | for (unsigned int i = 0; i < num_consumer; ++i) { 933 | consumer.emplace_back([&, i]() { 934 | while (true) { 935 | { 936 | auto deq_prt = myq.dequeue(); 937 | if (deq_prt.size() == 0) 938 | break; 939 | for (size_t j = 0; j < deq_prt.size(); ++j) { 940 | if (deq_prt[j]->timestamp >= num_producer) 941 | std::cerr << "Data corruption in ts: " << deq_prt[j]->timestamp << std::endl; 942 | consumer_counter[i] += deq_prt[j]->vlan; 943 | } 944 | deq_prt.clear(); 945 | } 946 | } 947 | }); 948 | } 949 | 950 | for (auto& e : producer) { 951 | e.join(); 952 | } 953 | for (auto& e : consumer) { 954 | e.join(); 955 | } 956 | 957 | double diff = std::chrono::duration(std::chrono::high_resolution_clock::now() - start).count(); 958 | 959 | int64_t producer_sum = std::accumulate(producer_counter.begin(), producer_counter.end(), 0); 960 | int64_t consumer_sum = std::accumulate(consumer_counter.begin(), consumer_counter.end(), 0); 961 | //assert(consumer_sum == producer_sum); 962 | const uint64_t total_data = (sizeof(packet_header) + packet_len) * producer_sum; 963 | std::cout << "#### New QQ ####" << std::endl; 964 | std::cout << "Debug" << std::endl; 965 | myq.debug(); 966 | std::cout << "END Debug" << std::endl; 967 | std::cout << "Producer: " << num_producer << std::endl; 968 | std::cout << "Consumer: " << num_consumer << std::endl; 969 | std::cout << "Bucket size: " << bucket_size << " huge pages" << std::endl; 970 | std::cout << "Number of buckets: " << num_buckets << std::endl; 971 | std::cout << "Producer sum: " << producer_sum << std::endl; 972 | std::cout << "Consumer sum: " << consumer_sum << std::endl; 973 | std::cout << "Sizeof packet: " << sizeof(packet_header) + packet_len << " B" << std::endl; 974 | std::cout << "Took: " << diff << " s" << std::endl; 975 | std::cout << format_SI((uint64_t) producer_sum) << " packets; " << format_SI(producer_sum / diff) << 976 | " packets/s" << std::endl; 977 | std::cout << "Throughput: " << format_bytes(total_data) << "; " << format_bits(total_data / diff) << 978 | "/s" << std::endl << std::endl; 979 | } 980 | 981 | static void perf_test_write_only() { 982 | constexpr uint64_t runs = 1024 * 1024 * 256; 983 | constexpr size_t packet_len = 32; 984 | constexpr int num_producer = 4; 985 | constexpr size_t bucket_size = 8; 986 | constexpr size_t num_buckets = 512; 987 | 988 | std::vector producer(0); 989 | std::vector consumer(0); 990 | std::vector producer_counter(num_producer, 0); 991 | 992 | QQ myq(num_buckets); 993 | 994 | auto start = std::chrono::high_resolution_clock::now(); 995 | 996 | for (unsigned int i = 0; i < num_producer; ++i) { 997 | producer.emplace_back([&, i]() { 998 | uint64_t counter = 0; 999 | while (counter < runs) { 1000 | { 1001 | auto enq_ptr = myq.enqueue(); 1002 | while (enq_ptr.store(i, 1, packet_len, (uint8_t*) &counter)) { 1003 | ++counter; 1004 | continue; 1005 | } 1006 | } 1007 | } 1008 | { 1009 | auto enq_ptr = myq.enqueue(); 1010 | enq_ptr.clear(); 1011 | } 1012 | producer_counter[i] = counter; 1013 | }); 1014 | } 1015 | 1016 | for (auto& e : producer) { 1017 | e.join(); 1018 | } 1019 | 1020 | double diff = std::chrono::duration(std::chrono::high_resolution_clock::now() - start).count(); 1021 | 1022 | int64_t producer_sum = std::accumulate(producer_counter.begin(), producer_counter.end(), 0); 1023 | //assert(consumer_sum == producer_sum); 1024 | const uint64_t total_data = (sizeof(packet_header) + packet_len) * producer_sum; 1025 | std::cout << "#### New QQ (write only) ####" << std::endl; 1026 | std::cout << "Debug" << std::endl; 1027 | myq.debug(); 1028 | std::cout << "END Debug" << std::endl; 1029 | std::cout << "Producer: " << num_producer << std::endl; 1030 | std::cout << "Bucket size: " << bucket_size << " huge pages" << std::endl; 1031 | std::cout << "Number of buckets: " << num_buckets << std::endl; 1032 | std::cout << "Producer sum: " << producer_sum << std::endl; 1033 | std::cout << "Sizeof packet: " << sizeof(packet_header) + packet_len << " B" << std::endl; 1034 | std::cout << "Took: " << diff << " s" << std::endl; 1035 | std::cout << format_SI((uint64_t) producer_sum) << " packets; " << format_SI(producer_sum / diff) << 1036 | " packets/s" << std::endl; 1037 | std::cout << "Throughput: " << format_bytes(total_data) << "; " << format_bits(total_data / diff) << 1038 | "/s" << std::endl << std::endl; 1039 | } 1040 | 1041 | static void cons_test() { 1042 | // TODO: implement consistency test that takes enqueue overflows into account 1043 | 1044 | constexpr size_t map_size = 5000000ULL; 1045 | 1046 | std::vector map(map_size, false); 1047 | QQ<1> qq(4); 1048 | 1049 | std::thread writer([&]() { 1050 | auto q_ptr = qq.enqueue(); 1051 | for (uint64_t i = 0ULL; i < map.size(); ++i) { 1052 | while (!q_ptr.store(uint64_t(123), 1, /*sizeof(i) * 10*/ 23, (uint8_t*) &i)) { 1053 | q_ptr = qq.waiting_enqueue(); 1054 | } 1055 | } 1056 | { 1057 | q_ptr = qq.waiting_enqueue(); 1058 | q_ptr.clear(); 1059 | } 1060 | }); 1061 | 1062 | // Dequeue everything 1063 | std::thread reader([&]() { 1064 | while (true) { 1065 | auto deq_ptr = qq.dequeue(); 1066 | if (deq_ptr.size() == 0) 1067 | break; 1068 | for (auto it = deq_ptr.cbegin(); it != deq_ptr.cend(); ++it) { 1069 | uint64_t* number = (uint64_t*) (*it)->data; 1070 | map.at(*number) = true; 1071 | } 1072 | } 1073 | }); 1074 | 1075 | writer.join(); 1076 | reader.join(); 1077 | 1078 | std::cout << "#### New QQ (consistency test) ####" << std::endl; 1079 | qq.debug(); 1080 | // Check bits 1081 | for (auto it = map.cbegin(); it != map.cend(); ++it) { 1082 | if (*it == false) { 1083 | std::cerr << "Found bit that should be set at " << it - map.cbegin() << std::endl; 1084 | return; 1085 | } 1086 | } 1087 | std::cout << "Map size: " << map_size << ", " << format_SI(map_size) << std::endl; 1088 | std::cout << "All packets there" << std::endl; 1089 | } 1090 | } // namespace QQ 1091 | --------------------------------------------------------------------------------