Forum Discussion

Abed_AL-R's avatar
Abed_AL-R
Icon for Cirrostratus rankCirrostratus
Jan 23, 2019

Source NAT based on XFF header

Hello all

I'm trying to implement irule that is doing source NAT based on XFF header received from our proxy server behind the f5 LTM .

This irule should serve more than 50 subnets , each subnet should have its source nat pool. So I think we would need to configure the irule with datagroup.

Is there any idea how to configure the irule?

I found irule at : https://devcentral.f5.com/questions/snat-based-on-xff-to-internet-58899

    when HTTP_REQUEST {
    if {[HTTP::header values "X-Forwarded-For"] ne ""}{

        foreach xff [split [string map [list " " ""] [HTTP::header "X-Forwarded-For"]] ","] {
            log local0. "Current XFF element: $xff"
            if { [class match $xff eq abc-address2] } {
                log local0. "$xff hit ABC"
                snatpool SNAT_POOL_1.1.1.1
                return
            }

            if { [class match $xff eq def-address] } {
                log local0. "$xff hit DEF"
                snatpool SNAT_POOL_2.2.2.2
                return
            }
        }
    }
    else {
        log local0. "No X-Forwarded-For header found."
    }

    if {!([class match [IP::client_addr] equals proxy-address])} {
        log local0. "Not matching any ip. traffic dropped"
        drop
    }
}

Any idea how to implement this irule but with datagroup mapping each subnet to its source nat pool ?

