From 800239028440cbdd82b7c04f019e687c4317d572 Mon Sep 17 00:00:00 2001 From: Emery Hemingway Date: Mon, 1 May 2023 22:26:19 +0100 Subject: [PATCH] Initial commit --- .envrc | 2 + .gitignore | 1 + README.md | 6 + Tuprules.tup | 2 + acpi.prs | 3 + acpi_actor.nimble | 13 ++ src/Tupfile | 2 + src/acpi_actor.nim | 50 ++++++++ src/netlink.nim | 309 +++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 388 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 README.md create mode 100644 Tuprules.tup create mode 100644 acpi.prs create mode 100644 acpi_actor.nimble create mode 100644 src/Tupfile create mode 100644 src/acpi_actor.nim create mode 100644 src/netlink.nim diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..abb2b41 --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +source_env .. +use flake work#battery_actor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ad6275 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.direnv diff --git a/README.md b/README.md new file mode 100644 index 0000000..207aee3 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# acpi_actor +A Syndicate actor for publishing Linux ACPI events as well as an example of interacting with Linux generic netlink sockets in pure Nim (Linux C headers aside). + +Not very useful. I thought that I would get battery capacity information over netlink but apparently that information is polled from files in /sys despite any stated deprecations. + +The lesson learned is that Netlink is of the sort of quality to be expected from the Linux kernel. diff --git a/Tuprules.tup b/Tuprules.tup new file mode 100644 index 0000000..f902ead --- /dev/null +++ b/Tuprules.tup @@ -0,0 +1,2 @@ +include ../syndicate-nim/depends.tup +NIM_FLAGS += --path:$(TUP_CWD)/../syndicate-nim/src diff --git a/acpi.prs b/acpi.prs new file mode 100644 index 0000000..bf8ae77 --- /dev/null +++ b/acpi.prs @@ -0,0 +1,3 @@ +version 1. + +AcpiEvent = . diff --git a/acpi_actor.nimble b/acpi_actor.nimble new file mode 100644 index 0000000..e4e93b2 --- /dev/null +++ b/acpi_actor.nimble @@ -0,0 +1,13 @@ +# Package + +version = "20230502" +author = "Emery Hemingway" +description = "Syndicate actor for publishing Linux ACPI events" +license = "Unlicense" +srcDir = "src" +bin = @["acpi_actor"] + + +# Dependencies + +requires "nim >= 1.6.12", "syndicate" diff --git a/src/Tupfile b/src/Tupfile new file mode 100644 index 0000000..795bd67 --- /dev/null +++ b/src/Tupfile @@ -0,0 +1,2 @@ +include_rules +: acpi_actor.nim | $(SYNDICATE_PROTOCOL) |> !nim_bin |> diff --git a/src/acpi_actor.nim b/src/acpi_actor.nim new file mode 100644 index 0000000..86f6da3 --- /dev/null +++ b/src/acpi_actor.nim @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: ☭ Emery Hemingway +# SPDX-License-Identifier: Unlicense + +import std/asyncdispatch +import preserves, syndicate +import ./netlink + +type AcpiGenlEvent {.packed, preservesRecord: "acpi_event".} = object + ## Found in the Linux source, see linux/drivers/acpi/event.c + device_class: array[20, char] + bus_id: array[15, char] + `type`, data: uint32 + +proc toPreserveHook(chars: openarray[char]; E = void): Preserve[E] = + ## Hack to convert fixed-width character strings to Preserves strings. + result = Preserve[E](kind: pkString, string: newStringOfCap(len(chars))) + for c in chars: + if c == '\0': break + add(result.string, c) + +proc recvAcpiEvent(nls: NetlinkSocket; family: uint16): AcpiGenlEvent = + var msg = recvMsg(nls) + if msg.hdr.n.nlmsg_type == family: + var parser: NlattrParser + while parse(parser, msg): + # If there is a label on these events then I haven't found it. + if copyObj(result, parser): return + next(parser) + +proc relayEvents(ds: Ref; facet: Facet) = + var info: MulticastInfo + block: + let nls = openSocket() + info = resolveMulticastInfo(nls, "acpi_event\0") + close(nls) + let mcast = openSocket(int info.mcastGrpId) + proc relayEvent = + let event = recvAcpiEvent(mcast, info.familyId) + run(facet) do (turn: var Turn): + message(turn, ds, event) + callSoon: relayEvent() + callSoon: relayEvent() + +# TODO seccomp + +bootDataspace("main") do (ds: Ref; turn: var Turn): + connectStdio(ds, turn) + relayEvents(ds, turn.facet) + +runForever() diff --git a/src/netlink.nim b/src/netlink.nim new file mode 100644 index 0000000..31114de --- /dev/null +++ b/src/netlink.nim @@ -0,0 +1,309 @@ +# SPDX-FileCopyrightText: ☭ Emery Hemingway +# SPDX-License-Identifier: Unlicense + +# https://kernel.org/doc/html/next/userspace-api/netlink/intro.html + +import std/[nativesockets, net, os, posix] + +var + AF_NETLINK* {.importc, header: "sys/socket.h", nodecl.}: uint16 + SOL_NETLINK {.importc, header: "", nodecl.}: cint + + +{.pragma: netlinkNodecl, importc, header: "linux/netlink.h", nodecl.} + +type Family* = distinct cint +var + NETLINK_ROUTE* {.netlinkNodecl.}: Family + NETLINK_W1* {.netlinkNodecl.}: Family + NETLINK_USERSOCK* {.netlinkNodecl.}: Family + NETLINK_FIREWALL* {.netlinkNodecl.}: Family + NETLINK_SOCK_DIAG* {.netlinkNodecl.}: Family + NETLINK_INET_DIAG* {.netlinkNodecl.}: Family + NETLINK_NFLOG* {.netlinkNodecl.}: Family + NETLINK_XFRM* {.netlinkNodecl.}: Family + NETLINK_SELINUX* {.netlinkNodecl.}: Family + NETLINK_ISCSI* {.netlinkNodecl.}: Family + NETLINK_AUDIT* {.netlinkNodecl.}: Family + NETLINK_FIB_LOOKUP* {.netlinkNodecl.}: Family + NETLINK_CONNECTOR* {.netlinkNodecl.}: Family + NETLINK_NETFILTER* {.netlinkNodecl.}: Family + NETLINK_SCSITRANSPORT* {.netlinkNodecl.}: Family + NETLINK_RDMA* {.netlinkNodecl.}: Family + NETLINK_IP6_FW* {.netlinkNodecl.}: Family + NETLINK_DNRTMSG* {.netlinkNodecl.}: Family + NETLINK_KOBJECT_UEVENT* {.netlinkNodecl.}: Family + NETLINK_GENERIC* {.netlinkNodecl.}: Family + NETLINK_CRYPTO* {.netlinkNodecl.}: Family + + NETLINK_ADD_MEMBERSHIP* {.importc, header: "linux/netlink.h", nodecl.}: int + NETLINK_DROP_MEMBERSHIP* {.importc, header: "linux/netlink.h", nodecl.}: int + NETLINK_PKTINFO* {.importc, header: "linux/netlink.h", nodecl.}: int + NETLINK_BROADCAST_ERROR* {.importc, header: "linux/netlink.h", nodecl.}: int + NETLINK_NO_ENOBUFS* {.importc, header: "linux/netlink.h", nodecl.}: int + NETLINK_RX_RING* {.importc, header: "linux/netlink.h", nodecl.}: int + NETLINK_TX_RING* {.importc, header: "linux/netlink.h", nodecl.}: int + NETLINK_LISTEN_ALL_NSID* {.importc, header: "linux/netlink.h", nodecl.}: int + NETLINK_LIST_MEMBERSHIPS* {.importc, header: "linux/netlink.h", nodecl.}: int + NETLINK_CAP_ACK* {.importc, header: "linux/netlink.h", nodecl.}: int + NETLINK_EXT_ACK* {.importc, header: "linux/netlink.h", nodecl.}: int + NETLINK_GET_STRICT_CHK* {.importc, header: "linux/netlink.h", nodecl.}: int + +type MessageType* = distinct uint16 +var + NLMSG_NOOP* {.netlinkNodecl.}: MessageType + NLMSG_ERROR* {.netlinkNodecl.}: MessageType + NLMSG_DONE* {.netlinkNodecl.}: MessageType + +func `==`(a, b: MessageType): bool {.borrow.} + +type Flags* = distinct uint16 +var + NLM_F_REQUEST* {.netlinkNodecl.}: Flags + NLM_F_MULTI* {.netlinkNodecl.}: Flags + NLM_F_ACK* {.netlinkNodecl.}: Flags + NLM_F_ECHO* {.netlinkNodecl.}: Flags + NLM_F_ROOT* {.netlinkNodecl.}: Flags + NLM_F_MATCH* {.netlinkNodecl.}: Flags + NLM_F_ATOMIC* {.netlinkNodecl.}: Flags + NLM_F_DUMP* {.netlinkNodecl.}: Flags + NLM_F_REPLACE* {.netlinkNodecl.}: Flags + NLM_F_EXCL* {.netlinkNodecl.}: Flags + NLM_F_CREATE* {.netlinkNodecl.}: Flags + NLM_F_APPEND* {.netlinkNodecl.}: Flags + +proc `or`*(a, b: Flags): Flags {.borrow.} + +type + Sockaddr_nl {.importc: "struct sockaddr_nl", header: "linux/netlink.h".} = object + nl_family: uint16 + nl_pad: uint16 + nl_pid: uint32 + nl_groups: uint32 + +proc initSockaddr: Sockaddr_nl = + Sockaddr_nl(nl_family: AF_NETLINK) + +proc saddr(sa_nl: var Sockaddr_nl): ptr Sockaddr = + cast[ptr Sockaddr](addr sa_nl) + +type + Nlmsghdr {.importc: "struct nlmsghdr", header: "linux/netlink.h", completeStruct.} = object + nlmsg_len*: uint32 + nlmsg_type*: uint16 + nlmsg_flags*: Flags + nlmsg_seq*: uint32 + nlmsg_pid*: uint32 + + Nlmsgerr* {.importc: "struct nlmsgerr", header: "linux/netlink.h".} = object + error*: cint + msg*: Nlmsghdr + + Nlattr* {.importc: "struct nlattr", header: "linux/netlink.h", completeStruct.} = object + nla_len*, nla_type*: uint16 + + Genlmsghdr* {.importc: "struct genlmsghdr", header: "linux/genetlink.h", completeStruct.} = object + cmd*, version*: uint8 + reserved: uint16 + +{.pragma: genetlink, importc, header: "linux/genetlink.h", nodecl.} +var + GENL_ID_CTRL* {.genetlink.}: MessageType + + CTRL_CMD_UNSPEC* {.genetlink.}: uint8 + CTRL_CMD_NEWFAMILY* {.genetlink.}: uint8 + CTRL_CMD_DELFAMILY* {.genetlink.}: uint8 + CTRL_CMD_GETFAMILY* {.genetlink.}: uint8 + CTRL_CMD_NEWOPS* {.genetlink.}: uint8 + CTRL_CMD_DELOPS* {.genetlink.}: uint8 + CTRL_CMD_GETOPS* {.genetlink.}: uint8 + CTRL_CMD_NEWMCAST_GRP* {.genetlink.}: uint8 + CTRL_CMD_DELMCAST_GRP* {.genetlink.}: uint8 + CTRL_CMD_GETMCAST_GRP* {.genetlink.}: uint8 ## unused + CTRL_CMD_GETPOLICY* {.genetlink.}: uint8 + + CTRL_ATTR_UNSPEC* {.genetlink.}: uint16 + CTRL_ATTR_FAMILY_ID* {.genetlink.}: uint16 + CTRL_ATTR_FAMILY_NAME* {.genetlink.}: uint16 + CTRL_ATTR_VERSION* {.genetlink.}: uint16 + CTRL_ATTR_HDRSIZE* {.genetlink.}: uint16 + CTRL_ATTR_MAXATTR* {.genetlink.}: uint16 + CTRL_ATTR_OPS* {.genetlink.}: uint16 + CTRL_ATTR_MCAST_GROUPS* {.genetlink.}: uint16 + CTRL_ATTR_POLICY* {.genetlink.}: uint16 + CTRL_ATTR_OP_POLICY* {.genetlink.}: uint16 + CTRL_ATTR_OP* {.genetlink.}: uint16 + + CTRL_ATTR_MCAST_GRP_UNSPEC* {.genetlink.}: uint16 + CTRL_ATTR_MCAST_GRP_NAME* {.genetlink.}: uint16 + CTRL_ATTR_MCAST_GRP_ID* {.genetlink.}: uint16 + +type NetlinkSocket* = ref object + saLocal, saPeer: Sockaddr_nl + sock: Socket + seqNum: uint32 + +proc openSocket*(multicastGroups: varargs[int]): NetlinkSocket = + result = NetlinkSocket( + saLocal: initSockaddr(), + saPeer: initSockaddr(), + sock: newSocket(cint AF_NETLINK, posix.SOCK_RAW, cint NETLINK_GENERIC) + ) + let fd = getFd(result.sock) + if bindAddr(fd, saddr result.saLocal, SockLen sizeof(result.saLocal)) != 0: + close(result.sock) + raiseOSError(osLastError(), "failed to bind Netlink socket") + + var + check: Sockaddr_nl + checkSize = SockLen sizeof(check) + if getsockname(fd, saddr check, addr checkSize) != 0: + # check what kind of socket the kernel has bound + close(result.sock) + raiseOSError(osLastError(), "failed to examine Netlink socket") + if checkSize.int != sizeof(check): + close(result.sock) + raise newException(IOError, "bad socket len") + if result.saLocal.nl_family != AF_NETLINK: + close(result.sock) + raise newException(IOError, "Netlink not supported") + result.saLocal = check + for group in multicastGroups: + setSockOptInt(fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, group) + +proc close*(nls: NetlinkSocket) = + close(nls.sock) + +type + GenericHeader {.packed.} = object + n*: Nlmsghdr + g*: Genlmsghdr + + GenericMessage* {.union.} = object + hdr*: GenericHeader + buf: array[4096, byte] + +proc initGenericMessage: GenericMessage = + result.hdr.n.nlmsg_len = uint32 sizeOf(GenericHeader) + +func len(msg: GenericMessage): int {.inline.} = int msg.hdr.n.nlmsg_len + +proc incLen(msg: var GenericMessage; n: Natural) = + msg.hdr.n.nlmsg_len = (msg.hdr.n.nlmsg_len + n.uint32 + 3) and (not 3'u32) + doAssert(len(msg) < sizeOf(GenericMessage)) + +proc addAttr(msg: var GenericMessage; ctrl: uint16; data: openarray[char]) = + let + off = len(msg) + len = sizeOf(Nlattr) + data.len + if off + len > sizeOf(msg): + raise newException(ResourceExhaustedError, "netlink message overflow") + let attr = cast[ptr Nlattr](addr msg.buf[off]) + attr.nla_len = uint16 len + attr.nla_type = ctrl + copyMem(addr msg.buf[off+sizeOf(Nlattr)], unsafeAddr data[0], data.len) + incLen(msg, attr.nla_len) + +type NlattrParser* = object + off: int + attr*: Nlattr + data: pointer + +proc parse*(parser: var NlattrParser; msg: GenericMessage): bool = + if parser.off == 0: parser.off = sizeOf(GenericHeader) + if parser.off+sizeOf(Nlattr) <= msg.len: + copyMem(addr parser.attr, unsafeAddr msg.buf[parser.off], sizeOf(parser.attr)) + if parser.off+parser.attr.nla_len.int <= msg.len: + parser.data = msg.buf[parser.off+sizeof(Nlattr)].unsafeAddr.pointer + result = true + +proc next*(parser: var NlattrParser) = + inc(parser.off, (parser.attr.nla_len.int + 3) and (not 3)) + +func len*(attr: Nlattr|ptr Nlattr): int {.inline.} = attr.nla_len.int - sizeOf(Nlattr) + +proc copyString*(s: var string; parser: NlattrParser): bool = + let n = parser.attr.len + result = n > 0 + if result: + setLen(s, parser.attr.len) + copyMem(addr s[0], parser.data, s.len) + +proc copyNum*(i: var SomeInteger; parser: NlattrParser): bool = + let n = parser.attr.len + result = n <= sizeOf(i) + if result: + reset(i) + copyMem(addr i, parser.data, n) + +proc copyObj*[T](x: var T; parser: NlattrParser): bool = + let n = parser.attr.len + result = sizeOf(T) <= n + if result: copyMem(addr x, parser.data, sizeOf(T)) + +proc send(nls: NetlinkSocket; buf: pointer; len: int): int = + result = sendTo( + nls.sock.getFd, + buf, len, 0, + saddr nls.saPeer, SockLen sizeOf(nls.saPeer)) + if result < 0: + raiseOSError(osLastError(), "sendTo Netlink socket failed") + +proc sendMsg*(nls: NetlinkSocket; msg: var GenericMessage) = + inc(nls.seqNum) + msg.hdr.n.nlmsg_seq = nls.seqNum + if send(nls, msg.addr, msg.len) != msg.len: + raise newException(IOError, "Netlink short send") + +proc recvMsg*(nls: NetlinkSocket): GenericMessage = + var + sa = initSockaddr() + sl = SockLen sizeOf(sa) + let n = recvFrom( + nls.sock.getFd, + addr(result), sizeof(result), cint 0, + saddr sa, addr sl) + if n < 0 or result.hdr.n.nlmsg_len.int != n: + raiseOSError(osLastError(), "recvFrom Netlink socket failed") + +type MulticastInfo* = tuple + mcastGrpName: string + mcastGrpId: uint64 + familyId: uint16 + +proc resolveMulticastInfo*(nls: NetlinkSocket; family: string): MulticastInfo = + block: + var msg = initGenericMessage() + msg.hdr.n.nlmsg_type = uint16 GENL_ID_CTRL + msg.hdr.n.nlmsg_flags = NLM_F_REQUEST or NLM_F_ACK + msg.hdr.g.cmd = uint8 CTRL_CMD_GETFAMILY + addAttr(msg, CTRL_ATTR_FAMILY_NAME, family) + sendMsg(nls, msg) + + var reply = recvMsg(nls) + if reply.hdr.n.nlmsg_type != GENL_ID_CTRL.uint16: + raise newException(IOError, "Expected GENL_ID_CTRL") + if reply.hdr.g.cmd != CTRL_CMD_NEWFAMILY: + raise newException(IOError, "Expected CTRL_CMD_NEWFAMILY") + + var parser: NlattrParser + while parse(parser, reply): + if parser.attr.nla_type == CTRL_ATTR_FAMILY_NAME: + var name: string + if copyString(name, parser) and name != family: + raise newException(IOError, "Expected " & family & " but got " & name) + elif parser.attr.nla_type == CTRL_ATTR_FAMILY_ID: + if not copyNum(result.familyId, parser): + raise newException(IOError, "Failed to collect family id") + elif parser.attr.nla_type == CTRL_ATTR_MCAST_GROUPS.uint16: + var parser = parser + inc(parser.off, sizeof(Nlattr)*2) # why two nlattrs headers? + while parse(parser, reply): + if parser.attr.nla_type == CTRL_ATTR_MCAST_GRP_NAME: + if not copyString(result.mcastGrpName, parser): + raise newException(IOError, "Failed to collect multicast group name") + elif parser.attr.nla_type == CTRL_ATTR_MCAST_GRP_ID: + if not copyNum(result.mcastGrpId, parser): + raise newException(IOError, "Failed to collect multicast group id") + next(parser) + next(parser)