Repository: eclipse-bluechi/bluechi (language: python)

Issue:
Implement proxy services
A proxy service is a service that can run on a node and mirror a service on another node for the purposes of remote dependencies.

The way it works is this:
 * hirte ships with a proxy service template file `hirte-remote-service@.service`
 * Another service file depends on `hirte-remote-service@nodename_servicename.service`
 * `remote-service@.service` when started calls via dbus to the local agent and tells it to create a proxy object for the service.
 * The manager sees the newly created proxy object on the agent, ensures that the service is running on the target node and then tells the original agent that its running now (or that starting it failed)
 * The proxy service gets notified of this success or failure
 * In the manager, the proxy gets handled similar to a monitor on the target service, and when it changes state (i.e. when it stops) the original agent is told to stop the proxy
 * When the agent gets told the service is stopped, it stops the proxy service

The template service would look something like this:
```
[Unit]
Description=Hirte proxy service

[Service]
ExecStart=/usr/libexec/hirte-proxy-service "%i"
Type=oneshot
RemainAfterExit=yes
KillMode=mixed
```

This is a one-shot, remain-after-exit service. On startup it calls hirte-proxy-service with the `nodename__servicename` par of the name as argument. This blocks until the target service is know to be running (EXIT_SUCCESS) or it failed to start (EXIT_FAILURE). If it failed the proxy service will be considered failed and dependencies fail. If it returns success the proxy service is now running. If we do nothing, it will be considered running forever, however eventually the agent will get told the service died, and the agent will then tell systemd to stop the proxy service.

Here is some proof of concept code for how this could work:
https://gist.github.com/alexlarsson/bed968e0043f5ba3b22637be08bf19ac


Discussion / hints:

Fix patch:
diff --git a/data/meson.build b/data/meson.build
index f7aec9dba7..c3d6949fa6 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -2,11 +2,14 @@
 
 # Installing the public DBus API
 install_data(
-    'org.containers.hirte.Job.xml',
-    'org.containers.hirte.Manager.xml',
-    'org.containers.hirte.Monitor.xml',
-    'org.containers.hirte.Node.xml',
-    install_dir: join_paths(get_option('datadir'), 'dbus-1', 'interfaces'),
+    [
+        'org.containers.hirte.Job.xml',
+        'org.containers.hirte.Manager.xml',
+        'org.containers.hirte.Monitor.xml',
+        'org.containers.hirte.Node.xml',
+        'org.containers.hirte.Agent.xml'
+    ],
+    install_dir: join_paths(get_option('datadir'), 'dbus-1', 'interfaces'), 
 )
 
 # Installing the DBus permission configuration files
diff --git a/data/org.containers.hirte.Agent.xml b/data/org.containers.hirte.Agent.xml
new file mode 100644
index 0000000000..64d692762e
--- /dev/null
+++ b/data/org.containers.hirte.Agent.xml
@@ -0,0 +1,16 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<!-- SPDX-License-Identifier: GPL-2.0-or-later -->
+<node>
+  <interface name="org.containers.hirte.Agent">
+    <method name="CreateProxy">
+      <arg name="local_service_name" type="s" direction="in" />
+      <arg name="node" type="s" direction="in" />
+      <arg name="unit" type="s" direction="in" />
+    </method>
+    <method name="RemoveProxy">
+      <arg name="local_service_name" type="s" direction="in" />
+      <arg name="node" type="s" direction="in" />
+      <arg name="unit" type="s" direction="in" />
+    </method>
+  </interface>
+</node>
diff --git a/data/org.containers.hirte.internal.Agent.xml b/data/org.containers.hirte.internal.Agent.xml
index de994915b4..fe90618cde 100644
--- a/data/org.containers.hirte.internal.Agent.xml
+++ b/data/org.containers.hirte.internal.Agent.xml
@@ -52,6 +52,12 @@
     <method name="Unsubscribe">
       <arg name="unit" type="s" direction="in" />
     </method>
+    <method name="StartDep">
+      <arg name="unit" type="s" direction="in" />
+    </method>
+    <method name="StopDep">
+      <arg name="unit" type="s" direction="in" />
+    </method>
 
     <signal name="JobDone">
       <arg name="id" type="u" />
@@ -87,7 +93,6 @@
     <signal name="ProxyRemoved">
       <arg name="nodeName" type="s" />
       <arg name="unitName" type="s" />
-      <arg name="proxy" type="o" />
     </signal>
     <signal name="Heartbeat">
       <arg name="agent_name" type="s" />
diff --git a/data/org.containers.hirte.internal.Proxy.xml b/data/org.containers.hirte.internal.Proxy.xml
index 118ef7817b..2f0cd4eed9 100644
--- a/data/org.containers.hirte.internal.Proxy.xml
+++ b/data/org.containers.hirte.internal.Proxy.xml
@@ -2,8 +2,19 @@
 <!-- SPDX-License-Identifier: GPL-2.0-or-later -->
 <node>
   <interface name="org.containers.hirte.internal.Proxy">
-    <method name="Ready">
-      <arg name="result" type="s" direction="in" />
+    <method name="Error">
+      <arg name="message" type="s" direction="in" />
+    </method>
+    <method name="TargetNew">
+      <arg name="reason" type="s" direction="in" />
+    </method>
+    <method name="TargetStateChanged">
+      <arg name="state" type="s" direction="in" />
+      <arg name="substate" type="s" direction="in" />
+      <arg name="reason" type="s" direction="in" />
+    </method>
+    <method name="TargetRemoved">
+      <arg name="reason" type="s" direction="in" />
     </method>
   </interface>
-</node>
\ No newline at end of file
+</node>
diff --git a/doc/dbus-interfaces.md b/doc/dbus-interfaces.md
index 5527b15f94..b9b4666203 100644
--- a/doc/dbus-interfaces.md
+++ b/doc/dbus-interfaces.md
@@ -217,6 +217,26 @@ Object path: `/org/containers/hirte/job/$id`
 
     The current state of the job, one of: `waiting` or `running`. Waiting is for queued jobs.
 
+## Hirte-Agent public D-Bus API
+
+The main entry point is at the `/org/containers/hirte` object path and implements the `org.containers.hirte.Agent`
+interface.
+
+### interface org.containers.hirte.Agent
+
+* Methods:
+
+  * `CreateProxy(in s service_name, in s node_name, in s unit_name)`
+
+    Whenever a service on the agent requires a service on another node it creates a proxy service and calls this method.
+    It then creates a new `org.containers.internal.Proxy` object and emits the `ProxyNew` signal on the internal bus to
+    tell the manager about it. The manager will then try to arrange that the requested unit on the specified node is
+    running and notifies the initial agent about the status by calling `Ready` on the internal bus.
+
+  * `RemoveProxy(in s service_name, in s node_name, in s unit_name)`
+
+    When a proxy is not needed anymore it is being removed on the node and a `ProxyRemoved` is emitted to notify the manager.
+
 ## Internal D-Bus APIs
 
 The above APIs are the public facing ones that users of Hirte would use. Additionally there are additional APIs that are
@@ -295,9 +315,10 @@ This is the main interface that the node implements and that is used by the mana
     will notice this and try to arrange that the requested unit is running on the requested node. If the unit is already
     running, when it is started, or when the start fails, the manager will call the `Ready()` method on it.
 
-  * `ProxyRemoved(s nodeName, s unitName, o proxy)`
+  * `ProxyRemoved(s nodeName, s unitName)`
 
-    This is emitted when a proxy is not needed anymore because the proxy service died.
+    This is emitted when a proxy is not needed anymore because the service requiring the proxy
+    service is stopped.
 
   * `Heartbeat(s nodeName)`
 
