[v3,1/1] app/graph: add example for different usecases

Message ID 20230908104907.4060511-1-skori@marvell.com (mailing list archive)
State Superseded, archived
Delegated to: Thomas Monjalon
Headers
Series [v3,1/1] app/graph: add example for different usecases |

Checks

Context Check Description
ci/checkpatch warning coding style issues
ci/Intel-compilation fail Compilation issues
ci/intel-Testing success Testing PASS
ci/github-robot: build fail github build: failed
ci/intel-Functional success Functional PASS
ci/iol-mellanox-Performance success Performance Testing PASS
ci/iol-broadcom-Performance success Performance Testing PASS
ci/iol-intel-Performance success Performance Testing PASS
ci/iol-broadcom-Functional success Functional Testing PASS
ci/iol-compile-amd64-testing success Testing PASS
ci/iol-intel-Functional success Functional Testing PASS
ci/iol-unit-arm64-testing warning Testing issues
ci/iol-unit-amd64-testing success Testing PASS
ci/iol-compile-arm64-testing fail Testing issues
ci/iol-sample-apps-testing success Testing PASS
ci/loongarch-compilation success Compilation OK
ci/loongarch-unit-testing success Unit Testing PASS

Commit Message

Sunil Kumar Kori Sept. 8, 2023, 10:49 a.m. UTC
  From: Sunil Kumar Kori <skori@marvell.com>

Current l3fwd-graph application only validates l3fwd use case.
To scale up this, new application will be added with a framework
to run as user's provided usecases.

Required configuration and use cases details are fetched via a
static .cli file which will be used to create a graph for
requested uscases.

Signed-off-by: Sunil Kumar Kori <skori@marvell.com>
Signed-off-by: Rakesh Kudurumalla <rkudurumalla@marvell.com>
---
 MAINTAINERS                                  |   7 +
 app/graph/cli.c                              | 208 ++++++
 app/graph/cli.h                              |  48 ++
 app/graph/cli_priv.h                         |  19 +
 app/graph/conn.c                             | 284 +++++++++
 app/graph/conn.h                             |  46 ++
 app/graph/ethdev.c                           | 632 +++++++++++++++++++
 app/graph/ethdev.h                           |  28 +
 app/graph/ethdev_priv.h                      |  46 ++
 app/graph/ethdev_rx.c                        | 139 ++++
 app/graph/ethdev_rx.h                        |  32 +
 app/graph/ethdev_rx_priv.h                   |  23 +
 app/graph/examples/l3fwd.cli                 |  87 +++
 app/graph/graph.c                            | 383 +++++++++++
 app/graph/graph.h                            |  11 +
 app/graph/graph_priv.h                       |  32 +
 app/graph/ip4_route.c                        | 146 +++++
 app/graph/ip6_route.c                        | 154 +++++
 app/graph/l3fwd.c                            | 152 +++++
 app/graph/l3fwd.h                            |  11 +
 app/graph/main.c                             | 201 ++++++
 app/graph/mempool.c                          | 134 ++++
 app/graph/mempool.h                          |  18 +
 app/graph/mempool_priv.h                     |  16 +
 app/graph/meson.build                        |  25 +
 app/graph/module_api.h                       |  33 +
 app/graph/neigh.c                            | 269 ++++++++
 app/graph/neigh.h                            |  11 +
 app/graph/neigh_priv.h                       |  22 +
 app/graph/route.h                            |  30 +
 app/graph/utils.c                            | 155 +++++
 app/graph/utils.h                            |  14 +
 app/meson.build                              |   1 +
 doc/guides/tools/graph.rst                   | 171 +++++
 doc/guides/tools/img/graph-usecase-l3fwd.svg | 210 ++++++
 doc/guides/tools/index.rst                   |   1 +
 36 files changed, 3799 insertions(+)
 create mode 100644 app/graph/cli.c
 create mode 100644 app/graph/cli.h
 create mode 100644 app/graph/cli_priv.h
 create mode 100644 app/graph/conn.c
 create mode 100644 app/graph/conn.h
 create mode 100644 app/graph/ethdev.c
 create mode 100644 app/graph/ethdev.h
 create mode 100644 app/graph/ethdev_priv.h
 create mode 100644 app/graph/ethdev_rx.c
 create mode 100644 app/graph/ethdev_rx.h
 create mode 100644 app/graph/ethdev_rx_priv.h
 create mode 100644 app/graph/examples/l3fwd.cli
 create mode 100644 app/graph/graph.c
 create mode 100644 app/graph/graph.h
 create mode 100644 app/graph/graph_priv.h
 create mode 100644 app/graph/ip4_route.c
 create mode 100644 app/graph/ip6_route.c
 create mode 100644 app/graph/l3fwd.c
 create mode 100644 app/graph/l3fwd.h
 create mode 100644 app/graph/main.c
 create mode 100644 app/graph/mempool.c
 create mode 100644 app/graph/mempool.h
 create mode 100644 app/graph/mempool_priv.h
 create mode 100644 app/graph/meson.build
 create mode 100644 app/graph/module_api.h
 create mode 100644 app/graph/neigh.c
 create mode 100644 app/graph/neigh.h
 create mode 100644 app/graph/neigh_priv.h
 create mode 100644 app/graph/route.h
 create mode 100644 app/graph/utils.c
 create mode 100644 app/graph/utils.h
 create mode 100644 doc/guides/tools/graph.rst
 create mode 100644 doc/guides/tools/img/graph-usecase-l3fwd.svg
  

Comments

Nithin Dabilpuram Sept. 9, 2023, 1:18 a.m. UTC | #1
Please see inline.

> -----Original Message-----
> From: skori@marvell.com <skori@marvell.com>
> Sent: Friday, September 8, 2023 4:19 PM
> To: Thomas Monjalon <thomas@monjalon.net>; Sunil Kumar Kori <skori@marvell.com>;
> Rakesh Kudurumalla <rkudurumalla@marvell.com>
> Cc: dev@dpdk.org
> Subject: [EXT] [PATCH v3 1/1] app/graph: add example for different usecases
> 
> External Email
> 
> ----------------------------------------------------------------------
> From: Sunil Kumar Kori <skori@marvell.com>
> 
> Current l3fwd-graph application only validates l3fwd use case.
> To scale up this, new application will be added with a framework
> to run as user's provided usecases.
> 
> Required configuration and use cases details are fetched via a
> static .cli file which will be used to create a graph for
> requested uscases.
> 
> Signed-off-by: Sunil Kumar Kori <skori@marvell.com>
> Signed-off-by: Rakesh Kudurumalla <rkudurumalla@marvell.com>
> ---
>  MAINTAINERS                                  |   7 +
>  app/graph/cli.c                              | 208 ++++++
>  app/graph/cli.h                              |  48 ++
>  app/graph/cli_priv.h                         |  19 +
>  app/graph/conn.c                             | 284 +++++++++
>  app/graph/conn.h                             |  46 ++
>  app/graph/ethdev.c                           | 632 +++++++++++++++++++
>  app/graph/ethdev.h                           |  28 +
>  app/graph/ethdev_priv.h                      |  46 ++
>  app/graph/ethdev_rx.c                        | 139 ++++
>  app/graph/ethdev_rx.h                        |  32 +
>  app/graph/ethdev_rx_priv.h                   |  23 +
>  app/graph/examples/l3fwd.cli                 |  87 +++
>  app/graph/graph.c                            | 383 +++++++++++
>  app/graph/graph.h                            |  11 +
>  app/graph/graph_priv.h                       |  32 +
>  app/graph/ip4_route.c                        | 146 +++++
>  app/graph/ip6_route.c                        | 154 +++++
>  app/graph/l3fwd.c                            | 152 +++++
>  app/graph/l3fwd.h                            |  11 +
>  app/graph/main.c                             | 201 ++++++
>  app/graph/mempool.c                          | 134 ++++
>  app/graph/mempool.h                          |  18 +
>  app/graph/mempool_priv.h                     |  16 +
>  app/graph/meson.build                        |  25 +
>  app/graph/module_api.h                       |  33 +
>  app/graph/neigh.c                            | 269 ++++++++
>  app/graph/neigh.h                            |  11 +
>  app/graph/neigh_priv.h                       |  22 +
>  app/graph/route.h                            |  30 +
>  app/graph/utils.c                            | 155 +++++
>  app/graph/utils.h                            |  14 +
>  app/meson.build                              |   1 +
>  doc/guides/tools/graph.rst                   | 171 +++++
>  doc/guides/tools/img/graph-usecase-l3fwd.svg | 210 ++++++
>  doc/guides/tools/index.rst                   |   1 +
>  36 files changed, 3799 insertions(+)
>  create mode 100644 app/graph/cli.c
>  create mode 100644 app/graph/cli.h
>  create mode 100644 app/graph/cli_priv.h
>  create mode 100644 app/graph/conn.c
>  create mode 100644 app/graph/conn.h
>  create mode 100644 app/graph/ethdev.c
>  create mode 100644 app/graph/ethdev.h
>  create mode 100644 app/graph/ethdev_priv.h
>  create mode 100644 app/graph/ethdev_rx.c
>  create mode 100644 app/graph/ethdev_rx.h
>  create mode 100644 app/graph/ethdev_rx_priv.h
>  create mode 100644 app/graph/examples/l3fwd.cli
>  create mode 100644 app/graph/graph.c
>  create mode 100644 app/graph/graph.h
>  create mode 100644 app/graph/graph_priv.h
>  create mode 100644 app/graph/ip4_route.c
>  create mode 100644 app/graph/ip6_route.c
>  create mode 100644 app/graph/l3fwd.c
>  create mode 100644 app/graph/l3fwd.h
>  create mode 100644 app/graph/main.c
>  create mode 100644 app/graph/mempool.c
>  create mode 100644 app/graph/mempool.h
>  create mode 100644 app/graph/mempool_priv.h
>  create mode 100644 app/graph/meson.build
>  create mode 100644 app/graph/module_api.h
>  create mode 100644 app/graph/neigh.c
>  create mode 100644 app/graph/neigh.h
>  create mode 100644 app/graph/neigh_priv.h
>  create mode 100644 app/graph/route.h
>  create mode 100644 app/graph/utils.c
>  create mode 100644 app/graph/utils.h
>  create mode 100644 doc/guides/tools/graph.rst
>  create mode 100644 doc/guides/tools/img/graph-usecase-l3fwd.svg
> 

[Nithin] Split to multiple smaller patches

