Here are my sparse notes on the BSD talk protocol. The goal was to arrive at understanding of the protocol.
Should update https://en.wikipedia.org/wiki/Talk_(software) with the info one day.
Exstablish a talk session, observe the messages/syscalls.
On Ubuntu, sudo apt get install talkd
installs talkd
and creates the following inetd
entries:
#:BSD: Shell, login, exec and talk are BSD protocols.
talk dgram udp wait nobody.tty /usr/sbin/in.talkd in.talkd
ntalk dgram udp wait nobody.tty /usr/sbin/in.ntalkd in.ntalkd
There is talk and ntalk, they have different ports:
$ grep talk /etc/services
talk 517/udp
ntalk 518/udp
The original talk version make it possible to communicate between 2 users logged into the same machine, newer talk (1983) made it possible to communicate over the network.
Q: can talk function without talkd ?
A: looks like not, at least not the "classic" network one. when talk (ntalk) is run on localhost and talkd
is not running, talk
will display the halved screen, however in the middle it will report 'Error on read from talk daemon: Connection refused. Press any key... '
and when a key is pressed it will exit
When talk
is run to establish talk session to a localhost user who is not logged in, the talk
will emit
a warning about the user not being logged in. It does not suffice to be su
'ed to the user, the user has to be logged
via console or SSH. (it seems the daemon checks the utmp entries)
Once the user is logged in, it works. The in.talkd
is visible in the process listing and there is related protocol activity:
$ netstat -an | grep 518
udp 0 0 127.0.0.1:55148 127.0.0.1:518 ESTABLISHED
udp 0 0 127.0.0.1:46517 127.0.0.1:518 ESTABLISHED
udp 0 0 127.0.0.1:49046 127.0.0.1:518 ESTABLISHED
udp 0 0 0.0.0.0:518 0.0.0.0:*
udp 0 0 127.0.0.1:50033 127.0.0.1:518 ESTABLISHED
When the client is strace
'd and one characters it put in:
$ sudo strace -p 2713748
strace: Process 2713748 attached
pselect6(8, [0 6], NULL, NULL, NULL, NULL) = 1 (in [0])
read(0, "a", 1) = 1
rt_sigaction(SIGTSTP, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f7fab128520}, {sa_handler=0x7f7fab360750, sa_mask=[ALRM], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f7fab128520}, 8) = 0
poll([{fd=0, events=POLLIN}], 1, 0) = 0 (Timeout)
poll([{fd=0, events=POLLIN}], 1, 0) = 0 (Timeout)
write(1, "a", 1) = 1
rt_sigaction(SIGTSTP, {sa_handler=0x7f7fab360750, sa_mask=[ALRM], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f7fab128520}, NULL, 8) = 0
write(6, "a", 1) = 1
lsof
reports:
talk 2713748 vkotal 3u IPv4 30796291 0t0 UDP localhost:50033->localhost:ntalk
talk 2713748 vkotal 4u IPv4 30796292 0t0 UDP localhost:55148->localhost:ntalk
talk 2713748 vkotal 6u IPv4 30791613 0t0 TCP localhost:42607->localhost:32818 (ESTABLISHED)
i.e. there is a TCP connection being active.
-
examine existing implementations
-
is there a RFC ?
- looks like not. basic description (1997) is on https://www.cs.columbia.edu/~hgs/internet/talk.html
- talk server (talkd) listens on UDP port 517/518
- the clients communicate via TCP
- for reliable communication over UDP it is necessary to wait for reply and resend if not received in given time limit FreeBSD: usr.bin/talk/ctl_transact.c
- there are 2 message types:
CTL_MSG
andCTL_RESPONSE
- both are names of data structures, which are directly sent on the wire
- it is therefore necessary to tackle endianess (CTL_RESPONSE contains multibyte members).
- both are names of data structures, which are directly sent on the wire
-
protocol:
- "control" protocol is UDP based, once the client is able to ring the remote party, a TCP session is established and the client communicate directly
- it is pretty well described in the source code comment on https://cgit.freebsd.org/src/tree/include/protocols/talkd.h
/*
* This describes the protocol used by the talk server and clients.
*
* The talk server acts a repository of invitations, responding to
* requests by clients wishing to rendezvous for the purpose of
* holding a conversation. In normal operation, a client, the caller,
* initiates a rendezvous by sending a CTL_MSG to the server of
* type LOOK_UP. This causes the server to search its invitation
* tables to check if an invitation currently exists for the caller
* (to speak to the callee specified in the message). If the lookup
* fails, the caller then sends an ANNOUNCE message causing the server
* to broadcast an announcement on the callee's login ports requesting
* contact. When the callee responds, the local server uses the
* recorded invitation to respond with the appropriate rendezvous
* address and the caller and callee client programs establish a
* stream connection through which the conversation takes place.
*/
- basic 2 structures:
/*
* Client->server request message format.
*/
typedef struct {
u_char vers; /* protocol version */
u_char type; /* request type, see below */
u_char answer; /* not used */
u_char pad;
u_int32_t id_num; /* message id */
struct tsockaddr addr; /* old (4.3) style */
struct tsockaddr ctl_addr; /* old (4.3) style */
int32_t pid; /* caller's process id */
#define NAME_SIZE 12
char l_name[NAME_SIZE];/* caller's name */
char r_name[NAME_SIZE];/* callee's name */
#define TTY_SIZE 16
char r_tty[TTY_SIZE];/* callee's tty name */
} CTL_MSG;
/*
* Server->client response message format.
*/
typedef struct {
u_char vers; /* protocol version */
u_char type; /* type of request message, see below */
u_char answer; /* respose to request message, see below */
u_char pad;
u_int32_t id_num; /* message id */
struct tsockaddr addr; /* address for establishing conversation */
} CTL_RESPONSE;
- the version is
#define TALK_VERSION 1 /* protocol version */
- is being checked when receiving a response from the server
and 4 message types:
/* message type values */
#define LEAVE_INVITE 0 /* leave invitation with server */
#define LOOK_UP 1 /* check for invitation by callee */
#define DELETE 2 /* delete invitation by caller */
#define ANNOUNCE 3 /* announce invitation by caller */
- CTL_MSG(LOOK_UP) -> localhost:518
a) local server responded with CTL_RESPONSE(SUCCESS), i.e. there was an invitation waiting for the client:
connect() to the address provided in the CTL_RESPONSE
- if the connect() was not successful => CTL_MSG(DELETE) -> remote server
b) CTL_MSG(ANNOUNCE) -> remote server
- if the CTL_RESPONSE is not a SUCCESS, quit (send CTL_MSG(DELETE) to both local and remote servers) CTL_MSG(LEAVE_INVITE) -> local server
- if there is already an invite in the table, it is just updated
- if the remote client does not connect within a timeout (ring wait), resend the ANNOUNCE+LEAVE_INVITE
- if the connect() was not successful => CTL_MSG(DELETE) -> remote server
b) CTL_MSG(ANNOUNCE) -> remote server
- once the communication is established, remove the invitations: CTL_MSG(DELETE) -> local server CTL_MSG(DELETE) -> remote server
- local_id vs. remote_id: how is this being used exactly ?
/*
* The msg.id's for the invitations
* on the local and remote machines.
* These are used to delete the
* invitations.
*/
static int local_id, remote_id;
-
local_id
is initialized from the answer (id_num
) toLEAVE_INVITE
on local server- server allocates the ID when entering the request to the table: https://cgit.freebsd.org/src/tree/libexec/talkd/table.c
-
remote_id
is initialized froom the respon (id_num
) to theANNOUNCE
message (remote server)- ditto
-
client sends
id_num
initially as 0 -
BSD address storage in structures: how does it differ to
sockaddr_in
?- https://cgit.freebsd.org/src/tree/usr.bin/talk/invite.c contains the conversion:
/* copy new style sockaddr to old, swap family (short in old) */
msg.addr = *(struct tsockaddr *)&my_addr;
msg.addr.sa_family = htons(my_addr.sin_family);
- i.e.
struct sockaddr_in
except different handling ofsin_family
- Linux header contains:
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;
- Wikipedia: every character types is immediately sent to the other party (no buffering)
- is
TCP_NODELAY
socket option used or something else ?- cannot find it being used in the FreeBSD implementation. Before the main I/O loop both the socket (fdopen()) as well as
stdin
is being set viasetvbuf()
as buffer-less- which I am not sure what will do to the TCP socket
- cannot find it being used in the FreeBSD implementation. Before the main I/O loop both the socket (fdopen()) as well as
- is
- when the terminal window is being resized while the talk client is running, it accomodates (the division and text contents)
- except there seems to be a bug in the talkd on Ubuntu: when I started typing after the resize, then
^Z
started appearing in endless loop- i.e. the
SIGWINCH
signal handling got into trouble for some reason- ad signal handling - in FreeBSD there is the
gotwinch
flag which is checked in the main I/O loopu https://cgit.freebsd.org/src/tree/usr.bin/talk/io.c
- ad signal handling - in FreeBSD there is the
- i.e. the
- talkd writes message to user's terminal that he is being called
- for that it likely needs access to the controlling terminal of given user (for given session)
- resp. this is being sent to all user's terminals (header file mentions "broadcast")
- also, the terminal bell character is sent (BEL - 0x07) so that the terminal can blink/ring (according to terminal settings)
- for that it likely needs access to the controlling terminal of given user (for given session)
- TCP port is randomly chosen, i.e. client will let kernel choose it (
sockaddr
containssin_port
0, bind() will then assign some)- the clint will need to get the port number so that it can be used in
CTL_MSG
getsockname()
- in FreeBSD (https://cgit.freebsd.org/src/tree/usr.bin/talk/ctl.c) getsockname() will overwrite the structure previously used for
bind()
- the clint will need to get the port number so that it can be used in
- caveat: it is necessary to get the IP address used for outgoing traffic to the remote client/server
- in FreeBSD https://cgit.freebsd.org/src/tree/usr.bin/talk/get_iface.c they use a trick: create UDP socket, bind() to port 60000 (will retry +1), connect() (UDP!) to port 60000 (will retry +1) and if successful, use getsockname()
- i.e. no special library for traversing the list of network interfaces in the system
- in FreeBSD https://cgit.freebsd.org/src/tree/usr.bin/talk/get_iface.c they use a trick: create UDP socket, bind() to port 60000 (will retry +1), connect() (UDP!) to port 60000 (will retry +1) and if successful, use getsockname()
- client can specify ttyname of remote user
- TCP session is used for raw data passing, i.e. typed characters (wide char on FreeBSD)
- it is possible to re-announce if the remote side is not responding to ringing
- the
ANNOUNCE CTL_MSG
is being sent with id_num incremented by 1- talkd know how to handle it, finds the matching entry in the table according to username/tty/... and when the id_num in the message is bigger, it will perform "announce", i.e. will write to user's terminal some information about the incoming request
- the
- the ringing uses the same mechanism ass wall(1)
- except there seems to be a bug in the talkd on Ubuntu: when I started typing after the resize, then
- when sending
CTL_MSG
and waiting for an answer a cekani na odpoved it is necessary to check type and version - endianess:
id_num
and other fields - in case of localhost<->localhost communication, the protocol is unchanged. the local/remote server is identical.
- hostname of the local machine can be retrieved via gethostname() (POSIX)