An irule to validate client ID via DNS lookup using the stream profile.

Problem this snippet solves:

This irule can be used to validate the ID (hostname or IP addr) an SMTP client claims at the start of the SMTP protocol.

The validation is done via a DNS lookup if the client ID is a name, and unless the name exists in the DNS and its resolved address is identical with the real IP address of the TCP peer, the client connection is dropped.

If the client ID is an address, unless the address is identical with the real IP address of the TCP peer, the client connection is dropped.

Obviously, one would need to edit some of the matching strings if one's SMTP server uses a different lingo in the SMTP dialogue.

(Updated 07-Aug-2014•Originally posted on 07-Aug-2014)

Below was updated 28-Aug-2014•Originally posted on 28-Aug-2014:

In this irule, I define explicitly a route-domain ID in the IP address of my name server in [RESOLV::lookup]. This ought not be necessary as the partition default route domain should be assumed; but it does not work without it. I opened a case with F5 Support and the issue has now been confirmed as a bug (ID 476920).

How to use this snippet:


Code :

when RULE_INIT {
    set static::smtp_debug 1
    set static::route_domain_id 1
}
when CLIENT_ACCEPTED {
    if {[class match [getfield [IP::client_addr] % 1] equals internal_IP]} {
    } else {
        drop
        return
    }
    STREAM::expression {@[hH][eE][lL][oO] .*[[:cntrl:]][[:cntrl:]]@@ @[eE][hH][lL][oO] .*[[:cntrl:]][[:cntrl:]]@@ @[.][[:cntrl:]][[:cntrl:]]@@ @354 End data with @@ @250 2.0.0 Ok: queued as @@}
    STREAM::enable
    set disable_matching 0
}
when STREAM_MATCHED {
    switch -glob [STREAM::match] {
        "354 End data with " {
           incr disable_matching
            STREAM::replace
            return
        }
        "250 2.0.0 Ok: queued as " {
            STREAM::replace
            return
        }
        default {
            set mstring_hex ""
            set mstring_hex_trimmed ""
            set mstring_ascii ""
            set c_initiation_string_hex_trimmed ""
            set detected_c_initiation_string ""
            binary scan [STREAM::match] H* mstring_hex
            set mstring_hex_trimmed [string range $mstring_hex 0 end-4]
            set mstring_ascii [binary format H* $mstring_hex_trimmed]

            if { $mstring_ascii eq "\." } {
                set disable_matching 0
                STREAM::replace
                return
            }
            if { $disable_matching > 0 } {
                STREAM::replace
                return
            }
            if { ( [string tolower $mstring_ascii] starts_with "helo " ) or ( [string tolower $mstring_ascii] starts_with "ehlo " ) } {
                set c_declared_id [string range $mstring_ascii 5 end]
            }
            if { $c_declared_id contains "\[" } {
                set c_declared_id [ string map [ list \[ "" \] "" ] $c_declared_id ]
            }
            set a null
            set b null
            set c null
            set d null
            scan $c_declared_id {%d.%d.%d.%d} a b d c
            if { !($a == "null") && !($b == "null") && !($c == "null") && !($d == "null") } {
                if { (0 <= $a) && ($a <= 255) &&
                     (0 <= $b) && ($b <= 255) &&
                     (0 <= $c) && ($c <= 255) &&
                     (0 <= $d) && ($d <= 255) } {
                    if { $static::route_domain_id != 0 } {
                        append c_declared_id "%" $static::route_domain_id
                    }
                    if { not ( $c_declared_id equals [IP::client_addr] ) } {
                        drop
                        return
                    }
                }
            } else {
                if { ! ( $c_declared_id contains "\." ) } {
                    drop
                    return
                }
                if { $static::route_domain_id != 0 } {
                    set resolved_addrs [RESOLV::lookup @172.18.240.210%$static::route_domain_id -a $c_declared_id]
                } else {
                    set resolved_addrs [RESOLV::lookup @172.18.240.210 -a $c_declared_id]
                }
                if { not ( $resolved_addrs equals "" ) } {
                    set addr_matched 0
                    foreach resolved_addr $resolved_addrs {
                        if { $static::route_domain_id != 0 } {
                            append resolved_addr "%" $static::route_domain_id
                        }
                        if { $resolved_addr equals [IP::client_addr] } {
                            incr addr_matched
                            break
                        }
                    }
                    if { $addr_matched < 1 } {
                        drop
                        return
                    }
                } else {
                    drop
                    return
                }
            }
            STREAM::replace
            return
        }
    }
}

