Antivirus scanning
Hand files to your own ICAP engine, keep what it said, and refuse flagged content to everyone outside your network.
MeshHold can hand your files to an antivirus engine and keep a record of what it said: which engine, running which signature database, at what time. Two nodes running two different engines both end up holding both answers.
It is off by default, it needs a scanner you supply, and it never stands between you and your own files.
TL;DR
- You supply the engine. MeshHold ships the client half of ICAP (RFC 3507) and nothing else. The scanner is something you already run: Kaspersky Scan Engine, Dr.Web, ESET, Bitdefender, or c-icap in front of ClamAV. There is no bundled antivirus.
- Nothing waits for a scan. Not a read, not a write, not replication. Scanning is a background job.
- Nothing is ever deleted or quarantined. A verdict is a note, not a sentence.
- One refusal: content an engine objected to is not served to people outside your network — public links, federated shares, and content pushed to a gateway. Your own reads are never affected. That refusal has an off switch.
- Verdicts replicate to your trusted nodes, which is what makes a second engine on a second machine worth having.
What this is, and what it is not
It is provenance. "ClamAV 1.4.2, database 27301, on 25 August, said this file was clean." That is a fact about the past. It is not a promise about the file, and nothing in MeshHold treats it as one.
It is not protection, and the distinction decides the whole shape. A protection feature has to stand between you and the bytes, which means latency on every read and false positives that break real work. A provenance feature only has to be eventually correct — so it can be free at the moment it would otherwise be expensive.
And it does nothing for you if you do not already run a scanner. We ship the client; the server is somebody else's software on somebody else's machine. If standing up c-icap and ClamAV is not something you were going to do anyway, this feature has nothing to offer you, and the rest of this page is for deployments that already have an engine.
Why it is worth having at all
Your desktop antivirus already watches your downloads folder. It cannot watch a MeshHold vault, because what is on disk is ciphertext: a vault's blocks are encrypted with a key the endpoint scanner does not have. Our own encryption is what blinds it. Handing the plaintext to an engine that you chose, from the node that already holds the key, is the only place that check can happen.
The honest scale of the benefit: a commodity engine catches roughly 60% of commodity malware. A second, independent engine is worth more than any amount of tuning the first, which is why the design is built around two nodes rather than around one good scanner.
What you need
An ICAP server. Two shapes work and MeshHold cannot tell them apart:
All in one. A container running c-icap with libclamav inside it:
docker run -d --name icap -p 1344:1344 deepdiver/icap-clamav-service:latest
Your existing engine. clamd, Kaspersky Scan Engine, Sophos, ESET, Trend Micro — all of them speak ICAP, most of them on their own host. If you run clamd already, put c-icap in front of it as a protocol adapter and point MeshHold at that.
The service path is part of the endpoint and differs per server: c-icap's
ClamAV module answers on srv_clamav, Kaspersky Scan Engine on
av/respmod. Getting it wrong looks like an outage rather than a typo,
so MeshHold refuses an endpoint without one.
Your files go to that server. Use
icaps://for TLS, and keep the endpoint on a machine you control. This is the one MeshHold feature that sends plaintext somewhere else on purpose.
Turning it on
Settings → Antivirus. Two fields matter:
- Scan files with an external engine — off by default. Turning it on is what sends file contents to the server below.
- ICAP endpoint —
icap://host:1344/srv_clamav, oricaps://for TLS.
Saving runs a self-test and waits for it, so the screen tells you what the endpoint actually did rather than saying "saved". The self-test goes both ways: it sends the EICAR test file and requires a detection, then sends a harmless file and requires a pass.
That second half is not ceremony. An ICAP server with no engine behind it does not fail — it answers "clean" to everything. Not an error, not a timeout: the most reassuring possible answer, from a service that did not look. A reachability check would call that working. Only a two-way self-test catches it.
When it passes you get a line like:
detected the test file, named in X-Infection-Found; database CI0001-CBve/AJBxJQ3baWAqLDTFgAA
The database string is the server's ISTag, and it is what a re-scan compares against later.
Without a browser
The same thing from a terminal, which is the only way in on a headless node:
meshhold avscan --on --endpoint icap://host:1344/srv_clamav
meshhold avscan # what this node is doing
meshhold avscan --off
It runs the same self-test and waits for it, so what it prints afterwards is what the endpoint did rather than that a value was stored. Turning it on without an endpoint is refused rather than saved: a node that reports scanning as configured and scans nothing is worse than one that plainly does not scan.
--json for a script. The vendor options below have flags of the same
names — --engine-name, --virus-header, --max-size-mb — and
--serve-infected is the refusal, switched off.
Vendor options
Behind Show vendor options, three settings you will usually not need:
- Engine name — the slug verdicts are filed under. Worth setting for exactly one deployment shape: c-icap names itself in its banner and is a wrapper, so the engine actually answering is ClamAV and the derived name says otherwise. Set it to what is really scanning.
- Threat header — which header a threat name is read from. The self-test learns this by watching a real detection, so it should never be needed.
- Size limit — 256 MB by default. Files over it are not sent, and are counted separately from "already checked", because "we chose not to" and "we already knew" are different answers.
What gets scanned, and when
When you ask. Select files or a folder and press Scan this folder. Those jobs run ahead of everything else in the background queue — somebody pressed a button.
By itself. A sweep walks each readable vault once an hour and queues anything with no current verdict, at the lowest priority. Those jobs pause the moment the machine gets busy and resume when it goes quiet, so a library gets covered over hours or days without you noticing.
On another node. A scan is an ordinary job, so it can be placed on whichever of your nodes holds the blocks and has an engine. A laptop with no scanner can ask a VPS that has one.
Files nobody could scan — over the size limit, a format the engine refuses, an endpoint that never came up — are recorded as given up on with a reason, and are counted as unscanned. A file nothing could look at is not a clean file, and letting it look like one would be the one way to make this feature actively harmful.
Re-scanning
A file is not re-scanned while the answer would be the same. What makes
the answer different is the file changing, or the engine's database
moving — and re-reading a whole library every time freshclam runs would
be absurd, so the unit is the calendar month plus the database
version. In practice: each file gets looked at again roughly monthly,
against whatever the engine knows by then.
To force it, use Check again on a file or Re-check everything here on a folder.
Two engines on two nodes
This is the feature, not an edge case.
Point node A at ClamAV and node B at something else, under different engine names. Each scans what it can. The verdicts replicate to your trusted nodes along with the rest of a file's metadata — ratings, play counts, thumbnails — sealed with the vault key, so only nodes that already hold it ever see them.
The result: both nodes hold both answers, and a node with no scanner at all shows what the ones that do have found. Open a file's details and the Scan history section lists every engine, its database version, and when it answered.
Two engines that disagree resolve to infected. The asymmetry is deliberate: a false "something is wrong" costs a glance, a false "all clear" costs everything.
What a verdict means
It describes bytes, not names
A verdict records the content it was made about. Edit the file and the old verdict stops answering for it — the panel says "this file has changed since it was last checked" and the status goes back to unscanned. A verdict cannot outlive the bytes it describes, and a rename cannot launder one.
Threat names are not portable
The same file is Eicar-Test-Signature to one engine and
Win.Test.EICAR_HDB-1 to another. The name is a string to show a person.
What identifies an answer is the engine and its database version.
False positives
They happen, and with a ~60% engine they happen to real work. Two ways out:
- Check again on the file drops what is recorded and asks the engine afresh — what you want when the engine has since been fixed.
- Refuse flagged files can be turned off entirely (below) when a detection is blocking something you meant to publish.
There is no automatic deletion under any setting, and there is no quarantine folder. In a content-addressed store there is nothing to move: one block may back several files in several vaults. Quarantine is a mark.
What refuses, and what does not
| Out to strangers | Your own surfaces | |
|---|---|---|
| unscanned | serve | serve |
| clean | serve | serve |
| flagged | refuse | serve, with a warning in the UI |
Out to strangers means a public link (encrypted or --plain), a
federated OCM share, and content pushed to a gateway. Your own
surfaces means a mounted drive, S3, WebDAV to your own client, and the
web UI.
The line is not the protocol — an OCM share and a local mount are served by the same WebDAV code. It is who is on the other end. A refusal is right on the left because it is your node and your domain handing known malware to somebody who is not you. It is wrong on the right because you are reading your own file, and a scanner standing between you and it would be the protection feature this deliberately is not.
A refused download answers 403 with a plain sentence saying why, not a 404. A silently vanishing file reads as a broken link; a refusal should read as a decision.
The switch
Settings → Antivirus → Refuse flagged files to people outside this network. On by default. Turn it off when a false positive is blocking something you meant to publish.
It applies even on a node that does no scanning of its own, because such a node still holds verdicts replicated from the node that does — and those are exactly as good a reason not to hand a file to a stranger.
Turning it off changes what is served, not what is known: the verdict stays, the history stays, and turning it back on refuses again.
Published links
A link can be pushed onto a gateway so it keeps working while your machine sleeps. A gateway holds sealed blobs and an index of hashes — by design it does not know what file it is serving, so it cannot check a verdict even in principle.
So the check happens before anything is sent, on your node, on every publishing pass. If any file in a share is flagged, the share is withdrawn from its gateways and marked refused with a note saying why. It still works as an ordinary live link, where the refusal applies normally. What it has lost is independence from your machine.
Clear the verdict and the next pass publishes it again.
Telling recipients
By default a share page says nothing about scanning. A gateway operator can turn on one line under the title:
All 12 files checked by clamav (27301), zoo (9).
Set node.share.show_scan_status: true in the gateway's config. It is a
gateway-wide setting rather than a per-share one on purpose: per share,
the presence of the line would itself say something about the share.
Off by default for two reasons.
The reader cannot check it. Everything else on that page is verifiable — the bytes decrypt or they do not. This is your node asserting that something looked, and a stranger has no way to know whether the engine was any good or whether its database was six years old. Shown by default it reads as a guarantee. It is a note.
It describes your infrastructure. "clamav 27301" tells whoever you sent a link to what you run and how current it is. That is a reasonable thing to volunteer and an unreasonable thing to leak — which is why the line is a summary and never names a node.
A detection is named, because those files refuse to download and a page that showed the refusal without the reason would look broken rather than careful.
Coverage
The file list carries a coverage strip: Scanned 812 of 2,431 (33%).
The denominator is the vault's file count, not the number of verdicts. A percentage computed from the record alone would read 100% on a library nothing has ever looked at, which is the single most misleading thing that line could do.
Expanding it lists what objected, with a forget next to each — which is what a false positive needs, since re-running the same engine against the same database would otherwise be suppressed by the very record it is trying to correct.
What this does not claim
MeshHold is not an antivirus and does not want to be. It ships the client half of a protocol and a record of what came back.
- A clean verdict is not a clean file. It is one engine's opinion, at one moment, against one database.
- The verdict can arrive after the file was served. There is no retraction. The system marks; it does not prevent.
- A forget is local. Dropping a verdict on one node leaves your other nodes holding it, and their copy can come back. Use Check again, which forgets and re-queues in one step, rather than relying on the absence of a record.
- Nothing here inspects network traffic. No intrusion detection, no TLS interception, no DNS filtering as a security control. Modern traffic is HTTPS end to end; a filter in the middle would be code, CPU and a database in exchange for very little.
Related: Public links for how a link and a gateway work, Security & hardening for the rest of the node's defences, and Core Concepts for what a vault key is and why holding it is the permission.