Forum Discussion

JG's avatar
JG
Icon for Cumulonimbus rankCumulonimbus
May 18, 2014

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

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.

 

  • John_Alam_45640's avatar
    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.U3i7UygfSok

     

    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's avatar
    JG
    Icon for Cumulonimbus rankCumulonimbus

    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's avatar
    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's avatar
    Hamish
    Icon for Cirrocumulus rankCirrocumulus

    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's avatar
    JG
    Icon for Cumulonimbus rankCumulonimbus

    Just a short note for now:

     

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

     

  • 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's avatar
    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's avatar
    JG
    Icon for Cumulonimbus rankCumulonimbus

    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.

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

      • JG's avatar
        JG
        Icon for Cumulonimbus rankCumulonimbus

        I am trying to get this back.