Repository: Azure/WALinuxAgent
Pull request #914 (2017-10-18 23:56:19)

Re-enable firewall / address #822
Disable non-root access to wireserver
[#822] -- Update docs

Signed-off-by: Brendan Dixon <brendandixon@me.com>


Patch:
diff --git a/README.md b/README.md
index 5611643b..d9850db7 100644
--- a/README.md
+++ b/README.md
@@ -181,7 +181,6 @@ ResourceDisk.MountPoint=/mnt/resource
 ResourceDisk.MountOptions=None
 ResourceDisk.EnableSwap=n
 ResourceDisk.SwapSizeMB=0
-LBProbeResponder=y
 Logs.Verbose=n
 OS.AllowHTTP=n
 OS.RootDeviceScsiTimeout=300
@@ -330,13 +329,6 @@ _Default: 0_
 
 The size of the swap file in megabytes.   
 
-* __LBProbeResponder__   
-_Type: Boolean_   
-_Default: y_   
-
-If set, waagent will respond to load balancer probes from the platform (if
-present).
-
 * Logs.Verbose   
 _Type: Boolean_    
 _Default: n_   
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index dbaf70a7..98df0471 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -57,10 +57,17 @@ IPTABLES_VERSION = "iptables --version"
 IPTABLES_LOCKING_VERSION = FlexibleVersion('1.4.21')
 
 FIREWALL_ACCEPT = "iptables {0} -t security -{1} OUTPUT -d {2} -p tcp -m owner --uid-owner {3} -j ACCEPT"
-FIREWALL_DROP = "iptables {0} -t security -{1} OUTPUT -d {2} -p tcp -j DROP"
-FIREWALL_LIST = "iptables {0} -t security -L"
+# Note:
+# -- Initially "flight" the change to ACCEPT packets and develop a metric baseline
+#    A subsequent release will convert the ACCEPT to DROP
+FIREWALL_DROP = "iptables {0} -t security -{1} OUTPUT -d {2} -p tcp -m conntrack --ctstate INVALID,NEW -j ACCEPT"
+# FIREWALL_DROP = "iptables {0} -t security -{1} OUTPUT -d {2} -p tcp -m conntrack --ctstate INVALID,NEW -j DROP"
+FIREWALL_LIST = "iptables {0} -t security -L -nxv"
+FIREWALL_PACKETS = "iptables {0} -t security -L OUTPUT -nxv"
 FIREWALL_FLUSH = "iptables {0} -t security --flush"
 
+PACKET_PATTERN = "^\s*(\d+)\s+(\d+)\s+DROP\s+.*{0}[^\d]*$"
+
 _enable_firewall = True
 
 DMIDECODE_CMD = 'dmidecode --string system-uuid'
@@ -76,6 +83,33 @@ class DefaultOSUtil(object):
         self.selinux = None
         self.disable_route_warning = False
 
+    def get_firewall_dropped_packets(self, dst_ip=None):
+        # If a previous attempt failed, do not retry
+        global _enable_firewall
+        if not _enable_firewall:
+            return 0
+
+        try:
+            wait = self.get_firewall_will_wait()
+
+            rc, output = shellutil.run_get_output(FIREWALL_PACKETS.format(wait))
+            if rc != 0:
+                return -1
+
+            pattern = re.compile(PACKET_PATTERN.format(dst_ip))
+            for line in output.split('\n'):
+                m = pattern.match(line)
+                if m is not None:
+                    return int(m.group(1))
+            
+            return 0
+
+        except Exception as e:
+            _enable_firewall = False
+            logger.warn("Unable to retrieve firewall packets dropped"
+                        "{0}".format(ustr(e)))
+            return -1
+
     def get_firewall_will_wait(self):
         # Determine if iptables will serialize access
         rc, output = shellutil.run_get_output(IPTABLES_VERSION)
@@ -118,8 +152,7 @@ class DefaultOSUtil(object):
             return False
 
     def enable_firewall(self, dst_ip=None, uid=None):
-
-        # If a previous attempt threw an exception, do not retry
+        # If a previous attempt failed, do not retry
         global _enable_firewall
         if not _enable_firewall:
             return False
diff --git a/azurelinuxagent/common/utils/flexible_version.py b/azurelinuxagent/common/utils/flexible_version.py
index 2fce88dc..14c2a733 100644
--- a/azurelinuxagent/common/utils/flexible_version.py
+++ b/azurelinuxagent/common/utils/flexible_version.py
@@ -37,7 +37,7 @@ class FlexibleVersion(version.Version):
         self.prerelease = None
         self.version = ()
         if vstring:
-            self._parse(vstring)
+            self._parse(str(vstring))
         return
 
     _nn_version = 'version'
diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py
index c109d584..1c8290d1 100644
--- a/azurelinuxagent/common/version.py
+++ b/azurelinuxagent/common/version.py
@@ -113,7 +113,7 @@ def get_distro():
 
 AGENT_NAME = "WALinuxAgent"
 AGENT_LONG_NAME = "Azure Linux Agent"
-AGENT_VERSION = '2.2.18.1'
+AGENT_VERSION = '2.2.18.2'
 AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION)
 AGENT_DESCRIPTION = """
 The Azure Linux Agent supports the provisioning and running of Linux
diff --git a/azurelinuxagent/ga/env.py b/azurelinuxagent/ga/env.py
index 45b10bb0..0456cb06 100644
--- a/azurelinuxagent/ga/env.py
+++ b/azurelinuxagent/ga/env.py
@@ -76,20 +76,17 @@ class EnvHandler(object):
         while not self.stopped:
             self.osutil.remove_rules_files()
 
-            # Disable setting firewall for now, regardless of configuration switch
-            # if conf.enable_firewall():
-            #     success = self.osutil.enable_firewall(
-            #                     dst_ip=protocol.endpoint,
-            #                     uid=os.getuid())
-            #     add_periodic(
-            #         logger.EVERY_HOUR,
-            #         AGENT_NAME,
-            #         version=CURRENT_VERSION,
-            #         op=WALAEventOperation.Firewall,
-            #         is_success=success,
-            #         log_event=True)
-
-            self.osutil.remove_firewall()
+            if conf.enable_firewall():
+                success = self.osutil.enable_firewall(
+                                dst_ip=protocol.endpoint,
+                                uid=os.getuid())
+                add_periodic(
+                    logger.EVERY_HOUR,
+                    AGENT_NAME,
+                    version=CURRENT_VERSION,
+                    op=WALAEventOperation.Firewall,
+                    is_success=success,
+                    log_event=True)
 
             timeout = conf.get_root_device_scsi_timeout()
             if timeout is not None:
diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py
index f8302e07..bcd1e964 100644
--- a/azurelinuxagent/ga/exthandlers.py
+++ b/azurelinuxagent/ga/exthandlers.py
@@ -192,15 +192,22 @@ class ExtHandlersHandler(object):
             add_event(AGENT_NAME, version=CURRENT_VERSION, is_success=False, message=msg)
             return
 
-        msg = u"Handle extensions updates for incarnation {0}".format(etag)
-        logger.verbose(msg)
-        # Log status report success on new config
-        self.log_report = True
-        self.handle_ext_handlers(etag)
-        self.last_etag = etag
-
-        self.report_ext_handlers_status()
-        self.cleanup_outdated_handlers()
+        try:
+            msg = u"Handle extensions updates for incarnation {0}".format(etag)
+            logger.verbose(msg)
+            # Log status report success on new config
+            self.log_report = True
+            self.handle_ext_handlers(etag)
+            self.last_etag = etag
+
+            self.report_ext_handlers_status()
+            self.cleanup_outdated_handlers()
+        except Exception as e:
+            msg = u"Exception processing extension handlers: {0}".format(
+                ustr(e))
+            logger.warn(msg)
+            add_event(AGENT_NAME, version=CURRENT_VERSION, is_success=False, message=msg)
+            return
 
     def run_status(self):
         self.report_ext_handlers_status()
@@ -335,7 +342,7 @@ class ExtHandlersHandler(object):
             else:
                 message = u"Unknown ext handler state:{0}".format(state)
                 raise ExtensionError(message)
-        except ExtensionError as e:
+        except Exception as e:
             ext_handler_i.set_handler_status(message=ustr(e), code=-1)
             ext_handler_i.report_event(message=ustr(e), is_success=False)
     
@@ -462,7 +469,8 @@ class ExtHandlerInstance(object):
             raise ExtensionError("Failed to get ext handler pkgs", e)
 
         # Determine the desired and installed versions
-        requested_version = FlexibleVersion(self.ext_handler.properties.version)
+        requested_version = FlexibleVersion(
+                                    str(self.ext_handler.properties.version))
         installed_version_string = self.get_installed_version()
         installed_version = requested_version \
             if installed_version_string is None \
@@ -545,14 +553,16 @@ class ExtHandlerInstance(object):
                     "to uninstall".format(self.ext_handler.name)
                 self.logger.warn(msg)
             self.pkg = installed_pkg
-            self.ext_handler.properties.version = installed_version
+            self.ext_handler.properties.version = str(installed_version) \
+                                    if installed_version is not None else None
         elif selected_pkg is None \
             or (installed_pkg is not None and selected_version < installed_version):
             self.pkg = installed_pkg
-            self.ext_handler.properties.version = installed_version
+            self.ext_handler.properties.version = str(installed_version) \
+                                    if installed_version is not None else None
         else:
             self.pkg = selected_pkg
-            self.ext_handler.properties.version = selected_pkg.version
+            self.ext_handler.properties.version = str(selected_pkg.version)
 
         # Note if the selected package is greater than that installed
         if installed_pkg is None \
@@ -568,7 +578,7 @@ class ExtHandlerInstance(object):
     def version_gt(self, other):
         self_version = self.ext_handler.properties.version
         other_version = other.ext_handler.properties.version
-        return Version(self_version) > Version(other_version)
+        return FlexibleVersion(self_version) > FlexibleVersion(other_version)
 
     def get_installed_ext_handler(self):
         lastest_version = self.get_installed_version()
@@ -988,7 +998,7 @@ class ExtHandlerInstance(object):
 
         handler_status = ExtHandlerStatus()
         handler_status.name = self.ext_handler.name
-        handler_status.version = self.ext_handler.properties.version
+        handler_status.version = str(self.ext_handler.properties.version)
         handler_status.message = message
         handler_status.code = code
         handler_status.status = status
diff --git a/azurelinuxagent/ga/monitor.py b/azurelinuxagent/ga/monitor.py
index 7304ff1c..2cec5d15 100644
--- a/azurelinuxagent/ga/monitor.py
+++ b/azurelinuxagent/ga/monitor.py
@@ -191,13 +191,19 @@ class MonitorHandler(object):
             if datetime.datetime.utcnow() >= (last_heartbeat + period):
                 last_heartbeat = datetime.datetime.utcnow()
                 incarnation = protocol.get_incarnation()
-                msg = "{0};{1};{2}".format(incarnation, counter, heartbeat_id)
+                dropped_packages = self.osutil.get_firewall_dropped_packets(
+                                                    protocol.endpoint)
+
+                msg = "{0};{1};{2};{3}".format(
+                    incarnation, counter, heartbeat_id, dropped_packets)
+
                 add_event(
                     name=AGENT_NAME,
                     version=CURRENT_VERSION,
                     op=WALAEventOperation.HeartBeat,
                     is_success=True,
                     message=msg)
+
                 counter += 1
             try:
                 self.collect_and_send_events()


Test patch:
diff --git a/tests/common/osutil/test_default.py b/tests/common/osutil/test_default.py
index 23f024a3..a73f4b41 100644
--- a/tests/common/osutil/test_default.py
+++ b/tests/common/osutil/test_default.py
@@ -489,6 +489,52 @@ Match host 192.168.1.2\n\
         print("WRITING TO {0}".format(waagent_sudoers))
         self.assertEqual(1, count)
 
+    def test_get_firewall_dropped_packets_returns_zero_if_firewall_disabled(self):
+        osutil._enable_firewall = False
+        util = osutil.DefaultOSUtil()
+
+        self.assertEqual(0, util.get_firewall_dropped_packets("not used"))
+
+    @patch('azurelinuxagent.common.utils.shellutil.run_get_output')
+    def test_get_firewall_dropped_packets_returns_negative_if_error(self, mock_output):
+        osutil._enable_firewall = True
+        util = osutil.DefaultOSUtil()
+
+        mock_output.side_effect = [
+            (0, "iptables v{0}".format(osutil.IPTABLES_LOCKING_VERSION)),
+            (1, "not used")]
+        self.assertEqual(-1, util.get_firewall_dropped_packets("not used"))
+
+    @patch('azurelinuxagent.common.utils.shellutil.run_get_output')
+    def test_get_firewall_dropped_packets_returns_negative_if_exception(self, mock_output):
+        osutil._enable_firewall = True
+        util = osutil.DefaultOSUtil()
+
+        mock_output.side_effect = [
+            (0, "iptables v{0}".format(osutil.IPTABLES_LOCKING_VERSION)),
+            (1, Exception)]
+        self.assertEqual(-1, util.get_firewall_dropped_packets("not used"))
+
+    @patch('azurelinuxagent.common.utils.shellutil.run_get_output')
+    def test_get_firewall_dropped_packets(self, mock_output):
+        osutil._enable_firewall = True
+        util = osutil.DefaultOSUtil()
+
+        mock_output.side_effect = [
+            (0, "iptables v{0}".format(osutil.IPTABLES_LOCKING_VERSION)),
+            (0,
+'''
+
+Chain OUTPUT (policy ACCEPT 104 packets, 43628 bytes)
+    pkts      bytes target     prot opt in     out     source               destination
+       0        0 ACCEPT     tcp  --  any    any     anywhere             168.63.129.16        owner UID match daemon
+      32     1920 DROP       tcp  --  any    any     anywhere             168.63.129.16
+
+''')]
+        dst = '168.63.129.16'
+
+        self.assertEqual(32, util.get_firewall_dropped_packets(dst))
+
     @patch('os.getuid', return_value=42)
     @patch('azurelinuxagent.common.utils.shellutil.run_get_output')
     @patch('azurelinuxagent.common.utils.shellutil.run')
diff --git a/tests/utils/test_flexible_version.py b/tests/utils/test_flexible_version.py
index 11620226..89a7dbfc 100644
--- a/tests/utils/test_flexible_version.py
+++ b/tests/utils/test_flexible_version.py
@@ -405,6 +405,60 @@ class TestFlexibleVersion(unittest.TestCase):
             self.assertEqual(test, str(FlexibleVersion(test)))
         return
 
+    def test_creation_from_flexible_version(self):
+        tests = [
+            '1',
+            '1.2',
+            '1.2.3',
+            '1.2.3.4',
+            '1.2.3.4.5',
+
+            '1alpha',
+            '1.alpha',
+            '1-alpha',
+            '1alpha0',
+            '1.alpha0',
+            '1-alpha0',
+            '1.2alpha',
+            '1.2.alpha',
+            '1.2-alpha',
+            '1.2alpha0',
+            '1.2.alpha0',
+            '1.2-alpha0',
+
+            '1beta',
+            '1.beta',
+            '1-beta',
+            '1beta0',
+            '1.beta0',
+            '1-beta0',
+            '1.2beta',
+            '1.2.beta',
+            '1.2-beta',
+            '1.2beta0',
+            '1.2.beta0',
+            '1.2-beta0',
+
+            '1rc',
+            '1.rc',
+            '1-rc',
+            '1rc0',
+            '1.rc0',
+            '1-rc0',
+            '1.2rc',
+            '1.2.rc',
+            '1.2-rc',
+            '1.2rc0',
+            '1.2.rc0',
+            '1.2-rc0',
+
+            '1.2.3.4alpha5',
+        ]
+        for test in tests:
+            v = FlexibleVersion(test)
+            self.assertEqual(test, str(FlexibleVersion(v)))
+        return
+
     def test_repr(self):
         v = FlexibleVersion('1,2,3rc4', ',', ['lol', 'rc'])
         expected = "FlexibleVersion ('1,2,3rc4', ',', ('lol', 'rc'))"