Documentation
¶
Overview ¶
Package clamav is a pure-Go client for clamd, the ClamAV daemon, designed for scanning untrusted user uploads before accepting them.
The library talks to clamd over its socket protocol (unix:// or tcp://) and streams file contents with the INSTREAM command, so clamd never needs filesystem access to the scanned data and no temporary files are involved.
Fail-closed contract ¶
This is a security control. Callers MUST apply the following rules:
- result.Infected() == true: reject (and quarantine/audit) the file.
- result.Clean() == true: the file may be accepted.
- err != nil (any error, any type): the verdict is UNKNOWN. The file must NOT be accepted. Reject it or retry; see IsRetryable.
When an error is returned the ScanResult is always the zero value, whose Verdict is VerdictUnknown; a zero ScanResult never reports Clean() true. Never treat a scan failure — including ErrSizeLimitExceeded, timeouts, and connection failures — as "no malware found".
Deployment notes ¶
The clamd protocol is unauthenticated cleartext. Connect over a unix socket, or over TCP only on loopback or an isolated private network. Never expose clamd to untrusted networks and never derive the address passed to New from request data.
The address, like the rest of the configuration, is fixed at New time. Signature freshness, StreamMaxLength, and scan limits are controlled by the clamd deployment (clamd.conf and freshclam); see docs/operations.md in the repository for hardening guidance.
Index ¶
- Constants
- Variables
- func IsRetryable(err error) bool
- type ClamdError
- type Client
- func (c *Client) Ping(ctx context.Context) error
- func (c *Client) Reload(ctx context.Context) error
- func (c *Client) Scan(ctx context.Context, r io.Reader) (ScanResult, error)
- func (c *Client) ScanBytes(ctx context.Context, data []byte) (ScanResult, error)
- func (c *Client) ScanFile(ctx context.Context, path string) (ScanResult, error)
- func (c *Client) Stats(ctx context.Context) (string, error)
- func (c *Client) Version(ctx context.Context) (string, error)
- type ConnectionError
- type DialFunc
- type Option
- type ProtocolError
- type ScanResult
- type Verdict
Examples ¶
Constants ¶
const ( // DefaultDialTimeout bounds connection establishment. DefaultDialTimeout = 10 * time.Second // DefaultIOTimeout bounds each individual read/write operation, i.e. // the maximum time the connection may make no progress at all. It is // not a whole-scan timeout; bound the total scan with the context. DefaultIOTimeout = 30 * time.Second // DefaultChunkSize is the INSTREAM chunk payload size. DefaultChunkSize = 32 << 10 // 32 KiB // DefaultMaxStreamSize is the client-side payload limit. It matches // clamd's historical StreamMaxLength default (25 MiB). Configure // WithMaxStreamSize to the StreamMaxLength of your clamd deployment. DefaultMaxStreamSize int64 = 25 << 20 // 25 MiB )
Defaults applied by New. All of them can be overridden with Options.
const NoSizeLimit int64 = -1
NoSizeLimit disables the client-side stream size limit when passed to WithMaxStreamSize. clamd's own StreamMaxLength still applies. Disabling the client-side limit is discouraged: it allows a single oversized upload to consume bandwidth and clamd resources before being rejected.
Variables ¶
var ErrSizeLimitExceeded = errors.New("clamav: stream size limit exceeded")
ErrSizeLimitExceeded reports that a payload exceeded a size limit: either the client-side limit configured with WithMaxStreamSize, or clamd's StreamMaxLength (clamd replies "INSTREAM size limit exceeded. ERROR"). Detect it with errors.Is. The error message states which side enforced the limit.
A size-limited file has NOT been scanned. Under the fail-closed contract it must be rejected, not accepted.
Functions ¶
func IsRetryable ¶
IsRetryable reports whether retrying the failed operation with the same input and a fresh context could plausibly succeed.
The library never retries by itself: an io.Reader cannot be replayed, and silent retries would double-stream large uploads. Callers that retry must re-supply the input (e.g. reopen the file) and should apply a bounded backoff.
Retryable: connection failures (ConnectionError) and deadline expiry. Not retryable: context.Canceled, ErrSizeLimitExceeded, ClamdError, ProtocolError, and anything unrecognized (conservative default).
A verdict of VerdictInfected is a scan result, not an error — it is never subject to retry.
Types ¶
type ClamdError ¶
type ClamdError struct {
// Message is the reply with the trailing " ERROR" token removed.
Message string
}
ClamdError reports that clamd itself replied with an "... ERROR" response (engine failure, size limit, malformed request, ...). Detect it with errors.As. When the underlying cause is a size limit violation the error also matches errors.Is(err, ErrSizeLimitExceeded).
func (*ClamdError) Error ¶
func (e *ClamdError) Error() string
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a clamd client. It is safe for concurrent use by multiple goroutines: configuration is immutable after New and every command runs on its own connection (clamd closes the connection after each non-session command anyway).
func New ¶
New creates a Client for the clamd instance at addr and validates the configuration. addr must use an explicit scheme:
unix:///run/clamav/clamd.sock tcp://127.0.0.1:3310
No connection is made yet; use Ping to probe reachability.
The address is part of the security configuration: it must come from deployment configuration, never from request or user input, and TCP should only point at loopback or an isolated private network (the clamd protocol is unauthenticated cleartext).
func (*Client) Ping ¶
Ping checks that clamd is reachable and answering. It is suitable as a readiness/health probe. Any reply other than PONG is an error.
Example ¶
Ping doubles as a readiness probe for the scanning dependency.
package main
import (
"context"
"fmt"
"log"
"time"
clamav "github.com/PyYoshi/go-clamav"
)
func main() {
client, err := clamav.New("tcp://127.0.0.1:3310")
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := client.Ping(ctx); err != nil {
fmt.Println("clamd not ready:", err)
}
}
Output:
func (*Client) Reload ¶
Reload asks clamd to reload its signature databases.
This is an administrative operation: signature updates are normally driven by freshclam on the clamd side, and a reload briefly increases clamd's memory and latency. Do not call it from a scanning code path.
func (*Client) Scan ¶
Scan streams r to clamd with the INSTREAM command and returns the verdict.
Fail-closed contract: when err != nil the returned ScanResult is the zero value (Verdict == VerdictUnknown) and the data MUST NOT be treated as clean. A VerdictInfected result is a successful scan, not an error.
r is consumed exactly once and is not replayed on failure; see IsRetryable for which errors are worth retrying with a fresh reader.
Example ¶
The canonical fail-closed usage: any error means the file must not be accepted — an unreachable or overloaded scanner never lets a file through.
package main
import (
"context"
"fmt"
"log"
"os"
"time"
clamav "github.com/PyYoshi/go-clamav"
)
func main() {
client, err := clamav.New("unix:///run/clamav/clamd.sock",
clamav.WithMaxStreamSize(25<<20), // keep equal to clamd's StreamMaxLength
)
if err != nil {
log.Fatal(err)
}
f, err := os.Open("/uploads/pending/document.pdf")
if err != nil {
log.Fatal(err)
}
defer f.Close()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
result, err := client.Scan(ctx, f)
switch {
case err != nil:
// UNKNOWN verdict: reject or retry (see IsRetryable). Never accept.
fmt.Println("scan failed, rejecting upload:", err)
case result.Infected():
fmt.Println("malware detected, quarantining:", result.Signature)
default:
fmt.Println("clean, accepting upload")
}
}
Output:
func (*Client) ScanBytes ¶
ScanBytes scans an in-memory payload. See Scan for the fail-closed contract.
func (*Client) ScanFile ¶
ScanFile opens path locally and streams its contents to clamd. clamd never sees the path and needs no access to the file (the path-based SCAN command is deliberately not used: it would require a shared filesystem and reintroduce time-of-check/time-of-use concerns).
Files larger than the client-side limit fail with ErrSizeLimitExceeded before any connection is made. Only regular files are accepted; the type is checked before opening (so a FIFO path cannot block the open) and re-checked race-free on the open descriptor. The file is opened only after a concurrency slot is acquired, so queued scans do not accumulate open descriptors. See Scan for the fail-closed contract.
func (*Client) Stats ¶
Stats returns clamd's multi-line STATS report (thread pool and queue state), for operational monitoring. The format is not stable across clamd versions; treat it as diagnostic text.
type ConnectionError ¶
type ConnectionError struct {
// Op is the failing operation: "dial", "read", or "write".
Op string
// Err is the underlying transport error.
Err error
}
ConnectionError reports a transport failure (dial, read, or write) while talking to clamd. Detect it with errors.As. These are usually transient (clamd restarting, socket backlog, network hiccup); see IsRetryable.
func (*ConnectionError) Error ¶
func (e *ConnectionError) Error() string
func (*ConnectionError) Unwrap ¶
func (e *ConnectionError) Unwrap() error
type DialFunc ¶
DialFunc establishes the transport connection. network is "unix" or "tcp" as derived from the address passed to New.
type Option ¶
type Option func(*config)
Option configures a Client at construction time. Configuration is validated and then immutable, which is what makes Client goroutine-safe.
func WithChunkSize ¶
WithChunkSize sets the INSTREAM chunk payload size in bytes.
func WithDialFunc ¶
WithDialFunc replaces the transport dialer. Intended for tests and for custom transports (e.g. proxies). The implementation is responsible for honoring ctx.
func WithDialTimeout ¶
WithDialTimeout bounds connection establishment. The context passed to each call still applies; the earlier deadline wins.
func WithIOTimeout ¶
WithIOTimeout bounds each individual read/write on the connection. The deadline is refreshed before every operation, so it limits the time the scan makes no progress, not the total scan duration — bound that with the context.
func WithMaxConcurrentScans ¶
WithMaxConcurrentScans caps the number of Scan/ScanBytes/ScanFile calls executing at once; further calls wait for a slot or fail when their context expires. 0 (the default) means no client-side cap.
Size this below MaxThreads in clamd.conf so a burst of uploads queues in the application instead of overloading clamd. Admin commands (Ping, Version, Stats, Reload) do not count against the cap.
func WithMaxStreamSize ¶
WithMaxStreamSize sets the client-side payload limit in bytes, or disables it with NoSizeLimit. Inputs that would exceed the limit fail with ErrSizeLimitExceeded before the excess is sent to clamd.
Set this to the same value as StreamMaxLength in your clamd.conf. If the client limit is larger than StreamMaxLength, oversized streams are still rejected — by clamd — but only after the bytes have been transferred.
type ProtocolError ¶
type ProtocolError struct {
// Command is the clamd command that was being executed, e.g. "INSTREAM".
Command string
// Response is the offending reply, bounded by the internal read limit.
Response string
}
ProtocolError reports a clamd reply that does not match the known reply grammar. Unknown replies are always errors: the library never guesses a verdict from a response it cannot classify (fail-closed).
func (*ProtocolError) Error ¶
func (e *ProtocolError) Error() string
type ScanResult ¶
type ScanResult struct {
// Verdict is the scan outcome. Never VerdictUnknown on a nil error.
Verdict Verdict
// Signature is the matched signature name (e.g. "Win.Test.EICAR_HDB-1").
// Set only when Verdict is VerdictInfected; it may be empty if clamd
// reported a detection without a usable name.
Signature string
// Raw is the verbatim clamd reply line, for logging and diagnostics.
// Do not parse it to derive a verdict; use Verdict.
Raw string
}
ScanResult is the outcome of a successfully completed scan. Methods on Scan-family functions return the zero ScanResult whenever they return a non-nil error, so its Verdict is VerdictUnknown in every error case.
func (ScanResult) Clean ¶
func (r ScanResult) Clean() bool
Clean reports whether the scan completed with no detection.
func (ScanResult) Infected ¶
func (r ScanResult) Infected() bool
Infected reports whether the scan completed with a detection.
type Verdict ¶
type Verdict uint8
Verdict is the outcome of a completed scan.
The zero value is VerdictUnknown, which is never produced by a successful scan: it exists so that a ScanResult obtained alongside a non-nil error (always the zero ScanResult) can never be mistaken for a clean verdict.
const ( // VerdictUnknown means no verdict was obtained. It is the zero value // and must never be treated as clean. VerdictUnknown Verdict = iota // VerdictClean means the scan completed and clamd found no signature. VerdictClean // VerdictInfected means the scan completed and clamd matched a signature. VerdictInfected )
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
basicscan
command
Command basicscan scans files with clamd and prints one verdict per file.
|
Command basicscan scans files with clamd and prints one verdict per file. |
|
httpupload
command
Command httpupload is a reference implementation of fail-closed upload scanning: an HTTP endpoint that accepts a file only after clamd has scanned it and returned a clean verdict.
|
Command httpupload is a reference implementation of fail-closed upload scanning: an HTTP endpoint that accepts a file only after clamd has scanned it and returned a clean verdict. |
|
mockscan
Package mockscan shows how to test code that uses go-clamav without a running clamd: define a minimal interface at your own boundary and hand it a test double (see mockscan_test.go).
|
Package mockscan shows how to test code that uses go-clamav without a running clamd: define a minimal interface at your own boundary and hand it a test double (see mockscan_test.go). |
|
internal
|
|
|
clamdtest
Package clamdtest provides a scriptable in-process fake clamd server for unit tests.
|
Package clamdtest provides a scriptable in-process fake clamd server for unit tests. |
|
proto
Package proto implements the clamd wire protocol primitives: z-format command encoding, INSTREAM chunk framing, and response parsing.
|
Package proto implements the clamd wire protocol primitives: z-format command encoding, INSTREAM chunk framing, and response parsing. |