diff --git a/doc/man/hirte-proxy.1.md b/doc/man/hirte-proxy.1.md
new file mode 100644
index 0000000000..e90002135d
--- /dev/null
+++ b/doc/man/hirte-proxy.1.md
@@ -0,0 +1,53 @@
+% hirte-proxy 1
+
+## NAME
+
+hirte-proxy - Proxy requesting services on other agents
+
+## SYNOPSIS
+
+**hirte-proxy** [*options*] *command*
+
+## DESCRIPTION
+
+Hirte is a service orchestrator tool intended for multi-node devices
+(e.g.: edge devices) clusters with a predefined number of nodes and
+with a focus on highly regulated environment such as those requiring
+functional safety (for example in cars).
+
+A `hirte-proxy` uses the public API of `hirte-agent` provided on the
+local D-Bus to request services required by a local unit to be started
+or stopped on a remote node managed by another `hirte-agent`.
+
+**hirte-proxy [OPTIONS]**
+
+## OPTIONS
+
+#### **--help**, **-h**
+
+Print usage statement and exit.
+
+
+## Commands
+
+### **hirte-proxy** create [*node_service*]
+
+Starts the `hirte-proxy` to call the `CreateProxy` method on the local
+`hirte-agent` to request a remote service to be started on the given
+node. `hirte-proxy` is exiting when starting the service has either
+succeeded or failed.
+
+### **hirte-proxy** remove [*node_service*]
+
+Starts the `hirte-proxy` to call the `RemoveProxy` method on the local
+`hirte-agent` to request a remote service to be started on the given
+node. `hirte-proxy` is exiting when starting the service has either
+succeeded or failed.
+
+## Exit Codes
+
+On successfully starting the remote service, 0 is returned. Otherwise, 1 is returned.
+
+## SEE ALSO
+
+**[systemd.unit(5)](https://www.freedesktop.org/software/systemd/man/systemd.unit.html)**
diff --git a/doc/man/meson.build b/doc/man/meson.build
index ee75685908..092f1ec792 100644
--- a/doc/man/meson.build
+++ b/doc/man/meson.build
@@ -11,6 +11,7 @@ man1 = [
   'hirte',
   'hirte-agent',
   'hirtectl',
+  'hirte-proxy',
 ]
 
 man5 = [
diff --git a/doc/proxies.md b/doc/proxies.md
new file mode 100644
index 0000000000..7ffcc1638f
--- /dev/null
+++ b/doc/proxies.md
@@ -0,0 +1,136 @@
+# Proxy Services
+
+## Using Proxy Services
+
+In order to support cross-node systemd unit dependencies, hirte
+introduces something called proxy services. The hirta agent
+comes with a template service called `hirte-proxy@.service`
+which is the core of this mechanism.
+
+Suppose there is a regular service file on node 'foo', which
+looks looks like this
+
+**`need-db.service`**
+
+``` INI
+[Unit]
+Wants=db.service
+After=db.service
+
+[Service]
+ExecStart=/bin/db-user
+```
+
+When this service is started, `db.service` is also started. But,
+suppose you want to start `db.service` on another node `bar`.
+With hirte you can do this like this:
+
+**`need-db.service`**
+
+``` INI
+[Unit]
+Wants=hirte-proxy@bar_db.service
+After=hirte-proxy@bar_db.service
+
+[Service]
+ExecStart=/bin/db-user
+```
+
+When the `hirte-proxy@bar_db.service` is started it will talk to hirte
+and initiate the process of starting `db.service` (if needed) on node
+`bar`, and when it is becomes active (or it is detected it was already
+active), the proxy service also becomes active.
+
+If there is any problem activating the service, then the proxy service
+will fail to activate. With the `Wants=` options used in the example
+above this doesn't do anything, but you can use stronger dependencies
+like `Requires=` which will make the activation of `need-db.service`
+not start if the dependency fails to start.
+
+Note: For startup performance and robustness it is generally better to
+use weaker dependencies and handle failuress in other ways, like
+service restarts.
+
+After a successful start, the proxy service will continue to be active
+as long as some other service depends on it, until the target service
+on the other node stops. When hirte detects that the target service
+becomes inactive, the proxy service will be stopped. This can be used
+with even stronger dependency options like `BindTo=` to cause the
+need-db service to stop when the db service stops.
+
+In addition, when the last dependency of the proxy server on a node
+exits, the proxy service will stop too (as it has
+`StopWhenUnneeded=yes`), and hirte will propagate this information to
+the target node. By default, in systemd this doesn't do anything, even
+if there are no other dependencies on the target service. However you
+can use `StopWhenUnneeded=yes` in the service to make it stop when
+the last dependency (local or via proxy) stops.
+
+## Internal Details on the source node
+
+The proxy service is a template service called `hirte-proxy@.service`
+of type `oneshoot` with `RemainAfterExit=yes`. This means that when it
+is started it will change into `activating` stage, and then start the
+`ExecStart` command. If this fails, it will go to `failed` state, but
+when it eventually succeeds it will go into `active` state (even though
+no process is running).
+
+The `ExecStart` command starts the hirte-proxy helper app that talks
+to the local agent, which in turn talks to the main hirte service,
+starting the target service. Once it is running hirte notifies the
+agent which in turns replies to the hirte-proxy which then exits with
+the correct (failed or activated) exit status.
+
+The proxy can be stopped on the local system (explicitly, or when the
+last dependency to is stops), which will trigger the `ExecPost` command,
+which tells the agent to unregister the proxy with hirte, which in turn
+stops the dependency on the target service on the target node.
+
+Alternatively, if hirte notices that the target service stopped, after
+we returned successfully in the `ExecStart` command, then the agent
+explicitly stops the proxy service (via systemd).
+
+## Internal Details on the target node
+
+The hirte agent also contains another template service called
+`hirte-dep@.service` which is used on the target node. This service is
+templated on the target service name, such that whenever
+`hirte-dep@XXX.service` it depends on `XXX.service` causing it to
+start. Whenever there is a proxy service running on some other node,
+hirte starts a dep service like this to mirror it, which makes systemd
+consider the target needed.
+
+The dependency used for the dep service is `BindsTo` and `After`,
+which is a very strong dependency. This means the state of the dep
+service mirrors the target service, i.e. stops when it stops. This is
+done to handle the case where the target service stops for some
+reason, and then there is a *new* proxy started. When this happens we
+will again start the dep service, but if it wasn't stopped with the
+target service it would already be running, and thus not trigger
+a re-start of the target service.
+
+## Implementation Details
+
+Tracking service state across multiple nodes is very tricky, as the
+state can diverge due to disconnects, delays, or races. To minimize
+such problems most state and intelligence is kept in the
+agent. Whenever the agent registers a new proxy it will announce this
+to the manager (if connected), and this will start a one-directional
+flow of non-interpreted state-change events from the target service to
+the manager to the agent, until the agent explicitly removes the
+proxy.
+
+If an agent is disconnected from the manager, then the manager treats
+that as if the agent removed the proxy. On re-connection the agent
+will re-send the registering of the proxy.
+
+In addition to the monitoring, each time a proxy is registered the
+manager will tell the target node to start the dep service for the
+target service. Hirte keeps track of how many proxies are outstanding
+for the target service and only tell the agent to stop the dep service
+when this reaches zero. Similar to the above, when the target node
+reconnect we re-sent starts for any outstanding proxies.
+
+Note that due to disconnects we may sometimes send multiple start
+events to the target service, and we may report virtual "stop" events
+of the target when its really still active, only disconnected.
diff --git a/hirte.spec.in b/hirte.spec.in
index 097d57d97a..ab9ba579bd 100644
--- a/hirte.spec.in
+++ b/hirte.spec.in
@@ -71,12 +71,19 @@ This package contains the node agent.
 %doc README.md
 %license LICENSE
 %{_bindir}/hirte-agent
+%{_bindir}/hirte-proxy
 %{_datadir}/dbus-1/system.d/org.containers.hirte.Agent.conf
 %{_datadir}/hirte-agent/config/hirte-default.conf
+%{_datadir}/dbus-1/interfaces/org.containers.hirte.Agent.xml
 %{_mandir}/man1/hirte-agent.*
+%{_mandir}/man1/hirte-proxy.*
 %{_mandir}/man5/hirte-agent.conf.*
 %{_unitdir}/hirte-agent.service
 %{_userunitdir}/hirte-agent.service
+%{_unitdir}/hirte-proxy@.service
+%{_userunitdir}/hirte-proxy@.service
+%{_unitdir}/hirte-dep@.service
+%{_userunitdir}/hirte-dep@.service
 
 
 %package ctl
diff --git a/meson.build b/meson.build
index 85fe900bfb..43657a7b02 100644
--- a/meson.build
+++ b/meson.build
@@ -55,6 +55,7 @@ subdir('config')
 subdir('src/manager')
 subdir('src/agent')
 subdir('src/client')
+subdir('src/proxy')
 
 # Subdirectory for the API description
 subdir('data')
diff --git a/src/agent/agent.c b/src/agent/agent.c
index b67593ca48..164bba5785 100644
--- a/src/agent/agent.c
+++ b/src/agent/agent.c
@@ -15,6 +15,7 @@
 #include "libhirte/service/shutdown.h"
 
 #include "agent.h"
+#include "proxy.h"
 
 #ifdef USE_USER_API_BUS
 #        define ALWAYS_USER_API_BUS 1
@@ -44,13 +45,19 @@ static bool
                                 void *userdata,
                                 free_func_t free_userdata);
 
+bool agent_is_connected(Agent *agent) {
+        return agent != NULL && agent->connection_state == AGENT_CONNECTION_STATE_CONNECTED;
+}
+
+static int agent_stop_local_proxy_service(Agent *agent, ProxyService *proxy);
 static bool agent_connect(Agent *agent);
 
 static int agent_disconnected(UNUSED sd_bus_message *message, void *userdata, UNUSED sd_bus_error *error) {
         Agent *agent = (Agent *) userdata;
 
+        hirte_log_error("Disconnected from manager");
+
         if (!agent_connect(agent)) {
-                hirte_log_errorf("Agent '%s' disconnected", agent->name);
                 agent->connection_state = AGENT_CONNECTION_STATE_RETRY;
         }
 
@@ -75,8 +82,9 @@ static int agent_heartbeat_timer_callback(sd_event_source *event_source, UNUSED
                 }
         } else if (agent->connection_state == AGENT_CONNECTION_STATE_RETRY) {
                 agent->connection_retry_count++;
+                hirte_log_infof("Trying to connect to manager (try %d)", agent->connection_retry_count);
                 if (!agent_connect(agent)) {
-                        hirte_log_errorf("Agent '%s' disconnected", agent->name);
+                        hirte_log_debugf("Connection retry %d failed", agent->connection_retry_count);
                 }
         }
 
@@ -315,6 +323,7 @@ Agent *agent_new(void) {
         agent->port = HIRTE_DEFAULT_PORT;
         LIST_HEAD_INIT(agent->outstanding_requests);
         LIST_HEAD_INIT(agent->tracked_jobs);
+        LIST_HEAD_INIT(agent->proxy_services);
 
         agent->unit_infos = hashmap_new(
                         sizeof(AgentUnitInfo), 0, 0, 0, unit_info_hash, unit_info_compare, unit_info_clear, NULL);
@@ -329,16 +338,66 @@ Agent *agent_new(void) {
 }
 
 Agent *agent_ref(Agent *agent) {
+        assert(agent->ref_count > 0);
+
         agent->ref_count++;
         return agent;
 }
 
+/* Emit == false on agent shutdown or proxy creation error */
+void agent_remove_proxy(Agent *agent, ProxyService *proxy, bool emit_removed) {
+        assert(proxy->agent == agent);
+
+        hirte_log_debugf("Removing proxy %s %s", proxy->node_name, proxy->unit_name);
+
+        if (emit_removed) {
+                int r = proxy_service_emit_proxy_removed(proxy);
+                if (r < 0) {
+                        hirte_log_errorf(
+                                        "Failed to emit ProxyRemoved signal for node %s and unit %s",
+                                        proxy->node_name,
+                                        proxy->unit_name);
+                }
+        }
+
+        if (proxy->request_message != NULL) {
+                int r = sd_bus_reply_method_errorf(
+                                proxy->request_message, SD_BUS_ERROR_FAILED, "Proxy service stopped");
+                if (r < 0) {
+                        hirte_log_errorf("Failed to sent ready status message to proxy: %s", strerror(-r));
+                }
+                sd_bus_message_unrefp(&proxy->request_message);
+                proxy->request_message = NULL;
+        }
+
+
+        if (proxy->sent_successful_ready) {
+                int r = agent_stop_local_proxy_service(agent, proxy);
+                if (r < 0) {
+                        hirte_log_errorf("Failed to stop proxy service: %s", strerror(-r));
+                }
+        }
+
+        proxy_service_unexport(proxy);
+
+        LIST_REMOVE(proxy_services, agent->proxy_services, proxy);
+        proxy->agent = NULL;
+        proxy_service_unref(proxy);
+}
+
 void agent_unref(Agent *agent) {
+        assert(agent->ref_count > 0);
+
         agent->ref_count--;
         if (agent->ref_count != 0) {
                 return;
         }
 
+        hirte_log_debug("Finalizing agent");
+
+        /* These are removed in agent_stop */
+        assert(agent->proxy_services == NULL);
+
         hashmap_free(agent->unit_infos);
 
         free(agent->name);
@@ -783,7 +842,7 @@ static void agent_job_done(UNUSED sd_bus_message *m, const char *result, void *u
         AgentJobOp *op = userdata;
         Agent *agent = op->agent;
 
-        hirte_log_debugf("Sending JobDone %u, result: %s", op->hirte_job_id, result);
+        hirte_log_infof("Sending JobDone %u, result: %s", op->hirte_job_id, result);
 
         int r = sd_bus_emit_signal(
                         agent->peer_dbus,
@@ -856,8 +915,6 @@ static int agent_run_unit_lifecycle_method(sd_bus_message *m, Agent *agent, cons
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
         }
 
-        hirte_log_debugf("Return value: %i", r);
-
         if (!systemd_request_start(req, unit_lifecycle_method_callback)) {
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
         }
@@ -1011,6 +1068,103 @@ static int agent_method_unsubscribe(sd_bus_message *m, void *userdata, UNUSED sd
         return sd_bus_reply_method_return(m, "");
 }
 
+/*************************************************************************
+ ********** org.containers.hirte.internal.Agent.StartDep *****************
+ ************************************************************************/
+
+static char *get_dep_unit(const char *unit_name) {
+        return strcat_dup("hirte-dep@", unit_name);
+}
+
+static int start_dep_callback(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        _cleanup_systemd_request_ SystemdRequest *req = userdata;
+
+        if (sd_bus_message_is_method_error(m, NULL)) {
+                hirte_log_errorf("Error starting dep service: %s", sd_bus_message_get_error(m)->message);
+        }
+        return 0;
+}
+
+static int agent_method_start_dep(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        Agent *agent = userdata;
+        const char *unit = NULL;
+
+        int r = sd_bus_message_read(m, "s", &unit);
+        if (r < 0) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "Invalid arguments");
+        }
+
+        _cleanup_free_ char *dep_unit = get_dep_unit(unit);
+        if (dep_unit == NULL) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        hirte_log_infof("Starting dependency %s", dep_unit);
+
+        _cleanup_systemd_request_ SystemdRequest *req = agent_create_request(agent, m, "StartUnit");
+        if (req == NULL) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        r = sd_bus_message_append(req->message, "ss", dep_unit, "replace");
+        if (r < 0) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        if (!systemd_request_start(req, start_dep_callback)) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        return 0;
+}
+
+/*************************************************************************
+ ********** org.containers.hirte.internal.Agent.StopDep ******************
+ ************************************************************************/
+
+static int stop_dep_callback(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        _cleanup_systemd_request_ SystemdRequest *req = userdata;
+
+        if (sd_bus_message_is_method_error(m, NULL)) {
+                hirte_log_errorf("Error stopping dep service: %s", sd_bus_message_get_error(m)->message);
+        }
+        return 0;
+}
+
+static int agent_method_stop_dep(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        Agent *agent = userdata;
+        const char *unit = NULL;
+
+        int r = sd_bus_message_read(m, "s", &unit);
+        if (r < 0) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "Invalid arguments");
+        }
+
+        _cleanup_free_ char *dep_unit = get_dep_unit(unit);
+        if (dep_unit == NULL) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        hirte_log_infof("Stopping dependency %s", dep_unit);
+
+        _cleanup_systemd_request_ SystemdRequest *req = agent_create_request(agent, m, "StopUnit");
+        if (req == NULL) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        r = sd_bus_message_append(req->message, "ss", dep_unit, "replace");
+        if (r < 0) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        if (!systemd_request_start(req, stop_dep_callback)) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        return 0;
+}
+
+
 static const sd_bus_vtable internal_agent_vtable[] = {
         SD_BUS_VTABLE_START(0),
         SD_BUS_METHOD("ListUnits", "", UNIT_INFO_STRUCT_ARRAY_TYPESTRING, agent_method_list_units, 0),
@@ -1039,6 +1193,109 @@ static const sd_bus_vtable internal_agent_vtable[] = {
                         0),
         SD_BUS_SIGNAL_WITH_NAMES("UnitRemoved", "s", SD_BUS_PARAM(unit), 0),
         SD_BUS_SIGNAL_WITH_NAMES("Heartbeat", "s", SD_BUS_PARAM(agent_name), 0),
+        SD_BUS_METHOD("StartDep", "s", "", agent_method_start_dep, 0),
+        SD_BUS_METHOD("StopDep", "s", "", agent_method_stop_dep, 0),
+        SD_BUS_VTABLE_END
+};
+
+/*************************************************************************
+ ********** org.containers.hirte.Agent.CreateProxy ***********************
+ ************************************************************************/
+
+static ProxyService *agent_find_proxy(
+                Agent *agent, const char *local_service_name, const char *node_name, const char *unit_name) {
+        ProxyService *proxy = NULL;
+
+        LIST_FOREACH(proxy_services, proxy, agent->proxy_services) {
+                if (streq(proxy->local_service_name, local_service_name) &&
+                    streq(proxy->node_name, node_name) && streq(proxy->unit_name, unit_name)) {
+                        return proxy;
+                }
+        }
+
+        return NULL;
+}
+
+static int agent_method_create_proxy(UNUSED sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        Agent *agent = (Agent *) userdata;
+        _cleanup_sd_bus_message_ sd_bus_message *reply = NULL;
+        const char *local_service_name = NULL;
+        const char *node_name = NULL;
+        const char *unit_name = NULL;
+        int r = sd_bus_message_read(m, "sss", &local_service_name, &node_name, &unit_name);
+        if (r < 0) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "Invalid arguments");
+        }
+
+        hirte_log_infof("CreateProxy request from %s", local_service_name);
+
+        ProxyService *old_proxy = agent_find_proxy(agent, local_service_name, node_name, unit_name);
+        if (old_proxy) {
+                return sd_bus_reply_method_errorf(reply, SD_BUS_ERROR_ADDRESS_IN_USE, "Proxy already exists");
+        }
+
+        _cleanup_proxy_service_ ProxyService *proxy = proxy_service_new(
+                        agent, local_service_name, node_name, unit_name, m);
+        if (proxy == NULL) {
+                return sd_bus_reply_method_errorf(reply, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        if (!proxy_service_export(proxy)) {
+                return sd_bus_reply_method_errorf(reply, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        r = proxy_service_emit_proxy_new(proxy);
+        if (r < 0 && r != -ENOTCONN) {
+                hirte_log_errorf("Failed to emit ProxyNew signal: %s", strerror(-r));
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        LIST_APPEND(proxy_services, agent->proxy_services, proxy_service_ref(proxy));
+
+        return 1;
+}
+
+/*************************************************************************
+ ********** org.containers.hirte.Agent.RemoveProxy ***********************
+ ************************************************************************/
+
+/* This is called when the proxy service is shut down, via the
+ * `ExecStop=hirte-proxy remove` command being called. This will
+ * happen when no services depends on the proxy anymore, as well as
+ * when the agent explicitly tells the proxy to stop.
+ */
+
+static int agent_method_remove_proxy(sd_bus_message *m, UNUSED void *userdata, UNUSED sd_bus_error *ret_error) {
+        Agent *agent = (Agent *) userdata;
+        const char *local_service_name = NULL;
+        const char *node_name = NULL;
+        const char *unit_name = NULL;
+        int r = sd_bus_message_read(m, "sss", &local_service_name, &node_name, &unit_name);
+        if (r < 0) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "Invalid arguments");
+        }
+
+        hirte_log_infof("RemoveProxy request from %s", local_service_name);
+
+        ProxyService *proxy = agent_find_proxy(agent, local_service_name, node_name, unit_name);
+        if (proxy == NULL) {
+                /* NOTE: This happens pretty often, when we stopped the proxy service ourselves
+                   and removed the proxy, but then the ExecStop triggers, so we sent a success here. */
+                return sd_bus_reply_method_return(m, "");
+        }
+
+        /* We got this because the proxy is shutting down, no need to manually do it */
+        proxy->dont_stop_proxy = true;
+
+        agent_remove_proxy(agent, proxy, true);
+
+        return sd_bus_reply_method_return(m, "");
+}
+
+static const sd_bus_vtable agent_vtable[] = {
+        SD_BUS_VTABLE_START(0),
+        SD_BUS_METHOD("CreateProxy", "sss", "", agent_method_create_proxy, 0),
+        SD_BUS_METHOD("RemoveProxy", "sss", "", agent_method_remove_proxy, 0),
         SD_BUS_VTABLE_END
 };
 
@@ -1219,8 +1476,12 @@ static int agent_match_unit_new(sd_bus_message *m, void *userdata, UNUSED sd_bus
         }
 
         info->loaded = true;
-        info->active_state = _UNIT_ACTIVE_STATE_INVALID;
-        info->substate = NULL;
+        /* Systemd services start in inactive(dead) state, so use this as the first
+         * state. This way we can detect when the state changed without sporadic
+         * changes to "dead".
+         */
+        info->active_state = UNIT_INACTIVE;
+        info->substate = strdup("dead");
 
         if (info->subscribed) {
                 /* Forward the event */
@@ -1416,13 +1677,20 @@ bool agent_start(Agent *agent) {
         }
 
         if (agent->api_bus == NULL) {
-                hirte_log_error("Failed to open user dbus");
+                hirte_log_error("Failed to open api dbus");
+                return false;
+        }
+
+        r = sd_bus_add_object_vtable(
+                        agent->api_bus, NULL, HIRTE_AGENT_OBJECT_PATH, AGENT_INTERFACE, agent_vtable, agent);
+        if (r < 0) {
+                hirte_log_errorf("Failed to add agent vtable: %s", strerror(-r));
                 return false;
         }
 
         r = sd_bus_request_name(agent->api_bus, agent->api_bus_service_name, SD_BUS_NAME_REPLACE_EXISTING);
         if (r < 0) {
-                hirte_log_errorf("Failed to acquire service name on user dbus: %s", strerror(-r));
+                hirte_log_errorf("Failed to acquire service name on api dbus: %s", strerror(-r));
                 return false;
         }
 
@@ -1470,7 +1738,7 @@ bool agent_start(Agent *agent) {
                         agent_match_unit_changed,
                         agent);
         if (r < 0) {
-                hirte_log_error("Failed to add match");
+                hirte_log_errorf("Failed to add match: %s", strerror(-r));
                 return false;
         }
 
@@ -1560,7 +1828,7 @@ bool agent_start(Agent *agent) {
         }
 
         if (!agent_connect(agent)) {
-                hirte_log_errorf("Agent '%s' disconnected", agent->name);
+                hirte_log_error("Initial manager connection failed, retrying");
                 agent->connection_state = AGENT_CONNECTION_STATE_RETRY;
         }
 
@@ -1578,12 +1846,20 @@ bool agent_stop(Agent *agent) {
                 return false;
         }
 
+        ProxyService *proxy = NULL;
+        ProxyService *next_proxy = NULL;
+        LIST_FOREACH_SAFE(proxy_services, proxy, next_proxy, agent->proxy_services) {
+                agent_remove_proxy(agent, proxy, false);
+        }
+
         return true;
 }
 
 static bool agent_connect(Agent *agent) {
         peer_bus_close(agent->peer_dbus);
 
+        hirte_log_infof("Connecting to manager on %s", agent->orch_addr);
+
         agent->peer_dbus = peer_bus_open(agent->event, "peer-bus-to-controller", agent->orch_addr);
         if (agent->peer_dbus == NULL) {
                 hirte_log_error("Failed to open peer dbus");
@@ -1625,10 +1901,9 @@ static bool agent_connect(Agent *agent) {
                         "s",
                         agent->name);
         if (r < 0) {
-                hirte_log_errorf(
-                                "Agent '%s' connection attempt %d: failure",
-                                agent->name,
-                                agent->connection_retry_count);
+                if (r != -ENOTCONN) {
+                        hirte_log_errorf("Registering as '%s' failed: %s", agent->name, error.message);
+                }
                 sd_bus_error_free(&error);
                 return false;
         }
@@ -1639,7 +1914,7 @@ static bool agent_connect(Agent *agent) {
                 return false;
         }
 
-        hirte_log_infof("Agent '%s' connection attempt %d: success", agent->name, agent->connection_retry_count);
+        hirte_log_infof("Connected to manager as '%s'", agent->name);
 
         agent->connection_state = AGENT_CONNECTION_STATE_CONNECTED;
         agent->connection_retry_count = 0;
@@ -1659,5 +1934,58 @@ static bool agent_connect(Agent *agent) {
                 return false;
         }
 
+        /* re-emit ProxyNew signals */
+
+        ProxyService *proxy = NULL;
+        ProxyService *next_proxy = NULL;
+        LIST_FOREACH_SAFE(proxy_services, proxy, next_proxy, agent->proxy_services) {
+                r = proxy_service_emit_proxy_new(proxy);
+                if (r < 0) {
+                        hirte_log_errorf(
+                                        "Failed to re-emit ProxyNew signal for proxy %u requesting unit %s on %s",
+                                        proxy->id,
+                                        proxy->unit_name,
+                                        proxy->node_name);
+                }
+        }
+
         return true;
 }
+
+static int stop_proxy_callback(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        _cleanup_systemd_request_ SystemdRequest *req = userdata;
+
+        if (sd_bus_message_is_method_error(m, NULL)) {
+                hirte_log_errorf("Error stopping proxy service: %s", sd_bus_message_get_error(m)->message);
+        }
+
+        return 0;
+}
+
+/* Force stop the service via systemd */
+static int agent_stop_local_proxy_service(Agent *agent, ProxyService *proxy) {
+        if (proxy->dont_stop_proxy) {
+                return 0;
+        }
+
+        hirte_log_infof("Stopping proxy service %s", proxy->local_service_name);
+
+        /* Don't stop twice */
+        proxy->dont_stop_proxy = true;
+        _cleanup_systemd_request_ SystemdRequest *req = agent_create_request(agent, NULL, "StopUnit");
+        if (req == NULL) {
+                return -ENOMEM;
+        }
+
+        int r = sd_bus_message_append(req->message, "ss", proxy->local_service_name, "replace");
+        if (r < 0) {
+                return -ENOMEM;
+        }
+
+        if (!systemd_request_start(req, stop_proxy_callback)) {
+                return -EIO;
+        }
+
+
+        return 0;
+}
diff --git a/src/agent/agent.h b/src/agent/agent.h
index f8f38e12bd..edbffe5fc3 100644
--- a/src/agent/agent.h
+++ b/src/agent/agent.h
@@ -9,6 +9,8 @@
 #include "libhirte/common/common.h"
 #include "libhirte/hashmap/hashmap.h"
 
+#include "types.h"
+
 typedef struct Agent Agent;
 typedef struct SystemdRequest SystemdRequest;
 
@@ -67,6 +69,8 @@ struct Agent {
 
         LIST_HEAD(JobTracker, tracked_jobs);
 
+        LIST_HEAD(ProxyService, proxy_services);
+
         struct hashmap *unit_infos;
 
         struct config *config;
@@ -96,5 +100,9 @@ bool agent_parse_config(Agent *agent, const char *configfile);
 bool agent_start(Agent *agent);
 bool agent_stop(Agent *agent);
 
+bool agent_is_connected(Agent *agent);
+
+void agent_remove_proxy(Agent *agent, ProxyService *proxy, bool emit);
+
 DEFINE_CLEANUP_FUNC(Agent, agent_unref)
 #define _cleanup_agent_ _cleanup_(agent_unrefp)
diff --git a/src/agent/main.c b/src/agent/main.c
index d87b04a6f9..5f0cfa31f2 100644
--- a/src/agent/main.c
+++ b/src/agent/main.c
@@ -112,9 +112,10 @@ int main(int argc, char *argv[]) {
                 return EXIT_FAILURE;
         }
 
-        if (agent_start(agent)) {
-                return EXIT_SUCCESS;
+        if (!agent_start(agent)) {
+                return EXIT_FAILURE;
         }
 
-        return EXIT_FAILURE;
+        agent_stop(agent);
+        return EXIT_SUCCESS;
 }
diff --git a/src/agent/meson.build b/src/agent/meson.build
index 3b8982a1a3..6c26068114 100644
--- a/src/agent/meson.build
+++ b/src/agent/meson.build
@@ -4,7 +4,8 @@
 
 node_src = [
   'main.c',
-  'agent.c'
+  'agent.c',
+  'proxy.c'
 ]
 
 executable(
diff --git a/src/agent/proxy.c b/src/agent/proxy.c
new file mode 100644
index 0000000000..f372431ea3
--- /dev/null
+++ b/src/agent/proxy.c
@@ -0,0 +1,331 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#include "libhirte/log/log.h"
+
+#include "agent.h"
+#include "proxy.h"
+
+
+static int proxy_service_method_error(sd_bus_message *m, void *userdata, sd_bus_error *ret_error);
+static int proxy_service_method_target_state_changed(sd_bus_message *m, void *userdata, sd_bus_error *ret_error);
+static int proxy_service_method_target_new(sd_bus_message *m, void *userdata, sd_bus_error *ret_error);
+static int proxy_service_method_target_removed(sd_bus_message *m, void *userdata, sd_bus_error *ret_error);
+
+static const sd_bus_vtable proxy_service_vtable[] = {
+        SD_BUS_VTABLE_START(0),
+        SD_BUS_METHOD("Error", "s", "", proxy_service_method_error, 0),
+        SD_BUS_METHOD("TargetNew", "s", "", proxy_service_method_target_new, 0),
+        SD_BUS_METHOD("TargetStateChanged", "sss", "", proxy_service_method_target_state_changed, 0),
+        SD_BUS_METHOD("TargetRemoved", "s", "", proxy_service_method_target_removed, 0),
+        SD_BUS_VTABLE_END
+};
+
+/* This is called by the manager when the service that is the target
+ * of a proxy changes state.
+ */
+
+static void proxy_service_initial_state_reached(ProxyService *proxy, bool success) {
+        Agent *agent = proxy->agent;
+        int r = 0;
+
+        assert(proxy->request_message != NULL);
+
+        if (success) {
+                hirte_log_infof("Replying to %s successfully", proxy->local_service_name);
+                proxy->sent_successful_ready = true;
+                r = sd_bus_reply_method_return(proxy->request_message, "");
+        } else {
+                hirte_log_infof("Replying to %s with failure", proxy->local_service_name);
+                r = sd_bus_reply_method_errorf(
+                                proxy->request_message,
+                                HIRTE_BUS_ERROR_ACTIVATION_FAILED,
+                                "Proxy service failed to start");
+        }
+
+        if (r < 0) {
+                hirte_log_errorf("Failed to send reply to proxy request: %s", strerror(-r));
+        }
+
+        sd_bus_message_unrefp(&proxy->request_message);
+        proxy->request_message = NULL;
+
+        if (!proxy->sent_successful_ready) {
+                agent_remove_proxy(agent, proxy, true);
+        }
+}
+
+static void proxy_service_target_stopped(ProxyService *proxy) {
+        Agent *agent = proxy->agent;
+
+        hirte_log_infof("Proxy for %s stopping due to target stopped", proxy->local_service_name);
+
+        assert(proxy->request_message == NULL);
+        agent_remove_proxy(agent, proxy, true);
+}
+
+static int proxy_service_method_target_state_changed(
+                sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        _cleanup_proxy_service_ ProxyService *proxy = proxy_service_ref((ProxyService *) userdata);
+        Agent *agent = proxy->agent;
+
+        if (agent == NULL) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+
+        const char *active_state_str = NULL;
+        const char *substate = NULL;
+        const char *reason = NULL;
+        int r = sd_bus_message_read(m, "sss", &active_state_str, &substate, &reason);
+        if (r < 0) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "Invalid arguments");
+        }
+
+        UnitActiveState active_state = active_state_from_string(active_state_str);
+
+        hirte_log_debugf(
+                        "Proxy service '%s' got TargetStateChanged from manager: %s %s %s",
+                        proxy->local_service_name,
+                        active_state_str,
+                        substate,
+                        reason);
+
+        if (proxy->request_message) {
+                /* We're waiting for the initial start-or-fail to report back
+                 *
+                 * There are two main things that can happen here.
+                 *
+                 * Either the state eventually changes to any kind of
+                 * active state. Then we consider the target service
+                 * to have started, and we report that, and switch to waiting
+                 * for it to stop. It is possible that we get a virtual
+                 * active state change because the service was already running,
+                 * but this is fine here.
+                 *
+                 * Alternatively, we get a message that says the state
+                 * switched to failed, or inactive. This means the service
+                 * stopped, which we report as a failure to start the target.
+                 * However, here we have to be careful about virtual inactive
+                 * events, as we may get a virtual inactive for the current
+                 * state before then getting a real transition to the new state.
+                 *
+                 * In addition, it is possible that we never ever change state, because
+                 * the service fails to parse, for instance. If so, we hit the (real) removed
+                 * event before seeing any activation. See below for that.
+                 */
+
+                if (active_state == UNIT_ACTIVE) {
+                        proxy_service_initial_state_reached(proxy, true);
+                } else if ((active_state == UNIT_FAILED || active_state == UNIT_INACTIVE) &&
+                           streq(reason, "real")) {
+                        proxy_service_initial_state_reached(proxy, false);
+                }
+        } else if (proxy->sent_successful_ready) {
+                /* We reached the initial state and are now monitoring for the target to exit */
+                if ((active_state == UNIT_FAILED || active_state == UNIT_INACTIVE) && streq(reason, "real")) {
+                        proxy_service_target_stopped(proxy);
+                }
+        }
+
+        return sd_bus_reply_method_return(m, "");
+}
+
+static int proxy_service_method_target_new(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        _cleanup_proxy_service_ ProxyService *proxy = proxy_service_ref((ProxyService *) userdata);
+        Agent *agent = proxy->agent;
+        if (agent == NULL) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        const char *reason = NULL;
+        int r = sd_bus_message_read(m, "s", &reason);
+        if (r < 0) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "Invalid arguments");
+        }
+
+        hirte_log_debugf("Proxy service '%s' got TargetNew from manager: %s", proxy->local_service_name, reason);
+
+        return sd_bus_reply_method_return(m, "");
+}
+
+static int proxy_service_method_target_removed(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        _cleanup_proxy_service_ ProxyService *proxy = proxy_service_ref((ProxyService *) userdata);
+        Agent *agent = proxy->agent;
+        if (agent == NULL) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        const char *reason = NULL;
+        int r = sd_bus_message_read(m, "s", &reason);
+        if (r < 0) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "Invalid arguments");
+        }
+
+        hirte_log_debugf(
+                        "Proxy service '%s' got TargetRemoved from manager: %s",
+                        proxy->local_service_name,
+                        reason);
+
+        /* See above in state_changed for details */
+
+        if (proxy->request_message != NULL && streq(reason, "real")) {
+                proxy_service_initial_state_reached(proxy, false);
+        }
+
+        return sd_bus_reply_method_return(m, "");
+}
+
+/* This is called by the manager when there was an error setting up the monitor.
+ * Note, this is only sent once, and if it is sent, expect no other messages.
+ */
+
+static int proxy_service_method_error(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        _cleanup_proxy_service_ ProxyService *proxy = proxy_service_ref((ProxyService *) userdata);
+        Agent *agent = proxy->agent;
+
+        if (agent == NULL) {
+                return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
+        }
+
+        const char *message = NULL;
+        int r = sd_bus_message_read(m, "s", &message);
+        if (r < 0) {
+                message = "Got no message";
+        }
+
+        hirte_log_errorf("Got proxy start error: %s", message);
+
+        proxy_service_initial_state_reached(proxy, false);
+
+        return sd_bus_reply_method_return(m, "");
+}
+
+ProxyService *proxy_service_new(
+                Agent *agent,
+                const char *local_service_name,
+                const char *node,
+                const char *unit,
+                sd_bus_message *request_message) {
+        static uint32_t next_id = 0;
+
+        _cleanup_proxy_service_ ProxyService *proxy = malloc0(sizeof(ProxyService));
+        if (proxy == NULL) {
+                return NULL;
+        }
+
+        proxy->ref_count = 1;
+        proxy->id = ++next_id;
+        proxy->agent = agent;
+        proxy->request_message = sd_bus_message_ref(request_message);
+        LIST_INIT(proxy_services, proxy);
+
+        proxy->node_name = strdup(node);
+        if (proxy->node_name == NULL) {
+                return NULL;
+        }
+        proxy->unit_name = strdup(unit);
+        if (proxy->unit_name == NULL) {
+                return NULL;
+        }
+        proxy->local_service_name = strdup(local_service_name);
+        if (proxy->local_service_name == NULL) {
+                return NULL;
+        }
+
+        int r = asprintf(&proxy->object_path, "%s/%u", INTERNAL_PROXY_OBJECT_PATH_PREFIX, proxy->id);
+        if (r < 0) {
+                return NULL;
+        }
+
+        return steal_pointer(&proxy);
+}
+
+ProxyService *proxy_service_ref(ProxyService *proxy) {
+        proxy->ref_count++;
+        return proxy;
+}
+
+void proxy_service_unref(ProxyService *proxy) {
+        proxy->ref_count--;
+        if (proxy->ref_count != 0) {
+                return;
+        }
+
+        /* Should have been removed from list by now */
+        assert(proxy->proxy_services_next == NULL);
+        assert(proxy->proxy_services_prev == NULL);
+
+        sd_bus_message_unrefp(&proxy->request_message);
+        sd_bus_slot_unrefp(&proxy->export_slot);
+
+        free(proxy->node_name);
+        free(proxy->unit_name);
+        free(proxy->local_service_name);
+        free(proxy->object_path);
+        free(proxy);
+}
+
+bool proxy_service_export(ProxyService *proxy) {
+        int r = sd_bus_add_object_vtable(
+                        proxy->agent->peer_dbus,
+                        &proxy->export_slot,
+                        proxy->object_path,
+                        INTERNAL_PROXY_INTERFACE,
+                        proxy_service_vtable,
+                        proxy);
+        if (r < 0) {
+                hirte_log_errorf("Failed to add service proxy vtable: %s", strerror(-r));
+                return false;
+        }
+        return true;
+}
+
+void proxy_service_unexport(ProxyService *proxy) {
+        sd_bus_slot_unrefp(&proxy->export_slot);
+        proxy->export_slot = NULL;
+}
+
+int proxy_service_emit_proxy_new(ProxyService *proxy) {
+        Agent *agent = proxy->agent;
+
+        if (!agent_is_connected(agent)) {
+                return -ENOTCONN;
+        }
+
+        hirte_log_infof("Registering new proxy for %s (on %s)", proxy->unit_name, proxy->node_name);
+        int res = sd_bus_emit_signal(
+                        agent->peer_dbus,
+                        INTERNAL_AGENT_OBJECT_PATH,
+                        INTERNAL_AGENT_INTERFACE,
+                        "ProxyNew",
+                        "sso",
+                        proxy->node_name,
+                        proxy->unit_name,
+                        proxy->object_path);
+        if (res >= 0) {
+                proxy->sent_new_proxy = true;
+        }
+        return res;
+}
+
+int proxy_service_emit_proxy_removed(ProxyService *proxy) {
+        Agent *agent = proxy->agent;
+
+        /* No need to tell the manager if we didn't announce this */
+        if (!proxy->sent_new_proxy) {
+                return 0;
+        }
+
+        if (!agent_is_connected(agent)) {
+                return -ENOTCONN;
+        }
+
+        hirte_log_infof("Unregistering proxy for %s (on %s)", proxy->unit_name, proxy->node_name);
+        return sd_bus_emit_signal(
+                        proxy->agent->peer_dbus,
+                        INTERNAL_AGENT_OBJECT_PATH,
+                        INTERNAL_AGENT_INTERFACE,
+                        "ProxyRemoved",
+                        "ss",
+                        proxy->node_name,
+                        proxy->unit_name);
+}
diff --git a/src/agent/proxy.h b/src/agent/proxy.h
new file mode 100644
index 0000000000..54583440f8
--- /dev/null
+++ b/src/agent/proxy.h
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include "libhirte/common/common.h"
+
+#include "types.h"
+
+struct ProxyService {
+        int ref_count;
+        uint32_t id;
+
+        Agent *agent; /* weak ref */
+
+        char *node_name;
+        char *unit_name;
+        char *local_service_name;
+
+        sd_bus_message *request_message;
+
+        bool sent_new_proxy; /* We told manager about the proxy */
+        bool dont_stop_proxy;
+        bool sent_successful_ready;
+
+        sd_bus_slot *export_slot;
+        char *object_path;
+
+        LIST_FIELDS(ProxyService, proxy_services); /* List in Agent */
+};
+
+ProxyService *proxy_service_new(
+                Agent *agent,
+                const char *local_service_name,
+                const char *node_name,
+                const char *unit_name,
+                sd_bus_message *request_message);
+ProxyService *proxy_service_ref(ProxyService *proxy);
+void proxy_service_unref(ProxyService *proxy);
+
+bool proxy_service_export(ProxyService *proxy);
+void proxy_service_unexport(ProxyService *proxy);
+
+int proxy_service_emit_proxy_new(ProxyService *proxy);
+int proxy_service_emit_proxy_removed(ProxyService *proxy);
+
+DEFINE_CLEANUP_FUNC(ProxyService, proxy_service_unref);
+#define _cleanup_proxy_service_ _cleanup_(proxy_service_unrefp)
diff --git a/src/agent/types.h b/src/agent/types.h
new file mode 100644
index 0000000000..46154e7763
--- /dev/null
+++ b/src/agent/types.h
@@ -0,0 +1,5 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+typedef struct ProxyService ProxyService;
+typedef struct Agent Agent;
diff --git a/src/libhirte/common/protocol.h b/src/libhirte/common/protocol.h
index 250a534796..9d76d25264 100644
--- a/src/libhirte/common/protocol.h
+++ b/src/libhirte/common/protocol.h
@@ -12,10 +12,12 @@
 
 /* Public objects */
 #define HIRTE_MANAGER_OBJECT_PATH HIRTE_OBJECT_PATH
