<?xml version="1.0" encoding="UTF-8"?>
<rfc xmlns:xi="http://www.w3.org/2001/XInclude"
     category="exp"
     docName="draft-g00se-honk-00"
     ipr="trust200902"
     submissionType="independent"
     xml:lang="en"
     version="3">

  <front>
    <title abbrev="HONK Protocol">
      The HONK Protocol: Word-Counting over TCP with
      Honk-Based Responses and Optional Obfuscation
    </title>

    <seriesInfo name="Internet-Draft" value="draft-g00se-honk-00"/>

    <author fullname="G00se" initials="G." surname="G00se">
      <organization>Independent</organization>
      <address>
        <email>me@elaine.is</email>
      </address>
    </author>

    <date year="2026" month="April" day="25"/>
    <area>Applications and Real-Time</area>

    <keyword>honk</keyword>
    <keyword>word counting</keyword>
    <keyword>UTF-8</keyword>
    <keyword>text processing</keyword>
    <keyword>obfuscation</keyword>

    <abstract>
      <t>
        This document defines the HONK protocol, a simple
        application-layer protocol operating over TCP. A HONK client
        submits a stream of UTF-8 encoded text terminated by a CRLF
        sequence, and the HONK server responds with a sequence of HONK
        tokens. The token count equals twice the number of
        whitespace-delimited words detected in the input. If no words
        are detected, the server responds with exactly three HONK
        tokens. The protocol includes an optional Privacy Mode in which
        message content is obfuscated using single-byte XOR with the
        fixed key value 0x48 (the ASCII code point for the letter H).
        This document requests that IANA assign TCP port 24565 to the
        HONK service.
      </t>
      <t>
        This document is an individual submission. It is NOT an April
        Fools Day publication. The author requests serious technical
        consideration from the IETF community.
      </t>
    </abstract>
  </front>

  <middle>

    <section anchor="intro" numbered="true" toc="default">
      <name>Introduction</name>
      <t>
        Many application-layer protocols accept text from a client and
        return structured feedback. The HONK protocol defines one such
        exchange: a client submits a line of UTF-8 text, and the server
        acknowledges the submission by emitting a number of HONK tokens
        proportional to the word count of that line.
      </t>
      <t>The design goals of the HONK protocol are:</t>
      <ul>
        <li>Simplicity: A conformant implementation requires only a TCP
            socket, a UTF-8 decoder, a whitespace tokenizer, and the
            ability to write a line of ASCII text.</li>
        <li>Clarity of signal: The HONK response is distinctive and
            human-readable. An operator monitoring a session can
            immediately confirm the server is functioning.</li>
        <li>Interoperability: TCP transport and UTF-8 encoding make the
            protocol accessible to any platform capable of opening a
            TCP connection.</li>
        <li>Optional obfuscation: Privacy Mode reduces the legibility of
            HONK traffic to casual observers. Privacy Mode is explicitly
            NOT a security mechanism and does not provide cryptographic
            confidentiality; see <xref target="security"/>.</li>
      </ul>
      <t>
        Practical applications include network-accessible word-count
        feedback services, smoke-test endpoints in text processing
        pipelines, and educational demonstrations of application-layer
        protocol design and implementation.
      </t>
    </section>

    <section anchor="terminology" numbered="true" toc="default">
      <name>Conventions and Terminology</name>
      <t>
        The key words "MUST", "MUST NOT", "REQUIRED", "SHALL",
        "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED",
        "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are
        to be interpreted as described in BCP 14
        <xref target="RFC2119"/> <xref target="RFC8174"/> when, and
        only when, they appear in all capitals, as shown here.
      </t>
      <dl newline="false" spacing="normal">
        <dt>HONK token</dt>
        <dd>The four-character ASCII string "HONK"
            (0x48 0x4F 0x4E 0x4B).</dd>
        <dt>Request</dt>
        <dd>A single line of UTF-8 encoded text submitted by a HONK
            client, terminated by CRLF. In Privacy Mode, the line
            content is XOR-obfuscated before transmission.</dd>
        <dt>Response</dt>
        <dd>A server-generated line of one or more HONK tokens
            separated by SP and terminated by CRLF. In Privacy Mode,
            the line content is XOR-obfuscated before transmission.</dd>
        <dt>Word</dt>
        <dd>A maximal non-empty sequence of Unicode code points none
            of which have the Unicode property White_Space=Yes per
            Unicode Standard Annex 44. The CRLF terminator is not
            part of the Request body and MUST NOT be counted.</dd>
        <dt>Honk count (N)</dt>
        <dd>The number of HONK tokens in a Response, computed per
            <xref target="honk-count"/>.</dd>
        <dt>CRLF</dt>
        <dd>The two-octet sequence CR LF (0x0D 0x0A). CRLF is the
            line terminator for all HONK messages and is never
            obfuscated, even in Privacy Mode.</dd>
        <dt>Privacy Mode</dt>
        <dd>An optional session mode negotiated at connection open.
            Request and Response content bytes are XOR'd with the key
            K = 0x48. Privacy Mode does not provide cryptographic
            confidentiality. See <xref target="privacy-mode"/>.</dd>
        <dt>Obfuscation key (K)</dt>
        <dd>The fixed single-octet value 0x48, the ASCII code point
            for the letter H. Chosen for its mnemonic association with
            the word HONK.</dd>
      </dl>
    </section>

    <section anchor="overview" numbered="true" toc="default">
      <name>Protocol Overview</name>
      <t>
        The HONK protocol uses a request-response model over a persistent
        TCP connection. Two session modes exist: Standard Mode and Privacy
        Mode. The mode is set at connection open and applies for the full
        lifetime of that connection.
      </t>

      <section anchor="standard-overview" numbered="true" toc="default">
        <name>Standard Mode</name>
        <ol>
          <li>Client establishes a TCP connection <xref target="RFC0793"/> to server port 24565.</li>
          <li>Client transmits a Request: UTF-8 text followed by CRLF.</li>
          <li>Server counts words, computes N, and sends a Response of N
              HONK tokens.</li>
          <li>Steps 2 and 3 MAY repeat. Either party MAY close the
              connection at any time.</li>
        </ol>
        <figure anchor="fig-standard">
          <name>Standard Mode Exchange</name>
          <artwork align="left"><![CDATA[
  Client                                   Server
    |                                         |
    |--- TCP SYN -------------------------->  |
    |<-- TCP SYN-ACK -----------------------  |
    |--- TCP ACK -------------------------->  |
    |                                         |
    |--- "hello world\r\n" --------------->  |
    |   (2 words)                             |
    |<-- "HONK HONK HONK HONK\r\n" --------  |
    |   (N = 2 * 2 = 4)                       |
    |                                         |
    |--- "\r\n" --------------------------->  |
    |   (0 words)                             |
    |<-- "HONK HONK HONK\r\n" -------------  |
    |   (N = 3, default)                      |
    |                                         |
    |--- TCP FIN -------------------------->  |
    |<-- TCP FIN-ACK -----------------------  |
]]></artwork>
        </figure>
      </section>

      <section anchor="privacy-overview" numbered="true" toc="default">
        <name>Privacy Mode</name>
        <t>
          The client sends "HONK PRIV\r\n" as the very first
          transmission. The server acknowledges with "HONK PRIV\r\n".
          Both lines are cleartext. All subsequent Request and Response
          content on that connection is XOR-obfuscated with K = 0x48.
          CRLF terminators are never obfuscated.
        </t>
        <figure anchor="fig-privacy">
          <name>Privacy Mode Exchange</name>
          <artwork align="left"><![CDATA[
  Client                                      Server
    |                                            |
    |--- "HONK PRIV\r\n" (cleartext) ------->   |
    |<-- "HONK PRIV\r\n" (cleartext) ---------  |
    |   (Privacy Mode now active)                |
    |                                            |
    |--- [XOR("hello world", K=0x48)]\r\n --->  |
    |<-- [XOR("HONK HONK HONK HONK",K)]\r\n -   |
    |                                            |
    |--- TCP FIN ---------------------------->   |
    |<-- TCP FIN-ACK -------------------------   |
]]></artwork>
        </figure>
        <t>
          Privacy Mode MUST be negotiated before any Request is sent. A
          server receiving any line other than "HONK PRIV\r\n" as the
          first line operates in Standard Mode for the lifetime of that
          connection.
        </t>
      </section>
    </section>

    <section anchor="message-format" numbered="true" toc="default">
      <name>Message Format</name>

      <section anchor="request-format" numbered="true" toc="default">
        <name>Request Format</name>
        <t>
          A Request is a UTF-8 encoded line terminated by CRLF. The body
          (everything before the CRLF) MUST be valid UTF-8 per
          <xref target="RFC3629"/>. The CRLF is not part of the body
          and MUST NOT be included in word counting. In Privacy Mode, the
          body bytes are obfuscated per <xref target="privacy-transform"/>
          before transmission; the CRLF is always transmitted as
          cleartext.
        </t>
        <t>The following grammar uses the ABNF notation defined in <xref target="RFC5234"/>:</t>
        <sourcecode type="abnf"><![CDATA[
request      = request-body CRLF
request-body = *UTF8-char
UTF8-char    = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
UTF8-1       = %x00-7F
UTF8-2       = %xC2-DF UTF8-cont
UTF8-3       = %xE0 %xA0-BF UTF8-cont /
               %xE1-EC 2UTF8-cont /
               %xED %x80-9F UTF8-cont /
               %xEE-EF 2UTF8-cont
UTF8-4       = %xF0 %x90-BF 2UTF8-cont /
               %xF1-F3 3UTF8-cont /
               %xF4 %x80-8F 2UTF8-cont
UTF8-cont    = %x80-BF
CRLF         = %x0D %x0A
]]></sourcecode>
        <t>
          Implementations SHOULD impose a maximum request body length.
          The RECOMMENDED maximum is 65535 octets excluding the CRLF.
          A server MAY close the connection if this limit is exceeded.
          A server receiving invalid UTF-8 (after de-obfuscating in
          Privacy Mode) MUST close the connection without a Response.
        </t>
      </section>

      <section anchor="response-format" numbered="true" toc="default">
        <name>Response Format</name>
        <t>
          A Response is N HONK tokens separated by SP and terminated by
          CRLF, where N is computed per <xref target="honk-count"/>. In
          Privacy Mode, the response content bytes are obfuscated per
          <xref target="privacy-transform"/> before transmission.
        </t>
        <sourcecode type="abnf"><![CDATA[
response    = honk-token *(SP honk-token) CRLF
honk-token  = "HONK"
SP          = %x20
CRLF        = %x0D %x0A
]]></sourcecode>
      </section>

      <section anchor="word-counting" numbered="true" toc="default">
        <name>Word Counting</name>
        <ol>
          <li>Interpret the request body as Unicode code points in
              UTF-8.</li>
          <li>Partition the sequence into runs of whitespace and
              non-whitespace, where a code point is whitespace if and
              only if it has Unicode property White_Space=Yes per
              Unicode Standard Annex 44.</li>
          <li>Each maximal non-empty run of non-whitespace code points
              is one word.</li>
          <li>The word count W is the total number of such runs.</li>
        </ol>
        <t>
          A body consisting entirely of whitespace has W = 0 and MUST
          receive the default response of three HONK tokens.
        </t>
      </section>

      <section anchor="honk-count" numbered="true" toc="default">
        <name>Honk Count Computation</name>
        <t>Let W be the word count. The honk count N is:</t>
        <ul>
          <li>If W is greater than zero: N = 2 * W</li>
          <li>If W equals zero: N = 3</li>
        </ul>
        <t>
          Implementations SHOULD impose a maximum N. The RECOMMENDED
          maximum is 65535. A server MAY cap N at its configured maximum
          without signaling an error to the client.
        </t>
      </section>
    </section>

    <section anchor="privacy-mode" numbered="true" toc="default">
      <name>Privacy Mode</name>

      <section anchor="privacy-purpose" numbered="true" toc="default">
        <name>Purpose and Limitations</name>
        <t>
          Privacy Mode provides lightweight obfuscation of HONK message
          content. It is designed solely to reduce readability of HONK
          sessions to casual, non-targeted observers such as automated
          log scanners not specifically looking for HONK traffic.
        </t>
        <t>
          Privacy Mode does NOT provide cryptographic confidentiality,
          integrity, or authentication. The key K = 0x48 is fixed and
          publicly specified in this document; any party with access to
          this specification can immediately de-obfuscate all Privacy
          Mode traffic. Furthermore, because HONK Responses consist
          entirely of predictable tokens, Privacy Mode is trivially
          broken by a known-plaintext attack using a single observed
          Response. Operators requiring genuine confidentiality MUST use
          TLS <xref target="RFC8446"/> or an equivalent mechanism.
        </t>
      </section>

      <section anchor="privacy-negotiation" numbered="true" toc="default">
        <name>Mode Negotiation</name>
        <t>
          A client wishing to use Privacy Mode MUST send the following
          as the very first transmission on the connection:
        </t>
        <sourcecode type="abnf"><![CDATA[
priv-request  = "HONK PRIV" CRLF
]]></sourcecode>
        <t>
          A server receiving "HONK PRIV\r\n" as the first line MUST
          respond with:
        </t>
        <sourcecode type="abnf"><![CDATA[
priv-response = "HONK PRIV" CRLF
]]></sourcecode>
        <t>
          Both lines are cleartext. After the server sends
          "HONK PRIV\r\n", Privacy Mode is active for the remainder of
          that connection. A client MUST NOT send "HONK PRIV\r\n" after
          sending any Request on the same connection.
        </t>
      </section>

      <section anchor="privacy-transform" numbered="true" toc="default">
        <name>Obfuscation Transform</name>
        <t>
          Every octet of a Request or Response body is XOR'd with
          K = 0x48 before transmission. The transform is its own
          inverse: applying it twice recovers the original content.
        </t>
        <t>
          Let B = (b_0, b_1, ..., b_{n-1}) be the body octets. The
          obfuscated sequence B' is defined as:
        </t>
        <artwork align="left"><![CDATA[
    b'_i = b_i XOR 0x48     for all i in [0, n-1]
]]></artwork>
        <t>
          The CRLF terminator is appended to B' after the transform and
          is never obfuscated, preserving line framing regardless of
          body content. The following table shows the obfuscated form of
          selected characters for implementor reference:
        </t>
        <table align="left" anchor="xor-table">
          <name>XOR Transform Reference (K = 0x48)</name>
          <thead>
            <tr>
              <th>Cleartext</th>
              <th>Hex</th>
              <th>Obfuscated hex</th>
              <th>Obfuscated char</th>
            </tr>
          </thead>
          <tbody>
            <tr><td>H</td><td>0x48</td><td>0x00</td><td>NUL</td></tr>
            <tr><td>O</td><td>0x4F</td><td>0x07</td><td>BEL</td></tr>
            <tr><td>N</td><td>0x4E</td><td>0x06</td><td>ACK</td></tr>
            <tr><td>K</td><td>0x4B</td><td>0x03</td><td>ETX</td></tr>
            <tr><td>SP</td><td>0x20</td><td>0x68</td><td>h</td></tr>
          </tbody>
        </table>
        <t>
          Note that the obfuscated form of H is the NUL byte (0x00).
          Implementations MUST NOT treat NUL bytes received in Privacy
          Mode as string terminators or error conditions.
        </t>
      </section>
    </section>

    <section anchor="session" numbered="true" toc="default">
      <name>Session Management</name>
      <t>
        A session begins when a client establishes a TCP connection. The
        client MAY send multiple sequential Requests after any Privacy
        Mode negotiation. The server MUST process each Request and send
        the corresponding Response before processing the next.
        Pipelining is NOT RECOMMENDED.
      </t>
      <t>
        The server SHOULD implement an idle connection timeout. A
        RECOMMENDED default is 30 seconds from the time the last
        Response was sent. Either party MAY close the connection at
        any time. The server SHOULD transmit any pending Response before
        initiating a close.
      </t>
    </section>

    <section anchor="impl" numbered="true" toc="default">
      <name>Implementation Considerations</name>

      <section anchor="impl-unicode" numbered="true" toc="default">
        <name>Unicode Whitespace Handling</name>
        <t>
          Implementations MUST use the Unicode White_Space property for
          word boundary detection and MUST NOT limit whitespace detection
          to ASCII characters only. Implementors SHOULD verify that the
          chosen library function covers the full Unicode White_Space
          property set.
        </t>
      </section>

      <section anchor="impl-crlf" numbered="true" toc="default">
        <name>Line Ending Handling</name>
        <t>
          CRLF is the canonical line terminator. Server implementations
          MAY additionally accept a bare LF for compatibility with
          clients that do not emit CR. In Privacy Mode the bare LF MAY
          also be accepted, since CRLF bytes are never obfuscated.
        </t>
      </section>

      <section anchor="impl-nul" numbered="true" toc="default">
        <name>NUL Bytes in Privacy Mode</name>
        <t>
          As shown in <xref target="xor-table"/>, the obfuscated form of
          the letter H is the NUL byte 0x00. Because every HONK token
          begins with H, the obfuscated form of every Response token
          begins with NUL. Implementations MUST NOT treat NUL bytes in
          Privacy Mode content as string terminators. Implementations
          using null-terminated string APIs MUST account for this
          explicitly.
        </t>
      </section>

      <section anchor="impl-concurrency" numbered="true" toc="default">
        <name>Concurrency</name>
        <t>
          A HONK server MUST handle multiple simultaneous client
          connections. Implementations SHOULD use concurrent I/O
          mechanisms appropriate to their runtime environment.
        </t>
      </section>
    </section>

    <section anchor="security" numbered="true" toc="default">
      <name>Security Considerations</name>

      <section anchor="sec-privacy" numbered="true" toc="default">
        <name>Privacy Mode Provides No Cryptographic Security</name>
        <t>
          Privacy Mode MUST NOT be construed as providing cryptographic
          confidentiality, authentication, or integrity. The key
          K = 0x48 is fixed and publicly documented in this
          specification. Any observer with access to this document can
          de-obfuscate all Privacy Mode traffic immediately. Privacy Mode
          is trivially broken by a known-plaintext attack using a single
          observed Response. Operators requiring genuine confidentiality
          MUST use TLS <xref target="RFC8446"/> or an equivalent
          cryptographically secure transport.
        </t>
      </section>

      <section anchor="sec-dos" numbered="true" toc="default">
        <name>Resource Exhaustion</name>
        <t>
          Servers MUST implement a maximum request body length per
          <xref target="request-format"/> and a maximum honk count N per
          <xref target="honk-count"/>. Servers SHOULD limit concurrent
          connections and MUST implement idle timeouts per
          <xref target="session"/>.
        </t>
      </section>

      <section anchor="sec-utf8" numbered="true" toc="default">
        <name>UTF-8 Input Validation</name>
        <t>
          Servers MUST validate that the request body is well-formed
          UTF-8 after de-obfuscating in Privacy Mode. Servers receiving
          invalid UTF-8 MUST close the connection without sending a
          Response.
        </t>
      </section>

      <section anchor="sec-network" numbered="true" toc="default">
        <name>Network Exposure</name>
        <t>
          The HONK protocol provides no authentication or authorization.
          Operators deploying a HONK server on a publicly accessible
          interface SHOULD restrict access using network-layer controls.
          Deployment on a public interface without access controls is
          NOT RECOMMENDED.
        </t>
      </section>
    </section>

    <section anchor="iana" numbered="true" toc="default">
      <name>IANA Considerations</name>
      <t>
        IANA is requested to assign the following entry in the Service
        Name and Transport Protocol Port Number Registry
        <xref target="RFC6335"/>:
      </t>
      <table align="left" anchor="iana-table">
        <name>IANA Port Assignment Request</name>
        <thead>
          <tr><th>Field</th><th>Value</th></tr>
        </thead>
        <tbody>
          <tr><td>Service Name</td><td>honk</td></tr>
          <tr><td>Port Number</td><td>24565</td></tr>
          <tr><td>Transport Protocol</td><td>TCP</td></tr>
          <tr><td>Description</td>
              <td>HONK: word-counting service with honk-based
                  responses</td></tr>
          <tr><td>Assignee</td><td>G00se (me@elaine.is)</td></tr>
          <tr><td>Contact</td><td>G00se (me@elaine.is)</td></tr>
          <tr><td>Reference</td><td>[This document]</td></tr>
        </tbody>
      </table>
      <t>
        At the time of this writing, port 24565 does not appear in the
        IANA Service Name and Transport Protocol Port Number Registry as
        an assigned or reserved value. UDP port 24565 is not requested
        at this time. Per <xref target="RFC6335"/>, IANA SHOULD mark
        UDP port 24565 as Reserved when assigning TCP port 24565.
      </t>
    </section>

  </middle>

  <back>
    <references>
      <name>References</name>
      <references>
        <name>Normative References</name>
        <xi:include href="https://www.rfc-editor.org/refs/bibxml/reference.RFC.2119.xml"/>
        <xi:include href="https://www.rfc-editor.org/refs/bibxml/reference.RFC.8174.xml"/>
        <xi:include href="https://www.rfc-editor.org/refs/bibxml/reference.RFC.3629.xml"/>
        <xi:include href="https://www.rfc-editor.org/refs/bibxml/reference.RFC.5234.xml"/>
        <xi:include href="https://www.rfc-editor.org/refs/bibxml/reference.RFC.0793.xml"/>
      </references>
      <references>
        <name>Informative References</name>
        <xi:include href="https://www.rfc-editor.org/refs/bibxml/reference.RFC.6335.xml"/>
        <xi:include href="https://www.rfc-editor.org/refs/bibxml/reference.RFC.8446.xml"/>
      </references>
    </references>

    <section anchor="appendix-go" numbered="false" toc="default">
      <name>Go Reference Implementation (Informative)</name>
      <t>
        The following is the reference implementation of the HONK
        protocol, providing both server and client in a single Go source
        file. It requires Go 1.21 or later. The compiled binary is named
        "honk".
      </t>
      <t>Usage:</t>
      <sourcecode type="bash"><![CDATA[
honk server [-addr :24565]
honk send   [-addr host:24565] [-priv] [text...]
]]></sourcecode>
      <t>
        When invoked as "honk send", text may be supplied as
        command-line arguments or read line-by-line from standard input
        if no arguments are given.
      </t>
      <sourcecode type="go" markers="true"><![CDATA[
// honk - HONK protocol client and server (draft-g00se-honk-00)
package main

import (
    "bufio"
    "bytes"
    "flag"
    "fmt"
    "log"
    "net"
    "os"
    "strings"
    "unicode"
    "unicode/utf8"
)

const (
    DefaultAddr     = ":24565"
    DefaultHonks    = 3
    MaxRequestBytes = 65535
    MaxHonkCount    = 65535
    PrivLine        = "HONK PRIV"
    XORKey          = byte(0x48) // ASCII 'H'
)

func main() {
    if len(os.Args) < 2 {
        usage()
    }
    switch os.Args[1] {
    case "server":
        runServer(os.Args[2:])
    case "send":
        runClient(os.Args[2:])
    default:
        usage()
    }
}

func usage() {
    fmt.Fprintln(os.Stderr, "usage:")
    fmt.Fprintln(os.Stderr, "  honk server [-addr :24565]")
    fmt.Fprintln(os.Stderr, "  honk send   [-addr host:24565] [-priv] [text...]")
    os.Exit(1)
}

// ---- Server ---------------------------------------------------------------

func runServer(args []string) {
    fs := flag.NewFlagSet("server", flag.ExitOnError)
    addr := fs.String("addr", DefaultAddr, "TCP listen address")
    fs.Parse(args)

    ln, err := net.Listen("tcp", *addr)
    if err != nil {
        log.Fatalf("honk server: listen %s: %v", *addr, err)
    }
    defer ln.Close()
    log.Printf("honk server: listening on %s (draft-g00se-honk-00)", *addr)

    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Printf("honk server: accept: %v", err)
            continue
        }
        go serveConn(conn)
    }
}

func serveConn(conn net.Conn) {
    defer conn.Close()
    remote := conn.RemoteAddr().String()
    log.Printf("honk server: connection from %s", remote)

    buf := make([]byte, MaxRequestBytes+2)
    sc := bufio.NewScanner(conn)
    sc.Buffer(buf, len(buf))

    priv := false
    first := true

    for sc.Scan() {
        line := bytes.TrimRight(sc.Bytes(), "\r")

        // Privacy Mode negotiation: always cleartext on the first line.
        if first {
            first = false
            if string(line) == PrivLine {
                priv = true
                fmt.Fprintf(conn, "%s\r\n", PrivLine)
                log.Printf("honk server: %s Privacy Mode active", remote)
                continue
            }
        }

        content := line
        if priv {
            content = xor(line)
        }

        if !utf8.ValidString(string(content)) {
            log.Printf("honk server: %s invalid UTF-8; closing", remote)
            return
        }

        n := honkCount(string(content))
        resp := buildResponse(n)

        if priv {
            conn.Write(append(xor([]byte(resp)), '\r', '\n'))
        } else {
            fmt.Fprintf(conn, "%s\r\n", resp)
        }

        log.Printf("honk server: %s priv=%v words=%d honks=%d",
            remote, priv, wordCount(string(content)), n)
    }

    if err := sc.Err(); err != nil {
        log.Printf("honk server: %s: %v", remote, err)
    }
    log.Printf("honk server: %s closed", remote)
}

