Index: uspace/srv/hid/console/console.c
===================================================================
--- uspace/srv/hid/console/console.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/console/console.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -334,10 +334,10 @@
 {
 	/* Got key press/release event */
-	cons_event_t *event =
-	    (cons_event_t *) malloc(sizeof(cons_event_t));
+	cons_qevent_t *event =
+	    (cons_qevent_t *) malloc(sizeof(cons_qevent_t));
 	if (event == NULL)
 		return;
 
-	*event = *ev;
+	event->ev = *ev;
 	link_initialize(&event->link);
 
@@ -556,6 +556,7 @@
 		if (pos < size) {
 			link_t *link = prodcons_consume(&cons->input_pc);
-			cons_event_t *event = list_get_instance(link,
-			    cons_event_t, link);
+			cons_qevent_t *qevent = list_get_instance(link,
+			    cons_qevent_t, link);
+			cons_event_t *event = &qevent->ev;
 
 			/* Accept key presses of printable chars only. */
@@ -567,5 +568,5 @@
 			}
 
-			free(event);
+			free(qevent);
 		}
 	}
@@ -703,8 +704,8 @@
 	console_t *cons = srv_to_console(srv);
 	link_t *link = prodcons_consume(&cons->input_pc);
-	cons_event_t *cevent = list_get_instance(link, cons_event_t, link);
-
-	*event = *cevent;
-	free(cevent);
+	cons_qevent_t *qevent = list_get_instance(link, cons_qevent_t, link);
+
+	*event = qevent->ev;
+	free(qevent);
 	return EOK;
 }
Index: uspace/srv/hid/console/console.h
===================================================================
--- uspace/srv/hid/console/console.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/console/console.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2006 Josef Cejka
  * All rights reserved.
@@ -36,5 +37,16 @@
 #define CONSOLE_CONSOLE_H__
 
+#include <adt/prodcons.h>
+#include <io/cons_event.h>
+
 #define CONSOLE_COUNT   11
+
+/** Console event queue entry */
+typedef struct {
+	/** Link to list of events */
+	link_t link;
+	/** Console event */
+	cons_event_t ev;
+} cons_qevent_t;
 
 #endif
Index: uspace/srv/hid/display/display.c
===================================================================
--- uspace/srv/hid/display/display.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/display/display.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -105,4 +105,6 @@
 	list_initialize(&disp->ddevs);
 	list_initialize(&disp->idevcfgs);
+	list_initialize(&disp->ievents);
+	fibril_condvar_initialize(&disp->ievent_cv);
 	list_initialize(&disp->seats);
 	list_initialize(&disp->windows);
@@ -129,4 +131,5 @@
 	assert(list_empty(&disp->ddevs));
 	assert(list_empty(&disp->idevcfgs));
+	assert(list_empty(&disp->ievents));
 	assert(list_empty(&disp->seats));
 	assert(list_empty(&disp->windows));
@@ -215,6 +218,10 @@
 		}
 
+		/*
+		 * Load device configuration entry. If the device
+		 * is not currently connected (ENOENT), skip it.
+		 */
 		rc = ds_idevcfg_load(display, nidevcfg, &idevcfg);
-		if (rc != EOK)
+		if (rc != EOK && rc != ENOENT)
 			goto error;
 
Index: uspace/srv/hid/display/idevcfg.c
===================================================================
--- uspace/srv/hid/display/idevcfg.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/display/idevcfg.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -96,5 +96,5 @@
 	const char *sseat_id;
 	char *endptr;
-	unsigned long svc_id;
+	service_id_t svc_id;
 	unsigned long seat_id;
 	ds_seat_t *seat;
