implant¶
Features¶
- Python >= 3.5 asyncio
- adhoc transferable remote procedures
- remote part of a implant.core.Command may reside in a separate module
- a implant.core.Command specific implant.core.Channel enables arbitrary protocols between local and remote side
- events
- quite small core
- tests
Limitations¶
- Python >= 3.5
- only pure Python modules are supported for remote import, if no venv is used
- implant.core.Command s must reside in a module other then __main__
- at the moment sudo must not ask for password
Example¶
General application¶
import asyncio
import pathlib
from implant import core, connect, commands
async def remote_tasks():
# create a connector for a python process
connector = connect.Lxd(
container='zesty',
hostname='localhost'
)
connector_args = {
'python_bin': pathlib.Path('/usr/bin/python3')
}
# connect to a remote python process
remote = await connector.launch(**connector_args)
# start remote communication tasks
com_remote = asyncio.ensure_future(remote.communicate())
try:
# execute command
cmd = commands.SystemLoad()
result = await remote.execute(cmd)
print("Remote system load:", result)
finally:
# stop communication tasks
com_remote.cancel()
await com_remote
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(remote_tasks())
loop.close()
An example Echo Command¶
import logging
import os
from implant import core
log = logging.getLogger(__name__)
class Echo(core.Command):
"""Demonstrate the basic command API."""
async def local(self, context):
"""The local side of the RPC.
:param context: :py:obj:`implant.core.DispatchLocalContext`
"""
# custom protocol
# first: send
await context.channel.send_iteration("send to remote")
# second: receive
from_remote = []
async for x in context.channel:
from_remote.append(x)
log.debug("************ receiving from remote: %s", from_remote)
# third: wait for remote to finish and return result
remote_result = await context.remote_future
result = {
'from_remote': ''.join(from_remote),
}
result.update(remote_result)
return result
async def remote(self, context):
"""The remote side of the RPC.
:param context: :py:obj:`implant.core.DispatchRemoteContext`
"""
# first: receive
from_local = []
async for x in context.channel:
from_local.append(x)
log.debug("************ receiving from local: %s", from_local)
# second: send
await context.channel.send_iteration("send to local")
# third: return result
return {
'from_local': ''.join(from_local),
'remote_self': self,
'pid': os.getpid()
}
Internals¶
master <-----------------------------------------> remote
|
stdin/stdout
|
chunks
|
channels
|
--> send ---> | | --> queue -->
| module:class/fqin |
<-- queue <-- | | <--- send <--
API¶
implant.core¶
The core module is transfered to the remote process and will bootstrap pipe communication.
It creates default channels and dispatches commands accordingly.
-
class
implant.core.
BaseHeaderItem
(*, default=None, encoder=None, decoder=None)[source]¶ Bases:
object
A base item of a header.
-
class
implant.core.
Channel
(name=None, *, send, loop=None)[source]¶ Bases:
asyncio.queues.Queue
Channel provides means to send and receive messages bound to a specific channel name.
-
__init__
(name=None, *, send, loop=None)[source]¶ Initialize the channel.
Parameters: - name – the channel name
- send – the partial send method of Channels
- loop – the event loop
-
send
= None¶ The send method bound to this channel’s name. See
Channels.send()
for details.
-
-
class
implant.core.
Channels
(reader, writer, *, loop=None)[source]¶ Bases:
object
Hold references to all channel queues and route messages accordingly.
-
__init__
(reader, writer, *, loop=None)[source]¶ Create a
Channels
instance which delegates incomming messages into their appropriateChannel
queues.Parameters: - reader –
asyncio.StreamReader
- writer –
asyncio.StreamWriter
- loop – the event loop
- reader –
-
coroutine
_finalize_message
(buffer, chunk)[source]¶ Finalize the message if
Header.eom
isTrue
.This will also acknowledge the message if
Header.send_ack
isTrue
.
-
acknowledgements
= None¶ Global acknowledgment futures distinctive by uid.
-
chunk_size
= 32768¶
-
coroutine
enqueue
()[source]¶ Schedule receive tasks.
Incomming chunks are collected and stored in the appropriate channel queue.
-
get_channel
(channel_name)[source]¶ Create a channel and weakly register its queue.
Parameters: channel_name – the name of the channel to create Returns: Channel
instance with a bound send method
-
incomming
= None¶ A collection of all active channels.
-
log
= <logging.Logger object>¶
-
coroutine
send
(channel_name, data, ack=False, compress=6)[source]¶ Send data in a encoded form to the channel.
Parameters: - channel_name – the name of the channel
- data – the python object to send
- ack – request acknowledgement of the reception of that message
- compress – compress the data with zlib
Messages are split into chunks and put into the outgoing queue.
-
-
class
implant.core.
Chunk
(header, channel_name, data)¶ Bases:
tuple
-
__getnewargs__
()¶ Return self as a plain tuple. Used by copy and pickle.
-
static
__new__
(_cls, header, channel_name, data)¶ Create new instance of Chunk(header, channel_name, data)
-
__repr__
()¶ Return a nicely formatted representation string
-
_asdict
()¶ Return a new OrderedDict which maps field names to their values.
-
classmethod
_make
(iterable, new=<built-in method __new__ of type object at 0xa385c0>, len=<built-in function len>)¶ Make a new Chunk object from a sequence or iterable
-
_replace
(**kwds)¶ Return a new Chunk object replacing specified fields with new values
-
channel_name
¶ Alias for field number 1
-
data
¶ Alias for field number 2
-
header
¶ Alias for field number 0
-
-
class
implant.core.
Command
(**parameters)[source]¶ Bases:
object
Common ancestor of all Commands.
-
command_name
= 'implant.core:Command'¶
-
dispatch_data
¶ Data to be dispatched.
-
parameters
= {}¶
-
-
class
implant.core.
CommandRemote
(full_classname)[source]¶ Bases:
object
Delegates remote task to another class.
This is usefull, if one wants not to import remote modules at the master side.
-
log
= <logging.Logger object>¶
-
-
class
implant.core.
ConnectionLostStreamReaderProtocol
(*args, connection_lost_cb, **kwargs)[source]¶ Bases:
asyncio.streams.StreamReaderProtocol
Call a callback on connection_lost.
-
class
implant.core.
Core
(loop, *, echo=None, **kwargs)[source]¶ Bases:
object
Core
starts theDispatcher
.-
coroutine
communicate
(reader, writer)[source]¶ Start the dispatcher and register the
ShutdownRemoteEvent
.- On shutdown:
- the import hook is removed
- the
Dispatcher.dispatch
task is stopped - the
Channels.enqueue
task is stopped
-
coroutine
connect_sysio
()[source]¶ Connect to
sys.stdin
andsys.stdout
.
-
log
= <logging.Logger object>¶
-
classmethod
main
(debug=False, log_config=None, *, loop=None, **kwargs)[source]¶ Start the event loop and schedule core communication.
-
static
setup_import_hook
(module_finder)[source]¶ Add module finder to
sys.meta_path
.
-
static
teardown_import_hook
(module_finder)[source]¶ Remove a module finder from
sys.meta_path
.
-
coroutine
-
class
implant.core.
DispatchCommand
(fqin, command_name, command_class, command_module, params)[source]¶ Bases:
implant.core.DispatchMessage
Arguments for a command dispatch.
-
log
= <logging.Logger object>¶
-
-
class
implant.core.
DispatchException
(fqin, exception, tb=None)[source]¶ Bases:
implant.core.DispatchMessage
Remote execution ended in an exception.
-
implant.core.
DispatchLocalContext
¶ alias of
implant.core.DispatchContext
-
class
implant.core.
DispatchMessage
(fqin)[source]¶ Bases:
object
Base class for command dispatch communication.
-
coroutine
__call__
(dispatcher)[source]¶ Executes appropriate
Dispatcher
methods to implement the core protocol.
-
log
= <logging.Logger object>¶
-
coroutine
-
class
implant.core.
DispatchReady
(fqin)[source]¶ Bases:
implant.core.DispatchMessage
Set the dispatch ready.
-
implant.core.
DispatchRemoteContext
¶ alias of
implant.core.DispatchContext
-
class
implant.core.
DispatchResult
(fqin, result=None)[source]¶ Bases:
implant.core.DispatchMessage
The result of a remote execution.
-
class
implant.core.
Dispatcher
(channels, *, loop=None)[source]¶ Bases:
object
Enables execution of
Command
s.A
Command
is split into local and remote part, where a context with a dedicatedChannel
is provided to enable streaming of arbitrary data. The local part also gets a remote future passed, which resolves to the result of the remote part of theCommand
.-
__init__
(channels, *, loop=None)[source]¶ Create a dispatcher, which executes messages on its own
Channel
to enable Command execution and communication via distinctChannel
s.
-
coroutine
_execute_channels
()[source]¶ Execute messages sent via our
Dispatcher.channel
.
-
channel
= None¶ A channel for the dispatcher itself.
-
channels
= None¶ The collection of all channels.
-
coroutine
execute
(command_name, **params)[source]¶ Execute a command.
First creating the remote side and its future and second executing its local part.
-
coroutine
execute_dispatch_command
(fqin, command_name, params)[source]¶ Create a command and execute it.
-
coroutine
execute_remote
(fqin, command)[source]¶ Execute the remote part of a Command. This method is called by a DispatchCommand message. The result is send via Dispatcher.channel to resolve the pending command future.
-
local_context
(fqin, remote_future)[source]¶ Create a local context to pass to a
Command
s local part.The
Channel
is built via a fully qualified instance name (fqin).
-
log
= <logging.Logger object>¶
-
pending_dispatches
= None¶ A collection of dispatches, which are still not finished.
-
remote_context
(fqin, pending_remote_task)[source]¶ Create a remote context to pass to a
Command
s remote part.The
Channel
is built via a fully qualified instance name (fqin).
-
remote_future
(fqin, command)[source]¶ Create a context for remote command future by sending DispatchCommand and returning its pending future.
-
-
class
implant.core.
FindSpecData
(**parameters)[source]¶ Bases:
implant.core.Command
Find spec data for a module to import from the remote side.
-
command_name
= 'implant.core:FindSpecData'¶
-
fullname
¶ Define a Command parameter.
-
parameters
= {'fullname': <implant.core.Parameter object at 0x7f1aa1a52080>}¶
-
-
class
implant.core.
Flag
(*, default=None, encoder=None, decoder=None)[source]¶ Bases:
implant.core.BaseHeaderItem
A boolean flag of a header.
-
class
implant.core.
Header
[source]¶ Bases:
bytes
The chunk header with flags and items.
-
channel_name_len
¶ An item of a header.
-
compression
¶ A boolean flag of a header.
-
data_len
¶ An item of a header.
-
eom
¶ A boolean flag of a header.
-
items
= {'channel_name_len': <implant.core.HeaderItem object at 0x7f1aa1a34e10>, 'compression': <implant.core.Flag object at 0x7f1aa1a34da0>, 'data_len': <implant.core.HeaderItem object at 0x7f1aa1a34e48>, 'eom': <implant.core.Flag object at 0x7f1aa1a34cf8>, 'recv_ack': <implant.core.Flag object at 0x7f1aa1a34d68>, 'send_ack': <implant.core.Flag object at 0x7f1aa1a34d30>, 'uid': <implant.core.HeaderItem object at 0x7f1aa1a34dd8>}¶
-
recv_ack
¶ A boolean flag of a header.
-
send_ack
¶ A boolean flag of a header.
-
size
= 23¶
-
uid
¶ An item of a header.
-
-
class
implant.core.
HeaderItem
(fmt, **kwargs)[source]¶ Bases:
implant.core.BaseHeaderItem
An item of a header.
-
size
¶ The size of the item.
-
-
class
implant.core.
Incomming
(*, connection_lost_cb=None, pipe=<_io.TextIOWrapper name='<stdin>' mode='r' encoding='UTF-8'>, loop=None)[source]¶ Bases:
asyncio.streams.StreamReader
A context for an incomming pipe.
-
coroutine
readexactly
(n)[source]¶ Read exactly n bytes from the stream.
This is a short and faster implementation then the original one (see of https://github.com/python/asyncio/issues/394).
-
coroutine
-
class
implant.core.
InvokeImport
(**parameters)[source]¶ Bases:
implant.core.Command
Invoke an import of a module on the remote side.
The local side will import the module first. The remote side will trigger the remote import hook, which in turn will receive all missing modules from the local side.
The import is executed in a separate executor thread, to have a separate event loop available.
-
command_name
= 'implant.core:InvokeImport'¶
-
fullname
¶ Define a Command parameter.
-
parameters
= {'fullname': <implant.core.Parameter object at 0x7f1aa1a52048>}¶
-
-
class
implant.core.
NoDefault
[source]¶ Bases:
object
Just a marker class to represent no default.
This is to separate really nothing and None.
-
class
implant.core.
NotifyEvent
(**parameters)[source]¶ Bases:
implant.core.Command
Notify about an event.
If the remote side registers for this event, it gets notified.
-
command_name
= 'implant.core:NotifyEvent'¶
-
dispatch_local
¶ Define a Command parameter.
-
event
¶ Define a Command parameter.
-
log
= <logging.Logger object>¶
-
parameters
= {'dispatch_local': <implant.core.Parameter object at 0x7f1aa19dcfd0>, 'event': <implant.core.Parameter object at 0x7f1aa19dcf98>}¶
-
-
class
implant.core.
OrderedMeta
[source]¶ Bases:
type
Preserve the order of instance creation.
-
items
= [<implant.core.Flag object>, <implant.core.Flag object>, <implant.core.Flag object>, <implant.core.Flag object>, <implant.core.HeaderItem object>, <implant.core.HeaderItem object>, <implant.core.HeaderItem object>]¶
-
-
class
implant.core.
Outgoing
(*, pipe=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>, reader=None, loop=None)[source]¶ Bases:
object
A context for an outgoing pipe.
-
class
implant.core.
Parameter
(*, default=<class 'implant.core.NoDefault'>, description=None)[source]¶ Bases:
object
Define a Command parameter.
-
exception
implant.core.
RemoteClassNotSetException
[source]¶ Bases:
Exception
Raised when remote class is not set for
CommandRemote
-
class
implant.core.
RemoteModuleFinder
(dispatcher, *, loop)[source]¶ Bases:
importlib.abc.MetaPathFinder
Import hook that execute a
FindSpecData
command in the main loop.See pep-0302, pep-0420 and pep-0451 for internals.
-
log
= <logging.Logger object>¶
-
-
class
implant.core.
RemoteModuleLoader
(source, filename=None, is_package=False)[source]¶ Bases:
importlib.abc.ExecutionLoader
Load the found module spec.
-
get_filename
(fullname)[source]¶ Abstract method which should return the value that __file__ is to be set to.
Raises ImportError if the module cannot be found.
-
get_source
(fullname)[source]¶ Abstract method which should return the source code for the module. The fullname is a str. Returns a str.
Raises ImportError if the module cannot be found.
-
-
class
implant.core.
ShutdownRemoteEvent
[source]¶ Bases:
object
A Shutdown event.
Shutting down a remote connection is done by gracefully canceling all remote tasks. See
Core.communicate
for details.
-
class
implant.core.
StopAsyncIterationEncoder
[source]¶ Bases:
object
Encoder for
StopAsyncIteration
.
-
class
implant.core.
Uid
(bytes=None)[source]¶ Bases:
uuid.UUID
A unique id, which is basically a
uuid.uuid1
instance.-
time
¶ The timestamp of the uuid1.
-
-
implant.core.
event_handler
(event_class, handler_=None, decorator=False)[source]¶ Define an event handler for a (new-style) class.
This can be called with a class and a handler, or with just a class and the result used as a handler decorator.
implant.bootstrap¶
Bootstrap of a remote python process.
implant.connect¶
Remote connection is established by a Connector.
-
class
implant.connect.
ConnectorMeta
[source]¶ Bases:
abc.ABCMeta
Connector meta base.
-
connectors
= {'local': <class 'implant.connect.Local'>, 'lxd': <class 'implant.connect.Lxd'>, 'ssh': <class 'implant.connect.Ssh'>}¶
-
scheme
¶ The unique connector scheme is the lowered class name.
-
-
class
implant.connect.
Local
(*, sudo=None)[source]¶ Bases:
implant.connect.SubprocessConnector
A Connector to a local python process.
-
arguments
(*, code=None, options=None, python_bin=None)[source]¶ Iterate over the arguments to start a process.
Parameters: - code – the code to bootstrap the remote process
- options – options for the remote process
- python_bin – the path to the python binary
-
sudo
¶
-
-
class
implant.connect.
Lxd
(*, container, hostname=None, user=None, sudo=None)[source]¶ Bases:
implant.connect.Ssh
A Connector for accessing a lxd container.
If the hostname is omitted, the lxd container is local.
-
arguments
(*, code=None, options=None, python_bin=None)[source]¶ Iterate over the arguments to start a process.
Parameters: - code – the code to bootstrap the remote process
- options – options for the remote process
- python_bin – the path to the python binary
-
container
¶
-
hostname
¶
-
sudo
¶
-
user
¶
-
-
class
implant.connect.
Remote
(*, stdin=None, stdout=None, stderr=None, loop=None)[source]¶ Bases:
object
A remote receiving commands.
-
exception
implant.connect.
RemoteMisbehavesError
[source]¶ Bases:
Exception
Exception is raised, when a remote process seems to be not what we expect.
-
class
implant.connect.
Ssh
(*, hostname=None, user=None, sudo=None)[source]¶ Bases:
implant.connect.Local
A Connector for remote hosts reachable via SSH.
If a hostname is omitted, this connector acts like Local.
-
arguments
(*, code=None, options=None, python_bin=None)[source]¶ Iterate over the arguments to start a process.
Parameters: - code – the code to bootstrap the remote process
- options – options for the remote process
- python_bin – the path to the python binary
-
hostname
¶
-
sudo
¶
-
user
¶
-
-
class
implant.connect.
SubprocessConnector
[source]¶ Bases:
implant.connect.Connector
A Connector uniquely defines a remote target.
-
arguments
(*, code=None, options=None, python_bin=None)[source]¶ Iterate over the arguments to start a process.
Parameters: - code – the code to bootstrap the remote process
- options – options for the remote process
- python_bin – the path to the python binary
-
static
bootstrap_code
(code=<module 'implant.core' from '/home/docs/checkouts/readthedocs.org/user_builds/implant/envs/latest/lib/python3.5/site-packages/implant-0.1.2-py3.5.egg/implant/core.py'>, options=None)[source]¶ Create the python bootstrap code.
-
coroutine
launch
(*, loop=None, code=None, options=None, python_bin=None, **kwargs)[source]¶ Launch a remote process.
Parameters: - code – the python module to bootstrap
- options – options to send to remote
- python_bin – the path to the python binary to execute
- kwargs – further arguments to create the process
-
-
class
implant.connect.
SubprocessRemote
(transport, protocol, *, loop=None)[source]¶ Bases:
implant.connect.Remote
A remote process.
-
returncode
¶ The exit code of the process.
-