diff --git a/Documentation/netlink/specs/ultraeth.yaml b/Documentation/netlink/specs/ultraeth.yaml
index 55ab4d9b82a9..e95c73a36892 100644
--- a/Documentation/netlink/specs/ultraeth.yaml
+++ b/Documentation/netlink/specs/ultraeth.yaml
@@ -24,6 +24,119 @@ attribute-sets:
         type: nest
         nested-attributes: context
         multi-attr: true
+  -
+    name: fep-in-addr
+    attributes:
+      -
+        name: ip
+        type: binary
+        display-hint: ipv4
+      -
+        name: ip6
+        type: binary
+        byte-order: big-endian
+        display-hint: ipv6
+      -
+        name: family
+        type: u16
+  -
+    name: fep-address
+    attributes:
+      -
+        name: in-address
+        type: nest
+        nested-attributes: fep-in-addr
+      -
+        name: flags
+        type: u16
+      -
+        name: caps
+        type: u16
+      -
+        name: start-resource-index
+        type: u16
+      -
+        name: num-resource-indices
+        type: u16
+      -
+        name: initiator-id
+        type: u32
+      -
+        name: pid-on-fep
+        type: u16
+      -
+        name: padding
+        type: u16
+      -
+        name: version
+        type: u8
+  -
+    name: fep-entry
+    attributes:
+      -
+        name: address
+        type: nest
+        nested-attributes: fep-address
+  -
+    name: flist
+    attributes:
+      -
+        name: fep
+        type: nest
+        multi-attr: true
+        nested-attributes: fep-entry
+  -
+    name: job-req
+    attributes:
+      -
+        name: context-id
+        type: s32
+      -
+        name: id
+        type : u32
+      -
+        name: address
+        type: nest
+        nested-attributes: fep-address
+      -
+        name: service-name
+        type: string
+  -
+    name: job
+    attributes:
+      -
+        name: id
+        type : u32
+      -
+        name: address
+        type: nest
+        nested-attributes: fep-address
+      -
+        name: service-name
+        type: string
+      -
+        name: flist
+        type: nest
+        nested-attributes: flist
+        multi-attr: true
+  -
+    name: jlist
+    attributes:
+      -
+        name: job
+        type: nest
+        nested-attributes: job
+        multi-attr: true
+  -
+    name: jobs
+    attributes:
+      -
+        name: context-id
+        type: s32
+      -
+        name: jlist
+        type: nest
+        nested-attributes: jlist
 
 operations:
   name-prefix: ultraeth-cmd-
@@ -54,3 +167,37 @@ operations:
         request:
           attributes:
             - id
+    -
+      name: job-get
+      doc: dump uecon context jobs
+      attribute-set: jobs
+      dump:
+        request:
+          attributes:
+            - context-id
+        reply:
+          attributes:
+            - context-id
+            - jlist
+    -
+      name: job-new
+      doc: add a new job to uecon context
+      attribute-set: job-req
+      flags: [ admin-perm ]
+      do:
+        request:
+          attributes:
+            - context-id
+            - id
+            - address
+            - service-name
+    -
+      name: job-del
+      doc: delete a job in uecon context
+      attribute-set: job-req
+      flags: [ admin-perm ]
+      do:
+        request:
+          attributes:
+            - context-id
+            - id
diff --git a/drivers/ultraeth/Makefile b/drivers/ultraeth/Makefile
index 599d91d205c1..bf41a62273f9 100644
--- a/drivers/ultraeth/Makefile
+++ b/drivers/ultraeth/Makefile
@@ -1,3 +1,3 @@
 obj-$(CONFIG_ULTRAETH) += ultraeth.o
 
-ultraeth-objs := uet_main.o uet_context.o uet_netlink.o
+ultraeth-objs := uet_main.o uet_context.o uet_netlink.o uet_job.o
diff --git a/drivers/ultraeth/uet_context.c b/drivers/ultraeth/uet_context.c
index 2444fa3f35cd..3d738c02e992 100644
--- a/drivers/ultraeth/uet_context.c
+++ b/drivers/ultraeth/uet_context.c
@@ -102,10 +102,16 @@ int uet_context_create(int id)
 		goto ctx_id_err;
 	}
 
+	err = uet_jobs_init(&ctx->job_reg);
+	if (err)
+		goto ctx_jobs_err;
+
 	uet_context_link(ctx);
 
 	return 0;
 
+ctx_jobs_err:
+	uet_context_put_id(ctx);
 ctx_id_err:
 	kfree(ctx);
 
@@ -115,6 +121,7 @@ int uet_context_create(int id)
 static void __uet_context_destroy(struct uet_context *ctx)
 {
 	uet_context_unlink(ctx);
+	uet_jobs_uninit(&ctx->job_reg);
 	uet_context_put_id(ctx);
 	kfree(ctx);
 }