> diff --git a/MAINTAINERS b/MAINTAINERS
> index 698608cdb2..7f149bd060 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -1806,6 +1806,13 @@ F: dts/
>  F: devtools/dts-check-format.sh
>  F: doc/guides/tools/dts.rst
> 
> +Graph application
> +M: Sunil Kumar Kori <skori@marvell.com>
> +M: Rakesh Kudurumalla <rkudurumalla@marvell.com>
> +F: app/graph/
> +F: doc/guides/tools/graph.rst
> +F: doc/guides/tools/img/graph-usecase-l3fwd.svg
> +
> 
>  Other Example Applications
>  --------------------------
> diff --git a/app/graph/cli.c b/app/graph/cli.c
> new file mode 100644
> index 0000000000..237fa8008f
> --- /dev/null
> +++ b/app/graph/cli.c
> @@ -0,0 +1,208 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <ctype.h>
> +#include <stdio.h>
> +#include <stdint.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <unistd.h>
> +
> +#include <rte_common.h>
> +#include <rte_ethdev.h>
> +#include <rte_malloc.h>
> +#include <rte_node_ip4_api.h>
> +#include <rte_node_ip6_api.h>
> +
> +#include "cli_priv.h"
> +#include "module_api.h"
> +
> +#define CMD_MAX_TOKENS 256
> +#define MAX_LINE_SIZE 2048
> +
> +static struct cli_node_head module_list = STAILQ_HEAD_INITIALIZER(module_list);
> +
> +#define PARSE_DELIMITER " \f\n\r\t\v"
> +
> +static int
> +tokenize_string_parse(char *string, char *tokens[], uint32_t *n_tokens)
> +{
> +	uint32_t i;
> +
> +	if ((string == NULL) ||
> +		(tokens == NULL) ||
> +		(*n_tokens < 1))
> +		return -EINVAL;
> +
> +	for (i = 0; i < *n_tokens; i++) {
> +		tokens[i] = strtok_r(string, PARSE_DELIMITER, &string);
> +		if (tokens[i] == NULL)
> +			break;
> +	}
> +
> +	if ((i == *n_tokens) && strtok_r(string, PARSE_DELIMITER, &string))
> +		return -E2BIG;
> +
> +	*n_tokens = i;
> +	return 0;
> +}
> +
> +static int
> +is_comment(char *in)
> +{
> +	if ((strlen(in) && index("!#%;", in[0])) ||
> +		(strncmp(in, "//", 2) == 0) ||
> +		(strncmp(in, "--", 2) == 0))
> +		return 1;
> +
> +	return 0;
> +}
> +
> +static bool
> +module_list_has_cmd_registered(const char *cmd)
> +{
> +	struct cli_node *node;
> +
> +	STAILQ_FOREACH(node, &module_list, next) {
> +		if (strcmp(node->cmd, cmd) == 0) {
> +			rte_errno = EEXIST;
> +			return 1;
> +		}
> +	}
> +	return 0;
> +}
> +
> +void
> +cli_module_register(const struct cli_module *module)
> +{
> +	struct cli_node *node;
> +
> +	/* Check sanity */
> +	if (module == NULL || module->process == NULL) {
> +		rte_errno = EINVAL;
> +		return;
> +	}
> +
> +	/* Check for duplicate name */
> +	if (module_list_has_cmd_registered(module->cmd)) {
> +		printf("module %s is already registered\n", module->cmd);
> +		return;
> +	}
> +
> +	node = malloc(sizeof(struct cli_node));
> +	if (node == NULL) {
> +		rte_errno = ENOMEM;
> +		return;
> +	}
> +
> +	/* Initialize the node */
> +	if (rte_strscpy(node->cmd, module->cmd, APP_CLI_CMD_NAME_SIZE) < 0) {
> +		free(node);
> +		return;
> +	}
> +	node->process = module->process;
> +	node->usage = module->usage;
> +
> +	/* Add the node at tail */
> +	STAILQ_INSERT_TAIL(&module_list, node, next);
> +}
> +
> +void
> +cli_process(char *in, char *out, size_t out_size, void *obj)
> +{
> +	char *tokens[CMD_MAX_TOKENS];
> +	struct cli_node *node;
> +	uint32_t n_tokens;
> +	int rc;
> +
> +	if (is_comment(in))
> +		return;
> +
> +	n_tokens = RTE_DIM(tokens);
> +	rc = tokenize_string_parse(in, tokens, &n_tokens);
> +	if (rc) {
> +		snprintf(out, out_size, MSG_ARG_TOO_MANY, "");
> +		return;
> +	}
> +
> +	if (n_tokens == 0)
> +		return;
> +
> +	if ((n_tokens == 1) && strcmp(tokens[0], "help") == 0) {
> +		STAILQ_FOREACH(node, &module_list, next) {
> +			node->usage(tokens, n_tokens, out, out_size, obj);
> +		}
> +		return;
> +	}
> +
> +	if ((n_tokens >= 2) && strcmp(tokens[0], "help") == 0) {
> +		STAILQ_FOREACH(node, &module_list, next) {
> +			if (strcmp(node->cmd, tokens[1]) == 0) {
> +				node->usage(tokens, n_tokens, out, out_size, obj);
> +				return;
> +			}
> +		}
> +		snprintf(out, out_size, MSG_CMD_UNKNOWN, tokens[0]);
> +		return;
> +	}
> +
> +	STAILQ_FOREACH(node, &module_list, next) {
> +		if (strcmp(node->cmd, tokens[0]) == 0) {
> +			rc = node->process(tokens, n_tokens, out, out_size, obj);
> +			if (rc < 0)
> +				snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
> +
> +			return;
> +		}
> +	}
> +
> +	snprintf(out, out_size, MSG_CMD_UNKNOWN, tokens[0]);
> +}
> +
> +int
> +cli_script_process(const char *file_name, size_t msg_in_len_max, size_t
> msg_out_len_max, void *obj)
> +{
> +	char *msg_in = NULL, *msg_out = NULL;
> +	FILE *f = NULL;
> +
> +	/* Check input arguments */
> +	if ((file_name == NULL) || (strlen(file_name) == 0) || (msg_in_len_max == 0) ||
> +	    (msg_out_len_max == 0))
> +		return -EINVAL;
> +
> +	msg_in = malloc(msg_in_len_max + 1);
> +	msg_out = malloc(msg_out_len_max + 1);
> +	if ((msg_in == NULL) || (msg_out == NULL)) {
> +		free(msg_out);
> +		free(msg_in);
> +		return -ENOMEM;
> +	}
> +
> +	/* Open input file */
> +	f = fopen(file_name, "r");
> +	if (f == NULL) {
> +		free(msg_out);
> +		free(msg_in);
> +		return -EIO;
> +	}
> +
> +	/* Read file */
> +	while (1) {
> +		if (fgets(msg_in, msg_in_len_max + 1, f) == NULL)
> +			break;
> +
> +		msg_out[0] = 0;
> +
> +		cli_process(msg_in, msg_out, msg_out_len_max, obj);
> +
> +		if (strlen(msg_out))
> +			printf("%s", msg_out);
> +	}
> +
> +	/* Close file */
> +	fclose(f);
> +	free(msg_out);
> +	free(msg_in);
> +	return 0;
> +}
> diff --git a/app/graph/cli.h b/app/graph/cli.h
> new file mode 100644
> index 0000000000..2bd89f3d1f
> --- /dev/null
> +++ b/app/graph/cli.h
> @@ -0,0 +1,48 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_CLI_H
> +#define APP_GRAPH_CLI_H
> +
> +/* Macros */
> +#define MSG_OUT_OF_MEMORY   "Not enough memory.\n"
> +#define MSG_CMD_UNKNOWN     "Unknown command \"%s\".\n"
> +#define MSG_CMD_UNIMPLEM    "Command \"%s\" not implemented.\n"
> +#define MSG_ARG_NOT_ENOUGH  "Not enough arguments for command \"%s\".\n"
> +#define MSG_ARG_TOO_MANY    "Too many arguments for command \"%s\".\n"
> +#define MSG_ARG_MISMATCH    "Wrong number of arguments for command \"%s\".\n"
> +#define MSG_ARG_NOT_FOUND   "Argument \"%s\" not found.\n"
> +#define MSG_ARG_INVALID     "Invalid value for argument \"%s\".\n"
> +#define MSG_FILE_ERR        "Error in file \"%s\" at line %u.\n"
> +#define MSG_FILE_NOT_ENOUGH "Not enough rules in file \"%s\".\n"
> +#define MSG_CMD_FAIL        "Command \"%s\" failed.\n"
> +
> +#define APP_CLI_CMD_NAME_SIZE	64
> +
> +/* Typedefs */
> +typedef int (*cli_module_t)(char **tokens, uint32_t n_tokens, char *out, size_t out_size,
> +			     void *obj);
> +
> +/* Structures */
> +struct cli_module {
> +	char cmd[APP_CLI_CMD_NAME_SIZE]; /**< Name of the command to be
> registered. */
> +	cli_module_t process; /**< Command process function. */
> +	cli_module_t usage; /**< Help command process function. */
> +};
> +
> +/* APIs */
> +void cli_module_register(const struct cli_module *module);
> +
> +#define CLI_REGISTER(module)			\
> +	RTE_INIT(cli_register_##module)		\
> +	{					\
> +		cli_module_register(&module);	\
> +	}
> +
> +void cli_process(char *in, char *out, size_t out_size, void *arg);
> +
> +int cli_script_process(const char *file_name, size_t msg_in_len_max, size_t
> msg_out_len_max,
> +		       void *arg);
> +
> +#endif
> diff --git a/app/graph/cli_priv.h b/app/graph/cli_priv.h
> new file mode 100644
> index 0000000000..9ecc89c353
> --- /dev/null
> +++ b/app/graph/cli_priv.h
> @@ -0,0 +1,19 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_CLI_PRIV_H
> +#define APP_GRAPH_CLI_PRIV_H
> +
> +#include "cli.h"
> +
> +struct cli_node {
> +	STAILQ_ENTRY(cli_node) next;	 /**< Next node in the list. */
> +	char cmd[APP_CLI_CMD_NAME_SIZE]; /**< Name of the command. */
> +	cli_module_t process;		 /**< Command process function. */
> +	cli_module_t usage;		/**< Help command process function. */
> +};
> +
> +STAILQ_HEAD(cli_node_head, cli_node);
> +
> +#endif
> diff --git a/app/graph/conn.c b/app/graph/conn.c
> new file mode 100644
> index 0000000000..dabc8deca2
> --- /dev/null
> +++ b/app/graph/conn.c
> @@ -0,0 +1,284 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <arpa/inet.h>
> +#include <errno.h>
> +#include <netinet/in.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/epoll.h>
> +#include <sys/socket.h>
> +#include <sys/types.h>
> +#include <unistd.h>
> +
> +#include "module_api.h"
> +
> +#define MSG_CMD_TOO_LONG "Command too long."
> +
> +static int
> +data_event_handle(struct conn *conn, int fd_client)
> +{
> +	ssize_t len, i, rc = 0;
> +
> +	/* Read input message */
> +	len = read(fd_client, conn->buf, conn->buf_size);
> +	if (len == -1) {
> +		if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
> +			return 0;
> +
> +		return -1;
> +	}
> +
> +	if (len == 0)
> +		return rc;
> +
> +	/* Handle input messages */
> +	for (i = 0; i < len; i++) {
> +		if (conn->buf[i] == '\n') {
> +			size_t n;
> +
> +			conn->msg_in[conn->msg_in_len] = 0;
> +			conn->msg_out[0] = 0;
> +
> +			conn->msg_handle(conn->msg_in, conn->msg_out, conn-
> >msg_out_len_max,
> +					 conn->msg_handle_arg);
> +
> +			n = strlen(conn->msg_out);
> +			if (n) {
> +				rc = write(fd_client, conn->msg_out, n);
> +				if (rc == -1)
> +					goto exit;
> +			}
> +
> +			conn->msg_in_len = 0;
> +		} else if (conn->msg_in_len < conn->msg_in_len_max) {
> +			conn->msg_in[conn->msg_in_len] = conn->buf[i];
> +			conn->msg_in_len++;
> +		} else {
> +			rc = write(fd_client, MSG_CMD_TOO_LONG,
> strlen(MSG_CMD_TOO_LONG));
> +			if (rc == -1)
> +				goto exit;
> +
> +			conn->msg_in_len = 0;
> +		}
> +	}
> +
> +	/* Write prompt */
> +	rc = write(fd_client, conn->prompt, strlen(conn->prompt));
> +	rc = (rc == -1) ? -1 : 0;
> +
> +exit:
> +	return rc;
> +}
> +
> +static int
> +control_event_handle(struct conn *conn, int fd_client)
> +{
> +	int rc;
> +
> +	rc = epoll_ctl(conn->fd_client_group, EPOLL_CTL_DEL, fd_client, NULL);
> +	if (rc == -1)
> +		goto exit;
> +
> +	rc = close(fd_client);
> +	if (rc == -1)
> +		goto exit;
> +
> +	rc = 0;
> +
> +exit:
> +	return rc;
> +}
> +
> +struct conn *
> +conn_init(struct conn_params *p)
> +{
> +	int fd_server, fd_client_group, rc;
> +	struct sockaddr_in server_address;
> +	struct conn *conn = NULL;
> +
> +	memset(&server_address, 0, sizeof(server_address));
> +
> +	/* Check input arguments */
> +	if ((p == NULL) || (p->welcome == NULL) || (p->prompt == NULL) || (p->addr ==
> NULL) ||
> +	    (p->buf_size == 0) || (p->msg_in_len_max == 0) || (p->msg_out_len_max == 0)
> ||
> +	    (p->msg_handle == NULL))
> +		goto exit;
> +
> +	rc = inet_aton(p->addr, &server_address.sin_addr);
> +	if (rc == 0)
> +		goto exit;
> +
> +	/* Memory allocation */
> +	conn = calloc(1, sizeof(struct conn));
> +	if (conn == NULL)
> +		goto exit;
> +
> +	conn->welcome = calloc(1, CONN_WELCOME_LEN_MAX + 1);
> +	conn->prompt = calloc(1, CONN_PROMPT_LEN_MAX + 1);
> +	conn->buf = calloc(1, p->buf_size);
> +	conn->msg_in = calloc(1, p->msg_in_len_max + 1);
> +	conn->msg_out = calloc(1, p->msg_out_len_max + 1);
> +
> +	if ((conn->welcome == NULL) || (conn->prompt == NULL) || (conn->buf == NULL)
> ||
> +	    (conn->msg_in == NULL) || (conn->msg_out == NULL)) {
> +		conn_free(conn);
> +		conn = NULL;
> +		goto exit;
> +	}
> +
> +	/* Server socket */
> +	server_address.sin_family = AF_INET;
> +	server_address.sin_port = htons(p->port);
> +
> +	fd_server = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
> +	if (fd_server == -1) {
> +		conn_free(conn);
> +		conn = NULL;
> +		goto exit;
> +	}
> +
> +	rc = bind(fd_server, (struct sockaddr *)&server_address, sizeof(server_address));
> +	if (rc == -1) {
> +		conn_free(conn);
> +		close(fd_server);
> +		conn = NULL;
> +		goto exit;
> +	}
> +
> +	rc = listen(fd_server, 16);
> +	if (rc == -1) {
> +		conn_free(conn);
> +		close(fd_server);
> +		conn = NULL;
> +		goto exit;
> +	}
> +
> +	/* Client group */
> +	fd_client_group = epoll_create(1);
> +	if (fd_client_group == -1) {
> +		conn_free(conn);
> +		close(fd_server);
> +		conn = NULL;
> +		goto exit;
> +	}
> +
> +	/* Fill in */
> +	strncpy(conn->welcome, p->welcome, CONN_WELCOME_LEN_MAX);
> +	strncpy(conn->prompt, p->prompt, CONN_PROMPT_LEN_MAX);
> +	conn->buf_size = p->buf_size;
> +	conn->msg_in_len_max = p->msg_in_len_max;
> +	conn->msg_out_len_max = p->msg_out_len_max;
> +	conn->msg_in_len = 0;
> +	conn->fd_server = fd_server;
> +	conn->fd_client_group = fd_client_group;
> +	conn->msg_handle = p->msg_handle;
> +	conn->msg_handle_arg = p->msg_handle_arg;
> +
> +exit:
> +	return conn;
> +}
> +
> +void
> +conn_free(struct conn *conn)
> +{
> +	if (conn == NULL)
> +		return;
> +
> +	if (conn->fd_client_group)
> +		close(conn->fd_client_group);
> +
> +	if (conn->fd_server)
> +		close(conn->fd_server);
> +
> +	free(conn->msg_out);
> +	free(conn->msg_in);
> +	free(conn->prompt);
> +	free(conn->welcome);
> +	free(conn);
> +}
> +
> +int
> +conn_req_poll(struct conn *conn)
> +{
> +	struct sockaddr_in client_address;
> +	socklen_t client_address_length;
> +	struct epoll_event event;
> +	int fd_client, rc;
> +
> +	/* Check input arguments */
> +	if (conn == NULL)
> +		return -1;
> +
> +	/* Server socket */
> +	client_address_length = sizeof(client_address);
> +	fd_client = accept4(conn->fd_server, (struct sockaddr *)&client_address,
> +			    &client_address_length, SOCK_NONBLOCK);
> +	if (fd_client == -1) {
> +		if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
> +			return 0;
> +
> +		return -1;
> +	}
> +
> +	/* Client group */
> +	event.events = EPOLLIN | EPOLLRDHUP | EPOLLHUP;
> +	event.data.fd = fd_client;
> +
> +	rc = epoll_ctl(conn->fd_client_group, EPOLL_CTL_ADD, fd_client, &event);
> +	if (rc == -1) {
> +		close(fd_client);
> +		goto exit;
> +	}
> +
> +	/* Client */
> +	rc = write(fd_client, conn->welcome, strlen(conn->welcome));
> +	if (rc == -1) {
> +		close(fd_client);
> +		goto exit;
> +	}
> +
> +	rc = write(fd_client, conn->prompt, strlen(conn->prompt));
> +	if (rc == -1) {
> +		close(fd_client);
> +		goto exit;
> +	}
> +
> +	rc = 0;
> +
> +exit:
> +	return rc;
> +}
> +
> +int
> +conn_msg_poll(struct conn *conn)
> +{
> +	int fd_client, rc, rc_data = 0, rc_control = 0;
> +	struct epoll_event event;
> +
> +	/* Check input arguments */
> +	if (conn == NULL)
> +		return -1;
> +
> +	/* Client group */
> +	rc = epoll_wait(conn->fd_client_group, &event, 1, 0);
> +	if ((rc == -1) || rc == 0)
> +		return rc;
> +
> +	fd_client = event.data.fd;
> +
> +	/* Data available */
> +	if (event.events & EPOLLIN)
> +		rc_data = data_event_handle(conn, fd_client);
> +
> +	/* Control events */
> +	if (event.events & (EPOLLRDHUP | EPOLLERR | EPOLLHUP))
> +		rc_control = control_event_handle(conn, fd_client);
> +
> +	if (rc_data || rc_control)
> +		return -1;
> +
> +	return 0;
> +}
> diff --git a/app/graph/conn.h b/app/graph/conn.h
> new file mode 100644
> index 0000000000..770964cf4c
> --- /dev/null
> +++ b/app/graph/conn.h
> @@ -0,0 +1,46 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_CONN_H
> +#define APP_GRAPH_CONN_H
> +
> +#define CONN_WELCOME_LEN_MAX 1024
> +#define CONN_PROMPT_LEN_MAX 16
> +
> +typedef void (*conn_msg_handle_t)(char *msg_in, char *msg_out, size_t
> msg_out_len_max, void *arg);
> +
> +struct conn {
> +	char *welcome;
> +	char *prompt;
> +	char *buf;
> +	char *msg_in;
> +	char *msg_out;
> +	size_t buf_size;
> +	size_t msg_in_len_max;
> +	size_t msg_out_len_max;
> +	size_t msg_in_len;
> +	int fd_server;
> +	int fd_client_group;
> +	conn_msg_handle_t msg_handle;
> +	void *msg_handle_arg;
> +};
> +
> +struct conn_params {
> +	const char *welcome;
> +	const char *prompt;
> +	const char *addr;
> +	uint16_t port;
> +	size_t buf_size;
> +	size_t msg_in_len_max;
> +	size_t msg_out_len_max;
> +	conn_msg_handle_t msg_handle;
> +	void *msg_handle_arg;
> +};
> +
> +struct conn *conn_init(struct conn_params *p);
> +void conn_free(struct conn *conn);
> +int conn_req_poll(struct conn *conn);
> +int conn_msg_poll(struct conn *conn);
> +
> +#endif
> diff --git a/app/graph/ethdev.c b/app/graph/ethdev.c
> new file mode 100644
> index 0000000000..840a8ca42f
> --- /dev/null
> +++ b/app/graph/ethdev.c
> @@ -0,0 +1,632 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include <rte_bitops.h>
> +#include <rte_ethdev.h>
> +#include <rte_mempool.h>
> +
> +#include "ethdev_priv.h"
> +#include "module_api.h"
> +
> +static const char
> +cmd_ethdev_mtu_help[] = "ethdev <ethdev_name> mtu <mtu_sz>";
> +
> +static const char
> +cmd_ethdev_prom_mode_help[] = "ethdev <ethdev_name> promiscuous <on/off>";
> +
> +static const char
> +cmd_ethdev_help[] = "ethdev <ethdev_name> rxq <n_queues> txq <n_queues>
> <mempool_name> "
> +		    "[mtu <mtu_sz>]";
> +static const char
> +cmd_ethdev_show_help[] = "ethdev <ethdev_name> show";
> +
> +static const char
> +cmd_ethdev_ip4_addr_help[] = "ethdev <ethdev_name> ip4 addr add <ip> netmask
> <mask>";
> +
> +static const char
> +cmd_ethdev_ip6_addr_help[] = "ethdev <ethdev_name> ip6 addr add <ip> netmask
> <mask>";
> +
> +static struct rte_eth_conf port_conf_default = {
> +	.link_speeds = 0,
> +	.rxmode = {
> +		.mq_mode = RTE_ETH_MQ_RX_NONE,
> +		.mtu = 9000 - (RTE_ETHER_HDR_LEN + RTE_ETHER_CRC_LEN), /* Jumbo
> frame MTU */
> +	},
> +	.rx_adv_conf = {
> +		.rss_conf = {
> +			.rss_key = NULL,
> +			.rss_key_len = 40,
> +			.rss_hf = 0,
> +		},
> +	},
> +	.txmode = {
> +		.mq_mode = RTE_ETH_MQ_TX_NONE,
> +	},
> +	.lpbk_mode = 0,
> +};
> +
> +uint32_t enabled_port_mask;
> +struct ethdev port_list[RTE_MAX_ETHPORTS];
> +
> +void *
> +ethdev_mempool_list_by_portid(uint16_t portid)
> +{
> +	if (portid >= RTE_MAX_ETHPORTS)
> +		return NULL;
> +
> +	return &port_list[portid].config.rx.mp;
> +}
> +
> +int16_t
> +ethdev_portid_by_ip4(uint32_t ip)
> +{
> +	int portid = -EINVAL;
> +	int i;
> +
> +	for (i = 0; i < RTE_MAX_ETHPORTS; i++) {
> +		if ((port_list[i].ip4_addr.ip & route4[i].netmask) == (ip &
> route4[i].netmask))
> +			break;
> +	}
> +
> +	if (i == RTE_MAX_ETHPORTS)
> +		return portid;
> +
> +	return port_list[i].config.port_id;
> +}
> +
> +int16_t
> +ethdev_portid_by_ip6(uint8_t *ip)
> +{
> +	int portid = -EINVAL;
> +	int i, j;
> +
> +	for (i = 0; i < RTE_MAX_ETHPORTS; i++) {
> +		for (j = 0; j < ETHDEV_IPV6_ADDR_LEN; j++) {
> +			if ((port_list[i].ip6_addr.ip[j] & route6[i].mask[j]) !=
> +			    (ip[j] & route6[i].mask[j]))
> +				break;
> +		}
> +
> +		if (j == ETHDEV_IPV6_ADDR_LEN)
> +			break;
> +	}
> +
> +	if (i == RTE_MAX_ETHPORTS)
> +		return portid;
> +
> +	return port_list[i].config.port_id;
> +}
> +
> +void
> +ethdev_stop(void)
> +{
> +	uint16_t portid;
> +	int rc;
> +
> +	RTE_ETH_FOREACH_DEV(portid) {
> +		if ((enabled_port_mask & (1 << portid)) == 0)
> +			continue;
> +		printf("Closing port %d...", portid);
> +		rc = rte_eth_dev_stop(portid);
> +		if (rc != 0)
> +			printf("Failed to stop port %u: %s\n",
> +					portid, rte_strerror(-rc));
> +		rte_eth_dev_close(portid);
> +		printf(" Done\n");
> +	}
> +
> +	/* clean up the EAL */
> +	rte_eal_cleanup();
> +	printf("Bye...\n");
> +}
> +
> +void
> +ethdev_start(void)
> +{
> +	uint16_t portid;
> +	int rc;
> +
> +	RTE_ETH_FOREACH_DEV(portid)
> +	{
> +		if ((enabled_port_mask & (1 << portid)) == 0)
> +			continue;
> +
> +		rc = rte_eth_dev_start(portid);
> +		if (rc < 0)
> +			rte_exit(EXIT_FAILURE, "rte_eth_dev_start: err=%d, port=%d\n",
> rc, portid);
> +	}
> +}
> +
> +
> +static int
> +ethdev_show(const char *name, char **out, size_t *out_size)
> +{
> +	uint16_t mtu = 0, port_id = 0;
> +	struct rte_eth_dev_info info;
> +	struct rte_eth_stats stats;
> +	struct rte_ether_addr addr;
> +	struct rte_eth_link link;
> +	uint32_t length;
> +	int rc;
> +
> +	rc = rte_eth_dev_get_port_by_name(name, &port_id);
> +	if (rc < 0)
> +		return rc;
> +
> +	rte_eth_dev_info_get(port_id, &info);
> +	rte_eth_stats_get(port_id, &stats);
> +	rte_eth_macaddr_get(port_id, &addr);
> +	rte_eth_link_get(port_id, &link);
> +	rte_eth_dev_get_mtu(port_id, &mtu);
> +
> +	snprintf(*out, *out_size,
> +		 "%s: flags=<%s> mtu %u\n"
> +		 "\tether " RTE_ETHER_ADDR_PRT_FMT " rxqueues %u txqueues %u\n"
> +		 "\tport# %u  speed %s\n"
> +		 "\tRX packets %" PRIu64"  bytes %" PRIu64"\n"
> +		 "\tRX errors %" PRIu64"  missed %" PRIu64"  no-mbuf %" PRIu64"\n"
> +		 "\tTX packets %" PRIu64"  bytes %" PRIu64"\n"
> +		 "\tTX errors %" PRIu64"\n\n",
> +		 name,
> +		 link.link_status ? "UP" : "DOWN",
> +		 mtu,
> +		 RTE_ETHER_ADDR_BYTES(&addr),
> +		 info.nb_rx_queues,
> +		 info.nb_tx_queues,
> +		 port_id,
> +		 rte_eth_link_speed_to_str(link.link_speed),
> +		 stats.ipackets,
> +		 stats.ibytes,
> +		 stats.ierrors,
> +		 stats.imissed,
> +		 stats.rx_nombuf,
> +		 stats.opackets,
> +		 stats.obytes,
> +		 stats.oerrors);
> +
> +	length = strlen(*out);
> +	*out_size -= length;
> +	*out += length;
> +	return 0;
> +}
> +
> +static int
> +ethdev_ip4_addr_add(const char *name, struct ipv4_addr_config *config)
> +{
> +	uint16_t portid = 0;
> +	int rc;
> +
> +	rc = rte_eth_dev_get_port_by_name(name, &portid);
> +	if (rc < 0)
> +		return rc;
> +
> +	port_list[portid].ip4_addr.ip = config->ip;
> +	port_list[portid].ip4_addr.mask = config->mask;
> +	return 0;
> +}
> +
> +static int
> +ethdev_ip6_addr_add(const char *name, struct ipv6_addr_config *config)
> +{
> +	uint16_t portid = 0;
> +	int rc, i;
> +
> +	rc = rte_eth_dev_get_port_by_name(name, &portid);
> +	if (rc < 0)
> +		return rc;
> +
> +	for (i = 0; i < ETHDEV_IPV6_ADDR_LEN; i++) {
> +		port_list[portid].ip6_addr.ip[i] = config->ip[i];
> +		port_list[portid].ip6_addr.mask[i] = config->mask[i];
> +	}
> +
> +	return 0;
> +}
> +
> +static int
> +ethdev_prom_mode_config(const char *name, bool enable)
> +{
> +	uint16_t portid = 0;
> +	int rc;
> +
> +	rc = rte_eth_dev_get_port_by_name(name, &portid);
> +	if (rc < 0)
> +		return rc;
> +
> +	if (enable)
> +		rc = rte_eth_promiscuous_enable(portid);
> +	else
> +		rc = rte_eth_promiscuous_disable(portid);
> +
> +	if (rc < 0)
> +		return rc;
> +
> +	port_list[portid].config.promiscuous = enable;
> +	return 0;
> +}
> +
> +static int
> +ethdev_mtu_config(const char *name, uint32_t mtu)
> +{
> +	uint16_t portid = 0;
> +	int rc;
> +
> +	rc = rte_eth_dev_get_port_by_name(name, &portid);
> +	if (rc < 0)
> +		return rc;
> +
> +	rc = rte_eth_dev_set_mtu(portid, mtu);
> +	if (rc < 0)
> +		return rc;
> +
> +	port_list[portid].config.mtu = mtu;
> +	return 0;
> +}
> +
> +static int
> +ethdev_process(const char *name, struct ethdev_config *params)
> +{
> +	struct rte_eth_dev_info port_info;
> +	struct rte_eth_conf port_conf;
> +	struct ethdev_rss_config *rss;
> +	struct rte_mempool *mempool;
> +	struct rte_ether_addr smac;
> +	int numa_node, rc;
> +	uint16_t port_id = 0;
> +	uint32_t i;
> +
> +	/* Check input params */
> +	if (!name || !name[0] || !params || !params->rx.n_queues || !params-
> >rx.queue_size ||
> +	    !params->tx.n_queues || !params->tx.queue_size)
> +		return -EINVAL;
> +
> +	rc = rte_eth_dev_get_port_by_name(name, &port_id);
> +	if (rc)
> +		return -EINVAL;
> +
> +	rc = rte_eth_dev_info_get(port_id, &port_info);
> +	if (rc)
> +		return -EINVAL;
> +
> +	mempool = rte_mempool_lookup(params->rx.mempool_name);
> +	if (!mempool)
> +		return -EINVAL;
> +
> +	params->rx.mp = mempool;
> +
> +	rss = params->rx.rss;
> +	if (rss) {
> +		if (!port_info.reta_size || port_info.reta_size >
> RTE_ETH_RSS_RETA_SIZE_512)
> +			return -EINVAL;
> +
> +		if (!rss->n_queues || rss->n_queues >= ETHDEV_RXQ_RSS_MAX)
> +			return -EINVAL;
> +
> +		for (i = 0; i < rss->n_queues; i++)
> +			if (rss->queue_id[i] >= port_info.max_rx_queues)
> +				return -EINVAL;
> +	}
> +
> +	/* Port */
> +	memcpy(&port_conf, &port_conf_default, sizeof(struct rte_eth_conf));
> +	if (rss) {
> +		uint64_t rss_hf = RTE_ETH_RSS_IP | RTE_ETH_RSS_TCP |
> RTE_ETH_RSS_UDP;
> +
> +		port_conf.rxmode.mq_mode = RTE_ETH_MQ_RX_RSS;
> +		port_conf.rx_adv_conf.rss_conf.rss_hf = rss_hf &
> port_info.flow_type_rss_offloads;
> +	}
> +
> +	numa_node = rte_eth_dev_socket_id(port_id);
> +	if (numa_node == SOCKET_ID_ANY)
> +		numa_node = 0;
> +
> +	if (params->mtu)
> +		port_conf.rxmode.mtu = params->mtu;
> +
> +	rc = rte_eth_dev_configure(port_id, params->rx.n_queues, params->tx.n_queues,
> +				       &port_conf);
> +	if (rc < 0)
> +		return -EINVAL;
> +
> +	rc = rte_eth_macaddr_get(port_id, &smac);
> +	if (rc < 0)
> +		return -EINVAL;
> +
> +	printf("Port_id = %d srcmac = %x:%x:%x:%x:%x:%x\n", port_id,
> +		smac.addr_bytes[0], smac.addr_bytes[1],
> +		smac.addr_bytes[2], smac.addr_bytes[3],
> +		smac.addr_bytes[4], smac.addr_bytes[5]);
> +
> +	/* Port RX */
> +	for (i = 0; i < params->rx.n_queues; i++) {
> +		rc = rte_eth_rx_queue_setup(port_id, i, params->rx.queue_size,
> numa_node, NULL,
> +			mempool);
> +		if (rc < 0)
> +			return -EINVAL;
> +	}
> +
> +	/* Port TX */
> +	for (i = 0; i < params->tx.n_queues; i++) {
> +		rc = rte_eth_tx_queue_setup(port_id, i, params->tx.queue_size,
> numa_node, NULL);
> +		if (rc < 0)
> +			return -EINVAL;
> +	}
> +
> +	memcpy(&port_list[port_id].config, params, sizeof(struct ethdev_config));
> +	memcpy(port_list[port_id].config.dev_name, name, strlen(name));
> +	port_list[port_id].config.port_id = port_id;
> +	enabled_port_mask |= RTE_BIT32(port_id);
> +	return 0;
> +}
> +
> +static int
> +cmd_ethdev_mtu(char **tokens, uint32_t n_tokens __rte_unused, char *out, size_t
> out_size,
> +	       void *obj __rte_unused)
> +{
> +	int rc = -EINVAL;
> +	uint32_t mtu = 0;
> +
> +	if (n_tokens != 4) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		return rc;
> +	}
> +
> +	if (parser_uint32_read(&mtu, tokens[3]) != 0) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "mtu_sz");
> +		return rc;
> +	}
> +
> +	rc = ethdev_mtu_config(tokens[1], mtu);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
> +
> +	return rc;
> +}
> +
> +static int
> +cmd_ethdev_prom_mode(char **tokens, uint32_t n_tokens __rte_unused, char *out,
> size_t out_size,
> +		     void *obj __rte_unused)
> +{
> +	bool enable = false;
> +	int rc = -EINVAL;
> +
> +	if (n_tokens != 4) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		return rc;
> +	}
> +
> +	if (strcmp(tokens[3], "on") == 0)
> +		enable = true;
> +
> +	rc = ethdev_prom_mode_config(tokens[1], enable);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
> +
> +	return rc;
> +}
> +
> +static int
> +cmd_ip4_addr(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj
> __rte_unused)
> +{
> +	struct ipv4_addr_config config;
> +	int rc = -EINVAL;
> +
> +	if (n_tokens != 8) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[3], "addr")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "addr");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[4], "add")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "add");
> +		goto exit;
> +	}
> +
> +	if (parser_ip4_read(&config.ip, tokens[5])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "ip");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[6], "netmask")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "netmask");
> +		goto exit;
> +	}
> +
> +	if (parser_ip4_read(&config.mask, tokens[7])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "netmask");
> +		goto exit;
> +	}
> +
> +	rc = ethdev_ip4_addr_add(tokens[1], &config);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_ARG_INVALID, tokens[0]);
> +
> +exit:
> +	return rc;
> +}
> +
> +static int
> +cmd_ip6_addr(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj
> __rte_unused)
> +{
> +	struct ipv6_addr_config config;
> +	int rc = -EINVAL;
> +
> +	if (n_tokens != 8) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[3], "addr")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "addr");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[4], "add")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "add");
> +		goto exit;
> +	}
> +
> +	if (parser_ip6_read(config.ip, tokens[5])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "ip");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[6], "netmask")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "netmask");
> +		goto exit;
> +	}
> +
> +	if (parser_ip6_read(config.mask, tokens[7])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "netmask");
> +		goto exit;
> +	}
> +
> +	rc = ethdev_ip6_addr_add(tokens[1], &config);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_ARG_INVALID, tokens[0]);
> +
> +exit:
> +	return rc;
> +}
> +
> +static int
> +cmd_ethdev_show(char **tokens, uint32_t n_tokens, char *out, size_t out_size,
> +	void *obj __rte_unused)
> +{
> +	int rc = -EINVAL;
> +
> +	if (n_tokens != 3) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		return rc;
> +	}
> +
> +	rc = ethdev_show(tokens[1], &out, &out_size);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_ARG_INVALID, tokens[0]);
> +
> +	return rc;
> +}
> +
> +static int
> +cmd_ethdev(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj
> __rte_unused)
> +{
> +	struct ethdev_config config;
> +	char *name;
> +	int rc;
> +
> +	if (n_tokens < 7) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		return -EINVAL;
> +	}
> +
> +	memset(&config, 0, sizeof(struct ethdev_config));
> +	name = tokens[1];
> +
> +	if (strcmp(tokens[2], "rxq") != 0) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "rxq");
> +		return -EINVAL;
> +	}
> +
> +	if (parser_uint32_read(&config.rx.n_queues, tokens[3]) != 0) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "n_queues");
> +		return -EINVAL;
> +	}
> +
> +	if (strcmp(tokens[4], "txq") != 0) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "txq");
> +		return -EINVAL;
> +	}
> +
> +	if (parser_uint32_read(&config.tx.n_queues, tokens[5]) != 0) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "n_queues");
> +		return -EINVAL;
> +	}
> +
> +	mempcpy(config.rx.mempool_name, tokens[6], strlen(tokens[6]));
> +
> +	if (n_tokens > 7) {
> +		if (strcmp(tokens[7], "mtu") != 0) {
> +			snprintf(out, out_size, MSG_ARG_NOT_FOUND, "mtu");
> +			return -EINVAL;
> +		}
> +
> +		if (parser_uint32_read(&config.mtu, tokens[8]) != 0) {
> +			snprintf(out, out_size, MSG_ARG_INVALID, "mtu_sz");
> +			return -EINVAL;
> +		}
> +	}
> +
> +	config.tx.queue_size = ETHDEV_TX_DESC_DEFAULT;
> +	config.rx.queue_size = ETHDEV_RX_DESC_DEFAULT;
> +
> +	rc = ethdev_process(name, &config);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
> +
> +	return rc;
> +}
> +
> +static int
> +cli_ethdev_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char
> *out,
> +		   size_t out_size, void *obj __rte_unused)
> +{
> +	size_t len;
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "\n%s\n",
> +		 "----------------------------- ethdev command help -----------------------------");
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_ethdev_help);
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_ethdev_ip4_addr_help);
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_ethdev_ip6_addr_help);
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_ethdev_prom_mode_help);
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_ethdev_mtu_help);
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_ethdev_show_help);
> +
> +	return 0;
> +}
> +
> +static int
> +cli_ethdev(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj)
> +{
> +	if (strcmp(tokens[2], "show") == 0)
> +		return cmd_ethdev_show(tokens, n_tokens, out, out_size, obj);
> +	else if (strcmp(tokens[2], "mtu") == 0)
> +		return cmd_ethdev_mtu(tokens, n_tokens, out, out_size, obj);
> +	else if (strcmp(tokens[2], "promiscuous") == 0)
> +		return cmd_ethdev_prom_mode(tokens, n_tokens, out, out_size, obj);
> +	else if (strcmp(tokens[2], "ip4") == 0)
> +		return cmd_ip4_addr(tokens, n_tokens, out, out_size, obj);
> +	else if (strcmp(tokens[2], "ip6") == 0)
> +		return cmd_ip6_addr(tokens, n_tokens, out, out_size, obj);
> +	else
> +		return cmd_ethdev(tokens, n_tokens, out, out_size, obj);
> +}
> +
> +static struct cli_module ethdev = {
> +	.cmd = "ethdev",
> +	.process = cli_ethdev,
> +	.usage = cli_ethdev_help,
> +};
> +
> +CLI_REGISTER(ethdev);
> diff --git a/app/graph/ethdev.h b/app/graph/ethdev.h
> new file mode 100644
> index 0000000000..9c3de49826
> --- /dev/null
> +++ b/app/graph/ethdev.h
> @@ -0,0 +1,28 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_ETHDEV_H
> +#define APP_GRAPH_ETHDEV_H
> +
> +#define ETHDEV_IPV6_ADDR_LEN	16
> +
> +struct ipv4_addr_config {
> +	uint32_t ip;
> +	uint32_t mask;
> +};
> +
> +struct ipv6_addr_config {
> +	uint8_t ip[ETHDEV_IPV6_ADDR_LEN];
> +	uint8_t mask[ETHDEV_IPV6_ADDR_LEN];
> +};
> +
> +extern uint32_t enabled_port_mask;
> +
> +void ethdev_start(void);
> +void ethdev_stop(void);
> +void *ethdev_mempool_list_by_portid(uint16_t portid);
> +int16_t ethdev_portid_by_ip4(uint32_t ip);
> +int16_t ethdev_portid_by_ip6(uint8_t *ip);
> +
> +#endif
> diff --git a/app/graph/ethdev_priv.h b/app/graph/ethdev_priv.h
> new file mode 100644
> index 0000000000..1026c2e5b6
> --- /dev/null
> +++ b/app/graph/ethdev_priv.h
> @@ -0,0 +1,46 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_ETHDEV_PRIV_H
> +#define APP_GRAPH_ETHDEV_PRIV_H
> +
> +#include "ethdev.h"
> +
> +#define ETHDEV_RXQ_RSS_MAX	16
> +#define ETHDEV_RX_DESC_DEFAULT 1024
> +#define ETHDEV_TX_DESC_DEFAULT 1024
> +
> +struct ethdev_rss_config {
> +	uint32_t queue_id[ETHDEV_RXQ_RSS_MAX];
> +	uint32_t n_queues;
> +};
> +
> +struct ethdev_config {
> +	char dev_name[RTE_ETH_NAME_MAX_LEN];
> +	uint16_t port_id;
> +
> +	struct {
> +		uint32_t n_queues;
> +		uint32_t queue_size;
> +		char mempool_name[RTE_MEMPOOL_NAMESIZE];
> +		struct rte_mempool *mp;
> +		struct ethdev_rss_config *rss;
> +	} rx;
> +
> +	struct {
> +		uint32_t n_queues;
> +		uint32_t queue_size;
> +	} tx;
> +
> +	int promiscuous;
> +	uint32_t mtu;
> +};
> +
> +struct ethdev {
> +	struct ethdev_config config;
> +	struct ipv4_addr_config ip4_addr;
> +	struct ipv6_addr_config ip6_addr;
> +};
> +
> +#endif
> diff --git a/app/graph/ethdev_rx.c b/app/graph/ethdev_rx.c
> new file mode 100644
> index 0000000000..d706d145c1
> --- /dev/null
> +++ b/app/graph/ethdev_rx.c
> @@ -0,0 +1,139 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include <rte_ethdev.h>
> +
> +#include "ethdev_rx_priv.h"
> +#include "module_api.h"
> +
> +static const char
> +cmd_ethdev_rx_help[] = "ethdev_rx map port <ethdev_name> queue <q_num> core
> <core_id>";
> +
> +static struct lcore_params lcore_params_array[ETHDEV_RX_LCORE_PARAMS_MAX];
> +struct rte_node_ethdev_config ethdev_conf[RTE_MAX_ETHPORTS];
> +struct lcore_params *lcore_params = lcore_params_array;
> +struct lcore_conf lcore_conf[RTE_MAX_LCORE];
> +uint16_t nb_lcore_params;
> +
> +static void
> +rx_map_configure(uint8_t port_id, uint32_t queue, uint32_t core)
> +{
> +	uint8_t n_rx_queue;
> +
> +	n_rx_queue = lcore_conf[core].n_rx_queue;
> +	lcore_conf[core].rx_queue_list[n_rx_queue].port_id = port_id;
> +	lcore_conf[core].rx_queue_list[n_rx_queue].queue_id = queue;
> +	lcore_conf[core].n_rx_queue++;
> +}
> +
> +uint8_t
> +ethdev_rx_num_rx_queues_get(uint16_t port)
> +{
> +	int queue = -1;
> +	uint16_t i;
> +
> +	for (i = 0; i < nb_lcore_params; ++i) {
> +		if (lcore_params[i].port_id == port) {
> +			if (lcore_params[i].queue_id == queue + 1)
> +				queue = lcore_params[i].queue_id;
> +			else
> +				rte_exit(EXIT_FAILURE,
> +					 "Queue ids of the port %d must be"
> +					 " in sequence and must start with 0\n",
> +					 lcore_params[i].port_id);
> +		}
> +	}
> +
> +	return (uint8_t)(++queue);
> +}
> +
> +static int
> +ethdev_rx_map_add(char *name, uint32_t queue, uint32_t core)
> +{
> +	uint16_t port_id;
> +	int rc;
> +
> +	if (nb_lcore_params >= ETHDEV_RX_LCORE_PARAMS_MAX)
> +		return -EINVAL;
> +
> +	rc = rte_eth_dev_get_port_by_name(name, &port_id);
> +	if (rc)
> +		return -EINVAL;
> +
> +	rx_map_configure(port_id, queue, core);
> +
> +	lcore_params_array[nb_lcore_params].port_id = port_id;
> +	lcore_params_array[nb_lcore_params].queue_id = queue;
> +	lcore_params_array[nb_lcore_params].lcore_id = core;
> +	nb_lcore_params++;
> +	return 0;
> +}
> +
> +static int
> +cli_ethdev_rx_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char
> *out,
> +		   size_t out_size, void *obj __rte_unused)
> +{
> +	size_t len;
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "\n%s\n",
> +		 "---------------------------- ethdev_rx command help ---------------------------");
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_ethdev_rx_help);
> +	return 0;
> +}
> +
> +static int
> +cli_ethdev_rx(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj
> __rte_unused)
> +{
> +	char name[RTE_ETH_NAME_MAX_LEN];
> +	uint32_t core_id, queue;
> +	int rc = -EINVAL;
> +
> +	if (n_tokens != 8) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		goto exit;
> +	}
> +
> +	strcpy(name, tokens[3]);
> +
> +	if (strcmp(tokens[4], "queue") != 0) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "rxq");
> +		goto exit;
> +	}
> +
> +	if (parser_uint32_read(&queue, tokens[5]) != 0) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "queue");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[6], "core") != 0) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "core_id");
> +		goto exit;
> +	}
> +
> +	if (parser_uint32_read(&core_id, tokens[7]) != 0) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "queue");
> +		goto exit;
> +	}
> +
> +	rc = ethdev_rx_map_add(name, queue, core_id);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
> +
> +exit:
> +	return rc;
> +}
> +
> +static struct cli_module ethdev_rx = {
> +	.cmd = "ethdev_rx",
> +	.process = cli_ethdev_rx,
> +	.usage = cli_ethdev_rx_help,
> +};
> +
> +CLI_REGISTER(ethdev_rx);
> diff --git a/app/graph/ethdev_rx.h b/app/graph/ethdev_rx.h
> new file mode 100644
> index 0000000000..d2c18f545f
> --- /dev/null
> +++ b/app/graph/ethdev_rx.h
> @@ -0,0 +1,32 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_ETHDEV_RX_H
> +#define APP_GRAPH_ETHDEV_RX_H
> +
> +#define ETHDEV_RX_LCORE_PARAMS_MAX 1024
> +#define ETHDEV_RX_QUEUE_PER_LCORE_MAX 16
> +
> +struct lcore_rx_queue {
> +	uint16_t port_id;
> +	uint8_t queue_id;
> +	char node_name[RTE_NODE_NAMESIZE];
> +};
> +
> +struct lcore_conf {
> +	uint16_t n_rx_queue;
> +	struct lcore_rx_queue rx_queue_list[ETHDEV_RX_QUEUE_PER_LCORE_MAX];
> +	struct rte_graph *graph;
> +	char name[RTE_GRAPH_NAMESIZE];
> +	rte_graph_t graph_id;
> +} __rte_cache_aligned;
> +
> +uint8_t ethdev_rx_num_rx_queues_get(uint16_t port);
> +
> +extern struct rte_node_ethdev_config ethdev_conf[RTE_MAX_ETHPORTS];
> +extern struct lcore_conf lcore_conf[RTE_MAX_LCORE];
> +extern struct lcore_params *lcore_params;
> +extern uint16_t nb_lcore_params;
> +
> +#endif
> diff --git a/app/graph/ethdev_rx_priv.h b/app/graph/ethdev_rx_priv.h
> new file mode 100644
> index 0000000000..d714f83739
> --- /dev/null
> +++ b/app/graph/ethdev_rx_priv.h
> @@ -0,0 +1,23 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_ETHDEV_RX_PRIV_H
> +#define APP_GRAPH_ETHDEV_RX_PRIV_H
> +
> +#include <stdint.h>
> +
> +#include <rte_graph.h>
> +#include <rte_node_eth_api.h>
> +
> +#define MAX_RX_QUEUE_PER_PORT 128
> +#define MAX_JUMBO_PKT_LEN  9600
> +#define NB_SOCKETS 8
> +
> +struct lcore_params {
> +	uint16_t port_id;
> +	uint8_t queue_id;
> +	uint8_t lcore_id;
> +} __rte_cache_aligned;
> +
> +#endif
> diff --git a/app/graph/examples/l3fwd.cli b/app/graph/examples/l3fwd.cli
> new file mode 100644
> index 0000000000..9986e1b73e
> --- /dev/null
> +++ b/app/graph/examples/l3fwd.cli
> @@ -0,0 +1,87 @@
> +; SPDX-License-Identifier: BSD-3-Clause
> +; Copyright(c) 2023 Marvell.
> +
> +;
> +; Graph configuration for given usecase
> +;
> +graph l3fwd coremask ff model default
> +
> +;
> +; Mempools to be attached with ethdev
> +;
> +mempool mempool0 size 8192 buffers 4000 cache 256 numa 0
> +
> +;
> +; DPDK devices and configuration.
> +;
> +; Note: Customize the parameters below to match your setup.
> +;
> +ethdev 0002:04:00.0 rxq 1 txq 8 mempool0 mtu 1500
> +ethdev 0002:05:00.0 rxq 1 txq 8 mempool0 mtu 1600
> +ethdev 0002:06:00.0 rxq 1 txq 8 mempool0 mtu 1500
> +ethdev 0002:07:00.0 rxq 1 txq 8 mempool0 mtu 1600
> +ethdev 0002:04:00.0 mtu 1700
> +ethdev 0002:05:00.0 promiscuous on
> +
> +;
> +; IPv4 addresses assigned to DPDK devices
> +;
> +ethdev 0002:04:00.0 ip4 addr add 10.0.2.1 netmask 255.255.255.0
> +ethdev 0002:05:00.0 ip4 addr add 20.0.2.1 netmask 255.255.255.0
> +ethdev 0002:06:00.0 ip4 addr add 30.0.2.1 netmask 255.255.255.0
> +ethdev 0002:07:00.0 ip4 addr add 40.0.2.1 netmask 255.255.255.0
> +
> +;
> +; IPv6 addresses assigned to DPDK devices
> +;
> +ethdev 0002:04:00.0 ip6 addr add 52:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4A
> netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00
> +ethdev 0002:05:00.0 ip6 addr add 62:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4B
> netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00
> +ethdev 0002:06:00.0 ip6 addr add 72:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4C
> netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00
> +ethdev 0002:07:00.0 ip6 addr add 82:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4D
> netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00
> +
> +;
> +; IPv4 routes which are installed to ipv4_lookup node for LPM processing
> +;
> +ipv4_lookup route add ipv4 10.0.2.0 netmask 255.255.255.0 via 10.0.2.1
> +ipv4_lookup route add ipv4 20.0.2.0 netmask 255.255.255.0 via 20.0.2.1
> +ipv4_lookup route add ipv4 30.0.2.0 netmask 255.255.255.0 via 30.0.2.1
> +ipv4_lookup route add ipv4 40.0.2.0 netmask 255.255.255.0 via 40.0.2.1
> +
> +;
> +; IPv6 routes which are installed to ipv6_lookup node for LPM processing
> +;
> +ipv6_lookup route add ipv6 52:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4A netmask
> FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00 via
> 52:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4A
> +ipv6_lookup route add ipv6 62:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4B netmask
> FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00 via
> 62:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4B
> +ipv6_lookup route add ipv6 72:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4C netmask
> FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00 via
> 72:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4C
> +ipv6_lookup route add ipv6 82:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4D netmask
> FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00 via
> 82:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4D
> +
> +;
> +; Peer MAC and IPv4 address mapping
> +;
> +neigh add ipv4 10.0.2.2 52:20:DA:4F:68:70
> +neigh add ipv4 20.0.2.2 62:20:DA:4F:68:70
> +neigh add ipv4 30.0.2.2 72:20:DA:4F:68:70
> +neigh add ipv4 40.0.2.2 82:20:DA:4F:68:70
> +
> +;
> +; Peer MAC and IPv6 address mapping
> +;
> +neigh add ipv6 52:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4A 52:20:DA:4F:68:70
> +neigh add ipv6 62:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4B 62:20:DA:4F:68:70
> +neigh add ipv6 72:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4C 72:20:DA:4F:68:70
> +neigh add ipv6 82:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4D 82:20:DA:4F:68:70
> +
> +;
> +; Port-Queue-Core mapping for ethdev_rx node
> +;
> +ethdev_rx map port 0002:04:00.0 queue 0 core 1
> +ethdev_rx map port 0002:05:00.0 queue 0 core 2
> +ethdev_rx map port 0002:06:00.0 queue 0 core 3
> +ethdev_rx map port 0002:07:00.0 queue 0 core 4
> +
> +;
> +; Graph start command to create graph.
> +;
> +; Note: No more command should come after this.
> +;
> +graph start
> diff --git a/app/graph/graph.c b/app/graph/graph.c
> new file mode 100644
> index 0000000000..8c75574ecd
> --- /dev/null
> +++ b/app/graph/graph.c
> @@ -0,0 +1,383 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include <rte_graph_worker.h>
> +#include <rte_log.h>
> +
> +#include "graph_priv.h"
> +#include "module_api.h"
> +
> +#define RTE_LOGTYPE_APP_GRAPH RTE_LOGTYPE_USER1
> +
> +static const char
> +cmd_graph_help[] = "graph <usecases> bsz <size> tmo <ns> coremask <bitmask> "
> +		   "model <rtc | mcd | default>";
> +
> +static const char * const supported_usecases[] = {"l3fwd"};
> +struct graph_config graph_config;
> +
> +/* Check the link rc of all ports in up to 9s, and print them finally */
> +static void
> +check_all_ports_link_status(uint32_t port_mask)
> +{
> +#define CHECK_INTERVAL 100 /* 100ms */
> +#define MAX_CHECK_TIME 90  /* 9s (90 * 100ms) in total */
> +	char link_rc_text[RTE_ETH_LINK_MAX_STR_LEN];
> +	uint8_t count, all_ports_up, print_flag = 0;
> +	struct rte_eth_link link;
> +	uint16_t portid;
> +	int rc;
> +
> +	printf("\nChecking link rc");
> +	fflush(stdout);
> +	for (count = 0; count <= MAX_CHECK_TIME; count++) {
> +		if (force_quit)
> +			return;
> +
> +		all_ports_up = 1;
> +		RTE_ETH_FOREACH_DEV(portid)
> +		{
> +			if (force_quit)
> +				return;
> +
> +			if ((port_mask & (1 << portid)) == 0)
> +				continue;
> +
> +			memset(&link, 0, sizeof(link));
> +			rc = rte_eth_link_get_nowait(portid, &link);
> +			if (rc < 0) {
> +				all_ports_up = 0;
> +				if (print_flag == 1)
> +					printf("Port %u link get failed: %s\n",
> +					       portid, rte_strerror(-rc));
> +				continue;
> +			}
> +
> +			/* Print link rc if flag set */
> +			if (print_flag == 1) {
> +				rte_eth_link_to_str(link_rc_text, sizeof(link_rc_text),
> +					&link);
> +				printf("Port %d %s\n", portid, link_rc_text);
> +				continue;
> +			}
> +
> +			/* Clear all_ports_up flag if any link down */
> +			if (link.link_status == RTE_ETH_LINK_DOWN) {
> +				all_ports_up = 0;
> +				break;
> +			}
> +		}
> +
> +		/* After finally printing all link rc, get out */
> +		if (print_flag == 1)
> +			break;
> +
> +		if (all_ports_up == 0) {
> +			printf(".");
> +			fflush(stdout);
> +			rte_delay_ms(CHECK_INTERVAL);
> +		}
> +
> +		/* Set the print_flag if all ports up or timeout */
> +		if (all_ports_up == 1 || count == (MAX_CHECK_TIME - 1)) {
> +			print_flag = 1;
> +			printf("Done\n");
> +		}
> +	}
> +}
> +
> +static bool
> +parser_usecases_read(char *usecases)
> +{
> +	bool valid = false;
> +	uint32_t i, j = 0;
> +	char *token;
> +
> +	token = strtok(usecases, ",");
> +	while (token != NULL) {
> +		for (i = 0; i < RTE_DIM(supported_usecases); i++) {
> +			if (strcmp(supported_usecases[i], token) == 0) {
> +				graph_config.usecases[j].enabled = true;
> +				strcpy(graph_config.usecases[j].name, token);
> +				valid = true;
> +				j++;
> +				break;
> +			}
> +		}
> +		token = strtok(NULL, ",");
> +	}
> +
> +	return valid;
> +}
> +
> +static uint64_t
> +graph_worker_count_get(void)
> +{
> +	uint64_t nb_worker = 0;
> +	uint64_t coremask;
> +
> +	coremask = graph_config.params.coremask;
> +	while (coremask) {
> +		if (coremask & 0x1)
> +			nb_worker++;
> +
> +		coremask = (coremask >> 1);
> +	}
> +
> +	return nb_worker;
> +}
> +
> +static struct rte_node_ethdev_config *
> +graph_rxtx_node_config_get(uint32_t *num_conf, uint32_t *num_graphs)
> +{
> +	uint32_t n_tx_queue, nb_conf = 0, lcore_id;
> +	uint16_t queueid, portid, nb_graphs = 0;
> +	uint8_t nb_rx_queue, queue;
> +	struct lcore_conf *qconf;
> +
> +	n_tx_queue = graph_worker_count_get();
> +	if (n_tx_queue > RTE_MAX_ETHPORTS)
> +		n_tx_queue = RTE_MAX_ETHPORTS;
> +
> +	RTE_ETH_FOREACH_DEV(portid) {
> +		/* Skip ports that are not enabled */
> +		if ((enabled_port_mask & (1 << portid)) == 0) {
> +			printf("\nSkipping disabled port %d\n", portid);
> +			continue;
> +		}
> +
> +		nb_rx_queue = ethdev_rx_num_rx_queues_get(portid);
> +
> +		/* Setup ethdev node config */
> +		ethdev_conf[nb_conf].port_id = portid;
> +		ethdev_conf[nb_conf].num_rx_queues = nb_rx_queue;
> +		ethdev_conf[nb_conf].num_tx_queues = n_tx_queue;
> +		ethdev_conf[nb_conf].mp = ethdev_mempool_list_by_portid(portid);
> +		ethdev_conf[nb_conf].mp_count = 1; /* Check with pools */
> +
> +		nb_conf++;
> +	}
> +
> +	for (lcore_id = 0; lcore_id < RTE_MAX_LCORE; lcore_id++) {
> +		if (rte_lcore_is_enabled(lcore_id) == 0)
> +			continue;
> +
> +		qconf = &lcore_conf[lcore_id];
> +		printf("\nInitializing rx queues on lcore %u ... ", lcore_id);
> +		fflush(stdout);
> +
> +		/* Init RX queues */
> +		for (queue = 0; queue < qconf->n_rx_queue; ++queue) {
> +			portid = qconf->rx_queue_list[queue].port_id;
> +			queueid = qconf->rx_queue_list[queue].queue_id;
> +
> +			/* Add this queue node to its graph */
> +			snprintf(qconf->rx_queue_list[queue].node_name,
> RTE_NODE_NAMESIZE,
> +				 "ethdev_rx-%u-%u", portid, queueid);
> +		}
> +		if (qconf->n_rx_queue)
> +			nb_graphs++;
> +	}
> +
> +	printf("\n");
> +
> +	ethdev_start();
> +	check_all_ports_link_status(enabled_port_mask);
> +
> +	*num_conf = nb_conf;
> +	*num_graphs = nb_graphs;
> +	return ethdev_conf;
> +}
> +
> +static int
> +graph_start(void)
> +{
> +	struct rte_node_ethdev_config *conf;
> +	uint32_t nb_graphs = 0, nb_conf, i;
> +
> +	conf = graph_rxtx_node_config_get(&nb_conf, &nb_graphs);
> +	for (i = 0; i < MAX_GRAPH_USECASES; i++) {
> +		if (!strcmp(graph_config.usecases[i].name, "l3fwd")) {
> +			if (graph_config.usecases[i].enabled) {
> +				usecase_l3fwd_configure(conf, nb_conf, nb_graphs);
> +				break;
> +			}
> +		}
> +	}
> +	return 0;
> +}
> +
> +static int
> +graph_config_add(char *usecases, struct graph_config *config)
> +{
> +	if (!parser_usecases_read(usecases))
> +		return -EINVAL;
> +
> +	graph_config.params.bsz = config->params.bsz;
> +	graph_config.params.tmo = config->params.tmo;
> +	graph_config.params.coremask = config->params.coremask;
> +	graph_config.model = config->model;
> +
> +	return 0;
> +}
> +
> +int
> +graph_walk_start(void *conf)
> +{
> +	struct lcore_conf *qconf;
> +	struct rte_graph *graph;
> +	uint32_t lcore_id;
> +
> +	RTE_SET_USED(conf);
> +
> +	lcore_id = rte_lcore_id();
> +	qconf = &lcore_conf[lcore_id];
> +	graph = qconf->graph;
> +
> +	if (!graph) {
> +		RTE_LOG(INFO, APP_GRAPH, "Lcore %u has nothing to do\n", lcore_id);
> +		return 0;
> +	}
> +
> +	RTE_LOG(INFO, APP_GRAPH, "Entering main loop on lcore %u, graph %s(%p)\n",
> lcore_id,
> +		qconf->name, graph);
> +
> +	while (likely(!force_quit))
> +		rte_graph_walk(graph);
> +
> +	return 0;
> +}
> +
> +void
> +graph_stats_print(void)
> +{
> +	const char topLeft[] = {27, '[', '1', ';', '1', 'H', '\0'};
> +	const char clr[] = {27, '[', '2', 'J', '\0'};
> +	struct rte_graph_cluster_stats_param s_param;
> +	struct rte_graph_cluster_stats *stats;
> +	const char *pattern = "worker_*";
> +
> +	/* Prepare stats object */
> +	memset(&s_param, 0, sizeof(s_param));
> +	s_param.f = stdout;
> +	s_param.socket_id = SOCKET_ID_ANY;
> +	s_param.graph_patterns = &pattern;
> +	s_param.nb_graph_patterns = 1;
> +
> +	stats = rte_graph_cluster_stats_create(&s_param);
> +	if (stats == NULL)
> +		rte_exit(EXIT_FAILURE, "Unable to create stats object\n");
> +
> +	while (!force_quit) {
> +		/* Clear screen and move to top left */
> +		printf("%s%s", clr, topLeft);
> +		rte_graph_cluster_stats_get(stats, 0);
> +		rte_delay_ms(1E3);
> +	}
> +
> +	rte_graph_cluster_stats_destroy(stats);
> +}
> +
> +static void
> +graph_config_process(char **tokens, uint32_t n_tokens, char *out, size_t out_size,
> +		     void *obj __rte_unused)
> +{
> +	uint32_t bsz = 32, tmo = 0, coremask = 0xf;
> +	struct graph_config config;
> +	int idx = 2, rc;
> +	uint8_t model;
> +
> +	if (n_tokens < 4) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		return;
> +	}
> +
> +next_arg:
> +	if (strcmp(tokens[idx], "model")) {
> +		if (strcmp(tokens[idx], "bsz") == 0) {
> +			if (parser_uint32_read(&bsz, tokens[idx + 1])) {
> +				snprintf(out, out_size, MSG_ARG_INVALID, "bsz");
> +				return;
> +			}
> +
> +		} else if (strcmp(tokens[idx], "tmo") == 0) {
> +			if (parser_uint32_read(&tmo, tokens[idx + 1])) {
> +				snprintf(out, out_size, MSG_ARG_INVALID, "tmo");
> +				return;
> +			}
> +		} else if (strcmp(tokens[idx], "coremask") == 0) {
> +			coremask = strtol(tokens[idx + 1], NULL, 16);
> +			if (coremask == 0) {
> +				snprintf(out, out_size, MSG_ARG_INVALID, "tmo");
> +				return;
> +			}
> +		} else {
> +			snprintf(out, out_size, MSG_ARG_NOT_FOUND, "usecases
> params");
> +			return;
> +		}
> +
> +		idx += 2;
> +		goto next_arg;
> +	} else {
> +		if (strcmp(tokens[idx + 1], "default") == 0) {
> +			model = GRAPH_MODEL_RTC;
> +		} else if (strcmp(tokens[idx + 1], "rtc") == 0) {
> +			model = GRAPH_MODEL_RTC;
> +		} else if (strcmp(tokens[idx + 1], "mcd") == 0) {
> +			model = GRAPH_MODEL_MCD;
> +		} else {
> +			snprintf(out, out_size, MSG_ARG_NOT_FOUND, "model
> arguments");
> +			return;
> +		}
> +	}
> +
> +	config.params.bsz = bsz;
> +	config.params.tmo = tmo;
> +	config.params.coremask = coremask;
> +	config.model = model;
> +	rc = graph_config_add(tokens[1], &config);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
> +}
> +
> +static int
> +cli_graph_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char
> *out,
> +	       size_t out_size, void *obj __rte_unused)
> +{
> +	size_t len;
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "\n%s\n",
> +		 "----------------------------- graph command help -----------------------------");
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_graph_help);
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", "graph start");
> +	return 0;
> +}
> +
> +static int
> +cli_graph(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj
> __rte_unused)
> +{
> +	if (strcmp(tokens[1], "start") == 0)
> +		graph_start();
> +	else
> +		graph_config_process(tokens, n_tokens, out, out_size, obj);
> +
> +	return 0;
> +}
> +
> +static struct cli_module graph = {
> +	.cmd = "graph",
> +	.process = cli_graph,
> +	.usage = cli_graph_help,
> +};
> +
> +CLI_REGISTER(graph);
> diff --git a/app/graph/graph.h b/app/graph/graph.h
> new file mode 100644
> index 0000000000..126e967d75
> --- /dev/null
> +++ b/app/graph/graph.h
> @@ -0,0 +1,11 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_H
> +#define APP_GRAPH_H
> +
> +int graph_walk_start(void *conf);
> +void graph_stats_print(void);
> +
> +#endif
> diff --git a/app/graph/graph_priv.h b/app/graph/graph_priv.h
> new file mode 100644
> index 0000000000..655a028fb2
> --- /dev/null
> +++ b/app/graph/graph_priv.h
> @@ -0,0 +1,32 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_PRIV_H
> +#define APP_GRAPH_PRIV_H
> +
> +#define MAX_GRAPH_USECASES 32
> +
> +enum graph_model {
> +	GRAPH_MODEL_RTC = 0x01,
> +	GRAPH_MODEL_MCD = 0x02,
> +};
> +
> +struct usecases {
> +	char name[32];
> +	bool enabled;
> +};
> +
> +struct usecase_params {
> +	uint64_t coremask;
> +	uint32_t bsz;
> +	uint32_t tmo;
> +};
> +
> +struct graph_config {
> +	struct usecases usecases[MAX_GRAPH_USECASES];
> +	struct usecase_params params;
> +	enum graph_model model;
> +};
> +
> +#endif
> diff --git a/app/graph/ip4_route.c b/app/graph/ip4_route.c
> new file mode 100644
> index 0000000000..5aba5b38f2
> --- /dev/null
> +++ b/app/graph/ip4_route.c
> @@ -0,0 +1,146 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include <rte_node_ip4_api.h>
> +
> +#include "module_api.h"
> +
> +static const char
> +cmd_ipv4_lookup_help[] = "ipv4_lookup route add ipv4 <ip> netmask <mask> via <ip>";
> +
> +struct ipv4_route_config route4[MAX_ROUTE_ENTRIES];
> +
> +static uint8_t
> +convert_netmask_to_depth(uint32_t netmask)
> +{
> +	uint8_t zerobits = 0;
> +
> +	while ((netmask & 0x1) == 0) {
> +		netmask = netmask >> 1;
> +		zerobits++;
> +	}
> +
> +	return (32 - zerobits);
> +}
> +
> +static int
> +route_ip4_add(struct ipv4_route_config *route)
> +{
> +	int i;
> +
> +	for (i = 0; i < MAX_ROUTE_ENTRIES; i++) {
> +		if (!route4[i].is_used)
> +			break;
> +	}
> +
> +	if (i == MAX_ROUTE_ENTRIES)
> +		return -ENOMEM;

[Nithin] Change neigh and route database to dynamic linked list instead of using static array.

> +
> +	route4[i].ip = route->ip;
> +	route4[i].netmask = route->netmask;
> +	route4[i].via = route->via;
> +	route4[i].is_used = true;
> +	return 0;
> +}
> +
> +int
> +route_ip4_add_to_lookup(void)
> +{
> +	struct ipv4_route_config *route = NULL;
> +	int rc = -EINVAL;
> +	uint8_t depth;
> +	int portid, i;
> +
> +	for (i = 0; i < MAX_ROUTE_ENTRIES; i++) {
> +		if (route4[i].is_used)
> +			route = &route4[i];
> +
> +		portid = ethdev_portid_by_ip4(route->via);
> +		if (portid < 0) {
> +			printf("Invalid portid found to install the route\n");
> +			return rc;
> +		}
> +
> +		depth = convert_netmask_to_depth(route->netmask);
> +
> +		rc = rte_node_ip4_route_add(route->ip, depth, portid,
> +					     RTE_NODE_IP4_LOOKUP_NEXT_REWRITE);
> +		if (rc < 0)
> +			return rc;
> +	}
> +
> +	return 0;
> +}
> +
> +static int
> +cli_ipv4_lookup_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused,
> char *out,
> +		     size_t out_size, void *obj __rte_unused)
> +{
> +	size_t len;
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "\n%s\n",
> +		 "--------------------------- ipv4_lookup command help --------------------------
> ");
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_ipv4_lookup_help);
> +	return 0;
> +}
> +
> +static int
> +cli_ipv4_lookup(char **tokens, uint32_t n_tokens, char *out, size_t out_size,
> +		void *obj __rte_unused)
> +{
> +	struct ipv4_route_config config;
> +	int rc = -EINVAL;
> +
> +	if (n_tokens != 9) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		goto exit;
> +	}
> +
> +	if (parser_ip4_read(&config.ip, tokens[4])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "ipv4");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[5], "netmask")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "netmask");
> +		goto exit;
> +	}
> +
> +	if (parser_ip4_read(&config.netmask, tokens[6])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "netmask");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[7], "via")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "via");
> +		goto exit;
> +	}
> +
> +	if (parser_ip4_read(&config.via, tokens[8])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "via ip");
> +		goto exit;
> +	}
> +
> +	rc = route_ip4_add(&config);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
> +
> +exit:
> +	return rc;
> +}
> +
> +static struct cli_module ipv4_lookup = {
> +	.cmd = "ipv4_lookup",
> +	.process = cli_ipv4_lookup,
> +	.usage = cli_ipv4_lookup_help,
> +};
> +
> +CLI_REGISTER(ipv4_lookup);
> diff --git a/app/graph/ip6_route.c b/app/graph/ip6_route.c
> new file mode 100644
> index 0000000000..2c5397f9d3
> --- /dev/null
> +++ b/app/graph/ip6_route.c
> @@ -0,0 +1,154 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include <rte_node_ip6_api.h>
> +
> +#include "module_api.h"
> +
> +static const char
> +cmd_ipv6_lookup_help[] = "ipv6_lookup route add ipv6 <ip> netmask <mask> via <ip>";
> +
> +struct ipv6_route_config route6[MAX_ROUTE_ENTRIES];
> +
> +static uint8_t
> +convert_ip6_netmask_to_depth(uint8_t *netmask)
> +{
> +	uint8_t setbits = 0;
> +	uint8_t mask;
> +	int i;
> +
> +	for (i = 0; i < ETHDEV_IPV6_ADDR_LEN; i++) {
> +		mask = netmask[i];
> +		while (mask & 0x80) {
> +			mask = mask << 1;
> +			setbits++;
> +		}
> +	}
> +
> +	return setbits;
> +}
> +
> +static int
> +route_ip6_add(struct ipv6_route_config *route)
> +{
> +	int i, j;
> +
> +	for (i = 0; i < RTE_MAX_ETHPORTS; i++) {
> +		if (!route6[i].is_used)
> +			break;
> +	}
> +
> +	if (i == RTE_MAX_ETHPORTS)
> +		return -ENOMEM;
> +
> +	for (j = 0; j < ETHDEV_IPV6_ADDR_LEN; j++) {
> +		route6[i].ip[j] = route->ip[j];
> +		route6[i].mask[j] = route->mask[j];
> +		route6[i].gateway[j] = route->gateway[j];
> +	}
> +	route6[i].is_used = true;
> +
> +	return 0;
> +}
> +
> +int
> +route_ip6_add_to_lookup(void)
> +{
> +	struct ipv6_route_config *route = NULL;
> +	int rc = -EINVAL;
> +	uint8_t depth;
> +	int portid, i;
> +
> +	for (i = 0; i < MAX_ROUTE_ENTRIES; i++) {
> +		if (route6[i].is_used)
> +			route = &route6[i];
> +
> +		portid = ethdev_portid_by_ip6(route->gateway);
> +		if (portid < 0) {
> +			printf("Invalid portid found to install the route\n");
> +			return rc;
> +		}
> +
> +		depth = convert_ip6_netmask_to_depth(route->mask);
> +
> +		rc = rte_node_ip6_route_add(route->ip, depth, portid,
> +					     RTE_NODE_IP6_LOOKUP_NEXT_REWRITE);
> +		if (rc < 0)
> +			return rc;
> +	}
> +
> +	return 0;
> +}
> +
> +static int
> +cli_ipv6_lookup_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused,
> char *out,
> +		     size_t out_size, void *obj __rte_unused)
> +{
> +	size_t len;
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "\n%s\n",
> +		 "--------------------------- ipv6_lookup command help --------------------------
> ");
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_ipv6_lookup_help);
> +	return 0;
> +}
> +
> +static int
> +cli_ipv6_lookup(char **tokens, uint32_t n_tokens, char *out, size_t out_size,
> +		void *obj __rte_unused)
> +{
> +	struct ipv6_route_config config;
> +	int rc = -EINVAL;
> +
> +	if (n_tokens != 9) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		goto exit;
> +	}
> +
> +	if (parser_ip6_read(config.ip, tokens[4])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "ipv6");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[5], "netmask")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "netmask");
> +		goto exit;
> +	}
> +
> +	if (parser_ip6_read(config.mask, tokens[6])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "netmask");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[7], "via")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "via");
> +		goto exit;
> +	}
> +
> +	if (parser_ip6_read(config.gateway, tokens[8])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "gateway ip");
> +		goto exit;
> +	}
> +
> +	rc = route_ip6_add(&config);
> +	if (rc)
> +		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
> +
> +exit:
> +	return rc;
> +}
> +
> +static struct cli_module ipv6_lookup = {
> +	.cmd = "ipv6_lookup",
> +	.process = cli_ipv6_lookup,
> +	.usage = cli_ipv6_lookup_help,
> +};
> +
> +CLI_REGISTER(ipv6_lookup);
> diff --git a/app/graph/l3fwd.c b/app/graph/l3fwd.c
> new file mode 100644
> index 0000000000..85b8b2618e
> --- /dev/null
> +++ b/app/graph/l3fwd.c
> @@ -0,0 +1,152 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <errno.h>
> +#include <stdbool.h>
> +#include <stdint.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include <rte_common.h>
> +#include <rte_ethdev.h>
> +#include <rte_graph.h>
> +#include <rte_graph_worker.h>
> +#include <rte_lcore.h>
> +#include <rte_node_eth_api.h>
> +
> +#include "module_api.h"
> +
> +static char pcap_filename[RTE_GRAPH_PCAP_FILE_SZ];
> +static uint64_t packet_to_capture;
> +static int pcap_trace_enable;
> +
> +static int
> +l3fwd_pattern_configure(void)
> +{
> +	/* Graph initialization. 8< */
> +	static const char * const default_patterns[] = {
> +		"ip4*",
> +		"ethdev_tx-*",
> +		"pkt_drop",
> +	};
> +
> +	struct rte_graph_param graph_conf;
> +	const char **node_patterns;
> +	struct lcore_conf *qconf;
> +	uint16_t nb_patterns;
> +	uint8_t lcore_id;
> +	int rc;
> +
> +	nb_patterns = RTE_DIM(default_patterns);
> +	node_patterns = malloc((ETHDEV_RX_QUEUE_PER_LCORE_MAX + nb_patterns) *
> +			sizeof(*node_patterns));
> +	if (!node_patterns)
> +		return -ENOMEM;
> +	memcpy(node_patterns, default_patterns,
> +			nb_patterns * sizeof(*node_patterns));
> +
> +	memset(&graph_conf, 0, sizeof(graph_conf));
> +	graph_conf.node_patterns = node_patterns;
> +
> +	/* Pcap config */
> +	graph_conf.pcap_enable = pcap_trace_enable;
> +	graph_conf.num_pkt_to_capture = packet_to_capture;
> +	graph_conf.pcap_filename = pcap_filename;
> +
> +	for (lcore_id = 0; lcore_id < RTE_MAX_LCORE; lcore_id++) {
> +		rte_graph_t graph_id;
> +		rte_edge_t i;
> +
> +		if (rte_lcore_is_enabled(lcore_id) == 0)
> +			continue;
> +
> +		qconf = &lcore_conf[lcore_id];
> +
> +		/* Skip graph creation if no source exists */
> +		if (!qconf->n_rx_queue)
> +			continue;
> +
> +		/* Add rx node patterns of this lcore */
> +		for (i = 0; i < qconf->n_rx_queue; i++) {
> +			graph_conf.node_patterns[nb_patterns + i] =
> +				qconf->rx_queue_list[i].node_name;
> +		}
> +
> +		graph_conf.nb_node_patterns = nb_patterns + i;
> +		graph_conf.socket_id = rte_lcore_to_socket_id(lcore_id);
> +
> +		snprintf(qconf->name, sizeof(qconf->name), "worker_%u",
> +				lcore_id);
> +
> +		graph_id = rte_graph_create(qconf->name, &graph_conf);
> +		if (graph_id == RTE_GRAPH_ID_INVALID)
> +			rte_exit(EXIT_FAILURE,
> +					"rte_graph_create(): graph_id invalid"
> +					" for lcore %u\n", lcore_id);
> +
> +		qconf->graph_id = graph_id;
> +		qconf->graph = rte_graph_lookup(qconf->name);
> +		/* >8 End of graph initialization. */
> +		if (!qconf->graph)
> +			rte_exit(EXIT_FAILURE,
> +					"rte_graph_lookup(): graph %s not found\n",
> +					qconf->name);
> +	}
> +
> +	rc = route_ip4_add_to_lookup();
> +	if (rc < 0)
> +		rte_exit(EXIT_FAILURE, "Unable to add v4 route to lookup table\n");
> +
> +	rc = route_ip6_add_to_lookup();
> +	if (rc < 0)
> +		rte_exit(EXIT_FAILURE, "Unable to add v6 route to lookup table\n");
> +
> +	rc = neigh_ip4_add_to_rewrite();
> +	if (rc < 0)
> +		rte_exit(EXIT_FAILURE, "Unable to add v4 to rewrite node\n");
> +
> +	rc = neigh_ip6_add_to_rewrite();
> +	if (rc < 0)
> +		rte_exit(EXIT_FAILURE, "Unable to add v6 to rewrite node\n");
> +
> +	/* Launch per-lcore init on every worker lcore */
> +	rte_eal_mp_remote_launch(graph_walk_start, NULL, SKIP_MAIN);
> +
> +	/* Accumulate and print stats on main until exit */
> +	if (rte_graph_has_stats_feature() && app_graph_stats_enabled())
> +		graph_stats_print();
> +
> +	/* Wait for worker cores to exit */
> +	rc = 0;
> +	RTE_LCORE_FOREACH_WORKER(lcore_id) {
> +		rc = rte_eal_wait_lcore(lcore_id);
> +		/* Destroy graph */
> +		if (rc < 0 ||
> +		    rte_graph_destroy(rte_graph_from_name(lcore_conf[lcore_id].name)))
> {
> +			rc = -1;
> +			break;
> +		}
> +	}
> +	free(node_patterns);
> +
> +	ethdev_stop();
> +	return rc;
> +}
> +
> +int
> +usecase_l3fwd_configure(struct rte_node_ethdev_config *conf, uint16_t nb_confs,
> uint16_t nb_graphs)
> +{
> +	int rc;
> +
> +	rc = rte_node_eth_config(conf, nb_confs, nb_graphs);
> +	if (rc)
> +		rte_exit(EXIT_FAILURE, "rte_node_eth_config: err=%d\n", rc);
> +
> +	rc = l3fwd_pattern_configure();
> +	if (rc)
> +		rte_exit(EXIT_FAILURE, "l3fwd_pattern_failure: err=%d\n", rc);
> +
> +	return rc;
> +}
> diff --git a/app/graph/l3fwd.h b/app/graph/l3fwd.h
> new file mode 100644
> index 0000000000..e1d23165e6
> --- /dev/null
> +++ b/app/graph/l3fwd.h
> @@ -0,0 +1,11 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_L3FWD_H
> +#define APP_GRAPH_L3FWD_H
> +
> +int usecase_l3fwd_configure(struct rte_node_ethdev_config *conf, uint16_t nb_conf,
> +			    uint16_t nb_graphs);
> +
> +#endif
> diff --git a/app/graph/main.c b/app/graph/main.c
> new file mode 100644
> index 0000000000..e9934025bf
> --- /dev/null
> +++ b/app/graph/main.c
> @@ -0,0 +1,201 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <fcntl.h>
> +#include <getopt.h>
> +#include <signal.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <unistd.h>
> +
> +#include <rte_eal.h>
> +#include <rte_launch.h>
> +
> +#include "module_api.h"
> +
> +volatile bool force_quit;
> +
> +static const char usage[] = "%s EAL_ARGS -- -s SCRIPT [-h HOST] [-p PORT] [--enable-graph-
> stats] "
> +			    "[--help]\n";
> +
> +static struct app_params {
> +	struct conn_params conn;
> +	char *script_name;
> +	bool enable_graph_stats;
> +} app = {
> +	.conn = {
> +		.welcome = "\nWelcome!\n\n",
> +		.prompt = "graph> ",
> +		.addr = "0.0.0.0",
> +		.port = 8086,
> +		.buf_size = 1024 * 1024,
> +		.msg_in_len_max = 1024,
> +		.msg_out_len_max = 1024 * 1024,
> +		.msg_handle = cli_process,
> +		.msg_handle_arg = NULL, /* set later. */
> +	},
> +	.script_name = NULL,
> +	.enable_graph_stats = false,
> +};
> +
> +static void
> +signal_handler(int signum)
> +{
> +	if (signum == SIGINT || signum == SIGTERM) {
> +		printf("\n\nSignal %d received, preparing to exit...\n", signum);
> +		force_quit = true;
> +	}
> +}
> +
> +static int
> +app_args_parse(int argc, char **argv)
> +{
> +	struct option lgopts[] = {
> +		{"help", 0, 0, 'H'},
> +		{"enable-graph-stats", 0, 0, 'g'},
> +	};
> +	int h_present, p_present, s_present, n_args, i;
> +	char *app_name = argv[0];
> +	int opt, option_index;
> +
> +	/* Skip EAL input args */
> +	n_args = argc;
> +	for (i = 0; i < n_args; i++)
> +		if (strcmp(argv[i], "--") == 0) {
> +			argc -= i;
> +			argv += i;
> +			break;
> +		}
> +
> +	if (i == n_args)
> +		return 0;
> +
> +	/* Parse args */
> +	h_present = 0;
> +	p_present = 0;
> +	s_present = 0;
> +
> +	while ((opt = getopt_long(argc, argv, "h:p:s:", lgopts, &option_index)) != EOF) {
> +		switch (opt) {
> +		case 'h':
> +			if (h_present) {
> +				printf("Error: Multiple -h arguments\n");
> +				return -1;
> +			}
> +			h_present = 1;
> +
> +			if (!strlen(optarg)) {
> +				printf("Error: Argument for -h not provided\n");
> +				return -1;
> +			}
> +
> +			app.conn.addr = strdup(optarg);
> +			if (app.conn.addr == NULL) {
> +				printf("Error: Not enough memory\n");
> +				return -1;
> +			}
> +			break;
> +
> +		case 'p':
> +			if (p_present) {
> +				printf("Error: Multiple -p arguments\n");
> +				return -1;
> +			}
> +			p_present = 1;
> +
> +			if (!strlen(optarg)) {
> +				printf("Error: Argument for -p not provided\n");
> +				return -1;
> +			}
> +
> +			app.conn.port = (uint16_t) atoi(optarg);
> +			break;
> +
> +		case 's':
> +			if (s_present) {
> +				printf("Error: Multiple -s arguments\n");
> +				return -1;
> +			}
> +			s_present = 1;
> +
> +			if (!strlen(optarg)) {
> +				printf("Error: Argument for -s not provided\n");
> +				return -1;
> +			}
> +
> +			app.script_name = strdup(optarg);
> +			if (app.script_name == NULL) {
> +				printf("Error: Not enough memory\n");
> +				return -1;
> +			}
> +			break;
> +
> +		case 'g':
> +			app.enable_graph_stats = true;
> +			break;
> +
> +		case 'H':
> +		default:
> +			printf(usage, app_name);
> +			return -1;
> +		}
> +	}
> +	optind = 1; /* reset getopt lib */
> +
> +	return 0;
> +}
> +
> +bool
> +app_graph_stats_enabled(void)
> +{
> +	return app.enable_graph_stats;
> +}
> +
> +int
> +main(int argc, char **argv)
> +{
> +	struct conn *conn;
> +	int rc;
> +
> +	/* Parse application arguments */
> +	rc = app_args_parse(argc, argv);
> +	if (rc < 0)
> +		return rc;
> +
> +	/* EAL */
> +	rc = rte_eal_init(argc, argv);
> +	if (rc < 0) {
> +		printf("Error: EAL initialization failed (%d)\n", rc);
> +		return rc;
> +	};
> +
> +	force_quit = false;
> +	signal(SIGINT, signal_handler);
> +	signal(SIGTERM, signal_handler);
> +
> +	/* Script */
> +	if (app.script_name) {
> +		cli_script_process(app.script_name, app.conn.msg_in_len_max,
> +			app.conn.msg_out_len_max, NULL);
> +	}
> +
> +	/* Connectivity */
> +	app.conn.msg_handle_arg = NULL;
> +	conn = conn_init(&app.conn);
> +	if (!conn) {
> +		printf("Error: Connectivity initialization failed (%d)\n", rc);
> +		return rc;
> +	};
> +
> +	/* Dispatch loop */
> +	while (1) {
> +		conn_req_poll(conn);
> +
> +		conn_msg_poll(conn);
> +	}
> +
> +	/* clean up the EAL */
> +	rte_eal_cleanup();
> +}
> diff --git a/app/graph/mempool.c b/app/graph/mempool.c
> new file mode 100644
> index 0000000000..1cee66abed
> --- /dev/null
> +++ b/app/graph/mempool.c
> @@ -0,0 +1,134 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include <rte_common.h>
> +#include <rte_mbuf.h>
> +
> +#include "mempool_priv.h"
> +#include "module_api.h"
> +
> +static const char
> +cmd_mempool_help[] = "mempool <mempool_name> size <mbuf_size> buffers
> <number_of_buffers> "
> +		     "cache <cache_size> numa <numa_id>";
> +
> +struct mempools mpconfig;
> +
> +int
> +mempool_process(struct mempool_config *config)
> +{
> +	struct rte_mempool *mp;
> +	uint8_t nb_pools;
> +
> +	nb_pools = mpconfig.nb_pools;
> +	strcpy(mpconfig.config[nb_pools].name, config->name);
> +	mpconfig.config[nb_pools].pool_size = config->pool_size;
> +	mpconfig.config[nb_pools].buffer_size = config->buffer_size;
> +	mpconfig.config[nb_pools].cache_size = config->cache_size;
> +	mpconfig.config[nb_pools].numa_node = config->numa_node;
> +
> +	mp = rte_pktmbuf_pool_create(config->name, config->pool_size, config-
> >cache_size,
> +		64, config->buffer_size, config->numa_node);
> +	if (!mp)
> +		return -EINVAL;
> +
> +	mpconfig.mp[nb_pools] = mp;
> +	nb_pools++;
> +	mpconfig.nb_pools = nb_pools;
> +
> +	return 0;
> +}
> +
> +static int
> +cli_mempool_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char
> *out,
> +		 size_t out_size, void *obj __rte_unused)
> +{
> +	size_t len;
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "\n%s\n",
> +		 "---------------------------- mempool command help ----------------------------");
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_mempool_help);
> +	return 0;
> +}
> +
> +static int
> +cli_mempool(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj
> __rte_unused)
> +{
> +	uint32_t pkt_buffer_size, pool_size, cache_size, numa_node;
> +	struct mempool_config config;
> +	int rc = -EINVAL;
> +
> +	if (n_tokens != 10) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[2], "size")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "size");
> +		goto exit;
> +	}
> +
> +	if (parser_uint32_read(&pkt_buffer_size, tokens[3])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "mbuf_size");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[4], "buffers")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "buffers");
> +		goto exit;
> +	}
> +
> +	if (parser_uint32_read(&pool_size, tokens[5])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "number_of_buffers");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[6], "cache")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "cache");
> +		goto exit;
> +	}
> +
> +	if (parser_uint32_read(&cache_size, tokens[7])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "cache_size");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[8], "numa")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "numa");
> +		goto exit;
> +	}
> +
> +	if (parser_uint32_read(&numa_node, tokens[9])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "numa_id");
> +		goto exit;
> +	}
> +
> +	strcpy(config.name, tokens[1]);
> +	config.name[strlen(tokens[1])] = '\0';
> +	config.pool_size = pool_size;
> +	config.buffer_size = pkt_buffer_size;
> +	config.cache_size = cache_size;
> +	config.numa_node = numa_node;
> +
> +	rc = mempool_process(&config);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
> +
> +exit:
> +	return rc;
> +}
> +
> +static struct cli_module mempool = {
> +	.cmd = "mempool",
> +	.process = cli_mempool,
> +	.usage = cli_mempool_help,
> +};
> +
> +CLI_REGISTER(mempool);
> diff --git a/app/graph/mempool.h b/app/graph/mempool.h
> new file mode 100644
> index 0000000000..5fc788199d
> --- /dev/null
> +++ b/app/graph/mempool.h
> @@ -0,0 +1,18 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_MEMPOOL_H
> +#define APP_GRAPH_MEMPOOL_H
> +
> +struct mempool_config {
> +	char name[RTE_MEMPOOL_NAMESIZE];
> +	int pool_size;
> +	int cache_size;
> +	int buffer_size;
> +	int numa_node;
> +};
> +
> +int mempool_process(struct mempool_config *config);
> +
> +#endif
> diff --git a/app/graph/mempool_priv.h b/app/graph/mempool_priv.h
> new file mode 100644
> index 0000000000..5a55722b32
> --- /dev/null
> +++ b/app/graph/mempool_priv.h
> @@ -0,0 +1,16 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_MEMPOOL_PRIV_H
> +#define APP_GRAPH_MEMPOOL_PRIV_H
> +
> +#include "mempool.h"
> +
> +struct mempools {
> +	struct mempool_config config[RTE_MAX_ETHPORTS];
> +	struct rte_mempool *mp[RTE_MAX_ETHPORTS];
> +	uint8_t	nb_pools;
> +};
> +
> +#endif
> diff --git a/app/graph/meson.build b/app/graph/meson.build
> new file mode 100644
> index 0000000000..a3011e504b
> --- /dev/null
> +++ b/app/graph/meson.build
> @@ -0,0 +1,25 @@
> +# SPDX-License-Identifier: BSD-3-Clause
> +# Copyright(c) 2023 Marvell.
> +
> +# override default name to drop the hyphen
> +name = 'graph'
> +build = cc.has_header('sys/epoll.h')
> +if not build
> +    subdir_done()
> +endif
> +
> +deps += ['bus_pci', 'graph', 'eal', 'lpm', 'ethdev', 'node']
> +sources = files(
> +        'cli.c',
> +        'conn.c',
> +        'ethdev_rx.c',
> +        'ethdev.c',
> +        'graph.c',
> +        'ip4_route.c',
> +        'ip6_route.c',
> +        'main.c',
> +        'mempool.c',
> +        'neigh.c',
> +        'l3fwd.c',
> +        'utils.c',
> +)
> diff --git a/app/graph/module_api.h b/app/graph/module_api.h
> new file mode 100644
> index 0000000000..09b10bc672
> --- /dev/null
> +++ b/app/graph/module_api.h
> @@ -0,0 +1,33 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_MODULE_API_H
> +#define APP_GRAPH_MODULE_API_H
> +
> +#include <stdint.h>
> +
> +#include <rte_common.h>
> +#include <rte_ethdev.h>
> +#include <rte_graph.h>
> +#include <rte_node_eth_api.h>
> +
> +#include "conn.h"
> +#include "cli.h"
> +#include "ethdev.h"
> +#include "ethdev_rx.h"
> +#include "graph.h"
> +#include "l3fwd.h"
> +#include "mempool.h"
> +#include "neigh.h"
> +#include "route.h"
> +#include "utils.h"
> +
> +/*
> + * Externs
> + */
> +extern volatile bool force_quit;
> +
> +bool app_graph_stats_enabled(void);
> +
> +#endif
> diff --git a/app/graph/neigh.c b/app/graph/neigh.c
> new file mode 100644
> index 0000000000..07766758c9
> --- /dev/null
> +++ b/app/graph/neigh.c
> @@ -0,0 +1,269 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <stdbool.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include <rte_node_ip4_api.h>
> +#include <rte_node_ip6_api.h>
> +
> +#include "neigh_priv.h"
> +#include "module_api.h"
> +
> +static const char
> +cmd_neigh_v4_help[] = "neigh add ipv4 <ip> <mac>";
> +
> +static const char
> +cmd_neigh_v6_help[] = "neigh add ipv6 <ip> <mac>";
> +
> +struct ipv4_neigh_config neigh4[MAX_NEIGH_ENTRIES];
> +struct ipv6_neigh_config neigh6[MAX_NEIGH_ENTRIES];
> +
> +static int
> +neigh_ip4_add(uint32_t ip, uint64_t mac)
> +{
> +	int i;
> +
> +	for (i = 0; i < MAX_NEIGH_ENTRIES; i++) {
> +		if (!neigh4[i].is_used)
> +			break;
> +	}
> +
> +	if (i == MAX_NEIGH_ENTRIES)
> +		return -ENOMEM;
> +
> +	neigh4[i].ip = ip;
> +	neigh4[i].mac = mac;
> +	neigh4[i].is_used = true;
> +	return 0;
> +}
> +
> +static int
> +neigh_ip6_add(uint8_t *ip, uint64_t mac)
> +{
> +	int i, j;
> +
> +	for (i = 0; i < MAX_NEIGH_ENTRIES; i++) {
> +		if (!neigh6[i].is_used)
> +			break;
> +	}
> +
> +	if (i == MAX_NEIGH_ENTRIES)
> +		return -ENOMEM;
> +
> +	for (j = 0; j < ETHDEV_IPV6_ADDR_LEN; j++)
> +		neigh6[i].ip[j] = ip[j];
> +
> +	neigh6[i].mac = mac;
> +	neigh6[i].is_used = true;
> +	return 0;
> +}
> +
> +int
> +neigh_ip4_add_to_rewrite(void)
> +{
> +	uint8_t data[2 * RTE_ETHER_ADDR_LEN];
> +	uint8_t len = 2 * RTE_ETHER_ADDR_LEN;
> +	struct rte_ether_addr smac = {0};
> +	struct ipv4_neigh_config *neigh;
> +	int16_t portid = 0;
> +	int rc, i;
> +
> +	for (i = 0; i < MAX_NEIGH_ENTRIES; i++) {
> +		if (!neigh4[i].is_used)
> +			continue;
> +
> +		neigh = &neigh4[i];
> +		portid = ethdev_portid_by_ip4(neigh->ip);
> +		if (portid < 0) {
> +			printf("Invalid portid found to add  neigh\n");
> +			return -EINVAL;
> +		}
> +
> +		memset(data, 0, len);
> +
> +		/* Copy dst mac */
> +		rte_memcpy((void *)&data[0], (void *)&neigh->mac,
> RTE_ETHER_ADDR_LEN);
> +
> +		/* Copy src mac */
> +		rc = rte_eth_macaddr_get(portid, &smac);
> +		if (rc < 0) {
> +			printf("Cannot get MAC address: err=%d, port=%d\n", rc, portid);
> +			return rc;
> +		}
> +
> +		rte_memcpy(&data[RTE_ETHER_ADDR_LEN], smac.addr_bytes,
> RTE_ETHER_ADDR_LEN);
> +
> +		rc = rte_node_ip4_rewrite_add(portid, data, len, portid);
> +		if (rc < 0) {
> +			printf("Error in writing rewrite data: err=%d, port=%d\n", rc, portid);
> +			return rc;
> +		}
> +	}
> +
> +	return 0;
> +}
> +
> +int
> +neigh_ip6_add_to_rewrite(void)
> +{
> +	uint8_t data[2 * RTE_ETHER_ADDR_LEN];
> +	uint8_t len = 2 * RTE_ETHER_ADDR_LEN;
> +	struct rte_ether_addr smac = {0};
> +	struct ipv6_neigh_config *neigh;
> +	int16_t portid = 0;
> +	int rc, i;
> +
> +	for (i = 0; i < MAX_NEIGH_ENTRIES; i++) {
> +		if (!neigh6[i].is_used)
> +			continue;
> +
> +		neigh = &neigh6[i];
> +		portid = ethdev_portid_by_ip6(neigh->ip);
> +		if (portid < 0) {
> +			printf("Invalid portid found to add neigh\n");
> +			return -EINVAL;
> +		}
> +
> +		memset(data, 0, len);
> +
> +		/* Copy dst mac */
> +		rte_memcpy((void *)&data[0], (void *)&neigh->mac,
> RTE_ETHER_ADDR_LEN);
> +
> +		/* Copy src mac */
> +		rc = rte_eth_macaddr_get(portid, &smac);
> +		if (rc < 0) {
> +			printf("Cannot get MAC address: err=%d, port=%d\n",
> +				rc, portid);
> +			return rc;
> +		}
> +
> +		rte_memcpy(&data[RTE_ETHER_ADDR_LEN], smac.addr_bytes,
> RTE_ETHER_ADDR_LEN);
> +
> +		rc = rte_node_ip6_rewrite_add(portid, data, len, portid);
> +		if (rc < 0) {
> +			printf("Error in writing rewrite data: err=%d, port=%d\n", rc, portid);
> +			return rc;
> +		}
> +	}
> +
> +	return 0;
> +}
> +
> +static int
> +cmd_neigh_v4(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj
> __rte_unused)
> +{
> +	int rc = -EINVAL;
> +	uint64_t mac;
> +	uint32_t ip;
> +
> +	if (n_tokens != 5) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[1], "add")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "add");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[2], "ipv4")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "ipv4");
> +		goto exit;
> +	}
> +
> +	if (parser_ip4_read(&ip, tokens[3])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "ip");
> +		goto exit;
> +	}
> +
> +	if (parser_mac_read(&mac, tokens[4])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "mac");
> +		goto exit;
> +	}
> +
> +	rc = neigh_ip4_add(ip, mac);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
> +
> +exit:
> +	return rc;
> +}
> +
> +static int
> +cmd_neigh_v6(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj
> __rte_unused)
> +{
> +	uint8_t ip[ETHDEV_IPV6_ADDR_LEN];
> +	int rc = -EINVAL;
> +	uint64_t mac;
> +
> +	if (n_tokens != 5) {
> +		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[1], "add")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "add");
> +		goto exit;
> +	}
> +
> +	if (strcmp(tokens[2], "ipv6")) {
> +		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "ipv6");
> +		goto exit;
> +	}
> +
> +	if (parser_ip6_read(ip, tokens[3])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "ip");
> +		goto exit;
> +	}
> +
> +	if (parser_mac_read(&mac, tokens[4])) {
> +		snprintf(out, out_size, MSG_ARG_INVALID, "mac");
> +		goto exit;
> +	}
> +
> +	rc = neigh_ip6_add(ip, mac);
> +	if (rc < 0)
> +		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
> +
> +exit:
> +	return rc;
> +}
> +
> +static int
> +cli_neigh_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char *out,
> +		     size_t out_size, void *obj __rte_unused)
> +{
> +	size_t len;
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "\n%s\n",
> +		 "----------------------------- neigh command help -----------------------------");
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_neigh_v4_help);
> +
> +	len = strlen(out);
> +	snprintf(out + len, out_size, "%s\n", cmd_neigh_v6_help);
> +	return 0;
> +}
> +
> +static int
> +cli_neigh(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj)
> +{
> +	if (strcmp(tokens[2], "ipv4") == 0)
> +		return cmd_neigh_v4(tokens, n_tokens, out, out_size, obj);
> +	else
> +		return cmd_neigh_v6(tokens, n_tokens, out, out_size, obj);
> +}
> +
> +static struct cli_module neigh = {
> +	.cmd = "neigh",
> +	.process = cli_neigh,
> +	.usage = cli_neigh_help,
> +};
> +
> +CLI_REGISTER(neigh);

