In seiner relativ kurzen Geschichte hat die Programmiersprache Rust außergewöhnliche Anerkennung erlangt und ein umfangreiches, ausgereiftes Ökosystem hervorgebracht. Sowohl Rust als auch Cargo (sein Build-System, seine Toolchain-Schnittstelle und sein Paketmanager) zählen zu den geschätzten und gefragten Technologien, wobei Rust in den Top 20 der Programmiersprachen in RedMonks Rangliste der Programmiersprachen fest etabliert ist. Darüber hinaus weisen Projekte, die Rust einsetzen, häufig eine höhere Stabilität und weniger sicherheitsrelevante Programmierfehler auf (so berichten etwa Android-Entwickler in einer überzeugenden Erfolgsgeschichte von deutlichen Verbesserungen).
F5 verfolgt diese Entwicklungen rund um Rust und seine Community von Rustaceans schon seit einiger Zeit mit Begeisterung. Wir haben dies zum Anlass genommen, uns aktiv für die Sprache einzusetzen, ebenso wie für ihre Toolchain und ihre künftige Verbreitung.
Bei NGINX gehen wir nun selbst ins Risiko, um den Wünschen und Anforderungen von Entwicklern in einer zunehmend digitalen und sicherheitsbewussten Welt gerecht zu werden. Wir freuen uns, das ngx-rust-Projekt ankündigen zu können – eine neue Möglichkeit, NGINX-Module in der Programmiersprache Rust zu schreiben. Rustaceans, das ist für euch!
Ein kurzer Überblick über die Geschichte von NGINX und Rust
Wer NGINX und unsere Aktivitäten auf GitHub aufmerksam verfolgt, weiß vielleicht, dass dies nicht unser erster Anlauf mit Rust-basierten Modulen ist. In den Anfangsjahren von Kubernetes und den frühen Tagen von Service Mesh entstanden einige Arbeiten rund um Rust, die den Grundstein für das Projekt ngx-rust legten.
Ursprünglich diente ngx-rust dazu, die Entwicklung eines Istio-kompatiblen Service-Mesh-Produkts mit NGINX zu beschleunigen. Nach der Entwicklung des ersten Prototyps blieb dieses Projekt viele Jahre lang unverändert. In dieser Zeit erstellten viele Mitglieder der Community Forks des Repositorys oder entwickelten Projekte, die von den ursprünglichen Beispielen für Rust-Bindings in ngx-rust inspiriert waren.
Einige Zeit später musste unser F5 Distributed Cloud Bot Defense-Team NGINX-Proxys in seine Schutzdienste integrieren. Dafür musste ein neues Modul entwickelt werden.
Außerdem wollten wir unser Rust-Portfolio weiter ausbauen, gleichzeitig die Entwicklererfahrung verbessern und den sich wandelnden Anforderungen unserer Kunden gerecht werden. Daher nutzten wir unsere internen Förderprogramme für Innovationen und arbeiteten mit dem ursprünglichen Autor von ngx-rust zusammen, um ein neues und verbessertes Projekt für Rust-Bindings zu entwickeln. Nach einer langen Pause nahmen wir die Veröffentlichung von ngx-rust-Crates wieder auf – mit einer erweiterten Dokumentation und Verbesserungen an der Build-Ergonomie für die Nutzung durch die Community.
Was bedeutet das für NGINX?
Module sind die zentralen Bausteine von NGINX und stellen den Großteil seiner Funktionalität bereit. Zugleich bieten sie NGINX-Benutzern die leistungsfähigste Möglichkeit, diese Funktionalität anzupassen und Unterstützung für bestimmte Anwendungsfälle zu implementieren.
Traditionell unterstützte NGINX ausschließlich in C geschriebene Module (da das Projekt selbst in C geschrieben ist, war die Unterstützung von Modulanbindungen in der Hostsprache eine naheliegende und einfache Entscheidung). Fortschritte in der Informatik und der Programmiersprachentheorie haben jedoch frühere Paradigmen verbessert, insbesondere im Hinblick auf Speichersicherheit und Korrektheit. Dies hat den Weg für Sprachen wie Rust geebnet, die nun für die Entwicklung von NGINX-Modulen eingesetzt werden können.
Erste Schritte mit ngx-rust
Nachdem wir nun einen Teil der Geschichte von NGINX und Rust behandelt haben, beginnen wir mit der Entwicklung eines Moduls. Sie können den Quellcode selbst kompilieren und Ihr Modul lokal entwickeln, den Quellcode von ngx-rust abrufen und dabei helfen, bessere Bindings zu erstellen, oder einfach das Crate von crates.io beziehen.
Die ngx-rust README enthält Richtlinien für Beiträge und die Anforderungen für lokale Builds, die Ihnen den Einstieg erleichtern. Das Projekt befindet sich noch in einer frühen Entwicklungsphase, aber mithilfe der Community möchten wir die Qualität verbessern und den Funktionsumfang erweitern. In diesem Tutorial konzentrieren wir uns auf die Erstellung eines einfachen, eigenständigen Moduls. Weitere, komplexere Anleitungen finden Sie auch in den ngx-rust examples.
Die Bindings sind in zwei Crates organisiert:
nginx-sysist eine Crate, die Bindings aus dem NGINX-Quellcode generiert. Die Datei lädt den NGINX-Quellcode sowie Abhängigkeiten herunter und verwendet die Codeautomatisierung vonbindgen, um die Bindings für das Foreign Function Interface (FFI) zu erstellen.- ngx ist das zentrale Crate, das Rust-Verbindungscode und APIs implementiert sowie nginx-sys reexportiert. Modulentwickler importieren diese Symbole und interagieren darüber mit NGINX. Durch den Reexport von nginx-sys muss dieses Crate nicht explizit importiert werden.
Mit den folgenden Anweisungen wird ein grundlegender Arbeitsbereich eingerichtet. Erstellen Sie zunächst ein Arbeitsverzeichnis und initialisieren Sie das Rust-Projekt:
cd $YOUR_DEV_ARENA
mkdir ngx-rust-howto
cd ngx-rust-howto
cargo init --lib
Öffnen Sie als Nächstes die Datei Cargo.toml und fügen Sie den folgenden Abschnitt hinzu:
[lib]
crate-type = ["cdylib"]
[dependencies]
ngx = "0.3.0-beta"
Alternativ können Sie das fertige Modul aus Git klonen, wenn Sie es sich beim Lesen ansehen möchten:
cd $YOUR_DEV_ARENA
git clone git@github.com:f5yacobucci/ngx-rust-howto.git
Damit sind Sie bereit, mit der Entwicklung Ihres ersten NGINX-Moduls in Rust zu beginnen. Struktur, Semantik und allgemeine Vorgehensweise beim Erstellen eines Moduls unterscheiden sich nicht wesentlich von dem, was bei der Verwendung von C erforderlich ist. Zunächst verfolgen wir einen iterativen Ansatz, um NGINX-Bindings bereitzustellen, sie zu generieren, nutzbar zu machen und Entwicklern an die Hand zu geben, damit sie ihre innovativen Ideen umsetzen können. Künftig werden wir daran arbeiten, die Entwicklung mit Rust weiter zu verbessern und idiomatischer zu gestalten.
Das bedeutet, dass Sie zunächst Ihr Modul zusammen mit allen Direktiven, Kontexten und sonstigen Komponenten erstellen müssen, die für die Installation und Ausführung in NGINX erforderlich sind. Ihr Modul wird ein einfacher Handler sein, der eine Anfrage abhängig von der HTTP-Methode annehmen oder ablehnen kann. Außerdem erstellt es eine neue Direktive, die ein einzelnes Argument akzeptiert. Wir erläutern dies Schritt für Schritt; den vollständigen Code finden Sie jedoch im Repository ngx-rust-howto auf GitHub.
Hinweis: Dieser Blogbeitrag konzentriert sich auf die Besonderheiten von Rust und nicht auf die allgemeine Entwicklung von NGINX-Modulen. Wenn Sie andere NGINX-Module entwickeln möchten, finden Sie in der Community zahlreiche hervorragende Beiträge zu diesem Thema. Diese vermitteln Ihnen auch ein grundlegenderes Verständnis davon, wie sich NGINX erweitern lässt (weitere Informationen finden Sie im nachfolgenden Abschnitt „Ressourcen“).
Modulregistrierung
Sie können Ihr Rust-Modul erstellen, indem Sie das HTTPModule-Trait implementieren, das alle NGINX-Einstiegspunkte (postconfiguration, preconfiguration, create_main_conf usw.) definiert. Modulentwickler müssen nur die für ihre jeweilige Aufgabe erforderlichen Funktionen implementieren. Dieses Modul implementiert die postconfiguration-Methode, um seinen Request-Handler zu registrieren.
Hinweis: Wenn Sie das Repository ngx-rust-howto noch nicht geklont haben, können Sie mit der Bearbeitung der von src/lib.rs erstellten Datei cargo init beginnen.
struct Module;
impl http::HTTPModule for Module {
type MainConf = ();
type SrvConf = ();
type LocConf = ModuleConfig;
unsafe extern "C" fn postconfiguration(cf: *mut ngx_conf_t) -> ngx_int_t {
let htcf = http::ngx_http_conf_get_module_main_conf(cf, &ngx_http_core_module);
let h = ngx_array_push(
&mut (*htcf).phases[ngx_http_phases_NGX_HTTP_ACCESS_PHASE as usize].handlers,
) as *mut ngx_http_handler_pt;
if h.is_null() {
return core::Status::NGX_ERROR.into();
}
// set an Access phase handler
*h = Some(howto_access_handler);
core::Status::NGX_OK.into()
}
} Das Rust-Modul benötigt lediglich einen postconfiguration-Hook in der Zugriffsphase NGX_HTTP_ACCESS_PHASE. Module können Handler für verschiedene Phasen der HTTP-Anfrage registrieren. Weitere Informationen hierzu finden Sie im Entwicklungsleitfaden.
Sie sehen, dass der Phasen-Handler „howto_access_handler“ unmittelbar vor der Rückgabe der Funktion hinzugefügt wird. Darauf kommen wir später zurück. Merken Sie sich zunächst nur, dass diese Funktion die Verarbeitungslogik innerhalb der Anfragekette ausführt.
Je nach Typ und Anforderungen Ihres Moduls stehen die folgenden Registrierungs-Hooks zur Verfügung:
Vorkonfigurationpostconfigurationcreate_main_confinit_main_confcreate_srv_confmerge_srv_confcreate_loc_confmerge_loc_conf
Konfigurationsstatus
Nun ist es an der Zeit, einen Speicher für Ihr Modul zu erstellen. Diese Daten umfassen alle erforderlichen Konfigurationsparameter sowie den internen Zustand, der zur Verarbeitung von Anfragen oder zur Änderung des Verhaltens verwendet wird. Im Grunde können alle Informationen, die das Modul dauerhaft speichern muss, in Strukturen abgelegt und gespeichert werden. Dieses Rust-Modul verwendet eine ModuleConfig-Struktur auf der Ebene der Location-Konfiguration. Der Konfigurationsspeicher muss die Traits Merge and Default implementieren.
Beim Definieren Ihres Moduls im vorherigen Schritt können Sie die Typen für Ihre Haupt-, Server- und Standortkonfigurationen festlegen. Das Rust-Modul, das Sie hier entwickeln, unterstützt nur Standorte, daher wird nur der Typ LocConf festgelegt.
Um Zustands- und Konfigurationsdaten für Ihr Modul zu speichern, definieren Sie eine Struktur und implementieren Sie das Trait <code>Merge</code>:
#[derive(Debug, Default)]
struct ModuleConfig {
enabled: bool,
method: String,
}
impl http::Merge for ModuleConfig {
fn merge(&mut self, prev: &ModuleConfig) -> Result<(), MergeConfigError> {
if prev.enabled {
self.enabled = true;
}
if self.method.is_empty() {
self.method = String::from(if !prev.method.is_empty() {
&prev.method
} else {
""
});
}
if self.enabled && self.method.is_empty() {
return Err(MergeConfigError::NoValue);
}
Ok(())
}
}
ModuleConfig speichert im Feld enabled einen Ein-/Aus-Status sowie eine HTTP-Anfragemethode. Der Handler prüft diese Methode und lässt Anfragen entweder zu oder lehnt sie ab.
Sobald der Speicher definiert ist, kann Ihr Modul Direktiven und Konfigurationsregeln erstellen, die Benutzer selbst festlegen können. NGINX verwendet den Typ ngx_command_t und ein Array, um die vom Modul definierten Direktiven im Kernsystem zu registrieren.
Über die FFI-Bindings haben Entwickler von Rust-Modulen Zugriff auf den Typ ngx_command_t und können Direktiven wie in C registrieren. Das Modul ngx-rust-howto definiert eine Direktive namens howto, die einen Zeichenfolgenwert akzeptiert. In diesem Fall definieren wir einen Befehl, implementieren eine Setter-Funktion und binden diese Befehle anschließend (im nächsten Abschnitt) in das Kernsystem ein. Denken Sie daran, Ihr Befehlsarray mit dem bereitgestellten Makro ngx_command_null! abzuschließen.
So erstellen Sie mithilfe von NGINX-Befehlen eine einfache Direktive:
#[no_mangle]
static mut ngx_http_howto_commands: [ngx_command_t; 2] = [
ngx_command_t {
name: ngx_string!("howto"),
type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t,
set: Some(ngx_http_howto_commands_set_method),
conf: NGX_RS_HTTP_LOC_CONF_OFFSET,
offset: 0,
post: std::ptr::null_mut(),
},
ngx_null_command!(),
];
#[no_mangle]
extern "C" fn ngx_http_howto_commands_set_method(
cf: *mut ngx_conf_t,
_cmd: *mut ngx_command_t,
conf: *mut c_void,
) -> *mut c_char {
unsafe {
let conf = &mut *(conf as *mut ModuleConfig);
let args = (*(*cf).args).elts as *mut ngx_str_t;
conf.enabled = true;
conf.method = (*args.add(1)).to_string();
};
std::ptr::null_mut()
}
Einbinden des Moduls
Da Sie nun über eine Registrierungsfunktion, einen Phasen-Handler und Befehle für die Konfiguration verfügen, können Sie alles miteinander verknüpfen und die Funktionen dem Kernsystem zur Verfügung stellen. Erstellen Sie eine statische ngx_module_t-Struktur mit Verweisen auf Ihre Registrierungsfunktion(en), Phasen-Handler und Direktivenbefehle. Jedes Modul muss eine globale Variable vom Typ ngx_module_t enthalten.
Erstellen Sie anschließend einen Kontext und einen statischen Modultyp und machen Sie beide mit dem Makro ngx_modules! verfügbar. Im folgenden Beispiel sehen Sie, wie commands im Befehlsfeld festgelegt wird und der Kontext, der auf die Registrierungsfunktionen des Moduls verweist, im Feld ctx festgelegt wird. Für dieses Modul entsprechen alle anderen Felder im Wesentlichen den Standardwerten.
#[no_mangle]
static ngx_http_howto_module_ctx: ngx_http_module_t = ngx_http_module_t {
preconfiguration: Some(Module::preconfiguration),
postconfiguration: Some(Module::postconfiguration),
create_main_conf: Some(Module::create_main_conf),
init_main_conf: Some(Module::init_main_conf),
create_srv_conf: Some(Module::create_srv_conf),
merge_srv_conf: Some(Module::merge_srv_conf),
create_loc_conf: Some(Module::create_loc_conf),
merge_loc_conf: Some(Module::merge_loc_conf),
};
ngx_modules!(ngx_http_howto_module);
#[no_mangle]
pub static mut ngx_http_howto_module: ngx_module_t = ngx_module_t {
ctx_index: ngx_uint_t::max_value(),
index: ngx_uint_t::max_value(),
name: std::ptr::null_mut(),
spare0: 0,
spare1: 0,
version: nginx_version as ngx_uint_t,
signature: NGX_RS_MODULE_SIGNATURE.as_ptr() as *const c_char,
ctx: &ngx_http_howto_module_ctx as *const _ as *mut _,
commands: unsafe { &ngx_http_howto_commands[0] as *const _ as *mut _ },
type_: NGX_HTTP_MODULE as ngx_uint_t,
init_master: None,
init_module: None,
init_process: None,
init_thread: None,
exit_thread: None,
exit_process: None,
exit_master: None,
spare_hook0: 0,
spare_hook1: 0,
spare_hook2: 0,
spare_hook3: 0,
spare_hook4: 0,
spare_hook5: 0,
spare_hook6: 0,
spare_hook7: 0,
};
Damit haben Sie praktisch alle erforderlichen Schritte abgeschlossen, um ein neues Rust-Modul einzurichten und zu registrieren. Allerdings müssen Sie noch den Phasen-Handler (howto_access_handler) implementieren, der im Hook postconfiguration festgelegt wurde.
Handler
Handler werden bei jeder eingehenden Anfrage aufgerufen und übernehmen den Großteil der Arbeit Ihres Moduls. Das ngx-rust-Team hat sich vor allem auf Anfrage-Handler konzentriert, weshalb dort die meisten der ersten Verbesserungen an der Benutzerfreundlichkeit vorgenommen wurden. Während die vorherigen Einrichtungsschritte erfordern, Rust in einem C-ähnlichen Stil zu schreiben, bietet ngx-rust mehr Komfort und Hilfsfunktionen für Anfrage-Handler.
Wie im folgenden Beispiel zu sehen ist, stellt ngx-rust das Makro http_request_handler! bereit, das eine Rust-Closure akzeptiert, die mit einer Request-Instanz aufgerufen wird. Darüber hinaus bietet es Hilfsfunktionen zum Abrufen der Konfiguration und von Variablen, zum Setzen dieser Variablen sowie für den Zugriff auf den Speicher, andere NGINX-Primitiven und APIs.
Um eine Handler-Prozedur zu starten, rufen Sie das Makro auf und übergeben Sie Ihre Geschäftslogik als Rust-Closure. Prüfen Sie beim Modul ngx-rust-howto die Methode der Anfrage, damit die Verarbeitung der Anfrage fortgesetzt werden kann.
http_request_handler!(howto_access_handler, |request: &mut http::Request| {
let co = unsafe { request.get_module_loc_conf::(&ngx_http_howto_module) }; let co = co.expect("module config is none"); ngx_log_debug_http!(request, "howto module enabled called"); match co.enabled { true => { let method = request.method(); if method.as_str() == co.method { return core::Status::NGX_OK; } http::HTTPStatus::FORBIDDEN.into() } false => core::Status::NGX_OK, } }); Damit haben Sie Ihr erstes Rust-Modul fertiggestellt!
Das Repository „ngx-rust-howto“ auf GitHub enthält im Verzeichnis conf eine NGINX-Konfigurationsdatei. Sie können das Modul auch kompilieren (mit cargo build), die Modulbinärdatei zur Direktive load_module in einer lokalen nginx.conf hinzufügen und es mit einer NGINX-Instanz ausführen. Für dieses Tutorial haben wir NGINX v1.23.3 verwendet, die standardmäßig von ngx-rust unterstützte NGINX_VERSION. Achten Sie beim Erstellen und Ausführen dynamischer Module darauf, für ngx-rust-Builds dieselbe NGINX_VERSION zu verwenden wie für die NGINX-Instanz, die auf Ihrem Rechner ausgeführt wird.
Fazit
NGINX ist ein ausgereiftes Softwaresystem, in das über viele Jahre hinweg zahlreiche Funktionen und Anwendungsfälle eingeflossen sind. Es ist ein leistungsfähiger Proxy und Load Balancer sowie ein Webserver von Weltklasse. Seine Marktpräsenz ist auch auf Jahre hinaus gesichert. Das motiviert uns, seine Funktionen weiter auszubauen und unseren Benutzern neue Möglichkeiten zur Interaktion mit NGINX zu bieten. Angesichts der Beliebtheit von Rust unter Entwicklern und seiner verbesserten Sicherheitsmechanismen freuen wir uns, die Möglichkeit anzubieten, Rust zusammen mit dem besten Webserver der Welt einzusetzen.
Die lange Entwicklungsgeschichte von NGINX und sein funktionsreiches Ökosystem führen jedoch zu einer umfangreichen API, deren Möglichkeiten ngx-rust bislang nur ansatzweise erschlossen hat. Das Projekt soll durch weitere idiomatische Rust-Schnittstellen, zusätzliche Referenzmodule und eine verbesserte Benutzerfreundlichkeit bei der Modulentwicklung ausgebaut und optimiert werden.
Und hier kommen Sie ins Spiel! Das ngx-rust-Projekt steht allen offen und ist auf GitHub verfügbar. Wir freuen uns darauf, gemeinsam mit der NGINX-Community die Funktionen und die Benutzerfreundlichkeit des Moduls kontinuierlich zu verbessern. Sehen Sie es sich an und experimentieren Sie selbst mit den Bindings! Kontaktieren Sie uns gerne, erstellen Sie Issues oder PRs und tauschen Sie sich mit uns im Slack-Kanal der NGINX-Community aus.
Ressourcen
- NGINX-Entwicklungsleitfaden
- NGINX erweitern
- Evan Millers Leitfaden zur Entwicklung von NGINX-Modulen
- Tejas’ Leitfaden für NGINX-Module
- Entwicklung dynamischer Module
- Erstellen installierbarer Pakete für dynamische Module
Ähnliche Blogbeiträge
Die Zukunft von NGINX: Zurück zu unseren Open-Source-Wurzeln
Beim NGINX Sprint 2022 besinnen wir uns auf unsere Open-Source-Wurzeln. Erfahren Sie mehr über die Initiativen, die wir im kommenden Jahr verfolgen, und teilen Sie uns mit, was Sie sich wünschen.