diff --git a/drivers/ultraeth/uet_job.c b/drivers/ultraeth/uet_job.c
new file mode 100644
index 000000000000..3a55a0f70749
--- /dev/null
+++ b/drivers/ultraeth/uet_job.c
@@ -0,0 +1,455 @@
+// SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause)
+
+#include <linux/kernel.h>
+#include <linux/slab.h>
+#include <linux/bug.h>
+#include <net/ipv6.h>
+#include <net/ultraeth/uet_context.h>
+
+#include "uet_netlink.h"
+
+static const struct rhashtable_params uet_job_registry_rht_params = {
+	.head_offset = offsetof(struct uet_job, rht_node),
+	.key_offset = offsetof(struct uet_job, id),
+	.key_len = sizeof(u32),
+	.nelem_hint = 128,
+	.automatic_shrinking = true,
+};
+
+int uet_jobs_init(struct uet_job_registry *jreg)
+{
+	int ret;
+
+	mutex_init(&jreg->jobs_lock);
+
+	ret = rhashtable_init(&jreg->jobs_hash, &uet_job_registry_rht_params);
+	if (ret)
+		mutex_destroy(&jreg->jobs_lock);
+
+	return ret;
+}
+
+static int __job_associate(struct uet_job *job, struct uet_fep *fep)
+{
+	lockdep_assert_held_once(&job->jreg->jobs_lock);
+
+	if (rcu_access_pointer(job->fep))
+		return -EBUSY;
+
+	WRITE_ONCE(fep->job_id, job->id);
+	rcu_assign_pointer(job->fep, fep);
+
+	return 0;
+}
+
+/* disassociate and close all PDCs related to the job */
+static void __job_disassociate(struct uet_job *job)
+{
+	struct uet_fep *fep;
+
+	fep = rcu_dereference_check(job->fep,
+				    lockdep_is_held(&job->jreg->jobs_lock));
+	if (!fep)
+		return;
+
+	WRITE_ONCE(fep->job_id, 0);
+	RCU_INIT_POINTER(job->fep, NULL);
+	synchronize_rcu();
+}
+
+struct uet_job *uet_job_find(struct uet_job_registry *jreg, u32 id)
+{
+	return rhashtable_lookup_fast(&jreg->jobs_hash, &id,
+				      uet_job_registry_rht_params);
+}
+
+static struct uet_job *uet_job_find_svc_name(struct uet_job_registry *jreg,
+					     char *service_name)
+{
+	struct uet_job *job;
+
+	lockdep_assert_held_once(&jreg->jobs_lock);
+
+	hlist_for_each_entry(job, &jreg->jobs_list, hnode) {
+		if (!strcmp(job->service_name, service_name))
+			return job;
+	}
+
+	return NULL;
+}
+
+static void __uet_job_remove(struct uet_job *job)
+{
+	struct uet_job_registry *jreg = job->jreg;
+
+	__job_disassociate(job);
+	hlist_del_init_rcu(&job->hnode);
+	rhashtable_remove_fast(&jreg->jobs_hash, &job->rht_node,
+			       uet_job_registry_rht_params);
+	kfree_rcu(job, rcu);
+}
+
+bool uet_job_remove(struct uet_job_registry *jreg, u32 job_id)
+{
+	bool removed = false;
+	struct uet_job *job;
+
+	mutex_lock(&jreg->jobs_lock);
+	job = uet_job_find(jreg, job_id);
+	if (job) {
+		__uet_job_remove(job);
+		removed = true;
+	}
+	mutex_unlock(&jreg->jobs_lock);
+
+	return removed;
+}
+
+void uet_jobs_uninit(struct uet_job_registry *jreg)
+{
+	struct hlist_node *tmp;
+	struct uet_job *job;
+
+	mutex_lock(&jreg->jobs_lock);
+	hlist_for_each_entry_safe(job, tmp, &jreg->jobs_list, hnode)
+		__uet_job_remove(job);
+	mutex_unlock(&jreg->jobs_lock);
+
+	rhashtable_destroy(&jreg->jobs_hash);
+	rcu_barrier();
+	mutex_destroy(&jreg->jobs_lock);
+}
+
+struct uet_job *uet_job_create(struct uet_job_registry *jreg,
+			       struct uet_job_ctrl_addr_req *job_req)
+{
+	struct uet_job *job;
+	int ret;
+
+	if (job_req->job_id == 0)
+		return ERR_PTR(-EINVAL);
+
+	mutex_lock(&jreg->jobs_lock);
+	if (uet_job_find_svc_name(jreg, job_req->service_name)) {
+		mutex_unlock(&jreg->jobs_lock);
+		return ERR_PTR(-EEXIST);
+	}
+
+	job = kzalloc(sizeof(*job), GFP_KERNEL);
+	if (!job)
+		return ERR_PTR(-ENOMEM);
+
+	job->jreg = jreg;
+	job->id = job_req->job_id;
+	strscpy(job->service_name, job_req->service_name, sizeof(job->service_name));
+
+	ret = rhashtable_lookup_insert_fast(&jreg->jobs_hash, &job->rht_node,
+					    uet_job_registry_rht_params);
+	if (ret) {
+		kfree_rcu(job, rcu);
+		mutex_unlock(&jreg->jobs_lock);
+		return ERR_PTR(ret);
+	}
+	hlist_add_head_rcu(&job->hnode, &jreg->jobs_list);
+	mutex_unlock(&jreg->jobs_lock);
+
+	return job;
+}
+
+int uet_job_reg_associate(struct uet_job_registry *jreg, struct uet_fep *fep,
+			  char *service_name)
+{
+	struct uet_job *job;
+	int ret = -ENOENT;
+
+	mutex_lock(&jreg->jobs_lock);
+	job = uet_job_find_svc_name(jreg, service_name);
+	if (job)
+		ret = __job_associate(job, fep);
+	mutex_unlock(&jreg->jobs_lock);
+
+	return ret;
+}
+
+void uet_job_reg_disassociate(struct uet_job_registry *jreg, u32 job_id)
+{
+	struct uet_job *job;
+
+	mutex_lock(&jreg->jobs_lock);
+	job = uet_job_find(jreg, job_id);
+	if (job)
+		__job_disassociate(job);
+	mutex_unlock(&jreg->jobs_lock);
+}
+
+/* returns <0 (error) or 1 (queued the skb) */
+int uet_job_fep_queue_skb(struct uet_context *ctx,
+			  u32 job_id, struct sk_buff *skb,
+			  __be32 remote_fep_addr)
+{
+	struct uet_job *job = uet_job_find(&ctx->job_reg, job_id);
+	struct uet_fep *fep;
+
+	if (!job)
+		return -ENOENT;
+
+	fep = rcu_dereference(job->fep);
+	if (!fep)
+		return -ENODEV;
+
+	skb_dst_drop(skb);
+	skb_queue_tail(&fep->rxq, skb);
+
+	return 1;
+}
+
+static int __nl_fep_addr_fill_one(struct sk_buff *skb,
+				  const struct fep_in_address *fep_addr,
+				  int fep_attr)
+{
+	struct nlattr *nest;
+	int attr, len;
+
+	if (!fep_addr->family)
+		return 0;
+
+	nest = nla_nest_start(skb, fep_attr);
+	if (!nest)
+		return -EMSGSIZE;
+
+	switch (fep_addr->family) {
+	case AF_INET:
+		attr = ULTRAETH_A_FEP_IN_ADDR_IP;
+		len = sizeof(fep_addr->ip);
+		break;
+	case AF_INET6:
+		attr = ULTRAETH_A_FEP_IN_ADDR_IP6;
+		len = sizeof(fep_addr->ip6);
+		break;
+	default:
+		WARN_ON_ONCE(1);
+		nla_nest_cancel(skb, nest);
+		return 0;
+	}
+
+	if (nla_put(skb, attr, len, &fep_addr->ip) ||
+	    nla_put_u16(skb, ULTRAETH_A_FEP_IN_ADDR_FAMILY, fep_addr->family)) {
+		nla_nest_cancel(skb, nest);
+		return -EMSGSIZE;
+	}
+
+	nla_nest_end(skb, nest);
+
+	return 0;
+}
+
+static int __nl_uet_addr_fill_one(struct sk_buff *skb,
+				    const struct fep_address *addr, int attr)
+{
+	struct nlattr *nest;
+
+	nest = nla_nest_start(skb, attr);
+	if (!nest)
+		return -EMSGSIZE;
+	if (__nl_fep_addr_fill_one(skb, &addr->in_address,
+				   ULTRAETH_A_FEP_ADDRESS_IN_ADDRESS) ||
+	    nla_put_u16(skb, ULTRAETH_A_FEP_ADDRESS_FLAGS, addr->flags) ||
+	    nla_put_u16(skb, ULTRAETH_A_FEP_ADDRESS_CAPS, addr->fep_caps) ||
+	    nla_put_u16(skb, ULTRAETH_A_FEP_ADDRESS_START_RESOURCE_INDEX,
+			addr->start_resource_index) ||
+	    nla_put_u16(skb, ULTRAETH_A_FEP_ADDRESS_NUM_RESOURCE_INDICES,
+			addr->num_resource_indices) ||
+	    nla_put_u32(skb, ULTRAETH_A_FEP_ADDRESS_INITIATOR_ID,
+			addr->initiator_id) ||
+	    nla_put_u16(skb, ULTRAETH_A_FEP_ADDRESS_PID_ON_FEP,
+			addr->pid_on_fep) ||
+	    nla_put_u8(skb, ULTRAETH_A_FEP_ADDRESS_VERSION, addr->version)) {
+		nla_nest_cancel(skb, nest);
+		return -EMSGSIZE;
+	}
+	nla_nest_end(skb, nest);
+
+	return 0;
+}
+
+static int __nl_fep_fill_one(struct sk_buff *skb,
+			     const struct uet_fep *fep, int attr)
+{
+	struct nlattr *nest;
+
+	nest = nla_nest_start(skb, attr);
+	if (!nest)
+		return -EMSGSIZE;
+	if (__nl_uet_addr_fill_one(skb, &fep->addr, ULTRAETH_A_FEP_ENTRY_ADDRESS)) {
+		nla_nest_cancel(skb, nest);
+		return -EMSGSIZE;
+	}
+	nla_nest_end(skb, nest);
+
+	return 0;
+}
+
+static int __nl_job_feps_fill(struct sk_buff *skb, const struct uet_fep *fep)
+{
+	struct nlattr *nest;
+
+	nest = nla_nest_start(skb, ULTRAETH_A_JOB_FLIST);
+	if (!nest)
+		return -EMSGSIZE;
+	if (fep && __nl_fep_fill_one(skb, fep, ULTRAETH_A_FLIST_FEP)) {
+		nla_nest_cancel(skb, nest);
+		return -EMSGSIZE;
+	}
+	nla_nest_end(skb, nest);
+
+	return 0;
+}
+
+static int __nl_job_fill_one(struct sk_buff *skb, const struct uet_job *job)
+{
+	struct nlattr *nest;
+
+	nest = nla_nest_start(skb, ULTRAETH_A_JLIST_JOB);
+	if (!nest)
+		return -EMSGSIZE;
+
+	if (__nl_uet_addr_fill_one(skb, &job->addr, ULTRAETH_A_JOB_ADDRESS) ||
+	    nla_put_u32(skb, ULTRAETH_A_JOB_ID, job->id) ||
+	    nla_put_string(skb, ULTRAETH_A_JOB_SERVICE_NAME, job->service_name) ||
+	    __nl_job_feps_fill(skb, rcu_dereference(job->fep))) {
+		nla_nest_cancel(skb, nest);
+		return -EMSGSIZE;
+	}
+
+	nla_nest_end(skb, nest);
+	return 0;
+}
+
+int ultraeth_nl_job_get_dumpit(struct sk_buff *skb, struct netlink_callback *cb)
+{
+	const struct genl_info *info = genl_info_dump(cb);
+	int idx = 0, s_idx = cb->args[0], err;
+	struct uet_context *ctx;
+	struct uet_job *job;
+	struct nlattr *nest;
+	int context_id;
+	void *hdr;
+
+	if (!info->attrs[ULTRAETH_A_JOBS_CONTEXT_ID]) {
+		NL_SET_ERR_MSG(info->extack, "context id must be specified");
+		return -EINVAL;
+	}
+
+	context_id = nla_get_s32(info->attrs[ULTRAETH_A_JOBS_CONTEXT_ID]);
+	ctx = uet_context_get_by_id(context_id);
+	if (!ctx) {
+		NL_SET_ERR_MSG(info->extack, "context doesn't exist");
+		return -ENOENT;
+	}
+
+	/* filled all, return 0 */
+	if (s_idx == atomic_read(&ctx->job_reg.jobs_hash.nelems))
+		goto out_put;
+
+	err = -EMSGSIZE;
+	hdr = genlmsg_put(skb, NETLINK_CB(cb->skb).portid, cb->nlh->nlmsg_seq,
+			  &ultraeth_nl_family, NLM_F_MULTI, ULTRAETH_CMD_JOB_GET);
+	if (!hdr)
+		goto out_put;
+	if (nla_put_s32(skb, ULTRAETH_A_JOBS_CONTEXT_ID, ctx->id))
+		goto out_end;
+	nest = nla_nest_start(skb, ULTRAETH_A_JOBS_JLIST);
+	if (!nest)
+		goto out_end;
+	err = 0;
+	rcu_read_lock();
+	hlist_for_each_entry_rcu(job, &ctx->job_reg.jobs_list, hnode) {
+		if (idx < s_idx) {
+			idx++;
+			continue;
+		}
+		err = __nl_job_fill_one(skb, job);
+		if (err)
+			break;
+		idx++;
+	}
+	cb->args[0] = idx;
+	rcu_read_unlock();
+	nla_nest_end(skb, nest);
+out_end:
+	genlmsg_end(skb, hdr);
+out_put:
+	uet_context_put(ctx);
+
+	return err ? err : skb->len;
+}
+
+int ultraeth_nl_job_new_doit(struct sk_buff *skb, struct genl_info *info)
+{
+	struct uet_job_ctrl_addr_req jreq;
+	struct uet_context *ctx;
+	int context_id, job_id;
+	struct uet_job *job;
+	char *service_name;
+	int ret = 0;
+
+	if (!info->attrs[ULTRAETH_A_JOB_REQ_CONTEXT_ID]) {
+		NL_SET_ERR_MSG(info->extack, "context id must be specified");
+		return -EINVAL;
+	}
+	if (!info->attrs[ULTRAETH_A_JOB_REQ_ID]) {
+		NL_SET_ERR_MSG(info->extack, "Job id must be specified");
+		return -EINVAL;
+	}
+	if (!info->attrs[ULTRAETH_A_JOB_REQ_SERVICE_NAME]) {
+		NL_SET_ERR_MSG(info->extack, "Job service name must be specified");
+		return -EINVAL;
+	}
+	service_name = nla_data(info->attrs[ULTRAETH_A_JOB_REQ_SERVICE_NAME]);
+	job_id = nla_get_u32(info->attrs[ULTRAETH_A_JOB_REQ_ID]);
+	context_id = nla_get_s32(info->attrs[ULTRAETH_A_JOB_REQ_CONTEXT_ID]);
+	ctx = uet_context_get_by_id(context_id);
+	if (!ctx) {
+		NL_SET_ERR_MSG(info->extack, "context doesn't exist");
+		return -ENOENT;
+	}
+
+	memset(&jreq, 0, sizeof(jreq));
+	jreq.job_id = job_id;
+	strscpy(jreq.service_name, service_name, sizeof(jreq.service_name));
+	job = uet_job_create(&ctx->job_reg, &jreq);
+	if (IS_ERR(job))
+		ret = PTR_ERR(job);
+
+	uet_context_put(ctx);
+
+	return ret;
+}
+
+int ultraeth_nl_job_del_doit(struct sk_buff *skb, struct genl_info *info)
+{
+	struct uet_context *ctx;
+	bool destroyed = false;
+	int context_id, job_id;
+
+	if (!info->attrs[ULTRAETH_A_JOB_REQ_CONTEXT_ID]) {
+		NL_SET_ERR_MSG(info->extack, "context id must be specified");
+		return -EINVAL;
+	}
+	if (!info->attrs[ULTRAETH_A_JOB_REQ_ID]) {
+		NL_SET_ERR_MSG(info->extack, "Job id must be specified");
+		return -EINVAL;
+	}
+	job_id = nla_get_u32(info->attrs[ULTRAETH_A_JOB_REQ_ID]);
+	context_id = nla_get_s32(info->attrs[ULTRAETH_A_JOB_REQ_CONTEXT_ID]);
+	ctx = uet_context_get_by_id(context_id);
+	if (!ctx) {
+		NL_SET_ERR_MSG(info->extack, "context doesn't exist");
+		return -ENOENT;
+	}
+
+	destroyed = uet_job_remove(&ctx->job_reg, job_id);
+	uet_context_put(ctx);
+
+	return destroyed ? 0 : -ENOENT;
+}
diff --git a/drivers/ultraeth/uet_netlink.c b/drivers/ultraeth/uet_netlink.c
index 39e4aa6092a9..7fdaf15e43e3 100644
--- a/drivers/ultraeth/uet_netlink.c
+++ b/drivers/ultraeth/uet_netlink.c
@@ -10,6 +10,25 @@
 
 #include <uapi/linux/ultraeth_nl.h>
 
