====== Netzwerk-Guide ======
Aufgabenorientierter Einstieg in ''std.net'': vom ersten TCP-Client über HTTP und TLS bis zum Server mit vielen gleichzeitigen Verbindungen. Diese Seite erklärt die **Arbeitsweise** — welche Unit für welche Aufgabe, welche Capability dazugehört, welche Fehler typisch sind. Die vollständige Funktionsliste je Unit steht in der [[lyx_-_programmiersprache:units:net|std.net-Referenz]].
→ [[lyx_-_programmiersprache:units:net|std.net — Unit-Referenz]] · [[lyx_-_programmiersprache:sprache:capabilities|Capabilities]] · [[lyx_-_programmiersprache:sprache:rohspeicher|Rohspeicher: alloc, peek & poke]]
----
===== 1. Orientierung: welche Unit für welche Aufgabe? =====
^ Aufgabe ^ Unit ^ Einstiegsfunktion ^
| Ausgehende TCP-Verbindung | ''std.net.socket'' | ''TCPConnect'' |
| TCP-Server | ''std.net.socket'' | ''TCPListenerNew'' |
| UDP senden/empfangen | ''std.net.socket'' | ''UDPSocketNew'' |
| Viele Verbindungen gleichzeitig | ''std.net.epoll'' | ''EpollCreate'' |
| Namen auflösen (A, MX, TXT …) | ''std.net.dns'' | ''DNSResolveGoogle'' |
| HTTP-Anfrage | ''std.net.http'' | ''HTTPGet'', ''HTTPSend'' |
| HTTPS-Anfrage | ''std.net.https'' | ''HTTPSGet'' |
| Verschlüsselter Kanal, selbst gesteuert | ''std.net.tls'' | ''TLSInit'', ''TLSConnect'' |
| REST-API mit Basis-URL und Token | ''std.net.rest'' | ''RestClientInit'' |
| Mail versenden / abholen | ''std.net.smtp'' / ''std.net.imap'' | ''SMTPSend'' / IMAP-Session |
| Geräte abfragen (SNMP), Verzeichnis (LDAP) | ''std.net.snmp'' / ''std.net.ldap'' | — |
Faustregel: **so hoch wie möglich einsteigen.** Wer eine JSON-API anspricht, nimmt ''std.net.rest'' und nicht ''std.net.socket''. Zur Socket-Ebene steigt man ab, wenn das Protokoll nicht abgedeckt ist oder man die Verbindung selbst verwalten muss (Timeouts, Wiederverbindung, eigenes Framing).
==== Die Schichten ====
rest · smtp · imap · mqtt · ssh · snmp · ldap ← Anwendungsprotokolle
http · https · dns ← baut auf socket (+ tls)
socket · tls · epoll · types ← Transport
syscalls ← sys_socket, sys_connect, …
Importiert wird nur die oberste benötigte Unit — die Abhängigkeiten zieht der Compiler nach.
----
===== 2. Das kleinste vollständige Programm =====
Ein TCP-Client, der sich verbindet, liest und wieder aufräumt:
import std.io;
import std.net.socket;
import std.net.types;
fn main(): int64 {
var conn: TCPConn := TCPConnect(IPPack(127, 0, 0, 1), 8080);
if (conn.fd < 0) {
PrintLn("Verbindung fehlgeschlagen");
return 1;
}
var buf: int64 := BufferAlloc(4096);
var n: int64 := TCPConnRead(conn, buf, 4096);
Print("Empfangen: "); PrintLn(IntToStr(n));
BufferFree(buf, 4096);
TCPConnClose(conn);
return 0;
}
Drei Dinge, die für **alle** Netz-Programme in Lyx gelten:
- **Adressen sind ''int64''.** ''IPPack(127, 0, 0, 1)'' packt eine IPv4-Adresse in eine Zahl. Es gibt keinen Zeigertyp und keinen Adressoperator — siehe [[lyx_-_programmiersprache:sprache:rohspeicher|Rohspeicher]].
- **Puffer werden selbst besorgt und selbst freigegeben.** ''BufferAlloc''/''BufferFree'' aus ''std.net.socket'' oder ''alloc''/''free'' aus ''std.alloc''. Die Größe gehört beim Freigeben wieder dazu — Lyx führt keine versteckten Metadaten.
- **Fehler kommen als Rückgabewert, nicht als Ausnahme.** Ein negativer Wert oder ''fd < 0'' ist der Fehlerfall. Es fliegt nichts.
----
===== 3. Adressen, Puffer, Byte-Reihenfolge =====
==== IPv4-Adressen ====
var ip: int64 := IPPack(93, 184, 216, 34); // Adresse als Zahl
var a: IPAddr := IPUnpack(ip); // zurück in vier Oktette
// a.a, a.b, a.c, a.d — jeweils uint8
''IPAddr'' trägt zusätzlich ''family'' und ''port''; ''TCPConnectAddr'' nimmt eine solche Struktur direkt entgegen, wenn der Port schon eingetragen ist.
> **Rohdaten aus dem Netz sind in Netzwerk-Byte-Reihenfolge.** Wer eine IPv4-Adresse aus einer DNS-Antwort oder einem Paket-Header mit ''peek32'' liest und durch ''IPUnpack'' schickt, bekommt die Oktette **rückwärts** (''172.66.147.243'' wird zu ''243.147.66.172''). ''IPPack''/''IPUnpack'' arbeiten mit der Lyx-internen Reihenfolge, nicht mit der des Kabels. Bei Rohdaten deshalb byteweise lesen:
>
>
> var o1: int64 := peek8(daten + 0);
> var o2: int64 := peek8(daten + 1);
> var o3: int64 := peek8(daten + 2);
> var o4: int64 := peek8(daten + 3);
>
==== Puffer ====
''BufferAlloc(n)'' liefert einen Puffer, ''BufferFree(buf, n)'' gibt ihn zurück. Für alles andere ''std.alloc''. Ein Puffer wird **einmal** vor der Schleife angelegt, nicht je Durchlauf — das spart Syscalls und macht die Laufzeit vorhersagbar.
----
===== 4. Namen auflösen — std.net.dns =====
''std.net.dns'' spricht DNS selbst, ohne ''/etc/resolv.conf'' und ohne libc-Resolver. Für jeden Record-Typ gibt es drei Varianten: mit eigenem Server (''DNSResolve…''), gegen Google (''…Google'') und gegen Cloudflare (''…Cloudflare'').
Das Ergebnis wird **nicht** zurückgegeben, sondern in einen bereitgestellten Speicherbereich geschrieben: 40 Byte Kopf (''DNSResult'') plus Datenbereich dahinter.
import std.io;
import std.alloc;
import std.net.dns;
con DNS_RESULT_SIZE: int64 := 40; // Kopf: success, recordType, ttl, rcode, dataLen
con DNS_DATA_MAX: int64 := 512; // Datenbereich ab Offset 40
fn main(): int64 {
var res: int64 := alloc(DNS_RESULT_SIZE + DNS_DATA_MAX);
if (res == 0) { return 1; }
var host: pchar := "example.com"c;
if (!DNSResolveGoogle(host as int64, 11, res)) {
PrintLn("DNS: keine Antwort");
free(res, DNS_RESULT_SIZE + DNS_DATA_MAX);
return 1;
}
if (peek64(res + DNSResult.rcode) != DNS_RCODE_OK) {
PrintLn("DNS: Fehlercode vom Server");
free(res, DNS_RESULT_SIZE + DNS_DATA_MAX);
return 1;
}
// A-Record: vier Oktette ab dem Datenbereich, byteweise
Print(IntToStr(peek8(res + DNS_RESULT_SIZE + 0))); Print(".");
Print(IntToStr(peek8(res + DNS_RESULT_SIZE + 1))); Print(".");
Print(IntToStr(peek8(res + DNS_RESULT_SIZE + 2))); Print(".");
PrintLn(IntToStr(peek8(res + DNS_RESULT_SIZE + 3)));
free(res, DNS_RESULT_SIZE + DNS_DATA_MAX);
return 0;
}
Zwei Ebenen von Fehlern, die man **beide** prüfen muss:
* **''false'' als Rückgabewert** — es kam gar keine Antwort (Timeout, Netz weg, Server nicht erreichbar).
* **''rcode != DNS_RCODE_OK''** — es kam eine Antwort, aber eine ablehnende. ''DNS_RCODE_NXDOMAIN'' heißt „Name existiert nicht", ''DNS_RCODE_SERVFAIL'' „Server hat versagt". Wer nur den ersten Fall prüft, hält ein NXDOMAIN für einen Erfolg mit leerem Ergebnis.
''A.feld'' ist in Lyx der **Byte-Offset** des Feldes — ''DNSResult.rcode'' liefert also die Zahl, die man auf den Basiszeiger addiert. Dasselbe Muster trägt durch die ganze Standardbibliothek.
^ Record ^ Funktion ^ Datenbereich enthält ^
| A | ''DNSResolveGoogle'' | 4 Byte IPv4 |
| AAAA | ''DNSResolveAAAAGoogle'' | 16 Byte IPv6 |
| MX | ''DNSResolveMXGoogle'' | Priorität + Hostname |
| TXT | ''DNSResolveTXTGoogle'' | Textsegmente |
| NS, SOA, SRV, CAA, PTR, DS, DNSKEY | ''DNSResolveGoogle'' | typabhängig |
----
===== 5. HTTP =====
==== Der kurze Weg ====
import std.io;
import std.net.http;
fn main(): int64 {
var resp: HTTPResponse := HTTPGet("example.com"c, "/"c);
Print("Status: "); PrintLn(IntToStr(resp.statusCode));
if (resp.statusCode == HTTP_OK) {
Print("Bytes im Rumpf: "); PrintLn(IntToStr(resp.bodySize));
}
HTTPResponseFree(resp);
return 0;
}
''HTTPResponse'' trägt ''statusCode'', ''contentLength'', ''bodyPtr'', ''bodySize'' sowie die Rohheader (''headersRaw''/''headersSize''). Der Rumpf liegt als Speicherbereich vor, **nicht** als Zeichenkette — ''bodyPtr'' plus ''bodySize'' byteweise lesen.
''HTTPResponseFree'' ist Pflicht. Ohne den Aufruf bleiben Rumpf und Header liegen.
Gegen ''example.com'' liefert das Programm ''Status: 200'' und 571 Byte im Rumpf. Zeigt der Host ins Leere — etwa ''127.0.0.1'' ohne lauschenden Dienst — kommt ''statusCode'' als ''0'' zurück; auch dieser Fall gehört geprüft.
> **Ein Rest bleibt: manche Server lassen ''HTTPGet'' hängen.** Antwortet die Gegenseite mit ''Content-Length'' und ''Connection: Upgrade'' statt mit ''Transfer-Encoding: chunked'' (beobachtet bei ''neverssl.com''), kehrt der Aufruf nicht zurück — das Programm wartet unbegrenzt, die Zeile nach ''HTTPGet'' wird nie erreicht. Dieselbe Adresse beantwortet ''curl'' in dreieinhalb Sekunden.
>
> Wer gegen fremde, unbekannte Server spricht, sollte den Aufruf deshalb nicht ohne Aufsicht laufen lassen: entweder ''HTTPSGet'' verwenden oder die Anfrage über ''std.net.socket'' mit eigenem Timeout selbst schreiben (Abschnitt 8). Geprüft mit lyxc 1.0.16F, stdlib-Stand ''80479041''.
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1309|Issue #1309]])
==== Header, Auth, Redirects ====
var hdrs: int64 := 0;
hdrs := HTTPSetHeader(hdrs, "Accept"c as int64, "application/json"c as int64);
hdrs := HTTPSetBearerToken(hdrs, "GEHEIM"c as int64);
var resp: HTTPResponse := HTTPGetH("api.example.com"c, "/v1/users"c, hdrs);
// ... auswerten ...
HTTPResponseFree(resp);
HTTPHeadersFree(hdrs);
* ''HTTPSetHeader'' gibt den (womöglich neu angelegten) Header-Block zurück — **Rückgabewert wieder zuweisen**, sonst geht das Ergebnis verloren.
* ''HTTPSetBasicAuth(hdrs, user, pass)'' für Basic-Auth, ''HTTPSetBearerToken'' für Token.
* ''HTTPGetH'', ''HTTPPostH'', ''HTTPDeleteH'' sind die Varianten mit Headern.
* ''HTTPSendWithRedirects'' bzw. ''HTTPGetWithRedirects'' folgen bis zu ''HTTP_MAX_REDIRECTS'' (10) Weiterleitungen. Die einfachen Funktionen folgen **keiner** — ein 301 kommt als 301 zurück.
* ''HTTPGetHeader(resp, name)'' liest einen Antwortheader aus.
==== Volle Kontrolle: HTTPRequest ====
var req: HTTPRequest;
req.method := HTTP_POST;
req.host := "api.example.com"c as int64;
req.path := "/v1/jobs"c as int64;
req.port := HTTP_PORT;
req.headers := hdrs;
req.body := rumpf;
req.bodySize := rumpfLen;
var resp: HTTPResponse := HTTPSend(req);
----
===== 6. REST-APIs — std.net.rest =====
''std.net.rest'' hält Basis-URL, Port, HTTPS-Schalter und Auth-Header an einem Ort fest, sodass die einzelnen Aufrufe nur noch den Pfad nennen.
import std.io;
import std.alloc;
import std.net.http;
import std.net.rest;
fn main(): int64 {
var client: int64 := alloc(REST_SIZE);
RestClientInit(client, "https://api.example.com/v1"c as int64, 0);
RestClientSetBearerToken(client, "GEHEIM"c as int64);
var resp: HTTPResponse := RestGet(client, "/users"c as int64);
if (RestIsSuccess(resp.statusCode) == 1) {
Print("Bytes: "); PrintLn(IntToStr(resp.bodySize));
} else {
Print("HTTP "); PrintLn(IntToStr(resp.statusCode));
}
HTTPResponseFree(resp);
RestClientFree(client);
free(client, REST_SIZE);
return 0;
}
* Der Client ist ein Speicherblock der Größe ''REST_SIZE'' — selbst anlegen, selbst freigeben. ''RestClientFree'' gibt die **inneren** Allokationen frei (Host, Pfad, Auth-Header), ''free'' danach den Block selbst.
* ''https://'' in der Basis-URL schaltet TLS und Port 443 automatisch.
* ''RestJsonPost''/''RestJsonPut'' setzen ''Content-Type: application/json'' mit.
* ''RestQueryAppend(buf, bufMax, key, value)'' baut Query-Strings mit korrekter Kodierung.
* ''RestIsSuccess'' / ''RestIsClientError'' / ''RestIsServerError'' klassifizieren den Status, statt Zahlen zu vergleichen.
* Jede Antwort braucht ihr ''HTTPResponseFree'' — auch die fehlgeschlagene.
----
===== 7. TLS =====
''std.net.https'' ist der bequeme Weg (''HTTPSGet'', ''HTTPSPost''). Wer den Kanal selbst steuert — eigenes Protokoll, langlebige Verbindung, eigenes Framing — nimmt ''std.net.tls'' direkt. Die Unit setzt auf OpenSSL auf; sie ist die einzige Net-Unit mit externer Abhängigkeit.
import std.io;
import std.alloc;
import std.net.socket;
import std.net.tls;
import std.net.types;
fn main(): int64 {
var ctx: TLSContext := TLSInit(); // einmal je Prozess
if (ctx.initialized != 1) { PrintLn("TLS-Init fehlgeschlagen"); return 1; }
var conn: TCPConn := TCPConnect(IPPack(172, 66, 147, 243), 443);
if (conn.fd < 0) { TLSFree(ctx); return 1; }
var host: pchar := "example.com"c; // Hostname für SNI und Zertifikatsprüfung
var tls: TLSConn := TLSConnect(ctx, conn.fd, host as int64);
if (tls.connected != 1) {
PrintLn("Handshake fehlgeschlagen");
TCPConnClose(conn); TLSFree(ctx);
return 1;
}
var req: pchar := "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"c;
TLSWrite(tls, req as int64, 56);
var buf: int64 := alloc(4096);
var n: int64 := TLSRead(tls, buf, 4095);
if (n > 0) {
poke8(buf + n, 0);
var antwort: pchar := buf as pchar;
PrintLn(antwort);
}
free(buf, 4096);
TLSClose(tls); // TLS-Schicht zuerst
TCPConnClose(conn); // dann das Socket
TLSFree(ctx); // zuletzt der Kontext
return 0;
}
Reihenfolge und Lebensdauer sind hier keine Kosmetik:
* ''TLSInit'' **einmal** je Prozess, nicht je Verbindung. Der Kontext hält die geladenen Wurzelzertifikate.
* Der Hostname muss an ''TLSConnect'' übergeben werden — er dient SNI **und** dem Abgleich mit dem Zertifikat. Ohne ihn verbindet sich das Programm mit irgendwem.
* Abgeräumt wird von innen nach außen: ''TLSClose'' → ''TCPConnClose'' → ''TLSFree''.
* Fehlercodes: ''TLS_ERR_CONNECT'' (Handshake), ''TLS_ERR_VERIFY'' (Zertifikat abgelehnt), ''TLS_ERR_HOSTNAME'' (Name passt nicht zum Zertifikat). ''TLS_ERR_HOSTNAME'' ist kein Netzfehler, sondern ein Sicherheitsbefund — nicht wegfangen.
----
===== 8. Server =====
==== Der Grundaufbau ====
import std.io;
import std.net.socket;
import std.net.types;
fn main(): int64 {
var l: TCPListener := TCPListenerNew();
if (l.fd < 0) { return 1; }
TCPListenerSetReuseAddr(l); // Neustart ohne TIME_WAIT-Sperre
if (TCPListenerBindTo(l, IPPack(0, 0, 0, 0), 8080) < 0) { return 1; }
if (TCPListenerListen(l, 16) < 0) { return 1; }
var buf: int64 := BufferAlloc(4096);
while (true) {
var c: TCPConn := TCPListenerAccept(l);
if (c.fd < 0) { continue; } // Fehler beim Annehmen: weiter
var n: int64 := TCPConnRead(c, buf, 4096);
if (n > 0) { TCPConnWrite(c, buf, n); }
TCPConnClose(c); // jede Verbindung schließen
}
BufferFree(buf, 4096);
TCPListenerClose(l);
return 0;
}
* **''TCPListenerSetReuseAddr'' vor dem Bind.** Ohne ihn scheitert ein Neustart, solange alte Verbindungen in ''TIME_WAIT'' hängen.
* ''IPPack(0,0,0,0)'' bindet an alle Schnittstellen, ''IPPack(127,0,0,1)'' nur an localhost. Für Dienste, die nicht nach außen sollen, ist das die erste Sicherheitsmaßnahme.
* Ports unter 1024 brauchen zusätzlich ''CAP_NET_BIND_SERVICE'' vom Betriebssystem — die Lyx-Capability allein genügt dafür nicht.
* ''TCPConnClose'' in **jedem** Zweig. Ein vergessenes Close leckt einen Dateideskriptor; nach ein paar tausend Verbindungen nimmt ''accept'' nichts mehr an.
==== Viele Verbindungen: epoll ====
Ein Durchlauf pro Verbindung skaliert nicht. ''std.net.epoll'' meldet, welche Deskriptoren bereit sind, und bedient damit tausende Verbindungen in einem Thread.
import std.io;
import std.alloc;
import std.net.socket;
import std.net.epoll;
import std.net.types;
con MAX_EVENTS: int64 := 64;
fn main(): int64 {
var l: TCPListener := TCPListenerNew();
TCPListenerSetReuseAddr(l);
if (TCPListenerBindTo(l, IPPack(0, 0, 0, 0), 8080) < 0) { return 1; }
if (TCPListenerListen(l, 128) < 0) { return 1; }
SocketSetNonBlocking(l.fd);
var ep: int64 := EpollCreate();
if (ep < 0) { return 1; }
EpollAdd(ep, l.fd, EPOLLIN, l.fd);
var evBuf: int64 := alloc(MAX_EVENTS * EPOLL_EVENT_SIZE);
var buf: int64 := alloc(4096);
while (true) {
var ready: int64 := EpollWait(ep, evBuf, MAX_EVENTS, -1); // -1 = warten
var i: int64 := 0;
while (i < ready) {
var fd: int64 := EpollEventFd(evBuf, i);
var flags: int64 := EpollEventFlags(evBuf, i);
if (fd == l.fd) {
var c: TCPConn := TCPListenerAccept(l);
if (c.fd >= 0) {
SocketSetNonBlocking(c.fd);
EpollAdd(ep, c.fd, EPOLLIN | EPOLLRDHUP, c.fd);
}
} else {
if ((flags & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) != 0) {
var dead: TCPConn;
dead.fd := fd;
EpollDel(ep, fd);
TCPConnClose(dead);
} else {
var conn: TCPConn;
conn.fd := fd;
var n: int64 := TCPConnRead(conn, buf, 4096);
if (n > 0) { TCPConnWrite(conn, buf, n); }
else { EpollDel(ep, fd); TCPConnClose(conn); }
}
}
i := i + 1;
}
}
return 0;
}
Was hier zählt:
* **Nicht-blockierend ist Pflicht.** Ohne ''SocketSetNonBlocking'' blockiert ein einziger langsamer Client die gesamte Schleife.
* ''EPOLLRDHUP'' meldet, dass die Gegenseite geschlossen hat. ''EPOLLERR'' und ''EPOLLHUP'' sind **immer** aktiv, auch ohne Anmeldung — sie müssen behandelt werden, sonst dreht die Schleife auf einem toten Deskriptor durch.
* Der Nutzwert (viertes Argument von ''EpollAdd'') kommt in ''EpollEventData'' zurück. Hier wird schlicht der Deskriptor mitgegeben; für eine Verbindungstabelle setzt man dort einen Index oder eine Adresse ein.
* ''EpollDel'' **vor** dem Schließen. Ein geschlossener Deskriptor verschwindet zwar von selbst aus dem Set, aber nur, wenn ihn nichts anderes offen hält.
* ''EpollWait'' mit Timeout in Millisekunden oder ''-1'' für unbegrenzt. Wer nebenher periodische Arbeit erledigen will, gibt ein Timeout an, statt einen zweiten Thread aufzumachen.
* ''EventFdCreate''/''EventFdSignal'' weckt eine wartende ''EpollWait'' aus einem anderen Thread — der saubere Weg, einen Server herunterzufahren.
==== Ein Socket aus einem Deskriptor ====
''TCPConn'' ist eine schlichte Struktur um den Deskriptor. Wer aus epoll nur eine Zahl zurückbekommt, baut sich die Struktur wieder zusammen:
var conn: TCPConn;
conn.fd := fd;
----
===== 9. UDP =====
Verbindungslos: kein Handshake, keine Reihenfolgegarantie, kein Wiederholen. Dafür ein Syscall weniger und kein Verbindungszustand.
@capabilities([system.exit, system.memory.heap, network.udp.bind])
import std.io grant [system.write];
import std.alloc grant [system.memory.heap];
import std.net.socket grant [network.udp.bind, system.memory.heap];
fn main(): int64 {
var sock: UDPSocket := UDPSocketNew();
if (sock.fd < 0) { return 1; }
UDPSocketSetReuseAddr(sock);
if (UDPSocketBindTo(sock, IPPack(0, 0, 0, 0), 9000) < 0) { return 1; }
var buf: int64 := alloc(2048);
var ipZelle: int64 := alloc(8); // Ausgabeparameter: Absender-IP
var portZelle: int64 := alloc(8); // Ausgabeparameter: Absender-Port
var n: int64 := UDPSocketRecv(sock, buf, 2048, ipZelle, portZelle);
if (n > 0) {
Print("Bytes: "); PrintLn(IntToStr(n));
Print("Absender-Port: "); PrintLn(IntToStr(peek64(portZelle)));
UDPSocketSendToAddr(sock, buf, n, peek64(ipZelle), peek64(portZelle));
}
free(portZelle, 8); free(ipZelle, 8); free(buf, 2048);
UDPSocketClose(sock);
return 0;
}
Absender-IP und -Port sind **Ausgabeparameter als Zelle** — acht Byte Speicher, die die Funktion beschreibt. Das ist in Lyx das durchgängige Muster für Rückgaben jenseits des einen Rückgabewerts, weil es keinen Adressoperator gibt.
Ein Datagramm passt in **einen** Aufruf. Der Puffer muss deshalb groß genug für das größte erwartete Paket sein — was nicht passt, wird abgeschnitten und ist verloren.
----
===== 10. Capabilities und die Sandbox =====
Netzwerkzugriff ist der Bereich, in dem sich das Capability-Modell am deutlichsten auswirkt: Ohne Deklaration läuft das Programm ungehärtet; **mit** Deklaration schaltet der Compiler seccomp, Landlock und den Netzwerk-Proxy scharf.
^ Capability ^ Erlaubt ^
| ''network.tcp.connect'' | ausgehende TCP-Verbindung (Client, HTTP, TLS) |
| ''network.tcp.bind'' | TCP-Port belegen (Server) |
| ''network.udp.connect'' | UDP ausgehend |
| ''network.udp.bind'' | UDP-Port belegen |
| ''network.unix'' | Unix-Domain-Sockets |
| ''network.raw'' | Raw-Sockets (ICMP, ARP) — höchste Stufe, nur mit gutem Grund |
Dazu praktisch immer: ''system.exit'', ''system.memory.heap'' (Puffer) und ''system.write'' (Ausgabe) für die importierten Units.
@capabilities([system.exit, system.memory.heap, network.tcp.connect])
import std.io grant [system.write];
import std.net.socket grant [network.tcp.connect, system.memory.heap];
import std.net.types grant [];
Der Compiler quittiert das im **LCBS Security Audit** nach jeder Übersetzung:
Explizite Capabilities:
+ system.exit
+ system.memory.heap
+ network.tcp.connect
Runtime-Schutz:
+ seccomp (SECCOMP_RET_KILL_PROCESS, 71 Regeln)
+ landlock (0 Pfad-Regeln)
+ Userspace Proxy (Netzwerk)
+ Stack Canaries
Sicherheits-Score: 38/45
* **Jeder Import ohne ''grant'' kostet 2 Punkte** und wird als Warnung gemeldet. Ein leeres ''grant []'' ist eine gültige Aussage („diese Unit braucht nichts") und kostet nichts.
* Ohne ''@capabilities'' steht dort ''Capability-Modell: NONE'' — kein seccomp, keine Härtung. Für ein produktives Netzprogramm ist das die falsche Voreinstellung.
* Die Sandbox tötet den Prozess bei einem nicht erlaubten Syscall (''SECCOMP_RET_KILL_PROCESS'') — es gibt keinen Fehlercode zum Abfangen. Wer im Betrieb ein wortloses Sterben sieht, prüft zuerst die Capability-Liste.
> **''host:'' und ''port:'' an Netzwerk-Capabilities werden nicht durchgesetzt.** ''network.tcp.connect(host: "api.example.com")'' prüft nur den Argumentnamen; die Verbindung zu jedem anderen Rechner bleibt möglich. Der Compiler meldet das an jeder betroffenen Stelle mit ''Capability-Argument wird NICHT durchgesetzt''. Nur ''path:'' an ''fs''-Capabilities wirkt (Landlock-Regel je Pfad).
> ([[https://github.com/SEOLizer/LyX-Compiler/issues/1108|Issue #1108]])
----
===== 11. Fehler behandeln =====
Es gibt drei Fehlerquellen, die man auseinanderhalten muss:
^ Ebene ^ Erkennbar an ^ Typische Ursache ^
| Socket | ''fd < 0'', Rückgabewert < 0 | kein Deskriptor frei, Adresse belegt, Ziel nicht erreichbar |
| Verbindung | ''TCPConnGetError(conn)'' | Verbindung abgewiesen, unterwegs abgerissen |
| Protokoll | ''statusCode'', ''rcode'' | HTTP 404, DNS NXDOMAIN — das Netz funktionierte |
var conn: TCPConn := TCPConnect(ip, 443);
if (conn.fd < 0) {
// gar nicht erst zustande gekommen
return 1;
}
var err: int64 := TCPConnGetError(conn);
if (err != 0) {
// Socket existiert, Verbindung ist trotzdem defekt
TCPConnClose(conn);
return 1;
}
Weitere Stellschrauben an einer bestehenden Verbindung:
* ''TCPConnSetNodelay(conn, true)'' — Nagle abschalten. Richtig für interaktive Protokolle mit kleinen Nachrichten, falsch für Massendurchsatz.
* ''TCPConnSetKeepAlive(conn, true)'' — tote Gegenstellen erkennen, die sich nicht verabschiedet haben.
* ''TCPConnSetRecvBuf'' / ''TCPConnSetSendBuf'' — Puffergrößen im Kernel.
**''TCPConnRead'' gibt 0 zurück, wenn die Gegenseite geschlossen hat** — das ist kein Fehler, sondern das Ende. Wer nur auf ''< 0'' prüft, dreht danach in einer Endlosschleife.
----
===== 12. Ressourcen: was muss wieder weg =====
^ Angelegt mit ^ Freigegeben mit ^
| ''BufferAlloc(n)'' | ''BufferFree(buf, n)'' — mit derselben Größe |
| ''alloc(n)'' | ''free(ptr, n)'' |
| ''TCPConnect'', ''TCPListenerAccept'' | ''TCPConnClose'' |
| ''TCPListenerNew'' | ''TCPListenerClose'' |
| ''UDPSocketNew'' | ''UDPSocketClose'' |
| ''EpollCreate'' | Deskriptor schließen |
| ''HTTPGet'' und alle Verwandten | ''HTTPResponseFree(resp)'' — auch im Fehlerfall |
| ''HTTPSetHeader''-Kette | ''HTTPHeadersFree(hdrs)'' |
| ''RestClientInit'' | ''RestClientFree'' **und** ''free'' auf den Block |
| ''TLSInit'' | ''TLSFree'' |
| ''TLSConnect'' | ''TLSClose'' |
Ein Serverprozess läuft Wochen. Jedes vergessene ''Close'' ist dort kein Schönheitsfehler, sondern ein Ausfall mit Ansage.
----
===== 13. Fallstricke =====
> **''PrintLn(feld)'' druckt bei einem ''pchar''-**Feld** die Adresse statt der Zeichenkette.** Betroffen ist der Zugriff auf ein Struct- oder Klassenfeld:
>
>
> PrintLn(resp.statusText); // gibt die Adresse aus
> var s: pchar := resp.statusText;
> PrintLn(s); // richtig
>
>
> Die Typumwandlung im Argument (''%%PrintLn(buf as pchar)%%'') war bis lyxc 1.0.15G ebenso betroffen und arbeitet seit **1.0.16F** korrekt. ''PrintStrLn'' ist in keinem der Fälle betroffen.
* **Der Empfangspuffer ist nicht nullterminiert.** Nach ''TCPConnRead'' / ''TLSRead'' steht am Ende der Daten kein ''0''. Wer den Inhalt als Zeichenkette behandeln will, setzt es selbst: ''poke8(buf + n, 0)'' — und liest deshalb nur ''bufsize - 1'' Byte ein.
* **Ein ''read'' ist keine Nachricht.** TCP kennt keine Grenzen. Eine „Zeile" oder ein „Paket" kommt womöglich in drei Teilen an oder zwei Nachrichten in einem Aufruf. Wer ein Protokoll spricht, muss selbst puffern und trennen.
* **''HTTPSetHeader'' gibt einen neuen Block zurück** — Rückgabewert zuweisen, sonst ist der Header weg.
* **Ports unter 1024** brauchen Betriebssystem-Rechte, unabhängig von Lyx-Capabilities.
* **Zwei Programme, ein Port:** ''SetReuseAddr'' erlaubt den schnellen Neustart, ''SetReusePort'' erlaubt mehrere Prozesse am selben Port (Lastverteilung durch den Kernel). Das sind verschiedene Dinge.
* **TLS ohne Hostname** hebt die Zertifikatsprüfung praktisch auf. Immer den echten Namen übergeben.
----
===== 14. Weiterführend =====
* [[lyx_-_programmiersprache:units:net|std.net — vollständige Unit-Referenz]] (alle 28 Units)
* [[lyx_-_programmiersprache:units:net:socket|std.net.socket]] · [[lyx_-_programmiersprache:units:net:http|std.net.http]] · [[lyx_-_programmiersprache:units:net:tls|std.net.tls]] · [[lyx_-_programmiersprache:units:net:epoll|std.net.epoll]] · [[lyx_-_programmiersprache:units:net:dns|std.net.dns]]
* [[lyx_-_programmiersprache:sprache:capabilities|Capabilities — das Sicherheitsmodell im Detail]]
* [[lyx_-_programmiersprache:sprache:rohspeicher|Rohspeicher: alloc, peek & poke]]
* [[lyx_-_programmiersprache:sprache:handles-und-fd|Handles und Dateideskriptoren]]
* [[lyx_-_programmiersprache:guides:cloud|Cloud-Guide]] — HTTP-Aufrufe gegen AWS, DigitalOcean, GCP
* [[lyx_-_programmiersprache:guides:kryptographie|Kryptographie-Guide]] — was hinter TLS liegt
Alle Codebeispiele dieser Seite sind mit lyxc 1.0.16F übersetzt worden.