diff --git a/_modules/node.py b/_modules/node.py
index 28fc3c9..4ec9b53 100644
--- a/_modules/node.py
+++ b/_modules/node.py
@@ -1,261 +1,317 @@
 # -*- coding: utf-8 -*-
 
 #   -------------------------------------------------------------
 #   Salt — Node execution module
 #   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 #   Project:        Nasqueron
 #   Created:        2017-10-21
 #   Description:    Functions related to the nodes' pillar entry
 #   License:        BSD-2-Clause
 #   -------------------------------------------------------------
 
 
 from salt.exceptions import CommandExecutionError, SaltCloudConfigError
 from salt._compat import ipaddress
 
 
 def _get_all_nodes():
     return __pillar__.get("nodes", {})
 
 
 def get_all_properties(nodename=None):
     """
     A function to get a node pillar configuration.
 
     CLI Example:
 
         salt * node.get_all_properties
     """
     if nodename is None:
         nodename = __grains__["id"]
 
     all_nodes = _get_all_nodes()
 
     if nodename not in all_nodes:
         raise CommandExecutionError(
             SaltCloudConfigError("Node {0} not declared in pillar.".format(nodename))
         )
 
     return all_nodes[nodename]
 
 
 def get(key, nodename=None):
     """
     A function to get a node pillar configuration key.
 
     CLI Example:
 
         salt * node.get hostname
     """
     return _get_property(key, nodename, None)
 
 
 def _explode_key(k):
     return k.split(":")
 
 
 def _get_first_key(k):
     return _explode_key(k)[0]
 
 
 def _strip_first_key(k):
     return ":".join(_explode_key(k)[1:])
 
 
 def _get_property(key, nodename, default_value, parent=None):
     if parent is None:
         parent = get_all_properties(nodename)
 
     if ":" in key:
         first_key = _get_first_key(key)
         if first_key in parent:
             return _get_property(
                 _strip_first_key(key), nodename, default_value, parent[first_key]
             )
     elif key in parent:
         return parent[key]
 
     return default_value
 
 
 def get_list(key, nodename=None):
     """
     A function to get a node pillar configuration.
 
     Returns a list if found, or an empty list if not found.
 
     CLI Example:
 
         salt * node.list network:ipv4_aliases
     """
     return _get_property(key, nodename, [])
 
 
 def has(key, nodename=None):
     """
     A function to get a node pillar configuration.
 
     Returns a boolean, False if not found.
 
     CLI Example:
 
         salt * node.has network:ipv6_tunnel
     """
     value = _get_property(key, nodename, False)
     return bool(value)
 
 
 def has_role(role, nodename=None):
     """
     A function to determine if a node has the specified role.
 
     Returns a boolean, False if not found.
 
     CLI Example:
 
         salt * node.has_role devserver
     """
     return role in get_list("roles", nodename)
 
 
 def filter_by_role(pillar_key, nodename=None):
     """
     A function to filter a dictionary by roles.
 
     The dictionary must respect the following structure:
       - keys are role to check the current node against
       - values are list of items
 
     If a key '*' is also present, it will be included
     for every role.
 
     Returns a list, extending all the filtered lists.
 
     CLI Example:
 
         salt * node.filter_by_role web_content_sls
     """
     roles = get_list("roles", nodename)
     dictionary = __pillar__.get(pillar_key, {})
     filtered_list = []
 
     for role, items in dictionary.items():
         if role == "*" or role in roles:
             filtered_list.extend(items)
 
     return filtered_list
 
 
 def filter_by_name(pillar_key, nodename=None):
     """
     A function to filter a dictionary by node name.
 
     The dictionary must respect the following structure:
       - keys are names to check the current node against
       - values are list of items
 
     If a key '*' is also present, it will be included
     for every node.
 
     Returns a list, extending all the filtered lists.
 
     CLI Example:
 
         salt * node.filter_by_name mars
     """
     if nodename is None:
         nodename = __grains__["id"]
 
     dictionary = __pillar__.get(pillar_key, {})
     filtered_list = []
 
     for name, items in dictionary.items():
         if name == "*" or name == nodename:
             filtered_list.extend(items)
 
     return filtered_list
 
 
 def has_web_content(content, nodename=None):
     return content in filter_by_role("web_content_sls", nodename)
 
 
 def get_wwwroot(nodename=None):
     """
     A function to determine the wwwroot folder to use.
 
     Returns a string depending on the FQDN.
 
     CLI Example:
 
         salt * node.get_wwwroot
     """
     hostname = _get_property("hostname", nodename, None)
 
     if hostname is None:
         raise CommandExecutionError(
             SaltCloudConfigError(
                 "Node {0} doesn't have a hostname property".format(nodename)
             )
         )
 
     if hostname.count(".") < 2:
         return "wwwroot/{0}/www".format(hostname)
 
     fqdn = hostname.split(".")
     return "wwwroot/{1}/{0}".format(".".join(fqdn[0:-2]), ".".join(fqdn[-2:]))
 
 
 def get_ipv6_list():
     """
     A function to get a list of IPv6, enclosed by [].
 
     Returns a string depending on the IPv6 currently assigned.
 
     CLI Example:
 
         salt * node.get_ipv6_list
     """
     ipv6 = __grains__.get("ipv6")
 
     return " ".join(["[" + ip + "]" for ip in ipv6])
 
 
 def resolve_network():
     """
     A function to determine canonical properties of networks
     from the nodes pillar.
 
     CLI Example:
         salt * node.resolve_network
     """
     network = {
         "ipv4_address": "",
         "ipv4_gateway": "",
     }
     private_network = network.copy()
 
     interfaces = _get_property("network:interfaces", __grains__["id"], {})
     for interface_name, interface in interfaces.items():
         if "ipv4" not in interface:
             continue
 
         ipv4 = interface["ipv4"]["address"]
         if ipaddress.ip_address(ipv4).is_private:
             target = private_network
         else:
             target = network
 
         if target["ipv4_address"] != "":
             continue
 
         target["ipv4_address"] = ipv4
         try:
             target["ipv4_gateway"] = interface["ipv4"]["gateway"]
         except KeyError:
             pass
 
     if network["ipv4_address"] == "":
         return private_network
 
     return network