Index: uspace/srv/hid/display/ievent.c
===================================================================
--- uspace/srv/hid/display/ievent.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
+++ uspace/srv/hid/display/ievent.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) 2024 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup display
+ * @{
+ */
+/** @file Input event queue
+ */
+
+#include <adt/list.h>
+#include <errno.h>
+#include <fibril_synch.h>
+#include <io/kbd_event.h>
+#include <stdlib.h>
+#include "display.h"
+#include "ievent.h"
+
+/** Post keyboard event to input event queue.
+ *
+ * @param disp Display
+ * @param kbd Keyboard event
+ *
+ * @return EOK on success or an error code
+ */
+errno_t ds_ievent_post_kbd(ds_display_t *disp, kbd_event_t *kbd)
+{
+	ds_ievent_t *ievent;
+
+	ievent = calloc(1, sizeof(ds_ievent_t));
+	if (ievent == NULL)
+		return ENOMEM;
+
+	ievent->display = disp;
+	ievent->etype = det_kbd;
+	ievent->ev.kbd = *kbd;
+
+	list_append(&ievent->lievents, &disp->ievents);
+	fibril_condvar_signal(&disp->ievent_cv);
+
+	return EOK;
+}
+
+/** Post pointing device event to input event queue.
+ *
+ * @param disp Display
+ * @param kbd Keyboard event
+ *
+ * @return EOK on success or an error code
+ */
+errno_t ds_ievent_post_ptd(ds_display_t *disp, ptd_event_t *ptd)
+{
+	ds_ievent_t *ievent;
+	ds_ievent_t *prev;
+	link_t *link;
+
+	ievent = calloc(1, sizeof(ds_ievent_t));
+	if (ievent == NULL)
+		return ENOMEM;
+
+	link = list_last(&disp->ievents);
+	if (link != NULL) {
+		prev = list_get_instance(link, ds_ievent_t, lievents);
+		if (prev->etype == det_ptd && prev->ev.ptd.pos_id ==
+		    ptd->pos_id) {
+			/*
+			 * Previous event is also a pointing device event
+			 * and it is from the same device.
+			 */
+			if (prev->ev.ptd.type == PTD_MOVE &&
+			    ptd->type == PTD_MOVE) {
+				/* Both events are relative move events */
+				gfx_coord2_add(&ptd->dmove, &prev->ev.ptd.dmove,
+				    &prev->ev.ptd.dmove);
+				return EOK;
+			} else if (prev->ev.ptd.type == PTD_ABS_MOVE &&
+			    ptd->type == PTD_ABS_MOVE) {
+				/* Both events are absolute move events */
+				prev->ev.ptd.apos = ptd->apos;
+				prev->ev.ptd.abounds = ptd->abounds;
+				return EOK;
+			}
+		}
+	}
+
+	ievent->display = disp;
+	ievent->etype = det_ptd;
+	ievent->ev.ptd = *ptd;
+
+	list_append(&ievent->lievents, &disp->ievents);
+	fibril_condvar_signal(&disp->ievent_cv);
+
+	return EOK;
+}
+
+/** Input event processing fibril.
+ *
+ * @param arg Argument (ds_display_t *)
+ * @return EOK success
+ */
+static errno_t ds_ievent_fibril(void *arg)
+{
+	ds_display_t *disp = (ds_display_t *)arg;
+	ds_ievent_t *ievent;
+	link_t *link;
+
+	fibril_mutex_lock(&disp->lock);
+
+	while (!disp->ievent_quit) {
+		while (list_empty(&disp->ievents))
+			fibril_condvar_wait(&disp->ievent_cv, &disp->lock);
+
+		link = list_first(&disp->ievents);
+		assert(link != NULL);
+		list_remove(link);
+		ievent = list_get_instance(link, ds_ievent_t, lievents);
+
+		switch (ievent->etype) {
+		case det_kbd:
+			(void)ds_display_post_kbd_event(disp, &ievent->ev.kbd);
+			break;
+		case det_ptd:
+			(void)ds_display_post_ptd_event(disp, &ievent->ev.ptd);
+			break;
+		}
+	}
+
+	/* Signal to ds_ievent_fini() that the event processing fibril quit */
+	disp->ievent_done = true;
+	fibril_condvar_signal(&disp->ievent_cv);
+	fibril_mutex_unlock(&disp->lock);
+
+	return EOK;
+}
+
+/** Initialize input event processing.
+ *
+ * @param disp Display
+ * @return EOK on success or an error code
+ */
+errno_t ds_ievent_init(ds_display_t *disp)
+{
+	assert(disp->ievent_fid == 0);
+
+	disp->ievent_fid = fibril_create(ds_ievent_fibril, (void *)disp);
+	if (disp->ievent_fid == 0)
+		return ENOMEM;
+
+	fibril_add_ready(disp->ievent_fid);
+	return EOK;
+}
+
+/** Deinitialize input event processing.
+ *
+ * @param disp Display
+ */
+void ds_ievent_fini(ds_display_t *disp)
+{
+	ds_ievent_t *ievent;
+	link_t *link;
+
+	if (disp->ievent_fid == 0)
+		return;
+
+	/* Signal event processing fibril to quit. */
+	fibril_mutex_lock(&disp->lock);
+	disp->ievent_quit = true;
+	fibril_condvar_signal(&disp->ievent_cv);
+
+	/* Wait for event processing fibril to quit. */
+	while (!disp->ievent_done)
+		fibril_condvar_wait(&disp->ievent_cv, &disp->lock);
+
+	/* Remove all events from the queue. */
+	while (!list_empty(&disp->ievents)) {
+		link = list_first(&disp->ievents);
+		assert(link != NULL);
+		list_remove(link);
+		ievent = list_get_instance(link, ds_ievent_t, lievents);
+		free(ievent);
+	}
+
+	fibril_mutex_unlock(&disp->lock);
+
+	fibril_detach(disp->ievent_fid);
+	disp->ievent_fid = 0;
+}
+
+/** @}
+ */
Index: uspace/srv/hid/display/ievent.h
===================================================================
--- uspace/srv/hid/display/ievent.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
+++ uspace/srv/hid/display/ievent.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2024 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup display
+ * @{
+ */
+/** @file Input event queue
+ */
+
+#ifndef IEVENT_H
+#define IEVENT_H
+
+#include <errno.h>
+#include <io/kbd_event.h>
+#include <io/pos_event.h>
+#include "types/display/display.h"
+#include "types/display/ievent.h"
+
+extern errno_t ds_ievent_post_kbd(ds_display_t *, kbd_event_t *);
+extern errno_t ds_ievent_post_ptd(ds_display_t *, ptd_event_t *);
+extern errno_t ds_ievent_init(ds_display_t *);
+extern void ds_ievent_fini(ds_display_t *);
+
+#endif
+
+/** @}
+ */
Index: uspace/srv/hid/display/input.c
===================================================================
--- uspace/srv/hid/display/input.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/display/input.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -40,4 +40,5 @@
 #include <str_error.h>
 #include "display.h"
+#include "ievent.h"
 #include "input.h"
 #include "main.h"
@@ -87,5 +88,5 @@
 
 	ds_display_lock(disp);
-	rc = ds_display_post_kbd_event(disp, &event);
+	rc = ds_ievent_post_kbd(disp, &event);
 	ds_display_unlock(disp);
 	return rc;
@@ -104,5 +105,5 @@
 
 	ds_display_lock(disp);
-	rc = ds_display_post_ptd_event(disp, &event);
+	rc = ds_ievent_post_ptd(disp, &event);
 	ds_display_unlock(disp);
 	return rc;
@@ -126,5 +127,5 @@
 
 	ds_display_lock(disp);
-	rc = ds_display_post_ptd_event(disp, &event);
+	rc = ds_ievent_post_ptd(disp, &event);
 	ds_display_unlock(disp);
 	return rc;
@@ -145,5 +146,5 @@
 
 	ds_display_lock(disp);
-	rc = ds_display_post_ptd_event(disp, &event);
+	rc = ds_ievent_post_ptd(disp, &event);
 	ds_display_unlock(disp);
 	return rc;
@@ -163,5 +164,5 @@
 
 	ds_display_lock(disp);
-	rc = ds_display_post_ptd_event(disp, &event);
+	rc = ds_ievent_post_ptd(disp, &event);
 	ds_display_unlock(disp);
 	return rc;
Index: uspace/srv/hid/display/main.c
===================================================================
--- uspace/srv/hid/display/main.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/display/main.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -54,4 +54,5 @@
 #include "display.h"
 #include "dsops.h"
+#include "ievent.h"
 #include "input.h"
 #include "main.h"
@@ -156,4 +157,8 @@
 	output->def_display = disp;
 	rc = ds_output_start_discovery(output);
+	if (rc != EOK)
+		goto error;
+
+	rc = ds_ievent_init(disp);
 	if (rc != EOK)
 		goto error;
@@ -211,4 +216,5 @@
 		ds_input_close(disp);
 #endif
+	ds_ievent_fini(disp);
 	if (output != NULL)
 		ds_output_destroy(output);
Index: uspace/srv/hid/display/meson.build
===================================================================
--- uspace/srv/hid/display/meson.build	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/display/meson.build	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -41,4 +41,5 @@
 	'dsops.c',
 	'idevcfg.c',
+	'ievent.c',
 	'input.c',
 	'main.c',
@@ -59,4 +60,5 @@
 	'display.c',
 	'idevcfg.c',
+	'ievent.c',
 	'seat.c',
 	'window.c',
@@ -67,4 +69,5 @@
 	'test/cursor.c',
 	'test/display.c',
+	'test/ievent.c',
 	'test/main.c',
 	'test/seat.c',
Index: uspace/srv/hid/display/seat.c
===================================================================
--- uspace/srv/hid/display/seat.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/display/seat.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -492,5 +492,6 @@
 	/* Focus window on button press */
 	if (event->type == PTD_PRESS && event->btn_num == 1) {
-		if (wnd != NULL && (wnd->flags & wndf_popup) == 0) {
+		if (wnd != NULL && (wnd->flags & wndf_popup) == 0 &&
+		    (wnd->flags & wndf_nofocus) == 0) {
 			ds_seat_set_focus(seat, wnd);
 		}
Index: uspace/srv/hid/display/test/ievent.c
===================================================================
--- uspace/srv/hid/display/test/ievent.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
+++ uspace/srv/hid/display/test/ievent.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2024 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <errno.h>
+#include <io/kbd_event.h>
+#include <pcut/pcut.h>
+
+#include "../display.h"
+#include "../types/display/ptd_event.h"
+#include "../ievent.h"
+
+PCUT_INIT;
+
+PCUT_TEST_SUITE(ievent);
+
+/* Test ds_ievent_init() and ds_ievent_fini() */
+PCUT_TEST(ievent_init_fini)
+{
+	ds_display_t *disp;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_ievent_init(disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ds_ievent_fini(disp);
+	ds_display_destroy(disp);
+}
+
+/* Test ds_ievent_post_kbd() */
+PCUT_TEST(ievent_post_kbd)
+{
+	ds_display_t *disp;
+	kbd_event_t kbd;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_ievent_init(disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	kbd.kbd_id = 0;
+	kbd.type = KEY_PRESS;
+	kbd.key = KC_ENTER;
+	kbd.mods = 0;
+	kbd.c = '\0';
+
+	rc = ds_ievent_post_kbd(disp, &kbd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ds_ievent_fini(disp);
+	ds_display_destroy(disp);
+}
+
+/* Test ds_ievent_post_ptd() */
+PCUT_TEST(ievent_post_ptd)
+{
+	ds_display_t *disp;
+	ptd_event_t ptd;
+	errno_t rc;
+
+	rc = ds_display_create(NULL, df_none, &disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	rc = ds_ievent_init(disp);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ptd.pos_id = 0;
+	ptd.type = PTD_MOVE;
+	ptd.dmove.x = 0;
+	ptd.dmove.y = 0;
+	ptd.apos.x = 0;
+	ptd.apos.y = 0;
+	ptd.abounds.p0.x = 0;
+	ptd.abounds.p0.y = 0;
+	ptd.abounds.p1.x = 0;
+	ptd.abounds.p1.y = 0;
+
+	rc = ds_ievent_post_ptd(disp, &ptd);
+	PCUT_ASSERT_ERRNO_VAL(EOK, rc);
+
+	ds_ievent_fini(disp);
+	ds_display_destroy(disp);
+}
+
+PCUT_EXPORT(ievent);
Index: uspace/srv/hid/display/test/main.c
===================================================================
--- uspace/srv/hid/display/test/main.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/display/test/main.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2023 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -36,4 +36,5 @@
 PCUT_IMPORT(cursor);
 PCUT_IMPORT(display);
+PCUT_IMPORT(ievent);
 PCUT_IMPORT(seat);
 PCUT_IMPORT(window);
Index: uspace/srv/hid/display/types/display/display.h
===================================================================
--- uspace/srv/hid/display/types/display/display.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/display/types/display/display.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2023 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -38,4 +38,5 @@
 
 #include <adt/list.h>
+#include <fibril.h>
 #include <fibril_synch.h>
 #include <gfx/color.h>
@@ -93,4 +94,16 @@
 	list_t idevcfgs;
 
+	/** Queue of input events */
+	list_t ievents;
+
+	/** Input event processing fibril ID */
+	fid_t ievent_fid;
+	/** Input event condition variable */
+	fibril_condvar_t ievent_cv;
+	/** Signal input event fibril to quit */
+	bool ievent_quit;
+	/** Input event fibril terminated */
+	bool ievent_done;
+
 	/** Background color */
 	gfx_color_t *bg_color;
Index: uspace/srv/hid/display/types/display/ievent.h
===================================================================
--- uspace/srv/hid/display/types/display/ievent.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
+++ uspace/srv/hid/display/types/display/ievent.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2024 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup display
+ * @{
+ */
+/**
+ * @file Display server input event types
+ */
+
+#ifndef TYPES_DISPLAY_IEVENT_H
+#define TYPES_DISPLAY_IEVENT_H
+
+#include <adt/list.h>
+#include <io/kbd_event.h>
+#include "ptd_event.h"
+
+/** Display server input event type */
+typedef enum {
+	/** Keyboard event */
+	det_kbd,
+	/** Pointing device event */
+	det_ptd
+} ds_ievent_type_t;
+
+/** Display server input event */
+typedef struct ds_ievent {
+	/** Parent display */
+	struct ds_display *display;
+	/** Link to display->ievents */
+	link_t lievents;
+	/** Input event type */
+	ds_ievent_type_t etype;
+	/** Event data */
+	union {
+		/** Keyboard event */
+		kbd_event_t kbd;
+		/** Pointing device ievent */
+		ptd_event_t ptd;
+	} ev;
+} ds_ievent_t;
+
+#endif
+
+/** @}
+ */
Index: uspace/srv/hid/display/window.c
===================================================================
--- uspace/srv/hid/display/window.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/display/window.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2023 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -153,8 +153,10 @@
 
 	/* Is this a popup window? */
-	if ((params->flags & wndf_popup) != 0)
+	if ((params->flags & wndf_popup) != 0) {
 		ds_seat_set_popup(seat, wnd);
-	else
-		ds_seat_set_focus(seat, wnd);
+	} else {
+		if ((params->flags & wndf_nofocus) == 0)
+			ds_seat_set_focus(seat, wnd);
+	}
 
 	/* Is this window a panel? */
Index: uspace/srv/hid/output/ctl/serial.c
===================================================================
--- uspace/srv/hid/output/ctl/serial.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/output/ctl/serial.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2006 Ondrej Palkovsky
  * Copyright (c) 2008 Martin Decky
@@ -37,10 +38,24 @@
 #include <errno.h>
 #include <io/chargrid.h>
+#include <vt/vt100.h>
 #include "../output.h"
-#include "../proto/vt100.h"
 #include "serial.h"
 
 #define SERIAL_COLS  80
 #define SERIAL_ROWS  24
+
+static serial_putuchar_t serial_putuchar_fn;
+static serial_control_puts_t serial_control_puts_fn;
+static serial_flush_t serial_flush_fn;
+
+static void serial_vt_putuchar(void *, char32_t);
+static void serial_vt_control_puts(void *, const char *);
+static void serial_vt_flush(void *);
+
+static vt100_cb_t serial_vt_cb = {
+	.putuchar = serial_vt_putuchar,
+	.control_puts = serial_vt_control_puts,
+	.flush = serial_vt_flush
+};
 
 /** Draw the character at the specified position.
@@ -52,5 +67,5 @@
  *
  */
-static void draw_char(vt100_state_t *state, charfield_t *field,
+static void draw_char(vt100_t *state, charfield_t *field,
     sysarg_t col, sysarg_t row)
 {
@@ -62,5 +77,5 @@
 static errno_t serial_yield(outdev_t *dev)
 {
-	vt100_state_t *state = (vt100_state_t *) dev->data;
+	vt100_t *state = (vt100_t *) dev->data;
 
 	return vt100_yield(state);
@@ -69,5 +84,5 @@
 static errno_t serial_claim(outdev_t *dev)
 {
-	vt100_state_t *state = (vt100_state_t *) dev->data;
+	vt100_t *state = (vt100_t *) dev->data;
 
 	return vt100_claim(state);
@@ -77,5 +92,5 @@
     sysarg_t *rows)
 {
-	vt100_state_t *state = (vt100_state_t *) dev->data;
+	vt100_t *state = (vt100_t *) dev->data;
 
 	vt100_get_dimensions(state, cols, rows);
@@ -84,5 +99,6 @@
 static console_caps_t serial_get_caps(outdev_t *dev)
 {
-	return (CONSOLE_CAP_STYLE | CONSOLE_CAP_INDEXED);
+	return (CONSOLE_CAP_CURSORCTL | CONSOLE_CAP_STYLE |
+	    CONSOLE_CAP_INDEXED | CONSOLE_CAP_RGB);
 }
 
@@ -90,5 +106,5 @@
     sysarg_t prev_row, sysarg_t col, sysarg_t row, bool visible)
 {
-	vt100_state_t *state = (vt100_state_t *) dev->data;
+	vt100_t *state = (vt100_t *) dev->data;
 
 	vt100_goto(state, col, row);
@@ -98,5 +114,5 @@
 static void serial_char_update(outdev_t *dev, sysarg_t col, sysarg_t row)
 {
-	vt100_state_t *state = (vt100_state_t *) dev->data;
+	vt100_t *state = (vt100_t *) dev->data;
 	charfield_t *field =
 	    chargrid_charfield_at(dev->backbuf, col, row);
@@ -107,5 +123,5 @@
 static void serial_flush(outdev_t *dev)
 {
-	vt100_state_t *state = (vt100_state_t *) dev->data;
+	vt100_t *state = (vt100_t *) dev->data;
 
 	vt100_flush(state);
@@ -122,16 +138,28 @@
 };
 
-errno_t serial_init(vt100_putuchar_t putuchar_fn,
-    vt100_control_puts_t control_puts_fn, vt100_flush_t flush_fn)
+errno_t serial_init(serial_putuchar_t putuchar_fn,
+    serial_control_puts_t control_puts_fn, serial_flush_t flush_fn)
 {
-	vt100_state_t *state =
-	    vt100_state_create(SERIAL_COLS, SERIAL_ROWS, putuchar_fn,
-	    control_puts_fn, flush_fn);
-	if (state == NULL)
+	char_attrs_t attrs;
+	vt100_t *vt100;
+
+	serial_putuchar_fn = putuchar_fn;
+	serial_control_puts_fn = control_puts_fn;
+	serial_flush_fn = flush_fn;
+
+	vt100 = vt100_create(NULL, SERIAL_COLS, SERIAL_ROWS, &serial_vt_cb);
+	if (vt100 == NULL)
 		return ENOMEM;
+	vt100->enable_rgb = true;
 
-	outdev_t *dev = outdev_register(&serial_ops, state);
+	vt100_cursor_visibility(vt100, false);
+	attrs.type = CHAR_ATTR_STYLE;
+	attrs.val.style = STYLE_NORMAL;
+	vt100_set_attr(vt100, attrs);
+	vt100_cls(vt100);
+
+	outdev_t *dev = outdev_register(&serial_ops, vt100);
 	if (dev == NULL) {
-		vt100_state_destroy(state);
+		vt100_destroy(vt100);
 		return ENOMEM;
 	}
@@ -140,4 +168,22 @@
 }
 
+static void serial_vt_putuchar(void *arg, char32_t c)
+{
+	(void)arg;
+	serial_putuchar_fn(c);
+}
+
+static void serial_vt_control_puts(void *arg, const char *str)
+{
+	(void)arg;
+	serial_control_puts_fn(str);
+}
+
+static void serial_vt_flush(void *arg)
+{
+	(void)arg;
+	serial_flush_fn();
+}
+
 /** @}
  */
Index: uspace/srv/hid/output/ctl/serial.h
===================================================================
--- uspace/srv/hid/output/ctl/serial.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/output/ctl/serial.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2006 Ondrej Palkovsky
  * Copyright (c) 2008 Martin Decky
@@ -35,7 +36,12 @@
 #define OUTPUT_CTL_SERIAL_H_
 
-#include "../proto/vt100.h"
+#include <vt/vt100.h>
 
-extern errno_t serial_init(vt100_putuchar_t, vt100_control_puts_t, vt100_flush_t);
+typedef void (*serial_putuchar_t)(char32_t);
+typedef void (*serial_control_puts_t)(const char *);
+typedef void (*serial_flush_t)(void);
+
+extern errno_t serial_init(serial_putuchar_t, serial_control_puts_t,
+    serial_flush_t);
 
 #endif
Index: uspace/srv/hid/output/meson.build
===================================================================
--- uspace/srv/hid/output/meson.build	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/output/meson.build	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,3 +1,4 @@
 #
+# Copyright (c) 2024 Jiri Svoboda
 # Copyright (c) 2005 Martin Decky
 # Copyright (c) 2007 Jakub Jermar
@@ -28,5 +29,6 @@
 #
 
-deps = [ 'codepage', 'console', 'drv', 'fbfont', 'pixconv', 'ddev', 'output' ]
+deps = [ 'codepage', 'console', 'drv', 'fbfont', 'pixconv', 'ddev', 'output',
+    'vt' ]
 src = files(
 	'ctl/serial.c',
@@ -34,5 +36,4 @@
 	'port/chardev.c',
 	'port/ddev.c',
-	'proto/vt100.c',
 	'output.c',
 )
Index: uspace/srv/hid/output/output.c
===================================================================
--- uspace/srv/hid/output/output.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/output/output.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2023 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2011 Martin Decky
  * All rights reserved.
@@ -40,4 +40,5 @@
 #include <ipc/output.h>
 #include <config.h>
+#include <vt/vt100.h>
 #include "port/ega.h"
 #include "port/chardev.h"
Index: uspace/srv/hid/output/output.h
===================================================================
--- uspace/srv/hid/output/output.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/output/output.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2011 Martin Decky
  * All rights reserved.
@@ -34,4 +35,5 @@
 #define OUTPUT_OUTPUT_H_
 
+#include <adt/list.h>
 #include <stdbool.h>
 #include <loc.h>
Index: uspace/srv/hid/output/port/chardev.c
===================================================================
--- uspace/srv/hid/output/port/chardev.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/output/port/chardev.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,5 +1,5 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2016 Jakub Jermar
- * Copyright (c) 2017 Jiri Svoboda
  * All rights reserved.
  *
Index: uspace/srv/hid/output/port/ddev.c
===================================================================
--- uspace/srv/hid/output/port/ddev.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/output/port/ddev.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2020 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2008 Martin Decky
  * Copyright (c) 2006 Jakub Vana
@@ -198,5 +198,6 @@
 static console_caps_t output_ddev_get_caps(outdev_t *dev)
 {
-	return (CONSOLE_CAP_STYLE | CONSOLE_CAP_INDEXED | CONSOLE_CAP_RGB);
+	return (CONSOLE_CAP_CURSORCTL | CONSOLE_CAP_STYLE |
+	    CONSOLE_CAP_INDEXED | CONSOLE_CAP_RGB);
 }
 
Index: uspace/srv/hid/output/port/ega.c
===================================================================
--- uspace/srv/hid/output/port/ega.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/output/port/ega.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -143,5 +143,6 @@
 static console_caps_t ega_get_caps(outdev_t *dev)
 {
-	return (CONSOLE_CAP_STYLE | CONSOLE_CAP_INDEXED);
+	return (CONSOLE_CAP_CURSORCTL | CONSOLE_CAP_STYLE |
+	    CONSOLE_CAP_INDEXED);
 }
 
Index: uspace/srv/hid/output/proto/vt100.c
===================================================================
--- uspace/srv/hid/output/proto/vt100.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ 	(revision )
@@ -1,249 +1,0 @@
-/*
- * Copyright (c) 2021 Jiri Svoboda
- * Copyright (c) 2011 Martin Decky
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- * - Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- * - The name of the author may not be used to endorse or promote products
- *   derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/** @addtogroup output
- * @{
- */
-
-#include <inttypes.h>
-#include <errno.h>
-#include <stddef.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <io/color.h>
-#include <types/common.h>
-#include "vt100.h"
-
-/** Buffer size when creating actual VT100 commands.
- *
- * This is absurdly large but since we accept numbers via sysarg_t,
- * we make it big enough for the largest value to be on the safe side
- * (and to silence compiler too).
- *
- * TODO: find out if VT100 has some hard limits or perhaps simply cut-out
- * values larger than 16 bits or something.
- */
-#define MAX_CONTROL 64
-
-typedef enum {
-	CI_BLACK   = 0,
-	CI_RED     = 1,
-	CI_GREEN   = 2,
-	CI_BROWN   = 3,
-	CI_BLUE    = 4,
-	CI_MAGENTA = 5,
-	CI_CYAN    = 6,
-	CI_WHITE   = 7
-} sgr_color_index_t;
-
-typedef enum {
-	SGR_RESET       = 0,
-	SGR_BOLD        = 1,
-	SGR_UNDERLINE   = 4,
-	SGR_BLINK       = 5,
-	SGR_REVERSE     = 7,
-	SGR_FGCOLOR     = 30,
-	SGR_BGCOLOR     = 40
-} sgr_command_t;
-
-static sgr_color_index_t color_map[] = {
-	[COLOR_BLACK]   = CI_BLACK,
-	[COLOR_BLUE]    = CI_BLUE,
-	[COLOR_GREEN]   = CI_GREEN,
-	[COLOR_CYAN]    = CI_CYAN,
-	[COLOR_RED]     = CI_RED,
-	[COLOR_MAGENTA] = CI_MAGENTA,
-	[COLOR_YELLOW]  = CI_BROWN,
-	[COLOR_WHITE]   = CI_WHITE
-};
-
-/** ECMA-48 Set Graphics Rendition. */
-static void vt100_sgr(vt100_state_t *state, unsigned int mode)
-{
-	char control[MAX_CONTROL];
-
-	snprintf(control, MAX_CONTROL, "\033[%um", mode);
-	state->control_puts(control);
-}
-
-static void vt100_set_pos(vt100_state_t *state, sysarg_t col, sysarg_t row)
-{
-	char control[MAX_CONTROL];
-
-	snprintf(control, MAX_CONTROL, "\033[%" PRIun ";%" PRIun "f",
-	    row + 1, col + 1);
-	state->control_puts(control);
-}
-
-static void vt100_set_sgr(vt100_state_t *state, char_attrs_t attrs)
-{
-	switch (attrs.type) {
-	case CHAR_ATTR_STYLE:
-		switch (attrs.val.style) {
-		case STYLE_NORMAL:
-			vt100_sgr(state, SGR_RESET);
-			vt100_sgr(state, SGR_BGCOLOR + CI_WHITE);
-			vt100_sgr(state, SGR_FGCOLOR + CI_BLACK);
-			break;
-		case STYLE_EMPHASIS:
-			vt100_sgr(state, SGR_RESET);
-			vt100_sgr(state, SGR_BGCOLOR + CI_WHITE);
-			vt100_sgr(state, SGR_FGCOLOR + CI_RED);
-			vt100_sgr(state, SGR_BOLD);
-			break;
-		case STYLE_INVERTED:
-			vt100_sgr(state, SGR_RESET);
-			vt100_sgr(state, SGR_BGCOLOR + CI_BLACK);
-			vt100_sgr(state, SGR_FGCOLOR + CI_WHITE);
-			break;
-		case STYLE_SELECTED:
-			vt100_sgr(state, SGR_RESET);
-			vt100_sgr(state, SGR_BGCOLOR + CI_RED);
-			vt100_sgr(state, SGR_FGCOLOR + CI_WHITE);
-			break;
-		}
-		break;
-	case CHAR_ATTR_INDEX:
-		vt100_sgr(state, SGR_RESET);
-		vt100_sgr(state, SGR_BGCOLOR + color_map[attrs.val.index.bgcolor & 7]);
-		vt100_sgr(state, SGR_FGCOLOR + color_map[attrs.val.index.fgcolor & 7]);
-
-		if (attrs.val.index.attr & CATTR_BRIGHT)
-			vt100_sgr(state, SGR_BOLD);
-
-		break;
-	case CHAR_ATTR_RGB:
-		vt100_sgr(state, SGR_RESET);
-
-		if (attrs.val.rgb.bgcolor <= attrs.val.rgb.fgcolor)
-			vt100_sgr(state, SGR_REVERSE);
-
-		break;
-	}
-}
-
-vt100_state_t *vt100_state_create(sysarg_t cols, sysarg_t rows,
-    vt100_putuchar_t putuchar_fn, vt100_control_puts_t control_puts_fn,
-    vt100_flush_t flush_fn)
-{
-	vt100_state_t *state = malloc(sizeof(vt100_state_t));
-	if (state == NULL)
-		return NULL;
-
-	state->putuchar = putuchar_fn;
-	state->control_puts = control_puts_fn;
-	state->flush = flush_fn;
-
-	state->cols = cols;
-	state->rows = rows;
-
-	state->cur_col = (sysarg_t) -1;
-	state->cur_row = (sysarg_t) -1;
-
-	state->cur_attrs.type = CHAR_ATTR_STYLE;
-	state->cur_attrs.val.style = STYLE_NORMAL;
-
-	/* Initialize graphic rendition attributes */
-	vt100_sgr(state, SGR_RESET);
-	vt100_sgr(state, SGR_FGCOLOR + CI_BLACK);
-	vt100_sgr(state, SGR_BGCOLOR + CI_WHITE);
-	state->control_puts("\033[2J");
-	state->control_puts("\033[?25l");
-
-	return state;
-}
-
-void vt100_state_destroy(vt100_state_t *state)
-{
-	free(state);
-}
-
-void vt100_get_dimensions(vt100_state_t *state, sysarg_t *cols,
-    sysarg_t *rows)
-{
-	*cols = state->cols;
-	*rows = state->rows;
-}
-
-errno_t vt100_yield(vt100_state_t *state)
-{
-	return EOK;
-}
-
-errno_t vt100_claim(vt100_state_t *state)
-{
-	return EOK;
-}
-
-void vt100_goto(vt100_state_t *state, sysarg_t col, sysarg_t row)
-{
-	if ((col >= state->cols) || (row >= state->rows))
-		return;
-
-	if ((col != state->cur_col) || (row != state->cur_row)) {
-		vt100_set_pos(state, col, row);
-		state->cur_col = col;
-		state->cur_row = row;
-	}
-}
-
-void vt100_set_attr(vt100_state_t *state, char_attrs_t attrs)
-{
-	if (!attrs_same(state->cur_attrs, attrs)) {
-		vt100_set_sgr(state, attrs);
-		state->cur_attrs = attrs;
-	}
-}
-
-void vt100_cursor_visibility(vt100_state_t *state, bool visible)
-{
-	if (visible)
-		state->control_puts("\033[?25h");
-	else
-		state->control_puts("\033[?25l");
-}
-
-void vt100_putuchar(vt100_state_t *state, char32_t ch)
-{
-	state->putuchar(ch == 0 ? ' ' : ch);
-	state->cur_col++;
-
-	if (state->cur_col >= state->cols) {
-		state->cur_row += state->cur_col / state->cols;
-		state->cur_col %= state->cols;
-	}
-}
-
-void vt100_flush(vt100_state_t *state)
-{
-	state->flush();
-}
-
-/** @}
- */
Index: uspace/srv/hid/output/proto/vt100.h
===================================================================
--- uspace/srv/hid/output/proto/vt100.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ 	(revision )
@@ -1,72 +1,0 @@
-/*
- * Copyright (c) 2011 Martin Decky
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- * - Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- * - The name of the author may not be used to endorse or promote products
- *   derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/** @addtogroup output
- * @{
- */
-
-#ifndef OUTPUT_PROTO_VT100_H_
-#define OUTPUT_PROTO_VT100_H_
-
-#include <io/charfield.h>
-
-typedef void (*vt100_putuchar_t)(char32_t ch);
-typedef void (*vt100_control_puts_t)(const char *str);
-typedef void (*vt100_flush_t)(void);
-
-typedef struct {
-	sysarg_t cols;
-	sysarg_t rows;
-
-	sysarg_t cur_col;
-	sysarg_t cur_row;
-	char_attrs_t cur_attrs;
-
-	vt100_putuchar_t putuchar;
-	vt100_control_puts_t control_puts;
-	vt100_flush_t flush;
-} vt100_state_t;
-
-extern vt100_state_t *vt100_state_create(sysarg_t, sysarg_t, vt100_putuchar_t,
-    vt100_control_puts_t, vt100_flush_t);
-extern void vt100_state_destroy(vt100_state_t *);
-
-extern errno_t vt100_yield(vt100_state_t *);
-extern errno_t vt100_claim(vt100_state_t *);
-extern void vt100_get_dimensions(vt100_state_t *, sysarg_t *, sysarg_t *);
-
-extern void vt100_goto(vt100_state_t *, sysarg_t, sysarg_t);
-extern void vt100_set_attr(vt100_state_t *, char_attrs_t);
-extern void vt100_cursor_visibility(vt100_state_t *, bool);
-extern void vt100_putuchar(vt100_state_t *, char32_t);
-extern void vt100_flush(vt100_state_t *);
-
-#endif
-
-/** @}
- */
Index: uspace/srv/hid/remcons/meson.build
===================================================================
--- uspace/srv/hid/remcons/meson.build	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/remcons/meson.build	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 #
-# Copyright (c) 2021 Jiri Svoboda
+# Copyright (c) 2024 Jiri Svoboda
 # Copyright (c) 2012 Vojtech Horky
 # All rights reserved.
@@ -28,4 +28,4 @@
 #
 
-deps = [ 'inet', 'console' ]
+deps = [ 'inet', 'console', 'vt' ]
 src = files('remcons.c', 'user.c')
Index: uspace/srv/hid/remcons/remcons.c
===================================================================
--- uspace/srv/hid/remcons/remcons.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/remcons/remcons.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2023 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2012 Vojtech Horky
  * All rights reserved.
@@ -34,4 +34,6 @@
  */
 
+#include <adt/list.h>
+#include <as.h>
 #include <async.h>
 #include <errno.h>
@@ -51,9 +53,13 @@
 #include <inttypes.h>
 #include <str.h>
+#include <vt/vt100.h>
 #include "telnet.h"
 #include "user.h"
+#include "remcons.h"
 
 #define APP_GETTERM  "/app/getterm"
 #define APP_SHELL "/app/bdsh"
+
+#define DEF_PORT 2223
 
 /** Telnet commands to force character mode
@@ -74,4 +80,5 @@
 static errno_t remcons_open(con_srvs_t *, con_srv_t *);
 static errno_t remcons_close(con_srv_t *);
+static errno_t remcons_read(con_srv_t *, void *, size_t, size_t *);
 static errno_t remcons_write(con_srv_t *, void *, size_t, size_t *);
 static void remcons_sync(con_srv_t *);
@@ -81,10 +88,22 @@
 static errno_t remcons_get_size(con_srv_t *, sysarg_t *, sysarg_t *);
 static errno_t remcons_get_color_cap(con_srv_t *, console_caps_t *);
+static void remcons_set_style(con_srv_t *, console_style_t);
+static void remcons_set_color(con_srv_t *, console_color_t,
+    console_color_t, console_color_attr_t);
+static void remcons_set_color(con_srv_t *, console_color_t,
+    console_color_t, console_color_attr_t);
+static void remcons_set_rgb_color(con_srv_t *, pixel_t, pixel_t);
+static void remcons_cursor_visibility(con_srv_t *, bool);
+static errno_t remcons_set_caption(con_srv_t *, const char *);
 static errno_t remcons_get_event(con_srv_t *, cons_event_t *);
+static errno_t remcons_map(con_srv_t *, sysarg_t, sysarg_t, charfield_t **);
+static void remcons_unmap(con_srv_t *);
+static void remcons_update(con_srv_t *, sysarg_t, sysarg_t, sysarg_t,
+    sysarg_t);
 
 static con_ops_t con_ops = {
 	.open = remcons_open,
 	.close = remcons_close,
-	.read = NULL,
+	.read = remcons_read,
 	.write = remcons_write,
 	.sync = remcons_sync,
@@ -94,9 +113,27 @@
 	.get_size = remcons_get_size,
 	.get_color_cap = remcons_get_color_cap,
-	.set_style = NULL,
-	.set_color = NULL,
-	.set_rgb_color = NULL,
-	.set_cursor_visibility = NULL,
-	.get_event = remcons_get_event
+	.set_style = remcons_set_style,
+	.set_color = remcons_set_color,
+	.set_rgb_color = remcons_set_rgb_color,
+	.set_cursor_visibility = remcons_cursor_visibility,
+	.set_caption = remcons_set_caption,
+	.get_event = remcons_get_event,
+	.map = remcons_map,
+	.unmap = remcons_unmap,
+	.update = remcons_update
+};
+
+static void remcons_vt_putchar(void *, char32_t);
+static void remcons_vt_cputs(void *, const char *);
+static void remcons_vt_flush(void *);
+static void remcons_vt_key(void *, keymod_t, keycode_t, char);
+static void remcons_vt_pos_event(void *, pos_event_t *);
+
+static vt100_cb_t remcons_vt_cb = {
+	.putuchar = remcons_vt_putchar,
+	.control_puts = remcons_vt_cputs,
+	.flush = remcons_vt_flush,
+	.key = remcons_vt_key,
+	.pos_event = remcons_vt_pos_event
 };
 
@@ -111,9 +148,24 @@
 };
 
+static void remcons_telnet_ws_update(void *, unsigned, unsigned);
+
+static telnet_cb_t remcons_telnet_cb = {
+	.ws_update = remcons_telnet_ws_update
+};
+
 static loc_srv_t *remcons_srv;
+static bool no_ctl;
+static bool no_rgb;
 
 static telnet_user_t *srv_to_user(con_srv_t *srv)
 {
-	return srv->srvs->sarg;
+	remcons_t *remcons = (remcons_t *)srv->srvs->sarg;
+	return remcons->user;
+}
+
+static remcons_t *srv_to_remcons(con_srv_t *srv)
+{
+	remcons_t *remcons = (remcons_t *)srv->srvs->sarg;
+	return remcons;
 }
 
@@ -141,13 +193,30 @@
 }
 
-static errno_t remcons_write(con_srv_t *srv, void *data, size_t size, size_t *nwritten)
+static errno_t remcons_read(con_srv_t *srv, void *data, size_t size,
+    size_t *nread)
 {
 	telnet_user_t *user = srv_to_user(srv);
 	errno_t rc;
 
-	rc = telnet_user_send_data(user, data, size);
+	rc = telnet_user_recv(user, data, size, nread);
 	if (rc != EOK)
 		return rc;
 
+	return EOK;
+}
+
+static errno_t remcons_write(con_srv_t *srv, void *data, size_t size, size_t *nwritten)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+	errno_t rc;
+
+	rc = telnet_user_send_data(remcons->user, data, size);
+	if (rc != EOK)
+		return rc;
+
+	rc = telnet_user_flush(remcons->user);
+	if (rc != EOK)
+		return rc;
+
 	*nwritten = size;
 	return EOK;
@@ -161,12 +230,27 @@
 static void remcons_clear(con_srv_t *srv)
 {
-	(void) srv;
+	remcons_t *remcons = srv_to_remcons(srv);
+
+	if (remcons->enable_ctl) {
+		vt100_cls(remcons->vt);
+		vt100_set_pos(remcons->vt, 0, 0);
+		remcons->user->cursor_x = 0;
+		remcons->user->cursor_y = 0;
+	}
 }
 
 static void remcons_set_pos(con_srv_t *srv, sysarg_t col, sysarg_t row)
 {
+	remcons_t *remcons = srv_to_remcons(srv);
 	telnet_user_t *user = srv_to_user(srv);
 
-	telnet_user_update_cursor_x(user, col);
+	if (remcons->enable_ctl) {
+		vt100_set_pos(remcons->vt, col, row);
+		remcons->user->cursor_x = col;
+		remcons->user->cursor_y = row;
+		(void)telnet_user_flush(remcons->user);
+	} else {
+		telnet_user_update_cursor_x(user, col);
+	}
 }
 
@@ -176,5 +260,5 @@
 
 	*col = user->cursor_x;
-	*row = 0;
+	*row = user->cursor_y;
 
 	return EOK;
@@ -183,8 +267,13 @@
 static errno_t remcons_get_size(con_srv_t *srv, sysarg_t *cols, sysarg_t *rows)
 {
-	(void) srv;
-
-	*cols = 100;
-	*rows = 1;
+	remcons_t *remcons = srv_to_remcons(srv);
+
+	if (remcons->enable_ctl) {
+		*cols = remcons->vt->cols;
+		*rows = remcons->vt->rows;
+	} else {
+		*cols = 100;
+		*rows = 1;
+	}
 
 	return EOK;
@@ -193,27 +282,265 @@
 static errno_t remcons_get_color_cap(con_srv_t *srv, console_caps_t *ccaps)
 {
-	(void) srv;
-	*ccaps = CONSOLE_CAP_NONE;
-
-	return EOK;
+	remcons_t *remcons = srv_to_remcons(srv);
+
+	*ccaps = 0;
+
+	if (remcons->enable_ctl) {
+		*ccaps |= CONSOLE_CAP_CURSORCTL | CONSOLE_CAP_STYLE |
+		    CONSOLE_CAP_INDEXED;
+	}
+
+	if (remcons->enable_rgb)
+		*ccaps |= CONSOLE_CAP_RGB;
+
+	return EOK;
+}
+
+static void remcons_set_style(con_srv_t *srv, console_style_t style)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+	char_attrs_t attrs;
+
+	if (remcons->enable_ctl) {
+		attrs.type = CHAR_ATTR_STYLE;
+		attrs.val.style = style;
+		vt100_set_attr(remcons->vt, attrs);
+	}
+}
+
+static void remcons_set_color(con_srv_t *srv, console_color_t bgcolor,
+    console_color_t fgcolor, console_color_attr_t flags)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+	char_attrs_t attrs;
+
+	if (remcons->enable_ctl) {
+		attrs.type = CHAR_ATTR_INDEX;
+		attrs.val.index.bgcolor = bgcolor;
+		attrs.val.index.fgcolor = fgcolor;
+		attrs.val.index.attr = flags;
+		vt100_set_attr(remcons->vt, attrs);
+	}
+}
+
+static void remcons_set_rgb_color(con_srv_t *srv, pixel_t bgcolor,
+    pixel_t fgcolor)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+	char_attrs_t attrs;
+
+	if (remcons->enable_ctl) {
+		attrs.type = CHAR_ATTR_RGB;
+		attrs.val.rgb.bgcolor = bgcolor;
+		attrs.val.rgb.fgcolor = fgcolor;
+		vt100_set_attr(remcons->vt, attrs);
+	}
+}
+
+static void remcons_cursor_visibility(con_srv_t *srv, bool visible)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+
+	if (remcons->enable_ctl) {
+		if (!remcons->curs_visible && visible) {
+			vt100_set_pos(remcons->vt, remcons->user->cursor_x,
+			    remcons->user->cursor_y);
+		}
+		vt100_cursor_visibility(remcons->vt, visible);
+	}
+
+	remcons->curs_visible = visible;
+}
+
+static errno_t remcons_set_caption(con_srv_t *srv, const char *caption)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+
+	if (remcons->enable_ctl) {
+		vt100_set_title(remcons->vt, caption);
+	}
+
+	return EOK;
+}
+
+/** Creates new keyboard event from given char.
+ *
+ * @param type Event type (press / release).
+ * @param c Pressed character.
+ */
+static remcons_event_t *new_kbd_event(kbd_event_type_t type, keymod_t mods,
+    keycode_t key, char c)
+{
+	remcons_event_t *event = malloc(sizeof(remcons_event_t));
+	if (event == NULL) {
+		fprintf(stderr, "Out of memory.\n");
+		return NULL;
+	}
+
+	link_initialize(&event->link);
+	event->cev.type = CEV_KEY;
+	event->cev.ev.key.type = type;
+	event->cev.ev.key.mods = mods;
+	event->cev.ev.key.key = key;
+	event->cev.ev.key.c = c;
+
+	return event;
+}
+
+/** Creates new position event.
+ *
+ * @param ev Position event.
+ * @param c Pressed character.
+ */
+static remcons_event_t *new_pos_event(pos_event_t *ev)
+{
+	remcons_event_t *event = malloc(sizeof(remcons_event_t));
+	if (event == NULL) {
+		fprintf(stderr, "Out of memory.\n");
+		return NULL;
+	}
+
+	link_initialize(&event->link);
+	event->cev.type = CEV_POS;
+	event->cev.ev.pos = *ev;
+
+	return event;
+}
+
+/** Creates new console resize event.
+ */
+static remcons_event_t *new_resize_event(void)
+{
+	remcons_event_t *event = malloc(sizeof(remcons_event_t));
+	if (event == NULL) {
+		fprintf(stderr, "Out of memory.\n");
+		return NULL;
+	}
+
+	link_initialize(&event->link);
+	event->cev.type = CEV_RESIZE;
+
+	return event;
 }
 
 static errno_t remcons_get_event(con_srv_t *srv, cons_event_t *event)
 {
+	remcons_t *remcons = srv_to_remcons(srv);
 	telnet_user_t *user = srv_to_user(srv);
-	kbd_event_t kevent;
-	errno_t rc;
-
-	rc = telnet_user_get_next_keyboard_event(user, &kevent);
-	if (rc != EOK) {
-		/* XXX What? */
-		memset(event, 0, sizeof(*event));
-		return EOK;
-	}
-
-	event->type = CEV_KEY;
-	event->ev.key = kevent;
-
-	return EOK;
+	size_t nread;
+
+	while (list_empty(&remcons->in_events)) {
+		char next_byte = 0;
+
+		errno_t rc = telnet_user_recv(user, &next_byte, 1,
+		    &nread);
+		if (rc != EOK)
+			return rc;
+
+		vt100_rcvd_char(remcons->vt, next_byte);
+	}
+
+	link_t *link = list_first(&remcons->in_events);
+	list_remove(link);
+
+	remcons_event_t *tmp = list_get_instance(link, remcons_event_t, link);
+
+	*event = tmp->cev;
+	free(tmp);
+
+	return EOK;
+}
+
+static errno_t remcons_map(con_srv_t *srv, sysarg_t cols, sysarg_t rows,
+    charfield_t **rbuf)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+	void *buf;
+
+	if (!remcons->enable_ctl)
+		return ENOTSUP;
+
+	if (remcons->ubuf != NULL)
+		return EBUSY;
+
+	buf = as_area_create(AS_AREA_ANY, cols * rows * sizeof(charfield_t),
+	    AS_AREA_READ | AS_AREA_WRITE | AS_AREA_CACHEABLE, AS_AREA_UNPAGED);
+	if (buf == AS_MAP_FAILED)
+		return ENOMEM;
+
+	remcons->ucols = cols;
+	remcons->urows = rows;
+	remcons->ubuf = buf;
+
+	*rbuf = buf;
+	return EOK;
+
+}
+
+static void remcons_unmap(con_srv_t *srv)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+	void *buf;
+
+	buf = remcons->ubuf;
+	remcons->ubuf = NULL;
+
+	if (buf != NULL)
+		as_area_destroy(buf);
+}
+
+static void remcons_update(con_srv_t *srv, sysarg_t c0, sysarg_t r0,
+    sysarg_t c1, sysarg_t r1)
+{
+	remcons_t *remcons = srv_to_remcons(srv);
+	charfield_t *ch;
+	sysarg_t col, row;
+	sysarg_t old_x, old_y;
+
+	if (remcons->ubuf == NULL)
+		return;
+
+	/* Make sure we have meaningful coordinates, within bounds */
+
+	if (c1 > remcons->ucols)
+		c1 = remcons->ucols;
+	if (c1 > remcons->user->cols)
+		c1 = remcons->user->cols;
+	if (c0 >= c1)
+		return;
+
+	if (r1 > remcons->urows)
+		r1 = remcons->urows;
+	if (r1 > remcons->user->rows)
+		r1 = remcons->user->rows;
+	if (r0 >= r1)
+		return;
+
+	/* Update screen from user buffer */
+
+	old_x = remcons->user->cursor_x;
+	old_y = remcons->user->cursor_y;
+
+	if (remcons->curs_visible)
+		vt100_cursor_visibility(remcons->vt, false);
+
+	for (row = r0; row < r1; row++) {
+		for (col = c0; col < c1; col++) {
+			vt100_set_pos(remcons->vt, col, row);
+			ch = &remcons->ubuf[row * remcons->ucols + col];
+			vt100_set_attr(remcons->vt, ch->attrs);
+			vt100_putuchar(remcons->vt, ch->ch);
+		}
+	}
+
+	if (remcons->curs_visible) {
+		old_x = remcons->user->cursor_x = old_x;
+		remcons->user->cursor_y = old_y;
+		vt100_set_pos(remcons->vt, old_x, old_y);
+		vt100_cursor_visibility(remcons->vt, true);
+	}
+
+	/* Flush data */
+	(void)telnet_user_flush(remcons->user);
 }
 
@@ -248,15 +575,15 @@
 		    "failed: %s.", APP_GETTERM, user->service_name, APP_SHELL,
 		    str_error(rc));
-		fibril_mutex_lock(&user->guard);
+		fibril_mutex_lock(&user->recv_lock);
 		user->task_finished = true;
 		user->srvs.aborted = true;
 		fibril_condvar_signal(&user->refcount_cv);
-		fibril_mutex_unlock(&user->guard);
+		fibril_mutex_unlock(&user->recv_lock);
 		return EOK;
 	}
 
-	fibril_mutex_lock(&user->guard);
+	fibril_mutex_lock(&user->recv_lock);
 	user->task_id = task;
-	fibril_mutex_unlock(&user->guard);
+	fibril_mutex_unlock(&user->recv_lock);
 
 	task_exit_t task_exit;
@@ -268,9 +595,9 @@
 
 	/* Announce destruction. */
-	fibril_mutex_lock(&user->guard);
+	fibril_mutex_lock(&user->recv_lock);
 	user->task_finished = true;
 	user->srvs.aborted = true;
 	fibril_condvar_signal(&user->refcount_cv);
-	fibril_mutex_unlock(&user->guard);
+	fibril_mutex_unlock(&user->recv_lock);
 
 	return EOK;
@@ -285,4 +612,83 @@
 	return user->task_finished && user->socket_closed &&
 	    (user->locsrv_connection_count == 0);
+}
+
+static void remcons_vt_putchar(void *arg, char32_t c)
+{
+	remcons_t *remcons = (remcons_t *)arg;
+	char buf[STR_BOUNDS(1)];
+	size_t off;
+	errno_t rc;
+
+	(void)arg;
+
+	off = 0;
+	rc = chr_encode(c, buf, &off, sizeof(buf));
+	if (rc != EOK)
+		return;
+
+	(void)telnet_user_send_data(remcons->user, buf, off);
+}
+
+static void remcons_vt_cputs(void *arg, const char *str)
+{
+	remcons_t *remcons = (remcons_t *)arg;
+
+	(void)telnet_user_send_raw(remcons->user, str, str_size(str));
+}
+
+static void remcons_vt_flush(void *arg)
+{
+	remcons_t *remcons = (remcons_t *)arg;
+	(void)telnet_user_flush(remcons->user);
+}
+
+static void remcons_vt_key(void *arg, keymod_t mods, keycode_t key, char c)
+{
+	remcons_t *remcons = (remcons_t *)arg;
+
+	remcons_event_t *down = new_kbd_event(KEY_PRESS, mods, key, c);
+	if (down == NULL)
+		return;
+
+	remcons_event_t *up = new_kbd_event(KEY_RELEASE, mods, key, c);
+	if (up == NULL) {
+		free(down);
+		return;
+	}
+
+	list_append(&down->link, &remcons->in_events);
+	list_append(&up->link, &remcons->in_events);
+}
+
+static void remcons_vt_pos_event(void *arg, pos_event_t *ev)
+{
+	remcons_t *remcons = (remcons_t *)arg;
+
+	remcons_event_t *cev = new_pos_event(ev);
+	if (cev == NULL)
+		return;
+
+	list_append(&cev->link, &remcons->in_events);
+}
+
+/** Window size update callback.
+ *
+ * @param arg Argument (remcons_t *)
+ * @param cols New number of columns
+ * @param rows New number of rows
+ */
+static void remcons_telnet_ws_update(void *arg, unsigned cols, unsigned rows)
+{
+	remcons_t *remcons = (remcons_t *)arg;
+
+	vt100_resize(remcons->vt, cols, rows);
+	telnet_user_resize(remcons->user, cols, rows);
+
+	remcons_event_t *resize = new_resize_event();
+	if (resize == NULL)
+		return;
+
+	list_append(&resize->link, &remcons->in_events);
 }
 
@@ -294,11 +700,57 @@
 static void remcons_new_conn(tcp_listener_t *lst, tcp_conn_t *conn)
 {
-	telnet_user_t *user = telnet_user_create(conn);
-	assert(user);
+	char_attrs_t attrs;
+	remcons_t *remcons = NULL;
+	telnet_user_t *user = NULL;
+
+	remcons = calloc(1, sizeof(remcons_t));
+	if (remcons == NULL) {
+		fprintf(stderr, "Out of memory.\n");
+		goto error;
+	}
+
+	user = telnet_user_create(conn, &remcons_telnet_cb,
+	    (void *)remcons);
+	if (user == NULL) {
+		fprintf(stderr, "Out of memory.\n");
+		goto error;
+	}
+
+	remcons->enable_ctl = !no_ctl;
+	remcons->enable_rgb = !no_ctl && !no_rgb;
+	remcons->user = user;
+	list_initialize(&remcons->in_events);
+
+	if (remcons->enable_ctl) {
+		user->cols = 80;
+		user->rows = 25;
+	} else {
+		user->cols = 100;
+		user->rows = 1;
+	}
+
+	remcons->curs_visible = true;
+
+	remcons->vt = vt100_create((void *)remcons, 80, 25, &remcons_vt_cb);
+	if (remcons->vt == NULL) {
+		fprintf(stderr, "Error creating VT100 driver instance.\n");
+		goto error;
+	}
+
+	remcons->vt->enable_rgb = remcons->enable_rgb;
+
+	if (remcons->enable_ctl) {
+		attrs.type = CHAR_ATTR_STYLE;
+		attrs.val.style = STYLE_NORMAL;
+		vt100_set_sgr(remcons->vt, attrs);
+		vt100_cls(remcons->vt);
+		vt100_set_pos(remcons->vt, 0, 0);
+		vt100_set_button_reporting(remcons->vt, true);
+	}
 
 	con_srvs_init(&user->srvs);
 	user->srvs.ops = &con_ops;
-	user->srvs.sarg = user;
-	user->srvs.abort_timeout = 1000;
+	user->srvs.sarg = remcons;
+	user->srvs.abort_timeout = 1000000;
 
 	telnet_user_add(user);
@@ -309,5 +761,5 @@
 		telnet_user_error(user, "Unable to register %s with loc: %s.",
 		    user->service_name, str_error(rc));
-		return;
+		goto error;
 	}
 
@@ -316,15 +768,16 @@
 
 	fid_t spawn_fibril = fibril_create(spawn_task_fibril, user);
-	assert(spawn_fibril);
+	if (spawn_fibril == 0) {
+		fprintf(stderr, "Failed creating fibril.\n");
+		goto error;
+	}
 	fibril_add_ready(spawn_fibril);
 
 	/* Wait for all clients to exit. */
-	fibril_mutex_lock(&user->guard);
+	fibril_mutex_lock(&user->recv_lock);
 	while (!user_can_be_destroyed_no_lock(user)) {
 		if (user->task_finished) {
-			user->conn = NULL;
 			user->socket_closed = true;
 			user->srvs.aborted = true;
-			continue;
 		} else if (user->socket_closed) {
 			if (user->task_id != 0) {
@@ -332,7 +785,8 @@
 			}
 		}
-		fibril_condvar_wait_timeout(&user->refcount_cv, &user->guard, 1000);
-	}
-	fibril_mutex_unlock(&user->guard);
+		fibril_condvar_wait_timeout(&user->refcount_cv,
+		    &user->recv_lock, 1000000);
+	}
+	fibril_mutex_unlock(&user->recv_lock);
 
 	rc = loc_service_unregister(remcons_srv, user->service_id);
@@ -344,5 +798,42 @@
 
 	telnet_user_log(user, "Destroying...");
+
+	if (remcons->enable_ctl) {
+		/* Disable mouse tracking */
+		vt100_set_button_reporting(remcons->vt, false);
+
+		/* Reset all character attributes and clear screen */
+		vt100_sgr(remcons->vt, 0);
+		vt100_cls(remcons->vt);
+		vt100_set_pos(remcons->vt, 0, 0);
+
+		telnet_user_flush(user);
+	}
+
+	tcp_conn_send_fin(user->conn);
+	user->conn = NULL;
+
 	telnet_user_destroy(user);
+	vt100_destroy(remcons->vt);
+	free(remcons);
+	return;
+error:
+	if (user != NULL && user->service_id != 0)
+		loc_service_unregister(remcons_srv, user->service_id);
+	if (user != NULL)
+		free(user);
+	if (remcons != NULL && remcons->vt != NULL)
+		vt100_destroy(remcons->vt);
+	if (remcons != NULL)
+		free(remcons);
+}
+
+static void print_syntax(void)
+{
+	fprintf(stderr, "syntax: remcons [<options>]\n");
+	fprintf(stderr, "\t--no-ctl      Disable all terminal control sequences\n");
+	fprintf(stderr, "\t--no-rgb      Disable RGB colors\n");
+	fprintf(stderr, "\t--port <port> Listening port (default: %u)\n",
+	    DEF_PORT);
 }
 
@@ -353,4 +844,45 @@
 	tcp_t *tcp;
 	inet_ep_t ep;
+	uint16_t port;
+	int i;
+
+	port = DEF_PORT;
+
+	i = 1;
+	while (i < argc) {
+		if (argv[i][0] == '-') {
+			if (str_cmp(argv[i], "--no-ctl") == 0) {
+				no_ctl = true;
+			} else if (str_cmp(argv[i], "--no-rgb") == 0) {
+				no_rgb = true;
+			} else if (str_cmp(argv[i], "--port") == 0) {
+				++i;
+				if (i >= argc) {
+					fprintf(stderr, "Option argument "
+					    "missing.\n");
+					print_syntax();
+					return EINVAL;
+				}
+				rc = str_uint16_t(argv[i], NULL, 10, true, &port);
+				if (rc != EOK) {
+					fprintf(stderr, "Invalid port number "
+					    "'%s'.\n", argv[i]);
+					print_syntax();
+					return EINVAL;
+				}
+			} else {
+				fprintf(stderr, "Unknown option '%s'.\n",
+				    argv[i]);
+				print_syntax();
+				return EINVAL;
+			}
+		} else {
+			fprintf(stderr, "Unexpected argument.\n");
+			print_syntax();
+			return EINVAL;
+		}
+
+		++i;
+	}
 
 	async_set_fallback_port_handler(client_connection, NULL);
@@ -368,5 +900,5 @@
 
 	inet_ep_init(&ep);
-	ep.port = 2223;
+	ep.port = port;
 
 	rc = tcp_listener_create(tcp, &ep, &listen_cb, NULL, &conn_cb, NULL,
Index: uspace/srv/hid/remcons/remcons.h
===================================================================
--- uspace/srv/hid/remcons/remcons.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/remcons/remcons.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2012 Vojtech Horky
  * All rights reserved.
@@ -36,6 +37,33 @@
 #define REMCONS_H_
 
+#include <adt/list.h>
+#include <io/cons_event.h>
+#include <stdbool.h>
+#include <vt/vt100.h>
+#include "user.h"
+
 #define NAME       "remcons"
 #define NAMESPACE  "term"
+
+/** Remote console */
+typedef struct {
+	telnet_user_t *user;	/**< telnet user */
+	vt100_t *vt;		/**< virtual terminal driver */
+	bool enable_ctl;	/**< enable escape control sequences */
+	bool enable_rgb;	/**< enable RGB color setting */
+	sysarg_t ucols;		/**< number of columns in user buffer */
+	sysarg_t urows;		/**< number of rows in user buffer */
+	charfield_t *ubuf;	/**< user buffer */
+	bool curs_visible;	/**< cursor is visible */
+
+	/** List of remcons_event_t. */
+	list_t in_events;
+} remcons_t;
+
+/** Remote console event */
+typedef struct {
+	link_t link;		/**< link to list of events */
+	cons_event_t cev;	/**< console event */
+} remcons_event_t;
 
 #endif
Index: uspace/srv/hid/remcons/telnet.h
===================================================================
--- uspace/srv/hid/remcons/telnet.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/remcons/telnet.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2012 Vojtech Horky
  * All rights reserved.
@@ -44,4 +45,6 @@
  */
 
+#define TELNET_SE 240
+#define TELNET_SB 250
 #define TELNET_IAC 255
 
@@ -55,4 +58,5 @@
 #define TELNET_ECHO 1
 #define TELNET_SUPPRESS_GO_AHEAD 3
+#define TELNET_NAWS 31
 #define TELNET_LINEMODE 34
 
Index: uspace/srv/hid/remcons/user.c
===================================================================
--- uspace/srv/hid/remcons/user.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/remcons/user.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2012 Vojtech Horky
  * All rights reserved.
@@ -37,4 +38,6 @@
 #include <adt/prodcons.h>
 #include <errno.h>
+#include <macros.h>
+#include <mem.h>
 #include <str_error.h>
 #include <loc.h>
@@ -48,4 +51,5 @@
 #include <inttypes.h>
 #include <assert.h>
+#include "remcons.h"
 #include "user.h"
 #include "telnet.h"
@@ -54,10 +58,16 @@
 static LIST_INITIALIZE(users);
 
+static errno_t telnet_user_send_raw_locked(telnet_user_t *, const void *,
+    size_t);
+static errno_t telnet_user_flush_locked(telnet_user_t *);
+
 /** Create new telnet user.
  *
  * @param conn Incoming connection.
+ * @param cb Callback functions
+ * @param arg Argument to callback functions
  * @return New telnet user or NULL when out of memory.
  */
-telnet_user_t *telnet_user_create(tcp_conn_t *conn)
+telnet_user_t *telnet_user_create(tcp_conn_t *conn, telnet_cb_t *cb, void *arg)
 {
 	static int telnet_user_id_counter = 0;
@@ -68,7 +78,10 @@
 	}
 
+	user->cb = cb;
+	user->arg = arg;
 	user->id = ++telnet_user_id_counter;
 
-	int rc = asprintf(&user->service_name, "%s/telnet%d", NAMESPACE, user->id);
+	int rc = asprintf(&user->service_name, "%s/telnet%u.%d", NAMESPACE,
+	    (unsigned)task_get_id(), user->id);
 	if (rc < 0) {
 		free(user);
@@ -78,11 +91,12 @@
 	user->conn = conn;
 	user->service_id = (service_id_t) -1;
-	prodcons_initialize(&user->in_events);
 	link_initialize(&user->link);
 	user->socket_buffer_len = 0;
 	user->socket_buffer_pos = 0;
+	user->send_buf_used = 0;
 
 	fibril_condvar_initialize(&user->refcount_cv);
-	fibril_mutex_initialize(&user->guard);
+	fibril_mutex_initialize(&user->send_lock);
+	fibril_mutex_initialize(&user->recv_lock);
 	user->task_finished = false;
 	user->socket_closed = false;
@@ -90,4 +104,5 @@
 
 	user->cursor_x = 0;
+	user->cursor_y = 0;
 
 	return user;
@@ -137,5 +152,5 @@
 
 	telnet_user_t *tmp = user;
-	fibril_mutex_lock(&tmp->guard);
+	fibril_mutex_lock(&tmp->recv_lock);
 	user->locsrv_connection_count++;
 
@@ -149,5 +164,5 @@
 	}
 
-	fibril_mutex_unlock(&tmp->guard);
+	fibril_mutex_unlock(&tmp->recv_lock);
 
 	fibril_mutex_unlock(&users_guard);
@@ -162,9 +177,9 @@
 void telnet_user_notify_client_disconnected(telnet_user_t *user)
 {
-	fibril_mutex_lock(&user->guard);
+	fibril_mutex_lock(&user->recv_lock);
 	assert(user->locsrv_connection_count > 0);
 	user->locsrv_connection_count--;
 	fibril_condvar_signal(&user->refcount_cv);
-	fibril_mutex_unlock(&user->guard);
+	fibril_mutex_unlock(&user->recv_lock);
 }
 
@@ -175,78 +190,178 @@
 bool telnet_user_is_zombie(telnet_user_t *user)
 {
-	fibril_mutex_lock(&user->guard);
+	fibril_mutex_lock(&user->recv_lock);
 	bool zombie = user->socket_closed || user->task_finished;
-	fibril_mutex_unlock(&user->guard);
+	fibril_mutex_unlock(&user->recv_lock);
 
 	return zombie;
 }
 
-/** Receive next byte from a socket (use buffering.
- * We need to return the value via extra argument because the read byte
- * might be negative.
- */
-static errno_t telnet_user_recv_next_byte_no_lock(telnet_user_t *user, char *byte)
-{
+static errno_t telnet_user_fill_recv_buf(telnet_user_t *user)
+{
+	errno_t rc;
+	size_t recv_length;
+
+	rc = tcp_conn_recv_wait(user->conn, user->socket_buffer,
+	    BUFFER_SIZE, &recv_length);
+	if (rc != EOK)
+		return rc;
+
+	if (recv_length == 0) {
+		user->socket_closed = true;
+		user->srvs.aborted = true;
+		return ENOENT;
+	}
+
+	user->socket_buffer_len = recv_length;
+	user->socket_buffer_pos = 0;
+
+	return EOK;
+}
+
+/** Receive next byte from a socket (use buffering).
+ *
+ * @param user Telnet user
+ * @param byte Place to store the received byte
+ * @return EOK on success or an error code
+ */
+static errno_t telnet_user_recv_next_byte_locked(telnet_user_t *user,
+    uint8_t *byte)
+{
+	errno_t rc;
+
 	/* No more buffered data? */
 	if (user->socket_buffer_len <= user->socket_buffer_pos) {
-		errno_t rc;
-		size_t recv_length;
-
-		rc = tcp_conn_recv_wait(user->conn, user->socket_buffer,
-		    BUFFER_SIZE, &recv_length);
+		rc = telnet_user_fill_recv_buf(user);
 		if (rc != EOK)
 			return rc;
-
-		if (recv_length == 0) {
-			user->socket_closed = true;
-			user->srvs.aborted = true;
-			return ENOENT;
-		}
-
-		user->socket_buffer_len = recv_length;
-		user->socket_buffer_pos = 0;
-	}
-
-	*byte = user->socket_buffer[user->socket_buffer_pos++];
-
+	}
+
+	*byte = (uint8_t)user->socket_buffer[user->socket_buffer_pos++];
 	return EOK;
 }
 
-/** Creates new keyboard event from given char.
- *
- * @param type Event type (press / release).
- * @param c Pressed character.
- */
-static kbd_event_t *new_kbd_event(kbd_event_type_t type, char32_t c)
-{
-	kbd_event_t *event = malloc(sizeof(kbd_event_t));
-	assert(event);
-
-	link_initialize(&event->link);
-	event->type = type;
-	event->c = c;
-	event->mods = 0;
-
-	switch (c) {
-	case '\n':
-		event->key = KC_ENTER;
-		break;
-	case '\t':
-		event->key = KC_TAB;
-		break;
-	case '\b':
-	case 127: /* This is what Linux telnet sends. */
-		event->key = KC_BACKSPACE;
-		event->c = '\b';
-		break;
-	default:
-		event->key = KC_A;
-		break;
-	}
-
-	return event;
-}
-
-/** Process telnet command (currently only print to screen).
+/** Determine if a received byte is available without waiting.
+ *
+ * @param user Telnet user
+ * @return @c true iff a byte is currently available
+ */
+static bool telnet_user_byte_avail(telnet_user_t *user)
+{
+	return user->socket_buffer_len > user->socket_buffer_pos;
+}
+
+static errno_t telnet_user_send_opt(telnet_user_t *user, telnet_cmd_t cmd,
+    telnet_cmd_t opt)
+{
+	uint8_t cmdb[3];
+
+	cmdb[0] = TELNET_IAC;
+	cmdb[1] = cmd;
+	cmdb[2] = opt;
+
+	return telnet_user_send_raw_locked(user, (char *)cmdb, sizeof(cmdb));
+}
+
+/** Process telnet WILL NAWS command.
+ *
+ * @param user Telnet user structure.
+ * @param cmd Telnet command.
+ */
+static void process_telnet_will_naws(telnet_user_t *user)
+{
+	telnet_user_log(user, "WILL NAWS");
+	/* Send DO NAWS */
+	(void) telnet_user_send_opt(user, TELNET_DO, TELNET_NAWS);
+	(void) telnet_user_flush_locked(user);
+}
+
+/** Process telnet SB NAWS command.
+ *
+ * @param user Telnet user structure.
+ * @param cmd Telnet command.
+ */
+static void process_telnet_sb_naws(telnet_user_t *user)
+{
+	uint8_t chi, clo;
+	uint8_t rhi, rlo;
+	uint16_t cols;
+	uint16_t rows;
+	uint8_t iac;
+	uint8_t se;
+	errno_t rc;
+
+	telnet_user_log(user, "SB NAWS...");
+
+	rc = telnet_user_recv_next_byte_locked(user, &chi);
+	if (rc != EOK)
+		return;
+	rc = telnet_user_recv_next_byte_locked(user, &clo);
+	if (rc != EOK)
+		return;
+
+	rc = telnet_user_recv_next_byte_locked(user, &rhi);
+	if (rc != EOK)
+		return;
+	rc = telnet_user_recv_next_byte_locked(user, &rlo);
+	if (rc != EOK)
+		return;
+
+	rc = telnet_user_recv_next_byte_locked(user, &iac);
+	if (rc != EOK)
+		return;
+	rc = telnet_user_recv_next_byte_locked(user, &se);
+	if (rc != EOK)
+		return;
+
+	cols = (chi << 8) | clo;
+	rows = (rhi << 8) | rlo;
+
+	telnet_user_log(user, "cols=%u rows=%u\n", cols, rows);
+
+	if (cols < 1 || rows < 1) {
+		telnet_user_log(user, "Ignoring invalid window size update.");
+		return;
+	}
+
+	user->cb->ws_update(user->arg, cols, rows);
+}
+
+/** Process telnet WILL command.
+ *
+ * @param user Telnet user structure.
+ * @param opt Option code.
+ */
+static void process_telnet_will(telnet_user_t *user, telnet_cmd_t opt)
+{
+	telnet_user_log(user, "WILL");
+	switch (opt) {
+	case TELNET_NAWS:
+		process_telnet_will_naws(user);
+		return;
+	}
+
+	telnet_user_log(user, "Ignoring telnet command %u %u %u.",
+	    TELNET_IAC, TELNET_WILL, opt);
+}
+
+/** Process telnet SB command.
+ *
+ * @param user Telnet user structure.
+ * @param opt Option code.
+ */
+static void process_telnet_sb(telnet_user_t *user, telnet_cmd_t opt)
+{
+	telnet_user_log(user, "SB");
+	switch (opt) {
+	case TELNET_NAWS:
+		process_telnet_sb_naws(user);
+		return;
+	}
+
+	telnet_user_log(user, "Ignoring telnet command %u %u %u.",
+	    TELNET_IAC, TELNET_SB, opt);
+}
+
+/** Process telnet command.
  *
  * @param user Telnet user structure.
@@ -257,4 +372,13 @@
     telnet_cmd_t option_code, telnet_cmd_t cmd)
 {
+	switch (option_code) {
+	case TELNET_SB:
+		process_telnet_sb(user, cmd);
+		return;
+	case TELNET_WILL:
+		process_telnet_will(user, cmd);
+		return;
+	}
+
 	if (option_code != 0) {
 		telnet_user_log(user, "Ignoring telnet command %u %u %u.",
@@ -266,15 +390,23 @@
 }
 
-/** Get next keyboard event.
+/** Receive data from telnet connection.
  *
  * @param user Telnet user.
- * @param event Where to store the keyboard event.
- * @return Error code.
- */
-errno_t telnet_user_get_next_keyboard_event(telnet_user_t *user, kbd_event_t *event)
-{
-	fibril_mutex_lock(&user->guard);
-	if (list_empty(&user->in_events.list)) {
-		char next_byte = 0;
+ * @param buf Destination buffer
+ * @param size Buffer size
+ * @param nread Place to store number of bytes read (>0 on success)
+ * @return EOK on success or an error code
+ */
+errno_t telnet_user_recv(telnet_user_t *user, void *buf, size_t size,
+    size_t *nread)
+{
+	uint8_t *bp = (uint8_t *)buf;
+	fibril_mutex_lock(&user->recv_lock);
+
+	assert(size > 0);
+	*nread = 0;
+
+	do {
+		uint8_t next_byte = 0;
 		bool inside_telnet_command = false;
 
@@ -282,11 +414,12 @@
 
 		/* Skip zeros, bail-out on error. */
-		while (next_byte == 0) {
-			errno_t rc = telnet_user_recv_next_byte_no_lock(user, &next_byte);
+		do {
+			errno_t rc = telnet_user_recv_next_byte_locked(user,
+			    &next_byte);
 			if (rc != EOK) {
-				fibril_mutex_unlock(&user->guard);
+				fibril_mutex_unlock(&user->recv_lock);
 				return rc;
 			}
-			uint8_t byte = (uint8_t) next_byte;
+			uint8_t byte = next_byte;
 
 			/* Skip telnet commands. */
@@ -294,5 +427,6 @@
 				inside_telnet_command = false;
 				next_byte = 0;
-				if (TELNET_IS_OPTION_CODE(byte)) {
+				if (TELNET_IS_OPTION_CODE(byte) ||
+				    byte == TELNET_SB) {
 					telnet_option_code = byte;
 					inside_telnet_command = true;
@@ -306,5 +440,5 @@
 				next_byte = 0;
 			}
-		}
+		} while (next_byte == 0 && telnet_user_byte_avail(user));
 
 		/* CR-LF conversions. */
@@ -313,20 +447,41 @@
 		}
 
-		kbd_event_t *down = new_kbd_event(KEY_PRESS, next_byte);
-		kbd_event_t *up = new_kbd_event(KEY_RELEASE, next_byte);
-		assert(down);
-		assert(up);
-		prodcons_produce(&user->in_events, &down->link);
-		prodcons_produce(&user->in_events, &up->link);
-	}
-
-	link_t *link = prodcons_consume(&user->in_events);
-	kbd_event_t *tmp = list_get_instance(link, kbd_event_t, link);
-
-	fibril_mutex_unlock(&user->guard);
-
-	*event = *tmp;
-
-	free(tmp);
+		if (next_byte != 0) {
+			*bp++ = next_byte;
+			++*nread;
+			--size;
+		}
+	} while (size > 0 && (telnet_user_byte_avail(user) || *nread == 0));
+
+	fibril_mutex_unlock(&user->recv_lock);
+	return EOK;
+}
+
+static errno_t telnet_user_send_raw_locked(telnet_user_t *user,
+    const void *data, size_t size)
+{
+	size_t remain;
+	size_t now;
+	errno_t rc;
+
+	remain = sizeof(user->send_buf) - user->send_buf_used;
+	while (size > 0) {
+		if (remain == 0) {
+			rc = tcp_conn_send(user->conn, user->send_buf,
+			    sizeof(user->send_buf));
+			if (rc != EOK)
+				return rc;
+
+			user->send_buf_used = 0;
+			remain = sizeof(user->send_buf);
+		}
+
+		now = min(remain, size);
+		memcpy(user->send_buf + user->send_buf_used, data, now);
+		user->send_buf_used += now;
+		remain -= now;
+		data += now;
+		size -= now;
+	}
 
 	return EOK;
@@ -339,5 +494,6 @@
  * @param size Size of @p data buffer in bytes.
  */
-static errno_t telnet_user_send_data_no_lock(telnet_user_t *user, uint8_t *data, size_t size)
+static errno_t telnet_user_send_data_locked(telnet_user_t *user,
+    const char *data, size_t size)
 {
 	uint8_t *converted = malloc(3 * size + 1);
@@ -351,4 +507,6 @@
 			converted[converted_size++] = 10;
 			user->cursor_x = 0;
+			if (user->cursor_y < (int)user->rows - 1)
+				++user->cursor_y;
 		} else {
 			converted[converted_size++] = data[i];
@@ -361,5 +519,6 @@
 	}
 
-	errno_t rc = tcp_conn_send(user->conn, converted, converted_size);
+	errno_t rc = telnet_user_send_raw_locked(user, converted,
+	    converted_size);
 	free(converted);
 
@@ -373,12 +532,53 @@
  * @param size Size of @p data buffer in bytes.
  */
-errno_t telnet_user_send_data(telnet_user_t *user, uint8_t *data, size_t size)
-{
-	fibril_mutex_lock(&user->guard);
-
-	errno_t rc = telnet_user_send_data_no_lock(user, data, size);
-
-	fibril_mutex_unlock(&user->guard);
-
+errno_t telnet_user_send_data(telnet_user_t *user, const char *data,
+    size_t size)
+{
+	fibril_mutex_lock(&user->send_lock);
+
+	errno_t rc = telnet_user_send_data_locked(user, data, size);
+
+	fibril_mutex_unlock(&user->send_lock);
+
+	return rc;
+}
+
+/** Send raw non-printable data to the socket.
+ *
+ * @param user Telnet user.
+ * @param data Data buffer (not zero terminated).
+ * @param size Size of @p data buffer in bytes.
+ */
+errno_t telnet_user_send_raw(telnet_user_t *user, const char *data,
+    size_t size)
+{
+	fibril_mutex_lock(&user->send_lock);
+
+	errno_t rc = telnet_user_send_raw_locked(user, data, size);
+
+	fibril_mutex_unlock(&user->send_lock);
+
+	return rc;
+}
+
+static errno_t telnet_user_flush_locked(telnet_user_t *user)
+{
+	errno_t rc;
+
+	rc = tcp_conn_send(user->conn, user->send_buf, user->send_buf_used);
+	if (rc != EOK)
+		return rc;
+
+	user->send_buf_used = 0;
+	return EOK;
+}
+
+errno_t telnet_user_flush(telnet_user_t *user)
+{
+	errno_t rc;
+
+	fibril_mutex_lock(&user->send_lock);
+	rc = telnet_user_flush_locked(user);
+	fibril_mutex_unlock(&user->send_lock);
 	return rc;
 }
@@ -393,13 +593,29 @@
 void telnet_user_update_cursor_x(telnet_user_t *user, int new_x)
 {
-	fibril_mutex_lock(&user->guard);
+	fibril_mutex_lock(&user->send_lock);
 	if (user->cursor_x - 1 == new_x) {
-		uint8_t data = '\b';
+		char data = '\b';
 		/* Ignore errors. */
-		telnet_user_send_data_no_lock(user, &data, 1);
+		telnet_user_send_data_locked(user, &data, 1);
 	}
 	user->cursor_x = new_x;
-	fibril_mutex_unlock(&user->guard);
-
+	fibril_mutex_unlock(&user->send_lock);
+
+}
+
+/** Resize telnet session.
+ *
+ * @param user Telnet user
+ * @param cols New number of columns
+ * @param rows New number of rows
+ */
+void telnet_user_resize(telnet_user_t *user, unsigned cols, unsigned rows)
+{
+	user->cols = cols;
+	user->rows = rows;
+	if ((unsigned)user->cursor_x > cols - 1)
+		user->cursor_x = cols - 1;
+	if ((unsigned)user->cursor_y > rows - 1)
+		user->cursor_y = rows - 1;
 }
 
Index: uspace/srv/hid/remcons/user.h
===================================================================
--- uspace/srv/hid/remcons/user.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/hid/remcons/user.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,3 +1,4 @@
 /*
+ * Copyright (c) 2024 Jiri Svoboda
  * Copyright (c) 2012 Vojtech Horky
  * All rights reserved.
@@ -36,17 +37,27 @@
 #define TELNET_USER_H_
 
-#include <adt/prodcons.h>
 #include <fibril_synch.h>
 #include <inet/tcp.h>
 #include <inttypes.h>
 #include <io/con_srv.h>
-#include "remcons.h"
 
 #define BUFFER_SIZE 32
+#define SEND_BUF_SIZE 512
+
+/** Telnet callbacks */
+typedef struct {
+	void (*ws_update)(void *, unsigned, unsigned);
+} telnet_cb_t;
 
 /** Representation of a connected (human) user. */
 typedef struct {
-	/** Mutex guarding the whole structure. */
-	fibril_mutex_t guard;
+	/** Synchronize send operations */
+	fibril_mutex_t send_lock;
+	/** Synchronize receive operations */
+	fibril_mutex_t recv_lock;
+	/** Callback functions */
+	telnet_cb_t *cb;
+	/** Argument to callback functions */
+	void *arg;
 
 	/** Internal id, used for creating locfs entries. */
@@ -61,10 +72,10 @@
 	con_srvs_t srvs;
 
-	/** Producer-consumer of kbd_event_t. */
-	prodcons_t in_events;
 	link_t link;
 	char socket_buffer[BUFFER_SIZE];
 	size_t socket_buffer_len;
 	size_t socket_buffer_pos;
+	char send_buf[SEND_BUF_SIZE];
+	size_t send_buf_used;
 
 	/** Task id of the launched application. */
@@ -79,7 +90,13 @@
 	/** X position of the cursor. */
 	int cursor_x;
+	/** Y position of the cursor. */
+	int cursor_y;
+	/** Total number of columns */
+	unsigned cols;
+	/** Total number of rows */
+	unsigned rows;
 } telnet_user_t;
 
-extern telnet_user_t *telnet_user_create(tcp_conn_t *);
+extern telnet_user_t *telnet_user_create(tcp_conn_t *, telnet_cb_t *, void *);
 extern void telnet_user_add(telnet_user_t *);
 extern void telnet_user_destroy(telnet_user_t *);
@@ -88,6 +105,10 @@
 extern void telnet_user_notify_client_disconnected(telnet_user_t *);
 extern errno_t telnet_user_get_next_keyboard_event(telnet_user_t *, kbd_event_t *);
-extern errno_t telnet_user_send_data(telnet_user_t *, uint8_t *, size_t);
+extern errno_t telnet_user_send_data(telnet_user_t *, const char *, size_t);
+extern errno_t telnet_user_send_raw(telnet_user_t *, const char *, size_t);
+extern errno_t telnet_user_flush(telnet_user_t *);
+extern errno_t telnet_user_recv(telnet_user_t *, void *, size_t, size_t *);
 extern void telnet_user_update_cursor_x(telnet_user_t *, int);
+extern void telnet_user_resize(telnet_user_t *, unsigned, unsigned);
 
 /** Print informational message about connected user. */
Index: uspace/srv/meson.build
===================================================================
--- uspace/srv/meson.build	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/meson.build	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,3 +1,4 @@
 #
+# Copyright (c) 2024 Jiri Svoboda
 # Copyright (c) 2019 Jiří Zárevúcky
 # All rights reserved.
@@ -62,9 +63,9 @@
 	'net/inetsrv',
 	'net/loopip',
-	'net/nconfsrv',
 	'net/slip',
 	'net/tcp',
 	'net/udp',
 	'ns',
+	'system',
 	'taskmon',
 	'test/chardev-test',
Index: uspace/srv/net/dhcp/dhcp.c
===================================================================
--- uspace/srv/net/dhcp/dhcp.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/dhcp/dhcp.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2022 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -71,4 +71,6 @@
 static list_t dhcp_links;
 
+bool inetcfg_inited = false;
+
 static void dhcpsrv_discover_timeout(void *);
 static void dhcpsrv_request_timeout(void *);
@@ -468,4 +470,15 @@
 	log_msg(LOG_DEFAULT, LVL_DEBUG, "dhcpsrv_link_add(%zu)", link_id);
 
+	if (!inetcfg_inited) {
+		rc = inetcfg_init();
+		if (rc != EOK) {
+			log_msg(LOG_DEFAULT, LVL_ERROR, "Error contacting "
+			    "inet configuration service.\n");
+			return EIO;
+		}
+
+		inetcfg_inited = true;
+	}
+
 	if (dhcpsrv_link_find(link_id) != NULL) {
 		log_msg(LOG_DEFAULT, LVL_NOTE, "Link %zu already added",
Index: uspace/srv/net/dhcp/main.c
===================================================================
--- uspace/srv/net/dhcp/main.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/dhcp/main.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2023 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -38,5 +38,4 @@
 #include <str_error.h>
 #include <io/log.h>
-#include <inet/inetcfg.h>
 #include <ipc/dhcp.h>
 #include <ipc/services.h>
@@ -60,10 +59,4 @@
 
 	dhcpsrv_links_init();
-
-	rc = inetcfg_init();
-	if (rc != EOK) {
-		log_msg(LOG_DEFAULT, LVL_ERROR, "Error contacting inet configuration service.\n");
-		return EIO;
-	}
 
 	async_set_fallback_port_handler(dhcp_client_conn, NULL);
Index: uspace/srv/net/doc/doxygroups.h
===================================================================
--- uspace/srv/net/doc/doxygroups.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/doc/doxygroups.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -20,9 +20,4 @@
 /**    @addtogroup slip slip
  *     @brief SLIP service
- *     @ingroup net
- */
-
-/**    @addtogroup nconfsrv nconfsrv
- *     @brief Network configuration service
  *     @ingroup net
  */
Index: uspace/srv/net/inetsrv/addrobj.c
===================================================================
--- uspace/srv/net/inetsrv/addrobj.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/inetsrv/addrobj.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -41,4 +41,6 @@
 #include <io/log.h>
 #include <ipc/loc.h>
+#include <sif.h>
+#include <stdio.h>
 #include <stdlib.h>
 #include <str.h>
@@ -211,4 +213,27 @@
 }
 
+/** Count number of non-temporary address objects configured for link.
+ *
+ * @param ilink Inet link
+ * @return Number of address objects configured for this link
+ */
+unsigned inet_addrobj_cnt_by_link(inet_link_t *ilink)
+{
+	unsigned cnt;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "inet_addrobj_cnt_by_link()");
+
+	fibril_mutex_lock(&addr_list_lock);
+
+	cnt = 0;
+	list_foreach(addr_list, addr_list, inet_addrobj_t, naddr) {
+		if (naddr->ilink == ilink && naddr->temp == false)
+			++cnt;
+	}
+
+	fibril_mutex_unlock(&addr_list_lock);
+	return cnt;
+}
+
 /** Send datagram from address object */
 errno_t inet_addrobj_send_dgram(inet_addrobj_t *addr, inet_addr_t *ldest,
@@ -282,4 +307,206 @@
 }
 
+/** Load address object from SIF node.
+ *
+ * @param anode SIF node to load address object from
+ * @return EOK on success or an error code
+ */
+static errno_t inet_addrobj_load(sif_node_t *anode)
+{
+	errno_t rc;
+	const char *sid;
+	const char *snaddr;
+	const char *slink;
+	const char *name;
+	char *endptr;
+	int id;
+	inet_naddr_t naddr;
+	inet_addrobj_t *addr;
+	inet_link_t *link;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "inet_addrobj_load()");
+
+	sid = sif_node_get_attr(anode, "id");
+	if (sid == NULL)
+		return EIO;
+
+	snaddr = sif_node_get_attr(anode, "naddr");
+	if (snaddr == NULL)
+		return EIO;
+
+	slink = sif_node_get_attr(anode, "link");
+	if (slink == NULL)
+		return EIO;
+
+	name = sif_node_get_attr(anode, "name");
+	if (name == NULL)
+		return EIO;
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inet_addrobj_load(): id='%s' "
+	    "naddr='%s' link='%s' name='%s'", sid, snaddr, slink, name);
+
+	id = strtoul(sid, &endptr, 10);
+	if (*endptr != '\0')
+		return EIO;
+
+	rc = inet_naddr_parse(snaddr, &naddr, NULL);
+	if (rc != EOK)
+		return EIO;
+
+	link = inet_link_get_by_svc_name(slink);
+	if (link == NULL) {
+		log_msg(LOG_DEFAULT, LVL_ERROR, "Link '%s' not found",
+		    slink);
+		return EIO;
+	}
+
+	addr = inet_addrobj_new();
+	if (addr == NULL)
+		return ENOMEM;
+
+	addr->id = id;
+	addr->naddr = naddr;
+	addr->ilink = link;
+	addr->name = str_dup(name);
+
+	if (addr->name == NULL) {
+		inet_addrobj_delete(addr);
+		return ENOMEM;
+	}
+
+	inet_addrobj_add(addr);
+	return EOK;
+}
+
+/** Load address objects from SIF node.
+ *
+ * @param naddrs SIF node to load address objects from
+ * @return EOK on success or an error code
+ */
+errno_t inet_addrobjs_load(sif_node_t *naddrs)
+{
+	sif_node_t *naddr;
+	const char *ntype;
+	errno_t rc;
+
+	naddr = sif_node_first_child(naddrs);
+	while (naddr != NULL) {
+		ntype = sif_node_get_type(naddr);
+		if (str_cmp(ntype, "address") != 0) {
+			rc = EIO;
+			goto error;
+		}
+
+		rc = inet_addrobj_load(naddr);
+		if (rc != EOK)
+			goto error;
+
+		naddr = sif_node_next_child(naddr);
+	}
+
+	return EOK;
+error:
+	return rc;
+
+}
+
+/** Save address object to SIF node.
+ *
+ * @param addr Address object
+ * @param naddr SIF node to save addres to
+ * @return EOK on success or an error code
+ */
+static errno_t inet_addrobj_save(inet_addrobj_t *addr, sif_node_t *naddr)
+{
+	char *str = NULL;
+	errno_t rc;
+	int rv;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "inet_addrobj_save(%p, %p)",
+	    addr, naddr);
+
+	/* id */
+
+	rv = asprintf(&str, "%zu", addr->id);
+	if (rv < 0) {
+		str = NULL;
+		rc = ENOMEM;
+		goto error;
+	}
+
+	rc = sif_node_set_attr(naddr, "id", str);
+	if (rc != EOK)
+		goto error;
+
+	free(str);
+	str = NULL;
+
+	/* dest */
+
+	rc = inet_naddr_format(&addr->naddr, &str);
+	if (rc != EOK)
+		goto error;
+
+	rc = sif_node_set_attr(naddr, "naddr", str);
+	if (rc != EOK)
+		goto error;
+
+	free(str);
+	str = NULL;
+
+	/* link */
+
+	rc = sif_node_set_attr(naddr, "link", addr->ilink->svc_name);
+	if (rc != EOK)
+		goto error;
+
+	/* name */
+
+	rc = sif_node_set_attr(naddr, "name", addr->name);
+	if (rc != EOK)
+		goto error;
+
+	free(str);
+
+	return rc;
+error:
+	if (str != NULL)
+		free(str);
+	return rc;
+}
+
+/** Save address objects to SIF node.
+ *
+ * @param cnode SIF node to save address objects to
+ * @return EOK on success or an error code
+ */
+errno_t inet_addrobjs_save(sif_node_t *cnode)
+{
+	sif_node_t *naddr;
+	errno_t rc;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "inet_addrobjs_save()");
+
+	fibril_mutex_lock(&addr_list_lock);
+
+	list_foreach(addr_list, addr_list, inet_addrobj_t, addr) {
+		if (addr->temp == false) {
+			rc = sif_node_append_child(cnode, "address", &naddr);
+			if (rc != EOK)
+				goto error;
+
+			rc = inet_addrobj_save(addr, naddr);
+			if (rc != EOK)
+				goto error;
+		}
+	}
+
+	fibril_mutex_unlock(&addr_list_lock);
+	return EOK;
+error:
+	fibril_mutex_unlock(&addr_list_lock);
+	return rc;
+}
+
 /** @}
  */
Index: uspace/srv/net/inetsrv/addrobj.h
===================================================================
--- uspace/srv/net/inetsrv/addrobj.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/inetsrv/addrobj.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2012 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -38,4 +38,5 @@
 #define INET_ADDROBJ_H_
 
+#include <sif.h>
 #include <stddef.h>
 #include <stdint.h>
@@ -56,7 +57,10 @@
 extern inet_addrobj_t *inet_addrobj_find_by_name(const char *, inet_link_t *);
 extern inet_addrobj_t *inet_addrobj_get_by_id(sysarg_t);
+extern unsigned inet_addrobj_cnt_by_link(inet_link_t *);
 extern errno_t inet_addrobj_send_dgram(inet_addrobj_t *, inet_addr_t *,
     inet_dgram_t *, uint8_t, uint8_t, int);
 extern errno_t inet_addrobj_get_id_list(sysarg_t **, size_t *);
+extern errno_t inet_addrobjs_load(sif_node_t *);
+extern errno_t inet_addrobjs_save(sif_node_t *);
 
 #endif
Index: uspace/srv/net/inetsrv/inet_link.c
===================================================================
--- uspace/srv/net/inetsrv/inet_link.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/inetsrv/inet_link.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -37,4 +37,5 @@
 #include <errno.h>
 #include <fibril_synch.h>
+#include <inet/dhcp.h>
 #include <inet/eth_addr.h>
 #include <inet/iplink.h>
@@ -164,5 +165,10 @@
 }
 
-errno_t inet_link_open(service_id_t sid)
+/** Open new IP link while inet_links_lock is held.
+ *
+ * @param sid IP link service ID
+ * @return EOK on success or an error code
+ */
+static errno_t inet_link_open_locked(service_id_t sid)
 {
 	inet_link_t *ilink;
@@ -170,5 +176,7 @@
 	errno_t rc;
 
-	log_msg(LOG_DEFAULT, LVL_DEBUG, "inet_link_open()");
+	assert(fibril_mutex_is_locked(&inet_links_lock));
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "inet_link_open_locked()");
 	ilink = inet_link_new();
 	if (ilink == NULL)
@@ -213,8 +221,5 @@
 	log_msg(LOG_DEFAULT, LVL_DEBUG, "Opened IP link '%s'", ilink->svc_name);
 
-	fibril_mutex_lock(&inet_links_lock);
-
 	if (inet_link_get_by_id_locked(sid) != NULL) {
-		fibril_mutex_unlock(&inet_links_lock);
 		log_msg(LOG_DEFAULT, LVL_DEBUG, "Link %zu already open",
 		    sid);
@@ -224,5 +229,4 @@
 
 	list_append(&ilink->link_list, &inet_links);
-	fibril_mutex_unlock(&inet_links_lock);
 
 	inet_addrobj_t *addr = NULL;
@@ -239,4 +243,5 @@
 		addr->ilink = ilink;
 		addr->name = str_dup("v4a");
+		addr->temp = true;
 
 		rc = inet_addrobj_add(addr);
@@ -275,4 +280,5 @@
 		addr6->ilink = ilink;
 		addr6->name = str_dup("v6a");
+		addr6->temp = true;
 
 		rc = inet_addrobj_add(addr6);
@@ -303,4 +309,20 @@
 }
 
+/** Open new IP link..
+ *
+ * @param sid IP link service ID
+ * @return EOK on success or an error code
+ */
+errno_t inet_link_open(service_id_t sid)
+{
+	errno_t rc;
+
+	fibril_mutex_lock(&inet_links_lock);
+	rc = inet_link_open_locked(sid);
+	fibril_mutex_unlock(&inet_links_lock);
+
+	return rc;
+}
+
 /** Send IPv4 datagram over Internet link
  *
@@ -476,4 +498,37 @@
 }
 
+/** Find link by service name while inet_links_lock is held.
+ *
+ * @param svc_name Service name
+ * @return Link or @c NULL if not found
+ */
+static inet_link_t *inet_link_get_by_svc_name_locked(const char *svc_name)
+{
+	assert(fibril_mutex_is_locked(&inet_links_lock));
+
+	list_foreach(inet_links, link_list, inet_link_t, ilink) {
+		if (str_cmp(ilink->svc_name, svc_name) == 0)
+			return ilink;
+	}
+
+	return NULL;
+}
+
+/** Find link by service name.
+ *
+ * @param svc_name Service name
+ * @return Link or @c NULL if not found
+ */
+inet_link_t *inet_link_get_by_svc_name(const char *svc_name)
+{
+	inet_link_t *ilink;
+
+	fibril_mutex_lock(&inet_links_lock);
+	ilink = inet_link_get_by_svc_name_locked(svc_name);
+	fibril_mutex_unlock(&inet_links_lock);
+
+	return ilink;
+}
+
 /** Get IDs of all links. */
 errno_t inet_link_get_id_list(sysarg_t **rid_list, size_t *rcount)
@@ -504,4 +559,212 @@
 }
 
+/** Check for new IP links.
+ *
+ * @return EOK on success or an error code
+ */
+static errno_t inet_link_check_new(void)
+{
+	bool already_known;
+	category_id_t iplink_cat;
+	service_id_t *svcs;
+	inet_link_cfg_info_t info;
+	size_t count, i;
+	errno_t rc;
+
+	fibril_mutex_lock(&inet_links_lock);
+
+	rc = loc_category_get_id("iplink", &iplink_cat, IPC_FLAG_BLOCKING);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed resolving category "
+		    "'iplink'.");
+		fibril_mutex_unlock(&inet_links_lock);
+		return ENOENT;
+	}
+
+	rc = loc_category_get_svcs(iplink_cat, &svcs, &count);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed getting list of IP "
+		    "links.");
+		fibril_mutex_unlock(&inet_links_lock);
+		return EIO;
+	}
+
+	for (i = 0; i < count; i++) {
+		already_known = false;
+
+		list_foreach(inet_links, link_list, inet_link_t, ilink) {
+			if (ilink->svc_id == svcs[i]) {
+				already_known = true;
+				break;
+			}
+		}
+
+		if (!already_known) {
+			log_msg(LOG_DEFAULT, LVL_NOTE, "Found IP link '%lu'",
+			    (unsigned long) svcs[i]);
+			rc = inet_link_open_locked(svcs[i]);
+			if (rc != EOK) {
+				log_msg(LOG_DEFAULT, LVL_ERROR, "Could not "
+				    "add IP link.");
+			}
+		} else {
+			/* Clear so it won't be autoconfigured below */
+			svcs[i] = 0;
+		}
+	}
+
+	fibril_mutex_unlock(&inet_links_lock);
+
+	/*
+	 * Auto-configure new links. Note that newly discovered links
+	 * cannot have any configured address objects, because we only
+	 * retain configuration for present links.
+	 */
+	for (i = 0; i < count; i++) {
+		if (svcs[i] != 0) {
+			info.svc_id = svcs[i];
+			rc = loc_service_get_name(info.svc_id, &info.svc_name);
+			if (rc != EOK) {
+				log_msg(LOG_DEFAULT, LVL_ERROR, "Failed "
+				    "getting service name.");
+				return rc;
+			}
+
+			inet_link_autoconf_link(&info);
+			free(info.svc_name);
+			info.svc_name = NULL;
+		}
+	}
+
+	return EOK;
+}
+
+/** IP link category change callback.
+ *
+ * @param arg Not used
+ */
+static void inet_link_cat_change_cb(void *arg)
+{
+	(void) inet_link_check_new();
+}
+
+/** Start IP link discovery.
+ *
+ * @return EOK on success or an error code
+ */
+errno_t inet_link_discovery_start(void)
+{
+	errno_t rc;
+
+	rc = loc_register_cat_change_cb(inet_link_cat_change_cb, NULL);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed registering callback "
+		    "for IP link discovery: %s.", str_error(rc));
+		return rc;
+	}
+
+	return inet_link_check_new();
+}
+
+/** Start DHCP autoconfiguration on IP link.
+ *
+ * @param info Link information
+ */
+void inet_link_autoconf_link(inet_link_cfg_info_t *info)
+{
+	errno_t rc;
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inet_link_autoconf_link");
+
+	if (str_lcmp(info->svc_name, "net/eth", str_length("net/eth")) == 0) {
+		log_msg(LOG_DEFAULT, LVL_NOTE, "inet_link_autoconf_link : dhcp link add for link '%s' (%u)",
+		    info->svc_name, (unsigned)info->svc_id);
+		rc = dhcp_link_add(info->svc_id);
+		log_msg(LOG_DEFAULT, LVL_NOTE, "inet_link_autoconf_link : dhcp link add for link '%s' (%u) DONE",
+		    info->svc_name, (unsigned)info->svc_id);
+		if (rc != EOK) {
+			log_msg(LOG_DEFAULT, LVL_WARN, "Failed configuring "
+			    "DHCP on  link '%s'.\n", info->svc_name);
+		}
+	}
+}
+
+/** Start DHCP autoconfiguration on IP links. */
+errno_t inet_link_autoconf(void)
+{
+	inet_link_cfg_info_t *link_info;
+	size_t link_cnt;
+	size_t acnt;
+	size_t i;
+	errno_t rc;
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inet_link_autoconf : initialize DHCP");
+	rc = dhcp_init();
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_WARN, "Failed initializing DHCP "
+		    "service.");
+		return rc;
+	}
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inet_link_autoconf : initialize DHCP done");
+
+	fibril_mutex_lock(&inet_links_lock);
+	link_cnt = list_count(&inet_links);
+
+	link_info = calloc(link_cnt, sizeof(inet_link_cfg_info_t));
+	if (link_info == NULL) {
+		fibril_mutex_unlock(&inet_links_lock);
+		return ENOMEM;
+	}
+
+	i = 0;
+	list_foreach(inet_links, link_list, inet_link_t, ilink) {
+		assert(i < link_cnt);
+
+		acnt = inet_addrobj_cnt_by_link(ilink);
+		if (acnt != 0) {
+			/*
+			 * No autoconfiguration if link has configured
+			 * addresses.
+			 */
+			continue;
+		}
+
+		link_info[i].svc_id = ilink->svc_id;
+		link_info[i].svc_name = str_dup(ilink->svc_name);
+		if (link_info[i].svc_name == NULL) {
+			fibril_mutex_unlock(&inet_links_lock);
+			goto error;
+		}
+
+		++i;
+	}
+
+	fibril_mutex_unlock(&inet_links_lock);
+
+	/* Update link_cnt to include only links slated for autoconfig. */
+	link_cnt = i;
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inet_link_autoconf : autoconf links...");
+
+	for (i = 0; i < link_cnt; i++)
+		inet_link_autoconf_link(&link_info[i]);
+
+	for (i = 0; i < link_cnt; i++) {
+		if (link_info[i].svc_name != NULL)
+			free(link_info[i].svc_name);
+	}
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inet_link_autoconf : autoconf links done");
+	return EOK;
+error:
+	for (i = 0; i < link_cnt; i++) {
+		if (link_info[i].svc_name != NULL)
+			free(link_info[i].svc_name);
+	}
+	free(link_info);
+	return ENOMEM;
+}
+
 /** @}
  */
Index: uspace/srv/net/inetsrv/inet_link.h
===================================================================
--- uspace/srv/net/inetsrv/inet_link.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/inetsrv/inet_link.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -49,5 +49,9 @@
     uint8_t, uint8_t, int);
 extern inet_link_t *inet_link_get_by_id(sysarg_t);
+extern inet_link_t *inet_link_get_by_svc_name(const char *);
 extern errno_t inet_link_get_id_list(sysarg_t **, size_t *);
+extern errno_t inet_link_discovery_start(void);
+extern errno_t inet_link_autoconf(void);
+extern void inet_link_autoconf_link(inet_link_cfg_info_t *);
 
 #endif
Index: uspace/srv/net/inetsrv/inetcfg.c
===================================================================
--- uspace/srv/net/inetsrv/inetcfg.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/inetsrv/inetcfg.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -92,4 +92,10 @@
 	}
 
+	rc = inet_cfg_sync(cfg);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_ERROR, "Error saving configuration.");
+		return rc;
+	}
+
 	return EOK;
 }
@@ -98,4 +104,10 @@
 {
 	inet_addrobj_t *addr;
+	inet_link_cfg_info_t info;
+	unsigned acnt;
+	inet_link_t *ilink;
+	errno_t rc;
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inetcfg_addr_delete()");
 
 	addr = inet_addrobj_get_by_id(addr_id);
@@ -103,7 +115,38 @@
 		return ENOENT;
 
+	info.svc_id = addr->ilink->svc_id;
+	info.svc_name = str_dup(addr->ilink->svc_name);
+	if (info.svc_name == NULL)
+		return ENOMEM;
+
 	inet_addrobj_remove(addr);
 	inet_addrobj_delete(addr);
 
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inetcfg_addr_delete(): sync");
+
+	rc = inet_cfg_sync(cfg);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_ERROR, "Error saving configuration.");
+		free(info.svc_name);
+		return rc;
+	}
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inetcfg_addr_delete(): get link by ID");
+
+	ilink = inet_link_get_by_id(info.svc_id);
+	if (ilink == NULL) {
+		log_msg(LOG_DEFAULT, LVL_ERROR, "Error finding link.");
+		return ENOENT;
+	}
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inetcfg_addr_delete(): check addrobj count");
+
+	/* If there are no configured addresses left, autoconfigure link */
+	acnt = inet_addrobj_cnt_by_link(ilink);
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inetcfg_addr_delete(): acnt=%u", acnt);
+	if (acnt == 0)
+		inet_link_autoconf_link(&info);
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inetcfg_addr_delete(): DONE");
 	return EOK;
 }
