diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py
index 3a16d4fd..db908e05 100644
--- a/daemon/core/api/grpc/client.py
+++ b/daemon/core/api/grpc/client.py
@@ -92,7 +92,7 @@ from core.api.grpc.wlan_pb2 import (
     WlanLinkRequest,
     WlanLinkResponse,
 )
-from core.emulator.emudata import IpPrefixes
+from core.emulator.data import IpPrefixes
 
 
 class InterfaceHelper:
@@ -110,27 +110,27 @@ class InterfaceHelper:
         """
         self.prefixes: IpPrefixes = IpPrefixes(ip4_prefix, ip6_prefix)
 
-    def create_interface(
-        self, node_id: int, interface_id: int, name: str = None, mac: str = None
+    def create_iface(
+        self, node_id: int, iface_id: int, name: str = None, mac: str = None
     ) -> core_pb2.Interface:
         """
         Create an interface protobuf object.
 
         :param node_id: node id to create interface for
-        :param interface_id: interface id
+        :param iface_id: interface id
         :param name: name of interface
         :param mac: mac address for interface
         :return: interface protobuf
         """
-        interface_data = self.prefixes.gen_interface(node_id, name, mac)
+        iface_data = self.prefixes.gen_iface(node_id, name, mac)
         return core_pb2.Interface(
-            id=interface_id,
-            name=interface_data.name,
-            ip4=interface_data.ip4,
-            ip4mask=interface_data.ip4_mask,
-            ip6=interface_data.ip6,
-            ip6mask=interface_data.ip6_mask,
-            mac=interface_data.mac,
+            id=iface_id,
+            name=iface_data.name,
+            ip4=iface_data.ip4,
+            ip4_mask=iface_data.ip4_mask,
+            ip6=iface_data.ip6,
+            ip6_mask=iface_data.ip6_mask,
+            mac=iface_data.mac,
         )
 
 
@@ -611,8 +611,8 @@ class CoreGrpcClient:
         session_id: int,
         node1_id: int,
         node2_id: int,
-        interface1: core_pb2.Interface = None,
-        interface2: core_pb2.Interface = None,
+        iface1: core_pb2.Interface = None,
+        iface2: core_pb2.Interface = None,
         options: core_pb2.LinkOptions = None,
     ) -> core_pb2.AddLinkResponse:
         """
@@ -621,8 +621,8 @@ class CoreGrpcClient:
         :param session_id: session id
         :param node1_id: node one id
         :param node2_id: node two id
-        :param interface1: node one interface data
-        :param interface2: node two interface data
+        :param iface1: node one interface data
+        :param iface2: node two interface data
         :param options: options for link (jitter, bandwidth, etc)
         :return: response with result of success or failure
         :raises grpc.RpcError: when session or one of the nodes don't exist
@@ -631,8 +631,8 @@ class CoreGrpcClient:
             node1_id=node1_id,
             node2_id=node2_id,
             type=core_pb2.LinkType.WIRED,
-            interface1=interface1,
-            interface2=interface2,
+            iface1=iface1,
+            iface2=iface2,
             options=options,
         )
         request = core_pb2.AddLinkRequest(session_id=session_id, link=link)
@@ -644,8 +644,8 @@ class CoreGrpcClient:
         node1_id: int,
         node2_id: int,
         options: core_pb2.LinkOptions,
-        interface1_id: int = None,
-        interface2_id: int = None,
+        iface1_id: int = None,
+        iface2_id: int = None,
     ) -> core_pb2.EditLinkResponse:
         """
         Edit a link between nodes.
@@ -654,8 +654,8 @@ class CoreGrpcClient:
         :param node1_id: node one id
         :param node2_id: node two id
         :param options: options for link (jitter, bandwidth, etc)
-        :param interface1_id: node one interface id
-        :param interface2_id: node two interface id
+        :param iface1_id: node one interface id
+        :param iface2_id: node two interface id
         :return: response with result of success or failure
         :raises grpc.RpcError: when session or one of the nodes don't exist
         """
@@ -664,8 +664,8 @@ class CoreGrpcClient:
             node1_id=node1_id,
             node2_id=node2_id,
             options=options,
-            interface1_id=interface1_id,
-            interface2_id=interface2_id,
+            iface1_id=iface1_id,
+            iface2_id=iface2_id,
         )
         return self.stub.EditLink(request)
 
@@ -674,8 +674,8 @@ class CoreGrpcClient:
         session_id: int,
         node1_id: int,
         node2_id: int,
-        interface1_id: int = None,
-        interface2_id: int = None,
+        iface1_id: int = None,
+        iface2_id: int = None,
     ) -> core_pb2.DeleteLinkResponse:
         """
         Delete a link between nodes.
@@ -683,8 +683,8 @@ class CoreGrpcClient:
         :param session_id: session id
         :param node1_id: node one id
         :param node2_id: node two id
-        :param interface1_id: node one interface id
-        :param interface2_id: node two interface id
+        :param iface1_id: node one interface id
+        :param iface2_id: node two interface id
         :return: response with result of success or failure
         :raises grpc.RpcError: when session doesn't exist
         """
@@ -692,8 +692,8 @@ class CoreGrpcClient:
             session_id=session_id,
             node1_id=node1_id,
             node2_id=node2_id,
-            interface1_id=interface1_id,
-            interface2_id=interface2_id,
+            iface1_id=iface1_id,
+            iface2_id=iface2_id,
         )
         return self.stub.DeleteLink(request)
 
@@ -1028,7 +1028,7 @@ class CoreGrpcClient:
         return self.stub.GetEmaneModels(request)
 
     def get_emane_model_config(
-        self, session_id: int, node_id: int, model: str, interface_id: int = -1
+        self, session_id: int, node_id: int, model: str, iface_id: int = -1
     ) -> GetEmaneModelConfigResponse:
         """
         Get emane model configuration for a node or a node's interface.
@@ -1036,12 +1036,12 @@ class CoreGrpcClient:
         :param session_id: session id
         :param node_id: node id
         :param model: emane model name
-        :param interface_id: node interface id
+        :param iface_id: node interface id
         :return: response with a list of configuration groups
         :raises grpc.RpcError: when session doesn't exist
         """
         request = GetEmaneModelConfigRequest(
-            session_id=session_id, node_id=node_id, model=model, interface=interface_id
+            session_id=session_id, node_id=node_id, model=model, iface_id=iface_id
         )
         return self.stub.GetEmaneModelConfig(request)
 
@@ -1051,7 +1051,7 @@ class CoreGrpcClient:
         node_id: int,
         model: str,
         config: Dict[str, str] = None,
-        interface_id: int = -1,
+        iface_id: int = -1,
     ) -> SetEmaneModelConfigResponse:
         """
         Set emane model configuration for a node or a node's interface.
@@ -1060,12 +1060,12 @@ class CoreGrpcClient:
         :param node_id: node id
         :param model: emane model name
         :param config: emane model configuration
-        :param interface_id: node interface id
+        :param iface_id: node interface id
         :return: response with result of success or failure
         :raises grpc.RpcError: when session doesn't exist
         """
         model_config = EmaneModelConfig(
-            node_id=node_id, model=model, config=config, interface_id=interface_id
+            node_id=node_id, model=model, config=config, iface_id=iface_id
         )
         request = SetEmaneModelConfigRequest(
             session_id=session_id, emane_model_config=model_config
@@ -1128,7 +1128,7 @@ class CoreGrpcClient:
         )
         return self.stub.EmaneLink(request)
 
-    def get_interfaces(self) -> core_pb2.GetInterfacesResponse:
+    def get_ifaces(self) -> core_pb2.GetInterfacesResponse:
         """
         Retrieves a list of interfaces available on the host machine that are not
         a part of a CORE session.
diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py
index 82cf1eac..75f9eb2e 100644
--- a/daemon/core/api/grpc/events.py
+++ b/daemon/core/api/grpc/events.py
@@ -15,24 +15,28 @@ from core.emulator.data import (
 from core.emulator.session import Session
 
 
-def handle_node_event(event: NodeData) -> core_pb2.NodeEvent:
+def handle_node_event(node_data: NodeData) -> core_pb2.NodeEvent:
     """
     Handle node event when there is a node event
 
-    :param event: node data
+    :param node_data: node data
     :return: node event that contains node id, name, model, position, and services
     """
-    position = core_pb2.Position(x=event.x_position, y=event.y_position)
-    geo = core_pb2.Geo(lat=event.latitude, lon=event.longitude, alt=event.altitude)
+    node = node_data.node
+    x, y, _ = node.position.get()
+    position = core_pb2.Position(x=x, y=y)
+    lon, lat, alt = node.position.get_geo()
+    geo = core_pb2.Geo(lon=lon, lat=lat, alt=alt)
+    services = [x.name for x in node.services]
     node_proto = core_pb2.Node(
-        id=event.id,
-        name=event.name,
-        model=event.model,
+        id=node.id,
+        name=node.name,
+        model=node.type,
         position=position,
         geo=geo,
-        services=event.services,
+        services=services,
     )
-    return core_pb2.NodeEvent(node=node_proto, source=event.source)
+    return core_pb2.NodeEvent(node=node_proto, source=node_data.source)
 
 
 def handle_link_event(event: LinkData) -> core_pb2.LinkEvent:
@@ -82,7 +86,7 @@ def handle_config_event(event: ConfigData) -> core_pb2.ConfigEvent:
         data_values=event.data_values,
         possible_values=event.possible_values,
         groups=event.groups,
-        interface=event.interface_number,
+        iface_id=event.iface_id,
         network_id=event.network_id,
         opaque=event.opaque,
         data_types=event.data_types,
diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py
index c9b76b73..d95b7555 100644
--- a/daemon/core/api/grpc/grpcutils.py
+++ b/daemon/core/api/grpc/grpcutils.py
@@ -11,8 +11,7 @@ from core.api.grpc import common_pb2, core_pb2
 from core.api.grpc.services_pb2 import NodeServiceData, ServiceConfig
 from core.config import ConfigurableOptions
 from core.emane.nodes import EmaneNet
-from core.emulator.data import LinkData
-from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
+from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
 from core.emulator.enumerations import LinkTypes, NodeTypes
 from core.emulator.session import Session
 from core.nodes.base import CoreNode, NodeBase
@@ -35,7 +34,6 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
         name=node_proto.name,
         model=node_proto.model,
         icon=node_proto.icon,
-        opaque=node_proto.opaque,
         image=node_proto.image,
         services=node_proto.services,
         config_services=node_proto.config_services,
@@ -52,58 +50,57 @@ def add_node_data(node_proto: core_pb2.Node) -> Tuple[NodeTypes, int, NodeOption
     return _type, _id, options
 
 
-def link_interface(interface_proto: core_pb2.Interface) -> InterfaceData:
+def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
     """
     Create interface data from interface proto.
 
-    :param interface_proto: interface proto
+    :param iface_proto: interface proto
     :return: interface data
     """
-    interface_data = None
-    if interface_proto:
-        name = interface_proto.name if interface_proto.name else None
-        mac = interface_proto.mac if interface_proto.mac else None
-        ip4 = interface_proto.ip4 if interface_proto.ip4 else None
-        ip6 = interface_proto.ip6 if interface_proto.ip6 else None
-        interface_data = InterfaceData(
-            id=interface_proto.id,
+    iface_data = None
+    if iface_proto:
+        name = iface_proto.name if iface_proto.name else None
+        mac = iface_proto.mac if iface_proto.mac else None
+        ip4 = iface_proto.ip4 if iface_proto.ip4 else None
+        ip6 = iface_proto.ip6 if iface_proto.ip6 else None
+        iface_data = InterfaceData(
+            id=iface_proto.id,
             name=name,
             mac=mac,
             ip4=ip4,
-            ip4_mask=interface_proto.ip4mask,
+            ip4_mask=iface_proto.ip4_mask,
             ip6=ip6,
-            ip6_mask=interface_proto.ip6mask,
+            ip6_mask=iface_proto.ip6_mask,
         )
-    return interface_data
+    return iface_data
 
 
 def add_link_data(
     link_proto: core_pb2.Link
-) -> Tuple[InterfaceData, InterfaceData, LinkOptions]:
+) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]:
     """
     Convert link proto to link interfaces and options data.
 
     :param link_proto: link  proto
     :return: link interfaces and options
     """
-    interface1_data = link_interface(link_proto.interface1)
-    interface2_data = link_interface(link_proto.interface2)
+    iface1_data = link_iface(link_proto.iface1)
+    iface2_data = link_iface(link_proto.iface2)
     link_type = LinkTypes(link_proto.type)
-    options = LinkOptions(type=link_type)
-    options_data = link_proto.options
-    if options_data:
-        options.delay = options_data.delay
-        options.bandwidth = options_data.bandwidth
-        options.loss = options_data.loss
-        options.dup = options_data.dup
-        options.jitter = options_data.jitter
-        options.mer = options_data.mer
-        options.burst = options_data.burst
-        options.mburst = options_data.mburst
-        options.unidirectional = options_data.unidirectional
-        options.key = options_data.key
-        options.opaque = options_data.opaque
-    return interface1_data, interface2_data, options
+    options = LinkOptions()
+    options_proto = link_proto.options
+    if options_proto:
+        options.delay = options_proto.delay
+        options.bandwidth = options_proto.bandwidth
+        options.loss = options_proto.loss
+        options.dup = options_proto.dup
+        options.jitter = options_proto.jitter
+        options.mer = options_proto.mer
+        options.burst = options_proto.burst
+        options.mburst = options_proto.mburst
+        options.unidirectional = options_proto.unidirectional
+        options.key = options_proto.key
+    return iface1_data, iface2_data, options, link_type
 
 
 def create_nodes(
@@ -143,8 +140,8 @@ def create_links(
     for link_proto in link_protos:
         node1_id = link_proto.node1_id
         node2_id = link_proto.node2_id
-        interface1, interface2, options = add_link_data(link_proto)
-        args = (node1_id, node2_id, interface1, interface2, options)
+        iface1, iface2, options, link_type = add_link_data(link_proto)
+        args = (node1_id, node2_id, iface1, iface2, options, link_type)
         funcs.append((session.add_link, args, {}))
     start = time.monotonic()
     results, exceptions = utils.threadpool(funcs)
@@ -167,8 +164,8 @@ def edit_links(
     for link_proto in link_protos:
         node1_id = link_proto.node1_id
         node2_id = link_proto.node2_id
-        interface1, interface2, options = add_link_data(link_proto)
-        args = (node1_id, node2_id, interface1.id, interface2.id, options)
+        iface1, iface2, options, link_type = add_link_data(link_proto)
+        args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type)
         funcs.append((session.update_link, args, {}))
     start = time.monotonic()
     results, exceptions = utils.threadpool(funcs)
@@ -279,16 +276,16 @@ def get_links(node: NodeBase):
     return links
 
 
-def get_emane_model_id(node_id: int, interface_id: int) -> int:
+def get_emane_model_id(node_id: int, iface_id: int) -> int:
     """
     Get EMANE model id
 
     :param node_id: node id
-    :param interface_id: interface id
+    :param iface_id: interface id
     :return: EMANE model id
     """
-    if interface_id >= 0:
-        return node_id * 1000 + interface_id
+    if iface_id >= 0:
+        return node_id * 1000 + iface_id
     else:
         return node_id
 
@@ -300,12 +297,39 @@ def parse_emane_model_id(_id: int) -> Tuple[int, int]:
     :param _id: id to parse
     :return: node id and interface id
     """
-    interface = -1
+    iface_id = -1
     node_id = _id
     if _id >= 1000:
-        interface = _id % 1000
+        iface_id = _id % 1000
         node_id = int(_id / 1000)
-    return node_id, interface
+    return node_id, iface_id
+
+
+def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface:
+    return core_pb2.Interface(
+        id=iface_data.id,
+        name=iface_data.name,
+        mac=iface_data.mac,
+        ip4=iface_data.ip4,
+        ip4_mask=iface_data.ip4_mask,
+        ip6=iface_data.ip6,
+        ip6_mask=iface_data.ip6_mask,
+    )
+
+
+def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions:
+    return core_pb2.LinkOptions(
+        jitter=options_data.jitter,
+        key=options_data.key,
+        mburst=options_data.mburst,
+        mer=options_data.mer,
+        loss=options_data.loss,
+        bandwidth=options_data.bandwidth,
+        burst=options_data.burst,
+        delay=options_data.delay,
+        dup=options_data.dup,
+        unidirectional=options_data.unidirectional,
+    )
 
 
 def convert_link(link_data: LinkData) -> core_pb2.Link:
@@ -315,47 +339,19 @@ def convert_link(link_data: LinkData) -> core_pb2.Link:
     :param link_data: link to convert
     :return: core protobuf Link
     """
-    interface1 = None
-    if link_data.interface1_id is not None:
-        interface1 = core_pb2.Interface(
-            id=link_data.interface1_id,
-            name=link_data.interface1_name,
-            mac=convert_value(link_data.interface1_mac),
-            ip4=convert_value(link_data.interface1_ip4),
-            ip4mask=link_data.interface1_ip4_mask,
-            ip6=convert_value(link_data.interface1_ip6),
-            ip6mask=link_data.interface1_ip6_mask,
-        )
-    interface2 = None
-    if link_data.interface2_id is not None:
-        interface2 = core_pb2.Interface(
-            id=link_data.interface2_id,
-            name=link_data.interface2_name,
-            mac=convert_value(link_data.interface2_mac),
-            ip4=convert_value(link_data.interface2_ip4),
-            ip4mask=link_data.interface2_ip4_mask,
-            ip6=convert_value(link_data.interface2_ip6),
-            ip6mask=link_data.interface2_ip6_mask,
-        )
-    options = core_pb2.LinkOptions(
-        opaque=link_data.opaque,
-        jitter=link_data.jitter,
-        key=link_data.key,
-        mburst=link_data.mburst,
-        mer=link_data.mer,
-        loss=link_data.loss,
-        bandwidth=link_data.bandwidth,
-        burst=link_data.burst,
-        delay=link_data.delay,
-        dup=link_data.dup,
-        unidirectional=link_data.unidirectional,
-    )
+    iface1 = None
+    if link_data.iface1 is not None:
+        iface1 = convert_iface(link_data.iface1)
+    iface2 = None
+    if link_data.iface2 is not None:
+        iface2 = convert_iface(link_data.iface2)
+    options = convert_link_options(link_data.options)
     return core_pb2.Link(
-        type=link_data.link_type.value,
+        type=link_data.type.value,
         node1_id=link_data.node1_id,
         node2_id=link_data.node2_id,
-        interface1=interface1,
-        interface2=interface2,
+        iface1=iface1,
+        iface2=iface2,
         options=options,
         network_id=link_data.network_id,
         label=link_data.label,
@@ -440,58 +436,58 @@ def get_service_configuration(service: CoreService) -> NodeServiceData:
     )
 
 
-def interface_to_proto(interface: CoreInterface) -> core_pb2.Interface:
+def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface:
     """
     Convenience for converting a core interface to the protobuf representation.
-    :param interface: interface to convert
+    :param iface: interface to convert
     :return: interface proto
     """
     net_id = None
-    if interface.net:
-        net_id = interface.net.id
+    if iface.net:
+        net_id = iface.net.id
     ip4 = None
-    ip4mask = None
+    ip4_mask = None
     ip6 = None
-    ip6mask = None
-    for addr in interface.addrlist:
+    ip6_mask = None
+    for addr in iface.addrlist:
         network = netaddr.IPNetwork(addr)
         mask = network.prefixlen
         ip = str(network.ip)
         if netaddr.valid_ipv4(ip) and not ip4:
             ip4 = ip
-            ip4mask = mask
+            ip4_mask = mask
         elif netaddr.valid_ipv6(ip) and not ip6:
             ip6 = ip
-            ip6mask = mask
+            ip6_mask = mask
     return core_pb2.Interface(
-        id=interface.netindex,
-        netid=net_id,
-        name=interface.name,
-        mac=str(interface.hwaddr),
-        mtu=interface.mtu,
-        flowid=interface.flow_id,
+        id=iface.node_id,
+        net_id=net_id,
+        name=iface.name,
+        mac=iface.mac,
+        mtu=iface.mtu,
+        flow_id=iface.flow_id,
         ip4=ip4,
-        ip4mask=ip4mask,
+        ip4_mask=ip4_mask,
         ip6=ip6,
-        ip6mask=ip6mask,
+        ip6_mask=ip6_mask,
     )
 
 
-def get_nem_id(node: CoreNode, netif_id: int, context: ServicerContext) -> int:
+def get_nem_id(node: CoreNode, iface_id: int, context: ServicerContext) -> int:
     """
     Get nem id for a given node and interface id.
 
     :param node: node to get nem id for
-    :param netif_id: id of interface on node to get nem id for
+    :param iface_id: id of interface on node to get nem id for
     :param context: request context
     :return: nem id
     """
-    netif = node.netif(netif_id)
-    if not netif:
-        message = f"{node.name} missing interface {netif_id}"
+    iface = node.ifaces.get(iface_id)
+    if not iface:
+        message = f"{node.name} missing interface {iface_id}"
         context.abort(grpc.StatusCode.NOT_FOUND, message)
-    net = netif.net
+    net = iface.net
     if not isinstance(net, EmaneNet):
-        message = f"{node.name} interface {netif_id} is not an EMANE network"
+        message = f"{node.name} interface {iface_id} is not an EMANE network"
         context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
-    return net.getnemid(netif)
+    return net.getnemid(iface)
diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py
index 8b349b67..1964b6e8 100644
--- a/daemon/core/api/grpc/server.py
+++ b/daemon/core/api/grpc/server.py
@@ -108,8 +108,7 @@ from core.api.grpc.wlan_pb2 import (
     WlanLinkResponse,
 )
 from core.emulator.coreemu import CoreEmu
-from core.emulator.data import LinkData
-from core.emulator.emudata import LinkOptions, NodeOptions
+from core.emulator.data import LinkData, LinkOptions, NodeOptions
 from core.emulator.enumerations import EventTypes, LinkTypes, MessageFlags
 from core.emulator.session import NT, Session
 from core.errors import CoreCommandError, CoreError
@@ -246,7 +245,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
         config = session.emane.get_configs()
         config.update(request.emane_config)
         for config in request.emane_model_configs:
-            _id = get_emane_model_id(config.node_id, config.interface_id)
+            _id = get_emane_model_id(config.node_id, config.iface_id)
             session.emane.set_model_config(_id, config.model, config.config)
 
         # wlan configs
@@ -625,16 +624,14 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
                         key = key.split(".")
                         node_id = _INTERFACE_REGEX.search(key[0]).group("node")
                         node_id = int(node_id, base=16)
-                        interface_id = int(key[1], base=16)
+                        iface_id = int(key[1], base=16)
                         session_id = int(key[2], base=16)
                         if session.id != session_id:
                             continue
-                        interface_throughput = (
-                            throughputs_event.interface_throughputs.add()
-                        )
-                        interface_throughput.node_id = node_id
-                        interface_throughput.interface_id = interface_id
-                        interface_throughput.throughput = throughput
+                        iface_throughput = throughputs_event.iface_throughputs.add()
+                        iface_throughput.node_id = node_id
+                        iface_throughput.iface_id = iface_id
+                        iface_throughput.throughput = throughput
                     elif key.startswith("b."):
                         try:
                             key = key.split(".")
@@ -686,13 +683,13 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
         logging.debug("get node: %s", request)
         session = self.get_session(request.session_id, context)
         node = self.get_node(session, request.node_id, context, NodeBase)
-        interfaces = []
-        for interface_id in node._netif:
-            interface = node._netif[interface_id]
-            interface_proto = grpcutils.interface_to_proto(interface)
-            interfaces.append(interface_proto)
+        ifaces = []
+        for iface_id in node.ifaces:
+            iface = node.ifaces[iface_id]
+            iface_proto = grpcutils.iface_to_proto(iface)
+            ifaces.append(iface_proto)
         node_proto = grpcutils.get_node_proto(session, node)
-        return core_pb2.GetNodeResponse(node=node_proto, interfaces=interfaces)
+        return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces)
 
     def MoveNodes(
         self,
@@ -850,18 +847,20 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
         node2_id = request.link.node2_id
         self.get_node(session, node1_id, context, NodeBase)
         self.get_node(session, node2_id, context, NodeBase)
-        interface1, interface2, options = grpcutils.add_link_data(request.link)
-        node1_interface, node2_interface = session.add_link(
-            node1_id, node2_id, interface1, interface2, options=options
+        iface1_data, iface2_data, options, link_type = grpcutils.add_link_data(
+            request.link
         )
-        interface1_proto = None
-        interface2_proto = None
-        if node1_interface:
-            interface1_proto = grpcutils.interface_to_proto(node1_interface)
-        if node2_interface:
-            interface2_proto = grpcutils.interface_to_proto(node2_interface)
+        node1_iface, node2_iface = session.add_link(
+            node1_id, node2_id, iface1_data, iface2_data, options, link_type
+        )
+        iface1_proto = None
+        iface2_proto = None
+        if node1_iface:
+            iface1_proto = grpcutils.iface_to_proto(node1_iface)
+        if node2_iface:
+            iface2_proto = grpcutils.iface_to_proto(node2_iface)
         return core_pb2.AddLinkResponse(
-            result=True, interface1=interface1_proto, interface2=interface2_proto
+            result=True, iface1=iface1_proto, iface2=iface2_proto
         )
 
     def EditLink(
@@ -878,23 +877,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
         session = self.get_session(request.session_id, context)
         node1_id = request.node1_id
         node2_id = request.node2_id
-        interface1_id = request.interface1_id
-        interface2_id = request.interface2_id
-        options_data = request.options
+        iface1_id = request.iface1_id
+        iface2_id = request.iface2_id
+        options_proto = request.options
         options = LinkOptions(
-            delay=options_data.delay,
-            bandwidth=options_data.bandwidth,
-            loss=options_data.loss,
-            dup=options_data.dup,
-            jitter=options_data.jitter,
-            mer=options_data.mer,
-            burst=options_data.burst,
-            mburst=options_data.mburst,
-            unidirectional=options_data.unidirectional,
-            key=options_data.key,
-            opaque=options_data.opaque,
+            delay=options_proto.delay,
+            bandwidth=options_proto.bandwidth,
+            loss=options_proto.loss,
+            dup=options_proto.dup,
+            jitter=options_proto.jitter,
+            mer=options_proto.mer,
+            burst=options_proto.burst,
+            mburst=options_proto.mburst,
+            unidirectional=options_proto.unidirectional,
+            key=options_proto.key,
         )
-        session.update_link(node1_id, node2_id, interface1_id, interface2_id, options)
+        session.update_link(node1_id, node2_id, iface1_id, iface2_id, options)
         return core_pb2.EditLinkResponse(result=True)
 
     def DeleteLink(
@@ -911,9 +909,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
         session = self.get_session(request.session_id, context)
         node1_id = request.node1_id
         node2_id = request.node2_id
-        interface1_id = request.interface1_id
-        interface2_id = request.interface2_id
-        session.delete_link(node1_id, node2_id, interface1_id, interface2_id)
+        iface1_id = request.iface1_id
+        iface2_id = request.iface2_id
+        session.delete_link(node1_id, node2_id, iface1_id, iface2_id)
         return core_pb2.DeleteLinkResponse(result=True)
 
     def GetHooks(
@@ -1371,7 +1369,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
         logging.debug("get emane model config: %s", request)
         session = self.get_session(request.session_id, context)
         model = session.emane.models[request.model]
-        _id = get_emane_model_id(request.node_id, request.interface)
+        _id = get_emane_model_id(request.node_id, request.iface_id)
         current_config = session.emane.get_model_config(_id, request.model)
         config = get_config_options(current_config, model)
         return GetEmaneModelConfigResponse(config=config)
@@ -1390,7 +1388,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
         logging.debug("set emane model config: %s", request)
         session = self.get_session(request.session_id, context)
         model_config = request.emane_model_config
-        _id = get_emane_model_id(model_config.node_id, model_config.interface_id)
+        _id = get_emane_model_id(model_config.node_id, model_config.iface_id)
         session.emane.set_model_config(_id, model_config.model, model_config.config)
         return SetEmaneModelConfigResponse(result=True)
 
@@ -1419,12 +1417,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
                 model = session.emane.models[model_name]
                 current_config = session.emane.get_model_config(_id, model_name)
                 config = get_config_options(current_config, model)
-                node_id, interface = grpcutils.parse_emane_model_id(_id)
+                node_id, iface_id = grpcutils.parse_emane_model_id(_id)
                 model_config = GetEmaneModelConfigsResponse.ModelConfig(
-                    node_id=node_id,
-                    model=model_name,
-                    interface=interface,
-                    config=config,
+                    node_id=node_id, model=model_name, iface_id=iface_id, config=config
                 )
                 configs.append(model_config)
         return GetEmaneModelConfigsResponse(configs=configs)
@@ -1489,16 +1484,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
         :param context: context object
         :return: get-interfaces response that has all the system's interfaces
         """
-        interfaces = []
-        for interface in os.listdir("/sys/class/net"):
-            if (
-                interface.startswith("b.")
-                or interface.startswith("veth")
-                or interface == "lo"
-            ):
+        ifaces = []
+        for iface in os.listdir("/sys/class/net"):
+            if iface.startswith("b.") or iface.startswith("veth") or iface == "lo":
                 continue
-            interfaces.append(interface)
-        return core_pb2.GetInterfacesResponse(interfaces=interfaces)
+            ifaces.append(iface)
+        return core_pb2.GetInterfacesResponse(ifaces=ifaces)
 
     def EmaneLink(
         self, request: EmaneLinkRequest, context: ServicerContext
@@ -1513,16 +1504,16 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
         logging.debug("emane link: %s", request)
         session = self.get_session(request.session_id, context)
         nem1 = request.nem1
-        emane1, netif = session.emane.nemlookup(nem1)
-        if not emane1 or not netif:
+        emane1, iface = session.emane.nemlookup(nem1)
+        if not emane1 or not iface:
             context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found")
-        node1 = netif.node
+        node1 = iface.node
 
         nem2 = request.nem2
-        emane2, netif = session.emane.nemlookup(nem2)
-        if not emane2 or not netif:
+        emane2, iface = session.emane.nemlookup(nem2)
+        if not emane2 or not iface:
             context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found")
-        node2 = netif.node
+        node2 = iface.node
 
         if emane1.id == emane2.id:
             if request.linked:
@@ -1532,7 +1523,7 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
             color = session.get_link_color(emane1.id)
             link = LinkData(
                 message_type=flag,
-                link_type=LinkTypes.WIRELESS,
+                type=LinkTypes.WIRELESS,
                 node1_id=node1.id,
                 node2_id=node2.id,
                 network_id=emane1.id,
@@ -1734,21 +1725,19 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
             )
         node1 = self.get_node(session, request.node1_id, context, CoreNode)
         node2 = self.get_node(session, request.node2_id, context, CoreNode)
-        node1_interface, node2_interface = None, None
-        for net, interface1, interface2 in node1.commonnets(node2):
+        node1_iface, node2_iface = None, None
+        for net, iface1, iface2 in node1.commonnets(node2):
             if net == wlan:
-                node1_interface = interface1
-                node2_interface = interface2
+                node1_iface = iface1
+                node2_iface = iface2
                 break
         result = False
-        if node1_interface and node2_interface:
+        if node1_iface and node2_iface:
             if request.linked:
-                wlan.link(node1_interface, node2_interface)
+                wlan.link(node1_iface, node2_iface)
             else:
-                wlan.unlink(node1_interface, node2_interface)
-            wlan.model.sendlinkmsg(
-                node1_interface, node2_interface, unlink=not request.linked
-            )
+                wlan.unlink(node1_iface, node2_iface)
+            wlan.model.sendlinkmsg(node1_iface, node2_iface, unlink=not request.linked)
             result = True
         return WlanLinkResponse(result=result)
 
@@ -1760,8 +1749,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
         for request in request_iterator:
             session = self.get_session(request.session_id, context)
             node1 = self.get_node(session, request.node1_id, context, CoreNode)
-            nem1 = grpcutils.get_nem_id(node1, request.interface1_id, context)
+            nem1 = grpcutils.get_nem_id(node1, request.iface1_id, context)
             node2 = self.get_node(session, request.node2_id, context, CoreNode)
-            nem2 = grpcutils.get_nem_id(node2, request.interface2_id, context)
+            nem2 = grpcutils.get_nem_id(node2, request.iface2_id, context)
             session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2)
         return EmanePathlossesResponse()
diff --git a/daemon/core/api/tlv/coreapi.py b/daemon/core/api/tlv/coreapi.py
index 088a7631..5d0b08e7 100644
--- a/daemon/core/api/tlv/coreapi.py
+++ b/daemon/core/api/tlv/coreapi.py
@@ -508,18 +508,18 @@ class CoreLinkTlv(CoreTlv):
         LinkTlvs.EMULATION_ID.value: CoreTlvDataUint32,
         LinkTlvs.NETWORK_ID.value: CoreTlvDataUint32,
         LinkTlvs.KEY.value: CoreTlvDataUint32,
-        LinkTlvs.INTERFACE1_NUMBER.value: CoreTlvDataUint16,
-        LinkTlvs.INTERFACE1_IP4.value: CoreTlvDataIpv4Addr,
-        LinkTlvs.INTERFACE1_IP4_MASK.value: CoreTlvDataUint16,
-        LinkTlvs.INTERFACE1_MAC.value: CoreTlvDataMacAddr,
-        LinkTlvs.INTERFACE1_IP6.value: CoreTlvDataIPv6Addr,
-        LinkTlvs.INTERFACE1_IP6_MASK.value: CoreTlvDataUint16,
-        LinkTlvs.INTERFACE2_NUMBER.value: CoreTlvDataUint16,
-        LinkTlvs.INTERFACE2_IP4.value: CoreTlvDataIpv4Addr,
-        LinkTlvs.INTERFACE2_IP4_MASK.value: CoreTlvDataUint16,
-        LinkTlvs.INTERFACE2_MAC.value: CoreTlvDataMacAddr,
-        LinkTlvs.INTERFACE2_IP6.value: CoreTlvDataIPv6Addr,
-        LinkTlvs.INTERFACE2_IP6_MASK.value: CoreTlvDataUint16,
+        LinkTlvs.IFACE1_NUMBER.value: CoreTlvDataUint16,
+        LinkTlvs.IFACE1_IP4.value: CoreTlvDataIpv4Addr,
+        LinkTlvs.IFACE1_IP4_MASK.value: CoreTlvDataUint16,
+        LinkTlvs.IFACE1_MAC.value: CoreTlvDataMacAddr,
+        LinkTlvs.IFACE1_IP6.value: CoreTlvDataIPv6Addr,
+        LinkTlvs.IFACE1_IP6_MASK.value: CoreTlvDataUint16,
+        LinkTlvs.IFACE2_NUMBER.value: CoreTlvDataUint16,
+        LinkTlvs.IFACE2_IP4.value: CoreTlvDataIpv4Addr,
+        LinkTlvs.IFACE2_IP4_MASK.value: CoreTlvDataUint16,
+        LinkTlvs.IFACE2_MAC.value: CoreTlvDataMacAddr,
+        LinkTlvs.IFACE2_IP6.value: CoreTlvDataIPv6Addr,
+        LinkTlvs.IFACE2_IP6_MASK.value: CoreTlvDataUint16,
         LinkTlvs.INTERFACE1_NAME.value: CoreTlvDataString,
         LinkTlvs.INTERFACE2_NAME.value: CoreTlvDataString,
         LinkTlvs.OPAQUE.value: CoreTlvDataString,
@@ -577,7 +577,7 @@ class CoreConfigTlv(CoreTlv):
         ConfigTlvs.POSSIBLE_VALUES.value: CoreTlvDataString,
         ConfigTlvs.GROUPS.value: CoreTlvDataString,
         ConfigTlvs.SESSION.value: CoreTlvDataString,
-        ConfigTlvs.INTERFACE_NUMBER.value: CoreTlvDataUint16,
+        ConfigTlvs.IFACE_ID.value: CoreTlvDataUint16,
         ConfigTlvs.NETWORK_ID.value: CoreTlvDataUint32,
         ConfigTlvs.OPAQUE.value: CoreTlvDataString,
     }
diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py
index 2cd7bfac..d01f15a3 100644
--- a/daemon/core/api/tlv/corehandlers.py
+++ b/daemon/core/api/tlv/corehandlers.py
@@ -29,8 +29,15 @@ from core.api.tlv.enumerations import (
     NodeTlvs,
     SessionTlvs,
 )
-from core.emulator.data import ConfigData, EventData, ExceptionData, FileData
-from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
+from core.emulator.data import (
+    ConfigData,
+    EventData,
+    ExceptionData,
+    FileData,
+    InterfaceData,
+    LinkOptions,
+    NodeOptions,
+)
 from core.emulator.enumerations import (
     ConfigDataTypes,
     EventTypes,
@@ -71,7 +78,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
             MessageTypes.REGISTER.value: self.handle_register_message,
             MessageTypes.CONFIG.value: self.handle_config_message,
             MessageTypes.FILE.value: self.handle_file_message,
-            MessageTypes.INTERFACE.value: self.handle_interface_message,
+            MessageTypes.INTERFACE.value: self.handle_iface_message,
             MessageTypes.EVENT.value: self.handle_event_message,
             MessageTypes.SESSION.value: self.handle_session_message,
         }
@@ -322,7 +329,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
         """
         logging.debug("handling broadcast node: %s", node_data)
         message = dataconversion.convert_node(node_data)
-
         try:
             self.sendall(message)
         except IOError:
@@ -336,46 +342,49 @@ class CoreHandler(socketserver.BaseRequestHandler):
         :return: nothing
         """
         logging.debug("handling broadcast link: %s", link_data)
+        options_data = link_data.options
         loss = ""
-        if link_data.loss is not None:
-            loss = str(link_data.loss)
+        if options_data.loss is not None:
+            loss = str(options_data.loss)
         dup = ""
-        if link_data.dup is not None:
-            dup = str(link_data.dup)
+        if options_data.dup is not None:
+            dup = str(options_data.dup)
+        iface1 = link_data.iface1
+        if iface1 is None:
+            iface1 = InterfaceData()
+        iface2 = link_data.iface2
+        if iface2 is None:
+            iface2 = InterfaceData()
 
         tlv_data = structutils.pack_values(
             coreapi.CoreLinkTlv,
             [
                 (LinkTlvs.N1_NUMBER, link_data.node1_id),
                 (LinkTlvs.N2_NUMBER, link_data.node2_id),
-                (LinkTlvs.DELAY, link_data.delay),
-                (LinkTlvs.BANDWIDTH, link_data.bandwidth),
+                (LinkTlvs.DELAY, options_data.delay),
+                (LinkTlvs.BANDWIDTH, options_data.bandwidth),
                 (LinkTlvs.LOSS, loss),
                 (LinkTlvs.DUP, dup),
-                (LinkTlvs.JITTER, link_data.jitter),
-                (LinkTlvs.MER, link_data.mer),
-                (LinkTlvs.BURST, link_data.burst),
-                (LinkTlvs.SESSION, link_data.session),
-                (LinkTlvs.MBURST, link_data.mburst),
-                (LinkTlvs.TYPE, link_data.link_type.value),
-                (LinkTlvs.GUI_ATTRIBUTES, link_data.gui_attributes),
-                (LinkTlvs.UNIDIRECTIONAL, link_data.unidirectional),
-                (LinkTlvs.EMULATION_ID, link_data.emulation_id),
+                (LinkTlvs.JITTER, options_data.jitter),
+                (LinkTlvs.MER, options_data.mer),
+                (LinkTlvs.BURST, options_data.burst),
+                (LinkTlvs.MBURST, options_data.mburst),
+                (LinkTlvs.TYPE, link_data.type.value),
+                (LinkTlvs.UNIDIRECTIONAL, options_data.unidirectional),
                 (LinkTlvs.NETWORK_ID, link_data.network_id),
-                (LinkTlvs.KEY, link_data.key),
-                (LinkTlvs.INTERFACE1_NUMBER, link_data.interface1_id),
-                (LinkTlvs.INTERFACE1_IP4, link_data.interface1_ip4),
-                (LinkTlvs.INTERFACE1_IP4_MASK, link_data.interface1_ip4_mask),
-                (LinkTlvs.INTERFACE1_MAC, link_data.interface1_mac),
-                (LinkTlvs.INTERFACE1_IP6, link_data.interface1_ip6),
-                (LinkTlvs.INTERFACE1_IP6_MASK, link_data.interface1_ip6_mask),
-                (LinkTlvs.INTERFACE2_NUMBER, link_data.interface2_id),
-                (LinkTlvs.INTERFACE2_IP4, link_data.interface2_ip4),
-                (LinkTlvs.INTERFACE2_IP4_MASK, link_data.interface2_ip4_mask),
-                (LinkTlvs.INTERFACE2_MAC, link_data.interface2_mac),
-                (LinkTlvs.INTERFACE2_IP6, link_data.interface2_ip6),
-                (LinkTlvs.INTERFACE2_IP6_MASK, link_data.interface2_ip6_mask),
-                (LinkTlvs.OPAQUE, link_data.opaque),
+                (LinkTlvs.KEY, options_data.key),
+                (LinkTlvs.IFACE1_NUMBER, iface1.id),
+                (LinkTlvs.IFACE1_IP4, iface1.ip4),
+                (LinkTlvs.IFACE1_IP4_MASK, iface1.ip4_mask),
+                (LinkTlvs.IFACE1_MAC, iface1.mac),
+                (LinkTlvs.IFACE1_IP6, iface1.ip6),
+                (LinkTlvs.IFACE1_IP6_MASK, iface1.ip6_mask),
+                (LinkTlvs.IFACE2_NUMBER, iface2.id),
+                (LinkTlvs.IFACE2_IP4, iface2.ip4),
+                (LinkTlvs.IFACE2_IP4_MASK, iface2.ip4_mask),
+                (LinkTlvs.IFACE2_MAC, iface2.mac),
+                (LinkTlvs.IFACE2_IP6, iface2.ip6),
+                (LinkTlvs.IFACE2_IP6_MASK, iface2.ip6_mask),
             ],
         )
 
@@ -709,7 +718,6 @@ class CoreHandler(socketserver.BaseRequestHandler):
 
         options.icon = message.get_tlv(NodeTlvs.ICON.value)
         options.canvas = message.get_tlv(NodeTlvs.CANVAS.value)
-        options.opaque = message.get_tlv(NodeTlvs.OPAQUE.value)
         options.server = message.get_tlv(NodeTlvs.EMULATION_SERVER.value)
 
         services = message.get_tlv(NodeTlvs.SERVICES.value)
@@ -749,56 +757,51 @@ class CoreHandler(socketserver.BaseRequestHandler):
         """
         node1_id = message.get_tlv(LinkTlvs.N1_NUMBER.value)
         node2_id = message.get_tlv(LinkTlvs.N2_NUMBER.value)
-        interface1_data = InterfaceData(
-            id=message.get_tlv(LinkTlvs.INTERFACE1_NUMBER.value),
+        iface1_data = InterfaceData(
+            id=message.get_tlv(LinkTlvs.IFACE1_NUMBER.value),
             name=message.get_tlv(LinkTlvs.INTERFACE1_NAME.value),
-            mac=message.get_tlv(LinkTlvs.INTERFACE1_MAC.value),
-            ip4=message.get_tlv(LinkTlvs.INTERFACE1_IP4.value),
-            ip4_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP4_MASK.value),
-            ip6=message.get_tlv(LinkTlvs.INTERFACE1_IP6.value),
-            ip6_mask=message.get_tlv(LinkTlvs.INTERFACE1_IP6_MASK.value),
+            mac=message.get_tlv(LinkTlvs.IFACE1_MAC.value),
+            ip4=message.get_tlv(LinkTlvs.IFACE1_IP4.value),
+            ip4_mask=message.get_tlv(LinkTlvs.IFACE1_IP4_MASK.value),
+            ip6=message.get_tlv(LinkTlvs.IFACE1_IP6.value),
+            ip6_mask=message.get_tlv(LinkTlvs.IFACE1_IP6_MASK.value),
         )
-        interface2_data = InterfaceData(
-            id=message.get_tlv(LinkTlvs.INTERFACE2_NUMBER.value),
+        iface2_data = InterfaceData(
+            id=message.get_tlv(LinkTlvs.IFACE2_NUMBER.value),
             name=message.get_tlv(LinkTlvs.INTERFACE2_NAME.value),
-            mac=message.get_tlv(LinkTlvs.INTERFACE2_MAC.value),
-            ip4=message.get_tlv(LinkTlvs.INTERFACE2_IP4.value),
-            ip4_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP4_MASK.value),
-            ip6=message.get_tlv(LinkTlvs.INTERFACE2_IP6.value),
-            ip6_mask=message.get_tlv(LinkTlvs.INTERFACE2_IP6_MASK.value),
+            mac=message.get_tlv(LinkTlvs.IFACE2_MAC.value),
+            ip4=message.get_tlv(LinkTlvs.IFACE2_IP4.value),
+            ip4_mask=message.get_tlv(LinkTlvs.IFACE2_IP4_MASK.value),
+            ip6=message.get_tlv(LinkTlvs.IFACE2_IP6.value),
+            ip6_mask=message.get_tlv(LinkTlvs.IFACE2_IP6_MASK.value),
         )
         link_type = LinkTypes.WIRED
         link_type_value = message.get_tlv(LinkTlvs.TYPE.value)
         if link_type_value is not None:
             link_type = LinkTypes(link_type_value)
-        options = LinkOptions(type=link_type)
+        options = LinkOptions()
         options.delay = message.get_tlv(LinkTlvs.DELAY.value)
         options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value)
-        options.session = message.get_tlv(LinkTlvs.SESSION.value)
         options.loss = message.get_tlv(LinkTlvs.LOSS.value)
         options.dup = message.get_tlv(LinkTlvs.DUP.value)
         options.jitter = message.get_tlv(LinkTlvs.JITTER.value)
         options.mer = message.get_tlv(LinkTlvs.MER.value)
         options.burst = message.get_tlv(LinkTlvs.BURST.value)
         options.mburst = message.get_tlv(LinkTlvs.MBURST.value)
-        options.gui_attributes = message.get_tlv(LinkTlvs.GUI_ATTRIBUTES.value)
         options.unidirectional = message.get_tlv(LinkTlvs.UNIDIRECTIONAL.value)
-        options.emulation_id = message.get_tlv(LinkTlvs.EMULATION_ID.value)
-        options.network_id = message.get_tlv(LinkTlvs.NETWORK_ID.value)
         options.key = message.get_tlv(LinkTlvs.KEY.value)
-        options.opaque = message.get_tlv(LinkTlvs.OPAQUE.value)
 
         if message.flags & MessageFlags.ADD.value:
             self.session.add_link(
-                node1_id, node2_id, interface1_data, interface2_data, options
+                node1_id, node2_id, iface1_data, iface2_data, options, link_type
             )
         elif message.flags & MessageFlags.DELETE.value:
             self.session.delete_link(
-                node1_id, node2_id, interface1_data.id, interface2_data.id
+                node1_id, node2_id, iface1_data.id, iface2_data.id, link_type
             )
         else:
             self.session.update_link(
-                node1_id, node2_id, interface1_data.id, interface2_data.id, options
+                node1_id, node2_id, iface1_data.id, iface2_data.id, options, link_type
             )
         return ()
 
@@ -1008,7 +1011,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
             possible_values=message.get_tlv(ConfigTlvs.POSSIBLE_VALUES.value),
             groups=message.get_tlv(ConfigTlvs.GROUPS.value),
             session=message.get_tlv(ConfigTlvs.SESSION.value),
-            interface_number=message.get_tlv(ConfigTlvs.INTERFACE_NUMBER.value),
+            iface_id=message.get_tlv(ConfigTlvs.IFACE_ID.value),
             network_id=message.get_tlv(ConfigTlvs.NETWORK_ID.value),
             opaque=message.get_tlv(ConfigTlvs.OPAQUE.value),
         )
@@ -1325,11 +1328,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
         replies = []
         node_id = config_data.node
         object_name = config_data.object
-        interface_id = config_data.interface_number
+        iface_id = config_data.iface_id
         values_str = config_data.data_values
 
-        if interface_id is not None:
-            node_id = node_id * 1000 + interface_id
+        if iface_id is not None:
+            node_id = node_id * 1000 + iface_id
 
         logging.debug(
             "received configure message for %s nodenum: %s", object_name, node_id
@@ -1375,11 +1378,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
         replies = []
         node_id = config_data.node
         object_name = config_data.object
-        interface_id = config_data.interface_number
+        iface_id = config_data.iface_id
         values_str = config_data.data_values
 
-        if interface_id is not None:
-            node_id = node_id * 1000 + interface_id
+        if iface_id is not None:
+            node_id = node_id * 1000 + iface_id
 
         logging.debug(
             "received configure message for %s nodenum: %s", object_name, node_id
@@ -1407,11 +1410,11 @@ class CoreHandler(socketserver.BaseRequestHandler):
         replies = []
         node_id = config_data.node
         object_name = config_data.object
-        interface_id = config_data.interface_number
+        iface_id = config_data.iface_id
         values_str = config_data.data_values
 
-        if interface_id is not None:
-            node_id = node_id * 1000 + interface_id
+        if iface_id is not None:
+            node_id = node_id * 1000 + iface_id
 
         logging.debug(
             "received configure message for %s nodenum: %s", object_name, node_id
@@ -1505,7 +1508,7 @@ class CoreHandler(socketserver.BaseRequestHandler):
 
         return ()
 
-    def handle_interface_message(self, message):
+    def handle_iface_message(self, message):
         """
         Interface Message handler.
 
@@ -1950,7 +1953,7 @@ class CoreUdpHandler(CoreHandler):
             MessageTypes.REGISTER.value: self.handle_register_message,
             MessageTypes.CONFIG.value: self.handle_config_message,
             MessageTypes.FILE.value: self.handle_file_message,
-            MessageTypes.INTERFACE.value: self.handle_interface_message,
+            MessageTypes.INTERFACE.value: self.handle_iface_message,
             MessageTypes.EVENT.value: self.handle_event_message,
             MessageTypes.SESSION.value: self.handle_session_message,
         }
diff --git a/daemon/core/api/tlv/dataconversion.py b/daemon/core/api/tlv/dataconversion.py
index 876e72a5..8a26300a 100644
--- a/daemon/core/api/tlv/dataconversion.py
+++ b/daemon/core/api/tlv/dataconversion.py
@@ -8,45 +8,39 @@ from typing import Dict, List
 from core.api.tlv import coreapi, structutils
 from core.api.tlv.enumerations import ConfigTlvs, NodeTlvs
 from core.config import ConfigGroup, ConfigurableOptions
-from core.emulator.data import ConfigData
+from core.emulator.data import ConfigData, NodeData
 
 
-def convert_node(node_data):
+def convert_node(node_data: NodeData):
     """
     Convenience method for converting NodeData to a packed TLV message.
 
     :param core.emulator.data.NodeData node_data: node data to convert
     :return: packed node message
     """
-    session = None
-    if node_data.session is not None:
-        session = str(node_data.session)
+    node = node_data.node
     services = None
-    if node_data.services is not None:
-        services = "|".join([x for x in node_data.services])
+    if node.services is not None:
+        services = "|".join([x.name for x in node.services])
+    server = None
+    if node.server is not None:
+        server = node.server.name
     tlv_data = structutils.pack_values(
         coreapi.CoreNodeTlv,
         [
-            (NodeTlvs.NUMBER, node_data.id),
-            (NodeTlvs.TYPE, node_data.node_type.value),
-            (NodeTlvs.NAME, node_data.name),
-            (NodeTlvs.IP_ADDRESS, node_data.ip_address),
-            (NodeTlvs.MAC_ADDRESS, node_data.mac_address),
-            (NodeTlvs.IP6_ADDRESS, node_data.ip6_address),
-            (NodeTlvs.MODEL, node_data.model),
-            (NodeTlvs.EMULATION_ID, node_data.emulation_id),
-            (NodeTlvs.EMULATION_SERVER, node_data.server),
-            (NodeTlvs.SESSION, session),
-            (NodeTlvs.X_POSITION, int(node_data.x_position)),
-            (NodeTlvs.Y_POSITION, int(node_data.y_position)),
-            (NodeTlvs.CANVAS, node_data.canvas),
-            (NodeTlvs.NETWORK_ID, node_data.network_id),
+            (NodeTlvs.NUMBER, node.id),
+            (NodeTlvs.TYPE, node.apitype.value),
+            (NodeTlvs.NAME, node.name),
+            (NodeTlvs.MODEL, node.type),
+            (NodeTlvs.EMULATION_SERVER, server),
+            (NodeTlvs.X_POSITION, int(node.position.x)),
+            (NodeTlvs.Y_POSITION, int(node.position.y)),
+            (NodeTlvs.CANVAS, node.canvas),
             (NodeTlvs.SERVICES, services),
-            (NodeTlvs.LATITUDE, str(node_data.latitude)),
-            (NodeTlvs.LONGITUDE, str(node_data.longitude)),
-            (NodeTlvs.ALTITUDE, str(node_data.altitude)),
-            (NodeTlvs.ICON, node_data.icon),
-            (NodeTlvs.OPAQUE, node_data.opaque),
+            (NodeTlvs.LATITUDE, str(node.position.lat)),
+            (NodeTlvs.LONGITUDE, str(node.position.lon)),
+            (NodeTlvs.ALTITUDE, str(node.position.alt)),
+            (NodeTlvs.ICON, node.icon),
         ],
     )
     return coreapi.CoreNodeMessage.pack(node_data.message_type.value, tlv_data)
@@ -75,7 +69,7 @@ def convert_config(config_data):
             (ConfigTlvs.POSSIBLE_VALUES, config_data.possible_values),
             (ConfigTlvs.GROUPS, config_data.groups),
             (ConfigTlvs.SESSION, session),
-            (ConfigTlvs.INTERFACE_NUMBER, config_data.interface_number),
+            (ConfigTlvs.IFACE_ID, config_data.iface_id),
             (ConfigTlvs.NETWORK_ID, config_data.network_id),
             (ConfigTlvs.OPAQUE, config_data.opaque),
         ],
diff --git a/daemon/core/api/tlv/enumerations.py b/daemon/core/api/tlv/enumerations.py
index 0efb7c99..b4ec254a 100644
--- a/daemon/core/api/tlv/enumerations.py
+++ b/daemon/core/api/tlv/enumerations.py
@@ -72,18 +72,18 @@ class LinkTlvs(Enum):
     EMULATION_ID = 0x23
     NETWORK_ID = 0x24
     KEY = 0x25
-    INTERFACE1_NUMBER = 0x30
-    INTERFACE1_IP4 = 0x31
-    INTERFACE1_IP4_MASK = 0x32
-    INTERFACE1_MAC = 0x33
-    INTERFACE1_IP6 = 0x34
-    INTERFACE1_IP6_MASK = 0x35
-    INTERFACE2_NUMBER = 0x36
-    INTERFACE2_IP4 = 0x37
-    INTERFACE2_IP4_MASK = 0x38
-    INTERFACE2_MAC = 0x39
-    INTERFACE2_IP6 = 0x40
-    INTERFACE2_IP6_MASK = 0x41
+    IFACE1_NUMBER = 0x30
+    IFACE1_IP4 = 0x31
+    IFACE1_IP4_MASK = 0x32
+    IFACE1_MAC = 0x33
+    IFACE1_IP6 = 0x34
+    IFACE1_IP6_MASK = 0x35
+    IFACE2_NUMBER = 0x36
+    IFACE2_IP4 = 0x37
+    IFACE2_IP4_MASK = 0x38
+    IFACE2_MAC = 0x39
+    IFACE2_IP6 = 0x40
+    IFACE2_IP6_MASK = 0x41
     INTERFACE1_NAME = 0x42
     INTERFACE2_NAME = 0x43
     OPAQUE = 0x50
@@ -118,7 +118,7 @@ class ConfigTlvs(Enum):
     POSSIBLE_VALUES = 0x08
     GROUPS = 0x09
     SESSION = 0x0A
-    INTERFACE_NUMBER = 0x0B
+    IFACE_ID = 0x0B
     NETWORK_ID = 0x24
     OPAQUE = 0x50
 
diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py
index c4502f86..8764e32c 100644
--- a/daemon/core/configservices/frrservices/services.py
+++ b/daemon/core/configservices/frrservices/services.py
@@ -13,33 +13,33 @@ from core.nodes.network import WlanNode
 GROUP = "FRR"
 
 
-def has_mtu_mismatch(ifc: CoreInterface) -> bool:
+def has_mtu_mismatch(iface: CoreInterface) -> bool:
     """
     Helper to detect MTU mismatch and add the appropriate FRR
     mtu-ignore command. This is needed when e.g. a node is linked via a
     GreTap device.
     """
-    if ifc.mtu != 1500:
+    if iface.mtu != 1500:
         return True
-    if not ifc.net:
+    if not iface.net:
         return False
-    for i in ifc.net.netifs():
-        if i.mtu != ifc.mtu:
+    for iface in iface.net.get_ifaces():
+        if iface.mtu != iface.mtu:
             return True
     return False
 
 
-def get_min_mtu(ifc):
+def get_min_mtu(iface):
     """
     Helper to discover the minimum MTU of interfaces linked with the
     given interface.
     """
-    mtu = ifc.mtu
-    if not ifc.net:
+    mtu = iface.mtu
+    if not iface.net:
         return mtu
-    for i in ifc.net.netifs():
-        if i.mtu < mtu:
-            mtu = i.mtu
+    for iface in iface.net.get_ifaces():
+        if iface.mtu < mtu:
+            mtu = iface.mtu
     return mtu
 
 
@@ -47,10 +47,8 @@ def get_router_id(node: CoreNodeBase) -> str:
     """
     Helper to return the first IPv4 address of a node as its router ID.
     """
-    for ifc in node.netifs():
-        if getattr(ifc, "control", False):
-            continue
-        for a in ifc.addrlist:
+    for iface in node.get_ifaces(control=False):
+        for a in iface.addrlist:
             a = a.split("/")[0]
             if netaddr.valid_ipv4(a):
                 return a
@@ -97,25 +95,25 @@ class FRRZebra(ConfigService):
                 want_ip6 = True
             services.append(service)
 
-        interfaces = []
-        for ifc in self.node.netifs():
+        ifaces = []
+        for iface in self.node.get_ifaces():
             ip4s = []
             ip6s = []
-            for x in ifc.addrlist:
+            for x in iface.addrlist:
                 addr = x.split("/")[0]
                 if netaddr.valid_ipv4(addr):
                     ip4s.append(x)
                 else:
                     ip6s.append(x)
-            is_control = getattr(ifc, "control", False)
-            interfaces.append((ifc, ip4s, ip6s, is_control))
+            is_control = getattr(iface, "control", False)
+            ifaces.append((iface, ip4s, ip6s, is_control))
 
         return dict(
             frr_conf=frr_conf,
             frr_sbin_search=frr_sbin_search,
             frr_bin_search=frr_bin_search,
             frr_state_dir=constants.FRR_STATE_DIR,
-            interfaces=interfaces,
+            ifaces=ifaces,
             want_ip4=want_ip4,
             want_ip6=want_ip6,
             services=services,
@@ -138,7 +136,7 @@ class FrrService(abc.ABC):
     ipv6_routing = False
 
     @abc.abstractmethod
-    def frr_interface_config(self, ifc: CoreInterface) -> str:
+    def frr_iface_config(self, iface: CoreInterface) -> str:
         raise NotImplementedError
 
     @abc.abstractmethod
@@ -162,10 +160,8 @@ class FRROspfv2(FrrService, ConfigService):
     def frr_config(self) -> str:
         router_id = get_router_id(self.node)
         addresses = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            for a in ifc.addrlist:
+        for iface in self.node.get_ifaces(control=False):
+            for a in iface.addrlist:
                 addr = a.split("/")[0]
                 if netaddr.valid_ipv4(addr):
                     addresses.append(a)
@@ -180,8 +176,8 @@ class FRROspfv2(FrrService, ConfigService):
         """
         return self.render_text(text, data)
 
-    def frr_interface_config(self, ifc: CoreInterface) -> str:
-        if has_mtu_mismatch(ifc):
+    def frr_iface_config(self, iface: CoreInterface) -> str:
+        if has_mtu_mismatch(iface):
             return "ip ospf mtu-ignore"
         else:
             return ""
@@ -203,10 +199,8 @@ class FRROspfv3(FrrService, ConfigService):
     def frr_config(self) -> str:
         router_id = get_router_id(self.node)
         ifnames = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifnames.append(ifc.name)
+        for iface in self.node.get_ifaces(control=False):
+            ifnames.append(iface.name)
         data = dict(router_id=router_id, ifnames=ifnames)
         text = """
         router ospf6
@@ -218,9 +212,9 @@ class FRROspfv3(FrrService, ConfigService):
         """
         return self.render_text(text, data)
 
-    def frr_interface_config(self, ifc: CoreInterface) -> str:
-        mtu = get_min_mtu(ifc)
-        if mtu < ifc.mtu:
+    def frr_iface_config(self, iface: CoreInterface) -> str:
+        mtu = get_min_mtu(iface)
+        if mtu < iface.mtu:
             return f"ipv6 ospf6 ifmtu {mtu}"
         else:
             return ""
@@ -254,7 +248,7 @@ class FRRBgp(FrrService, ConfigService):
         """
         return self.clean_text(text)
 
-    def frr_interface_config(self, ifc: CoreInterface) -> str:
+    def frr_iface_config(self, iface: CoreInterface) -> str:
         return ""
 
 
@@ -279,7 +273,7 @@ class FRRRip(FrrService, ConfigService):
         """
         return self.clean_text(text)
 
-    def frr_interface_config(self, ifc: CoreInterface) -> str:
+    def frr_iface_config(self, iface: CoreInterface) -> str:
         return ""
 
 
@@ -304,7 +298,7 @@ class FRRRipng(FrrService, ConfigService):
         """
         return self.clean_text(text)
 
-    def frr_interface_config(self, ifc: CoreInterface) -> str:
+    def frr_iface_config(self, iface: CoreInterface) -> str:
         return ""
 
 
@@ -321,10 +315,8 @@ class FRRBabel(FrrService, ConfigService):
 
     def frr_config(self) -> str:
         ifnames = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifnames.append(ifc.name)
+        for iface in self.node.get_ifaces(control=False):
+            ifnames.append(iface.name)
         text = """
         router babel
           % for ifname in ifnames:
@@ -337,8 +329,8 @@ class FRRBabel(FrrService, ConfigService):
         data = dict(ifnames=ifnames)
         return self.render_text(text, data)
 
-    def frr_interface_config(self, ifc: CoreInterface) -> str:
-        if isinstance(ifc.net, (WlanNode, EmaneNet)):
+    def frr_iface_config(self, iface: CoreInterface) -> str:
+        if isinstance(iface.net, (WlanNode, EmaneNet)):
             text = """
             babel wireless
             no babel split-horizon
@@ -363,9 +355,9 @@ class FRRpimd(FrrService, ConfigService):
 
     def frr_config(self) -> str:
         ifname = "eth0"
-        for ifc in self.node.netifs():
-            if ifc.name != "lo":
-                ifname = ifc.name
+        for iface in self.node.get_ifaces():
+            if iface.name != "lo":
+                ifname = iface.name
                 break
 
         text = f"""
@@ -382,7 +374,7 @@ class FRRpimd(FrrService, ConfigService):
         """
         return self.clean_text(text)
 
-    def frr_interface_config(self, ifc: CoreInterface) -> str:
+    def frr_iface_config(self, iface: CoreInterface) -> str:
         text = """
         ip mfea
         ip igmp
diff --git a/daemon/core/configservices/frrservices/templates/frr.conf b/daemon/core/configservices/frrservices/templates/frr.conf
index 748c8692..8e036136 100644
--- a/daemon/core/configservices/frrservices/templates/frr.conf
+++ b/daemon/core/configservices/frrservices/templates/frr.conf
@@ -1,5 +1,5 @@
-% for ifc, ip4s, ip6s, is_control in interfaces:
-interface ${ifc.name}
+% for iface, ip4s, ip6s, is_control in ifaces:
+interface ${iface.name}
     % if want_ip4:
         % for addr in ip4s:
     ip address ${addr}
@@ -12,7 +12,7 @@ interface ${ifc.name}
     % endif
     % if not is_control:
         % for service in services:
-            % for line in service.frr_interface_config(ifc).split("\n"):
+            % for line in service.frr_iface_config(iface).split("\n"):
     ${line}
             % endfor
         % endfor
diff --git a/daemon/core/configservices/frrservices/templates/frrboot.sh b/daemon/core/configservices/frrservices/templates/frrboot.sh
index 3c14cd1a..db47b6d1 100644
--- a/daemon/core/configservices/frrservices/templates/frrboot.sh
+++ b/daemon/core/configservices/frrservices/templates/frrboot.sh
@@ -98,8 +98,8 @@ confcheck
 bootfrr
 
 # reset interfaces
-% for ifc, _, _ , _ in interfaces:
-ip link set dev ${ifc.name} down
+% for iface, _, _ , _ in ifaces:
+ip link set dev ${iface.name} down
 sleep 1
-ip link set dev ${ifc.name} up
+ip link set dev ${iface.name} up
 % endfor
diff --git a/daemon/core/configservices/nrlservices/services.py b/daemon/core/configservices/nrlservices/services.py
index 3dddf1ba..ca95b8f6 100644
--- a/daemon/core/configservices/nrlservices/services.py
+++ b/daemon/core/configservices/nrlservices/services.py
@@ -24,8 +24,8 @@ class MgenSinkService(ConfigService):
 
     def data(self) -> Dict[str, Any]:
         ifnames = []
-        for ifc in self.node.netifs():
-            name = utils.sysctl_devname(ifc.name)
+        for iface in self.node.get_ifaces():
+            name = utils.sysctl_devname(iface.name)
             ifnames.append(name)
         return dict(ifnames=ifnames)
 
@@ -47,10 +47,8 @@ class NrlNhdp(ConfigService):
     def data(self) -> Dict[str, Any]:
         has_smf = "SMF" in self.node.config_services
         ifnames = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifnames.append(ifc.name)
+        for iface in self.node.get_ifaces(control=False):
+            ifnames.append(iface.name)
         return dict(has_smf=has_smf, ifnames=ifnames)
 
 
@@ -74,13 +72,11 @@ class NrlSmf(ConfigService):
         has_olsr = "OLSR" in self.node.config_services
         ifnames = []
         ip4_prefix = None
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifnames.append(ifc.name)
+        for iface in self.node.get_ifaces(control=False):
+            ifnames.append(iface.name)
             if ip4_prefix:
                 continue
-            for a in ifc.addrlist:
+            for a in iface.addrlist:
                 a = a.split("/")[0]
                 if netaddr.valid_ipv4(a):
                     ip4_prefix = f"{a}/{24}"
@@ -112,10 +108,8 @@ class NrlOlsr(ConfigService):
         has_smf = "SMF" in self.node.config_services
         has_zebra = "zebra" in self.node.config_services
         ifname = None
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifname = ifc.name
+        for iface in self.node.get_ifaces(control=False):
+            ifname = iface.name
             break
         return dict(has_smf=has_smf, has_zebra=has_zebra, ifname=ifname)
 
@@ -137,10 +131,8 @@ class NrlOlsrv2(ConfigService):
     def data(self) -> Dict[str, Any]:
         has_smf = "SMF" in self.node.config_services
         ifnames = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifnames.append(ifc.name)
+        for iface in self.node.get_ifaces(control=False):
+            ifnames.append(iface.name)
         return dict(has_smf=has_smf, ifnames=ifnames)
 
 
@@ -161,10 +153,8 @@ class OlsrOrg(ConfigService):
     def data(self) -> Dict[str, Any]:
         has_smf = "SMF" in self.node.config_services
         ifnames = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifnames.append(ifc.name)
+        for iface in self.node.get_ifaces(control=False):
+            ifnames.append(iface.name)
         return dict(has_smf=has_smf, ifnames=ifnames)
 
 
@@ -199,12 +189,10 @@ class Arouted(ConfigService):
 
     def data(self) -> Dict[str, Any]:
         ip4_prefix = None
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
+        for iface in self.node.get_ifaces(control=False):
             if ip4_prefix:
                 continue
-            for a in ifc.addrlist:
+            for a in iface.addrlist:
                 a = a.split("/")[0]
                 if netaddr.valid_ipv4(a):
                     ip4_prefix = f"{a}/{24}"
diff --git a/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh b/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh
index 00b7e11d..4513dfe9 100644
--- a/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh
+++ b/daemon/core/configservices/nrlservices/templates/nrlnhdp.sh
@@ -1,7 +1,7 @@
 <%
-  interfaces = "-i " + " -i ".join(ifnames)
+  ifaces = "-i " + " -i ".join(ifnames)
   smf = ""
   if has_smf:
     smf = "-flooding ecds -smfClient %s_smf" % node.name
 %>
-nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${interfaces}
+nrlnhdp -l /var/log/nrlnhdp.log -rpipe ${node.name}_nhdp ${smf} ${ifaces}
diff --git a/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh b/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh
index d7a8d3b6..81196e26 100644
--- a/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh
+++ b/daemon/core/configservices/nrlservices/templates/nrlolsrv2.sh
@@ -1,7 +1,7 @@
 <%
-  interfaces = "-i " + " -i ".join(ifnames)
+  ifaces = "-i " + " -i ".join(ifnames)
   smf = ""
   if has_smf:
     smf = "-flooding ecds -smfClient %s_smf" % node.name
 %>
-nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${interfaces}
+nrlolsrv2 -l /var/log/nrlolsrv2.log -rpipe ${node.name}_olsrv2 -p olsr ${smf} ${ifaces}
diff --git a/daemon/core/configservices/nrlservices/templates/olsrd.sh b/daemon/core/configservices/nrlservices/templates/olsrd.sh
index 076f049b..3040ca6b 100644
--- a/daemon/core/configservices/nrlservices/templates/olsrd.sh
+++ b/daemon/core/configservices/nrlservices/templates/olsrd.sh
@@ -1,4 +1,4 @@
 <%
-  interfaces = "-i " + " -i ".join(ifnames)
+  ifaces = "-i " + " -i ".join(ifnames)
 %>
-olsrd ${interfaces}
+olsrd ${ifaces}
diff --git a/daemon/core/configservices/nrlservices/templates/startsmf.sh b/daemon/core/configservices/nrlservices/templates/startsmf.sh
index 67fc0fe6..921568de 100644
--- a/daemon/core/configservices/nrlservices/templates/startsmf.sh
+++ b/daemon/core/configservices/nrlservices/templates/startsmf.sh
@@ -1,5 +1,5 @@
 <%
-  interfaces = ",".join(ifnames)
+  ifaces = ",".join(ifnames)
   arouted = ""
   if has_arouted:
     arouted = "tap %s_tap unicast %s push lo,%s resequence on" % (node.name, ip4_prefix, ifnames[0])
@@ -12,4 +12,4 @@
 %>
 #!/bin/sh
 # auto-generated by NrlSmf service
-nrlsmf instance ${node.name}_smf ${interfaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
+nrlsmf instance ${node.name}_smf ${ifaces} ${arouted} ${flood} hash MD5 log /var/log/nrlsmf.log < /dev/null > /dev/null 2>&1 &
diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py
index 32ce99be..19e21476 100644
--- a/daemon/core/configservices/quaggaservices/services.py
+++ b/daemon/core/configservices/quaggaservices/services.py
@@ -14,33 +14,33 @@ from core.nodes.network import WlanNode
 GROUP = "Quagga"
 
 
-def has_mtu_mismatch(ifc: CoreInterface) -> bool:
+def has_mtu_mismatch(iface: CoreInterface) -> bool:
     """
     Helper to detect MTU mismatch and add the appropriate OSPF
     mtu-ignore command. This is needed when e.g. a node is linked via a
     GreTap device.
     """
-    if ifc.mtu != 1500:
+    if iface.mtu != 1500:
         return True
-    if not ifc.net:
+    if not iface.net:
         return False
-    for i in ifc.net.netifs():
-        if i.mtu != ifc.mtu:
+    for iface in iface.net.get_ifaces():
+        if iface.mtu != iface.mtu:
             return True
     return False
 
 
-def get_min_mtu(ifc):
+def get_min_mtu(iface: CoreInterface):
     """
     Helper to discover the minimum MTU of interfaces linked with the
     given interface.
     """
-    mtu = ifc.mtu
-    if not ifc.net:
+    mtu = iface.mtu
+    if not iface.net:
         return mtu
-    for i in ifc.net.netifs():
-        if i.mtu < mtu:
-            mtu = i.mtu
+    for iface in iface.net.get_ifaces():
+        if iface.mtu < mtu:
+            mtu = iface.mtu
     return mtu
 
 
@@ -48,10 +48,8 @@ def get_router_id(node: CoreNodeBase) -> str:
     """
     Helper to return the first IPv4 address of a node as its router ID.
     """
-    for ifc in node.netifs():
-        if getattr(ifc, "control", False):
-            continue
-        for a in ifc.addrlist:
+    for iface in node.get_ifaces(control=False):
+        for a in iface.addrlist:
             a = a.split("/")[0]
             if netaddr.valid_ipv4(a):
                 return a
@@ -98,25 +96,25 @@ class Zebra(ConfigService):
                 want_ip6 = True
             services.append(service)
 
-        interfaces = []
-        for ifc in self.node.netifs():
+        ifaces = []
+        for iface in self.node.get_ifaces():
             ip4s = []
             ip6s = []
-            for x in ifc.addrlist:
+            for x in iface.addrlist:
                 addr = x.split("/")[0]
                 if netaddr.valid_ipv4(addr):
                     ip4s.append(x)
                 else:
                     ip6s.append(x)
-            is_control = getattr(ifc, "control", False)
-            interfaces.append((ifc, ip4s, ip6s, is_control))
+            is_control = getattr(iface, "control", False)
+            ifaces.append((iface, ip4s, ip6s, is_control))
 
         return dict(
             quagga_bin_search=quagga_bin_search,
             quagga_sbin_search=quagga_sbin_search,
             quagga_state_dir=quagga_state_dir,
             quagga_conf=quagga_conf,
-            interfaces=interfaces,
+            ifaces=ifaces,
             want_ip4=want_ip4,
             want_ip6=want_ip6,
             services=services,
@@ -139,7 +137,7 @@ class QuaggaService(abc.ABC):
     ipv6_routing = False
 
     @abc.abstractmethod
-    def quagga_interface_config(self, ifc: CoreInterface) -> str:
+    def quagga_iface_config(self, iface: CoreInterface) -> str:
         raise NotImplementedError
 
     @abc.abstractmethod
@@ -159,8 +157,8 @@ class Ospfv2(QuaggaService, ConfigService):
     shutdown = ["killall ospfd"]
     ipv4_routing = True
 
-    def quagga_interface_config(self, ifc: CoreInterface) -> str:
-        if has_mtu_mismatch(ifc):
+    def quagga_iface_config(self, iface: CoreInterface) -> str:
+        if has_mtu_mismatch(iface):
             return "ip ospf mtu-ignore"
         else:
             return ""
@@ -168,10 +166,8 @@ class Ospfv2(QuaggaService, ConfigService):
     def quagga_config(self) -> str:
         router_id = get_router_id(self.node)
         addresses = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            for a in ifc.addrlist:
+        for iface in self.node.get_ifaces(control=False):
+            for a in iface.addrlist:
                 addr = a.split("/")[0]
                 if netaddr.valid_ipv4(addr):
                     addresses.append(a)
@@ -200,9 +196,9 @@ class Ospfv3(QuaggaService, ConfigService):
     ipv4_routing = True
     ipv6_routing = True
 
-    def quagga_interface_config(self, ifc: CoreInterface) -> str:
-        mtu = get_min_mtu(ifc)
-        if mtu < ifc.mtu:
+    def quagga_iface_config(self, iface: CoreInterface) -> str:
+        mtu = get_min_mtu(iface)
+        if mtu < iface.mtu:
             return f"ipv6 ospf6 ifmtu {mtu}"
         else:
             return ""
@@ -210,10 +206,8 @@ class Ospfv3(QuaggaService, ConfigService):
     def quagga_config(self) -> str:
         router_id = get_router_id(self.node)
         ifnames = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifnames.append(ifc.name)
+        for iface in self.node.get_ifaces(control=False):
+            ifnames.append(iface.name)
         data = dict(router_id=router_id, ifnames=ifnames)
         text = """
         router ospf6
@@ -238,14 +232,14 @@ class Ospfv3mdr(Ospfv3):
     name = "OSPFv3MDR"
 
     def data(self) -> Dict[str, Any]:
-        for ifc in self.node.netifs():
-            is_wireless = isinstance(ifc.net, (WlanNode, EmaneNet))
+        for iface in self.node.get_ifaces():
+            is_wireless = isinstance(iface.net, (WlanNode, EmaneNet))
             logging.info("MDR wireless: %s", is_wireless)
         return dict()
 
-    def quagga_interface_config(self, ifc: CoreInterface) -> str:
-        config = super().quagga_interface_config(ifc)
-        if isinstance(ifc.net, (WlanNode, EmaneNet)):
+    def quagga_iface_config(self, iface: CoreInterface) -> str:
+        config = super().quagga_iface_config(iface)
+        if isinstance(iface.net, (WlanNode, EmaneNet)):
             config = self.clean_text(
                 f"""
                 {config}
@@ -277,7 +271,7 @@ class Bgp(QuaggaService, ConfigService):
     def quagga_config(self) -> str:
         return ""
 
-    def quagga_interface_config(self, ifc: CoreInterface) -> str:
+    def quagga_iface_config(self, iface: CoreInterface) -> str:
         router_id = get_router_id(self.node)
         text = f"""
         ! BGP configuration
@@ -313,7 +307,7 @@ class Rip(QuaggaService, ConfigService):
         """
         return self.clean_text(text)
 
-    def quagga_interface_config(self, ifc: CoreInterface) -> str:
+    def quagga_iface_config(self, iface: CoreInterface) -> str:
         return ""
 
 
@@ -338,7 +332,7 @@ class Ripng(QuaggaService, ConfigService):
         """
         return self.clean_text(text)
 
-    def quagga_interface_config(self, ifc: CoreInterface) -> str:
+    def quagga_iface_config(self, iface: CoreInterface) -> str:
         return ""
 
 
@@ -355,10 +349,8 @@ class Babel(QuaggaService, ConfigService):
 
     def quagga_config(self) -> str:
         ifnames = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifnames.append(ifc.name)
+        for iface in self.node.get_ifaces(control=False):
+            ifnames.append(iface.name)
         text = """
         router babel
           % for ifname in ifnames:
@@ -371,8 +363,8 @@ class Babel(QuaggaService, ConfigService):
         data = dict(ifnames=ifnames)
         return self.render_text(text, data)
 
-    def quagga_interface_config(self, ifc: CoreInterface) -> str:
-        if isinstance(ifc.net, (WlanNode, EmaneNet)):
+    def quagga_iface_config(self, iface: CoreInterface) -> str:
+        if isinstance(iface.net, (WlanNode, EmaneNet)):
             text = """
             babel wireless
             no babel split-horizon
@@ -397,9 +389,9 @@ class Xpimd(QuaggaService, ConfigService):
 
     def quagga_config(self) -> str:
         ifname = "eth0"
-        for ifc in self.node.netifs():
-            if ifc.name != "lo":
-                ifname = ifc.name
+        for iface in self.node.get_ifaces():
+            if iface.name != "lo":
+                ifname = iface.name
                 break
 
         text = f"""
@@ -416,7 +408,7 @@ class Xpimd(QuaggaService, ConfigService):
         """
         return self.clean_text(text)
 
-    def quagga_interface_config(self, ifc: CoreInterface) -> str:
+    def quagga_iface_config(self, iface: CoreInterface) -> str:
         text = """
         ip mfea
         ip pim
diff --git a/daemon/core/configservices/quaggaservices/templates/Quagga.conf b/daemon/core/configservices/quaggaservices/templates/Quagga.conf
index 853b1707..1d69838f 100644
--- a/daemon/core/configservices/quaggaservices/templates/Quagga.conf
+++ b/daemon/core/configservices/quaggaservices/templates/Quagga.conf
@@ -1,5 +1,5 @@
-% for ifc, ip4s, ip6s, is_control in interfaces:
-interface ${ifc.name}
+% for iface, ip4s, ip6s, is_control in ifaces:
+interface ${iface.name}
     % if want_ip4:
         % for addr in ip4s:
     ip address ${addr}
@@ -12,7 +12,7 @@ interface ${ifc.name}
     % endif
     % if not is_control:
         % for service in services:
-            % for line in service.quagga_interface_config(ifc).split("\n"):
+            % for line in service.quagga_iface_config(iface).split("\n"):
     ${line}
             % endfor
         % endfor
diff --git a/daemon/core/configservices/sercurityservices/services.py b/daemon/core/configservices/sercurityservices/services.py
index 17f081cd..6e92bf62 100644
--- a/daemon/core/configservices/sercurityservices/services.py
+++ b/daemon/core/configservices/sercurityservices/services.py
@@ -78,10 +78,8 @@ class VpnServer(ConfigService):
 
     def data(self) -> Dict[str, Any]:
         address = None
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            for x in ifc.addrlist:
+        for iface in self.node.get_ifaces(control=False):
+            for x in iface.addrlist:
                 addr = x.split("/")[0]
                 if netaddr.valid_ipv4(addr):
                     address = addr
@@ -134,8 +132,6 @@ class Nat(ConfigService):
 
     def data(self) -> Dict[str, Any]:
         ifnames = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifnames.append(ifc.name)
+        for iface in self.node.get_ifaces(control=False):
+            ifnames.append(iface.name)
         return dict(ifnames=ifnames)
diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py
index 8ddf1cc7..5aa3bb54 100644
--- a/daemon/core/configservices/utilservices/services.py
+++ b/daemon/core/configservices/utilservices/services.py
@@ -25,10 +25,10 @@ class DefaultRouteService(ConfigService):
     def data(self) -> Dict[str, Any]:
         # only add default routes for linked routing nodes
         routes = []
-        netifs = self.node.netifs(sort=True)
-        if netifs:
-            netif = netifs[0]
-            for x in netif.addrlist:
+        ifaces = self.node.get_ifaces()
+        if ifaces:
+            iface = ifaces[0]
+            for x in iface.addrlist:
                 net = netaddr.IPNetwork(x).cidr
                 if net.size > 1:
                     router = net[1]
@@ -52,10 +52,8 @@ class DefaultMulticastRouteService(ConfigService):
 
     def data(self) -> Dict[str, Any]:
         ifname = None
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifname = ifc.name
+        for iface in self.node.get_ifaces(control=False):
+            ifname = iface.name
             break
         return dict(ifname=ifname)
 
@@ -76,10 +74,8 @@ class StaticRouteService(ConfigService):
 
     def data(self) -> Dict[str, Any]:
         routes = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            for x in ifc.addrlist:
+        for iface in self.node.get_ifaces(control=False):
+            for x in iface.addrlist:
                 addr = x.split("/")[0]
                 if netaddr.valid_ipv6(addr):
                     dst = "3ffe:4::/64"
@@ -107,8 +103,8 @@ class IpForwardService(ConfigService):
 
     def data(self) -> Dict[str, Any]:
         devnames = []
-        for ifc in self.node.netifs():
-            devname = utils.sysctl_devname(ifc.name)
+        for iface in self.node.get_ifaces():
+            devname = utils.sysctl_devname(iface.name)
             devnames.append(devname)
         return dict(devnames=devnames)
 
@@ -151,10 +147,8 @@ class DhcpService(ConfigService):
 
     def data(self) -> Dict[str, Any]:
         subnets = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            for x in ifc.addrlist:
+        for iface in self.node.get_ifaces(control=False):
+            for x in iface.addrlist:
                 addr = x.split("/")[0]
                 if netaddr.valid_ipv4(addr):
                     net = netaddr.IPNetwork(x)
@@ -182,10 +176,8 @@ class DhcpClientService(ConfigService):
 
     def data(self) -> Dict[str, Any]:
         ifnames = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifnames.append(ifc.name)
+        for iface in self.node.get_ifaces(control=False):
+            ifnames.append(iface.name)
         return dict(ifnames=ifnames)
 
 
@@ -220,10 +212,8 @@ class PcapService(ConfigService):
 
     def data(self) -> Dict[str, Any]:
         ifnames = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            ifnames.append(ifc.name)
+        for iface in self.node.get_ifaces(control=False):
+            ifnames.append(iface.name)
         return dict()
 
 
@@ -242,19 +232,17 @@ class RadvdService(ConfigService):
     modes = {}
 
     def data(self) -> Dict[str, Any]:
-        interfaces = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
+        ifaces = []
+        for iface in self.node.get_ifaces(control=False):
             prefixes = []
-            for x in ifc.addrlist:
+            for x in iface.addrlist:
                 addr = x.split("/")[0]
                 if netaddr.valid_ipv6(addr):
                     prefixes.append(x)
             if not prefixes:
                 continue
-            interfaces.append((ifc.name, prefixes))
-        return dict(interfaces=interfaces)
+            ifaces.append((iface.name, prefixes))
+        return dict(ifaces=ifaces)
 
 
 class AtdService(ConfigService):
@@ -294,9 +282,7 @@ class HttpService(ConfigService):
     modes = {}
 
     def data(self) -> Dict[str, Any]:
-        interfaces = []
-        for ifc in self.node.netifs():
-            if getattr(ifc, "control", False):
-                continue
-            interfaces.append(ifc)
-        return dict(interfaces=interfaces)
+        ifaces = []
+        for iface in self.node.get_ifaces(control=False):
+            ifaces.append(iface)
+        return dict(ifaces=ifaces)
diff --git a/daemon/core/configservices/utilservices/templates/index.html b/daemon/core/configservices/utilservices/templates/index.html
index aaf9d9fa..bed270ae 100644
--- a/daemon/core/configservices/utilservices/templates/index.html
+++ b/daemon/core/configservices/utilservices/templates/index.html
@@ -5,8 +5,8 @@
 <p>This is the default web page for this server.</p>
 <p>The web server software is running but no content has been added, yet.</p>
 <ul>
-% for ifc in interfaces:
-    <li>${ifc.name} - ${ifc.addrlist}</li>
+% for iface in ifaces:
+    <li>${iface.name} - ${iface.addrlist}</li>
 % endfor
 </ul>
 </body>
diff --git a/daemon/core/emane/commeffect.py b/daemon/core/emane/commeffect.py
index 21252b6f..610099f1 100644
--- a/daemon/core/emane/commeffect.py
+++ b/daemon/core/emane/commeffect.py
@@ -11,7 +11,7 @@ from lxml import etree
 from core.config import ConfigGroup, Configuration
 from core.emane import emanemanifest, emanemodel
 from core.emane.nodes import EmaneNet
-from core.emulator.emudata import LinkOptions
+from core.emulator.data import LinkOptions
 from core.emulator.enumerations import TransportType
 from core.nodes.interface import CoreInterface
 from core.xml import emanexml
@@ -63,7 +63,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
         return [ConfigGroup("CommEffect SHIM Parameters", 1, len(cls.configurations()))]
 
     def build_xml_files(
-        self, config: Dict[str, str], interface: CoreInterface = None
+        self, config: Dict[str, str], iface: CoreInterface = None
     ) -> None:
         """
         Build the necessary nem and commeffect XMLs in the given path.
@@ -72,17 +72,17 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
         nXXemane_commeffectnem.xml, nXXemane_commeffectshim.xml are used.
 
         :param config: emane model configuration for the node and interface
-        :param interface: interface for the emane node
+        :param iface: interface for the emane node
         :return: nothing
         """
         # retrieve xml names
-        nem_name = emanexml.nem_file_name(self, interface)
-        shim_name = emanexml.shim_file_name(self, interface)
+        nem_name = emanexml.nem_file_name(self, iface)
+        shim_name = emanexml.shim_file_name(self, iface)
 
         # create and write nem document
         nem_element = etree.Element("nem", name=f"{self.name} NEM", type="unstructured")
         transport_type = TransportType.VIRTUAL
-        if interface and interface.transport_type == TransportType.RAW:
+        if iface and iface.transport_type == TransportType.RAW:
             transport_type = TransportType.RAW
         transport_file = emanexml.transport_file_name(self.id, transport_type)
         etree.SubElement(nem_element, "transport", definition=transport_file)
@@ -115,7 +115,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
         emanexml.create_file(shim_element, "shim", shim_file)
 
     def linkconfig(
-        self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
+        self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
     ) -> None:
         """
         Generate CommEffect events when a Link Message is received having
@@ -126,7 +126,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
             logging.warning("%s: EMANE event service unavailable", self.name)
             return
 
-        if netif is None or netif2 is None:
+        if iface is None or iface2 is None:
             logging.warning("%s: missing NEM information", self.name)
             return
 
@@ -134,8 +134,8 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
         # TODO: may want to split out seconds portion of delay and jitter
         event = CommEffectEvent()
         emane_node = self.session.get_node(self.id, EmaneNet)
-        nemid = emane_node.getnemid(netif)
-        nemid2 = emane_node.getnemid(netif2)
+        nemid = emane_node.getnemid(iface)
+        nemid2 = emane_node.getnemid(iface2)
         logging.info("sending comm effect event")
         event.append(
             nemid,
diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py
index cb978cb9..fc561b5f 100644
--- a/daemon/core/emane/emanemanager.py
+++ b/daemon/core/emane/emanemanager.py
@@ -111,41 +111,39 @@ class EmaneManager(ModelManager):
         self.event_device: Optional[str] = None
         self.emane_check()
 
-    def getifcconfig(
-        self, node_id: int, interface: CoreInterface, model_name: str
+    def get_iface_config(
+        self, node_id: int, iface: CoreInterface, model_name: str
     ) -> Dict[str, str]:
         """
         Retrieve interface configuration or node configuration if not provided.
 
         :param node_id: node id
-        :param interface: node interface
+        :param iface: node interface
         :param model_name: model to get configuration for
         :return: node/interface model configuration
         """
         # use the network-wide config values or interface(NEM)-specific values?
-        if interface is None:
+        if iface is None:
             return self.get_configs(node_id=node_id, config_type=model_name)
         else:
             # don"t use default values when interface config is the same as net
-            # note here that using ifc.node.id as key allows for only one type
+            # note here that using iface.node.id as key allows for only one type
             # of each model per node;
             # TODO: use both node and interface as key
 
-            # Adamson change: first check for iface config keyed by "node:ifc.name"
+            # Adamson change: first check for iface config keyed by "node:iface.name"
             # (so that nodes w/ multiple interfaces of same conftype can have
             #  different configs for each separate interface)
-            key = 1000 * interface.node.id
-            if interface.netindex is not None:
-                key += interface.netindex
+            key = 1000 * iface.node.id
+            if iface.node_id is not None:
+                key += iface.node_id
 
             # try retrieve interface specific configuration, avoid getting defaults
             config = self.get_configs(node_id=key, config_type=model_name)
 
             # otherwise retrieve the interfaces node configuration, avoid using defaults
             if not config:
-                config = self.get_configs(
-                    node_id=interface.node.id, config_type=model_name
-                )
+                config = self.get_configs(node_id=iface.node.id, config_type=model_name)
 
             # get non interface config, when none found
             if not config:
@@ -265,8 +263,8 @@ class EmaneManager(ModelManager):
         # assumes self._objslock already held
         nodes = set()
         for emane_net in self._emane_nets.values():
-            for netif in emane_net.netifs():
-                nodes.add(netif.node)
+            for iface in emane_net.get_ifaces():
+                nodes.add(iface.node)
         return nodes
 
     def setup(self) -> int:
@@ -352,13 +350,13 @@ class EmaneManager(ModelManager):
 
             if self.numnems() > 0:
                 self.startdaemons()
-                self.installnetifs()
+                self.install_ifaces()
 
             for node_id in self._emane_nets:
                 emane_node = self._emane_nets[node_id]
-                for netif in emane_node.netifs():
+                for iface in emane_node.get_ifaces():
                     nems.append(
-                        (netif.node.name, netif.name, emane_node.getnemid(netif))
+                        (iface.node.name, iface.name, emane_node.getnemid(iface))
                     )
 
         if nems:
@@ -392,8 +390,8 @@ class EmaneManager(ModelManager):
                     emane_node.name,
                 )
                 emane_node.model.post_startup()
-                for netif in emane_node.netifs():
-                    netif.setposition()
+                for iface in emane_node.get_ifaces():
+                    iface.setposition()
 
     def reset(self) -> None:
         """
@@ -420,7 +418,7 @@ class EmaneManager(ModelManager):
             logging.info("stopping EMANE daemons")
             if self.links_enabled():
                 self.link_monitor.stop()
-            self.deinstallnetifs()
+            self.deinstall_ifaces()
             self.stopdaemons()
             self.stopeventmonitor()
 
@@ -474,38 +472,38 @@ class EmaneManager(ModelManager):
         EMANE network and NEM interface.
         """
         emane_node = None
-        netif = None
+        iface = None
 
         for node_id in self._emane_nets:
             emane_node = self._emane_nets[node_id]
-            netif = emane_node.getnemnetif(nemid)
-            if netif is not None:
+            iface = emane_node.get_nem_iface(nemid)
+            if iface is not None:
                 break
             else:
                 emane_node = None
 
-        return emane_node, netif
+        return emane_node, iface
 
     def get_nem_link(
         self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE
     ) -> Optional[LinkData]:
-        emane1, netif = self.nemlookup(nem1)
-        if not emane1 or not netif:
+        emane1, iface = self.nemlookup(nem1)
+        if not emane1 or not iface:
             logging.error("invalid nem: %s", nem1)
             return None
-        node1 = netif.node
-        emane2, netif = self.nemlookup(nem2)
-        if not emane2 or not netif:
+        node1 = iface.node
+        emane2, iface = self.nemlookup(nem2)
+        if not emane2 or not iface:
             logging.error("invalid nem: %s", nem2)
             return None
-        node2 = netif.node
+        node2 = iface.node
         color = self.session.get_link_color(emane1.id)
         return LinkData(
             message_type=flags,
+            type=LinkTypes.WIRELESS,
             node1_id=node1.id,
             node2_id=node2.id,
             network_id=emane1.id,
-            link_type=LinkTypes.WIRELESS,
             color=color,
         )
 
@@ -516,7 +514,7 @@ class EmaneManager(ModelManager):
         count = 0
         for node_id in self._emane_nets:
             emane_node = self._emane_nets[node_id]
-            count += len(emane_node.netifs())
+            count += len(emane_node.ifaces)
         return count
 
     def buildplatformxml(self, ctrlnet: CtrlNet) -> None:
@@ -607,19 +605,19 @@ class EmaneManager(ModelManager):
             n = node.id
 
             # control network not yet started here
-            self.session.add_remove_control_interface(
+            self.session.add_remove_control_iface(
                 node, 0, remove=False, conf_required=False
             )
 
             if otanetidx > 0:
                 logging.info("adding ota device ctrl%d", otanetidx)
-                self.session.add_remove_control_interface(
+                self.session.add_remove_control_iface(
                     node, otanetidx, remove=False, conf_required=False
                 )
 
             if eventservicenetidx >= 0:
                 logging.info("adding event service device ctrl%d", eventservicenetidx)
-                self.session.add_remove_control_interface(
+                self.session.add_remove_control_iface(
                     node, eventservicenetidx, remove=False, conf_required=False
                 )
 
@@ -676,23 +674,23 @@ class EmaneManager(ModelManager):
             except CoreCommandError:
                 logging.exception("error shutting down emane daemons")
 
-    def installnetifs(self) -> None:
+    def install_ifaces(self) -> None:
         """
         Install TUN/TAP virtual interfaces into their proper namespaces
         now that the EMANE daemons are running.
         """
         for key in sorted(self._emane_nets.keys()):
-            emane_node = self._emane_nets[key]
-            logging.info("emane install netifs for node: %d", key)
-            emane_node.installnetifs()
+            node = self._emane_nets[key]
+            logging.info("emane install interface for node(%s): %d", node.name, key)
+            node.install_ifaces()
 
-    def deinstallnetifs(self) -> None:
+    def deinstall_ifaces(self) -> None:
         """
         Uninstall TUN/TAP virtual interfaces.
         """
         for key in sorted(self._emane_nets.keys()):
             emane_node = self._emane_nets[key]
-            emane_node.deinstallnetifs()
+            emane_node.deinstall_ifaces()
 
     def doeventmonitor(self) -> bool:
         """
@@ -808,12 +806,12 @@ class EmaneManager(ModelManager):
         Returns True if successfully parsed and a Node Message was sent.
         """
         # convert nemid to node number
-        _emanenode, netif = self.nemlookup(nemid)
-        if netif is None:
+        _emanenode, iface = self.nemlookup(nemid)
+        if iface is None:
             logging.info("location event for unknown NEM %s", nemid)
             return False
 
-        n = netif.node.id
+        n = iface.node.id
         # convert from lat/long/alt to x,y,z coordinates
         x, y, z = self.session.location.getxyz(lat, lon, alt)
         x = int(x)
diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py
index 78d5ec5e..43fbc0fb 100644
--- a/daemon/core/emane/emanemodel.py
+++ b/daemon/core/emane/emanemodel.py
@@ -8,7 +8,7 @@ from typing import Dict, List, Optional, Set
 from core.config import ConfigGroup, Configuration
 from core.emane import emanemanifest
 from core.emane.nodes import EmaneNet
-from core.emulator.emudata import LinkOptions
+from core.emulator.data import LinkOptions
 from core.emulator.enumerations import ConfigDataTypes, TransportType
 from core.errors import CoreError
 from core.location.mobility import WirelessModel
@@ -97,28 +97,28 @@ class EmaneModel(WirelessModel):
         ]
 
     def build_xml_files(
-        self, config: Dict[str, str], interface: CoreInterface = None
+        self, config: Dict[str, str], iface: CoreInterface = None
     ) -> None:
         """
         Builds xml files for this emane model. Creates a nem.xml file that points to
         both mac.xml and phy.xml definitions.
 
         :param config: emane model configuration for the node and interface
-        :param interface: interface for the emane node
+        :param iface: interface for the emane node
         :return: nothing
         """
-        nem_name = emanexml.nem_file_name(self, interface)
-        mac_name = emanexml.mac_file_name(self, interface)
-        phy_name = emanexml.phy_file_name(self, interface)
+        nem_name = emanexml.nem_file_name(self, iface)
+        mac_name = emanexml.mac_file_name(self, iface)
+        phy_name = emanexml.phy_file_name(self, iface)
 
         # remote server for file
         server = None
-        if interface is not None:
-            server = interface.node.server
+        if iface is not None:
+            server = iface.node.server
 
         # check if this is external
         transport_type = TransportType.VIRTUAL
-        if interface and interface.transport_type == TransportType.RAW:
+        if iface and iface.transport_type == TransportType.RAW:
             transport_type = TransportType.RAW
         transport_name = emanexml.transport_file_name(self.id, transport_type)
 
@@ -144,31 +144,31 @@ class EmaneModel(WirelessModel):
         """
         logging.debug("emane model(%s) has no post setup tasks", self.name)
 
-    def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> None:
+    def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None:
         """
         Invoked from MobilityModel when nodes are moved; this causes
         emane location events to be generated for the nodes in the moved
         list, making EmaneModels compatible with Ns2ScriptedMobility.
 
         :param moved: moved nodes
-        :param moved_netifs: interfaces that were moved
+        :param moved_ifaces: interfaces that were moved
         :return: nothing
         """
         try:
             wlan = self.session.get_node(self.id, EmaneNet)
-            wlan.setnempositions(moved_netifs)
+            wlan.setnempositions(moved_ifaces)
         except CoreError:
             logging.exception("error during update")
 
     def linkconfig(
-        self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
+        self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
     ) -> None:
         """
         Invoked when a Link Message is received. Default is unimplemented.
 
-        :param netif: interface one
+        :param iface: interface one
         :param options: options for configuring link
-        :param netif2: interface two
+        :param iface2: interface two
         :return: nothing
         """
         logging.warning("emane model(%s) does not support link config", self.name)
diff --git a/daemon/core/emane/linkmonitor.py b/daemon/core/emane/linkmonitor.py
index ca9f4493..1a9ac41a 100644
--- a/daemon/core/emane/linkmonitor.py
+++ b/daemon/core/emane/linkmonitor.py
@@ -212,10 +212,10 @@ class EmaneLinkMonitor:
         addresses = []
         nodes = self.emane_manager.getnodes()
         for node in nodes:
-            for netif in node.netifs():
-                if isinstance(netif.net, CtrlNet):
+            for iface in node.get_ifaces():
+                if isinstance(iface.net, CtrlNet):
                     ip4 = None
-                    for x in netif.addrlist:
+                    for x in iface.addrlist:
                         address, prefix = x.split("/")
                         if netaddr.valid_ipv4(address):
                             ip4 = address
@@ -305,11 +305,11 @@ class EmaneLinkMonitor:
         color = self.emane_manager.session.get_link_color(emane_id)
         link_data = LinkData(
             message_type=message_type,
+            type=LinkTypes.WIRELESS,
             label=label,
             node1_id=node1,
             node2_id=node2,
             network_id=emane_id,
-            link_type=LinkTypes.WIRELESS,
             color=color,
         )
         self.emane_manager.session.broadcast_link(link_data)
diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py
index c4c3428b..c28f1382 100644
--- a/daemon/core/emane/nodes.py
+++ b/daemon/core/emane/nodes.py
@@ -6,9 +6,8 @@ share the same MAC+PHY model.
 import logging
 from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
 
-from core.emulator.data import LinkData
+from core.emulator.data import LinkData, LinkOptions
 from core.emulator.distributed import DistributedServer
-from core.emulator.emudata import LinkOptions
 from core.emulator.enumerations import (
     LinkTypes,
     MessageFlags,
@@ -64,14 +63,14 @@ class EmaneNet(CoreNetworkBase):
         self.mobility: Optional[WayPointMobility] = None
 
     def linkconfig(
-        self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
+        self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
     ) -> None:
         """
         The CommEffect model supports link configuration.
         """
         if not self.model:
             return
-        self.model.linkconfig(netif, options, netif2)
+        self.model.linkconfig(iface, options, iface2)
 
     def config(self, conf: str) -> None:
         self.conf = conf
@@ -82,10 +81,10 @@ class EmaneNet(CoreNetworkBase):
     def shutdown(self) -> None:
         pass
 
-    def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
+    def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
         pass
 
-    def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
+    def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
         pass
 
     def linknet(self, net: "CoreNetworkBase") -> CoreInterface:
@@ -113,39 +112,33 @@ class EmaneNet(CoreNetworkBase):
             self.mobility = model(session=self.session, _id=self.id)
             self.mobility.update_config(config)
 
-    def setnemid(self, netif: CoreInterface, nemid: int) -> None:
+    def setnemid(self, iface: CoreInterface, nemid: int) -> None:
         """
         Record an interface to numerical ID mapping. The Emane controller
         object manages and assigns these IDs for all NEMs.
         """
-        self.nemidmap[netif] = nemid
+        self.nemidmap[iface] = nemid
 
-    def getnemid(self, netif: CoreInterface) -> Optional[int]:
+    def getnemid(self, iface: CoreInterface) -> Optional[int]:
         """
         Given an interface, return its numerical ID.
         """
-        if netif not in self.nemidmap:
+        if iface not in self.nemidmap:
             return None
         else:
-            return self.nemidmap[netif]
+            return self.nemidmap[iface]
 
-    def getnemnetif(self, nemid: int) -> Optional[CoreInterface]:
+    def get_nem_iface(self, nemid: int) -> Optional[CoreInterface]:
         """
         Given a numerical NEM ID, return its interface. This returns the
         first interface that matches the given NEM ID.
         """
-        for netif in self.nemidmap:
-            if self.nemidmap[netif] == nemid:
-                return netif
+        for iface in self.nemidmap:
+            if self.nemidmap[iface] == nemid:
+                return iface
         return None
 
-    def netifs(self, sort: bool = True) -> List[CoreInterface]:
-        """
-        Retrieve list of linked interfaces sorted by node number.
-        """
-        return sorted(self._netif.values(), key=lambda ifc: ifc.node.id)
-
-    def installnetifs(self) -> None:
+    def install_ifaces(self) -> None:
         """
         Install TAP devices into their namespaces. This is done after
         EMANE daemons have been started, because that is their only chance
@@ -159,48 +152,48 @@ class EmaneNet(CoreNetworkBase):
             warntxt += "Python bindings failed to load"
             logging.error(warntxt)
 
-        for netif in self.netifs():
+        for iface in self.get_ifaces():
             external = self.session.emane.get_config(
                 "external", self.id, self.model.name
             )
             if external == "0":
-                netif.setaddrs()
+                iface.setaddrs()
 
             if not self.session.emane.genlocationevents():
-                netif.poshook = None
+                iface.poshook = None
                 continue
 
             # at this point we register location handlers for generating
             # EMANE location events
-            netif.poshook = self.setnemposition
-            netif.setposition()
+            iface.poshook = self.setnemposition
+            iface.setposition()
 
-    def deinstallnetifs(self) -> None:
+    def deinstall_ifaces(self) -> None:
         """
         Uninstall TAP devices. This invokes their shutdown method for
         any required cleanup; the device may be actually removed when
         emanetransportd terminates.
         """
-        for netif in self.netifs():
-            if netif.transport_type == TransportType.VIRTUAL:
-                netif.shutdown()
-            netif.poshook = None
+        for iface in self.get_ifaces():
+            if iface.transport_type == TransportType.VIRTUAL:
+                iface.shutdown()
+            iface.poshook = None
 
     def _nem_position(
-        self, netif: CoreInterface
+        self, iface: CoreInterface
     ) -> Optional[Tuple[int, float, float, float]]:
         """
         Creates nem position for emane event for a given interface.
 
-        :param netif: interface to get nem emane position for
+        :param iface: interface to get nem emane position for
         :return: nem position tuple, None otherwise
         """
-        nemid = self.getnemid(netif)
-        ifname = netif.localname
+        nemid = self.getnemid(iface)
+        ifname = iface.localname
         if nemid is None:
             logging.info("nemid for %s is unknown", ifname)
             return
-        node = netif.node
+        node = iface.node
         x, y, z = node.getposition()
         lat, lon, alt = self.session.location.getgeo(x, y, z)
         if node.position.alt is not None:
@@ -210,30 +203,30 @@ class EmaneNet(CoreNetworkBase):
         alt = int(round(alt))
         return nemid, lon, lat, alt
 
-    def setnemposition(self, netif: CoreInterface) -> None:
+    def setnemposition(self, iface: CoreInterface) -> None:
         """
         Publish a NEM location change event using the EMANE event service.
 
-        :param netif: interface to set nem position for
+        :param iface: interface to set nem position for
         """
         if self.session.emane.service is None:
             logging.info("position service not available")
             return
 
-        position = self._nem_position(netif)
+        position = self._nem_position(iface)
         if position:
             nemid, lon, lat, alt = position
             event = LocationEvent()
             event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
             self.session.emane.service.publish(0, event)
 
-    def setnempositions(self, moved_netifs: List[CoreInterface]) -> None:
+    def setnempositions(self, moved_ifaces: List[CoreInterface]) -> None:
         """
         Several NEMs have moved, from e.g. a WaypointMobilityModel
         calculation. Generate an EMANE Location Event having several
-        entries for each netif that has moved.
+        entries for each interface that has moved.
         """
-        if len(moved_netifs) == 0:
+        if len(moved_ifaces) == 0:
             return
 
         if self.session.emane.service is None:
@@ -241,8 +234,8 @@ class EmaneNet(CoreNetworkBase):
             return
 
         event = LocationEvent()
-        for netif in moved_netifs:
-            position = self._nem_position(netif)
+        for iface in moved_ifaces:
+            position = self._nem_position(iface)
             if position:
                 nemid, lon, lat, alt = position
                 event.append(nemid, latitude=lat, longitude=lon, altitude=alt)
diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py
index 819716e3..5b6479ae 100644
--- a/daemon/core/emulator/data.py
+++ b/daemon/core/emulator/data.py
@@ -1,18 +1,22 @@
 """
 CORE data objects.
 """
+from dataclasses import dataclass, field
+from typing import TYPE_CHECKING, List, Optional, Tuple
 
-from dataclasses import dataclass
-from typing import List, Tuple
+import netaddr
 
+from core import utils
 from core.emulator.enumerations import (
     EventTypes,
     ExceptionLevels,
     LinkTypes,
     MessageFlags,
-    NodeTypes,
 )
 
+if TYPE_CHECKING:
+    from core.nodes.base import CoreNode, NodeBase
+
 
 @dataclass
 class ConfigData:
@@ -27,7 +31,7 @@ class ConfigData:
     possible_values: str = None
     groups: str = None
     session: int = None
-    interface_number: int = None
+    iface_id: int = None
     network_id: int = None
     opaque: str = None
 
@@ -68,65 +72,217 @@ class FileData:
 
 
 @dataclass
-class NodeData:
-    message_type: MessageFlags = None
-    id: int = None
-    node_type: NodeTypes = None
+class NodeOptions:
+    """
+    Options for creating and updating nodes within core.
+    """
+
     name: str = None
-    ip_address: str = None
-    mac_address: str = None
-    ip6_address: str = None
-    model: str = None
-    emulation_id: int = None
-    server: str = None
-    session: int = None
-    x_position: float = None
-    y_position: float = None
+    model: Optional[str] = "PC"
     canvas: int = None
-    network_id: int = None
-    services: List[str] = None
-    latitude: float = None
-    longitude: float = None
-    altitude: float = None
     icon: str = None
-    opaque: str = None
+    services: List[str] = field(default_factory=list)
+    config_services: List[str] = field(default_factory=list)
+    x: float = None
+    y: float = None
+    lat: float = None
+    lon: float = None
+    alt: float = None
+    server: str = None
+    image: str = None
+    emane: str = None
+
+    def set_position(self, x: float, y: float) -> None:
+        """
+        Convenience method for setting position.
+
+        :param x: x position
+        :param y: y position
+        :return: nothing
+        """
+        self.x = x
+        self.y = y
+
+    def set_location(self, lat: float, lon: float, alt: float) -> None:
+        """
+        Convenience method for setting location.
+
+        :param lat: latitude
+        :param lon: longitude
+        :param alt: altitude
+        :return: nothing
+        """
+        self.lat = lat
+        self.lon = lon
+        self.alt = alt
+
+
+@dataclass
+class NodeData:
+    """
+    Node to broadcast.
+    """
+
+    node: "NodeBase"
+    message_type: MessageFlags = None
     source: str = None
 
 
+@dataclass
+class InterfaceData:
+    """
+    Convenience class for storing interface data.
+    """
+
+    id: int = None
+    name: str = None
+    mac: str = None
+    ip4: str = None
+    ip4_mask: int = None
+    ip6: str = None
+    ip6_mask: int = None
+
+    def get_addresses(self) -> List[str]:
+        """
+        Returns a list of ip4 and ip6 addresses when present.
+
+        :return: list of addresses
+        """
+        addresses = []
+        if self.ip4 and self.ip4_mask:
+            addresses.append(f"{self.ip4}/{self.ip4_mask}")
+        if self.ip6 and self.ip6_mask:
+            addresses.append(f"{self.ip6}/{self.ip6_mask}")
+        return addresses
+
+
+@dataclass
+class LinkOptions:
+    """
+    Options for creating and updating links within core.
+    """
+
+    delay: int = None
+    bandwidth: int = None
+    loss: float = None
+    dup: int = None
+    jitter: int = None
+    mer: int = None
+    burst: int = None
+    mburst: int = None
+    unidirectional: int = None
+    key: int = None
+
+
 @dataclass
 class LinkData:
+    """
+    Represents all data associated with a link.
+    """
+
     message_type: MessageFlags = None
+    type: LinkTypes = LinkTypes.WIRED
     label: str = None
     node1_id: int = None
     node2_id: int = None
-    delay: float = None
-    bandwidth: float = None
-    loss: float = None
-    dup: float = None
-    jitter: float = None
-    mer: float = None
-    burst: float = None
-    session: int = None
-    mburst: float = None
-    link_type: LinkTypes = None
-    gui_attributes: str = None
-    unidirectional: int = None
-    emulation_id: int = None
     network_id: int = None
-    key: int = None
-    interface1_id: int = None
-    interface1_name: str = None
-    interface1_ip4: str = None
-    interface1_ip4_mask: int = None
-    interface1_mac: str = None
-    interface1_ip6: str = None
-    interface1_ip6_mask: int = None
-    interface2_id: int = None
-    interface2_name: str = None
-    interface2_ip4: str = None
-    interface2_ip4_mask: int = None
-    interface2_mac: str = None
-    interface2_ip6: str = None
-    interface2_ip6_mask: int = None
-    opaque: str = None
+    iface1: InterfaceData = None
+    iface2: InterfaceData = None
+    options: LinkOptions = LinkOptions()
     color: str = None
+
+
+class IpPrefixes:
+    """
+    Convenience class to help generate IP4 and IP6 addresses for nodes within CORE.
+    """
+
+    def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None:
+        """
+        Creates an IpPrefixes object.
+
+        :param ip4_prefix: ip4 prefix to use for generation
+        :param ip6_prefix: ip6 prefix to use for generation
+        :raises ValueError: when both ip4 and ip6 prefixes have not been provided
+        """
+        if not ip4_prefix and not ip6_prefix:
+            raise ValueError("ip4 or ip6 must be provided")
+
+        self.ip4 = None
+        if ip4_prefix:
+            self.ip4 = netaddr.IPNetwork(ip4_prefix)
+        self.ip6 = None
+        if ip6_prefix:
+            self.ip6 = netaddr.IPNetwork(ip6_prefix)
+
+    def ip4_address(self, node_id: int) -> str:
+        """
+        Convenience method to return the IP4 address for a node.
+
+        :param node_id: node id to get IP4 address for
+        :return: IP4 address or None
+        """
+        if not self.ip4:
+            raise ValueError("ip4 prefixes have not been set")
+        return str(self.ip4[node_id])
+
+    def ip6_address(self, node_id: int) -> str:
+        """
+        Convenience method to return the IP6 address for a node.
+
+        :param node_id: node id to get IP6 address for
+        :return: IP4 address or None
+        """
+        if not self.ip6:
+            raise ValueError("ip6 prefixes have not been set")
+        return str(self.ip6[node_id])
+
+    def gen_iface(self, node_id: int, name: str = None, mac: str = None):
+        """
+        Creates interface data for linking nodes, using the nodes unique id for
+        generation, along with a random mac address, unless provided.
+
+        :param node_id: node id to create an interface for
+        :param name: name to set for interface, default is eth{id}
+        :param mac: mac address to use for this interface, default is random
+            generation
+        :return: new interface data for the provided node
+        """
+        # generate ip4 data
+        ip4 = None
+        ip4_mask = None
+        if self.ip4:
+            ip4 = self.ip4_address(node_id)
+            ip4_mask = self.ip4.prefixlen
+
+        # generate ip6 data
+        ip6 = None
+        ip6_mask = None
+        if self.ip6:
+            ip6 = self.ip6_address(node_id)
+            ip6_mask = self.ip6.prefixlen
+
+        # random mac
+        if not mac:
+            mac = utils.random_mac()
+
+        return InterfaceData(
+            name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac
+        )
+
+    def create_iface(
+        self, node: "CoreNode", name: str = None, mac: str = None
+    ) -> InterfaceData:
+        """
+        Creates interface data for linking nodes, using the nodes unique id for
+        generation, along with a random mac address, unless provided.
+
+        :param node: node to create interface for
+        :param name: name to set for interface, default is eth{id}
+        :param mac: mac address to use for this interface, default is random
+            generation
+        :return: new interface data for the provided node
+        """
+        iface_data = self.gen_iface(node.id, name, mac)
+        iface_data.id = node.next_iface_id()
+        return iface_data
diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py
index 75081447..381eb019 100644
--- a/daemon/core/emulator/distributed.py
+++ b/daemon/core/emulator/distributed.py
@@ -208,7 +208,7 @@ class DistributedController:
             "local tunnel node(%s) to remote(%s) key(%s)", node.name, host, key
         )
         local_tap = GreTap(session=self.session, remoteip=host, key=key)
-        local_tap.net_client.set_interface_master(node.brname, local_tap.localname)
+        local_tap.net_client.set_iface_master(node.brname, local_tap.localname)
 
         # server to local
         logging.info(
@@ -217,7 +217,7 @@ class DistributedController:
         remote_tap = GreTap(
             session=self.session, remoteip=self.address, key=key, server=server
         )
-        remote_tap.net_client.set_interface_master(node.brname, remote_tap.localname)
+        remote_tap.net_client.set_iface_master(node.brname, remote_tap.localname)
 
         # save tunnels for shutdown
         tunnel = (local_tap, remote_tap)
diff --git a/daemon/core/emulator/emudata.py b/daemon/core/emulator/emudata.py
deleted file mode 100644
index 2aecdace..00000000
--- a/daemon/core/emulator/emudata.py
+++ /dev/null
@@ -1,206 +0,0 @@
-from dataclasses import dataclass, field
-from typing import TYPE_CHECKING, List, Optional
-
-import netaddr
-
-from core import utils
-from core.emulator.enumerations import LinkTypes
-
-if TYPE_CHECKING:
-    from core.nodes.base import CoreNode
-
-
-@dataclass
-class NodeOptions:
-    """
-    Options for creating and updating nodes within core.
-    """
-
-    name: str = None
-    model: Optional[str] = "PC"
-    canvas: int = None
-    icon: str = None
-    opaque: str = None
-    services: List[str] = field(default_factory=list)
-    config_services: List[str] = field(default_factory=list)
-    x: float = None
-    y: float = None
-    lat: float = None
-    lon: float = None
-    alt: float = None
-    emulation_id: int = None
-    server: str = None
-    image: str = None
-    emane: str = None
-
-    def set_position(self, x: float, y: float) -> None:
-        """
-        Convenience method for setting position.
-
-        :param x: x position
-        :param y: y position
-        :return: nothing
-        """
-        self.x = x
-        self.y = y
-
-    def set_location(self, lat: float, lon: float, alt: float) -> None:
-        """
-        Convenience method for setting location.
-
-        :param lat: latitude
-        :param lon: longitude
-        :param alt: altitude
-        :return: nothing
-        """
-        self.lat = lat
-        self.lon = lon
-        self.alt = alt
-
-
-@dataclass
-class LinkOptions:
-    """
-    Options for creating and updating links within core.
-    """
-
-    type: LinkTypes = LinkTypes.WIRED
-    session: int = None
-    delay: int = None
-    bandwidth: int = None
-    loss: float = None
-    dup: int = None
-    jitter: int = None
-    mer: int = None
-    burst: int = None
-    mburst: int = None
-    gui_attributes: str = None
-    unidirectional: bool = None
-    emulation_id: int = None
-    network_id: int = None
-    key: int = None
-    opaque: str = None
-
-
-@dataclass
-class InterfaceData:
-    """
-    Convenience class for storing interface data.
-    """
-
-    id: int = None
-    name: str = None
-    mac: str = None
-    ip4: str = None
-    ip4_mask: int = None
-    ip6: str = None
-    ip6_mask: int = None
-
-    def get_addresses(self) -> List[str]:
-        """
-        Returns a list of ip4 and ip6 addresses when present.
-
-        :return: list of addresses
-        """
-        addresses = []
-        if self.ip4 and self.ip4_mask:
-            addresses.append(f"{self.ip4}/{self.ip4_mask}")
-        if self.ip6 and self.ip6_mask:
-            addresses.append(f"{self.ip6}/{self.ip6_mask}")
-        return addresses
-
-
-class IpPrefixes:
-    """
-    Convenience class to help generate IP4 and IP6 addresses for nodes within CORE.
-    """
-
-    def __init__(self, ip4_prefix: str = None, ip6_prefix: str = None) -> None:
-        """
-        Creates an IpPrefixes object.
-
-        :param ip4_prefix: ip4 prefix to use for generation
-        :param ip6_prefix: ip6 prefix to use for generation
-        :raises ValueError: when both ip4 and ip6 prefixes have not been provided
-        """
-        if not ip4_prefix and not ip6_prefix:
-            raise ValueError("ip4 or ip6 must be provided")
-
-        self.ip4 = None
-        if ip4_prefix:
-            self.ip4 = netaddr.IPNetwork(ip4_prefix)
-        self.ip6 = None
-        if ip6_prefix:
-            self.ip6 = netaddr.IPNetwork(ip6_prefix)
-
-    def ip4_address(self, node_id: int) -> str:
-        """
-        Convenience method to return the IP4 address for a node.
-
-        :param node_id: node id to get IP4 address for
-        :return: IP4 address or None
-        """
-        if not self.ip4:
-            raise ValueError("ip4 prefixes have not been set")
-        return str(self.ip4[node_id])
-
-    def ip6_address(self, node_id: int) -> str:
-        """
-        Convenience method to return the IP6 address for a node.
-
-        :param node_id: node id to get IP6 address for
-        :return: IP4 address or None
-        """
-        if not self.ip6:
-            raise ValueError("ip6 prefixes have not been set")
-        return str(self.ip6[node_id])
-
-    def gen_interface(self, node_id: int, name: str = None, mac: str = None):
-        """
-        Creates interface data for linking nodes, using the nodes unique id for
-        generation, along with a random mac address, unless provided.
-
-        :param node_id: node id to create an interface for
-        :param name: name to set for interface, default is eth{id}
-        :param mac: mac address to use for this interface, default is random
-            generation
-        :return: new interface data for the provided node
-        """
-        # generate ip4 data
-        ip4 = None
-        ip4_mask = None
-        if self.ip4:
-            ip4 = self.ip4_address(node_id)
-            ip4_mask = self.ip4.prefixlen
-
-        # generate ip6 data
-        ip6 = None
-        ip6_mask = None
-        if self.ip6:
-            ip6 = self.ip6_address(node_id)
-            ip6_mask = self.ip6.prefixlen
-
-        # random mac
-        if not mac:
-            mac = utils.random_mac()
-
-        return InterfaceData(
-            name=name, ip4=ip4, ip4_mask=ip4_mask, ip6=ip6, ip6_mask=ip6_mask, mac=mac
-        )
-
-    def create_interface(
-        self, node: "CoreNode", name: str = None, mac: str = None
-    ) -> InterfaceData:
-        """
-        Creates interface data for linking nodes, using the nodes unique id for
-        generation, along with a random mac address, unless provided.
-
-        :param node: node to create interface for
-        :param name: name to set for interface, default is eth{id}
-        :param mac: mac address to use for this interface, default is random
-            generation
-        :return: new interface data for the provided node
-        """
-        interface_data = self.gen_interface(node.id, name, mac)
-        interface_data.id = node.newifindex()
-        return interface_data
diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py
index e63c30c7..0b97da93 100644
--- a/daemon/core/emulator/session.py
+++ b/daemon/core/emulator/session.py
@@ -22,11 +22,13 @@ from core.emulator.data import (
     EventData,
     ExceptionData,
     FileData,
+    InterfaceData,
     LinkData,
+    LinkOptions,
     NodeData,
+    NodeOptions,
 )
 from core.emulator.distributed import DistributedController
-from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
 from core.emulator.enumerations import (
     EventTypes,
     ExceptionLevels,
@@ -203,7 +205,7 @@ class Session:
         common_networks = node1.commonnets(node1)
         if not common_networks:
             raise CoreError("no common network found for wireless link/unlink")
-        for common_network, interface1, interface2 in common_networks:
+        for common_network, iface1, iface2 in common_networks:
             if not isinstance(common_network, (WlanNode, EmaneNet)):
                 logging.info(
                     "skipping common network that is not wireless/emane: %s",
@@ -211,40 +213,42 @@ class Session:
                 )
                 continue
             if connect:
-                common_network.link(interface1, interface2)
+                common_network.link(iface1, iface2)
             else:
-                common_network.unlink(interface1, interface2)
+                common_network.unlink(iface1, iface2)
 
     def add_link(
         self,
         node1_id: int,
         node2_id: int,
-        interface1_data: InterfaceData = None,
-        interface2_data: InterfaceData = None,
+        iface1_data: InterfaceData = None,
+        iface2_data: InterfaceData = None,
         options: LinkOptions = None,
+        link_type: LinkTypes = LinkTypes.WIRED,
     ) -> Tuple[CoreInterface, CoreInterface]:
         """
         Add a link between nodes.
 
         :param node1_id: node one id
         :param node2_id: node two id
-        :param interface1_data: node one interface
+        :param iface1_data: node one interface
             data, defaults to none
-        :param interface2_data: node two interface
+        :param iface2_data: node two interface
             data, defaults to none
         :param options: data for creating link,
             defaults to no options
+        :param link_type: type of link to add
         :return: tuple of created core interfaces, depending on link
         """
         if not options:
             options = LinkOptions()
         node1 = self.get_node(node1_id, NodeBase)
         node2 = self.get_node(node2_id, NodeBase)
-        interface1 = None
-        interface2 = None
+        iface1 = None
+        iface2 = None
 
         # wireless link
-        if options.type == LinkTypes.WIRELESS:
+        if link_type == LinkTypes.WIRELESS:
             if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
                 self._link_wireless(node1, node2, connect=True)
             else:
@@ -258,22 +262,22 @@ class Session:
                 logging.info("linking ptp: %s - %s", node1.name, node2.name)
                 start = self.state.should_start()
                 ptp = self.create_node(PtpNet, start)
-                interface1 = node1.newnetif(ptp, interface1_data)
-                interface2 = node2.newnetif(ptp, interface2_data)
-                ptp.linkconfig(interface1, options)
+                iface1 = node1.new_iface(ptp, iface1_data)
+                iface2 = node2.new_iface(ptp, iface2_data)
+                ptp.linkconfig(iface1, options)
                 if not options.unidirectional:
-                    ptp.linkconfig(interface2, options)
+                    ptp.linkconfig(iface2, options)
             # link node to net
             elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase):
-                interface1 = node1.newnetif(node2, interface1_data)
+                iface1 = node1.new_iface(node2, iface1_data)
                 if not isinstance(node2, (EmaneNet, WlanNode)):
-                    node2.linkconfig(interface1, options)
+                    node2.linkconfig(iface1, options)
             # link net to node
             elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase):
-                interface2 = node2.newnetif(node1, interface2_data)
+                iface2 = node2.new_iface(node1, iface2_data)
                 wireless_net = isinstance(node1, (EmaneNet, WlanNode))
                 if not options.unidirectional and not wireless_net:
-                    node1.linkconfig(interface2, options)
+                    node1.linkconfig(iface2, options)
             # network to network
             elif isinstance(node1, CoreNetworkBase) and isinstance(
                 node2, CoreNetworkBase
@@ -281,12 +285,12 @@ class Session:
                 logging.info(
                     "linking network to network: %s - %s", node1.name, node2.name
                 )
-                interface1 = node1.linknet(node2)
-                node1.linkconfig(interface1, options)
+                iface1 = node1.linknet(node2)
+                node1.linkconfig(iface1, options)
                 if not options.unidirectional:
-                    interface1.swapparams("_params_up")
-                    node2.linkconfig(interface1, options)
-                    interface1.swapparams("_params_up")
+                    iface1.swapparams("_params_up")
+                    node2.linkconfig(iface1, options)
+                    iface1.swapparams("_params_up")
             else:
                 raise CoreError(
                     f"cannot link node1({type(node1)}) node2({type(node2)})"
@@ -296,19 +300,19 @@ class Session:
             key = options.key
             if isinstance(node1, TunnelNode):
                 logging.info("setting tunnel key for: %s", node1.name)
-                node1.setkey(key, interface1_data)
+                node1.setkey(key, iface1_data)
             if isinstance(node2, TunnelNode):
                 logging.info("setting tunnel key for: %s", node2.name)
-                node2.setkey(key, interface2_data)
+                node2.setkey(key, iface2_data)
         self.sdt.add_link(node1_id, node2_id)
-        return interface1, interface2
+        return iface1, iface2
 
     def delete_link(
         self,
         node1_id: int,
         node2_id: int,
-        interface1_id: int = None,
-        interface2_id: int = None,
+        iface1_id: int = None,
+        iface2_id: int = None,
         link_type: LinkTypes = LinkTypes.WIRED,
     ) -> None:
         """
@@ -316,8 +320,8 @@ class Session:
 
         :param node1_id: node one id
         :param node2_id: node two id
-        :param interface1_id: interface id for node one
-        :param interface2_id: interface id for node two
+        :param iface1_id: interface id for node one
+        :param iface2_id: interface id for node two
         :param link_type: link type to delete
         :return: nothing
         :raises core.CoreError: when no common network is found for link being deleted
@@ -328,9 +332,9 @@ class Session:
             "deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)",
             link_type.name,
             node1.name,
-            interface1_id,
+            iface1_id,
             node2.name,
-            interface2_id,
+            iface2_id,
         )
 
         # wireless link
@@ -345,47 +349,41 @@ class Session:
         # wired link
         else:
             if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
-                interface1 = node1.netif(interface1_id)
-                interface2 = node2.netif(interface2_id)
-                if not interface1:
-                    raise CoreError(
-                        f"node({node1.name}) missing interface({interface1_id})"
-                    )
-                if not interface2:
-                    raise CoreError(
-                        f"node({node2.name}) missing interface({interface2_id})"
-                    )
-                if interface1.net != interface2.net:
+                iface1 = node1.get_iface(iface1_id)
+                iface2 = node2.get_iface(iface2_id)
+                if iface1.net != iface2.net:
                     raise CoreError(
                         f"node1({node1.name}) node2({node2.name}) "
                         "not connected to same net"
                     )
-                ptp = interface1.net
-                node1.delnetif(interface1_id)
-                node2.delnetif(interface2_id)
+                ptp = iface1.net
+                node1.delete_iface(iface1_id)
+                node2.delete_iface(iface2_id)
                 self.delete_node(ptp.id)
             elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase):
-                node1.delnetif(interface1_id)
+                node1.delete_iface(iface1_id)
             elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase):
-                node2.delnetif(interface2_id)
+                node2.delete_iface(iface2_id)
         self.sdt.delete_link(node1_id, node2_id)
 
     def update_link(
         self,
         node1_id: int,
         node2_id: int,
-        interface1_id: int = None,
-        interface2_id: int = None,
+        iface1_id: int = None,
+        iface2_id: int = None,
         options: LinkOptions = None,
+        link_type: LinkTypes = LinkTypes.WIRED,
     ) -> None:
         """
         Update link information between nodes.
 
         :param node1_id: node one id
         :param node2_id: node two id
-        :param interface1_id: interface id for node one
-        :param interface2_id: interface id for node two
+        :param iface1_id: interface id for node one
+        :param iface2_id: interface id for node two
         :param options: data to update link with
+        :param link_type: type of link to update
         :return: nothing
         :raises core.CoreError: when updating a wireless type link, when there is a
             unknown link between networks
@@ -396,66 +394,66 @@ class Session:
         node2 = self.get_node(node2_id, NodeBase)
         logging.info(
             "update link(%s) node(%s):interface(%s) node(%s):interface(%s)",
-            options.type.name,
+            link_type.name,
             node1.name,
-            interface1_id,
+            iface1_id,
             node2.name,
-            interface2_id,
+            iface2_id,
         )
 
         # wireless link
-        if options.type == LinkTypes.WIRELESS:
+        if link_type == LinkTypes.WIRELESS:
             raise CoreError("cannot update wireless link")
         else:
             if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase):
-                interface1 = node1.netif(interface1_id)
-                interface2 = node2.netif(interface2_id)
-                if not interface1:
+                iface1 = node1.ifaces.get(iface1_id)
+                iface2 = node2.ifaces.get(iface2_id)
+                if not iface1:
                     raise CoreError(
-                        f"node({node1.name}) missing interface({interface1_id})"
+                        f"node({node1.name}) missing interface({iface1_id})"
                     )
-                if not interface2:
+                if not iface2:
                     raise CoreError(
-                        f"node({node2.name}) missing interface({interface2_id})"
+                        f"node({node2.name}) missing interface({iface2_id})"
                     )
-                if interface1.net != interface2.net:
+                if iface1.net != iface2.net:
                     raise CoreError(
                         f"node1({node1.name}) node2({node2.name}) "
                         "not connected to same net"
                     )
-                ptp = interface1.net
-                ptp.linkconfig(interface1, options, interface2)
+                ptp = iface1.net
+                ptp.linkconfig(iface1, options, iface2)
                 if not options.unidirectional:
-                    ptp.linkconfig(interface2, options, interface1)
+                    ptp.linkconfig(iface2, options, iface1)
             elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase):
-                interface = node1.netif(interface1_id)
-                node2.linkconfig(interface, options)
+                iface = node1.get_iface(iface1_id)
+                node2.linkconfig(iface, options)
             elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase):
-                interface = node2.netif(interface2_id)
-                node1.linkconfig(interface, options)
+                iface = node2.get_iface(iface2_id)
+                node1.linkconfig(iface, options)
             elif isinstance(node1, CoreNetworkBase) and isinstance(
                 node2, CoreNetworkBase
             ):
-                interface = node1.getlinknetif(node2)
+                iface = node1.get_linked_iface(node2)
                 upstream = False
-                if not interface:
+                if not iface:
                     upstream = True
-                    interface = node2.getlinknetif(node1)
-                if not interface:
+                    iface = node2.get_linked_iface(node1)
+                if not iface:
                     raise CoreError("modify unknown link between nets")
                 if upstream:
-                    interface.swapparams("_params_up")
-                    node1.linkconfig(interface, options)
-                    interface.swapparams("_params_up")
+                    iface.swapparams("_params_up")
+                    node1.linkconfig(iface, options)
+                    iface.swapparams("_params_up")
                 else:
-                    node1.linkconfig(interface, options)
+                    node1.linkconfig(iface, options)
                 if not options.unidirectional:
                     if upstream:
-                        node2.linkconfig(interface, options)
+                        node2.linkconfig(iface, options)
                     else:
-                        interface.swapparams("_params_up")
-                        node2.linkconfig(interface, options)
-                        interface.swapparams("_params_up")
+                        iface.swapparams("_params_up")
+                        node2.linkconfig(iface, options)
+                        iface.swapparams("_params_up")
             else:
                 raise CoreError(
                     f"cannot update link node1({type(node1)}) node2({type(node2)})"
@@ -525,7 +523,6 @@ class Session:
         # set node attributes
         node.icon = options.icon
         node.canvas = options.canvas
-        node.opaque = options.opaque
 
         # set node position and broadcast it
         self.set_node_position(node, options)
@@ -553,7 +550,7 @@ class Session:
         is_boot_node = isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node)
         if self.state == EventTypes.RUNTIME_STATE and is_boot_node:
             self.write_nodes()
-            self.add_remove_control_interface(node=node, remove=False)
+            self.add_remove_control_iface(node=node, remove=False)
             self.services.boot_services(node)
 
         self.sdt.add_node(node)
@@ -810,9 +807,9 @@ class Session:
         :param source: source of broadcast, None by default
         :return: nothing
         """
-        node_data = node.data(message_type, source)
-        if not node_data:
+        if not node.apitype:
             return
+        node_data = NodeData(node=node, message_type=message_type, source=source)
         for handler in self.node_handlers:
             handler(node_data)
 
@@ -1268,7 +1265,7 @@ class Session:
         self.emane.shutdown()
 
         # update control interface hosts
-        self.update_control_interface_hosts(remove=True)
+        self.update_control_iface_hosts(remove=True)
 
         # remove all four possible control networks
         self.add_remove_control_net(0, remove=True)
@@ -1314,7 +1311,7 @@ class Session:
         :return: nothing
         """
         logging.info("booting node(%s): %s", node.name, [x.name for x in node.services])
-        self.add_remove_control_interface(node=node, remove=False)
+        self.add_remove_control_iface(node=node, remove=False)
         self.services.boot_services(node)
         node.start_config_services()
 
@@ -1338,7 +1335,7 @@ class Session:
             total = time.monotonic() - start
             logging.debug("boot run time: %s", total)
         if not exceptions:
-            self.update_control_interface_hosts()
+            self.update_control_iface_hosts()
         return exceptions
 
     def get_control_net_prefixes(self) -> List[str]:
@@ -1356,7 +1353,7 @@ class Session:
             p0 = p
         return [p0, p1, p2, p3]
 
-    def get_control_net_server_interfaces(self) -> List[str]:
+    def get_control_net_server_ifaces(self) -> List[str]:
         """
         Retrieve control net server interfaces.
 
@@ -1424,7 +1421,7 @@ class Session:
             else:
                 prefix_spec = CtrlNet.DEFAULT_PREFIX_LIST[net_index]
         logging.debug("prefix spec: %s", prefix_spec)
-        server_interface = self.get_control_net_server_interfaces()[net_index]
+        server_iface = self.get_control_net_server_ifaces()[net_index]
 
         # return any existing controlnet bridge
         try:
@@ -1465,7 +1462,7 @@ class Session:
             _id,
             prefix,
             updown_script,
-            server_interface,
+            server_iface,
         )
         control_net = self.create_node(
             CtrlNet,
@@ -1473,11 +1470,11 @@ class Session:
             prefix,
             _id=_id,
             updown_script=updown_script,
-            serverintf=server_interface,
+            serverintf=server_iface,
         )
         return control_net
 
-    def add_remove_control_interface(
+    def add_remove_control_iface(
         self,
         node: CoreNode,
         net_index: int = 0,
@@ -1503,27 +1500,27 @@ class Session:
         if not node:
             return
         # ctrl# already exists
-        if node.netif(control_net.CTRLIF_IDX_BASE + net_index):
+        if node.ifaces.get(control_net.CTRLIF_IDX_BASE + net_index):
             return
         try:
             ip4 = control_net.prefix[node.id]
             ip4_mask = control_net.prefix.prefixlen
-            interface_data = InterfaceData(
+            iface_data = InterfaceData(
                 id=control_net.CTRLIF_IDX_BASE + net_index,
                 name=f"ctrl{net_index}",
                 mac=utils.random_mac(),
                 ip4=ip4,
                 ip4_mask=ip4_mask,
             )
-            interface = node.newnetif(control_net, interface_data)
-            interface.control = True
+            iface = node.new_iface(control_net, iface_data)
+            iface.control = True
         except ValueError:
             msg = f"Control interface not added to node {node.id}. "
             msg += f"Invalid control network prefix ({control_net.prefix}). "
             msg += "A longer prefix length may be required for this many nodes."
             logging.exception(msg)
 
-    def update_control_interface_hosts(
+    def update_control_iface_hosts(
         self, net_index: int = 0, remove: bool = False
     ) -> None:
         """
@@ -1549,9 +1546,9 @@ class Session:
             return
 
         entries = []
-        for interface in control_net.netifs():
-            name = interface.node.name
-            for address in interface.addrlist:
+        for iface in control_net.get_ifaces():
+            name = iface.node.name
+            for address in iface.addrlist:
                 address = address.split("/")[0]
                 entries.append(f"{address} {name}")
 
diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py
index 5c1c52a0..8b0c423c 100644
--- a/daemon/core/gui/coreclient.py
+++ b/daemon/core/gui/coreclient.py
@@ -57,8 +57,8 @@ class CoreClient:
         self.read_config()
 
         # helpers
-        self.interface_to_edge = {}
-        self.interfaces_manager = InterfaceManager(self.app)
+        self.iface_to_edge = {}
+        self.ifaces_manager = InterfaceManager(self.app)
 
         # session data
         self.state = None
@@ -91,8 +91,8 @@ class CoreClient:
 
     def reset(self):
         # helpers
-        self.interfaces_manager.reset()
-        self.interface_to_edge.clear()
+        self.ifaces_manager.reset()
+        self.iface_to_edge.clear()
         # session data
         self.canvas_nodes.clear()
         self.links.clear()
@@ -263,7 +263,7 @@ class CoreClient:
             self.emane_config = response.config
 
             # update interface manager
-            self.interfaces_manager.joined(session.links)
+            self.ifaces_manager.joined(session.links)
 
             # draw session
             self.app.canvas.reset_and_redraw(session)
@@ -278,11 +278,11 @@ class CoreClient:
             # get emane model config
             response = self.client.get_emane_model_configs(self.session_id)
             for config in response.configs:
-                interface = None
-                if config.interface != -1:
-                    interface = config.interface
+                iface_id = None
+                if config.iface_id != -1:
+                    iface_id = config.iface_id
                 canvas_node = self.canvas_nodes[config.node_id]
-                canvas_node.emane_model_configs[(config.model, interface)] = dict(
+                canvas_node.emane_model_configs[(config.model, iface_id)] = dict(
                     config.config
                 )
 
@@ -460,16 +460,16 @@ class CoreClient:
             self.app.show_grpc_exception("Edit Node Error", e)
 
     def start_session(self) -> core_pb2.StartSessionResponse:
-        self.interfaces_manager.reset_mac()
+        self.ifaces_manager.reset_mac()
         nodes = [x.core_node for x in self.canvas_nodes.values()]
         links = []
         for edge in self.links.values():
             link = core_pb2.Link()
             link.CopyFrom(edge.link)
-            if link.HasField("interface1") and not link.interface1.mac:
-                link.interface1.mac = self.interfaces_manager.next_mac()
-            if link.HasField("interface2") and not link.interface2.mac:
-                link.interface2.mac = self.interfaces_manager.next_mac()
+            if link.HasField("iface1") and not link.iface1.mac:
+                link.iface1.mac = self.ifaces_manager.next_mac()
+            if link.HasField("iface2") and not link.iface2.mac:
+                link.iface2.mac = self.ifaces_manager.next_mac()
             links.append(link)
         wlan_configs = self.get_wlan_configs_proto()
         mobility_configs = self.get_mobility_configs_proto()
@@ -689,8 +689,8 @@ class CoreClient:
                 self.session_id,
                 link_proto.node1_id,
                 link_proto.node2_id,
-                link_proto.interface1,
-                link_proto.interface2,
+                link_proto.iface1,
+                link_proto.iface2,
                 link_proto.options,
             )
             logging.debug("create link: %s", response)
@@ -733,7 +733,7 @@ class CoreClient:
                 config_proto.node_id,
                 config_proto.model,
                 config_proto.config,
-                config_proto.interface_id,
+                config_proto.iface_id,
             )
         if self.emane_config:
             config = {x: self.emane_config[x].value for x in self.emane_config}
@@ -824,31 +824,31 @@ class CoreClient:
         for edge in edges:
             del self.links[edge.token]
             links.append(edge.link)
-        self.interfaces_manager.removed(links)
+        self.ifaces_manager.removed(links)
 
-    def create_interface(self, canvas_node: CanvasNode) -> core_pb2.Interface:
+    def create_iface(self, canvas_node: CanvasNode) -> core_pb2.Interface:
         node = canvas_node.core_node
-        ip4, ip6 = self.interfaces_manager.get_ips(node)
-        ip4_mask = self.interfaces_manager.ip4_mask
-        ip6_mask = self.interfaces_manager.ip6_mask
-        interface_id = canvas_node.next_interface_id()
-        name = f"eth{interface_id}"
-        interface = core_pb2.Interface(
-            id=interface_id,
+        ip4, ip6 = self.ifaces_manager.get_ips(node)
+        ip4_mask = self.ifaces_manager.ip4_mask
+        ip6_mask = self.ifaces_manager.ip6_mask
+        iface_id = canvas_node.next_iface_id()
+        name = f"eth{iface_id}"
+        iface = core_pb2.Interface(
+            id=iface_id,
             name=name,
             ip4=ip4,
-            ip4mask=ip4_mask,
+            ip4_mask=ip4_mask,
             ip6=ip6,
-            ip6mask=ip6_mask,
+            ip6_mask=ip6_mask,
         )
         logging.info(
             "create node(%s) interface(%s) IPv4(%s) IPv6(%s)",
             node.name,
-            interface.name,
-            interface.ip4,
-            interface.ip6,
+            iface.name,
+            iface.ip4,
+            iface.ip6,
         )
-        return interface
+        return iface
 
     def create_link(
         self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode
@@ -861,34 +861,34 @@ class CoreClient:
         dst_node = canvas_dst_node.core_node
 
         # determine subnet
-        self.interfaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)
+        self.ifaces_manager.determine_subnets(canvas_src_node, canvas_dst_node)
 
-        src_interface = None
+        src_iface = None
         if NodeUtils.is_container_node(src_node.type):
-            src_interface = self.create_interface(canvas_src_node)
-            self.interface_to_edge[(src_node.id, src_interface.id)] = edge.token
+            src_iface = self.create_iface(canvas_src_node)
+            self.iface_to_edge[(src_node.id, src_iface.id)] = edge.token
 
-        dst_interface = None
+        dst_iface = None
         if NodeUtils.is_container_node(dst_node.type):
-            dst_interface = self.create_interface(canvas_dst_node)
-            self.interface_to_edge[(dst_node.id, dst_interface.id)] = edge.token
+            dst_iface = self.create_iface(canvas_dst_node)
+            self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token
 
         link = core_pb2.Link(
             type=core_pb2.LinkType.WIRED,
             node1_id=src_node.id,
             node2_id=dst_node.id,
-            interface1=src_interface,
-            interface2=dst_interface,
+            iface1=src_iface,
+            iface2=dst_iface,
         )
         # assign after creating link proto, since interfaces are copied
-        if src_interface:
-            interface1 = link.interface1
-            edge.src_interface = interface1
-            canvas_src_node.interfaces[interface1.id] = interface1
-        if dst_interface:
-            interface2 = link.interface2
-            edge.dst_interface = interface2
-            canvas_dst_node.interfaces[interface2.id] = interface2
+        if src_iface:
+            iface1 = link.iface1
+            edge.src_iface = iface1
+            canvas_src_node.ifaces[iface1.id] = iface1
+        if dst_iface:
+            iface2 = link.iface2
+            edge.dst_iface = iface2
+            canvas_dst_node.ifaces[iface2.id] = iface2
         edge.set_link(link)
         self.links[edge.token] = edge
         logging.info("Add link between %s and %s", src_node.name, dst_node.name)
@@ -928,12 +928,12 @@ class CoreClient:
                 continue
             node_id = canvas_node.core_node.id
             for key, config in canvas_node.emane_model_configs.items():
-                model, interface = key
+                model, iface_id = key
                 config = {x: config[x].value for x in config}
-                if interface is None:
-                    interface = -1
+                if iface_id is None:
+                    iface_id = -1
                 config_proto = EmaneModelConfig(
-                    node_id=node_id, interface_id=interface, model=model, config=config
+                    node_id=node_id, iface_id=iface_id, model=model, config=config
                 )
                 configs.append(config_proto)
         return configs
@@ -1021,19 +1021,19 @@ class CoreClient:
         return dict(config)
 
     def get_emane_model_config(
-        self, node_id: int, model: str, interface: int = None
+        self, node_id: int, model: str, iface_id: int = None
     ) -> Dict[str, common_pb2.ConfigOption]:
-        if interface is None:
-            interface = -1
+        if iface_id is None:
+            iface_id = -1
         response = self.client.get_emane_model_config(
-            self.session_id, node_id, model, interface
+            self.session_id, node_id, model, iface_id
         )
         config = response.config
         logging.debug(
             "get emane model config: node id: %s, EMANE model: %s, interface: %s, config: %s",
             node_id,
             model,
-            interface,
+            iface_id,
             config,
         )
         return dict(config)
diff --git a/daemon/core/gui/dialogs/emaneconfig.py b/daemon/core/gui/dialogs/emaneconfig.py
index 000ebb05..8f7ca089 100644
--- a/daemon/core/gui/dialogs/emaneconfig.py
+++ b/daemon/core/gui/dialogs/emaneconfig.py
@@ -56,7 +56,7 @@ class EmaneModelDialog(Dialog):
         app: "Application",
         canvas_node: "CanvasNode",
         model: str,
-        interface: int = None,
+        iface_id: int = None,
     ):
         super().__init__(
             app, f"{canvas_node.core_node.name} {model} Configuration", master=master
@@ -64,16 +64,16 @@ class EmaneModelDialog(Dialog):
         self.canvas_node = canvas_node
         self.node = canvas_node.core_node
         self.model = f"emane_{model}"
-        self.interface = interface
+        self.iface_id = iface_id
         self.config_frame = None
         self.has_error = False
         try:
             self.config = self.canvas_node.emane_model_configs.get(
-                (self.model, self.interface)
+                (self.model, self.iface_id)
             )
             if not self.config:
                 self.config = self.app.core.get_emane_model_config(
-                    self.node.id, self.model, self.interface
+                    self.node.id, self.model, self.iface_id
                 )
             self.draw()
         except grpc.RpcError as e:
@@ -103,7 +103,7 @@ class EmaneModelDialog(Dialog):
 
     def click_apply(self):
         self.config_frame.parse_config()
-        key = (self.model, self.interface)
+        key = (self.model, self.iface_id)
         self.canvas_node.emane_model_configs[key] = self.config
         self.destroy()
 
diff --git a/daemon/core/gui/dialogs/ipdialog.py b/daemon/core/gui/dialogs/ipdialog.py
index 62f5d0ba..d31dcdff 100644
--- a/daemon/core/gui/dialogs/ipdialog.py
+++ b/daemon/core/gui/dialogs/ipdialog.py
@@ -146,6 +146,6 @@ class IpConfigDialog(Dialog):
         ip_config.ip6 = self.ip6
         ip_config.ip4s = ip4s
         ip_config.ip6s = ip6s
-        self.app.core.interfaces_manager.update_ips(self.ip4, self.ip6)
+        self.app.core.ifaces_manager.update_ips(self.ip4, self.ip6)
         self.app.save_config()
         self.destroy()
diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py
index 9c3fc987..adf8156f 100644
--- a/daemon/core/gui/dialogs/linkconfig.py
+++ b/daemon/core/gui/dialogs/linkconfig.py
@@ -227,21 +227,21 @@ class LinkConfigurationDialog(Dialog):
         )
         link.options.CopyFrom(options)
 
-        interface1_id = None
-        if link.HasField("interface1"):
-            interface1_id = link.interface1.id
-        interface2_id = None
-        if link.HasField("interface2"):
-            interface2_id = link.interface2.id
+        iface1_id = None
+        if link.HasField("iface1"):
+            iface1_id = link.iface1.id
+        iface2_id = None
+        if link.HasField("iface2"):
+            iface2_id = link.iface2.id
 
         if not self.is_symmetric:
             link.options.unidirectional = True
-            asym_interface1 = None
-            if interface1_id:
-                asym_interface1 = core_pb2.Interface(id=interface1_id)
-            asym_interface2 = None
-            if interface2_id:
-                asym_interface2 = core_pb2.Interface(id=interface2_id)
+            asym_iface1 = None
+            if iface1_id:
+                asym_iface1 = core_pb2.Interface(id=iface1_id)
+            asym_iface2 = None
+            if iface2_id:
+                asym_iface2 = core_pb2.Interface(id=iface2_id)
             down_bandwidth = get_int(self.down_bandwidth)
             down_jitter = get_int(self.down_jitter)
             down_delay = get_int(self.down_delay)
@@ -258,8 +258,8 @@ class LinkConfigurationDialog(Dialog):
             self.edge.asymmetric_link = core_pb2.Link(
                 node1_id=link.node2_id,
                 node2_id=link.node1_id,
-                interface1=asym_interface1,
-                interface2=asym_interface2,
+                iface1=asym_iface1,
+                iface2=asym_iface2,
                 options=options,
             )
         else:
@@ -273,8 +273,8 @@ class LinkConfigurationDialog(Dialog):
                 link.node1_id,
                 link.node2_id,
                 link.options,
-                interface1_id,
-                interface2_id,
+                iface1_id,
+                iface2_id,
             )
             if self.edge.asymmetric_link:
                 self.app.core.client.edit_link(
@@ -282,8 +282,8 @@ class LinkConfigurationDialog(Dialog):
                     link.node2_id,
                     link.node1_id,
                     self.edge.asymmetric_link.options,
-                    interface1_id,
-                    interface2_id,
+                    iface1_id,
+                    iface2_id,
                 )
 
         self.destroy()
diff --git a/daemon/core/gui/dialogs/macdialog.py b/daemon/core/gui/dialogs/macdialog.py
index caca9fd0..46414cf9 100644
--- a/daemon/core/gui/dialogs/macdialog.py
+++ b/daemon/core/gui/dialogs/macdialog.py
@@ -55,7 +55,7 @@ class MacConfigDialog(Dialog):
         if not netaddr.valid_mac(mac):
             messagebox.showerror("MAC Error", f"{mac} is an invalid mac")
         else:
-            self.app.core.interfaces_manager.mac = netaddr.EUI(mac)
+            self.app.core.ifaces_manager.mac = netaddr.EUI(mac)
             self.app.guiconfig.mac = mac
             self.app.save_config()
             self.destroy()
diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py
index 0d46ae06..cec9e9f9 100644
--- a/daemon/core/gui/dialogs/nodeconfig.py
+++ b/daemon/core/gui/dialogs/nodeconfig.py
@@ -111,7 +111,7 @@ class NodeConfigDialog(Dialog):
         if self.node.server:
             server = self.node.server
         self.server = tk.StringVar(value=server)
-        self.interfaces = {}
+        self.ifaces = {}
         self.draw()
 
     def draw(self):
@@ -183,53 +183,53 @@ class NodeConfigDialog(Dialog):
             row += 1
 
         if NodeUtils.is_rj45_node(self.node.type):
-            response = self.app.core.client.get_interfaces()
+            response = self.app.core.client.get_ifaces()
             logging.debug("host machine available interfaces: %s", response)
-            interfaces = ListboxScroll(frame)
-            interfaces.listbox.config(state=state)
-            interfaces.grid(
+            ifaces = ListboxScroll(frame)
+            ifaces.listbox.config(state=state)
+            ifaces.grid(
                 row=row, column=0, columnspan=2, sticky="ew", padx=PADX, pady=PADY
             )
-            for inf in sorted(response.interfaces[:]):
-                interfaces.listbox.insert(tk.END, inf)
+            for inf in sorted(response.ifaces[:]):
+                ifaces.listbox.insert(tk.END, inf)
             row += 1
-            interfaces.listbox.bind("<<ListboxSelect>>", self.interface_select)
+            ifaces.listbox.bind("<<ListboxSelect>>", self.iface_select)
 
         # interfaces
-        if self.canvas_node.interfaces:
-            self.draw_interfaces()
+        if self.canvas_node.ifaces:
+            self.draw_ifaces()
 
         self.draw_spacer()
         self.draw_buttons()
 
-    def draw_interfaces(self):
+    def draw_ifaces(self):
         notebook = ttk.Notebook(self.top)
         notebook.grid(sticky="nsew", pady=PADY)
         self.top.rowconfigure(notebook.grid_info()["row"], weight=1)
         state = tk.DISABLED if self.app.core.is_runtime() else tk.NORMAL
-        for interface_id in sorted(self.canvas_node.interfaces):
-            interface = self.canvas_node.interfaces[interface_id]
+        for iface_id in sorted(self.canvas_node.ifaces):
+            iface = self.canvas_node.ifaces[iface_id]
             tab = ttk.Frame(notebook, padding=FRAME_PAD)
             tab.grid(sticky="nsew", pady=PADY)
             tab.columnconfigure(1, weight=1)
             tab.columnconfigure(2, weight=1)
-            notebook.add(tab, text=interface.name)
+            notebook.add(tab, text=iface.name)
 
             row = 0
-            emane_node = self.canvas_node.has_emane_link(interface.id)
+            emane_node = self.canvas_node.has_emane_link(iface.id)
             if emane_node:
                 emane_model = emane_node.emane.split("_")[1]
                 button = ttk.Button(
                     tab,
                     text=f"Configure EMANE {emane_model}",
-                    command=lambda: self.click_emane_config(emane_model, interface.id),
+                    command=lambda: self.click_emane_config(emane_model, iface.id),
                 )
                 button.grid(row=row, sticky="ew", columnspan=3, pady=PADY)
                 row += 1
 
             label = ttk.Label(tab, text="MAC")
             label.grid(row=row, column=0, padx=PADX, pady=PADY)
-            auto_set = not interface.mac
+            auto_set = not iface.mac
             mac_state = tk.DISABLED if auto_set else tk.NORMAL
             is_auto = tk.BooleanVar(value=auto_set)
             checkbutton = ttk.Checkbutton(
@@ -237,7 +237,7 @@ class NodeConfigDialog(Dialog):
             )
             checkbutton.var = is_auto
             checkbutton.grid(row=row, column=1, padx=PADX)
-            mac = tk.StringVar(value=interface.mac)
+            mac = tk.StringVar(value=iface.mac)
             entry = ttk.Entry(tab, textvariable=mac, state=mac_state)
             entry.grid(row=row, column=2, sticky="ew")
             func = partial(mac_auto, is_auto, entry, mac)
@@ -247,8 +247,8 @@ class NodeConfigDialog(Dialog):
             label = ttk.Label(tab, text="IPv4")
             label.grid(row=row, column=0, padx=PADX, pady=PADY)
             ip4_net = ""
-            if interface.ip4:
-                ip4_net = f"{interface.ip4}/{interface.ip4mask}"
+            if iface.ip4:
+                ip4_net = f"{iface.ip4}/{iface.ip4_mask}"
             ip4 = tk.StringVar(value=ip4_net)
             entry = ttk.Entry(tab, textvariable=ip4, state=state)
             entry.grid(row=row, column=1, columnspan=2, sticky="ew")
@@ -257,13 +257,13 @@ class NodeConfigDialog(Dialog):
             label = ttk.Label(tab, text="IPv6")
             label.grid(row=row, column=0, padx=PADX, pady=PADY)
             ip6_net = ""
-            if interface.ip6:
-                ip6_net = f"{interface.ip6}/{interface.ip6mask}"
+            if iface.ip6:
+                ip6_net = f"{iface.ip6}/{iface.ip6_mask}"
             ip6 = tk.StringVar(value=ip6_net)
             entry = ttk.Entry(tab, textvariable=ip6, state=state)
             entry.grid(row=row, column=1, columnspan=2, sticky="ew")
 
-            self.interfaces[interface.id] = InterfaceData(is_auto, mac, ip4, ip6)
+            self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6)
 
     def draw_buttons(self):
         frame = ttk.Frame(self.top)
@@ -277,9 +277,9 @@ class NodeConfigDialog(Dialog):
         button = ttk.Button(frame, text="Cancel", command=self.destroy)
         button.grid(row=0, column=1, sticky="ew")
 
-    def click_emane_config(self, emane_model: str, interface_id: int):
+    def click_emane_config(self, emane_model: str, iface_id: int):
         dialog = EmaneModelDialog(
-            self, self.app, self.canvas_node, emane_model, interface_id
+            self, self.app, self.canvas_node, emane_model, iface_id
         )
         dialog.show()
 
@@ -309,54 +309,54 @@ class NodeConfigDialog(Dialog):
         self.canvas_node.image = self.image
 
         # update node interface data
-        for interface in self.canvas_node.interfaces.values():
-            data = self.interfaces[interface.id]
+        for iface in self.canvas_node.ifaces.values():
+            data = self.ifaces[iface.id]
 
             # validate ip4
             ip4_net = data.ip4.get()
-            if not check_ip4(self, interface.name, ip4_net):
+            if not check_ip4(self, iface.name, ip4_net):
                 error = True
                 break
             if ip4_net:
-                ip4, ip4mask = ip4_net.split("/")
-                ip4mask = int(ip4mask)
+                ip4, ip4_mask = ip4_net.split("/")
+                ip4_mask = int(ip4_mask)
             else:
-                ip4, ip4mask = "", 0
-            interface.ip4 = ip4
-            interface.ip4mask = ip4mask
+                ip4, ip4_mask = "", 0
+            iface.ip4 = ip4
+            iface.ip4_mask = ip4_mask
 
             # validate ip6
             ip6_net = data.ip6.get()
-            if not check_ip6(self, interface.name, ip6_net):
+            if not check_ip6(self, iface.name, ip6_net):
                 error = True
                 break
             if ip6_net:
-                ip6, ip6mask = ip6_net.split("/")
-                ip6mask = int(ip6mask)
+                ip6, ip6_mask = ip6_net.split("/")
+                ip6_mask = int(ip6_mask)
             else:
-                ip6, ip6mask = "", 0
-            interface.ip6 = ip6
-            interface.ip6mask = ip6mask
+                ip6, ip6_mask = "", 0
+            iface.ip6 = ip6
+            iface.ip6_mask = ip6_mask
 
             mac = data.mac.get()
             auto_mac = data.is_auto.get()
             if not auto_mac and not netaddr.valid_mac(mac):
-                title = f"MAC Error for {interface.name}"
+                title = f"MAC Error for {iface.name}"
                 messagebox.showerror(title, "Invalid MAC Address")
                 error = True
                 break
             elif not auto_mac:
                 mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded)
-                interface.mac = str(mac)
+                iface.mac = str(mac)
 
         # redraw
         if not error:
             self.canvas_node.redraw()
             self.destroy()
 
-    def interface_select(self, event: tk.Event):
+    def iface_select(self, event: tk.Event):
         listbox = event.widget
         cur = listbox.curselection()
         if cur:
-            interface = listbox.get(cur[0])
-            self.name.set(interface)
+            iface = listbox.get(cur[0])
+            self.name.set(iface)
diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py
index 1d2264eb..ac637b28 100644
--- a/daemon/core/gui/graph/edges.py
+++ b/daemon/core/gui/graph/edges.py
@@ -259,8 +259,8 @@ class CanvasEdge(Edge):
         Create an instance of canvas edge object
         """
         super().__init__(canvas, src)
-        self.src_interface = None
-        self.dst_interface = None
+        self.src_iface = None
+        self.dst_iface = None
         self.text_src = None
         self.text_dst = None
         self.link = None
@@ -283,25 +283,25 @@ class CanvasEdge(Edge):
         self.link = link
         self.draw_labels()
 
-    def interface_label(self, interface: core_pb2.Interface) -> str:
+    def iface_label(self, iface: core_pb2.Interface) -> str:
         label = ""
-        if interface.name and self.canvas.show_interface_names.get():
-            label = f"{interface.name}"
-        if interface.ip4 and self.canvas.show_ip4s.get():
+        if iface.name and self.canvas.show_iface_names.get():
+            label = f"{iface.name}"
+        if iface.ip4 and self.canvas.show_ip4s.get():
             label = f"{label}\n" if label else ""
-            label += f"{interface.ip4}/{interface.ip4mask}"
-        if interface.ip6 and self.canvas.show_ip6s.get():
+            label += f"{iface.ip4}/{iface.ip4_mask}"
+        if iface.ip6 and self.canvas.show_ip6s.get():
             label = f"{label}\n" if label else ""
-            label += f"{interface.ip6}/{interface.ip6mask}"
+            label += f"{iface.ip6}/{iface.ip6_mask}"
         return label
 
     def create_node_labels(self) -> Tuple[str, str]:
         label1 = None
-        if self.link.HasField("interface1"):
-            label1 = self.interface_label(self.link.interface1)
+        if self.link.HasField("iface1"):
+            label1 = self.iface_label(self.link.iface1)
         label2 = None
-        if self.link.HasField("interface2"):
-            label2 = self.interface_label(self.link.interface2)
+        if self.link.HasField("iface2"):
+            label2 = self.iface_label(self.link.iface2)
         return label1, label2
 
     def draw_labels(self) -> None:
diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py
index 90dcd9f6..269e3973 100644
--- a/daemon/core/gui/graph/graph.py
+++ b/daemon/core/gui/graph/graph.py
@@ -97,7 +97,7 @@ class CanvasGraph(tk.Canvas):
         self.show_link_labels = ShowVar(self, tags.LINK_LABEL, value=True)
         self.show_grid = ShowVar(self, tags.GRIDLINE, value=True)
         self.show_annotations = ShowVar(self, tags.ANNOTATION, value=True)
-        self.show_interface_names = BooleanVar(value=False)
+        self.show_iface_names = BooleanVar(value=False)
         self.show_ip4s = BooleanVar(value=True)
         self.show_ip6s = BooleanVar(value=True)
 
@@ -136,7 +136,7 @@ class CanvasGraph(tk.Canvas):
         self.show_link_labels.set(True)
         self.show_grid.set(True)
         self.show_annotations.set(True)
-        self.show_interface_names.set(False)
+        self.show_iface_names.set(False)
         self.show_ip4s.set(True)
         self.show_ip6s.set(True)
 
@@ -195,19 +195,19 @@ class CanvasGraph(tk.Canvas):
         return valid_topleft and valid_bottomright
 
     def set_throughputs(self, throughputs_event: core_pb2.ThroughputsEvent):
-        for interface_throughput in throughputs_event.interface_throughputs:
-            node_id = interface_throughput.node_id
-            interface_id = interface_throughput.interface_id
-            throughput = interface_throughput.throughput
-            interface_to_edge_id = (node_id, interface_id)
-            token = self.core.interface_to_edge.get(interface_to_edge_id)
+        for iface_throughput in throughputs_event.iface_throughputs:
+            node_id = iface_throughput.node_id
+            iface_id = iface_throughput.iface_id
+            throughput = iface_throughput.throughput
+            iface_to_edge_id = (node_id, iface_id)
+            token = self.core.iface_to_edge.get(iface_to_edge_id)
             if not token:
                 continue
             edge = self.edges.get(token)
             if edge:
                 edge.set_throughput(throughput)
             else:
-                del self.core.interface_to_edge[interface_to_edge_id]
+                del self.core.iface_to_edge[iface_to_edge_id]
 
     def draw_grid(self):
         """
@@ -321,18 +321,16 @@ class CanvasGraph(tk.Canvas):
                     canvas_node2.edges.add(edge)
                     self.edges[edge.token] = edge
                     self.core.links[edge.token] = edge
-                    if link.HasField("interface1"):
-                        interface1 = link.interface1
-                        self.core.interface_to_edge[(node1.id, interface1.id)] = token
-                        canvas_node1.interfaces[interface1.id] = interface1
-                        edge.src_interface = interface1
-                    if link.HasField("interface2"):
-                        interface2 = link.interface2
-                        self.core.interface_to_edge[
-                            (node2.id, interface2.id)
-                        ] = edge.token
-                        canvas_node2.interfaces[interface2.id] = interface2
-                        edge.dst_interface = interface2
+                    if link.HasField("iface1"):
+                        iface1 = link.iface1
+                        self.core.iface_to_edge[(node1.id, iface1.id)] = token
+                        canvas_node1.ifaces[iface1.id] = iface1
+                        edge.src_iface = iface1
+                    if link.HasField("iface2"):
+                        iface2 = link.iface2
+                        self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token
+                        canvas_node2.ifaces[iface2.id] = iface2
+                        edge.dst_iface = iface2
                 elif link.options.unidirectional:
                     edge = self.edges[token]
                     edge.asymmetric_link = link
@@ -513,14 +511,14 @@ class CanvasGraph(tk.Canvas):
                     edge.delete()
                     # update node connected to edge being deleted
                     other_id = edge.src
-                    other_interface = edge.src_interface
+                    other_iface = edge.src_iface
                     if edge.src == object_id:
                         other_id = edge.dst
-                        other_interface = edge.dst_interface
+                        other_iface = edge.dst_iface
                     other_node = self.nodes[other_id]
                     other_node.edges.remove(edge)
-                    if other_interface:
-                        del other_node.interfaces[other_interface.id]
+                    if other_iface:
+                        del other_node.ifaces[other_iface.id]
                     if is_wireless:
                         other_node.delete_antenna()
 
@@ -538,12 +536,12 @@ class CanvasGraph(tk.Canvas):
         del self.edges[edge.token]
         src_node = self.nodes[edge.src]
         src_node.edges.discard(edge)
-        if edge.src_interface:
-            del src_node.interfaces[edge.src_interface.id]
+        if edge.src_iface:
+            del src_node.ifaces[edge.src_iface.id]
         dst_node = self.nodes[edge.dst]
         dst_node.edges.discard(edge)
-        if edge.dst_interface:
-            del dst_node.interfaces[edge.dst_interface.id]
+        if edge.dst_iface:
+            del dst_node.ifaces[edge.dst_iface.id]
         src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type)
         if src_wireless:
             dst_node.delete_antenna()
@@ -963,26 +961,26 @@ class CanvasGraph(tk.Canvas):
             copy_link = copy_edge.link
             options = edge.link.options
             copy_link.options.CopyFrom(options)
-            interface1_id = None
-            if copy_link.HasField("interface1"):
-                interface1_id = copy_link.interface1.id
-            interface2_id = None
-            if copy_link.HasField("interface2"):
-                interface2_id = copy_link.interface2.id
+            iface1_id = None
+            if copy_link.HasField("iface1"):
+                iface1_id = copy_link.iface1.id
+            iface2_id = None
+            if copy_link.HasField("iface2"):
+                iface2_id = copy_link.iface2.id
             if not options.unidirectional:
                 copy_edge.asymmetric_link = None
             else:
-                asym_interface1 = None
-                if interface1_id:
-                    asym_interface1 = core_pb2.Interface(id=interface1_id)
-                asym_interface2 = None
-                if interface2_id:
-                    asym_interface2 = core_pb2.Interface(id=interface2_id)
+                asym_iface1 = None
+                if iface1_id:
+                    asym_iface1 = core_pb2.Interface(id=iface1_id)
+                asym_iface2 = None
+                if iface2_id:
+                    asym_iface2 = core_pb2.Interface(id=iface2_id)
                 copy_edge.asymmetric_link = core_pb2.Link(
                     node1_id=copy_link.node2_id,
                     node2_id=copy_link.node1_id,
-                    interface1=asym_interface1,
-                    interface2=asym_interface2,
+                    iface1=asym_iface1,
+                    iface2=asym_iface2,
                     options=edge.asymmetric_link.options,
                 )
             self.itemconfig(
diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py
index 8ad3f02a..3ba4b3f7 100644
--- a/daemon/core/gui/graph/node.py
+++ b/daemon/core/gui/graph/node.py
@@ -55,7 +55,7 @@ class CanvasNode:
         )
         self.tooltip = CanvasTooltip(self.canvas)
         self.edges = set()
-        self.interfaces = {}
+        self.ifaces = {}
         self.wireless_edges = set()
         self.antennas = []
         self.antenna_images = {}
@@ -70,9 +70,9 @@ class CanvasNode:
         self.context = tk.Menu(self.canvas)
         themes.style_menu(self.context)
 
-    def next_interface_id(self) -> int:
+    def next_iface_id(self) -> int:
         i = 0
-        while i in self.interfaces:
+        while i in self.ifaces:
             i += 1
         return i
 
@@ -300,16 +300,16 @@ class CanvasNode:
         dialog = NodeConfigServiceDialog(self.app, self)
         dialog.show()
 
-    def has_emane_link(self, interface_id: int) -> core_pb2.Node:
+    def has_emane_link(self, iface_id: int) -> core_pb2.Node:
         result = None
         for edge in self.edges:
             if self.id == edge.src:
                 other_id = edge.dst
-                edge_interface_id = edge.src_interface.id
+                edge_iface_id = edge.src_iface.id
             else:
                 other_id = edge.src
-                edge_interface_id = edge.dst_interface.id
-            if edge_interface_id != interface_id:
+                edge_iface_id = edge.dst_iface.id
+            if edge_iface_id != iface_id:
                 continue
             other_node = self.canvas.nodes[other_id]
             if other_node.core_node.type == NodeType.EMANE:
diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py
index 34270f56..6c82ca51 100644
--- a/daemon/core/gui/interface.py
+++ b/daemon/core/gui/interface.py
@@ -12,10 +12,10 @@ if TYPE_CHECKING:
     from core.gui.graph.node import CanvasNode
 
 
-def get_index(interface: "core_pb2.Interface") -> Optional[int]:
-    if not interface.ip4:
+def get_index(iface: "core_pb2.Interface") -> Optional[int]:
+    if not iface.ip4:
         return None
-    net = netaddr.IPNetwork(f"{interface.ip4}/{interface.ip4mask}")
+    net = netaddr.IPNetwork(f"{iface.ip4}/{iface.ip4_mask}")
     ip_value = net.value
     cidr_value = net.cidr.value
     return ip_value - cidr_value
@@ -89,43 +89,43 @@ class InterfaceManager:
         remaining_subnets = set()
         for edge in self.app.core.links.values():
             link = edge.link
-            if link.HasField("interface1"):
-                subnets = self.get_subnets(link.interface1)
+            if link.HasField("iface1"):
+                subnets = self.get_subnets(link.iface1)
                 remaining_subnets.add(subnets)
-            if link.HasField("interface2"):
-                subnets = self.get_subnets(link.interface2)
+            if link.HasField("iface2"):
+                subnets = self.get_subnets(link.iface2)
                 remaining_subnets.add(subnets)
 
         # remove all subnets from used subnets when no longer present
         # or remove used indexes from subnet
-        interfaces = []
+        ifaces = []
         for link in links:
-            if link.HasField("interface1"):
-                interfaces.append(link.interface1)
-            if link.HasField("interface2"):
-                interfaces.append(link.interface2)
-        for interface in interfaces:
-            subnets = self.get_subnets(interface)
+            if link.HasField("iface1"):
+                ifaces.append(link.iface1)
+            if link.HasField("iface2"):
+                ifaces.append(link.iface2)
+        for iface in ifaces:
+            subnets = self.get_subnets(iface)
             if subnets not in remaining_subnets:
                 self.used_subnets.pop(subnets.key(), None)
             else:
-                index = get_index(interface)
+                index = get_index(iface)
                 if index is not None:
                     subnets.used_indexes.discard(index)
         self.current_subnets = None
 
     def joined(self, links: List["core_pb2.Link"]) -> None:
-        interfaces = []
+        ifaces = []
         for link in links:
-            if link.HasField("interface1"):
-                interfaces.append(link.interface1)
-            if link.HasField("interface2"):
-                interfaces.append(link.interface2)
+            if link.HasField("iface1"):
+                ifaces.append(link.iface1)
+            if link.HasField("iface2"):
+                ifaces.append(link.iface2)
 
         # add to used subnets and mark used indexes
-        for interface in interfaces:
-            subnets = self.get_subnets(interface)
-            index = get_index(interface)
+        for iface in ifaces:
+            subnets = self.get_subnets(iface)
+            index = get_index(iface)
             if index is None:
                 continue
             subnets.used_indexes.add(index)
@@ -150,13 +150,13 @@ class InterfaceManager:
         ip6 = self.current_subnets.ip6[index]
         return str(ip4), str(ip6)
 
-    def get_subnets(self, interface: "core_pb2.Interface") -> Subnets:
+    def get_subnets(self, iface: "core_pb2.Interface") -> Subnets:
         ip4_subnet = self.ip4_subnets
-        if interface.ip4:
-            ip4_subnet = IPNetwork(f"{interface.ip4}/{interface.ip4mask}").cidr
+        if iface.ip4:
+            ip4_subnet = IPNetwork(f"{iface.ip4}/{iface.ip4_mask}").cidr
         ip6_subnet = self.ip6_subnets
-        if interface.ip6:
-            ip6_subnet = IPNetwork(f"{interface.ip6}/{interface.ip6mask}").cidr
+        if iface.ip6:
+            ip6_subnet = IPNetwork(f"{iface.ip6}/{iface.ip6_mask}").cidr
         subnets = Subnets(ip4_subnet, ip6_subnet)
         return self.used_subnets.get(subnets.key(), subnets)
 
@@ -196,16 +196,16 @@ class InterfaceManager:
         for edge in canvas_node.edges:
             src_node = canvas.nodes[edge.src]
             dst_node = canvas.nodes[edge.dst]
-            interface = edge.src_interface
+            iface = edge.src_iface
             check_node = src_node
             if src_node == canvas_node:
-                interface = edge.dst_interface
+                iface = edge.dst_iface
                 check_node = dst_node
             if check_node.core_node.id in visited:
                 continue
             visited.add(check_node.core_node.id)
-            if interface:
-                subnets = self.get_subnets(interface)
+            if iface:
+                subnets = self.get_subnets(iface)
             else:
                 subnets = self.find_subnets(check_node, visited)
             if subnets:
diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py
index 62a9ceae..cf4216d8 100644
--- a/daemon/core/gui/menubar.py
+++ b/daemon/core/gui/menubar.py
@@ -139,7 +139,7 @@ class Menubar(tk.Menu):
         menu.add_checkbutton(
             label="Interface Names",
             command=self.click_edge_label_change,
-            variable=self.canvas.show_interface_names,
+            variable=self.canvas.show_iface_names,
         )
         menu.add_checkbutton(
             label="IPv4 Addresses",
diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py
index 43996ba3..9bb2966e 100644
--- a/daemon/core/location/mobility.py
+++ b/daemon/core/location/mobility.py
@@ -13,8 +13,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
 
 from core import utils
 from core.config import ConfigGroup, ConfigurableOptions, Configuration, ModelManager
-from core.emulator.data import EventData, LinkData
-from core.emulator.emudata import LinkOptions
+from core.emulator.data import EventData, LinkData, LinkOptions
 from core.emulator.enumerations import (
     ConfigDataTypes,
     EventTypes,
@@ -178,7 +177,7 @@ class MobilityManager(ModelManager):
         self.session.broadcast_event(event_data)
 
     def updatewlans(
-        self, moved: List[CoreNode], moved_netifs: List[CoreInterface]
+        self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]
     ) -> None:
         """
         A mobility script has caused nodes in the 'moved' list to move.
@@ -186,7 +185,7 @@ class MobilityManager(ModelManager):
         were to recalculate for each individual node movement.
 
         :param moved: moved nodes
-        :param moved_netifs: moved network interfaces
+        :param moved_ifaces: moved network interfaces
         :return: nothing
         """
         for node_id in self.nodes():
@@ -195,7 +194,7 @@ class MobilityManager(ModelManager):
             except CoreError:
                 continue
             if node.model:
-                node.model.update(moved, moved_netifs)
+                node.model.update(moved, moved_ifaces)
 
 
 class WirelessModel(ConfigurableOptions):
@@ -228,12 +227,12 @@ class WirelessModel(ConfigurableOptions):
         """
         return []
 
-    def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> None:
+    def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None:
         """
         Update this wireless model.
 
         :param moved: moved nodes
-        :param moved_netifs: moved network interfaces
+        :param moved_ifaces: moved network interfaces
         :return: nothing
         """
         raise NotImplementedError
@@ -301,8 +300,8 @@ class BasicRangeModel(WirelessModel):
         super().__init__(session, _id)
         self.session: "Session" = session
         self.wlan: WlanNode = session.get_node(_id, WlanNode)
-        self._netifs: Dict[CoreInterface, Tuple[float, float, float]] = {}
-        self._netifslock: threading.Lock = threading.Lock()
+        self.iface_to_pos: Dict[CoreInterface, Tuple[float, float, float]] = {}
+        self.iface_lock: threading.Lock = threading.Lock()
         self.range: int = 0
         self.bw: Optional[int] = None
         self.delay: Optional[int] = None
@@ -333,48 +332,48 @@ class BasicRangeModel(WirelessModel):
         Apply link parameters to all interfaces. This is invoked from
         WlanNode.setmodel() after the position callback has been set.
         """
-        with self._netifslock:
-            for netif in self._netifs:
+        with self.iface_lock:
+            for iface in self.iface_to_pos:
                 options = LinkOptions(
                     bandwidth=self.bw,
                     delay=self.delay,
                     loss=self.loss,
                     jitter=self.jitter,
                 )
-                self.wlan.linkconfig(netif, options)
+                self.wlan.linkconfig(iface, options)
 
-    def get_position(self, netif: CoreInterface) -> Tuple[float, float, float]:
+    def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]:
         """
         Retrieve network interface position.
 
-        :param netif: network interface position to retrieve
+        :param iface: network interface position to retrieve
         :return: network interface position
         """
-        with self._netifslock:
-            return self._netifs[netif]
+        with self.iface_lock:
+            return self.iface_to_pos[iface]
 
-    def set_position(self, netif: CoreInterface) -> None:
+    def set_position(self, iface: CoreInterface) -> None:
         """
         A node has moved; given an interface, a new (x,y,z) position has
         been set; calculate the new distance between other nodes and link or
         unlink node pairs based on the configured range.
 
-        :param netif: network interface to set position for
+        :param iface: network interface to set position for
         :return: nothing
         """
-        x, y, z = netif.node.position.get()
-        self._netifslock.acquire()
-        self._netifs[netif] = (x, y, z)
+        x, y, z = iface.node.position.get()
+        self.iface_lock.acquire()
+        self.iface_to_pos[iface] = (x, y, z)
         if x is None or y is None:
-            self._netifslock.release()
+            self.iface_lock.release()
             return
-        for netif2 in self._netifs:
-            self.calclink(netif, netif2)
-        self._netifslock.release()
+        for iface2 in self.iface_to_pos:
+            self.calclink(iface, iface2)
+        self.iface_lock.release()
 
     position_callback = set_position
 
-    def update(self, moved: List[CoreNode], moved_netifs: List[CoreInterface]) -> None:
+    def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None:
         """
         Node positions have changed without recalc. Update positions from
         node.position, then re-calculate links for those that have moved.
@@ -382,37 +381,37 @@ class BasicRangeModel(WirelessModel):
         one of the nodes has moved.
 
         :param moved: moved nodes
-        :param moved_netifs: moved network interfaces
+        :param moved_ifaces: moved network interfaces
         :return: nothing
         """
-        with self._netifslock:
-            while len(moved_netifs):
-                netif = moved_netifs.pop()
-                nx, ny, nz = netif.node.getposition()
-                if netif in self._netifs:
-                    self._netifs[netif] = (nx, ny, nz)
-                for netif2 in self._netifs:
-                    if netif2 in moved_netifs:
+        with self.iface_lock:
+            while len(moved_ifaces):
+                iface = moved_ifaces.pop()
+                nx, ny, nz = iface.node.getposition()
+                if iface in self.iface_to_pos:
+                    self.iface_to_pos[iface] = (nx, ny, nz)
+                for iface2 in self.iface_to_pos:
+                    if iface2 in moved_ifaces:
                         continue
-                    self.calclink(netif, netif2)
+                    self.calclink(iface, iface2)
 
-    def calclink(self, netif: CoreInterface, netif2: CoreInterface) -> None:
+    def calclink(self, iface: CoreInterface, iface2: CoreInterface) -> None:
         """
         Helper used by set_position() and update() to
         calculate distance between two interfaces and perform
         linking/unlinking. Sends link/unlink messages and updates the
         WlanNode's linked dict.
 
-        :param netif: interface one
-        :param netif2: interface two
+        :param iface: interface one
+        :param iface2: interface two
         :return: nothing
         """
-        if netif == netif2:
+        if iface == iface2:
             return
 
         try:
-            x, y, z = self._netifs[netif]
-            x2, y2, z2 = self._netifs[netif2]
+            x, y, z = self.iface_to_pos[iface]
+            x2, y2, z2 = self.iface_to_pos[iface2]
 
             if x2 is None or y2 is None:
                 return
@@ -420,8 +419,8 @@ class BasicRangeModel(WirelessModel):
             d = self.calcdistance((x, y, z), (x2, y2, z2))
 
             # ordering is important, to keep the wlan._linked dict organized
-            a = min(netif, netif2)
-            b = max(netif, netif2)
+            a = min(iface, iface2)
+            b = max(iface, iface2)
 
             with self.wlan._linked_lock:
                 linked = self.wlan.linked(a, b)
@@ -475,42 +474,39 @@ class BasicRangeModel(WirelessModel):
         self.setlinkparams()
 
     def create_link_data(
-        self,
-        interface1: CoreInterface,
-        interface2: CoreInterface,
-        message_type: MessageFlags,
+        self, iface1: CoreInterface, iface2: CoreInterface, message_type: MessageFlags
     ) -> LinkData:
         """
         Create a wireless link/unlink data message.
 
-        :param interface1: interface one
-        :param interface2: interface two
+        :param iface1: interface one
+        :param iface2: interface two
         :param message_type: link message type
         :return: link data
         """
         color = self.session.get_link_color(self.wlan.id)
         return LinkData(
             message_type=message_type,
-            node1_id=interface1.node.id,
-            node2_id=interface2.node.id,
+            type=LinkTypes.WIRELESS,
+            node1_id=iface1.node.id,
+            node2_id=iface2.node.id,
             network_id=self.wlan.id,
-            link_type=LinkTypes.WIRELESS,
             color=color,
         )
 
     def sendlinkmsg(
-        self, netif: CoreInterface, netif2: CoreInterface, unlink: bool = False
+        self, iface: CoreInterface, iface2: CoreInterface, unlink: bool = False
     ) -> None:
         """
         Send a wireless link/unlink API message to the GUI.
 
-        :param netif: interface one
-        :param netif2: interface two
+        :param iface: interface one
+        :param iface2: interface two
         :param unlink: unlink or not
         :return: nothing
         """
         message_type = MessageFlags.DELETE if unlink else MessageFlags.ADD
-        link_data = self.create_link_data(netif, netif2, message_type)
+        link_data = self.create_link_data(iface, iface2, message_type)
         self.session.broadcast_link(link_data)
 
     def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
@@ -643,17 +639,17 @@ class WayPointMobility(WirelessModel):
                     return
                 return self.run()
 
-        # only move netifs attached to self.wlan, or all nodenum in script?
+        # only move interfaces attached to self.wlan, or all nodenum in script?
         moved = []
-        moved_netifs = []
-        for netif in self.wlan.netifs():
-            node = netif.node
+        moved_ifaces = []
+        for iface in self.wlan.get_ifaces():
+            node = iface.node
             if self.movenode(node, dt):
                 moved.append(node)
-                moved_netifs.append(netif)
+                moved_ifaces.append(iface)
 
         # calculate all ranges after moving nodes; this saves calculations
-        self.session.mobility.updatewlans(moved, moved_netifs)
+        self.session.mobility.updatewlans(moved, moved_ifaces)
 
         # TODO: check session state
         self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround)
@@ -725,16 +721,16 @@ class WayPointMobility(WirelessModel):
         :return: nothing
         """
         moved = []
-        moved_netifs = []
-        for netif in self.wlan.netifs():
-            node = netif.node
+        moved_ifaces = []
+        for iface in self.wlan.get_ifaces():
+            node = iface.node
             if node.id not in self.initial:
                 continue
             x, y, z = self.initial[node.id].coords
             self.setnodeposition(node, x, y, z)
             moved.append(node)
-            moved_netifs.append(netif)
-        self.session.mobility.updatewlans(moved, moved_netifs)
+            moved_ifaces.append(iface)
+        self.session.mobility.updatewlans(moved, moved_ifaces)
 
     def addwaypoint(
         self,
diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py
index 6c7ebcf0..8a5c579a 100644
--- a/daemon/core/nodes/base.py
+++ b/daemon/core/nodes/base.py
@@ -14,8 +14,7 @@ import netaddr
 from core import utils
 from core.configservice.dependencies import ConfigServiceDependencies
 from core.constants import MOUNT_BIN, VNODED_BIN
-from core.emulator.data import LinkData, NodeData
-from core.emulator.emudata import InterfaceData, LinkOptions
+from core.emulator.data import InterfaceData, LinkData, LinkOptions
 from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes
 from core.errors import CoreCommandError, CoreError
 from core.nodes.client import VnodeClient
@@ -68,11 +67,10 @@ class NodeBase(abc.ABC):
         self.server: "DistributedServer" = server
         self.type: Optional[str] = None
         self.services: CoreServices = []
-        self._netif: Dict[int, CoreInterface] = {}
-        self.ifindex: int = 0
+        self.ifaces: Dict[int, CoreInterface] = {}
+        self.iface_id: int = 0
         self.canvas: Optional[int] = None
         self.icon: Optional[str] = None
-        self.opaque: Optional[str] = None
         self.position: Position = Position()
         self.up: bool = False
         use_ovs = session.options.get_config("ovs") == "True"
@@ -139,103 +137,54 @@ class NodeBase(abc.ABC):
         """
         return self.position.get()
 
-    def ifname(self, ifindex: int) -> str:
-        """
-        Retrieve interface name for index.
+    def get_iface(self, iface_id: int) -> CoreInterface:
+        if iface_id not in self.ifaces:
+            raise CoreError(f"node({self.name}) does not have interface({iface_id})")
+        return self.ifaces[iface_id]
 
-        :param ifindex: interface index
-        :return: interface name
+    def get_ifaces(self, control: bool = True) -> List[CoreInterface]:
         """
-        return self._netif[ifindex].name
+        Retrieve sorted list of interfaces, optionally do not include control
+        interfaces.
 
-    def netifs(self, sort: bool = False) -> List[CoreInterface]:
+        :param control: False to exclude control interfaces, included otherwise
+        :return: list of interfaces
         """
-        Retrieve network interfaces, sorted if desired.
+        ifaces = []
+        for iface_id in sorted(self.ifaces):
+            iface = self.ifaces[iface_id]
+            if not control and getattr(iface, "control", False):
+                continue
+            ifaces.append(iface)
+        return ifaces
 
-        :param sort: boolean used to determine if interfaces should be sorted
-        :return: network interfaces
+    def get_iface_id(self, iface: CoreInterface) -> int:
         """
-        if sort:
-            return [self._netif[x] for x in sorted(self._netif)]
-        else:
-            return list(self._netif.values())
+        Retrieve id for an interface.
 
-    def numnetif(self) -> int:
-        """
-        Return the attached interface count.
-
-        :return: number of network interfaces
-        """
-        return len(self._netif)
-
-    def getifindex(self, netif: CoreInterface) -> int:
-        """
-        Retrieve index for an interface.
-
-        :param netif: interface to get index for
+        :param iface: interface to get id for
         :return: interface index if found, -1 otherwise
         """
-        for ifindex in self._netif:
-            if self._netif[ifindex] is netif:
-                return ifindex
-        return -1
+        for iface_id, local_iface in self.ifaces.items():
+            if local_iface is iface:
+                return iface_id
+        raise CoreError(f"node({self.name}) does not have interface({iface.name})")
 
-    def newifindex(self) -> int:
+    def next_iface_id(self) -> int:
         """
         Create a new interface index.
 
         :return: interface index
         """
-        while self.ifindex in self._netif:
-            self.ifindex += 1
-        ifindex = self.ifindex
-        self.ifindex += 1
-        return ifindex
-
-    def data(
-        self, message_type: MessageFlags = MessageFlags.NONE, source: str = None
-    ) -> Optional[NodeData]:
-        """
-        Build a data object for this node.
-
-        :param message_type: purpose for the data object we are creating
-        :param source: source of node data
-        :return: node data object
-        """
-        if self.apitype is None:
-            return None
-
-        x, y, _ = self.getposition()
-        model = self.type
-        server = None
-        if self.server is not None:
-            server = self.server.name
-        services = [service.name for service in self.services]
-        return NodeData(
-            message_type=message_type,
-            id=self.id,
-            node_type=self.apitype,
-            name=self.name,
-            emulation_id=self.id,
-            canvas=self.canvas,
-            icon=self.icon,
-            opaque=self.opaque,
-            x_position=x,
-            y_position=y,
-            latitude=self.position.lat,
-            longitude=self.position.lon,
-            altitude=self.position.alt,
-            model=model,
-            server=server,
-            services=services,
-            source=source,
-        )
+        while self.iface_id in self.ifaces:
+            self.iface_id += 1
+        iface_id = self.iface_id
+        self.iface_id += 1
+        return iface_id
 
     def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
         """
-        Build CORE Link data for this object. There is no default
-        method for PyCoreObjs as PyCoreNodes do not implement this but
-        PyCoreNets do.
+        Build link data for this node.
 
         :param flags: message flags
         :return: list of link data
@@ -325,14 +274,14 @@ class CoreNodeBase(NodeBase):
         raise NotImplementedError
 
     @abc.abstractmethod
-    def newnetif(
-        self, net: "CoreNetworkBase", interface_data: InterfaceData
+    def new_iface(
+        self, net: "CoreNetworkBase", iface_data: InterfaceData
     ) -> CoreInterface:
         """
-        Create a new network interface.
+        Create a new interface.
 
         :param net: network to associate with
-        :param interface_data: interface data for new interface
+        :param iface_data: interface data for new interface
         :return: interface index
         """
         raise NotImplementedError
@@ -399,67 +348,53 @@ class CoreNodeBase(NodeBase):
         if self.tmpnodedir:
             self.host_cmd(f"rm -rf {self.nodedir}")
 
-    def addnetif(self, netif: CoreInterface, ifindex: int) -> None:
+    def add_iface(self, iface: CoreInterface, iface_id: int) -> None:
         """
         Add network interface to node and set the network interface index if successful.
 
-        :param netif: network interface to add
-        :param ifindex: interface index
+        :param iface: network interface to add
+        :param iface_id: interface id
         :return: nothing
         """
-        if ifindex in self._netif:
-            raise ValueError(f"ifindex {ifindex} already exists")
-        self._netif[ifindex] = netif
-        netif.netindex = ifindex
+        if iface_id in self.ifaces:
+            raise CoreError(f"interface({iface_id}) already exists")
+        self.ifaces[iface_id] = iface
+        iface.node_id = iface_id
 
-    def delnetif(self, ifindex: int) -> None:
+    def delete_iface(self, iface_id: int) -> None:
         """
         Delete a network interface
 
-        :param ifindex: interface index to delete
+        :param iface_id: interface index to delete
         :return: nothing
         """
-        if ifindex not in self._netif:
-            raise CoreError(f"node({self.name}) ifindex({ifindex}) does not exist")
-        netif = self._netif.pop(ifindex)
-        logging.info("node(%s) removing interface(%s)", self.name, netif.name)
-        netif.detachnet()
-        netif.shutdown()
+        if iface_id not in self.ifaces:
+            raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
+        iface = self.ifaces.pop(iface_id)
+        logging.info("node(%s) removing interface(%s)", self.name, iface.name)
+        iface.detachnet()
+        iface.shutdown()
 
-    def netif(self, ifindex: int) -> Optional[CoreInterface]:
-        """
-        Retrieve network interface.
-
-        :param ifindex: index of interface to retrieve
-        :return: network interface, or None if not found
-        """
-        if ifindex in self._netif:
-            return self._netif[ifindex]
-        else:
-            return None
-
-    def attachnet(self, ifindex: int, net: "CoreNetworkBase") -> None:
+    def attachnet(self, iface_id: int, net: "CoreNetworkBase") -> None:
         """
         Attach a network.
 
-        :param ifindex: interface of index to attach
+        :param iface_id: interface of index to attach
         :param net: network to attach
         :return: nothing
         """
-        if ifindex not in self._netif:
-            raise ValueError(f"ifindex {ifindex} does not exist")
-        self._netif[ifindex].attachnet(net)
+        iface = self.get_iface(iface_id)
+        iface.attachnet(net)
 
-    def detachnet(self, ifindex: int) -> None:
+    def detachnet(self, iface_id: int) -> None:
         """
         Detach network interface.
 
-        :param ifindex: interface index to detach
+        :param iface_id: interface id to detach
         :return: nothing
         """
-        if ifindex not in self._netif:
-            raise ValueError(f"ifindex {ifindex} does not exist")
-        self._netif[ifindex].detachnet()
+        iface = self.get_iface(iface_id)
+        iface.detachnet()
 
     def setposition(self, x: float = None, y: float = None, z: float = None) -> None:
         """
@@ -472,8 +407,8 @@ class CoreNodeBase(NodeBase):
         """
         changed = super().setposition(x, y, z)
         if changed:
-            for netif in self.netifs(sort=True):
-                netif.setposition()
+            for iface in self.get_ifaces():
+                iface.setposition()
 
     def commonnets(
         self, node: "CoreNodeBase", want_ctrl: bool = False
@@ -488,12 +423,10 @@ class CoreNodeBase(NodeBase):
         :return: tuples of common networks
         """
         common = []
-        for netif1 in self.netifs():
-            if not want_ctrl and hasattr(netif1, "control"):
-                continue
-            for netif2 in node.netifs():
-                if netif1.net == netif2.net:
-                    common.append((netif1.net, netif1, netif2))
+        for iface1 in self.get_ifaces(control=want_ctrl):
+            for iface2 in node.get_ifaces():
+                if iface1.net == iface2.net:
+                    common.append((iface1.net, iface1, iface2))
         return common
 
 
@@ -620,8 +553,8 @@ class CoreNode(CoreNodeBase):
                 self._mounts = []
 
                 # shutdown all interfaces
-                for netif in self.netifs():
-                    netif.shutdown()
+                for iface in self.get_ifaces():
+                    iface.shutdown()
 
                 # kill node process if present
                 try:
@@ -636,7 +569,7 @@ class CoreNode(CoreNodeBase):
                     logging.exception("error removing node directory")
 
                 # clear interface data, close client, and mark self and not up
-                self._netif.clear()
+                self.ifaces.clear()
                 self.client.close()
                 self.up = False
             except OSError:
@@ -704,36 +637,36 @@ class CoreNode(CoreNodeBase):
         self.cmd(f"{MOUNT_BIN} -n --bind {source} {target}")
         self._mounts.append((source, target))
 
-    def newifindex(self) -> int:
+    def next_iface_id(self) -> int:
         """
         Retrieve a new interface index.
 
         :return: new interface index
         """
         with self.lock:
-            return super().newifindex()
+            return super().next_iface_id()
 
-    def newveth(self, ifindex: int = None, ifname: str = None) -> int:
+    def newveth(self, iface_id: int = None, ifname: str = None) -> int:
         """
         Create a new interface.
 
-        :param ifindex: index for the new interface
+        :param iface_id: id for the new interface
         :param ifname: name for the new interface
         :return: nothing
         """
         with self.lock:
-            if ifindex is None:
-                ifindex = self.newifindex()
+            if iface_id is None:
+                iface_id = self.next_iface_id()
 
             if ifname is None:
-                ifname = f"eth{ifindex}"
+                ifname = f"eth{iface_id}"
 
             sessionid = self.session.short_session_id()
 
             try:
-                suffix = f"{self.id:x}.{ifindex}.{sessionid}"
+                suffix = f"{self.id:x}.{iface_id}.{sessionid}"
             except TypeError:
-                suffix = f"{self.id}.{ifindex}.{sessionid}"
+                suffix = f"{self.id}.{iface_id}.{sessionid}"
 
             localname = f"veth{suffix}"
             if len(localname) >= 16:
@@ -758,147 +691,145 @@ class CoreNode(CoreNodeBase):
                 flow_id = self.node_net_client.get_ifindex(veth.name)
                 veth.flow_id = int(flow_id)
                 logging.debug("interface flow index: %s - %s", veth.name, veth.flow_id)
-                hwaddr = self.node_net_client.get_mac(veth.name)
-                logging.debug("interface mac: %s - %s", veth.name, hwaddr)
-                veth.sethwaddr(hwaddr)
+                mac = self.node_net_client.get_mac(veth.name)
+                logging.debug("interface mac: %s - %s", veth.name, mac)
+                veth.set_mac(mac)
 
             try:
                 # add network interface to the node. If unsuccessful, destroy the
                 # network interface and raise exception.
-                self.addnetif(veth, ifindex)
+                self.add_iface(veth, iface_id)
             except ValueError as e:
                 veth.shutdown()
                 del veth
                 raise e
 
-            return ifindex
+            return iface_id
 
-    def newtuntap(self, ifindex: int = None, ifname: str = None) -> int:
+    def newtuntap(self, iface_id: int = None, ifname: str = None) -> int:
         """
         Create a new tunnel tap.
 
-        :param ifindex: interface index
+        :param iface_id: interface id
         :param ifname: interface name
         :return: interface index
         """
         with self.lock:
-            if ifindex is None:
-                ifindex = self.newifindex()
+            if iface_id is None:
+                iface_id = self.next_iface_id()
 
             if ifname is None:
-                ifname = f"eth{ifindex}"
+                ifname = f"eth{iface_id}"
 
             sessionid = self.session.short_session_id()
-            localname = f"tap{self.id}.{ifindex}.{sessionid}"
+            localname = f"tap{self.id}.{iface_id}.{sessionid}"
             name = ifname
             tuntap = TunTap(self.session, self, name, localname, start=self.up)
 
             try:
-                self.addnetif(tuntap, ifindex)
+                self.add_iface(tuntap, iface_id)
             except ValueError as e:
                 tuntap.shutdown()
                 del tuntap
                 raise e
 
-            return ifindex
+            return iface_id
 
-    def sethwaddr(self, ifindex: int, addr: str) -> None:
+    def set_mac(self, iface_id: int, mac: str) -> None:
         """
-        Set hardware addres for an interface.
+        Set hardware address for an interface.
 
-        :param ifindex: index of interface to set hardware address for
-        :param addr: hardware address to set
+        :param iface_id: id of interface to set hardware address for
+        :param mac: mac address to set
         :return: nothing
         :raises CoreCommandError: when a non-zero exit status occurs
         """
-        addr = utils.validate_mac(addr)
-        interface = self._netif[ifindex]
-        interface.sethwaddr(addr)
+        mac = utils.validate_mac(mac)
+        iface = self.get_iface(iface_id)
+        iface.set_mac(mac)
         if self.up:
-            self.node_net_client.device_mac(interface.name, addr)
+            self.node_net_client.device_mac(iface.name, mac)
 
-    def addaddr(self, ifindex: int, addr: str) -> None:
+    def addaddr(self, iface_id: int, addr: str) -> None:
         """
         Add interface address.
 
-        :param ifindex: index of interface to add address to
+        :param iface_id: id of interface to add address to
         :param addr: address to add to interface
         :return: nothing
         """
         addr = utils.validate_ip(addr)
-        interface = self._netif[ifindex]
-        interface.addaddr(addr)
+        iface = self.get_iface(iface_id)
+        iface.addaddr(addr)
         if self.up:
             # ipv4 check
             broadcast = None
             if netaddr.valid_ipv4(addr):
                 broadcast = "+"
-            self.node_net_client.create_address(interface.name, addr, broadcast)
+            self.node_net_client.create_address(iface.name, addr, broadcast)
 
-    def deladdr(self, ifindex: int, addr: str) -> None:
+    def deladdr(self, iface_id: int, addr: str) -> None:
         """
         Delete address from an interface.
 
-        :param ifindex: index of interface to delete address from
+        :param iface_id: id of interface to delete address from
         :param addr: address to delete from interface
         :return: nothing
         :raises CoreCommandError: when a non-zero exit status occurs
         """
-        interface = self._netif[ifindex]
-
+        iface = self.get_iface(iface_id)
         try:
-            interface.deladdr(addr)
+            iface.deladdr(addr)
         except ValueError:
             logging.exception("trying to delete unknown address: %s", addr)
-
         if self.up:
-            self.node_net_client.delete_address(interface.name, addr)
+            self.node_net_client.delete_address(iface.name, addr)
 
-    def ifup(self, ifindex: int) -> None:
+    def ifup(self, iface_id: int) -> None:
         """
         Bring an interface up.
 
-        :param ifindex: index of interface to bring up
+        :param iface_id: index of interface to bring up
         :return: nothing
         """
         if self.up:
-            interface_name = self.ifname(ifindex)
-            self.node_net_client.device_up(interface_name)
+            iface = self.get_iface(iface_id)
+            self.node_net_client.device_up(iface.name)
 
-    def newnetif(
-        self, net: "CoreNetworkBase", interface_data: InterfaceData
+    def new_iface(
+        self, net: "CoreNetworkBase", iface_data: InterfaceData
     ) -> CoreInterface:
         """
         Create a new network interface.
 
         :param net: network to associate with
-        :param interface_data: interface data for new interface
+        :param iface_data: interface data for new interface
         :return: interface index
         """
-        addresses = interface_data.get_addresses()
+        addresses = iface_data.get_addresses()
         with self.lock:
             # TODO: emane specific code
             if net.is_emane is True:
-                ifindex = self.newtuntap(interface_data.id, interface_data.name)
+                iface_id = self.newtuntap(iface_data.id, iface_data.name)
                 # TUN/TAP is not ready for addressing yet; the device may
                 #   take some time to appear, and installing it into a
                 #   namespace after it has been bound removes addressing;
                 #   save addresses with the interface now
-                self.attachnet(ifindex, net)
-                netif = self.netif(ifindex)
-                netif.sethwaddr(interface_data.mac)
+                self.attachnet(iface_id, net)
+                iface = self.get_iface(iface_id)
+                iface.set_mac(iface_data.mac)
                 for address in addresses:
-                    netif.addaddr(address)
+                    iface.addaddr(address)
             else:
-                ifindex = self.newveth(interface_data.id, interface_data.name)
-                self.attachnet(ifindex, net)
-                if interface_data.mac:
-                    self.sethwaddr(ifindex, interface_data.mac)
+                iface_id = self.newveth(iface_data.id, iface_data.name)
+                self.attachnet(iface_id, net)
+                if iface_data.mac:
+                    self.set_mac(iface_id, iface_data.mac)
                 for address in addresses:
-                    self.addaddr(ifindex, address)
-                self.ifup(ifindex)
-                netif = self.netif(ifindex)
-            return netif
+                    self.addaddr(iface_id, address)
+                self.ifup(iface_id)
+                iface = self.get_iface(iface_id)
+            return iface
 
     def addfile(self, srcname: str, filename: str) -> None:
         """
@@ -1041,54 +972,54 @@ class CoreNetworkBase(NodeBase):
 
     @abc.abstractmethod
     def linkconfig(
-        self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
+        self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
     ) -> None:
         """
         Configure link parameters by applying tc queuing disciplines on the interface.
 
-        :param netif: interface one
+        :param iface: interface one
         :param options: options for configuring link
-        :param netif2: interface two
+        :param iface2: interface two
         :return: nothing
         """
         raise NotImplementedError
 
-    def getlinknetif(self, net: "CoreNetworkBase") -> Optional[CoreInterface]:
+    def get_linked_iface(self, net: "CoreNetworkBase") -> Optional[CoreInterface]:
         """
-        Return the interface of that links this net with another net.
+        Return the interface that links this net with another net.
 
         :param net: interface to get link for
         :return: interface the provided network is linked to
         """
-        for netif in self.netifs():
-            if netif.othernet == net:
-                return netif
+        for iface in self.get_ifaces():
+            if iface.othernet == net:
+                return iface
         return None
 
-    def attach(self, netif: CoreInterface) -> None:
+    def attach(self, iface: CoreInterface) -> None:
         """
         Attach network interface.
 
-        :param netif: network interface to attach
+        :param iface: network interface to attach
         :return: nothing
         """
-        i = self.newifindex()
-        self._netif[i] = netif
-        netif.netifi = i
+        i = self.next_iface_id()
+        self.ifaces[i] = iface
+        iface.net_id = i
         with self._linked_lock:
-            self._linked[netif] = {}
+            self._linked[iface] = {}
 
-    def detach(self, netif: CoreInterface) -> None:
+    def detach(self, iface: CoreInterface) -> None:
         """
         Detach network interface.
 
-        :param netif: network interface to detach
+        :param iface: network interface to detach
         :return: nothing
         """
-        del self._netif[netif.netifi]
-        netif.netifi = None
+        del self.ifaces[iface.net_id]
+        iface.net_id = None
         with self._linked_lock:
-            del self._linked[netif]
+            del self._linked[iface]
 
     def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
         """
@@ -1102,84 +1033,63 @@ class CoreNetworkBase(NodeBase):
 
         # build a link message from this network node to each node having a
         # connected interface
-        for netif in self.netifs(sort=True):
-            if not hasattr(netif, "node"):
-                continue
+        for iface in self.get_ifaces():
             uni = False
-            linked_node = netif.node
+            linked_node = iface.node
             if linked_node is None:
                 # two layer-2 switches/hubs linked together via linknet()
-                if not netif.othernet:
+                if not iface.othernet:
                     continue
-                linked_node = netif.othernet
+                linked_node = iface.othernet
                 if linked_node.id == self.id:
                     continue
-                netif.swapparams("_params_up")
-                upstream_params = netif.getparams()
-                netif.swapparams("_params_up")
-                if netif.getparams() != upstream_params:
+                iface.swapparams("_params_up")
+                upstream_params = iface.getparams()
+                iface.swapparams("_params_up")
+                if iface.getparams() != upstream_params:
                     uni = True
 
             unidirectional = 0
             if uni:
                 unidirectional = 1
 
-            interface2_ip4 = None
-            interface2_ip4_mask = None
-            interface2_ip6 = None
-            interface2_ip6_mask = None
-            for address in netif.addrlist:
+            iface2 = InterfaceData(
+                id=linked_node.get_iface_id(iface), name=iface.name, mac=iface.mac
+            )
+            for address in iface.addrlist:
                 ip, _sep, mask = address.partition("/")
                 mask = int(mask)
                 if netaddr.valid_ipv4(ip):
-                    interface2_ip4 = ip
-                    interface2_ip4_mask = mask
+                    iface2.ip4 = ip
+                    iface2.ip4_mask = mask
                 else:
-                    interface2_ip6 = ip
-                    interface2_ip6_mask = mask
+                    iface2.ip6 = ip
+                    iface2.ip6_mask = mask
 
+            options_data = iface.get_link_options(unidirectional)
             link_data = LinkData(
                 message_type=flags,
+                type=self.linktype,
                 node1_id=self.id,
                 node2_id=linked_node.id,
-                link_type=self.linktype,
-                unidirectional=unidirectional,
-                interface2_id=linked_node.getifindex(netif),
-                interface2_name=netif.name,
-                interface2_mac=netif.hwaddr,
-                interface2_ip4=interface2_ip4,
-                interface2_ip4_mask=interface2_ip4_mask,
-                interface2_ip6=interface2_ip6,
-                interface2_ip6_mask=interface2_ip6_mask,
-                delay=netif.getparam("delay"),
-                bandwidth=netif.getparam("bw"),
-                dup=netif.getparam("duplicate"),
-                jitter=netif.getparam("jitter"),
-                loss=netif.getparam("loss"),
+                iface2=iface2,
+                options=options_data,
             )
-
             all_links.append(link_data)
 
             if not uni:
                 continue
-
-            netif.swapparams("_params_up")
+            iface.swapparams("_params_up")
+            options_data = iface.get_link_options(unidirectional)
             link_data = LinkData(
                 message_type=MessageFlags.NONE,
+                type=self.linktype,
                 node1_id=linked_node.id,
                 node2_id=self.id,
-                link_type=self.linktype,
-                unidirectional=1,
-                delay=netif.getparam("delay"),
-                bandwidth=netif.getparam("bw"),
-                dup=netif.getparam("duplicate"),
-                jitter=netif.getparam("jitter"),
-                loss=netif.getparam("loss"),
+                options=options_data,
             )
-            netif.swapparams("_params_up")
-
+            iface.swapparams("_params_up")
             all_links.append(link_data)
-
         return all_links
 
 
diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py
index e911db74..1ef814ee 100644
--- a/daemon/core/nodes/docker.py
+++ b/daemon/core/nodes/docker.py
@@ -141,7 +141,7 @@ class DockerNode(CoreNode):
             return
 
         with self.lock:
-            self._netif.clear()
+            self.ifaces.clear()
             self.client.stop_container()
             self.up = False
 
diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py
index e73e2989..42522362 100644
--- a/daemon/core/nodes/interface.py
+++ b/daemon/core/nodes/interface.py
@@ -7,8 +7,9 @@ import time
 from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple
 
 from core import utils
-from core.emulator.enumerations import MessageFlags, TransportType
-from core.errors import CoreCommandError
+from core.emulator.data import LinkOptions
+from core.emulator.enumerations import TransportType
+from core.errors import CoreCommandError, CoreError
 from core.nodes.netclient import LinuxNetClient, get_net_client
 
 if TYPE_CHECKING:
@@ -52,16 +53,16 @@ class CoreInterface:
         self.othernet: Optional[CoreNetworkBase] = None
         self._params: Dict[str, float] = {}
         self.addrlist: List[str] = []
-        self.hwaddr: Optional[str] = None
+        self.mac: Optional[str] = None
         # placeholder position hook
         self.poshook: Callable[[CoreInterface], None] = lambda x: None
         # used with EMANE
         self.transport_type: Optional[TransportType] = None
-        # node interface index
-        self.netindex: Optional[int] = None
-        # net interface index
-        self.netifi: Optional[int] = None
-        # index used to find flow data
+        # id of interface for node
+        self.node_id: Optional[int] = None
+        # id of interface for network
+        self.net_id: Optional[int] = None
+        # id used to find flow data
         self.flow_id: Optional[int] = None
         self.server: Optional["DistributedServer"] = server
         use_ovs = session.options.get_config("ovs") == "True"
@@ -149,16 +150,16 @@ class CoreInterface:
         """
         self.addrlist.remove(addr)
 
-    def sethwaddr(self, addr: str) -> None:
+    def set_mac(self, mac: str) -> None:
         """
-        Set hardware address.
+        Set mac address.
 
-        :param addr: hardware address to set to.
+        :param mac: mac address to set
         :return: nothing
         """
-        if addr is not None:
-            addr = utils.validate_mac(addr)
-        self.hwaddr = addr
+        if mac is not None:
+            mac = utils.validate_mac(mac)
+        self.mac = mac
 
     def getparam(self, key: str) -> float:
         """
@@ -169,6 +170,34 @@ class CoreInterface:
         """
         return self._params.get(key)
 
+    def get_link_options(self, unidirectional: int) -> LinkOptions:
+        """
+        Get currently set params as link options.
+
+        :param unidirectional: unidirectional setting
+        :return: link options
+        """
+        delay = self.getparam("delay")
+        if delay is not None:
+            delay = int(delay)
+        bandwidth = self.getparam("bw")
+        if bandwidth is not None:
+            bandwidth = int(bandwidth)
+        dup = self.getparam("duplicate")
+        if dup is not None:
+            dup = int(dup)
+        jitter = self.getparam("jitter")
+        if jitter is not None:
+            jitter = int(jitter)
+        return LinkOptions(
+            delay=delay,
+            bandwidth=bandwidth,
+            dup=dup,
+            jitter=jitter,
+            loss=self.getparam("loss"),
+            unidirectional=unidirectional,
+        )
+
     def getparams(self) -> List[Tuple[str, float]]:
         """
         Return (key, value) pairs for parameters.
@@ -284,19 +313,16 @@ class Veth(CoreInterface):
         """
         if not self.up:
             return
-
         if self.node:
             try:
                 self.node.node_net_client.device_flush(self.name)
             except CoreCommandError:
                 logging.exception("error shutting down interface")
-
         if self.localname:
             try:
                 self.net_client.delete_device(self.localname)
             except CoreCommandError:
                 logging.info("link already removed: %s", self.localname)
-
         self.up = False
 
 
@@ -518,7 +544,7 @@ class GreTap(CoreInterface):
         if not start:
             return
         if remoteip is None:
-            raise ValueError("missing remote IP required for GRE TAP device")
+            raise CoreError("missing remote IP required for GRE TAP device")
         self.net_client.create_gretap(self.localname, remoteip, localip, ttl, key)
         self.net_client.device_up(self.localname)
         self.up = True
@@ -535,23 +561,4 @@ class GreTap(CoreInterface):
                 self.net_client.delete_device(self.localname)
             except CoreCommandError:
                 logging.exception("error during shutdown")
-
             self.localname = None
-
-    def data(self, message_type: int) -> None:
-        """
-        Data for a gre tap.
-
-        :param message_type: message type for data
-        :return: None
-        """
-        return None
-
-    def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List:
-        """
-        Retrieve link data.
-
-        :param flags: link flags
-        :return: link data
-        """
-        return []
diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py
index a66791ce..9773cb95 100644
--- a/daemon/core/nodes/lxd.py
+++ b/daemon/core/nodes/lxd.py
@@ -126,7 +126,7 @@ class LxcNode(CoreNode):
             return
 
         with self.lock:
-            self._netif.clear()
+            self.ifaces.clear()
             self.client.stop_container()
             self.up = False
 
@@ -215,7 +215,7 @@ class LxcNode(CoreNode):
         self.client.copy_file(source, filename)
         self.cmd(f"chmod {mode:o} {filename}")
 
-    def addnetif(self, netif: CoreInterface, ifindex: int) -> None:
-        super().addnetif(netif, ifindex)
+    def add_iface(self, iface: CoreInterface, iface_id: int) -> None:
+        super().add_iface(iface, iface_id)
         # adding small delay to allow time for adding addresses to work correctly
         time.sleep(0.5)
diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py
index 25a10b99..b6c164b5 100644
--- a/daemon/core/nodes/netclient.py
+++ b/daemon/core/nodes/netclient.py
@@ -155,14 +155,14 @@ class LinuxNetClient:
         """
         self.run(f"{TC_BIN} qdisc delete dev {device} root")
 
-    def checksums_off(self, interface_name: str) -> None:
+    def checksums_off(self, iface_name: str) -> None:
         """
         Turns interface checksums off.
 
-        :param interface_name: interface to update
+        :param iface_name: interface to update
         :return: nothing
         """
-        self.run(f"{ETHTOOL_BIN} -K {interface_name} rx off tx off")
+        self.run(f"{ETHTOOL_BIN} -K {iface_name} rx off tx off")
 
     def create_address(self, device: str, address: str, broadcast: str = None) -> None:
         """
@@ -250,26 +250,26 @@ class LinuxNetClient:
         self.device_down(name)
         self.run(f"{IP_BIN} link delete {name} type bridge")
 
-    def set_interface_master(self, bridge_name: str, interface_name: str) -> None:
+    def set_iface_master(self, bridge_name: str, iface_name: str) -> None:
         """
         Assign interface master to a Linux bridge.
 
         :param bridge_name: bridge name
-        :param interface_name: interface name
+        :param iface_name: interface name
         :return: nothing
         """
-        self.run(f"{IP_BIN} link set dev {interface_name} master {bridge_name}")
-        self.device_up(interface_name)
+        self.run(f"{IP_BIN} link set dev {iface_name} master {bridge_name}")
+        self.device_up(iface_name)
 
-    def delete_interface(self, bridge_name: str, interface_name: str) -> None:
+    def delete_iface(self, bridge_name: str, iface_name: str) -> None:
         """
         Delete an interface associated with a Linux bridge.
 
         :param bridge_name: bridge name
-        :param interface_name: interface name
+        :param iface_name: interface name
         :return: nothing
         """
-        self.run(f"{IP_BIN} link set dev {interface_name} nomaster")
+        self.run(f"{IP_BIN} link set dev {iface_name} nomaster")
 
     def existing_bridges(self, _id: int) -> bool:
         """
@@ -330,26 +330,26 @@ class OvsNetClient(LinuxNetClient):
         self.device_down(name)
         self.run(f"{OVS_BIN} del-br {name}")
 
-    def set_interface_master(self, bridge_name: str, interface_name: str) -> None:
+    def set_iface_master(self, bridge_name: str, iface_name: str) -> None:
         """
         Create an interface associated with a network bridge.
 
         :param bridge_name: bridge name
-        :param interface_name: interface name
+        :param iface_name: interface name
         :return: nothing
         """
-        self.run(f"{OVS_BIN} add-port {bridge_name} {interface_name}")
-        self.device_up(interface_name)
+        self.run(f"{OVS_BIN} add-port {bridge_name} {iface_name}")
+        self.device_up(iface_name)
 
-    def delete_interface(self, bridge_name: str, interface_name: str) -> None:
+    def delete_iface(self, bridge_name: str, iface_name: str) -> None:
         """
         Delete an interface associated with a OVS bridge.
 
         :param bridge_name: bridge name
-        :param interface_name: interface name
+        :param iface_name: interface name
         :return: nothing
         """
-        self.run(f"{OVS_BIN} del-port {bridge_name} {interface_name}")
+        self.run(f"{OVS_BIN} del-port {bridge_name} {iface_name}")
 
     def existing_bridges(self, _id: int) -> bool:
         """
diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py
index b85c2eee..f20b6dfb 100644
--- a/daemon/core/nodes/network.py
+++ b/daemon/core/nodes/network.py
@@ -11,8 +11,7 @@ import netaddr
 
 from core import utils
 from core.constants import EBTABLES_BIN, TC_BIN
-from core.emulator.data import LinkData, NodeData
-from core.emulator.emudata import InterfaceData, LinkOptions
+from core.emulator.data import InterfaceData, LinkData, LinkOptions
 from core.emulator.enumerations import (
     LinkTypes,
     MessageFlags,
@@ -216,20 +215,20 @@ class EbtablesQueue:
                     ]
                 )
             # rebuild the chain
-            for netif1, v in wlan._linked.items():
-                for netif2, linked in v.items():
+            for iface1, v in wlan._linked.items():
+                for oface2, linked in v.items():
                     if wlan.policy == NetworkPolicy.DROP and linked:
                         self.cmds.extend(
                             [
-                                f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j ACCEPT",
-                                f"-A {wlan.brname} -o {netif1.localname} -i {netif2.localname} -j ACCEPT",
+                                f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j ACCEPT",
+                                f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j ACCEPT",
                             ]
                         )
                     elif wlan.policy == NetworkPolicy.ACCEPT and not linked:
                         self.cmds.extend(
                             [
-                                f"-A {wlan.brname} -i {netif1.localname} -o {netif2.localname} -j DROP",
-                                f"-A {wlan.brname} -o {netif1.localname} -i {netif2.localname} -j DROP",
+                                f"-A {wlan.brname} -i {iface1.localname} -o {oface2.localname} -j DROP",
+                                f"-A {wlan.brname} -o {iface1.localname} -i {oface2.localname} -j DROP",
                             ]
                         )
 
@@ -347,53 +346,53 @@ class CoreNetwork(CoreNetworkBase):
             logging.exception("error during shutdown")
 
         # removes veth pairs used for bridge-to-bridge connections
-        for netif in self.netifs():
-            netif.shutdown()
+        for iface in self.get_ifaces():
+            iface.shutdown()
 
-        self._netif.clear()
+        self.ifaces.clear()
         self._linked.clear()
         del self.session
         self.up = False
 
-    def attach(self, netif: CoreInterface) -> None:
+    def attach(self, iface: CoreInterface) -> None:
         """
         Attach a network interface.
 
-        :param netif: network interface to attach
+        :param iface: network interface to attach
         :return: nothing
         """
         if self.up:
-            netif.net_client.set_interface_master(self.brname, netif.localname)
-        super().attach(netif)
+            iface.net_client.set_iface_master(self.brname, iface.localname)
+        super().attach(iface)
 
-    def detach(self, netif: CoreInterface) -> None:
+    def detach(self, iface: CoreInterface) -> None:
         """
         Detach a network interface.
 
-        :param netif: network interface to detach
+        :param iface: network interface to detach
         :return: nothing
         """
         if self.up:
-            netif.net_client.delete_interface(self.brname, netif.localname)
-        super().detach(netif)
+            iface.net_client.delete_iface(self.brname, iface.localname)
+        super().detach(iface)
 
-    def linked(self, netif1: CoreInterface, netif2: CoreInterface) -> bool:
+    def linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool:
         """
         Determine if the provided network interfaces are linked.
 
-        :param netif1: interface one
-        :param netif2: interface two
+        :param iface1: interface one
+        :param iface2: interface two
         :return: True if interfaces are linked, False otherwise
         """
         # check if the network interfaces are attached to this network
-        if self._netif[netif1.netifi] != netif1:
-            raise ValueError(f"inconsistency for netif {netif1.name}")
+        if self.ifaces[iface1.net_id] != iface1:
+            raise ValueError(f"inconsistency for interface {iface1.name}")
 
-        if self._netif[netif2.netifi] != netif2:
-            raise ValueError(f"inconsistency for netif {netif2.name}")
+        if self.ifaces[iface2.net_id] != iface2:
+            raise ValueError(f"inconsistency for interface {iface2.name}")
 
         try:
-            linked = self._linked[netif1][netif2]
+            linked = self._linked[iface1][iface2]
         except KeyError:
             if self.policy == NetworkPolicy.ACCEPT:
                 linked = True
@@ -401,93 +400,93 @@ class CoreNetwork(CoreNetworkBase):
                 linked = False
             else:
                 raise Exception(f"unknown policy: {self.policy.value}")
-            self._linked[netif1][netif2] = linked
+            self._linked[iface1][iface2] = linked
 
         return linked
 
-    def unlink(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
+    def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
         """
         Unlink two interfaces, resulting in adding or removing ebtables
         filtering rules.
 
-        :param netif1: interface one
-        :param netif2: interface two
+        :param iface1: interface one
+        :param iface2: interface two
         :return: nothing
         """
         with self._linked_lock:
-            if not self.linked(netif1, netif2):
+            if not self.linked(iface1, iface2):
                 return
-            self._linked[netif1][netif2] = False
+            self._linked[iface1][iface2] = False
 
         ebq.ebchange(self)
 
-    def link(self, netif1: CoreInterface, netif2: CoreInterface) -> None:
+    def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
         """
         Link two interfaces together, resulting in adding or removing
         ebtables filtering rules.
 
-        :param netif1: interface one
-        :param netif2: interface two
+        :param iface1: interface one
+        :param iface2: interface two
         :return: nothing
         """
         with self._linked_lock:
-            if self.linked(netif1, netif2):
+            if self.linked(iface1, iface2):
                 return
-            self._linked[netif1][netif2] = True
+            self._linked[iface1][iface2] = True
 
         ebq.ebchange(self)
 
     def linkconfig(
-        self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
+        self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
     ) -> None:
         """
         Configure link parameters by applying tc queuing disciplines on the interface.
 
-        :param netif: interface one
+        :param iface: interface one
         :param options: options for configuring link
-        :param netif2: interface two
+        :param iface2: interface two
         :return: nothing
         """
-        devname = netif.localname
+        devname = iface.localname
         tc = f"{TC_BIN} qdisc replace dev {devname}"
         parent = "root"
         changed = False
         bw = options.bandwidth
-        if netif.setparam("bw", bw):
+        if iface.setparam("bw", bw):
             # from tc-tbf(8): minimum value for burst is rate / kernel_hz
-            burst = max(2 * netif.mtu, int(bw / 1000))
+            burst = max(2 * iface.mtu, int(bw / 1000))
             # max IP payload
             limit = 0xFFFF
             tbf = f"tbf rate {bw} burst {burst} limit {limit}"
             if bw > 0:
                 if self.up:
                     cmd = f"{tc} {parent} handle 1: {tbf}"
-                    netif.host_cmd(cmd)
-                netif.setparam("has_tbf", True)
+                    iface.host_cmd(cmd)
+                iface.setparam("has_tbf", True)
                 changed = True
-            elif netif.getparam("has_tbf") and bw <= 0:
+            elif iface.getparam("has_tbf") and bw <= 0:
                 if self.up:
                     cmd = f"{TC_BIN} qdisc delete dev {devname} {parent}"
-                    netif.host_cmd(cmd)
-                netif.setparam("has_tbf", False)
+                    iface.host_cmd(cmd)
+                iface.setparam("has_tbf", False)
                 # removing the parent removes the child
-                netif.setparam("has_netem", False)
+                iface.setparam("has_netem", False)
                 changed = True
-        if netif.getparam("has_tbf"):
+        if iface.getparam("has_tbf"):
             parent = "parent 1:1"
         netem = "netem"
         delay = options.delay
-        changed = max(changed, netif.setparam("delay", delay))
+        changed = max(changed, iface.setparam("delay", delay))
         loss = options.loss
         if loss is not None:
             loss = float(loss)
-        changed = max(changed, netif.setparam("loss", loss))
+        changed = max(changed, iface.setparam("loss", loss))
         duplicate = options.dup
         if duplicate is not None:
             duplicate = int(duplicate)
-        changed = max(changed, netif.setparam("duplicate", duplicate))
+        changed = max(changed, iface.setparam("duplicate", duplicate))
         jitter = options.jitter
-        changed = max(changed, netif.setparam("jitter", jitter))
+        changed = max(changed, iface.setparam("jitter", jitter))
         if not changed:
             return
         # jitter and delay use the same delay statement
@@ -510,19 +509,19 @@ class CoreNetwork(CoreNetworkBase):
         duplicate_check = duplicate is None or duplicate <= 0
         if all([delay_check, jitter_check, loss_check, duplicate_check]):
             # possibly remove netem if it exists and parent queue wasn't removed
-            if not netif.getparam("has_netem"):
+            if not iface.getparam("has_netem"):
                 return
             if self.up:
                 cmd = f"{TC_BIN} qdisc delete dev {devname} {parent} handle 10:"
-                netif.host_cmd(cmd)
-            netif.setparam("has_netem", False)
+                iface.host_cmd(cmd)
+            iface.setparam("has_netem", False)
         elif len(netem) > 1:
             if self.up:
                 cmd = (
                     f"{TC_BIN} qdisc replace dev {devname} {parent} handle 10: {netem}"
                 )
-                netif.host_cmd(cmd)
-            netif.setparam("has_netem", True)
+                iface.host_cmd(cmd)
+            iface.setparam("has_netem", True)
 
     def linknet(self, net: CoreNetworkBase) -> CoreInterface:
         """
@@ -551,19 +550,19 @@ class CoreNetwork(CoreNetworkBase):
         if len(name) >= 16:
             raise ValueError(f"interface name {name} too long")
 
-        netif = Veth(self.session, None, name, localname, start=self.up)
-        self.attach(netif)
+        iface = Veth(self.session, None, name, localname, start=self.up)
+        self.attach(iface)
         if net.up and net.brname:
-            netif.net_client.set_interface_master(net.brname, netif.name)
-        i = net.newifindex()
-        net._netif[i] = netif
+            iface.net_client.set_iface_master(net.brname, iface.name)
+        i = net.next_iface_id()
+        net.ifaces[i] = iface
         with net._linked_lock:
-            net._linked[netif] = {}
-        netif.net = self
-        netif.othernet = net
-        return netif
+            net._linked[iface] = {}
+        iface.net = self
+        iface.othernet = net
+        return iface
 
-    def getlinknetif(self, net: CoreNetworkBase) -> Optional[CoreInterface]:
+    def get_linked_iface(self, net: CoreNetworkBase) -> Optional[CoreInterface]:
         """
         Return the interface of that links this net with another net
         (that were linked using linknet()).
@@ -571,9 +570,9 @@ class CoreNetwork(CoreNetworkBase):
         :param net: interface to get link for
         :return: interface the provided network is linked to
         """
-        for netif in self.netifs():
-            if netif.othernet == net:
-                return netif
+        for iface in self.get_ifaces():
+            if iface.othernet == net:
+                return iface
         return None
 
     def addrconfig(self, addrlist: List[str]) -> None:
@@ -619,7 +618,6 @@ class GreTapBridge(CoreNetwork):
         :param localip: local address
         :param ttl: ttl value
         :param key: gre tap key
-        :param start: start flag
         :param server: remote server node
             will run on, default is None for localhost
         """
@@ -690,17 +688,17 @@ class GreTapBridge(CoreNetwork):
         )
         self.attach(self.gretap)
 
-    def setkey(self, key: int, interface_data: InterfaceData) -> None:
+    def setkey(self, key: int, iface_data: InterfaceData) -> None:
         """
         Set the GRE key used for the GreTap device. This needs to be set
         prior to instantiating the GreTap device (before addrconfig).
 
         :param key: gre key
-        :param interface_data: interface data for setting up tunnel key
+        :param iface_data: interface data for setting up tunnel key
         :return: nothing
         """
         self.grekey = key
-        addresses = interface_data.get_addresses()
+        addresses = iface_data.get_addresses()
         if addresses:
             self.addrconfig(addresses)
 
@@ -802,7 +800,7 @@ class CtrlNet(CoreNetwork):
             self.host_cmd(f"{self.updown_script} {self.brname} startup")
 
         if self.serverintf:
-            self.net_client.set_interface_master(self.brname, self.serverintf)
+            self.net_client.set_iface_master(self.brname, self.serverintf)
 
     def shutdown(self) -> None:
         """
@@ -812,7 +810,7 @@ class CtrlNet(CoreNetwork):
         """
         if self.serverintf is not None:
             try:
-                self.net_client.delete_interface(self.brname, self.serverintf)
+                self.net_client.delete_iface(self.brname, self.serverintf)
             except CoreCommandError:
                 logging.exception(
                     "error deleting server interface %s from bridge %s",
@@ -850,31 +848,16 @@ class PtpNet(CoreNetwork):
 
     policy: NetworkPolicy = NetworkPolicy.ACCEPT
 
-    def attach(self, netif: CoreInterface) -> None:
+    def attach(self, iface: CoreInterface) -> None:
         """
         Attach a network interface, but limit attachment to two interfaces.
 
-        :param netif: network interface
+        :param iface: network interface
         :return: nothing
         """
-        if len(self._netif) >= 2:
-            raise ValueError(
-                "Point-to-point links support at most 2 network interfaces"
-            )
-        super().attach(netif)
-
-    def data(
-        self, message_type: MessageFlags = MessageFlags.NONE, source: str = None
-    ) -> Optional[NodeData]:
-        """
-        Do not generate a Node Message for point-to-point links. They are
-        built using a link message instead.
-
-        :param message_type: purpose for the data object we are creating
-        :param source: source of node data
-        :return: node data object
-        """
-        return None
+        if len(self.ifaces) >= 2:
+            raise CoreError("ptp links support at most 2 network interfaces")
+        super().attach(iface)
 
     def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
         """
@@ -885,87 +868,68 @@ class PtpNet(CoreNetwork):
         :return: list of link data
         """
         all_links = []
-
-        if len(self._netif) != 2:
+        if len(self.ifaces) != 2:
             return all_links
 
-        interface1, interface2 = self._netif.values()
+        ifaces = self.get_ifaces()
+        iface1 = ifaces[0]
+        iface2 = ifaces[1]
         unidirectional = 0
-        if interface1.getparams() != interface2.getparams():
+        if iface1.getparams() != iface2.getparams():
             unidirectional = 1
 
-        interface1_ip4 = None
-        interface1_ip4_mask = None
-        interface1_ip6 = None
-        interface1_ip6_mask = None
-        for address in interface1.addrlist:
+        iface1_data = InterfaceData(
+            id=iface1.node.get_iface_id(iface1), name=iface1.name, mac=iface1.mac
+        )
+        for address in iface1.addrlist:
             ip, _sep, mask = address.partition("/")
             mask = int(mask)
             if netaddr.valid_ipv4(ip):
-                interface1_ip4 = ip
-                interface1_ip4_mask = mask
+                iface1.ip4 = ip
+                iface1.ip4_mask = mask
             else:
-                interface1_ip6 = ip
-                interface1_ip6_mask = mask
+                iface1.ip6 = ip
+                iface1.ip6_mask = mask
 
-        interface2_ip4 = None
-        interface2_ip4_mask = None
-        interface2_ip6 = None
-        interface2_ip6_mask = None
-        for address in interface2.addrlist:
+        iface2_data = InterfaceData(
+            id=iface2.node.get_iface_id(iface2), name=iface2.name, mac=iface2.mac
+        )
+        for address in iface2.addrlist:
             ip, _sep, mask = address.partition("/")
             mask = int(mask)
             if netaddr.valid_ipv4(ip):
-                interface2_ip4 = ip
-                interface2_ip4_mask = mask
+                iface2.ip4 = ip
+                iface2.ip4_mask = mask
             else:
-                interface2_ip6 = ip
-                interface2_ip6_mask = mask
+                iface2.ip6 = ip
+                iface2.ip6_mask = mask
 
+        options_data = iface1.get_link_options(unidirectional)
         link_data = LinkData(
             message_type=flags,
-            node1_id=interface1.node.id,
-            node2_id=interface2.node.id,
-            link_type=self.linktype,
-            unidirectional=unidirectional,
-            delay=interface1.getparam("delay"),
-            bandwidth=interface1.getparam("bw"),
-            loss=interface1.getparam("loss"),
-            dup=interface1.getparam("duplicate"),
-            jitter=interface1.getparam("jitter"),
-            interface1_id=interface1.node.getifindex(interface1),
-            interface1_name=interface1.name,
-            interface1_mac=interface1.hwaddr,
-            interface1_ip4=interface1_ip4,
-            interface1_ip4_mask=interface1_ip4_mask,
-            interface1_ip6=interface1_ip6,
-            interface1_ip6_mask=interface1_ip6_mask,
-            interface2_id=interface2.node.getifindex(interface2),
-            interface2_name=interface2.name,
-            interface2_mac=interface2.hwaddr,
-            interface2_ip4=interface2_ip4,
-            interface2_ip4_mask=interface2_ip4_mask,
-            interface2_ip6=interface2_ip6,
-            interface2_ip6_mask=interface2_ip6_mask,
+            type=self.linktype,
+            node1_id=iface1.node.id,
+            node2_id=iface2.node.id,
+            iface1=iface1_data,
+            iface2=iface2_data,
+            options=options_data,
         )
         all_links.append(link_data)
 
         # build a 2nd link message for the upstream link parameters
         # (swap if1 and if2)
         if unidirectional:
+            iface1_data = InterfaceData(id=iface2.node.get_iface_id(iface2))
+            iface2_data = InterfaceData(id=iface1.node.get_iface_id(iface1))
+            options_data = iface2.get_link_options(unidirectional)
             link_data = LinkData(
                 message_type=MessageFlags.NONE,
-                link_type=self.linktype,
-                node1_id=interface2.node.id,
-                node2_id=interface1.node.id,
-                delay=interface2.getparam("delay"),
-                bandwidth=interface2.getparam("bw"),
-                loss=interface2.getparam("loss"),
-                dup=interface2.getparam("duplicate"),
-                jitter=interface2.getparam("jitter"),
-                unidirectional=1,
-                interface1_id=interface2.node.getifindex(interface2),
-                interface2_id=interface1.node.getifindex(interface1),
+                type=self.linktype,
+                node1_id=iface2.node.id,
+                node2_id=iface1.node.id,
+                iface1=iface1_data,
+                iface2=iface2_data,
+                options=options_data,
             )
             all_links.append(link_data)
         return all_links
@@ -1025,7 +989,6 @@ class WlanNode(CoreNetwork):
         :param session: core session instance
         :param _id: node id
         :param name: node name
-        :param start: start flag
         :param server: remote server node
             will run on, default is None for localhost
         :param policy: wlan policy
@@ -1045,17 +1008,17 @@ class WlanNode(CoreNetwork):
         self.net_client.disable_mac_learning(self.brname)
         ebq.ebchange(self)
 
-    def attach(self, netif: CoreInterface) -> None:
+    def attach(self, iface: CoreInterface) -> None:
         """
         Attach a network interface.
 
-        :param netif: network interface
+        :param iface: network interface
         :return: nothing
         """
-        super().attach(netif)
+        super().attach(iface)
         if self.model:
-            netif.poshook = self.model.position_callback
-            netif.setposition()
+            iface.poshook = self.model.position_callback
+            iface.setposition()
 
     def setmodel(self, model: "WirelessModelType", config: Dict[str, str]):
         """
@@ -1068,9 +1031,9 @@ class WlanNode(CoreNetwork):
         logging.debug("node(%s) setting model: %s", self.name, model.name)
         if model.config_type == RegisterTlvs.WIRELESS:
             self.model = model(session=self.session, _id=self.id)
-            for netif in self.netifs():
-                netif.poshook = self.model.position_callback
-                netif.setposition()
+            for iface in self.get_ifaces():
+                iface.poshook = self.model.position_callback
+                iface.setposition()
             self.updatemodel(config)
         elif model.config_type == RegisterTlvs.MOBILITY:
             self.mobility = model(session=self.session, _id=self.id)
@@ -1088,8 +1051,8 @@ class WlanNode(CoreNetwork):
             "node(%s) updating model(%s): %s", self.id, self.model.name, config
         )
         self.model.update_config(config)
-        for netif in self.netifs():
-            netif.setposition()
+        for iface in self.get_ifaces():
+            iface.setposition()
 
     def all_link_data(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
         """
diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py
index 741fe7d5..0ce8946a 100644
--- a/daemon/core/nodes/physical.py
+++ b/daemon/core/nodes/physical.py
@@ -9,8 +9,8 @@ from typing import IO, TYPE_CHECKING, List, Optional, Tuple
 
 from core import utils
 from core.constants import MOUNT_BIN, UMOUNT_BIN
+from core.emulator.data import InterfaceData, LinkOptions
 from core.emulator.distributed import DistributedServer
-from core.emulator.emudata import InterfaceData, LinkOptions
 from core.emulator.enumerations import NodeTypes, TransportType
 from core.errors import CoreCommandError, CoreError
 from core.nodes.base import CoreNetworkBase, CoreNodeBase
@@ -51,8 +51,8 @@ class PhysicalNode(CoreNodeBase):
                 _source, target = self._mounts.pop(-1)
                 self.umount(target)
 
-            for netif in self.netifs():
-                netif.shutdown()
+            for iface in self.get_ifaces():
+                iface.shutdown()
 
             self.rmnodedir()
 
@@ -65,117 +65,115 @@ class PhysicalNode(CoreNodeBase):
         """
         return sh
 
-    def sethwaddr(self, ifindex: int, addr: str) -> None:
+    def set_mac(self, iface_id: int, mac: str) -> None:
         """
-        Set hardware address for an interface.
+        Set mac address for an interface.
 
-        :param ifindex: index of interface to set hardware address for
-        :param addr: hardware address to set
+        :param iface_id: index of interface to set hardware address for
+        :param mac: mac address to set
         :return: nothing
         :raises CoreCommandError: when a non-zero exit status occurs
         """
-        addr = utils.validate_mac(addr)
-        interface = self._netif[ifindex]
-        interface.sethwaddr(addr)
+        mac = utils.validate_mac(mac)
+        iface = self.ifaces[iface_id]
+        iface.set_mac(mac)
         if self.up:
-            self.net_client.device_mac(interface.name, addr)
+            self.net_client.device_mac(iface.name, mac)
 
-    def addaddr(self, ifindex: int, addr: str) -> None:
+    def addaddr(self, iface_id: int, addr: str) -> None:
         """
         Add an address to an interface.
 
-        :param ifindex: index of interface to add address to
+        :param iface_id: index of interface to add address to
         :param addr: address to add
         :return: nothing
         """
         addr = utils.validate_ip(addr)
-        interface = self._netif[ifindex]
+        iface = self.get_iface(iface_id)
         if self.up:
-            self.net_client.create_address(interface.name, addr)
-        interface.addaddr(addr)
+            self.net_client.create_address(iface.name, addr)
+        iface.addaddr(addr)
 
-    def deladdr(self, ifindex: int, addr: str) -> None:
+    def deladdr(self, iface_id: int, addr: str) -> None:
         """
         Delete an address from an interface.
 
-        :param ifindex: index of interface to delete
+        :param iface_id: index of interface to delete
         :param addr: address to delete
         :return: nothing
         """
-        interface = self._netif[ifindex]
-
+        iface = self.ifaces[iface_id]
         try:
-            interface.deladdr(addr)
+            iface.deladdr(addr)
         except ValueError:
             logging.exception("trying to delete unknown address: %s", addr)
-
         if self.up:
-            self.net_client.delete_address(interface.name, addr)
+            self.net_client.delete_address(iface.name, addr)
 
-    def adoptnetif(
-        self, netif: CoreInterface, ifindex: int, hwaddr: str, addrlist: List[str]
+    def adopt_iface(
+        self, iface: CoreInterface, iface_id: int, mac: str, addrlist: List[str]
     ) -> None:
         """
         When a link message is received linking this node to another part of
         the emulation, no new interface is created; instead, adopt the
-        GreTap netif as the node interface.
+        GreTap interface as the node interface.
         """
-        netif.name = f"gt{ifindex}"
-        netif.node = self
-        self.addnetif(netif, ifindex)
+        iface.name = f"gt{iface_id}"
+        iface.node = self
+        self.add_iface(iface, iface_id)
         # use a more reasonable name, e.g. "gt0" instead of "gt.56286.150"
         if self.up:
-            self.net_client.device_down(netif.localname)
-            self.net_client.device_name(netif.localname, netif.name)
-        netif.localname = netif.name
-        if hwaddr:
-            self.sethwaddr(ifindex, hwaddr)
+            self.net_client.device_down(iface.localname)
+            self.net_client.device_name(iface.localname, iface.name)
+        iface.localname = iface.name
+        if mac:
+            self.set_mac(iface_id, mac)
         for addr in addrlist:
-            self.addaddr(ifindex, addr)
+            self.addaddr(iface_id, addr)
         if self.up:
-            self.net_client.device_up(netif.localname)
+            self.net_client.device_up(iface.localname)
 
     def linkconfig(
-        self, netif: CoreInterface, options: LinkOptions, netif2: CoreInterface = None
+        self, iface: CoreInterface, options: LinkOptions, iface2: CoreInterface = None
     ) -> None:
         """
         Apply tc queing disciplines using linkconfig.
         """
         linux_bridge = CoreNetwork(self.session)
         linux_bridge.up = True
-        linux_bridge.linkconfig(netif, options, netif2)
+        linux_bridge.linkconfig(iface, options, iface2)
         del linux_bridge
 
-    def newifindex(self) -> int:
+    def next_iface_id(self) -> int:
         with self.lock:
-            while self.ifindex in self._netif:
-                self.ifindex += 1
-            ifindex = self.ifindex
-            self.ifindex += 1
-            return ifindex
+            while self.iface_id in self.ifaces:
+                self.iface_id += 1
+            iface_id = self.iface_id
+            self.iface_id += 1
+            return iface_id
 
-    def newnetif(
-        self, net: CoreNetworkBase, interface_data: InterfaceData
+    def new_iface(
+        self, net: CoreNetworkBase, iface_data: InterfaceData
     ) -> CoreInterface:
         logging.info("creating interface")
-        addresses = interface_data.get_addresses()
-        ifindex = interface_data.id
-        if ifindex is None:
-            ifindex = self.newifindex()
-        name = interface_data.name
+        addresses = iface_data.get_addresses()
+        iface_id = iface_data.id
+        if iface_id is None:
+            iface_id = self.next_iface_id()
+        name = iface_data.name
         if name is None:
-            name = f"gt{ifindex}"
+            name = f"gt{iface_id}"
         if self.up:
             # this is reached when this node is linked to a network node
             # tunnel to net not built yet, so build it now and adopt it
             _, remote_tap = self.session.distributed.create_gre_tunnel(net, self.server)
-            self.adoptnetif(remote_tap, ifindex, interface_data.mac, addresses)
+            self.adopt_iface(remote_tap, iface_id, iface_data.mac, addresses)
             return remote_tap
         else:
             # this is reached when configuring services (self.up=False)
-            netif = GreTap(node=self, name=name, session=self.session, start=False)
-            self.adoptnetif(netif, ifindex, interface_data.mac, addresses)
-            return netif
+            iface = GreTap(node=self, name=name, session=self.session, start=False)
+            self.adopt_iface(iface, iface_id, iface_data.mac, addresses)
+            return iface
 
     def privatedir(self, path: str) -> None:
         if path[0] != "/":
@@ -257,10 +255,10 @@ class Rj45Node(CoreNodeBase):
             will run on, default is None for localhost
         """
         super().__init__(session, _id, name, server)
-        self.interface = CoreInterface(session, self, name, name, mtu, server)
-        self.interface.transport_type = TransportType.RAW
+        self.iface = CoreInterface(session, self, name, name, mtu, server)
+        self.iface.transport_type = TransportType.RAW
         self.lock: threading.RLock = threading.RLock()
-        self.ifindex: Optional[int] = None
+        self.iface_id: Optional[int] = None
         self.old_up: bool = False
         self.old_addrs: List[Tuple[str, Optional[str]]] = []
 
@@ -273,7 +271,7 @@ class Rj45Node(CoreNodeBase):
         """
         # interface will also be marked up during net.attach()
         self.savestate()
-        self.net_client.device_up(self.interface.localname)
+        self.net_client.device_up(self.iface.localname)
         self.up = True
 
     def shutdown(self) -> None:
@@ -285,7 +283,7 @@ class Rj45Node(CoreNodeBase):
         """
         if not self.up:
             return
-        localname = self.interface.localname
+        localname = self.iface.localname
         self.net_client.device_down(localname)
         self.net_client.device_flush(localname)
         try:
@@ -295,8 +293,8 @@ class Rj45Node(CoreNodeBase):
         self.up = False
         self.restorestate()
 
-    def newnetif(
-        self, net: CoreNetworkBase, interface_data: InterfaceData
+    def new_iface(
+        self, net: CoreNetworkBase, iface_data: InterfaceData
     ) -> CoreInterface:
         """
         This is called when linking with another node. Since this node
@@ -304,70 +302,51 @@ class Rj45Node(CoreNodeBase):
         but attach ourselves to the given network.
 
         :param net: new network instance
-        :param interface_data: interface data for new interface
+        :param iface_data: interface data for new interface
         :return: interface index
         :raises ValueError: when an interface has already been created, one max
         """
         with self.lock:
-            ifindex = interface_data.id
-            if ifindex is None:
-                ifindex = 0
-            if self.interface.net is not None:
-                raise ValueError("RJ45 nodes support at most 1 network interface")
-            self._netif[ifindex] = self.interface
-            self.ifindex = ifindex
+            iface_id = iface_data.id
+            if iface_id is None:
+                iface_id = 0
+            if self.iface.net is not None:
+                raise CoreError("RJ45 nodes support at most 1 network interface")
+            self.ifaces[iface_id] = self.iface
+            self.iface_id = iface_id
             if net is not None:
-                self.interface.attachnet(net)
-            for addr in interface_data.get_addresses():
+                self.iface.attachnet(net)
+            for addr in iface_data.get_addresses():
                 self.addaddr(addr)
-            return self.interface
+            return self.iface
 
-    def delnetif(self, ifindex: int) -> None:
+    def delete_iface(self, iface_id: int) -> None:
         """
         Delete a network interface.
 
-        :param ifindex: interface index to delete
+        :param iface_id: interface index to delete
         :return: nothing
         """
-        if ifindex is None:
-            ifindex = 0
-        self._netif.pop(ifindex)
-        if ifindex == self.ifindex:
-            self.shutdown()
-        else:
-            raise ValueError(f"ifindex {ifindex} does not exist")
+        self.get_iface(iface_id)
+        self.ifaces.pop(iface_id)
+        self.shutdown()
 
-    def netif(
-        self, ifindex: int, net: CoreNetworkBase = None
-    ) -> Optional[CoreInterface]:
-        """
-        This object is considered the network interface, so we only
-        return self here. This keeps the RJ45Node compatible with
-        real nodes.
+    def get_iface(self, iface_id: int) -> CoreInterface:
+        if iface_id != self.iface_id or iface_id not in self.ifaces:
+            raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
+        return self.iface
 
-        :param ifindex: interface index to retrieve
-        :param net: network to retrieve
-        :return: a network interface
-        """
-        if net is not None and net == self.interface.net:
-            return self.interface
-        if ifindex is None:
-            ifindex = 0
-        if ifindex == self.ifindex:
-            return self.interface
-        return None
-
-    def getifindex(self, netif: CoreInterface) -> Optional[int]:
+    def get_iface_id(self, iface: CoreInterface) -> Optional[int]:
         """
         Retrieve network interface index.
 
-        :param netif: network interface to retrieve
+        :param iface: network interface to retrieve
             index for
         :return: interface index, None otherwise
         """
-        if netif != self.interface:
-            return None
-        return self.ifindex
+        if iface is not self.iface:
+            raise CoreError(f"node({self.name}) does not have interface({iface.name})")
+        return self.iface_id
 
     def addaddr(self, addr: str) -> None:
         """
@@ -380,7 +359,7 @@ class Rj45Node(CoreNodeBase):
         addr = utils.validate_ip(addr)
         if self.up:
             self.net_client.create_address(self.name, addr)
-        self.interface.addaddr(addr)
+        self.iface.addaddr(addr)
 
     def deladdr(self, addr: str) -> None:
         """
@@ -392,7 +371,7 @@ class Rj45Node(CoreNodeBase):
         """
         if self.up:
             self.net_client.delete_address(self.name, addr)
-        self.interface.deladdr(addr)
+        self.iface.deladdr(addr)
 
     def savestate(self) -> None:
         """
@@ -404,7 +383,7 @@ class Rj45Node(CoreNodeBase):
         """
         self.old_up = False
         self.old_addrs: List[Tuple[str, Optional[str]]] = []
-        localname = self.interface.localname
+        localname = self.iface.localname
         output = self.net_client.address_show(localname)
         for line in output.split("\n"):
             items = line.split()
@@ -429,7 +408,7 @@ class Rj45Node(CoreNodeBase):
         :return: nothing
         :raises CoreCommandError: when there is a command exception
         """
-        localname = self.interface.localname
+        localname = self.iface.localname
         logging.info("restoring rj45 state: %s", localname)
         for addr in self.old_addrs:
             self.net_client.create_address(localname, addr[0], addr[1])
@@ -446,7 +425,7 @@ class Rj45Node(CoreNodeBase):
         :return: True if position changed, False otherwise
         """
         super().setposition(x, y, z)
-        self.interface.setposition()
+        self.iface.setposition()
 
     def termcmdstring(self, sh: str) -> str:
         raise CoreError("rj45 does not support terminal commands")
diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py
index 04fff3e4..84c90730 100644
--- a/daemon/core/plugins/sdt.py
+++ b/daemon/core/plugins/sdt.py
@@ -314,26 +314,22 @@ class Sdt:
         :param node_data: node data being updated
         :return: nothing
         """
-        logging.debug("sdt handle node update: %s - %s", node_data.id, node_data.name)
         if not self.connect():
             return
-
-        # delete node
+        node = node_data.node
+        logging.debug("sdt handle node update: %s - %s", node.id, node.name)
         if node_data.message_type == MessageFlags.DELETE:
-            self.cmd(f"delete node,{node_data.id}")
+            self.cmd(f"delete node,{node.id}")
         else:
-            x = node_data.x_position
-            y = node_data.y_position
-            lat = node_data.latitude
-            lon = node_data.longitude
-            alt = node_data.altitude
+            x, y, _ = node.position.get()
+            lon, lat, alt = node.position.get_geo()
             if all([lat is not None, lon is not None, alt is not None]):
                 pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
-                self.cmd(f"node {node_data.id} {pos}")
+                self.cmd(f"node {node.id} {pos}")
             elif node_data.message_type == 0:
                 lat, lon, alt = self.session.location.getgeo(x, y, 0)
                 pos = f"pos {lon:.6f},{lat:.6f},{alt:.6f}"
-                self.cmd(f"node {node_data.id} {pos}")
+                self.cmd(f"node {node.id} {pos}")
 
     def wireless_net_check(self, node_id: int) -> bool:
         """
diff --git a/daemon/core/services/bird.py b/daemon/core/services/bird.py
index 4901ea56..16f0bb84 100644
--- a/daemon/core/services/bird.py
+++ b/daemon/core/services/bird.py
@@ -35,10 +35,8 @@ class Bird(CoreService):
         """
         Helper to return the first IPv4 address of a node as its router ID.
         """
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            for a in ifc.addrlist:
+        for iface in node.get_ifaces(control=False):
+            for a in iface.addrlist:
                 a = a.split("/")[0]
                 if netaddr.valid_ipv4(a):
                     return a
@@ -84,7 +82,7 @@ protocol device {
         for s in node.services:
             if cls.name not in s.dependencies:
                 continue
-            cfg += s.generatebirdconfig(node)
+            cfg += s.generate_bird_config(node)
 
         return cfg
 
@@ -106,11 +104,11 @@ class BirdService(CoreService):
     meta = "The config file for this service can be found in the bird service."
 
     @classmethod
-    def generatebirdconfig(cls, node):
+    def generate_bird_config(cls, node):
         return ""
 
     @classmethod
-    def generatebirdifcconfig(cls, node):
+    def generate_bird_iface_config(cls, node):
         """
         Use only bare interfaces descriptions in generated protocol
         configurations. This has the slight advantage of being the same
@@ -118,10 +116,8 @@ class BirdService(CoreService):
         """
         cfg = ""
 
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += '        interface "%s";\n' % ifc.name
+        for iface in node.get_ifaces(control=False):
+            cfg += '        interface "%s";\n' % iface.name
 
         return cfg
 
@@ -135,7 +131,7 @@ class BirdBgp(BirdService):
     custom_needed = True
 
     @classmethod
-    def generatebirdconfig(cls, node):
+    def generate_bird_config(cls, node):
         return """
 /* This is a sample config that should be customized with appropriate AS numbers
  * and peers; add one section like this for each neighbor */
@@ -165,7 +161,7 @@ class BirdOspf(BirdService):
     name = "BIRD_OSPFv2"
 
     @classmethod
-    def generatebirdconfig(cls, node):
+    def generate_bird_config(cls, node):
         cfg = "protocol ospf {\n"
         cfg += "    export filter {\n"
         cfg += "        if source = RTS_BGP then {\n"
@@ -175,7 +171,7 @@ class BirdOspf(BirdService):
         cfg += "        accept;\n"
         cfg += "    };\n"
         cfg += "    area 0.0.0.0 {\n"
-        cfg += cls.generatebirdifcconfig(node)
+        cfg += cls.generate_bird_iface_config(node)
         cfg += "    };\n"
         cfg += "}\n\n"
 
@@ -190,12 +186,12 @@ class BirdRadv(BirdService):
     name = "BIRD_RADV"
 
     @classmethod
-    def generatebirdconfig(cls, node):
+    def generate_bird_config(cls, node):
         cfg = "/* This is a sample config that must be customized */\n"
 
         cfg += "protocol radv {\n"
         cfg += "    # auto configuration on all interfaces\n"
-        cfg += cls.generatebirdifcconfig(node)
+        cfg += cls.generate_bird_iface_config(node)
         cfg += "    # Advertise DNS\n"
         cfg += "    rdnss {\n"
         cfg += "#        lifetime mult 10;\n"
@@ -218,11 +214,11 @@ class BirdRip(BirdService):
     name = "BIRD_RIP"
 
     @classmethod
-    def generatebirdconfig(cls, node):
+    def generate_bird_config(cls, node):
         cfg = "protocol rip {\n"
         cfg += "    period 10;\n"
         cfg += "    garbage time 60;\n"
-        cfg += cls.generatebirdifcconfig(node)
+        cfg += cls.generate_bird_iface_config(node)
         cfg += "    honor neighbor;\n"
         cfg += "    authentication none;\n"
         cfg += "    import all;\n"
@@ -241,7 +237,7 @@ class BirdStatic(BirdService):
     custom_needed = True
 
     @classmethod
-    def generatebirdconfig(cls, node):
+    def generate_bird_config(cls, node):
         cfg = "/* This is a sample config that must be customized */\n"
         cfg += "protocol static {\n"
         cfg += "#    route 0.0.0.0/0 via 198.51.100.130; # Default route. Do NOT advertise on BGP !\n"
diff --git a/daemon/core/services/emaneservices.py b/daemon/core/services/emaneservices.py
index 9d09516e..da438bab 100644
--- a/daemon/core/services/emaneservices.py
+++ b/daemon/core/services/emaneservices.py
@@ -20,14 +20,14 @@ class EmaneTransportService(CoreService):
     def generate_config(cls, node, filename):
         if filename == cls.configs[0]:
             transport_commands = []
-            for interface in node.netifs(sort=True):
+            for iface in node.get_ifaces():
                 try:
-                    network_node = node.session.get_node(interface.net.id, EmaneNet)
+                    network_node = node.session.get_node(iface.net.id, EmaneNet)
                     config = node.session.emane.get_configs(
                         network_node.id, network_node.model.name
                     )
                     if config and emanexml.is_external(config):
-                        nem_id = network_node.getnemid(interface)
+                        nem_id = network_node.getnemid(iface)
                         command = (
                             "emanetransportd -r -l 0 -d ../transportdaemon%s.xml"
                             % nem_id
diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py
index 9a344339..97a8b334 100644
--- a/daemon/core/services/frr.py
+++ b/daemon/core/services/frr.py
@@ -59,12 +59,12 @@ class FRRZebra(CoreService):
         """
         # we could verify here that filename == frr.conf
         cfg = ""
-        for ifc in node.netifs():
-            cfg += "interface %s\n" % ifc.name
+        for iface in node.get_ifaces():
+            cfg += "interface %s\n" % iface.name
             # include control interfaces in addressing but not routing daemons
-            if hasattr(ifc, "control") and ifc.control is True:
+            if hasattr(iface, "control") and iface.control is True:
                 cfg += "  "
-                cfg += "\n  ".join(map(cls.addrstr, ifc.addrlist))
+                cfg += "\n  ".join(map(cls.addrstr, iface.addrlist))
                 cfg += "\n"
                 continue
             cfgv4 = ""
@@ -74,18 +74,18 @@ class FRRZebra(CoreService):
             for s in node.services:
                 if cls.name not in s.dependencies:
                     continue
-                ifccfg = s.generatefrrifcconfig(node, ifc)
+                iface_config = s.generate_frr_iface_config(node, iface)
                 if s.ipv4_routing:
                     want_ipv4 = True
                 if s.ipv6_routing:
                     want_ipv6 = True
-                    cfgv6 += ifccfg
+                    cfgv6 += iface_config
                 else:
-                    cfgv4 += ifccfg
+                    cfgv4 += iface_config
 
             if want_ipv4:
                 ipv4list = filter(
-                    lambda x: netaddr.valid_ipv4(x.split("/")[0]), ifc.addrlist
+                    lambda x: netaddr.valid_ipv4(x.split("/")[0]), iface.addrlist
                 )
                 cfg += "  "
                 cfg += "\n  ".join(map(cls.addrstr, ipv4list))
@@ -93,7 +93,7 @@ class FRRZebra(CoreService):
                 cfg += cfgv4
             if want_ipv6:
                 ipv6list = filter(
-                    lambda x: netaddr.valid_ipv6(x.split("/")[0]), ifc.addrlist
+                    lambda x: netaddr.valid_ipv6(x.split("/")[0]), iface.addrlist
                 )
                 cfg += "  "
                 cfg += "\n  ".join(map(cls.addrstr, ipv6list))
@@ -104,7 +104,7 @@ class FRRZebra(CoreService):
         for s in node.services:
             if cls.name not in s.dependencies:
                 continue
-            cfg += s.generatefrrconfig(node)
+            cfg += s.generate_frr_config(node)
         return cfg
 
     @staticmethod
@@ -237,10 +237,10 @@ bootfrr
             frr_bin_search,
             constants.FRR_STATE_DIR,
         )
-        for ifc in node.netifs():
-            cfg += f"ip link set dev {ifc.name} down\n"
+        for iface in node.get_ifaces():
+            cfg += f"ip link set dev {iface.name} down\n"
             cfg += "sleep 1\n"
-            cfg += f"ip link set dev {ifc.name} up\n"
+            cfg += f"ip link set dev {iface.name} up\n"
         return cfg
 
     @classmethod
@@ -334,10 +334,8 @@ class FrrService(CoreService):
         """
         Helper to return the first IPv4 address of a node as its router ID.
         """
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            for a in ifc.addrlist:
+        for iface in node.get_ifaces(control=False):
+            for a in iface.addrlist:
                 a = a.split("/")[0]
                 if netaddr.valid_ipv4(a):
                     return a
@@ -345,16 +343,16 @@ class FrrService(CoreService):
         return "0.0.0.0"
 
     @staticmethod
-    def rj45check(ifc):
+    def rj45check(iface):
         """
         Helper to detect whether interface is connected an external RJ45
         link.
         """
-        if ifc.net:
-            for peerifc in ifc.net.netifs():
-                if peerifc == ifc:
+        if iface.net:
+            for peer_iface in iface.net.get_ifaces():
+                if peer_iface == iface:
                     continue
-                if isinstance(peerifc.node, Rj45Node):
+                if isinstance(peer_iface.node, Rj45Node):
                     return True
         return False
 
@@ -363,11 +361,11 @@ class FrrService(CoreService):
         return ""
 
     @classmethod
-    def generatefrrifcconfig(cls, node, ifc):
+    def generate_frr_iface_config(cls, node, iface):
         return ""
 
     @classmethod
-    def generatefrrconfig(cls, node):
+    def generate_frr_config(cls, node):
         return ""
 
 
@@ -385,43 +383,41 @@ class FRROspfv2(FrrService):
     ipv4_routing = True
 
     @staticmethod
-    def mtucheck(ifc):
+    def mtucheck(iface):
         """
         Helper to detect MTU mismatch and add the appropriate OSPF
         mtu-ignore command. This is needed when e.g. a node is linked via a
         GreTap device.
         """
-        if ifc.mtu != 1500:
+        if iface.mtu != 1500:
             # a workaround for PhysicalNode GreTap, which has no knowledge of
             # the other nodes/nets
             return "  ip ospf mtu-ignore\n"
-        if not ifc.net:
+        if not iface.net:
             return ""
-        for i in ifc.net.netifs():
-            if i.mtu != ifc.mtu:
+        for iface in iface.net.get_ifaces():
+            if iface.mtu != iface.mtu:
                 return "  ip ospf mtu-ignore\n"
         return ""
 
     @staticmethod
-    def ptpcheck(ifc):
+    def ptpcheck(iface):
         """
         Helper to detect whether interface is connected to a notional
         point-to-point link.
         """
-        if isinstance(ifc.net, PtpNet):
+        if isinstance(iface.net, PtpNet):
             return "  ip ospf network point-to-point\n"
         return ""
 
     @classmethod
-    def generatefrrconfig(cls, node):
+    def generate_frr_config(cls, node):
         cfg = "router ospf\n"
         rtrid = cls.routerid(node)
         cfg += "  router-id %s\n" % rtrid
         # network 10.0.0.0/24 area 0
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            for a in ifc.addrlist:
+        for iface in node.get_ifaces(control=False):
+            for a in iface.addrlist:
                 addr = a.split("/")[0]
                 if not netaddr.valid_ipv4(addr):
                     continue
@@ -430,8 +426,8 @@ class FRROspfv2(FrrService):
         return cfg
 
     @classmethod
-    def generatefrrifcconfig(cls, node, ifc):
-        return cls.mtucheck(ifc)
+    def generate_frr_iface_config(cls, node, iface):
+        return cls.mtucheck(iface)
 
 
 class FRROspfv3(FrrService):
@@ -449,57 +445,55 @@ class FRROspfv3(FrrService):
     ipv6_routing = True
 
     @staticmethod
-    def minmtu(ifc):
+    def minmtu(iface):
         """
         Helper to discover the minimum MTU of interfaces linked with the
         given interface.
         """
-        mtu = ifc.mtu
-        if not ifc.net:
+        mtu = iface.mtu
+        if not iface.net:
             return mtu
-        for i in ifc.net.netifs():
-            if i.mtu < mtu:
-                mtu = i.mtu
+        for iface in iface.net.get_ifaces():
+            if iface.mtu < mtu:
+                mtu = iface.mtu
         return mtu
 
     @classmethod
-    def mtucheck(cls, ifc):
+    def mtucheck(cls, iface):
         """
         Helper to detect MTU mismatch and add the appropriate OSPFv3
         ifmtu command. This is needed when e.g. a node is linked via a
         GreTap device.
         """
-        minmtu = cls.minmtu(ifc)
-        if minmtu < ifc.mtu:
+        minmtu = cls.minmtu(iface)
+        if minmtu < iface.mtu:
             return "  ipv6 ospf6 ifmtu %d\n" % minmtu
         else:
             return ""
 
     @staticmethod
-    def ptpcheck(ifc):
+    def ptpcheck(iface):
         """
         Helper to detect whether interface is connected to a notional
         point-to-point link.
         """
-        if isinstance(ifc.net, PtpNet):
+        if isinstance(iface.net, PtpNet):
             return "  ipv6 ospf6 network point-to-point\n"
         return ""
 
     @classmethod
-    def generatefrrconfig(cls, node):
+    def generate_frr_config(cls, node):
         cfg = "router ospf6\n"
         rtrid = cls.routerid(node)
         cfg += "  router-id %s\n" % rtrid
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "  interface %s area 0.0.0.0\n" % ifc.name
+        for iface in node.get_ifaces(control=False):
+            cfg += "  interface %s area 0.0.0.0\n" % iface.name
         cfg += "!\n"
         return cfg
 
     @classmethod
-    def generatefrrifcconfig(cls, node, ifc):
-        return cls.mtucheck(ifc)
+    def generate_frr_iface_config(cls, node, iface):
+        return cls.mtucheck(iface)
         # cfg = cls.mtucheck(ifc)
         # external RJ45 connections will use default OSPF timers
         # if cls.rj45check(ifc):
@@ -531,7 +525,7 @@ class FRRBgp(FrrService):
     ipv6_routing = True
 
     @classmethod
-    def generatefrrconfig(cls, node):
+    def generate_frr_config(cls, node):
         cfg = "!\n! BGP configuration\n!\n"
         cfg += "! You should configure the AS number below,\n"
         cfg += "! along with this router's peers.\n!\n"
@@ -555,7 +549,7 @@ class FRRRip(FrrService):
     ipv4_routing = True
 
     @classmethod
-    def generatefrrconfig(cls, node):
+    def generate_frr_config(cls, node):
         cfg = """\
 router rip
   redistribute static
@@ -579,7 +573,7 @@ class FRRRipng(FrrService):
     ipv6_routing = True
 
     @classmethod
-    def generatefrrconfig(cls, node):
+    def generate_frr_config(cls, node):
         cfg = """\
 router ripng
   redistribute static
@@ -604,18 +598,16 @@ class FRRBabel(FrrService):
     ipv6_routing = True
 
     @classmethod
-    def generatefrrconfig(cls, node):
+    def generate_frr_config(cls, node):
         cfg = "router babel\n"
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "  network %s\n" % ifc.name
+        for iface in node.get_ifaces(control=False):
+            cfg += "  network %s\n" % iface.name
         cfg += "  redistribute static\n  redistribute ipv4 connected\n"
         return cfg
 
     @classmethod
-    def generatefrrifcconfig(cls, node, ifc):
-        if ifc.net and isinstance(ifc.net, (EmaneNet, WlanNode)):
+    def generate_frr_iface_config(cls, node, iface):
+        if iface.net and isinstance(iface.net, (EmaneNet, WlanNode)):
             return "  babel wireless\n  no babel split-horizon\n"
         else:
             return "  babel wired\n  babel split-horizon\n"
@@ -633,11 +625,11 @@ class FRRpimd(FrrService):
     ipv4_routing = True
 
     @classmethod
-    def generatefrrconfig(cls, node):
+    def generate_frr_config(cls, node):
         ifname = "eth0"
-        for ifc in node.netifs():
-            if ifc.name != "lo":
-                ifname = ifc.name
+        for iface in node.get_ifaces():
+            if iface.name != "lo":
+                ifname = iface.name
                 break
         cfg = "router mfea\n!\n"
         cfg += "router igmp\n!\n"
@@ -649,7 +641,7 @@ class FRRpimd(FrrService):
         return cfg
 
     @classmethod
-    def generatefrrifcconfig(cls, node, ifc):
+    def generate_frr_iface_config(cls, node, iface):
         return "  ip mfea\n  ip igmp\n  ip pim\n"
 
 
@@ -668,17 +660,17 @@ class FRRIsis(FrrService):
     ipv6_routing = True
 
     @staticmethod
-    def ptpcheck(ifc):
+    def ptpcheck(iface):
         """
         Helper to detect whether interface is connected to a notional
         point-to-point link.
         """
-        if isinstance(ifc.net, PtpNet):
+        if isinstance(iface.net, PtpNet):
             return "  isis network point-to-point\n"
         return ""
 
     @classmethod
-    def generatefrrconfig(cls, node):
+    def generate_frr_config(cls, node):
         cfg = "router isis DEFAULT\n"
         cfg += "  net 47.0001.0000.1900.%04x.00\n" % node.id
         cfg += "  metric-style wide\n"
@@ -687,9 +679,9 @@ class FRRIsis(FrrService):
         return cfg
 
     @classmethod
-    def generatefrrifcconfig(cls, node, ifc):
+    def generate_frr_iface_config(cls, node, iface):
         cfg = "  ip router isis DEFAULT\n"
         cfg += "  ipv6 router isis DEFAULT\n"
         cfg += "  isis circuit-type level-2-only\n"
-        cfg += cls.ptpcheck(ifc)
+        cfg += cls.ptpcheck(iface)
         return cfg
diff --git a/daemon/core/services/nrl.py b/daemon/core/services/nrl.py
index 3c9f262d..38b90d48 100644
--- a/daemon/core/services/nrl.py
+++ b/daemon/core/services/nrl.py
@@ -32,10 +32,8 @@ class NrlService(CoreService):
         prefix of a node, using the supplied prefix length. This ignores the
         interface's prefix length, so e.g. '/32' can turn into '/24'.
         """
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            for a in ifc.addrlist:
+        for iface in node.get_ifaces(control=False):
+            for a in iface.addrlist:
                 a = a.split("/")[0]
                 if netaddr.valid_ipv4(a):
                     return f"{a}/{prefixlen}"
@@ -54,8 +52,8 @@ class MgenSinkService(NrlService):
     @classmethod
     def generate_config(cls, node, filename):
         cfg = "0.0 LISTEN UDP 5000\n"
-        for ifc in node.netifs():
-            name = utils.sysctl_devname(ifc.name)
+        for iface in node.get_ifaces():
+            name = utils.sysctl_devname(iface.name)
             cfg += "0.0 Join 224.225.1.2 INTERFACE %s\n" % name
         return cfg
 
@@ -91,11 +89,11 @@ class NrlNhdp(NrlService):
             cmd += " -flooding ecds"
             cmd += " -smfClient %s_smf" % node.name
 
-        netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs()))
-        if len(netifs) > 0:
-            interfacenames = map(lambda x: x.name, netifs)
+        ifaces = node.get_ifaces(control=False)
+        if len(ifaces) > 0:
+            iface_names = map(lambda x: x.name, ifaces)
             cmd += " -i "
-            cmd += " -i ".join(interfacenames)
+            cmd += " -i ".join(iface_names)
 
         return (cmd,)
 
@@ -125,16 +123,16 @@ class NrlSmf(NrlService):
         cmd = "nrlsmf instance %s_smf" % node.name
 
         servicenames = map(lambda x: x.name, node.services)
-        netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs()))
-        if len(netifs) == 0:
+        ifaces = node.get_ifaces(control=False)
+        if len(ifaces) == 0:
             return ""
 
         if "arouted" in servicenames:
             comments += "# arouted service is enabled\n"
             cmd += " tap %s_tap" % (node.name,)
             cmd += " unicast %s" % cls.firstipv4prefix(node, 24)
-            cmd += " push lo,%s resequence on" % netifs[0].name
-        if len(netifs) > 0:
+            cmd += " push lo,%s resequence on" % ifaces[0].name
+        if len(ifaces) > 0:
             if "NHDP" in servicenames:
                 comments += "# NHDP service is enabled\n"
                 cmd += " ecds "
@@ -143,8 +141,8 @@ class NrlSmf(NrlService):
                 cmd += " smpr "
             else:
                 cmd += " cf "
-            interfacenames = map(lambda x: x.name, netifs)
-            cmd += ",".join(interfacenames)
+            iface_names = map(lambda x: x.name, ifaces)
+            cmd += ",".join(iface_names)
 
         cmd += " hash MD5"
         cmd += " log /var/log/nrlsmf.log"
@@ -171,10 +169,10 @@ class NrlOlsr(NrlService):
         """
         cmd = cls.startup[0]
         # are multiple interfaces supported? No.
-        netifs = list(node.netifs())
-        if len(netifs) > 0:
-            ifc = netifs[0]
-            cmd += " -i %s" % ifc.name
+        ifaces = node.get_ifaces()
+        if len(ifaces) > 0:
+            iface = ifaces[0]
+            cmd += " -i %s" % iface.name
         cmd += " -l /var/log/nrlolsrd.log"
         cmd += " -rpipe %s_olsr" % node.name
 
@@ -215,11 +213,11 @@ class NrlOlsrv2(NrlService):
 
         cmd += " -p olsr"
 
-        netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs()))
-        if len(netifs) > 0:
-            interfacenames = map(lambda x: x.name, netifs)
+        ifaces = node.get_ifaces(control=False)
+        if len(ifaces) > 0:
+            iface_names = map(lambda x: x.name, ifaces)
             cmd += " -i "
-            cmd += " -i ".join(interfacenames)
+            cmd += " -i ".join(iface_names)
 
         return (cmd,)
 
@@ -243,11 +241,11 @@ class OlsrOrg(NrlService):
         Generate the appropriate command-line based on node interfaces.
         """
         cmd = cls.startup[0]
-        netifs = list(filter(lambda x: not getattr(x, "control", False), node.netifs()))
-        if len(netifs) > 0:
-            interfacenames = map(lambda x: x.name, netifs)
+        ifaces = node.get_ifaces(control=False)
+        if len(ifaces) > 0:
+            iface_names = map(lambda x: x.name, ifaces)
             cmd += " -i "
-            cmd += " -i ".join(interfacenames)
+            cmd += " -i ".join(iface_names)
 
         return (cmd,)
 
@@ -607,8 +605,8 @@ class MgenActor(NrlService):
         comments = ""
         cmd = "mgenBasicActor.py -n %s -a 0.0.0.0" % node.name
 
-        netifs = [x for x in node.netifs() if not getattr(x, "control", False)]
-        if len(netifs) == 0:
+        ifaces = node.get_ifaces(control=False)
+        if len(ifaces) == 0:
             return ""
 
         cfg += comments + cmd + " < /dev/null > /dev/null 2>&1 &\n\n"
diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py
index a62cbc5c..41cfa3d8 100644
--- a/daemon/core/services/quagga.py
+++ b/daemon/core/services/quagga.py
@@ -56,12 +56,12 @@ class Zebra(CoreService):
         """
         # we could verify here that filename == Quagga.conf
         cfg = ""
-        for ifc in node.netifs():
-            cfg += "interface %s\n" % ifc.name
+        for iface in node.get_ifaces():
+            cfg += "interface %s\n" % iface.name
             # include control interfaces in addressing but not routing daemons
-            if hasattr(ifc, "control") and ifc.control is True:
+            if hasattr(iface, "control") and iface.control is True:
                 cfg += "  "
-                cfg += "\n  ".join(map(cls.addrstr, ifc.addrlist))
+                cfg += "\n  ".join(map(cls.addrstr, iface.addrlist))
                 cfg += "\n"
                 continue
             cfgv4 = ""
@@ -71,18 +71,18 @@ class Zebra(CoreService):
             for s in node.services:
                 if cls.name not in s.dependencies:
                     continue
-                ifccfg = s.generatequaggaifcconfig(node, ifc)
+                iface_config = s.generate_quagga_iface_config(node, iface)
                 if s.ipv4_routing:
                     want_ipv4 = True
                 if s.ipv6_routing:
                     want_ipv6 = True
-                    cfgv6 += ifccfg
+                    cfgv6 += iface_config
                 else:
-                    cfgv4 += ifccfg
+                    cfgv4 += iface_config
 
             if want_ipv4:
                 ipv4list = filter(
-                    lambda x: netaddr.valid_ipv4(x.split("/")[0]), ifc.addrlist
+                    lambda x: netaddr.valid_ipv4(x.split("/")[0]), iface.addrlist
                 )
                 cfg += "  "
                 cfg += "\n  ".join(map(cls.addrstr, ipv4list))
@@ -90,7 +90,7 @@ class Zebra(CoreService):
                 cfg += cfgv4
             if want_ipv6:
                 ipv6list = filter(
-                    lambda x: netaddr.valid_ipv6(x.split("/")[0]), ifc.addrlist
+                    lambda x: netaddr.valid_ipv6(x.split("/")[0]), iface.addrlist
                 )
                 cfg += "  "
                 cfg += "\n  ".join(map(cls.addrstr, ipv6list))
@@ -101,7 +101,7 @@ class Zebra(CoreService):
         for s in node.services:
             if cls.name not in s.dependencies:
                 continue
-            cfg += s.generatequaggaconfig(node)
+            cfg += s.generate_quagga_config(node)
         return cfg
 
     @staticmethod
@@ -252,10 +252,8 @@ class QuaggaService(CoreService):
         """
         Helper to return the first IPv4 address of a node as its router ID.
         """
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            for a in ifc.addrlist:
+        for iface in node.get_ifaces(control=False):
+            for a in iface.addrlist:
                 a = a.split("/")[0]
                 if netaddr.valid_ipv4(a):
                     return a
@@ -263,16 +261,16 @@ class QuaggaService(CoreService):
         return "0.0.0.%d" % node.id
 
     @staticmethod
-    def rj45check(ifc):
+    def rj45check(iface):
         """
         Helper to detect whether interface is connected an external RJ45
         link.
         """
-        if ifc.net:
-            for peerifc in ifc.net.netifs():
-                if peerifc == ifc:
+        if iface.net:
+            for peer_iface in iface.net.get_ifaces():
+                if peer_iface == iface:
                     continue
-                if isinstance(peerifc.node, Rj45Node):
+                if isinstance(peer_iface.node, Rj45Node):
                     return True
         return False
 
@@ -281,11 +279,11 @@ class QuaggaService(CoreService):
         return ""
 
     @classmethod
-    def generatequaggaifcconfig(cls, node, ifc):
+    def generate_quagga_iface_config(cls, node, iface):
         return ""
 
     @classmethod
-    def generatequaggaconfig(cls, node):
+    def generate_quagga_config(cls, node):
         return ""
 
 
@@ -303,43 +301,41 @@ class Ospfv2(QuaggaService):
     ipv4_routing = True
 
     @staticmethod
-    def mtucheck(ifc):
+    def mtucheck(iface):
         """
         Helper to detect MTU mismatch and add the appropriate OSPF
         mtu-ignore command. This is needed when e.g. a node is linked via a
         GreTap device.
         """
-        if ifc.mtu != 1500:
+        if iface.mtu != 1500:
             # a workaround for PhysicalNode GreTap, which has no knowledge of
             # the other nodes/nets
             return "  ip ospf mtu-ignore\n"
-        if not ifc.net:
+        if not iface.net:
             return ""
-        for i in ifc.net.netifs():
-            if i.mtu != ifc.mtu:
+        for iface in iface.net.get_ifaces():
+            if iface.mtu != iface.mtu:
                 return "  ip ospf mtu-ignore\n"
         return ""
 
     @staticmethod
-    def ptpcheck(ifc):
+    def ptpcheck(iface):
         """
         Helper to detect whether interface is connected to a notional
         point-to-point link.
         """
-        if isinstance(ifc.net, PtpNet):
+        if isinstance(iface.net, PtpNet):
             return "  ip ospf network point-to-point\n"
         return ""
 
     @classmethod
-    def generatequaggaconfig(cls, node):
+    def generate_quagga_config(cls, node):
         cfg = "router ospf\n"
         rtrid = cls.routerid(node)
         cfg += "  router-id %s\n" % rtrid
         # network 10.0.0.0/24 area 0
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            for a in ifc.addrlist:
+        for iface in node.get_ifaces(control=False):
+            for a in iface.addrlist:
                 addr = a.split("/")[0]
                 if netaddr.valid_ipv4(addr):
                     cfg += "  network %s area 0\n" % a
@@ -347,12 +343,12 @@ class Ospfv2(QuaggaService):
         return cfg
 
     @classmethod
-    def generatequaggaifcconfig(cls, node, ifc):
-        cfg = cls.mtucheck(ifc)
+    def generate_quagga_iface_config(cls, node, iface):
+        cfg = cls.mtucheck(iface)
         # external RJ45 connections will use default OSPF timers
-        if cls.rj45check(ifc):
+        if cls.rj45check(iface):
             return cfg
-        cfg += cls.ptpcheck(ifc)
+        cfg += cls.ptpcheck(iface)
         return (
             cfg
             + """\
@@ -378,58 +374,56 @@ class Ospfv3(QuaggaService):
     ipv6_routing = True
 
     @staticmethod
-    def minmtu(ifc):
+    def minmtu(iface):
         """
         Helper to discover the minimum MTU of interfaces linked with the
         given interface.
         """
-        mtu = ifc.mtu
-        if not ifc.net:
+        mtu = iface.mtu
+        if not iface.net:
             return mtu
-        for i in ifc.net.netifs():
-            if i.mtu < mtu:
-                mtu = i.mtu
+        for iface in iface.net.get_ifaces():
+            if iface.mtu < mtu:
+                mtu = iface.mtu
         return mtu
 
     @classmethod
-    def mtucheck(cls, ifc):
+    def mtucheck(cls, iface):
         """
         Helper to detect MTU mismatch and add the appropriate OSPFv3
         ifmtu command. This is needed when e.g. a node is linked via a
         GreTap device.
         """
-        minmtu = cls.minmtu(ifc)
-        if minmtu < ifc.mtu:
+        minmtu = cls.minmtu(iface)
+        if minmtu < iface.mtu:
             return "  ipv6 ospf6 ifmtu %d\n" % minmtu
         else:
             return ""
 
     @staticmethod
-    def ptpcheck(ifc):
+    def ptpcheck(iface):
         """
         Helper to detect whether interface is connected to a notional
         point-to-point link.
         """
-        if isinstance(ifc.net, PtpNet):
+        if isinstance(iface.net, PtpNet):
             return "  ipv6 ospf6 network point-to-point\n"
         return ""
 
     @classmethod
-    def generatequaggaconfig(cls, node):
+    def generate_quagga_config(cls, node):
         cfg = "router ospf6\n"
         rtrid = cls.routerid(node)
         cfg += "  instance-id 65\n"
         cfg += "  router-id %s\n" % rtrid
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "  interface %s area 0.0.0.0\n" % ifc.name
+        for iface in node.get_ifaces(control=False):
+            cfg += "  interface %s area 0.0.0.0\n" % iface.name
         cfg += "!\n"
         return cfg
 
     @classmethod
-    def generatequaggaifcconfig(cls, node, ifc):
-        return cls.mtucheck(ifc)
+    def generate_quagga_iface_config(cls, node, iface):
+        return cls.mtucheck(iface)
 
 
 class Ospfv3mdr(Ospfv3):
@@ -444,9 +438,9 @@ class Ospfv3mdr(Ospfv3):
     ipv4_routing = True
 
     @classmethod
-    def generatequaggaifcconfig(cls, node, ifc):
-        cfg = cls.mtucheck(ifc)
-        if ifc.net is not None and isinstance(ifc.net, (WlanNode, EmaneNet)):
+    def generate_quagga_iface_config(cls, node, iface):
+        cfg = cls.mtucheck(iface)
+        if iface.net is not None and isinstance(iface.net, (WlanNode, EmaneNet)):
             return (
                 cfg
                 + """\
@@ -479,7 +473,7 @@ class Bgp(QuaggaService):
     ipv6_routing = True
 
     @classmethod
-    def generatequaggaconfig(cls, node):
+    def generate_quagga_config(cls, node):
         cfg = "!\n! BGP configuration\n!\n"
         cfg += "! You should configure the AS number below,\n"
         cfg += "! along with this router's peers.\n!\n"
@@ -503,7 +497,7 @@ class Rip(QuaggaService):
     ipv4_routing = True
 
     @classmethod
-    def generatequaggaconfig(cls, node):
+    def generate_quagga_config(cls, node):
         cfg = """\
 router rip
   redistribute static
@@ -527,7 +521,7 @@ class Ripng(QuaggaService):
     ipv6_routing = True
 
     @classmethod
-    def generatequaggaconfig(cls, node):
+    def generate_quagga_config(cls, node):
         cfg = """\
 router ripng
   redistribute static
@@ -552,18 +546,16 @@ class Babel(QuaggaService):
     ipv6_routing = True
 
     @classmethod
-    def generatequaggaconfig(cls, node):
+    def generate_quagga_config(cls, node):
         cfg = "router babel\n"
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "  network %s\n" % ifc.name
+        for iface in node.get_ifaces(control=False):
+            cfg += "  network %s\n" % iface.name
         cfg += "  redistribute static\n  redistribute connected\n"
         return cfg
 
     @classmethod
-    def generatequaggaifcconfig(cls, node, ifc):
-        if ifc.net and ifc.net.linktype == LinkTypes.WIRELESS:
+    def generate_quagga_iface_config(cls, node, iface):
+        if iface.net and iface.net.linktype == LinkTypes.WIRELESS:
             return "  babel wireless\n  no babel split-horizon\n"
         else:
             return "  babel wired\n  babel split-horizon\n"
@@ -581,11 +573,11 @@ class Xpimd(QuaggaService):
     ipv4_routing = True
 
     @classmethod
-    def generatequaggaconfig(cls, node):
+    def generate_quagga_config(cls, node):
         ifname = "eth0"
-        for ifc in node.netifs():
-            if ifc.name != "lo":
-                ifname = ifc.name
+        for iface in node.get_ifaces():
+            if iface.name != "lo":
+                ifname = iface.name
                 break
         cfg = "router mfea\n!\n"
         cfg += "router igmp\n!\n"
@@ -597,5 +589,5 @@ class Xpimd(QuaggaService):
         return cfg
 
     @classmethod
-    def generatequaggaifcconfig(cls, node, ifc):
+    def generate_quagga_iface_config(cls, node, iface):
         return "  ip mfea\n  ip igmp\n  ip pim\n"
diff --git a/daemon/core/services/sdn.py b/daemon/core/services/sdn.py
index ab46f551..71ab815f 100644
--- a/daemon/core/services/sdn.py
+++ b/daemon/core/services/sdn.py
@@ -49,10 +49,8 @@ class OvsService(SdnService):
 
         cfg += "\n## Now add all our interfaces as ports to the switch\n"
         portnum = 1
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            ifnumstr = re.findall(r"\d+", ifc.name)
+        for iface in node.get_ifaces(control=False):
+            ifnumstr = re.findall(r"\d+", iface.name)
             ifnum = ifnumstr[0]
 
             # create virtual interfaces
@@ -61,18 +59,18 @@ class OvsService(SdnService):
 
             # remove ip address of eths because quagga/zebra will assign same IPs to rtr interfaces
             # or assign them manually to rtr interfaces if zebra is not running
-            for ifcaddr in ifc.addrlist:
-                addr = ifcaddr.split("/")[0]
+            for addr in iface.addrlist:
+                addr = addr.split("/")[0]
                 if netaddr.valid_ipv4(addr):
-                    cfg += "ip addr del %s dev %s\n" % (ifcaddr, ifc.name)
+                    cfg += "ip addr del %s dev %s\n" % (addr, iface.name)
                     if has_zebra == 0:
-                        cfg += "ip addr add %s dev rtr%s\n" % (ifcaddr, ifnum)
+                        cfg += "ip addr add %s dev rtr%s\n" % (addr, ifnum)
                 elif netaddr.valid_ipv6(addr):
-                    cfg += "ip -6 addr del %s dev %s\n" % (ifcaddr, ifc.name)
+                    cfg += "ip -6 addr del %s dev %s\n" % (addr, iface.name)
                     if has_zebra == 0:
-                        cfg += "ip -6 addr add %s dev rtr%s\n" % (ifcaddr, ifnum)
+                        cfg += "ip -6 addr add %s dev rtr%s\n" % (addr, ifnum)
                 else:
-                    raise ValueError("invalid address: %s" % ifcaddr)
+                    raise ValueError("invalid address: %s" % addr)
 
             # add interfaces to bridge
             # Make port numbers explicit so they're easier to follow in reading the script
@@ -102,9 +100,7 @@ class OvsService(SdnService):
         cfg += "## if the above controller will be present then you probably want to delete them\n"
         # Setup default flows
         portnum = 1
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
+        for iface in node.get_ifaces(control=False):
             cfg += "## Take the data from the CORE interface and put it on the veth and vice versa\n"
             cfg += (
                 "ovs-ofctl add-flow ovsbr0 priority=1000,in_port=%d,action=output:%d\n"
diff --git a/daemon/core/services/security.py b/daemon/core/services/security.py
index eb6545b2..91c942f1 100644
--- a/daemon/core/services/security.py
+++ b/daemon/core/services/security.py
@@ -131,18 +131,18 @@ class Nat(CoreService):
     custom_needed = False
 
     @classmethod
-    def generateifcnatrule(cls, ifc, line_prefix=""):
+    def generate_iface_nat_rule(cls, iface, line_prefix=""):
         """
         Generate a NAT line for one interface.
         """
         cfg = line_prefix + "iptables -t nat -A POSTROUTING -o "
-        cfg += ifc.name + " -j MASQUERADE\n"
+        cfg += iface.name + " -j MASQUERADE\n"
 
-        cfg += line_prefix + "iptables -A FORWARD -i " + ifc.name
+        cfg += line_prefix + "iptables -A FORWARD -i " + iface.name
         cfg += " -m state --state RELATED,ESTABLISHED -j ACCEPT\n"
 
         cfg += line_prefix + "iptables -A FORWARD -i "
-        cfg += ifc.name + " -j DROP\n"
+        cfg += iface.name + " -j DROP\n"
         return cfg
 
     @classmethod
@@ -154,14 +154,12 @@ class Nat(CoreService):
         cfg += "# generated by security.py\n"
         cfg += "# NAT out the first interface by default\n"
         have_nat = False
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
+        for iface in node.get_ifaces(control=False):
             if have_nat:
-                cfg += cls.generateifcnatrule(ifc, line_prefix="#")
+                cfg += cls.generate_iface_nat_rule(iface, line_prefix="#")
             else:
                 have_nat = True
-                cfg += "# NAT out the " + ifc.name + " interface\n"
-                cfg += cls.generateifcnatrule(ifc)
+                cfg += "# NAT out the " + iface.name + " interface\n"
+                cfg += cls.generate_iface_nat_rule(iface)
                 cfg += "\n"
         return cfg
diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py
index 8a6e828b..273318e1 100644
--- a/daemon/core/services/utility.py
+++ b/daemon/core/services/utility.py
@@ -55,8 +55,8 @@ class IPForwardService(UtilService):
 """ % {
             "sysctl": constants.SYSCTL_BIN
         }
-        for ifc in node.netifs():
-            name = utils.sysctl_devname(ifc.name)
+        for iface in node.get_ifaces():
+            name = utils.sysctl_devname(iface.name)
             cfg += "%s -w net.ipv4.conf.%s.forwarding=1\n" % (
                 constants.SYSCTL_BIN,
                 name,
@@ -77,10 +77,10 @@ class DefaultRouteService(UtilService):
     @classmethod
     def generate_config(cls, node, filename):
         routes = []
-        netifs = node.netifs(sort=True)
-        if netifs:
-            netif = netifs[0]
-            for x in netif.addrlist:
+        ifaces = node.get_ifaces()
+        if ifaces:
+            iface = ifaces[0]
+            for x in iface.addrlist:
                 net = netaddr.IPNetwork(x).cidr
                 if net.size > 1:
                     router = net[1]
@@ -104,14 +104,12 @@ class DefaultMulticastRouteService(UtilService):
         cfg += "# the first interface is chosen below; please change it "
         cfg += "as needed\n"
 
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
+        for iface in node.get_ifaces(control=False):
             if os.uname()[0] == "Linux":
                 rtcmd = "ip route add 224.0.0.0/4 dev"
             else:
                 raise Exception("unknown platform")
-            cfg += "%s %s\n" % (rtcmd, ifc.name)
+            cfg += "%s %s\n" % (rtcmd, iface.name)
             cfg += "\n"
             break
         return cfg
@@ -129,10 +127,8 @@ class StaticRouteService(UtilService):
         cfg += "# auto-generated by StaticRoute service (utility.py)\n#\n"
         cfg += "# NOTE: this service must be customized to be of any use\n"
         cfg += "#       Below are samples that you can uncomment and edit.\n#\n"
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "\n".join(map(cls.routestr, ifc.addrlist))
+        for iface in node.get_ifaces(control=False):
+            cfg += "\n".join(map(cls.routestr, iface.addrlist))
             cfg += "\n"
         return cfg
 
@@ -259,10 +255,8 @@ max-lease-time 7200;
 
 ddns-update-style none;
 """
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "\n".join(map(cls.subnetentry, ifc.addrlist))
+        for iface in node.get_ifaces(control=False):
+            cfg += "\n".join(map(cls.subnetentry, iface.addrlist))
             cfg += "\n"
         return cfg
 
@@ -320,13 +314,11 @@ class DhcpClientService(UtilService):
         cfg += "side DNS\n# resolution based on the DHCP server response.\n"
         cfg += "#mkdir -p /var/run/resolvconf/interface\n"
 
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "#ln -s /var/run/resolvconf/interface/%s.dhclient" % ifc.name
+        for iface in node.get_ifaces(control=False):
+            cfg += "#ln -s /var/run/resolvconf/interface/%s.dhclient" % iface.name
             cfg += " /var/run/resolvconf/resolv.conf\n"
-            cfg += "/sbin/dhclient -nw -pf /var/run/dhclient-%s.pid" % ifc.name
-            cfg += " -lf /var/run/dhclient-%s.lease %s\n" % (ifc.name, ifc.name)
+            cfg += "/sbin/dhclient -nw -pf /var/run/dhclient-%s.pid" % iface.name
+            cfg += " -lf /var/run/dhclient-%s.lease %s\n" % (iface.name, iface.name)
         return cfg
 
 
@@ -585,10 +577,8 @@ export LANG
 """
             % node.name
         )
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            body += "<li>%s - %s</li>\n" % (ifc.name, ifc.addrlist)
+        for iface in node.get_ifaces(control=False):
+            body += "<li>%s - %s</li>\n" % (iface.name, iface.addrlist)
         return "<html><body>%s</body></html>" % body
 
 
@@ -619,14 +609,14 @@ DUMPOPTS="-s 12288 -C 10 -n"
 if [ "x$1" = "xstart" ]; then
 
 """
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
+        for iface in node.get_ifaces():
+            if hasattr(iface, "control") and iface.control is True:
                 cfg += "# "
             redir = "< /dev/null"
             cfg += "tcpdump ${DUMPOPTS} -w %s.%s.pcap -i %s %s &\n" % (
                 node.name,
-                ifc.name,
-                ifc.name,
+                iface.name,
+                iface.name,
                 redir,
             )
         cfg += """
@@ -654,10 +644,8 @@ class RadvdService(UtilService):
         using the network address of each interface.
         """
         cfg = "# auto-generated by RADVD service (utility.py)\n"
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            prefixes = list(map(cls.subnetentry, ifc.addrlist))
+        for iface in node.get_ifaces(control=False):
+            prefixes = list(map(cls.subnetentry, iface.addrlist))
             if len(prefixes) < 1:
                 continue
             cfg += (
@@ -670,7 +658,7 @@ interface %s
         AdvDefaultPreference low;
         AdvHomeAgentFlag off;
 """
-                % ifc.name
+                % iface.name
             )
             for prefix in prefixes:
                 if prefix == "":
diff --git a/daemon/core/services/xorp.py b/daemon/core/services/xorp.py
index 2312e6d4..776b1d16 100644
--- a/daemon/core/services/xorp.py
+++ b/daemon/core/services/xorp.py
@@ -35,11 +35,11 @@ class XorpRtrmgr(CoreService):
         invoked here. Filename currently ignored.
         """
         cfg = "interfaces {\n"
-        for ifc in node.netifs():
-            cfg += "    interface %s {\n" % ifc.name
-            cfg += "\tvif %s {\n" % ifc.name
-            cfg += "".join(map(cls.addrstr, ifc.addrlist))
-            cfg += cls.lladdrstr(ifc)
+        for iface in node.get_ifaces():
+            cfg += "    interface %s {\n" % iface.name
+            cfg += "\tvif %s {\n" % iface.name
+            cfg += "".join(map(cls.addrstr, iface.addrlist))
+            cfg += cls.lladdrstr(iface)
             cfg += "\t}\n"
             cfg += "    }\n"
         cfg += "}\n\n"
@@ -65,11 +65,11 @@ class XorpRtrmgr(CoreService):
         return cfg
 
     @staticmethod
-    def lladdrstr(ifc):
+    def lladdrstr(iface):
         """
         helper for adding link-local address entries (required by OSPFv3)
         """
-        cfg = "\t    address %s {\n" % ifc.hwaddr.tolinklocal()
+        cfg = "\t    address %s {\n" % netaddr.EUI(iface.mac).eui64()
         cfg += "\t\tprefix-length: 64\n"
         cfg += "\t    }\n"
         return cfg
@@ -104,15 +104,15 @@ class XorpService(CoreService):
         return cfg
 
     @staticmethod
-    def mfea(forwarding, ifcs):
+    def mfea(forwarding, ifaces):
         """
         Helper to add a multicast forwarding engine entry to the config file.
         """
         names = []
-        for ifc in ifcs:
-            if hasattr(ifc, "control") and ifc.control is True:
+        for iface in ifaces:
+            if hasattr(iface, "control") and iface.control is True:
                 continue
-            names.append(ifc.name)
+            names.append(iface.name)
         names.append("register_vif")
 
         cfg = "plumbing {\n"
@@ -148,10 +148,8 @@ class XorpService(CoreService):
         """
         Helper to return the first IPv4 address of a node as its router ID.
         """
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            for a in ifc.addrlist:
+        for iface in node.get_ifaces(control=False):
+            for a in iface.addrlist:
                 a = a.split("/")[0]
                 if netaddr.valid_ipv4(a):
                     return a
@@ -184,12 +182,10 @@ class XorpOspfv2(XorpService):
         cfg += "    ospf4 {\n"
         cfg += "\trouter-id: %s\n" % rtrid
         cfg += "\tarea 0.0.0.0 {\n"
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "\t    interface %s {\n" % ifc.name
-            cfg += "\t\tvif %s {\n" % ifc.name
-            for a in ifc.addrlist:
+        for iface in node.get_ifaces(control=False):
+            cfg += "\t    interface %s {\n" % iface.name
+            cfg += "\t\tvif %s {\n" % iface.name
+            for a in iface.addrlist:
                 addr = a.split("/")[0]
                 if not netaddr.valid_ipv4(addr):
                     continue
@@ -220,11 +216,9 @@ class XorpOspfv3(XorpService):
         cfg += "    ospf6 0 { /* Instance ID 0 */\n"
         cfg += "\trouter-id: %s\n" % rtrid
         cfg += "\tarea 0.0.0.0 {\n"
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "\t    interface %s {\n" % ifc.name
-            cfg += "\t\tvif %s {\n" % ifc.name
+        for iface in node.get_ifaces(control=False):
+            cfg += "\t    interface %s {\n" % iface.name
+            cfg += "\t\tvif %s {\n" % iface.name
             cfg += "\t\t}\n"
             cfg += "\t    }\n"
         cfg += "\t}\n"
@@ -277,12 +271,10 @@ class XorpRip(XorpService):
         cfg += "\nprotocols {\n"
         cfg += "    rip {\n"
         cfg += '\texport: "export-connected"\n'
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "\tinterface %s {\n" % ifc.name
-            cfg += "\t    vif %s {\n" % ifc.name
-            for a in ifc.addrlist:
+        for iface in node.get_ifaces(control=False):
+            cfg += "\tinterface %s {\n" % iface.name
+            cfg += "\t    vif %s {\n" % iface.name
+            for a in iface.addrlist:
                 addr = a.split("/")[0]
                 if not netaddr.valid_ipv4(addr):
                     continue
@@ -310,12 +302,10 @@ class XorpRipng(XorpService):
         cfg += "\nprotocols {\n"
         cfg += "    ripng {\n"
         cfg += '\texport: "export-connected"\n'
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "\tinterface %s {\n" % ifc.name
-            cfg += "\t    vif %s {\n" % ifc.name
-            cfg += "\t\taddress %s {\n" % ifc.hwaddr.tolinklocal()
+        for iface in node.get_ifaces(control=False):
+            cfg += "\tinterface %s {\n" % iface.name
+            cfg += "\t    vif %s {\n" % iface.name
+            cfg += "\t\taddress %s {\n" % netaddr.EUI(iface.mac).eui64()
             cfg += "\t\t    disable: false\n"
             cfg += "\t\t}\n"
             cfg += "\t    }\n"
@@ -334,17 +324,15 @@ class XorpPimSm4(XorpService):
 
     @classmethod
     def generatexorpconfig(cls, node):
-        cfg = cls.mfea("mfea4", node.netifs())
+        cfg = cls.mfea("mfea4", node.get_ifaces())
 
         cfg += "\nprotocols {\n"
         cfg += "    igmp {\n"
         names = []
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            names.append(ifc.name)
-            cfg += "\tinterface %s {\n" % ifc.name
-            cfg += "\t    vif %s {\n" % ifc.name
+        for iface in node.get_ifaces(control=False):
+            names.append(iface.name)
+            cfg += "\tinterface %s {\n" % iface.name
+            cfg += "\t    vif %s {\n" % iface.name
             cfg += "\t\tdisable: false\n"
             cfg += "\t    }\n"
             cfg += "\t}\n"
@@ -394,17 +382,15 @@ class XorpPimSm6(XorpService):
 
     @classmethod
     def generatexorpconfig(cls, node):
-        cfg = cls.mfea("mfea6", node.netifs())
+        cfg = cls.mfea("mfea6", node.get_ifaces())
 
         cfg += "\nprotocols {\n"
         cfg += "    mld {\n"
         names = []
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            names.append(ifc.name)
-            cfg += "\tinterface %s {\n" % ifc.name
-            cfg += "\t    vif %s {\n" % ifc.name
+        for iface in node.get_ifaces(control=False):
+            names.append(iface.name)
+            cfg += "\tinterface %s {\n" % iface.name
+            cfg += "\t    vif %s {\n" % iface.name
             cfg += "\t\tdisable: false\n"
             cfg += "\t    }\n"
             cfg += "\t}\n"
@@ -459,12 +445,10 @@ class XorpOlsr(XorpService):
         cfg += "\nprotocols {\n"
         cfg += "    olsr4 {\n"
         cfg += "\tmain-address: %s\n" % rtrid
-        for ifc in node.netifs():
-            if hasattr(ifc, "control") and ifc.control is True:
-                continue
-            cfg += "\tinterface %s {\n" % ifc.name
-            cfg += "\t    vif %s {\n" % ifc.name
-            for a in ifc.addrlist:
+        for iface in node.get_ifaces(control=False):
+            cfg += "\tinterface %s {\n" % iface.name
+            cfg += "\t    vif %s {\n" % iface.name
+            for a in iface.addrlist:
                 addr = a.split("/")[0]
                 if not netaddr.valid_ipv4(addr):
                     continue
diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py
index 759de680..190cf8f7 100644
--- a/daemon/core/xml/corexml.py
+++ b/daemon/core/xml/corexml.py
@@ -6,8 +6,7 @@ from lxml import etree
 import core.nodes.base
 import core.nodes.physical
 from core.emane.nodes import EmaneNet
-from core.emulator.data import LinkData
-from core.emulator.emudata import InterfaceData, LinkOptions, NodeOptions
+from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
 from core.emulator.enumerations import EventTypes, NodeTypes
 from core.errors import CoreXmlError
 from core.nodes.base import CoreNodeBase, NodeBase
@@ -58,16 +57,16 @@ def add_attribute(element: etree.Element, name: str, value: Any) -> None:
         element.set(name, str(value))
 
 
-def create_interface_data(interface_element: etree.Element) -> InterfaceData:
-    interface_id = int(interface_element.get("id"))
-    name = interface_element.get("name")
-    mac = interface_element.get("mac")
-    ip4 = interface_element.get("ip4")
-    ip4_mask = get_int(interface_element, "ip4_mask")
-    ip6 = interface_element.get("ip6")
-    ip6_mask = get_int(interface_element, "ip6_mask")
+def create_iface_data(iface_element: etree.Element) -> InterfaceData:
+    iface_id = int(iface_element.get("id"))
+    name = iface_element.get("name")
+    mac = iface_element.get("mac")
+    ip4 = iface_element.get("ip4")
+    ip4_mask = get_int(iface_element, "ip4_mask")
+    ip6 = iface_element.get("ip6")
+    ip6_mask = get_int(iface_element, "ip6_mask")
     return InterfaceData(
-        id=interface_id,
+        id=iface_id,
         name=name,
         mac=mac,
         ip4=ip4,
@@ -482,12 +481,10 @@ class CoreXmlWriter:
         # add link data
         for link_data in links:
             # skip basic range links
-            if link_data.interface1_id is None and link_data.interface2_id is None:
+            if link_data.iface1 is None and link_data.iface2 is None:
                 continue
-
             link_element = self.create_link_element(link_data)
             link_elements.append(link_element)
-
         if link_elements.getchildren():
             self.scenario.append(link_elements)
 
@@ -495,37 +492,25 @@ class CoreXmlWriter:
         device = DeviceElement(self.session, node)
         self.devices.append(device.element)
 
-    def create_interface_element(
-        self,
-        element_name: str,
-        node_id: int,
-        interface_id: int,
-        mac: str,
-        ip4: str,
-        ip4_mask: int,
-        ip6: str,
-        ip6_mask: int,
+    def create_iface_element(
+        self, element_name: str, node_id: int, iface_data: InterfaceData
     ) -> etree.Element:
-        interface = etree.Element(element_name)
+        iface_element = etree.Element(element_name)
         node = self.session.get_node(node_id, NodeBase)
-        interface_name = None
         if isinstance(node, CoreNodeBase):
-            node_interface = node.netif(interface_id)
-            interface_name = node_interface.name
-
+            iface = node.get_iface(iface_data.id)
             # check if emane interface
-            if isinstance(node_interface.net, EmaneNet):
-                nem = node_interface.net.getnemid(node_interface)
-                add_attribute(interface, "nem", nem)
-
-        add_attribute(interface, "id", interface_id)
-        add_attribute(interface, "name", interface_name)
-        add_attribute(interface, "mac", mac)
-        add_attribute(interface, "ip4", ip4)
-        add_attribute(interface, "ip4_mask", ip4_mask)
-        add_attribute(interface, "ip6", ip6)
-        add_attribute(interface, "ip6_mask", ip6_mask)
-        return interface
+            if isinstance(iface.net, EmaneNet):
+                nem = iface.net.getnemid(iface)
+                add_attribute(iface_element, "nem", nem)
+        add_attribute(iface_element, "id", iface_data.id)
+        add_attribute(iface_element, "name", iface_data.name)
+        add_attribute(iface_element, "mac", iface_data.mac)
+        add_attribute(iface_element, "ip4", iface_data.ip4)
+        add_attribute(iface_element, "ip4_mask", iface_data.ip4_mask)
+        add_attribute(iface_element, "ip6", iface_data.ip6)
+        add_attribute(iface_element, "ip6_mask", iface_data.ip6_mask)
+        return iface_element
 
     def create_link_element(self, link_data: LinkData) -> etree.Element:
         link_element = etree.Element("link")
@@ -533,32 +518,18 @@ class CoreXmlWriter:
         add_attribute(link_element, "node2", link_data.node2_id)
 
         # check for interface one
-        if link_data.interface1_id is not None:
-            interface1 = self.create_interface_element(
-                "interface1",
-                link_data.node1_id,
-                link_data.interface1_id,
-                link_data.interface1_mac,
-                link_data.interface1_ip4,
-                link_data.interface1_ip4_mask,
-                link_data.interface1_ip6,
-                link_data.interface1_ip6_mask,
+        if link_data.iface1 is not None:
+            iface1 = self.create_iface_element(
+                "interface1", link_data.node1_id, link_data.iface1
             )
-            link_element.append(interface1)
+            link_element.append(iface1)
 
         # check for interface two
-        if link_data.interface2_id is not None:
-            interface2 = self.create_interface_element(
-                "interface2",
-                link_data.node2_id,
-                link_data.interface2_id,
-                link_data.interface2_mac,
-                link_data.interface2_ip4,
-                link_data.interface2_ip4_mask,
-                link_data.interface2_ip6,
-                link_data.interface2_ip6_mask,
+        if link_data.iface2 is not None:
+            iface2 = self.create_iface_element(
+                "interface2", link_data.node2_id, link_data.iface2
             )
-            link_element.append(interface2)
+            link_element.append(iface2)
 
         # check for options, don't write for emane/wlan links
         node1 = self.session.get_node(link_data.node1_id, NodeBase)
@@ -566,23 +537,19 @@ class CoreXmlWriter:
         is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet))
         is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet))
         if not any([is_node1_wireless, is_node2_wireless]):
+            options_data = link_data.options
             options = etree.Element("options")
-            add_attribute(options, "delay", link_data.delay)
-            add_attribute(options, "bandwidth", link_data.bandwidth)
-            add_attribute(options, "loss", link_data.loss)
-            add_attribute(options, "dup", link_data.dup)
-            add_attribute(options, "jitter", link_data.jitter)
-            add_attribute(options, "mer", link_data.mer)
-            add_attribute(options, "burst", link_data.burst)
-            add_attribute(options, "mburst", link_data.mburst)
-            add_attribute(options, "type", link_data.link_type)
-            add_attribute(options, "gui_attributes", link_data.gui_attributes)
-            add_attribute(options, "unidirectional", link_data.unidirectional)
-            add_attribute(options, "emulation_id", link_data.emulation_id)
+            add_attribute(options, "delay", options_data.delay)
+            add_attribute(options, "bandwidth", options_data.bandwidth)
+            add_attribute(options, "loss", options_data.loss)
+            add_attribute(options, "dup", options_data.dup)
+            add_attribute(options, "jitter", options_data.jitter)
+            add_attribute(options, "mer", options_data.mer)
+            add_attribute(options, "burst", options_data.burst)
+            add_attribute(options, "mburst", options_data.mburst)
+            add_attribute(options, "unidirectional", options_data.unidirectional)
             add_attribute(options, "network_id", link_data.network_id)
-            add_attribute(options, "key", link_data.key)
-            add_attribute(options, "opaque", link_data.opaque)
-            add_attribute(options, "session", link_data.session)
+            add_attribute(options, "key", options_data.key)
             if options.items():
                 link_element.append(options)
 
@@ -940,19 +907,19 @@ class CoreXmlReader:
                 node2_id = get_int(link_element, "node_two")
             node_set = frozenset((node1_id, node2_id))
 
-            interface1_element = link_element.find("interface1")
-            if interface1_element is None:
-                interface1_element = link_element.find("interface_one")
-            interface1_data = None
-            if interface1_element is not None:
-                interface1_data = create_interface_data(interface1_element)
+            iface1_element = link_element.find("interface1")
+            if iface1_element is None:
+                iface1_element = link_element.find("interface_one")
+            iface1_data = None
+            if iface1_element is not None:
+                iface1_data = create_iface_data(iface1_element)
 
-            interface2_element = link_element.find("interface2")
-            if interface2_element is None:
-                interface2_element = link_element.find("interface_two")
-            interface2_data = None
-            if interface2_element is not None:
-                interface2_data = create_interface_data(interface2_element)
+            iface2_element = link_element.find("interface2")
+            if iface2_element is None:
+                iface2_element = link_element.find("interface_two")
+            iface2_data = None
+            if iface2_element is not None:
+                iface2_data = create_iface_data(iface2_element)
 
             options_element = link_element.find("options")
             options = LinkOptions()
@@ -969,21 +936,16 @@ class CoreXmlReader:
                 if options.loss is None:
                     options.loss = get_float(options_element, "per")
                 options.unidirectional = get_int(options_element, "unidirectional")
-                options.session = options_element.get("session")
-                options.emulation_id = get_int(options_element, "emulation_id")
-                options.network_id = get_int(options_element, "network_id")
-                options.opaque = options_element.get("opaque")
-                options.gui_attributes = options_element.get("gui_attributes")
 
             if options.unidirectional == 1 and node_set in node_sets:
                 logging.info("updating link node1(%s) node2(%s)", node1_id, node2_id)
                 self.session.update_link(
-                    node1_id, node2_id, interface1_data.id, interface2_data.id, options
+                    node1_id, node2_id, iface1_data.id, iface2_data.id, options
                 )
             else:
                 logging.info("adding link node1(%s) node2(%s)", node1_id, node2_id)
                 self.session.add_link(
-                    node1_id, node2_id, interface1_data, interface2_data, options
+                    node1_id, node2_id, iface1_data, iface2_data, options
                 )
 
             node_sets.add(node_set)
diff --git a/daemon/core/xml/corexmldeployment.py b/daemon/core/xml/corexmldeployment.py
index 04915bf1..7954b71a 100644
--- a/daemon/core/xml/corexmldeployment.py
+++ b/daemon/core/xml/corexmldeployment.py
@@ -24,25 +24,25 @@ def add_address(
     parent_element: etree.Element,
     address_type: str,
     address: str,
-    interface_name: str = None,
+    iface_name: str = None,
 ) -> None:
     address_element = etree.SubElement(parent_element, "address", type=address_type)
     address_element.text = address
-    if interface_name is not None:
-        address_element.set("iface", interface_name)
+    if iface_name is not None:
+        address_element.set("iface", iface_name)
 
 
 def add_mapping(parent_element: etree.Element, maptype: str, mapref: str) -> None:
     etree.SubElement(parent_element, "mapping", type=maptype, ref=mapref)
 
 
-def add_emane_interface(
+def add_emane_iface(
     host_element: etree.Element,
-    netif: CoreInterface,
+    iface: CoreInterface,
     platform_name: str = "p1",
     transport_name: str = "t1",
 ) -> etree.Element:
-    nem_id = netif.net.nemidmap[netif]
+    nem_id = iface.net.nemidmap[iface]
     host_id = host_element.get("id")
 
     # platform data
@@ -89,10 +89,10 @@ def get_ipv4_addresses(hostname: str) -> List[Tuple[str, str]]:
             split = line.split()
             if not split:
                 continue
-            interface_name = split[1]
+            iface_name = split[1]
             address = split[3]
             if not address.startswith("127."):
-                addresses.append((interface_name, address))
+                addresses.append((iface_name, address))
         return addresses
     else:
         # TODO: handle other hosts
@@ -112,11 +112,11 @@ class CoreXmlDeployment:
         device = self.scenario.find(f"devices/device[@name='{name}']")
         return device
 
-    def find_interface(self, device: NodeBase, name: str) -> etree.Element:
-        interface = self.scenario.find(
+    def find_iface(self, device: NodeBase, name: str) -> etree.Element:
+        iface = self.scenario.find(
             f"devices/device[@name='{device.name}']/interfaces/interface[@name='{name}']"
         )
-        return interface
+        return iface
 
     def add_deployment(self) -> None:
         physical_host = self.add_physical_host(socket.gethostname())
@@ -136,8 +136,8 @@ class CoreXmlDeployment:
         add_type(host_element, "physical")
 
         # add ipv4 addresses
-        for interface_name, address in get_ipv4_addresses("localhost"):
-            add_address(host_element, "IPv4", address, interface_name)
+        for iface_name, address in get_ipv4_addresses("localhost"):
+            add_address(host_element, "IPv4", address, iface_name)
 
         return host_element
 
@@ -155,15 +155,15 @@ class CoreXmlDeployment:
         # add host type
         add_type(host_element, "virtual")
 
-        for netif in node.netifs():
+        for iface in node.get_ifaces():
             emane_element = None
-            if isinstance(netif.net, EmaneNet):
-                emane_element = add_emane_interface(host_element, netif)
+            if isinstance(iface.net, EmaneNet):
+                emane_element = add_emane_iface(host_element, iface)
 
             parent_element = host_element
             if emane_element is not None:
                 parent_element = emane_element
 
-            for address in netif.addrlist:
+            for address in iface.addrlist:
                 address_type = get_address_type(address)
-                add_address(parent_element, address_type, address, netif.name)
+                add_address(parent_element, address_type, address, iface.name)
diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py
index 2589edd9..d716777b 100644
--- a/daemon/core/xml/emanexml.py
+++ b/daemon/core/xml/emanexml.py
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
     from core.emane.emanemanager import EmaneManager
     from core.emane.emanemodel import EmaneModel
 
-_hwaddr_prefix = "02:02"
+_MAC_PREFIX = "02:02"
 
 
 def is_external(config: Dict[str, str]) -> bool:
@@ -158,19 +158,19 @@ def build_node_platform_xml(
         logging.warning("warning: EMANE network %s has no associated model", node.name)
         return nem_id
 
-    for netif in node.netifs():
+    for iface in node.get_ifaces():
         logging.debug(
-            "building platform xml for interface(%s) nem_id(%s)", netif.name, nem_id
+            "building platform xml for interface(%s) nem_id(%s)", iface.name, nem_id
         )
         # build nem xml
-        nem_definition = nem_file_name(node.model, netif)
+        nem_definition = nem_file_name(node.model, iface)
         nem_element = etree.Element(
-            "nem", id=str(nem_id), name=netif.localname, definition=nem_definition
+            "nem", id=str(nem_id), name=iface.localname, definition=nem_definition
         )
 
         # check if this is an external transport, get default config if an interface
         # specific one does not exist
-        config = emane_manager.getifcconfig(node.model.id, netif, node.model.name)
+        config = emane_manager.get_iface_config(node.model.id, iface, node.model.name)
 
         if is_external(config):
             nem_element.set("transport", "external")
@@ -180,9 +180,9 @@ def build_node_platform_xml(
             add_param(nem_element, transport_endpoint, config[transport_endpoint])
         else:
             # build transport xml
-            transport_type = netif.transport_type
+            transport_type = iface.transport_type
             if not transport_type:
-                logging.info("warning: %s interface type unsupported!", netif.name)
+                logging.info("warning: %s interface type unsupported!", iface.name)
                 transport_type = TransportType.RAW
             transport_file = transport_file_name(node.id, transport_type)
             transport_element = etree.SubElement(
@@ -190,14 +190,14 @@ def build_node_platform_xml(
             )
 
             # add transport parameter
-            add_param(transport_element, "device", netif.name)
+            add_param(transport_element, "device", iface.name)
 
         # add nem entry
-        nem_entries[netif] = nem_element
+        nem_entries[iface] = nem_element
 
         # merging code
-        key = netif.node.id
-        if netif.transport_type == TransportType.RAW:
+        key = iface.node.id
+        if iface.transport_type == TransportType.RAW:
             key = "host"
             otadev = control_net.brname
             eventdev = control_net.brname
@@ -229,10 +229,10 @@ def build_node_platform_xml(
 
         platform_element.append(nem_element)
 
-        node.setnemid(netif, nem_id)
-        macstr = _hwaddr_prefix + ":00:00:"
+        node.setnemid(iface, nem_id)
+        macstr = _MAC_PREFIX + ":00:00:"
         macstr += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}"
-        netif.sethwaddr(macstr)
+        iface.set_mac(macstr)
 
         # increment nem id
         nem_id += 1
@@ -280,19 +280,19 @@ def build_xml_files(emane_manager: "EmaneManager", node: EmaneNet) -> None:
     vtype = TransportType.VIRTUAL
     rtype = TransportType.RAW
 
-    for netif in node.netifs():
+    for iface in node.get_ifaces():
         # check for interface specific emane configuration and write xml files
-        config = emane_manager.getifcconfig(node.model.id, netif, node.model.name)
+        config = emane_manager.get_iface_config(node.model.id, iface, node.model.name)
         if config:
-            node.model.build_xml_files(config, netif)
+            node.model.build_xml_files(config, iface)
 
         # check transport type needed for interface
-        if netif.transport_type == TransportType.VIRTUAL:
+        if iface.transport_type == TransportType.VIRTUAL:
             need_virtual = True
-            vtype = netif.transport_type
+            vtype = iface.transport_type
         else:
             need_raw = True
-            rtype = netif.transport_type
+            rtype = iface.transport_type
 
     if need_virtual:
         build_transport_xml(emane_manager, node, vtype)
@@ -494,70 +494,70 @@ def transport_file_name(node_id: int, transport_type: TransportType) -> str:
     return f"n{node_id}trans{transport_type.value}.xml"
 
 
-def _basename(emane_model: "EmaneModel", interface: CoreInterface = None) -> str:
+def _basename(emane_model: "EmaneModel", iface: CoreInterface = None) -> str:
     """
     Create name that is leveraged for configuration file creation.
 
     :param emane_model: emane model to create name for
-    :param interface: interface for this model
+    :param iface: interface for this model
     :return: basename used for file creation
     """
     name = f"n{emane_model.id}"
 
-    if interface:
-        node_id = interface.node.id
-        if emane_model.session.emane.getifcconfig(node_id, interface, emane_model.name):
-            name = interface.localname.replace(".", "_")
+    if iface:
+        node_id = iface.node.id
+        if emane_model.session.emane.get_iface_config(node_id, iface, emane_model.name):
+            name = iface.localname.replace(".", "_")
 
     return f"{name}{emane_model.name}"
 
 
-def nem_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str:
+def nem_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str:
     """
     Return the string name for the NEM XML file, e.g. "n3rfpipenem.xml"
 
     :param emane_model: emane model to create file
-    :param interface: interface for this model
+    :param iface: interface for this model
     :return: nem xml filename
     """
-    basename = _basename(emane_model, interface)
+    basename = _basename(emane_model, iface)
     append = ""
-    if interface and interface.transport_type == TransportType.RAW:
+    if iface and iface.transport_type == TransportType.RAW:
         append = "_raw"
     return f"{basename}nem{append}.xml"
 
 
-def shim_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str:
+def shim_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str:
     """
     Return the string name for the SHIM XML file, e.g. "commeffectshim.xml"
 
     :param emane_model: emane model to create file
-    :param interface: interface for this model
+    :param iface: interface for this model
     :return: shim xml filename
     """
-    name = _basename(emane_model, interface)
+    name = _basename(emane_model, iface)
     return f"{name}shim.xml"
 
 
-def mac_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str:
+def mac_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str:
     """
     Return the string name for the MAC XML file, e.g. "n3rfpipemac.xml"
 
     :param emane_model: emane model to create file
-    :param interface: interface for this model
+    :param iface: interface for this model
     :return: mac xml filename
     """
-    name = _basename(emane_model, interface)
+    name = _basename(emane_model, iface)
     return f"{name}mac.xml"
 
 
-def phy_file_name(emane_model: "EmaneModel", interface: CoreInterface = None) -> str:
+def phy_file_name(emane_model: "EmaneModel", iface: CoreInterface = None) -> str:
     """
     Return the string name for the PHY XML file, e.g. "n3rfpipephy.xml"
 
     :param emane_model: emane model to create file
-    :param interface: interface for this model
+    :param iface: interface for this model
     :return: phy xml filename
     """
-    name = _basename(emane_model, interface)
+    name = _basename(emane_model, iface)
     return f"{name}phy.xml"
diff --git a/daemon/examples/configservices/testing.py b/daemon/examples/configservices/testing.py
index 948ec739..9706f2c9 100644
--- a/daemon/examples/configservices/testing.py
+++ b/daemon/examples/configservices/testing.py
@@ -1,7 +1,7 @@
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 from core.nodes.network import SwitchNode
@@ -20,13 +20,13 @@ if __name__ == "__main__":
     # node one
     options.config_services = ["DefaultRoute", "IPForward"]
     node1 = session.add_node(CoreNode, options=options)
-    interface = prefixes.create_interface(node1)
-    session.add_link(node1.id, switch.id, interface1_data=interface)
+    interface = prefixes.create_iface(node1)
+    session.add_link(node1.id, switch.id, iface1_data=interface)
 
     # node two
     node2 = session.add_node(CoreNode, options=options)
-    interface = prefixes.create_interface(node2)
-    session.add_link(node2.id, switch.id, interface1_data=interface)
+    interface = prefixes.create_iface(node2)
+    session.add_link(node2.id, switch.id, iface1_data=interface)
 
     # start session and run services
     session.instantiate()
diff --git a/daemon/examples/docker/docker2core.py b/daemon/examples/docker/docker2core.py
index 8151a590..ae7dae79 100644
--- a/daemon/examples/docker/docker2core.py
+++ b/daemon/examples/docker/docker2core.py
@@ -1,7 +1,7 @@
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 from core.nodes.docker import DockerNode
@@ -18,11 +18,11 @@ if __name__ == "__main__":
 
         # create node one
         node1 = session.add_node(DockerNode, options=options)
-        interface1_data = prefixes.create_interface(node1)
+        interface1_data = prefixes.create_iface(node1)
 
         # create node two
         node2 = session.add_node(CoreNode)
-        interface2_data = prefixes.create_interface(node2)
+        interface2_data = prefixes.create_iface(node2)
 
         # add link
         session.add_link(node1.id, node2.id, interface1_data, interface2_data)
diff --git a/daemon/examples/docker/docker2docker.py b/daemon/examples/docker/docker2docker.py
index a7a70534..308fd00f 100644
--- a/daemon/examples/docker/docker2docker.py
+++ b/daemon/examples/docker/docker2docker.py
@@ -1,7 +1,7 @@
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.docker import DockerNode
 
@@ -19,11 +19,11 @@ if __name__ == "__main__":
 
         # create node one
         node1 = session.add_node(DockerNode, options=options)
-        interface1_data = prefixes.create_interface(node1)
+        interface1_data = prefixes.create_iface(node1)
 
         # create node two
         node2 = session.add_node(DockerNode, options=options)
-        interface2_data = prefixes.create_interface(node2)
+        interface2_data = prefixes.create_iface(node2)
 
         # add link
         session.add_link(node1.id, node2.id, interface1_data, interface2_data)
diff --git a/daemon/examples/docker/switch.py b/daemon/examples/docker/switch.py
index ef057945..fa9e4e40 100644
--- a/daemon/examples/docker/switch.py
+++ b/daemon/examples/docker/switch.py
@@ -1,7 +1,7 @@
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 from core.nodes.docker import DockerNode
@@ -23,15 +23,15 @@ if __name__ == "__main__":
 
         # node one
         node1 = session.add_node(DockerNode, options=options)
-        interface1_data = prefixes.create_interface(node1)
+        interface1_data = prefixes.create_iface(node1)
 
         # node two
         node2 = session.add_node(DockerNode, options=options)
-        interface2_data = prefixes.create_interface(node2)
+        interface2_data = prefixes.create_iface(node2)
 
         # node three
         node_three = session.add_node(CoreNode)
-        interface_three = prefixes.create_interface(node_three)
+        interface_three = prefixes.create_iface(node_three)
 
         # add links
         session.add_link(node1.id, switch.id, interface1_data)
diff --git a/daemon/examples/grpc/distributed_switch.py b/daemon/examples/grpc/distributed_switch.py
index e847016f..0d781c19 100644
--- a/daemon/examples/grpc/distributed_switch.py
+++ b/daemon/examples/grpc/distributed_switch.py
@@ -47,7 +47,7 @@ def main(args):
         node1_id = response.node_id
 
         # create link
-        interface1 = interface_helper.create_interface(node1_id, 0)
+        interface1 = interface_helper.create_iface(node1_id, 0)
         response = core.add_link(session_id, node1_id, switch_id, interface1)
         logging.info("created link from node one to switch: %s", response)
 
@@ -59,7 +59,7 @@ def main(args):
         node2_id = response.node_id
 
         # create link
-        interface1 = interface_helper.create_interface(node2_id, 0)
+        interface1 = interface_helper.create_iface(node2_id, 0)
         response = core.add_link(session_id, node2_id, switch_id, interface1)
         logging.info("created link from node two to switch: %s", response)
 
diff --git a/daemon/examples/grpc/emane80211.py b/daemon/examples/grpc/emane80211.py
index 24532266..b8036db0 100644
--- a/daemon/examples/grpc/emane80211.py
+++ b/daemon/examples/grpc/emane80211.py
@@ -57,10 +57,10 @@ def main():
         node2_id = response.node_id
 
         # links nodes to switch
-        interface1 = interface_helper.create_interface(node1_id, 0)
+        interface1 = interface_helper.create_iface(node1_id, 0)
         response = core.add_link(session_id, node1_id, emane_id, interface1)
         logging.info("created link: %s", response)
-        interface1 = interface_helper.create_interface(node2_id, 0)
+        interface1 = interface_helper.create_iface(node2_id, 0)
         response = core.add_link(session_id, node2_id, emane_id, interface1)
         logging.info("created link: %s", response)
 
diff --git a/daemon/examples/grpc/switch.py b/daemon/examples/grpc/switch.py
index 74e315c6..1ed7c684 100644
--- a/daemon/examples/grpc/switch.py
+++ b/daemon/examples/grpc/switch.py
@@ -53,10 +53,10 @@ def main():
         node2_id = response.node_id
 
         # links nodes to switch
-        interface1 = interface_helper.create_interface(node1_id, 0)
+        interface1 = interface_helper.create_iface(node1_id, 0)
         response = core.add_link(session_id, node1_id, switch_id, interface1)
         logging.info("created link: %s", response)
-        interface1 = interface_helper.create_interface(node2_id, 0)
+        interface1 = interface_helper.create_iface(node2_id, 0)
         response = core.add_link(session_id, node2_id, switch_id, interface1)
         logging.info("created link: %s", response)
 
diff --git a/daemon/examples/grpc/wlan.py b/daemon/examples/grpc/wlan.py
index d60ca1be..715d4706 100644
--- a/daemon/examples/grpc/wlan.py
+++ b/daemon/examples/grpc/wlan.py
@@ -65,10 +65,10 @@ def main():
         node2_id = response.node_id
 
         # links nodes to switch
-        interface1 = interface_helper.create_interface(node1_id, 0)
+        interface1 = interface_helper.create_iface(node1_id, 0)
         response = core.add_link(session_id, node1_id, wlan_id, interface1)
         logging.info("created link: %s", response)
-        interface1 = interface_helper.create_interface(node2_id, 0)
+        interface1 = interface_helper.create_iface(node2_id, 0)
         response = core.add_link(session_id, node2_id, wlan_id, interface1)
         logging.info("created link: %s", response)
 
diff --git a/daemon/examples/lxd/lxd2core.py b/daemon/examples/lxd/lxd2core.py
index 49b68943..b41520d8 100644
--- a/daemon/examples/lxd/lxd2core.py
+++ b/daemon/examples/lxd/lxd2core.py
@@ -1,7 +1,7 @@
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 from core.nodes.lxd import LxcNode
@@ -18,11 +18,11 @@ if __name__ == "__main__":
 
         # create node one
         node1 = session.add_node(LxcNode, options=options)
-        interface1_data = prefixes.create_interface(node1)
+        interface1_data = prefixes.create_iface(node1)
 
         # create node two
         node2 = session.add_node(CoreNode)
-        interface2_data = prefixes.create_interface(node2)
+        interface2_data = prefixes.create_iface(node2)
 
         # add link
         session.add_link(node1.id, node2.id, interface1_data, interface2_data)
diff --git a/daemon/examples/lxd/lxd2lxd.py b/daemon/examples/lxd/lxd2lxd.py
index 18af8037..3a55e2e1 100644
--- a/daemon/examples/lxd/lxd2lxd.py
+++ b/daemon/examples/lxd/lxd2lxd.py
@@ -1,7 +1,7 @@
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.lxd import LxcNode
 
@@ -19,11 +19,11 @@ if __name__ == "__main__":
 
         # create node one
         node1 = session.add_node(LxcNode, options=options)
-        interface1_data = prefixes.create_interface(node1)
+        interface1_data = prefixes.create_iface(node1)
 
         # create node two
         node2 = session.add_node(LxcNode, options=options)
-        interface2_data = prefixes.create_interface(node2)
+        interface2_data = prefixes.create_iface(node2)
 
         # add link
         session.add_link(node1.id, node2.id, interface1_data, interface2_data)
diff --git a/daemon/examples/lxd/switch.py b/daemon/examples/lxd/switch.py
index 31a79887..12767e71 100644
--- a/daemon/examples/lxd/switch.py
+++ b/daemon/examples/lxd/switch.py
@@ -1,7 +1,7 @@
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 from core.nodes.lxd import LxcNode
@@ -23,15 +23,15 @@ if __name__ == "__main__":
 
         # node one
         node1 = session.add_node(LxcNode, options=options)
-        interface1_data = prefixes.create_interface(node1)
+        interface1_data = prefixes.create_iface(node1)
 
         # node two
         node2 = session.add_node(LxcNode, options=options)
-        interface2_data = prefixes.create_interface(node2)
+        interface2_data = prefixes.create_iface(node2)
 
         # node three
         node3 = session.add_node(CoreNode)
-        interface3_data = prefixes.create_interface(node3)
+        interface3_data = prefixes.create_iface(node3)
 
         # add links
         session.add_link(node1.id, switch.id, interface1_data)
diff --git a/daemon/examples/myservices/sample.py b/daemon/examples/myservices/sample.py
index 8c6dbe06..e0c9a232 100644
--- a/daemon/examples/myservices/sample.py
+++ b/daemon/examples/myservices/sample.py
@@ -80,8 +80,8 @@ class MyService(CoreService):
 
         if filename == cls.configs[0]:
             cfg += "# auto-generated by MyService (sample.py)\n"
-            for ifc in node.netifs():
-                cfg += f'echo "Node {node.name} has interface {ifc.name}"\n'
+            for iface in node.get_ifaces():
+                cfg += f'echo "Node {node.name} has interface {iface.name}"\n'
         elif filename == cls.configs[1]:
             cfg += "echo hello"
 
diff --git a/daemon/examples/python/distributed_emane.py b/daemon/examples/python/distributed_emane.py
index d9b41ea4..4421283f 100644
--- a/daemon/examples/python/distributed_emane.py
+++ b/daemon/examples/python/distributed_emane.py
@@ -9,7 +9,7 @@ import logging
 from core.emane.ieee80211abg import EmaneIeee80211abgModel
 from core.emane.nodes import EmaneNet
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 
@@ -59,10 +59,10 @@ def main(args):
     node2 = session.add_node(CoreNode, options=options)
 
     # create node interfaces and link
-    interface1_data = prefixes.create_interface(node1)
-    interface2_data = prefixes.create_interface(node2)
-    session.add_link(node1.id, emane_net.id, interface1_data=interface1_data)
-    session.add_link(node2.id, emane_net.id, interface1_data=interface2_data)
+    interface1_data = prefixes.create_iface(node1)
+    interface2_data = prefixes.create_iface(node2)
+    session.add_link(node1.id, emane_net.id, iface1_data=interface1_data)
+    session.add_link(node2.id, emane_net.id, iface1_data=interface2_data)
 
     # instantiate session
     session.instantiate()
diff --git a/daemon/examples/python/distributed_lxd.py b/daemon/examples/python/distributed_lxd.py
index affb16a8..26f7caa6 100644
--- a/daemon/examples/python/distributed_lxd.py
+++ b/daemon/examples/python/distributed_lxd.py
@@ -7,7 +7,7 @@ import argparse
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.lxd import LxcNode
 
@@ -48,8 +48,8 @@ def main(args):
     node2 = session.add_node(LxcNode, options=options)
 
     # create node interfaces and link
-    interface1_data = prefixes.create_interface(node1)
-    interface2_data = prefixes.create_interface(node2)
+    interface1_data = prefixes.create_iface(node1)
+    interface2_data = prefixes.create_iface(node2)
     session.add_link(node1.id, node2.id, interface1_data, interface2_data)
 
     # instantiate session
diff --git a/daemon/examples/python/distributed_ptp.py b/daemon/examples/python/distributed_ptp.py
index 6bf33474..fe714e1d 100644
--- a/daemon/examples/python/distributed_ptp.py
+++ b/daemon/examples/python/distributed_ptp.py
@@ -7,7 +7,7 @@ import argparse
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 
@@ -48,8 +48,8 @@ def main(args):
     node2 = session.add_node(CoreNode, options=options)
 
     # create node interfaces and link
-    interface1_data = prefixes.create_interface(node1)
-    interface2_data = prefixes.create_interface(node2)
+    interface1_data = prefixes.create_iface(node1)
+    interface2_data = prefixes.create_iface(node2)
     session.add_link(node1.id, node2.id, interface1_data, interface2_data)
 
     # instantiate session
diff --git a/daemon/examples/python/distributed_switch.py b/daemon/examples/python/distributed_switch.py
index 8991161e..35de1cad 100644
--- a/daemon/examples/python/distributed_switch.py
+++ b/daemon/examples/python/distributed_switch.py
@@ -7,7 +7,7 @@ import argparse
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 from core.nodes.network import SwitchNode
@@ -52,10 +52,10 @@ def main(args):
     node2 = session.add_node(CoreNode, options=options)
 
     # create node interfaces and link
-    interface1_data = prefixes.create_interface(node1)
-    interface2_data = prefixes.create_interface(node2)
-    session.add_link(node1.id, switch.id, interface1_data=interface1_data)
-    session.add_link(node2.id, switch.id, interface1_data=interface2_data)
+    interface1_data = prefixes.create_iface(node1)
+    interface2_data = prefixes.create_iface(node2)
+    session.add_link(node1.id, switch.id, iface1_data=interface1_data)
+    session.add_link(node2.id, switch.id, iface1_data=interface2_data)
 
     # instantiate session
     session.instantiate()
diff --git a/daemon/examples/python/emane80211.py b/daemon/examples/python/emane80211.py
index d3f6652a..9d6def4a 100644
--- a/daemon/examples/python/emane80211.py
+++ b/daemon/examples/python/emane80211.py
@@ -10,7 +10,7 @@ import time
 from core.emane.ieee80211abg import EmaneIeee80211abgModel
 from core.emane.nodes import EmaneNet
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 
@@ -42,8 +42,8 @@ def main():
     for i in range(NODES):
         node = session.add_node(CoreNode, options=options)
         node.setposition(x=150 * (i + 1), y=150)
-        interface = prefixes.create_interface(node)
-        session.add_link(node.id, emane_network.id, interface1_data=interface)
+        interface = prefixes.create_iface(node)
+        session.add_link(node.id, emane_network.id, iface1_data=interface)
 
     # instantiate session
     session.instantiate()
diff --git a/daemon/examples/python/switch.py b/daemon/examples/python/switch.py
index 1b939cd7..f05176a3 100644
--- a/daemon/examples/python/switch.py
+++ b/daemon/examples/python/switch.py
@@ -6,7 +6,7 @@ interact with the GUI.
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes
+from core.emulator.data import IpPrefixes
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 from core.nodes.network import SwitchNode
@@ -31,8 +31,8 @@ def main():
     # create nodes
     for _ in range(NODES):
         node = session.add_node(CoreNode)
-        interface = prefixes.create_interface(node)
-        session.add_link(node.id, switch.id, interface1_data=interface)
+        interface = prefixes.create_iface(node)
+        session.add_link(node.id, switch.id, iface1_data=interface)
 
     # instantiate session
     session.instantiate()
diff --git a/daemon/examples/python/switch_inject.py b/daemon/examples/python/switch_inject.py
index 59816b19..18a75a49 100644
--- a/daemon/examples/python/switch_inject.py
+++ b/daemon/examples/python/switch_inject.py
@@ -8,7 +8,7 @@ same CoreEmu instance the GUI is using.
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes
+from core.emulator.data import IpPrefixes
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 from core.nodes.network import SwitchNode
@@ -33,8 +33,8 @@ def main():
     # create nodes
     for _ in range(NODES):
         node = session.add_node(CoreNode)
-        interface = prefixes.create_interface(node)
-        session.add_link(node.id, switch.id, interface1_data=interface)
+        interface = prefixes.create_iface(node)
+        session.add_link(node.id, switch.id, iface1_data=interface)
 
     # instantiate session
     session.instantiate()
diff --git a/daemon/examples/python/wlan.py b/daemon/examples/python/wlan.py
index 0302bbd3..de26ab97 100644
--- a/daemon/examples/python/wlan.py
+++ b/daemon/examples/python/wlan.py
@@ -6,7 +6,7 @@ interact with the GUI.
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.location.mobility import BasicRangeModel
 from core.nodes.base import CoreNode
@@ -35,8 +35,8 @@ def main():
     options.set_position(0, 0)
     for _ in range(NODES):
         node = session.add_node(CoreNode, options=options)
-        interface = prefixes.create_interface(node)
-        session.add_link(node.id, wlan.id, interface1_data=interface)
+        interface = prefixes.create_iface(node)
+        session.add_link(node.id, wlan.id, iface1_data=interface)
 
     # instantiate session
     session.instantiate()
diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto
index 828b41fb..46e1da91 100644
--- a/daemon/proto/core/api/grpc/core.proto
+++ b/daemon/proto/core/api/grpc/core.proto
@@ -319,12 +319,12 @@ message ThroughputsRequest {
 message ThroughputsEvent {
     int32 session_id = 1;
     repeated BridgeThroughput bridge_throughputs = 2;
-    repeated InterfaceThroughput interface_throughputs = 3;
+    repeated InterfaceThroughput iface_throughputs = 3;
 }
 
 message InterfaceThroughput {
     int32 node_id = 1;
-    int32 interface_id = 2;
+    int32 iface_id = 2;
     double throughput = 3;
 }
 
@@ -374,7 +374,7 @@ message ConfigEvent {
     string bitmap = 8;
     string possible_values = 9;
     string groups = 10;
-    int32 interface = 11;
+    int32 iface_id = 11;
     int32 network_id = 12;
     string opaque = 13;
 }
@@ -416,7 +416,7 @@ message GetNodeRequest {
 
 message GetNodeResponse {
     Node node = 1;
-    repeated Interface interfaces = 2;
+    repeated Interface ifaces = 2;
 }
 
 message EditNodeRequest {
@@ -492,16 +492,16 @@ message AddLinkRequest {
 
 message AddLinkResponse {
     bool result = 1;
-    Interface interface1 = 2;
-    Interface interface2 = 3;
+    Interface iface1 = 2;
+    Interface iface2 = 3;
 }
 
 message EditLinkRequest {
     int32 session_id = 1;
     int32 node1_id = 2;
     int32 node2_id = 3;
-    int32 interface1_id = 4;
-    int32 interface2_id = 5;
+    int32 iface1_id = 4;
+    int32 iface2_id = 5;
     LinkOptions options = 6;
 }
 
@@ -513,8 +513,8 @@ message DeleteLinkRequest {
     int32 session_id = 1;
     int32 node1_id = 2;
     int32 node2_id = 3;
-    int32 interface1_id = 4;
-    int32 interface2_id = 5;
+    int32 iface1_id = 4;
+    int32 iface2_id = 5;
 }
 
 message DeleteLinkResponse {
@@ -561,7 +561,7 @@ message GetInterfacesRequest {
 }
 
 message GetInterfacesResponse {
-    repeated string interfaces = 1;
+    repeated string ifaces = 1;
 }
 
 message ExecuteScriptRequest {
@@ -692,21 +692,20 @@ message Node {
     repeated string services = 6;
     string emane = 7;
     string icon = 8;
-    string opaque = 9;
-    string image = 10;
-    string server = 11;
-    repeated string config_services = 12;
-    Geo geo = 13;
-    string dir = 14;
-    string channel = 15;
+    string image = 9;
+    string server = 10;
+    repeated string config_services = 11;
+    Geo geo = 12;
+    string dir = 13;
+    string channel = 14;
 }
 
 message Link {
     int32 node1_id = 1;
     int32 node2_id = 2;
     LinkType.Enum type = 3;
-    Interface interface1 = 4;
-    Interface interface2 = 5;
+    Interface iface1 = 4;
+    Interface iface2 = 5;
     LinkOptions options = 6;
     int32 network_id = 7;
     string label = 8;
@@ -732,11 +731,11 @@ message Interface {
     string name = 2;
     string mac = 3;
     string ip4 = 4;
-    int32 ip4mask = 5;
+    int32 ip4_mask = 5;
     string ip6 = 6;
-    int32 ip6mask = 7;
-    int32 netid = 8;
-    int32 flowid = 9;
+    int32 ip6_mask = 7;
+    int32 net_id = 8;
+    int32 flow_id = 9;
     int32 mtu = 10;
 }
 
diff --git a/daemon/proto/core/api/grpc/emane.proto b/daemon/proto/core/api/grpc/emane.proto
index e4189700..ac5456fd 100644
--- a/daemon/proto/core/api/grpc/emane.proto
+++ b/daemon/proto/core/api/grpc/emane.proto
@@ -32,7 +32,7 @@ message GetEmaneModelsResponse {
 message GetEmaneModelConfigRequest {
     int32 session_id = 1;
     int32 node_id = 2;
-    int32 interface = 3;
+    int32 iface_id = 3;
     string model = 4;
 }
 
@@ -57,7 +57,7 @@ message GetEmaneModelConfigsResponse {
     message ModelConfig {
         int32 node_id = 1;
         string model = 2;
-        int32 interface = 3;
+        int32 iface_id = 3;
         map<string, common.ConfigOption> config = 4;
     }
     repeated ModelConfig configs = 1;
@@ -86,7 +86,7 @@ message EmaneLinkResponse {
 
 message EmaneModelConfig {
     int32 node_id = 1;
-    int32 interface_id = 2;
+    int32 iface_id = 2;
     string model = 3;
     map<string, string> config = 4;
 }
@@ -95,10 +95,10 @@ message EmanePathlossesRequest {
     int32 session_id = 1;
     int32 node1_id = 2;
     float rx1 = 3;
-    int32 interface1_id = 4;
+    int32 iface1_id = 4;
     int32 node2_id = 5;
     float rx2 = 6;
-    int32 interface2_id = 7;
+    int32 iface2_id = 7;
 }
 
 message EmanePathlossesResponse {
diff --git a/daemon/scripts/core-route-monitor b/daemon/scripts/core-route-monitor
index b12e6205..d644ae1b 100755
--- a/daemon/scripts/core-route-monitor
+++ b/daemon/scripts/core-route-monitor
@@ -101,8 +101,8 @@ class RouterMonitor:
                     node_map[node.id] = node.channel
                     if self.src_id is None:
                         response = self.core.get_node(self.session, node.id)
-                        for netif in response.interfaces:
-                            if self.src == netif.ip4:
+                        for iface in response.ifaces:
+                            if self.src == iface.ip4:
                                 self.src_id = node.id
                                 break
             except grpc.RpcError:
diff --git a/daemon/tests/conftest.py b/daemon/tests/conftest.py
index 9d54d9c2..be62fc03 100644
--- a/daemon/tests/conftest.py
+++ b/daemon/tests/conftest.py
@@ -14,8 +14,8 @@ from core.api.grpc.server import CoreGrpcServer
 from core.api.tlv.corehandlers import CoreHandler
 from core.emane.emanemanager import EmaneManager
 from core.emulator.coreemu import CoreEmu
+from core.emulator.data import IpPrefixes
 from core.emulator.distributed import DistributedServer
-from core.emulator.emudata import IpPrefixes
 from core.emulator.enumerations import EventTypes
 from core.emulator.session import Session
 from core.nodes.base import CoreNode
@@ -89,7 +89,7 @@ def ip_prefixes():
 
 
 @pytest.fixture(scope="session")
-def interface_helper():
+def iface_helper():
     return InterfaceHelper(ip4_prefix="10.83.0.0/16")
 
 
diff --git a/daemon/tests/emane/test_emane.py b/daemon/tests/emane/test_emane.py
index 15e3d869..f51e30b9 100644
--- a/daemon/tests/emane/test_emane.py
+++ b/daemon/tests/emane/test_emane.py
@@ -15,7 +15,7 @@ from core.emane.ieee80211abg import EmaneIeee80211abgModel
 from core.emane.nodes import EmaneNet
 from core.emane.rfpipe import EmaneRfPipeModel
 from core.emane.tdma import EmaneTdmaModel
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.session import Session
 from core.errors import CoreCommandError, CoreError
 from core.nodes.base import CoreNode
@@ -79,8 +79,8 @@ class TestEmane:
 
         for i, node in enumerate([node1, node2]):
             node.setposition(x=150 * (i + 1), y=150)
-            interface = ip_prefixes.create_interface(node)
-            session.add_link(node.id, emane_network.id, interface1_data=interface)
+            iface_data = ip_prefixes.create_iface(node)
+            session.add_link(node.id, emane_network.id, iface1_data=iface_data)
 
         # instantiate session
         session.instantiate()
@@ -119,8 +119,8 @@ class TestEmane:
 
         for i, node in enumerate([node1, node2]):
             node.setposition(x=150 * (i + 1), y=150)
-            interface = ip_prefixes.create_interface(node)
-            session.add_link(node.id, emane_network.id, interface1_data=interface)
+            iface_data = ip_prefixes.create_iface(node)
+            session.add_link(node.id, emane_network.id, iface1_data=iface_data)
 
         # instantiate session
         session.instantiate()
diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py
index 626f84a7..2623b0df 100644
--- a/daemon/tests/test_core.py
+++ b/daemon/tests/test_core.py
@@ -8,7 +8,7 @@ from typing import Type
 
 import pytest
 
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import IpPrefixes, NodeOptions
 from core.emulator.enumerations import MessageFlags
 from core.emulator.session import Session
 from core.errors import CoreCommandError
@@ -53,8 +53,8 @@ class TestCore:
 
         # link nodes to net node
         for node in [node1, node2]:
-            interface = ip_prefixes.create_interface(node)
-            session.add_link(node.id, net_node.id, interface1_data=interface)
+            iface_data = ip_prefixes.create_iface(node)
+            session.add_link(node.id, net_node.id, iface1_data=iface_data)
 
         # instantiate session
         session.instantiate()
@@ -80,8 +80,8 @@ class TestCore:
 
         # link nodes to ptp net
         for node in [node1, node2]:
-            interface = ip_prefixes.create_interface(node)
-            session.add_link(node.id, ptp_node.id, interface1_data=interface)
+            iface_data = ip_prefixes.create_iface(node)
+            session.add_link(node.id, ptp_node.id, iface1_data=iface_data)
 
         # get node client for testing
         client = node1.client
@@ -96,9 +96,9 @@ class TestCore:
         if not request.config.getoption("mock"):
             assert client.check_cmd("echo hello") == "hello"
 
-    def test_netif(self, session: Session, ip_prefixes: IpPrefixes):
+    def test_iface(self, session: Session, ip_prefixes: IpPrefixes):
         """
-        Test netif methods.
+        Test interface methods.
 
         :param session: session for test
         :param ip_prefixes: generates ip addresses for nodes
@@ -113,8 +113,8 @@ class TestCore:
 
         # link nodes to ptp net
         for node in [node1, node2]:
-            interface = ip_prefixes.create_interface(node)
-            session.add_link(node.id, ptp_node.id, interface1_data=interface)
+            iface = ip_prefixes.create_iface(node)
+            session.add_link(node.id, ptp_node.id, iface1_data=iface)
 
         # instantiate session
         session.instantiate()
@@ -126,19 +126,19 @@ class TestCore:
         assert node1.commonnets(node2)
         assert node2.commonnets(node1)
 
-        # check we can retrieve netif index
-        assert node1.ifname(0)
-        assert node2.ifname(0)
+        # check we can retrieve interface id
+        assert 0 in node1.ifaces
+        assert 0 in node2.ifaces
 
         # check interface parameters
-        interface = node1.netif(0)
-        interface.setparam("test", 1)
-        assert interface.getparam("test") == 1
-        assert interface.getparams()
+        iface = node1.get_iface(0)
+        iface.setparam("test", 1)
+        assert iface.getparam("test") == 1
+        assert iface.getparams()
 
-        # delete netif and test that if no longer exists
-        node1.delnetif(0)
-        assert not node1.netif(0)
+        # delete interface and test that if no longer exists
+        node1.delete_iface(0)
+        assert 0 not in node1.ifaces
 
     def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes):
         """
@@ -160,8 +160,8 @@ class TestCore:
 
         # link nodes
         for node in [node1, node2]:
-            interface = ip_prefixes.create_interface(node)
-            session.add_link(node.id, wlan_node.id, interface1_data=interface)
+            iface_id = ip_prefixes.create_iface(node)
+            session.add_link(node.id, wlan_node.id, iface1_data=iface_id)
 
         # instantiate session
         session.instantiate()
@@ -190,8 +190,8 @@ class TestCore:
 
         # link nodes
         for node in [node1, node2]:
-            interface = ip_prefixes.create_interface(node)
-            session.add_link(node.id, wlan_node.id, interface1_data=interface)
+            iface_id = ip_prefixes.create_iface(node)
+            session.add_link(node.id, wlan_node.id, iface1_data=iface_id)
 
         # configure mobility script for session
         config = {
diff --git a/daemon/tests/test_distributed.py b/daemon/tests/test_distributed.py
index 0f4b1731..01362cae 100644
--- a/daemon/tests/test_distributed.py
+++ b/daemon/tests/test_distributed.py
@@ -1,4 +1,4 @@
-from core.emulator.emudata import NodeOptions
+from core.emulator.data import NodeOptions
 from core.emulator.session import Session
 from core.nodes.base import CoreNode
 from core.nodes.network import HubNode
diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py
index 8beb4b9a..8abf33aa 100644
--- a/daemon/tests/test_grpc.py
+++ b/daemon/tests/test_grpc.py
@@ -18,8 +18,7 @@ from core.api.tlv.dataconversion import ConfigShim
 from core.api.tlv.enumerations import ConfigFlags
 from core.emane.ieee80211abg import EmaneIeee80211abgModel
 from core.emane.nodes import EmaneNet
-from core.emulator.data import EventData, NodeData
-from core.emulator.emudata import IpPrefixes, NodeOptions
+from core.emulator.data import EventData, IpPrefixes, NodeData, NodeOptions
 from core.emulator.enumerations import EventTypes, ExceptionLevels, NodeTypes
 from core.errors import CoreError
 from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
@@ -42,15 +41,17 @@ class TestGrpc:
             id=3, type=NodeTypes.WIRELESS_LAN.value, position=position
         )
         nodes = [node1, node2, wlan_node]
-        interface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16")
-        interface1 = interface_helper.create_interface(node1.id, 0)
-        interface2 = interface_helper.create_interface(node2.id, 0)
+        iface_helper = InterfaceHelper(ip4_prefix="10.83.0.0/16")
+        iface1_id = 0
+        iface1 = iface_helper.create_iface(node1.id, iface1_id)
+        iface2_id = 0
+        iface2 = iface_helper.create_iface(node2.id, iface2_id)
         link = core_pb2.Link(
             type=core_pb2.LinkType.WIRED,
             node1_id=node1.id,
             node2_id=node2.id,
-            interface1=interface1,
-            interface2=interface2,
+            iface1=iface1,
+            iface2=iface2,
         )
         links = [link]
         hook = core_pb2.Hook(
@@ -81,7 +82,7 @@ class TestGrpc:
         model_config_value = "500000"
         model_config = EmaneModelConfig(
             node_id=model_node_id,
-            interface_id=-1,
+            iface_id=-1,
             model=EmaneIeee80211abgModel.name,
             config={model_config_key: model_config_value},
         )
@@ -131,8 +132,8 @@ class TestGrpc:
         assert node1.id in session.nodes
         assert node2.id in session.nodes
         assert wlan_node.id in session.nodes
-        assert session.nodes[node1.id].netif(0) is not None
-        assert session.nodes[node2.id].netif(0) is not None
+        assert iface1_id in session.nodes[node1.id].ifaces
+        assert iface2_id in session.nodes[node2.id].ifaces
         hook_file, hook_data = session.hooks[EventTypes.RUNTIME_STATE][0]
         assert hook_file == hook.file
         assert hook_data == hook.data
@@ -522,8 +523,8 @@ class TestGrpc:
         session = grpc_server.coreemu.create_session()
         switch = session.add_node(SwitchNode)
         node = session.add_node(CoreNode)
-        interface = ip_prefixes.create_interface(node)
-        session.add_link(node.id, switch.id, interface)
+        iface_data = ip_prefixes.create_iface(node)
+        session.add_link(node.id, switch.id, iface_data)
 
         # then
         with client.context_connect():
@@ -540,17 +541,15 @@ class TestGrpc:
         session = grpc_server.coreemu.create_session()
         switch = session.add_node(SwitchNode)
         node = session.add_node(CoreNode)
-        interface = ip_prefixes.create_interface(node)
-        session.add_link(node.id, switch.id, interface)
+        iface_data = ip_prefixes.create_iface(node)
+        session.add_link(node.id, switch.id, iface_data)
 
         # then
         with pytest.raises(grpc.RpcError):
             with client.context_connect():
                 client.get_node_links(session.id, 3)
 
-    def test_add_link(
-        self, grpc_server: CoreGrpcServer, interface_helper: InterfaceHelper
-    ):
+    def test_add_link(self, grpc_server: CoreGrpcServer, iface_helper: InterfaceHelper):
         # given
         client = CoreGrpcClient()
         session = grpc_server.coreemu.create_session()
@@ -559,16 +558,16 @@ class TestGrpc:
         assert len(switch.all_link_data()) == 0
 
         # then
-        interface = interface_helper.create_interface(node.id, 0)
+        iface = iface_helper.create_iface(node.id, 0)
         with client.context_connect():
-            response = client.add_link(session.id, node.id, switch.id, interface)
+            response = client.add_link(session.id, node.id, switch.id, iface)
 
         # then
         assert response.result is True
         assert len(switch.all_link_data()) == 1
 
     def test_add_link_exception(
-        self, grpc_server: CoreGrpcServer, interface_helper: InterfaceHelper
+        self, grpc_server: CoreGrpcServer, iface_helper: InterfaceHelper
     ):
         # given
         client = CoreGrpcClient()
@@ -576,10 +575,10 @@ class TestGrpc:
         node = session.add_node(CoreNode)
 
         # then
-        interface = interface_helper.create_interface(node.id, 0)
+        iface = iface_helper.create_iface(node.id, 0)
         with pytest.raises(grpc.RpcError):
             with client.context_connect():
-                client.add_link(session.id, 1, 3, interface)
+                client.add_link(session.id, 1, 3, iface)
 
     def test_edit_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes):
         # given
@@ -587,32 +586,32 @@ class TestGrpc:
         session = grpc_server.coreemu.create_session()
         switch = session.add_node(SwitchNode)
         node = session.add_node(CoreNode)
-        interface = ip_prefixes.create_interface(node)
-        session.add_link(node.id, switch.id, interface)
+        iface = ip_prefixes.create_iface(node)
+        session.add_link(node.id, switch.id, iface)
         options = core_pb2.LinkOptions(bandwidth=30000)
         link = switch.all_link_data()[0]
-        assert options.bandwidth != link.bandwidth
+        assert options.bandwidth != link.options.bandwidth
 
         # then
         with client.context_connect():
             response = client.edit_link(
-                session.id, node.id, switch.id, options, interface1_id=interface.id
+                session.id, node.id, switch.id, options, iface1_id=iface.id
             )
 
         # then
         assert response.result is True
         link = switch.all_link_data()[0]
-        assert options.bandwidth == link.bandwidth
+        assert options.bandwidth == link.options.bandwidth
 
     def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes):
         # given
         client = CoreGrpcClient()
         session = grpc_server.coreemu.create_session()
         node1 = session.add_node(CoreNode)
-        interface1 = ip_prefixes.create_interface(node1)
+        iface1 = ip_prefixes.create_iface(node1)
         node2 = session.add_node(CoreNode)
-        interface2 = ip_prefixes.create_interface(node2)
-        session.add_link(node1.id, node2.id, interface1, interface2)
+        iface2 = ip_prefixes.create_iface(node2)
+        session.add_link(node1.id, node2.id, iface1, iface2)
         link_node = None
         for node_id in session.nodes:
             node = session.nodes[node_id]
@@ -624,7 +623,7 @@ class TestGrpc:
         # then
         with client.context_connect():
             response = client.delete_link(
-                session.id, node1.id, node2.id, interface1.id, interface2.id
+                session.id, node1.id, node2.id, iface1.id, iface2.id
             )
 
         # then
@@ -729,7 +728,7 @@ class TestGrpc:
         assert emane_network.id == model_config.node_id
         assert model_config.model == EmaneIeee80211abgModel.name
         assert len(model_config.config) > 0
-        assert model_config.interface == -1
+        assert model_config.iface_id == -1
 
     def test_set_emane_model_config(self, grpc_server: CoreGrpcServer):
         # given
@@ -1028,8 +1027,8 @@ class TestGrpc:
         session = grpc_server.coreemu.create_session()
         wlan = session.add_node(WlanNode)
         node = session.add_node(CoreNode)
-        interface = ip_prefixes.create_interface(node)
-        session.add_link(node.id, wlan.id, interface)
+        iface = ip_prefixes.create_iface(node)
+        session.add_link(node.id, wlan.id, iface)
         link_data = wlan.all_link_data()[0]
         queue = Queue()
 
@@ -1199,9 +1198,10 @@ class TestGrpc:
         queue = Queue()
 
         def node_handler(node_data: NodeData):
-            assert node_data.longitude == lon
-            assert node_data.latitude == lat
-            assert node_data.altitude == alt
+            n = node_data.node
+            assert n.position.lon == lon
+            assert n.position.lat == lat
+            assert n.position.alt == alt
             queue.put(node_data)
 
         session.node_handlers.append(node_handler)
diff --git a/daemon/tests/test_gui.py b/daemon/tests/test_gui.py
index d3b9362d..8f01a2bf 100644
--- a/daemon/tests/test_gui.py
+++ b/daemon/tests/test_gui.py
@@ -107,15 +107,15 @@ class TestGui:
         switch_id = 2
         coretlv.session.add_node(SwitchNode, _id=switch_id)
         ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
-        interface1_ip4 = str(ip_prefix[node1_id])
+        iface1_ip4 = str(ip_prefix[node1_id])
         message = coreapi.CoreLinkMessage.create(
             MessageFlags.ADD.value,
             [
                 (LinkTlvs.N1_NUMBER, node1_id),
                 (LinkTlvs.N2_NUMBER, switch_id),
-                (LinkTlvs.INTERFACE1_NUMBER, 0),
-                (LinkTlvs.INTERFACE1_IP4, interface1_ip4),
-                (LinkTlvs.INTERFACE1_IP4_MASK, 24),
+                (LinkTlvs.IFACE1_NUMBER, 0),
+                (LinkTlvs.IFACE1_IP4, iface1_ip4),
+                (LinkTlvs.IFACE1_IP4_MASK, 24),
             ],
         )
 
@@ -131,15 +131,15 @@ class TestGui:
         switch_id = 2
         coretlv.session.add_node(SwitchNode, _id=switch_id)
         ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
-        interface2_ip4 = str(ip_prefix[node1_id])
+        iface2_ip4 = str(ip_prefix[node1_id])
         message = coreapi.CoreLinkMessage.create(
             MessageFlags.ADD.value,
             [
                 (LinkTlvs.N1_NUMBER, switch_id),
                 (LinkTlvs.N2_NUMBER, node1_id),
-                (LinkTlvs.INTERFACE2_NUMBER, 0),
-                (LinkTlvs.INTERFACE2_IP4, interface2_ip4),
-                (LinkTlvs.INTERFACE2_IP4_MASK, 24),
+                (LinkTlvs.IFACE2_NUMBER, 0),
+                (LinkTlvs.IFACE2_IP4, iface2_ip4),
+                (LinkTlvs.IFACE2_IP4_MASK, 24),
             ],
         )
 
@@ -155,19 +155,19 @@ class TestGui:
         node2_id = 2
         coretlv.session.add_node(CoreNode, _id=node2_id)
         ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
-        interface1_ip4 = str(ip_prefix[node1_id])
-        interface2_ip4 = str(ip_prefix[node2_id])
+        iface1_ip4 = str(ip_prefix[node1_id])
+        iface2_ip4 = str(ip_prefix[node2_id])
         message = coreapi.CoreLinkMessage.create(
             MessageFlags.ADD.value,
             [
                 (LinkTlvs.N1_NUMBER, node1_id),
                 (LinkTlvs.N2_NUMBER, node2_id),
-                (LinkTlvs.INTERFACE1_NUMBER, 0),
-                (LinkTlvs.INTERFACE1_IP4, interface1_ip4),
-                (LinkTlvs.INTERFACE1_IP4_MASK, 24),
-                (LinkTlvs.INTERFACE2_NUMBER, 0),
-                (LinkTlvs.INTERFACE2_IP4, interface2_ip4),
-                (LinkTlvs.INTERFACE2_IP4_MASK, 24),
+                (LinkTlvs.IFACE1_NUMBER, 0),
+                (LinkTlvs.IFACE1_IP4, iface1_ip4),
+                (LinkTlvs.IFACE1_IP4_MASK, 24),
+                (LinkTlvs.IFACE2_NUMBER, 0),
+                (LinkTlvs.IFACE2_IP4, iface2_ip4),
+                (LinkTlvs.IFACE2_IP4_MASK, 24),
             ],
         )
 
@@ -185,15 +185,15 @@ class TestGui:
         switch_id = 2
         coretlv.session.add_node(SwitchNode, _id=switch_id)
         ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
-        interface1_ip4 = str(ip_prefix[node1_id])
+        iface1_ip4 = str(ip_prefix[node1_id])
         message = coreapi.CoreLinkMessage.create(
             MessageFlags.ADD.value,
             [
                 (LinkTlvs.N1_NUMBER, node1_id),
                 (LinkTlvs.N2_NUMBER, switch_id),
-                (LinkTlvs.INTERFACE1_NUMBER, 0),
-                (LinkTlvs.INTERFACE1_IP4, interface1_ip4),
-                (LinkTlvs.INTERFACE1_IP4_MASK, 24),
+                (LinkTlvs.IFACE1_NUMBER, 0),
+                (LinkTlvs.IFACE1_IP4, iface1_ip4),
+                (LinkTlvs.IFACE1_IP4_MASK, 24),
             ],
         )
         coretlv.handle_message(message)
@@ -201,7 +201,7 @@ class TestGui:
         all_links = switch_node.all_link_data()
         assert len(all_links) == 1
         link = all_links[0]
-        assert link.bandwidth is None
+        assert link.options.bandwidth is None
 
         bandwidth = 50000
         message = coreapi.CoreLinkMessage.create(
@@ -209,7 +209,7 @@ class TestGui:
             [
                 (LinkTlvs.N1_NUMBER, node1_id),
                 (LinkTlvs.N2_NUMBER, switch_id),
-                (LinkTlvs.INTERFACE1_NUMBER, 0),
+                (LinkTlvs.IFACE1_NUMBER, 0),
                 (LinkTlvs.BANDWIDTH, bandwidth),
             ],
         )
@@ -219,7 +219,7 @@ class TestGui:
         all_links = switch_node.all_link_data()
         assert len(all_links) == 1
         link = all_links[0]
-        assert link.bandwidth == bandwidth
+        assert link.options.bandwidth == bandwidth
 
     def test_link_delete_node_to_node(self, coretlv: CoreHandler):
         node1_id = 1
@@ -227,18 +227,18 @@ class TestGui:
         node2_id = 2
         coretlv.session.add_node(CoreNode, _id=node2_id)
         ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
-        interface1_ip4 = str(ip_prefix[node1_id])
-        interface2_ip4 = str(ip_prefix[node2_id])
+        iface1_ip4 = str(ip_prefix[node1_id])
+        iface2_ip4 = str(ip_prefix[node2_id])
         message = coreapi.CoreLinkMessage.create(
             MessageFlags.ADD.value,
             [
                 (LinkTlvs.N1_NUMBER, node1_id),
                 (LinkTlvs.N2_NUMBER, node2_id),
-                (LinkTlvs.INTERFACE1_NUMBER, 0),
-                (LinkTlvs.INTERFACE1_IP4, interface1_ip4),
-                (LinkTlvs.INTERFACE1_IP4_MASK, 24),
-                (LinkTlvs.INTERFACE2_IP4, interface2_ip4),
-                (LinkTlvs.INTERFACE2_IP4_MASK, 24),
+                (LinkTlvs.IFACE1_NUMBER, 0),
+                (LinkTlvs.IFACE1_IP4, iface1_ip4),
+                (LinkTlvs.IFACE1_IP4_MASK, 24),
+                (LinkTlvs.IFACE2_IP4, iface2_ip4),
+                (LinkTlvs.IFACE2_IP4_MASK, 24),
             ],
         )
         coretlv.handle_message(message)
@@ -253,8 +253,8 @@ class TestGui:
             [
                 (LinkTlvs.N1_NUMBER, node1_id),
                 (LinkTlvs.N2_NUMBER, node2_id),
-                (LinkTlvs.INTERFACE1_NUMBER, 0),
-                (LinkTlvs.INTERFACE2_NUMBER, 0),
+                (LinkTlvs.IFACE1_NUMBER, 0),
+                (LinkTlvs.IFACE2_NUMBER, 0),
             ],
         )
         coretlv.handle_message(message)
@@ -271,15 +271,15 @@ class TestGui:
         switch_id = 2
         coretlv.session.add_node(SwitchNode, _id=switch_id)
         ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
-        interface1_ip4 = str(ip_prefix[node1_id])
+        iface1_ip4 = str(ip_prefix[node1_id])
         message = coreapi.CoreLinkMessage.create(
             MessageFlags.ADD.value,
             [
                 (LinkTlvs.N1_NUMBER, node1_id),
                 (LinkTlvs.N2_NUMBER, switch_id),
-                (LinkTlvs.INTERFACE1_NUMBER, 0),
-                (LinkTlvs.INTERFACE1_IP4, interface1_ip4),
-                (LinkTlvs.INTERFACE1_IP4_MASK, 24),
+                (LinkTlvs.IFACE1_NUMBER, 0),
+                (LinkTlvs.IFACE1_IP4, iface1_ip4),
+                (LinkTlvs.IFACE1_IP4_MASK, 24),
             ],
         )
         coretlv.handle_message(message)
@@ -292,7 +292,7 @@ class TestGui:
             [
                 (LinkTlvs.N1_NUMBER, node1_id),
                 (LinkTlvs.N2_NUMBER, switch_id),
-                (LinkTlvs.INTERFACE1_NUMBER, 0),
+                (LinkTlvs.IFACE1_NUMBER, 0),
             ],
         )
         coretlv.handle_message(message)
@@ -307,15 +307,15 @@ class TestGui:
         switch_id = 2
         coretlv.session.add_node(SwitchNode, _id=switch_id)
         ip_prefix = netaddr.IPNetwork("10.0.0.0/24")
-        interface1_ip4 = str(ip_prefix[node1_id])
+        iface1_ip4 = str(ip_prefix[node1_id])
         message = coreapi.CoreLinkMessage.create(
             MessageFlags.ADD.value,
             [
                 (LinkTlvs.N1_NUMBER, node1_id),
                 (LinkTlvs.N2_NUMBER, switch_id),
-                (LinkTlvs.INTERFACE1_NUMBER, 0),
-                (LinkTlvs.INTERFACE1_IP4, interface1_ip4),
-                (LinkTlvs.INTERFACE1_IP4_MASK, 24),
+                (LinkTlvs.IFACE1_NUMBER, 0),
+                (LinkTlvs.IFACE1_IP4, iface1_ip4),
+                (LinkTlvs.IFACE1_IP4_MASK, 24),
             ],
         )
         coretlv.handle_message(message)
@@ -328,7 +328,7 @@ class TestGui:
             [
                 (LinkTlvs.N1_NUMBER, switch_id),
                 (LinkTlvs.N2_NUMBER, node1_id),
-                (LinkTlvs.INTERFACE2_NUMBER, 0),
+                (LinkTlvs.IFACE2_NUMBER, 0),
             ],
         )
         coretlv.handle_message(message)
diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py
index 819e2be8..4078d8bc 100644
--- a/daemon/tests/test_links.py
+++ b/daemon/tests/test_links.py
@@ -1,6 +1,6 @@
 from typing import Tuple
 
-from core.emulator.emudata import IpPrefixes, LinkOptions
+from core.emulator.data import IpPrefixes, LinkOptions
 from core.emulator.session import Session
 from core.nodes.base import CoreNode
 from core.nodes.network import SwitchNode
@@ -14,9 +14,9 @@ def create_ptp_network(
     node2 = session.add_node(CoreNode)
 
     # link nodes to net node
-    interface1_data = ip_prefixes.create_interface(node1)
-    interface2_data = ip_prefixes.create_interface(node2)
-    session.add_link(node1.id, node2.id, interface1_data, interface2_data)
+    iface1_data = ip_prefixes.create_iface(node1)
+    iface2_data = ip_prefixes.create_iface(node2)
+    session.add_link(node1.id, node2.id, iface1_data, iface2_data)
 
     # instantiate session
     session.instantiate()
@@ -29,41 +29,41 @@ class TestLinks:
         # given
         node1 = session.add_node(CoreNode)
         node2 = session.add_node(CoreNode)
-        interface1_data = ip_prefixes.create_interface(node1)
-        interface2_data = ip_prefixes.create_interface(node2)
+        iface1_data = ip_prefixes.create_iface(node1)
+        iface2_data = ip_prefixes.create_iface(node2)
 
         # when
-        session.add_link(node1.id, node2.id, interface1_data, interface2_data)
+        session.add_link(node1.id, node2.id, iface1_data, iface2_data)
 
         # then
-        assert node1.netif(interface1_data.id)
-        assert node2.netif(interface2_data.id)
+        assert node1.get_iface(iface1_data.id)
+        assert node2.get_iface(iface2_data.id)
 
     def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
         # given
         node1 = session.add_node(CoreNode)
         node2 = session.add_node(SwitchNode)
-        interface1_data = ip_prefixes.create_interface(node1)
+        iface1_data = ip_prefixes.create_iface(node1)
 
         # when
-        session.add_link(node1.id, node2.id, interface1_data=interface1_data)
+        session.add_link(node1.id, node2.id, iface1_data=iface1_data)
 
         # then
         assert node2.all_link_data()
-        assert node1.netif(interface1_data.id)
+        assert node1.get_iface(iface1_data.id)
 
     def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
         # given
         node1 = session.add_node(SwitchNode)
         node2 = session.add_node(CoreNode)
-        interface2_data = ip_prefixes.create_interface(node2)
+        iface2_data = ip_prefixes.create_iface(node2)
 
         # when
-        session.add_link(node1.id, node2.id, interface2_data=interface2_data)
+        session.add_link(node1.id, node2.id, iface2_data=iface2_data)
 
         # then
         assert node1.all_link_data()
-        assert node2.netif(interface2_data.id)
+        assert node2.get_iface(iface2_data.id)
 
     def test_add_net_to_net(self, session):
         # given
@@ -85,29 +85,29 @@ class TestLinks:
         jitter = 10
         node1 = session.add_node(CoreNode)
         node2 = session.add_node(SwitchNode)
-        interface1_data = ip_prefixes.create_interface(node1)
-        session.add_link(node1.id, node2.id, interface1_data)
-        interface1 = node1.netif(interface1_data.id)
-        assert interface1.getparam("delay") != delay
-        assert interface1.getparam("bw") != bandwidth
-        assert interface1.getparam("loss") != loss
-        assert interface1.getparam("duplicate") != dup
-        assert interface1.getparam("jitter") != jitter
+        iface1_data = ip_prefixes.create_iface(node1)
+        session.add_link(node1.id, node2.id, iface1_data)
+        iface1 = node1.get_iface(iface1_data.id)
+        assert iface1.getparam("delay") != delay
+        assert iface1.getparam("bw") != bandwidth
+        assert iface1.getparam("loss") != loss
+        assert iface1.getparam("duplicate") != dup
+        assert iface1.getparam("jitter") != jitter
 
         # when
         options = LinkOptions(
             delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter
         )
         session.update_link(
-            node1.id, node2.id, interface1_id=interface1_data.id, options=options
+            node1.id, node2.id, iface1_id=iface1_data.id, options=options
         )
 
         # then
-        assert interface1.getparam("delay") == delay
-        assert interface1.getparam("bw") == bandwidth
-        assert interface1.getparam("loss") == loss
-        assert interface1.getparam("duplicate") == dup
-        assert interface1.getparam("jitter") == jitter
+        assert iface1.getparam("delay") == delay
+        assert iface1.getparam("bw") == bandwidth
+        assert iface1.getparam("loss") == loss
+        assert iface1.getparam("duplicate") == dup
+        assert iface1.getparam("jitter") == jitter
 
     def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
         # given
@@ -118,29 +118,29 @@ class TestLinks:
         jitter = 10
         node1 = session.add_node(SwitchNode)
         node2 = session.add_node(CoreNode)
-        interface2_data = ip_prefixes.create_interface(node2)
-        session.add_link(node1.id, node2.id, interface2_data=interface2_data)
-        interface2 = node2.netif(interface2_data.id)
-        assert interface2.getparam("delay") != delay
-        assert interface2.getparam("bw") != bandwidth
-        assert interface2.getparam("loss") != loss
-        assert interface2.getparam("duplicate") != dup
-        assert interface2.getparam("jitter") != jitter
+        iface2_data = ip_prefixes.create_iface(node2)
+        session.add_link(node1.id, node2.id, iface2_data=iface2_data)
+        iface2 = node2.get_iface(iface2_data.id)
+        assert iface2.getparam("delay") != delay
+        assert iface2.getparam("bw") != bandwidth
+        assert iface2.getparam("loss") != loss
+        assert iface2.getparam("duplicate") != dup
+        assert iface2.getparam("jitter") != jitter
 
         # when
         options = LinkOptions(
             delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter
         )
         session.update_link(
-            node1.id, node2.id, interface2_id=interface2_data.id, options=options
+            node1.id, node2.id, iface2_id=iface2_data.id, options=options
         )
 
         # then
-        assert interface2.getparam("delay") == delay
-        assert interface2.getparam("bw") == bandwidth
-        assert interface2.getparam("loss") == loss
-        assert interface2.getparam("duplicate") == dup
-        assert interface2.getparam("jitter") == jitter
+        assert iface2.getparam("delay") == delay
+        assert iface2.getparam("bw") == bandwidth
+        assert iface2.getparam("loss") == loss
+        assert iface2.getparam("duplicate") == dup
+        assert iface2.getparam("jitter") == jitter
 
     def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes):
         # given
@@ -151,83 +151,81 @@ class TestLinks:
         jitter = 10
         node1 = session.add_node(CoreNode)
         node2 = session.add_node(CoreNode)
-        interface1_data = ip_prefixes.create_interface(node1)
-        interface2_data = ip_prefixes.create_interface(node2)
-        session.add_link(node1.id, node2.id, interface1_data, interface2_data)
-        interface1 = node1.netif(interface1_data.id)
-        interface2 = node2.netif(interface2_data.id)
-        assert interface1.getparam("delay") != delay
-        assert interface1.getparam("bw") != bandwidth
-        assert interface1.getparam("loss") != loss
-        assert interface1.getparam("duplicate") != dup
-        assert interface1.getparam("jitter") != jitter
-        assert interface2.getparam("delay") != delay
-        assert interface2.getparam("bw") != bandwidth
-        assert interface2.getparam("loss") != loss
-        assert interface2.getparam("duplicate") != dup
-        assert interface2.getparam("jitter") != jitter
+        iface1_data = ip_prefixes.create_iface(node1)
+        iface2_data = ip_prefixes.create_iface(node2)
+        session.add_link(node1.id, node2.id, iface1_data, iface2_data)
+        iface1 = node1.get_iface(iface1_data.id)
+        iface2 = node2.get_iface(iface2_data.id)
+        assert iface1.getparam("delay") != delay
+        assert iface1.getparam("bw") != bandwidth
+        assert iface1.getparam("loss") != loss
+        assert iface1.getparam("duplicate") != dup
+        assert iface1.getparam("jitter") != jitter
+        assert iface2.getparam("delay") != delay
+        assert iface2.getparam("bw") != bandwidth
+        assert iface2.getparam("loss") != loss
+        assert iface2.getparam("duplicate") != dup
+        assert iface2.getparam("jitter") != jitter
 
         # when
         options = LinkOptions(
             delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter
         )
-        session.update_link(
-            node1.id, node2.id, interface1_data.id, interface2_data.id, options
-        )
+        session.update_link(node1.id, node2.id, iface1_data.id, iface2_data.id, options)
 
         # then
-        assert interface1.getparam("delay") == delay
-        assert interface1.getparam("bw") == bandwidth
-        assert interface1.getparam("loss") == loss
-        assert interface1.getparam("duplicate") == dup
-        assert interface1.getparam("jitter") == jitter
-        assert interface2.getparam("delay") == delay
-        assert interface2.getparam("bw") == bandwidth
-        assert interface2.getparam("loss") == loss
-        assert interface2.getparam("duplicate") == dup
-        assert interface2.getparam("jitter") == jitter
+        assert iface1.getparam("delay") == delay
+        assert iface1.getparam("bw") == bandwidth
+        assert iface1.getparam("loss") == loss
+        assert iface1.getparam("duplicate") == dup
+        assert iface1.getparam("jitter") == jitter
+        assert iface2.getparam("delay") == delay
+        assert iface2.getparam("bw") == bandwidth
+        assert iface2.getparam("loss") == loss
+        assert iface2.getparam("duplicate") == dup
+        assert iface2.getparam("jitter") == jitter
 
     def test_delete_ptp(self, session: Session, ip_prefixes: IpPrefixes):
         # given
         node1 = session.add_node(CoreNode)
         node2 = session.add_node(CoreNode)
-        interface1_data = ip_prefixes.create_interface(node1)
-        interface2_data = ip_prefixes.create_interface(node2)
-        session.add_link(node1.id, node2.id, interface1_data, interface2_data)
-        assert node1.netif(interface1_data.id)
-        assert node2.netif(interface2_data.id)
+        iface1_data = ip_prefixes.create_iface(node1)
+        iface2_data = ip_prefixes.create_iface(node2)
+        session.add_link(node1.id, node2.id, iface1_data, iface2_data)
+        assert node1.get_iface(iface1_data.id)
+        assert node2.get_iface(iface2_data.id)
 
         # when
-        session.delete_link(node1.id, node2.id, interface1_data.id, interface2_data.id)
+        session.delete_link(node1.id, node2.id, iface1_data.id, iface2_data.id)
 
         # then
-        assert not node1.netif(interface1_data.id)
-        assert not node2.netif(interface2_data.id)
+        assert iface1_data.id not in node1.ifaces
+        assert iface2_data.id not in node2.ifaces
 
     def test_delete_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
         # given
         node1 = session.add_node(CoreNode)
         node2 = session.add_node(SwitchNode)
-        interface1_data = ip_prefixes.create_interface(node1)
-        session.add_link(node1.id, node2.id, interface1_data)
-        assert node1.netif(interface1_data.id)
+        iface1_data = ip_prefixes.create_iface(node1)
+        session.add_link(node1.id, node2.id, iface1_data)
+        assert node1.get_iface(iface1_data.id)
 
         # when
-        session.delete_link(node1.id, node2.id, interface1_id=interface1_data.id)
+        session.delete_link(node1.id, node2.id, iface1_id=iface1_data.id)
 
         # then
-        assert not node1.netif(interface1_data.id)
+        assert iface1_data.id not in node1.ifaces
 
     def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
         # given
         node1 = session.add_node(SwitchNode)
         node2 = session.add_node(CoreNode)
-        interface2_data = ip_prefixes.create_interface(node2)
-        session.add_link(node1.id, node2.id, interface2_data=interface2_data)
-        assert node2.netif(interface2_data.id)
+        iface2_data = ip_prefixes.create_iface(node2)
+        session.add_link(node1.id, node2.id, iface2_data=iface2_data)
+        assert node2.get_iface(iface2_data.id)
 
         # when
-        session.delete_link(node1.id, node2.id, interface2_id=interface2_data.id)
+        session.delete_link(node1.id, node2.id, iface2_id=iface2_data.id)
 
         # then
-        assert not node2.netif(interface2_data.id)
+        assert iface2_data.id not in node2.ifaces
diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py
index 0cbdb8ae..8af2e895 100644
--- a/daemon/tests/test_nodes.py
+++ b/daemon/tests/test_nodes.py
@@ -1,6 +1,6 @@
 import pytest
 
-from core.emulator.emudata import InterfaceData, NodeOptions
+from core.emulator.data import InterfaceData, NodeOptions
 from core.emulator.session import Session
 from core.errors import CoreError
 from core.nodes.base import CoreNode
@@ -49,57 +49,57 @@ class TestNodes:
         with pytest.raises(CoreError):
             session.get_node(node.id, CoreNode)
 
-    def test_node_sethwaddr(self, session: Session):
+    def test_node_set_mac(self, session: Session):
         # given
         node = session.add_node(CoreNode)
         switch = session.add_node(SwitchNode)
-        interface_data = InterfaceData()
-        interface = node.newnetif(switch, interface_data)
+        iface_data = InterfaceData()
+        iface = node.new_iface(switch, iface_data)
         mac = "aa:aa:aa:ff:ff:ff"
 
         # when
-        node.sethwaddr(interface.netindex, mac)
+        node.set_mac(iface.node_id, mac)
 
         # then
-        assert interface.hwaddr == mac
+        assert iface.mac == mac
 
-    def test_node_sethwaddr_exception(self, session: Session):
+    def test_node_set_mac_exception(self, session: Session):
         # given
         node = session.add_node(CoreNode)
         switch = session.add_node(SwitchNode)
-        interface_data = InterfaceData()
-        interface = node.newnetif(switch, interface_data)
+        iface_data = InterfaceData()
+        iface = node.new_iface(switch, iface_data)
         mac = "aa:aa:aa:ff:ff:fff"
 
         # when
         with pytest.raises(CoreError):
-            node.sethwaddr(interface.netindex, mac)
+            node.set_mac(iface.node_id, mac)
 
     def test_node_addaddr(self, session: Session):
         # given
         node = session.add_node(CoreNode)
         switch = session.add_node(SwitchNode)
-        interface_data = InterfaceData()
-        interface = node.newnetif(switch, interface_data)
+        iface_data = InterfaceData()
+        iface = node.new_iface(switch, iface_data)
         addr = "192.168.0.1/24"
 
         # when
-        node.addaddr(interface.netindex, addr)
+        node.addaddr(iface.node_id, addr)
 
         # then
-        assert interface.addrlist[0] == addr
+        assert iface.addrlist[0] == addr
 
     def test_node_addaddr_exception(self, session):
         # given
         node = session.add_node(CoreNode)
         switch = session.add_node(SwitchNode)
-        interface_data = InterfaceData()
-        interface = node.newnetif(switch, interface_data)
+        iface_data = InterfaceData()
+        iface = node.new_iface(switch, iface_data)
         addr = "256.168.0.1/24"
 
         # when
         with pytest.raises(CoreError):
-            node.addaddr(interface.netindex, addr)
+            node.addaddr(iface.node_id, addr)
 
     @pytest.mark.parametrize("net_type", NET_TYPES)
     def test_net(self, session, net_type):
diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py
index 0b44a354..91b598f3 100644
--- a/daemon/tests/test_xml.py
+++ b/daemon/tests/test_xml.py
@@ -3,7 +3,7 @@ from xml.etree import ElementTree
 
 import pytest
 
-from core.emulator.emudata import IpPrefixes, LinkOptions, NodeOptions
+from core.emulator.data import IpPrefixes, LinkOptions, NodeOptions
 from core.emulator.enumerations import EventTypes
 from core.emulator.session import Session
 from core.errors import CoreError
@@ -73,8 +73,8 @@ class TestXml:
 
         # link nodes to ptp net
         for node in [node1, node2]:
-            interface = ip_prefixes.create_interface(node)
-            session.add_link(node.id, ptp_node.id, interface1_data=interface)
+            iface_data = ip_prefixes.create_iface(node)
+            session.add_link(node.id, ptp_node.id, iface1_data=iface_data)
 
         # instantiate session
         session.instantiate()
@@ -128,8 +128,8 @@ class TestXml:
 
         # link nodes to ptp net
         for node in [node1, node2]:
-            interface = ip_prefixes.create_interface(node)
-            session.add_link(node.id, ptp_node.id, interface1_data=interface)
+            iface_data = ip_prefixes.create_iface(node)
+            session.add_link(node.id, ptp_node.id, iface1_data=iface_data)
 
         # set custom values for node service
         session.services.set_service(node1.id, SshService.name)
@@ -197,8 +197,8 @@ class TestXml:
 
         # link nodes
         for node in [node1, node2]:
-            interface = ip_prefixes.create_interface(node)
-            session.add_link(node.id, wlan_node.id, interface1_data=interface)
+            iface_data = ip_prefixes.create_iface(node)
+            session.add_link(node.id, wlan_node.id, iface1_data=iface_data)
 
         # instantiate session
         session.instantiate()
@@ -299,7 +299,7 @@ class TestXml:
         """
         # create nodes
         node1 = session.add_node(CoreNode)
-        interface1_data = ip_prefixes.create_interface(node1)
+        iface1_data = ip_prefixes.create_iface(node1)
         switch = session.add_node(SwitchNode)
 
         # create link
@@ -309,7 +309,7 @@ class TestXml:
         options.jitter = 10
         options.delay = 30
         options.dup = 5
-        session.add_link(node1.id, switch.id, interface1_data, options=options)
+        session.add_link(node1.id, switch.id, iface1_data, options=options)
 
         # instantiate session
         session.instantiate()
@@ -347,11 +347,11 @@ class TestXml:
             node = session.nodes[node_id]
             links += node.all_link_data()
         link = links[0]
-        assert options.loss == link.loss
-        assert options.bandwidth == link.bandwidth
-        assert options.jitter == link.jitter
-        assert options.delay == link.delay
-        assert options.dup == link.dup
+        assert options.loss == link.options.loss
+        assert options.bandwidth == link.options.bandwidth
+        assert options.jitter == link.options.jitter
+        assert options.delay == link.options.delay
+        assert options.dup == link.options.dup
 
     def test_link_options_ptp(
         self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@@ -365,9 +365,9 @@ class TestXml:
         """
         # create nodes
         node1 = session.add_node(CoreNode)
-        interface1_data = ip_prefixes.create_interface(node1)
+        iface1_data = ip_prefixes.create_iface(node1)
         node2 = session.add_node(CoreNode)
-        interface2_data = ip_prefixes.create_interface(node2)
+        iface2_data = ip_prefixes.create_iface(node2)
 
         # create link
         options = LinkOptions()
@@ -376,7 +376,7 @@ class TestXml:
         options.jitter = 10
         options.delay = 30
         options.dup = 5
-        session.add_link(node1.id, node2.id, interface1_data, interface2_data, options)
+        session.add_link(node1.id, node2.id, iface1_data, iface2_data, options)
 
         # instantiate session
         session.instantiate()
@@ -414,11 +414,11 @@ class TestXml:
             node = session.nodes[node_id]
             links += node.all_link_data()
         link = links[0]
-        assert options.loss == link.loss
-        assert options.bandwidth == link.bandwidth
-        assert options.jitter == link.jitter
-        assert options.delay == link.delay
-        assert options.dup == link.dup
+        assert options.loss == link.options.loss
+        assert options.bandwidth == link.options.bandwidth
+        assert options.jitter == link.options.jitter
+        assert options.delay == link.options.delay
+        assert options.dup == link.options.dup
 
     def test_link_options_bidirectional(
         self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@@ -432,9 +432,9 @@ class TestXml:
         """
         # create nodes
         node1 = session.add_node(CoreNode)
-        interface1_data = ip_prefixes.create_interface(node1)
+        iface1_data = ip_prefixes.create_iface(node1)
         node2 = session.add_node(CoreNode)
-        interface2_data = ip_prefixes.create_interface(node2)
+        iface2_data = ip_prefixes.create_iface(node2)
 
         # create link
         options1 = LinkOptions()
@@ -444,7 +444,7 @@ class TestXml:
         options1.loss = 10.5
         options1.dup = 5
         options1.jitter = 5
-        session.add_link(node1.id, node2.id, interface1_data, interface2_data, options1)
+        session.add_link(node1.id, node2.id, iface1_data, iface2_data, options1)
         options2 = LinkOptions()
         options2.unidirectional = 1
         options2.bandwidth = 10000
@@ -453,7 +453,7 @@ class TestXml:
         options2.dup = 10
         options2.jitter = 10
         session.update_link(
-            node2.id, node1.id, interface2_data.id, interface1_data.id, options2
+            node2.id, node1.id, iface2_data.id, iface1_data.id, options2
         )
 
         # instantiate session
@@ -494,13 +494,13 @@ class TestXml:
         assert len(links) == 2
         link1 = links[0]
         link2 = links[1]
-        assert options1.bandwidth == link1.bandwidth
-        assert options1.delay == link1.delay
-        assert options1.loss == link1.loss
-        assert options1.dup == link1.dup
-        assert options1.jitter == link1.jitter
-        assert options2.bandwidth == link2.bandwidth
-        assert options2.delay == link2.delay
-        assert options2.loss == link2.loss
-        assert options2.dup == link2.dup
-        assert options2.jitter == link2.jitter
+        assert options1.bandwidth == link1.options.bandwidth
+        assert options1.delay == link1.options.delay
+        assert options1.loss == link1.options.loss
+        assert options1.dup == link1.options.dup
+        assert options1.jitter == link1.options.jitter
+        assert options2.bandwidth == link2.options.bandwidth
+        assert options2.delay == link2.options.delay
+        assert options2.loss == link2.options.loss
+        assert options2.dup == link2.options.dup
+        assert options2.jitter == link2.options.jitter
diff --git a/docs/scripting.md b/docs/scripting.md
index 59bc02ae..f65d66a3 100644
--- a/docs/scripting.md
+++ b/docs/scripting.md
@@ -36,7 +36,7 @@ interact with the GUI.
 import logging
 
 from core.emulator.coreemu import CoreEmu
-from core.emulator.emudata import IpPrefixes
+from core.emulator.data import IpPrefixes
 from core.emulator.enumerations import EventTypes
 from core.nodes.base import CoreNode
 from core.nodes.network import SwitchNode
@@ -61,8 +61,8 @@ def main():
     # create nodes
     for _ in range(NODES):
         node = session.add_node(CoreNode)
-        interface = prefixes.create_interface(node)
-        session.add_link(node.id, switch.id, interface1_data=interface)
+        interface = prefixes.create_iface(node)
+        session.add_link(node.id, switch.id, iface1_data=interface)
 
     # instantiate session
     session.instantiate()
diff --git a/docs/services.md b/docs/services.md
index 9f47ae48..2ce52e99 100644
--- a/docs/services.md
+++ b/docs/services.md
@@ -263,7 +263,7 @@ class MyService(CoreService):
 
         if filename == cls.configs[0]:
             cfg += "# auto-generated by MyService (sample.py)\n"
-            for ifc in node.netifs():
+            for ifc in node.get_ifaces():
                 cfg += f'echo "Node {node.name} has interface {ifc.name}"\n'
         elif filename == cls.configs[1]:
             cfg += "echo hello"