[Nithin] Move logic for tokenizing into cmdline library itself. Module should only register the commands to libcmdline.

> diff --git a/app/graph/neigh.h b/app/graph/neigh.h
> new file mode 100644
> index 0000000000..3964c37bb0
> --- /dev/null
> +++ b/app/graph/neigh.h
> @@ -0,0 +1,11 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_NEIGH_H
> +#define APP_GRAPH_NEIGH_H
> +
> +int neigh_ip4_add_to_rewrite(void);
> +int neigh_ip6_add_to_rewrite(void);
> +
> +#endif
> diff --git a/app/graph/neigh_priv.h b/app/graph/neigh_priv.h
> new file mode 100644
> index 0000000000..745dc7d671
> --- /dev/null
> +++ b/app/graph/neigh_priv.h
> @@ -0,0 +1,22 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_NEIGH_PRIV_H
> +#define APP_GRAPH_NEIGH_PRIV_H
> +
> +#define MAX_NEIGH_ENTRIES 32
> +
> +struct ipv4_neigh_config {
> +	uint32_t ip;
> +	uint64_t mac;
> +	bool is_used;
> +};
> +
> +struct ipv6_neigh_config {
> +	uint8_t ip[16];
> +	uint64_t mac;
> +	bool is_used;
> +};
> +
> +#endif
> diff --git a/app/graph/route.h b/app/graph/route.h
> new file mode 100644
> index 0000000000..6b4acf3344
> --- /dev/null
> +++ b/app/graph/route.h
> @@ -0,0 +1,30 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_ROUTE_H
> +#define APP_GRAPH_ROUTE_H
> +
> +#define MAX_ROUTE_ENTRIES 32
> +
> +struct ipv4_route_config {
> +	uint32_t ip;
> +	uint32_t netmask;
> +	uint32_t via;
> +	bool is_used;
> +};
> +
> +struct ipv6_route_config {
> +	uint8_t ip[16];
> +	uint8_t mask[16];
> +	uint8_t gateway[16];
> +	bool is_used;
> +};
> +
> +extern struct ipv4_route_config route4[MAX_ROUTE_ENTRIES];
> +extern struct ipv6_route_config route6[MAX_ROUTE_ENTRIES];
> +
> +int route_ip4_add_to_lookup(void);
> +int route_ip6_add_to_lookup(void);
> +
> +#endif
> diff --git a/app/graph/utils.c b/app/graph/utils.c
> new file mode 100644
> index 0000000000..48a83e738c
> --- /dev/null
> +++ b/app/graph/utils.c
> @@ -0,0 +1,155 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#include <ctype.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +#include <rte_common.h>
> +
> +#include "module_api.h"
> +
> +#define white_spaces_skip(pos)			\
> +({						\
> +	__typeof__(pos) _p = (pos);		\
> +	for ( ; isspace(*_p); _p++)		\
> +		;				\
> +	_p;					\
> +})
> +
> +static void
> +hex_string_to_uint64(uint64_t *dst, const char *hexs)
> +{
> +	char buf[2] = {0};
> +	uint8_t shift = 4;
> +	int iter = 0;
> +	char c;
> +
> +	while ((c = *hexs++)) {
> +		buf[0] = c;
> +		*dst |= (strtol(buf, NULL, 16) << shift);
> +		shift -= 4;
> +		iter++;
> +		if (iter == 2) {
> +			iter = 0;
> +			shift = 4;
> +			dst++;
> +		}
> +	}
> +}
> +
> +int
> +parser_uint64_read(uint64_t *value, const char *p)
> +{
> +	char *next;
> +	uint64_t val;
> +
> +	p = white_spaces_skip(p);
> +	if (!isdigit(*p))
> +		return -EINVAL;
> +
> +	val = strtoul(p, &next, 0);
> +	if (p == next)
> +		return -EINVAL;
> +
> +	p = next;
> +	switch (*p) {
> +	case 'T':
> +		val *= 1024ULL;
> +		/* fall through */
> +	case 'G':
> +		val *= 1024ULL;
> +		/* fall through */
> +	case 'M':
> +		val *= 1024ULL;
> +		/* fall through */
> +	case 'k':
> +	case 'K':
> +		val *= 1024ULL;
> +		p++;
> +		break;
> +	}
> +
> +	p = white_spaces_skip(p);
> +	if (*p != '\0')
> +		return -EINVAL;
> +
> +	*value = val;
> +	return 0;
> +}
> +
> +int
> +parser_uint32_read(uint32_t *value, const char *p)
> +{
> +	uint64_t val = 0;
> +	int rc = parser_uint64_read(&val, p);
> +
> +	if (rc < 0)
> +		return rc;
> +
> +	if (val > UINT32_MAX)
> +		return -ERANGE;
> +
> +	*value = val;
> +	return 0;
> +}
> +
> +int
> +parser_ip4_read(uint32_t *value, char *p)
> +{
> +	uint8_t shift = 24;
> +	uint32_t ip = 0;
> +	char *token;
> +
> +	token = strtok(p, ".");
> +	while (token != NULL) {
> +		ip |= (atoi(token) << shift);
> +		token = strtok(NULL, ".");
> +		shift -= 8;
> +	}
> +
> +	*value = ip;
> +
> +	return 0;
> +}
> +
> +int
> +parser_ip6_read(uint8_t *value, char *p)
> +{
> +	uint64_t val = 0;
> +	char *token;
> +
> +	token = strtok(p, ":");
> +	while (token != NULL) {
> +		hex_string_to_uint64(&val, token);
> +		*value = val;
> +		token = strtok(NULL, ":");
> +		value++;
> +		val = 0;
> +	}
> +
> +	return 0;
> +}
> +
> +int
> +parser_mac_read(uint64_t *value, char *p)
> +{
> +	uint64_t mac = 0, val = 0;
> +	uint8_t shift = 40;
> +	char *token;
> +
> +	token = strtok(p, ":");
> +	while (token != NULL) {
> +		hex_string_to_uint64(&val, token);
> +		mac |= val << shift;
> +		token = strtok(NULL, ":");
> +		shift -= 8;
> +		val = 0;
> +	}
> +
> +	*value = mac;
> +
> +	return 0;
> +}
> diff --git a/app/graph/utils.h b/app/graph/utils.h
> new file mode 100644
> index 0000000000..0ebb5de55a
> --- /dev/null
> +++ b/app/graph/utils.h
> @@ -0,0 +1,14 @@
> +/* SPDX-License-Identifier: BSD-3-Clause
> + * Copyright(c) 2023 Marvell.
> + */
> +
> +#ifndef APP_GRAPH_UTILS_H
> +#define APP_GRAPH_UTILS_H
> +
> +int parser_uint64_read(uint64_t *value, const char *p);
> +int parser_uint32_read(uint32_t *value, const char *p);
> +int parser_ip4_read(uint32_t *value, char *p);
> +int parser_ip6_read(uint8_t *value, char *p);
> +int parser_mac_read(uint64_t *value, char *p);
> +
> +#endif
> diff --git a/app/meson.build b/app/meson.build
> index e4bf5c531c..728c936383 100644
> --- a/app/meson.build
> +++ b/app/meson.build
> @@ -17,6 +17,7 @@ endif
>  apps = [
>          'dumpcap',
>          'pdump',
> +        'graph',
>          'proc-info',
>          'test-acl',
>          'test-bbdev',
> diff --git a/doc/guides/tools/graph.rst b/doc/guides/tools/graph.rst
> new file mode 100644
> index 0000000000..c90dc9ad7f
> --- /dev/null
> +++ b/doc/guides/tools/graph.rst
> @@ -0,0 +1,171 @@
> +..  SPDX-License-Identifier: BSD-3-Clause
> +    Copyright(c) 2023 Marvell.
> +
> +dpdk-graph Application
> +======================
> +
> +The ``dpdk-graph`` tool is a Data Plane Development Kit (DPDK)
> +application that allows exercising various graph use cases.
> +This application has a generic framework to add new graph based use cases to
> +verify functionality. Each use case is defined as a ``<usecase>.cli`` file.
> +Based on the input file, application creates a graph to cater the use case.
> +
> +Supported Use cases
> +-------------------
> + * l3fwd
> +
> +Running the Application
> +-----------------------
> +
> +The application has a number of command line options which can be provided in
> +following syntax
> +
> +.. code-block:: console
> +
> +   dpdk-graph [EAL Options] -- [application options]
> +
> +EAL Options
> +~~~~~~~~~~~
> +
> +Following are the EAL command-line options that can be used in conjunction
> +with the ``dpdk-graph`` application.
> +See the DPDK Getting Started Guides for more information on these options.
> +
> +*   ``-c <COREMASK>`` or ``-l <CORELIST>``
> +
> +        Set the hexadecimal bit mask of the cores to run on. The CORELIST is a
> +        list of cores to be used.
> +
> +Application Options
> +~~~~~~~~~~~~~~~~~~~
> +
> +Following are the application command-line options:
> +
> +* ``-h``
> +
> +        Set the host IPv4 address over which telnet session can be opened.
> +        It is an optional parameter. Default host address is 0.0.0.0.
> +
> +* ``-p``
> +
> +        Set the L4 port number over which telnet session can be opened.
> +	It is an optional parameter. Default port is 8086.
> +
> +* ``-s``
> +
> +        Script name with absolute path which specifies the use case. It is
> +        a mandatory parameter which will be used to create desired graph
> +        for a given use case.
> +
> +* ``--enable-graph-stats``
> +
> +       Enable graph statistics printing on console. By default graph statistics are disabled.
> +
> +* ``--help``
> +
> +       Dumps application usage
> +
> +Supported CLI commands
> +----------------------
> +
> +This section provides details on commands which can be used in ``<usecase>.cli``
> +file to express the requested use case configuration.
> +
> +.. list-table:: Exposed CLIs
> +   :header-rows: 1
> +   :widths: auto
> +
> +   * - Command
> +     - Description
> +     - Dynamic
> +     - Optional
> +   * - graph <usecases> [bsz <size>] [tmo <ns>] [coremask <bitmask>] model <rtc | mcd |
> default>
> +     - Command to express the desired use case
> +     - No
> +     - No
> +   * - graph start
> +     - Command to start the graph.
> +       This command triggers that no more commands are left to be parsed and graph
> +       initialization can be started now. It must be the last command in ``<usecase>.cli``
> +     - No
> +     - No
> +   * - mempool <mempool_name> size <mbuf_size> buffers <number_of_buffers> cache
> <cache_size> numa <numa_id>
> +     - Command to create mempool which will be further associated to RxQ to dequeue the
> packets
> +     - No
> +     - No
> +   * - ethdev <ethdev_name> rxq <n_queues> txq <n_queues> <mempool_name> [mtu
> <mtu_sz>]
> +     - Command to create DPDK port with given number of Rx and Tx queues. Also attached
> +       RxQ with given mempool. Each port can have single mempool only i.e. all RxQs will
> +       share the same mempool.
> +     - No
> +     - No
> +   * - ethdev <ethdev_name> mtu <mtu_sz>
> +     - Command to configure MTU of DPDK port
> +     - Yes
> +     - Yes
> +   * - ethdev <ethdev_name> promiscuous <on/off>
> +     - Command to enable/disable promiscuous mode on DPDK port
> +     - Yes
> +     - Yes
> +   * - ethdev <ethdev_name> show
> +     - Command to dump current ethdev configuration
> +     - Yes
> +     - Yes
> +   * - ethdev <ethdev_name> ip4 addr add <ip> netmask <mask>
> +     - Command to configure IPv4 address on given PCI device. It is needed if user
> +       wishes to use ``ipv4_lookup`` node
> +     - No
> +     - Yes
> +   * - ethdev <ethdev_name> ip6 addr add <ip> netmask <mask>
> +     - Command to configure IPv6 address on given PCI device. It is needed if user
> +       wishes to use ``ipv6_lookup`` node
> +     - No
> +     - Yes
> +   * - ipv4_lookup route add ipv4 <ip> netmask <mask> via <ip>
> +     - Command to add a route into ``ipv4_lookup`` LPM table. It is needed if user
> +       wishes to route the packets based on LPM lookup table.
> +     - No
> +     - Yes
> +   * - ipv6_lookup route add ipv6 <ip> netmask <mask> via <ip>
> +     - Command to add a route into ``ipv6_lookup`` LPM table. It is needed if user
> +       wishes to route the packets based on LPM6 lookup table.
> +     - No
> +     - Yes
> +   * - neigh add ipv4 <ip> <mac>
> +     - Command to add a neighbour information into ``ipv4_rewrite`` node.
> +     - No
> +     - Yes
> +   * - neigh add ipv6 <ip> <mac>
> +     - Command to add a neighbour information into ``ipv6_rewrite`` node.
> +     - No
> +     - Yes
> +   * - ethdev_rx map port <ethdev_name> queue <q_num> core <core_id>
> +     - Command to add port-queue-core mapping to ``ethdev_rx`` node. ``ethdev_rx``
> +       node instance will be pinned on given core and will poll on requested
> +       port/queue pair.
> +     - No
> +     - No
> +

[Nithin] Add CLI to get ethdev stats, graph stats via telnet

> +Runtime configuration
> +---------------------
> +
> +Application allows some configuration to be modified at runtime using a telnet session.
> +Application initiates a telnet server with host address 0.0.0.0 and port number 8086 if
> +``-h`` and ``-p`` is not given otherwise user provided IPv4 address and port number will
> +be used.
> +
> +After successful launch of application, client can connect to using host/port address and
> +console will be accessed with prompt ``graph>``.

[Nithin] Update Telnet session connection example with log

> +
> +Created graph for use case
> +--------------------------
> +
> +On the successful execution of ``<usecase>.cli`` file, corresponding graph will be created.
> +This section mentions the created graph for each use case.
> +
> +l3fwd
> +~~~~~
> +
> +.. _figure_l3fwd_graph:
> +
> +.. figure:: img/graph-usecase-l3fwd.*
> diff --git a/doc/guides/tools/img/graph-usecase-l3fwd.svg b/doc/guides/tools/img/graph-
> usecase-l3fwd.svg
> new file mode 100644
> index 0000000000..3b991c4cf0
> --- /dev/null
> +++ b/doc/guides/tools/img/graph-usecase-l3fwd.svg
> @@ -0,0 +1,210 @@
> +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
> + "https://urldefense.proofpoint.com/v2/url?u=http-
> 3A__www.w3.org_Graphics_SVG_1.1_DTD_svg11.dtd&d=DwIDAg&c=nKjWec2b6R0mOyPa
> z7xtfQ&r=FZ_tPCbgFOh18zwRPO9H0yDx8VW38vuapifdDfc8SFQ&m=Ee0JsG0yj736hYMlfAp
> ezlB2TtUrWk19QRB6M5nl_A9wsLObEPqlXYSqUau7ZfBV&s=QNoHRxZ4WaV84k-
> 6IlzN8d9bMQsg8Go9iFXzE0lgT-o&e= ">
> +<!-- Generated by graphviz version 2.43.0 (0)
> + -->
> +<!-- SPDX-License-Identifier: BSD-3-Clause -->
> +<!-- Copyright(C) 2023 Marvell. -->
> +<!--
> +
> +Generated with following command
> +dot -Tsvg dot.dot -o doc/guides/tools/img/graph-usecase-l3fwd.svg
> +
> +cat dot.dot
> +digraph dpdk_app_graph_l3fwd_nodes_flow {
> +    ingress_port [shape=rect]
> +    ethdev_rx
> +    pkt_cls
> +    ip4_lookup
> +    ip6_lookup
> +    ip4_rewrite
> +    ip6_rewrite
> +    ethdev_tx
> +    pkt_drop
> +    egress_port  [shape=rect]
> +
> +    ingress_port -> ethdev_rx [label="ingress packet"]
> +
> +    ethdev_rx -> pkt_cls
> +
> +    pkt_cls -> ip4_lookup [color="green"]
> +    pkt_cls -> ip6_lookup [color="blue"]
> +    pkt_cls -> pkt_drop   [color="red" style="dashed"]
> +
> +    ip4_lookup -> ip4_rewrite [color="green"]
> +    ip4_lookup -> pkt_drop [color="red" style="dashed"]
> +
> +    ip6_lookup -> ip6_rewrite [color="blue"]
> +    ip6_lookup -> pkt_drop [color="red" style="dashed"]
> +
> +    ip4_rewrite -> ethdev_tx [color="green"]
> +    ip4_rewrite -> pkt_drop  [color="red" style="dashed"]
> +
> +    ip6_rewrite -> ethdev_tx [color="blue"]
> +    ip6_rewrite -> pkt_drop  [color="red" style="dashed"]
> +
> +    ethdev_tx -> egress_port [label="egress packet"]
> +    ethdev_tx -> pkt_drop [color="red" style="dashed"]
> +}
> +
> + -->
> +<!-- Title: dpdk_app_graph_l3fwd_nodes_flow Pages: 1 -->
> +<svg width="550pt" height="510pt"
> + viewBox="0.00 0.00 549.50 510.00"
> xmlns="https://urldefense.proofpoint.com/v2/url?u=http-
> 3A__www.w3.org_2000_svg&d=DwIDAg&c=nKjWec2b6R0mOyPaz7xtfQ&r=FZ_tPCbgFOh1
> 8zwRPO9H0yDx8VW38vuapifdDfc8SFQ&m=Ee0JsG0yj736hYMlfApezlB2TtUrWk19QRB6M5nl
> _A9wsLObEPqlXYSqUau7ZfBV&s=KLiZ62_Z8HSL_a9Mq0OR-PCG_h1JavRUfbKsPOc4IAo&e=
> " xmlns:xlink="http://www.w3.org/1999/xlink">
> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 506)">
> +<title>dpdk_app_graph_l3fwd_nodes_flow</title>
> +<polygon fill="white" stroke="transparent" points="-4,4 -4,-506 545.5,-506 545.5,4 -4,4"/>
> +<!-- ingress_port -->
> +<g id="node1" class="node">
> +<title>ingress_port</title>
> +<polygon fill="none" stroke="black" points="489.5,-502 383.5,-502 383.5,-466 489.5,-466
> 489.5,-502"/>
> +<text text-anchor="middle" x="436.5" y="-480.3" font-family="Times,serif" font-
> size="14.00">ingress_port</text>
> +</g>
> +<!-- ethdev_rx -->
> +<g id="node2" class="node">
> +<title>ethdev_rx</title>
> +<ellipse fill="none" stroke="black" cx="436.5" cy="-397" rx="56.59" ry="18"/>
> +<text text-anchor="middle" x="436.5" y="-393.3" font-family="Times,serif" font-
> size="14.00">ethdev_rx</text>
> +</g>
> +<!-- ingress_port&#45;&gt;ethdev_rx -->
> +<g id="edge1" class="edge">
> +<title>ingress_port&#45;&gt;ethdev_rx</title>
> +<path fill="none" stroke="black" d="M436.5,-465.8C436.5,-454.16 436.5,-438.55 436.5,-
> 425.24"/>
> +<polygon fill="black" stroke="black" points="440,-425.18 436.5,-415.18 433,-425.18 440,-
> 425.18"/>
> +<text text-anchor="middle" x="489" y="-436.8" font-family="Times,serif" font-
> size="14.00">ingress packet</text>
> +</g>
> +<!-- pkt_cls -->
> +<g id="node3" class="node">
> +<title>pkt_cls</title>
> +<ellipse fill="none" stroke="black" cx="436.5" cy="-324" rx="42.79" ry="18"/>
> +<text text-anchor="middle" x="436.5" y="-320.3" font-family="Times,serif" font-
> size="14.00">pkt_cls</text>
> +</g>
> +<!-- ethdev_rx&#45;&gt;pkt_cls -->
> +<g id="edge2" class="edge">
> +<title>ethdev_rx&#45;&gt;pkt_cls</title>
> +<path fill="none" stroke="black" d="M436.5,-378.81C436.5,-370.79 436.5,-361.05 436.5,-
> 352.07"/>
> +<polygon fill="black" stroke="black" points="440,-352.03 436.5,-342.03 433,-352.03 440,-
> 352.03"/>
> +</g>
> +<!-- ip4_lookup -->
> +<g id="node4" class="node">
> +<title>ip4_lookup</title>
> +<ellipse fill="none" stroke="black" cx="436.5" cy="-251" rx="60.39" ry="18"/>
> +<text text-anchor="middle" x="436.5" y="-247.3" font-family="Times,serif" font-
> size="14.00">ip4_lookup</text>
> +</g>
> +<!-- pkt_cls&#45;&gt;ip4_lookup -->
> +<g id="edge3" class="edge">
> +<title>pkt_cls&#45;&gt;ip4_lookup</title>
> +<path fill="none" stroke="green" d="M436.5,-305.81C436.5,-297.79 436.5,-288.05 436.5,-
> 279.07"/>
> +<polygon fill="green" stroke="green" points="440,-279.03 436.5,-269.03 433,-279.03 440,-
> 279.03"/>
> +</g>
> +<!-- ip6_lookup -->
> +<g id="node5" class="node">
> +<title>ip6_lookup</title>
> +<ellipse fill="none" stroke="black" cx="297.5" cy="-251" rx="60.39" ry="18"/>
> +<text text-anchor="middle" x="297.5" y="-247.3" font-family="Times,serif" font-
> size="14.00">ip6_lookup</text>
> +</g>
> +<!-- pkt_cls&#45;&gt;ip6_lookup -->
> +<g id="edge4" class="edge">
> +<title>pkt_cls&#45;&gt;ip6_lookup</title>
> +<path fill="none" stroke="blue" d="M410.36,-309.65C389.39,-298.94 359.66,-283.75
> 335.97,-271.65"/>
> +<polygon fill="blue" stroke="blue" points="337.39,-268.45 326.9,-267.02 334.21,-274.68
> 337.39,-268.45"/>
> +</g>
> +<!-- pkt_drop -->
> +<g id="node9" class="node">
> +<title>pkt_drop</title>
> +<ellipse fill="none" stroke="black" cx="361.5" cy="-18" rx="51.99" ry="18"/>
> +<text text-anchor="middle" x="361.5" y="-14.3" font-family="Times,serif" font-
> size="14.00">pkt_drop</text>
> +</g>
> +<!-- pkt_cls&#45;&gt;pkt_drop -->
> +<g id="edge5" class="edge">
> +<title>pkt_cls&#45;&gt;pkt_drop</title>
> +<path fill="none" stroke="red" stroke-dasharray="5,2" d="M468.77,-311.93C493.88,-
> 301.02 524.5,-281.64 524.5,-252 524.5,-252 524.5,-252 524.5,-104 524.5,-55.68 467.5,-34.79
> 420.91,-25.78"/>
> +<polygon fill="red" stroke="red" points="421.31,-22.29 410.85,-23.98 420.08,-29.18 421.31,-
> 22.29"/>
> +</g>
> +<!-- ip4_rewrite -->
> +<g id="node6" class="node">
> +<title>ip4_rewrite</title>
> +<ellipse fill="none" stroke="black" cx="394.5" cy="-178" rx="63.89" ry="18"/>
> +<text text-anchor="middle" x="394.5" y="-174.3" font-family="Times,serif" font-
> size="14.00">ip4_rewrite</text>
> +</g>
> +<!-- ip4_lookup&#45;&gt;ip4_rewrite -->
> +<g id="edge6" class="edge">
> +<title>ip4_lookup&#45;&gt;ip4_rewrite</title>
> +<path fill="none" stroke="green" d="M426.55,-233.17C421.55,-224.72 415.38,-214.29
> 409.79,-204.85"/>
> +<polygon fill="green" stroke="green" points="412.78,-203.02 404.67,-196.2 406.75,-206.59
> 412.78,-203.02"/>
> +</g>
> +<!-- ip4_lookup&#45;&gt;pkt_drop -->
> +<g id="edge7" class="edge">
> +<title>ip4_lookup&#45;&gt;pkt_drop</title>
> +<path fill="none" stroke="red" stroke-dasharray="5,2" d="M449.33,-233.03C456.19,-
> 222.87 463.94,-209.37 467.5,-196 471.62,-180.54 472.57,-175.18 467.5,-160 451.61,-112.41
> 412.64,-67.99 386.65,-42.17"/>
> +<polygon fill="red" stroke="red" points="388.97,-39.54 379.36,-35.08 384.09,-44.56 388.97,-
> 39.54"/>
> +</g>
> +<!-- ip6_rewrite -->
> +<g id="node7" class="node">
> +<title>ip6_rewrite</title>
> +<ellipse fill="none" stroke="black" cx="210.5" cy="-178" rx="63.89" ry="18"/>
> +<text text-anchor="middle" x="210.5" y="-174.3" font-family="Times,serif" font-
> size="14.00">ip6_rewrite</text>
> +</g>
> +<!-- ip6_lookup&#45;&gt;ip6_rewrite -->
> +<g id="edge8" class="edge">
> +<title>ip6_lookup&#45;&gt;ip6_rewrite</title>
> +<path fill="none" stroke="blue" d="M277.76,-233.89C266.16,-224.42 251.31,-212.31
> 238.52,-201.87"/>
> +<polygon fill="blue" stroke="blue" points="240.43,-198.9 230.46,-195.29 236,-204.33
> 240.43,-198.9"/>
> +</g>
> +<!-- ip6_lookup&#45;&gt;pkt_drop -->
> +<g id="edge9" class="edge">
> +<title>ip6_lookup&#45;&gt;pkt_drop</title>
> +<path fill="none" stroke="red" stroke-dasharray="5,2" d="M302.02,-232.72C306.79,-
> 214.59 314.55,-185.26 321.5,-160 332.39,-120.41 345.45,-74.7 353.61,-46.32"/>
> +<polygon fill="red" stroke="red" points="357.08,-46.92 356.49,-36.34 350.35,-44.98 357.08,-
> 46.92"/>
> +</g>
> +<!-- ethdev_tx -->
> +<g id="node8" class="node">
> +<title>ethdev_tx</title>
> +<ellipse fill="none" stroke="black" cx="249.5" cy="-105" rx="55.79" ry="18"/>
> +<text text-anchor="middle" x="249.5" y="-101.3" font-family="Times,serif" font-
> size="14.00">ethdev_tx</text>
> +</g>
> +<!-- ip4_rewrite&#45;&gt;ethdev_tx -->
> +<g id="edge10" class="edge">
> +<title>ip4_rewrite&#45;&gt;ethdev_tx</title>
> +<path fill="none" stroke="green" d="M364.1,-162.12C341.96,-151.27 311.81,-136.51
> 287.98,-124.84"/>
> +<polygon fill="green" stroke="green" points="289.39,-121.63 278.87,-120.38 286.31,-127.92
> 289.39,-121.63"/>
> +</g>
> +<!-- ip4_rewrite&#45;&gt;pkt_drop -->
> +<g id="edge11" class="edge">
> +<title>ip4_rewrite&#45;&gt;pkt_drop</title>
> +<path fill="none" stroke="red" stroke-dasharray="5,2" d="M390.91,-159.79C385.2,-132.48
> 374.03,-78.99 367.22,-46.38"/>
> +<polygon fill="red" stroke="red" points="370.56,-45.26 365.09,-36.19 363.71,-46.69 370.56,-
> 45.26"/>
> +</g>
> +<!-- ip6_rewrite&#45;&gt;ethdev_tx -->
> +<g id="edge12" class="edge">
> +<title>ip6_rewrite&#45;&gt;ethdev_tx</title>
> +<path fill="none" stroke="blue" d="M219.74,-160.17C224.34,-151.81 230,-141.51 235.14,-
> 132.14"/>
> +<polygon fill="blue" stroke="blue" points="238.31,-133.65 240.05,-123.2 232.17,-130.28
> 238.31,-133.65"/>
> +</g>
> +<!-- ip6_rewrite&#45;&gt;pkt_drop -->
> +<g id="edge13" class="edge">
> +<title>ip6_rewrite&#45;&gt;pkt_drop</title>
> +<path fill="none" stroke="red" stroke-dasharray="5,2" d="M197.68,-160.05C184.87,-
> 140.87 169.12,-109.39 184.5,-87 210.62,-48.98 261.18,-32.21 301.59,-24.82"/>
> +<polygon fill="red" stroke="red" points="302.35,-28.24 311.63,-23.13 301.19,-21.33 302.35,-
> 28.24"/>
> +</g>
> +<!-- ethdev_tx&#45;&gt;pkt_drop -->
> +<g id="edge15" class="edge">
> +<title>ethdev_tx&#45;&gt;pkt_drop</title>
> +<path fill="none" stroke="red" stroke-dasharray="5,2" d="M270.3,-88.21C287.91,-74.85
> 313.31,-55.57 332.84,-40.75"/>
> +<polygon fill="red" stroke="red" points="334.96,-43.54 340.81,-34.7 330.73,-37.96 334.96,-
> 43.54"/>
> +</g>
> +<!-- egress_port -->
> +<g id="node10" class="node">
> +<title>egress_port</title>
> +<polygon fill="none" stroke="black" points="101,-36 0,-36 0,0 101,0 101,-36"/>
> +<text text-anchor="middle" x="50.5" y="-14.3" font-family="Times,serif" font-
> size="14.00">egress_port</text>
> +</g>
> +<!-- ethdev_tx&#45;&gt;egress_port -->
> +<g id="edge14" class="edge">
> +<title>ethdev_tx&#45;&gt;egress_port</title>
> +<path fill="none" stroke="black" d="M217.08,-90.15C185.34,-76.59 136.54,-55.75 99.95,-
> 40.12"/>
> +<polygon fill="black" stroke="black" points="101.03,-36.78 90.45,-36.07 98.28,-43.21
> 101.03,-36.78"/>
> +<text text-anchor="middle" x="211.5" y="-57.8" font-family="Times,serif" font-
> size="14.00">egress packet</text>
> +</g>
> +</g>
> +</svg>
> diff --git a/doc/guides/tools/index.rst b/doc/guides/tools/index.rst
> index f2afb1fcc5..4f4dc8b518 100644
> --- a/doc/guides/tools/index.rst
> +++ b/doc/guides/tools/index.rst
> @@ -23,4 +23,5 @@ DPDK Tools User Guides
>      testeventdev
>      testregex
>      testmldev
> +    graph
>      dts
> --
> 2.25.1
  

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index 698608cdb2..7f149bd060 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1806,6 +1806,13 @@  F: dts/
 F: devtools/dts-check-format.sh
 F: doc/guides/tools/dts.rst
 