@@ -193,4 +236,5 @@
     inet_addr_t *router, sysarg_t *sroute_id)
 {
+	errno_t rc;
 	inet_sroute_t *sroute;
 
@@ -207,4 +251,11 @@
 
 	*sroute_id = sroute->id;
+
+	rc = inet_cfg_sync(cfg);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_ERROR, "Error saving configuration.");
+		return rc;
+	}
+
 	return EOK;
 }
@@ -212,4 +263,5 @@
 static errno_t inetcfg_sroute_delete(sysarg_t sroute_id)
 {
+	errno_t rc;
 	inet_sroute_t *sroute;
 
@@ -220,4 +272,10 @@
 	inet_sroute_remove(sroute);
 	inet_sroute_delete(sroute);
+
+	rc = inet_cfg_sync(cfg);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_ERROR, "Error saving configuration.");
+		return rc;
+	}
 
 	return EOK;
@@ -805,4 +863,143 @@
 }
 
+static errno_t inet_cfg_load(const char *cfg_path)
+{
+	sif_doc_t *doc = NULL;
+	sif_node_t *rnode;
+	sif_node_t *naddrs;
+	sif_node_t *nroutes;
+	const char *ntype;
+	errno_t rc;
+
+	rc = sif_load(cfg_path, &doc);
+	if (rc != EOK)
+		goto error;
+
+	rnode = sif_get_root(doc);
+	naddrs = sif_node_first_child(rnode);
+	ntype = sif_node_get_type(naddrs);
+	if (str_cmp(ntype, "addresses") != 0) {
+		rc = EIO;
+		goto error;
+	}
+
+	rc = inet_addrobjs_load(naddrs);
+	if (rc != EOK)
+		goto error;
+
+	nroutes = sif_node_next_child(naddrs);
+	ntype = sif_node_get_type(nroutes);
+	if (str_cmp(ntype, "static-routes") != 0) {
+		rc = EIO;
+		goto error;
+	}
+
+	rc = inet_sroutes_load(nroutes);
+	if (rc != EOK)
+		goto error;
+
+	sif_delete(doc);
+	return EOK;
+error:
+	if (doc != NULL)
+		sif_delete(doc);
+	return rc;
+
+}
+
+static errno_t inet_cfg_save(const char *cfg_path)
+{
+	sif_doc_t *doc = NULL;
+	sif_node_t *rnode;
+	sif_node_t *nsroutes;
+	sif_node_t *naddrobjs;
+	errno_t rc;
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inet_cfg_save(%s)", cfg_path);
+
+	rc = sif_new(&doc);
+	if (rc != EOK)
+		goto error;
+
+	rnode = sif_get_root(doc);
+
+	/* Address objects */
+
+	rc = sif_node_append_child(rnode, "addresses", &naddrobjs);
+	if (rc != EOK)
+		goto error;
+
+	rc = inet_addrobjs_save(naddrobjs);
+	if (rc != EOK)
+		goto error;
+
+	/* Static routes */
+
+	rc = sif_node_append_child(rnode, "static-routes", &nsroutes);
+	if (rc != EOK)
+		goto error;
+
+	rc = inet_sroutes_save(nsroutes);
+	if (rc != EOK)
+		goto error;
+
+	/* Save */
+
+	rc = sif_save(doc, cfg_path);
+	if (rc != EOK)
+		goto error;
+
+	sif_delete(doc);
+	return EOK;
+error:
+	if (doc != NULL)
+		sif_delete(doc);
+	return rc;
+}
+
+/** Open internet server configuration.
+ *
+ * @param cfg_path Configuration file path
+ * @param rcfg Place to store pointer to configuration object
+ * @return EOK on success or an error code
+ */
+errno_t inet_cfg_open(const char *cfg_path, inet_cfg_t **rcfg)
+{
+	inet_cfg_t *cfg;
+	errno_t rc;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "inet_cfg_open(%s)", cfg_path);
+
+	rc = inet_cfg_load(cfg_path);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_WARN, "inet_cfg_open(%s) :"
+		    "could not load configuration.", cfg_path);
+	}
+
+	cfg = calloc(1, sizeof(inet_cfg_t));
+	if (cfg == NULL)
+		return ENOMEM;
+
+	cfg->cfg_path = str_dup(cfg_path);
+	if (cfg->cfg_path == NULL) {
+		free(cfg);
+		return ENOMEM;
+	}
+
+	*rcfg = cfg;
+	return EOK;
+}
+
+errno_t inet_cfg_sync(inet_cfg_t *cfg)
+{
+	log_msg(LOG_DEFAULT, LVL_NOTE, "inet_cfg_sync(cfg=%p)", cfg);
+	return inet_cfg_save(cfg->cfg_path);
+}
+
+void inet_cfg_close(inet_cfg_t *cfg)
+{
+	free(cfg);
+}
+
 /** @}
  */
Index: uspace/srv/net/inetsrv/inetcfg.h
===================================================================
--- uspace/srv/net/inetsrv/inetcfg.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/inetsrv/inetcfg.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2012 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -38,5 +38,11 @@
 #define INETCFG_H_
 
+#include <errno.h>
+#include "inetsrv.h"
+
 extern void inet_cfg_conn(ipc_call_t *, void *);
+extern errno_t inet_cfg_open(const char *, inet_cfg_t **);
+extern errno_t inet_cfg_sync(inet_cfg_t *);
+extern void inet_cfg_close(inet_cfg_t *);
 
 #endif
Index: uspace/srv/net/inetsrv/inetsrv.c
===================================================================
--- uspace/srv/net/inetsrv/inetsrv.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/inetsrv/inetsrv.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2023 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -79,6 +79,9 @@
 };
 
+static const char *inet_cfg_path = "/w/cfg/inetsrv.sif";
+
 static FIBRIL_MUTEX_INITIALIZE(client_list_lock);
 static LIST_INITIALIZE(client_list);
+inet_cfg_t *cfg;
 
 static void inet_default_conn(ipc_call_t *, void *);
@@ -86,10 +89,19 @@
 static errno_t inet_init(void)
 {
+	port_id_t port;
+	errno_t rc;
 	loc_srv_t *srv;
 
 	log_msg(LOG_DEFAULT, LVL_DEBUG, "inet_init()");
 
-	port_id_t port;
-	errno_t rc = async_create_port(INTERFACE_INET,
+	rc = inet_link_discovery_start();
+	if (rc != EOK)
+		return rc;
+
+	rc = inet_cfg_open(inet_cfg_path, &cfg);
+	if (rc != EOK)
+		return rc;
+
+	rc = async_create_port(INTERFACE_INET,
 	    inet_default_conn, NULL, &port);
 	if (rc != EOK)
@@ -556,5 +568,8 @@
 
 	printf(NAME ": Accepting connections.\n");
+
 	task_retval(0);
+
+	(void)inet_link_autoconf();
 	async_manager();
 
Index: uspace/srv/net/inetsrv/inetsrv.h
===================================================================
--- uspace/srv/net/inetsrv/inetsrv.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/inetsrv/inetsrv.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2021 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -44,4 +44,5 @@
 #include <inet/iplink.h>
 #include <ipc/loc.h>
+#include <sif.h>
 #include <stddef.h>
 #include <stdint.h>
@@ -114,10 +115,24 @@
 } inet_link_t;
 
+/** Link information needed for autoconfiguration */
 typedef struct {
+	service_id_t svc_id;
+	char *svc_name;
+} inet_link_cfg_info_t;
+
+/** Address object */
+typedef struct {
+	/** Link to list of addresses */
 	link_t addr_list;
+	/** Address object ID */
 	sysarg_t id;
+	/** Network address */
 	inet_naddr_t naddr;
+	/** Underlying IP link */
 	inet_link_t *ilink;
+	/** Address name */
 	char *name;
+	/** Temporary object */
+	bool temp;
 } inet_addrobj_t;
 
@@ -125,4 +140,5 @@
 typedef struct {
 	link_t sroute_list;
+	/** ID */
 	sysarg_t id;
 	/** Destination network */
@@ -130,5 +146,8 @@
 	/** Router via which to route packets */
 	inet_addr_t router;
+	/** Route name */
 	char *name;
+	/** Temporary route */
+	bool temp;
 } inet_sroute_t;
 
@@ -152,4 +171,12 @@
 } inet_dir_t;
 
+/** Internet server configuration */
+typedef struct {
+	/** Configuration file path */
+	char *cfg_path;
+} inet_cfg_t;
+
+extern inet_cfg_t *cfg;
+
 extern errno_t inet_ev_recv(inet_client_t *, inet_dgram_t *);
 extern errno_t inet_recv_packet(inet_packet_t *);
Index: uspace/srv/net/inetsrv/meson.build
===================================================================
--- uspace/srv/net/inetsrv/meson.build	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/inetsrv/meson.build	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 #
-# Copyright (c) 2021 Jiri Svoboda
+# Copyright (c) 2024 Jiri Svoboda
 # All rights reserved.
 #
@@ -27,5 +27,5 @@
 #
 
-deps = [ 'inet' ]
+deps = [ 'inet', 'sif' ]
 src = files(
 	'addrobj.c',
Index: uspace/srv/net/inetsrv/sroute.c
===================================================================
--- uspace/srv/net/inetsrv/sroute.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/inetsrv/sroute.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2012 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -40,4 +40,6 @@
 #include <io/log.h>
 #include <ipc/loc.h>
+#include <sif.h>
+#include <stdio.h>
 #include <stdlib.h>
 #include <str.h>
@@ -210,4 +212,201 @@
 }
 
+/** Load static route from SIF node.
+ *
+ * @param nroute SIF node to load static route from
+ * @return EOK on success or an error code
+ */
+static errno_t inet_sroute_load(sif_node_t *nroute)
+{
+	errno_t rc;
+	const char *sid;
+	const char *sdest;
+	const char *srouter;
+	const char *name;
+	char *endptr;
+	int id;
+	inet_naddr_t dest;
+	inet_addr_t router;
+	inet_sroute_t *sroute;
+
+	sid = sif_node_get_attr(nroute, "id");
+	if (sid == NULL)
+		return EIO;
+
+	sdest = sif_node_get_attr(nroute, "dest");
+	if (sdest == NULL)
+		return EIO;
+
+	srouter = sif_node_get_attr(nroute, "router");
+	if (srouter == NULL)
+		return EIO;
+
+	name = sif_node_get_attr(nroute, "name");
+	if (name == NULL)
+		return EIO;
+
+	id = strtoul(sid, &endptr, 10);
+	if (*endptr != '\0')
+		return EIO;
+
+	rc = inet_naddr_parse(sdest, &dest, NULL);
+	if (rc != EOK)
+		return EIO;
+
+	rc = inet_addr_parse(srouter, &router, NULL);
+	if (rc != EOK)
+		return EIO;
+
+	sroute = inet_sroute_new();
+	if (sroute == NULL)
+		return ENOMEM;
+
+	sroute->id = id;
+	sroute->dest = dest;
+	sroute->router = router;
+	sroute->name = str_dup(name);
+
+	if (sroute->name == NULL) {
+		inet_sroute_delete(sroute);
+		return ENOMEM;
+	}
+
+	inet_sroute_add(sroute);
+	return EOK;
+}
+
+/** Load static routes from SIF node.
+ *
+ * @param nroutes SIF node to load static routes from
+ * @return EOK on success or an error code
+ */
+errno_t inet_sroutes_load(sif_node_t *nroutes)
+{
+	sif_node_t *nroute;
+	const char *ntype;
+	errno_t rc;
+
+	nroute = sif_node_first_child(nroutes);
+	while (nroute != NULL) {
+		ntype = sif_node_get_type(nroute);
+		if (str_cmp(ntype, "route") != 0) {
+			rc = EIO;
+			goto error;
+		}
+
+		rc = inet_sroute_load(nroute);
+		if (rc != EOK)
+			goto error;
+
+		nroute = sif_node_next_child(nroute);
+	}
+
+	return EOK;
+error:
+	return rc;
+}
+
+/** Save static route to SIF node.
+ *
+ * @param sroute Static route
+ * @param nroute SIF node to save static route to
+ * @return EOK on success or an error code
+ */
+static errno_t inet_sroute_save(inet_sroute_t *sroute, sif_node_t *nroute)
+{
+	char *str = NULL;
+	errno_t rc;
+	int rv;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "inet_sroute_save(%p, %p)",
+	    sroute, nroute);
+
+	/* id */
+
+	rv = asprintf(&str, "%zu", sroute->id);
+	if (rv < 0) {
+		str = NULL;
+		rc = ENOMEM;
+		goto error;
+	}
+
+	rc = sif_node_set_attr(nroute, "id", str);
+	if (rc != EOK)
+		goto error;
+
+	free(str);
+	str = NULL;
+
+	/* dest */
+
+	rc = inet_naddr_format(&sroute->dest, &str);
+	if (rc != EOK)
+		goto error;
+
+	rc = sif_node_set_attr(nroute, "dest", str);
+	if (rc != EOK)
+		goto error;
+
+	free(str);
+	str = NULL;
+
+	/* router */
+
+	rc = inet_addr_format(&sroute->router, &str);
+	if (rc != EOK)
+		goto error;
+
+	rc = sif_node_set_attr(nroute, "router", str);
+	if (rc != EOK)
+		goto error;
+
+	/* name */
+
+	rc = sif_node_set_attr(nroute, "name", sroute->name);
+	if (rc != EOK)
+		goto error;
+
+	free(str);
+
+	return rc;
+error:
+	if (str != NULL)
+		free(str);
+	return rc;
+}
+
+/** Save static routes to SIF node.
+ *
+ * @param nroutes SIF node to save static routes to
+ * @return EOK on success or an error code
+ */
+errno_t inet_sroutes_save(sif_node_t *nroutes)
+{
+	sif_node_t *nroute;
+	errno_t rc;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "inet_sroutes_save()");
+
+	fibril_mutex_lock(&sroute_list_lock);
+
+	list_foreach(sroute_list, sroute_list, inet_sroute_t, sroute) {
+		if (sroute->temp == false) {
+			rc = sif_node_append_child(nroutes, "route", &nroute);
+			if (rc != EOK)
+				goto error;
+
+			rc = inet_sroute_save(sroute, nroute);
+			if (rc != EOK)
+				goto error;
+		}
+	}
+
+	fibril_mutex_unlock(&sroute_list_lock);
+	return EOK;
+error:
+	fibril_mutex_unlock(&sroute_list_lock);
+	return rc;
+}
+
 /** @}
  */
