Skip to content

doc: new tls.TLSSocket() supportsALPNCallback option but it is undocumented #61047

@ikeyan

Description

@ikeyan

What is the problem?

tls.createServer() documents the ALPNCallback option, but the constructor
new tls.TLSSocket(socket[, options]) does not list ALPNCallback in its
supported options.

However, in practice, new tls.TLSSocket() accepts ALPNCallback (in server
mode with isServer: true) and uses it to negotiate ALPN.

This makes the feature effectively undiscoverable when using TLSSocket
directly.

Affected documentation:

Related (reference docs showing the option exists on createServer):


Observed behavior / reproduction

The following test wraps a net.Server connection using new tls.TLSSocket()
and provides an ALPNCallback option.

Expected/observed results:

  • The callback is called once with { servername, protocols }
  • Returning "h2" selects HTTP/2, and the client observes alpnProtocol === "h2"
  • Without ALPNCallback (control), the negotiated protocol is false
Test code
import * as assert from "node:assert";
import * as fs from "node:fs";
import * as net from "node:net";
import { describe, it } from "node:test";
import * as tls from "node:tls";

const domain = "ocsp.example.test";

const certPem = fs.readFileSync("server.crt");
const keyPem = fs.readFileSync("server.key");

declare module "node:tls" {
    interface TLSSocketOptions {
        ALPNCallback?: (opts: { servername: string | false; protocols: string[] }) => string | undefined;
    }
}

async function listen(server: net.Server) {
    await new Promise<void>((resolve, reject) => {
        server.once("error", reject);
        server.listen(0, "127.0.0.1", resolve);
    });
    const addr = server.address();
    assert.ok(addr && typeof addr === "object");
    return addr.port;
}

function connectClient(port: number, alpnProtocols: string[]) {
    return new Promise<string | false | null>((resolve, reject) => {
        const s = tls.connect(
            {
                host: "127.0.0.1",
                port,
                servername: domain,
                rejectUnauthorized: false,
                ALPNProtocols: alpnProtocols,
                minVersion: "TLSv1.2",
                maxVersion: "TLSv1.2",
            },
            () => {
                const selected = s.alpnProtocol;
                s.end();
                resolve(selected);
            },
        );
        s.once("error", reject);
        s.setTimeout(3_000, () => s.destroy(new Error("TLS handshake timeout")));
    });
}

describe("undocumented TLSSocket option: ALPNCallback", () => {
    it("new tls.TLSSocket({ isServer:true, ALPNCallback }) negotiates ALPN and calls callback", async () => {
        const calledAlpnOptions: Array<{ servername: string | false; protocols: string[] }> = [];

        await using server = net.createServer((raw) => {
            // ★ここが検証対象:TLSSocket の options に ALPNCallback を渡す
            const tlsSocket = new tls.TLSSocket(raw, {
                isServer: true,
                cert: certPem,
                key: keyPem,
                minVersion: "TLSv1.2",
                maxVersion: "TLSv1.2",

                // Undocumented (hypothesis): should be called with { servername, protocols }
                ALPNCallback: ({ servername, protocols }) => {
                    calledAlpnOptions.push({ servername, protocols });

                    // offered list includes "h2" → choose it
                    return protocols.includes("h2") ? "h2" : undefined;
                },
            });

            tlsSocket.once("secure", () => {
                console.log("server negotiated protocol:", tlsSocket.alpnProtocol);
                // server side should also see negotiated protocol
                assert.strictEqual(tlsSocket.alpnProtocol, "h2");
                tlsSocket.end();
            });

            tlsSocket.once("error", (e) => {
                // surface errors clearly
                raw.destroy(e);
            });
        });

        const port = await listen(server);

        const clientSelected = await connectClient(port, ["h2", "http/1.1"]);
        assert.strictEqual(clientSelected, "h2");
        assert.deepStrictEqual(calledAlpnOptions, [{
            servername: domain,
            protocols: ["h2", "http/1.1"],
        }]);
    });

    it("control: without ALPNCallback, negotiated protocol is false (no selection)", async () => {
        await using server = net.createServer((raw) => {
            const tlsSocket = new tls.TLSSocket(raw, {
                isServer: true,
                cert: certPem,
                key: keyPem,
                minVersion: "TLSv1.2",
                maxVersion: "TLSv1.2",
            });

            tlsSocket.once("secure", () => tlsSocket.end());
            tlsSocket.once("error", (e) => raw.destroy(e));
        });

        const port = await listen(server);

        const clientSelected = await connectClient(port, ["h2", "http/1.1"]);
        assert.strictEqual(clientSelected, false);
    });
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions