Monday, May 23, 2011

What Endian the SETTINGS SPDY Frame?

‹prev | My Chain | next›

Today I begin my investigation of some things I did not quite understand while looking at SPDY requests and SPDY responses in detail. To get up to speed with the node-spdy codebase, I start with an easy one: the SETTINGS frame.

While examining the SPDY response SETTINGS frame last night, I noticed that it does not match up with the spec:
              +----------------------------------+
80 02 00 04 |1| version | 4 |
+----------------------------------+
00 00 00 0c | Flags (8) | Length (24 bits) |
+----------------------------------+
00 00 00 01 | Number of entries |
+----------------------------------+
04 00 00 00 | ID / |
00 00 00 64 | Value Pairs ... |
Specifically, the ID is wrong. The first octet ought to be an optional flag (0x1 = client should persist this setting; 0x2 = this is a persisted value). The actual value being set, 0x04 is not a valid SPDY SETTINGS frame flag, so I would guess that this is the ID. Per the SPDY draft (#2) spec, an ID of 0x04 is SETTINGS_MAX_CONCURRENT_STREAMS.

Looking through the node-spdy source, I see that a server is created with the following default options:
var Server = core.Server = function(options, requestListener) {
var that = this,
connectionSettings = options.connectionSettings || {
SETTINGS_MAX_CONCURRENT_STREAMS: 100
};
...
Those settings are sent back to the client at the end of the TLS server callback (i.e. for each TLS server connection):

...
// Send settings
c.write(createSettingsFrame(c.zlib, connectionSettings));
...
Checking out the createSettingsFrame method, I find:
/**
* Create SETTINGS frame
*/
exports.createSettingsFrame = function(zlib, settings) {
var keys = Object.keys(settings),
keysLen = keys.length,
buff = new Buffer(4 + 8 * keysLen);

// Insert keys count
buff.writeUInt32(keysLen, 0, 'big');

keys.reduce(function(offset, key) {
var raw_key = enums[key];

buff.writeUInt32(raw_key & 0xffffff, offset, 'little');

var value = settings[key];
buff.writeUInt32(value, offset + 4, 'big');

return offset + 8;
}, 4);

return createControlFrame(zlib, {
type: enums.SETTINGS
}, buff);
};
The zlib context being passed in is required to decompress/compress multiple frames over the course of a single connection. The settings are an object literal like that from the server core ({SETTINGS_MAX_CONCURRENT_STREAMS: 100}). The createSettingsFrame function then builds a buffer large enough to hold 4 bytes/octets describing the number of keys being set (one in this case) plus 8 bytes/octets for each key. The number of keys are written to the first four octets (via writeUInt32 / 32 bits). Next, the code iterates (using a reduce) over each key writing the ID in the first 32 bits and the value in the second 32 bits.

But why the use of little-endian byte order? All packets that I have seen so far have been in big-endian order—something the spec confirms:
All integer values, including length, version, and type, are in network byte order.
So I change the ID/Value pair code to read:
...
var raw_key = enums[key];

buff.writeUInt32(raw_key, offset, 'big');

var value = settings[key];
buff.writeUInt32(value, offset + 4, 'big');

return offset + 8;
...
Checking this out in the console, I find:
➜  node-spdy git:(master) ✗ node
> var createSettingsFrame = require('./lib/spdy').createSettingsFrame,
... createZLib = require('./lib/spdy').createZLib,
... enums = require('./lib/spdy').enums;
>
> createSettingsFrame(createZLib(), {SETTINGS_MAX_CONCURRENT_STREAMS: 100});
SETTINGS_MAX_CONCURRENT_STREAMS
<Buffer 80 02 00 04 00 00 00 0c 00 00 00 01 00 00 00 04 00 00 00 64>
Matching that up with the spec, I find:
                +----------------------------------+
80 02 00 04 |1| 1 | 4 |
+----------------------------------+
00 00 00 0c | Flags (8) | Length (24 bits) |
+----------------------------------+
00 00 00 01 | Number of entries |
+----------------------------------+
00 00 00 04 | ID/ |
00 00 00 64 | Value Pairs... |
Easy-peasy.

A quick sanity check in Wireshark reveals that the packet is coming through as I expect:
0000  80 02 00 04 00 00 00 0c  00 00 00 01 00 00 00 04   ........ ........
0010 00 00 00 64 ...d
Still...

I find it very odd that node-spdy is choosing to be so explicit about the endian-ness here. One more sanity check reveals why. Examining the SPDY packets in Chrome's about:net-internals panel, I find:
t=1306207841256 [st=  8]     SPDY_SESSION_RECV_SETTINGS  
--> settings = ["[0:100]"]
Bizarre. I would have expected 4:100, where 4 indicates that I am trying to set SETTINGS_MAX_CONCURRENT_STREAMS.

If I revert to the little-endian order, then reload, net-internals tells me:
(P) t=1306207452900 [st=  6]     SPDY_SESSION_RECV_SETTINGS  
--> settings = ["[4:100]"]
So I guess there was a reason for that after all. It just seems to be a reason that violates the spec.

A mystery for another day.


Day #29

No comments:

Post a Comment