Index: uspace/srv/net/inetsrv/sroute.h
===================================================================
--- uspace/srv/net/inetsrv/sroute.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/net/inetsrv/sroute.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2012 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -38,4 +38,5 @@
 #define INET_SROUTE_H_
 
+#include <sif.h>
 #include <stddef.h>
 #include <stdint.h>
@@ -52,4 +53,6 @@
     inet_dgram_t *, uint8_t, uint8_t, int);
 extern errno_t inet_sroute_get_id_list(sysarg_t **, size_t *);
+extern errno_t inet_sroutes_load(sif_node_t *);
+extern errno_t inet_sroutes_save(sif_node_t *);
 
 #endif
Index: uspace/srv/net/nconfsrv/iplink.c
===================================================================
--- uspace/srv/net/nconfsrv/iplink.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ 	(revision )
@@ -1,237 +1,0 @@
-/*
- * Copyright (c) 2013 Jiri Svoboda
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- * - Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- * - The name of the author may not be used to endorse or promote products
- *   derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/** @addtogroup nconfsrv
- * @{
- */
-/**
- * @file
- * @brief
- */
-
-#include <stdbool.h>
-#include <errno.h>
-#include <str_error.h>
-#include <fibril_synch.h>
-#include <inet/dhcp.h>
-#include <inet/inetcfg.h>
-#include <io/log.h>
-#include <loc.h>
-#include <stdlib.h>
-#include <str.h>
-
-#include "iplink.h"
-#include "nconfsrv.h"
-
-static errno_t ncs_link_add(service_id_t);
-
-static LIST_INITIALIZE(ncs_links);
-static FIBRIL_MUTEX_INITIALIZE(ncs_links_lock);
-
-static errno_t ncs_link_check_new(void)
-{
-	bool already_known;
-	category_id_t iplink_cat;
-	service_id_t *svcs;
-	size_t count, i;
-	errno_t rc;
-
-	fibril_mutex_lock(&ncs_links_lock);
-
-	rc = loc_category_get_id("iplink", &iplink_cat, IPC_FLAG_BLOCKING);
-	if (rc != EOK) {
-		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed resolving category 'iplink'.");
-		fibril_mutex_unlock(&ncs_links_lock);
-		return ENOENT;
-	}
-
-	rc = loc_category_get_svcs(iplink_cat, &svcs, &count);
-	if (rc != EOK) {
-		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed getting list of IP links.");
-		fibril_mutex_unlock(&ncs_links_lock);
-		return EIO;
-	}
-
-	for (i = 0; i < count; i++) {
-		already_known = false;
-
-		list_foreach(ncs_links, link_list, ncs_link_t, ilink) {
-			if (ilink->svc_id == svcs[i]) {
-				already_known = true;
-				break;
-			}
-		}
-
-		if (!already_known) {
-			log_msg(LOG_DEFAULT, LVL_NOTE, "Found IP link '%lu'",
-			    (unsigned long) svcs[i]);
-			rc = ncs_link_add(svcs[i]);
-			if (rc != EOK)
-				log_msg(LOG_DEFAULT, LVL_ERROR, "Could not add IP link.");
-		}
-	}
-
-	fibril_mutex_unlock(&ncs_links_lock);
-	return EOK;
-}
-
-static ncs_link_t *ncs_link_new(void)
-{
-	ncs_link_t *nlink = calloc(1, sizeof(ncs_link_t));
-
-	if (nlink == NULL) {
-		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed allocating link structure. "
-		    "Out of memory.");
-		return NULL;
-	}
-
-	link_initialize(&nlink->link_list);
-
-	return nlink;
-}
-
-static void ncs_link_delete(ncs_link_t *nlink)
-{
-	if (nlink->svc_name != NULL)
-		free(nlink->svc_name);
-
-	free(nlink);
-}
-
-static errno_t ncs_link_add(service_id_t sid)
-{
-	ncs_link_t *nlink;
-	errno_t rc;
-
-	assert(fibril_mutex_is_locked(&ncs_links_lock));
-
-	log_msg(LOG_DEFAULT, LVL_DEBUG, "ncs_link_add()");
-	nlink = ncs_link_new();
-	if (nlink == NULL)
-		return ENOMEM;
-
-	nlink->svc_id = sid;
-
-	rc = loc_service_get_name(sid, &nlink->svc_name);
-	if (rc != EOK) {
-		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed getting service name.");
-		goto error;
-	}
-
-	log_msg(LOG_DEFAULT, LVL_NOTE, "Configure link %s", nlink->svc_name);
-	rc = inetcfg_link_add(sid);
-	if (rc != EOK) {
-		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed configuring link "
-		    "'%s'.\n", nlink->svc_name);
-		goto error;
-	}
-
-	if (str_lcmp(nlink->svc_name, "net/eth", str_length("net/eth")) == 0) {
-		rc = dhcp_link_add(sid);
-		if (rc != EOK) {
-			log_msg(LOG_DEFAULT, LVL_ERROR, "Failed configuring DHCP on "
-			    " link '%s'.\n", nlink->svc_name);
-			goto error;
-		}
-	}
-
-	list_append(&nlink->link_list, &ncs_links);
-
-	return EOK;
-
-error:
-	ncs_link_delete(nlink);
-	return rc;
-}
-
-static void ncs_link_cat_change_cb(void *arg)
-{
-	(void) ncs_link_check_new();
-}
-
-errno_t ncs_link_discovery_start(void)
-{
-	errno_t rc;
-
-	rc = loc_register_cat_change_cb(ncs_link_cat_change_cb, NULL);
-	if (rc != EOK) {
-		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed registering callback for IP link "
-		    "discovery: %s.", str_error(rc));
-		return rc;
-	}
-
-	return ncs_link_check_new();
-}
-
-ncs_link_t *ncs_link_get_by_id(sysarg_t link_id)
-{
-	fibril_mutex_lock(&ncs_links_lock);
-
-	list_foreach(ncs_links, link_list, ncs_link_t, nlink) {
-		if (nlink->svc_id == link_id) {
-			fibril_mutex_unlock(&ncs_links_lock);
-			return nlink;
-		}
-	}
-
-	fibril_mutex_unlock(&ncs_links_lock);
-	return NULL;
-}
-
-/** Get IDs of all links. */
-errno_t ncs_link_get_id_list(sysarg_t **rid_list, size_t *rcount)
-{
-	sysarg_t *id_list;
-	size_t count, i;
-
-	fibril_mutex_lock(&ncs_links_lock);
-	count = list_count(&ncs_links);
-
-	id_list = calloc(count, sizeof(sysarg_t));
-	if (id_list == NULL) {
-		fibril_mutex_unlock(&ncs_links_lock);
-		return ENOMEM;
-	}
-
-	i = 0;
-	list_foreach(ncs_links, link_list, ncs_link_t, nlink) {
-		id_list[i++] = nlink->svc_id;
-		log_msg(LOG_DEFAULT, LVL_NOTE, "add link to list");
-	}
-
-	fibril_mutex_unlock(&ncs_links_lock);
-
-	log_msg(LOG_DEFAULT, LVL_NOTE, "return %zu links", count);
-	*rid_list = id_list;
-	*rcount = count;
-
-	return EOK;
-}
-
-/** @}
- */
Index: uspace/srv/net/nconfsrv/iplink.h
===================================================================
--- uspace/srv/net/nconfsrv/iplink.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ 	(revision )
@@ -1,50 +1,0 @@
-/*
- * Copyright (c) 2013 Jiri Svoboda
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- * - Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- * - The name of the author may not be used to endorse or promote products
- *   derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/** @addtogroup nconfsrv
- * @{
- */
-/**
- * @file
- * @brief
- */
-
-#ifndef NCONFSRV_IPLINK_H_
-#define NCONFSRV_IPLINK_H_
-
-#include <stddef.h>
-#include "nconfsrv.h"
-
-extern errno_t ncs_link_discovery_start(void);
-extern ncs_link_t *ncs_link_get_by_id(sysarg_t);
-extern errno_t ncs_link_get_id_list(sysarg_t **, size_t *);
-
-#endif
-
-/** @}
- */
Index: uspace/srv/net/nconfsrv/meson.build
===================================================================
--- uspace/srv/net/nconfsrv/meson.build	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ 	(revision )
@@ -1,30 +1,0 @@
-#
-# Copyright (c) 2021 Jiri Svoboda
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# - Redistributions of source code must retain the above copyright
-#   notice, this list of conditions and the following disclaimer.
-# - Redistributions in binary form must reproduce the above copyright
-#   notice, this list of conditions and the following disclaimer in the
-#   documentation and/or other materials provided with the distribution.
-# - The name of the author may not be used to endorse or promote products
-#   derived from this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
-# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
-# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
-# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
-# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
-# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-#
-
-deps = [ 'inet' ]
-src = files('iplink.c', 'nconfsrv.c')
Index: uspace/srv/net/nconfsrv/nconfsrv.c
===================================================================
--- uspace/srv/net/nconfsrv/nconfsrv.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ 	(revision )
@@ -1,134 +1,0 @@
-/*
- * Copyright (c) 2023 Jiri Svoboda
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- * - Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- * - The name of the author may not be used to endorse or promote products
- *   derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/** @addtogroup nconfsrv
- * @{
- */
-/**
- * @file
- * @brief Network configuration Service
- */
-
-#include <adt/list.h>
-#include <async.h>
-#include <errno.h>
-#include <str_error.h>
-#include <fibril_synch.h>
-#include <inet/dhcp.h>
-#include <inet/inetcfg.h>
-#include <io/log.h>
-#include <ipc/inet.h>
-#include <ipc/services.h>
-#include <loc.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <task.h>
-#include "iplink.h"
-#include "nconfsrv.h"
-
-#define NAME "nconfsrv"
-
-static void ncs_client_conn(ipc_call_t *icall, void *arg);
-
-static errno_t ncs_init(void)
-{
-	service_id_t sid;
-	loc_srv_t *srv;
-	errno_t rc;
-
-	log_msg(LOG_DEFAULT, LVL_DEBUG, "ncs_init()");
-
-	rc = inetcfg_init();
-	if (rc != EOK) {
-		log_msg(LOG_DEFAULT, LVL_ERROR, "Error contacting inet "
-		    "configuration service.");
-		return EIO;
-	}
-
-	rc = dhcp_init();
-	if (rc != EOK) {
-		log_msg(LOG_DEFAULT, LVL_ERROR, "Error contacting dhcp "
-		    "configuration service.");
-		return EIO;
-	}
-
-	async_set_fallback_port_handler(ncs_client_conn, NULL);
-
-	rc = loc_server_register(NAME, &srv);
-	if (rc != EOK) {
-		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed registering server: %s.", str_error(rc));
-		return EEXIST;
-	}
-
-	rc = loc_service_register(srv, SERVICE_NAME_NETCONF, &sid);
-	if (rc != EOK) {
-		loc_server_unregister(srv);
-		log_msg(LOG_DEFAULT, LVL_ERROR, "Failed registering service: %s.", str_error(rc));
-		return EEXIST;
-	}
-
-	rc = ncs_link_discovery_start();
-	if (rc != EOK) {
-		loc_service_unregister(srv, sid);
-		loc_server_unregister(srv);
-		return EEXIST;
-	}
-
-	return EOK;
-}
-
-static void ncs_client_conn(ipc_call_t *icall, void *arg)
-{
-	async_answer_0(icall, ENOTSUP);
-}
-
-int main(int argc, char *argv[])
-{
-	errno_t rc;
-
-	printf(NAME ": HelenOS Network configuration service\n");
-
-	if (log_init(NAME) != EOK) {
-		printf(NAME ": Failed to initialize logging.\n");
-		return 1;
-	}
-
-	rc = ncs_init();
-	if (rc != EOK)
-		return 1;
-
-	printf(NAME ": Accepting connections.\n");
-	task_retval(0);
-	async_manager();
-
-	/* Not reached */
-	return 0;
-}
-
-/** @}
- */
Index: uspace/srv/net/nconfsrv/nconfsrv.h
===================================================================
--- uspace/srv/net/nconfsrv/nconfsrv.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ 	(revision )
@@ -1,52 +1,0 @@
-/*
- * Copyright (c) 2013 Jiri Svoboda
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- * - Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- * - The name of the author may not be used to endorse or promote products
- *   derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/** @addtogroup inetsrv
- * @{
- */
-/**
- * @file
- * @brief
- */
-
-#ifndef INETSRV_H_
-#define INETSRV_H_
-
-#include <adt/list.h>
-#include <ipc/loc.h>
-
-typedef struct {
-	link_t link_list;
-	service_id_t svc_id;
-	char *svc_name;
-} ncs_link_t;
-
-#endif
-
-/** @}
- */
Index: uspace/srv/system/doc/doxygroups.h
===================================================================
--- uspace/srv/system/doc/doxygroups.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
+++ uspace/srv/system/doc/doxygroups.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -0,0 +1,4 @@
+/** @addtogroup system system
+ * @brief System state control server
+ * @ingroup srvs
+ */
Index: uspace/srv/system/meson.build
===================================================================
--- uspace/srv/system/meson.build	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
+++ uspace/srv/system/meson.build	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -0,0 +1,30 @@
+#
+# Copyright (c) 2024 Jiri Svoboda
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# - Redistributions of source code must retain the above copyright
+#   notice, this list of conditions and the following disclaimer.
+# - Redistributions in binary form must reproduce the above copyright
+#   notice, this list of conditions and the following disclaimer in the
+#   documentation and/or other materials provided with the distribution.
+# - The name of the author may not be used to endorse or promote products
+#   derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+deps = [ 'device', 'futil', 'system' ]
+src = files('system.c')
Index: uspace/srv/system/system.c
===================================================================
--- uspace/srv/system/system.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
+++ uspace/srv/system/system.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -0,0 +1,664 @@
+/*
+ * Copyright (c) 2024 Jiri Svoboda
+ * Copyright (c) 2005 Martin Decky
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup init
+ * @{
+ */
+/**
+ * @file
+ */
+
+#include <fibril.h>
+#include <futil.h>
+#include <io/log.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <vfs/vfs.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <task.h>
+#include <stdlib.h>
+#include <macros.h>
+#include <str.h>
+#include <loc.h>
+#include <str_error.h>
+#include <config.h>
+#include <io/logctl.h>
+#include <vfs/vfs.h>
+#include <vol.h>
+#include <system.h>
+#include <system_srv.h>
+#include "system.h"
+
+#define BANNER_LEFT   "######> "
+#define BANNER_RIGHT  " <######"
+
+#define LOCFS_FS_TYPE      "locfs"
+#define LOCFS_MOUNT_POINT  "/loc"
+
+#define TMPFS_FS_TYPE      "tmpfs"
+#define TMPFS_MOUNT_POINT  "/tmp"
+
+#define SRV_CONSOLE  "/srv/hid/console"
+#define APP_GETTERM  "/app/getterm"
+
+#define SRV_DISPLAY  "/srv/hid/display"
+
+#define HID_INPUT              "hid/input"
+#define HID_OUTPUT             "hid/output"
+
+#define srv_start(path, ...) \
+	srv_startl(path, path, ##__VA_ARGS__, NULL)
+
+static const char *sys_dirs[] = {
+	"/w/cfg",
+	"/w/data",
+	NULL,
+};
+
+static void system_srv_conn(ipc_call_t *, void *);
+static errno_t system_srv_shutdown(void *);
+
+system_ops_t system_srv_ops = {
+	.shutdown = system_srv_shutdown
+};
+
+/** Print banner */
+static void info_print(void)
+{
+	printf("%s: HelenOS system server\n", NAME);
+}
+
+static void oom_check(errno_t rc, const char *path)
+{
+	if (rc == ENOMEM) {
+		printf("%sOut-of-memory condition detected%s\n", BANNER_LEFT,
+		    BANNER_RIGHT);
+		printf("%sBailing out of the boot process after %s%s\n",
+		    BANNER_LEFT, path, BANNER_RIGHT);
+		printf("%sMore physical memory is required%s\n", BANNER_LEFT,
+		    BANNER_RIGHT);
+		exit(ENOMEM);
+	}
+}
+
+/** Report mount operation success */
+static bool mount_report(const char *desc, const char *mntpt,
+    const char *fstype, const char *dev, errno_t rc)
+{
+	switch (rc) {
+	case EOK:
+		if ((dev != NULL) && (str_cmp(dev, "") != 0))
+			printf("%s: %s mounted on %s (%s at %s)\n", NAME, desc, mntpt,
+			    fstype, dev);
+		else
+			printf("%s: %s mounted on %s (%s)\n", NAME, desc, mntpt, fstype);
+		break;
+	case EBUSY:
+		printf("%s: %s already mounted on %s\n", NAME, desc, mntpt);
+		return false;
+	case ELIMIT:
+		printf("%s: %s limit exceeded\n", NAME, desc);
+		return false;
+	case ENOENT:
+		printf("%s: %s unknown type (%s)\n", NAME, desc, fstype);
+		return false;
+	default:
+		printf("%s: %s not mounted on %s (%s)\n", NAME, desc, mntpt,
+		    str_error(rc));
+		return false;
+	}
+
+	return true;
+}
+
+/** Mount locfs file system
+ *
+ * The operation blocks until the locfs file system
+ * server is ready for mounting.
+ *
+ * @return True on success.
+ * @return False on failure.
+ *
+ */
+static bool mount_locfs(void)
+{
+	errno_t rc = vfs_mount_path(LOCFS_MOUNT_POINT, LOCFS_FS_TYPE, "", "",
+	    IPC_FLAG_BLOCKING, 0);
+	return mount_report("Location service file system", LOCFS_MOUNT_POINT,
+	    LOCFS_FS_TYPE, NULL, rc);
+}
+
+static errno_t srv_startl(const char *path, ...)
+{
+	vfs_stat_t s;
+	if (vfs_stat_path(path, &s) != EOK) {
+		printf("%s: Unable to stat %s\n", NAME, path);
+		return ENOENT;
+	}
+
+	printf("%s: Starting %s\n", NAME, path);
+
+	va_list ap;
+	const char *arg;
+	int cnt = 0;
+
+	va_start(ap, path);
+	do {
+		arg = va_arg(ap, const char *);
+		cnt++;
+	} while (arg != NULL);
+	va_end(ap);
+
+	va_start(ap, path);
+	task_id_t id;
+	task_wait_t wait;
+	errno_t rc = task_spawn(&id, &wait, path, cnt, ap);
+	va_end(ap);
+
+	if (rc != EOK) {
+		oom_check(rc, path);
+		printf("%s: Error spawning %s (%s)\n", NAME, path,
+		    str_error(rc));
+		return rc;
+	}
+
+	if (!id) {
+		printf("%s: Error spawning %s (invalid task id)\n", NAME,
+		    path);
+		return EINVAL;
+	}
+
+	task_exit_t texit;
+	int retval;
+	rc = task_wait(&wait, &texit, &retval);
+	if (rc != EOK) {
+		printf("%s: Error waiting for %s (%s)\n", NAME, path,
+		    str_error(rc));
+		return rc;
+	}
+
+	if (texit != TASK_EXIT_NORMAL) {
+		printf("%s: Server %s failed to start (unexpectedly "
+		    "terminated)\n", NAME, path);
+		return EINVAL;
+	}
+
+	if (retval != 0)
+		printf("%s: Server %s failed to start (exit code %d)\n", NAME,
+		    path, retval);
+
+	return retval == 0 ? EOK : EPARTY;
+}
+
+static errno_t console(const char *isvc, const char *osvc)
+{
+	/* Wait for the input service to be ready */
+	service_id_t service_id;
+	errno_t rc = loc_service_get_id(isvc, &service_id, IPC_FLAG_BLOCKING);
+	if (rc != EOK) {
+		printf("%s: Error waiting on %s (%s)\n", NAME, isvc,
+		    str_error(rc));
+		return rc;
+	}
+
+	/* Wait for the output service to be ready */
+	rc = loc_service_get_id(osvc, &service_id, IPC_FLAG_BLOCKING);
+	if (rc != EOK) {
+		printf("%s: Error waiting on %s (%s)\n", NAME, osvc,
+		    str_error(rc));
+		return rc;
+	}
+
+	return srv_start(SRV_CONSOLE, isvc, osvc);
+}
+
+#ifdef CONFIG_WINSYS
+
+static errno_t display_server(void)
+{
+	return srv_start(SRV_DISPLAY);
+}
+
+static int app_start(const char *app, const char *arg)
+{
+	printf("%s: Spawning %s\n", NAME, app);
+
+	task_id_t id;
+	task_wait_t wait;
+	errno_t rc = task_spawnl(&id, &wait, app, app, arg, NULL);
+	if (rc != EOK) {
+		oom_check(rc, app);
+		printf("%s: Error spawning %s (%s)\n", NAME, app,
+		    str_error(rc));
+		return -1;
+	}
+
+	task_exit_t texit;
+	int retval;
+	rc = task_wait(&wait, &texit, &retval);
+	if ((rc != EOK) || (texit != TASK_EXIT_NORMAL)) {
+		printf("%s: Error retrieving retval from %s (%s)\n", NAME,
+		    app, str_error(rc));
+		return rc;
+	}
+
+	return retval;
+}
+
+#endif
+
+static void getterm(const char *svc, const char *app, bool msg)
+{
+	if (msg) {
+		printf("%s: Spawning %s %s %s --msg --wait -- %s\n", NAME,
+		    APP_GETTERM, svc, LOCFS_MOUNT_POINT, app);
+
+		errno_t rc = task_spawnl(NULL, NULL, APP_GETTERM, APP_GETTERM, svc,
+		    LOCFS_MOUNT_POINT, "--msg", "--wait", "--", app, NULL);
+		if (rc != EOK) {
+			oom_check(rc, APP_GETTERM);
+			printf("%s: Error spawning %s %s %s --msg --wait -- %s\n",
+			    NAME, APP_GETTERM, svc, LOCFS_MOUNT_POINT, app);
+		}
+	} else {
+		printf("%s: Spawning %s %s %s --wait -- %s\n", NAME,
+		    APP_GETTERM, svc, LOCFS_MOUNT_POINT, app);
+
+		errno_t rc = task_spawnl(NULL, NULL, APP_GETTERM, APP_GETTERM, svc,
+		    LOCFS_MOUNT_POINT, "--wait", "--", app, NULL);
+		if (rc != EOK) {
+			oom_check(rc, APP_GETTERM);
+			printf("%s: Error spawning %s %s %s --wait -- %s\n",
+			    NAME, APP_GETTERM, svc, LOCFS_MOUNT_POINT, app);
+		}
+	}
+}
+
+static bool mount_tmpfs(void)
+{
+	errno_t rc = vfs_mount_path(TMPFS_MOUNT_POINT, TMPFS_FS_TYPE, "", "", 0, 0);
+	return mount_report("Temporary file system", TMPFS_MOUNT_POINT,
+	    TMPFS_FS_TYPE, NULL, rc);
+}
+
+/** Init system volume.
+ *
+ * See if system volume is configured. If so, try to wait for it to become
+ * available. If not, create basic directories for live image omde.
+ */
+static errno_t init_sysvol(void)
+{
+	vol_t *vol = NULL;
+	vol_info_t vinfo;
+	volume_id_t *volume_ids = NULL;
+	service_id_t *part_ids = NULL;
+	vol_part_info_t pinfo;
+	size_t nvols;
+	size_t nparts;
+	bool sv_mounted;
+	size_t i;
+	errno_t rc;
+	bool found_cfg;
+	const char **cp;
+
+	rc = vol_create(&vol);
+	if (rc != EOK) {
+		printf("Error contacting volume service.\n");
+		goto error;
+	}
+
+	rc = vol_get_volumes(vol, &volume_ids, &nvols);
+	if (rc != EOK) {
+		printf("Error getting list of volumes.\n");
+		goto error;
+	}
+
+	/* XXX This could be handled more efficiently by volsrv itself */
+	found_cfg = false;
+	for (i = 0; i < nvols; i++) {
+		rc = vol_info(vol, volume_ids[i], &vinfo);
+		if (rc != EOK) {
+			printf("Error getting volume information.\n");
+			rc = EIO;
+			goto error;
+		}
+
+		if (str_cmp(vinfo.path, "/w") == 0) {
+			found_cfg = true;
+			break;
+		}
+	}
+
+	free(volume_ids);
+	volume_ids = NULL;
+
+	if (!found_cfg) {
+		/* Prepare directory structure for live image mode */
+		printf("%s: Creating live image directory structure.\n", NAME);
+		cp = sys_dirs;
+		while (*cp != NULL) {
+			rc = vfs_link_path(*cp, KIND_DIRECTORY, NULL);
+			if (rc != EOK) {
+				printf("%s: Error creating directory '%s'.\n",
+				    NAME, *cp);
+				goto error;
+			}
+
+			++cp;
+		}
+
+		/* Copy initial configuration files */
+		rc = futil_rcopy_contents("/cfg", "/w/cfg");
+		if (rc != EOK)
+			goto error;
+	} else {
+		printf("%s: System volume is configured.\n", NAME);
+
+		/* Wait until system volume is mounted */
+		sv_mounted = false;
+
+		while (true) {
+			rc = vol_get_parts(vol, &part_ids, &nparts);
+			if (rc != EOK) {
+				printf("Error getting list of volumes.\n");
+				goto error;
+			}
+
+			for (i = 0; i < nparts; i++) {
+				rc = vol_part_info(vol, part_ids[i], &pinfo);
+				if (rc != EOK) {
+					printf("Error getting partition "
+					    "information.\n");
+					rc = EIO;
+					goto error;
+				}
+
+				if (str_cmp(pinfo.cur_mp, "/w") == 0) {
+					sv_mounted = true;
+					break;
+				}
+			}
+
+			if (sv_mounted)
+				break;
+
+			free(part_ids);
+			part_ids = NULL;
+
+			fibril_sleep(1);
+			printf("Sleeping(1) for system volume.\n");
+		}
+	}
+
+	vol_destroy(vol);
+	return EOK;
+error:
+	vol_destroy(vol);
+	if (volume_ids != NULL)
+		free(volume_ids);
+	if (part_ids != NULL)
+		free(part_ids);
+
+	return rc;
+}
+
+/** Perform sytem startup tasks.
+ *
+ * @return EOK on success or an error code
+ */
+static errno_t system_startup(void)
+{
+	errno_t rc;
+
+	/* Make sure file systems are running. */
+	if (str_cmp(STRING(RDFMT), "tmpfs") != 0)
+		srv_start("/srv/fs/tmpfs");
+	if (str_cmp(STRING(RDFMT), "exfat") != 0)
+		srv_start("/srv/fs/exfat");
+	if (str_cmp(STRING(RDFMT), "fat") != 0)
+		srv_start("/srv/fs/fat");
+	srv_start("/srv/fs/cdfs");
+	srv_start("/srv/fs/mfs");
+
+	srv_start("/srv/klog");
+	srv_start("/srv/fs/locfs");
+
+	if (!mount_locfs()) {
+		printf("%s: Exiting\n", NAME);
+		return EIO;
+	}
+
+	mount_tmpfs();
+
+	srv_start("/srv/devman");
+	srv_start("/srv/hid/s3c24xx_uart");
+	srv_start("/srv/hid/s3c24xx_ts");
+
+	srv_start("/srv/bd/vbd");
+	srv_start("/srv/volsrv");
+	srv_start("/srv/bd/hr");
+
+	init_sysvol();
+
+	srv_start("/srv/taskmon");
+
+	srv_start("/srv/net/loopip");
+	srv_start("/srv/net/ethip");
+	srv_start("/srv/net/dhcp");
+	srv_start("/srv/net/inetsrv");
+	srv_start("/srv/net/tcp");
+	srv_start("/srv/net/udp");
+	srv_start("/srv/net/dnsrsrv");
+
+	srv_start("/srv/clipboard");
+	srv_start("/srv/hid/remcons");
+
+	srv_start("/srv/hid/input", HID_INPUT);
+	srv_start("/srv/hid/output", HID_OUTPUT);
+	srv_start("/srv/audio/hound");
+
+#ifdef CONFIG_WINSYS
+	if (!config_key_exists("console")) {
+		rc = display_server();
+		if (rc == EOK) {
+			app_start("/app/taskbar", NULL);
+			app_start("/app/terminal", "-topleft");
+		}
+	}
+#endif
+	rc = console(HID_INPUT, HID_OUTPUT);
+	if (rc == EOK) {
+		getterm("term/vc0", "/app/bdsh", true);
+		getterm("term/vc1", "/app/bdsh", false);
+		getterm("term/vc2", "/app/bdsh", false);
+		getterm("term/vc3", "/app/bdsh", false);
+		getterm("term/vc4", "/app/bdsh", false);
+		getterm("term/vc5", "/app/bdsh", false);
+	}
+
+	return EOK;
+}
+
+/** Perform sytem shutdown tasks.
+ *
+ * @return EOK on success or an error code
+ */
+static errno_t system_sys_shutdown(void)
+{
+	vol_t *vol = NULL;
+	service_id_t *part_ids = NULL;
+	size_t nparts;
+	size_t i;
+	errno_t rc;
+
+	/* Eject all volumes. */
+
+	rc = vol_create(&vol);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_ERROR, "Error contacting volume "
+		    "service.");
+		goto error;
+	}
+
+	rc = vol_get_parts(vol, &part_ids, &nparts);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_ERROR, "Error getting volume list.");
+		goto error;
+	}
+
+	for (i = 0; i < nparts; i++) {
+		rc = vol_part_eject(vol, part_ids[i]);
+		if (rc != EOK) {
+			log_msg(LOG_DEFAULT, LVL_ERROR, "Error ejecting "
+			    "volume %zu", (size_t)part_ids[i]);
+			goto error;
+		}
+	}
+
+	free(part_ids);
+	vol_destroy(vol);
+	return EOK;
+error:
+	if (part_ids != NULL)
+		free(part_ids);
+	if (vol != NULL)
+		vol_destroy(vol);
+	return rc;
+}
+
+/** Initialize system control service. */
+static errno_t system_srv_init(sys_srv_t *syssrv)
+{
+	port_id_t port;
+	loc_srv_t *srv = NULL;
+	service_id_t sid = 0;
+	errno_t rc;
+
+	(void)system;
+
+	log_msg(LOG_DEFAULT, LVL_DEBUG, "system_srv_init()");
+
+	rc = async_create_port(INTERFACE_SYSTEM, system_srv_conn, syssrv,
+	    &port);
+	if (rc != EOK)
+		goto error;
+
+	rc = loc_server_register(NAME, &srv);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_ERROR,
+		    "Failed registering server: %s.", str_error(rc));
+		rc = EEXIST;
+		goto error;
+	}
+
+	rc = loc_service_register(srv, SYSTEM_DEFAULT, &sid);
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_ERROR,
+		    "Failed registering service: %s.", str_error(rc));
+		rc = EEXIST;
+		goto error;
+	}
+
+	return EOK;
+error:
+	if (sid != 0)
+		loc_service_unregister(srv, sid);
+	if (srv != NULL)
+		loc_server_unregister(srv);
+	// XXX destroy port
+	return rc;
+}
+
+/** Handle connection to system server. */
+static void system_srv_conn(ipc_call_t *icall, void *arg)
+{
+	sys_srv_t *syssrv = (sys_srv_t *)arg;
+
+	/* Set up protocol structure */
+	system_srv_initialize(&syssrv->srv);
+	syssrv->srv.ops = &system_srv_ops;
+	syssrv->srv.arg = syssrv;
+
+	/* Handle connection */
+	system_conn(icall, &syssrv->srv);
+}
+
+/** System shutdown request.
+ *
+ * @param arg Argument (sys_srv_t *)
+ */
+static errno_t system_srv_shutdown(void *arg)
+{
+	sys_srv_t *syssrv = (sys_srv_t *)arg;
+	errno_t rc;
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "system_srv_shutdown");
+
+	rc = system_sys_shutdown();
+	if (rc != EOK) {
+		log_msg(LOG_DEFAULT, LVL_NOTE, "system_srv_shutdown failed");
+		system_srv_shutdown_failed(&syssrv->srv);
+	}
+
+	log_msg(LOG_DEFAULT, LVL_NOTE, "system_srv_shutdown complete");
+	system_srv_shutdown_complete(&syssrv->srv);
+	return EOK;
+}
+
+int main(int argc, char *argv[])
+{
+	errno_t rc;
+	sys_srv_t srv;
+
+	info_print();
+
+	if (log_init(NAME) != EOK) {
+		printf(NAME ": Failed to initialize logging.\n");
+		return 1;
+	}
+
+	/* Perform startup tasks. */
+	rc = system_startup();
+	if (rc != EOK)
+		return 1;
+
+	rc = system_srv_init(&srv);
+	if (rc != EOK)
+		return 1;
+
+	printf(NAME ": Accepting connections.\n");
+	task_retval(0);
+	async_manager();
+
+	return 0;
+}
+
+/** @}
+ */
Index: uspace/srv/system/system.h
===================================================================
--- uspace/srv/system/system.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
+++ uspace/srv/system/system.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2024 Jiri Svoboda
+ * Copyright (c) 2006 Martin Decky
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup system
+ * @{
+ */
+/**
+ * @file
+ */
+
+#ifndef SYSTEM_H
+#define SYSTEM_H
+
+#include <system_srv.h>
+
+#define NAME  "system"
+
+typedef struct {
+	system_srv_t srv;
+} sys_srv_t;
+
+#endif
+
+/** @}
+ */
Index: uspace/srv/volsrv/part.c
===================================================================
--- uspace/srv/volsrv/part.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/volsrv/part.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -52,4 +52,5 @@
 #include "types/part.h"
 #include "volume.h"
+#include "volsrv.h"
 
 static errno_t vol_part_add_locked(vol_parts_t *, service_id_t);
@@ -403,4 +404,16 @@
 	part->cur_mp_auto = mp_auto;
 
+	if (str_cmp(mp, "/w") == 0) {
+		log_msg(LOG_DEFAULT, LVL_NOTE, "Mounted system volume - "
+		    "loading additional configuration.");
+		rc = vol_volumes_merge_to(part->parts->volumes,
+		    vol_cfg_file);
+		if (rc != EOK) {
+			log_msg(LOG_DEFAULT, LVL_ERROR, "Error loading "
+			    "additional configuration.");
+			return rc;
+		}
+	}
+
 	return rc;
 }
Index: uspace/srv/volsrv/volsrv.c
===================================================================
--- uspace/srv/volsrv/volsrv.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/volsrv/volsrv.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -1,4 +1,4 @@
 /*
- * Copyright (c) 2023 Jiri Svoboda
+ * Copyright (c) 2024 Jiri Svoboda
  * All rights reserved.
  *
@@ -53,5 +53,6 @@
 #define NAME  "volsrv"
 
-const char *vol_cfg_file = "/cfg/volsrv.sif";
+const char *vol_icfg_file = "/cfg/initvol.sif";
+const char *vol_cfg_file = "/w/cfg/volsrv.sif";
 
 static void vol_client_conn(ipc_call_t *, void *);
@@ -66,5 +67,5 @@
 	log_msg(LOG_DEFAULT, LVL_DEBUG, "vol_init()");
 
-	rc = vol_volumes_create(vol_cfg_file, &volumes);
+	rc = vol_volumes_create(vol_icfg_file, &volumes);
 	if (rc != EOK)
 		goto error;
Index: uspace/srv/volsrv/volsrv.h
===================================================================
--- uspace/srv/volsrv/volsrv.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
+++ uspace/srv/volsrv/volsrv.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2024 Jiri Svoboda
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * - The name of the author may not be used to endorse or promote products
+ *   derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/** @addtogroup volsrv
+ * @{
+ */
+/**
+ * @file Volume service
+ */
+
+#ifndef VOLSRV_H
+#define VOLSRV_H
+
+extern const char *vol_cfg_file;
+
+#endif
+
+/** @}
+ */
Index: uspace/srv/volsrv/volume.c
===================================================================
--- uspace/srv/volsrv/volume.c	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/volsrv/volume.c	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -185,4 +185,62 @@
 		free(volumes);
 
+	return rc;
+}
+
+/** Merge list of volumes into new file.
+ *
+ * @param volumes List of volumes
+ * @param cfg_path Path to file containing configuration repository in SIF
+ * @return EOK on success, ENOMEM if out of memory
+ */
+errno_t vol_volumes_merge_to(vol_volumes_t *volumes, const char *cfg_path)
+{
+	sif_doc_t *doc = NULL;
+	sif_node_t *node;
+	const char *ntype;
+	char *dcfg_path;
+	errno_t rc;
+
+	dcfg_path = str_dup(cfg_path);
+	if (dcfg_path == NULL) {
+		rc = ENOMEM;
+		goto error;
+	}
+
+	free(volumes->cfg_path);
+	volumes->cfg_path = dcfg_path;
+
+	/* Try opening existing repository */
+	rc = sif_load(cfg_path, &doc);
+	if (rc != EOK) {
+		/* Failed to open existing, create new repository */
+		rc = vol_volumes_sync(volumes);
+		if (rc != EOK)
+			goto error;
+	} else {
+		/*
+		 * Loaded existing configuration. Find 'volumes' node, should
+		 * be the first child of the root node.
+		 */
+		node = sif_node_first_child(sif_get_root(doc));
+
+		/* Verify it's the correct node type */
+		ntype = sif_node_get_type(node);
+		if (str_cmp(ntype, "volumes") != 0) {
+			rc = EIO;
+			goto error;
+		}
+
+		rc = vol_volumes_load(node, volumes);
+		if (rc != EOK)
+			goto error;
+
+		sif_delete(doc);
+	}
+
+	return EOK;
+error:
+	if (doc != NULL)
+		(void) sif_delete(doc);
 	return rc;
 }
Index: uspace/srv/volsrv/volume.h
===================================================================
--- uspace/srv/volsrv/volume.h	(revision bc3d695b6287b7807d133734f5b8790bdcb16cb4)
+++ uspace/srv/volsrv/volume.h	(revision 7bf29e5804bf7e7ba130e38d0e9ba083e5976676)
@@ -42,4 +42,5 @@
 
 extern errno_t vol_volumes_create(const char *, vol_volumes_t **);
+extern errno_t vol_volumes_merge_to(vol_volumes_t *, const char *);
 extern errno_t vol_volumes_sync(vol_volumes_t *);
 extern void vol_volumes_destroy(vol_volumes_t *);
