Accessing TCP Options from iRules
I’ve written several articles on the TCP profile and enjoy digging into TCP. It’s a beast, and I am constantly re-learning the inner workings. Still etched in my visual memory map, however, is the TCP header format, shown in Figure 1 below.
Since 9.0 was released, TCP payload data (that which comes after the header) has been consumable in iRules via the TCP::payload and the port information has been available in the contextual commands TCP::local_port/TCP::remote_port and of course TCP::client_port/TCP::server_port. Options, however, have been inaccessible. But beginning with version 10.2.0-HF2, it is now possible to retrieve data from the options fields.
Preparing the BIG-IP
Prior to version 11.0, it was necessary to set a bigpipe database key with the option (or options) of interest:
bigpipe db Rules.Tcpoption.settings [option, first|last], [option, first|last]
In version 11.0 and forward, the DB keys are no more and you need to create a tcp profile with the these options defined, like so:
ltm profile tcp tcp_opt {
app-service none
tcp-options "{option first|last} {option first|last}"
}
The option is an integer between 2 and 255, and the first/last setting indicates whether the system will retain the first or last instance of the specified option. Once that key is set, you’ll need to do a bigstart restart for it to take (warning: service impacting). Note also that the LTM only collects option data starting with the ACK of a connection. The initial SYN is ignored even if you select the first keyword. This is done to prevent a SYN flood attack (in keeping with SYN-cookies).
A New iRules Command: TCP::option
The TCP::option command has the following syntax:
TCP::option get <option>
v11 Additions/Changes:
TCP::option set <option number> <value> <next|all>
TCP::option noset <option number>
Pretty simple, no? So now that you can access them, what fun can be had?
Real World Scenario: Akamai
In Akamai’s IPA and SXL product lines, they support client IP visibility by embedding a version number (one byte) and an IPv4 address (four bytes) as part of their overlay path feature in tcp option number 28. To access this data, we first set the database key:
tmsh create ltm profile tcp tcp_opt tcp-options “{28 first}”
Now, the iRule utilizing the TCP::option command:
when CLIENT_ACCEPTED {
set opt28 [TCP::option get 28]
if { [string length $opt28] == 5 } {
binary scan $opt28 cH8 ver addr
if { $ver != 1 } {
log local0. "Unsupported Akamai version: $ver"
} else {
scan $addr "%2x%2x%2x%2x" ip1 ip2 ip3 ip4
set optaddr "$ip1.$ip2.$ip3.$ip4"
}
}
}
when HTTP_REQUEST {
if { [info exists optaddr] } {
HTTP::header insert "X-Forwarded-For" $optaddr
}
}
The Akamai version should be one, so we log if not. Otherwise, we take the address (stored in the variable addr in hex) and scan it to get the decimal equivalents to build the address for inserting in the X-Forwarded-For header. Cool, right? Also cool—along with the new TCP::option command , an extension was made to the IP::addr command to parse binary fields into a dotted decimal IP address. This extension is also available beginning in 10.2.0-HF2, but extended in 11.0. Here’s the syntax:
IP::addr parse [-ipv4 | -ipv6 [swap]] <binary field> [<offset>]
So for example, if you had an IPv6 address in option 28 with a 1 byte offset, you would parse that like:
log local0. "IP::addr parse IPv6 output: [IP::addr parse -ipv6 [TCP::option get 28] 1]"
Log Result
May 27 21:51:34 ltm13 info tmm[27207]: Rule /Common/tcpopt_test
But in the context of our TCP option, we have 5-bytes of data with the first byte not mattering in the context of an address, so we get at the address with this:
set optaddr [IP::addr parse -ipv4 [TCP::option get 28] 1]
This cleans up the rule a bit:
when CLIENT_ACCEPTED {
set opt28 [TCP::option get 28]
if { [string length $opt28] == 5 } {
binary scan $opt c ver
if { $ver != 1 } {
log local0. "Unsupported Akamai version: $ver"
} else {
set optaddr [IP::addr parse -ipv4 $opt28 1]
}
}
}
when HTTP_REQUEST {
if { [info exists optaddr] } {
HTTP::header insert "X-Forwarded-For" $optaddr
}
}
No need to store the address in the first binary scan and no need for the scan command at all so I eliminated those. Setting a forwarding header is not the only thing we can do with this data. It could also be shipped off to a logging server, or used as a snat address (assuming the server had either a default route to the BIG-IP, or specific routes for the customer destinations, which is doubtful). Logging is trivial, shown below with the log command. The HSL commands could be used in lieu of log if sending off-box to a log server.
when CLIENT_ACCEPTED {
set opt28 [TCP::option get 28]
if { [string length $opt28] == 5 } {
binary scan $opt c ver
if { $ver != 1 } {
log local0. "Unsupported Akamai version: $ver"
} else {
set optaddr [IP::addr parse -ipv4 $opt28 1]
log local0. "Client IP extracted from Akamai TCP option is $optaddr"
}
}
}
If setting the provided IP as a snat address, you’ll want to make sure it’s a valid IP address before doing so. You can use the TCL catch command and IP::addr to perform this check as seen in the iRule below:
when CLIENT_ACCEPTED {
set addrs [list \
"192.168.1.1" \
"256.168.1.1" \
"192.256.1.1" \
"192.168.256.1" \
"192.168.1.256" \
]
foreach x $addrs {
if { [catch {IP::addr $x mask 255.255.255.255}] } {
log local0. "IP $x is invalid"
} else { log local0. "IP $x is valid" }
}
}
The output of this iRule:
Adding this logic into a functional rule with snat:
when CLIENT_ACCEPTED {
set opt28 [TCP::option get 28]
if { [string length $opt28] == 5 } {
binary scan $opt c ver
if { $ver != 1 } {
log local0. "Unsupported Akamai version: $ver"
} else {
set optaddr [IP::addr parse -ipv4 $opt28 1]
if { [catch {IP::addr $x mask 255.255.255.255}] } {
log local0. "$optaddr is not a valid address"
snat automap
} else {
log local0. "Akamai inserted Client IP is $optaddr. Setting as snat address."
snat $optaddr
}
}
}
Alternative TCP Option Use Cases
The Akamai solution shows an application implementation taking advantage of normally unused space in TCP headers. There are, however, defined uses for several option “kind” numbers. The list is available here: http://www.iana.org/assignments/tcp-parameters/tcp-parameters.xml. Some options that might be useful in troubleshooting efforts:
Opkind 2 – Max Segment Size
Opkind 3 – Window Scaling
Opkind 5 – Selective Acknowledgements
Opkind 8 – Timestamps
Of course, with tcpdump you get all this plus the context of other header information and data, but hey, another tool in the toolbox, right?
Addendum
I’ve been working with F5 SE Leonardo Simon on on additional examples I wanted to share here that uses option 28 or 253 to extract an IPv6 address if the version is 34 and otherwise extracts an IPv4 address if the version is 1 or 2.
Option 28
when CLIENT_ACCEPTED {
set opt28 [TCP::option get 28]
binary scan $opt28 c ver
#log local0. "version: $ver"
if { $ver == 34 } {
set optaddr [IP::addr parse -ipv6 $opt28 1]
log local0. "opt28 ipv6 address: $optaddr"
}
elseif { $ver == 1 || $ver == 2 } {
set optaddr [IP::addr parse -ipv4 $opt28 1]
log local0. "opt28 ipv4 address: $optaddr"
}
}
Option 253
when CLIENT_ACCEPTED {
set opt253 [TCP::option get 253]
binary scan $opt253 c ver
#log local0. "version: $ver"
if { $ver == 34 } {
set optaddr [IP::addr parse -ipv6 $opt253 1]
log local0. "opt253 ipv6 address: $optaddr"
}
elseif { $ver == 1 || $ver == 2 } {
set optaddr [IP::addr parse -ipv4 $opt253 1]
log local0. "opt253 ipv4 address: $optaddr"
}
}