+#define HIRTE_AGENT_OBJECT_PATH HIRTE_OBJECT_PATH
 
 /* Public interfaces */
 #define HIRTE_INTERFACE_BASE_NAME "org.containers.hirte"
 #define MANAGER_INTERFACE HIRTE_INTERFACE_BASE_NAME ".Manager"
+#define AGENT_INTERFACE HIRTE_INTERFACE_BASE_NAME ".Agent"
 #define NODE_INTERFACE HIRTE_INTERFACE_BASE_NAME ".Node"
 #define JOB_INTERFACE HIRTE_INTERFACE_BASE_NAME ".Job"
 #define MONITOR_INTERFACE HIRTE_INTERFACE_BASE_NAME ".Monitor"
@@ -27,13 +29,16 @@
 /* Internal objects */
 #define INTERNAL_MANAGER_OBJECT_PATH HIRTE_OBJECT_PATH "/internal"
 #define INTERNAL_AGENT_OBJECT_PATH HIRTE_OBJECT_PATH "/internal/agent"
+#define INTERNAL_PROXY_OBJECT_PATH_PREFIX HIRTE_OBJECT_PATH "/internal/proxy"
 
 /* Internal interfaces */
 #define INTERNAL_MANAGER_INTERFACE HIRTE_INTERFACE_BASE_NAME ".internal.Manager"
 #define INTERNAL_AGENT_INTERFACE HIRTE_INTERFACE_BASE_NAME ".internal.Agent"