// ---- Client ---------------------------------------------------------------

func runClient(args []string) {
    fs := flag.NewFlagSet("send", flag.ExitOnError)
    addr := fs.String("addr", DefaultAddr, "server address")
    priv := fs.Bool("priv", false, "use Privacy Mode")
    fs.Parse(args)

    conn, err := net.Dial("tcp", *addr)
    if err != nil {
        log.Fatalf("honk client: connect %s: %v", *addr, err)
    }
    defer conn.Close()
    sc := bufio.NewScanner(conn)

    if *priv {
        fmt.Fprintf(conn, "%s\r\n", PrivLine)
        if !sc.Scan() {
            log.Fatalf("honk client: no priv ack")
        }
        if sc.Text() != PrivLine {
            log.Fatalf("honk client: unexpected priv ack: %q", sc.Text())
        }
        log.Printf("honk client: Privacy Mode active (K=0x48)")
    }

    send := func(text string) {
        if *priv {
            conn.Write(append(xor([]byte(text)), '\r', '\n'))
        } else {
            fmt.Fprintf(conn, "%s\r\n", text)
        }
        if !sc.Scan() {
            log.Fatalf("honk client: connection closed before response")
        }
        resp := sc.Text()
        if *priv {
            resp = string(xor([]byte(resp)))
        }
        fmt.Println(resp)
    }

    if rest := fs.Args(); len(rest) > 0 {
        send(strings.Join(rest, " "))
        return
    }
    stdin := bufio.NewScanner(os.Stdin)
    for stdin.Scan() {
        send(stdin.Text())
    }
}

