ICAP server
An icap:// address on --listen serves ICAP (RFC 3507), the protocol a proxy
uses to hand an HTTP request or response to a scanner before letting it through.
It replaces a c-icap container running the virus_scan service against ClamAV:
same port, same service names, same X-Infection-Found header, so the proxy keeps
its configuration.
Run it
Section titled “Run it”exav --listen icap://0.0.0.0:1344 -d /var/lib/exav # the default servicesexav --listen icap://0.0.0.0:1344/avscan -d /var/lib/exav # only that oneICAP runs over TCP, so an icap:// address is a host:port; a socket path is
refused.
The service name is the path
Section titled “The service name is the path”An ICAP service name is the path of the URL the client is configured with, so the
icap://… line from a squid.conf works unchanged:
exav --listen icap://scanner:1344/avscan -d /var/lib/exavWith no path, exav answers on the three names a c-icap virus_scan deployment
uses: avscan, srv_clamav, virus_scan. Naming a service replaces that set.
An unknown service gets 404 ICAP Service not found. For two proxies that
disagree about the name, repeat service=:
exav --listen 'icap://0.0.0.0:1344?service=avscan&service=srv_clamav' -d /var/lib/exav(A comma would not work: --listen separates addresses with commas.)
With the clamd listener
Section titled “With the clamd listener”Each address is a listener, so the protocols combine:
| Command | What listens |
|---|---|
exav --listen clamd://0.0.0.0:3310 |
clamd only |
exav --listen icap://0.0.0.0:1344 |
ICAP only |
exav --listen clamd://0.0.0.0:3310 --listen icap://0.0.0.0:1344 |
both, in one process, over one loaded database |
One process for both replaces a c-icap + clamav pair, and loads the signature
set once instead of twice.
Under the worker pool the ICAP listener runs in a dedicated child of the supervisor, sharing the loaded database copy-on-write. It gets its own process rather than a pool slot because ICAP connections are keep-alive and long-lived, and idle proxy connections would otherwise occupy every worker. On a signature reload the child is replaced along with the workers: the old one finishes the requests in progress and closes its idle connections, and proxies reconnect to the new one. The ICAP child’s own scans run in threads, one per connection, and the kernel’s per-job limits do not apply to them (see Limits); what each scan holds is still kept inside the per-job memory, as in a worker.
ICAP served alone runs without the pool: one process, scans in threads,
--max-scan-secs refused. Signatures reload on SIGUSR2 or when the source
changes on disk, and the new database is swapped in for the next request. With
no clamd listener there is no STATS command; the scan totals are written to
stderr every --metrics-secs (300 by default).
For the container, see Docker.
Point Squid at it
Section titled “Point Squid at it”icap_enable onicap_preview_enable onicap_preview_size 4096icap_service exav_resp respmod_precache icap://127.0.0.1:1344/avscan bypass=officap_service exav_req reqmod_precache icap://127.0.0.1:1344/avscan bypass=offadaptation_access exav_resp allow alladaptation_access exav_req allow allKeep bypass=off: with bypass=on, a scanner that is down lets traffic through
unscanned.
Verdict mapping
Section titled “Verdict mapping”The mapping is keyed on the status, so a new partial condition is already mapped to the blocking side:
| Status | ICAP response | Headers |
|---|---|---|
OK |
204 No Content when the client sent Allow: 204, else 200 echoing the message back unchanged |
none |
FOUND |
200 OK with a block page |
X-Infection-Found: Type=0; Resolution=2; Threat=<signature>; |
PARTIAL |
200 OK with a block page |
X-Exav-Status: PARTIAL, X-Exav-Category: <condition>, X-Exav-Reason: <detail>, and X-Infection-Found: … Threat=Heuristics.Exav.<Condition>; under the default --icap-infection-header blocks |
ERROR (the scan failed) |
200 OK with a block page |
X-Exav-Status: ERROR, X-Exav-Reason: <detail>, and X-Infection-Found: … Threat=Heuristics.Exav.ScanError; under blocks |
The threat name for a partial block follows the category:
X-Exav-Category |
Threat name in X-Infection-Found |
|---|---|
LIMITS-EXCEEDED |
Heuristics.Exav.LimitsExceeded |
UNSCANNABLE |
Heuristics.Exav.Unscannable |
PASSWORD-PROTECTED |
Heuristics.Exav.PasswordProtected |
Every block carries X-Infection-Found, because ICAP clients come in two kinds. A
proxy acts on the message: a 200 with a replacement body stops the object
whatever the headers say. A scan wrapper acts on the headers: it greps for
X-Infection-Found and calls anything without it clean. A partial block stays
distinguishable from a database hit by its Heuristics.Exav. name (Heuristics.
is ClamAV’s prefix for a policy block, and no database ships .Exav. names) and
by X-Exav-Category. The header format is byte for byte what c-icap’s
virus_scan emits, since deployed clients parse it by hand.
The block page is an HTTP 403 Forbidden with a short HTML body naming the
signature or the condition. A blocked REQMOD works the same way: the client is
handed an HTTP response to serve instead of forwarding the request (RFC 3507’s
request satisfaction).
Blocking partial objects is the default; --partial-as
delivers them instead. A detection is never delivered.
If the header should mean a database hit only
Section titled “If the header should mean a database hit only”--icap-infection-header detections restricts X-Infection-Found to signature
matches; a partial object then blocks with the X-Exav-* headers alone.
Delivering what exav could not examine
Section titled “Delivering what exav could not examine”Blocking an object nobody could look inside is right for a proxy, but an upload
service may prefer to accept a user’s password-protected ZIP. c-icap + ClamAV make
that trade silently: virus_scan.MaxObjectSize passes an oversized object, and
ClamAV calls an encrypted archive clean unless told otherwise. In exav it is an
explicit setting. --partial-as is not ICAP-specific: the same policy decides a
one-shot scan’s exit code and a clamd reply.
exav -d /var/lib/exav --listen icap://0.0.0.0:1344 --partial-as ok # all threeexav -d /var/lib/exav --listen icap://0.0.0.0:1344 --partial-as password-protected=ok # just this oneexav -d /var/lib/exav --listen icap://0.0.0.0:1344 --partial-as limits-exceeded=ok,unscannable=ok| Value | Effect on an ICAP response |
|---|---|
partial (default) |
Blocks, with X-Exav-Status: PARTIAL and the category. |
found |
Blocks as an ordinary detection named Heuristics.*. |
error |
The same response as partial: ICAP has no exit code, which is all error changes. |
ok |
Delivered, as a clean object is (204, or 200 echoing the message), with the X-Exav-* headers still saying what was skipped. Covers any category a later exav adds. |
| a per-condition list | Only the named conditions take that value. Conditions are limits-exceeded, unscannable, password-protected. |
An unknown condition is a startup error. A delivered object is never silent: the
listener announces the policy at startup, every delivered object is logged with
its verdict and reason, and the response carries X-Exav-Status,
X-Exav-Category and X-Exav-Reason. It never carries X-Infection-Found,
which would make header-only clients block it.
Behind a script that shells out to an ICAP client
Section titled “Behind a script that shells out to an ICAP client”Some wrappers (matrix-content-scanner, for one) run c-icap-client, grep
the response and turn it into an exit code:
resp=$(c-icap-client -i "$ICAP_HOST" -p 1344 -f "$1" \ -s 'srv_clamav?allow204=on&force=on&sizelimit=off&mode=simple' -v 2>&1)echo "$resp" | grep -q "X-Infection-Found" && exit 4 # infectedexit 0 # cleanNothing needs to change to point it at exav: the service name, the per-request
options, the 204 on a clean object and X-Infection-Found on a block are what
it expects, and under the default policy an encrypted archive arrives as a block.
The setting to decide is the delivery policy, which for this client decides
whether a file is accepted:
# Like the c-icap + ClamAV container it replaces: deliver what could not be# examined, block only what matched.exav -d /var/lib/exav --listen icap://0.0.0.0:1344 --partial-as okSize ceilings
Section titled “Size ceilings”In c-icap, virus_scan.MaxObjectSize means anything larger passes unscanned.
exav has no ICAP-specific ceiling. An encapsulated body is buffered like a clamd
stream (in memory up to --spill-threshold-bytes, then in a temporary file up
to --max-spill-bytes) and scanned like a file, so the limits that apply are
the same as for every other input: --max-input-bytes (unset by default, the
clamscan --max-filesize equivalent), the spill ceilings, and
--max-object-bytes, past which the checks that parse an object whole (PE
structure, RAR, OLE) do not run (see
Streaming & memory). Past --max-input-bytes or
a spill ceiling the object is LIMITS-EXCEEDED, and past --max-object-bytes
too when one of those checks applied, unless something was found. Never quietly
clean.
Translating virus_scan.MaxObjectSize 5M usually means nothing at all: exav scans
the objects c-icap skipped. To keep the limit, --max-input-bytes 5M sets it on
every surface; to keep c-icap’s answer too, add
--partial-as limits-exceeded=ok, and exav still says so in the log and in
X-Exav-Category.
Translating a c-icap configuration
Section titled “Translating a c-icap configuration”| c-icap directive | exav setting | Default |
|---|---|---|
Port 1344 |
--listen icap://0.0.0.0:1344 |
(nothing listens without it) |
Service / ServiceAlias |
the path of the --listen address, or a repeated ?service= |
avscan, srv_clamav, virus_scan |
MaxServers x ThreadsPerChild |
?max-connections= on the address |
100 |
MaxKeepAliveRequests |
--icap-max-requests |
100 |
KeepAliveTimeout |
--icap-idle-secs |
600 |
virus_scan.MaxObjectSize’s size |
--max-input-bytes (all surfaces) |
(unset) |
virus_scan.MaxObjectSize’s pass |
--partial-as (all surfaces) |
partial (block) |
| (preview size, per service) | --icap-preview-bytes |
4096 |
| (none) | --icap-transfer-preview |
* |
| (none) | --icap-options-ttl-secs |
3600 |
| (none) | --icap-max-header-bytes |
64K |
| (none) | --icap-infection-header |
blocks |
Every flag has an EXAV_* variable spelled the same way (--icap-max-requests
reads EXAV_ICAP_MAX_REQUESTS); a flag beats the variable. Pipelining on needs
no setting: several requests per connection always work.
The service names and the connection cap go on the address, since they belong to that listener (see what belongs to one listener):
exav --listen 'icap://0.0.0.0:1344/avscan?max-connections=200' -d /var/lib/exavDraining an over-limit body
Section titled “Draining an over-limit body”When exav has its answer before the client has finished sending (the object went
past --max-input-bytes, or a budget refused it), it reads and discards the rest
before answering. Closing a socket with unread input makes the kernel send an
RST, which also discards the response just written, so the client would get no
verdict. Draining keeps the verdict and leaves the connection at the next request.
A client that never stops sending would hold a thread forever, so draining stops
at 64 MiB; past that exav answers and closes, and that client may lose the
response. The clamd listener drains INSTREAM the same way (see
size over the wire).
Caching and the ISTag
Section titled “Caching and the ISTag”ICAP clients cache results against the ISTag. exav derives it from its version
and the loaded signature set (database version, build time, signature count), so
a reload changes the tag and invalidates cached verdicts downstream. Signatures
hot-reload as in daemon mode.
What OPTIONS advertises
Section titled “What OPTIONS advertises”ICAP/1.0 200 OKServer: exav/0.0.2ISTag: "exav-ae0b21d4572e711f"Methods: REQMOD, RESPMODService: exav/0.0.2 ICAP serviceAllow: 204Max-Connections: 100Preview: 4096Options-TTL: 3600Transfer-Preview: *Connection: keep-aliveEncapsulated: null-body=0Not implemented yet
Section titled “Not implemented yet”- Trickling (
SendPercentData/StartSendPercentDataAfter): c-icap can send part of a body while scanning so a slow download does not time out. exav buffers the whole object first. - ICAP-level ACLs (
icap_access,acl): restrict access with the bind address and a firewall. - TLS (ICAPS): terminate it in front, or keep the hop on loopback or a private network.
X-Violations-Found, the older multi-violation header: exav reports one verdict per object throughX-Infection-Found/X-Exav-Category.206 Partial Content: exav returns a full replacement body instead.