BLOG | NGINX

Erweiterung von NGINX mit Rust (eine Alternative zu C)

NGINX-Teil-von-F5-horiz-schwarz-Typ-RGB
Matthew Yacobucci Miniaturbild
Matthew Yacobucci
Veröffentlicht am 12. Oktober 2023

Im Laufe ihrer relativ kurzen Geschichte hat die Programmiersprache Rust außergewöhnliche Anerkennung und ein reichhaltiges und ausgereiftes Ökosystem erlangt. Sowohl Rust als auch Cargo (sein Build-System, seine Toolchain-Schnittstelle und sein Paketmanager) sind in der Landschaft geschätzte und gefragte Technologien, wobei Rust in der Programmiersprachen-Rangliste von RedMonk einen festen Platz in den Top 20 der Sprachen einnimmt. Darüber hinaus weisen Projekte, die Rust einsetzen, häufig Verbesserungen bei der Stabilität und bei der Beseitigung sicherheitsrelevanter Programmierfehler auf (Android-Entwickler berichten beispielsweise von überzeugenden punktuellen Verbesserungen).

F5 beobachtet diese Entwicklungen rund um Rust und seine Rustaceans-Community seit einiger Zeit mit Spannung. Wir haben dies zur Kenntnis genommen und uns aktiv für die Sprache , ihre Toolchain und ihre künftige Einführung eingesetzt.

Bei NGINX setzen wir uns jetzt dafür ein, die Wünsche und Bedürfnisse der Entwickler in einer zunehmend digitalen und sicherheitsbewussten Welt zu erfüllen. Wir freuen uns, das Projekt ngx-rust ankündigen zu können – eine neue Möglichkeit, NGINX-Module mit der Sprache Rust zu schreiben. Rustaceans, das hier ist für euch!

Eine kurze Geschichte von NGINX und Rust

Enge Anhänger von NGINX und unserem GitHub werden möglicherweise erkennen, dass dies nicht unsere erste Inkarnation von Rust-basierten Modulen ist. In den Anfangsjahren von Kubernetes und den Anfängen von Service Mesh entstanden einige Arbeiten rund um Rust, die den Grundstein für das ngx-rust-Projekt 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. Während dieser Zeit haben viele Community-Mitglieder das Repository geforkt oder Projekte erstellt, die von den ursprünglichen Rust-Binding-Beispielen inspiriert waren, die in ngx-rust bereitgestellt wurden.

Schneller Vorlauf und unser F5 Distributed Cloud Bot Defense -Team musste NGINX-Proxys in seine Schutzdienste integrieren. Dies erforderte den Bau eines neuen Moduls.

Darüber hinaus wollten wir unser Rust-Portfolio weiter ausbauen und gleichzeitig die Entwicklererfahrung verbessern und die sich entwickelnden Bedürfnisse der Kunden erfüllen. Daher haben wir unsere internen Innovationssponsorings genutzt und mit dem ursprünglichen Autor von ngx-rust zusammengearbeitet, um ein neues und verbessertes Rust-Bindings-Projekt zu entwickeln. Nach einer langen Pause haben wir die Veröffentlichung von ngx-rust-Kisten mit erweiterter Dokumentation und Verbesserungen zur Erhöhung der Ergonomie für die Community-Nutzung wieder aufgenommen.

Was bedeutet das für NGINX?

Module sind die zentralen Bausteine von NGINX und implementieren den Großteil seiner Funktionalität. Module stellen für NGINX-Benutzer zudem die leistungsstärkste Möglichkeit dar, diese Funktionalität anzupassen und Unterstützung für bestimmte Anwendungsfälle aufzubauen.

