Technical Forum
Ask questions. Discover Answers.
cancel
Showing results for 
Search instead for 
Did you mean: 

iRule for SMTP: Passing Client IP Addr to backend mail servers

JG
Cumulonimbus
Cumulonimbus

When SNATs are used for a virtual server, the backend SMTP servers cannot get the client IP address. This irule is intended to replace the string after "EHLO" or "HELO" in mail client initiation with the client's real IP address. For us, this could enable us to track down an offending mail originating device.

 

when CLIENT_ACCEPTED {
    set c-addr [IP::client_addr]
    log local0. "Client addr: $c-addr"
}

when CLIENT_DATA {
    STREAM::expression {@^EHLO.*\r\n@@ @^HELO.*\r\n@@}
    STREAM::enable
    event STREAM_MATCHED enable
}

when STREAM_MATCHED {
    set mstring [STREAM::match]
    log local0. "STREAM_MATCHED: string: $mstring"
    if {$mstring starts_with "EHLO"} {
        set replacment "EHLO $c-addr\r\n"
        log local0. "STREAM_MATCHED: replacement string: $replacement"
        STREAM::replace "$mstring/$replacment"
    }
    if {$mstring starts_with "HELO"} {
        set replacment "HELO $c-addr\r\n"
        log local0. "STREAM_MATCHED: replacement string: $replacement"
        STREAM::replace "$mstring/$replacment"
    }
    event STREAM_MATCHED disable
}

when SERVER_DATA {
    STREAM::disable
}

This is just an idea at this moment, and I won't be able to test the code until I find a suitable test environment for it; but for now, any comment is welcome as to if this will work at all and if yes what can be improved. Thanks.

 

15 REPLIES 15

John_Alam_45640
Historic F5 Account

This is pretty nice.

 

CLIENT_DATA and SERVER_DATA are not going to trigger on their own. You have to initiate TCP::collect in the CLIENT_ACCEPTED or SERVER_CONNECTED. That said, you don't need these events.

 

Also, "event STREAM_MATCHED enable" is not needed. When there is a match, this event will fire.

 

Simplify the code under STREAM_MATCHED for easier troubleshooting:

 

when STREAM_MATCHED {
    set mstring [STREAM::match]
    log local0. "STREAM_MATCHED: string: $mstring"
    set replacment [string range $mstring 0 1]
    append replacment "LO $c-addr\r\n"
    log local0. "STREAM_MATCHED: replacement string: $replacement"
    STREAM::replace "$mstring/$replacment"
}

Don't forget the generic (empty) STREAM profile to be added to the virtual. Here is a good link for more info: https://devcentral.f5.com/articles/ltm-stream-profile-multiple-replacements-regular-expressions.U3i7...

 

Finally, why not test this using a mocked up SMTP server. Setup up an echo server and use telnet for a client. See if the BigIP does the replacement.

 

JG
Cumulonimbus
Cumulonimbus

Thanks so much, John! Here's a new version of it based on your advice:

when CLIENT_ACCEPTED {
    set c-addr [IP::client_addr]
    log local0. "Client addr: $c-addr"
    STREAM::expression {@^EHLO.*\r\n@@ @^HELO.*\r\n@@}
    STREAM::enable
}

when STREAM_MATCHED {
    set mstring [STREAM::match]
    log local0. "STREAM_MATCHED: string: $mstring"
    set replacment [string range $mstring 0 1]
    append replacment "LO $c-addr\r\n"
    log local0. "STREAM_MATCHED: replacement string: $replacement"
    STREAM::replace "$mstring/$replacment"
    event STREAM_MATCHED disable
}

when SERVER_CONNECTED {
    STREAM::disable
}

I can't test this (it compiled alright) yet because my devices are all firewall'ed off at the moment.

John_Alam_45640
Historic F5 Account

The SERVER_CONNECTED event is not needed.

One more thing. In the STREAM::replace command, only the replacement is needed.

STREAM::replace $replacment

Now test it by using a linux server behind a BigIP virtual and netcat such as in these examples.

http://stackoverflow.com/questions/8375860/echo-server-with-bash

You should be able to use telnet and type:

HELO 1.2.3.4

or

EHLO 1.2.3.4

and it echos back "xxLO [your IP address]"

Hamish
Cirrocumulus
Cirrocumulus

Just a thought... But why don't you add a received: header? That way the sending IP is available for anything you want to put in the sequence (e.g. spamassassin etc).

 

H

 