+Graph application
+M: Sunil Kumar Kori <skori@marvell.com>
+M: Rakesh Kudurumalla <rkudurumalla@marvell.com>
+F: app/graph/
+F: doc/guides/tools/graph.rst
+F: doc/guides/tools/img/graph-usecase-l3fwd.svg
+
 
 Other Example Applications
 --------------------------
diff --git a/app/graph/cli.c b/app/graph/cli.c
new file mode 100644
index 0000000000..237fa8008f
--- /dev/null
+++ b/app/graph/cli.c
@@ -0,0 +1,208 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <rte_common.h>
+#include <rte_ethdev.h>
+#include <rte_malloc.h>
+#include <rte_node_ip4_api.h>
+#include <rte_node_ip6_api.h>
+
+#include "cli_priv.h"
+#include "module_api.h"
+
+#define CMD_MAX_TOKENS 256
+#define MAX_LINE_SIZE 2048
+
+static struct cli_node_head module_list = STAILQ_HEAD_INITIALIZER(module_list);
+
+#define PARSE_DELIMITER " \f\n\r\t\v"
+
+static int
+tokenize_string_parse(char *string, char *tokens[], uint32_t *n_tokens)
+{
+	uint32_t i;
+
+	if ((string == NULL) ||
+		(tokens == NULL) ||
+		(*n_tokens < 1))
+		return -EINVAL;
+
+	for (i = 0; i < *n_tokens; i++) {
+		tokens[i] = strtok_r(string, PARSE_DELIMITER, &string);
+		if (tokens[i] == NULL)
+			break;
+	}
+
+	if ((i == *n_tokens) && strtok_r(string, PARSE_DELIMITER, &string))
+		return -E2BIG;
+
+	*n_tokens = i;
+	return 0;
+}
+
+static int
+is_comment(char *in)
+{
+	if ((strlen(in) && index("!#%;", in[0])) ||
+		(strncmp(in, "//", 2) == 0) ||
+		(strncmp(in, "--", 2) == 0))
+		return 1;
+
+	return 0;
+}
+
+static bool
+module_list_has_cmd_registered(const char *cmd)
+{
+	struct cli_node *node;
+
+	STAILQ_FOREACH(node, &module_list, next) {
+		if (strcmp(node->cmd, cmd) == 0) {
+			rte_errno = EEXIST;
+			return 1;
+		}
+	}
+	return 0;
+}
+
+void
+cli_module_register(const struct cli_module *module)
+{
+	struct cli_node *node;
+
+	/* Check sanity */
+	if (module == NULL || module->process == NULL) {
+		rte_errno = EINVAL;
+		return;
+	}
+
+	/* Check for duplicate name */
+	if (module_list_has_cmd_registered(module->cmd)) {
+		printf("module %s is already registered\n", module->cmd);
+		return;
+	}
+
+	node = malloc(sizeof(struct cli_node));
+	if (node == NULL) {
+		rte_errno = ENOMEM;
+		return;
+	}
+
+	/* Initialize the node */
+	if (rte_strscpy(node->cmd, module->cmd, APP_CLI_CMD_NAME_SIZE) < 0) {
+		free(node);
+		return;
+	}
+	node->process = module->process;
+	node->usage = module->usage;
+
+	/* Add the node at tail */
+	STAILQ_INSERT_TAIL(&module_list, node, next);
+}
+
+void
+cli_process(char *in, char *out, size_t out_size, void *obj)
+{
+	char *tokens[CMD_MAX_TOKENS];
+	struct cli_node *node;
+	uint32_t n_tokens;
+	int rc;
+
+	if (is_comment(in))
+		return;
+
+	n_tokens = RTE_DIM(tokens);
+	rc = tokenize_string_parse(in, tokens, &n_tokens);
+	if (rc) {
+		snprintf(out, out_size, MSG_ARG_TOO_MANY, "");
+		return;
+	}
+
+	if (n_tokens == 0)
+		return;
+
+	if ((n_tokens == 1) && strcmp(tokens[0], "help") == 0) {
+		STAILQ_FOREACH(node, &module_list, next) {
+			node->usage(tokens, n_tokens, out, out_size, obj);
+		}
+		return;
+	}
+
+	if ((n_tokens >= 2) && strcmp(tokens[0], "help") == 0) {
+		STAILQ_FOREACH(node, &module_list, next) {
+			if (strcmp(node->cmd, tokens[1]) == 0) {
+				node->usage(tokens, n_tokens, out, out_size, obj);
+				return;
+			}
+		}
+		snprintf(out, out_size, MSG_CMD_UNKNOWN, tokens[0]);
+		return;
+	}
+
+	STAILQ_FOREACH(node, &module_list, next) {
+		if (strcmp(node->cmd, tokens[0]) == 0) {
+			rc = node->process(tokens, n_tokens, out, out_size, obj);
+			if (rc < 0)
+				snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
+
+			return;
+		}
+	}
+
+	snprintf(out, out_size, MSG_CMD_UNKNOWN, tokens[0]);
+}
+
+int
+cli_script_process(const char *file_name, size_t msg_in_len_max, size_t msg_out_len_max, void *obj)
+{
+	char *msg_in = NULL, *msg_out = NULL;
+	FILE *f = NULL;
+
+	/* Check input arguments */
+	if ((file_name == NULL) || (strlen(file_name) == 0) || (msg_in_len_max == 0) ||
+	    (msg_out_len_max == 0))
+		return -EINVAL;
+
+	msg_in = malloc(msg_in_len_max + 1);
+	msg_out = malloc(msg_out_len_max + 1);
+	if ((msg_in == NULL) || (msg_out == NULL)) {
+		free(msg_out);
+		free(msg_in);
+		return -ENOMEM;
+	}
+
+	/* Open input file */
+	f = fopen(file_name, "r");
+	if (f == NULL) {
+		free(msg_out);
+		free(msg_in);
+		return -EIO;
+	}
+
+	/* Read file */
+	while (1) {
+		if (fgets(msg_in, msg_in_len_max + 1, f) == NULL)
+			break;
+
+		msg_out[0] = 0;
+
+		cli_process(msg_in, msg_out, msg_out_len_max, obj);
+
+		if (strlen(msg_out))
+			printf("%s", msg_out);
+	}
+
+	/* Close file */
+	fclose(f);
+	free(msg_out);
+	free(msg_in);
+	return 0;
+}
diff --git a/app/graph/cli.h b/app/graph/cli.h
new file mode 100644
index 0000000000..2bd89f3d1f
--- /dev/null
+++ b/app/graph/cli.h
@@ -0,0 +1,48 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_CLI_H
+#define APP_GRAPH_CLI_H
+
+/* Macros */
+#define MSG_OUT_OF_MEMORY   "Not enough memory.\n"
+#define MSG_CMD_UNKNOWN     "Unknown command \"%s\".\n"
+#define MSG_CMD_UNIMPLEM    "Command \"%s\" not implemented.\n"
+#define MSG_ARG_NOT_ENOUGH  "Not enough arguments for command \"%s\".\n"
+#define MSG_ARG_TOO_MANY    "Too many arguments for command \"%s\".\n"
+#define MSG_ARG_MISMATCH    "Wrong number of arguments for command \"%s\".\n"
+#define MSG_ARG_NOT_FOUND   "Argument \"%s\" not found.\n"
+#define MSG_ARG_INVALID     "Invalid value for argument \"%s\".\n"
+#define MSG_FILE_ERR        "Error in file \"%s\" at line %u.\n"
+#define MSG_FILE_NOT_ENOUGH "Not enough rules in file \"%s\".\n"
+#define MSG_CMD_FAIL        "Command \"%s\" failed.\n"
+
+#define APP_CLI_CMD_NAME_SIZE	64
+
+/* Typedefs */
+typedef int (*cli_module_t)(char **tokens, uint32_t n_tokens, char *out, size_t out_size,
+			     void *obj);
+
+/* Structures */
+struct cli_module {
+	char cmd[APP_CLI_CMD_NAME_SIZE]; /**< Name of the command to be registered. */
+	cli_module_t process; /**< Command process function. */
+	cli_module_t usage; /**< Help command process function. */
+};
+
+/* APIs */
+void cli_module_register(const struct cli_module *module);
+
+#define CLI_REGISTER(module)			\
+	RTE_INIT(cli_register_##module)		\
+	{					\
+		cli_module_register(&module);	\
+	}
+
+void cli_process(char *in, char *out, size_t out_size, void *arg);
+
+int cli_script_process(const char *file_name, size_t msg_in_len_max, size_t msg_out_len_max,
+		       void *arg);
+
+#endif
diff --git a/app/graph/cli_priv.h b/app/graph/cli_priv.h
new file mode 100644
index 0000000000..9ecc89c353
--- /dev/null
+++ b/app/graph/cli_priv.h
@@ -0,0 +1,19 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_CLI_PRIV_H
+#define APP_GRAPH_CLI_PRIV_H
+
+#include "cli.h"
+
+struct cli_node {
+	STAILQ_ENTRY(cli_node) next;	 /**< Next node in the list. */
+	char cmd[APP_CLI_CMD_NAME_SIZE]; /**< Name of the command. */
+	cli_module_t process;		 /**< Command process function. */
+	cli_module_t usage;		/**< Help command process function. */
+};
+
+STAILQ_HEAD(cli_node_head, cli_node);
+
+#endif
diff --git a/app/graph/conn.c b/app/graph/conn.c
new file mode 100644
index 0000000000..dabc8deca2
--- /dev/null
+++ b/app/graph/conn.c
@@ -0,0 +1,284 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "module_api.h"
+
+#define MSG_CMD_TOO_LONG "Command too long."
+
+static int
+data_event_handle(struct conn *conn, int fd_client)
+{
+	ssize_t len, i, rc = 0;
+
+	/* Read input message */
+	len = read(fd_client, conn->buf, conn->buf_size);
+	if (len == -1) {
+		if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
+			return 0;
+
+		return -1;
+	}
+
+	if (len == 0)
+		return rc;
+
+	/* Handle input messages */
+	for (i = 0; i < len; i++) {
+		if (conn->buf[i] == '\n') {
+			size_t n;
+
+			conn->msg_in[conn->msg_in_len] = 0;
+			conn->msg_out[0] = 0;
+
+			conn->msg_handle(conn->msg_in, conn->msg_out, conn->msg_out_len_max,
+					 conn->msg_handle_arg);
+
+			n = strlen(conn->msg_out);
+			if (n) {
+				rc = write(fd_client, conn->msg_out, n);
+				if (rc == -1)
+					goto exit;
+			}
+
+			conn->msg_in_len = 0;
+		} else if (conn->msg_in_len < conn->msg_in_len_max) {
+			conn->msg_in[conn->msg_in_len] = conn->buf[i];
+			conn->msg_in_len++;
+		} else {
+			rc = write(fd_client, MSG_CMD_TOO_LONG, strlen(MSG_CMD_TOO_LONG));
+			if (rc == -1)
+				goto exit;
+
+			conn->msg_in_len = 0;
+		}
+	}
+
+	/* Write prompt */
+	rc = write(fd_client, conn->prompt, strlen(conn->prompt));
+	rc = (rc == -1) ? -1 : 0;
+
+exit:
+	return rc;
+}
+
+static int
+control_event_handle(struct conn *conn, int fd_client)
+{
+	int rc;
+
+	rc = epoll_ctl(conn->fd_client_group, EPOLL_CTL_DEL, fd_client, NULL);
+	if (rc == -1)
+		goto exit;
+
+	rc = close(fd_client);
+	if (rc == -1)
+		goto exit;
+
+	rc = 0;
+
+exit:
+	return rc;
+}
+
+struct conn *
+conn_init(struct conn_params *p)
+{
+	int fd_server, fd_client_group, rc;
+	struct sockaddr_in server_address;
+	struct conn *conn = NULL;
+
+	memset(&server_address, 0, sizeof(server_address));
+
+	/* Check input arguments */
+	if ((p == NULL) || (p->welcome == NULL) || (p->prompt == NULL) || (p->addr == NULL) ||
+	    (p->buf_size == 0) || (p->msg_in_len_max == 0) || (p->msg_out_len_max == 0) ||
+	    (p->msg_handle == NULL))
+		goto exit;
+
+	rc = inet_aton(p->addr, &server_address.sin_addr);
+	if (rc == 0)
+		goto exit;
+
+	/* Memory allocation */
+	conn = calloc(1, sizeof(struct conn));
+	if (conn == NULL)
+		goto exit;
+
+	conn->welcome = calloc(1, CONN_WELCOME_LEN_MAX + 1);
+	conn->prompt = calloc(1, CONN_PROMPT_LEN_MAX + 1);
+	conn->buf = calloc(1, p->buf_size);
+	conn->msg_in = calloc(1, p->msg_in_len_max + 1);
+	conn->msg_out = calloc(1, p->msg_out_len_max + 1);
+
+	if ((conn->welcome == NULL) || (conn->prompt == NULL) || (conn->buf == NULL) ||
+	    (conn->msg_in == NULL) || (conn->msg_out == NULL)) {
+		conn_free(conn);
+		conn = NULL;
+		goto exit;
+	}
+
+	/* Server socket */
+	server_address.sin_family = AF_INET;
+	server_address.sin_port = htons(p->port);
+
+	fd_server = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
+	if (fd_server == -1) {
+		conn_free(conn);
+		conn = NULL;
+		goto exit;
+	}
+
+	rc = bind(fd_server, (struct sockaddr *)&server_address, sizeof(server_address));
+	if (rc == -1) {
+		conn_free(conn);
+		close(fd_server);
+		conn = NULL;
+		goto exit;
+	}
+
+	rc = listen(fd_server, 16);
+	if (rc == -1) {
+		conn_free(conn);
+		close(fd_server);
+		conn = NULL;
+		goto exit;
+	}
+
+	/* Client group */
+	fd_client_group = epoll_create(1);
+	if (fd_client_group == -1) {
+		conn_free(conn);
+		close(fd_server);
+		conn = NULL;
+		goto exit;
+	}
+
+	/* Fill in */
+	strncpy(conn->welcome, p->welcome, CONN_WELCOME_LEN_MAX);
+	strncpy(conn->prompt, p->prompt, CONN_PROMPT_LEN_MAX);
+	conn->buf_size = p->buf_size;
+	conn->msg_in_len_max = p->msg_in_len_max;
+	conn->msg_out_len_max = p->msg_out_len_max;
+	conn->msg_in_len = 0;
+	conn->fd_server = fd_server;
+	conn->fd_client_group = fd_client_group;
+	conn->msg_handle = p->msg_handle;
+	conn->msg_handle_arg = p->msg_handle_arg;
+
+exit:
+	return conn;
+}
+
+void
+conn_free(struct conn *conn)
+{
+	if (conn == NULL)
+		return;
+
+	if (conn->fd_client_group)
+		close(conn->fd_client_group);
+
+	if (conn->fd_server)
+		close(conn->fd_server);
+
+	free(conn->msg_out);
+	free(conn->msg_in);
+	free(conn->prompt);
+	free(conn->welcome);
+	free(conn);
+}
+
+int
+conn_req_poll(struct conn *conn)
+{
+	struct sockaddr_in client_address;
+	socklen_t client_address_length;
+	struct epoll_event event;
+	int fd_client, rc;
+
+	/* Check input arguments */
+	if (conn == NULL)
+		return -1;
+
+	/* Server socket */
+	client_address_length = sizeof(client_address);
+	fd_client = accept4(conn->fd_server, (struct sockaddr *)&client_address,
+			    &client_address_length, SOCK_NONBLOCK);
+	if (fd_client == -1) {
+		if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
+			return 0;
+
+		return -1;
+	}
+
+	/* Client group */
+	event.events = EPOLLIN | EPOLLRDHUP | EPOLLHUP;
+	event.data.fd = fd_client;
+
+	rc = epoll_ctl(conn->fd_client_group, EPOLL_CTL_ADD, fd_client, &event);
+	if (rc == -1) {
+		close(fd_client);
+		goto exit;
+	}
+
+	/* Client */
+	rc = write(fd_client, conn->welcome, strlen(conn->welcome));
+	if (rc == -1) {
+		close(fd_client);
+		goto exit;
+	}
+
+	rc = write(fd_client, conn->prompt, strlen(conn->prompt));
+	if (rc == -1) {
+		close(fd_client);
+		goto exit;
+	}
+
+	rc = 0;
+
+exit:
+	return rc;
+}
+
+int
+conn_msg_poll(struct conn *conn)
+{
+	int fd_client, rc, rc_data = 0, rc_control = 0;
+	struct epoll_event event;
+
+	/* Check input arguments */
+	if (conn == NULL)
+		return -1;
+
+	/* Client group */
+	rc = epoll_wait(conn->fd_client_group, &event, 1, 0);
+	if ((rc == -1) || rc == 0)
+		return rc;
+
+	fd_client = event.data.fd;
+
+	/* Data available */
+	if (event.events & EPOLLIN)
+		rc_data = data_event_handle(conn, fd_client);
+
+	/* Control events */
+	if (event.events & (EPOLLRDHUP | EPOLLERR | EPOLLHUP))
+		rc_control = control_event_handle(conn, fd_client);
+
+	if (rc_data || rc_control)
+		return -1;
+
+	return 0;
+}
diff --git a/app/graph/conn.h b/app/graph/conn.h
new file mode 100644
index 0000000000..770964cf4c
--- /dev/null
+++ b/app/graph/conn.h
@@ -0,0 +1,46 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_CONN_H
+#define APP_GRAPH_CONN_H
+
+#define CONN_WELCOME_LEN_MAX 1024
+#define CONN_PROMPT_LEN_MAX 16
+
+typedef void (*conn_msg_handle_t)(char *msg_in, char *msg_out, size_t msg_out_len_max, void *arg);
+
+struct conn {
+	char *welcome;
+	char *prompt;
+	char *buf;
+	char *msg_in;
+	char *msg_out;
+	size_t buf_size;
+	size_t msg_in_len_max;
+	size_t msg_out_len_max;
+	size_t msg_in_len;
+	int fd_server;
+	int fd_client_group;
+	conn_msg_handle_t msg_handle;
+	void *msg_handle_arg;
+};
+
+struct conn_params {
+	const char *welcome;
+	const char *prompt;
+	const char *addr;
+	uint16_t port;
+	size_t buf_size;
+	size_t msg_in_len_max;
+	size_t msg_out_len_max;
+	conn_msg_handle_t msg_handle;
+	void *msg_handle_arg;
+};
+
+struct conn *conn_init(struct conn_params *p);
+void conn_free(struct conn *conn);
+int conn_req_poll(struct conn *conn);
+int conn_msg_poll(struct conn *conn);
+
+#endif
diff --git a/app/graph/ethdev.c b/app/graph/ethdev.c
new file mode 100644
index 0000000000..840a8ca42f
--- /dev/null
+++ b/app/graph/ethdev.c
@@ -0,0 +1,632 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <rte_bitops.h>
+#include <rte_ethdev.h>
+#include <rte_mempool.h>
+
+#include "ethdev_priv.h"
+#include "module_api.h"
+
+static const char
+cmd_ethdev_mtu_help[] = "ethdev <ethdev_name> mtu <mtu_sz>";
+
+static const char
+cmd_ethdev_prom_mode_help[] = "ethdev <ethdev_name> promiscuous <on/off>";
+
+static const char
+cmd_ethdev_help[] = "ethdev <ethdev_name> rxq <n_queues> txq <n_queues> <mempool_name> "
+		    "[mtu <mtu_sz>]";
+static const char
+cmd_ethdev_show_help[] = "ethdev <ethdev_name> show";
+
+static const char
+cmd_ethdev_ip4_addr_help[] = "ethdev <ethdev_name> ip4 addr add <ip> netmask <mask>";
+
+static const char
+cmd_ethdev_ip6_addr_help[] = "ethdev <ethdev_name> ip6 addr add <ip> netmask <mask>";
+
+static struct rte_eth_conf port_conf_default = {
+	.link_speeds = 0,
+	.rxmode = {
+		.mq_mode = RTE_ETH_MQ_RX_NONE,
+		.mtu = 9000 - (RTE_ETHER_HDR_LEN + RTE_ETHER_CRC_LEN), /* Jumbo frame MTU */
+	},
+	.rx_adv_conf = {
+		.rss_conf = {
+			.rss_key = NULL,
+			.rss_key_len = 40,
+			.rss_hf = 0,
+		},
+	},
+	.txmode = {
+		.mq_mode = RTE_ETH_MQ_TX_NONE,
+	},
+	.lpbk_mode = 0,
+};
+
+uint32_t enabled_port_mask;
+struct ethdev port_list[RTE_MAX_ETHPORTS];
+
+void *
+ethdev_mempool_list_by_portid(uint16_t portid)
+{
+	if (portid >= RTE_MAX_ETHPORTS)
+		return NULL;
+
+	return &port_list[portid].config.rx.mp;
+}
+
+int16_t
+ethdev_portid_by_ip4(uint32_t ip)
+{
+	int portid = -EINVAL;
+	int i;
+
+	for (i = 0; i < RTE_MAX_ETHPORTS; i++) {
+		if ((port_list[i].ip4_addr.ip & route4[i].netmask) == (ip & route4[i].netmask))
+			break;
+	}
+
+	if (i == RTE_MAX_ETHPORTS)
+		return portid;
+
+	return port_list[i].config.port_id;
+}
+
+int16_t
+ethdev_portid_by_ip6(uint8_t *ip)
+{
+	int portid = -EINVAL;
+	int i, j;
+
+	for (i = 0; i < RTE_MAX_ETHPORTS; i++) {
+		for (j = 0; j < ETHDEV_IPV6_ADDR_LEN; j++) {
+			if ((port_list[i].ip6_addr.ip[j] & route6[i].mask[j]) !=
+			    (ip[j] & route6[i].mask[j]))
+				break;
+		}
+
+		if (j == ETHDEV_IPV6_ADDR_LEN)
+			break;
+	}
+
+	if (i == RTE_MAX_ETHPORTS)
+		return portid;
+
+	return port_list[i].config.port_id;
+}
+
+void
+ethdev_stop(void)
+{
+	uint16_t portid;
+	int rc;
+
+	RTE_ETH_FOREACH_DEV(portid) {
+		if ((enabled_port_mask & (1 << portid)) == 0)
+			continue;
+		printf("Closing port %d...", portid);
+		rc = rte_eth_dev_stop(portid);
+		if (rc != 0)
+			printf("Failed to stop port %u: %s\n",
+					portid, rte_strerror(-rc));
+		rte_eth_dev_close(portid);
+		printf(" Done\n");
+	}
+
+	/* clean up the EAL */
+	rte_eal_cleanup();
+	printf("Bye...\n");
+}
+
+void
+ethdev_start(void)
+{
+	uint16_t portid;
+	int rc;
+
+	RTE_ETH_FOREACH_DEV(portid)
+	{
+		if ((enabled_port_mask & (1 << portid)) == 0)
+			continue;
+
+		rc = rte_eth_dev_start(portid);
+		if (rc < 0)
+			rte_exit(EXIT_FAILURE, "rte_eth_dev_start: err=%d, port=%d\n", rc, portid);
+	}
+}
+
+
+static int
+ethdev_show(const char *name, char **out, size_t *out_size)
+{
+	uint16_t mtu = 0, port_id = 0;
+	struct rte_eth_dev_info info;
+	struct rte_eth_stats stats;
+	struct rte_ether_addr addr;
+	struct rte_eth_link link;
+	uint32_t length;
+	int rc;
+
+	rc = rte_eth_dev_get_port_by_name(name, &port_id);
+	if (rc < 0)
+		return rc;
+
+	rte_eth_dev_info_get(port_id, &info);
+	rte_eth_stats_get(port_id, &stats);
+	rte_eth_macaddr_get(port_id, &addr);
+	rte_eth_link_get(port_id, &link);
+	rte_eth_dev_get_mtu(port_id, &mtu);
+
+	snprintf(*out, *out_size,
+		 "%s: flags=<%s> mtu %u\n"
+		 "\tether " RTE_ETHER_ADDR_PRT_FMT " rxqueues %u txqueues %u\n"
+		 "\tport# %u  speed %s\n"
+		 "\tRX packets %" PRIu64"  bytes %" PRIu64"\n"
+		 "\tRX errors %" PRIu64"  missed %" PRIu64"  no-mbuf %" PRIu64"\n"
+		 "\tTX packets %" PRIu64"  bytes %" PRIu64"\n"
+		 "\tTX errors %" PRIu64"\n\n",
+		 name,
+		 link.link_status ? "UP" : "DOWN",
+		 mtu,
+		 RTE_ETHER_ADDR_BYTES(&addr),
+		 info.nb_rx_queues,
+		 info.nb_tx_queues,
+		 port_id,
+		 rte_eth_link_speed_to_str(link.link_speed),
+		 stats.ipackets,
+		 stats.ibytes,
+		 stats.ierrors,
+		 stats.imissed,
+		 stats.rx_nombuf,
+		 stats.opackets,
+		 stats.obytes,
+		 stats.oerrors);
+
+	length = strlen(*out);
+	*out_size -= length;
+	*out += length;
+	return 0;
+}
+
+static int
+ethdev_ip4_addr_add(const char *name, struct ipv4_addr_config *config)
+{
+	uint16_t portid = 0;
+	int rc;
+
+	rc = rte_eth_dev_get_port_by_name(name, &portid);
+	if (rc < 0)
+		return rc;
+
+	port_list[portid].ip4_addr.ip = config->ip;
+	port_list[portid].ip4_addr.mask = config->mask;
+	return 0;
+}
+
+static int
+ethdev_ip6_addr_add(const char *name, struct ipv6_addr_config *config)
+{
+	uint16_t portid = 0;
+	int rc, i;
+
+	rc = rte_eth_dev_get_port_by_name(name, &portid);
+	if (rc < 0)
+		return rc;
+
+	for (i = 0; i < ETHDEV_IPV6_ADDR_LEN; i++) {
+		port_list[portid].ip6_addr.ip[i] = config->ip[i];
+		port_list[portid].ip6_addr.mask[i] = config->mask[i];
+	}
+
+	return 0;
+}
+
+static int
+ethdev_prom_mode_config(const char *name, bool enable)
+{
+	uint16_t portid = 0;
+	int rc;
+
+	rc = rte_eth_dev_get_port_by_name(name, &portid);
+	if (rc < 0)
+		return rc;
+
+	if (enable)
+		rc = rte_eth_promiscuous_enable(portid);
+	else
+		rc = rte_eth_promiscuous_disable(portid);
+
+	if (rc < 0)
+		return rc;
+
+	port_list[portid].config.promiscuous = enable;
+	return 0;
+}
+
+static int
+ethdev_mtu_config(const char *name, uint32_t mtu)
+{
+	uint16_t portid = 0;
+	int rc;
+
+	rc = rte_eth_dev_get_port_by_name(name, &portid);
+	if (rc < 0)
+		return rc;
+
+	rc = rte_eth_dev_set_mtu(portid, mtu);
+	if (rc < 0)
+		return rc;
+
+	port_list[portid].config.mtu = mtu;
+	return 0;
+}
+
+static int
+ethdev_process(const char *name, struct ethdev_config *params)
+{
+	struct rte_eth_dev_info port_info;
+	struct rte_eth_conf port_conf;
+	struct ethdev_rss_config *rss;
+	struct rte_mempool *mempool;
+	struct rte_ether_addr smac;
+	int numa_node, rc;
+	uint16_t port_id = 0;
+	uint32_t i;
+
+	/* Check input params */
+	if (!name || !name[0] || !params || !params->rx.n_queues || !params->rx.queue_size ||
+	    !params->tx.n_queues || !params->tx.queue_size)
+		return -EINVAL;
+
+	rc = rte_eth_dev_get_port_by_name(name, &port_id);
+	if (rc)
+		return -EINVAL;
+
+	rc = rte_eth_dev_info_get(port_id, &port_info);
+	if (rc)
+		return -EINVAL;
+
+	mempool = rte_mempool_lookup(params->rx.mempool_name);
+	if (!mempool)
+		return -EINVAL;
+
+	params->rx.mp = mempool;
+
+	rss = params->rx.rss;
+	if (rss) {
+		if (!port_info.reta_size || port_info.reta_size > RTE_ETH_RSS_RETA_SIZE_512)
+			return -EINVAL;
+
+		if (!rss->n_queues || rss->n_queues >= ETHDEV_RXQ_RSS_MAX)
+			return -EINVAL;
+
+		for (i = 0; i < rss->n_queues; i++)
+			if (rss->queue_id[i] >= port_info.max_rx_queues)
+				return -EINVAL;
+	}
+
+	/* Port */
+	memcpy(&port_conf, &port_conf_default, sizeof(struct rte_eth_conf));
+	if (rss) {
+		uint64_t rss_hf = RTE_ETH_RSS_IP | RTE_ETH_RSS_TCP | RTE_ETH_RSS_UDP;
+
+		port_conf.rxmode.mq_mode = RTE_ETH_MQ_RX_RSS;
+		port_conf.rx_adv_conf.rss_conf.rss_hf = rss_hf & port_info.flow_type_rss_offloads;
+	}
+
+	numa_node = rte_eth_dev_socket_id(port_id);
+	if (numa_node == SOCKET_ID_ANY)
+		numa_node = 0;
+
+	if (params->mtu)
+		port_conf.rxmode.mtu = params->mtu;
+
+	rc = rte_eth_dev_configure(port_id, params->rx.n_queues, params->tx.n_queues,
+				       &port_conf);
+	if (rc < 0)
+		return -EINVAL;
+
+	rc = rte_eth_macaddr_get(port_id, &smac);
+	if (rc < 0)
+		return -EINVAL;
+
+	printf("Port_id = %d srcmac = %x:%x:%x:%x:%x:%x\n", port_id,
+		smac.addr_bytes[0], smac.addr_bytes[1],
+		smac.addr_bytes[2], smac.addr_bytes[3],
+		smac.addr_bytes[4], smac.addr_bytes[5]);
+
+	/* Port RX */
+	for (i = 0; i < params->rx.n_queues; i++) {
+		rc = rte_eth_rx_queue_setup(port_id, i, params->rx.queue_size, numa_node, NULL,
+			mempool);
+		if (rc < 0)
+			return -EINVAL;
+	}
+
+	/* Port TX */
+	for (i = 0; i < params->tx.n_queues; i++) {
+		rc = rte_eth_tx_queue_setup(port_id, i, params->tx.queue_size, numa_node, NULL);
+		if (rc < 0)
+			return -EINVAL;
+	}
+
+	memcpy(&port_list[port_id].config, params, sizeof(struct ethdev_config));
+	memcpy(port_list[port_id].config.dev_name, name, strlen(name));
+	port_list[port_id].config.port_id = port_id;
+	enabled_port_mask |= RTE_BIT32(port_id);
+	return 0;
+}
+
+static int
+cmd_ethdev_mtu(char **tokens, uint32_t n_tokens __rte_unused, char *out, size_t out_size,
+	       void *obj __rte_unused)
+{
+	int rc = -EINVAL;
+	uint32_t mtu = 0;
+
+	if (n_tokens != 4) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		return rc;
+	}
+
+	if (parser_uint32_read(&mtu, tokens[3]) != 0) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "mtu_sz");
+		return rc;
+	}
+
+	rc = ethdev_mtu_config(tokens[1], mtu);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
+
+	return rc;
+}
+
+static int
+cmd_ethdev_prom_mode(char **tokens, uint32_t n_tokens __rte_unused, char *out, size_t out_size,
+		     void *obj __rte_unused)
+{
+	bool enable = false;
+	int rc = -EINVAL;
+
+	if (n_tokens != 4) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		return rc;
+	}
+
+	if (strcmp(tokens[3], "on") == 0)
+		enable = true;
+
+	rc = ethdev_prom_mode_config(tokens[1], enable);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
+
+	return rc;
+}
+
+static int
+cmd_ip4_addr(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj __rte_unused)
+{
+	struct ipv4_addr_config config;
+	int rc = -EINVAL;
+
+	if (n_tokens != 8) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		goto exit;
+	}
+
+	if (strcmp(tokens[3], "addr")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "addr");
+		goto exit;
+	}
+
+	if (strcmp(tokens[4], "add")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "add");
+		goto exit;
+	}
+
+	if (parser_ip4_read(&config.ip, tokens[5])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "ip");
+		goto exit;
+	}
+
+	if (strcmp(tokens[6], "netmask")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "netmask");
+		goto exit;
+	}
+
+	if (parser_ip4_read(&config.mask, tokens[7])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "netmask");
+		goto exit;
+	}
+
+	rc = ethdev_ip4_addr_add(tokens[1], &config);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_ARG_INVALID, tokens[0]);
+
+exit:
+	return rc;
+}
+
+static int
+cmd_ip6_addr(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj __rte_unused)
+{
+	struct ipv6_addr_config config;
+	int rc = -EINVAL;
+
+	if (n_tokens != 8) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		goto exit;
+	}
+
+	if (strcmp(tokens[3], "addr")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "addr");
+		goto exit;
+	}
+
+	if (strcmp(tokens[4], "add")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "add");
+		goto exit;
+	}
+
+	if (parser_ip6_read(config.ip, tokens[5])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "ip");
+		goto exit;
+	}
+
+	if (strcmp(tokens[6], "netmask")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "netmask");
+		goto exit;
+	}
+
+	if (parser_ip6_read(config.mask, tokens[7])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "netmask");
+		goto exit;
+	}
+
+	rc = ethdev_ip6_addr_add(tokens[1], &config);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_ARG_INVALID, tokens[0]);
+
+exit:
+	return rc;
+}
+
+static int
+cmd_ethdev_show(char **tokens, uint32_t n_tokens, char *out, size_t out_size,
+	void *obj __rte_unused)
+{
+	int rc = -EINVAL;
+
+	if (n_tokens != 3) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		return rc;
+	}
+
+	rc = ethdev_show(tokens[1], &out, &out_size);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_ARG_INVALID, tokens[0]);
+
+	return rc;
+}
+
+static int
+cmd_ethdev(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj __rte_unused)
+{
+	struct ethdev_config config;
+	char *name;
+	int rc;
+
+	if (n_tokens < 7) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		return -EINVAL;
+	}
+
+	memset(&config, 0, sizeof(struct ethdev_config));
+	name = tokens[1];
+
+	if (strcmp(tokens[2], "rxq") != 0) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "rxq");
+		return -EINVAL;
+	}
+
+	if (parser_uint32_read(&config.rx.n_queues, tokens[3]) != 0) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "n_queues");
+		return -EINVAL;
+	}
+
+	if (strcmp(tokens[4], "txq") != 0) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "txq");
+		return -EINVAL;
+	}
+
+	if (parser_uint32_read(&config.tx.n_queues, tokens[5]) != 0) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "n_queues");
+		return -EINVAL;
+	}
+
+	mempcpy(config.rx.mempool_name, tokens[6], strlen(tokens[6]));
+
+	if (n_tokens > 7) {
+		if (strcmp(tokens[7], "mtu") != 0) {
+			snprintf(out, out_size, MSG_ARG_NOT_FOUND, "mtu");
+			return -EINVAL;
+		}
+
+		if (parser_uint32_read(&config.mtu, tokens[8]) != 0) {
+			snprintf(out, out_size, MSG_ARG_INVALID, "mtu_sz");
+			return -EINVAL;
+		}
+	}
+
+	config.tx.queue_size = ETHDEV_TX_DESC_DEFAULT;
+	config.rx.queue_size = ETHDEV_RX_DESC_DEFAULT;
+
+	rc = ethdev_process(name, &config);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
+
+	return rc;
+}
+
+static int
+cli_ethdev_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char *out,
+		   size_t out_size, void *obj __rte_unused)
+{
+	size_t len;
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "\n%s\n",
+		 "----------------------------- ethdev command help -----------------------------");
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_ethdev_help);
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_ethdev_ip4_addr_help);
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_ethdev_ip6_addr_help);
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_ethdev_prom_mode_help);
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_ethdev_mtu_help);
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_ethdev_show_help);
+
+	return 0;
+}
+
+static int
+cli_ethdev(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj)
+{
+	if (strcmp(tokens[2], "show") == 0)
+		return cmd_ethdev_show(tokens, n_tokens, out, out_size, obj);
+	else if (strcmp(tokens[2], "mtu") == 0)
+		return cmd_ethdev_mtu(tokens, n_tokens, out, out_size, obj);
+	else if (strcmp(tokens[2], "promiscuous") == 0)
+		return cmd_ethdev_prom_mode(tokens, n_tokens, out, out_size, obj);
+	else if (strcmp(tokens[2], "ip4") == 0)
+		return cmd_ip4_addr(tokens, n_tokens, out, out_size, obj);
+	else if (strcmp(tokens[2], "ip6") == 0)
+		return cmd_ip6_addr(tokens, n_tokens, out, out_size, obj);
+	else
+		return cmd_ethdev(tokens, n_tokens, out, out_size, obj);
+}
+
+static struct cli_module ethdev = {
+	.cmd = "ethdev",
+	.process = cli_ethdev,
+	.usage = cli_ethdev_help,
+};
+
+CLI_REGISTER(ethdev);
diff --git a/app/graph/ethdev.h b/app/graph/ethdev.h
new file mode 100644
index 0000000000..9c3de49826
--- /dev/null
+++ b/app/graph/ethdev.h
@@ -0,0 +1,28 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_ETHDEV_H
+#define APP_GRAPH_ETHDEV_H
+
+#define ETHDEV_IPV6_ADDR_LEN	16
+
+struct ipv4_addr_config {
+	uint32_t ip;
+	uint32_t mask;
+};
+
+struct ipv6_addr_config {
+	uint8_t ip[ETHDEV_IPV6_ADDR_LEN];
+	uint8_t mask[ETHDEV_IPV6_ADDR_LEN];
+};
+
+extern uint32_t enabled_port_mask;
+
+void ethdev_start(void);
+void ethdev_stop(void);
+void *ethdev_mempool_list_by_portid(uint16_t portid);
+int16_t ethdev_portid_by_ip4(uint32_t ip);
+int16_t ethdev_portid_by_ip6(uint8_t *ip);
+
+#endif
diff --git a/app/graph/ethdev_priv.h b/app/graph/ethdev_priv.h
new file mode 100644
index 0000000000..1026c2e5b6
--- /dev/null
+++ b/app/graph/ethdev_priv.h
@@ -0,0 +1,46 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_ETHDEV_PRIV_H
+#define APP_GRAPH_ETHDEV_PRIV_H
+
+#include "ethdev.h"
+
+#define ETHDEV_RXQ_RSS_MAX	16
+#define ETHDEV_RX_DESC_DEFAULT 1024
+#define ETHDEV_TX_DESC_DEFAULT 1024
+
+struct ethdev_rss_config {
+	uint32_t queue_id[ETHDEV_RXQ_RSS_MAX];
+	uint32_t n_queues;
+};
+
+struct ethdev_config {
+	char dev_name[RTE_ETH_NAME_MAX_LEN];
+	uint16_t port_id;
+
+	struct {
+		uint32_t n_queues;
+		uint32_t queue_size;
+		char mempool_name[RTE_MEMPOOL_NAMESIZE];
+		struct rte_mempool *mp;
+		struct ethdev_rss_config *rss;
+	} rx;
+
+	struct {
+		uint32_t n_queues;
+		uint32_t queue_size;
+	} tx;
+
+	int promiscuous;
+	uint32_t mtu;
+};
+
+struct ethdev {
+	struct ethdev_config config;
+	struct ipv4_addr_config ip4_addr;
+	struct ipv6_addr_config ip6_addr;
+};
+
+#endif
diff --git a/app/graph/ethdev_rx.c b/app/graph/ethdev_rx.c
new file mode 100644
index 0000000000..d706d145c1
--- /dev/null
+++ b/app/graph/ethdev_rx.c
@@ -0,0 +1,139 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <rte_ethdev.h>
+
+#include "ethdev_rx_priv.h"
+#include "module_api.h"
+
+static const char
+cmd_ethdev_rx_help[] = "ethdev_rx map port <ethdev_name> queue <q_num> core <core_id>";
+
+static struct lcore_params lcore_params_array[ETHDEV_RX_LCORE_PARAMS_MAX];
+struct rte_node_ethdev_config ethdev_conf[RTE_MAX_ETHPORTS];
+struct lcore_params *lcore_params = lcore_params_array;
+struct lcore_conf lcore_conf[RTE_MAX_LCORE];
+uint16_t nb_lcore_params;
+
+static void
+rx_map_configure(uint8_t port_id, uint32_t queue, uint32_t core)
+{
+	uint8_t n_rx_queue;
+
+	n_rx_queue = lcore_conf[core].n_rx_queue;
+	lcore_conf[core].rx_queue_list[n_rx_queue].port_id = port_id;
+	lcore_conf[core].rx_queue_list[n_rx_queue].queue_id = queue;
+	lcore_conf[core].n_rx_queue++;
+}
+
+uint8_t
+ethdev_rx_num_rx_queues_get(uint16_t port)
+{
+	int queue = -1;
+	uint16_t i;
+
+	for (i = 0; i < nb_lcore_params; ++i) {
+		if (lcore_params[i].port_id == port) {
+			if (lcore_params[i].queue_id == queue + 1)
+				queue = lcore_params[i].queue_id;
+			else
+				rte_exit(EXIT_FAILURE,
+					 "Queue ids of the port %d must be"
+					 " in sequence and must start with 0\n",
+					 lcore_params[i].port_id);
+		}
+	}
+
+	return (uint8_t)(++queue);
+}
+
+static int
+ethdev_rx_map_add(char *name, uint32_t queue, uint32_t core)
+{
+	uint16_t port_id;
+	int rc;
+
+	if (nb_lcore_params >= ETHDEV_RX_LCORE_PARAMS_MAX)
+		return -EINVAL;
+
+	rc = rte_eth_dev_get_port_by_name(name, &port_id);
+	if (rc)
+		return -EINVAL;
+
+	rx_map_configure(port_id, queue, core);
+
+	lcore_params_array[nb_lcore_params].port_id = port_id;
+	lcore_params_array[nb_lcore_params].queue_id = queue;
+	lcore_params_array[nb_lcore_params].lcore_id = core;
+	nb_lcore_params++;
+	return 0;
+}
+
+static int
+cli_ethdev_rx_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char *out,
+		   size_t out_size, void *obj __rte_unused)
+{
+	size_t len;
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "\n%s\n",
+		 "---------------------------- ethdev_rx command help ---------------------------");
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_ethdev_rx_help);
+	return 0;
+}
+
+static int
+cli_ethdev_rx(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj __rte_unused)
+{
+	char name[RTE_ETH_NAME_MAX_LEN];
+	uint32_t core_id, queue;
+	int rc = -EINVAL;
+
+	if (n_tokens != 8) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		goto exit;
+	}
+
+	strcpy(name, tokens[3]);
+
+	if (strcmp(tokens[4], "queue") != 0) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "rxq");
+		goto exit;
+	}
+
+	if (parser_uint32_read(&queue, tokens[5]) != 0) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "queue");
+		goto exit;
+	}
+
+	if (strcmp(tokens[6], "core") != 0) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "core_id");
+		goto exit;
+	}
+
+	if (parser_uint32_read(&core_id, tokens[7]) != 0) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "queue");
+		goto exit;
+	}
+
+	rc = ethdev_rx_map_add(name, queue, core_id);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
+
+exit:
+	return rc;
+}
+
+static struct cli_module ethdev_rx = {
+	.cmd = "ethdev_rx",
+	.process = cli_ethdev_rx,
+	.usage = cli_ethdev_rx_help,
+};
+
+CLI_REGISTER(ethdev_rx);
diff --git a/app/graph/ethdev_rx.h b/app/graph/ethdev_rx.h
new file mode 100644
index 0000000000..d2c18f545f
--- /dev/null
+++ b/app/graph/ethdev_rx.h
@@ -0,0 +1,32 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_ETHDEV_RX_H
+#define APP_GRAPH_ETHDEV_RX_H
+
+#define ETHDEV_RX_LCORE_PARAMS_MAX 1024
+#define ETHDEV_RX_QUEUE_PER_LCORE_MAX 16
+
+struct lcore_rx_queue {
+	uint16_t port_id;
+	uint8_t queue_id;
+	char node_name[RTE_NODE_NAMESIZE];
+};
+
+struct lcore_conf {
+	uint16_t n_rx_queue;
+	struct lcore_rx_queue rx_queue_list[ETHDEV_RX_QUEUE_PER_LCORE_MAX];
+	struct rte_graph *graph;
+	char name[RTE_GRAPH_NAMESIZE];
+	rte_graph_t graph_id;
+} __rte_cache_aligned;
+
+uint8_t ethdev_rx_num_rx_queues_get(uint16_t port);
+
+extern struct rte_node_ethdev_config ethdev_conf[RTE_MAX_ETHPORTS];
+extern struct lcore_conf lcore_conf[RTE_MAX_LCORE];
+extern struct lcore_params *lcore_params;
+extern uint16_t nb_lcore_params;
+
+#endif
diff --git a/app/graph/ethdev_rx_priv.h b/app/graph/ethdev_rx_priv.h
new file mode 100644
index 0000000000..d714f83739
--- /dev/null
+++ b/app/graph/ethdev_rx_priv.h
@@ -0,0 +1,23 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_ETHDEV_RX_PRIV_H
+#define APP_GRAPH_ETHDEV_RX_PRIV_H
+
+#include <stdint.h>
+
+#include <rte_graph.h>
+#include <rte_node_eth_api.h>
+
+#define MAX_RX_QUEUE_PER_PORT 128
+#define MAX_JUMBO_PKT_LEN  9600
+#define NB_SOCKETS 8
+
+struct lcore_params {
+	uint16_t port_id;
+	uint8_t queue_id;
+	uint8_t lcore_id;
+} __rte_cache_aligned;
+
+#endif
diff --git a/app/graph/examples/l3fwd.cli b/app/graph/examples/l3fwd.cli
new file mode 100644
index 0000000000..9986e1b73e
--- /dev/null
+++ b/app/graph/examples/l3fwd.cli
@@ -0,0 +1,87 @@ 
+; SPDX-License-Identifier: BSD-3-Clause
+; Copyright(c) 2023 Marvell.
+
+;
+; Graph configuration for given usecase
+;
+graph l3fwd coremask ff model default
+
+;
+; Mempools to be attached with ethdev
+;
+mempool mempool0 size 8192 buffers 4000 cache 256 numa 0
+
+;
+; DPDK devices and configuration.
+;
+; Note: Customize the parameters below to match your setup.
+;
+ethdev 0002:04:00.0 rxq 1 txq 8 mempool0 mtu 1500
+ethdev 0002:05:00.0 rxq 1 txq 8 mempool0 mtu 1600
+ethdev 0002:06:00.0 rxq 1 txq 8 mempool0 mtu 1500
+ethdev 0002:07:00.0 rxq 1 txq 8 mempool0 mtu 1600
+ethdev 0002:04:00.0 mtu 1700
+ethdev 0002:05:00.0 promiscuous on
+
+;
+; IPv4 addresses assigned to DPDK devices
+;
+ethdev 0002:04:00.0 ip4 addr add 10.0.2.1 netmask 255.255.255.0
+ethdev 0002:05:00.0 ip4 addr add 20.0.2.1 netmask 255.255.255.0
+ethdev 0002:06:00.0 ip4 addr add 30.0.2.1 netmask 255.255.255.0
+ethdev 0002:07:00.0 ip4 addr add 40.0.2.1 netmask 255.255.255.0
+
+;
+; IPv6 addresses assigned to DPDK devices
+;
+ethdev 0002:04:00.0 ip6 addr add 52:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4A netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00
+ethdev 0002:05:00.0 ip6 addr add 62:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4B netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00
+ethdev 0002:06:00.0 ip6 addr add 72:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4C netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00
+ethdev 0002:07:00.0 ip6 addr add 82:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4D netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00
+
+;
+; IPv4 routes which are installed to ipv4_lookup node for LPM processing
+;
+ipv4_lookup route add ipv4 10.0.2.0 netmask 255.255.255.0 via 10.0.2.1
+ipv4_lookup route add ipv4 20.0.2.0 netmask 255.255.255.0 via 20.0.2.1
+ipv4_lookup route add ipv4 30.0.2.0 netmask 255.255.255.0 via 30.0.2.1
+ipv4_lookup route add ipv4 40.0.2.0 netmask 255.255.255.0 via 40.0.2.1
+
+;
+; IPv6 routes which are installed to ipv6_lookup node for LPM processing
+;
+ipv6_lookup route add ipv6 52:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4A netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00 via 52:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4A
+ipv6_lookup route add ipv6 62:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4B netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00 via 62:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4B
+ipv6_lookup route add ipv6 72:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4C netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00 via 72:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4C
+ipv6_lookup route add ipv6 82:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4D netmask FF:FF:FF:FF:FF:FF:FF:FF:FF:00:00:00:00:00:00:00 via 82:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4D
+
+;
+; Peer MAC and IPv4 address mapping
+;
+neigh add ipv4 10.0.2.2 52:20:DA:4F:68:70
+neigh add ipv4 20.0.2.2 62:20:DA:4F:68:70
+neigh add ipv4 30.0.2.2 72:20:DA:4F:68:70
+neigh add ipv4 40.0.2.2 82:20:DA:4F:68:70
+
+;
+; Peer MAC and IPv6 address mapping
+;
+neigh add ipv6 52:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4A 52:20:DA:4F:68:70
+neigh add ipv6 62:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4B 62:20:DA:4F:68:70
+neigh add ipv6 72:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4C 72:20:DA:4F:68:70
+neigh add ipv6 82:20:DA:4F:68:70:52:20:DA:4F:68:70:52:20:DA:4D 82:20:DA:4F:68:70
+
+;
+; Port-Queue-Core mapping for ethdev_rx node
+;
+ethdev_rx map port 0002:04:00.0 queue 0 core 1
+ethdev_rx map port 0002:05:00.0 queue 0 core 2
+ethdev_rx map port 0002:06:00.0 queue 0 core 3
+ethdev_rx map port 0002:07:00.0 queue 0 core 4
+
+;
+; Graph start command to create graph.
+;
+; Note: No more command should come after this.
+;
+graph start
diff --git a/app/graph/graph.c b/app/graph/graph.c
new file mode 100644
index 0000000000..8c75574ecd
--- /dev/null
+++ b/app/graph/graph.c
@@ -0,0 +1,383 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <rte_graph_worker.h>
+#include <rte_log.h>
+
+#include "graph_priv.h"
+#include "module_api.h"
+
+#define RTE_LOGTYPE_APP_GRAPH RTE_LOGTYPE_USER1
+
+static const char
+cmd_graph_help[] = "graph <usecases> bsz <size> tmo <ns> coremask <bitmask> "
+		   "model <rtc | mcd | default>";
+
+static const char * const supported_usecases[] = {"l3fwd"};
+struct graph_config graph_config;
+
+/* Check the link rc of all ports in up to 9s, and print them finally */
+static void
+check_all_ports_link_status(uint32_t port_mask)
+{
+#define CHECK_INTERVAL 100 /* 100ms */
+#define MAX_CHECK_TIME 90  /* 9s (90 * 100ms) in total */
+	char link_rc_text[RTE_ETH_LINK_MAX_STR_LEN];
+	uint8_t count, all_ports_up, print_flag = 0;
+	struct rte_eth_link link;
+	uint16_t portid;
+	int rc;
+
+	printf("\nChecking link rc");
+	fflush(stdout);
+	for (count = 0; count <= MAX_CHECK_TIME; count++) {
+		if (force_quit)
+			return;
+
+		all_ports_up = 1;
+		RTE_ETH_FOREACH_DEV(portid)
+		{
+			if (force_quit)
+				return;
+
+			if ((port_mask & (1 << portid)) == 0)
+				continue;
+
+			memset(&link, 0, sizeof(link));
+			rc = rte_eth_link_get_nowait(portid, &link);
+			if (rc < 0) {
+				all_ports_up = 0;
+				if (print_flag == 1)
+					printf("Port %u link get failed: %s\n",
+					       portid, rte_strerror(-rc));
+				continue;
+			}
+
+			/* Print link rc if flag set */
+			if (print_flag == 1) {
+				rte_eth_link_to_str(link_rc_text, sizeof(link_rc_text),
+					&link);
+				printf("Port %d %s\n", portid, link_rc_text);
+				continue;
+			}
+
+			/* Clear all_ports_up flag if any link down */
+			if (link.link_status == RTE_ETH_LINK_DOWN) {
+				all_ports_up = 0;
+				break;
+			}
+		}
+
+		/* After finally printing all link rc, get out */
+		if (print_flag == 1)
+			break;
+
+		if (all_ports_up == 0) {
+			printf(".");
+			fflush(stdout);
+			rte_delay_ms(CHECK_INTERVAL);
+		}
+
+		/* Set the print_flag if all ports up or timeout */
+		if (all_ports_up == 1 || count == (MAX_CHECK_TIME - 1)) {
+			print_flag = 1;
+			printf("Done\n");
+		}
+	}
+}
+
+static bool
+parser_usecases_read(char *usecases)
+{
+	bool valid = false;
+	uint32_t i, j = 0;
+	char *token;
+
+	token = strtok(usecases, ",");
+	while (token != NULL) {
+		for (i = 0; i < RTE_DIM(supported_usecases); i++) {
+			if (strcmp(supported_usecases[i], token) == 0) {
+				graph_config.usecases[j].enabled = true;
+				strcpy(graph_config.usecases[j].name, token);
+				valid = true;
+				j++;
+				break;
+			}
+		}
+		token = strtok(NULL, ",");
+	}
+
+	return valid;
+}
+
+static uint64_t
+graph_worker_count_get(void)
+{
+	uint64_t nb_worker = 0;
+	uint64_t coremask;
+
+	coremask = graph_config.params.coremask;
+	while (coremask) {
+		if (coremask & 0x1)
+			nb_worker++;
+
+		coremask = (coremask >> 1);
+	}
+
+	return nb_worker;
+}
+
+static struct rte_node_ethdev_config *
+graph_rxtx_node_config_get(uint32_t *num_conf, uint32_t *num_graphs)
+{
+	uint32_t n_tx_queue, nb_conf = 0, lcore_id;
+	uint16_t queueid, portid, nb_graphs = 0;
+	uint8_t nb_rx_queue, queue;
+	struct lcore_conf *qconf;
+
+	n_tx_queue = graph_worker_count_get();
+	if (n_tx_queue > RTE_MAX_ETHPORTS)
+		n_tx_queue = RTE_MAX_ETHPORTS;
+
+	RTE_ETH_FOREACH_DEV(portid) {
+		/* Skip ports that are not enabled */
+		if ((enabled_port_mask & (1 << portid)) == 0) {
+			printf("\nSkipping disabled port %d\n", portid);
+			continue;
+		}
+
+		nb_rx_queue = ethdev_rx_num_rx_queues_get(portid);
+
+		/* Setup ethdev node config */
+		ethdev_conf[nb_conf].port_id = portid;
+		ethdev_conf[nb_conf].num_rx_queues = nb_rx_queue;
+		ethdev_conf[nb_conf].num_tx_queues = n_tx_queue;
+		ethdev_conf[nb_conf].mp = ethdev_mempool_list_by_portid(portid);
+		ethdev_conf[nb_conf].mp_count = 1; /* Check with pools */
+
+		nb_conf++;
+	}
+
+	for (lcore_id = 0; lcore_id < RTE_MAX_LCORE; lcore_id++) {
+		if (rte_lcore_is_enabled(lcore_id) == 0)
+			continue;
+
+		qconf = &lcore_conf[lcore_id];
+		printf("\nInitializing rx queues on lcore %u ... ", lcore_id);
+		fflush(stdout);
+
+		/* Init RX queues */
+		for (queue = 0; queue < qconf->n_rx_queue; ++queue) {
+			portid = qconf->rx_queue_list[queue].port_id;
+			queueid = qconf->rx_queue_list[queue].queue_id;
+
+			/* Add this queue node to its graph */
+			snprintf(qconf->rx_queue_list[queue].node_name, RTE_NODE_NAMESIZE,
+				 "ethdev_rx-%u-%u", portid, queueid);
+		}
+		if (qconf->n_rx_queue)
+			nb_graphs++;
+	}
+
+	printf("\n");
+
+	ethdev_start();
+	check_all_ports_link_status(enabled_port_mask);
+
+	*num_conf = nb_conf;
+	*num_graphs = nb_graphs;
+	return ethdev_conf;
+}
+
+static int
+graph_start(void)
+{
+	struct rte_node_ethdev_config *conf;
+	uint32_t nb_graphs = 0, nb_conf, i;
+
+	conf = graph_rxtx_node_config_get(&nb_conf, &nb_graphs);
+	for (i = 0; i < MAX_GRAPH_USECASES; i++) {
+		if (!strcmp(graph_config.usecases[i].name, "l3fwd")) {
+			if (graph_config.usecases[i].enabled) {
+				usecase_l3fwd_configure(conf, nb_conf, nb_graphs);
+				break;
+			}
+		}
+	}
+	return 0;
+}
+
+static int
+graph_config_add(char *usecases, struct graph_config *config)
+{
+	if (!parser_usecases_read(usecases))
+		return -EINVAL;
+
+	graph_config.params.bsz = config->params.bsz;
+	graph_config.params.tmo = config->params.tmo;
+	graph_config.params.coremask = config->params.coremask;
+	graph_config.model = config->model;
+
+	return 0;
+}
+
+int
+graph_walk_start(void *conf)
+{
+	struct lcore_conf *qconf;
+	struct rte_graph *graph;
+	uint32_t lcore_id;
+
+	RTE_SET_USED(conf);
+
+	lcore_id = rte_lcore_id();
+	qconf = &lcore_conf[lcore_id];
+	graph = qconf->graph;
+
+	if (!graph) {
+		RTE_LOG(INFO, APP_GRAPH, "Lcore %u has nothing to do\n", lcore_id);
+		return 0;
+	}
+
+	RTE_LOG(INFO, APP_GRAPH, "Entering main loop on lcore %u, graph %s(%p)\n", lcore_id,
+		qconf->name, graph);
+
+	while (likely(!force_quit))
+		rte_graph_walk(graph);
+
+	return 0;
+}
+
+void
+graph_stats_print(void)
+{
+	const char topLeft[] = {27, '[', '1', ';', '1', 'H', '\0'};
+	const char clr[] = {27, '[', '2', 'J', '\0'};
+	struct rte_graph_cluster_stats_param s_param;
+	struct rte_graph_cluster_stats *stats;
+	const char *pattern = "worker_*";
+
+	/* Prepare stats object */
+	memset(&s_param, 0, sizeof(s_param));
+	s_param.f = stdout;
+	s_param.socket_id = SOCKET_ID_ANY;
+	s_param.graph_patterns = &pattern;
+	s_param.nb_graph_patterns = 1;
+
+	stats = rte_graph_cluster_stats_create(&s_param);
+	if (stats == NULL)
+		rte_exit(EXIT_FAILURE, "Unable to create stats object\n");
+
+	while (!force_quit) {
+		/* Clear screen and move to top left */
+		printf("%s%s", clr, topLeft);
+		rte_graph_cluster_stats_get(stats, 0);
+		rte_delay_ms(1E3);
+	}
+
+	rte_graph_cluster_stats_destroy(stats);
+}
+
+static void
+graph_config_process(char **tokens, uint32_t n_tokens, char *out, size_t out_size,
+		     void *obj __rte_unused)
+{
+	uint32_t bsz = 32, tmo = 0, coremask = 0xf;
+	struct graph_config config;
+	int idx = 2, rc;
+	uint8_t model;
+
+	if (n_tokens < 4) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		return;
+	}
+
+next_arg:
+	if (strcmp(tokens[idx], "model")) {
+		if (strcmp(tokens[idx], "bsz") == 0) {
+			if (parser_uint32_read(&bsz, tokens[idx + 1])) {
+				snprintf(out, out_size, MSG_ARG_INVALID, "bsz");
+				return;
+			}
+
+		} else if (strcmp(tokens[idx], "tmo") == 0) {
+			if (parser_uint32_read(&tmo, tokens[idx + 1])) {
+				snprintf(out, out_size, MSG_ARG_INVALID, "tmo");
+				return;
+			}
+		} else if (strcmp(tokens[idx], "coremask") == 0) {
+			coremask = strtol(tokens[idx + 1], NULL, 16);
+			if (coremask == 0) {
+				snprintf(out, out_size, MSG_ARG_INVALID, "tmo");
+				return;
+			}
+		} else {
+			snprintf(out, out_size, MSG_ARG_NOT_FOUND, "usecases params");
+			return;
+		}
+
+		idx += 2;
+		goto next_arg;
+	} else {
+		if (strcmp(tokens[idx + 1], "default") == 0) {
+			model = GRAPH_MODEL_RTC;
+		} else if (strcmp(tokens[idx + 1], "rtc") == 0) {
+			model = GRAPH_MODEL_RTC;
+		} else if (strcmp(tokens[idx + 1], "mcd") == 0) {
+			model = GRAPH_MODEL_MCD;
+		} else {
+			snprintf(out, out_size, MSG_ARG_NOT_FOUND, "model arguments");
+			return;
+		}
+	}
+
+	config.params.bsz = bsz;
+	config.params.tmo = tmo;
+	config.params.coremask = coremask;
+	config.model = model;
+	rc = graph_config_add(tokens[1], &config);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
+}
+
+static int
+cli_graph_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char *out,
+	       size_t out_size, void *obj __rte_unused)
+{
+	size_t len;
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "\n%s\n",
+		 "----------------------------- graph command help -----------------------------");
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_graph_help);
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", "graph start");
+	return 0;
+}
+
+static int
+cli_graph(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj __rte_unused)
+{
+	if (strcmp(tokens[1], "start") == 0)
+		graph_start();
+	else
+		graph_config_process(tokens, n_tokens, out, out_size, obj);
+
+	return 0;
+}
+
+static struct cli_module graph = {
+	.cmd = "graph",
+	.process = cli_graph,
+	.usage = cli_graph_help,
+};
+
+CLI_REGISTER(graph);
diff --git a/app/graph/graph.h b/app/graph/graph.h
new file mode 100644
index 0000000000..126e967d75
--- /dev/null
+++ b/app/graph/graph.h
@@ -0,0 +1,11 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_H
+#define APP_GRAPH_H
+
+int graph_walk_start(void *conf);
+void graph_stats_print(void);
+
+#endif
diff --git a/app/graph/graph_priv.h b/app/graph/graph_priv.h
new file mode 100644
index 0000000000..655a028fb2
--- /dev/null
+++ b/app/graph/graph_priv.h
@@ -0,0 +1,32 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_PRIV_H
+#define APP_GRAPH_PRIV_H
+
+#define MAX_GRAPH_USECASES 32
+
+enum graph_model {
+	GRAPH_MODEL_RTC = 0x01,
+	GRAPH_MODEL_MCD = 0x02,
+};
+
+struct usecases {
+	char name[32];
+	bool enabled;
+};
+
+struct usecase_params {
+	uint64_t coremask;
+	uint32_t bsz;
+	uint32_t tmo;
+};
+
+struct graph_config {
+	struct usecases usecases[MAX_GRAPH_USECASES];
+	struct usecase_params params;
+	enum graph_model model;
+};
+
+#endif
diff --git a/app/graph/ip4_route.c b/app/graph/ip4_route.c
new file mode 100644
index 0000000000..5aba5b38f2
--- /dev/null
+++ b/app/graph/ip4_route.c
@@ -0,0 +1,146 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <rte_node_ip4_api.h>
+
+#include "module_api.h"
+
+static const char
+cmd_ipv4_lookup_help[] = "ipv4_lookup route add ipv4 <ip> netmask <mask> via <ip>";
+
+struct ipv4_route_config route4[MAX_ROUTE_ENTRIES];
+
+static uint8_t
+convert_netmask_to_depth(uint32_t netmask)
+{
+	uint8_t zerobits = 0;
+
+	while ((netmask & 0x1) == 0) {
+		netmask = netmask >> 1;
+		zerobits++;
+	}
+
+	return (32 - zerobits);
+}
+
+static int
+route_ip4_add(struct ipv4_route_config *route)
+{
+	int i;
+
+	for (i = 0; i < MAX_ROUTE_ENTRIES; i++) {
+		if (!route4[i].is_used)
+			break;
+	}
+
+	if (i == MAX_ROUTE_ENTRIES)
+		return -ENOMEM;
+
+	route4[i].ip = route->ip;
+	route4[i].netmask = route->netmask;
+	route4[i].via = route->via;
+	route4[i].is_used = true;
+	return 0;
+}
+
+int
+route_ip4_add_to_lookup(void)
+{
+	struct ipv4_route_config *route = NULL;
+	int rc = -EINVAL;
+	uint8_t depth;
+	int portid, i;
+
+	for (i = 0; i < MAX_ROUTE_ENTRIES; i++) {
+		if (route4[i].is_used)
+			route = &route4[i];
+
+		portid = ethdev_portid_by_ip4(route->via);
+		if (portid < 0) {
+			printf("Invalid portid found to install the route\n");
+			return rc;
+		}
+
+		depth = convert_netmask_to_depth(route->netmask);
+
+		rc = rte_node_ip4_route_add(route->ip, depth, portid,
+					     RTE_NODE_IP4_LOOKUP_NEXT_REWRITE);
+		if (rc < 0)
+			return rc;
+	}
+
+	return 0;
+}
+
+static int
+cli_ipv4_lookup_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char *out,
+		     size_t out_size, void *obj __rte_unused)
+{
+	size_t len;
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "\n%s\n",
+		 "--------------------------- ipv4_lookup command help --------------------------");
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_ipv4_lookup_help);
+	return 0;
+}
+
+static int
+cli_ipv4_lookup(char **tokens, uint32_t n_tokens, char *out, size_t out_size,
+		void *obj __rte_unused)
+{
+	struct ipv4_route_config config;
+	int rc = -EINVAL;
+
+	if (n_tokens != 9) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		goto exit;
+	}
+
+	if (parser_ip4_read(&config.ip, tokens[4])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "ipv4");
+		goto exit;
+	}
+
+	if (strcmp(tokens[5], "netmask")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "netmask");
+		goto exit;
+	}
+
+	if (parser_ip4_read(&config.netmask, tokens[6])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "netmask");
+		goto exit;
+	}
+
+	if (strcmp(tokens[7], "via")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "via");
+		goto exit;
+	}
+
+	if (parser_ip4_read(&config.via, tokens[8])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "via ip");
+		goto exit;
+	}
+
+	rc = route_ip4_add(&config);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
+
+exit:
+	return rc;
+}
+
+static struct cli_module ipv4_lookup = {
+	.cmd = "ipv4_lookup",
+	.process = cli_ipv4_lookup,
+	.usage = cli_ipv4_lookup_help,
+};
+
+CLI_REGISTER(ipv4_lookup);
diff --git a/app/graph/ip6_route.c b/app/graph/ip6_route.c
new file mode 100644
index 0000000000..2c5397f9d3
--- /dev/null
+++ b/app/graph/ip6_route.c
@@ -0,0 +1,154 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <rte_node_ip6_api.h>
+
+#include "module_api.h"
+
+static const char
+cmd_ipv6_lookup_help[] = "ipv6_lookup route add ipv6 <ip> netmask <mask> via <ip>";
+
+struct ipv6_route_config route6[MAX_ROUTE_ENTRIES];
+
+static uint8_t
+convert_ip6_netmask_to_depth(uint8_t *netmask)
+{
+	uint8_t setbits = 0;
+	uint8_t mask;
+	int i;
+
+	for (i = 0; i < ETHDEV_IPV6_ADDR_LEN; i++) {
+		mask = netmask[i];
+		while (mask & 0x80) {
+			mask = mask << 1;
+			setbits++;
+		}
+	}
+
+	return setbits;
+}
+
+static int
+route_ip6_add(struct ipv6_route_config *route)
+{
+	int i, j;
+
+	for (i = 0; i < RTE_MAX_ETHPORTS; i++) {
+		if (!route6[i].is_used)
+			break;
+	}
+
+	if (i == RTE_MAX_ETHPORTS)
+		return -ENOMEM;
+
+	for (j = 0; j < ETHDEV_IPV6_ADDR_LEN; j++) {
+		route6[i].ip[j] = route->ip[j];
+		route6[i].mask[j] = route->mask[j];
+		route6[i].gateway[j] = route->gateway[j];
+	}
+	route6[i].is_used = true;
+
+	return 0;
+}
+
+int
+route_ip6_add_to_lookup(void)
+{
+	struct ipv6_route_config *route = NULL;
+	int rc = -EINVAL;
+	uint8_t depth;
+	int portid, i;
+
+	for (i = 0; i < MAX_ROUTE_ENTRIES; i++) {
+		if (route6[i].is_used)
+			route = &route6[i];
+
+		portid = ethdev_portid_by_ip6(route->gateway);
+		if (portid < 0) {
+			printf("Invalid portid found to install the route\n");
+			return rc;
+		}
+
+		depth = convert_ip6_netmask_to_depth(route->mask);
+
+		rc = rte_node_ip6_route_add(route->ip, depth, portid,
+					     RTE_NODE_IP6_LOOKUP_NEXT_REWRITE);
+		if (rc < 0)
+			return rc;
+	}
+
+	return 0;
+}
+
+static int
+cli_ipv6_lookup_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char *out,
+		     size_t out_size, void *obj __rte_unused)
+{
+	size_t len;
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "\n%s\n",
+		 "--------------------------- ipv6_lookup command help --------------------------");
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_ipv6_lookup_help);
+	return 0;
+}
+
+static int
+cli_ipv6_lookup(char **tokens, uint32_t n_tokens, char *out, size_t out_size,
+		void *obj __rte_unused)
+{
+	struct ipv6_route_config config;
+	int rc = -EINVAL;
+
+	if (n_tokens != 9) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		goto exit;
+	}
+
+	if (parser_ip6_read(config.ip, tokens[4])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "ipv6");
+		goto exit;
+	}
+
+	if (strcmp(tokens[5], "netmask")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "netmask");
+		goto exit;
+	}
+
+	if (parser_ip6_read(config.mask, tokens[6])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "netmask");
+		goto exit;
+	}
+
+	if (strcmp(tokens[7], "via")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "via");
+		goto exit;
+	}
+
+	if (parser_ip6_read(config.gateway, tokens[8])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "gateway ip");
+		goto exit;
+	}
+
+	rc = route_ip6_add(&config);
+	if (rc)
+		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
+
+exit:
+	return rc;
+}
+
+static struct cli_module ipv6_lookup = {
+	.cmd = "ipv6_lookup",
+	.process = cli_ipv6_lookup,
+	.usage = cli_ipv6_lookup_help,
+};
+
+CLI_REGISTER(ipv6_lookup);
diff --git a/app/graph/l3fwd.c b/app/graph/l3fwd.c
new file mode 100644
index 0000000000..85b8b2618e
--- /dev/null
+++ b/app/graph/l3fwd.c
@@ -0,0 +1,152 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <rte_common.h>
+#include <rte_ethdev.h>
+#include <rte_graph.h>
+#include <rte_graph_worker.h>
+#include <rte_lcore.h>
+#include <rte_node_eth_api.h>
+
+#include "module_api.h"
+
+static char pcap_filename[RTE_GRAPH_PCAP_FILE_SZ];
+static uint64_t packet_to_capture;
+static int pcap_trace_enable;
+
+static int
+l3fwd_pattern_configure(void)
+{
+	/* Graph initialization. 8< */
+	static const char * const default_patterns[] = {
+		"ip4*",
+		"ethdev_tx-*",
+		"pkt_drop",
+	};
+
+	struct rte_graph_param graph_conf;
+	const char **node_patterns;
+	struct lcore_conf *qconf;
+	uint16_t nb_patterns;
+	uint8_t lcore_id;
+	int rc;
+
+	nb_patterns = RTE_DIM(default_patterns);
+	node_patterns = malloc((ETHDEV_RX_QUEUE_PER_LCORE_MAX + nb_patterns) *
+			sizeof(*node_patterns));
+	if (!node_patterns)
+		return -ENOMEM;
+	memcpy(node_patterns, default_patterns,
+			nb_patterns * sizeof(*node_patterns));
+
+	memset(&graph_conf, 0, sizeof(graph_conf));
+	graph_conf.node_patterns = node_patterns;
+
+	/* Pcap config */
+	graph_conf.pcap_enable = pcap_trace_enable;
+	graph_conf.num_pkt_to_capture = packet_to_capture;
+	graph_conf.pcap_filename = pcap_filename;
+
+	for (lcore_id = 0; lcore_id < RTE_MAX_LCORE; lcore_id++) {
+		rte_graph_t graph_id;
+		rte_edge_t i;
+
+		if (rte_lcore_is_enabled(lcore_id) == 0)
+			continue;
+
+		qconf = &lcore_conf[lcore_id];
+
+		/* Skip graph creation if no source exists */
+		if (!qconf->n_rx_queue)
+			continue;
+
+		/* Add rx node patterns of this lcore */
+		for (i = 0; i < qconf->n_rx_queue; i++) {
+			graph_conf.node_patterns[nb_patterns + i] =
+				qconf->rx_queue_list[i].node_name;
+		}
+
+		graph_conf.nb_node_patterns = nb_patterns + i;
+		graph_conf.socket_id = rte_lcore_to_socket_id(lcore_id);
+
+		snprintf(qconf->name, sizeof(qconf->name), "worker_%u",
+				lcore_id);
+
+		graph_id = rte_graph_create(qconf->name, &graph_conf);
+		if (graph_id == RTE_GRAPH_ID_INVALID)
+			rte_exit(EXIT_FAILURE,
+					"rte_graph_create(): graph_id invalid"
+					" for lcore %u\n", lcore_id);
+
+		qconf->graph_id = graph_id;
+		qconf->graph = rte_graph_lookup(qconf->name);
+		/* >8 End of graph initialization. */
+		if (!qconf->graph)
+			rte_exit(EXIT_FAILURE,
+					"rte_graph_lookup(): graph %s not found\n",
+					qconf->name);
+	}
+
+	rc = route_ip4_add_to_lookup();
+	if (rc < 0)
+		rte_exit(EXIT_FAILURE, "Unable to add v4 route to lookup table\n");
+
+	rc = route_ip6_add_to_lookup();
+	if (rc < 0)
+		rte_exit(EXIT_FAILURE, "Unable to add v6 route to lookup table\n");
+
+	rc = neigh_ip4_add_to_rewrite();
+	if (rc < 0)
+		rte_exit(EXIT_FAILURE, "Unable to add v4 to rewrite node\n");
+
+	rc = neigh_ip6_add_to_rewrite();
+	if (rc < 0)
+		rte_exit(EXIT_FAILURE, "Unable to add v6 to rewrite node\n");
+
+	/* Launch per-lcore init on every worker lcore */
+	rte_eal_mp_remote_launch(graph_walk_start, NULL, SKIP_MAIN);
+
+	/* Accumulate and print stats on main until exit */
+	if (rte_graph_has_stats_feature() && app_graph_stats_enabled())
+		graph_stats_print();
+
+	/* Wait for worker cores to exit */
+	rc = 0;
+	RTE_LCORE_FOREACH_WORKER(lcore_id) {
+		rc = rte_eal_wait_lcore(lcore_id);
+		/* Destroy graph */
+		if (rc < 0 ||
+		    rte_graph_destroy(rte_graph_from_name(lcore_conf[lcore_id].name))) {
+			rc = -1;
+			break;
+		}
+	}
+	free(node_patterns);
+
+	ethdev_stop();
+	return rc;
+}
+
+int
+usecase_l3fwd_configure(struct rte_node_ethdev_config *conf, uint16_t nb_confs, uint16_t nb_graphs)
+{
+	int rc;
+
+	rc = rte_node_eth_config(conf, nb_confs, nb_graphs);
+	if (rc)
+		rte_exit(EXIT_FAILURE, "rte_node_eth_config: err=%d\n", rc);
+
+	rc = l3fwd_pattern_configure();
+	if (rc)
+		rte_exit(EXIT_FAILURE, "l3fwd_pattern_failure: err=%d\n", rc);
+
+	return rc;
+}
diff --git a/app/graph/l3fwd.h b/app/graph/l3fwd.h
new file mode 100644
index 0000000000..e1d23165e6
--- /dev/null
+++ b/app/graph/l3fwd.h
@@ -0,0 +1,11 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_L3FWD_H
+#define APP_GRAPH_L3FWD_H
+
+int usecase_l3fwd_configure(struct rte_node_ethdev_config *conf, uint16_t nb_conf,
+			    uint16_t nb_graphs);
+
+#endif
diff --git a/app/graph/main.c b/app/graph/main.c
new file mode 100644
index 0000000000..e9934025bf
--- /dev/null
+++ b/app/graph/main.c
@@ -0,0 +1,201 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <fcntl.h>
+#include <getopt.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <rte_eal.h>
+#include <rte_launch.h>
+
+#include "module_api.h"
+
+volatile bool force_quit;
+
+static const char usage[] = "%s EAL_ARGS -- -s SCRIPT [-h HOST] [-p PORT] [--enable-graph-stats] "
+			    "[--help]\n";
+
+static struct app_params {
+	struct conn_params conn;
+	char *script_name;
+	bool enable_graph_stats;
+} app = {
+	.conn = {
+		.welcome = "\nWelcome!\n\n",
+		.prompt = "graph> ",
+		.addr = "0.0.0.0",
+		.port = 8086,
+		.buf_size = 1024 * 1024,
+		.msg_in_len_max = 1024,
+		.msg_out_len_max = 1024 * 1024,
+		.msg_handle = cli_process,
+		.msg_handle_arg = NULL, /* set later. */
+	},
+	.script_name = NULL,
+	.enable_graph_stats = false,
+};
+
+static void
+signal_handler(int signum)
+{
+	if (signum == SIGINT || signum == SIGTERM) {
+		printf("\n\nSignal %d received, preparing to exit...\n", signum);
+		force_quit = true;
+	}
+}
+
+static int
+app_args_parse(int argc, char **argv)
+{
+	struct option lgopts[] = {
+		{"help", 0, 0, 'H'},
+		{"enable-graph-stats", 0, 0, 'g'},
+	};
+	int h_present, p_present, s_present, n_args, i;
+	char *app_name = argv[0];
+	int opt, option_index;
+
+	/* Skip EAL input args */
+	n_args = argc;
+	for (i = 0; i < n_args; i++)
+		if (strcmp(argv[i], "--") == 0) {
+			argc -= i;
+			argv += i;
+			break;
+		}
+
+	if (i == n_args)
+		return 0;
+
+	/* Parse args */
+	h_present = 0;
+	p_present = 0;
+	s_present = 0;
+
+	while ((opt = getopt_long(argc, argv, "h:p:s:", lgopts, &option_index)) != EOF) {
+		switch (opt) {
+		case 'h':
+			if (h_present) {
+				printf("Error: Multiple -h arguments\n");
+				return -1;
+			}
+			h_present = 1;
+
+			if (!strlen(optarg)) {
+				printf("Error: Argument for -h not provided\n");
+				return -1;
+			}
+
+			app.conn.addr = strdup(optarg);
+			if (app.conn.addr == NULL) {
+				printf("Error: Not enough memory\n");
+				return -1;
+			}
+			break;
+
+		case 'p':
+			if (p_present) {
+				printf("Error: Multiple -p arguments\n");
+				return -1;
+			}
+			p_present = 1;
+
+			if (!strlen(optarg)) {
+				printf("Error: Argument for -p not provided\n");
+				return -1;
+			}
+
+			app.conn.port = (uint16_t) atoi(optarg);
+			break;
+
+		case 's':
+			if (s_present) {
+				printf("Error: Multiple -s arguments\n");
+				return -1;
+			}
+			s_present = 1;
+
+			if (!strlen(optarg)) {
+				printf("Error: Argument for -s not provided\n");
+				return -1;
+			}
+
+			app.script_name = strdup(optarg);
+			if (app.script_name == NULL) {
+				printf("Error: Not enough memory\n");
+				return -1;
+			}
+			break;
+
+		case 'g':
+			app.enable_graph_stats = true;
+			break;
+
+		case 'H':
+		default:
+			printf(usage, app_name);
+			return -1;
+		}
+	}
+	optind = 1; /* reset getopt lib */
+
+	return 0;
+}
+
+bool
+app_graph_stats_enabled(void)
+{
+	return app.enable_graph_stats;
+}
+
+int
+main(int argc, char **argv)
+{
+	struct conn *conn;
+	int rc;
+
+	/* Parse application arguments */
+	rc = app_args_parse(argc, argv);
+	if (rc < 0)
+		return rc;
+
+	/* EAL */
+	rc = rte_eal_init(argc, argv);
+	if (rc < 0) {
+		printf("Error: EAL initialization failed (%d)\n", rc);
+		return rc;
+	};
+
+	force_quit = false;
+	signal(SIGINT, signal_handler);
+	signal(SIGTERM, signal_handler);
+
+	/* Script */
+	if (app.script_name) {
+		cli_script_process(app.script_name, app.conn.msg_in_len_max,
+			app.conn.msg_out_len_max, NULL);
+	}
+
+	/* Connectivity */
+	app.conn.msg_handle_arg = NULL;
+	conn = conn_init(&app.conn);
+	if (!conn) {
+		printf("Error: Connectivity initialization failed (%d)\n", rc);
+		return rc;
+	};
+
+	/* Dispatch loop */
+	while (1) {
+		conn_req_poll(conn);
+
+		conn_msg_poll(conn);
+	}
+
+	/* clean up the EAL */
+	rte_eal_cleanup();
+}
diff --git a/app/graph/mempool.c b/app/graph/mempool.c
new file mode 100644
index 0000000000..1cee66abed
--- /dev/null
+++ b/app/graph/mempool.c
@@ -0,0 +1,134 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <rte_common.h>
+#include <rte_mbuf.h>
+
+#include "mempool_priv.h"
+#include "module_api.h"
+
+static const char
+cmd_mempool_help[] = "mempool <mempool_name> size <mbuf_size> buffers <number_of_buffers> "
+		     "cache <cache_size> numa <numa_id>";
+
+struct mempools mpconfig;
+
+int
+mempool_process(struct mempool_config *config)
+{
+	struct rte_mempool *mp;
+	uint8_t nb_pools;
+
+	nb_pools = mpconfig.nb_pools;
+	strcpy(mpconfig.config[nb_pools].name, config->name);
+	mpconfig.config[nb_pools].pool_size = config->pool_size;
+	mpconfig.config[nb_pools].buffer_size = config->buffer_size;
+	mpconfig.config[nb_pools].cache_size = config->cache_size;
+	mpconfig.config[nb_pools].numa_node = config->numa_node;
+
+	mp = rte_pktmbuf_pool_create(config->name, config->pool_size, config->cache_size,
+		64, config->buffer_size, config->numa_node);
+	if (!mp)
+		return -EINVAL;
+
+	mpconfig.mp[nb_pools] = mp;
+	nb_pools++;
+	mpconfig.nb_pools = nb_pools;
+
+	return 0;
+}
+
+static int
+cli_mempool_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char *out,
+		 size_t out_size, void *obj __rte_unused)
+{
+	size_t len;
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "\n%s\n",
+		 "---------------------------- mempool command help ----------------------------");
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_mempool_help);
+	return 0;
+}
+
+static int
+cli_mempool(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj __rte_unused)
+{
+	uint32_t pkt_buffer_size, pool_size, cache_size, numa_node;
+	struct mempool_config config;
+	int rc = -EINVAL;
+
+	if (n_tokens != 10) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		goto exit;
+	}
+
+	if (strcmp(tokens[2], "size")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "size");
+		goto exit;
+	}
+
+	if (parser_uint32_read(&pkt_buffer_size, tokens[3])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "mbuf_size");
+		goto exit;
+	}
+
+	if (strcmp(tokens[4], "buffers")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "buffers");
+		goto exit;
+	}
+
+	if (parser_uint32_read(&pool_size, tokens[5])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "number_of_buffers");
+		goto exit;
+	}
+
+	if (strcmp(tokens[6], "cache")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "cache");
+		goto exit;
+	}
+
+	if (parser_uint32_read(&cache_size, tokens[7])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "cache_size");
+		goto exit;
+	}
+
+	if (strcmp(tokens[8], "numa")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "numa");
+		goto exit;
+	}
+
+	if (parser_uint32_read(&numa_node, tokens[9])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "numa_id");
+		goto exit;
+	}
+
+	strcpy(config.name, tokens[1]);
+	config.name[strlen(tokens[1])] = '\0';
+	config.pool_size = pool_size;
+	config.buffer_size = pkt_buffer_size;
+	config.cache_size = cache_size;
+	config.numa_node = numa_node;
+
+	rc = mempool_process(&config);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
+
+exit:
+	return rc;
+}
+
+static struct cli_module mempool = {
+	.cmd = "mempool",
+	.process = cli_mempool,
+	.usage = cli_mempool_help,
+};
+
+CLI_REGISTER(mempool);
diff --git a/app/graph/mempool.h b/app/graph/mempool.h
new file mode 100644
index 0000000000..5fc788199d
--- /dev/null
+++ b/app/graph/mempool.h
@@ -0,0 +1,18 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_MEMPOOL_H
+#define APP_GRAPH_MEMPOOL_H
+
+struct mempool_config {
+	char name[RTE_MEMPOOL_NAMESIZE];
+	int pool_size;
+	int cache_size;
+	int buffer_size;
+	int numa_node;
+};
+
+int mempool_process(struct mempool_config *config);
+
+#endif
diff --git a/app/graph/mempool_priv.h b/app/graph/mempool_priv.h
new file mode 100644
index 0000000000..5a55722b32
--- /dev/null
+++ b/app/graph/mempool_priv.h
@@ -0,0 +1,16 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_MEMPOOL_PRIV_H
+#define APP_GRAPH_MEMPOOL_PRIV_H
+
+#include "mempool.h"
+
+struct mempools {
+	struct mempool_config config[RTE_MAX_ETHPORTS];
+	struct rte_mempool *mp[RTE_MAX_ETHPORTS];
+	uint8_t	nb_pools;
+};
+
+#endif
diff --git a/app/graph/meson.build b/app/graph/meson.build
new file mode 100644
index 0000000000..a3011e504b
--- /dev/null
+++ b/app/graph/meson.build
@@ -0,0 +1,25 @@ 
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2023 Marvell.
+
+# override default name to drop the hyphen
+name = 'graph'
+build = cc.has_header('sys/epoll.h')
+if not build
+    subdir_done()
+endif
+
+deps += ['bus_pci', 'graph', 'eal', 'lpm', 'ethdev', 'node']
+sources = files(
+        'cli.c',
+        'conn.c',
+        'ethdev_rx.c',
+        'ethdev.c',
+        'graph.c',
+        'ip4_route.c',
+        'ip6_route.c',
+        'main.c',
+        'mempool.c',
+        'neigh.c',
+        'l3fwd.c',
+        'utils.c',
+)
diff --git a/app/graph/module_api.h b/app/graph/module_api.h
new file mode 100644
index 0000000000..09b10bc672
--- /dev/null
+++ b/app/graph/module_api.h
@@ -0,0 +1,33 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_MODULE_API_H
+#define APP_GRAPH_MODULE_API_H
+
+#include <stdint.h>
+
+#include <rte_common.h>
+#include <rte_ethdev.h>
+#include <rte_graph.h>
+#include <rte_node_eth_api.h>
+
+#include "conn.h"
+#include "cli.h"
+#include "ethdev.h"
+#include "ethdev_rx.h"
+#include "graph.h"
+#include "l3fwd.h"
+#include "mempool.h"
+#include "neigh.h"
+#include "route.h"
+#include "utils.h"
+
+/*
+ * Externs
+ */
+extern volatile bool force_quit;
+
+bool app_graph_stats_enabled(void);
+
+#endif
diff --git a/app/graph/neigh.c b/app/graph/neigh.c
new file mode 100644
index 0000000000..07766758c9
--- /dev/null
+++ b/app/graph/neigh.c
@@ -0,0 +1,269 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <rte_node_ip4_api.h>
+#include <rte_node_ip6_api.h>
+
+#include "neigh_priv.h"
+#include "module_api.h"
+
+static const char
+cmd_neigh_v4_help[] = "neigh add ipv4 <ip> <mac>";
+
+static const char
+cmd_neigh_v6_help[] = "neigh add ipv6 <ip> <mac>";
+
+struct ipv4_neigh_config neigh4[MAX_NEIGH_ENTRIES];
+struct ipv6_neigh_config neigh6[MAX_NEIGH_ENTRIES];
+
+static int
+neigh_ip4_add(uint32_t ip, uint64_t mac)
+{
+	int i;
+
+	for (i = 0; i < MAX_NEIGH_ENTRIES; i++) {
+		if (!neigh4[i].is_used)
+			break;
+	}
+
+	if (i == MAX_NEIGH_ENTRIES)
+		return -ENOMEM;
+
+	neigh4[i].ip = ip;
+	neigh4[i].mac = mac;
+	neigh4[i].is_used = true;
+	return 0;
+}
+
+static int
+neigh_ip6_add(uint8_t *ip, uint64_t mac)
+{
+	int i, j;
+
+	for (i = 0; i < MAX_NEIGH_ENTRIES; i++) {
+		if (!neigh6[i].is_used)
+			break;
+	}
+
+	if (i == MAX_NEIGH_ENTRIES)
+		return -ENOMEM;
+
+	for (j = 0; j < ETHDEV_IPV6_ADDR_LEN; j++)
+		neigh6[i].ip[j] = ip[j];
+
+	neigh6[i].mac = mac;
+	neigh6[i].is_used = true;
+	return 0;
+}
+
+int
+neigh_ip4_add_to_rewrite(void)
+{
+	uint8_t data[2 * RTE_ETHER_ADDR_LEN];
+	uint8_t len = 2 * RTE_ETHER_ADDR_LEN;
+	struct rte_ether_addr smac = {0};
+	struct ipv4_neigh_config *neigh;
+	int16_t portid = 0;
+	int rc, i;
+
+	for (i = 0; i < MAX_NEIGH_ENTRIES; i++) {
+		if (!neigh4[i].is_used)
+			continue;
+
+		neigh = &neigh4[i];
+		portid = ethdev_portid_by_ip4(neigh->ip);
+		if (portid < 0) {
+			printf("Invalid portid found to add  neigh\n");
+			return -EINVAL;
+		}
+
+		memset(data, 0, len);
+
+		/* Copy dst mac */
+		rte_memcpy((void *)&data[0], (void *)&neigh->mac, RTE_ETHER_ADDR_LEN);
+
+		/* Copy src mac */
+		rc = rte_eth_macaddr_get(portid, &smac);
+		if (rc < 0) {
+			printf("Cannot get MAC address: err=%d, port=%d\n", rc, portid);
+			return rc;
+		}
+
+		rte_memcpy(&data[RTE_ETHER_ADDR_LEN], smac.addr_bytes, RTE_ETHER_ADDR_LEN);
+
+		rc = rte_node_ip4_rewrite_add(portid, data, len, portid);
+		if (rc < 0) {
+			printf("Error in writing rewrite data: err=%d, port=%d\n", rc, portid);
+			return rc;
+		}
+	}
+
+	return 0;
+}
+
+int
+neigh_ip6_add_to_rewrite(void)
+{
+	uint8_t data[2 * RTE_ETHER_ADDR_LEN];
+	uint8_t len = 2 * RTE_ETHER_ADDR_LEN;
+	struct rte_ether_addr smac = {0};
+	struct ipv6_neigh_config *neigh;
+	int16_t portid = 0;
+	int rc, i;
+
+	for (i = 0; i < MAX_NEIGH_ENTRIES; i++) {
+		if (!neigh6[i].is_used)
+			continue;
+
+		neigh = &neigh6[i];
+		portid = ethdev_portid_by_ip6(neigh->ip);
+		if (portid < 0) {
+			printf("Invalid portid found to add neigh\n");
+			return -EINVAL;
+		}
+
+		memset(data, 0, len);
+
+		/* Copy dst mac */
+		rte_memcpy((void *)&data[0], (void *)&neigh->mac, RTE_ETHER_ADDR_LEN);
+
+		/* Copy src mac */
+		rc = rte_eth_macaddr_get(portid, &smac);
+		if (rc < 0) {
+			printf("Cannot get MAC address: err=%d, port=%d\n",
+				rc, portid);
+			return rc;
+		}
+
+		rte_memcpy(&data[RTE_ETHER_ADDR_LEN], smac.addr_bytes, RTE_ETHER_ADDR_LEN);
+
+		rc = rte_node_ip6_rewrite_add(portid, data, len, portid);
+		if (rc < 0) {
+			printf("Error in writing rewrite data: err=%d, port=%d\n", rc, portid);
+			return rc;
+		}
+	}
+
+	return 0;
+}
+
+static int
+cmd_neigh_v4(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj __rte_unused)
+{
+	int rc = -EINVAL;
+	uint64_t mac;
+	uint32_t ip;
+
+	if (n_tokens != 5) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		goto exit;
+	}
+
+	if (strcmp(tokens[1], "add")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "add");
+		goto exit;
+	}
+
+	if (strcmp(tokens[2], "ipv4")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "ipv4");
+		goto exit;
+	}
+
+	if (parser_ip4_read(&ip, tokens[3])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "ip");
+		goto exit;
+	}
+
+	if (parser_mac_read(&mac, tokens[4])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "mac");
+		goto exit;
+	}
+
+	rc = neigh_ip4_add(ip, mac);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
+
+exit:
+	return rc;
+}
+
+static int
+cmd_neigh_v6(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj __rte_unused)
+{
+	uint8_t ip[ETHDEV_IPV6_ADDR_LEN];
+	int rc = -EINVAL;
+	uint64_t mac;
+
+	if (n_tokens != 5) {
+		snprintf(out, out_size, MSG_ARG_MISMATCH, tokens[0]);
+		goto exit;
+	}
+
+	if (strcmp(tokens[1], "add")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "add");
+		goto exit;
+	}
+
+	if (strcmp(tokens[2], "ipv6")) {
+		snprintf(out, out_size, MSG_ARG_NOT_FOUND, "ipv6");
+		goto exit;
+	}
+
+	if (parser_ip6_read(ip, tokens[3])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "ip");
+		goto exit;
+	}
+
+	if (parser_mac_read(&mac, tokens[4])) {
+		snprintf(out, out_size, MSG_ARG_INVALID, "mac");
+		goto exit;
+	}
+
+	rc = neigh_ip6_add(ip, mac);
+	if (rc < 0)
+		snprintf(out, out_size, MSG_CMD_FAIL, tokens[0]);
+
+exit:
+	return rc;
+}
+
+static int
+cli_neigh_help(char **tokens __rte_unused, uint32_t n_tokens __rte_unused, char *out,
+		     size_t out_size, void *obj __rte_unused)
+{
+	size_t len;
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "\n%s\n",
+		 "----------------------------- neigh command help -----------------------------");
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_neigh_v4_help);
+
+	len = strlen(out);
+	snprintf(out + len, out_size, "%s\n", cmd_neigh_v6_help);
+	return 0;
+}
+
+static int
+cli_neigh(char **tokens, uint32_t n_tokens, char *out, size_t out_size, void *obj)
+{
+	if (strcmp(tokens[2], "ipv4") == 0)
+		return cmd_neigh_v4(tokens, n_tokens, out, out_size, obj);
+	else
+		return cmd_neigh_v6(tokens, n_tokens, out, out_size, obj);
+}
+
+static struct cli_module neigh = {
+	.cmd = "neigh",
+	.process = cli_neigh,
+	.usage = cli_neigh_help,
+};
+
+CLI_REGISTER(neigh);
diff --git a/app/graph/neigh.h b/app/graph/neigh.h
new file mode 100644
index 0000000000..3964c37bb0
--- /dev/null
+++ b/app/graph/neigh.h
@@ -0,0 +1,11 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_NEIGH_H
+#define APP_GRAPH_NEIGH_H
+
+int neigh_ip4_add_to_rewrite(void);
+int neigh_ip6_add_to_rewrite(void);
+
+#endif
diff --git a/app/graph/neigh_priv.h b/app/graph/neigh_priv.h
new file mode 100644
index 0000000000..745dc7d671
--- /dev/null
+++ b/app/graph/neigh_priv.h
@@ -0,0 +1,22 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_NEIGH_PRIV_H
+#define APP_GRAPH_NEIGH_PRIV_H
+
+#define MAX_NEIGH_ENTRIES 32
+
+struct ipv4_neigh_config {
+	uint32_t ip;
+	uint64_t mac;
+	bool is_used;
+};
+
+struct ipv6_neigh_config {
+	uint8_t ip[16];
+	uint64_t mac;
+	bool is_used;
+};
+
+#endif
diff --git a/app/graph/route.h b/app/graph/route.h
new file mode 100644
index 0000000000..6b4acf3344
--- /dev/null
+++ b/app/graph/route.h
@@ -0,0 +1,30 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_ROUTE_H
+#define APP_GRAPH_ROUTE_H
+
+#define MAX_ROUTE_ENTRIES 32
+
+struct ipv4_route_config {
+	uint32_t ip;
+	uint32_t netmask;
+	uint32_t via;
+	bool is_used;
+};
+
+struct ipv6_route_config {
+	uint8_t ip[16];
+	uint8_t mask[16];
+	uint8_t gateway[16];
+	bool is_used;
+};
+
+extern struct ipv4_route_config route4[MAX_ROUTE_ENTRIES];
+extern struct ipv6_route_config route6[MAX_ROUTE_ENTRIES];
+
+int route_ip4_add_to_lookup(void);
+int route_ip6_add_to_lookup(void);
+
+#endif
diff --git a/app/graph/utils.c b/app/graph/utils.c
new file mode 100644
index 0000000000..48a83e738c
--- /dev/null
+++ b/app/graph/utils.c
@@ -0,0 +1,155 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <rte_common.h>
+
+#include "module_api.h"
+
+#define white_spaces_skip(pos)			\
+({						\
+	__typeof__(pos) _p = (pos);		\
+	for ( ; isspace(*_p); _p++)		\
+		;				\
+	_p;					\
+})
+
+static void
+hex_string_to_uint64(uint64_t *dst, const char *hexs)
+{
+	char buf[2] = {0};
+	uint8_t shift = 4;
+	int iter = 0;
+	char c;
+
+	while ((c = *hexs++)) {
+		buf[0] = c;
+		*dst |= (strtol(buf, NULL, 16) << shift);
+		shift -= 4;
+		iter++;
+		if (iter == 2) {
+			iter = 0;
+			shift = 4;
+			dst++;
+		}
+	}
+}
+
+int
+parser_uint64_read(uint64_t *value, const char *p)
+{
+	char *next;
+	uint64_t val;
+
+	p = white_spaces_skip(p);
+	if (!isdigit(*p))
+		return -EINVAL;
+
+	val = strtoul(p, &next, 0);
+	if (p == next)
+		return -EINVAL;
+
+	p = next;
+	switch (*p) {
+	case 'T':
+		val *= 1024ULL;
+		/* fall through */
+	case 'G':
+		val *= 1024ULL;
+		/* fall through */
+	case 'M':
+		val *= 1024ULL;
+		/* fall through */
+	case 'k':
+	case 'K':
+		val *= 1024ULL;
+		p++;
+		break;
+	}
+
+	p = white_spaces_skip(p);
+	if (*p != '\0')
+		return -EINVAL;
+
+	*value = val;
+	return 0;
+}
+
+int
+parser_uint32_read(uint32_t *value, const char *p)
+{
+	uint64_t val = 0;
+	int rc = parser_uint64_read(&val, p);
+
+	if (rc < 0)
+		return rc;
+
+	if (val > UINT32_MAX)
+		return -ERANGE;
+
+	*value = val;
+	return 0;
+}
+
+int
+parser_ip4_read(uint32_t *value, char *p)
+{
+	uint8_t shift = 24;
+	uint32_t ip = 0;
+	char *token;
+
+	token = strtok(p, ".");
+	while (token != NULL) {
+		ip |= (atoi(token) << shift);
+		token = strtok(NULL, ".");
+		shift -= 8;
+	}
+
+	*value = ip;
+
+	return 0;
+}
+
+int
+parser_ip6_read(uint8_t *value, char *p)
+{
+	uint64_t val = 0;
+	char *token;
+
+	token = strtok(p, ":");
+	while (token != NULL) {
+		hex_string_to_uint64(&val, token);
+		*value = val;
+		token = strtok(NULL, ":");
+		value++;
+		val = 0;
+	}
+
+	return 0;
+}
+
+int
+parser_mac_read(uint64_t *value, char *p)
+{
+	uint64_t mac = 0, val = 0;
+	uint8_t shift = 40;
+	char *token;
+
+	token = strtok(p, ":");
+	while (token != NULL) {
+		hex_string_to_uint64(&val, token);
+		mac |= val << shift;
+		token = strtok(NULL, ":");
+		shift -= 8;
+		val = 0;
+	}
+
+	*value = mac;
+
+	return 0;
+}
diff --git a/app/graph/utils.h b/app/graph/utils.h
new file mode 100644
index 0000000000..0ebb5de55a
--- /dev/null
+++ b/app/graph/utils.h
@@ -0,0 +1,14 @@ 
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2023 Marvell.
+ */
+
+#ifndef APP_GRAPH_UTILS_H
+#define APP_GRAPH_UTILS_H
+
+int parser_uint64_read(uint64_t *value, const char *p);
+int parser_uint32_read(uint32_t *value, const char *p);
+int parser_ip4_read(uint32_t *value, char *p);
+int parser_ip6_read(uint8_t *value, char *p);
+int parser_mac_read(uint64_t *value, char *p);
+
+#endif
diff --git a/app/meson.build b/app/meson.build
index e4bf5c531c..728c936383 100644
--- a/app/meson.build
+++ b/app/meson.build
@@ -17,6 +17,7 @@  endif
 apps = [
         'dumpcap',
         'pdump',
+        'graph',
         'proc-info',
         'test-acl',
         'test-bbdev',
diff --git a/doc/guides/tools/graph.rst b/doc/guides/tools/graph.rst
new file mode 100644
index 0000000000..c90dc9ad7f
--- /dev/null
+++ b/doc/guides/tools/graph.rst
@@ -0,0 +1,171 @@ 
+..  SPDX-License-Identifier: BSD-3-Clause
+    Copyright(c) 2023 Marvell.
+
+dpdk-graph Application
+======================
+
+The ``dpdk-graph`` tool is a Data Plane Development Kit (DPDK)
+application that allows exercising various graph use cases.
+This application has a generic framework to add new graph based use cases to
+verify functionality. Each use case is defined as a ``<usecase>.cli`` file.
+Based on the input file, application creates a graph to cater the use case.
+
+Supported Use cases
+-------------------
+ * l3fwd
+
+Running the Application
+-----------------------
+
+The application has a number of command line options which can be provided in
+following syntax
+
+.. code-block:: console
+
+   dpdk-graph [EAL Options] -- [application options]
+
+EAL Options
+~~~~~~~~~~~
+
+Following are the EAL command-line options that can be used in conjunction
+with the ``dpdk-graph`` application.
+See the DPDK Getting Started Guides for more information on these options.
+
+*   ``-c <COREMASK>`` or ``-l <CORELIST>``
+
+        Set the hexadecimal bit mask of the cores to run on. The CORELIST is a
+        list of cores to be used.
+
+Application Options
+~~~~~~~~~~~~~~~~~~~
+
+Following are the application command-line options:
+
+* ``-h``
+
+        Set the host IPv4 address over which telnet session can be opened.
+        It is an optional parameter. Default host address is 0.0.0.0.
+
+* ``-p``
+
+        Set the L4 port number over which telnet session can be opened.
+	It is an optional parameter. Default port is 8086.
+
+* ``-s``
+
+        Script name with absolute path which specifies the use case. It is
+        a mandatory parameter which will be used to create desired graph
+        for a given use case.
+
+* ``--enable-graph-stats``
+
+       Enable graph statistics printing on console. By default graph statistics are disabled.
+
+* ``--help``
+
+       Dumps application usage
+
+Supported CLI commands
+----------------------
+
+This section provides details on commands which can be used in ``<usecase>.cli``
+file to express the requested use case configuration.
+
+.. list-table:: Exposed CLIs
+   :header-rows: 1
+   :widths: auto
+
+   * - Command
+     - Description
+     - Dynamic
+     - Optional
+   * - graph <usecases> [bsz <size>] [tmo <ns>] [coremask <bitmask>] model <rtc | mcd | default>
+     - Command to express the desired use case
+     - No
+     - No
+   * - graph start
+     - Command to start the graph.
+       This command triggers that no more commands are left to be parsed and graph
+       initialization can be started now. It must be the last command in ``<usecase>.cli``
+     - No
+     - No
+   * - mempool <mempool_name> size <mbuf_size> buffers <number_of_buffers> cache <cache_size> numa <numa_id>
+     - Command to create mempool which will be further associated to RxQ to dequeue the packets
+     - No
+     - No
+   * - ethdev <ethdev_name> rxq <n_queues> txq <n_queues> <mempool_name> [mtu <mtu_sz>]
+     - Command to create DPDK port with given number of Rx and Tx queues. Also attached
+       RxQ with given mempool. Each port can have single mempool only i.e. all RxQs will
+       share the same mempool.
+     - No
+     - No
+   * - ethdev <ethdev_name> mtu <mtu_sz>
+     - Command to configure MTU of DPDK port
+     - Yes
+     - Yes
+   * - ethdev <ethdev_name> promiscuous <on/off>
+     - Command to enable/disable promiscuous mode on DPDK port
+     - Yes
+     - Yes
+   * - ethdev <ethdev_name> show
+     - Command to dump current ethdev configuration
+     - Yes
+     - Yes
+   * - ethdev <ethdev_name> ip4 addr add <ip> netmask <mask>
+     - Command to configure IPv4 address on given PCI device. It is needed if user
+       wishes to use ``ipv4_lookup`` node
+     - No
+     - Yes
+   * - ethdev <ethdev_name> ip6 addr add <ip> netmask <mask>
+     - Command to configure IPv6 address on given PCI device. It is needed if user
+       wishes to use ``ipv6_lookup`` node
+     - No
+     - Yes
+   * - ipv4_lookup route add ipv4 <ip> netmask <mask> via <ip>
+     - Command to add a route into ``ipv4_lookup`` LPM table. It is needed if user
+       wishes to route the packets based on LPM lookup table.
+     - No
+     - Yes
+   * - ipv6_lookup route add ipv6 <ip> netmask <mask> via <ip>
+     - Command to add a route into ``ipv6_lookup`` LPM table. It is needed if user
+       wishes to route the packets based on LPM6 lookup table.
+     - No
+     - Yes
+   * - neigh add ipv4 <ip> <mac>
+     - Command to add a neighbour information into ``ipv4_rewrite`` node.
+     - No
+     - Yes
+   * - neigh add ipv6 <ip> <mac>
+     - Command to add a neighbour information into ``ipv6_rewrite`` node.
+     - No
+     - Yes
+   * - ethdev_rx map port <ethdev_name> queue <q_num> core <core_id>
+     - Command to add port-queue-core mapping to ``ethdev_rx`` node. ``ethdev_rx``
+       node instance will be pinned on given core and will poll on requested
+       port/queue pair.
+     - No
+     - No
+
+Runtime configuration
+---------------------
+
+Application allows some configuration to be modified at runtime using a telnet session.
+Application initiates a telnet server with host address 0.0.0.0 and port number 8086 if
+``-h`` and ``-p`` is not given otherwise user provided IPv4 address and port number will
+be used.
+
+After successful launch of application, client can connect to using host/port address and
+console will be accessed with prompt ``graph>``.
+
+Created graph for use case
+--------------------------
+
+On the successful execution of ``<usecase>.cli`` file, corresponding graph will be created.
+This section mentions the created graph for each use case.
+
+l3fwd
+~~~~~
+
+.. _figure_l3fwd_graph:
+
+.. figure:: img/graph-usecase-l3fwd.*
diff --git a/doc/guides/tools/img/graph-usecase-l3fwd.svg b/doc/guides/tools/img/graph-usecase-l3fwd.svg
new file mode 100644
index 0000000000..3b991c4cf0
--- /dev/null
+++ b/doc/guides/tools/img/graph-usecase-l3fwd.svg
@@ -0,0 +1,210 @@ 
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.43.0 (0)
+ -->
+<!-- SPDX-License-Identifier: BSD-3-Clause -->
+<!-- Copyright(C) 2023 Marvell. -->
+<!--
+
+Generated with following command
+dot -Tsvg dot.dot -o doc/guides/tools/img/graph-usecase-l3fwd.svg
+
+cat dot.dot
+digraph dpdk_app_graph_l3fwd_nodes_flow {
+    ingress_port [shape=rect]
+    ethdev_rx
+    pkt_cls
+    ip4_lookup
+    ip6_lookup
+    ip4_rewrite
+    ip6_rewrite
+    ethdev_tx
+    pkt_drop
+    egress_port  [shape=rect]
+
+    ingress_port -> ethdev_rx [label="ingress packet"]
+
+    ethdev_rx -> pkt_cls
+
+    pkt_cls -> ip4_lookup [color="green"]
+    pkt_cls -> ip6_lookup [color="blue"]
+    pkt_cls -> pkt_drop   [color="red" style="dashed"]
+
+    ip4_lookup -> ip4_rewrite [color="green"]
+    ip4_lookup -> pkt_drop [color="red" style="dashed"]
+
+    ip6_lookup -> ip6_rewrite [color="blue"]
+    ip6_lookup -> pkt_drop [color="red" style="dashed"]
+
+    ip4_rewrite -> ethdev_tx [color="green"]
+    ip4_rewrite -> pkt_drop  [color="red" style="dashed"]
+
+    ip6_rewrite -> ethdev_tx [color="blue"]
+    ip6_rewrite -> pkt_drop  [color="red" style="dashed"]
+
+    ethdev_tx -> egress_port [label="egress packet"]
+    ethdev_tx -> pkt_drop [color="red" style="dashed"]
+}
+
+ -->
+<!-- Title: dpdk_app_graph_l3fwd_nodes_flow Pages: 1 -->
+<svg width="550pt" height="510pt"
+ viewBox="0.00 0.00 549.50 510.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 506)">
+<title>dpdk_app_graph_l3fwd_nodes_flow</title>
+<polygon fill="white" stroke="transparent" points="-4,4 -4,-506 545.5,-506 545.5,4 -4,4"/>
+<!-- ingress_port -->
+<g id="node1" class="node">
+<title>ingress_port</title>
+<polygon fill="none" stroke="black" points="489.5,-502 383.5,-502 383.5,-466 489.5,-466 489.5,-502"/>
+<text text-anchor="middle" x="436.5" y="-480.3" font-family="Times,serif" font-size="14.00">ingress_port</text>
+</g>
+<!-- ethdev_rx -->
+<g id="node2" class="node">
+<title>ethdev_rx</title>
+<ellipse fill="none" stroke="black" cx="436.5" cy="-397" rx="56.59" ry="18"/>
+<text text-anchor="middle" x="436.5" y="-393.3" font-family="Times,serif" font-size="14.00">ethdev_rx</text>
+</g>
+<!-- ingress_port&#45;&gt;ethdev_rx -->
+<g id="edge1" class="edge">
+<title>ingress_port&#45;&gt;ethdev_rx</title>
+<path fill="none" stroke="black" d="M436.5,-465.8C436.5,-454.16 436.5,-438.55 436.5,-425.24"/>
+<polygon fill="black" stroke="black" points="440,-425.18 436.5,-415.18 433,-425.18 440,-425.18"/>
+<text text-anchor="middle" x="489" y="-436.8" font-family="Times,serif" font-size="14.00">ingress packet</text>
+</g>
+<!-- pkt_cls -->
+<g id="node3" class="node">
+<title>pkt_cls</title>
+<ellipse fill="none" stroke="black" cx="436.5" cy="-324" rx="42.79" ry="18"/>
+<text text-anchor="middle" x="436.5" y="-320.3" font-family="Times,serif" font-size="14.00">pkt_cls</text>
+</g>
+<!-- ethdev_rx&#45;&gt;pkt_cls -->
+<g id="edge2" class="edge">
+<title>ethdev_rx&#45;&gt;pkt_cls</title>
+<path fill="none" stroke="black" d="M436.5,-378.81C436.5,-370.79 436.5,-361.05 436.5,-352.07"/>
+<polygon fill="black" stroke="black" points="440,-352.03 436.5,-342.03 433,-352.03 440,-352.03"/>
+</g>
+<!-- ip4_lookup -->
+<g id="node4" class="node">
+<title>ip4_lookup</title>
+<ellipse fill="none" stroke="black" cx="436.5" cy="-251" rx="60.39" ry="18"/>
+<text text-anchor="middle" x="436.5" y="-247.3" font-family="Times,serif" font-size="14.00">ip4_lookup</text>
+</g>
+<!-- pkt_cls&#45;&gt;ip4_lookup -->
+<g id="edge3" class="edge">
+<title>pkt_cls&#45;&gt;ip4_lookup</title>
+<path fill="none" stroke="green" d="M436.5,-305.81C436.5,-297.79 436.5,-288.05 436.5,-279.07"/>
+<polygon fill="green" stroke="green" points="440,-279.03 436.5,-269.03 433,-279.03 440,-279.03"/>
+</g>
+<!-- ip6_lookup -->
+<g id="node5" class="node">
+<title>ip6_lookup</title>
+<ellipse fill="none" stroke="black" cx="297.5" cy="-251" rx="60.39" ry="18"/>
+<text text-anchor="middle" x="297.5" y="-247.3" font-family="Times,serif" font-size="14.00">ip6_lookup</text>
+</g>
+<!-- pkt_cls&#45;&gt;ip6_lookup -->
+<g id="edge4" class="edge">
+<title>pkt_cls&#45;&gt;ip6_lookup</title>
+<path fill="none" stroke="blue" d="M410.36,-309.65C389.39,-298.94 359.66,-283.75 335.97,-271.65"/>
+<polygon fill="blue" stroke="blue" points="337.39,-268.45 326.9,-267.02 334.21,-274.68 337.39,-268.45"/>
+</g>
+<!-- pkt_drop -->
+<g id="node9" class="node">
+<title>pkt_drop</title>
+<ellipse fill="none" stroke="black" cx="361.5" cy="-18" rx="51.99" ry="18"/>
+<text text-anchor="middle" x="361.5" y="-14.3" font-family="Times,serif" font-size="14.00">pkt_drop</text>
+</g>
+<!-- pkt_cls&#45;&gt;pkt_drop -->
+<g id="edge5" class="edge">
+<title>pkt_cls&#45;&gt;pkt_drop</title>
+<path fill="none" stroke="red" stroke-dasharray="5,2" d="M468.77,-311.93C493.88,-301.02 524.5,-281.64 524.5,-252 524.5,-252 524.5,-252 524.5,-104 524.5,-55.68 467.5,-34.79 420.91,-25.78"/>
+<polygon fill="red" stroke="red" points="421.31,-22.29 410.85,-23.98 420.08,-29.18 421.31,-22.29"/>
+</g>
+<!-- ip4_rewrite -->
+<g id="node6" class="node">
+<title>ip4_rewrite</title>
+<ellipse fill="none" stroke="black" cx="394.5" cy="-178" rx="63.89" ry="18"/>
+<text text-anchor="middle" x="394.5" y="-174.3" font-family="Times,serif" font-size="14.00">ip4_rewrite</text>
+</g>
+<!-- ip4_lookup&#45;&gt;ip4_rewrite -->
+<g id="edge6" class="edge">
+<title>ip4_lookup&#45;&gt;ip4_rewrite</title>
+<path fill="none" stroke="green" d="M426.55,-233.17C421.55,-224.72 415.38,-214.29 409.79,-204.85"/>
+<polygon fill="green" stroke="green" points="412.78,-203.02 404.67,-196.2 406.75,-206.59 412.78,-203.02"/>
+</g>
+<!-- ip4_lookup&#45;&gt;pkt_drop -->
+<g id="edge7" class="edge">
+<title>ip4_lookup&#45;&gt;pkt_drop</title>
+<path fill="none" stroke="red" stroke-dasharray="5,2" d="M449.33,-233.03C456.19,-222.87 463.94,-209.37 467.5,-196 471.62,-180.54 472.57,-175.18 467.5,-160 451.61,-112.41 412.64,-67.99 386.65,-42.17"/>
+<polygon fill="red" stroke="red" points="388.97,-39.54 379.36,-35.08 384.09,-44.56 388.97,-39.54"/>
+</g>
+<!-- ip6_rewrite -->
+<g id="node7" class="node">
+<title>ip6_rewrite</title>
+<ellipse fill="none" stroke="black" cx="210.5" cy="-178" rx="63.89" ry="18"/>
+<text text-anchor="middle" x="210.5" y="-174.3" font-family="Times,serif" font-size="14.00">ip6_rewrite</text>
+</g>
+<!-- ip6_lookup&#45;&gt;ip6_rewrite -->
+<g id="edge8" class="edge">
+<title>ip6_lookup&#45;&gt;ip6_rewrite</title>
+<path fill="none" stroke="blue" d="M277.76,-233.89C266.16,-224.42 251.31,-212.31 238.52,-201.87"/>
+<polygon fill="blue" stroke="blue" points="240.43,-198.9 230.46,-195.29 236,-204.33 240.43,-198.9"/>
+</g>
+<!-- ip6_lookup&#45;&gt;pkt_drop -->
+<g id="edge9" class="edge">
+<title>ip6_lookup&#45;&gt;pkt_drop</title>
+<path fill="none" stroke="red" stroke-dasharray="5,2" d="M302.02,-232.72C306.79,-214.59 314.55,-185.26 321.5,-160 332.39,-120.41 345.45,-74.7 353.61,-46.32"/>
+<polygon fill="red" stroke="red" points="357.08,-46.92 356.49,-36.34 350.35,-44.98 357.08,-46.92"/>
+</g>
+<!-- ethdev_tx -->
+<g id="node8" class="node">
+<title>ethdev_tx</title>
+<ellipse fill="none" stroke="black" cx="249.5" cy="-105" rx="55.79" ry="18"/>
+<text text-anchor="middle" x="249.5" y="-101.3" font-family="Times,serif" font-size="14.00">ethdev_tx</text>
+</g>
+<!-- ip4_rewrite&#45;&gt;ethdev_tx -->
+<g id="edge10" class="edge">
+<title>ip4_rewrite&#45;&gt;ethdev_tx</title>
+<path fill="none" stroke="green" d="M364.1,-162.12C341.96,-151.27 311.81,-136.51 287.98,-124.84"/>
+<polygon fill="green" stroke="green" points="289.39,-121.63 278.87,-120.38 286.31,-127.92 289.39,-121.63"/>
+</g>
+<!-- ip4_rewrite&#45;&gt;pkt_drop -->
+<g id="edge11" class="edge">
+<title>ip4_rewrite&#45;&gt;pkt_drop</title>
+<path fill="none" stroke="red" stroke-dasharray="5,2" d="M390.91,-159.79C385.2,-132.48 374.03,-78.99 367.22,-46.38"/>
+<polygon fill="red" stroke="red" points="370.56,-45.26 365.09,-36.19 363.71,-46.69 370.56,-45.26"/>
+</g>
+<!-- ip6_rewrite&#45;&gt;ethdev_tx -->
+<g id="edge12" class="edge">
+<title>ip6_rewrite&#45;&gt;ethdev_tx</title>
+<path fill="none" stroke="blue" d="M219.74,-160.17C224.34,-151.81 230,-141.51 235.14,-132.14"/>
+<polygon fill="blue" stroke="blue" points="238.31,-133.65 240.05,-123.2 232.17,-130.28 238.31,-133.65"/>
+</g>
+<!-- ip6_rewrite&#45;&gt;pkt_drop -->
+<g id="edge13" class="edge">
+<title>ip6_rewrite&#45;&gt;pkt_drop</title>
+<path fill="none" stroke="red" stroke-dasharray="5,2" d="M197.68,-160.05C184.87,-140.87 169.12,-109.39 184.5,-87 210.62,-48.98 261.18,-32.21 301.59,-24.82"/>
+<polygon fill="red" stroke="red" points="302.35,-28.24 311.63,-23.13 301.19,-21.33 302.35,-28.24"/>
+</g>
+<!-- ethdev_tx&#45;&gt;pkt_drop -->
+<g id="edge15" class="edge">
+<title>ethdev_tx&#45;&gt;pkt_drop</title>
+<path fill="none" stroke="red" stroke-dasharray="5,2" d="M270.3,-88.21C287.91,-74.85 313.31,-55.57 332.84,-40.75"/>
+<polygon fill="red" stroke="red" points="334.96,-43.54 340.81,-34.7 330.73,-37.96 334.96,-43.54"/>
+</g>
+<!-- egress_port -->
+<g id="node10" class="node">
+<title>egress_port</title>
+<polygon fill="none" stroke="black" points="101,-36 0,-36 0,0 101,0 101,-36"/>
+<text text-anchor="middle" x="50.5" y="-14.3" font-family="Times,serif" font-size="14.00">egress_port</text>
+</g>
+<!-- ethdev_tx&#45;&gt;egress_port -->
+<g id="edge14" class="edge">
+<title>ethdev_tx&#45;&gt;egress_port</title>
+<path fill="none" stroke="black" d="M217.08,-90.15C185.34,-76.59 136.54,-55.75 99.95,-40.12"/>
+<polygon fill="black" stroke="black" points="101.03,-36.78 90.45,-36.07 98.28,-43.21 101.03,-36.78"/>
+<text text-anchor="middle" x="211.5" y="-57.8" font-family="Times,serif" font-size="14.00">egress packet</text>
+</g>
+</g>
+</svg>
diff --git a/doc/guides/tools/index.rst b/doc/guides/tools/index.rst
index f2afb1fcc5..4f4dc8b518 100644
--- a/doc/guides/tools/index.rst
+++ b/doc/guides/tools/index.rst
@@ -23,4 +23,5 @@  DPDK Tools User Guides
     testeventdev
     testregex
     testmldev
+    graph
     dts