NGINX hat traditionell nur in C geschriebene Module unterstützt (da es sich um ein in C geschriebenes Projekt handelt, war die Unterstützung von Modulbindungen in der Hostsprache eine klare und einfache Entscheidung). Fortschritte in der Informatik und der Programmiersprachentheorie haben jedoch gegenüber früheren Paradigmen Verbesserungen gebracht, insbesondere im Hinblick auf Speichersicherheit und Korrektheit. Dies hat den Weg für Sprachen wie Rust geebnet, die jetzt für die Entwicklung von NGINX-Modulen verfügbar gemacht werden können.

Erste Schritte mit ngx-rust

Nachdem wir nun einen Teil der Geschichte von NGINX und Rust abgedeckt haben, beginnen wir mit dem Erstellen eines Moduls. Sie können aus der Quelle erstellen und Ihr Modul lokal entwickeln, die NGX-Rust -Quelle abrufen und beim Erstellen besserer Bindungen helfen oder die Kiste einfach von crates.io abrufen.

Die README-Datei zu ngx-rust enthält Richtlinien zum Mitwirken und lokale Build-Anforderungen für den Einstieg. Es ist noch früh und befindet sich in der Anfangsphase der Entwicklung, aber wir möchten mit Unterstützung der Community Qualität und Funktionen verbessern. In diesem Tutorial konzentrieren wir uns auf die Erstellung eines einfachen, unabhängigen Moduls. Sie können sich für komplexere Lektionen auch die ngx-rust-Beispiele ansehen.

Die Bindungen sind in zwei Kisten organisiert:

  • nginx-sys ist eine Kiste, die Bindungen aus dem NGINX-Quellcode generiert. Die Datei lädt den NGINX-Quellcode und die Abhängigkeiten herunter und verwendet die Bindgen- Codeautomatisierung, um die FFI-Bindungen (Foreign Function Interface) zu erstellen.
  • ngx ist die Hauptkiste, die Rust-Glue-Code und APIs implementiert und nginx-sys erneut exportiert. Modulautoren importieren und interagieren mit NGINX über diese Symbole, während durch den erneuten Export von nginx-sys die Notwendigkeit eines expliziten Imports entfällt.

Die folgenden Anweisungen initialisieren einen Skelett-Arbeitsbereich. Beginnen Sie mit der Erstellung eines Arbeitsverzeichnisses 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] 
Kistentyp = ["cdylib"] 

[Abhängigkeiten] 
ngx = "0.3.0-beta"

Wenn Sie beim Mitlesen das fertige Modul sehen möchten, können Sie es alternativ von Git klonen:

cd $YOUR_DEV_ARENA 
git-Klon git@github.com:f5yacobucci/ngx-rust-howto.git

Und damit können Sie mit der Entwicklung Ihres ersten NGINX Rust-Moduls beginnen. Struktur, Semantik und allgemeine Vorgehensweise beim Erstellen eines Moduls unterscheiden sich nicht sehr von dem, was bei der Verwendung von C erforderlich ist. Vorerst haben wir uns vorgenommen, NGINX-Bindungen in einem iterativen Ansatz anzubieten, um die Bindungen zu generieren, nutzbar zu machen und sie den Entwicklern in die Hände zu geben, damit sie ihre eigenen innovativen Angebote erstellen können. In Zukunft werden wir daran arbeiten, ein besseres und idiomatischeres Rust-Erlebnis zu schaffen.

Das bedeutet, dass Ihr erster Schritt darin besteht, Ihr Modul zusammen mit allen Anweisungen, Kontexten und anderen Aspekten zu konstruieren, die für die Installation und Ausführung in NGINX erforderlich sind. Ihr Modul wird ein einfacher Handler sein, der eine Anfrage basierend auf der HTTP-Methode annehmen oder ablehnen kann, und es wird eine neue Anweisung erstellen, die ein einzelnes Argument akzeptiert. Wir werden dies schrittweise besprechen, Sie können jedoch den vollständigen Code im Repository „ngx-rust-howto“ auf GitHub einsehen.

