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
| 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).
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.
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:
int64. IPPack(127, 0, 0, 1) packt eine IPv4-Adresse in eine Zahl. Es gibt keinen Zeigertyp und keinen Adressoperator — siehe Rohspeicher.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.fd < 0 ist der Fehlerfall. Es fliegt nichts.
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); >
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.
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 |
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)
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.
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);
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;
}
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.
<box gh>
> 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 #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>
@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.
<WRAP tip>
Die Options-Setter wirken (#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.
</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 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.
<box gh>
> SO_BROADCAST ist da (#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.
</box>
=== 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.
<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 tip>
> 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.
</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.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).