+
+
+def _resolve_gre_tunnels_for_router(network, netmask):
+    tunnels = []
+
+    for node, tunnel in __pillar__.get(f"{network}_gre_tunnels", {}).items():
+        tunnels.append(
+            {
+                "description": f"{network}_to_{node}",
+                "interface": tunnel["router"]["interface"],
+                "src": tunnel["router"]["addr"],
+                "dst": tunnel["node"]["addr"],
+                "netmask": netmask,
+                "icann_src": get("network")["canonical_public_ipv4"],
+                "icann_dst": get("network", node)["canonical_public_ipv4"],
+            }
+        )
+
+    return tunnels
+
+
+def resolve_gre_tunnels():
+    """
+    A function to get the GRE tunnels for a node
+
+    CLI Example:
+        salt * node.resolve_gre_tunnels
+    """
+    gre_tunnels = []
+
+    for network, network_args in __pillar__.get("networks", {}).items():
+        if __grains__["id"] == network_args["router"]:
+            gre_tunnels += _resolve_gre_tunnels_for_router(
+                network, network_args["netmask"]
+            )
+            continue
+
+        tunnel = __salt__["pillar.get"](f"{network}_gre_tunnels:{__grains__['id']}")
+        if not tunnel:
+            continue
+
+        gre_tunnels.append(
+            {
+                "description": f"{network}_via_{network_args['router']}",
+                "interface": tunnel["node"].get("interface", "gre0"),
+                "src": tunnel["node"]["addr"],
+                "dst": tunnel["router"]["addr"],
+                "netmask": network_args["netmask"],
+                "icann_src": get("network")["canonical_public_ipv4"],
+                "icann_dst": get("network", network_args["router"])[
+                    "canonical_public_ipv4"
+                ],
+            }
+        )
+
+    return gre_tunnels
diff --git a/pillar/core/network.sls b/pillar/core/network.sls
index 8afb63c..ab5e9b8 100644
--- a/pillar/core/network.sls
+++ b/pillar/core/network.sls
@@ -1,29 +1,49 @@
 networks:
   drake:
     netmask: 255.255.255.0
-    addr:
-      cloudhugger: 172.27.27.28
-      router-001: 172.27.27.1
-      windriver: 172.27.27.35
-      ysul: 172.27.27.33
+    router: router-001
 
-gre_tunnels:
-  windriver:
-    wind-cloud:
-      interface: gre0
-      network: drake
-      to: cloudhugger
-
-    wind-ysul:
-      interface: gre0
-      network: drake
-      to: ysul
+#   -------------------------------------------------------------
+#   Drake - GRE tunnels
+#
+#   Describe GRE tunnels between a node and {networks.drake.router}
+#
+#   {id}:
+#   router:
+#       interface: gre0, gre1, ... / increment for each tunnel
+#       addr:      The tunnel IPv4 on the router in 172.27.27.240/28
+#
+#     node:
+#       interface: Not needed on Linux as the interface name will be
+#                  descriptive: gre-drake_via_{networks.drake.router}
+#
+#                  Not needed on FreeBSD if default value "gre0" is fine.
+#                  If there is several GRE tunnels, can be gre1, gre2, etc.
+#
+#       addr:      The canonical IPv4 for the server in 172.27.27.0/24
+#
+#   IP should be sync between the pillar and the Operations grimoire
+#   at https://agora.nasqueron.org/Operations_grimoire/Network
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
+drake_gre_tunnels:
   ysul:
-    wind-ysul: &gre_drake_to_windriver
+    router:
       interface: gre0
-      network: drake
-      to: windriver
+      addr: 172.27.27.252
+    node:
+      addr: 172.27.27.33
 
   cloudhugger:
-    wind-cloud: *gre_drake_to_windriver
+    router:
+      interface: gre1
+      addr: 172.27.27.253
+    node:
+      addr: 172.27.27.28
+
+  windriver:
+    router:
+      interface: gre2
+      addr: 172.27.27.254
+    node:
+      addr: 172.27.27.35
diff --git a/roles/core/network/gre.sls b/roles/core/network/gre.sls
index 0a6dd6f..2738cad 100644
--- a/roles/core/network/gre.sls
+++ b/roles/core/network/gre.sls
@@ -1,79 +1,54 @@
 #   -------------------------------------------------------------
 #   Salt — Network — GRE tunnels
 #   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 #   Project:        Nasqueron
 #   Created:        2020-09-20
 #   License:        Trivial work, not eligible to copyright
 #   -------------------------------------------------------------
 
-{% set network = salt['node.get']('network') %}
-{% set gre_tunnels = salt['pillar.get']("gre_tunnels:" + grains['id'], {}) %}
+{% from "roles/core/network/map.jinja" import gre with context %}
 {% set boot_loader = namespace(gre=false) %}
 
 #   -------------------------------------------------------------
 #   Tunnels network configuration files
 #   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
-{% for description, tunnel in gre_tunnels.items() %}
+{% for tunnel in salt['node.resolve_gre_tunnels']() %}
 
 {% set boot_loader.gre = True %}
-{% set tunnel_network = pillar['networks'][tunnel['network']] %}
 
-{% if grains['os'] == 'FreeBSD' %}
-/etc/rc.conf.d/netif/gre_{{ description }}:
+{{ gre.config_path }}{{ tunnel["description"] }}:
   file.managed:
-    - source: salt://roles/core/network/files/FreeBSD/netif_gre.rc
+    - source: salt://roles/core/network/files/{{ gre.source_path }}
     - makedirs: True
     - template: jinja
-    - context:
-        description: {{ description }}
-        interface: {{ tunnel['interface'] }}
-
-        src: {{ tunnel_network['addr'][grains['id']] }}
-        dst: {{ tunnel_network['addr'][tunnel['to']] }}
-
-        icann_src: {{ network['ipv4_address'] }}
-        icann_dst: {{ salt['node.get']('network', tunnel['to'])['ipv4_address'] }}
-{% endif %}
-
+    - defaults: {{ tunnel }}
 {% if grains['os_family'] == 'Debian' %}
-/etc/network/interfaces.d/10-gre-{{ description }}:
-  file.managed:
-    - source: salt://roles/core/network/files/Debian/10-gre.jinja
-    - makedirs: True
-    - template: jinja
     - context:
         interface: gre-{{ description }}
-
-        src: {{ tunnel_network['addr'][grains['id']] }}
-        dst: {{ tunnel_network['addr'][tunnel['to']] }}
-        netmask: {{ tunnel_network['netmask'] }}
-
-        icann_src: {{ network['ipv4_address'] }}
-        icann_dst: {{ salt['node.get']('network', tunnel['to'])['ipv4_address'] }}
 {% endif %}
 
 {% endfor %}
 
 #   -------------------------------------------------------------
 #   Kernel configuration
 #   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
 {% if boot_loader.gre %}
 
 {% if grains['os'] == 'FreeBSD' %}
 load_gre_kernel_module:
   file.append:
     - name: /boot/loader.conf
     - text: |
 
         if_gre_load="YES"
 {% endif %}
 
 {% if grains['os_family'] == 'Debian' %}
 ip_gre:
   kmod.present:
     - persist: True
 {% endif %}
 
 {% endif %}
diff --git a/roles/core/network/map.jinja b/roles/core/network/map.jinja
index 6cb6f46..5961208 100644
--- a/roles/core/network/map.jinja
+++ b/roles/core/network/map.jinja
@@ -1,34 +1,51 @@
 #   -------------------------------------------------------------
 #   Salt — Network
 #   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 #   Project:        Nasqueron
 #   License:        Trivial work, not eligible to copyright
 #   -------------------------------------------------------------
 
 #   -------------------------------------------------------------
 #   Interface configuration by OS/distro
 #
 #   config_path:   the configuration file to write in OS
 #   source_path:   in this repo, roles/core/network/files/<source_path>
 #
 #   Don't set default value, so we MUST define them
 #   for EACH os/distro.
 #   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
 {% set interface_config = salt['grains.filter_by']({
   'FreeBSD': {
     "config_path": "/etc/rc.conf.d/netif/ipv4_",
     "source_path": "FreeBSD/netif_ipv4.rc",
     "suffix": "interface",
   },
   'RedHat': {
     "config_path": "/etc/sysconfig/network-scripts/ifcfg-",
     "source_path": "RedHat/ifcfg",
     "suffix": "device",
   },
   'Debian': {
     "config_path": "/etc/network/interfaces.d/10-net-",
     "source_path": "Debian/10-net.jinja",
     "suffix": "device",
   },
 }) %}
+
+#   -------------------------------------------------------------
+#   GRE tunnels configuration by OS/distro
+#
+#   See interface configuration for the documentation.
+#   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+{% set gre = salt['grains.filter_by']({
+  'FreeBSD': {
+    "config_path": "/etc/rc.conf.d/netif/gre_",
+    "source_path": "FreeBSD/netif_gre.rc",
+  },
+  'Debian': {
+    "config_path": "/etc/network/interfaces.d/10-gre-",
+    "source_path": "Debian/10-gre.jinja",
+  },
+}) %}