4 Replies

  • I'm trying to configure this irule

    when HTTP_REQUEST {
    
        if { [class match [HTTP::header values "X-Forwarded-For"] equals XFF_SourceNAT] } {
        log local0.alert "Matched XFF [HTTP::header values "X-Forwarded-For"] to group"
            set category [class match -value [HTTP::header values "X-Forwarded-For"] equals XFF_SourceNAT]
           log local0.alert "Setting category to $category"
    
     NAT traffic according to xforwarded-for header
                snatpool $category
        } else {
        log local0. "No X-Forwarded-For header found."
         either for websense updates or traffic is not hitting the xff datagroup
                if { [[IP::client_addr] equals 192.168.182.0/16] } 
        {
        pool FW-Pool
        } else {
        drop
    
        }
    }
    }
    

    And in datagroup XFF_SourceNAT {address type) I configured:

    172.28.0.0/16:= 2.2.2.2

    But in the /var/log/ltm is see the follwoing error:

    `Jan 23 21:22:49 slot2/f5 err tmm5[9013]: 01220001:3: TCL error: /partition1/SNAT-XFF-irule  - bad IP network address format (line 1)invalid IP match item  for IP class /ORT/XFF_SourceNAT (line 1)     invoked from within "class match [HTTP::header values "X-Forwarded-For"] equals XFF_SourceNAT"
    
    
    What can be done to fix the issue?
  • I was able to recreate that error message by sending a curl with no XFF header to a VS with this iRule attached. I suggest going over the datagroup, ensuring that it is set to 'type: address', and checking to make sure the requests sent have an XFF header. Also do note that this iRule expects the value of the datagroups to be set up as the names of preconfigured snatpools. If you want it to SNAT to the IP address that is defined in the datagroup, change

     NAT traffic according to xforwarded-for header
                snatpool $category
    

    to this

     NAT traffic according to xforwarded-for header
                snat $category
    

    Once I solved those issues, I was able to get the iRule to function properly.

    If you have any more questions, I am sure I can help.

  • Hi Abed AL-R,

    parsing and evaluating "X-Forwareded-For" headers correctly could be a challenging task.

    Keep in mind that XFF header(s) may use either a DNS or IP format and that a single request may include multiple XFF header instances. The XXF structure below outlines how XFF headers may look like. This structure should be always keeped in mind when developing XFF based iRules...

    ...
    X-Forwarded-For: 10.10.10.10  
    X-Forwarded-For: 10.10.20.10  
    X-Forwarded-For: host.domain.tld  
    X-Forwarded-For: 10.10.30.10, 10.10.40.10, 10.10.50.10
    ...
    

    Per XFF definition (XFF is not a RFC), the first entry of the first header is the originator of the request and subsequent entries and/or headers are building the upstream chain. The

    [HTTP::header value "X-Forwarded-For"]
    command will always pick the last header as a whole and ignore any leading XFF headers. The
    [HTTP::header values "X-Forwarded-For"]
    command (notice the
    value
    vs.
    values
    directive) will fetch every single XFF header in the request and combine them into a TCL list-item, which would require some additional
    [string]
    replacements to create a list of consecutive XFF value.

    To pick accurately the very first XFF entry you may use a command like...

    set original_client [lindex [string map { ", " " " "\{" "" "\}" ""} [HTTP::header values "X-Forwarded-For"]] 0]
    

    To pick accurately the very last XFF entry you may use a command like...

    set last_hop [lindex [string map { ", " " " "\{" "" "\}" ""} [HTTP::header value "X-Forwarded-For"]] end]
    

    And to skip through the list of XFF values starting from the client to the last_hop you may use a [foreach] loop like...

    foreach xff_value [string map { ", " " " "\{" "" "\}" ""} [HTTP::header values "X-Forwarded-For"]] {
        log local0.debug "XFF Value: $xff_value"
    }
    

    So depending on your detailed requirements you would need to select the right commands to select the XFF value(s) of your choice.

    A very generic iRule to evaluate every single XFF value starting from first to last XFF value with an IP based Data-Group would look like that.

    iRule:

    when HTTP_REQUEST {
        if { [HTTP::header values "X-Forwarded-For"] ne "" } then {
             Client has passed at least one XFF header. Combining and formating the XFF header as a list and skipping through the individual values one by one...
            foreach xff_value [string map { ", " " " "\{" "" "\}" ""} [HTTP::header values "X-Forwarded-For"]] {
                if { [catch {
                     Try to find the XFF in the IP-Address data-group. This command may fail if the XFF value is not an IP address.
                    set snatpool [class match -value $xff equals MY_XFF_to_SNATPOOL_DG]
                }] } then {
                     The XFF value was not an IP address and caused a TCL exemption. Skipping to the next XFF value...
                    continue
                }
                if { $snatpool ne "" } then {
                     The data-group query found a matching entry for the currently processed XFF value. 
                    snatpool $snatpool
                     Successfully selected a SNATPOOL for this connection. Stopping further iRule processing...
                    return
                } else {
                     The XFF value was not found in the data-group. Skipping to the next XFF value...
                }
            }
             Finish the evaluation of the individual XFF values. The client has send just unknown XFF value(s)...  
        } else {
             The clients has not send a XFF header...  
        }
         Define a default action here...
        drop
    }
    

    Data-Group

    172.28.0.0/16 := /Common/SNATPOOL_1
    172.29.0.0/16 := /Common/SNATPOOL_2
    172.29.10.0/24 := /Common/SNATPOOL_3
    

    Cheers, Kai

  • Thank you guys.

    This is the final iRule that is doing the work just fine:

    when HTTP_REQUEST {
    
    if {[HTTP::header values "Client-IP"] ne ""}{
    
        foreach clientip [split [string map [list " " ""] [HTTP::header "Client-IP"]] ","] {
    
        if { [class match -- $clientip equals XFF_SourceNAT] } {
        log local0.alert "Matched clientip [HTTP::header values "Client-IP"] to group"
            set category [class match -value $clientip equals XFF_SourceNAT]
            log local0.alert "Setting snatpool to $category"
    
     NAT traffic according to xforwarded-for header
                snatpool $category
                HTTP::header remove "Client-IP"
        } else {
    
        log local0. "No X-Forwarded-For header found."
    
        pool FW-Pool
    }
    }
    }   
    }
    

    Forcepoint (websense) proxy can insert either "x-forwarded-for" header or "Client-IP" header. But unfortunately we didn't see the "x-forwarded-for" header even it was enabled on the wensense proxy. So just to save time we did it with the "Client-IP" header.

    We removed the Client-IP header at the end of the iRule because there was some sites doing the " what is my ip" showingthe "Client-IP" calue instead of the public NAT IP.

    a datagroup value example: 172.28.0.0/16:=school1

    "school1" has a value of public IP address in SNAT pool list.