+/* Common nested types */
+const struct nla_policy ultraeth_fep_address_nl_policy[ULTRAETH_A_FEP_ADDRESS_VERSION + 1] = {
+	[ULTRAETH_A_FEP_ADDRESS_IN_ADDRESS] = NLA_POLICY_NESTED(ultraeth_fep_in_addr_nl_policy),
+	[ULTRAETH_A_FEP_ADDRESS_FLAGS] = { .type = NLA_U16, },
+	[ULTRAETH_A_FEP_ADDRESS_CAPS] = { .type = NLA_U16, },
+	[ULTRAETH_A_FEP_ADDRESS_START_RESOURCE_INDEX] = { .type = NLA_U16, },
+	[ULTRAETH_A_FEP_ADDRESS_NUM_RESOURCE_INDICES] = { .type = NLA_U16, },
+	[ULTRAETH_A_FEP_ADDRESS_INITIATOR_ID] = { .type = NLA_U32, },
+	[ULTRAETH_A_FEP_ADDRESS_PID_ON_FEP] = { .type = NLA_U16, },
+	[ULTRAETH_A_FEP_ADDRESS_PADDING] = { .type = NLA_U16, },
+	[ULTRAETH_A_FEP_ADDRESS_VERSION] = { .type = NLA_U8, },
+};
+
+const struct nla_policy ultraeth_fep_in_addr_nl_policy[ULTRAETH_A_FEP_IN_ADDR_FAMILY + 1] = {
+	[ULTRAETH_A_FEP_IN_ADDR_IP] = { .type = NLA_BINARY, },
+	[ULTRAETH_A_FEP_IN_ADDR_IP6] = { .type = NLA_BINARY, },
+	[ULTRAETH_A_FEP_IN_ADDR_FAMILY] = { .type = NLA_U16, },
+};
+
 /* ULTRAETH_CMD_CONTEXT_NEW - do */
 static const struct nla_policy ultraeth_context_new_nl_policy[ULTRAETH_A_CONTEXT_ID + 1] = {
 	[ULTRAETH_A_CONTEXT_ID] = NLA_POLICY_RANGE(NLA_S32, 0, 255),
@@ -20,6 +39,25 @@ static const struct nla_policy ultraeth_context_del_nl_policy[ULTRAETH_A_CONTEXT
 	[ULTRAETH_A_CONTEXT_ID] = NLA_POLICY_RANGE(NLA_S32, 0, 255),
 };
 
+/* ULTRAETH_CMD_JOB_GET - dump */
+static const struct nla_policy ultraeth_job_get_nl_policy[ULTRAETH_A_JOBS_CONTEXT_ID + 1] = {
+	[ULTRAETH_A_JOBS_CONTEXT_ID] = { .type = NLA_S32, },
+};
+
+/* ULTRAETH_CMD_JOB_NEW - do */
+static const struct nla_policy ultraeth_job_new_nl_policy[ULTRAETH_A_JOB_REQ_SERVICE_NAME + 1] = {
+	[ULTRAETH_A_JOB_REQ_CONTEXT_ID] = { .type = NLA_S32, },
+	[ULTRAETH_A_JOB_REQ_ID] = { .type = NLA_U32, },
+	[ULTRAETH_A_JOB_REQ_ADDRESS] = NLA_POLICY_NESTED(ultraeth_fep_address_nl_policy),
+	[ULTRAETH_A_JOB_REQ_SERVICE_NAME] = { .type = NLA_NUL_STRING, },
+};
+
+/* ULTRAETH_CMD_JOB_DEL - do */
+static const struct nla_policy ultraeth_job_del_nl_policy[ULTRAETH_A_JOB_REQ_ID + 1] = {
+	[ULTRAETH_A_JOB_REQ_CONTEXT_ID] = { .type = NLA_S32, },
+	[ULTRAETH_A_JOB_REQ_ID] = { .type = NLA_U32, },
+};
+
 /* Ops table for ultraeth */
 static const struct genl_split_ops ultraeth_nl_ops[] = {
 	{
@@ -41,6 +79,27 @@ static const struct genl_split_ops ultraeth_nl_ops[] = {
 		.maxattr	= ULTRAETH_A_CONTEXT_ID,
 		.flags		= GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
 	},
+	{
+		.cmd		= ULTRAETH_CMD_JOB_GET,
+		.dumpit		= ultraeth_nl_job_get_dumpit,
+		.policy		= ultraeth_job_get_nl_policy,
+		.maxattr	= ULTRAETH_A_JOBS_CONTEXT_ID,
+		.flags		= GENL_CMD_CAP_DUMP,
+	},
+	{
+		.cmd		= ULTRAETH_CMD_JOB_NEW,
+		.doit		= ultraeth_nl_job_new_doit,
+		.policy		= ultraeth_job_new_nl_policy,
+		.maxattr	= ULTRAETH_A_JOB_REQ_SERVICE_NAME,
+		.flags		= GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+	},
+	{
+		.cmd		= ULTRAETH_CMD_JOB_DEL,
+		.doit		= ultraeth_nl_job_del_doit,
+		.policy		= ultraeth_job_del_nl_policy,
+		.maxattr	= ULTRAETH_A_JOB_REQ_ID,
+		.flags		= GENL_ADMIN_PERM | GENL_CMD_CAP_DO,
+	},
 };
 
 struct genl_family ultraeth_nl_family __ro_after_init = {
diff --git a/drivers/ultraeth/uet_netlink.h b/drivers/ultraeth/uet_netlink.h
index 9dd9df24513a..6e7226f39ddf 100644
--- a/drivers/ultraeth/uet_netlink.h
+++ b/drivers/ultraeth/uet_netlink.h
@@ -11,10 +11,18 @@
 
 #include <uapi/linux/ultraeth_nl.h>
 
+/* Common nested types */
+extern const struct nla_policy ultraeth_fep_address_nl_policy[ULTRAETH_A_FEP_ADDRESS_VERSION + 1];
+extern const struct nla_policy ultraeth_fep_in_addr_nl_policy[ULTRAETH_A_FEP_IN_ADDR_FAMILY + 1];
+
 int ultraeth_nl_context_get_dumpit(struct sk_buff *skb,
 				   struct netlink_callback *cb);
 int ultraeth_nl_context_new_doit(struct sk_buff *skb, struct genl_info *info);
 int ultraeth_nl_context_del_doit(struct sk_buff *skb, struct genl_info *info);
+int ultraeth_nl_job_get_dumpit(struct sk_buff *skb,
+			       struct netlink_callback *cb);
+int ultraeth_nl_job_new_doit(struct sk_buff *skb, struct genl_info *info);
+int ultraeth_nl_job_del_doit(struct sk_buff *skb, struct genl_info *info);
 
 extern struct genl_family ultraeth_nl_family;
 
diff --git a/include/net/ultraeth/uet_context.h b/include/net/ultraeth/uet_context.h
index 150ad2c9b456..7638c768597e 100644
--- a/include/net/ultraeth/uet_context.h
+++ b/include/net/ultraeth/uet_context.h
@@ -9,12 +9,15 @@
 #include <linux/mutex.h>
 #include <linux/refcount.h>
 #include <linux/wait.h>
+#include <net/ultraeth/uet_job.h>
 
 struct uet_context {
 	int id;
 	refcount_t refcnt;
 	wait_queue_head_t refcnt_wait;
 	struct list_head list;
+
+	struct uet_job_registry job_reg;
 };
 
 struct uet_context *uet_context_get_by_id(int id);
diff --git a/include/net/ultraeth/uet_job.h b/include/net/ultraeth/uet_job.h
new file mode 100644
index 000000000000..fac1f0752a78
--- /dev/null
+++ b/include/net/ultraeth/uet_job.h
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+
+#ifndef _UET_JOB_H
+#define _UET_JOB_H
+
+#include <linux/types.h>
+#include <linux/rhashtable.h>
+#include <linux/skbuff.h>
+#include <linux/mutex.h>
+#include <uapi/linux/ultraeth.h>
+
+struct uet_context;
+
+struct uet_job_registry {
+	struct mutex jobs_lock;
+	struct hlist_head jobs_list;
+	struct rhashtable jobs_hash;
+};
+
+struct uet_fep {
+	struct uet_context *context;
+	struct sk_buff_head rxq;
+	struct fep_address addr;
+	u32 job_id;
+};
+
+/**
+ * struct uet_job - single job
+ *
+ * @rht_node: link into the job registry's job hash table
+ * @hnode: link into the job registry's list
+ * @jreg: pointer to job registry (owner)
+ * @service_name: service name used for lookups on address req
+ * @addr: job specific address (XXX)
+ * @job_id: unique job id
+ * @rcu: used for freeing
+ *
+ * if @fep is set then the job is considered associated, i.e. there is
+ * an fd for the context's character device which is bound to this
+ * job (FEP)
+ */
+struct uet_job {
+	struct rhash_head rht_node;
+	struct hlist_node hnode;
+
+	struct uet_job_registry *jreg;
+
+	char service_name[UET_SVC_MAX_LEN];
+
+	struct fep_address addr;
+	struct uet_fep __rcu *fep;
+
+	u32 id;
+
+	struct rcu_head rcu;
+};
+
+struct uet_job_ctrl_addr_req {
+	char service_name[UET_SVC_MAX_LEN];
+	struct fep_in_address address;
+	__u32 job_id;
+	__u32 os_pid;
+	__u8 flags;
+};
+
+int uet_jobs_init(struct uet_job_registry *jreg);
+void uet_jobs_uninit(struct uet_job_registry *jreg);
+
+struct uet_job *uet_job_create(struct uet_job_registry *jreg,
+			       struct uet_job_ctrl_addr_req *job_req);
+bool uet_job_remove(struct uet_job_registry *jreg, u32 job_id);
+struct uet_job *uet_job_find(struct uet_job_registry *jreg, u32 id);
+void uet_job_reg_disassociate(struct uet_job_registry *jreg, u32 job_id);
+int uet_job_reg_associate(struct uet_job_registry *jreg, struct uet_fep *fep,
+			  char *service_name);
+int uet_job_fep_queue_skb(struct uet_context *ctx, u32 job_id,
+			  struct sk_buff *skb, __be32 remote_fep_addr);
+#endif /* _UET_JOB_H */
diff --git a/include/uapi/linux/ultraeth.h b/include/uapi/linux/ultraeth.h
new file mode 100644
index 000000000000..a6f244de6d75
--- /dev/null
+++ b/include/uapi/linux/ultraeth.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: ((GPL-2.0 WITH Linux-syscall-note) OR BSD-3-Clause) */
+
+#ifndef _UAPI_LINUX_ULTRAETH_H
+#define _UAPI_LINUX_ULTRAETH_H
+
+#include <asm/byteorder.h>
+#include <linux/types.h>
+
+#define UET_SVC_MAX_LEN 64
+
+enum {
+	UET_ADDR_F_VALID_FEP_CAP	= (1 << 0),
+	UET_ADDR_F_VALID_ADDR		= (1 << 1),
+	UET_ADDR_F_VALID_PID_ON_FEP	= (1 << 2),
+	UET_ADDR_F_VALID_RI		= (1 << 3),
+	UET_ADDR_F_VALID_INIT_ID	= (1 << 4),
+	UET_ADDR_F_ADDRESS_MODE		= (1 << 5),
+	UET_ADDR_F_ADDRESS_TYPE		= (1 << 6),
+	UET_ADDR_F_MTU_LIMITED		= (1 << 7),
+};
+
+#define UET_ADDR_FLAG_IP_VER (1 << 6)
+
+struct fep_in_address {
+	union {
+		__be32 ip;
+		__u8 ip6[16];
+	};
+	__u16 family;
+};
+
+struct fep_address {
+	struct fep_in_address in_address;
+
+	__u16 flags;
+	__u16 fep_caps;
+	__u16 start_resource_index;
+	__u16 num_resource_indices;
+	__u32 initiator_id;
+	__u16 pid_on_fep;
+	__u16 padding;
+	__u8 version;
+};
+#endif /* _UAPI_LINUX_ULTRAETH_H */
diff --git a/include/uapi/linux/ultraeth_nl.h b/include/uapi/linux/ultraeth_nl.h
index f3bdf8111623..d65521de196a 100644
--- a/include/uapi/linux/ultraeth_nl.h
+++ b/include/uapi/linux/ultraeth_nl.h
@@ -23,10 +23,86 @@ enum {
 	ULTRAETH_A_CONTEXTS_MAX = (__ULTRAETH_A_CONTEXTS_MAX - 1)
 };
 
+enum {
+	ULTRAETH_A_FEP_IN_ADDR_IP = 1,
+	ULTRAETH_A_FEP_IN_ADDR_IP6,
+	ULTRAETH_A_FEP_IN_ADDR_FAMILY,
+
+	__ULTRAETH_A_FEP_IN_ADDR_MAX,
+	ULTRAETH_A_FEP_IN_ADDR_MAX = (__ULTRAETH_A_FEP_IN_ADDR_MAX - 1)
+};
+
+enum {
+	ULTRAETH_A_FEP_ADDRESS_IN_ADDRESS = 1,
+	ULTRAETH_A_FEP_ADDRESS_FLAGS,
+	ULTRAETH_A_FEP_ADDRESS_CAPS,
+	ULTRAETH_A_FEP_ADDRESS_START_RESOURCE_INDEX,
+	ULTRAETH_A_FEP_ADDRESS_NUM_RESOURCE_INDICES,
+	ULTRAETH_A_FEP_ADDRESS_INITIATOR_ID,
+	ULTRAETH_A_FEP_ADDRESS_PID_ON_FEP,
+	ULTRAETH_A_FEP_ADDRESS_PADDING,
+	ULTRAETH_A_FEP_ADDRESS_VERSION,
+
+	__ULTRAETH_A_FEP_ADDRESS_MAX,
+	ULTRAETH_A_FEP_ADDRESS_MAX = (__ULTRAETH_A_FEP_ADDRESS_MAX - 1)
+};
+
+enum {
+	ULTRAETH_A_FEP_ENTRY_ADDRESS = 1,
+
+	__ULTRAETH_A_FEP_ENTRY_MAX,
+	ULTRAETH_A_FEP_ENTRY_MAX = (__ULTRAETH_A_FEP_ENTRY_MAX - 1)
+};
+
+enum {
+	ULTRAETH_A_FLIST_FEP = 1,
+
+	__ULTRAETH_A_FLIST_MAX,
+	ULTRAETH_A_FLIST_MAX = (__ULTRAETH_A_FLIST_MAX - 1)
+};
+
+enum {
+	ULTRAETH_A_JOB_REQ_CONTEXT_ID = 1,
+	ULTRAETH_A_JOB_REQ_ID,
+	ULTRAETH_A_JOB_REQ_ADDRESS,
+	ULTRAETH_A_JOB_REQ_SERVICE_NAME,
+
+	__ULTRAETH_A_JOB_REQ_MAX,
+	ULTRAETH_A_JOB_REQ_MAX = (__ULTRAETH_A_JOB_REQ_MAX - 1)
+};
+
+enum {
+	ULTRAETH_A_JOB_ID = 1,
+	ULTRAETH_A_JOB_ADDRESS,
+	ULTRAETH_A_JOB_SERVICE_NAME,
+	ULTRAETH_A_JOB_FLIST,
+
+	__ULTRAETH_A_JOB_MAX,
+	ULTRAETH_A_JOB_MAX = (__ULTRAETH_A_JOB_MAX - 1)
+};
+
+enum {
+	ULTRAETH_A_JLIST_JOB = 1,
+
+	__ULTRAETH_A_JLIST_MAX,
+	ULTRAETH_A_JLIST_MAX = (__ULTRAETH_A_JLIST_MAX - 1)
+};
+
+enum {
+	ULTRAETH_A_JOBS_CONTEXT_ID = 1,
+	ULTRAETH_A_JOBS_JLIST,
+
+	__ULTRAETH_A_JOBS_MAX,
+	ULTRAETH_A_JOBS_MAX = (__ULTRAETH_A_JOBS_MAX - 1)
+};
+
 enum {
 	ULTRAETH_CMD_CONTEXT_GET = 1,
 	ULTRAETH_CMD_CONTEXT_NEW,
 	ULTRAETH_CMD_CONTEXT_DEL,
+	ULTRAETH_CMD_JOB_GET,
+	ULTRAETH_CMD_JOB_NEW,
+	ULTRAETH_CMD_JOB_DEL,
 
 	__ULTRAETH_CMD_MAX,
 	ULTRAETH_CMD_MAX = (__ULTRAETH_CMD_MAX - 1)
