diff --git a/gso/__init__.py b/gso/__init__.py
index 478efc94c1cba9cec5745153b8a47890c83feab0..333891c348a8d73aa6271760a252caba472a6cb9 100644
--- a/gso/__init__.py
+++ b/gso/__init__.py
@@ -1,13 +1,15 @@
 """The main entrypoint for :term:`GSO`, and the different ways in which it can be run."""
 
 import os
+from typing import Annotated
 
 import sentry_sdk
+import strawberry
 import typer
 from celery import Celery
 from orchestrator import OrchestratorCore, app_settings
 from orchestrator.cli.main import app as cli_app
-from orchestrator.graphql import SCALAR_OVERRIDES
+from orchestrator.graphql import DEFAULT_GRAPHQL_MODELS, SCALAR_OVERRIDES
 from orchestrator.services.tasks import initialise_celery
 from orchestrator.settings import ExecutorType
 
@@ -19,6 +21,7 @@ from gso.auth.oidc import oidc_instance
 from gso.auth.opa import graphql_opa_instance, opa_instance
 from gso.graphql_api.types import GSO_SCALAR_OVERRIDES
 from gso.settings import load_oss_params
+from gso.utils.types.interfaces import LAGMember
 
 SCALAR_OVERRIDES.update(GSO_SCALAR_OVERRIDES)
 
@@ -35,7 +38,18 @@ def init_gso_app() -> OrchestratorCore:
     app.register_authentication(oidc_instance)
     app.register_authorization(opa_instance)
     app.register_graphql_authorization(graphql_opa_instance)
-    app.register_graphql()
+
+    @strawberry.experimental.pydantic.type(model=LAGMember)
+    class LAGMemberGraphql:
+        self_reference_block: Annotated["LAGMemberGraphql", strawberry.lazy("gso.utils.types.interfaces")] | None = None
+        interface_name: str
+        interface_description: str
+
+    updated_graphql_models = DEFAULT_GRAPHQL_MODELS | {
+        "LAGMemberGraphql": LAGMemberGraphql,
+    }
+
+    app.register_graphql(graphql_models=updated_graphql_models)
     app.include_router(api_router, prefix="/api")
 
     if app_settings.EXECUTOR == ExecutorType.WORKER:
diff --git a/gso/products/__init__.py b/gso/products/__init__.py
index 5fc483613fdcb05a56f0c9b02f153406047e67b9..df2f7ff16c8835a6a778bfd5b770239a0f4d642e 100644
--- a/gso/products/__init__.py
+++ b/gso/products/__init__.py
@@ -9,6 +9,7 @@ from orchestrator.domain import SUBSCRIPTION_MODEL_REGISTRY
 from pydantic_forms.types import strEnum
 
 from gso.products.product_types.edge_port import EdgePort
+from gso.products.product_types.geant_ip import GeantIP
 from gso.products.product_types.iptrunk import ImportedIptrunk, Iptrunk
 from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect
 from gso.products.product_types.office_router import ImportedOfficeRouter, OfficeRouter
@@ -39,6 +40,7 @@ class ProductName(strEnum):
     OPENGEAR = "Opengear"
     IMPORTED_OPENGEAR = "Imported Opengear"
     EDGE_PORT = "Edge port"
+    GEANT_IP = "GEANT IP"
 
 
 class ProductType(strEnum):
@@ -60,6 +62,7 @@ class ProductType(strEnum):
     OPENGEAR = Opengear.__name__
     IMPORTED_OPENGEAR = Opengear.__name__
     EDGE_PORT = EdgePort.__name__
+    GEANT_IP = GeantIP.__name__
 
 
 SUBSCRIPTION_MODEL_REGISTRY.update(
@@ -80,5 +83,6 @@ SUBSCRIPTION_MODEL_REGISTRY.update(
         ProductName.OPENGEAR.value: Opengear,
         ProductName.IMPORTED_OPENGEAR.value: ImportedOpengear,
         ProductName.EDGE_PORT.value: EdgePort,
+        ProductType.GEANT_IP.value: GeantIP,
     },
 )
diff --git a/gso/products/product_blocks/bgp_session.py b/gso/products/product_blocks/bgp_session.py
index ebf82d32de63d1d2893f2e5622a6e3b8a9e5f50c..09d578cf54294bb87e98762923152d2aea13ca1b 100644
--- a/gso/products/product_blocks/bgp_session.py
+++ b/gso/products/product_blocks/bgp_session.py
@@ -4,6 +4,7 @@ from ipaddress import IPv4Address, IPv6Address
 
 from orchestrator.domain.base import ProductBlockModel
 from orchestrator.types import SubscriptionLifecycle
+from pydantic import Field
 from pydantic_forms.types import strEnum
 
 
@@ -23,7 +24,7 @@ class BGPSessionInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INI
     bfd_enabled: bool | None = None
     bfd_interval: int | None = None
     bfd_multiplier: int | None = None
-    families: list[IPFamily] | None = None
+    families: list[IPFamily] = Field(default_factory=list)
     has_custom_policies: bool | None = None
     authentication_key: str | None = None
     multipath_enabled: bool | None = None
diff --git a/gso/products/product_blocks/geant_ip.py b/gso/products/product_blocks/geant_ip.py
new file mode 100644
index 0000000000000000000000000000000000000000..7b7213a38bc9eae161267dad94219063e6ed04ec
--- /dev/null
+++ b/gso/products/product_blocks/geant_ip.py
@@ -0,0 +1,61 @@
+"""Product blocks for :class:`GeantIP` products."""
+
+from orchestrator.domain.base import ProductBlockModel
+from orchestrator.types import SubscriptionLifecycle
+from pydantic import Field
+
+from gso.products.product_blocks.edge_port import EdgePortBlock, EdgePortBlockInactive, EdgePortBlockProvisioning
+from gso.products.product_blocks.service_binding_port import (
+    ServiceBindingPort,
+    ServiceBindingPortInactive,
+    ServiceBindingPortProvisioning,
+)
+from gso.utils.shared_enums import APType
+
+
+class NRENAccessPortInactive(
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="NRENAccessPort"
+):
+    """An access port for an R&E :term:`NREN` service that is inactive."""
+
+    nren_ap_sbp_list: list[ServiceBindingPortInactive] = Field(default_factory=list)
+    nren_ap_type: APType | None = None
+
+
+class NRENAccessPortProvisioning(NRENAccessPortInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """An access port for an R&E :term:`NREN` service that is being provisioned."""
+
+    nren_ap_sbp_list: list[ServiceBindingPortProvisioning]  # type: ignore[assignment]
+    nren_ap_type: APType
+
+
+class NRENAccessPort(NRENAccessPortProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An access port for an R&E :term:`NREN` service."""
+
+    nren_ap_sbp_list: list[ServiceBindingPort]  # type: ignore[assignment]
+    nren_ap_type: APType
+
+
+class GeantIPBlockInactive(
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="GeantIPBlock"
+):
+    """A GÉANT IP subscription that is currently inactive. See :class:`GeantIPBlock`."""
+
+    geant_ip_ap_list: list[NRENAccessPortInactive] = Field(default_factory=list)
+    geant_ip_ep_list: list[EdgePortBlockInactive] = Field(default_factory=list)
+
+
+class GeantIPBlockProvisioning(GeantIPBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A GÉANT IP subscription that is currently being provisioned. See :class:`GeantIPBlock`."""
+
+    geant_ip_ap_list: list[NRENAccessPortProvisioning]  # type: ignore[assignment]
+    geant_ip_ep_list: list[EdgePortBlockProvisioning]  # type: ignore[assignment]
+
+
+class GeantIPBlock(GeantIPBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """A GÉANT IP subscription block."""
+
+    #: The list of Access Points where this service is present.
+    geant_ip_ap_list: list[NRENAccessPort]  # type: ignore[assignment]
+    #: The list of Edge Ports where this service terminates.
+    geant_ip_ep_list: list[EdgePortBlock]  # type: ignore[assignment]
diff --git a/gso/products/product_blocks/service_binding_port.py b/gso/products/product_blocks/service_binding_port.py
index a38a5c6e79c1e2765b9adb70abe28da1d928b640..a88c973b13dc9e5e89f00679e2d132a5f73c8e5b 100644
--- a/gso/products/product_blocks/service_binding_port.py
+++ b/gso/products/product_blocks/service_binding_port.py
@@ -6,45 +6,42 @@ A service binding port is used to logically attach an edge port to a customer se
 from ipaddress import IPv4Address, IPv6Address
 from typing import Annotated
 
-from orchestrator.domain.base import ProductModel
+from orchestrator.domain.base import ProductBlockModel
 from orchestrator.types import SubscriptionLifecycle
 from pydantic import Field
-from pydantic_forms.types import strEnum
 
-VLANID = Annotated[int, Field(gt=0, lt=4096)]
+from gso.utils.shared_enums import SBPType
+from gso.utils.types.bgp_session import BGPSessionSet
 
-
-class SBPType(strEnum):
-    """Enumerator for the two allowed types of service binding port: layer 2 or layer 3."""
-
-    L2 = "l2"
-    L3 = "l3"
+VLAN_ID = Annotated[int, Field(gt=0, lt=4096)]
 
 
 class ServiceBindingPortInactive(
-    ProductModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="ServiceBindingPort"
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="ServiceBindingPort"
 ):
     """A Service Binding Port that's currently inactive. See :class:`ServiceBindingPort`."""
 
     is_tagged: bool | None = None
-    vlan_id: VLANID | None = None
+    vlan_id: VLAN_ID | None = None
     sbp_type: SBPType | None = None
     ipv4_address: IPv4Address | None = None
     ipv6_address: IPv6Address | None = None
     custom_firewall_filters: bool | None = None
     geant_sid: str | None = None
+    sbp_bgp_session_list: BGPSessionSet | None = None
 
 
 class ServiceBindingPortProvisioning(ServiceBindingPortInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     """A Service Binding Port that's currently being provisioned. See :class:`ServiceBindingPort`."""
 
     is_tagged: bool
-    vlan_id: VLANID | None = None
+    vlan_id: VLAN_ID | None = None
     sbp_type: SBPType
     ipv4_address: IPv4Address | None = None
     ipv6_address: IPv6Address | None = None
     custom_firewall_filters: bool
     geant_sid: str
+    sbp_bgp_session_list: BGPSessionSet
 
 
 class ServiceBindingPort(ServiceBindingPortProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
@@ -53,7 +50,7 @@ class ServiceBindingPort(ServiceBindingPortProvisioning, lifecycle=[Subscription
     #: Whether this :term:`VLAN` is tagged or not.
     is_tagged: bool
     #: The :term:`VLAN` ID.
-    vlan_id: VLANID | None = None
+    vlan_id: VLAN_ID | None = None
     #: Is this service binding port layer 2 or 3?
     sbp_type: SBPType
     #: If layer 3, IPv4 resources.
@@ -64,3 +61,5 @@ class ServiceBindingPort(ServiceBindingPortProvisioning, lifecycle=[Subscription
     custom_firewall_filters: bool
     #: The GÉANT service ID of this binding port.
     geant_sid: str
+    #: The :term:`BGP` sessions associated with this service binding port.
+    sbp_bgp_session_list: BGPSessionSet
diff --git a/gso/products/product_types/geant_ip.py b/gso/products/product_types/geant_ip.py
new file mode 100644
index 0000000000000000000000000000000000000000..064a01ea2d23676f975c820a40d07ea19766ea1b
--- /dev/null
+++ b/gso/products/product_types/geant_ip.py
@@ -0,0 +1,38 @@
+"""GÉANT IP product type."""
+
+from orchestrator.domain import SubscriptionModel
+from orchestrator.types import SubscriptionLifecycle
+
+from gso.products.product_blocks.geant_ip import GeantIPBlock, GeantIPBlockInactive, GeantIPBlockProvisioning
+
+
+class GeantIPInactive(SubscriptionModel, is_base=True):
+    """An inactive GÉANT IP subscription."""
+
+    geant_ip: GeantIPBlockInactive
+
+
+class GeantIPProvisioning(GeantIPInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    """A GÉANT IP subscription that's being provisioned."""
+
+    geant_ip: GeantIPBlockProvisioning
+
+
+class GeantIP(GeantIPProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    """An active GÉANT IP subscription."""
+
+    geant_ip: GeantIPBlock
+
+
+class ImportedGeantIPInactive(SubscriptionModel, is_base=True):
+    """An imported, inactive GÉANT IP subscription."""
+
+    geant_ip: GeantIPBlockInactive
+
+
+class ImportedGeantIP(
+    ImportedGeantIPInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE]
+):
+    """An imported GÉANT IP subscription."""
+
+    geant_ip: GeantIPBlock
diff --git a/gso/services/librenms_client.py b/gso/services/librenms_client.py
index b7c21f888ebe281ada67e64edb3e9fd671a0a655..b514f0009c814fcc0e698b07fbb8e86cb27e3150 100644
--- a/gso/services/librenms_client.py
+++ b/gso/services/librenms_client.py
@@ -10,7 +10,7 @@ from requests import HTTPError, Response
 from requests.adapters import HTTPAdapter
 
 from gso.settings import load_oss_params
-from gso.utils.types.snmp import SNMPVersion
+from gso.utils.shared_enums import SNMPVersion
 
 logger = logging.getLogger(__name__)
 
diff --git a/gso/utils/shared_enums.py b/gso/utils/shared_enums.py
index 929e5b0bc2c2553c90b36ce8f056c1f22fed74cc..a6050c9bc72de8f7554d41785b41c1e91d38a483 100644
--- a/gso/utils/shared_enums.py
+++ b/gso/utils/shared_enums.py
@@ -1,5 +1,7 @@
 """Shared choices for the different models."""
 
+from enum import StrEnum
+
 from pydantic_forms.types import strEnum
 
 
@@ -15,3 +17,25 @@ class ConnectionStrategy(strEnum):
 
     IN_BAND = "IN BAND"
     OUT_OF_BAND = "OUT OF BAND"
+
+
+class SNMPVersion(StrEnum):
+    """An enumerator for the two relevant versions of :term:`SNMP`: v2c and 3."""
+
+    V2C = "v2c"
+    V3 = "v3"
+
+
+class APType(strEnum):
+    """Enumerator of the types of Access Port."""
+
+    PRIMARY = "PRIMARY"
+    BACKUP = "BACKUP"
+    LOAD_BALANCED = "LOAD_BALANCED"
+
+
+class SBPType(strEnum):
+    """Enumerator for the two allowed types of service binding port: layer 2 or layer 3."""
+
+    L2 = "l2"
+    L3 = "l3"
diff --git a/gso/utils/types/bgp_session.py b/gso/utils/types/bgp_session.py
new file mode 100644
index 0000000000000000000000000000000000000000..681ab1c72690bdb392432cc8661df4401e900877
--- /dev/null
+++ b/gso/utils/types/bgp_session.py
@@ -0,0 +1,33 @@
+""":term:`BGP` session sets."""
+
+from typing import Annotated, TypeVar
+
+from annotated_types import Len
+from pydantic import AfterValidator
+
+from gso.products.product_blocks.bgp_session import BGPSession, BGPSessionInactive, BGPSessionProvisioning
+
+BGPSessionTypes = TypeVar("BGPSessionTypes", BGPSessionInactive, BGPSessionProvisioning, BGPSession)
+
+
+def validate_bgp_session_set(bgp_session_set: list[BGPSessionTypes]) -> list[BGPSessionTypes]:
+    """:term:`BGP` sessions grouped together.
+
+    It consists of either a single item, or a pair of IPv4 and v6 sessions. It is not allowed to have two IPv4 or IPv6
+    :term:`BGP` sessions as part of a set.
+    """
+    if any(bgp_session.peer_address is None for bgp_session in bgp_session_set):
+        msg = "BGP session is missing a peer address."
+        raise ValueError(msg)
+    if len(bgp_session_set) == 2 and bgp_session_set[0].peer_address.version == bgp_session_set[1].peer_address.version:  # type: ignore[union-attr] # noqa: PLR2004
+        msg = (
+            "When defining two separate BGP sessions, IP families must differ. "
+            f"Both are IPv{bgp_session_set[0].peer_address.version}."  # type: ignore[union-attr]
+        )
+        raise ValueError(msg)
+    return bgp_session_set
+
+
+BGPSessionSet = Annotated[
+    list[BGPSessionTypes], Len(min_length=1, max_length=2), AfterValidator(validate_bgp_session_set)
+]
diff --git a/gso/utils/types/snmp.py b/gso/utils/types/snmp.py
deleted file mode 100644
index 03581cf970c036db37ea901d8b159d25fc480136..0000000000000000000000000000000000000000
--- a/gso/utils/types/snmp.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""An enumerator of SNMP version numbers."""
-
-from enum import StrEnum
-
-
-class SNMPVersion(StrEnum):
-    """An enumerator for the two relevant versions of :term:`SNMP`: v2c and 3."""
-
-    V2C = "v2c"
-    V3 = "v3"
diff --git a/gso/workflows/router/update_ibgp_mesh.py b/gso/workflows/router/update_ibgp_mesh.py
index 298c0878a2f82449173c78d24604e82ae8eb5bb1..fc071ab8fc657cd2f5fb001a165d84175c0a513b 100644
--- a/gso/workflows/router/update_ibgp_mesh.py
+++ b/gso/workflows/router/update_ibgp_mesh.py
@@ -18,7 +18,7 @@ from gso.services import librenms_client
 from gso.services.lso_client import LSOState, lso_interaction
 from gso.services.subscriptions import get_trunks_that_terminate_on_router
 from gso.utils.helpers import generate_inventory_for_active_routers
-from gso.utils.types.snmp import SNMPVersion
+from gso.utils.shared_enums import SNMPVersion
 from gso.utils.types.tt_number import TTNumber
 from gso.utils.workflow_steps import (
     add_all_p_to_pe_dry,
diff --git a/test/services/test_librenms_client.py b/test/services/test_librenms_client.py
index c64b5933ba2f4b845fe34c288cd2be34c8655aca..18b922da43913430628ab869f63685f99fc7d0c6 100644
--- a/test/services/test_librenms_client.py
+++ b/test/services/test_librenms_client.py
@@ -5,7 +5,7 @@ import pytest
 from requests import HTTPError
 
 from gso.services.librenms_client import LibreNMSClient
-from gso.utils.types.snmp import SNMPVersion
+from gso.utils.shared_enums import SNMPVersion
 
 
 @pytest.fixture()