Dateien in Go mit ClamAV auf Viren prüfen
Anwendungen, die Uploads annehmen, benötigen eine klare Regel für die Freigabe nach der Prüfung. 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 die Virenprüfung mit Go
Bewahren Sie neue Uploads in einer privaten Quarantäne auf, bis die Prüfung abgeschlossen ist. Verbindungsfehler, Zeitüberschreitungen, leere Antworten oder überschrittene Prüflimits belegen nicht, dass eine Datei sauber ist. Die Virenprüfung ist eine Schutzebene; sie kann nicht sicherstellen, dass jede Datei harmlos ist.
Wir streamen einen in der Größe begrenzten Snapshot einer vollständig hochgeladenen Datei an einen lokalen Daemon. Das Beispiel prüft weder Verzeichnisse noch löscht es Uploads oder versucht, Dateien zu reparieren. Ihre Anwendung muss den geprüften Snapshot unveränderlich halten und darf erst nach einem sauberen Prüfergebnis genau diese Bytes veröffentlichen.
Überblick über ClamAV und seine Funktionen
ClamAV bietet den Befehl clamscan, den dauerhaft laufenden Daemon
clamd und freshclam für Signaturaktualisierungen.
Mit einem Daemon muss die Signaturdatenbank nicht bei jeder Anfrage 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. Verwenden Sie einen Unix-Socket mit eingeschränkten Zugriffsrechten auf demselben Host und machen Sie clamd nicht über das öffentliche Netzwerk erreichbar. Weitere Informationen finden Sie in der ClamAV-Anleitung zur Virenprüfung.
ClamAV in Ihrer Go-Umgebung einrichten
Installieren Sie unter Debian oder Ubuntu Scanner und Daemon mit den Distributionspaketen:
sudo apt-get update
sudo apt-get install clamav clamav-daemon
sudo systemctl status clamav-freshclam
Lassen Sie die Signaturaktualisierung den ersten Download der Datenbank abschließen. Starten Sie
keinen zweiten Prozess für freshclam, solange dessen Dienst bereits die
Aktualisierungen verwaltet. Prüfen Sie die Datei clamd.conf Ihrer Distribution,
bevor Sie clamav-daemon starten oder neu starten. Verwenden Sie die unten genannten
Limits und überprüfen Sie Socket-Pfad und Gruppenberechtigungen. Auf anderen Betriebssystemen
unterscheiden sich die Dienstpfade.
sudo systemctl restart clamav-daemon
sudo systemctl status clamav-daemon
go-clamd für die Virenprüfung implementieren
Erstellen Sie ein separates Go-Modul und legen Sie die hier verwendete Client-Version fest:
mkdir scan-example
cd scan-example
go mod init example.com/scan-example
go get github.com/dutchcoders/go-clamd@v0.0.0-20170520113014-b970184f4d9e
Dies ist ein alter, kleiner Client. Sein ScanResult
hat ein Feld Path, nicht Filename.
ScanStream gibt einen Kanal und einen Fehler zurück und akzeptiert einen
Abbruchkanal. Das Schließen dieses Abbruchkanals schließt eine bestehende Verbindung. Das Paket
bietet jedoch keine vollständige, kontextabhängige Zeitbegrenzung für den Verbindungsaufbau und
sämtliche Ein- und Ausgaben.
Das Beispiel führt den Client daher in einem kurzlebigen Worker-Prozess aus. Die Go-Funktion
exec.CommandContext beendet diesen Prozess bei einer Zeitüberschreitung und wartet auf
sein Ende, wodurch Socket und Goroutinen freigegeben werden. Dafür ist pro Prüfung ein Prozess
nötig. Begrenzen Sie in einem Dienst die Anzahl gleichzeitiger Worker; starten Sie nicht
unbegrenzt Goroutinen oder Prozesse für jeden eingehenden Upload.
Praxisbeispiele und Codeausschnitte
Speichern Sie das vollständige Programm als main.go. Es nimmt eine von der
Anwendung ausgewählte reguläre Datei entgegen, liest höchstens 10 MiB plus ein Byte und streamt
diesen Snapshot. CLAMD_SOCKET ist eine Serverkonfiguration, kein Upload-Parameter.
Bei einem leeren oder mehrdeutigen Ergebnis wird die Freigabe verweigert, auch bei einer
unerwarteten zweiten Antwort. Unverarbeitete Diagnosemeldungen des Daemons erscheinen nicht in
der öffentlichen Ausgabe des Programms.
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")
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) == 2 && os.Args[1] == "--worker" {
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 Wrapper zur Zeitbegrenzung startet dieselbe ausführbare Datei im Worker-Modus. Die Exit-Codes sind 0 für sauber, 1 für infiziert, 2 für Scannerfehler und 3 für Größenlimit.
go build -o scan-example .
CLAMD_SOCKET=/var/run/clamav/clamd.ctl ./scan-example /path/to/quarantine/completed-upload
Die Zeitbegrenzung gilt für die Kommunikation mit dem Daemon, nachdem der lokale Snapshot gelesen wurde. Der Quarantänespeicher muss lokal sein und von Ihrer Anwendung kontrolliert werden. Dies ist weder eine Zeitbegrenzung für beliebige Dateien auf eingebundenen Netzlaufwerken noch eine Berechtigungsprüfung für Pfade, die Nutzer angeben.
ClamAV für den Produktivbetrieb konfigurieren
Stimmen Sie die Limits des Daemons auf das Anwendungslimit von 10 MiB ab. Die folgenden Beispiele
sind 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 Gruppe, die auch Ihre Anwendung verwendet, 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
Ressourcenverbrauch, können aber auch dazu führen, dass Inhalte ungeprüft bleiben.
AlertExceedsMax yes sorgt dafür, dass zutreffende Engine-Limits Befunde vom Typ
Heuristics.Limits.Exceeded… erzeugen. Dieses Programm meldet sie als size-limit statt als
Schadsoftware. Derselbe Status umfasst auch Limits für Rekursion und Dateianzahl.
Eine Ablehnung durch StreamMaxLength ist ein separater Protokollfehler. Sie kann
eintreffen, bevor der Client den Schreibvorgang abgeschlossen 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 geprüft wurde. Die weiteren
Formateinschränkungen und Parser-Limits von ClamAV gelten auch dann, wenn das Gesamtergebnis
sauber ist.
Halten Sie die offizielle Signaturdatenbank mit freshclam aktuell. Überwachen
Sie fehlgeschlagene Aktualisierungen, Daemon-Protokolle, Prüflatenz und Quarantänerückstand.
Starten oder laden Sie Dienste nach Konfigurationsänderungen entsprechend den Vorgaben der Pakete
Ihrer Plattform neu. Stimmen Sie die Parallelität auf den verfügbaren Arbeitsspeicher und die
CPU ab, einschließlich des Aufwands für die Dekomprimierung, statt nur auf die Anzahl der
HTTP-Anfragen.
Mit EICAR testen
EICAR veröffentlicht eine harmlose Testzeichenfolge mit 68 Bytes,
die Virenscanner absichtlich erkennen. Erzeugen Sie diese nur in einem isolierten Testverzeichnis.
Speichern Sie Folgendes 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)
}
}
go test -run '^TestEICAR$' -count=1 .
Bei einem gestoppten Daemon muss dieser Test fehlschlagen; dies darf nicht als erfolgreiche Erkennung von Schadsoftware zählen. Testen Sie außerdem 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 keine EICAR-Prüfung mit dem tatsächlichen Scanner und seiner konfigurierten Datenbank.
Bewährte Verfahren für die Virenprüfung in Go-Anwendungen
- Zuerst Quarantäne: speichern Sie abgeschlossene Uploads privat, prüfen Sie einen unveränderlichen Snapshot und geben Sie nach einem sauberen Ergebnis nur genau diese Bytes frei. Verwenden Sie kein öffentliches Upload-Verzeichnis.
- Im Fehlerfall sperren: lehnen Sie Dateien bei Scannerfehlern und erreichten Limits ab oder halten Sie sie zurück. Wiederholen Sie die Prüfung über eine begrenzte Warteschlange mit zunehmenden Wartezeiten; ein ausgeschöpftes Wiederholungsbudget darf die Datei nicht freigeben.
- Ressourcen begrenzen: begrenzen Sie Upload-Größe, gleichzeitige Worker-Prozesse, Archivtiefe, entpackte Bytes, Prüfdauer und Warteschlangenlänge. Überwachen Sie jede Art abgelehnter Prüfung.
- Scanner isolieren: verwenden Sie einen lokalen Socket mit eingeschränktem Zugriff und ein eigenes Dienstkonto. Die Anwendung streamt Bytes, sodass der Daemon keinen direkten Zugriff auf die Quarantänedateien benötigt.
- Nützliche Statusinformationen bewahren: geben Sie stabile Prüfergebnisse an Aufrufer zurück; bewahren Sie detaillierte Diagnosen in zugriffsgeschützten Protokollen auf. Behandeln Sie nicht jeden zurückgegebenen Fehler als „Virus gefunden“.
- Abhängigkeit überprüfen: der Client in der festgelegten Version ist alt und meldet Fehler bei Ein- und Ausgaben nur eingeschränkt. Die Prozessisolierung ermöglicht hier den Abbruch, macht daraus jedoch keine moderne, kontextabhängige API.
Häufige Probleme beheben
- Scannerfehler: prüfen Sie Socket-Berechtigungen, Datenbankverfügbarkeit, Zustand des Daemons und Protokolle. Stellen Sie sicher, dass der konfigurierte Socket mit dem von Go verwendeten übereinstimmt. Machen Sie den Socket nicht öffentlich zugänglich.
- Zeitüberschreitungen: prüfen Sie die Warteschlangenlänge und die Ressourcen des Daemons, bevor Sie das Budget von fünf Sekunden erhöhen. Eine Zeitüberschreitung 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: überprüfen Sie die Fixture mit 68 Bytes, die geladene Signaturdatenbank und das tatsächliche Prüfergebnis. Ein Verbindungsfehler ist kein positiver Nachweis.
- Unerwartet saubere Ergebnisse: überprüfen Sie
HeuristicAlertsundAlertExceedsMax, sehen Sie die unterstützten Formate und Prüflimits durch und stellen Sie sicher, dass Sie die tatsächlich geprüften Bytes veröffentlichen.
Fazit und weiterführende Ressourcen
ClamAV kann eine Go-Upload-Pipeline um eine nützliche Prüfung auf Schadsoftware ergänzen, wenn seine Fehlerzustände explizit definiert sind. Anwendung, Daemon-Konfiguration und Freigaberegeln müssen darin übereinstimmen, was eine erfolgreiche Prüfung bedeutet. Nutzen Sie beim Anpassen dieses Beispiels die Primärquellen:
Der Robot 🤖 /file/virusscan
von Transloadit nutzt ClamAV mit täglichen Signaturaktualisierungen 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. Ziehen Sie die Robot-Dokumentation heran, wenn Sie
dieses Verhalten festlegen.
