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
Updated Jun 06, 2023
Version 2.0
  • AP's avatar
    AP
    Icon for Nimbostratus rankNimbostratus

    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
    
  • AP's avatar
    AP
    Icon for Nimbostratus rankNimbostratus

    Looking good for me also. Thanks Niels!

     

  • AP's avatar
    AP
    Icon for Nimbostratus rankNimbostratus

    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
        }