====== 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.