Site hosted by Angelfire.com: Build your free website today!

Hands-on Projects for the Linux Graphics Subsystem

New book from Christos Karayiannis

Available in Amazon Kindle format at:

amazon.com   amazon.co.uk   amazon.de   amazon.es   amazon.fr   amazon.it

Inspecting the X Client-Server handshake using strace and Wireshark

Part I: strace

Gdb cannot help us obtain the real file descriptor number of the socket connection between the X client and server, since when we run ./test through gdb two more file descriptors are used which increases the real fd number from 3 to 5 (see the first line of gdb output produced by the 'print *d' command, where fd = 5). We know that the fd is 3, the next available from 0,1,2 (UNIX standard input, output and error fle descriptors), by writing the following instruction in test.c after XOpenDisplay() is called:

printf("%d", d->fd);
or by using the strace utility. We read from the strace man page:

In the simplest case strace runs the specified command until it exits. It intercepts and records the system calls which are called by a process and the signals which are received by a process.

We run strace after using xhost to provide access to all clients from any host, as:

sudo xhost +
DISPLAY=tcp/192.168.1.101:0.0 strace -o testlog ./test
where testlog is the xtrace output file and we press Ctrl+C to stop recording. We read testlog using the more command and we extract three lines after IPPROTO_TCP is found:

As we see at the first line after the 'more' command the socket() call returns successful file descriptor 3.

Next setsockopt() sets to the socket 3 the TCP_NODELAY socket option. As we read from the tcp man page with this option "segments are always sent as soon as possible, even if there is only a small amount of data".

At the third line SO_KEEPALIVE socket option is set also with setsockopt(). From the setsockopt man page we read: "Keeps connections active by enabling the periodic transmission of messages, if this is supported by the protocol."

Finally a connect() call takes place that aims to connect the X client with the X server. The X server target address is 192.168.1.101 and the port number 6000. This number corresponds to the first display of the X Server, which is the first zero in the DISPLAY environment variable used by ./test.

DISPLAY=tcp/192.168.1.101:0.0
If we were to use a second display (in other words a second X Server) the previous variable would be given as:
DISPLAY=tcp/192.168.1.101:1.0 
and the port number of the X Server would be 6001, and so on.

The socket number, used, is the one returned previously by socket(), 3 and the address type AF_INET since the protocol used and defined in the DISPLAY variable is tcp.

Next we continue reading file testlog using the more command:

As we see there is one writev() call, which writes 12 bytes to the file descriptor (fd) 3. This represents the socket previously opened by this client process. It is followed by two continuous read() calls at the same fd. The first call reads 8 bytes and the second 1740. The previous writev() and read() calls are the ones that exchange the xConnClientPrefix (from the client's side) and xConnSetupPrefix, xConnSetup (from the server's side) that were described in the first part of the current section.

In the image of the general case of the previous section we include the number of bytes for the current example:

writev() returns 12 bytes, which is the exact size of xConnClientPrefix:

typedef struct {
    CARD8	byteOrder;
    BYTE	pad;
    CARD16	majorVersion B16, minorVersion B16;
    CARD16	nbytesAuthProto B16;	/* Authorization protocol */
    CARD16	nbytesAuthString B16;	/* Authorization string */
    CARD16	pad2 B16;
} xConnClientPrefix;

The first read() call reads exactly the size of xConnSetupPrefix, which is 8 bytes:

typedef struct {
    CARD8          success;
    BYTE           lengthReason; /*num bytes in string following if failure */
    CARD16         majorVersion B16, 
                   minorVersion B16;
    CARD16         length B16;  /* 1/4 additional bytes in setup info */
} xConnSetupPrefix;

This read() (see first part of the currrent section) takes place in XOpenDisplay() as:

	    _XRead (dpy, (char *)&prefix,(long)SIZEOF(xConnSetupPrefix));
The second read() returns 1740 bytes, which include info related to the u fields returned from the X Server to the client. Union u is defined locally in XOpenDisplay() and in the case of succesfull initiation between the X client and the server all u members except 'failure' are used to decode the X Server reply.
	union {
		xConnSetup *setup;
		char *failure;
		char *vendor;
		xPixmapFormat *sf;
		xWindowRoot *rp;
		xDepth *dp;
		xVisualType *vp;
	} u;				/* proto data returned from server */
A xConnSetup struct is read first from the X Server's message. The xConnSetup struct as we recall from section 1.6 is the following:

typedef struct {
    CARD32         release B32;
    CARD32         ridBase B32, 
                   ridMask B32;
    CARD32         motionBufferSize B32;
    CARD16         nbytesVendor B16;  /* number of bytes in vendor string */
    CARD16         maxRequestSize B16;
    CARD8          numRoots;          /* number of roots structs to follow */
    CARD8          numFormats;        /* number of pixmap formats */
    CARD8          imageByteOrder;        /* LSBFirst, MSBFirst */
    CARD8          bitmapBitOrder;        /* LeastSignificant, MostSign...*/
    CARD8          bitmapScanlineUnit,     /* 8, 16, 32 */
                   bitmapScanlinePad;     /* 8, 16, 32 */
    KeyCode	   minKeyCode, maxKeyCode;
    CARD32	   pad2 B32;
} xConnSetup;

