Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ starts with the computer, before anyone signs in, and that the window then drive
or encoding, and watch its latest messages change before saving — then read the stored
history again with it. Start from a template, save your own, and export them to share.
**Settings > Devices** lists every sender, and marks those that could use a rule; on the
local network, with its MAC address and maker, and the devices whose address changed
local network, with its MAC address and maker, and the devices whose address changed — and a
rule can name a device by its MAC, which follows it through DHCP (local network only)
- **Mutual TLS** where senders must prove who they are
- **Allowed sources** by IP literal or CIDR (hygiene, not authentication — see the note below)
- **Per-IP connection cap** so one host cannot occupy every slot
Expand Down
7 changes: 7 additions & 0 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ A rule has three parts.
- **Addresses**: comma-separated, each matched whole: an address (`10.0.7.12`), a CIDR range
(`10.0.7.0/24`) or a pattern (`10.0.7.*`). An address is that device only; a rule naming
`10.0.7.1` does not read `10.0.7.12`.
- **MAC addresses** — **local network only**: a MAC whole, in any spelling (`aa:bb:cc:dd:ee:ff`,
`AA-BB-CC-DD-EE-FF`, `aabb.ccdd.eeff`), or a pattern for a maker's devices (`04:03:12:*`). A rule
by MAC follows a device whose address changes (DHCP). The MAC comes from this computer's neighbor
table, read every 30 seconds, which only knows the devices on its own networks: behind a router,
every device arrives with the router's MAC, and **a rule by MAC never matches it** — name its
address instead. A device's very first messages may arrive before its MAC is known. When a rule
names one address whose MAC is known, the editor offers to name the MAC instead.
- **Hostnames** and **Applications**: patterns such as `fw-*`, matched against the message as
read without the rule.
- **Raw text contains**: a regular expression, case-insensitive. Behind a relay every device
Expand Down
4 changes: 3 additions & 1 deletion docs/documentation.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,9 @@ <h3>Messages that bend the standard</h3>
send no hostname with no rule to read them. On this computer's own networks it adds each
device's MAC address and maker, read from the system's neighbor table — a device behind
a router shows none, since only the router's would be known — and points out a device
whose address changed or an address whose device did. The messages already stored can be read
whose address changed or an address whose device did. A rule can name devices by MAC
address too, and follow one through DHCP — on the local network only, since behind a
router every device arrives with the router's MAC. The messages already stored can be read
again from their raw text with the rules as they are now — from every device or from
one rule's addresses, over the last day, week, month or all of it.
</p>
Expand Down
10 changes: 8 additions & 2 deletions frontend/screenshots/bridge/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,17 @@ let sourceTemplates = clone(SOURCE_TEMPLATES);
*/
function trySource(rule, samples) {
const named = (rule.match.sources || []).filter((s) => s.trim()).map((s) => addressMatcher(s, false));
const matches = (ip) => named.some((take) => take(ip));
const macs = (rule.match.macs || []).map((m) => m.trim().toLowerCase()).filter(Boolean);
const macOf = (ip) => (SOURCE_DEVICES.find((d) => d.address === ip) || {}).mac || '';
const matches = (ip) => (!named.length || named.some((take) => take(ip)))
&& (!macs.length || macs.some((m) => {
const mac = macOf(ip);
return mac && (m.endsWith('*') ? mac.startsWith(m.slice(0, -1)) : mac === m);
}));
return samples.map((s) => {
const before = messages.find((m) => m.rawMessage === s.raw)
|| { ...messages[0], rawMessage: s.raw, message: s.raw, sourceIP: s.sourceIP };
const matched = named.length > 0 && matches(s.sourceIP);
const matched = (named.length > 0 || macs.length > 0) && matches(s.sourceIP);
const after = { ...before };
if (matched) {
if (rule.hostnameFromSource) after.hostname = s.sourceIP;
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/SourceDevices.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
$: needing = rows.filter(r => r.device && attention(r.device)).length;

function readDifferently(row: Row) {
const existing = ruleForSource(rules, row.address);
const existing = ruleForSource(rules, row.address, row.device?.mac);
const rule = existing ? structuredClone(existing) : ruleForDevice(row.address, row.device);
let samples = samplesForRule($messages, rule);
if (!samples.some(s => s.sourceIP === row.address) && row.device?.lastRaw) {
Expand All @@ -92,7 +92,7 @@

/** The rule that names an address this device had, when none names its own. */
function oldRule(d: SourceDevice): { name: string; address: string } | undefined {
if (ruleForSource(rules, d.address)) return undefined;
if (ruleForSource(rules, d.address, d.mac)) return undefined;
for (const a of d.alsoAt || []) {
const r = rules.find(x => (x.match.sources || []).some(s => s.trim() === a));
if (r) return { name: r.name, address: a };
Expand Down Expand Up @@ -215,7 +215,7 @@
<td class="act">
<button class="action-btn" on:click={() => readDifferently(row)}
title={d?.lastRaw || ''}>
{ruleForSource(rules, row.address) ? $_('sources.devEditRule') : $_('sources.devRead')}
{ruleForSource(rules, row.address, d?.mac) ? $_('sources.devEditRule') : $_('sources.devRead')}
</button>
</td>
</tr>
Expand Down
38 changes: 37 additions & 1 deletion frontend/src/components/SourceRuleEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import FormatFields from './FormatFields.svelte';
import SaveTemplate from './SaveTemplate.svelte';
import {
deleteSourceRule, getSourceTemplates, isOwnTemplate, listText, parseList, ruleDraft, rulesVersion,
deleteSourceRule, getSourceTemplates, isOwnTemplate, listText, lookupAddress, parseList, ruleDraft, rulesVersion,
samplesForRule, saveSourceRule, templateName, templatesVersion, trySourceRule, withTemplate,
} from '../lib/sources';
import type { SourceRule, SourceSample, SourceTemplate, SourceTrial } from '../lib/sources';
Expand All @@ -34,6 +34,25 @@
rule.levelFrom = rule.levelFrom || '';
rule.encoding = rule.encoding || '';
let sourcesText = listText(rule.match.sources);
let macsText = listText(rule.match.macs);
// A rule naming one address whose MAC is known can name the MAC
// instead, and follow the device when its address changes.
let knownMAC = '';
$: single = parseList(sourcesText).length === 1 ? parseList(sourcesText)[0] : '';
$: lookupMAC(single);
async function lookupMAC(address: string) {
knownMAC = '';
if (!address || /[*?/]/.test(address)) return;
try {
const info = await lookupAddress(address);
if (address === single && info.mac && !info.thisMachine) knownMAC = info.mac;
} catch { /* nothing to offer */ }
}
function useMAC() {
macsText = knownMAC;
sourcesText = '';
retry();
}
// The samples it opened with, until the addresses change: then the
// latest messages from the addresses as now written.
const openedWith = sourcesText;
Expand Down Expand Up @@ -77,6 +96,7 @@
...rule,
match: {
sources: parseList(sourcesText),
macs: parseList(macsText),
hostnames: parseList(hostnamesText),
appNames: parseList(appNamesText),
pattern: (rule.match.pattern || '').trim(),
Expand Down Expand Up @@ -217,6 +237,14 @@
<input class="mono" placeholder="10.0.0.9, 10.0.1.0/24, 10.0.2.*" bind:value={sourcesText} on:input={retry} />
</label>
<p class="note">{$_('sources.sourcesHint')}</p>
<label class="row">
<span class="row-label">{$_('sources.macs')}</span>
<input class="mono" placeholder="aa:bb:cc:dd:ee:ff, 04:03:12:*" bind:value={macsText} on:input={retry} />
</label>
<p class="note warn">{$_('sources.macsHint')}</p>
{#if knownMAC && !parseList(macsText).length}
<button class="link" on:click={useMAC}>{$_('sources.useMac', { values: { mac: knownMAC } })}</button>
{/if}
<div class="line">
<label class="row grow">
<span class="row-label">{$_('sources.hostnames')}</span>
Expand Down Expand Up @@ -396,6 +424,14 @@
padding: 12px 16px; border-top: 1px solid var(--border-color);
}
.spacer { flex: 1; }
.note.warn {
padding: 6px 8px; border-left: 3px solid var(--warning); background: var(--warning-bg);
color: var(--text-primary); font-size: 11px;
}
.link {
align-self: flex-start; background: none; border: none; padding: 0;
color: var(--accent); font-size: 11px; cursor: pointer; text-decoration: underline;
}
.suggest {
display: flex; align-items: center; gap: 10px; padding: 8px 10px; font-size: 12px;
border: 1px solid var(--accent); border-radius: 6px; background: var(--accent-bg);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/SourceRules.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
/** What a rule matches, as one line. */
function who(r: SourceRule): string {
const m = r.match;
return [listText(m.sources), listText(m.hostnames), listText(m.appNames), m.pattern ? `/${m.pattern}/` : '']
return [listText(m.sources), listText(m.macs), listText(m.hostnames), listText(m.appNames), m.pattern ? `/${m.pattern}/` : '']
.filter(Boolean).join(' · ');
}

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lib/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,9 @@
"which": "Welche Meldungen",
"sources": "Adressen",
"sourcesHint": "Jede Adresse wird vollständig verglichen: eine Adresse (10.0.0.9), ein CIDR-Bereich (10.0.1.0/24) oder ein Muster, in dem * für beliebige Zeichen und ? für ein Zeichen steht (10.0.2.*).",
"macs": "MAC-Adressen",
"macsHint": "Nur im lokalen Netz. Eine MAC ist nur für Geräte in den eigenen Netzen dieses Computers bekannt, aus seiner Nachbartabelle (alle 30 Sekunden gelesen). Hinter einem Router kommt jedes Gerät mit der MAC des Routers an: Eine Regel nach MAC trifft es nie — nennen Sie stattdessen seine Adresse. Ein Muster erfasst die Geräte eines Herstellers: 04:03:12:*.",
"useMac": "Seine MAC, {mac}, statt seiner Adresse nennen — die Regel folgt ihm, wenn sich seine Adresse ändert",
"hostnames": "Hostnamen",
"appNames": "Anwendungen",
"pattern": "Rohtext enthält",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,9 @@
"which": "Which messages",
"sources": "Addresses",
"sourcesHint": "Each address is matched whole: one address (10.0.0.9), a CIDR range (10.0.1.0/24), or a pattern where * stands for any characters and ? for one (10.0.2.*).",
"macs": "MAC addresses",
"macsHint": "Local network only. A MAC is known only for the devices on this computer’s own networks, from its neighbor table (read every 30 seconds). Behind a router, every device arrives with the router’s MAC: a rule by MAC never matches it — name its address instead. A pattern takes a maker’s devices: 04:03:12:*.",
"useMac": "Name its MAC, {mac}, instead of its address — the rule follows it when its address changes",
"hostnames": "Hostnames",
"appNames": "Applications",
"pattern": "Raw text contains",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lib/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,9 @@
"which": "Qué mensajes",
"sources": "Direcciones",
"sourcesHint": "Cada dirección se compara entera: una dirección (10.0.0.9), un rango CIDR (10.0.1.0/24) o un patrón donde * sustituye a cualquier secuencia de caracteres y ? a uno solo (10.0.2.*).",
"macs": "Direcciones MAC",
"macsHint": "Solo red local. Una MAC solo se conoce para los dispositivos de las propias redes de este equipo, por su tabla de vecinos (leída cada 30 segundos). Tras un router, todos los dispositivos llegan con la MAC del router: una regla por MAC nunca los reconoce — indique su dirección. Un patrón abarca los dispositivos de un fabricante: 04:03:12:*.",
"useMac": "Indicar su MAC, {mac}, en lugar de su dirección — la regla lo sigue cuando su dirección cambia",
"hostnames": "Nombres de host",
"appNames": "Aplicaciones",
"pattern": "El texto bruto contiene",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lib/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,9 @@
"which": "Quels messages",
"sources": "Adresses",
"sourcesHint": "Chaque adresse est comparée en entier : une adresse (10.0.0.9), une plage CIDR (10.0.1.0/24), ou un motif où * remplace n'importe quels caractères et ? un seul (10.0.2.*).",
"macs": "Adresses MAC",
"macsHint": "Réseau local uniquement. Une MAC n’est connue que pour les appareils des propres réseaux de cet ordinateur, d’après sa table de voisinage (lue toutes les 30 secondes). Derrière un routeur, tous les appareils arrivent avec la MAC du routeur : une règle par MAC ne les reconnaît jamais — visez plutôt leur adresse. Un motif désigne les appareils d’un fabricant : 04:03:12:*.",
"useMac": "Viser sa MAC, {mac}, plutôt que son adresse — la règle le suit quand son adresse change",
"hostnames": "Noms d’hôte",
"appNames": "Applications",
"pattern": "Le texte brut contient",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lib/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,9 @@
"which": "Quali messaggi",
"sources": "Indirizzi",
"sourcesHint": "Ogni indirizzo viene confrontato per intero: un indirizzo (10.0.0.9), un intervallo CIDR (10.0.1.0/24) o un modello in cui * sostituisce qualsiasi sequenza di caratteri e ? uno solo (10.0.2.*).",
"macs": "Indirizzi MAC",
"macsHint": "Solo rete locale. Un MAC è noto solo per i dispositivi delle reti di questo computer, dalla sua tabella dei vicini (letta ogni 30 secondi). Dietro un router, ogni dispositivo arriva con il MAC del router: una regola per MAC non lo riconosce mai — indica invece il suo indirizzo. Un modello comprende i dispositivi di un produttore: 04:03:12:*.",
"useMac": "Indicare il suo MAC, {mac}, invece dell’indirizzo — la regola lo segue quando l’indirizzo cambia",
"hostnames": "Nomi host",
"appNames": "Applicazioni",
"pattern": "Il testo grezzo contiene",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lib/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,9 @@
"which": "対象のメッセージ",
"sources": "アドレス",
"sourcesHint": "各アドレスは全体で照合されます: 1 つのアドレス (10.0.0.9)、CIDR 範囲 (10.0.1.0/24)、または * が任意の文字列、? が 1 文字を表すパターン (10.0.2.*)。",
"macs": "MAC アドレス",
"macsHint": "ローカルネットワークのみ。MAC はこのコンピューター自身のネットワーク上のデバイスについてのみ、近隣テーブル (30 秒ごとに読み取り) から判明します。ルーターの先にあるデバイスはすべてルーターの MAC で届くため、MAC によるルールには一致しません。代わりにアドレスを指定してください。パターンでメーカーのデバイスを指定できます: 04:03:12:*。",
"useMac": "アドレスの代わりに MAC ({mac}) を指定 — アドレスが変わってもルールが追従します",
"hostnames": "ホスト名",
"appNames": "アプリケーション",
"pattern": "生テキストに含む",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lib/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,9 @@
"which": "Que mensagens",
"sources": "Endereços",
"sourcesHint": "Cada endereço é comparado por inteiro: um endereço (10.0.0.9), um intervalo CIDR (10.0.1.0/24) ou um padrão em que * substitui quaisquer caracteres e ? um só (10.0.2.*).",
"macs": "Endereços MAC",
"macsHint": "Só rede local. Um MAC só é conhecido para os dispositivos das próprias redes deste computador, pela sua tabela de vizinhos (lida a cada 30 segundos). Atrás de um router, todos os dispositivos chegam com o MAC do router: uma regra por MAC nunca os reconhece — indique o endereço. Um padrão abrange os dispositivos de um fabricante: 04:03:12:*.",
"useMac": "Indicar o seu MAC, {mac}, em vez do endereço — a regra segue-o quando o endereço muda",
"hostnames": "Nomes de host",
"appNames": "Aplicações",
"pattern": "O texto bruto contém",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lib/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,9 @@
"which": "哪些消息",
"sources": "地址",
"sourcesHint": "每个地址都按整体匹配:单个地址 (10.0.0.9)、CIDR 范围 (10.0.1.0/24),或用 * 代表任意字符、? 代表一个字符的模式 (10.0.2.*)。",
"macs": "MAC 地址",
"macsHint": "仅限局域网。只有本机所在网络中的设备才能从邻居表(每 30 秒读取一次)得知 MAC。路由器之后的设备都以路由器的 MAC 到达:按 MAC 的规则永远不会匹配它们——请改用其地址。模式可指定某制造商的设备:04:03:12:*。",
"useMac": "改用其 MAC({mac})而不是地址——地址变化时规则仍会跟随",
"hostnames": "主机名",
"appNames": "应用",
"pattern": "原始文本包含",
Expand Down
24 changes: 22 additions & 2 deletions frontend/src/lib/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import type { SyslogMessage } from './stores';
export interface SourceMatch {
/** Addresses messages arrive from: an IP, a CIDR, or a pattern with "*". */
sources?: string[];
/** MAC addresses, whole or a pattern ("04:03:12:*"): on the local network only. */
macs?: string[];
/** Globs, matched against the message as read before the rule. */
hostnames?: string[];
appNames?: string[];
Expand Down Expand Up @@ -248,14 +250,32 @@ export function namesAddress(rule: SourceRule, ip: string): boolean {
return (rule.match.sources || []).some(s => s.trim() !== '' && addressMatcher(s, false)(ip));
}

/** A MAC as the engine writes it — lowercase, colon-separated — from any spelling; '' if it is none. */
export function normalizeMAC(s: string): string {
const hex = s.trim().toLowerCase().replace(/[:.-]/g, '');
return /^[0-9a-f]{12}$/.test(hex) ? hex.match(/../g)!.join(':') : '';
}

/** Whether one of a rule's MACs takes mac, as the engine reads them (sources.macEntry). */
export function namesMAC(rule: SourceRule, mac?: string): boolean {
if (!mac) return false;
return (rule.match.macs || []).some(e => {
const t = e.trim().toLowerCase();
if (!t) return false;
if (/[*?]/.test(t)) return addressMatcher(t.replace(/-/g, ':'), false)(mac);
return normalizeMAC(t) === mac;
});
}

/**
* The rule that already reads a device by its address — the first in order,
* as the engine tries them; a rule turned off only when none is on. A CIDR
* or a pattern counts: a new rule for one of its devices, added after it,
* would never be reached.
*/
export function ruleForSource(rules: SourceRule[], ip: string): SourceRule | undefined {
return rules.find(r => r.enabled && namesAddress(r, ip)) ?? rules.find(r => namesAddress(r, ip));
export function ruleForSource(rules: SourceRule[], ip: string, mac?: string): SourceRule | undefined {
const names = (r: SourceRule) => namesAddress(r, ip) || namesMAC(r, mac);
return rules.find(r => r.enabled && names(r)) ?? rules.find(names);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions frontend/wailsjs/go/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,7 @@ export namespace models {
}
export class SourceMatch {
sources?: string[];
macs?: string[];
hostnames?: string[];
appNames?: string[];
pattern?: string;
Expand All @@ -1017,6 +1018,7 @@ export namespace models {
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.sources = source["sources"];
this.macs = source["macs"];
this.hostnames = source["hostnames"];
this.appNames = source["appNames"];
this.pattern = source["pattern"];
Expand Down
1 change: 1 addition & 0 deletions internal/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func (e *Engine) Open(emitter event.EventEmitter) error {
e.inventory = sources.NewInventory()
e.reader.SetInventory(e.inventory)
e.neighbors = neighbors.NewCache()
e.reader.SetMACLookup(e.neighbors.MAC)
e.stopNeighbors = make(chan struct{})
go e.watchNeighbors(e.stopNeighbors)
e.server.SetReader(e.reader.Read)
Expand Down
3 changes: 3 additions & 0 deletions internal/engine/reread.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ func (e *Engine) RereadHistory(opts models.RereadOptions) error {
// A rule that does not compile is left out, as when it is applied
// to what arrives.
_ = reader.SetRules(opts.Rules)
if lookup := e.macLookup(); lookup != nil {
reader.SetMACLookup(lookup)
}
}
var since time.Time
if opts.Since != "" {
Expand Down
Loading