feat(tsconnect): add TCPListener and tighten dial/listen network types

Type the new TCP listening surface from the tailscale submodule:

- listen() is now overloaded: tcp/tcp4/tcp6 returns a TCPListener,
  udp/udp4/udp6 returns a PacketConn. The network parameter is a
  literal union instead of a bare string so callers get completion
  and typo protection.
- TCPListener extends AsyncIterable<Conn>, so consumers can write
  `for await (const conn of listener)`.
- dial() drops "udp" from its accepted networks. Connected UDP
  through a stream-shaped Conn was confusing; UDP belongs in
  listen(), and ICMP in listenICMP(). The Go side still accepts
  whatever string for callers who really want to bypass the type.

Bumps the TS lib/target to ES2018 for AsyncIterable, bumps the
tailscale submodule, and updates the test-app inline example to
demonstrate `for await` over a TCP listener.
This commit is contained in:
2026-04-10 21:09:55 +00:00
parent 08ddc31da1
commit bdc4bcccee
5 changed files with 47 additions and 9 deletions
+8 -1
View File
@@ -46,7 +46,14 @@ ipn.login()
// Once running, try the networking APIs:
const conn = await ipn.dial("tcp", "some-peer:22")
const pc = await ipn.listenICMP("icmp4")
const tls = await ipn.dialTLS("some-peer.tail-scale.ts.net:443")</code></pre>
const tls = await ipn.dialTLS("some-peer.tail-scale.ts.net:443")
// Accept incoming TCP connections via async iteration:
const ln = await ipn.listen("tcp", "0.0.0.0:8080")
for await (const c of ln) {
console.log("got conn from", c.remoteAddr())
c.close()
}</code></pre>
<p id="status">Loading…</p>
<script type="module" src="/src/main.ts"></script>
</body>
+1
View File
@@ -5,6 +5,7 @@ import type { IPN, IPNConfig } from "./types"
export type {
Conn,
PacketConn,
TCPListener,
TLSDialOptions,
IPN,
IPNConfig,
+35 -5
View File
@@ -83,6 +83,24 @@ export interface PacketConn {
localAddr(): string
}
/**
* A TCP listener accepting incoming connections from the Tailscale network.
*
* Implements `AsyncIterable<Conn>` so callers can write:
*
* for await (const conn of listener) { ... }
*
* The iterator finishes cleanly when `close()` is called; any other accept
* error rejects the iterator's pending `next()`.
*/
export interface TCPListener extends AsyncIterable<Conn> {
/** Accept a single incoming connection. */
accept(): Promise<Conn>
close(): boolean
/** The local listening address, e.g. "100.64.0.1:8080". */
addr(): string
}
export interface IPN {
run(callbacks: IPNCallbacks): void
login(): void
@@ -109,18 +127,30 @@ export interface IPN {
}>
/**
* Dial a TCP or UDP connection to a Tailscale peer.
* @param network - "tcp" or "udp"
* Dial a TCP connection to a Tailscale peer.
* @param network - "tcp", "tcp4", or "tcp6"
* @param addr - "host:port" (host can be a Tailscale IP or MagicDNS name)
*/
dial(network: "tcp" | "udp", addr: string): Promise<Conn>
dial(network: "tcp" | "tcp4" | "tcp6", addr: string): Promise<Conn>
/**
* Listen for incoming TCP connections on the Tailscale network.
* @param network - "tcp", "tcp4", or "tcp6"
* @param addr - "host:port" to bind to (e.g. "0.0.0.0:0" for any address and port)
*/
listen(
network: "tcp" | "tcp4" | "tcp6",
addr: string
): Promise<TCPListener>
/**
* Listen for incoming UDP packets on the Tailscale network.
* @param network - "udp", "udp4", or "udp6"
* @param addr - "host:port" to bind to (e.g. "0.0.0.0:0" for any)
* @param addr - "host:port" to bind to (e.g. "0.0.0.0:0" for any address and port)
*/
listen(network: string, addr: string): Promise<PacketConn>
listen(
network: "udp" | "udp4" | "udp6",
addr: string
): Promise<PacketConn>
/**
* Open an ICMP endpoint on the Tailscale network stack.
+2 -2
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2017",
"target": "ES2018",
"module": "ES2020",
"moduleResolution": "node",
"strict": true,
@@ -8,7 +8,7 @@
"outDir": "out",
"rootDir": "src",
"sourceMap": true,
"lib": ["ES2017", "DOM"]
"lib": ["ES2018", "DOM"]
},
"include": ["src/**/*"]
}