Notiz: In diesem Blog geht es eher um die Besonderheiten von Rust und weniger um die allgemeine Erstellung von NGINX-Modulen. Wenn Sie am Erstellen anderer NGINX-Module interessiert sind, sehen Sie sich bitte die vielen hervorragenden Diskussionen in der Community an. In diesen Diskussionen erhalten Sie auch eine grundlegendere Erklärung zur Erweiterung von NGINX (weitere Informationen finden Sie weiter unten im Abschnitt „Ressourcen“ ).

Modulanmeldung

Sie können Ihr Rust-Modul erstellen, indem Sie das HTTPModule- Merkmal implementieren, das alle NGINX-Einstiegspunkte definiert ( postconfiguration , preconfiguration , create_main_conf usw.). Ein Modulautor muss nur die für seine Aufgabe erforderlichen Funktionen implementieren. Dieses Modul implementiert die Postkonfigurationsmethode, um seinen Anforderungshandler zu installieren.

Notiz: Wenn Sie das NGX-Rust-Howto- Repository nicht geklont haben, können Sie mit der Bearbeitung der von Cargo Init erstellten Datei src/lib.rs beginnen.

struct Module; 

impl http::HTTPModule für Module { 
Typ MainConf = (); 
Typ SrvConf = (); 
Typ LocConf = ModuleConfig; 

unsicheres externes "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(); 
} 

// setze einen Access-Phasenhandler 
*h = Some(howto_access_handler); 
core::Status::NGX_OK.into() 
} 
} 

Das Rust-Modul benötigt nur einen Postkonfigurations- 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 Entwicklungshandbuch .

Sie sehen, dass der Phasenhandler howto_access_handler unmittelbar vor der Rückgabe der Funktion hinzugefügt wird. Wir kommen später darauf zurück. Beachten Sie vorerst nur, dass es sich um die Funktion handelt, die die Verarbeitungslogik während der Anforderungskette ausführt.

Abhängig von Ihrem Modultyp und seinen Anforderungen sind folgende Registrierungs-Hooks verfügbar:

  • Vorkonfiguration
  • Nachkonfiguration
  • Hauptconf erstellen
  • init_main_conf
  • Erstellen_Server_Conf
  • merge_srv_conf
  • Erstellen Sie einen_Standort_conf
  • merge_loc_conf

Konfigurationsstatus

Jetzt ist es Zeit, Speicher für Ihr Modul zu erstellen. Zu diesen Daten gehören etwaige erforderliche Konfigurationsparameter oder der interne Status, der zur Verarbeitung von Anfragen oder zur Verhaltensänderung verwendet wird. Grundsätzlich können alle Informationen, die das Modul dauerhaft benötigt, in Strukturen abgelegt und gespeichert werden. Dieses Rust-Modul verwendet eine ModuleConfig- Struktur auf der Standortkonfigurationsebene. Der Konfigurationsspeicher muss die Merkmale „Merge“ und „Default“ implementieren.

Wenn Sie im obigen Schritt Ihr Modul definieren, 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 ist nur der Typ LocConf festgelegt.

Um Status- und Konfigurationsspeicher für Ihr Modul zu erstellen, definieren Sie eine Struktur und implementieren Sie das Merge -Merkmal:

#[derive(Debug, Default)] 
struct ModuleConfig { 
aktiviert: bool, 
Methode: String, 
} 

