Forwarded HTTP Extension Insertion (RFC 7239)

Problem this snippet solves:

Until June 2014, there was no standard HTTP headers to share with web server behind reverse proxy client side request properties like:

  • Client IP Address
  • requested Host header
  • requested protocol (HTTP / HTTPS)
  • Reverse Proxy egress IP address

These informations were injected in following non standard headers

  • Client IP Address : X-Forwarded-For
  • requested Host header : X-Forwarded-Host
  • requested protocol (HTTP / HTTPS) : X-Forwarded-Proto
  • Reverse Proxy egress IP address : Via

The RFC 7239 (Forwarded HTTP Extension) provide a single header which contains all these informations : Forwarded

Here is an examples of Forwarded header :

   Forwarded: for="_gazonk"
   Forwarded: For="[2001:db8:cafe::17]:4711"
   Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43
   Forwarded: for=192.0.2.43, for=198.51.100.17

This code insert the Forwarded header with expected values to HTTP request

  • insert for parameter with Client IP address (IPv6 format support with bracket)
  • include in For parameter the client request value of For parameter
  • include in For parameter the client request value of X-Forwarded-For header
  • insert by parameter with IP address (IPv6 format support with bracket)
  • insert proto parameter depending on clientssl profile enabled on VS
  • insert host parameter with HTTP client request Host header

version 1.1 : add route domain id support (remove route domain ID from IP addresses before insert it)

How to use this snippet:

Enable this irule on virtual server an set following variables to 0 or 1 to disable / enable parameter in header:

# Insert "For" parameter
set INSERT_FORWARDED_FOR 1
# Include in "For" parameter values from request Forwarded header
set KEEP_FORWARDED_FOR 1
# Include in "For" parameter values from request X-Forwarded-For header
set CONVERT_XFF_TO_FORWARDED_FOR 1
# Insert "By" parameter
set INSERT_FORWARDED_BY 0
# Insert "Proto" parameter
set INSERT_FORWARDED_PROTO 1
# Insert "Host" parameter
set INSERT_FORWARDED_HOST 0

Code :

when CLIENT_ACCEPTED {
    ############### Configure Which info insert in Forwarded header ###############
    # Insert "For" parameter
    set INSERT_FORWARDED_FOR 1
    # Include in "For" parameter values from request Forwarded header
    set KEEP_FORWARDED_FOR 1
    # Include in "For" parameter values from request X-Forwarded-For header
    set CONVERT_XFF_TO_FORWARDED_FOR 1
    # Insert "By" parameter
    set INSERT_FORWARDED_BY 0
    # Insert "Proto" parameter
    set INSERT_FORWARDED_PROTO 0
    # Insert "Host" parameter
    set INSERT_FORWARDED_HOST 1
    ############### Get HTTP / HTTPS info based on the clientssl profile assigned to the VS ###############
    if { [PROFILE::exists clientssl] == 1} {
        set REQUEST_PROTO "https"
    } else {
        set REQUEST_PROTO "http"
    }
    ############### prepare IPV6 encoding ###############
    if {[set CLIENT_ADDR [getfield [IP::remote_addr] "%" 1]] contains ":"} {set CLIENT_ADDR "\[$CLIENT_ADDR\]"}
}
 
when HTTP_REQUEST {
    set FFOR_HEADER ""
    set FPROTO_HEADER ""
    set FHOST_HEADER ""
    
    ############### Insert "for" parameter ###############
    if {$INSERT_FORWARDED_FOR} {
        set FFOR_HEADER_LIST [list]
        lappend FFOR_HEADER_LIST "for=$CLIENT_ADDR"
        if {$KEEP_FORWARDED_FOR} {
            foreach item [split [HTTP::header "Forwarded"] ";"] {
                if {[string tolower [string trimleft $item]] starts_with "for"} {
                    lappend FFOR_HEADER_LIST "$item"
                    break
                }
            }
        }
        if {$CONVERT_XFF_TO_FORWARDED_FOR && [HTTP::header exists "X-Forwarded-For"]} {
            if {[HTTP::header value "X-Forwarded-For"] contains ","} {
                foreach item [split [HTTP::header value "X-Forwarded-For"] ","] {
                    if {$item contains ":" && !($item contains "\[")} {
                        lappend FFOR_HEADER_LIST "for=\[[string trim $item]\]"
                    } else {
                        lappend FFOR_HEADER_LIST "for=[string trim $item]"
                    }
                }
            } else {
                lappend FFOR_HEADER_LIST "for=[HTTP::header value "X-Forwarded-For"]"
            }
            HTTP::header remove "X-Forwarded-For"
        }
        set FFOR_HEADER [join $FFOR_HEADER_LIST ","]
    }
    ############### Insert "proto" parameter ###############
    if {$INSERT_FORWARDED_PROTO} {
        set FPROTO_HEADER "proto=$REQUEST_PROTO"
    }
    ############### Insert "host" parameter ###############
    if {$INSERT_FORWARDED_HOST} {
        set FHOST_HEADER "host=[HTTP::host]"
    }
    ############### Insert HTTP header only if "by" parameter won't be included          ###############
    ############### Else the whole header will be inserted in HTTP_REQUEST_RELEASE Event ###############
    if {!$INSERT_FORWARDED_BY} {
        HTTP::header remove "Forwarded"
        HTTP::header insert "Forwarded" [join "$FFOR_HEADER $FPROTO_HEADER $FHOST_HEADER" ";"]
    }
}

