---
title: "Use tcp, udp and unix sockets in your app"
description: "Declare TCP, UDP and unix socket destinations and listeners in main.yaml, open sockets with TLS through net, test them on the in-process loopback, and check that nothing goes outside the rules."
section: "guides"
status: "Building for 0.4"
spec: "net-sockets"
canonical: "https://mainsdk.dev/docs/guides/use-net"
updated: "2026-10-28"
---

# Use tcp, udp and unix sockets in your app

> Declare TCP, UDP and unix socket destinations and listeners in main.yaml, open sockets with TLS through net, test them on the in-process loopback, and check that nothing goes outside the rules.

Status: Building for 0.4 · Canonical: https://mainsdk.dev/docs/guides/use-net · Updated: 2026-10-28

You will let your app talk to a TCP service (a database, a mail relay, a collector), run a small TCP server, send a UDP datagram, and prove the rules hold in a test with no real socket. Every call is checked against `main.yaml`, protocol and port included. It takes about 15 minutes.

## You need

- A MainSDK app with a `main.yaml` (see [Thirty-minute path](https://mainsdk.dev/docs/getting-started/thirty-minutes)).
- The hosts and ports your app talks to, and the ports it listens on.

## 1. Declare what the app may reach

```yaml title="main.yaml"
network:
  allow:
    - tcp://db.acme.dev:5432    # net.connect to this host and port
    - udp://8.8.8.8:53          # net.udp send to this peer and port
  listen:
    - tcp:7000                  # net.listen, loopback only
```

A plain entry such as `db.acme.dev` allows only `http.*` calls. A raw socket needs a `tcp://` or `udp://` entry with the port, so a host you declared for HTTPS can't be used to carry data out on another port.

## 2. Open a TCP connection

```ts title="app/db.ts"
import { net } from "mainsdk";

const conn = await net.connect("db.acme.dev", 5432, { tls: { ca: "secret:INTERNAL_CA" } });
const writer = conn.writable.getWriter();
await writer.ready;
await writer.write(new TextEncoder().encode("PING\r\n"));
for await (const chunk of conn.readable) console.log(chunk.length);
await conn.close();
```

`tls: true` verifies against the system roots, and `ca` adds a private CA from a secret. Await `writer.ready` before each write: it waits while the peer can't keep up, which is how a slow reader slows your app down instead of filling its memory.

## 3. Listen for TCP

```ts title="app/server.ts"
import { net } from "mainsdk";

await net.listen(7000, async (conn) => {
  const writer = conn.writable.getWriter();
  for await (const chunk of conn.readable) await writer.write(chunk);
});
```

The server binds loopback. To accept peers from outside, add the bind address and limit who may connect:

```yaml title="main.yaml"
network:
  listen:
    - { protocol: tcp, port: 7100, bind: 0.0.0.0, from: [10.0.0.0/8] }
```

## 4. Use a unix socket

```yaml title="main.yaml"
network:
  allow:
    - unix:///run/app/redis.sock     # net.connectUnix to this path
  listen:
    - unix:///run/app/ctl.sock       # net.listenUnix at exactly this path
```

```ts title="app/ctl.ts"
import { net } from "mainsdk";

const redis = await net.connectUnix("/run/app/redis.sock");
const ctl = await net.listenUnix("/run/app/ctl.sock", (conn) => void conn.close());
```

A unix socket reaches this machine, so `network.default: allow` never opens one. `net.listenUnix` makes the directory with mode 0700, refuses one that is a symlink, another user's or open to group or others (`MAIN-NET-0033`), and gives the socket file mode 0600. Unix sockets work on Linux and macOS; on Windows use a loopback TCP port. In tests, `app.loop.unixServer` adds a fake peer and `app.loop.dialUnix` connects to your listener.

## 5. Send UDP

```ts title="app/dns.ts"
import { net } from "mainsdk";

declare const query: Uint8Array;   // a DNS query you built

const sock = await net.udp();
await sock.send(query, 53, "8.8.8.8");
for await (const reply of sock.receive({ signal: AbortSignal.timeout(2000) })) {
  console.log(reply.data.length);
  break;
}
await sock.close();
```

UDP has no delivery promise: a lost datagram is not retried. Replies are accepted only from the peers your `udp://` entries name.

## 6. Test it with no real socket

```ts title="test/db.test.ts"
import { createTestApp } from "@mainsdk/testing";

const app = createTestApp({
  yaml: "schema: 1\nnetwork:\n  allow: [tcp://db.acme.dev:5432]\n",
  dns: { "db.acme.dev": ["203.0.113.5"] },
});
app.loop!.server("203.0.113.5", 5432, (peer) => {
  peer.attach({ data: (c) => void peer.write(c), end() {}, close() {}, error() {}, drain() {} });
});
const conn = await app.net.connect("db.acme.dev", 5432);
```

`app.loop` is an in-process network with fake DNS, latency and datagram loss. `app.loop.reset(ip, port)` cuts every open connection to a peer and `app.loop.stats()` counts what happened. A call to `db.acme.dev:5433` fails with `MAIN-NET-0002` in the same test.

## 7. Find the sockets your code uses

```bash
npx mainsdk discover
```

It lists each `net.connect` and UDP `send` as `tcp://host:port` or `udp://host:port`, each `net.listen` and bind as `tcp:<port>` or `udp:<port>`, with the file and line, and ends with the lines to add to `main.yaml`. A call with a variable host or port is listed as dynamic and checked when it runs.

## Check it worked

Run `npx mainsdk check`. It passes when every socket the code opens is declared, no listen entry is open to every peer without `from` (a warning, `MAIN-NET-0030`), and no code imports `node:net`, `node:tls` or `node:dgram` directly (`MAIN-NET-0001`). On a Vercel, Netlify or Lambda target it also reports `net.listen` and `net.udp` (`MAIN-NET-0028`): use `http.*` or a provider's HTTP API there.
