From bcc9de7bc2b3bc33edcf6aaf53ce74d3e4887330 Mon Sep 17 00:00:00 2001 From: Julien Pinsonneau Date: Tue, 31 Oct 2023 12:56:34 +0100 Subject: [PATCH] add DNS error number --- pkg/loki/flow_query.go | 8 +++ pkg/loki/topology_query.go | 52 ++++++++++--------- web/locales/en/plugin__netobserv-plugin.json | 3 ++ web/src/api/ipfix.ts | 2 + .../netflow-record/record-field.tsx | 10 +++- web/src/components/netflow-traffic.tsx | 3 +- web/src/model/filters.ts | 1 + web/src/utils/columns.ts | 13 +++++ web/src/utils/dns.ts | 48 +++++++++++++++++ web/src/utils/filter-definitions.ts | 13 ++++- web/src/utils/filter-options.ts | 10 +++- 11 files changed, 135 insertions(+), 28 deletions(-) diff --git a/pkg/loki/flow_query.go b/pkg/loki/flow_query.go index 9de3083e1..35beb3693 100644 --- a/pkg/loki/flow_query.go +++ b/pkg/loki/flow_query.go @@ -272,6 +272,14 @@ func (q *FlowQueryBuilder) appendDNSFilter(sb *strings.Builder) { sb.WriteString("`") } +func (q *FlowQueryBuilder) appendDNSLatencyFilter(sb *strings.Builder) { + // ensure DnsLatencyMs field is specified + // |~`"DnsLatencyMs` + sb.WriteString("|~`") + sb.WriteString(`"DnsLatencyMs`) + sb.WriteString("`") +} + func (q *FlowQueryBuilder) appendDNSRCodeFilter(sb *strings.Builder) { // ensure DnsFlagsResponseCode field is specified with valid error // |~`"DnsFlagsResponseCode"` diff --git a/pkg/loki/topology_query.go b/pkg/loki/topology_query.go index a21e28039..b10af7c03 100644 --- a/pkg/loki/topology_query.go +++ b/pkg/loki/topology_query.go @@ -12,18 +12,19 @@ const ( ) type Topology struct { - limit string - rateInterval string - step string - function string - dataField string - fields string - skipEmptyDropState bool - skipEmptyDropCause bool - skipNonDNS bool - skipEmptyDNSRCode bool - skipEmptyRTT bool - factor string + limit string + rateInterval string + step string + function string + dataField string + fields string + skipEmptyDropState bool + skipEmptyDropCause bool + skipNonDNS bool + skipEmptyDNSLatency bool + skipEmptyDNSRCode bool + skipEmptyRTT bool + factor string } type TopologyQueryBuilder struct { @@ -79,18 +80,19 @@ func NewTopologyQuery(cfg *Config, start, end, limit, rateInterval, step string, return &TopologyQueryBuilder{ FlowQueryBuilder: NewFlowQueryBuilder(cfg, start, end, limit, d, rt, packetLoss), topology: &Topology{ - rateInterval: rateInterval, - step: step, - limit: l, - function: f, - dataField: t, - fields: fields, - skipEmptyDropState: aggregate == "droppedState", - skipEmptyDropCause: aggregate == "droppedCause", - skipNonDNS: metricType == constants.MetricTypeDNSLatencies || metricType == constants.MetricTypeCountDNS, - skipEmptyDNSRCode: aggregate == "dnsRCode", - skipEmptyRTT: metricType == constants.MetricTypeFlowRTT, - factor: factor, + rateInterval: rateInterval, + step: step, + limit: l, + function: f, + dataField: t, + fields: fields, + skipEmptyDropState: aggregate == "droppedState", + skipEmptyDropCause: aggregate == "droppedCause", + skipNonDNS: metricType == constants.MetricTypeCountDNS, + skipEmptyDNSLatency: metricType == constants.MetricTypeDNSLatencies, + skipEmptyDNSRCode: aggregate == "dnsRCode", + skipEmptyRTT: metricType == constants.MetricTypeFlowRTT, + factor: factor, }, }, nil } @@ -177,6 +179,8 @@ func (q *TopologyQueryBuilder) Build() string { if q.topology.skipEmptyDNSRCode { q.appendDNSRCodeFilter(sb) + } else if q.topology.skipEmptyDNSLatency { + q.appendDNSLatencyFilter(sb) } else if q.topology.skipNonDNS { q.appendDNSFilter(sb) } diff --git a/web/locales/en/plugin__netobserv-plugin.json b/web/locales/en/plugin__netobserv-plugin.json index fcab00596..546e1d2a6 100644 --- a/web/locales/en/plugin__netobserv-plugin.json +++ b/web/locales/en/plugin__netobserv-plugin.json @@ -398,6 +398,8 @@ "Time elapsed between DNS request and response.": "Time elapsed between DNS request and response.", "DNS Response Code": "DNS Response Code", "DNS RCODE name from response header.": "DNS RCODE name from response header.", + "DNS Error": "DNS Error", + "DNS error number returned by bpf_skb_load_bytes function.": "DNS error number returned by bpf_skb_load_bytes function.", "Start Time": "Start Time", "Time of the first packet observed. Unlike End Time, it is not used in queries to select records in an interval.": "Time of the first packet observed. Unlike End Time, it is not used in queries to select records in an interval.", "End Time": "End Time", @@ -483,6 +485,7 @@ "Specify a single DNS RCODE name like:": "Specify a single DNS RCODE name like:", "A IANA RCODE number like 0, 3, 9": "A IANA RCODE number like 0, 3, 9", "A IANA RCODE name like NoError, NXDomain, NotAuth": "A IANA RCODE name like NoError, NXDomain, NotAuth", + "Specify a single DNS error number.": "Specify a single DNS error number.", "Specify a TCP handshake Round Trip Time in nanoseconds.": "Specify a TCP handshake Round Trip Time in nanoseconds.", "P": "P", "Pps": "Pps", diff --git a/web/src/api/ipfix.ts b/web/src/api/ipfix.ts index e10cd4b44..828e26002 100644 --- a/web/src/api/ipfix.ts +++ b/web/src/api/ipfix.ts @@ -128,6 +128,8 @@ export interface Fields { DnsFlagsResponseCode?: string; /** Calculated time between response and request, in milliseconds */ DnsLatencyMs?: number; + /** Error number returned by bpf_skb_load_bytes */ + DnsErrno?: number; /** Start timestamp of this flow, in milliseconds */ TimeFlowStartMs: number; /** End timestamp of this flow, in milliseconds */ diff --git a/web/src/components/netflow-record/record-field.tsx b/web/src/components/netflow-record/record-field.tsx index 25b2ffa21..d0b89a250 100644 --- a/web/src/components/netflow-record/record-field.tsx +++ b/web/src/components/netflow-record/record-field.tsx @@ -7,7 +7,7 @@ import { Link } from 'react-router-dom'; import { FlowDirection, Record } from '../../api/ipfix'; import { Column, ColumnsId, getFullColumnName } from '../../utils/columns'; import { dateFormatter, getFormattedDate, timeMSFormatter, utcDateTimeFormatter } from '../../utils/datetime'; -import { DNS_CODE_NAMES, getDNSRcodeDescription } from '../../utils/dns'; +import { DNS_CODE_NAMES, DNS_ERRORS_VALUES, getDNSErrorDescription, getDNSRcodeDescription } from '../../utils/dns'; import { getICMPCode, getICMPDocUrl, @@ -470,6 +470,14 @@ export const RecordField: React.FC<{ ? simpleTextWithTooltip(detailed ? `${value}: ${getDNSRcodeDescription(value as DNS_CODE_NAMES)}` : value) : emptyText() ); + case ColumnsId.dnserrno: + return singleContainer( + typeof value === 'number' && !isNaN(value) + ? simpleTextWithTooltip( + detailed ? `${value}: ${getDNSErrorDescription(value as DNS_ERRORS_VALUES)}` : String(value) + ) + : emptyText() + ); default: if (Array.isArray(value) && value.length) { return doubleContainer(simpleTextWithTooltip(String(value[0])), simpleTextWithTooltip(String(value[1]))); diff --git a/web/src/components/netflow-traffic.tsx b/web/src/components/netflow-traffic.tsx index 3bf9deb24..ca3c0a21d 100644 --- a/web/src/components/netflow-traffic.tsx +++ b/web/src/components/netflow-traffic.tsx @@ -293,7 +293,8 @@ export const NetflowTraffic: React.FC<{ return (isSidePanel ? getDefaultColumns(t, false, false) : columns).filter( col => (isConnectionTracking() || ![ColumnsId.recordtype, ColumnsId.hashid].includes(col.id)) && - (isDNSTracking() || ![ColumnsId.dnsid, ColumnsId.dnslatency, ColumnsId.dnsresponsecode].includes(col.id)) && + (isDNSTracking() || + ![ColumnsId.dnsid, ColumnsId.dnslatency, ColumnsId.dnsresponsecode, ColumnsId.dnserrno].includes(col.id)) && (isFlowRTT() || ![ColumnsId.rttTime].includes(col.id)) ); }, diff --git a/web/src/model/filters.ts b/web/src/model/filters.ts index 8ec13e1a5..72d858762 100644 --- a/web/src/model/filters.ts +++ b/web/src/model/filters.ts @@ -41,6 +41,7 @@ export type FilterId = | 'dns_id' | 'dns_latency' | 'dns_flag_response_code' + | 'dns_errno' | 'time_flow_rtt'; export interface FilterDefinition { diff --git a/web/src/utils/columns.ts b/web/src/utils/columns.ts index 328c5ced3..8d87aa9c0 100644 --- a/web/src/utils/columns.ts +++ b/web/src/utils/columns.ts @@ -53,6 +53,7 @@ export enum ColumnsId { dnsid = 'DNSId', dnslatency = 'DNSLatency', dnsresponsecode = 'DNSResponseCode', + dnserrno = 'DNSErrNo', hostaddr = 'K8S_HostIP', srchostaddr = 'SrcK8S_HostIP', dsthostaddr = 'DstK8S_HostIP', @@ -848,6 +849,18 @@ export const getExtraColumns = (t: TFunction): Column[] => { value: f => f.fields.DnsFlagsResponseCode || '', sort: (a, b, col) => compareNumbers(col.value(a) as number, col.value(b) as number), width: 5 + }, + { + id: ColumnsId.dnserrno, + group: t('DNS'), + name: t('DNS Error'), + tooltip: t('DNS error number returned by bpf_skb_load_bytes function.'), + fieldName: 'DnsErrno', + quickFilter: 'dns_errno', + isSelected: false, + value: f => (f.fields.DnsErrno === undefined ? Number.NaN : f.fields.DnsErrno), + sort: (a, b, col) => compareNumbers(col.value(a) as number, col.value(b) as number), + width: 5 } ]; }; diff --git a/web/src/utils/dns.ts b/web/src/utils/dns.ts index 12e9bd753..02194ae34 100644 --- a/web/src/utils/dns.ts +++ b/web/src/utils/dns.ts @@ -35,3 +35,51 @@ export type DNS_CODE_NAMES = typeof dnsRcodesNames[number]; export const getDNSRcodeDescription = (name: DNS_CODE_NAMES): string => { return DNS_RCODES.find(v => v.name === name)?.description || 'Unassigned'; }; + +// https://elixir.bootlin.com/linux/v4.7/source/include/uapi/asm-generic/errno-base.h +export const DNS_ERRORS: ReadOnlyValues = [ + { value: 1, name: 'EPERM', description: 'Operation not permitted' }, + { value: 2, name: 'ENOENT', description: 'No such file or directory' }, + { value: 3, name: 'ESRCH', description: 'No such process' }, + { value: 4, name: 'EINTR', description: 'Interrupted system call' }, + { value: 5, name: 'EIO', description: 'I/O error' }, + { value: 6, name: 'ENXIO', description: 'No such device or address' }, + { value: 7, name: 'E2BIG', description: 'Argument list too long' }, + { value: 8, name: 'ENOEXEC', description: 'Exec format error' }, + { value: 9, name: 'EBADF', description: 'Bad file number' }, + { value: 10, name: 'ECHILD', description: 'No child processes' }, + { value: 11, name: 'EAGAIN', description: 'Try again' }, + { value: 12, name: 'ENOMEM', description: 'Out of memory' }, + { value: 13, name: 'EACCES', description: 'Permission denied' }, + { value: 14, name: 'EFAULT', description: 'Bad address' }, + { value: 15, name: 'ENOTBLK', description: 'Block device required' }, + { value: 16, name: 'EBUSY', description: 'Device or resource busy' }, + { value: 17, name: 'EEXIST', description: 'File exists' }, + { value: 18, name: 'EXDEV', description: 'Cross-device link' }, + { value: 19, name: 'ENODEV', description: 'No such device' }, + { value: 20, name: 'ENOTDIR', description: 'Not a directory' }, + { value: 21, name: 'EISDIR', description: 'Is a directory' }, + { value: 22, name: 'EINVAL', description: 'Invalid argument' }, + { value: 23, name: 'ENFILE', description: 'File table overflow' }, + { value: 24, name: 'EMFILE', description: 'Too many open files' }, + { value: 25, name: 'ENOTTY', description: 'Not a typewriter' }, + { value: 26, name: 'ETXBSY', description: 'Text file busy' }, + { value: 27, name: 'EFBIG', description: 'File too large' }, + { value: 28, name: 'ENOSPC', description: 'No space left on device' }, + { value: 29, name: 'ESPIPE', description: 'Illegal seek' }, + { value: 30, name: 'EROFS', description: 'Read-only file system' }, + { value: 31, name: 'EMLINK', description: 'Too many links' }, + { value: 32, name: 'EPIPE', description: 'Broken pipe' }, + { value: 33, name: 'EDOM', description: 'Math argument out of domain of func' }, + { value: 34, name: 'ERANGE', description: 'Math result not representable' } +] as const; + +const dnsErrorsValues = DNS_ERRORS.map(v => v.value); +export type DNS_ERRORS_VALUES = typeof dnsErrorsValues[number]; + +const dnsErrorsNames = DNS_ERRORS.map(v => v.name); +export type DNS_ERRORS_NAMES = typeof dnsErrorsNames[number]; + +export const getDNSErrorDescription = (value: DNS_ERRORS_VALUES): string => { + return DNS_ERRORS.find(v => v.value === value)?.description || ''; +}; diff --git a/web/src/utils/filter-definitions.ts b/web/src/utils/filter-definitions.ts index 5751c704b..78150b4a9 100644 --- a/web/src/utils/filter-definitions.ts +++ b/web/src/utils/filter-definitions.ts @@ -26,7 +26,8 @@ import { getDropStateOptions, getDropCauseOptions, getDirectionOptionsAsync, - findDirectionOption + findDirectionOption, + getDnsErrorCodeOptions } from './filter-options'; // Convenience string to filter by undefined field values @@ -538,6 +539,16 @@ export const getFilterDefinitions = ( docUrl: 'https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6', encoder: simpleFiltersEncoder('DnsFlagsResponseCode') }, + { + id: 'dns_errno', + name: t('DNS Error'), + category: FilterCategory.None, + component: FilterComponent.Autocomplete, + getOptions: cap10(getDnsErrorCodeOptions), + validate: rejectEmptyValue, + hint: t('Specify a single DNS error number.'), + encoder: simpleFiltersEncoder('DnsErrno') + }, { id: 'time_flow_rtt', name: t('Flow RTT'), diff --git a/web/src/utils/filter-options.ts b/web/src/utils/filter-options.ts index c6e5cb604..e568a4048 100644 --- a/web/src/utils/filter-options.ts +++ b/web/src/utils/filter-options.ts @@ -5,7 +5,7 @@ import { FlowDirection } from '../api/ipfix'; import { FilterOption } from '../model/filters'; import { splitResource, SplitStage } from '../model/resource'; import { autoCompleteCache } from './autocomplete-cache'; -import { DNS_RCODES } from './dns'; +import { DNS_ERRORS, DNS_RCODES } from './dns'; import { getPort, getService } from './port'; import { DROP_CAUSES, DROP_STATES } from './pkt-drop'; import { TFunction } from 'i18next'; @@ -125,6 +125,14 @@ export const getDnsResponseCodeOptions = (value: string): Promise => { + return Promise.resolve( + DNS_ERRORS.filter( + opt => String(opt.value).includes(value) || opt.name.toLowerCase().includes(value.toLowerCase()) + ).map(v => ({ name: v.name, value: String(v.value) })) + ); +}; + export const findProtocolOption = (nameOrVal: string) => { return protocolOptions.find(p => p.name.toLowerCase() === nameOrVal.toLowerCase() || p.value === nameOrVal); };