Merge pull request #1268 from kernelkit/syslog-matching

Syslog Enhancements
This commit is contained in:
Joachim Wiberg
2025-11-20 16:11:26 +01:00
committed by GitHub
40 changed files with 1622 additions and 122 deletions
+2
View File
@@ -31,6 +31,8 @@ bind "set completion-ignore-case on"
# show all completions immediately instead of ringing bell
bind "set show-all-if-ambiguous on"
export LANG=C.UTF-8
log()
{
local fn="/var/log/syslog"
+2 -1
View File
@@ -1,2 +1,3 @@
set COLORTERM=yes
rlimit soft core infinity
set LANG=C.UTF-8
rlimit soft core infinity
@@ -3,6 +3,7 @@ alias la='ls -A'
alias ll='ls -alF'
alias ls='ls --color=auto'
export LANG=C.UTF-8
export EDITOR=/usr/bin/edit
export VISUAL=/usr/bin/edit
export LESS="-P %f (press h for help or q to quit)"
+11
View File
@@ -48,6 +48,17 @@ All notable changes to the project are documented in this file.
- Add CLI commands for managing boot partition order: `show boot-order` and
`set boot-order` allow viewing and changing the boot order from the CLI,
complementing the existing YANG RPC support, issue #1032
- Extended syslog filtering capabilities, issue #1091:
- Add support for pattern matching using POSIX extended regular expressions
on message content (IETF `select-match` feature)
- Add support for advanced severity comparison: exact match (`equals`) and
exclusion (`block`/`stop`) in addition to the default equals-or-higher
(IETF `select-adv-compare` feature)
- Add support for hostname-based filtering, useful when acting as a log
server to route messages from different devices to separate log files
- Add support for property-based filtering with operators (contains, isequal,
startswith, regex, ereregex) on message properties (msg, msgid, programname,
hostname, source, data), with optional case-insensitive and negate modifiers
### Fixes
+108 -2
View File
@@ -191,8 +191,114 @@ admin@example:/>
```
See the above [Log to File](#log-to-file) section on how to set up
filtering of received logs to local files. Please note, filtering based
on property, e.g., hostname, is not supported yet.
filtering of received logs to local files. Advanced filtering based
on hostname and message properties is also available, see the next
section for details.
## Advanced Filtering
The syslog subsystem supports several advanced filtering options that
allow fine-grained control over which messages are logged. These can
be combined with facility and severity filters to create sophisticated
logging rules.
### Pattern Matching
Messages can be filtered using regular expressions (POSIX extended regex)
on the message content. This is useful when you want to log only messages
containing specific keywords or patterns:
```
admin@example:/config/> edit syslog actions log-file file:errors
admin@example:/config/syslog/…/file:errors/> set pattern-match "ERROR|CRITICAL|FATAL"
admin@example:/config/syslog/…/file:errors/> set facility-list all severity info
admin@example:/config/syslog/…/file:errors/> leave
admin@example:/>
```
This will log all messages containing ERROR, CRITICAL, or FATAL.
### Advanced Severity Comparison
By default, severity filtering uses "equals-or-higher" comparison,
meaning a severity of `error` will match error, critical, alert, and
emergency messages. You can change this behavior:
```
admin@example:/config/> edit syslog actions log-file file:daemon-errors
admin@example:/config/syslog/…/file:daemon-errors/> set facility-list daemon
admin@example:/config/syslog/…/daemon/> set severity error
admin@example:/config/syslog/…/daemon/> set advanced-compare compare equals
admin@example:/config/syslog/…/daemon/> leave
admin@example:/>
```
This will log only `error` severity messages, not higher severities.
You can also block specific severities:
```
admin@example:/config/syslog/…/daemon/> set advanced-compare action block
```
This will exclude `error` messages from the log.
### Hostname Filtering
When acting as a log server, you can filter messages by hostname. This
is useful for directing logs from different devices to separate files:
```
admin@example:/config/> edit syslog actions log-file file:router1
admin@example:/config/syslog/…/file:router1/> set hostname-filter router1
admin@example:/config/syslog/…/file:router1/> set facility-list all severity info
admin@example:/config/syslog/…/file:router1/> leave
admin@example:/>
```
Multiple hostnames can be added to the filter list.
### Property-Based Filtering
For more advanced filtering, you can match on specific message properties
using various comparison operators:
```
admin@example:/config/> edit syslog actions log-file file:myapp
admin@example:/config/syslog/…/file:myapp/> edit property-filter
admin@example:/config/syslog/…/property-filter/> set property programname
admin@example:/config/syslog/…/property-filter/> set operator isequal
admin@example:/config/syslog/…/property-filter/> set value myapp
admin@example:/config/syslog/…/property-filter/> leave
admin@example:/>
```
Available properties:
- `msg`: Message body
- `msgid`: RFC5424 message identifier
- `programname`: Program/tag name
- `hostname`: Source hostname
- `source`: Alias for hostname
- `data`: RFC5424 structured data
Available operators:
- `contains`: Substring match
- `isequal`: Exact equality
- `startswith`: Prefix match
- `regex`: Basic regular expression
- `ereregex`: Extended regular expression (POSIX ERE)
The comparison can be made case-insensitive:
```
admin@example:/config/syslog/…/property-filter/> set case-insensitive true
```
Or negated to exclude matching messages:
```
admin@example:/config/syslog/…/property-filter/> set negate true
```
### Facilities
@@ -1,5 +1,7 @@
# -l :: Keep kernel looging to console
# -H :: Log remote messages using hostname from the message
# -l :: Keep kernel logging to console
# -m0 :: Disable periodic syslog MARK entries
# -n :: Disable DNS query for every request, trust hostname in message
# -s :: Enable secure mode, don't listen to remote logs
# -r 1M:5 :: Log rotation every 1 MiB and keep 5 rotated ones
SYSLOGD_ARGS="-l -m0"
SYSLOGD_ARGS="-H -l -m0"
@@ -1,7 +1,7 @@
From 31dfe0d16461d2852f3fc56cb82aed3a23db022f Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Thu, 25 Sep 2025 16:48:31 +0200
Subject: [PATCH] syslogd: fix UTF-8 handling with -8 flag, for RFC5424
Subject: [PATCH 1/4] syslogd: fix UTF-8 handling with -8 flag, for RFC5424
compliance
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
@@ -0,0 +1,78 @@
From f9b6531430c6485af4697a5868a6f43c93d20419 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Fri, 26 Sep 2025 08:42:57 +0200
Subject: [PATCH 2/4] syslogd: fix parentheses handling in RFC 3164 tag parsing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Some applications use tag names like "(polkit-agent)" which sysklogd do
not handle well. This patch aims to address this and also add a bit of
logic to normalize such tags:
- Allow parentheses in tag character set for broader compatibility
- Smart parsing: strip surrounding parentheses from complete tags like
"(polkit-agent):" → "polkit-agent", while preserving partial parentheses
like "app(version):" unchanged
- Maintain RFC compliance: both RFC3164 and RFC5424 allow parentheses
- Preserve all existing functionality and edge case handling
Fixes #104
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/syslogd.c | 35 ++++++++++++++++++++++++++++-------
1 file changed, 28 insertions(+), 7 deletions(-)
diff --git a/src/syslogd.c b/src/syslogd.c
index fa82d98..37e1920 100644
--- a/src/syslogd.c
+++ b/src/syslogd.c
@@ -1309,15 +1309,36 @@ parsemsg_rfc3164_app_name_procid(char **msg, char **app_name, char **procid)
m = *msg;
/* Application name. */
- app_name_begin = m;
- app_name_length = strspn(m,
- "abcdefghijklmnopqrstuvwxyz"
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- "0123456789"
- "._-/");
+ /* Check if tag is surrounded by parentheses like (polkit-agent) */
+ if (*m == '(') {
+ char *closing = strchr(m + 1, ')');
+ if (closing && (closing[1] == ':' || closing[1] == '[' || isblank(closing[1]) || closing[1] == '\0')) {
+ /* Found complete parenthetical tag, strip parentheses */
+ app_name_begin = m + 1;
+ app_name_length = closing - (m + 1);
+ m = closing + 1;
+ } else {
+ /* Incomplete or malformed, treat normally */
+ app_name_begin = m;
+ app_name_length = strspn(m,
+ "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ "._-/()");
+ m += app_name_length;
+ }
+ } else {
+ app_name_begin = m;
+ app_name_length = strspn(m,
+ "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ "._-/()");
+ m += app_name_length;
+ }
+
if (app_name_length == 0)
goto bad;
- m += app_name_length;
/* Process identifier (optional). */
if (*m == '[') {
--
2.43.0
@@ -0,0 +1,168 @@
From 10372296093cc70f3d21b46359a4cfbd8312c968 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Fri, 26 Sep 2025 10:07:45 +0200
Subject: [PATCH 3/4] test: verify parenthetical tag parsing
Organization: Wires
Regression test for issue #104.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
test/Makefile.am | 5 +-
test/parens.sh | 119 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 122 insertions(+), 2 deletions(-)
create mode 100755 test/parens.sh
diff --git a/test/Makefile.am b/test/Makefile.am
index 7162e6c..59a38cf 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -3,10 +3,10 @@ EXTRA_DIST += api.sh local.sh unicode.sh remote.sh fwd.sh mark.sh \
memleak.sh facility.sh notify.sh rotate_all.sh secure.sh \
logger.sh listen.sh sighup.sh tag.sh hostname.sh \
property.sh raw.sh regression.sh multicast.sh \
- mcast-fwd.sh mcast-iface.sh
+ mcast-fwd.sh mcast-iface.sh parens.sh
CLEANFILES = *~ *.trs *.log
TEST_EXTENSIONS = .sh
-TESTS_ENVIRONMENT= unshare -mrun
+TESTS_ENVIRONMENT= unshare -mrun --map-auto
check_PROGRAMS = api
api_SOURCES = api.c
@@ -30,6 +30,7 @@ TESTS += rotate_all.sh
TESTS += secure.sh
TESTS += sighup.sh
TESTS += tag.sh
+TESTS += parens.sh
TESTS += hostname.sh
TESTS += property.sh
TESTS += raw.sh
diff --git a/test/parens.sh b/test/parens.sh
new file mode 100755
index 0000000..43b8744
--- /dev/null
+++ b/test/parens.sh
@@ -0,0 +1,119 @@
+#!/bin/sh
+# Verify parentheses handling in log tags for RFC3164 messages.
+# Regression test for issue #104.
+#
+. "${srcdir:-.}/lib.sh"
+
+
+MSG1="Failed to execute /usr/bin/pkttyagent: No such file or directory"
+MSG2="Normal application message"
+MSG3="Version specific message"
+MSG4="Service startup message"
+
+LOGDIR="$DIR/log"
+SYSLOG="${LOGDIR}/syslog"
+
+setup_syslogd()
+{
+ mkdir -p "$LOGDIR"
+ cat <<-EOF >"${CONF}"
+ # Log everything for testing
+ *.* -$SYSLOG
+ EOF
+ setup -m0
+}
+
+
+extract_tag()
+{
+ msg="$1"
+ actual_line=$(grep "$msg" "$SYSLOG" | tail -1)
+ if [ -n "$actual_line" ]; then
+ echo "$actual_line" | sed -n 's/.*[0-9][0-9] [^ ]* \([^:]*\):.*/\1/p'
+ fi
+}
+
+show_result()
+{
+ input="$1"
+ expected="$2"
+ got="$3"
+
+ echo "Input: '$input'"
+ echo "Expected: '$expected'"
+ echo "Got: '$got'"
+}
+
+verify()
+{
+ input_tag="$1"
+ expected_tag="$2"
+ msg="$3"
+ expected_pattern="${4:-$expected_tag}"
+
+ actual_tag=$(extract_tag "$msg")
+ if [ -n "$actual_tag" ]; then
+ show_result "$input_tag" "$expected_tag" "$actual_tag"
+ if grep -q "$expected_pattern.*$msg" "$SYSLOG"; then
+ return 0
+ else
+ echo "Log contents:"
+ cat "$SYSLOG"
+ return 1
+ fi
+ else
+ echo "Message not found in log"
+ echo "Log contents:"
+ cat "$SYSLOG"
+ return 1
+ fi
+}
+
+test_normal_tag()
+{
+ logger -b -ip user.info -t "normal-app" "$MSG2"
+ verify "normal-app" "normal-app" "$MSG2"
+}
+
+test_paren_stripping()
+{
+ logger -b -ip user.info -t "(polkit-agent)" "$MSG1"
+ verify "(polkit-agent)" "polkit-agent" "$MSG1"
+}
+
+test_service_stripping()
+{
+ logger -b -ip user.info -t "(service-name)" "$MSG4"
+ verify "(service-name)" "service-name" "$MSG4"
+}
+
+test_partial_parens()
+{
+ logger -b -ip user.info -t "app(version)" "$MSG3"
+ verify "app(version)" "app(version)" "$MSG3"
+}
+
+test_normal_tag_with_pid()
+{
+ pid="$$"
+
+ logger -b -ip user.info -t "normal-app[${pid}]" "$MSG2"
+ verify "normal-app[${pid}]" "normal-app[${pid}]" "$MSG2" "normal-app\[${pid}\]"
+}
+
+test_paren_with_pid()
+{
+ pid="$$"
+
+ logger -b -ip user.info -t "(polkit-agent)[$pid]" "$MSG1"
+ verify "(polkit-agent)[$pid]" "polkit-agent[$pid]" "$MSG1" "polkit-agent\[$pid\]"
+}
+
+
+run_step "Set up syslogd for parentheses testing" setup_syslogd
+run_step "Test parenthetical tag stripping" test_paren_stripping
+run_step "Test normal tag preservation" test_normal_tag
+run_step "Test partial parentheses preservation" test_partial_parens
+run_step "Test service parenthetical tag" test_service_stripping
+run_step "Test parenthetical tag with PID" test_paren_with_pid
+run_step "Test normal tag with PID" test_normal_tag_with_pid
--
2.43.0
@@ -0,0 +1,142 @@
From c1c3f253a8963c6e9e41fafcf9b888f3f5a34906 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Mon, 3 Nov 2025 13:11:56 +0100
Subject: [PATCH 4/4] syslogd: unescape Linux /dev/kmsg messages before
sanitization
Organization: Wires
Linux's /dev/kmsg interface escapes all non-printable characters and
backslashes using C-style hex encoding (\xHH format). Preventing the
recent UTF-8 sanitization improvements from working correctly, since
UTF-8 sequences arrive as escaped text like "\xe2\x80\x94" rather than
as actual bytes, see [1] for details.
This commit implements "unescaping" of the kernel's C-style format
before applying the UTF-8-aware sanitization. Ensuring that UTF-8
content also in kernel messages is properly preserved when using the
-8 flag.
[1]: https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/syslogd.c | 81 ++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 77 insertions(+), 4 deletions(-)
diff --git a/src/syslogd.c b/src/syslogd.c
index 37e1920..0b6bc30 100644
--- a/src/syslogd.c
+++ b/src/syslogd.c
@@ -155,6 +155,7 @@ static int KernLog = 1; /* Track kernel logs by default */
static int KeepKernFac; /* Keep remotely logged kernel facility */
static int KeepKernTime; /* Keep kernel timestamp, evern after initial read */
static int KeepKernConsole; /* Keep kernel logging to console */
+static int IsLinuxKmsg; /* Set if reading from Linux /dev/kmsg */
static int rotate_opt; /* Set if command line option has been given (wins) */
static off_t RotateSz = 0; /* Max file size (bytes) before rotating, disabled by default */
@@ -651,8 +652,10 @@ int main(int argc, char *argv[])
_PATH_KLOG);
else
kern_console_off();
- } else
+ } else {
+ IsLinuxKmsg = 1;
kern_console_off();
+ }
}
no_klogd:
consfile.f_type = F_CONSOLE;
@@ -1062,6 +1065,61 @@ utf8_valid(const unsigned char *in, size_t len)
return 1;
}
+/*
+ * Unescapes Linux /dev/kmsg messages that use C-style hex encoding.
+ * Converts "\xHH" sequences back to bytes and "\\" back to "\".
+ * Returns the new length of the unescaped string.
+ */
+static size_t kmsg_unescape(char *msg)
+{
+ char *src, *dst;
+ int hi, lo;
+
+ src = dst = msg;
+ while (*src) {
+ if (*src == '\\' && src[1]) {
+ if (src[1] == 'x' && src[2] && src[3]) {
+ /* Decode \xHH */
+ hi = src[2];
+ lo = src[3];
+
+ /* Convert hex digits to values */
+ if (hi >= '0' && hi <= '9')
+ hi = hi - '0';
+ else if (hi >= 'a' && hi <= 'f')
+ hi = hi - 'a' + 10;
+ else if (hi >= 'A' && hi <= 'F')
+ hi = hi - 'A' + 10;
+ else
+ goto copy_literal;
+
+ if (lo >= '0' && lo <= '9')
+ lo = lo - '0';
+ else if (lo >= 'a' && lo <= 'f')
+ lo = lo - 'a' + 10;
+ else if (lo >= 'A' && lo <= 'F')
+ lo = lo - 'A' + 10;
+ else
+ goto copy_literal;
+
+ *dst++ = (char)((hi << 4) | lo);
+ src += 4;
+ continue;
+ } else if (src[1] == '\\') {
+ /* Decode \\ to \ */
+ *dst++ = '\\';
+ src += 2;
+ continue;
+ }
+ }
+copy_literal:
+ *dst++ = *src++;
+ }
+ *dst = '\0';
+
+ return dst - msg;
+}
+
/*
* Removes characters from log messages that are unsafe to display.
* Preserves valid UTF-8 sequences, including BOM, with -8 flag.
@@ -1773,9 +1831,24 @@ void printsys(char *msg)
parsemsg_rfc3164_app_name_procid(&p, &buffer.app_name, &buffer.proc_id);
q = lp;
- while (*p != '\0' && (c = *p++) != '\n' && q < &line[MAXLINE])
- *q++ = c;
- *q = '\0';
+ if (IsLinuxKmsg) {
+ char tmp[MAXLINE + 1];
+ char *t = tmp;
+
+ while (*p != '\0' && *p != '\n' && t < &tmp[MAXLINE])
+ *t++ = *p++;
+ *t = '\0';
+
+ /* Unescape \xHH sequences and \\ */
+ kmsg_unescape(tmp);
+
+ /* Sanitize the unescaped message with UTF-8 support */
+ parsemsg_remove_unsafe_characters(tmp, lp, MAXLINE);
+ } else {
+ while (*p != '\0' && (c = *p++) != '\n' && q < &line[MAXLINE])
+ *q++ = c;
+ *q = '\0';
+ }
logmsg(&buffer);
}
--
2.43.0
+119 -20
View File
@@ -6,17 +6,19 @@
#include "core.h"
#define XPATH_BASE_ "/ietf-syslog:syslog"
#define XPATH_FILE_ XPATH_BASE_"/actions/file/log-file"
#define XPATH_REMOTE_ XPATH_BASE_"/actions/remote/destination"
#define XPATH_ROTATE_ XPATH_BASE_"/infix-syslog:file-rotation"
#define XPATH_SERVER_ XPATH_BASE_"/infix-syslog:server"
#define XPATH_BASE_ "/ietf-syslog:syslog"
#define XPATH_FILE_ XPATH_BASE_"/actions/file"
#define XPATH_LOG_FILE XPATH_BASE_"/actions/file/log-file"
#define XPATH_REMOTE_ XPATH_BASE_"/actions/remote"
#define XPATH_REMOTE_DST XPATH_BASE_"/actions/remote/destination"
#define XPATH_ROTATE_ XPATH_BASE_"/infix-syslog:file-rotation"
#define XPATH_SERVER_ XPATH_BASE_"/infix-syslog:server"
#define SYSLOG_D_ "/etc/syslog.d"
#define SYSLOG_FILE SYSLOG_D_"/log-file-%s.conf"
#define SYSLOG_REMOTE SYSLOG_D_"/remote-%s.conf"
#define SYSLOG_ROTATE SYSLOG_D_"/rotate.conf"
#define SYSLOG_SERVER SYSLOG_D_"/server.conf"
#define SYSLOG_D_ "/etc/syslog.d"
#define SYSLOG_FILE SYSLOG_D_"/log-file-%s.conf"
#define SYSLOG_REMOTE SYSLOG_D_"/remote-%s.conf"
#define SYSLOG_ROTATE SYSLOG_D_"/rotate.conf"
#define SYSLOG_SERVER SYSLOG_D_"/server.conf"
struct addr {
char *address;
@@ -94,22 +96,86 @@ static const char *sxlate(const char *severity)
static size_t selector(sr_session_ctx_t *session, struct action *act)
{
char xpath[strlen(act->xpath) + 32];
char xpath[strlen(act->xpath) + 64];
bool has_pattern = false;
sr_val_t *list = NULL;
size_t count = 0;
size_t num = 0;
char *pattern;
int rc;
/* Check for hostname-filter (infix-syslog augment) */
snprintf(xpath, sizeof(xpath), "%s/infix-syslog:hostname-filter", act->xpath);
rc = sr_get_items(session, xpath, 0, 0, &list, &count);
if (rc == SR_ERR_OK && count > 0) {
fprintf(act->fp, "+");
for (size_t i = 0; i < count; i++)
fprintf(act->fp, "%s%s", i ? "," : "", list[i].data.string_val);
fprintf(act->fp, "\n");
sr_free_values(list, count);
list = NULL;
count = 0;
}
/* Check for property-filter (infix-syslog augment) */
char *property = srx_get_str(session, "%s/infix-syslog:property-filter/property", act->xpath);
if (property) {
char *operator = srx_get_str(session, "%s/infix-syslog:property-filter/operator", act->xpath);
char *value = srx_get_str(session, "%s/infix-syslog:property-filter/value", act->xpath);
char *case_str = srx_get_str(session, "%s/infix-syslog:property-filter/case-insensitive", act->xpath);
char *negate_str = srx_get_str(session, "%s/infix-syslog:property-filter/negate", act->xpath);
if (operator && value) {
/* Build operator prefix: [!][icase_]operator */
char op_prefix[32] = "";
/* Only apply negate if explicitly set to true */
if (negate_str && !strcmp(negate_str, "true"))
strlcat(op_prefix, "!", sizeof(op_prefix));
/* Only apply icase_ if explicitly set to true */
if (case_str && !strcmp(case_str, "true"))
strlcat(op_prefix, "icase_", sizeof(op_prefix));
strlcat(op_prefix, operator, sizeof(op_prefix));
/* Property-based filter: :property, [!][icase_]operator, "value" */
fprintf(act->fp, ":%s, %s, \"%s\"\n", property, op_prefix, value);
has_pattern = true;
}
free(property);
free(operator);
free(value);
free(case_str);
free(negate_str);
}
/* Check for pattern-match (select-match feature) */
pattern = srx_get_str(session, "%s/pattern-match", act->xpath);
if (pattern) {
/* Property-based filter: :msg, ereregex, "pattern" */
fprintf(act->fp, ":msg, ereregex, \"%s\"\n", pattern);
has_pattern = true;
free(pattern);
}
snprintf(xpath, sizeof(xpath), "%s/facility-filter/facility-list", act->xpath);
rc = sr_get_items(session, xpath, 0, 0, &list, &count);
if (rc != SR_ERR_OK) {
ERROR("Cannot find facility-list for syslog file:%s: %s\n", act->name, sr_strerror(rc));
if (rc || !count) {
if (has_pattern) {
fprintf(act->fp, "*.*");
return 1;
}
return 0;
}
for (size_t i = 0; i < count; ++i) {
sr_val_t *entry = &list[i];
char *facility, *severity;
char *facility, *severity, *compare, *action_str;
const char *prefix = "";
facility = srx_get_str(session, "%s/facility", entry->xpath);
if (!facility)
@@ -120,16 +186,45 @@ static size_t selector(sr_session_ctx_t *session, struct action *act)
continue;
}
fprintf(act->fp, "%s%s.%s", i ? ";" : "", fxlate(facility), sxlate(severity));
/* Check for advanced-compare (select-adv-compare feature) */
compare = srx_get_str(session, "%s/advanced-compare/compare", entry->xpath);
action_str = srx_get_str(session, "%s/advanced-compare/action", entry->xpath);
/*
* Handle compare + action combinations:
* - compare: equals -> use '=' after dot (facility.=severity)
* - compare: equals-or-higher (default) -> no prefix
* - action: block/stop -> use '!' after dot (facility.!severity)
* - action: log (default) -> no prefix modification
*
* Note: action block/stop takes precedence over compare equals
*/
/* First check compare */
if (compare && strstr(compare, "equals") && !strstr(compare, "equals-or-higher")) {
prefix = "=";
}
/* Then check action (can override compare) */
if (action_str) {
const char *a = action_str;
if (strstr(a, "block") || strstr(a, "stop")) {
prefix = "!";
}
free(action_str);
}
fprintf(act->fp, "%s%s.%s%s", i ? ";" : "", fxlate(facility), prefix, sxlate(severity));
num++;
free(facility);
free(severity);
free(compare);
}
sr_free_values(list, count);
return num;
/* Return non-zero if we have either pattern or facilities */
return has_pattern ? (num > 0 ? num : 1) : num;
}
static void action(sr_session_ctx_t *session, const char *name, const char *xpath, struct addr *addr)
@@ -142,10 +237,12 @@ static void action(sr_session_ctx_t *session, const char *name, const char *xpat
char *sz, *cnt, *fmt;
char opts[80] = "\t";
char *sep = ";";
const char *fn;
act.fp = fopen(filename(name, addr ? true : false, act.path, sizeof(act.path)), "w");
fn = filename(name, addr ? true : false, act.path, sizeof(act.path));
act.fp = fopen(fn, "w");
if (!act.fp) {
ERRNO("Failed opening %s", act.path);
ERRNO("Failed opening %s", fn);
return;
}
@@ -234,7 +331,7 @@ static int file_change(sr_session_ctx_t *session, struct lyd_node *config,struct
LYX_LIST_FOR_EACH(files, file, "log-file") {
struct lyd_node *node = lydx_get_child(file, "name");
enum lydx_op op = lydx_get_op(node);
char path[512] = XPATH_FILE_;
char path[512] = XPATH_LOG_FILE;
const char *name;
name = getnm(node, path, sizeof(path));
@@ -263,7 +360,7 @@ static int remote_change(sr_session_ctx_t *session, struct lyd_node *config, str
LYX_LIST_FOR_EACH(dremotes, remote, "destination") {
struct lyd_node *node = lydx_get_child(remote, "name");
enum lydx_op op = lydx_get_op(node);
char path[512] = XPATH_REMOTE_;
char path[512] = XPATH_REMOTE_DST;
const char *name;
name = getnm(node, path, sizeof(path));
@@ -365,9 +462,11 @@ done:
return SR_ERR_OK;
}
int ietf_syslog_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd)
{
int rc = SR_ERR_OK;
if ((rc = file_change(session, config, diff, event, confd)))
return rc;
if ((rc = remote_change(session, config, diff, event, confd)))
+1
View File
@@ -218,6 +218,7 @@ static int change_clock(sr_session_ctx_t *session, struct lyd_node *config, stru
if (diff && !lydx_get_xpathf(diff, XPATH_BASE_"/clock"))
return SR_ERR_OK;
switch (event) {
case SR_EV_ENABLED: /* first time, on register. */
case SR_EV_CHANGE: /* regular change (copy cand running) */
+2 -2
View File
@@ -18,8 +18,8 @@ MODULES=(
"iana-if-type@2023-01-26.yang"
# NOTE: ietf-tls-client must be version matched with ietf-tls-server, used by netopeer2!
"ietf-tls-client@2023-12-28.yang"
"ietf-syslog@2024-03-21.yang -e file-action -e file-limit-size -e remote-action"
"infix-syslog@2024-07-19.yang"
"ietf-syslog@2024-03-21.yang -e file-action -e file-limit-size -e remote-action -e select-adv-compare -e select-match"
"infix-syslog@2025-11-17.yang"
"iana-hardware@2018-03-13.yang"
"ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor"
"infix-hardware@2025-10-30.yang"
+88
View File
@@ -16,6 +16,11 @@ module infix-syslog {
contact "kernelkit@googlegroups.com";
description "Infix augments and deviations to ietf-syslog, draft 32.";
revision 2025-11-17 {
description "Add hostname-filter support.";
reference "internal";
}
revision 2024-07-19 {
description "Initial revision, based on IETF syslog YANG draft 32.
The following changes have been made in this model:
@@ -164,6 +169,89 @@ module infix-syslog {
}
}
}
leaf-list hostname-filter {
type inet:domain-name;
description "Filter messages by hostname. Messages from the listed
hostnames will be logged to this file.";
}
container property-filter {
presence "Enable property-based filtering";
description "Advanced property-based message filtering. Allows filtering
on specific message fields with various comparison operators.";
leaf property {
type enumeration {
enum msg {
description "The message body (SYSLOG-MSG field).";
}
enum msgid {
description "The RFC5424 message identifier (MSGID field).";
}
enum programname {
description "The originating program or tag name.";
}
enum hostname {
description "The message source hostname.";
}
enum source {
description "The message source (alias for hostname).";
}
enum data {
description "The RFC5424 structured data (SD field).";
}
}
mandatory true;
description "The message property to match against.";
}
leaf operator {
type enumeration {
enum contains {
description "Substring match (case-sensitive).";
}
enum isequal {
description "Exact equality match.";
}
enum startswith {
description "Prefix match.";
}
enum regex {
description "Basic regular expression match.";
}
enum ereregex {
description "Extended regular expression match (POSIX ERE).";
}
}
mandatory true;
description "The comparison operator to use.";
}
leaf value {
type string;
mandatory true;
description "The value to compare against.";
}
leaf case-insensitive {
type boolean;
description "Perform case-insensitive comparison.";
}
leaf negate {
type boolean;
description "Negate the comparison result. When set to true, the
filter matches messages that do NOT satisfy the
specified condition.
For example:
- operator: contains, value: 'ERROR', negate: false
→ matches messages containing 'ERROR'
- operator: contains, value: 'ERROR', negate: true
→ matches messages NOT containing 'ERROR'";
}
}
}
augment "/syslog:syslog/syslog:actions/syslog:remote/syslog:destination" {
+20
View File
@@ -5,9 +5,29 @@ Tests verifying IETF standard syslog configuration and operation:
- Basic local syslog functionality and log generation
- Remote syslog forwarding and network logging
- Pattern matching using POSIX regex on message content
- Advanced comparison operators and actions (equals vs equals-or-higher)
- Hostname-based filtering for incoming messages
- Property-based filtering with operators and negation
include::basic/Readme.adoc[]
<<<
include::remote/Readme.adoc[]
<<<
include::pattern_match/Readme.adoc[]
<<<
include::advanced_compare/Readme.adoc[]
<<<
include::hostname_filter/Readme.adoc[]
<<<
include::property_filter/Readme.adoc[]
+1
View File
@@ -0,0 +1 @@
test.adoc
@@ -0,0 +1,25 @@
=== Syslog Advanced Compare
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ietf_syslog/advanced_compare]
==== Description
Verify the select-adv-compare feature: filtering syslog messages based on
severity with advanced comparison operators (equals vs equals-or-higher) and
actions (log vs block/stop).
==== Topology
image::topology.svg[Syslog Advanced Compare topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to target DUT
. Clean up old log files from previous test runs
. Configure syslog with advanced-compare
. Send test messages at all severity levels
. Verify exact-errors log contains only error messages
. Verify no-debug log blocks all messages
. Verify baseline log contains info and higher
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Syslog Advanced Compare
Verify the select-adv-compare feature: filtering syslog messages based on
severity with advanced comparison operators (equals vs equals-or-higher) and
actions (log vs block/stop).
"""
import infamy
import time
with infamy.Test() as test:
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
target = env.attach("target", "mgmt")
tgtssh = env.attach("target", "mgmt", "ssh")
with test.step("Clean up old log files from previous test runs"):
tgtssh.runsh("sudo rm -f /var/log/{exact-errors,no-debug,baseline}")
with test.step("Configure syslog with advanced-compare"):
target.put_config_dicts({
"ietf-syslog": {
"syslog": {
"actions": {
"file": {
"log-file": [{
"name": "file:exact-errors",
"facility-filter": {
"facility-list": [{
"facility": "daemon",
"severity": "error",
"advanced-compare": {
"compare": "equals"
}
}]
}
}, {
"name": "file:no-debug",
"facility-filter": {
"facility-list": [{
"facility": "daemon",
"severity": "debug",
"advanced-compare": {
"action": "block"
}
}]
}
}, {
"name": "file:baseline",
"facility-filter": {
"facility-list": [{
"facility": "daemon",
"severity": "info"
}]
}
}]
}
}
}
}
})
with test.step("Send test messages at all severity levels"):
tgtssh.runsh("logger -t advtest -p daemon.emerg 'Emergency: system is unusable'")
tgtssh.runsh("logger -t advtest -p daemon.alert 'Alert: immediate action required'")
tgtssh.runsh("logger -t advtest -p daemon.crit 'Critical: critical condition'")
tgtssh.runsh("logger -t advtest -p daemon.err 'Error: error condition'")
tgtssh.runsh("logger -t advtest -p daemon.warning 'Warning: warning condition'")
tgtssh.runsh("logger -t advtest -p daemon.notice 'Notice: normal but significant'")
tgtssh.runsh("logger -t advtest -p daemon.info 'Info: informational message'")
tgtssh.runsh("logger -t advtest -p daemon.debug 'Debug: debug-level message'")
time.sleep(1)
with test.step("Verify exact-errors log contains only error messages"):
rc = tgtssh.runsh("grep -c 'advtest' /var/log/exact-errors 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 1:
test.fail(f"Expected 1 message in /var/log/exact-errors (error only), got {count}")
rc = tgtssh.runsh("grep -q 'Error: error condition' /var/log/exact-errors 2>/dev/null")
if rc.returncode != 0:
test.fail("Expected error message in /var/log/exact-errors")
rc = tgtssh.runsh("grep -c 'Emergency\\|Alert\\|Critical' /var/log/exact-errors 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 0:
test.fail(f"Expected 0 higher severity messages in /var/log/exact-errors, got {count}")
with test.step("Verify no-debug log blocks all messages"):
rc = tgtssh.runsh("grep -c 'advtest' /var/log/no-debug 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 0:
test.fail(f"Expected 0 messages in /var/log/no-debug (all blocked), got {count}")
with test.step("Verify baseline log contains info and higher"):
rc = tgtssh.runsh("grep -c 'advtest' /var/log/baseline 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 7:
test.fail(f"Expected 7 messages in /var/log/baseline (info and higher), got {count}")
rc = tgtssh.runsh("grep -c 'Debug: debug-level' /var/log/baseline 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 0:
test.fail(f"Expected 0 debug messages in /var/log/baseline, got {count}")
rc = tgtssh.runsh("grep -q 'Info: informational' /var/log/baseline 2>/dev/null")
if rc.returncode != 0:
test.fail("Expected info message in /var/log/baseline")
test.succeed()
+1
View File
@@ -0,0 +1 @@
../../../infamy/topologies/1x1.dot
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x1 Pages: 1 -->
<svg width="424pt" height="45pt"
viewBox="0.00 0.00 424.03 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
<title>1x1</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 420.03,-41 420.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-36.5 416.03,-36.5 416.03,-0.5 300.03,-0.5"/>
<text text-anchor="middle" x="325.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-36.5 "/>
<text text-anchor="middle" x="383.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-18.5C100,-18.5 300.03,-18.5 300.03,-18.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+12
View File
@@ -4,3 +4,15 @@
- name: Remote syslog
case: remote/test.py
- name: Syslog Pattern Matching
case: pattern_match/test.py
- name: Syslog Advanced Compare
case: advanced_compare/test.py
- name: Syslog Hostname Filtering
case: hostname_filter/test.py
- name: Syslog Property Filtering
case: property_filter/test.py
+6 -3
View File
@@ -4,7 +4,10 @@ ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ietf_syslog/basic]
==== Description
Add syslog actions to log to local files, then verify new log files have been created.
Add syslog actions matching on different facility and severity levels to
log to local files, then verify new log files have been created. In one
case we use an absolute `file:/path/to/bar.log` and in another a relative
path `file:foo`.
==== Topology
@@ -13,7 +16,7 @@ image::topology.svg[Syslog Basic topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to target DUT
. Configure syslog on DUT to log to files '/log/bar.log' (absolute path) and 'foo' (non-absolute).
. Verify log files /var/log/foo and /var/log/bar.log have been created
. Configure syslog on DUT to log to files '/log/bar.log' and 'foo'
. Verify log files have been created
+25 -31
View File
@@ -1,8 +1,11 @@
#!/usr/bin/env python3
"""
Syslog Basic
"""Syslog Basic
Add syslog actions matching on different facility and severity levels to
log to local files, then verify new log files have been created. In one
case we use an absolute `file:/path/to/bar.log` and in another a relative
path `file:foo`.
Add syslog actions to log to local files, then verify new log files have been created.
"""
import infamy
@@ -16,51 +19,42 @@ with infamy.Test() as test:
factory = env.get_password("target")
address = target.get_mgmt_ip()
with test.step("Configure syslog on DUT to log to files '/log/bar.log' (absolute path) and 'foo' (non-absolute)."):
with test.step("Configure syslog on DUT to log to files '/log/bar.log' and 'foo'"):
target.put_config_dicts({
"ietf-syslog": {
"syslog": {
"actions": {
"file": {
"log-file": [
{
"log-file": [{
"name": "file:foo",
"facility-filter": {
"facility-list": [
{
"facility": "auth",
"severity": "info"
},
{
"facility": "authpriv",
"severity": "debug"
}
]
"facility-list": [{
"facility": "auth",
"severity": "info"
}, {
"facility": "authpriv",
"severity": "debug"
}]
}
},
{
}, {
"name": "file:/log/bar.log",
"facility-filter": {
"facility-list": [
{
"facility": "all",
"severity": "critical"
},
{
"facility": "mail",
"severity": "warning"
}
]
"facility-list": [{
"facility": "all",
"severity": "critical"
}, {
"facility": "mail",
"severity": "warning"
}]
}
}
]
}]
}
}
}
}
})
with test.step("Verify log files /var/log/foo and /var/log/bar.log have been created"):
with test.step("Verify log files have been created"):
user = tgtssh.runsh("ls /var/log/{foo,bar.log}").stdout
if "/var/log/foo" not in user:
test.fail()
+1
View File
@@ -0,0 +1 @@
test.adoc
@@ -0,0 +1,25 @@
=== Syslog Hostname Filtering
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ietf_syslog/hostname_filter]
==== Description
Verify hostname-filter feature: filtering incoming syslog messages based
on the hostname. Tests log sink scenario with multiple remote clients.
==== Topology
image::topology.svg[Syslog Hostname Filtering topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to DUTs
. Clean up old log files on server
. Configure server as syslog sink with hostname filtering
. Configure client to forward logs to server
. Send log messages with different hostnames
. Verify router1 log contains only router1 messages
. Verify router2 log contains only router2 messages
. Verify all-hosts log contains all messages
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Syslog Hostname Filtering
Verify hostname-filter feature: filtering incoming syslog messages based
on the hostname. Tests log sink scenario with multiple remote clients.
"""
import infamy
import time
TEST_MESSAGES = [
("router1", "Message from router1"),
("router1", "Another from router1"),
("router2", "Message from router2"),
("other", "Message from a different host"),
]
with infamy.Test() as test:
with test.step("Set up topology and attach to DUTs"):
env = infamy.Env()
client = env.attach("client", "mgmt")
server = env.attach("server", "mgmt")
clientssh = env.attach("client", "mgmt", "ssh")
serverssh = env.attach("server", "mgmt", "ssh")
with test.step("Clean up old log files on server"):
serverssh.runsh("sudo rm -f /var/log/{router1,router2,all-hosts}")
with test.step("Configure server as syslog sink with hostname filtering"):
_, server_link = env.ltop.xlate("server", "link")
server.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": server_link,
"enabled": True,
"ipv4": {
"address": [{
"ip": "10.0.0.1",
"prefix-length": 24,
}]
}
}]
}
},
"ietf-syslog": {
"syslog": {
"actions": {
"file": {
"log-file": [{
"name": "file:router1",
"infix-syslog:hostname-filter": ["router1"],
"facility-filter": {
"facility-list": [{
"facility": "all",
"severity": "info"
}]
}
}, {
"name": "file:router2",
"infix-syslog:hostname-filter": ["router2"],
"facility-filter": {
"facility-list": [{
"facility": "all",
"severity": "info"
}]
}
}, {
"name": "file:all-hosts",
"facility-filter": {
"facility-list": [{
"facility": "all",
"severity": "info"
}]
}
}]
}
},
"infix-syslog:server": {
"enabled": True,
"listen": {
"udp": [{
"port": 514,
"address": "10.0.0.1"
}]
}
}
}
}
})
with test.step("Configure client to forward logs to server"):
_, client_link = env.ltop.xlate("client", "link")
client.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": client_link,
"enabled": True,
"ipv4": {
"address": [{
"ip": "10.0.0.2",
"prefix-length": 24,
}]
}
}]
}
},
"ietf-syslog": {
"syslog": {
"actions": {
"remote": {
"destination": [{
"name": "server",
"udp": {
"address": "10.0.0.1"
},
"facility-filter": {
"facility-list": [{
"facility": "all",
"severity": "info"
}]
}
}]
}
}
}
}
})
time.sleep(2)
with test.step("Send log messages with different hostnames"):
for hostname, message in TEST_MESSAGES:
clientssh.runsh(f"logger -t test -p daemon.info -H {hostname} -h 10.0.0.1 '{message}'")
time.sleep(2)
with test.step("Verify router1 log contains only router1 messages"):
rc = serverssh.runsh("cat /var/log/router1 2>/dev/null")
log_content = rc.stdout if rc.returncode == 0 else ""
router1_messages = [msg for host, msg in TEST_MESSAGES if host == "router1"]
missing = [msg for msg in router1_messages if msg not in log_content]
if missing:
test.fail(f"Missing router1 messages in /var/log/router1: {missing}")
unwanted_messages = [msg for host, msg in TEST_MESSAGES if host != "router1"]
found = [msg for msg in unwanted_messages if msg in log_content]
if found:
test.fail(f"Found unwanted messages in /var/log/router1: {found}")
with test.step("Verify router2 log contains only router2 messages"):
rc = serverssh.runsh("cat /var/log/router2 2>/dev/null")
log_content = rc.stdout if rc.returncode == 0 else ""
router2_messages = [msg for host, msg in TEST_MESSAGES if host == "router2"]
missing = [msg for msg in router2_messages if msg not in log_content]
if missing:
test.fail(f"Missing router2 messages in /var/log/router2: {missing}")
unwanted_messages = [msg for host, msg in TEST_MESSAGES if host != "router2"]
found = [msg for msg in unwanted_messages if msg in log_content]
if found:
test.fail(f"Found unwanted messages in /var/log/router2: {found}")
with test.step("Verify all-hosts log contains all messages"):
rc = serverssh.runsh("cat /var/log/all-hosts 2>/dev/null")
log_content = rc.stdout if rc.returncode == 0 else ""
expected_messages = [msg for _, msg in TEST_MESSAGES]
missing = [msg for msg in expected_messages if msg not in log_content]
if missing:
test.fail(f"Missing messages in /var/log/all-hosts: {missing}")
test.succeed()
+1
View File
@@ -0,0 +1 @@
../remote/topology.dot
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 2x2 Pages: 1 -->
<svg width="432pt" height="305pt"
viewBox="0.00 0.00 432.03 304.62" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 300.62)">
<title>2x2</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-300.62 428.03,-300.62 428.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-125.31 0,-171.31 108,-171.31 108,-125.31 0,-125.31"/>
<text text-anchor="middle" x="25" y="-144.61" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-125.31 50,-171.31 "/>
<text text-anchor="middle" x="79" y="-156.11" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
<polyline fill="none" stroke="black" points="50,-148.31 108,-148.31 "/>
<text text-anchor="middle" x="79" y="-133.11" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
</g>
<!-- client -->
<g id="node2" class="node">
<title>client</title>
<polygon fill="none" stroke="black" points="308.03,-250.12 308.03,-296.12 424.03,-296.12 424.03,-250.12 308.03,-250.12"/>
<text text-anchor="middle" x="333.03" y="-280.92" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="308.03,-273.12 358.03,-273.12 "/>
<text text-anchor="middle" x="333.03" y="-257.92" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="358.03,-250.12 358.03,-296.12 "/>
<text text-anchor="middle" x="391.03" y="-269.42" font-family="DejaVu Sans Mono, Book" font-size="14.00">client</text>
</g>
<!-- host&#45;&#45;client -->
<g id="edge1" class="edge">
<title>host:mgmt1&#45;&#45;client:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-160.31C108,-160.31 308.03,-285.12 308.03,-285.12"/>
</g>
<!-- server -->
<g id="node3" class="node">
<title>server</title>
<polygon fill="none" stroke="black" points="308.03,-0.5 308.03,-46.5 424.03,-46.5 424.03,-0.5 308.03,-0.5"/>
<text text-anchor="middle" x="333.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="308.03,-23.5 358.03,-23.5 "/>
<text text-anchor="middle" x="333.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="358.03,-0.5 358.03,-46.5 "/>
<text text-anchor="middle" x="391.03" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">server</text>
</g>
<!-- host&#45;&#45;server -->
<g id="edge2" class="edge">
<title>host:mgmt2&#45;&#45;server:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-136.31C108,-136.31 308.03,-11.5 308.03,-11.5"/>
</g>
<!-- client&#45;&#45;server -->
<g id="edge3" class="edge">
<title>client:link&#45;&#45;server:link</title>
<path fill="none" stroke="black" stroke-width="2" d="M333.03,-250.12C333.03,-250.12 333.03,-46.5 333.03,-46.5"/>
<text text-anchor="middle" x="292.03" y="-50.3" font-family="DejaVu Serif, Book" font-size="14.00">10.0.0.1/24</text>
<text text-anchor="middle" x="292.03" y="-238.92" font-family="DejaVu Serif, Book" font-size="14.00">10.0.0.2/24</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

+1
View File
@@ -0,0 +1 @@
test.adoc
@@ -0,0 +1,25 @@
=== Syslog Pattern Matching
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ietf_syslog/pattern_match]
==== Description
Verify the select-match feature: filtering syslog messages based on
pattern-match (POSIX regex) on message content. Tests both simple
substring matching and complex regex patterns.
==== Topology
image::topology.svg[Syslog Pattern Matching topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to target DUT
. Clean up old log files from previous test runs
. Configure syslog with pattern-match filters
. Send test messages with various patterns
. Verify errors log contains ERROR and CRITICAL messages
. Verify routers log contains matching router[0-9]+ pattern
. Verify all-messages log contains all test messages
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""
Syslog Pattern Matching
Verify the select-match feature: filtering syslog messages based on
pattern-match (POSIX regex) on message content. Tests both simple
substring matching and complex regex patterns.
"""
import infamy
import time
with infamy.Test() as test:
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
target = env.attach("target", "mgmt")
tgtssh = env.attach("target", "mgmt", "ssh")
with test.step("Clean up old log files from previous test runs"):
tgtssh.runsh("sudo rm -f /var/log/{errors,routers,all-messages}")
with test.step("Configure syslog with pattern-match filters"):
target.put_config_dicts({
"ietf-syslog": {
"syslog": {
"actions": {
"file": {
"log-file": [{
"name": "file:errors",
"pattern-match": "ERROR|CRITICAL",
}, {
"name": "file:routers",
"pattern-match": "router[0-9]+",
"facility-filter": {
"facility-list": [{
"facility": "all",
"severity": "info"
}]
}
}, {
"name": "file:all-messages",
"facility-filter": {
"facility-list": [{
"facility": "all",
"severity": "info"
}]
}
}]
}
}
}
}
})
with test.step("Send test messages with various patterns"):
tgtssh.runsh("logger -t test -p daemon.info 'ERROR: Connection failed on interface eth0'")
tgtssh.runsh("logger -t test -p daemon.info 'CRITICAL: System temperature high'")
tgtssh.runsh("logger -t test -p daemon.info 'Status update from router1: link up'")
tgtssh.runsh("logger -t test -p daemon.info 'Status update from router42: link down'")
tgtssh.runsh("logger -t test -p daemon.info 'INFO: Normal operation message'")
tgtssh.runsh("logger -t test -p daemon.info 'DEBUG: Verbose logging enabled'")
time.sleep(1)
with test.step("Verify errors log contains ERROR and CRITICAL messages"):
rc = tgtssh.runsh("grep -c 'ERROR\\|CRITICAL' /var/log/errors 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 2:
test.fail(f"Expected 2 ERROR/CRITICAL messages in /var/log/errors, got {count}")
# Verify it does NOT contain other messages
rc = tgtssh.runsh("grep -c 'router1\\|Normal operation\\|Verbose' /var/log/errors 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 0:
test.fail(f"Expected 0 non-error messages in /var/log/errors, got {count}")
with test.step("Verify routers log contains matching router[0-9]+ pattern"):
rc = tgtssh.runsh("grep -c 'router[0-9]\\+' /var/log/routers 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 2:
test.fail(f"Expected 2 router messages in /var/log/routers, got {count}")
# Verify both router1 and router42 are present
rc = tgtssh.runsh("grep -q 'router1' /var/log/routers 2>/dev/null")
if rc.returncode != 0:
test.fail("Expected router1 message in /var/log/routers")
rc = tgtssh.runsh("grep -q 'router42' /var/log/routers 2>/dev/null")
if rc.returncode != 0:
test.fail("Expected router42 message in /var/log/routers")
# Verify it does NOT contain error or normal messages
rc = tgtssh.runsh("grep -c 'ERROR\\|CRITICAL\\|Normal operation\\|Verbose' /var/log/routers 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 0:
test.fail(f"Expected 0 non-router messages in /var/log/routers, got {count}")
with test.step("Verify all-messages log contains all test messages"):
rc = tgtssh.runsh("grep -c 'test' /var/log/all-messages 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 6:
test.fail(f"Expected 6 total messages in /var/log/all-messages, got {count}")
test.succeed()
+1
View File
@@ -0,0 +1 @@
../../../infamy/topologies/1x1.dot
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x1 Pages: 1 -->
<svg width="424pt" height="45pt"
viewBox="0.00 0.00 424.03 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
<title>1x1</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 420.03,-41 420.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-36.5 416.03,-36.5 416.03,-0.5 300.03,-0.5"/>
<text text-anchor="middle" x="325.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-36.5 "/>
<text text-anchor="middle" x="383.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-18.5C100,-18.5 300.03,-18.5 300.03,-18.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
test.adoc
@@ -0,0 +1,25 @@
=== Syslog Property Filtering
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ietf_syslog/property_filter]
==== Description
Verify property-filter feature: filtering syslog messages based on various
message properties with different operators, case-insensitivity, and negation.
==== Topology
image::topology.svg[Syslog Property Filtering topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to target DUT
. Clean up old log files
. Configure syslog with property filters
. Send test messages
. Verify myapp log contains only myapp messages
. Verify not-error log excludes ERROR messages
. Verify case-test log matches case-insensitive 'warning'
. Verify baseline log contains all messages
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""Syslog Property Filtering
Verify property-filter feature: filtering syslog messages based on various
message properties with different operators, case-insensitivity, and negation.
"""
import infamy
import time
TEST_MESSAGES = [
("myapp", "Application startup"),
("myapp", "Processing request"),
("otherapp", "Different program"),
("test", "ERROR: Connection failed"),
("test", "INFO: Normal message"),
("test", "WARNING: Check config"),
("test", "Warning: lowercase"),
]
with infamy.Test() as test:
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
target = env.attach("target", "mgmt")
tgtssh = env.attach("target", "mgmt", "ssh")
with test.step("Clean up old log files"):
tgtssh.runsh("sudo rm -f /var/log/{myapp,not-error,case-test,baseline}")
with test.step("Configure syslog with property filters"):
target.put_config_dicts({
"ietf-syslog": {
"syslog": {
"actions": {
"file": {
"log-file": [{
"name": "file:myapp",
"infix-syslog:property-filter": {
"property": "programname",
"operator": "isequal",
"value": "myapp"
},
"facility-filter": {
"facility-list": [{
"facility": "all",
"severity": "info"
}]
}
}, {
"name": "file:not-error",
"infix-syslog:property-filter": {
"property": "msg",
"operator": "contains",
"value": "ERROR",
"negate": True
},
"facility-filter": {
"facility-list": [{
"facility": "all",
"severity": "info"
}]
}
}, {
"name": "file:case-test",
"infix-syslog:property-filter": {
"property": "msg",
"operator": "contains",
"value": "warning",
"case-insensitive": True
},
"facility-filter": {
"facility-list": [{
"facility": "all",
"severity": "info"
}]
}
}, {
"name": "file:baseline",
"facility-filter": {
"facility-list": [{
"facility": "all",
"severity": "info"
}]
}
}]
}
}
}
}
})
time.sleep(2)
with test.step("Send test messages"):
for tag, msg in TEST_MESSAGES:
tgtssh.runsh(f"logger -t {tag} -p daemon.info '{msg}'")
time.sleep(1)
with test.step("Verify myapp log contains only myapp messages"):
rc = tgtssh.runsh("grep -c 'myapp' /var/log/myapp 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 2:
test.fail(f"Expected 2 myapp messages in /var/log/myapp, got {count}")
rc = tgtssh.runsh("grep -c 'otherapp' /var/log/myapp 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 0:
test.fail(f"Expected 0 otherapp messages in /var/log/myapp, got {count}")
with test.step("Verify not-error log excludes ERROR messages"):
rc = tgtssh.runsh("grep -c 'test' /var/log/not-error 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 3:
test.fail(f"Expected 3 non-ERROR messages in /var/log/not-error, got {count}")
rc = tgtssh.runsh("grep -c 'ERROR' /var/log/not-error 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 0:
test.fail(f"Expected 0 ERROR messages in /var/log/not-error, got {count}")
with test.step("Verify case-test log matches case-insensitive 'warning'"):
rc = tgtssh.runsh("grep -c 'WARNING\\|Warning' /var/log/case-test 2>/dev/null")
count = int(rc.stdout.strip()) if rc.returncode == 0 else 0
if count != 2:
test.fail(f"Expected 2 warning messages in /var/log/case-test, got {count}")
with test.step("Verify baseline log contains all messages"):
for tag, msg in TEST_MESSAGES:
rc = tgtssh.runsh(f"grep -q '{msg}' /var/log/baseline 2>/dev/null")
if rc.returncode != 0:
test.fail(f"Expected message '{msg}' not found in /var/log/baseline")
test.succeed()
+1
View File
@@ -0,0 +1 @@
../../../infamy/topologies/1x1.dot
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x1 Pages: 1 -->
<svg width="424pt" height="45pt"
viewBox="0.00 0.00 424.03 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
<title>1x1</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 420.03,-41 420.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-36.5 416.03,-36.5 416.03,-0.5 300.03,-0.5"/>
<text text-anchor="middle" x="325.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-36.5 "/>
<text text-anchor="middle" x="383.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-18.5C100,-18.5 300.03,-18.5 300.03,-18.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+38 -60
View File
@@ -30,15 +30,13 @@ with infamy.Test() as test:
"name": client_link,
"enabled": True,
"ipv4": {
"address": [
{
"address": [{
"ip": "10.0.0.2",
"prefix-length": 24,
}
]
}]
}
}
]}
}]
}
},
"ietf-syslog": {
"syslog": {
@@ -47,100 +45,81 @@ with infamy.Test() as test:
"log-file": [{
"name": "file:security",
"facility-filter": {
"facility-list": [
{
"facility-list": [{
"facility": "auth",
"severity": "all"
},
{
}, {
"facility": "audit",
"severity": "all"
}
]
}]
},
"infix-syslog:log-format": "rfc5424"
}]
},
"remote": {
"destination": [
{
"destination": [{
"name": "server",
"udp": {
"address": "10.0.0.1"
},
"facility-filter": {
"facility-list": [
{
"facility-list": [{
"facility": "audit",
"severity": "all"
},
{
}, {
"facility": "auth",
"severity": "all"
}]
},
"infix-syslog:log-format": "rfc5424"
}
]
}]
}
}
}
}
})
server.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [
{
"name": server_link,
"enabled": True,
"ipv4": {
"address": [
{
"ip": "10.0.0.1",
"prefix-length": 24,
}
]
}
"interface": [{
"name": server_link,
"enabled": True,
"ipv4": {
"address": [{
"ip": "10.0.0.1",
"prefix-length": 24,
}]
}
]
}]
}
},
"ietf-syslog": {
"syslog": {
"actions": {
"file": {
"log-file": [
{
"name": "file:security",
"facility-filter": {
"facility-list": [
{
"facility": "auth",
"severity": "all"
},
{
"facility": "audit",
"severity": "all"
}
]
},
"infix-syslog:log-format": "rfc5424"
}
]
"log-file": [{
"name": "file:security",
"facility-filter": {
"facility-list": [{
"facility": "auth",
"severity": "all"
}, {
"facility": "audit",
"severity": "all"
}]
},
"infix-syslog:log-format": "rfc5424"
}]
}
},
"infix-syslog:server": {
"enabled": True,
"listen": {
"udp": [
{
"port": 514,
"address": "10.0.0.1"
}
]
"udp": [{
"port": 514,
"address": "10.0.0.1"
}]
}
}
}
@@ -148,7 +127,6 @@ with infamy.Test() as test:
})
with test.step("Send security:notice log message from client and verify reception of client log message, incl. sorting to /log/security on server"):
infamy.until(lambda: syslog_check(clientssh, serverssh) == True)
infamy.until(lambda: syslog_check(clientssh, serverssh) == True)
test.succeed()