:title: Raw Sockets and Packet Layouts in Unicon
:author: Jafar Al-Gharaibeh
:trnumber: 29
:date: July 2026
:copyright: 2026, Jafar Al-Gharaibeh
:abstract: Unicon already opened TCP and UDP sockets through
   open(), but IP-level protocols such as ICMP and IGMP were not
   supported. This report describes raw sockets:
   mode "nr" opens SOCK_RAW, proto= selects the IP protocol
   number, and writes()/receive() carry upper-layer bytes on the
   same datagram path UDP already uses. The runtime does not
   assemble protocol payloads. Package net supplies PacketSpec,
   a declarative engine for fixed binary layouts, plus ICMP echo
   and IGMPv2/v3 classes and session wrappers.
:keywords: Unicon, raw sockets, SOCK_RAW, ICMP, IGMP, PacketSpec,
   runtime, technical report.
:docclass: report

.. _sec-intro:

1. Introduction
===============

This report describes raw IP sockets in Unicon: how a handle is
opened, how datagrams are sent and received, how protocol bytes
are built and decoded, and what remains out of scope. It is
formatted as a Unicon Technical Report :cite:`Jeffery:UTR15`.
Socket attributes (``ttl``, ``iface``, ``join``, and the rest)
are the multicast facility :cite:`AlGharaibeh:UTR28`; this
report adds mode ``nr`` and the packet library on top of that.
The language-facing reference lives in *Programming with Unicon*
:cite:`Jeffery:PwU` (Chapter 5, "Raw Sockets" and "PacketSpec
and package net"; Chapter 14, ``uping`` and ``uigmp``) and the
language reference (``open`` mode ``nr``, attributes ``proto``
and ``hdrincl``, subscript peek). Automated plumbing and peek tests live in
``tests/posix/raw_sock.icn``.

Raw sockets are part of the POSIX network facility.
A build without POSIX networking has no ``n`` modes at all.
Opening ``SOCK_RAW`` usually needs elevated
privileges (root or administrator); that is an operating-system
restriction, not a Unicon one.

.. _sec-motivation:

2. Motivation
=============

TCP and UDP cover most application protocols. Diagnostic and
control traffic sits one layer down: ICMP echo :cite:`RFC792`,
IGMP membership :cite:`RFC2236` :cite:`RFC3376`, OSPF, GRE,
PIM. Those messages are IP protocol payloads, not port-addressed
transport streams. ICMP, IGMP, and other raw IP protocols
were not supported.

The rest of Unicon networking already treats ``open()`` as the
constructor and ``Attrib()`` as the option verb. Raw sockets
follow that pattern instead of adding ``rawsocket()`` or a
parallel packet API. Payload construction is a library problem:
the runtime delivers bytes, and ``PacketSpec`` names the fields.

.. _sec-principles:

3. Design principles
====================

**``r`` after ``n`` is the socket type, like ``u`` for UDP.**
Mode letter ``r`` remains ordinary read mode unless a preceding
``n`` has already marked the handle as a network socket. Then
``"nr"`` means ``SOCK_RAW``, the same way ``"nu"`` means
``SOCK_DGRAM``. ``"rn"`` is not a raw socket: ``r`` is consumed
as read mode before ``n`` is seen.

**``proto=`` is required and is fixed at ``socket()`` time.**
The third argument to ``socket(2)`` is the IP protocol number.
There is no useful default: ICMP, IGMP, and OSPF are different
sockets. A missing, empty, or unknown ``proto=`` is error 1310
(``bad socket attribute``). ``f["proto"]`` peeks the protocol
number stored at ``open()``; ``Attrib()`` cannot change it.

**The runtime does not build protocol payloads.** Writes send
whatever bytes the caller supplies. Receives return whatever the
kernel delivered, often including an IPv4 header on Linux.
``import net`` and ``PacketSpec`` (or hand-assembled strings)
fill that gap.

**Datagram I/O matches UDP.** Raw sockets are not connected.
``writes()`` uses ``sendto(2)`` with the destination saved at
``open()``. Incoming packets use ``receive()``, which returns a
record with ``addr``, ``msg``, ``saddr``, ``daddr``, and (for IPv4 raw
datagrams that include a header) ``ttl`` and ``proto``.
Those extra fields are also available as ``r.ttl`` /
``r.saddr``. Use ``receive()``, not ``read()`` / ``reads()``.

**Two layers for callers.** Session wrappers (``Ping``,
``Igmp``) hide mode letters for the common cases. PacketSpec
classes are there when the program must craft or classify bytes
itself.

.. _sec-open:

4. Opening a raw socket
=======================

.. code-block:: unicon

   f := open("8.8.8.8", "nr", "proto=icmp", "ttl=64") |
        stop(&errortext)

The first argument is a bare host or address, not
``host:port``. Raw destinations are hosts. A single
``host:port`` form is accepted when there is exactly one colon
and a non-empty port, but ICMP and IGMP do not use ports. IPv6
literals contain colons; they are treated as hosts and are not
split as ``host:port``.

``proto=`` takes a symbolic name or an integer 0--255:

.. list-table::
   :header-rows: 1

   * - Name
     - Protocol
   * - ``icmp``
     - ``IPPROTO_ICMP``
   * - ``icmpv6`` / ``icmp6``
     - ``IPPROTO_ICMPV6`` (fails if the host has no constant)
   * - ``igmp``
     - ``IPPROTO_IGMP``
   * - ``tcp`` / ``udp``
     - ``IPPROTO_TCP`` / ``IPPROTO_UDP``
   * - ``gre``
     - 47
   * - ``ospf``
     - 89
   * - ``pim``
     - ``IPPROTO_PIM``
   * - ``raw``
     - ``IPPROTO_RAW``
   * - ``0`` .. ``255``
     - that protocol number

Names are case-insensitive. Unknown names and numbers outside
0--255 fail at ``open()`` with 1310.

If the kernel refuses ``SOCK_RAW``, ``open()`` fails and
``&errortext`` is the system message (typically "Operation not
permitted" or "Permission denied"). That is success of the
Unicon plumbing and a privilege problem on the host.

.. code-block:: unicon

   procedure main()
      local f
      if f := open("8.8.8.8", "nr") then
         write("oops missing proto")
      else
         write("missing-proto: ", &errortext)
      if f := open("8.8.8.8", "nr", "proto=nope") then
         write("oops bad proto")
      else
         write("bad-proto: ", &errortext)
      if f := open("8.8.8.8", "nr", "proto=icmp") then
         write("raw-icmp: ok")
      else
         write("raw-icmp: ", &errortext)
   end

Output::

   missing-proto: bad socket attribute
   bad-proto: bad socket attribute
   raw-icmp: Operation not permitted

The last line is the unprivileged case. With privileges it
prints ``raw-icmp: ok`` (or the test suite's ``plumbing-ok``).

.. _sec-attrs:

5. Attributes and status peek
=============================

Trailing ``open()`` arguments are the same ``name=value``
socket attributes used for TCP and UDP :cite:`AlGharaibeh:UTR28`.
Booleans are exactly ``yes`` or ``no``. ``ttl`` and ``iface``
were named for this sharing: hop limit and interface are not
multicast-only knobs.

.. list-table::
   :header-rows: 1

   * - Attribute
     - Role on a raw socket
   * - ``proto=``
     - Required. Consumed at ``socket()``. Peek with
       ``f["proto"]``; ``Attrib()`` cannot change it.
   * - ``hdrincl=yes``
     - Sets ``IP_HDRINCL`` so writes may include a complete IPv4
       header. Optional; ordinary ICMP/IGMP leave it off.
   * - ``ttl=``
     - Unicast and multicast hop limits (``IP_TTL`` /
       ``IP_MULTICAST_TTL``, or the IPv6 hop options).
   * - ``rcvbuf=`` / ``sndbuf=``
     - Socket buffer sizes in bytes.
   * - ``broadcast=`` / ``mcastloop=``
     - Same meaning as UDP.
   * - ``join=`` / ``leave=`` / ``source=`` / ``iface=``
     - Multicast membership, including SSM ``source@group``
       forms. Useful when sniffing IGMP on a group.

``Attrib(f, "ttl=4")`` sets hop limits after ``open()``.
Status uses the same ``[ ]`` / ``key()`` get as UDP
:cite:`AlGharaibeh:UTR28`: unknown names raise 1310, unpopulated
fields fail. Boolean peek fields that answered succeed with
``"yes"`` or ``&null`` (never ``"no"``); that is output only --
``open()`` and ``Attrib()`` still take ``yes`` or ``no``.
Peeking a closed handle is error 174. ``key(f)`` that has not
yet produced a name also raises 174 if the handle is already
closed; after the first suspend, ``close()`` makes the generator
fail instead of raising. Raw sockets add ``proto`` (fixed at
``socket()``) and ``hdrincl``.

.. list-table::
   :header-rows: 1

   * - Name
     - Peek value
   * - ``proto``
     - IP protocol number from ``proto=``: ``1`` (``icmp``),
       ``2`` (``igmp``), ``89`` (``ospf``)
   * - ``hdrincl``
     - ``"yes"`` or ``&null`` (not ``"no"``)
   * - ``ttl``
     - Hop limit (integer), e.g. ``64``
   * - ``iface``
     - Outbound / join interface, e.g. ``127.0.0.1``
   * - ``groups``
     - List of joined groups, e.g. ``239.1.1.1``; unpopulated
       until ``join=``
   * - ``mcastloop`` / ``reuseaddr`` / ``reuseport`` /
       ``broadcast``
     - ``"yes"`` or ``&null`` (not ``"no"``)
   * - ``rcvbuf`` / ``sndbuf``
     - Buffer sizes in bytes (integers)

.. code-block:: unicon

   f := open("127.0.0.1", "nr", "proto=icmp") | stop(&errortext)
   write("proto=", f["proto"])
   write("ttl=", f["ttl"] | "unpopulated")
   every k := key(f) do
      write("field ", k)
   close(f)

.. _sec-io:

6. Send and receive
===================

.. code-block:: unicon

   writes(f, icmp_bytes)
   r := receive(f)
   write(r.addr, "  ", *r.msg, " bytes")

``writes()`` calls ``sock_write()``, which for ``SOCK_RAW`` (and
UDP) does ``sendto`` to the ``addrinfo`` saved at ``open()``.
There is no ``connect(2)``. Changing the destination means
opening another handle (or, with ``hdrincl=yes``, putting the
destination in a supplied IP header).

``receive()`` peeks with ``MSG_PEEK`` into a 2K buffer, then
consumes with ``recvfrom``. If the peek fills 2K, the consume
uses a 64K buffer so a large datagram is not truncated. The
result is a ``posix_message``: ``addr`` is the peer (often
``a.b.c.d:0`` for raw IPv4), ``msg`` is the byte string.
``saddr`` / ``daddr`` use the usual IP-header names: on an IPv4
raw receive they are the header source and destination; on UDP,
``saddr`` is the numeric peer address and ``daddr`` is
unpopulated. When the receive buffer starts
with an IPv4 header, ``ttl`` and ``proto`` are that header's
hop limit and protocol; otherwise those fields are null.

On many IPv4 stacks a raw receive buffer starts with the IP
header, then the protocol message. Linux ICMP is the usual
example. Strip it before decoding ICMP or IGMP
(:ref:`section 7 <sec-packetspec>`). IPv6 raw sockets typically
deliver the upper layer only; there is no IPv6 header-strip
helper yet.

``select()`` works on the handle. Session wrappers use it to
implement timeouts around ``receive()``.

As with UDP, ``read()`` / ``reads()`` are the wrong verbs for
datagrams. Use ``receive()``.

.. _sec-packetspec:

7. PacketSpec
=============

Module ``packetspec`` (package ``net``, ``uni/lib/packetspec.icn``)
is a small declarative engine for fixed binary layouts. Protocol
classes inherit ``PacketSpec`` and override ``define()``.

Layer-1 helpers pack and unpack network-order integers
(``u8`` / ``u16`` / ``u32`` / ``u64`` and the matching
``get_u*`` readers) and IPv4 addresses (``ipv4`` / ``to_ipv4`` /
``get_ipv4``). ``inet_cksum()`` is RFC 1071 :cite:`RFC1071`.

.. _sec-ps-fields:

7.1 Declaring a layout
----------------------

.. code-block:: unicon

   class IcmpEchoRequest : IcmpPacket()
      method define()
         field("type", 1, 8)
         field("code", 1, 0)
         checksum_field("checksum")
         field("id", 2)
         field("seq", 2)
      end
   end

``field(name, size, defval)`` is a network-order integer.
``bytes_field`` is opaque fixed-width bytes. ``ipv4_field``
accepts a dotted-quad, integer, or four raw bytes on build and
returns a dotted-quad on decode. ``checksum_field`` is a 16-bit
placeholder filled at ``build()`` time over the header plus
payload. Pass ``exclude_off`` / ``exclude_len`` (1-based) to omit
a range from the sum --- OSPF Authentication and PIM Register
are the documented cases; those layouts are not in the library
yet.

``require(name)`` marks a field that ``build()`` must see via
``set()`` or the optional extra table. ``relax()`` / ``build(...,
lax)`` skip that check for incomplete test packets. ``strict()``
and ``clear()`` turn checking back on.

.. _sec-ps-build:

7.2 Build, decode, describe
---------------------------

Preferred call shape (no string-keyed tables required). This
needs no raw socket:

.. code-block:: unicon

   import net

   procedure main()
      local pkt, v
      write(IcmpEchoRequest().describe())
      write(IcmpEchoRequest().set("id", 1).set("seq", 2).describe())
      pkt := IcmpEchoRequest().set("id", 1).set("seq", 2).build(, "hi")
      write("len=", *pkt)
      v := IcmpEchoRequest().decode(pkt)
      write("type=", v["type"], " id=", v["id"], " seq=", v["seq"])
   end

Output::

   type:int=8  code:int=0  checksum:checksum(auto)  id:int=0  seq:int=0
   type:int=8  code:int=0  checksum:checksum(auto)  id:int=1  seq:int=2
   len=10
   type=8 id=1 seq=2

``set()`` and ``set_payload()`` are chainable. ``build(extra,
payload, lax)`` overlays an optional table, appends payload, and
returns the complete byte string with checksum filled.
``decode(s)`` returns a table of field names to values.
``describe()`` is a one-line summary for the REPL: names, kinds,
defaults, required markers, and current ``set()`` values.

.. _sec-ps-ip:

7.3 IPv4 receive helpers
------------------------

``strip_ipv4(pkt)`` uses the header IHL to return the
upper-layer message. ``ipv4_ttl(pkt)`` and ``ipv4_src(pkt)``
read TTL and source from the same leading header. ICMP and IGMP
classes call ``strip_ipv4`` before matching.

.. _sec-icmp:

8. ICMP echo
============

``uni/lib/icmp.icn`` adds echo request (type 8) and echo reply
(type 0) :cite:`RFC792` on top of ``PacketSpec``:

::

   PacketSpec
     IcmpPacket          strip_ip / match
       IcmpEchoRequest   type 8; echo(id, seq, payload)
       IcmpEchoReply     type 0; match_echo(pkt, id, seq)
   Ping                  session wrapper

``Ping(host)`` opens ``proto=icmp`` with TTL 64 and a 2000 ms
default wait. ``echo()`` sends a request (56 zero bytes unless a
payload is given), waits with ``select()``, and succeeds with a
decode table plus ``rtt`` (milliseconds, real), ``rtt_us``,
``ttl`` (from the IPv4 header), and ``addr``. Round-trip time
uses ``gettimeofday()``.

.. code-block:: unicon

   import net

   procedure main()
      local p, v
      p := Ping("8.8.8.8") | stop(&errortext)
      write(p.describe())
      if v := p.echo() then
         write("rtt=", v["rtt"], " from ", v["addr"])
      p.close()
   end

``set_timeout(ms)`` changes the default wait. An explicit
sequence number and payload can be passed to ``echo()``;
otherwise the session increments ``seq``.

The same exchange with an explicit socket:

.. code-block:: unicon

   import net

   procedure main()
      local f, req, reply, id, pkt, r
      req := IcmpEchoRequest()
      reply := IcmpEchoReply()
      id := iand(?100000 | 1, 16rFFFF)
      f := open("8.8.8.8", "nr", "proto=icmp", "ttl=64") |
           stop(&errortext)
      pkt := req.set("id", id).set("seq", 1).build(, "unicon")
      writes(f, pkt)
      r := receive(f)
      if reply.match_echo(r.msg, id, 1) then
         write("reply from ", r.addr)
      close(f)
   end

Other ICMP types (destination unreachable, time exceeded, and so
on) are not in the library. ``proto=icmpv6`` opens an ICMPv6
socket, but there are no Neighbor Discovery or echo layouts yet.

.. _sec-igmp:

9. IGMP
=======

``uni/lib/igmp.icn`` covers IGMPv2 (RFC 2236) and IGMPv3 (RFC
3376):

::

   PacketSpec
     IgmpPacket
       Igmpv2Query / Igmpv2Report / Igmpv2Leave
       Igmpv3Query / Igmpv3Report
   Igmp                     session wrapper

IGMPv2 messages are an eight-octet header. Report and leave
``require()`` a group. IGMPv3 queries add flags, QQIC, and a
source list. IGMPv3 reports carry group records: ASM join/leave
and SSM source filters (``join_sources``, ``allow``,
``block``).

``Igmp()`` opens ``proto=igmp`` bound at ``0.0.0.0``.
``recv(ms)`` waits, then returns a table with ``kind``,
``addr``, ``msg``, ``len``, and decoded fields when recognized.
``kind`` is one of ``v3_report``, ``v3_query``, ``v2_query``,
``v2_report``, ``v2_leave``, or ``unknown``.

PacketSpec classes work without a raw socket:

.. code-block:: unicon

   import net

   procedure main()
      local pkt, v
      write(Igmpv2Report().describe())
      write(Igmpv3Report().describe())
      pkt := Igmpv2Report().report("239.1.1.1")
      write("v2 report len=", *pkt)
      v := Igmpv2Report().decode(pkt)
      write("type=", v["type"], " group=", v["group"])
      pkt := Igmpv3Report().join("239.1.1.1")
      write("v3 join len=", *pkt)
   end

Output::

   type:int=22  max_resp_time:int=0  checksum:checksum(auto)  group:ipv4(required)
   type:int=34  reserved1:int=0  checksum:checksum(auto)  reserved2:int=0  nrecords:int=0
   v2 report len=8
   type=22 group=239.1.1.1
   v3 join len=16

Opening a live IGMP socket needs the same privileges as ICMP:

.. code-block:: unicon

   import net

   procedure main()
      local g, v
      g := Igmp() | stop(&errortext)
      write(g.describe())
      if v := g.recv(1000) then
         write(v["kind"], " from ", v["addr"])
      g.report("239.1.1.1")    # IGMPv2 membership report
      g.join("239.1.1.1")      # IGMPv3 ASM join
      g.close()
   end

Session send helpers: ``report`` / ``leave`` / ``query`` (v2),
``join`` / ``leave3`` (v3 ASM), and ``send(bytes)`` for a
hand-built packet. SSM source-filter joins use the PacketSpec
class, not the session wrapper:

.. code-block:: unicon

   pkt := Igmpv3Report().join_sources("239.1.1.1", ["192.0.2.1"])
   g.send(pkt)

Exported constants (``IGMP_MEMBERSHIP_QUERY``,
``IGMP_V3_MEMBERSHIP_REPORT``, ``IGMP_CHANGE_TO_EXCLUDE_MODE``,
and the rest) are initialized on first ``IgmpPacket``
construction.

.. _sec-progs:

10. Sample programs
===================

``uni/progs/uping`` and ``uni/progs/uigmp`` are teaching clients,
not replacements for ``ping(8)`` or ``tcpdump(1)``. They need
``SOCK_RAW`` privileges:

.. code-block:: sh

   unicon uping
   unicon uigmp
   sudo ./uping host [count]
   sudo ./uigmp [seconds]
   sudo ./uping -v host    # also show open()/Ping API demos
   sudo ./uigmp -v         # also show open()/Igmp API demos

Default mode uses the session wrappers. ``-v`` / ``-d`` also
runs the lower-level ``open(..., "nr", ...)`` plus PacketSpec
path and prints ``describe()`` output.

.. code-block:: sh

   sudo ./uping 192.0.2.1

Output::

   PING 192.0.2.1 (192.0.2.1): 56 data bytes
   64 bytes from 192.0.2.1: icmp_seq=0 ttl=118 time=18.390 ms
   64 bytes from 192.0.2.1: icmp_seq=1 ttl=118 time=16.245 ms
   64 bytes from 192.0.2.1: icmp_seq=2 ttl=118 time=15.533 ms
   64 bytes from 192.0.2.1: icmp_seq=3 ttl=118 time=17.311 ms
   --- 192.0.2.1 ping statistics ---
   4 packets transmitted, 4 packets received, 0.0% packet loss
   round-trip min/avg/max = 15.533/16.869/18.390 ms

.. code-block:: sh

   sudo ./uigmp

Output::

   listening on IGMP

   36 bytes from 192.0.2.10: IGMPv3 query group=0.0.0.0 max_resp=100 qrv=2
   36 bytes from 192.0.2.10: IGMPv3 query group=239.1.1.1 max_resp=100 qrv=2
   36 bytes from 192.0.2.20: IGMPv3 report {239.1.1.1 CHANGE_TO_EXCLUDE}

.. _sec-impl:

11. Runtime implementation
==========================

The work is in the existing POSIX socket code, not a new file
type.

**Mode letter.** ``src/runtime/fsys.r``: after ``n`` has set
``Fs_Socket``, ``r`` / ``R`` sets ``sock_type = SOCK_T_RAW`` and
does not set the ordinary read bit. Without ``Fs_Socket``,
``r`` remains ``Fs_Read``. ``e`` (crypto raw-key material) is a
later check on the same letter; ``"nr"`` is a socket, ``"er"``
is a crypto handle.

**Create.** ``src/runtime/rposix.r``: ``sock_connect`` (and the
listen/bind path) require ``proto=`` before ``socket()``.
``uni_getaddrinfo`` allows a missing port for ``SOCK_T_RAW``.
The real protocol number is passed as the third argument to
``socket()``; ``getaddrinfo`` is told protocol 0 so it does not
override it.

**I/O.** UDP and raw share ``saddrs[]``: the ``addrinfo`` from
``open()`` is kept until ``sock_close()``. ``sock_write``
``sendto``s there. ``sock_recv`` accepts ``SOCK_DGRAM`` and
``SOCK_RAW``. ``proto=`` is skipped in ``setsockopt`` (already
consumed). ``hdrincl=`` sets ``IP_HDRINCL``.

**Errors.** Unknown or missing socket attributes use error 1310
with the offending string (``proto``, ``proto=nope``,
``hdrincl=maybe``). Kernel failures use the system
``&errortext``.

There is no ``&features`` flag named "raw sockets". If POSIX
sockets exist, mode ``nr`` exists.

.. _sec-status:

12. Status and remaining work
=============================

The language surface described here is implemented: ``"nr"``,
required ``proto=``, ``hdrincl=``, datagram ``writes`` /
``receive``, PacketSpec, ICMP echo, IGMPv2/v3, ``uping``, and
``uigmp``.

**More PacketSpec layouts.** GRE, OSPF, and PIM names are
accepted at ``socket()`` and the checksum helper documents their
exclude ranges, but there are no protocol classes yet.

**ICMPv6.** ``proto=icmpv6`` opens the socket. Echo, Neighbor
Discovery, and an IPv6 header strip are not in ``net``.

**IPv6 receive metadata.** ``strip_ipv4`` / ``ipv4_ttl`` assume
a leading IPv4 header. IPv6 raw receives need their own helpers.

**``read()`` on raw handles.** UDP clears the read bit so
``read()`` fails. Raw currently keeps ``Fs_Read`` because it
shares the TCP-like status assignment. The documented API is
still ``receive()``. Aligning the status bits with UDP would
make misuse fail earlier.

**Privileged tests.** ``tests/posix/raw_sock.icn`` checks
plumbing without sending packets. A live ICMP echo test would
need root in CI and a reachable responder; it is not in the
suite.

**Windows.** Opening ``SOCK_RAW`` is possible with
administrator rights, but Windows historically restricts which
protocols a raw socket may use. Treat ``uping`` / ``uigmp`` as
UNIX-first tools.

**Header inclusion.** ``hdrincl=yes`` is wired. There is no
PacketSpec IPv4 header class to go with it; callers who craft
full datagrams build those bytes themselves.

.. _sec-coverage:

Appendix: test coverage
=======================

``tests/posix/raw_sock.icn`` does not require ``SOCK_RAW``
success. It checks that:

- missing, empty, and unknown ``proto=`` fail with 1310
- ``proto=icmp``, ``gre``, ``ospf``, and numeric ``89`` reach
  ``socket()`` (success or a privilege denial both print
  ``plumbing-ok``)
- ``hdrincl=maybe`` is rejected
- without a preceding ``n``, mode ``"r"`` still opens a file
  for reading
- when ``SOCK_RAW`` is allowed, ``f["proto"]`` is 1 after
  ``proto=icmp`` and ``key(f)`` yields populated names
  (privilege denial still prints ``raw-peek:ok`` /
  ``raw-key:ok`` so the ``.std`` stays portable)

Expected output is ``tests/posix/stand/raw_sock.std``:

::

   missing-proto: bad socket attribute
   bad-proto: bad socket attribute
   empty-proto: bad socket attribute
   raw-icmp: plumbing-ok
   raw-gre: plumbing-ok
   raw-ospf: plumbing-ok
   raw-numeric: plumbing-ok
   bad-hdrincl: bad socket attribute
   read-mode-r: ok
   raw-peek:ok
   raw-key:ok

On an unprivileged account the ``raw-*`` lines still print
``plumbing-ok``: the test treats both a successful ``socket()``
and a privilege denial as passing plumbing. The posix suite
picks up every ``*.icn`` in that directory.

References
==========

.. bibliography:: utr29.bib
