====== 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 ([[lyx_-_programmiersprache:guides:daten-serialisierung|Daten & Serialisierung]])-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 as pchar, 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 [[lyx_-_programmiersprache:units|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.21A, 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, "application/json"c); hdrs := HTTPSetBearerToken(hdrs, "GEHEIM"c); 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, 0); RestClientSetBearerToken(client, "GEHEIM"c); var resp: HTTPResponse := RestGet(client, "/users"c); 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); 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 as pchar, 4096); if (n > 0) { TCPConnWrite(c, buf as pchar, 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 as pchar, 4096); if (n > 0) { TCPConnWrite(conn, buf as pchar, 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. > **Warum in den Beispielen alle vier Netz-Capabilities stehen.** ''std.net.socket'' enthält TCP **und** UDP in einer Unit. Seit ''grant'' geprüft wird, muss die ''grant''-Liste alles führen, was das Modul deklariert — und die ''@capabilities'' des Programms müssen das wiederum decken. Ein reines UDP-Programm muss deshalb auch ''network.tcp.connect'' und ''network.tcp.bind'' erklären, obwohl es keinen TCP-Sockel anfasst (nachgemessen mit ''lyxc 1.1.11B''; die Aufteilung der Unit hängt an [[https://github.com/SEOLizer/LyX-Compiler/issues/1340|#1340]]). > > Für den seccomp-Filter heißt das: das Programm bekommt mehr zugestanden, als es braucht. Wer das eng führen will, kommt heute nur über die Syscall-Ebene (''std.net.syscalls'') dorthin. @capabilities([system.exit, system.memory.heap, network.udp.bind, network.tcp.bind, network.tcp.connect, network.udp.connect]) import std.io grant [system.write]; import std.alloc grant [system.memory.heap]; import std.net.socket grant [network.udp.connect, network.tcp.connect, network.tcp.bind, 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. **Die Options-Setter wirken** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1611|#1611]] behoben, mit ''lyxc 1.1.11B'' nachgemessen): ''UDPSocketSetReuseAddr'' und ''TCPListenerSetReuseAddr'' liefern beide ''0'' statt des früheren ''-14'' (EFAULT). Ursache war ein vertauschter Parameter — ''setsockopt'' bekommt jetzt die **Adresse** des Wertes statt des Wertes selbst. Der frühere Umweg über ''SetSockOpt'' mit eigenem Puffer ist damit nicht mehr nötig. Zu beachten bleibt die Stelligkeit: ''UDPSocketSetReuseAddr(sock)'' nimmt **nur** den Socket, ''UDPSocketSetBroadcast(sock, an)'' dagegen zusätzlich den Schalter. ==== Broadcast — an alle im lokalen Netz ==== Ein Broadcast erreicht jeden Zuhörer im selben Netz, ohne dass der Sender deren Adressen kennt. Das ist die übliche Grundlage für Geräteerkennung im LAN: „wer ist da?" an alle, und wer antwortet, meldet sich per Unicast zurück. **Der Kernel lässt das nicht ohne Weiteres zu.** Ein Ziel mit gesetzten Broadcast-Bits wird abgewiesen, solange die Option ''SO_BROADCAST'' nicht gesetzt ist: ohne SO_BROADCAST an 255.255.255.255 -> -13 (EACCES) an 127.0.0.1 am selben Socket -> 5 Unicast geht Das ist eine Schutzmaßnahme, keine Schikane: ein versehentlicher Broadcast belastet jedes Gerät im Netz, deshalb muss die Absicht ausdrücklich erklärt werden. > **''SO_BROADCAST'' ist da** ([[https://github.com/SEOLizer/LyX-Compiler/issues/1611|#1611]], nachgemessen mit ''lyxc 1.1.11B''): die Konstante steht mit dem Linux-Wert **6** in ''std.net.types'', und ''UDPSocketSetBroadcast(sock, an)'' gibt es ebenfalls — Rückgabe ''0''. Die frühere Behelfslösung, sich die Zahl selbst hinzuschreiben, entfällt. === Der Sender === @capabilities([system.exit, system.memory.heap, network.udp.connect, network.tcp.bind, network.tcp.connect, network.udp.bind]) import std.io grant [system.write]; import std.string grant [system.memory.heap]; import std.alloc grant [system.memory.heap]; import std.net.socket grant [network.udp.bind, network.tcp.connect, network.tcp.bind, network.udp.connect, system.memory.heap]; import std.net.syscalls grant [network.udp.connect]; import std.net.types; con PORT: int64 := 9977; con SO_BROADCAST_LINUX: int64 := 6; fn main(): int64 { var sock: UDPSocket := UDPSocketNew(); if (sock.fd < 0) { return 1; } // Ohne diese Option weist der Kernel jedes Broadcast-Ziel mit -13 ab. var opt: int64 := alloc(8); poke64(opt, 1); var rc: int64 := SetSockOpt(sock.fd, SOL_SOCKET, SO_BROADCAST_LINUX, opt, 4); Print("SO_BROADCAST rc = "); PrintLn(IntToStr(rc)); var msg: pchar := "HALLO-NETZ"c; var n: int64 := StrLen(msg); var g: int64 := UDPSocketSendToAddr(sock, msg as int64, n, IPPack(255, 255, 255, 255), PORT); Print("global 255.255.255.255 -> gesendet "); PrintLn(IntToStr(g)); var l: int64 := UDPSocketSendToAddr(sock, msg as int64, n, IPPack(127, 255, 255, 255), PORT); Print("lokal 127.255.255.255 -> gesendet "); PrintLn(IntToStr(l)); free(opt, 8); UDPSocketClose(sock); return 0; } **Der Wert muss in einem Speicherbereich stehen**, dessen Adresse übergeben wird — ''SetSockOpt(fd, level, name, 1, 4)'' mit der nackten Eins liefert ''-14''. Genau daran kranken die Komfortfunktionen der Unit. === Der Empfänger === @capabilities([system.exit, system.memory.heap, network.udp.bind, network.tcp.bind, network.tcp.connect, network.udp.connect]) import std.io grant [system.write]; import std.string grant [system.memory.heap]; import std.alloc grant [system.memory.heap]; import std.net.socket grant [network.udp.connect, network.tcp.connect, network.tcp.bind, network.udp.bind, system.memory.heap]; import std.net.syscalls grant [network.udp.bind]; import std.net.types; con PORT: int64 := 9977; fn main(): int64 { var sock: UDPSocket := UDPSocketNew(); if (sock.fd < 0) { return 1; } // SO_REUSEADDR, damit mehrere Zuhoerer denselben Port belegen duerfen. // Nicht ueber UDPSocketSetReuseAddr — die Funktion liefert EFAULT (#1611). var opt: int64 := alloc(8); poke64(opt, 1); SetSockOpt(sock.fd, SOL_SOCKET, SO_REUSEADDR, opt, 4); if (UDPSocketBindTo(sock, IPPack(0, 0, 0, 0), PORT) < 0) { return 1; } PrintLn("Lausche auf 0.0.0.0:9977"); var buf: int64 := alloc(1024); var ipZ: int64 := alloc(8); var ptZ: int64 := alloc(8); var i: int64 := 0; while (i < 2) { var n: int64 := UDPSocketRecv(sock, buf, 1023, ipZ, ptZ); if (n > 0) { poke8(buf + n, 0); // Textende setzen var a: IPAddr := IPUnpack(peek64(ipZ)); Print("von "); Print(IntToStr(a.a)); Print("."); Print(IntToStr(a.b)); Print("."); Print(IntToStr(a.c)); Print("."); Print(IntToStr(a.d)); Print(":"); Print(IntToStr(peek64(ptZ))); Print(" ("); Print(IntToStr(n)); Print(" Byte): "); PrintLn(buf as pchar); } i := i + 1; } free(ptZ, 8); free(ipZ, 8); free(buf, 1024); free(opt, 8); UDPSocketClose(sock); return 0; } === Der Lauf === Empfänger starten, dann den Sender. Beide auf derselben Maschine: Sender: SO_BROADCAST rc = 0 global 255.255.255.255 -> gesendet 10 lokal 127.255.255.255 -> gesendet 10 Empfaenger: Lausche auf 0.0.0.0:9977 von 192.168.x.y:60871 (10 Byte): HALLO-NETZ von 127.0.0.1:60871 (10 Byte): HALLO-NETZ (Die erste Adresse ist die LAN-Adresse des sendenden Rechners, hier unkenntlich gemacht.) **Drei Dinge zeigt dieser Lauf:** * Der Empfänger bindet auf ''0.0.0.0'' — ohne dieses ''INADDR_ANY'' kommt nur an, was an die eine gebundene Adresse gerichtet ist, und Broadcasts gehören nicht dazu. * **Beide Sendungen kommen an, mit unterschiedlicher Absenderadresse.** Bei ''255.255.255.255'' wählt der Kernel die Schnittstelle ins LAN, bei ''127.255.255.255'' bleibt alles im Loopback. Die zweite Variante ist der Grund, warum sich dieses Beispiel ohne zweites Gerät ausprobieren lässt. * Der Absender-Port ist beide Male derselbe (''60871'') — der Sender hat keinen Port gebunden, das Betriebssystem hat ihm einen zugeteilt. Wer Antworten erwartet, muss ihn aus der Empfangszelle lesen, nicht raten. === Was Broadcast nicht kann === * **Router leiten ihn nicht weiter.** ''255.255.255.255'' verlässt das lokale Netz nie. Über Netzgrenzen hinweg braucht es Multicast oder eine bekannte Gegenstelle. * **Keine Zustellgarantie.** Es bleibt UDP: kein Wiederholen, keine Reihenfolge, keine Bestätigung. Wer wissen muss, wer geantwortet hat, führt selbst Buch. * **Jedes Gerät im Netz bearbeitet das Paket**, auch die, die nichts damit anfangen können. Für regelmäßige Abfragen ist Multicast die freundlichere Wahl, weil dort nur angemeldete Empfänger belastet werden. * **Der eigene Broadcast kommt beim Sender selbst wieder an**, wenn er denselben Port belegt hat. Wer das nicht will, filtert über die Absenderadresse. ---- ===== 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, network.tcp.bind, network.udp.connect, network.udp.bind]) import std.io grant [system.write]; import std.net.socket grant [network.udp.connect, network.udp.bind, network.tcp.bind, 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 seit lyxc 1.0.17C den Text, nicht die Adresse** (#1328). Ein ''pchar''-Feld eines Structs oder einer Klasse lässt sich direkt ausgeben: ''PrintLn(resp.statusText)''. Der Umweg über eine Zwischenvariable ist nicht mehr nötig. > > Bis 1.0.16x kam dort die Adresse heraus; dieselbe Verwechslung von Wert und Adresse betraf Verkettungen und Array-Parameter und ist mit behoben. Die Typumwandlung im Argument (''%%PrintLn(buf as pchar)%%'') arbeitet seit 1.0.16F korrekt. * **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.21A übersetzt worden. Letzte Aktualisierung: 2026-08-27 — Socket-Optionen gegen ''lyxc 1.1.11B'' nachgemessen (#1611 behoben, ''SO_BROADCAST'' und ''UDPSocketSetBroadcast'' vorhanden); ''grant''-Listen der Beispiele auf den geprüften Stand gebracht — alle zehn Vollprogramme der Seite übersetzen wieder. Codebeispiele geprüft: gegen **lyxc 1.2.5C** übersetzt (Prüflauf 2026-09-08 über die gesamte Doku: 574 Vollprogramme, 0 echte Fehler; zusätzlich 5159 Aufrufe gegen die ''pub fn''-Signaturen in ''aurum/std'' gehalten, 0 Abweichungen).