Forum Discussion

ant77's avatar
ant77
Icon for Cirrostratus rankCirrostratus
Jul 01, 2020

Alternative to getfield to check XFF client IP using data group

Hi All,

We ran into a bug when upgrading to 13.1.3.3 that process an iRule to check the client IP address in an XFF header

against what is defined in a data group "DG-ALLOWED-IP".

Is there an alternative to re-writing this to not use the "getfield" but still use the "DG-ALLOWED-IP" data group and see if the client

IP from the XFF header matches this or not. If it does not match, then reject?

Thanks!

ERROR:

01220001:3: TCL error: /Common/iRULE-WEB-REDIRECT <HTTP_REQUEST> - bad IP network address format (line 2)invalid IP match item for IP class /Common/DG-ALLOWED-IP (line 2)   invoked from within "class match $CHECK_IP eq DG-ALLOWED-IP"

when HTTP_REQUEST {
set CHECK_IP [getfield [HTTP::header values X-Forwarded-For] " " 1]
 if { !([class match $CHECK_IP eq DG-ALLOWED-IP]) } {
     if { [class match [HTTP::uri] eq DG-ALLOWED-URI-LIST] } {
          reject 
        }
     }
}

10 Replies

  • So

    [HTTP::header values X-Forwarded-For]

    returns a list of values for X-Forwarded-For.

    But X-Forwarded-For may have multiple forwarding IPs, and multiple headers, and look like:

    X-Forwarded-For: 10.0.0.1, 192.168.10.10
    X-Forwarded-For: 172.16.0.200

    So what does the irule see

    [HTTP::header values X-Forwarded-For]
    {10.0.0.1, 192.168.10.10} 172.16.0.200

    This is a TCL list of X-Forwarded-For headers, with the first entry being

    {10.0.0.1, 192.168.10.10}

    You need to grab the first item in the TCL list using the following sequence

    {10.0.0.1, 192.168.10.10} 172.16.0.200
    [split $xff "\{\} ,"]
    {} 10.0.0.1 {} 192.168.10.10 {} 172.16.0.200
    [lsearch -all -inline -not -exact [split $xff "\{\} ,"] {}]
    10.0.0.1 192.168.10.10 172.16.0.200
    [lindex [lsearch -all -inline -not -exact [split $xff "\{\} ,"] {}] 0]
    10.0.0.1

    So your irule should be

    when HTTP_REQUEST {
    set CHECK_IP [lindex [lsearch -all -inline -not -exact [split [HTTP::header values X-Forwarded-For] "\{\} ,"] {}] 0]
     if { !([class match $CHECK_IP eq DG-ALLOWED-IP]) } {
         if { [class match [HTTP::uri] eq DG-ALLOWED-URI-LIST] } {
              reject 
            }
         }
    }
  • This should do the trick, it has the added bonus of allowing for an empty header. Remove the break to process all the IP addresses on the X-Forwarded-For line instead of only the first. Note: If there are multiple of the same header or multiple addresses on the header the HTTP::header values command returns them all as a comma separated list.

    when HTTP_REQUEST {
      foreach CHECK_IP [split [HTTP::header values X-Forwarded-For] ","] {
        if { !([class match [string trim $CHECK_IP] eq DG-ALLOWED-IP]) } {
          if { [class match [HTTP::uri] eq DG-ALLOWED-URI-LIST] } {
           reject 
          }
        }
        break
      }
    }

  • Here's an old proc for sorting this down

    proc get_xff_ip {{xff_hdr {X-Forwarded-For}}} {
        foreach ip "[string map {- { } \{ { } \} { } , { } \[ { } \] { } \" { } \( { } \) { } ; { } \$ { } # { } \\ { }} [HTTP::header values $xff_hdr]] [IP::client_addr]" {
            if {![catch {IP::addr $ip mask ::}]} {
                if {![IP::addr $ip equals 127.0.0.0/8] && ![IP::addr $ip equals 0.0.0.0/32] && ![IP::addr $ip equals ::/127]} {
                    return $ip
                }
            }
        }
    }

    This returns the first non-localhost, non bogon 0.0.0.0 valid IP address in either the specified header or the client's IP.

  • ant77's avatar
    ant77
    Icon for Cirrostratus rankCirrostratus

    Hi All,

     

    Can anyone please help resolve this? I have tried the recommendation that Kevin suggested, however I am not sure if I am doing it correctly as it does not seem to work.

    Here is the old code that was working on 12.x code, but after upgrading to 13.x, it no longer works...

     

    F5, can you help with this here? How to change this so with version 13.x it is able to look/compare the client XFF IP against the data group and allow/deny/redirect based on that..

     

     

    **** Original code with getfield that looks at the client IP address in the XFF header, if it does not match the IP address listed in the "DG-ALLOWED-IP" data group, and the user tries to access the URIs listed in the "DG-ALLOWED-URI-LIST", they will get dropped...The switch command is also used for access and redirect based on the match in the "DG-ALLOWED-IP" data group..

     

    when HTTP_REQUEST {
    if { [active_members POOL-WEBSERVERS] < 1 } {
    HTTP::redirect " http://maintenance.mysite.com" 
        } else { 
     
    set CHECK_IP [getfield [HTTP::header values X-Forwarded-For] " " 1]
     if { !([class match $CHECK_IP eq DG-ALLOWED-IP]) } {
         if { [class match [HTTP::uri] eq DG-ALLOWED-URI-LIST] } {
                reject 
                 }
              }
              switch -glob [HTTP::uri] {
             "*/abc/portal1/idtrack*" -
             "*/cde/portal2/idtrack2*" -
             "*/fgh/abc/portal3/idtrack*" {
              if { ([class match $CHECK_IP eq DG-ALLOWED-IP]) } {
    			if { [HTTP::uri] contains "/public/idtracker" } {
    				HTTP::redirect "https://[HTTP::host]/secure/idtracker"
    			} else {
              HTTP::redirect "https://[HTTP::host]/login2/public/idtracker/" 
       	   }
    	 }
           }
        }
      }
    }
     

     

     

    • Simon_Blakely's avatar
      Simon_Blakely
      Icon for Employee rankEmployee

      First - did you try the irule I provided, or try the proc  provided?

      Two - if this occurred as a result of an upgrade, you could raise a support ticket with F5.

      Three - try some logging statements to determine where the error is occurring:

      when HTTP_REQUEST {
      if { [active_members POOL-WEBSERVERS] < 1 } {
      HTTP::redirect " http://maintenance.mysite.com" 
          } else { 
       
      set CHECK_IP [getfield [HTTP::header values X-Forwarded-For] " " 1]
      log local0. "the X-Forwarded-For header value is $CHECK_IP"
       if { !([class match $CHECK_IP eq DG-ALLOWED-IP]) } {
           if { [class match [HTTP::uri] eq DG-ALLOWED-URI-LIST] } {
                  reject 
                   }
                }
                switch -glob [HTTP::uri] {
               "*/abc/portal1/idtrack*" -
               "*/cde/portal2/idtrack2*" -
               "*/fgh/abc/portal3/idtrack*" {
                if { ([class match $CHECK_IP eq DG-ALLOWED-IP]) } {
      			if { [HTTP::uri] contains "/public/idtracker" } {
      				HTTP::redirect "https://[HTTP::host]/secure/idtracker"
      			} else {
                HTTP::redirect "https://[HTTP::host]/login2/public/idtracker/" 
         	   }
      	 }
             }
          }
        }
      }

      On the BigIP, run

      # tail -f /var/log/ltm | grep "the X-Forwarded-For header value is"

      to see the logged lines from the irule.

      Either you are getting an IP address with a leading "{" that needs to be removed, or Check_IP is empty - both will cause the IP address validation failure.

      Use irule logging to determine which one it is.

      • ant77's avatar
        ant77
        Icon for Cirrostratus rankCirrostratus

        Hi Simon,

        Sorry for the delay in replying. I tried your irule and it works perfectly. For some reason, after the upgrade, the F5 would not read the original "getfiled" irule correctly and would add a "," (comma) after the XFF client IP. This caused the iRule to not see the IP correct and the function would stop working.

        I added the following to the iRule and check the LTM logs and now the IP is without a comma and doing what it is supposed to.

        Added your portion to my original IRule.

        set CHECK_IP [lindex [lsearch -all -inline -not -exact [split [HTTP::header values X-Forwarded-For] "\{\} ,"] {}] 0]

        Thanks again!

  • Having not been notified of updates to this topic...

    Update [string trim $CHECK_IP] to...

    [string trim $CHECK_IP {{} }]

    But if you put James proc above the top if your original iRule you can simplify to this...

    set CHECK_IP [call get_xff_ip]

    • ant77's avatar
      ant77
      Icon for Cirrostratus rankCirrostratus

      Thank you Kevin for all your help. Much appreciated man!

  • ant77's avatar
    ant77
    Icon for Cirrostratus rankCirrostratus

    Thank you Simon and Kevin. You guys are awesome!

     

    let me test this out now and let you guys know how it goes.

  • ant77's avatar
    ant77
    Icon for Cirrostratus rankCirrostratus

    Hello Kevin,

     

    I just tested this and it does not seem to be working. It doesn't look like the client IP in the XFF header is being matched. This was working without issues when we were on code 12.1.3, but after the upgrade to 13.1.3, it stopped working and causes the website to fail.

     

    Conditions:

     

    1. The iRule should check the XFF header, and if the IP is other then what is in the data group (DG-ALLOWED-IP) when users are trying to go to the URIs listed in the other data group (DG-ALLOWED-URI-LIST), then it should be dropped.

     

    2. If they try to go to the other URI listed under the "switch", and their source XFF client IP matches what is in the "DG-ALLOWED-IP) data group, they should be allowed in.

     

    3. If they go to "/public/idtracker" and their IP matches the "DG-ALLOWED-IP" list, they should get redirected to "/secure/idtracker"

     

     

    We use a CDN that adds the original client IP to the XFF header..that is what we should be matching against.

     

    Data Group= DG-ALLOWED-IP

    Data group IP: 1.1.1.1 and 2.2.2.2

     

    Data Group=DG-ALLOWED-URI-LIST

    /auth1

    /auth2

    /auth3

     

    *** Below is your recommendation that I tried use in our iRule. I had to make some changes to our actual script for confidentiality of course...but you can see what we are trying to do here...It does not seem to be working. We have to temporarily remove this Irule to get the website to even load.

     

    when HTTP_REQUEST {
    if { [active_members POOL-WEBSERVERS] < 1 } {
    HTTP::redirect " http://maintenance.mysite.com" 
        } else { 
     
    foreach CHECK_IP [split [HTTP::header values X-Forwarded-For] ","] {
        if { !([class match [string trim $CHECK_IP] eq DG-ALLOWED-IP]) } {
          if { [class match [HTTP::uri] eq DG-ALLOWED-URI-LIST] } {
                reject 
                 }
                }
              break
              }
              switch -glob [HTTP::uri] {
             "*/abc/portal1/idtrack*" -
             "*/cde/portal2/idtrack2*" -
             "*/fgh/abc/portal3/idtrack*" {
              if { ([class match $CHECK_IP eq DG-ALLOWED-IP]) } {
    			if { [HTTP::uri] contains "/public/idtracker" } {
    				HTTP::redirect "https://[HTTP::host]/secure/idtracker"
    			} else {
              HTTP::redirect "https://[HTTP::host]/login2/public/idtracker/" 
       	   }
    	 }
           }
        }
      }
    }