Microsoft Office 365 IP intelligence
Problem this snippet solves:
This snippet adds Microsoft Office 365 IP intelligence. This snippet parses the O365IPAddresses.xml file that Microsoft supplies to help identify Microsoft URLs and IP address ranges. With this snippet you can check if an IP address belongs to Microsoft and let the BIG-IP decide to allow or deny traffic.
For more info see: Office 365 URLs and IP address ranges
DISCLAIMER: This code has never been tested outside a lab environment. Consider this snippet to be a proof-of-concept.
How to use this snippet:
Prepare BIG-IP
- Create LX Workspace: office365_ipi
- Add iRule: office365_ipi_irule
- Add Extension: office365_ipi_extension
- Add LX Plugin: office365_ipi_plugin -> From Workspace: office365_ipi
Install node.js modules
# cd /var/ilx/workspaces/Common/office365_ipi/extensions/office365_ipi_extension # npm install xml2js https repeat lokijs ip-range-check --save
office365_ipi_irule
### ### Name : office365_ipi_irule ### Author : Niels van Sluis, (niels@van-sluis.nl) ### Version: 0.1 ### Date : 2017-07-25 ### when RULE_INIT { # set table timeout to 1 hour set static::office365_ipi_timeout 3600 set static::office365_ipi_lifetime 3600 } when CLIENT_ACCEPTED { # Valid product names are: # o365, LYO, Planner, Teams, ProPlus, OneNote, Yammer, # EXO, Identity, Office365Video, WAC, SPO, RCA, Sway, # EX-Fed, OfficeMobile, CRLs, OfficeiPad, EOP # # Use 'any' to match an IP address in all products. set productName "o365" set ipAddress [IP::client_addr] set key $productName:$ipAddress set verdict [table lookup -notouch $key] if { $verdict eq "" } { log local0. "Need to retrieve verdict via iruleslx" set rpc_handle [ILX::init office365_ipi_plugin office365_ipi_extension] if {[catch {ILX::call $rpc_handle checkProductIP $productName $ipAddress} verdict]} { log local0.error "Client - [IP::client_addr], ILX failure: $verdict" return } log local0. "The verdict for $ipAddress: $verdict"; # cache verdict table set $key $verdict $static::office365_ipi_timeout $static::office365_ipi_lifetime } # verdict is 0 (reject) or 1 (allow) if { !($verdict) } { log local0. "rejected IP address: $ipAddress" reject } }
Code :
/** *** Name : office365_ipi_extension *** Author : Niels van Sluis,*** Version: 0.1 *** Date : 2017-07-25 **/ 'use strict'; // Import the f5-nodejs module and others. var f5 = require('f5-nodejs'); var parseString = require('xml2js').parseString; var https = require('https'); var repeat = require('repeat'); var loki = require('lokijs'); var ipRangeCheck = require('ip-range-check'); // Create (in-memory) LokiJS database. var db = new loki('db.json'); var products = db.addCollection('products'); // Create a new rpc server for listening to TCL iRule calls. var ilx = new f5.ILXServer(); // URL to Microsoft Office 365 XML file. var url = "https://support.content.office.net/en-us/static/O365IPAddresses.xml"; // Function to get XML file and convert to JSON object. function xmlToJson(url, callback) { var req = https.get(url, function(res) { var xml = ''; res.on('data', function(chunk) { xml += chunk; }); res.on('error', function(e) { callback(e, null); }); res.on('timeout', function(e) { callback(e, null); }); res.on('end', function() { if(res.statusCode == 200) { parseString(xml, function(err, result) { callback(null, result); }); } }); }); } // Function that uses the data in the XML file to create a database // that can be used to perform IP address and URL lookups. function getOffice365() { xmlToJson(url, function(err,data) { if(err) { console.log("Error: xmlToJson failed"); return; } // if xml happens to be empty due to an error, do not continue. if(!data) { console.log("Error: No data in XML file"); return; } // Get date updated: 7/13/2017 var productsUpdated = data.products.$.updated; // Only update if version changed var versionCheck = products.findObject({'name':'any'}); if(versionCheck && versionCheck.version == productsUpdated) { console.log("Info: product version didn't changed; No update required"); return; } var allIpAdresses = []; var allUrls = []; data.products.product.forEach(function (product) { var ipAddresses = []; var urls = []; product.addresslist.forEach(function (addresslist) { if(addresslist.$.type == "IPv4" || addresslist.$.type == "IPv6") { if ( typeof addresslist.address !== 'undefined' && addresslist.address ) { addresslist.address.forEach(function (address) { ipAddresses.push(address); allIpAdresses.push(address); }); } } else if(addresslist.$.type == "URL") { if ( typeof addresslist.address !== 'undefined' && addresslist.address ) { addresslist.address.forEach(function (address) { urls.push(address); allUrls.push(address); }); } } }); var p = products.findObject({'name':product.$.name.toLowerCase()}); if(!p) { products.insert({ name: product.$.name.toLowerCase(), ipAddresses: ipAddresses, urls: urls, version: productsUpdated }); } else { p.ipAddresses = ipAddresses; p.urls = urls; p.version = productsUpdated; } }); var p = products.findObject({'name':'any'}); if(!p) { products.insert({ name: 'any', ipAddresses: allIpAdresses, urls: allUrls, version: productsUpdated }); } else { p.ipAddresses = Array.from(new Set(allIpAdresses)); p.urls = Array.from(new Set(allUrls)); p.version = productsUpdated; } console.log("Info: update finished; " + products.data.length + " product records in database."); }); } // refresh Microsoft Office 365 XML every hour repeat(getOffice365).every(1, 'hour').start.now(); // ILX::call to check if an IP address is part of Office365 ilx.addMethod('checkProductIP', function(objArgs, objResponse) { var productName = objArgs.params()[0]; var ipAddress = objArgs.params()[1]; // fail-open = true, fail-close = false var verdict = true; var req = products.findObject( { 'name':productName.toLowerCase()}); if(req) { verdict = ipRangeCheck(ipAddress, req.ipAddresses); } // return AuthnRequest to Tcl iRule objResponse.reply(verdict); }); // Start listening for ILX::call and ILX::notify events. ilx.listen();
Tested this on version:
13.0- APNimbostratus
Hi Niels,
Have you seen any unhandled errors during network outage events? I've seen a couple since implementing a solution based on your v0.3 iRule (obtained via Brett).
Error during network outage:
err sdmd[6365]: 018e0018:3: pid[12638] plugin[/Common/office365_ipi_PROD_plugin.office365_ipi_extension] events.js:160 err sdmd[6365]: 018e0018:3: pid[12638] plugin[/Common/office365_ipi_PROD_plugin.office365_ipi_extension] throw er; // Unhandled 'error' event err sdmd[6365]: 018e0018:3: pid[12638] plugin[/Common/office365_ipi_PROD_plugin.office365_ipi_extension] ^ err sdmd[6365]: 018e0018:3: pid[12638] plugin[/Common/office365_ipi_PROD_plugin.office365_ipi_extension] err sdmd[6365]: 018e0018:3: pid[12638] plugin[/Common/office365_ipi_PROD_plugin.office365_ipi_extension] Error: connect EHOSTUNREACH
Another variation of the last line. Presumably a DNS issue:
err sdmd[23042]: 018e0018:3: pid[812] plugin[/Common/office365_ipi_PROD_plugin.office365_ipi_extension] Error: getaddrinfo EAI_AGAIN endpoints.office.com:443
So the above issues ultimately end in the extension being terminated after it's maximum restart attempts. I'm looking at a few ways to improve this, however to begin with, your error handling actually looks fine, so I'm not sure why I get an unhandled error:
res.on('error', function(e) { callback(e, null); });
Complete function:
// helper to call the webservice function getJson(methodName, instanceName, clientRequestId, callback) { var ws = "https://endpoints.office.com"; var requestPath = ws + '/' + methodName + '/' + instanceName + '?clientRequestId=' + clientRequestId; var req = https.get(requestPath, function(res) { var data = ''; res.on('data', function(chunk) { data += chunk; }); res.on('error', function(e) { callback(e, null); }); res.on('timeout', function(e) { callback(e, null); }); res.on('end', function() { if(res.statusCode == 200) { callback(null, data); } }); }); }
Any thoughts?
Nice, so you are testing this code. Thanks for the feedback 🙂
It seems the getJson function needs some extra error handling. This works for me:
// helper to call the webservice function getJson(methodName, instanceName, clientRequestId, callback) { var ws = "https://endpoints.office.com"; var requestPath = ws + '/' + methodName + '/' + instanceName + '?clientRequestId=' + clientRequestId; var req = https.get(requestPath, function(res) { var data = ''; res.on('data', function(chunk) { data += chunk; }); res.on('error', function(e) { callback(e, null); }); res.on('timeout', function(e) { callback(e, null); }); res.on('end', function() { if(res.statusCode == 200) { callback(null, data); } }); }).on('error', function(e) { console.log("Got error: " + e.message); }); }
Now the error message will be logged, but not throw an error.
Nov 13 14:41:54 nielsvs-bigip info sdmd[7798]: 018e0017:6: pid[5428] plugin[/Common/office365_endpoints_plugin.office365_endpoints_extension] Got error: getaddrinfo EAI_AGAIN endpoints.office.com:443 Nov 13 14:41:54 nielsvs-bigip info sdmd[7798]: 018e0017:6: pid[5428] plugin[/Common/office365_endpoints_plugin.office365_endpoints_extension] Got error: getaddrinfo EAI_AGAIN endpoints.office.com:443 Nov 13 14:41:54 nielsvs-bigip info sdmd[7798]: 018e0017:6: pid[5429] plugin[/Common/office365_endpoints_plugin.office365_endpoints_extension] Got error: getaddrinfo EAI_AGAIN endpoints.office.com:443 Nov 13 14:41:54 nielsvs-bigip info sdmd[7798]: 018e0017:6: pid[5429] plugin[/Common/office365_endpoints_plugin.office365_endpoints_extension] Got error: getaddrinfo EAI_AGAIN endpoints.office.com:443
For anyone else that is interested in this code. See:
https://github.com/nvansluis/f5.office365_endpoints_extension
- APNimbostratus
Looking good for me also. Thanks Niels!
- APNimbostratus
In my TCL iRule, I also added a catch to the ILX::init command as when the extension terminates (like with the earlier unhandled exceptions) it results in a TCL error.
if { [catch { set rpc_handle [ILX::init office365_ipi_DEV_plugin office365_ipi_extension] } ] } { log local0.error "Host - $host, ILX::init failure. The ILX Extension may have been terminated. Try reloading the plugin or restarting the sdmd service if necessary." return }