Dateien in Go mit ClamAV auf Viren scannen
Anwendungen, die Uploads annehmen, brauchen eine klare Regel für die Freigabe nach dem Scan. Diese Anleitung unterscheidet mit ClamAV und Go vier Ergebnisse: sauber, infiziert, Scannerfehler und Größenlimit. Nur ein explizit sauberes Ergebnis erlaubt der Anwendung, die Verarbeitung fortzusetzen.
Einführung in Virenscans mit Go
Halten Sie neue Uploads in privater Quarantäne, bis der Scan abgeschlossen ist. Ein Verbindungsfehler, ein Timeout, eine leere Antwort oder ein überschrittenes Scanlimit belegen nicht, dass eine Datei sauber ist. Virenscans sind eine Schutzschicht; sie können nicht nachweisen, dass jede Datei harmlos ist.
Wir streamen einen größenbegrenzten Snapshot einer vollständig hochgeladenen Datei an einen lokalen Daemon. Das Beispiel scannt niemals ein Verzeichnis, löscht keinen Upload und versucht nicht, eine Datei zu reparieren. Ihre Anwendung muss den gescannten Snapshot unveränderlich halten und genau diese Bytes erst nach einem sauberen Scanergebnis veröffentlichen.
ClamAV und seine Funktionen im Überblick
ClamAV bietet den Befehl clamscan, den dauerhaft laufenden Daemon
clamd und freshclam für Signaturupdates.
Mit einem Daemon muss die Signaturdatenbank nicht für jede Anfrage neu geladen werden. Diese
Anleitung nutzt den Go-Client für das Daemon-Protokoll, nicht die separaten Bindings
go-clamav für libclamav.
Das TCP-Protokoll des Daemons bietet keine Authentifizierung. Nutzen Sie einen Unix-Socket mit beschränkten Zugriffsrechten auf demselben Host und machen Sie clamd nicht im öffentlichen Netzwerk zugänglich. Siehe die ClamAV-Anleitung für Scans.
ClamAV in Ihrer Go-Umgebung einrichten
Das ausführbare Beispiel unten wurde unter Linux mit Bash, Go 1.26.8 und ClamAV 1.5.4 getestet.
Installieren Sie Go nach der offiziellen Installationsanleitung,
falls go version nicht funktioniert. Nutzen Sie eine gepflegte ClamAV-Version
mit aktuellen Signaturen. Beachten Sie die
ClamAV-Support-Richtlinie für die Version Ihrer Distribution.
Installieren Sie unter Debian oder Ubuntu mit systemd den Scanner und Daemon aus den Paketen der
Distribution. Fügen Sie jeden Bash-Block vollständig ein: && verhindert,
dass nach einem fehlgeschlagenen Schritt der nächste ausgeführt wird.
sudo apt-get update &&
sudo apt-get install clamav clamav-daemon &&
sudo systemctl status clamav-freshclam
Warten Sie, bis das Signaturupdate den ersten Download der Datenbank abgeschlossen hat. Starten Sie
keinen zweiten Prozess von freshclam, solange dessen Dienst bereits die
Updates verwaltet. Prüfen Sie die Datei clamd.conf Ihrer Distribution, bevor Sie
clamav-daemon starten oder neu starten. Verwenden Sie die unten angegebenen
Grenzwerte und bestätigen Sie den Socket-Pfad und die Gruppenberechtigungen. Auf anderen
Betriebssystemen unterscheiden sich die Dienstpfade.
sudo systemctl restart clamav-daemon &&
sudo systemctl status clamav-daemon
go-clamd für Virenscans implementieren
Führen Sie dies in dem Verzeichnis aus, in dem Sie ein neues Projekt namens
scan-example anlegen möchten. Der Block erstellt ein separates Go-Modul und
legt die hier verwendete Client-Version fest. Ein bereits vorhandenes Verzeichnis führt zu einem
Fehler. Wählen Sie ein anderes übergeordnetes Verzeichnis, statt Ihr Projekt zu löschen. Bei Erfolg
wechselt Ihre Shell in das neue Projekt. Schlägt das Erstellen, der Verzeichniswechsel oder das
Einrichten der Abhängigkeiten fehl, bleibt sie im ursprünglichen Verzeichnis. Prüfen Sie den Fehler,
bevor Sie fortfahren.
(
mkdir scan-example &&
cd scan-example &&
GOWORK=off go mod init example.com/scan-example &&
GOWORK=off go get github.com/dutchcoders/go-clamd@v0.0.0-20170520113014-b970184f4d9e
) && cd scan-example
GOWORK=off hält dieses eigenständige Modul unabhängig von einer übergeordneten
Datei namens go.work. Verwenden Sie es auch für die Build- und Testbefehle.
Siehe die Go-Workspace-Dokumentation.
Dies ist ein alter, kleiner Client. Sein Typ
ScanResult hat ein Feld namens
Path, nicht Filename.
ScanStream gibt einen Kanal und einen Fehler zurück und akzeptiert einen
Abbruchkanal. Wird dieser Abbruchkanal geschlossen, schließt sich eine bestehende Verbindung.
Das Paket bietet jedoch keine vollständige kontextabhängige Deadline für den Verbindungsaufbau
und sämtliche Ein- und Ausgabevorgänge.
Das Beispiel führt den Client deshalb in einem kurzlebigen Worker-Prozess aus. Die Go-Funktion
exec.CommandContext beendet diesen Prozess bei einem Timeout und wartet auf sein Ende,
wodurch sein Socket und seine Goroutinen freigegeben werden. Das kostet einen Prozess pro Scan.
Begrenzen Sie in einem Dienst die Anzahl gleichzeitiger Worker. Starten Sie nicht für jeden
Upload unbegrenzt eine Goroutine oder einen Prozess.
Praxisbeispiele und Codeausschnitte
Speichern Sie das vollständige Programm als main.go. Es akzeptiert eine
reguläre Datei, die von der Anwendung ausgewählt wird, liest höchstens 10 MiB plus ein Byte und
streamt diesen Snapshot. CLAMD_SOCKET ist eine Serverkonfiguration, kein
Upload-Parameter. Fehlende Antworten, fehlerhafte Einträge oder eine zweite Antwort im Kanal des
Clients führen zur Sperre. Ungefilterte Daemon-Diagnosen erscheinen nicht in der öffentlichen
Ausgabe des Programms.
Der Client in der festgelegten Version kann eine nicht terminierte abschließende Antwort verwerfen und einen Lesefehler nach einer vollständigen Antwort verbergen. Die Worker-Deadline beendet Hänger; sie behebt keine Fehler im Antwortparser. Ein sauberes Scanergebnis bedeutet, dass der Client genau eine saubere Antwort von Ihrem vertrauenswürdigen lokalen Daemon bereitgestellt hat. Nutzen Sie einen Client mit expliziter Meldung von Transportfehlern, wenn Ihre Integration stärkere Garantien benötigt.
package main
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/dutchcoders/go-clamd"
)
type Verdict string
const (
Clean Verdict = "clean"
Infected Verdict = "infected"
ScannerError Verdict = "scanner-error"
SizeLimit Verdict = "size-limit"
maxBytes = 10 * 1024 * 1024
)
func scanStream(data []byte, socket string) Verdict {
abort := make(chan bool)
defer close(abort)
replies, err := clamd.NewClamd("unix:" + socket).ScanStream(bytes.NewReader(data), abort)
if err != nil {
return ScannerError
}
verdict := ScannerError
count := 0
for result := range replies {
count++
if result == nil || count != 1 {
return ScannerError
}
if result.Raw == "INSTREAM size limit exceeded. ERROR" ||
(result.Status == clamd.RES_FOUND &&
strings.HasPrefix(result.Description, "Heuristics.Limits.Exceeded")) {
verdict = SizeLimit
continue
}
switch {
case result.Raw == "stream: OK":
verdict = Clean
case result.Status == clamd.RES_FOUND && result.Path == "stream" && result.Description != "":
verdict = Infected
default:
verdict = ScannerError
}
}
return verdict
}
func scanFile(filePath, executable string, timeout time.Duration) Verdict {
// The caller supplies a completed, immutable file in its private quarantine directory.
info, err := os.Lstat(filePath)
if err != nil || !info.Mode().IsRegular() {
return ScannerError
}
if info.Size() > maxBytes {
return SizeLimit
}
file, err := os.Open(filePath)
if err != nil {
return ScannerError
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, maxBytes+1))
if err != nil {
return ScannerError
}
if len(data) > maxBytes {
return SizeLimit
}
socket := os.Getenv("CLAMD_SOCKET")
if socket == "" {
socket = "/var/run/clamav/clamd.ctl"
}
if !strings.HasPrefix(socket, "/") || timeout <= 0 {
return ScannerError
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
command := exec.CommandContext(ctx, executable, "--worker", "--stdin")
command.Env = []string{"CLAMD_SOCKET=" + socket}
command.Stdin = bytes.NewReader(data)
output, err := command.Output()
if err != nil || ctx.Err() != nil {
return ScannerError
}
switch verdict := Verdict(output); verdict {
case Clean, Infected, ScannerError, SizeLimit:
return verdict
default:
return ScannerError
}
}
func main() {
if len(os.Args) == 3 && os.Args[1] == "--worker" && os.Args[2] == "--stdin" {
data, err := io.ReadAll(io.LimitReader(os.Stdin, maxBytes+1))
verdict := ScannerError
if err == nil && len(data) <= maxBytes {
verdict = scanStream(data, os.Getenv("CLAMD_SOCKET"))
}
fmt.Print(verdict)
return
}
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "Usage: scan-example <quarantined-file>")
os.Exit(2)
}
executable, err := os.Executable()
if err != nil {
fmt.Println(ScannerError)
os.Exit(2)
}
verdict := scanFile(os.Args[1], executable, 5*time.Second)
fmt.Println(verdict)
switch verdict {
case Clean:
return
case Infected:
os.Exit(1)
case SizeLimit:
os.Exit(3)
default:
os.Exit(2)
}
}
Kompilieren Sie das Programm vor der Ausführung. Der Timeout-Wrapper startet dieselbe ausführbare Datei im Worker-Modus. Die Exitcodes sind 0 für sauber, 1 für infiziert, 2 für Scannerfehler und 3 für Größenlimit.
GOWORK=off go build -o scan-example . &&
CLAMD_SOCKET=/var/run/clamav/clamd.ctl ./scan-example "/path/to/quarantine/completed-upload"
Ersetzen Sie den Dateipfad durch den Pfad einer vollständig hochgeladenen Datei in Ihrem privaten
Quarantäneverzeichnis und den Socket-Pfad durch den Wert von LocalSocket Ihres
Daemons, falls er abweicht. Eine saubere Datei gibt clean aus; EICAR gibt
infected aus. Der Build muss erfolgreich sein, bevor ein Scan läuft. So kann
ein fehlgeschlagener neuer Build keine veraltete Binärdatei ausführen.
Der Timeout begrenzt die Daemon-Kommunikation, nachdem der lokale Snapshot gelesen wurde. Der Quarantänespeicher muss lokal sein und von Ihrer Anwendung kontrolliert werden. Dies ist weder eine Deadline für beliebige Dateien auf eingebundenen Netzlaufwerken noch eine Berechtigungsprüfung für Pfade, die Nutzer angeben.
ClamAV für den Produktiveinsatz konfigurieren
Stimmen Sie die Grenzwerte des Daemons auf das Anwendungslimit von 10 MiB ab. Dies sind zum Beispiel
Einstellungen für clamd.conf, keine
Shell-Befehle:
LocalSocket /var/run/clamav/clamd.ctl
LocalSocketMode 660
StreamMaxLength 10M
MaxFileSize 10M
MaxScanSize 50M
MaxRecursion 16
MaxFiles 1000
MaxThreads 4
HeuristicAlerts yes
AlertExceedsMax yes
Setzen Sie LocalSocketGroup auf die mit Ihrer Anwendung geteilte Gruppe und entfernen
Sie alle nicht benötigten TCP-Listener. Beschränken Sie den Zugriff auf das Socket-Verzeichnis auf
vertrauenswürdige Prozesse. Größen- und Archivlimits verhindern übermäßigen Aufwand, können aber
auch dazu führen, dass Inhalte ungescannt bleiben. AlertExceedsMax yes sorgt dafür, dass
entsprechende Engine-Limits Befunde vom Typ Heuristics.Limits.Exceeded… erzeugen. Dieses Programm
meldet sie als size-limit statt als Malware. Derselbe Status deckt Grenzen für Rekursion und
Dateianzahl ab.
Eine Ablehnung durch StreamMaxLength ist ein separater Protokollfehler. Sie kann
auftreten, bevor der Client das Schreiben beendet hat. In diesem Fall wird ein Verbindungs- oder
Schreibfehler als scanner-error gemeldet. Beide Zustände verhindern die Freigabe; keiner bedeutet,
dass die gesamte Datei gescannt wurde. Die anderen Formatbeschränkungen und Parsergrenzen von ClamAV
gelten auch dann, wenn das Gesamtergebnis sauber ist.
Halten Sie die offizielle Signaturdatenbank mit freshclam aktuell. Überwachen
Sie Updatefehler, Daemon-Logs, Scanlatenz und den Quarantänerückstau. Starten Sie Dienste nach
Konfigurationsänderungen neu oder laden Sie sie neu, wie es die Pakete Ihrer Plattform vorsehen.
Richten Sie die Parallelität nach verfügbarem Arbeitsspeicher und CPU aus, einschließlich des
Aufwands für die Dekomprimierung, nicht nur nach der Anzahl der HTTP-Anfragen.
Mit EICAR testen
EICAR veröffentlicht eine harmlose, 68 Byte lange Testzeichenfolge,
die Virenscanner absichtlich erkennen. Erzeugen Sie sie nur in einem isolierten Testverzeichnis.
Speichern Sie dies als main_test.go. Es verwendet die exakten EICAR-Bytes aus dem
Client-Paket und das kompilierte Programm aus dem vorherigen Schritt. Setzen Sie
CLAMD_SOCKET auf den privaten Socket Ihres Test-Daemons.
package main
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/dutchcoders/go-clamd"
)
func TestEICAR(t *testing.T) {
if len(clamd.EICAR) != 68 {
t.Fatal("EICAR fixture must contain exactly 68 bytes")
}
executable, err := filepath.Abs("scan-example")
if err != nil {
t.Fatal(err)
}
file := filepath.Join(t.TempDir(), "eicar.txt")
if err := os.WriteFile(file, clamd.EICAR, 0600); err != nil {
t.Fatal(err)
}
if verdict := scanFile(file, executable, 5*time.Second); verdict != Infected {
t.Fatalf("EICAR must be detected as infected; got %s", verdict)
}
}
GOWORK=off go test -run '^TestEICAR$' -count=1 .
Bei einem gestoppten Daemon muss dieser Test fehlschlagen; dies darf nicht als erfolgreiche Malware-Erkennung zählen. Testen Sie auch eine kleine saubere Fixture, eine zu große Fixture, fehlende Dateien, fehlerhafte oder leere Daemon-Antworten und einen Daemon, der nie antwortet. Protokoll-Fixtures können die Fehlerbehandlung deterministisch prüfen. Sie ersetzen keinen EICAR-Test mit dem tatsächlichen Scanner und seiner konfigurierten Datenbank.
Bewährte Verfahren für Virenscans in Go-Anwendungen
- Zuerst Quarantäne: Speichern Sie vollständig hochgeladene Dateien privat, scannen Sie einen unveränderlichen Snapshot und geben Sie nur genau diese Bytes nach einem sauberen Ergebnis frei. Verwenden Sie kein öffentliches Upload-Verzeichnis.
- Im Fehlerfall sperren: Lehnen Sie Dateien bei Scannerfehlern und erreichten Grenzwerten ab oder behalten Sie sie in Quarantäne. Wiederholen Sie Scans über eine begrenzte Queue mit Backoff. Ein ausgeschöpftes Retry-Budget darf nicht zur Freigabe der Datei führen.
- Ressourcen begrenzen: Begrenzen Sie Upload-Größe, gleichzeitige Worker-Prozesse, Archivtiefe, entpackte Bytes, Scandauer und Queue-Länge. Überwachen Sie jede Art von abgelehntem Scan.
- Scanner isolieren: Nutzen Sie einen zugriffsbeschränkten lokalen Socket und ein eigenes Dienstkonto. Die Anwendung streamt Bytes, daher braucht der Daemon keinen direkten Zugriff auf die Quarantänedateien.
- Aussagekräftigen Status beibehalten: Geben Sie Aufrufern stabile Scanergebnisse zurück. Bewahren Sie detaillierte Diagnosen in zugriffsgeschützten Logs auf. Behandeln Sie niemals jeden zurückgegebenen Fehler als „Virus gefunden“.
- Abhängigkeit prüfen: Der Client in der festgelegten Version ist alt und meldet Ein- und Ausgabefehler nur eingeschränkt. Prozessisolation ermöglicht hier den Abbruch, macht daraus aber keine moderne kontextabhängige API.
Häufige Probleme beheben
- Scannerfehler: Prüfen Sie Socket-Berechtigungen, Datenbankverfügbarkeit, Daemon-Zustand und Logs. Stellen Sie sicher, dass der konfigurierte Socket dem von Go verwendeten entspricht. Machen Sie den Socket nicht öffentlich zugänglich.
- Timeouts: Prüfen Sie Queue-Tiefe und Daemon-Ressourcen, bevor Sie das Zeitbudget von fünf Sekunden erhöhen. Ein Timeout beendet den Worker und verhindert die Dateifreigabe.
- Größenlimits: Vergleichen Sie das Größenlimit der Anwendung,
StreamMaxLength,MaxFileSizeund die Limits für entpackte Archive. Wenn Sie nur einen Wert erhöhen, kann ein anderes Limit weiterhin greifen. - EICAR nicht erkannt: Prüfen Sie die 68 Byte lange Fixture, die geladene Signaturdatenbank und das tatsächliche Scanergebnis. Ein Verbindungsfehler ist kein positiver Fund.
- Unerwartet saubere Ergebnisse: Bestätigen Sie
HeuristicAlertsundAlertExceedsMax, prüfen Sie unterstützte Formate und Scanlimits und stellen Sie sicher, dass Sie genau die gescannten Bytes veröffentlichen.
Fazit und weitere Ressourcen
ClamAV kann eine Go-Upload-Pipeline um einen nützlichen Malware-Scan ergänzen, wenn Fehlerzustände explizit behandelt werden. Anwendung, Daemon-Konfiguration und Freigaberichtlinie müssen darin übereinstimmen, was ein erfolgreicher Scan bedeutet. Nutzen Sie beim Anpassen dieses Beispiels die Primärquellen:
Der Robot 🤖 /file/virusscan
von Transloadit nutzt ClamAV mit täglichen Signaturupdates als Teil unseres
Dienstes zur Dateifilterung. Seine Option
error_on_decline steuert, ob eine abgelehnte Datei die Assembly stoppt oder von der
weiteren Verarbeitung ausgeschlossen wird. Lesen Sie die Robot-Dokumentation, wenn Sie dieses
Verhalten festlegen.
