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 (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 Rohspeicher. - Puffer werden selbst besorgt und selbst freigegeben.
BufferAlloc/BufferFreeausstd.net.socketoderalloc/freeausstd.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 < 0ist 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 mitpeek32liest und durchIPUnpackschickt, bekommt die Oktette rückwärts (172.66.147.243wird zu243.147.66.172).IPPack/IPUnpackarbeiten 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:
falseals 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_NXDOMAINheiß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 lassenHTTPGethängen. Antwortet die Gegenseite mitContent-LengthundConnection: Upgradestatt mitTransfer-Encoding: chunked(beobachtet beineverssl.com), kehrt der Aufruf nicht zurück — das Programm wartet unbegrenzt, die Zeile nachHTTPGetwird nie erreicht. Dieselbe Adresse beantwortetcurlin dreieinhalb Sekunden.
Wer gegen fremde, unbekannte Server spricht, sollte den Aufruf deshalb nicht ohne Aufsicht laufen lassen: entwederHTTPSGetverwenden oder die Anfrage überstd.net.socketmit eigenem Timeout selbst schreiben (Abschnitt 8). Geprüft mit lyxc 1.0.21A, stdlib-Stand80479041.
(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);
HTTPSetHeadergibt 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,HTTPSetBearerTokenfür Token.HTTPGetH,HTTPPostH,HTTPDeleteHsind die Varianten mit Headern.HTTPSendWithRedirectsbzw.HTTPGetWithRedirectsfolgen bis zuHTTP_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.RestClientFreegibt die inneren Allokationen frei (Host, Pfad, Auth-Header),freedanach den Block selbst. https:in der Basis-URL schaltet TLS und Port 443 automatisch. *RestJsonPost/RestJsonPutsetzenContent-Type: application/jsonmit. *RestQueryAppend(buf, bufMax, key, value)baut Query-Strings mit korrekter Kodierung. *RestIsSuccess/RestIsClientError/RestIsServerErrorklassifizieren den Status, statt Zahlen zu vergleichen. * Jede Antwort braucht ihrHTTPResponseFree— auch die fehlgeschlagene. —- ===== 7. TLS =====std.net.httpsist der bequeme Weg (HTTPSGet,HTTPSPost). Wer den Kanal selbst steuert — eigenes Protokoll, langlebige Verbindung, eigenes Framing — nimmtstd.net.tlsdirekt. Die Unit setzt auf OpenSSL auf; sie ist die einzige Net-Unit mit externer Abhängigkeit.TLSInit
Reihenfolge und Lebensdauer sind hier keine Kosmetik: *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; }einmal je Prozess, nicht je Verbindung. Der Kontext hält die geladenen Wurzelzertifikate. * Der Hostname muss anTLSConnectü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_HOSTNAMEist kein Netzfehler, sondern ein Sicherheitsbefund — nicht wegfangen. —- ===== 8. Server ===== ==== Der Grundaufbau ====TIME_WAIT
*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; }TCPListenerSetReuseAddrvor dem Bind. Ohne ihn scheitert ein Neustart, solange alte Verbindungen inhä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ätzlichCAP_NET_BIND_SERVICEvom Betriebssystem — die Lyx-Capability allein genügt dafür nicht. *TCPConnClosein jedem Zweig. Ein vergessenes Close leckt einen Dateideskriptor; nach ein paar tausend Verbindungen nimmtacceptnichts mehr an. ==== Viele Verbindungen: epoll ==== Ein Durchlauf pro Verbindung skaliert nicht.std.net.epollmeldet, welche Deskriptoren bereit sind, und bedient damit tausende Verbindungen in einem Thread.SocketSetNonBlocking
Was hier zählt: * Nicht-blockierend ist Pflicht. Ohneimport 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; }blockiert ein einziger langsamer Client die gesamte Schleife. *EPOLLRDHUPmeldet, dass die Gegenseite geschlossen hat.EPOLLERRundEPOLLHUPsind immer aktiv, auch ohne Anmeldung — sie müssen behandelt werden, sonst dreht die Schleife auf einem toten Deskriptor durch. * Der Nutzwert (viertes Argument vonEpollAdd) kommt inEpollEventDatazurück. Hier wird schlicht der Deskriptor mitgegeben; für eine Verbindungstabelle setzt man dort einen Index oder eine Adresse ein. *EpollDelvor dem Schließen. Ein geschlossener Deskriptor verschwindet zwar von selbst aus dem Set, aber nur, wenn ihn nichts anderes offen hält. *EpollWaitmit Timeout in Millisekunden oder-1für unbegrenzt. Wer nebenher periodische Arbeit erledigen will, gibt ein Timeout an, statt einen zweiten Thread aufzumachen. *EventFdCreate/EventFdSignalweckt eine wartendeEpollWaitaus einem anderen Thread — der saubere Weg, einen Server herunterzufahren. ==== Ein Socket aus einem Deskriptor ====TCPConnist eine schlichte Struktur um den Deskriptor. Wer aus epoll nur eine Zahl zurückbekommt, baut sich die Struktur wieder zusammen:std.net.socket
—- ===== 9. UDP ===== Verbindungslos: kein Handshake, keine Reihenfolgegarantie, kein Wiederholen. Dafür ein Syscall weniger und kein Verbindungszustand. <box gh> > Warum in den Beispielen alle vier Netz-Capabilities stehen.var conn: TCPConn; conn.fd := fd;enthält TCP und UDP in einer Unit. Seitgrantgeprüft wird, muss diegrant-Liste alles führen, was das Modul deklariert — und die@capabilitiesdes Programms müssen das wiederum decken. Ein reines UDP-Programm muss deshalb auchnetwork.tcp.connectundnetwork.tcp.binderklären, obwohl es keinen TCP-Sockel anfasst (nachgemessen mitlyxc 1.1.11B; die Aufteilung der Unit hängt an #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. </box>lyxc 1.1.11B
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. <WRAP tip> Die Options-Setter wirken (#1611 behoben, mit@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; }nachgemessen):UDPSocketSetReuseAddrundTCPListenerSetReuseAddrliefern beide0statt des früheren-14(EFAULT). Ursache war ein vertauschter Parameter —setsockoptbekommt jetzt die Adresse des Wertes statt des Wertes selbst. Der frühere Umweg überSetSockOptmit 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. </WRAP> ==== 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 OptionSO_BROADCASTnicht gesetzt ist:lyxc 1.1.11B
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. <box gh> >ohne SO_BROADCAST an 255.255.255.255 -> -13 (EACCES) an 127.0.0.1 am selben Socket -> 5 Unicast gehtSO_BROADCASTist da (#1611, nachgemessen mit): die Konstante steht mit dem Linux-Wert 6 instd.net.types, undUDPSocketSetBroadcast(sock, an)gibt es ebenfalls — Rückgabe0. Die frühere Behelfslösung, sich die Zahl selbst hinzuschreiben, entfällt. </box> === Der Sender ===SetSockOpt(fd, level, name, 1, 4)
Der Wert muss in einem Speicherbereich stehen, dessen Adresse übergeben wird —@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; }mit der nackten Eins liefert-14. Genau daran kranken die Komfortfunktionen der Unit. === Der Empfänger ===0.0.0.0
=== Der Lauf === Empfänger starten, dann den Sender. Beide auf derselben Maschine:@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; }
(Die erste Adresse ist die LAN-Adresse des sendenden Rechners, hier unkenntlich gemacht.) Drei Dinge zeigt dieser Lauf: * Der Empfänger bindet aufSender: 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— ohne diesesINADDR_ANYkommt nur an, was an die eine gebundene Adresse gerichtet ist, und Broadcasts gehören nicht dazu. * Beide Sendungen kommen an, mit unterschiedlicher Absenderadresse. Bei255.255.255.255wählt der Kernel die Schnittstelle ins LAN, bei127.255.255.255bleibt 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.255verlä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) undsystem.write(Ausgabe) für die importierten Units.grant []
Der Compiler quittiert das im LCBS Security Audit nach jeder Übersetzung:@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 [];
* Jeder Import ohneExplizite 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/45grantkostet 2 Punkte und wird als Warnung gemeldet. Ein leeresist eine gültige Aussage („diese Unit braucht nichts“) und kostet nichts. * Ohne@capabilitiessteht dortCapability-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> >network.tcp.connect(host: „api.example.com“)host:undport:an Netzwerk-Capabilities werden nicht durchgesetzt.prüft nur den Argumentnamen; die Verbindung zu jedem anderen Rechner bleibt möglich. Der Compiler meldet das an jeder betroffenen Stelle mitCapability-Argument wird NICHT durchgesetzt. Nurpath:anfs-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 |TCPConnSetNodelay(conn, true)
Weitere Stellschrauben an einer bestehenden Verbindung: *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; }— 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.< 0TCPConnReadgibt 0 zurück, wenn die Gegenseite geschlossen hat — das ist kein Fehler, sondern das Ende. Wer nur aufprü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 | |HTTPGetund alle Verwandten |HTTPResponseFree(resp)— auch im Fehlerfall | |HTTPSetHeader-Kette |HTTPHeadersFree(hdrs)| |RestClientInit|RestClientFreeundfreeauf den Block | |TLSInit|TLSFree| |TLSConnect|TLSClose| Ein Serverprozess läuft Wochen. Jedes vergesseneCloseist dort kein Schönheitsfehler, sondern ein Ausfall mit Ansage. —- ===== 13. Fallstricke ===== <box tip> >pcharPrintLn(feld)druckt seit lyxc 1.0.17C den Text, nicht die Adresse (#1328). Ein-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. </box> * Der Empfangspuffer ist nicht nullterminiert. NachTCPConnRead/TLSReadsteht am Ende der Daten kein0. Wer den Inhalt als Zeichenkette behandeln will, setzt es selbst:poke8(buf + n, 0)— und liest deshalb nurbufsize - 1Byte ein. * EinSetReuseAddrreadist 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. *HTTPSetHeadergibt 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:erlaubt den schnellen Neustart,SetReusePorterlaubt 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.21A übersetzt worden. Letzte Aktualisierung: 2026-08-27 — Socket-Optionen gegenlyxc 1.1.11Bnachgemessen (#1611 behoben,SO_BROADCASTundUDPSocketSetBroadcastvorhanden);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 diepub fn-Signaturen inaurum/std'' gehalten, 0 Abweichungen).
