Thursday, April 23, 2026

F5+ASM+XFF header

 When using ASM with conjunction with LTM+XFF sometimes based on the traffic path XFF header would have multiple IP address.

Example -
X-Forwarded-For: 8.8.8.8 4.4.4.2 10.137.255.232

Real value of XFF header is furthest left IP which is 8.8.8.8, But F5 selects the middle IP 4.4.4.2 because it's most "Trusted" in the context of RFC so we have to make sure that the leftmost one is selected via iRule.

when HTTP_REQUEST {
    # Check if the X-Forwarded-For header exists
    if { [HTTP::header exists "X-Forwarded-For"] } {
       
        # Get the full XFF string and split it by comma
        # The first element [lindex $list 0] is the original client IP

        set xff_list [split [HTTP::header "X-Forwarded-For"] ","]
        set client_ip [string trim [lindex $xff_list 0]]

        # Log the extracted IP for verification
        log local0. "Original Client IP extracted: $client_ip"

        # Optional: Insert the extracted IP into a new custom header
        # for the backend server to consume easily

        HTTP::header insert "X-Actual-Client-IP" $client_ip
    }
}

We can further tune the iRule to ignore private IP's or 127.0.01 to ignore if the XFF starts with them
X-Forwarded-For: 127.0.0.1 8.8.8.8  4.4.4.2  10.137.255.232

when HTTP_REQUEST {
    if { [HTTP::header exists "X-Forwarded-For"] } {
        set xff_list [split [HTTP::header "X-Forwarded-For"] ","]
        set client_ip ""

        foreach ip_item $xff_list {
            set tmp_ip [string trim $ip_item]
           
            # Skip if IP is local or private (RFC1918)
            if { [IP::addr $tmp_ip equals 127.0.0.1] or
                 [IP::addr $tmp_ip mask 255.0.0.0 equals 10.0.0.0] or
                 [IP::addr $tmp_ip mask 255.240.0.0 equals 172.16.0.0] or
                 [IP::addr $tmp_ip mask 255.255.0.0 equals 192.168.0.0] } {
                continue
            } else {
                # Found the first public IP
                set client_ip $tmp_ip
                break
            }
        }

        if { $client_ip ne "" } {
            # Log the result: in your doc example, this would capture 8.8.8.8
            log local0. "Extracted Public Client IP: $client_ip"
            HTTP::header insert "X-Actual-Client-IP" $client_ip
        }
    }
}


No comments: