Skip to content

Instantly share code, notes, and snippets.

@vladak
Last active February 8, 2025 19:10
Show Gist options
  • Save vladak/c64372c3119e8d251c07deca054801f5 to your computer and use it in GitHub Desktop.
Save vladak/c64372c3119e8d251c07deca054801f5 to your computer and use it in GitHub Desktop.
talk protocol notes

talk protocol

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.

Demo/practical

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.

Protocol

  • 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 and CTL_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).
  • protocol:

/*
 * 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 */

protocol in detail (from the client's perspective):

  1. 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
  2. once the communication is established, remove the invitations: CTL_MSG(DELETE) -> local server CTL_MSG(DELETE) -> remote server

details

/*
 * 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;
        /* 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 of sin_family
  • Linux header contains:
/* POSIX.1g specifies this type name for the `sa_family' member.  */            
typedef unsigned short int sa_family_t;

interesting properties

  • 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 via setvbuf() as buffer-less
        • which I am not sure what will do to the TCP socket
  • 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
    • 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)
    • TCP port is randomly chosen, i.e. client will let kernel choose it (sockaddr contains sin_port 0, bind() will then assign some)
    • 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
    • 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 ringing uses the same mechanism ass wall(1)

implementation caveats

  • 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment