Network Card
A network card sends frames over the cables to another card’s address, or to every card it reaches. It has the shape of a real NIC.
Facts
| | |
|—|—|
| Type | network |
| Address | An expansion slot path such as exp:0 |
| Interrupt | Raised while frames wait; also a wake line |
| Power | 2 W at rest; a send costs a tenth of a tick of a running CPU |
| Frame size | At most 8192 bytes |
| Info | kind (network), wireless (false) |
The wireless cards, which send through the air, are on WLAN Cards; both kinds have the same methods. A wired card hears only wired cards, and a WLAN card only WLAN cards. Nothing routes between the two networks.
Addresses
A card’s address is its id (a MAC address burned into the card: its item’s serial number), without
the id: prefix. send takes it with or without the prefix. A frame goes to the card with that
address, or to all cards in reach (a broadcast).
A wired frame reaches the cards the cables reach from the card’s block, through hubs and screens and across Bluetooth links, ending at a computer. A cable is a shared fabric: two cases on one cable are two hosts on it. Two cards in one block are on the same wire, 0 meters apart.
Nothing is dropped
A frame sent to one card is taken, or the sender hears why not at once. The card never tries again; that is software’s job (mcos’s TCP does).
"busy": the other card’s queue is full, or no program on its computer is taking data."unreachable": no card at that address in reach, or it is off.
A broadcast is best effort: it says how many cards took it.
In mcos
mcos sends and receives through sockets (UDP and TCP), with Linux’s names for interfaces (eth0,
wlan0). Ports belong to the kernel, in the frame’s data: the card knows only addresses. See
Networking. A card’s address is shown by lsdev -i.
The card’s file is /dev/network0 (network1 for the next card, wired or not). It is its
traffic, as tcpdump shows a card’s: a line for each frame in or out. Each line is the
direction, the other card’s address (or broadcast), how far it is in meters (- for a broadcast
sent), the frame’s bytes, and how it went:
in 5f0c2a9e-41d7-4c4e-9b0a-2f3c4d5e6f70 0.0m 5 taken
out 5f0c2a9e-41d7-4c4e-9b0a-2f3c4d5e6f70 0.0m 8 taken
out 9a1b2c3d-0000-4000-8000-000000000001 - 12 unreachable
in 5f0c2a9e-41d7-4c4e-9b0a-2f3c4d5e6f70 0.0m 9 broadcast
out broadcast - 4 2 took it
An outcome is taken, busy or unreachable for a frame to one card, broadcast for a broadcast
that came in, and N took it for one sent. The card keeps only its last 8 lines, so a reader sees
what’s current.
- Reading gives the next line; with none waiting, a read waits for one, as a keyboard’s does.
cat /dev/network0shows the traffic as it goes (Ctrl+C stops it). - Writing is refused: opening it for writing fails with
EACCES. Frames are sent through sockets, or thesendandbroadcastmethods. - poll:
INwhile a line waits to be read;PRIwhile the card has news (a frame came since the file was last read or asked). - ioctls are the card’s methods, by name, with their arguments, in any open mode:
local sys = require("sys")
local card = assert(sys.open("/dev/network0"))
print(sys.ioctl(card, "address")) -- this card's address
sys.ioctl(card, "setWake", "letmein")
A method’s failure is nil, its message and EINVAL; a card that has gone is ENODEV. The
kernel’s own ioctl setLabel sets the card’s label (see /dev).
Once a program has used a socket, the kernel takes every frame the card gets, for its sockets, so
the receive method finds nothing: receive through sockets. Sending by send and broadcast works
alongside them.
Methods
address() → string
The card’s address: its id, which other cards send to.
send(address, data) → true, or false and why
Sends a frame of at most 8192 bytes to the card with that address (with or without id:). Gives
true when it was taken, else false and "busy" or "unreachable". A longer frame fails with
frame too long. Sending costs power. The card never tries again itself.
broadcast(data) → count
Sends a frame to every card in reach and gives how many took it. A longer frame than 8192 bytes
fails with frame too long.
receive() → frame or nil
The oldest frame that came, or nil. A frame is a table:
| Field | Value |
|---|---|
from |
the sender’s address |
data |
the bytes |
distance |
meters; a wired frame’s is the length of cable it went along |
broadcast |
true, only when it was a broadcast |
pull() → entry or nil
The traffic log’s oldest entry, or nil: what the card’s file reads as a line.
| Field | Value |
|---|---|
direction |
"in" or "out" |
address |
the other card’s, or "broadcast" |
distance |
meters; missing for a broadcast sent |
bytes |
the frame’s size |
outcome |
"taken", "busy", "unreachable", "broadcast", or "N took it" |
getWake() → string or nil
The wake message, or nil for none.
setWake([message]) → old message or nil
Sets the message that turns the computer on while it is off. Nil or an empty string clears it. At
most 256 bytes (wake message too long). Returns the old message.
getStrength(), setStrength(blocks)
A WLAN card’s signal strength (see WLAN Cards). On a wired card both fail with
a wired card has no signal strength.
Events
Each frame that arrives is an event, taken with receive, and raises the card’s interrupt; it stays
raised while frames wait. At most 32 wait, which is the card’s ring; a frame that finds it full is
refused as "busy", not dropped. Frames wait only while the card has power and a program runs on
its computer; they are dropped when it stops. Each frame in or out also adds a line to the traffic
log (pull).
Wake-on-LAN. With a wake message set, a card listens while its computer is off. A frame holding
the message anywhere in its data presses the computer’s power button, so a UDP datagram carrying it
does. The card takes such a frame (the sender hears true) whether it holds the message or not;
without a message set, an off computer’s card is "unreachable". The setting is kept across power
off and saves. This is the mod’s own wake message, not a real magic packet.
Examples
Each is a complete program: copy it to a computer with a network card and run it with lua. For why
they loop without yielding, see Writing programs. Two computers on one cable can
try the socket ones; nc is a handy other end.
Log the traffic
Through the card’s file: each line it reads goes into a log, with the time.
-- Keep a log of everything a network card sends and takes, each frame timed, in a file.
--
-- The card's file, /dev/network0, is its traffic: a line for each frame in or out, as
-- "in 5f0c2a9e-... 3.0m 17 taken" (the direction, the other card or "broadcast", how far, the
-- frame's bytes, and how it went). A read waits until a frame comes or goes, so the program uses
-- no time between frames. Each line is added to the log, and the log closed again, so what's
-- logged is on the disk at once (a file's writes reach the disk when it's closed).
--
-- usage: lua network-traffic.lua [log [card]]
local sys = require("sys")
local LOG = arg[1] or "/home/traffic.log"
local CARD = arg[2] or "/dev/network0"
local READ_SIZE = 512 -- the most one read takes (a line is far shorter)
local traffic = assert(sys.open(CARD))
local pending = "" -- what has been read of a line not yet ended
while true do
local data = assert(sys.read(traffic, READ_SIZE)) -- waits for a frame
pending = pending .. data
for line in pending:gmatch("([^\n]*)\n") do
local log = assert(io.open(LOG, "a"))
log:write(string.format("%10.2f %s\n", sys.time(), line))
log:close()
end
pending = pending:match("[^\n]*$")
end
A chat over UDP
Broadcasts what’s typed, shows what others send. Run it on each computer: lua network-chat.lua alice.
-- A chat between every computer on the network: what you type goes to all of them, and what they
-- send shows here, with who sent it.
--
-- Each line typed is a UDP datagram broadcast to PORT ("name: text"), and each datagram that comes
-- in on PORT is shown. A broadcast comes back to this computer's own sockets too, so lines from its
-- own cards (their addresses, from the socket's SIOCGIFCONF ioctl) are skipped. sys.poll waits for
-- whichever comes first, the keyboard or the network, so the program uses no time while nobody
-- talks. It ends at the end of its input (Ctrl+D at the start of a line).
--
-- usage: lua network-chat.lua [name]
local sys = require("sys")
local PORT = 4000 -- every computer in the chat listens and sends here
local READ_SIZE = 4096 -- the most one read of the keyboard takes
local name = arg[1] or sys.gethostname()
local sock = assert(sys.socket()) -- UDP
assert(sys.bind(sock, PORT))
local own = {} -- this computer's cards' addresses
for _, interface in ipairs(sys.ioctl(sock, "SIOCGIFCONF")) do
own[interface.address] = true
end
local typed = "" -- keys read, up to a line's end
local watch = {[0] = {IN = true}, [sock] = {IN = true}}
while true do
local ready = sys.poll(watch) -- blocks until a line is typed or a datagram comes
if ready[sock] then
local data, from = sys.recvfrom(sock)
if not own[from] then
io.write(data)
io.flush()
end
end
if ready[0] then
local keys = sys.read(0, READ_SIZE)
if not keys or keys == "" then
break -- the end of the input
end
typed = typed .. keys
for line in typed:gmatch("([^\n]*)\n") do
local ok, why = sys.sendto(sock, name .. ": " .. line .. "\n", "broadcast", PORT)
if not ok then
io.stderr:write("chat: not sent: ", why, "\n")
end
end
typed = typed:match("[^\n]*$")
end
end
sys.close(sock)
A TCP echo server
Serves any number of clients at once with one sys.poll. Start it with lua network-echo.lua &,
then from another computer:
echo hello | nc <the server card's address> 7
-- An echo server over TCP: everything a client sends comes back to it, for as many clients as
-- connect at once. Try it from another computer with nc: echo hello | nc <this card's id> 7
--
-- The listening socket is ready (IN) when a connection waits to be accepted, and a connection when
-- it has data or its client is done sending (a read gives "" then: the connection is closed).
-- sys.poll waits on all of them at once, so the server uses no time while nobody talks.
--
-- usage: lua network-echo.lua [port]
local sys = require("sys")
local PORT = tonumber(arg[1]) or 7 -- echo's own port
local READ_SIZE = 4096 -- the most one read of a connection takes
local server = assert(sys.socket("stream"))
assert(sys.bind(server, PORT))
assert(sys.listen(server))
-- Writes all of 'data' (a write takes what the connection has room for, and waits when it has none).
local function send(fd, data)
while #data > 0 do
local count = sys.write(fd, data)
if not count then
return false -- the client went away
end
data = data:sub(count + 1)
end
return true
end
local clients = {} -- each connection's descriptor, to its client's address
while true do
local watch = {[server] = {IN = true}}
for fd in pairs(clients) do
watch[fd] = {IN = true}
end
local ready = sys.poll(watch)
for fd in pairs(ready) do
if fd == server then
local conn, address = sys.accept(server)
if conn then
clients[conn] = address
end
else
local data = sys.read(fd, READ_SIZE)
if not (data and data ~= "" and send(fd, data)) then
sys.close(fd) -- its end, or it's gone
clients[fd] = nil
end
end
end
end
Wake-on-LAN
The card’s setWake and getWake methods as ioctls, and a datagram to wake another computer. On
the computer to be woken, lua network-wol.lua enable letmein, then poweroff; from another,
lua network-wol.lua <its card's address> letmein.
-- Wake-on-LAN: get this computer ready to be turned on over the network, or turn another one on.
--
-- lua network-wol.lua enable MESSAGE from now on, a frame holding MESSAGE turns this one on
-- lua network-wol.lua disable it doesn't any more
-- lua network-wol.lua status the message set, if any
-- lua network-wol.lua ADDRESS MESSAGE turn on the computer whose card is at ADDRESS
--
-- The wake message is the card's own setting, set and read with its methods setWake and getWake,
-- which are ioctls on its file. A card with one listens while its computer is off, and a frame
-- holding the message anywhere in its data presses the power button, so a UDP datagram carrying it
-- does. The sender hears whether the card took it (EHOSTUNREACH: no card there, or it isn't
-- listening).
local sys = require("sys")
local CARD = "/dev/network0"
local PORT = 9 -- discard's port, where real wake-on-LAN packets go: no program answers there
local command, message = arg[1], arg[2]
local function fail(...)
io.stderr:write("wol: ", ...)
io.stderr:write("\n")
os.exit(1)
end
if command == "enable" or command == "disable" or command == "status" then
local card = assert(sys.open(CARD))
if command == "status" then
print(sys.ioctl(card, "getWake") or "(none)")
else
if command == "enable" and not message then
fail("enable needs a message")
end
local old, why = sys.ioctl(card, "setWake", command == "enable" and message or nil)
if old == nil and why then
fail(why) -- (a message over 256 bytes)
end
print("was: " .. tostring(old or "(none)"))
end
sys.close(card)
elseif command and message then
local sock = assert(sys.socket())
local sent, why = sys.sendto(sock, message, command, PORT)
if not sent then
fail(command, ": ", why)
end
print("sent to " .. command)
else
fail("usage: wol enable MESSAGE | disable | status | ADDRESS MESSAGE")
end