mirror of https://github.com/Kkevsterrr/geneva
277 lines
11 KiB
ReStructuredText
277 lines
11 KiB
ReStructuredText
|
Adding New Plugins
|
||
|
==================
|
||
|
|
||
|
This section will describe the process to add a new application plugin to Geneva.
|
||
|
Application plugins serve as the fitness function for Geneva during evolution,
|
||
|
and allow it to evolve strategies to defeat certain types of censorship.
|
||
|
|
||
|
Plugins are run by the Evaluator; if you have not yet read how the Evaluator
|
||
|
works, see the :ref:`Strategy Evaluation` section.
|
||
|
|
||
|
There are three types of plugins: clients, servers, and overriding plugins.
|
||
|
A developer can choose to implement any one of, or all three of these plugins.
|
||
|
|
||
|
For this section, we will build an example plugin and walk through the existing
|
||
|
plugins to tour through the plugin API.
|
||
|
|
||
|
Plugins are expected to be in the :code:`plugins/` folder in geneva's repo. The
|
||
|
folder name is the plugin name, and Geneva will discover these automatically.
|
||
|
Plugins are specified to the evaluator or evolve with the :code:`--test-type`
|
||
|
flag. Within the plugin folder, plugins must adhere to the following naming
|
||
|
scheme:
|
||
|
|
||
|
- :code:`client.py` - for plugin clients
|
||
|
- :code:`server.py` - for plugin servers [optional]
|
||
|
- :code:`plugin.py` - for an overriding plugin to customize logic [optional]
|
||
|
|
||
|
:code:`server.py` and :code:`plugin.py` are optional. The server plugin is
|
||
|
required to do server-side evaluation, but the overriding plugin definition is
|
||
|
only required to if a developer wishes to override the evaluator's default
|
||
|
behavior.
|
||
|
|
||
|
If an overriding plugin is provided, the evaluator will simply invoke it at the
|
||
|
start of strategy evaluation and the overriding plugin will be responsible for
|
||
|
calling the client and server. This section will assume that no overriding
|
||
|
plugin is specified to describe the evaluator's default behavior with plugins,
|
||
|
and cover use cases for overriding plugins at the end.
|
||
|
|
||
|
Depending on the evaluation setup, some (or all) of these plugins will be used
|
||
|
during evaluation. For example, during an exclusively client-side evaluation,
|
||
|
only the client plugin is needed.
|
||
|
|
||
|
Client Plugins
|
||
|
^^^^^^^^^^^^^^
|
||
|
|
||
|
During exclusively client-side evolution, the evaluator will start the engine
|
||
|
with the strategy under evaluation, and then run the client plugin. During
|
||
|
server-side evolution, the evaluator will run the engine on the server-side,
|
||
|
start the server plugin, and then start the client plugin via an SSH session to
|
||
|
the remote client worker. (See :ref:`Adding a Worker` on how external workers
|
||
|
can be used).
|
||
|
|
||
|
The client plugin subclasses from the PluginClient object. To tour through the
|
||
|
API, we will walk through the development of a custom client plugin.
|
||
|
|
||
|
Writing Our Own
|
||
|
~~~~~~~~~~~~~~~
|
||
|
|
||
|
Let us write a fitness function to test Iran's whitelisting system.
|
||
|
|
||
|
Iran's protocol whitelister was a recently deployed new censorship mechanism to
|
||
|
censor non-whitelisted protocols on certain ports (53, 80, 443). We deployed
|
||
|
Geneva against the whitelister, and discovered multiple ways to evade it in just
|
||
|
one evolution of the genetic algorithm. (The results of that investigation is
|
||
|
located `here <https://geneva.cs.umd.edu/posts/iran-whitelister>`_).
|
||
|
|
||
|
The whitelister worked by checking the first 2 packets of a flow, and if they
|
||
|
did not match a fingerprint, it would destroy the flow.
|
||
|
|
||
|
In order to run Geneva against whitelister, we will define a client plugin that
|
||
|
will try to trigger the whitelister and record whether the whitelister
|
||
|
successfully censored its connection or not.
|
||
|
|
||
|
First, let's make a new folder in the :code:`plugins/` directory called
|
||
|
"whitelister". We'll create a "client.py" and create the plugin object as
|
||
|
a subclass of ClientPlugin.
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
class WhitelisterClient(ClientPlugin):
|
||
|
"""
|
||
|
Defines the whitelister client.
|
||
|
"""
|
||
|
name = "whitelister"
|
||
|
|
||
|
def __init__(self, args):
|
||
|
"""
|
||
|
Initializes the whitelister client.
|
||
|
"""
|
||
|
ClientPlugin.__init__(self)
|
||
|
self.args = args
|
||
|
|
||
|
|
||
|
Done! Next, let's define argument parsing for this plugin. Geneva uses a
|
||
|
pass-through system of argument parsing: when command-line arguments are
|
||
|
specified, evolve.py parses the options it knows and passes the rest to the
|
||
|
evaluator. The evaluator prases the options it knows, and passes the list to the
|
||
|
plugins. This allows developers to easily add their own arguments just to their
|
||
|
plugin and use them from the command-line without changing any of the
|
||
|
intermediate code.
|
||
|
|
||
|
In this case, we need our client to make a TCP connection to a server located
|
||
|
outside of Iran to send our whitelister triggering messages to. Let's add an
|
||
|
argument so the user can specify which server to connect to.
|
||
|
|
||
|
We can do this by adding a :code:`get_args` static method. The evaluator will
|
||
|
call this method when the plugin is created and give it the full command line
|
||
|
list, so the plugin is free to parse it how it chooses. For this example, we
|
||
|
will use the standard :code:`argparse` library.
|
||
|
|
||
|
Since the superclass also defines args, we'll pass the command line list up to
|
||
|
the super class as well to collect those arguments.
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
@staticmethod
|
||
|
def get_args(command):
|
||
|
"""
|
||
|
Defines args for this plugin
|
||
|
"""
|
||
|
super_args = ClientPlugin.get_args(command)
|
||
|
parser = argparse.ArgumentParser(description='Whitelister Client')
|
||
|
|
||
|
parser.add_argument('--server', action='store', help="server to connect to")
|
||
|
|
||
|
args, _ = parser.parse_known_args(command)
|
||
|
args = vars(args)
|
||
|
|
||
|
super_args.update(args)
|
||
|
return super_args
|
||
|
|
||
|
Now, we just need to define a :code:`run()` method. The :code:`run()` method is
|
||
|
called by the evaluator to run the plugin. It provides the parsed arguments, a
|
||
|
logger to log with, and a reference to an instance of the strategy engine that
|
||
|
is running the strategy (see :ref:`Engine` for more information on how the
|
||
|
engine works.)
|
||
|
|
||
|
Let's start by defining the run method. We'll pull out the argument for the
|
||
|
server we defined earlier, connect to it with a python socket, and then just
|
||
|
send "G", "E", and "T" in separate messages to trigger the whitelister. Since
|
||
|
the whitelister censors connections by blackholing them, if the strategy failed
|
||
|
to defeat the whitelister, we would expect our network connection to timeout;
|
||
|
if we can send our messages and get a response from the server, the strategy
|
||
|
under evaluation may have defeated the whitelister.
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
def run(self, args, logger, engine=None):
|
||
|
"""
|
||
|
Try to open a socket, send two messages, and see if the messages
|
||
|
time out.
|
||
|
"""
|
||
|
fitness = 0
|
||
|
port = int(args["port"])
|
||
|
server = args["server"]
|
||
|
try:
|
||
|
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
|
client.settimeout(3)
|
||
|
client.connect((server, port))
|
||
|
client.sendall(b"G")
|
||
|
time.sleep(0.25)
|
||
|
client.sendall(b"E")
|
||
|
time.sleep(0.25)
|
||
|
client.sendall(b"T\r\n\r\n")
|
||
|
server_data = client.recv(1024)
|
||
|
logger.debug("Data recieved: %s", server_data.decode('utf-8', 'ignore'))
|
||
|
if server_data:
|
||
|
fitness += 100
|
||
|
else:
|
||
|
fitness -= 90
|
||
|
client.close()
|
||
|
# ...
|
||
|
|
||
|
Now we just need to define the error handling for this code. This is critical to
|
||
|
the fitness function: we want to kill off strategies that damage the underlying
|
||
|
TCP connection, so Geneva does not waste time searching this space of
|
||
|
strategies.
|
||
|
|
||
|
Our goal is to set the fitness metric such that a *censorship event has a higher
|
||
|
fitness than the strategy damaging the connection*. Since we can distinguish
|
||
|
these cases based on the socket error, we will set a lower fitness if any other
|
||
|
exception is raised besides the timeout.
|
||
|
|
||
|
Lastly, we'll inflate the numerical fitness metric to make it a larger number.
|
||
|
The evaluator does additional punishments to the fitness score based on the
|
||
|
strategy (see :ref:`Strategy Evaluation`), so we want the number to be
|
||
|
sufficiently large to not push succeeding strategies to negative numbers.
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
except socket.timeout:
|
||
|
logger.debug("Client: Timeout")
|
||
|
fitness -= 90
|
||
|
except socket.error as exc:
|
||
|
fitness -= 100
|
||
|
logger.exception("Socket error caught in client echo test.")
|
||
|
finally:
|
||
|
logger.debug("Client finished whitelister test.")
|
||
|
return fitness * 4
|
||
|
|
||
|
|
||
|
Putting it all together:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
class WhitelisterClient(ClientPlugin):
|
||
|
"""
|
||
|
Defines the whitelister client.
|
||
|
"""
|
||
|
name = "whitelister"
|
||
|
|
||
|
def __init__(self, args):
|
||
|
"""
|
||
|
Initializes the whitelister client.
|
||
|
"""
|
||
|
ClientPlugin.__init__(self)
|
||
|
self.args = args
|
||
|
|
||
|
@staticmethod
|
||
|
def get_args(command):
|
||
|
"""
|
||
|
Defines args for this plugin
|
||
|
"""
|
||
|
super_args = ClientPlugin.get_args(command)
|
||
|
parser = argparse.ArgumentParser(description='Whitelister Client')
|
||
|
|
||
|
parser.add_argument('--server', action='store', help="server to connect to")
|
||
|
|
||
|
args, _ = parser.parse_known_args(command)
|
||
|
args = vars(args)
|
||
|
|
||
|
super_args.update(args)
|
||
|
return super_args
|
||
|
|
||
|
def run(self, args, logger, engine=None):
|
||
|
"""
|
||
|
Try to open a socket, send two messages, and see if the messages
|
||
|
time out.
|
||
|
"""
|
||
|
fitness = 0
|
||
|
port = int(args["port"])
|
||
|
server = args["server"]
|
||
|
try:
|
||
|
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
|
client.settimeout(3)
|
||
|
client.connect((server, port))
|
||
|
client.sendall(b"G")
|
||
|
time.sleep(0.25)
|
||
|
client.sendall(b"E")
|
||
|
time.sleep(0.25)
|
||
|
client.sendall(b"T\r\n\r\n")
|
||
|
server_data = client.recv(1024)
|
||
|
logger.debug("Data recieved: %s", server_data.decode('utf-8', 'ignore'))
|
||
|
if server_data:
|
||
|
fitness += 100
|
||
|
else:
|
||
|
fitness -= 90
|
||
|
client.close()
|
||
|
except socket.timeout:
|
||
|
logger.debug("Client: Timeout")
|
||
|
fitness -= 90
|
||
|
except socket.error as exc:
|
||
|
fitness -= 100
|
||
|
logger.exception("Socket error caught in client echo test.")
|
||
|
finally:
|
||
|
logger.debug("Client finished whitelister test.")
|
||
|
return fitness * 4
|
||
|
|
||
|
Server Plugins
|
||
|
^^^^^^^^^^^^^^
|
||
|
|
||
|
Coming soon!
|
||
|
|
||
|
Override Plugins
|
||
|
^^^^^^^^^^^^^^^^
|
||
|
|
||
|
Coming soon!
|