Ein Leitfaden zur Auswahl eines Ingress-Controllers, Teil 1: Ermitteln Sie Ihre Anforderungen
Ein Ingress-Controller kann eines der leistungsstärksten Tools in Ihrem Kubernetes-Stack sein. Erfahren Sie, wie Sie Ihre Ingress-Anforderungen ermitteln, um die beste Option auszuwählen.
Bereitstellung des NGINX Ingress Controllers auf Amazon EKS: So haben wir getestet
Wir haben kürzlich den Lösungsleitfaden zum NGINX Ingress Controller um Empfehlungen zur Dimensionierung für Amazon Elastic Kubernetes Service (EKS) ergänzt. In diesem Blogbeitrag erläutern wir, wie wir diese Empfehlungen erarbeitet haben, und stellen Ihnen alle Informationen zur Verfügung, die Sie benötigen, um selbst ähnliche Tests durchzuführen.

Protokollierung des Datenverkehrs zwischen NGINX und Upstream-Servern bei CDN77
Bei der Fehlersuche bei Datenverkehrsproblemen zwischen NGINX und Upstream-Servern konnte CDN77 die benötigten Daten nur aus dem Fehlerprotokoll mit aktivierter Debug-Ausgabe beziehen, was die Leistung beeinträchtigt und riesige Dateien erzeugt. Daher entwickelte das Team ein neues benutzerdefiniertes Modul, das ausschließlich die benötigten Daten protokolliert.
Ankündigung von NGINX Plus R16
NGINX Plus R16 ist eine einheitliche, elastische Ingress- und Egress-Schicht für Anwendungen, die die Funktionen eines Load Balancers, API-Gateways und einer WAF vereint und neue Features wie clusterfähige Ratenbegrenzung, Schlüssel-Wert-Speicher, „Random with Two Choices“, verbessertes UDP-Load-Balancing und vieles mehr bietet.
5 Gründe für den Umstieg auf softwarebasiertes Load Balancing
Entdecken Sie fünf Gründe für den Wechsel zu einem softwarebasierten Load Balancer wie NGINX Plus, darunter Kosteneinsparungen, die Einführung von DevOps und eine einheitliche Lösung für alle Umgebungen.