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 std.net-Referenz.

std.net — Unit-Referenz · Capabilities · 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:

  1. Adressen sind int64. IPPack(127, 0, 0, 1) packt eine IPv4-Adresse in eine Zahl. Es gibt keinen Zeigertyp und keinen Adressoperator — siehe Rohspeicher.
  2. 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.
  3. 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 DNSResolve<Typ>Google 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.
(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: TLSCloseTCPConnCloseTLSFree. * 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. <box gh> > 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). > (Issue #1108) </box> —- ===== 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 ===== <box gh> > 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. </box> * 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 ===== * std.net — vollständige Unit-Referenz (alle 28 Units) * std.net.socket · std.net.http · std.net.tls · std.net.epoll · std.net.dns * Capabilities — das Sicherheitsmodell im Detail * Rohspeicher: alloc, peek & poke * Handles und Dateideskriptoren * Cloud-Guide — HTTP-Aufrufe gegen AWS, DigitalOcean, GCP * Kryptographie-Guide — was hinter TLS liegt Alle Codebeispiele dieser Seite sind mit lyxc 1.0.16F übersetzt worden.