when SERVER_CONNECTED {
    STREAM::expression {@354 End data with @@ @250 2.0.0 Ok: queued as @@}
    STREAM::enable
}

#############################################################
#
#  Below is a modified version after removal of checking of an acl, which is not of general use.
#
#############################################################
when RULE_INIT {
    set static::route_domain_id 1
}
when CLIENT_ACCEPTED {
    STREAM::expression {@[hH][eE][lL][oO] .*[[:cntrl:]][[:cntrl:]]@@ @[eE][hH][lL][oO] .*[[:cntrl:]][[:cntrl:]]@@ @[.][[:cntrl:]][[:cntrl:]]@@ @354 End data with @@ @250 2.0.0 Ok: queued as @@}
    STREAM::enable
    set disable_matching 0
}
when STREAM_MATCHED {
    switch -glob [STREAM::match] {
        "354 End data with " {
           incr disable_matching
            STREAM::replace
            return
        }
        "250 2.0.0 Ok: queued as " {
            STREAM::replace
            return
        }
        default {
            set mstring_hex ""
            set mstring_hex_trimmed ""
            set mstring_ascii ""
            set c_initiation_string_hex_trimmed ""
            set detected_c_initiation_string ""
            binary scan [STREAM::match] H* mstring_hex
            set mstring_hex_trimmed [string range $mstring_hex 0 end-4]
            set mstring_ascii [binary format H* $mstring_hex_trimmed]

            if { $mstring_ascii eq "\." } {
                set disable_matching 0
                STREAM::replace
                return
            }
            if { $disable_matching > 0 } {
                STREAM::replace
                return
            }
            if { ( [string tolower $mstring_ascii] starts_with "helo " ) or ( [string tolower $mstring_ascii] starts_with "ehlo " ) } {
                set c_declared_id [string range $mstring_ascii 5 end]
            }
            if { $c_declared_id contains "\[" } {
                set c_declared_id [ string map [ list \[ "" \] "" ] $c_declared_id ]
            }
            set a null
            set b null
            set c null
            set d null
            scan $c_declared_id {%d.%d.%d.%d} a b d c
            if { !($a == "null") && !($b == "null") && !($c == "null") && !($d == "null") } {
                if { (0 <= $a) && ($a <= 255) &&
                     (0 <= $b) && ($b <= 255) &&
                     (0 <= $c) && ($c <= 255) &&
                     (0 <= $d) && ($d <= 255) } {
                    if { $static::route_domain_id != 0 } {
                        append c_declared_id "%" $static::route_domain_id
                    }
                    if { not ( $c_declared_id equals [IP::client_addr] ) } {
                        drop
                        return
                    }
                }
            } else {
                if { ! ( $c_declared_id contains "\." ) } {
                    drop
                    return
                }
                if { $static::route_domain_id != 0 } {
                    set resolved_addrs [RESOLV::lookup @172.18.240.210%$static::route_domain_id -a $c_declared_id]
                } else {
                    set resolved_addrs [RESOLV::lookup @172.18.240.210 -a $c_declared_id]
                }
                if { not ( $resolved_addrs equals "" ) } {
                    set addr_matched 0
                    foreach resolved_addr $resolved_addrs {
                        if { $static::route_domain_id != 0 } {
                            append resolved_addr "%" $static::route_domain_id
                        }
                        if { $resolved_addr equals [IP::client_addr] } {
                            incr addr_matched
                            break
                        }
                    }
                    if { $addr_matched < 1 } {
                        drop
                        return
                    }
                } else {
                    drop
                    return
                }
            }
            STREAM::replace
            return
        }
    }
}
when SERVER_CONNECTED {
    STREAM::expression {@354 End data with @@ @250 2.0.0 Ok: queued as @@}
    STREAM::enable
}

Tested this on version:

11.6
Published Jul 02, 2021
Version 1.0

Was this article helpful?

No CommentsBe the first to comment