External SMTP monitor (written in Go)
Problem this snippet solves: We wrote an external SMTP monitor to work around K99840695. It moderatly customizable, supports STARTTLS and has builtin support to test content filtering via EICAR/GTUBE. The code is on Github, auto-generated releases are on the project's [https://github.com/hreese/f5-smtp-monitor/releases](release page). Code : https://github.com/hreese/f5-smtp-monitor Tested this on version: 11.6340Views0likes0CommentsSMTP Proxy
Problem this snippet solves: This iRule implements a simple SMTP proxy. This is just to show an idea to relay smtp message and to route mail traffic based on FROM or TO... this rule is tested and work with one default pool and basic smtp client. Code : when CLIENT_ACCEPTED { set chelo "" set cfrom "" set crcpt "" TCP::respond "220\r\n" log local0. "client accepted" TCP::collect } when CLIENT_DATA { set cdata [TCP::payload] if { [ string length $cdata ] <= 0 } { return } if { not ( $cdata contains "\r\n" ) } { log local0. "get <$cdata> so far" return } if { $cdata starts_with "HELO" } { set chelo [TCP::payload] log local0. "get helo <$cdata>" TCP::respond "250 OK\r\n" TCP::payload replace 0 [string length $chelo] "" return } if { $cdata starts_with "MAIL FROM:" } { set cfrom [TCP::payload] log local0. "get from <$cfrom>" TCP::respond "250 OK\r\n" TCP::payload replace 0 [string length $cfrom] "" return } if { $cdata starts_with "RCPT TO:" } { set crcpt "$crcpt[TCP::payload]" log local0. "get rcpt <$crcpt>" TCP::respond "250 OK\r\n" TCP::payload replace 0 [string length [TCP::payload]] "" return } if { $cdata starts_with "DATA" } { log local0. "get data <$cdata>" TCP::payload replace 0 0 $chelo$cfrom$crcpt } log local0. "payload [TCP::payload]" TCP::release TCP::collect } when SERVER_CONNECTED { log "server connected" TCP::collect } when SERVER_DATA { set sdata [TCP::payload] if { $sdata starts_with "220" } { log local0. "get data <$sdata>" TCP::payload replace 0 [string length $sdata] "" return } if { $sdata contains "\r\n354 " } { log local0. "get data <$sdata>" TCP::payload replace 0 [string length $sdata] "354\r\n" } if { [ string length $sdata ] <= 0 } { return } log local0. "payload <[TCP::payload]>" TCP::release TCP::collect } when CLIENT_CLOSED { log local0. "client closed" }2.7KViews0likes2CommentsSTARTTLS Server SMTP with cleartext and STARTTLS client support
Problem this snippet solves: We were looking at our O365 security score and our SMTP scores were pretty sad, so I looked at how I could create a STARTTLS connection up to O365 regardless of the client support. Originally I just supported cleartext clients, but I was able to get it to support clients running STARTTLS too. Links in the iRule to some helpful threads I used while developing this. How to use this snippet: Attach this iRule to a SMTP VS with a clientssl, serverssl and a SMTP profile. Code : # This iRule allows dynamic cleartext and STARTTLS client connection with a STARTTLS connection to the upstream server # unfortunately, cleartext to server connections do not work with the iRule as-is when CLIENT_ACCEPTED { # No SSL client side, also check no SSL running already on server side # Debug mode set DEBUG 1 set SERVER_SSL 0 set CLIENT_SSL 0 set EHLO_Name "smtp.domain.com" if { $DEBUG } { log local0. "CLIENT_ACCEPTED" } SSL::disable serverside # Disable All TLS so we can dynamically enable it SSL::disable } when SERVER_CONNECTED { if { $DEBUG } { log local0. "SERVER_CONNECTED" } # Start collecting from the server because in SMTP the server responds first TCP::collect } when CLIENT_DATA { set lcpayload [string tolower [TCP::payload]] if { $DEBUG } { log local0.debug "CLIENT_DATA - PAYLOAD - $lcpayload" } if { $lcpayload starts_with "ehlo" } { if { $DEBUG } { log local0.debug "CLIENT_DATA - ehlo" } # https://devcentral.f5.com/s/articles/offload-smtp-encryption-via-irules # Spoof back STARTTLS headers to the client so they'll tell us if the support it or not TCP::respond "250-STARTTLS\r\n250 OK\r\n" TCP::payload replace 0 [TCP::payload length] "" TCP::release TCP::collect } elseif { $lcpayload starts_with "starttls" } { if { $DEBUG } { log local0.debug "CLIENT_DATA - Starttls" } # https://devcentral.f5.com/s/articles/smtp-start-tls # Spoof back the 'Ready to start TLS' header to the client so we can do client<->F5 SSL TCP::respond "220 Ready to start TLS\r\n" TCP::payload replace 0 [TCP::payload length] "" TCP::release clientside {SSL::enable} } elseif { $lcpayload starts_with "rset" and $SERVER_SSL == 1 } { if { $DEBUG } { log local0.debug "CLIENT_DATA - Client RSET" } # In situations where the client is not encrypting, but the server is, it appears nessecary to reset and re-EHLO to the SMTP server. # In this case, these are the advertisements from the O365 relay so you may have to adjust them according to your SMTP server responses. # This can be attained by setting DEBUG variable to '1' and watching /var/log/ltm when you send a message for the response returned from # the SMTP server for 'Hello'. # Example: Rule /Common/SMTP_STARTTLS : server SSL payload: 250-blah.mail.protection.outlook.com Hello [removed-IP] 250-SIZE 157286400 250-PIPELINING 250-DSN 250-ENHANCEDSTATUSCODES 250-8BITMIME 250-BINARYMIME 250-CHUNKING 250 SMTPUTF8 TCP::respond "250-STARTTLS 250-SIZE 157286400 250-PIPELINING 250-DSN 250-ENHANCEDSTATUSCODES 250-8BITMIME 250-BINARYMIME 250-CHUNKING 250 SMTPUTF8\r\n" TCP::payload replace 0 [TCP::payload length] "" TCP::release TCP::collect # /var/log/ltm complains about this, but it appears to work serverside { SSL::respond "EHLO $EHLO_Name\r\n" } } else { if { $DEBUG } { log local0.debug "CLIENT_DATA - Default release" } TCP::release } } when SERVER_DATA { # Most of thise was helpfully taken from https://devcentral.f5.com/s/questions/need-an-irule-for-starttls-for-smtps-server-side-only-not-client-side # Read in responses from remote server into a variable and log to /var/log/ltm set payload [string tolower [TCP::payload]] if { $DEBUG } { log local0. "SERVER_DATA - PAYLOAD - $payload" } if {$payload starts_with "220" and $payload contains "esmtp"} { # Listen for remote servers opening 220 and esmtp message # NOTE the ‘if’ statement above may need to be tweaked to except what message the other # side is actually sending in reply. Logs should show this. # Respond with a EHLO to server, most servers require a name after the EHLO as well. TCP::respond "EHLO $EHLO_Name\r\n" TCP::payload replace 0 [TCP::payload length] "" TCP::release if { $DEBUG } { log local0. "SERVER_DATA - Responded to server with EHLO" } serverside {TCP::collect} } elseif {$payload contains "250-starttls" } { # Check server responds with "250-starttls", if so, respond with a STARTTLS TCP::respond "STARTTLS\r\n" TCP::payload replace 0 [TCP::payload length] "" TCP::release if { $DEBUG } { log local0. "SERVER_DATA - Sent the server a STARTTLS" } serverside {TCP::collect} } elseif {$payload contains "220 ready for tls" or $payload contains "220 2.0.0 continue" or $payload contains "220 2.0.0 smtp server ready" } { # if server gives a 220 response, then start server side ssl profile # NOTE the ‘if’ statement above may need to be tweaked to except what message the other # side is actually sending in reply. Logs should show this. ###### # O365 Edit - O365 returns 220 2.0.0 smtp server ready after enabling TLS - Adjust as needed ###### if { $DEBUG } { log local0. "SERVER_DATA - server said he is ready for TLS, enable the SSL profile" } TCP::payload replace 0 [TCP::payload length] "" TCP::release serverside {SSL::enable} # TLS hanshake should now start, which is best seen in wireshark packet captures. } else { if { $DEBUG } { log local0.debug "SERVER_DATA - Default release" } TCP::release clientside { TCP::collect } } } when SERVERSSL_HANDSHAKE { # This will only trigger if that is completed successfully. # ServerSSL profile will need a certificate to match the outbound IP and DNS name, # and you may want to set the "Server certificate" setting to "require", # and the "Trusted Certificate Authorities" set to "ca-bundle". if { $DEBUG } { log local0. "SERVERSSL_HANDSHAKE - SSL handshake completed." } set SERVER_SSL 1 if { $CLIENT_SSL == 1 } { if { $DEBUG } { log local0.debug "SERVERSSL_HANDSHAKE - Client respond SSL" } clientside { SSL::respond "220 SMTP ESMTP Relay F5\r\n" } } else { if { $DEBUG } { log local0.debug "SERVERSSL_HANDSHAKE - Client respond TCP" } clientside { TCP::respond "220 SMTP ESMTP Relay F5\r\n" } # Give the client side a chance to STARTTLS clientside { TCP::collect } } if { $DEBUG } { log local0.debug "SERVERSSL_HANDSHAKE - SSL collect" } SSL::collect } when CLIENTSSL_HANDSHAKE { # This will only trigger if that is completed successfully. # ServerSSL profile will need a certificate to match the outbound IP and DNS name, # and you may want to set the "Server certificate" setting to "require", # and the "Trusted Certificate Authorities" set to "ca-bundle". if { $DEBUG } { log local0. "SSL handshake completed." } set CLIENT_SSL 1 SSL::collect } when SERVERSSL_DATA { # Log the SMTP responses to see any errors. if { $DEBUG } { log local0. "SERVERSSL_DATA - PAYLOAD - [SSL::payload]" } SSL::release if { $CLIENT_SSL == 0 } { if { $DEBUG } { log local0.debug "SERVERSSL_DATA - Client TCP Collect"} clientside { TCP::collect } } SSL::collect } when CLIENTSSL_DATA { # Log the SMTP responses to see any errors. if { $DEBUG } { log local0. "CLIENTSSL_DATA - PAYLOAD - [SSL::payload]" } #log local0.debug "Clientssl_data - release" SSL::release SSL::collect }2.6KViews0likes6CommentsSMTP Start TLS
Problem this snippet solves: Summary: This iRule allows either clear text or TLS encrypted communication with the LTM initiating the encryption process if it sees the appropriate "starttls" command in the SMTP communication. Code : when CLIENT_ACCEPTED { set ehlo 0 SSL::disable } when SERVER_CONNECTED { TCP::collect } when CLIENT_DATA { set lcpayload [string tolower [TCP::payload]] if { $lcpayload starts_with "ehlo" } { set ehlo 1 serverside { TCP::collect } TCP::release TCP::collect } elseif { $lcpayload starts_with "starttls" } { TCP::respond "220 Ready to start TLS\r\n" TCP::payload replace 0 [TCP::payload length] "" TCP::release SSL::enable } else { TCP::release } } when SERVER_DATA { if { $ehlo == 1 and not([string tolower [TCP::payload]] contains "starttls") } { TCP::payload replace 0 0 "250-STARTTLS\r\n" } TCP::release clientside { TCP::collect } }1.3KViews0likes3CommentsSMTPStartTLS
Problem this snippet solves: Summary: This iRule allows either clear text or TLS encrypted communication with the LTM initiating the encryption process if it sees the appropriate "starttls" command in the SMTP communication. Code : when CLIENT_ACCEPTED { set ehlo 0 SSL::disable } when SERVER_CONNECTED { TCP::collect } when CLIENT_DATA { set lcpayload [string tolower [TCP::payload]] if { $lcpayload starts_with "ehlo" } { set ehlo 1 serverside { TCP::collect } TCP::release TCP::collect } elseif { $lcpayload starts_with "starttls" } { TCP::respond "220 Ready to start TLS\r\n" TCP::payload replace 0 [TCP::payload length] "" TCP::release SSL::enable } else { TCP::release } } when SERVER_DATA { if { $ehlo == 1 and not([string tolower [TCP::payload]] contains "starttls") } { TCP::payload replace 0 0 "250-STARTTLS\r\n" } TCP::release clientside { TCP::collect } }529Views0likes0CommentsSMTP 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 }1.1KViews0likes0CommentsSMTP Stream catching unapproved domains
Problem this snippet solves: Use the default Stream profile and filter out mail to unapproved domains, and send to a bucket, the code is basic but the regex is very useful. 'mydomain' = the approved domain, everything else is matched. Code : when CLIENT_ACCEPTED { log local0. "client accepted" STREAM::expression {=RCPT TO:<[A-Za-z0-9._%+-]+@(?!(myOKDomain|myOtherOKDomain))[A-Za-z0-9.-]+\.[A-Za-z]{2,4}>=RCPT TO: =} STREAM::enable } when STREAM_MATCHED { log local0. "Stream filter matched: [STREAM::match]" }253Views0likes0CommentsDistribute Email by Source IP
Problem this snippet solves: I had a customer who wanted to use a single virtual IP address for a mail server, but have the mail go to different pools depending on whether the mail was internal or external. So I wrote the following iRule which I connected to the SMTP virtual server definition. This script assumes that the LTM floating IP address is set as the default gateway for the physical servers, because the physical servers are going to see source IP addresses from all over the world. If, for some reason, the physical servers have to have their default gateways set to someplace else, then add a snat automap command after the when CLIENT_ACCEPTED command. Code : rule smtp_direct_rule { when CLIENT_ACCEPTED { if { [IP::addr [IP::remote_addr] equals 10.2.0.0/255.255.0.0 ] } { log local0. "Node IP address is: [IP::remote_addr] and sent to SMTP_clients_from_10.2" pool smtp_clients_from_10.2 } else { log local0. "Node IP address is: [IP::remote_addr] and sent to SMTP_clients_from_elsewhere" pool SMTP_clients_from_elsewhere } }549Views0likes1Comment