impl http::Merge für ModuleConfig { 
fn merge(&mut self, prev: &ModuleConfig) -> Ergebnis<(), 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 einen Ein/Aus-Status zusammen mit einer HTTP-Anforderungsmethode im aktivierten Feld. Der Handler prüft diese Methode und lässt Anfragen zu oder verbietet sie.

Sobald der Speicher definiert ist, kann Ihr Modul Anweisungen und Konfigurationsregeln erstellen, die die Benutzer selbst festlegen können. NGINX verwendet den Typ ngx_command_t und ein Array, um moduldefinierte Anweisungen beim Kernsystem zu registrieren.

Über die FFI-Bindungen haben Rust-Modulautoren Zugriff auf den Typ ngx_command_t und können Anweisungen wie in C registrieren. Das Modul ngx-rust-howto definiert eine Howto- Anweisung, die einen Zeichenfolgenwert akzeptiert. Für diesen Fall definieren wir einen Befehl, implementieren eine Setter-Funktion und binden diese Befehle dann (im nächsten Abschnitt) in das Kernsystem ein. Denken Sie daran, Ihr Befehlsarray mit dem bereitgestellten Makro „ngx_command_null! “ zu beenden.

So erstellen Sie eine einfache Direktive mit NGINX-Befehlen:

#[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) als ngx_uint_t, 
gesetzt: Einige (ngx_http_howto_commands_set_method), 
conf: NGX_RS_HTTP_LOC_CONF_OFFSET, 
Versatz: 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 { 
unsicher { 
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 Phasenhandler und Befehle zur Konfiguration verfügen, können Sie alles miteinander verknüpfen und die Funktionen dem Kernsystem zugänglich machen. Erstellen Sie eine statische ngx_module_t- Struktur mit Verweisen auf Ihre Registrierungsfunktion(en), Phasenhandler und Direktivbefehle. Jedes Modul muss eine globale Variable vom Typ ngx_module_t enthalten.

Erstellen Sie dann einen Kontext und einen statischen Modultyp und stellen Sie sie mit dem Makro „ngx_modules! “ bereit. Im folgenden Beispiel sehen Sie, wie Befehle im Feld „Befehle“ und der Kontext, der auf die Registrierungsfunktionen der Module verweist, im Feld „ctx“ festgelegt werden. Für dieses Modul sind alle anderen Felder tatsächlich Standardwerte.

#[no_mangle] 
static ngx_http_howto_module_ctx: ngx_http_module_t = ngx_http_module_t { 
Vorkonfiguration: Einige(Modul::Vorkonfiguration), 
Nachkonfiguration: Einige(Modul::postconfiguration), 
create_main_conf: Einige(Modul::create_main_conf), 
init_main_conf: Einige(Modul::init_main_conf), 
create_srv_conf: Einige(Modul::create_srv_conf), 
merge_srv_conf: Einige(Modul::merge_srv_conf), 
create_loc_conf: Einige(Modul::create_loc_conf), 
merge_loc_conf: Einige(Modul::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 als ngx_uint_t, 
Signatur: NGX_RS_MODULE_SIGNATURE.as_ptr() als *const c_char, 

ctx: &ngx_http_howto_module_ctx als *const _ als *mut _, 
commands: unsicher { &ngx_http_howto_commands[0] als *const _ als *mut _ }, 
type_: NGX_HTTP_MODULE als ngx_uint_t, 

init_master: Keine, 
init_module: Keine, 
init_process: Keine, 
init_thread: Keine, 
exit_thread: Keine, 
exit_process: Keine, 
exit_master: Keine, 

spare_hook0: 0, 
Ersatzhaken1: 0, 
Ersatzhaken2: 0, 
spare_hook3: 0, 
spare_hook4: 0, 
spare_hook5: 0, 
spare_hook6: 0, 
spare_hook7: 0, 
}; 

Damit haben Sie die notwendigen Schritte zum Einrichten und Registrieren eines neuen Rust-Moduls praktisch abgeschlossen. Allerdings müssen Sie noch den Phasenhandler (howto_access_handler) implementieren, der im Postkonfigurations- Hook festgelegt wurde.

Handler

Handler werden für jede eingehende Anforderung aufgerufen und führen den Großteil der Arbeit Ihres Moduls aus. Der Schwerpunkt des ngx-rust-Teams lag auf den Anforderungshandlern, und hier wurden auch die meisten anfänglichen ergonomischen Verbesserungen vorgenommen. Während die vorherigen Einrichtungsschritte das Schreiben von Rust in einem C-ähnlichen Stil erfordern, bietet ngx-rust mehr Komfort und Dienstprogramme für Anforderungshandler.

Wie im folgenden Beispiel zu sehen ist, stellt ngx-rust das Makro http_request_handler! bereit, um einen Rust-Abschluss zu akzeptieren, der mit einer Request -Instanz aufgerufen wird. Es bietet außerdem Dienstprogramme zum Abrufen von Konfigurationen und Variablen, zum Festlegen dieser Variablen und zum Zugreifen auf den Speicher, andere NGINX-Grundelemente und APIs.

Um eine Handlerprozedur zu initiieren, rufen Sie das Makro auf und stellen Sie Ihre Geschäftslogik als Rust-Closure bereit. Überprüfen Sie für das Modul „ngx-rust-howto“ die Methode der Anforderung, um die weitere Verarbeitung der Anforderung zu ermöglichen.

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("Modulkonfiguration ist keine"); 

ngx_log_debug_http!(request, "howto-Modul aktiviert aufgerufen"); 
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 abgeschlossen!

Das ngx-rust-howto- Repo auf GitHub enthält eine NGINX-Konfigurationsdatei im conf-Verzeichnis. Sie können auch erstellen (mit cargo build ), das Modul-Binärprogramm zur load_module- Direktive in einer lokalen nginx.conf hinzufügen und es mit einer Instanz von NGINX ausführen. Beim Schreiben dieses Tutorials haben wir NGINX v1.23.3 verwendet, die standardmäßige NGINX_VERSION, die von ngx-rust unterstützt wird. 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 Sie auf Ihrem Computer ausführen.

Abschluss

NGINX ist ein ausgereiftes Softwaresystem mit jahrelanger Erfahrung und integrierten Anwendungsfällen. Es ist ein leistungsfähiger Proxy, Lastenausgleich und ein erstklassiger Webserver. Seine Präsenz auf dem Markt wird in den kommenden Jahren mit Sicherheit anhalten, was uns motiviert, seine Fähigkeiten weiter auszubauen und unseren Benutzern neue Möglichkeiten der Interaktion mit ihm zu bieten. Angesichts der Beliebtheit von Rust unter Entwicklern und seiner verbesserten Sicherheitseinschränkungen freuen wir uns, die Möglichkeit bieten zu können, Rust zusammen mit dem besten Webserver der Welt zu verwenden.

Allerdings schaffen sowohl die Reife von NGINX als auch sein funktionsreiches Ökosystem eine große API-Oberfläche, und ngx-rust hat gerade erst an der Oberfläche gekratzt. Das Projekt zielt auf Verbesserungen und Erweiterungen durch das Hinzufügen weiterer idiomatischer Rust-Schnittstellen, den Aufbau zusätzlicher Referenzmodule und die Verbesserung der Ergonomie beim Schreiben von Modulen ab.

Hier kommen Sie ins Spiel! Das ngx-rust-Projekt steht allen offen und ist auf GitHub verfügbar . Wir freuen uns darauf, mit der NGINX-Community zusammenzuarbeiten, um die Funktionen und die Benutzerfreundlichkeit des Moduls weiter zu verbessern. Probieren Sie es aus und experimentieren Sie selbst mit den Bindungen! Und nehmen Sie bitte Kontakt mit uns auf, melden Sie sich bei Problemen oder PRs und interagieren Sie mit uns im Slack-Kanal der NGINX-Community .

Ressourcen


„Dieser Blogbeitrag kann auf Produkte verweisen, die nicht mehr verfügbar und/oder nicht mehr unterstützt werden. Die aktuellsten Informationen zu verfügbaren F5 NGINX-Produkten und -Lösungen finden Sie in unserer NGINX-Produktfamilie . NGINX ist jetzt Teil von F5. Alle vorherigen NGINX.com-Links werden auf ähnliche NGINX-Inhalte auf F5.com umgeleitet."