SMTP filter and forward proxy

Problem this snippet solves:

This iRule provides a forward proxy for SMTP messages; it also filters messages by limiting the maximum size of the DATA sent, and also limiting the amount of messages per TCP connection.

The iRule is limited to a single RCPT at the moment... I'm not sure this could have multiple RCPTs as this would need support of multiple server connections, and a way to store the DATA which doesn't sound like a good thing to do. The other limitation is that it does not support pipelining; I think this is something that could be done in the next version, but will complicate the iRule somewhat.

WARNING: This iRule has NOT been tested in earnest.

Code :

when RULE_INIT {
# The following parameter enables local logging - 0=off, 1=on
set static::SMTPfiltprxdebug 1
# Set maximum message size in bytes (21504000 is 21Mbyte)
set static::maxMsgLen 21504000
# Set maximum number of messages in a single TCP connection (20)
set static::maxNumMsgs 20
#
# High speed logging setup - local7.info
set static::bigip [info hostname]
set static::facility <190>
set static::hsl_prefix "$static::facility|host=$static::bigip"
}

when CLIENT_ACCEPTED {
# Open a connection for high speed logging to hsl_syslog_pool & define log prefix
set hsl [HSL::open -proto UDP -pool hsl_syslog_pool]
set hsl_prefix "${static::hsl_prefix}|client=[IP::client_addr]:[TCP::client_port]"
# Set up a variable to collect number of RCPTs
set numRcpts 0
# Set up variables to collect length of email
set dataFlag 0
set msgLen 0
# Set up variable to collect number of messages in TCP connection
set numMsgs 0
# Set up other variables use to collect payload until we have a RCPT to act upon
set heloCmd ""
set mailCmd ""
set rcptCmd ""
set destIP ""
set smtpSeq 0
# Send back a SMTP service ready and collect client data
TCP::respond "220 Proxy Ready\r\n"
TCP::collect
}

when CLIENT_DATA {
if { [string length [TCP::payload]] <= 0 } {
return
}
if { not ( [TCP::payload] contains "\r\n" ) } {
return
}
switch -glob {[string tolower [TCP::payload]]} {
rset* {
# Blank out payload
TCP::payload replace 0 [string length [TCP::payload]] ""
# Send back an SMTP OK
TCP::respond "250 OK\r\n"
TCP::collect
return
}
helo* { 
# Store the command
set heloCmd [TCP::payload]
# Blank out payload
TCP::payload replace 0 [string length $heloCmd] ""
# Send back an SMTP OK
TCP::respond "250 Hello client at [IP::client_addr]\r\n"
TCP::collect
return
}
ehlo* {
# Store the command
set heloCmd [TCP::payload]
# Blank out payload
TCP::payload replace 0 [string length $heloCmd] ""
# Send back an SMTP OK
TCP::respond "250 Hello client at [IP::client_addr] \r\n"
# Restart collection
TCP::collect
return
}
mail* { 
# Release any server connection if applicable
LB::detach
# MAIL FROM is first command of every message so this is a good place reset variables
set numRcpts 0
set dataFlag 0
set msgLen 0
set smtpSeq 0
# Increment number of messages
incr numMsgs 1
# Store the command
set mailCmd [TCP::payload]
# Blank out payload
TCP::payload replace 0 [string length $mailCmd] ""
# Send back an SMTP OK
TCP::respond "250 sender ok\r\n"
# Restart collection
TCP::collect
return
}
rcpt* {
incr numRcpts 1
# Check to ensure the maximum number of recipients has not been breached
if { $numRcpts > 1 } {
# This session is going to be closed down as too many RCPTs have been detected
set log_message "event=CLIENT_DATA|desc=Over $static::maxNumRcpts recipients detected - session aborted|\n"
HSL::send $hsl "$hsl_prefix|$log_message"
if { $static::SMTPfiltprxdebug ne "0" } {
log local0. "$log_message"
}
reject
return
}
# Store the command
set rcptCmd [TCP::payload]
# Need to get the RCPT domain and lookup onward host
set rcptDomain [lindex [split [lindex [split [lindex [split [lindex [split $rcptCmd ":"] 1] "@"] 1] "\r\n"] 0] ">"] 0]
# Use RCPT domain to find mail host
set mailSrvs [RESOLV::lookup @/Common/dns -mx $rcptDomain]
#  NB: If there are multiple entries it could be a TCL list.
#  Check if the first list element was empty
if { $mailSrvs equals "" } {
reject
set log_message "event=CLIENT_DATA|desc=DNS lookup (MX) failed for $rcptDomain|\n"
HSL::send $hsl "$hsl_prefix|$log_message"
if { $static::SMTPfiltprxdebug ne "0" } {
log local0. "$log_message"
}
return
} else {
# Select the 1st returned mail server name and find IP address
set destHost [lindex $mailSrvs 1]
set ips [RESOLV::lookup @/Common/dns -a $destHost]
# Again this could be a TCL list
if { $ips equals "" } {
reject
set log_message "event=CLIENT_DATA|desc=DNS lookup (A) failed for $destHost|\n"
HSL::send $hsl "$hsl_prefix|$log_message"
if { $static::SMTPfiltprxdebug ne "0" } {
log local0. "$log_message"
}
return
} else {
set destIP [lindex $ips 0]
}
}
# Blank out payload
TCP::payload replace 0 [string length $rcptCmd] ""
# Connect to server and release stored SMTP messages
set log_message "event=CLIENT_DATA|desc=Connection to server $destIP|\n"
HSL::send $hsl "$hsl_prefix|$log_message"
if { $static::SMTPfiltprxdebug ne "0" } {
log local0. "$log_message"
}
node $destIP 25
}
data* { set dataFlag 1 }
}
# Start counting payload lengths once the DATA command has been seen
if { $dataFlag ne "0" } {
incr msgLen [TCP::payload length]
}
# Check to ensure the maximum message size has not been breached
if { $msgLen > $static::maxMsgLen } {
# This session is going to be closed down as it is too big
set log_message "event=CLIENT_DATA|desc=Over $static::maxMsgLen byte message size detected - session aborted|\n"
HSL::send $hsl "$hsl_prefix|$log_message"
if { $static::SMTPfiltprxdebug ne "0" } {
log local0. "$log_message"
}
reject
return
}
# Check to ensure the maximum number of messages has not been breached
if { $numMsgs > $static::maxNumMsgs } {
#This session is going to be closed down as too many messages have been attempted
set log_message "event=CLIENT_DATA|desc=Over $static::maxNumMsgs messages detected - session aborted|\n"
HSL::send $hsl "$hsl_prefix|$log_message"
if { $static::SMTPfiltprxdebug ne "0" } {
log local0. "$log_message"
}
reject
return
}
    TCP::release
    TCP::collect
}

when SERVER_CONNECTED {
TCP::collect
}

when SERVER_DATA {
switch -glob [string tolower [TCP::payload]] {
220* {
# Suppress message to client
TCP::payload replace 0 [string length [TCP::payload]] ""
# Send stored HELO command to server
TCP::respond $heloCmd
# Restart collection
TCP::collect
return
}
250* {
incr smtpSeq 1
switch $smtpSeq {
1 {
# Suppress message to client
TCP::payload replace 0 [string length [TCP::payload]] ""
# First time we see this we need to send MAIL command
TCP::respond $mailCmd
# Restart collection
TCP::collect
return
}
2 {
# Suppress message to client
TCP::payload replace 0 [string length [TCP::payload]] ""
# First time we see this we need to send MAIL command
TCP::respond $rcptCmd
# Restart collection
TCP::collect
return
}
}

}
}
TCP::release
}
Published Mar 18, 2015
Version 1.0

Was this article helpful?

No CommentsBe the first to comment