when HTTP_REQUEST_RELEASE {
    ############### Insert "by" parameter ###############
    if {$INSERT_FORWARDED_BY} {
        if {[set BY_ADDR [getfield [IP::local_addr] "%" 1]] contains ":"} {set CLIENT_ADDR "\[$BY_ADDR\]"}
        set FBY_HEADER "by=$BY_ADDR"
        HTTP::header remove "Forwarded"
        HTTP::header insert "Forwarded" [join "$FFOR_HEADER $FPROTO_HEADER $FHOST_HEADER $FBY_HEADER" ";"]
    }
}

Tested this on version:

13.0
Updated Jun 06, 2023
Version 2.0
  • Hi,

    thanks for this irule. A few notes:

    • If other irules are attached to the same virtual server I suggest to attach a priority to the HTTP_REQUEST event to ensure it runs before any irule changes headers.

      priority 100
      when HTTP_REQUEST{
      
    • If you would like to add TCP ports of the client side connection to the header change the following:

      • line 33:

        lappend FFOR_HEADER_LIST "for=$CLIENT_ADDR:[TCP::remote_port]"
        
      • line 78:

        set FBY_HEADER "by=$BY_ADDR:[TCP::local_port]"
        

    Regards, René

  • Hi René,

     

    Thank you for the update. I will update this code with your suggestion, but including new variables :

     

     Include source port in "For" parameter (If "For" parameter is inserted)
    set INSERT_FORWARDED_FOR_PORT 1
     Include source port in "By" parameter (If "By" parameter is inserted)
    set INSERT_FORWARDED_BY_PORT 0
    

    then replacing line 33 with :

     

    lappend FFOR_HEADER_LIST "for=$CLIENT_ADDR[ expr {$INSERT_FORWARDED_FOR_PORT ? ":[TCP::remote_port]" : ""}]"
    

    and line 78 with :

     

    set FBY_HEADER "by=$BY_ADDR[ expr {$INSERT_FORWARDED_BY_PORT ? ":[TCP::local_port]" : ""}]"
    

    And I will also add priority option.

     

  • Latest update, today, from F5 regarding native support; "Consulting PD, there is an RFE for this, but currently there is no target version to officially add native support for RFC7239 in HTTP profile."

     

    Looks like F5 is spreading a bit thin due to so many products these days, the base-platform doesnt get the attention that their customers require. But yes, the iRule above is great to get this working. So thanks for making such a public and great effort on providing this.

     

  • Hello, I was hoping to get some understanding on a situation.

     

    Currently we have a request to prevent antispoofing by inserting remote ip address and hostname in the http header field. What is the proper way to accomplish this?

     

    Thanks Scott

     

  • t2's avatar
    t2
    Icon for Nimbostratus rankNimbostratus

    This irule appears to violate RFC 7239. According to section 4 of RFC 7239, "The first element in this list holds information added by the first proxy that implements and uses this header field, and each subsequent element holds information added by each subsequent proxy." Also see section 7.5 of the RFC 7239 for an example of multiple proxy usage.

     

    At the time this rule fires, the F5 is the last proxy in the chain, and therefore all of the information the F5 adds should be placed at the end of the Forwarded list. However, as written this rule splits the information, incorrectly placing the "for" element at the start of the list while correctly placing the "proto", "host", and "by" elements at the end.

     

    To fix this bug, line 33 (lappend FFOR_HEADER_LIST "for=$CLIENT_ADDR") should be moved to just before line 56 (set FFOR_HEADER [join $FFOR_HEADER_LIST ","]). That way the $KEEP_FORWARDED_FOR and $CONVERT_XFF_TO_FORWARDED_FOR blocks include the information added by the earlier proxies before the F5's "for" information is appended.

     

    Note that if you don't enable the KEEP_FORWARDED_FOR or CONVERT_XFF_TO_FORWARDED_FOR options or you enable them but your incoming requests don't have Forwarded or X-Forwarded-For headers to keep, you won't see the bug, because there will be nothing to go between the misplaced "for" element and the correctly placed "proto", "host", and "by" elements.

     

    Also note that if you opt to enable both the KEEP_FORWARDED_FOR and CONVERT_XFF_TO_FORWARDED_FOR options, the resultant Forwarded header may have out of order entries. This, however, isn't a fault of this rule. It is an unavoidable byproduct of mixing two different headers that work independently of each other.