+#define INTERNAL_PROXY_INTERFACE HIRTE_INTERFACE_BASE_NAME ".internal.Proxy"
 
 #define HIRTE_BUS_ERROR_OFFLINE "org.containers.hirte.Offline"
 #define HIRTE_BUS_ERROR_NO_SUCH_SUBSCRIPTION "org.containers.hirte.NoSuchSubscription"
+#define HIRTE_BUS_ERROR_ACTIVATION_FAILED "org.containers.hirte.ActivationFailed"
 
 /* Systemd protocol */
 
diff --git a/src/manager/meson.build b/src/manager/meson.build
index 25c06d6609..30a1130a98 100644
--- a/src/manager/meson.build
+++ b/src/manager/meson.build
@@ -11,6 +11,8 @@ orch_src = [
     'job.c',
     'monitor.h',
     'monitor.c',
+    'proxy_monitor.c',
+    'proxy_monitor.h',
     'main.c',
 ]
 
diff --git a/src/manager/monitor.c b/src/manager/monitor.c
index d6a1e30f09..1f2422c21e 100644
--- a/src/manager/monitor.c
+++ b/src/manager/monitor.c
@@ -6,14 +6,13 @@
 #include "monitor.h"
 #include "node.h"
 
-Subscription *subscription_new(Monitor *monitor, const char *node, const char *unit) {
+Subscription *subscription_new(const char *node, const char *unit) {
         _cleanup_subscription_ Subscription *subscription = malloc0(sizeof(Subscription));
         if (subscription == NULL) {
                 return NULL;
         }
 
         subscription->ref_count = 1;
-        subscription->monitor = monitor_ref(monitor);
         LIST_INIT(subscriptions, subscription);
         LIST_INIT(all_subscriptions, subscription);
 
@@ -30,6 +29,23 @@ Subscription *subscription_new(Monitor *monitor, const char *node, const char *u
         return steal_pointer(&subscription);
 }
 
+Subscription *create_monitor_subscription(Monitor *monitor, const char *node, const char *unit) {
+        Subscription *subscription = subscription_new(node, unit);
+        if (subscription == NULL) {
+                return NULL;
+        }
+
+        subscription->monitor = monitor_ref(monitor);
+        subscription->free_monitor = (free_func_t) monitor_unref;
+
+        subscription->handle_unit_new = monitor_on_unit_new;
+        subscription->handle_unit_removed = monitor_on_unit_removed;
+        subscription->handle_unit_state_changed = monitor_on_unit_state_changed;
+        subscription->handle_unit_property_changed = monitor_on_unit_property_changed;
+
+        return subscription;
+}
+
 Subscription *subscription_ref(Subscription *subscription) {
         subscription->ref_count++;
         return subscription;
@@ -41,12 +57,19 @@ void subscription_unref(Subscription *subscription) {
                 return;
         }
 
-        monitor_unrefp(&subscription->monitor);
+        if (subscription->free_monitor) {
+                subscription->free_monitor(subscription->monitor);
+        }
         free(subscription->unit);
         free(subscription->node);
         free(subscription);
 }
 
+
+/***********************************
+ ********** Monitor ****************
+ ***********************************/
+
 static int monitor_method_close(sd_bus_message *m, void *userdata, sd_bus_error *ret_error);
 static int monitor_method_subscribe(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error);
 static int monitor_method_unsubscribe(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error);
@@ -182,7 +205,7 @@ static int monitor_method_subscribe(sd_bus_message *m, void *userdata, UNUSED sd
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_INVALID_ARGS, "Invalid arguments");
         }
 
-        _cleanup_subscription_ Subscription *sub = subscription_new(monitor, node, unit);
+        _cleanup_subscription_ Subscription *sub = create_monitor_subscription(monitor, node, unit);
         if (sub == NULL) {
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
         }
@@ -234,8 +257,9 @@ static int monitor_method_unsubscribe(sd_bus_message *m, void *userdata, UNUSED
         return sd_bus_reply_method_return(m, "");
 }
 
-int monitor_emit_unit_property_changed(
-                Monitor *monitor, const char *node, const char *unit, const char *interface, sd_bus_message *m) {
+int monitor_on_unit_property_changed(
+                void *userdata, const char *node, const char *unit, const char *interface, sd_bus_message *m) {
+        Monitor *monitor = (Monitor *) userdata;
         Manager *manager = monitor->manager;
 
         _cleanup_sd_bus_message_ sd_bus_message *sig = NULL;
@@ -268,7 +292,8 @@ int monitor_emit_unit_property_changed(
         return sd_bus_message_rewind(m, false);
 }
 
-int monitor_emit_unit_new(Monitor *monitor, const char *node, const char *unit, const char *reason) {
+int monitor_on_unit_new(void *userdata, const char *node, const char *unit, const char *reason) {
+        Monitor *monitor = (Monitor *) userdata;
         Manager *manager = monitor->manager;
 
         return sd_bus_emit_signal(
@@ -282,13 +307,14 @@ int monitor_emit_unit_new(Monitor *monitor, const char *node, const char *unit,
                         reason);
 }
 
-int monitor_emit_unit_state_changed(
-                Monitor *monitor,
+int monitor_on_unit_state_changed(
+                void *userdata,
                 const char *node,
                 const char *unit,
                 const char *active_state,
                 const char *substate,
                 const char *reason) {
+        Monitor *monitor = (Monitor *) userdata;
         Manager *manager = monitor->manager;
 
         return sd_bus_emit_signal(
@@ -304,7 +330,8 @@ int monitor_emit_unit_state_changed(
                         reason);
 }
 
-int monitor_emit_unit_removed(Monitor *monitor, const char *node, const char *unit, const char *reason) {
+int monitor_on_unit_removed(void *userdata, const char *node, const char *unit, const char *reason) {
+        Monitor *monitor = (Monitor *) userdata;
         Manager *manager = monitor->manager;
 
         return sd_bus_emit_signal(
diff --git a/src/manager/monitor.h b/src/manager/monitor.h
index 70addade43..cbbe8e7a5f 100644
--- a/src/manager/monitor.h
+++ b/src/manager/monitor.h
@@ -5,9 +5,29 @@
 
 #include "types.h"
 
+typedef int(unit_property_changed_handler_func_t)(
+                void *monitor, const char *node, const char *unit, const char *interface, sd_bus_message *m);
+typedef int(unit_new_handler_func_t)(void *monitor, const char *node, const char *unit, const char *reason);
+typedef int(unit_state_changed_handler_func_t)(
+                void *monitor,
+                const char *node,
+                const char *unit,
+                const char *active_state,
+                const char *substate,
+                const char *reason);
+typedef int(unit_removed_handler_func_t)(void *monitor, const char *node, const char *unit, const char *reason);
+
+
 struct Subscription {
         int ref_count;
-        Monitor *monitor;
+
+        void *monitor;
+        free_func_t free_monitor;
+
+        unit_new_handler_func_t *handle_unit_new;
+        unit_removed_handler_func_t *handle_unit_removed;
+        unit_state_changed_handler_func_t *handle_unit_state_changed;
+        unit_property_changed_handler_func_t *handle_unit_property_changed;
 
         char *node;
         char *unit;
@@ -16,7 +36,8 @@ struct Subscription {
         LIST_FIELDS(Subscription, all_subscriptions); /* List in Manager */
 };
 
-Subscription *subscription_new(Monitor *monitor, const char *node, const char *unit);
+Subscription *subscription_new(const char *node, const char *unit);
+Subscription *create_monitor_subscription(Monitor *monitor, const char *node, const char *unit);
 Subscription *subscription_ref(Subscription *subscription);
 void subscription_unref(Subscription *subscription);
 
@@ -46,17 +67,17 @@ void monitor_close(Monitor *monitor);
 
 bool monitor_export(Monitor *monitor);
 
-int monitor_emit_unit_property_changed(
-                Monitor *monitor, const char *node, const char *unit, const char *interface, sd_bus_message *m);
-int monitor_emit_unit_new(Monitor *monitor, const char *node, const char *unit, const char *reason);
-int monitor_emit_unit_state_changed(
-                Monitor *monitor,
+int monitor_on_unit_property_changed(
+                void *userdata, const char *node, const char *unit, const char *interface, sd_bus_message *m);
+int monitor_on_unit_new(void *userdata, const char *node, const char *unit, const char *reason);
+int monitor_on_unit_state_changed(
+                void *userdata,
                 const char *node,
                 const char *unit,
                 const char *active_state,
                 const char *substate,
                 const char *reason);
-int monitor_emit_unit_removed(Monitor *monitor, const char *node, const char *unit, const char *reason);
+int monitor_on_unit_removed(void *userdata, const char *node, const char *unit, const char *reason);
 
 DEFINE_CLEANUP_FUNC(Monitor, monitor_unref)
 #define _cleanup_monitor_ _cleanup_(monitor_unrefp)
diff --git a/src/manager/node.c b/src/manager/node.c
index 6e62c53021..766bce8041 100644
--- a/src/manager/node.c
+++ b/src/manager/node.c
@@ -6,10 +6,13 @@
 #include "manager.h"
 #include "monitor.h"
 #include "node.h"
+#include "proxy_monitor.h"
 
 #define DEBUG_AGENT_MESSAGES 0
 
 static void node_send_agent_subscribe_all(Node *node);
+static void node_start_proxy_dependency_all(Node *node);
+static int node_run_unit_lifecycle_method(sd_bus_message *m, Node *node, const char *job_type, const char *method);
 
 static int node_method_register(sd_bus_message *m, void *userdata, sd_bus_error *ret_error);
 static int node_disconnected(sd_bus_message *message, void *userdata, sd_bus_error *error);
@@ -57,6 +60,17 @@ static const sd_bus_vtable node_vtable[] = {
         SD_BUS_VTABLE_END
 };
 
+struct ProxyDependency {
+        char *unit_name;
+        int n_deps;
+        LIST_FIELDS(ProxyDependency, deps);
+};
+
+static void proxy_dependency_free(struct ProxyDependency *dep) {
+        free(dep->unit_name);
+        free(dep);
+}
+
 typedef struct UnitSubscription UnitSubscription;
 
 struct UnitSubscription {
@@ -104,6 +118,9 @@ Node *node_new(Manager *manager, const char *name) {
         node->ref_count = 1;
         node->manager = manager;
         LIST_INIT(nodes, node);
+        LIST_HEAD_INIT(node->outstanding_requests);
+        LIST_HEAD_INIT(node->proxy_monitors);
+        LIST_HEAD_INIT(node->proxy_dependencies);
 
         node->unit_subscriptions = hashmap_new(
                         sizeof(UnitSubscriptions),
@@ -143,10 +160,23 @@ void node_unref(Node *node) {
         if (node->ref_count != 0) {
                 return;
         }
+
+        ProxyMonitor *proxy_monitor = NULL;
+        ProxyMonitor *next_proxy_monitor = NULL;
+        LIST_FOREACH_SAFE(monitors, proxy_monitor, next_proxy_monitor, node->proxy_monitors) {
+                node_remove_proxy_monitor(node, proxy_monitor);
+        }
+
+
+        ProxyDependency *dep = NULL;
+        ProxyDependency *next_dep = NULL;
+        LIST_FOREACH_SAFE(deps, dep, next_dep, node->proxy_dependencies) {
+                proxy_dependency_free(dep);
+        }
+
         sd_bus_slot_unrefp(&node->export_slot);
 
         node_unset_agent_bus(node);
-
         hashmap_free(node->unit_subscriptions);
 
         free(node->name);
@@ -238,9 +268,10 @@ static int node_match_unit_properties_changed(sd_bus_message *m, void *userdata,
         }
 
         UnitSubscription *usub = NULL;
-        LIST_FOREACH(subs, usub, usubs->subs) {
+        UnitSubscription *next_usub = NULL;
+        LIST_FOREACH_SAFE(subs, usub, next_usub, usubs->subs) {
                 Subscription *sub = usub->sub;
-                int r = monitor_emit_unit_property_changed(sub->monitor, node->name, sub->unit, interface, m);
+                int r = sub->handle_unit_property_changed(sub->monitor, node->name, sub->unit, interface, m);
                 if (r < 0) {
                         hirte_log_error("Failed to emit UnitPropertyChanged signal");
                 }
@@ -270,9 +301,10 @@ static int node_match_unit_new(sd_bus_message *m, void *userdata, UNUSED sd_bus_
         usubs->loaded = true;
 
         UnitSubscription *usub = NULL;
-        LIST_FOREACH(subs, usub, usubs->subs) {
+        UnitSubscription *next_usub = NULL;
+        LIST_FOREACH_SAFE(subs, usub, next_usub, usubs->subs) {
                 Subscription *sub = usub->sub;
-                int r = monitor_emit_unit_new(sub->monitor, node->name, sub->unit, reason);
+                int r = sub->handle_unit_new(sub->monitor, node->name, sub->unit, reason);
                 if (r < 0) {
                         hirte_log_error("Failed to emit UnitNew signal");
                 }
@@ -307,9 +339,10 @@ static int node_match_unit_state_changed(sd_bus_message *m, void *userdata, UNUS
         usubs->substate = strdup(substate);
 
         UnitSubscription *usub = NULL;
-        LIST_FOREACH(subs, usub, usubs->subs) {
+        UnitSubscription *next_usub = NULL;
+        LIST_FOREACH_SAFE(subs, usub, next_usub, usubs->subs) {
                 Subscription *sub = usub->sub;
-                int r = monitor_emit_unit_state_changed(
+                int r = sub->handle_unit_state_changed(
                                 sub->monitor, node->name, sub->unit, active_state, substate, reason);
                 if (r < 0) {
                         hirte_log_error("Failed to emit UnitNew signal");
@@ -339,9 +372,10 @@ static int node_match_unit_removed(sd_bus_message *m, void *userdata, UNUSED sd_
         usubs->loaded = false;
 
         UnitSubscription *usub = NULL;
-        LIST_FOREACH(subs, usub, usubs->subs) {
+        UnitSubscription *next_usub = NULL;
+        LIST_FOREACH_SAFE(subs, usub, next_usub, usubs->subs) {
                 Subscription *sub = usub->sub;
-                int r = monitor_emit_unit_removed(sub->monitor, node->name, sub->unit, "real");
+                int r = sub->handle_unit_removed(sub->monitor, node->name, sub->unit, "real");
                 if (r < 0) {
                         hirte_log_error("Failed to emit UnitNew signal");
                 }
@@ -350,7 +384,6 @@ static int node_match_unit_removed(sd_bus_message *m, void *userdata, UNUSED sd_
         return 1;
 }
 
-
 static int node_match_job_done(UNUSED sd_bus_message *m, UNUSED void *userdata, UNUSED sd_bus_error *error) {
         Node *node = userdata;
         Manager *manager = node->manager;
@@ -379,6 +412,111 @@ static int node_match_heartbeat(UNUSED sd_bus_message *m, UNUSED void *userdata,
         return 1;
 }
 
+static ProxyMonitor *node_find_proxy_monitor(Node *node, const char *target_node_name, const char *unit_name) {
+        ProxyMonitor *proxy_monitor = NULL;
+        LIST_FOREACH(monitors, proxy_monitor, node->proxy_monitors) {
+                if (streq(proxy_monitor->target_node->name, target_node_name) &&
+                    streq(proxy_monitor->unit_name, unit_name)) {
+                        return proxy_monitor;
+                }
+        }
+
+        return NULL;
+}
+
+static int node_on_match_proxy_new(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *error) {
+        Node *node = userdata;
+        Manager *manager = node->manager;
+
+        const char *target_node_name = NULL;
+        const char *unit_name = NULL;
+        const char *proxy_object_path = NULL;
+
+        int r = sd_bus_message_read(m, "sso", &target_node_name, &unit_name, &proxy_object_path);
+        if (r < 0) {
+                hirte_log_errorf("Invalid arguments in ProxyNew signal: %s", strerror(-r));
+                return r;
+        }
+        hirte_log_infof("Node '%s' registered new proxy for unit '%s' on node '%s'",
+                        node->name,
+                        unit_name,
+                        target_node_name);
+
+        _cleanup_proxy_monitor_ ProxyMonitor *monitor = proxy_monitor_new(
+                        node, target_node_name, unit_name, proxy_object_path);
+        if (monitor == NULL) {
+                hirte_log_error("Failed to create proxy monitor, OOM");
+                return -ENOMEM;
+        }
+
+        Node *target_node = manager_find_node(manager, target_node_name);
+        if (target_node == NULL) {
+                hirte_log_error("Proxy requested for non-existing node");
+                proxy_monitor_send_error(monitor, "No such node");
+                return 0;
+        }
+
+        ProxyMonitor *old_monitor = node_find_proxy_monitor(node, target_node_name, unit_name);
+        if (old_monitor != NULL) {
+                hirte_log_warnf("Proxy for '%s' (on '%s') requested, but old proxy already exists, removing it",
+                                unit_name,
+                                target_node_name);
+                node_remove_proxy_monitor(node, old_monitor);
+        }
+
+        r = proxy_monitor_set_target_node(monitor, target_node);
+        if (r < 0) {
+                hirte_log_errorf("Failed to add proxy dependency: %s", strerror(-r));
+                proxy_monitor_send_error(monitor, "Failed to add proxy dependency");
+                return 0;
+        }
+
+        /* We now have a valid monitor, add it to the list and enable monitor.
+           From this point we should not send errors. */
+        manager_add_subscription(manager, monitor->subscription);
+        LIST_APPEND(monitors, node->proxy_monitors, proxy_monitor_ref(monitor));
+
+        /* TODO: What about !!node_is_online(target_node) ? Tell now or wait for it to connect? */
+
+        return 0;
+}
+
+void node_remove_proxy_monitor(Node *node, ProxyMonitor *monitor) {
+        Manager *manager = node->manager;
+
+        proxy_monitor_close(monitor);
+
+        manager_remove_subscription(manager, monitor->subscription);
+        LIST_REMOVE(monitors, node->proxy_monitors, monitor);
+
+        proxy_monitor_unref(monitor);
+}
+
+static int node_on_match_proxy_removed(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *error) {
+        Node *node = userdata;
+        const char *target_node_name = NULL;
+        const char *unit_name = NULL;
+
+        int r = sd_bus_message_read(m, "ss", &target_node_name, &unit_name);
+        if (r < 0) {
+                hirte_log_errorf("Invalid arguments in ProxyRemoved signal: %s", strerror(-r));
+                return r;
+        }
+        hirte_log_infof("Node '%s' unregistered proxy for unit '%s' on node '%s'",
+                        node->name,
+                        unit_name,
+                        target_node_name);
+
+        ProxyMonitor *proxy_monitor = node_find_proxy_monitor(node, target_node_name, unit_name);
+        if (proxy_monitor == NULL) {
+                hirte_log_error("Got ProxyRemoved for unknown monitor");
+                return 0;
+        }
+
+        node_remove_proxy_monitor(node, proxy_monitor);
+        return 0;
+}
+
 bool node_set_agent_bus(Node *node, sd_bus *bus) {
         int r = 0;
 
@@ -489,6 +627,34 @@ bool node_set_agent_bus(Node *node, sd_bus *bus) {
                         return false;
                 }
 
+                r = sd_bus_match_signal(
+                                bus,
+                                NULL,
+                                NULL,
+                                INTERNAL_AGENT_OBJECT_PATH,
+                                INTERNAL_AGENT_INTERFACE,
+                                "ProxyNew",
+                                node_on_match_proxy_new,
+                                node);
+                if (r < 0) {
+                        hirte_log_errorf("Failed to add ProxyNew peer bus match: %s", strerror(-r));
+                        return false;
+                }
+
+                r = sd_bus_match_signal(
+                                bus,
+                                NULL,
+                                NULL,
+                                INTERNAL_AGENT_OBJECT_PATH,
+                                INTERNAL_AGENT_INTERFACE,
+                                "ProxyRemoved",
+                                node_on_match_proxy_removed,
+                                node);
+                if (r < 0) {
+                        hirte_log_errorf("Failed to add ProxyNew peer bus match: %s", strerror(-r));
+                        return false;
+                }
+
                 r = sd_bus_emit_properties_changed(
                                 node->manager->api_bus, node->object_path, NODE_INTERFACE, "Status", NULL);
                 if (r < 0) {
@@ -534,6 +700,9 @@ bool node_set_agent_bus(Node *node, sd_bus *bus) {
         /* Register any active subscriptions with new agent */
         node_send_agent_subscribe_all(node);
 
+        /* Register any active dependencies with new agent */
+        node_start_proxy_dependency_all(node);
+
         return true;
 }
 
@@ -631,10 +800,12 @@ static int node_disconnected(UNUSED sd_bus_message *message, void *userdata, UNU
 
                 usubs->loaded = false;
                 UnitSubscription *usub = NULL;
-                LIST_FOREACH(subs, usub, usubs->subs) {
-                        Subscription *sub = usub->sub;
-                        if (send_state_change) {
-                                int r = monitor_emit_unit_state_changed(
+                UnitSubscription *next_usub = NULL;
+
+                if (send_state_change) {
+                        LIST_FOREACH_SAFE(subs, usub, next_usub, usubs->subs) {
+                                Subscription *sub = usub->sub;
+                                int r = sub->handle_unit_state_changed(
                                                 sub->monitor,
                                                 node->name,
                                                 sub->unit,
@@ -645,14 +816,24 @@ static int node_disconnected(UNUSED sd_bus_message *message, void *userdata, UNU
                                         hirte_log_error("Failed to emit UnitRemoved signal");
                                 }
                         }
+                }
 
-                        int r = monitor_emit_unit_removed(sub->monitor, node->name, sub->unit, "virtual");
+                LIST_FOREACH_SAFE(subs, usub, next_usub, usubs->subs) {
+                        Subscription *sub = usub->sub;
+
+                        int r = sub->handle_unit_removed(sub->monitor, node->name, sub->unit, "virtual");
                         if (r < 0) {
                                 hirte_log_error("Failed to emit UnitRemoved signal");
                         }
                 }
         }
 
+        ProxyMonitor *proxy_monitor = NULL;
+        ProxyMonitor *next_proxy_monitor = NULL;
+        LIST_FOREACH_SAFE(monitors, proxy_monitor, next_proxy_monitor, node->proxy_monitors) {
+                node_remove_proxy_monitor(node, proxy_monitor);
+        }
+
         if (node->name) {
                 hirte_log_infof("Node '%s' disconnected", node->name);
         } else {
@@ -727,7 +908,8 @@ void agent_request_unref(AgentRequest *req) {
         free(req);
 }
 
-static AgentRequest *node_create_request(
+int node_create_request(
+                AgentRequest **ret,
                 Node *node,
                 const char *method,
                 agent_request_response_t cb,
@@ -735,7 +917,7 @@ static AgentRequest *node_create_request(
                 free_func_t free_userdata) {
         _cleanup_agent_request_ AgentRequest *req = malloc0(sizeof(AgentRequest));
         if (req == NULL) {
-                return NULL;
+                return -ENOMEM;
         }
 
         req->ref_count = 1;
@@ -755,11 +937,11 @@ static AgentRequest *node_create_request(
                         INTERNAL_AGENT_INTERFACE,
                         method);
         if (r < 0) {
-                hirte_log_errorf("Failed to create new bus message: %s", strerror(-r));
-                return NULL;
+                return r;
         }
 
-        return steal_pointer(&req);
+        *ret = steal_pointer(&req);
+        return 0;
 }
 
 static int agent_request_callback(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
@@ -768,7 +950,7 @@ static int agent_request_callback(sd_bus_message *m, void *userdata, UNUSED sd_b
         return req->cb(req, m, ret_error);
 }
 
-static bool agent_request_start(AgentRequest *req) {
+int agent_request_start(AgentRequest *req) {
         Node *node = req->node;
 
         int r = sd_bus_call_async(
@@ -779,12 +961,11 @@ static bool agent_request_start(AgentRequest *req) {
                         req,
                         HIRTE_DEFAULT_DBUS_TIMEOUT);
         if (r < 0) {
-                hirte_log_errorf("Failed to call async: %s", strerror(-r));
-                return false;
+                return r;
         }
 
         agent_request_ref(req); /* Keep alive while operation is outstanding */
-        return true;
+        return 1;
 }
 
 AgentRequest *node_request_list_units(
@@ -793,13 +974,13 @@ AgentRequest *node_request_list_units(
                 return false;
         }
 
-        _cleanup_agent_request_ AgentRequest *req = node_create_request(
-                        node, "ListUnits", cb, userdata, free_userdata);
+        _cleanup_agent_request_ AgentRequest *req = NULL;
+        node_create_request(&req, node, "ListUnits", cb, userdata, free_userdata);
         if (req == NULL) {
                 return false;
         }
 
-        if (!agent_request_start(req)) {
+        if (agent_request_start(req) < 0) {
                 return false;
         }
 
@@ -884,7 +1065,9 @@ static int node_method_get_unit_properties(sd_bus_message *m, void *userdata, UN
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal Error");
         }
 
-        _cleanup_agent_request_ AgentRequest *req = node_create_request(
+        _cleanup_agent_request_ AgentRequest *req = NULL;
+        node_create_request(
+                        &req,
                         node,
                         "GetUnitProperties",
                         node_method_get_unit_properties_callback,
@@ -899,7 +1082,7 @@ static int node_method_get_unit_properties(sd_bus_message *m, void *userdata, UN
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
         }
 
-        if (!agent_request_start(req)) {
+        if (agent_request_start(req) < 0) {
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
         }
 
@@ -944,7 +1127,9 @@ static int node_method_get_unit_property(sd_bus_message *m, void *userdata, UNUS
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal Error");
         }
 
-        _cleanup_agent_request_ AgentRequest *req = node_create_request(
+        _cleanup_agent_request_ AgentRequest *req = NULL;
+        node_create_request(
+                        &req,
                         node,
                         "GetUnitProperty",
                         node_method_get_unit_property_callback,
@@ -959,7 +1144,7 @@ static int node_method_get_unit_property(sd_bus_message *m, void *userdata, UNUS
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
         }
 
-        if (!agent_request_start(req)) {
+        if (agent_request_start(req) < 0) {
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
         }
 
@@ -998,7 +1183,9 @@ static int node_method_set_unit_properties(sd_bus_message *m, void *userdata, UN
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal Error");
         }
 
-        _cleanup_agent_request_ AgentRequest *req = node_create_request(
+        _cleanup_agent_request_ AgentRequest *req = NULL;
+        node_create_request(
+                        &req,
                         node,
                         "SetUnitProperties",
                         node_method_set_unit_properties_callback,
@@ -1018,7 +1205,7 @@ static int node_method_set_unit_properties(sd_bus_message *m, void *userdata, UN
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
         }
 
-        if (!agent_request_start(req)) {
+        if (agent_request_start(req) < 0) {
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
         }
 
@@ -1085,7 +1272,6 @@ static int unit_lifecycle_method_callback(AgentRequest *req, sd_bus_message *m,
 
 static int node_run_unit_lifecycle_method(
                 sd_bus_message *m, Node *node, const char *job_type, const char *method) {
-        _cleanup_agent_request_ AgentRequest *req = NULL;
         const char *unit = NULL;
         const char *mode = NULL;
 
@@ -1099,7 +1285,9 @@ static int node_run_unit_lifecycle_method(
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Out of memory");
         }
 
-        req = node_create_request(
+        _cleanup_agent_request_ AgentRequest *req = NULL;
+        node_create_request(
+                        &req,
                         node,
                         method,
                         unit_lifecycle_method_callback,
@@ -1114,7 +1302,7 @@ static int node_run_unit_lifecycle_method(
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
         }
 
-        if (!agent_request_start(req)) {
+        if (agent_request_start(req) < 0) {
                 return sd_bus_reply_method_errorf(m, SD_BUS_ERROR_FAILED, "Internal error");
         }
 
@@ -1245,16 +1433,16 @@ void node_subscribe(Node *node, Subscription *sub) {
 
         LIST_APPEND(subs, usubs->subs, steal_pointer(&usub));
 
-        /* We know this is loaded, so we won't get virtual notify from
+        /* We know this is loaded, so we won't get notified from
            the agent, instead send a virtual event here. */
         if (usubs->loaded) {
-                int r = monitor_emit_unit_new(sub->monitor, node->name, sub->unit, "virtual");
+                int r = sub->handle_unit_new(sub->monitor, node->name, sub->unit, "virtual");
                 if (r < 0) {
                         hirte_log_error("Failed to emit UnitNew signal");
                 }
 
                 if (usubs->active_state >= 0) {
-                        r = monitor_emit_unit_state_changed(
+                        r = sub->handle_unit_state_changed(
                                         sub->monitor,
                                         node->name,
                                         sub->unit,
@@ -1307,3 +1495,100 @@ void node_unsubscribe(Node *node, Subscription *sub) {
                 }
         }
 }
+
+static void node_start_proxy_dependency(Node *node, ProxyDependency *dep) {
+        if (!node_has_agent(node)) {
+                return;
+        }
+
+        hirte_log_infof("Starting dependency %s on node %s", dep->unit_name, node->name);
+
+        int r = send_agent_simple_message(node, "StartDep", dep->unit_name);
+        if (r < 0) {
+                hirte_log_error("Failed to send StartDep to agent");
+        }
+}
+
+static void node_start_proxy_dependency_all(Node *node) {
+        ProxyDependency *dep = NULL;
+        LIST_FOREACH(deps, dep, node->proxy_dependencies) {
+                node_start_proxy_dependency(node, dep);
+        }
+}
+
+static void node_stop_proxy_dependency(Node *node, ProxyDependency *dep) {
+        if (!node_has_agent(node)) {
+                return;
+        }
+
+        hirte_log_infof("Stopping dependency %s on node %s", dep->unit_name, node->name);
+
+        int r = send_agent_simple_message(node, "StopDep", dep->unit_name);
+        if (r < 0) {
+                hirte_log_error("Failed to send StopDep to agent");
+        }
+}
+
+static struct ProxyDependency *node_find_proxy_dependency(Node *node, const char *unit_name) {
+        ProxyDependency *dep = NULL;
+        LIST_FOREACH(deps, dep, node->proxy_dependencies) {
+                if (streq(dep->unit_name, unit_name)) {
+                        return dep;
+                }
+        }
+
+        return NULL;
+}
+
+int node_add_proxy_dependency(Node *node, const char *unit_name) {
+        ProxyDependency *dep = NULL;
+
+        dep = node_find_proxy_dependency(node, unit_name);
+        if (dep) {
+                dep->n_deps++;
+                /* Always start, if the dep service was stopped by
+                   the target service stopping */
+                node_start_proxy_dependency(node, dep);
+                return 0;
+        }
+
+        _cleanup_free_ char *unit_name_copy = strdup(unit_name);
+        if (unit_name_copy == NULL) {
+                return -ENOMEM;
+        }
+
+        dep = malloc0(sizeof(ProxyDependency));
+        if (dep == NULL) {
+                return -ENOMEM;
+        }
+
+        dep->unit_name = steal_pointer(&unit_name_copy);
+        dep->n_deps = 1;
+        LIST_APPEND(deps, node->proxy_dependencies, dep);
+
+        node_start_proxy_dependency(node, dep);
+
+        return 0;
+}
+
+int node_remove_proxy_dependency(Node *node, const char *unit_name) {
+        ProxyDependency *dep = NULL;
+        _cleanup_free_ char *unit_name_copy = NULL;
+
+        dep = node_find_proxy_dependency(node, unit_name);
+        if (!dep) {
+                return -ENOENT;
+        }
+
+        dep->n_deps--;
+
+        if (dep->n_deps == 0) {
+                /* Only stop on the last dep */
+                node_stop_proxy_dependency(node, dep);
+
+                LIST_REMOVE(deps, node->proxy_dependencies, dep);
+                proxy_dependency_free(dep);
+        }
+
+        return 0;
+}
diff --git a/src/manager/node.h b/src/manager/node.h
index a271047bd4..720f3fa664 100644
--- a/src/manager/node.h
+++ b/src/manager/node.h
@@ -26,6 +26,8 @@ struct AgentRequest {
 AgentRequest *agent_request_ref(AgentRequest *req);
 void agent_request_unref(AgentRequest *req);
 
+int agent_request_start(AgentRequest *req);
+
 struct Node {
         int ref_count;
 
@@ -41,10 +43,12 @@ struct Node {
 
         LIST_FIELDS(Node, nodes);
 
-        char *name; /* NULL for not yet unregistered nodes */
+        char *name; /* NULL for not yet registered nodes */
         char *object_path;
 
         LIST_HEAD(AgentRequest, outstanding_requests);
+        LIST_HEAD(ProxyMonitor, proxy_monitors);
+        LIST_HEAD(ProxyDependency, proxy_dependencies);
 
         struct hashmap *unit_subscriptions;
 };
@@ -67,6 +71,19 @@ AgentRequest *node_request_list_units(
 void node_subscribe(Node *node, Subscription *sub);
 void node_unsubscribe(Node *node, Subscription *sub);
 
+int node_add_proxy_dependency(Node *node, const char *unit_name);
+int node_remove_proxy_dependency(Node *node, const char *unit_name);
+
+int node_create_request(
+                AgentRequest **ret,
+                Node *node,
+                const char *method,
+                agent_request_response_t cb,
+                void *userdata,
+                free_func_t free_userdata);
+
+void node_remove_proxy_monitor(Node *node, ProxyMonitor *proxy_monitor);
+
 DEFINE_CLEANUP_FUNC(Node, node_unref)
 #define _cleanup_node_ _cleanup_(node_unrefp)
 DEFINE_CLEANUP_FUNC(AgentRequest, agent_request_unref)
diff --git a/src/manager/proxy_monitor.c b/src/manager/proxy_monitor.c
new file mode 100644
index 0000000000..902a34f4f2
--- /dev/null
+++ b/src/manager/proxy_monitor.c
@@ -0,0 +1,301 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#include "libhirte/log/log.h"
+
+#include "job.h"
+#include "manager.h"
+#include "monitor.h"
+#include "node.h"
+#include "proxy_monitor.h"
+
+Subscription *create_proxy_monitor_subscription(ProxyMonitor *monitor, const char *node, const char *unit) {
+        Subscription *subscription = subscription_new(node, unit);
+        if (subscription == NULL) {
+                return NULL;
+        }
+
+        subscription->monitor = monitor; /* Weak ref to avoid circular dep */
+        subscription->free_monitor = NULL;
+
+        subscription->handle_unit_new = proxy_monitor_on_unit_new;
+        subscription->handle_unit_removed = proxy_monitor_on_unit_removed;
+        subscription->handle_unit_state_changed = proxy_monitor_on_unit_state_changed;
+        subscription->handle_unit_property_changed = proxy_monitor_on_unit_property_changed;
+
+        return subscription;
+}
+
+ProxyMonitor *proxy_monitor_new(
+                Node *node, const char *target_node_name, const char *unit_name, const char *proxy_object_path) {
+        _cleanup_proxy_monitor_ ProxyMonitor *monitor = malloc0(sizeof(ProxyMonitor));
+        if (monitor == NULL) {
+                return NULL;
+        }
+
+        monitor->ref_count = 1;
+        monitor->node = node;
+
+        monitor->unit_name = strdup(unit_name);
+        if (monitor->unit_name == NULL) {
+                return NULL;
+        }
+
+        monitor->proxy_object_path = strdup(proxy_object_path);
+        if (monitor->proxy_object_path == NULL) {
+                return NULL;
+        }
+
+        monitor->subscription = create_proxy_monitor_subscription(monitor, target_node_name, unit_name);
+        if (monitor->subscription == NULL) {
+                return NULL;
+        }
+
+        return steal_pointer(&monitor);
+}
+
+ProxyMonitor *proxy_monitor_ref(ProxyMonitor *monitor) {
+        monitor->ref_count++;
+        return monitor;
+}
+
+static void proxy_monitor_drop_dep(ProxyMonitor *monitor);
+
+void proxy_monitor_unref(ProxyMonitor *monitor) {
+        monitor->ref_count--;
+        if (monitor->ref_count != 0) {
+                return;
+        }
+
+        if (monitor->target_node) {
+                proxy_monitor_drop_dep(monitor);
+                node_unref(monitor->target_node);
+        }
+
+        if (monitor->subscription) {
+                subscription_unref(monitor->subscription);
+        }
+
+        free(monitor->unit_name);
+        free(monitor->proxy_object_path);
+        free(monitor);
+}
+
+int proxy_monitor_set_target_node(ProxyMonitor *monitor, Node *target_node) {
+        monitor->target_node = node_ref(target_node);
+
+        int res = node_add_proxy_dependency(target_node, monitor->unit_name);
+        if (res < 0) {
+                return res;
+        }
+        monitor->added_dep = true;
+        return 0;
+}
+
+static void proxy_monitor_drop_dep(ProxyMonitor *monitor) {
+        if (monitor->added_dep) {
+                int res = node_remove_proxy_dependency(monitor->target_node, monitor->unit_name);
+                if (res < 0) {
+                        hirte_log_error("Failed to remove proxy dependency");
+                }
+                monitor->added_dep = false;
+        }
+}
+
+void proxy_monitor_close(ProxyMonitor *monitor) {
+        proxy_monitor_drop_dep(monitor);
+}
+
+int proxy_monitor_on_unit_new(
+                UNUSED void *userdata, UNUSED const char *node, UNUSED const char *unit, const char *reason) {
+        ProxyMonitor *monitor = (ProxyMonitor *) userdata;
+        hirte_log_debugf("ProxyMonitor processing unit new %s %s %s", node, unit, reason);
+        proxy_monitor_send_new(monitor, reason);
+        return 0;
+}
+
+int proxy_monitor_on_unit_state_changed(
+                UNUSED void *userdata,
+                UNUSED const char *node,
+                UNUSED const char *unit,
+                const char *active_state,
+                const char *substate,
+                const char *reason) {
+        ProxyMonitor *monitor = (ProxyMonitor *) userdata;
+
+        hirte_log_debugf(
+                        "ProxyMonitor processing unit state changed %s %s %s %s %s",
+                        node,
+                        unit,
+                        active_state,
+                        substate,
+                        reason);
+        proxy_monitor_send_state_changed(monitor, active_state, substate, reason);
+
+        return 0;
+}
+
+int proxy_monitor_on_unit_removed(
+                UNUSED void *userdata, UNUSED const char *node, UNUSED const char *unit, const char *reason) {
+        ProxyMonitor *monitor = (ProxyMonitor *) userdata;
+        hirte_log_debugf("ProxyMonitor processing unit removed %s %s %s", node, unit, reason);
+        proxy_monitor_send_removed(monitor, reason);
+        return 0;
+}
+
+int proxy_monitor_on_unit_property_changed(
+                UNUSED void *userdata,
+                UNUSED const char *node,
+                UNUSED const char *unit,
+                UNUSED const char *interface,
+                UNUSED sd_bus_message *m) {
+        return 0;
+}
+
+static int proxy_monitor_send_error_callback(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        _cleanup_proxy_monitor_ ProxyMonitor *monitor = userdata;
+
+        if (sd_bus_message_is_method_error(m, NULL)) {
+                hirte_log_errorf(
+                                "Failed to send proxy error on node %s with object path %s: %s",
+                                monitor->node->name,
+                                monitor->proxy_object_path,
+                                sd_bus_message_get_error(m)->message);
+        }
+        return 0;
+}
+
+void proxy_monitor_send_error(ProxyMonitor *monitor, const char *message) {
+        int r = sd_bus_call_method_async(
+                        monitor->node->agent_bus,
+                        NULL,
+                        HIRTE_INTERFACE_BASE_NAME,
+                        monitor->proxy_object_path,
+                        INTERNAL_PROXY_INTERFACE,
+                        "Error",
+                        proxy_monitor_send_error_callback,
+                        proxy_monitor_ref(monitor),
+                        "s",
+                        message);
+        if (r < 0) {
+                hirte_log_errorf(
+                                "Failed to call Error on node %s object %s with message '%s': %s",
+                                monitor->node->name,
+                                monitor->proxy_object_path,
+                                message,
+                                strerror(-r));
+        }
+}
+
+static int proxy_monitor_send_state_changed_callback(
+                sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        _cleanup_proxy_monitor_ ProxyMonitor *monitor = userdata;
+
+        if (sd_bus_message_is_method_error(m, NULL)) {
+                hirte_log_errorf(
+                                "Failed to send proxy state change node %s with object path %s: %s",
+                                monitor->node->name,
+                                monitor->proxy_object_path,
+                                sd_bus_message_get_error(m)->message);
+        }
+        return 0;
+}
+
+int proxy_monitor_send_state_changed(
+                ProxyMonitor *monitor, const char *active_state, const char *substate, const char *reason) {
+        _cleanup_sd_bus_error_ sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_sd_bus_message_ sd_bus_message *message = NULL;
+        int r = sd_bus_call_method_async(
+                        monitor->node->agent_bus,
+                        NULL,
+                        HIRTE_INTERFACE_BASE_NAME,
+                        monitor->proxy_object_path,
+                        INTERNAL_PROXY_INTERFACE,
+                        "TargetStateChanged",
+                        proxy_monitor_send_state_changed_callback,
+                        proxy_monitor_ref(monitor),
+                        "sss",
+                        active_state,
+                        substate,
+                        reason);
+        if (r < 0) {
+                hirte_log_errorf(
+                                "Failed to send proxy state changed on node %s object %s: %s",
+                                monitor->node->name,
+                                monitor->proxy_object_path,
+                                error.message);
+        }
+        return r;
+}
+
+static int proxy_monitor_send_new_callback(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        _cleanup_proxy_monitor_ ProxyMonitor *monitor = userdata;
+
+        if (sd_bus_message_is_method_error(m, NULL)) {
+                hirte_log_errorf(
+                                "Failed to send proxy new %s with object path %s: %s",
+                                monitor->node->name,
+                                monitor->proxy_object_path,
+                                sd_bus_message_get_error(m)->message);
+        }
+        return 0;
+}
+
+int proxy_monitor_send_new(ProxyMonitor *monitor, const char *reason) {
+        _cleanup_sd_bus_error_ sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_sd_bus_message_ sd_bus_message *message = NULL;
+        int r = sd_bus_call_method_async(
+                        monitor->node->agent_bus,
+                        NULL,
+                        HIRTE_INTERFACE_BASE_NAME,
+                        monitor->proxy_object_path,
+                        INTERNAL_PROXY_INTERFACE,
+                        "TargetNew",
+                        proxy_monitor_send_new_callback,
+                        proxy_monitor_ref(monitor),
+                        "s",
+                        reason);
+        if (r < 0) {
+                hirte_log_errorf(
+                                "Failed to send proxy new on node %s object %s: %s",
+                                monitor->node->name,
+                                monitor->proxy_object_path,
+                                error.message);
+        }
+        return r;
+}
+
+static int proxy_monitor_send_removed_callback(sd_bus_message *m, void *userdata, UNUSED sd_bus_error *ret_error) {
+        _cleanup_proxy_monitor_ ProxyMonitor *monitor = userdata;
+
+        if (sd_bus_message_is_method_error(m, NULL)) {
+                hirte_log_errorf(
+                                "Failed to send proxy removed %s with object path %s: %s",
+                                monitor->node->name,
+                                monitor->proxy_object_path,
+                                sd_bus_message_get_error(m)->message);
+        }
+        return 0;
+}
+
+int proxy_monitor_send_removed(ProxyMonitor *monitor, const char *reason) {
+        _cleanup_sd_bus_error_ sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_sd_bus_message_ sd_bus_message *message = NULL;
+        int r = sd_bus_call_method_async(
+                        monitor->node->agent_bus,
+                        NULL,
+                        HIRTE_INTERFACE_BASE_NAME,
+                        monitor->proxy_object_path,
+                        INTERNAL_PROXY_INTERFACE,
+                        "TargetRemoved",
+                        proxy_monitor_send_removed_callback,
+                        proxy_monitor_ref(monitor),
+                        "s",
+                        reason);
+        if (r < 0) {
+                hirte_log_errorf(
+                                "Failed to send proxy removed on node %s object %s: %s",
+                                monitor->node->name,
+                                monitor->proxy_object_path,
+                                error.message);
+        }
+        return r;
+}
diff --git a/src/manager/proxy_monitor.h b/src/manager/proxy_monitor.h
new file mode 100644
index 0000000000..515ccb2b82
--- /dev/null
+++ b/src/manager/proxy_monitor.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include "libhirte/common/common.h"
+
+#include "types.h"
+
+struct ProxyMonitor {
+        int ref_count;
+
+        Node *node; /* weak ref */
+        Subscription *subscription;
+
+        bool added_dep;
+
+        char *unit_name;
+        Node *target_node;
+
+        char *proxy_object_path;
+
+        LIST_FIELDS(ProxyMonitor, monitors);
+};
+
+Subscription *create_proxy_monitor_subscription(ProxyMonitor *monitor, const char *node, const char *unit);
+
+ProxyMonitor *proxy_monitor_new(
+                Node *node, const char *target_node_name, const char *unit_name, const char *proxy_object_path);
+ProxyMonitor *proxy_monitor_ref(ProxyMonitor *monitor);
+void proxy_monitor_unref(ProxyMonitor *monitor);
+
+int proxy_monitor_set_target_node(ProxyMonitor *monitor, Node *target_node);
+
+void proxy_monitor_close(ProxyMonitor *monitor);
+
+int proxy_monitor_on_unit_new(void *userdata, const char *node, const char *unit, const char *reason);
+int proxy_monitor_on_unit_state_changed(
+                void *userdata,
+                const char *node,
+                const char *unit,
+                const char *active_state,
+                const char *substate,
+                const char *reason);
+int proxy_monitor_on_unit_removed(void *userdata, const char *node, const char *unit, const char *reason);
+int proxy_monitor_on_unit_property_changed(
+                void *userdata, const char *node, const char *unit, const char *interface, sd_bus_message *m);
+
+DEFINE_CLEANUP_FUNC(ProxyMonitor, proxy_monitor_unref)
+#define _cleanup_proxy_monitor_ _cleanup_(proxy_monitor_unrefp)
+
+void proxy_monitor_send_error(ProxyMonitor *monitor, const char *message);
+int proxy_monitor_send_new(ProxyMonitor *monitor, const char *reason);
+int proxy_monitor_send_removed(ProxyMonitor *monitor, const char *reason);
+int proxy_monitor_send_state_changed(
+                ProxyMonitor *monitor, const char *active_state, const char *substate, const char *reason);
+int proxy_monitor_start_dep_unit(ProxyMonitor *monitor);
+int proxy_monitor_stop_dep_unit(ProxyMonitor *monitor);
diff --git a/src/manager/types.h b/src/manager/types.h
index 4eedce74a5..e3285a8a01 100644
--- a/src/manager/types.h
+++ b/src/manager/types.h
@@ -6,4 +6,6 @@ typedef struct Node Node;
 typedef struct AgentRequest AgentRequest;
 typedef struct Job Job;
 typedef struct Monitor Monitor;
+typedef struct ProxyMonitor ProxyMonitor;
 typedef struct Subscription Subscription;
+typedef struct ProxyDependency ProxyDependency;
diff --git a/src/proxy/main.c b/src/proxy/main.c
new file mode 100644
index 0000000000..32e20d70c6
--- /dev/null
+++ b/src/proxy/main.c
@@ -0,0 +1,185 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#include <errno.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "libhirte/bus/bus.h"
+#include "libhirte/common/common.h"
+#include "libhirte/common/opt.h"
+#include "libhirte/log/log.h"
+
+const struct option options[] = { { ARG_USER, no_argument, 0, ARG_USER_SHORT },
+                                  { ARG_HELP, no_argument, 0, ARG_HELP_SHORT },
+                                  { NULL, 0, 0, '\0' } };
+
+#define OPTIONS_STR ARG_HELP_SHORT_S ARG_USER_SHORT_S
+
+static const char *opt_node_unit = NULL;
+static const char *opt_operation = NULL;
+static bool opt_user = false;
+
+#ifdef USE_USER_API_BUS
+#        define ALWAYS_USER_API_BUS 1
+#else
+#        define ALWAYS_USER_API_BUS 0
+#endif
+
+#define OPT_OPERATION_CREATE "create"
+#define OPT_OPERATION_REMOVE "remove"
+
+static void usage(char *argv[]) {
+        printf("Usage: %s \n"
+               "\t[-h help]\t\t Print this help message.\n"
+               "\t<operation>\t\t Required. Operation to perform. Must be one of [%s,%s].\n"
+               "\t<node>_<service>\t Required. Input the name of the node the required service is supposed to run on.\n",
+               argv[0],
+               OPT_OPERATION_CREATE,
+               OPT_OPERATION_REMOVE);
+}
+
+int parse_node_unit_opt(const char *opt_node_unit, UNUSED char **ret_node_name, UNUSED char **ret_unit_name) {
+        if (opt_node_unit == NULL || ret_node_name == NULL || ret_unit_name == NULL) {
+                return -EINVAL;
+        }
+
+        char *split = strchr(opt_node_unit, '_');
+        if (split == NULL || split == opt_node_unit) {
+                fprintf(stderr, "No underscore in unit name '%s'\n", opt_node_unit);
+                return -EINVAL;
+        }
+        if (split[1] == 0) {
+                fprintf(stderr, "No service specified in unit name '%s'\n", opt_node_unit);
+                return -EINVAL;
+        }
+
+        *ret_node_name = strndup(opt_node_unit, split - opt_node_unit);
+        *ret_unit_name = strdup(split + 1);
+
+        if (*ret_node_name == NULL || *ret_unit_name == NULL) {
+                fprintf(stderr, "Out of memory\n");
+                return -ENOMEM;
+        }
+
+        return 0;
+}
+
+static int get_opts(int argc, char *argv[]) {
+        int opt = 0;
+        while ((opt = getopt_long(argc, argv, OPTIONS_STR, options, NULL)) != -1) {
+                switch (opt) {
+                case ARG_HELP_SHORT:
+                        usage(argv);
+                        return 0;
+                case ARG_USER_SHORT:
+                        opt_user = true;
+                        break;
+                default:
+                        fprintf(stderr, "Unsupported option %c\n", opt);
+                        usage(argv);
+                        return -EINVAL;
+                }
+        }
+
+        if (argc < 3) {
+                fprintf(stderr, "Not enough arguments specified\n");
+                return -EINVAL;
+        }
+        opt_operation = argv[optind];
+        opt_node_unit = argv[optind + 1];
+
+        if (!streqi(opt_operation, OPT_OPERATION_CREATE) && !streqi(opt_operation, OPT_OPERATION_REMOVE)) {
+                fprintf(stderr, "Unsupported command %s\n", opt_operation);
+                return -EINVAL;
+        }
+
+        return 0;
+}
+
+
+static int call_proxy_method(
+                sd_bus *api_bus,
+                const char *method,
+                const char *service_name,
+                const char *node_name,
+                const char *unit_name) {
+        _cleanup_sd_bus_error_ sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_sd_bus_message_ sd_bus_message *message = NULL;
+        _cleanup_free_ char *proxy_service = strcat_dup("hirte-proxy@", service_name);
+
+        int r = sd_bus_call_method(
+                        api_bus,
+                        HIRTE_AGENT_DBUS_NAME,
+                        HIRTE_AGENT_OBJECT_PATH,
+                        AGENT_INTERFACE,
+                        method,
+                        &error,
+                        &message,
+                        "sss",
+                        proxy_service,
+                        node_name,
+                        unit_name);
+        if (r < 0) {
+                if (streq(error.name, HIRTE_BUS_ERROR_ACTIVATION_FAILED)) {
+                        fprintf(stderr,
+                                "Failed to activate %s (on %s): %s\n",
+                                proxy_service,
+                                node_name,
+                                error.message);
+                } else {
+                        fprintf(stderr, "Failed to call %s: %s\n", method, error.message);
+                }
+                return r;
+        }
+        return 0;
+}
+
+int main(int argc, char *argv[]) {
+        hirte_log_set_quiet(false);
+        hirte_log_set_level(LOG_LEVEL_INFO);
+        hirte_log_set_log_fn(hirte_log_to_stderr_with_location);
+
+        int r = get_opts(argc, argv);
+        if (r < 0) {
+                return EXIT_FAILURE;
+        }
+
+        _cleanup_free_ char *node_name = NULL;
+        _cleanup_free_ char *unit_name = NULL;
+        parse_node_unit_opt(opt_node_unit, &node_name, &unit_name);
+
+        _cleanup_sd_event_ sd_event *event = NULL;
+        r = sd_event_default(&event);
+        if (r < 0) {
+                fprintf(stderr, "Failed to create event loop: %s", strerror(-r));
+                return EXIT_FAILURE;
+        }
+
+        _cleanup_sd_bus_ sd_bus *api_bus = NULL;
+        if (opt_user || ALWAYS_USER_API_BUS) {
+                api_bus = user_bus_open(event);
+        } else {
+                api_bus = system_bus_open(event);
+        }
+        if (api_bus == NULL) {
+                hirte_log_error("Failed to create api bus");
+                return EXIT_FAILURE;
+        }
+
+        /* Don't time out in the proxy calls, we want to wait until the target service is running */
+        sd_bus_set_method_call_timeout(api_bus, UINT64_MAX);
+
+        /* lint complains due to passing NULL. check for NULL done in other func. Disabling it. */
+        // NOLINTNEXTLINE
+        if (streqi(opt_operation, OPT_OPERATION_CREATE)) {
+                r = call_proxy_method(api_bus, "CreateProxy", opt_node_unit, node_name, unit_name);
+        } else if (streqi(opt_operation, OPT_OPERATION_REMOVE)) {
+                r = call_proxy_method(api_bus, "RemoveProxy", opt_node_unit, node_name, unit_name);
+        }
+
+        if (r < 0) {
+                return EXIT_FAILURE;
+        }
+        return EXIT_SUCCESS;
+}
diff --git a/src/proxy/meson.build b/src/proxy/meson.build
new file mode 100644
index 0000000000..e4c8062c82
--- /dev/null
+++ b/src/proxy/meson.build
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# proxy build configuration
+
+proxy_src = [
+  'main.c',
+]
+
+executable(
+  'hirte-proxy',
+  proxy_src,
+  dependencies: [
+    systemd_dep,
+  ],
+  link_with: [
+    hirte_lib,
+  ],
+  c_args: common_cflags,
+  include_directories: include_directories('..'),
+  install: true
+)
\ No newline at end of file
diff --git a/systemd-units/hirte-dep@.service b/systemd-units/hirte-dep@.service
new file mode 100644
index 0000000000..3bb2f7047e
--- /dev/null
+++ b/systemd-units/hirte-dep@.service
@@ -0,0 +1,14 @@
+#  SPDX-License-Identifier: GPL-2.0-or-later
+#
+#  This file is part of hirte-agent.
+#
+[Unit]
+Description=Hirte proxy dependency service
+Documentation=man:hirte-proxy(1)
+BindsTo=%i.service
+After=%i.service
+
+[Service]
+ExecStart=true
+Type=oneshot
+RemainAfterExit=yes
diff --git a/systemd-units/hirte-proxy-user@.service b/systemd-units/hirte-proxy-user@.service
new file mode 100644
index 0000000000..78f52af546
--- /dev/null
+++ b/systemd-units/hirte-proxy-user@.service
@@ -0,0 +1,15 @@
+#  SPDX-License-Identifier: GPL-2.0-or-later
+#
+#  This file is part of hirte-agent.
+#
+[Unit]
+Description=Hirte proxy service
+Documentation=man:hirte-proxy(1)
+StopWhenUnneeded=yes
+
+[Service]
+ExecStart=hirte-proxy --user create %i.service
+ExecStop=hirte-proxy --user remove %i.service
+RemainAfterExit=yes
+Type=oneshot
+KillMode=mixed
diff --git a/systemd-units/hirte-proxy@.service b/systemd-units/hirte-proxy@.service
new file mode 100644
index 0000000000..ee469952e1
--- /dev/null
+++ b/systemd-units/hirte-proxy@.service
@@ -0,0 +1,15 @@
+#  SPDX-License-Identifier: GPL-2.0-or-later
+#
+#  This file is part of hirte-agent.
+#
+[Unit]
+Description=Hirte proxy service
+Documentation=man:hirte-proxy(1)
+StopWhenUnneeded=yes
+
+[Service]
+ExecStart=hirte-proxy create %i.service
+ExecStop=hirte-proxy remove %i.service
+RemainAfterExit=yes
+Type=oneshot
+KillMode=mixed
diff --git a/systemd-units/meson.build b/systemd-units/meson.build
index f4ca3a7774..fcfdf026f7 100644
--- a/systemd-units/meson.build
+++ b/systemd-units/meson.build
@@ -4,22 +4,12 @@ systemd_unit_dir = join_paths(get_option('prefix'), 'lib', 'systemd', 'system')
 systemd_user_unit_dir = join_paths(get_option('prefix'), 'lib', 'systemd', 'user')
 
 install_data(
-  'hirte.service',
+  ['hirte.service', 'hirte-agent.service', 'hirte.socket', 'hirte-proxy@.service', 'hirte-dep@.service'],
   install_dir : systemd_unit_dir
 )
 
 install_data(
-  'hirte.socket',
-  install_dir : systemd_unit_dir
-)
-
-install_data(
-  'hirte-agent.service',
-  install_dir : systemd_unit_dir
-)
-
-install_data(
-  ['hirte-agent-user.service'],
-  rename  : ['hirte-agent.service'],
+  ['hirte-agent-user.service', 'hirte-proxy-user@.service', 'hirte-dep@.service'],
+  rename  : ['hirte-agent.service', 'hirte-proxy@.service', 'hirte-dep@.service'],
   install_dir : systemd_user_unit_dir
 )


Test patch:
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index ee115f3df8..e7841e0a3e 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -70,7 +70,7 @@ jobs:
           podman exec -d hirte-node-bar \
                bash -c "systemctl start hirte-agent.service"
           podman exec -it hirte-node-bar \
-               bash -c "journalctl -b | grep \"Agent 'bar' connection attempt 0: success\""
+               bash -c "journalctl -b | grep \"Connected to manager as 'bar'\""
 
       - name: run hirte agent-foo
         run: |
@@ -86,7 +86,7 @@ jobs:
           podman exec -d hirte-node-foo \
                bash -c "systemctl start hirte-agent.service"
           podman exec -it hirte-node-foo \
-               bash -c "journalctl -b | grep \"Agent 'foo' connection attempt 0: success\""
+               bash -c "journalctl -b | grep \"Connected to manager as 'foo'\""
 
       - name: Download logs from containers
         if: always()