// ---- Protocol helpers -----------------------------------------------------

// xor applies single-byte XOR with K=0x48 to every byte of b.
// The transform is its own inverse.
func xor(b []byte) []byte {
    out := make([]byte, len(b))
    for i, v := range b {
        out[i] = v ^ XORKey
    }
    return out
}

// wordCount returns the number of Unicode-whitespace-delimited words.
func wordCount(s string) int {
    return len(strings.FieldsFunc(s, unicode.IsSpace))
}

// honkCount computes N per Section 4.4:
//   N = 2*W if W > 0; N = 3 if W == 0. Capped at MaxHonkCount.
func honkCount(s string) int {
    w := wordCount(s)
    if w == 0 {
        return DefaultHonks
    }
    n := 2 * w
    if n > MaxHonkCount {
        return MaxHonkCount
    }
    return n
}

// buildResponse returns n SP-separated HONK tokens.
func buildResponse(n int) string {
    tokens := make([]string, n)
    for i := range tokens {
        tokens[i] = "HONK"
    }
    return strings.Join(tokens, " ")
}
]]></sourcecode>
      <t>To build:</t>
      <sourcecode type="bash"><![CDATA[
$ go build -o honk ./honk.go
$ ./honk server
2026/04/25 00:00:00 honk server: listening on :24565 (draft-g00se-honk-00)

$ ./honk send "hello world"
HONK HONK HONK HONK

$ ./honk send -priv "hello world"
HONK HONK HONK HONK
]]></sourcecode>
    </section>
  </back>

</rfc>