The XOpenDisplay() command that reads the xConnSetup is the following:

_XRead (dpy, (char *)u.setup, setuplength);
We notice that the amount of bytes read, the setuplength, is bigger than the xConnSetup size. This is for our example 1740 bytes. Since u is a union and contrary to a struct where each member present in structure takes its own memory, in the case of union, only the largest sized member memory is reserved by the union. After xConnSetup is read and some Display fields are assigned according to the xConnSetup fields:

	dpy->release 		= u.setup->release;
	dpy->resource_base	= u.setup->ridBase;
        . . .

the following XOpenDisplay() instructions are executed:

 	u.setup = (xConnSetup *) (((char *) u.setup) + sz_xConnSetup);
  	(void) strncpy(dpy->vendor, u.vendor, vendorlen);

As we see the u.setup value, which is of type xConnSetup struct is moved after the xConnSetup size in bytes, at the next memory location we have to read. Then the u.vendor union member is used at this memory location to fill the Display's vendor field.

Similarly the remaining u members are extracted from the X Server's reply: the PixmapFormat, the WindowRoot, the Depth and the available VisualType.

Part II: Wireshark

Wireshark is a network protocol analyser. We will use this utility to have a closer look at the packets exchanged between the X Client and X Server that make the read-writev triplett that as we mentioned previously make the initial negotiation and as an outcome many of the Display fields are filled.

We use the test program for another time and we make the client-server connection using the tcp/ip protocol. We perform the following steps:

We start Wireshark from the command line with root privilege:


sudo wireshark
At the Wireshark Capture->Options menu option we set 'Interface' to 'Pseudo-Device that captures all interfaces: any' and at the 'Capture Filter' we enter 'host 192.168.1.101', which is the IP address of the system used. We set also the 'Stop Capture ... after' to a small number of packets, e.g. 12 and we start monitoring with the 'Start' button.

Then we start the 'test' X Client, using the tcp/ip protocol. First we enter at the command line:


xhost +
to allow connections from any sytem and then we run test as:

DISPLAY=192.168.1.101:0.0 ./test  
The packets selected by Wireshark are displayed in the Wireshark pane:

We are especially interested about the 4th packet, the 'Initial connection request' as we read in the 'info' field. This corresponds to the xConnClientPrefix (see section 1.5):

typedef struct {
    CARD8	byteOrder;
    BYTE	pad;
    CARD16	majorVersion B16, minorVersion B16;
    CARD16	nbytesAuthProto B16;	/* Authorization protocol */
    CARD16	nbytesAuthString B16;	/* Authorization string */
    CARD16	pad2 B16;
} xConnClientPrefix;

By double clicking to this packet area in the Wireshark pane we open it to a new window. As we see the byte-order is Little-endian, the protocol major version is 11 and the rest of the fields are zero or unused.

As we see Wireshark has the ability to recognize the X11 protocol messages. For instance value 11 corresponds to the 'protocol-major-version'. For a complete list of all X11 messages that Wireshark recognizes read this wiki.

Next we double click in the 6th entry, the 'Initial connection reply'. This corresponds to both xConnSetupPrefix and xConnSetup (again see section 1.5):

typedef struct {
    CARD8          success;
    BYTE           lengthReason; /*num bytes in string following if failure */
    CARD16         majorVersion B16, 
                   minorVersion B16;
    CARD16         length B16;  /* 1/4 additional bytes in setup info */
} xConnSetupPrefix;

typedef struct {
    CARD32         release B32;
    CARD32         ridBase B32, 
                   ridMask B32;
    CARD32         motionBufferSize B32;
    CARD16         nbytesVendor B16;  /* number of bytes in vendor string */
    CARD16         maxRequestSize B16;
    CARD8          numRoots;          /* number of roots structs to follow */
    CARD8          numFormats;        /* number of pixmap formats */
    CARD8          imageByteOrder;        /* LSBFirst, MSBFirst */
    CARD8          bitmapBitOrder;        /* LeastSignificant, MostSign...*/
    CARD8          bitmapScanlineUnit,     /* 8, 16, 32 */
                   bitmapScanlinePad;     /* 8, 16, 32 */
    KeyCode	   minKeyCode, maxKeyCode;
    CARD32	   pad2 B32;
} xConnSetup;

The two successive writev() calls of the X Server, are placed now in the same packet.

The field value of xConnSetupPrefix and xConnSetup are displayed in the new window:

The replylength value corresponds to the 1740 bytes of the second read(). This is 435 and the result of 435 * 4 is 1740. As we read in the xConnSetupPrefix definition 435 is "1/4 additional bytes in setup info".

Since xConnSetup is only a small portion of the 1740 bytes of the second read() call we have to find the value of the rest. Those are the values that correspond to the u union members, discussed at the first part of the current section and the only member value that is displayed is of vendor, which is 'The X.Org Foundation'. The rest are left as undecoded. Wireshark shows those bytes in hex values and we could make some decoding in the hard way, however as we see in the next section xtrace gives us this info readily.