JG
Cumulonimbus
Cumulonimbus

Just a short note for now:

 

I will need to modify this to accommodate the sending of multiple messages in the same TCP connection.

 

John_Alam_45640
Historic F5 Account

To accommodate multiple messages, check out this SMTP proxy iRule example. https://devcentral.f5.com/wiki/iRules.SMTPProxy.ashx

 

It uses [TCP::collect] and the CLIENT_DATA event as well as [TCP::payload replace] instead of [STREAM::replace]. It offers more flexibility and allows multiple messages search/replace operations.

 

nitass
F5 Employee
F5 Employee

i think stream profile may be fine with multiple messages. anyway, i think we are having an issue about newline (\r\n) in stream expression.

 

John_Alam_45640
Historic F5 Account

Take out the \r\n from the expression. The (.*) should match that as well.

STREAM::expression {@^EHLO.*@@ @^HELO.*@@}

Here is an example:

(System32) 30 % regexp {EHLO.*} "EHLO 1.1.1.1\r\n" var1
1
(System32) 31 % puts $var1
EHLO 1.1.1.1

(System32) 32 % 

JG
Cumulonimbus
Cumulonimbus

I have finally had a chance to actually test an SMTP irule using the stream profile, and here's my latest version, which seems to work, at least under v10.2.4:

when RULE_INIT {
    set static::smtp_debug 1
}

when CLIENT_ACCEPTED {
    set caddr [IP::client_addr]
    if { ${static::smtp_debug} } { log local0. "Client addr: $caddr" }
    STREAM::expression {@[hH][eE][lL][oO] @@ @[eE][hH][lL][oO] @@ @354 End data with @354 End data with @ @250 2.0.0 Ok: queued as @250 2.0.0 Ok: queued as @}
    STREAM::enable
    set end_data_with_seen 0
}

when STREAM_MATCHED {
    set mstring [STREAM::match]
    if { ${static::smtp_debug} } { log local0. "STREAM_MATCHED: string: \"$mstring\"" }

    if { $mstring contains "354 End data with " } {
        if { ${static::smtp_debug} } { log local0. "STREAM_MATCHED: end_data_with_seen incremented." }
        incr end_data_with_seen
        return
    } elseif { $mstring starts_with "250 2.0.0 Ok: queued as " } {
        if { ${static::smtp_debug} } { log local0. "STREAM_MATCHED: seen queued_as: closing conn." }
        TCP::close
        return
    } else {
        if { $end_data_with_seen < 1 } {
            if { ${static::smtp_debug} } { log local0. "STREAM_MATCHED: init string seen: end_data_with_seen is 0." }
            set replacement [string range $mstring 0 1]
            append replacement "LO \[$caddr\]"

            if { ${static::smtp_debug} } { log local0. "STREAM_MATCHED: replacement string: \"$replacement\"" }
            STREAM::replace $replacement
        } else {
            if { ${static::smtp_debug} } { log local0. "STREAM_MATCHED: end_data_with_seen: not 0: stream disabled." }
            STREAM::disable
        }
    }
}

when SERVER_CONNECTED {
    STREAM::expression {@[hH][eE][lL][oO] @@ @[eE][hH][lL][oO] @@ @354 End data with @354 End data with @ @250 2.0.0 Ok: queued as @250 2.0.0 Ok: queued as @}
    STREAM::enable
}

Of course it is not perfect, and I don't think it can be perfect, as it seems we have run into the limitations of the BRE library the stream profile uses, or of my understanding of how to use the stream functionality.

As a result, I have to terminate the connection after a mail message is delivered to the backend SMTP server in this irule.

Some of the text strings in the regex expressions are specific to my mail server in order to minimize the chances of unexpected/incorrect/unwanted matching.

JG
Cumulonimbus
Cumulonimbus

I have finally decided to change to another approach: to validate client's ID (hostname or IP addr) via DNS lookup and make a deny/allow decision based on the result of the lookup. The irule is here:

 

Editors note: Updated this link to point to the new Codeshare @JG created.

An irule to validate client ID via DNS lookup using the stream profile. DevCentral (f5.com)

 

The link for the irule needs fixing as I don't find it even in the search in Devcentral.

I am trying to get this back.

Found it in the archives:

 

http://web.archive.org/web/20171128045302/https:/devcentral.f5.com/questions/an-irule-to-validate-client-id-via-dns-lookup-using-the-stream-profile​

Thanks! You can reshare the code in code share but I will attach it here just in case:

 

 

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

}