gtphub: refactor: use side_idx everywhere.

This is a mostly cosmetic change. Instead of separate buffer handling
functions, reduce some code duplication by using a side_idx just like the
plane_idx, with arrays.

Sponsored-by: On-Waves ehi
diff --git a/openbsc/include/openbsc/gtphub.h b/openbsc/include/openbsc/gtphub.h
index a142867..005c3f8 100644
--- a/openbsc/include/openbsc/gtphub.h
+++ b/openbsc/include/openbsc/gtphub.h
@@ -136,8 +136,8 @@
 };
 
 enum gtphub_side_idx {
-	GTPH_SIDE_GGSN = 0,
-	GTPH_SIDE_SGSN = 1,
+	GTPH_SIDE_SGSN = 0,
+	GTPH_SIDE_GGSN = 1,
 	GTPH_SIDE_N
 };
 
@@ -153,6 +153,8 @@
 extern const char* const gtphub_plane_idx_names[GTPH_PLANE_N];
 extern const uint16_t gtphub_plane_idx_default_port[GTPH_PLANE_N];
 
+extern const char* const gtphub_side_idx_names[GTPH_SIDE_N];
+
 /* A host address in the form that is expected in the 7.7.32 GSN Address IE.
  * len is either 4 (IPv4) or 16 (IPv6), any other value is invalid. If no
  * address is set, len shall be 0. */
@@ -357,10 +359,8 @@
 };
 
 struct gtphub_cfg {
-	struct gtphub_cfg_bind to_sgsns[GTPH_PLANE_N];
-	struct gtphub_cfg_bind to_ggsns[GTPH_PLANE_N];
-	struct gtphub_cfg_addr sgsn_proxy[GTPH_PLANE_N];
-	struct gtphub_cfg_addr ggsn_proxy[GTPH_PLANE_N];
+	struct gtphub_cfg_bind to_gsns[GTPH_SIDE_N][GTPH_PLANE_N];
+	struct gtphub_cfg_addr proxy[GTPH_SIDE_N][GTPH_PLANE_N];
 };
 
 
@@ -429,14 +429,10 @@
 };
 
 struct gtphub {
-	struct gtphub_bind to_sgsns[GTPH_PLANE_N];
-	struct gtphub_bind to_ggsns[GTPH_PLANE_N];
+	struct gtphub_bind to_gsns[GTPH_SIDE_N][GTPH_PLANE_N];
 
-	/* pointers to an entry of to_sgsns[x].peers */
-	struct gtphub_peer_port *sgsn_proxy[GTPH_PLANE_N];
-
-	/* pointers to an entry of to_ggsns[x].peers */
-	struct gtphub_peer_port *ggsn_proxy[GTPH_PLANE_N];
+	/* pointers to an entry of to_gsns[s][p].peers */
+	struct gtphub_peer_port *proxy[GTPH_SIDE_N][GTPH_PLANE_N];
 
 	/* The TEI numbers will simply wrap and be reused, which will work out
 	 * in practice. Problems would arise if one given peer maintained the
@@ -490,25 +486,16 @@
 /* Return 1 if all of tun's endpoints are fully established, 0 otherwise. */
 int gtphub_tunnel_complete(struct gtphub_tunnel *tun);
 
-int gtphub_from_sgsns_handle_buf(struct gtphub *hub,
-				 unsigned int port_idx,
-				 const struct osmo_sockaddr *from_addr,
-				 uint8_t *buf,
-				 size_t received,
-				 time_t now,
-				 uint8_t **reply_buf,
-				 struct osmo_fd **to_ofd,
-				 struct osmo_sockaddr *to_addr);
-
-int gtphub_from_ggsns_handle_buf(struct gtphub *hub,
-				 unsigned int port_idx,
-				 const struct osmo_sockaddr *from_addr,
-				 uint8_t *buf,
-				 size_t received,
-				 time_t now,
-				 uint8_t **reply_buf,
-				 struct osmo_fd **to_ofd,
-				 struct osmo_sockaddr *to_addr);
+int gtphub_handle_buf(struct gtphub *hub,
+		      unsigned int side_idx,
+		      unsigned int port_idx,
+		      const struct osmo_sockaddr *from_addr,
+		      uint8_t *buf,
+		      size_t received,
+		      time_t now,
+		      uint8_t **reply_buf,
+		      struct osmo_fd **to_ofd,
+		      struct osmo_sockaddr *to_addr);
 
 struct gtphub_peer_port *gtphub_port_have(struct gtphub *hub,
 					  struct gtphub_bind *bind,
diff --git a/openbsc/src/gprs/gtphub.c b/openbsc/src/gprs/gtphub.c
index 6037d3b..110438c 100644
--- a/openbsc/src/gprs/gtphub.c
+++ b/openbsc/src/gprs/gtphub.c
@@ -746,6 +746,11 @@
 	2152,
 };
 
+const char* const gtphub_side_idx_names[GTPH_SIDE_N] = {
+	"SGSN",
+	"GGSN",
+};
+
 time_t gtphub_now(void)
 {
 	struct timespec now_tp;
@@ -1402,56 +1407,52 @@
 	return 0;
 }
 
-/* Read GSN address IEs from p, and make sure these peer addresses exist in
- * bind[plane_idx] with default ports, in their respective planes (both Ctrl
- * and User). Map TEIs announced in IEs, and write mapped TEIs in-place into
- * the packet p. */
-static int gtphub_handle_pdp_ctx_ies(struct gtphub *hub,
-				     struct gtphub_bind from_bind_arr[],
-				     struct gtphub_bind to_bind_arr[],
-				     struct gtphub_peer_port *sgsn_ctrl,
-				     struct gtphub_peer_port *ggsn_ctrl,
-				     struct gtp_packet_desc *p)
+static int gtphub_handle_create_pdp_ctx(struct gtphub *hub,
+					struct gtp_packet_desc *p,
+					struct gtphub_peer_port *from_ctrl,
+					struct gtphub_peer_port *to_ctrl)
 {
-	OSMO_ASSERT(p->plane_idx == GTPH_PLANE_CTRL);
-
 	int rc;
 	int plane_idx;
 
-	switch (p->type) {
-	case GTP_CREATE_PDP_REQ:
-	case GTP_CREATE_PDP_RSP:
-		/* Go for it below */
-		break;
-	default:
-		/* Nothing to do for this message type. */
-		return 0;
-	}
-
 	/* TODO enforce a Request only from SGSN, a Response only from GGSN? */
-
 	osmo_static_assert((GTPH_PLANE_CTRL == 0) && (GTPH_PLANE_USER == 1),
 			   plane_nrs_match_GSN_addr_IE_indices);
 
 	struct gtphub_tunnel *tun = NULL;
 
 	if (p->type == GTP_CREATE_PDP_REQ) {
+		if (p->side_idx != GTPH_SIDE_SGSN) {
+			LOG(LOGL_ERROR, "Wrong side: Create PDP Context"
+			    " Request from the GGSN side");
+			return -1;
+		}
+
 		/* A new tunnel. */
 		tun = gtphub_tunnel_new();
 		llist_add(&tun->entry, &hub->tunnels);
 		gtphub_tunnel_refresh(hub, tun, p->timestamp);
+		/* The endpoint peers on this side (SGSN) will be set from IEs
+		 * below. Also set the GGSN Ctrl endpoint, for logging. */
 		gtphub_tunnel_endpoint_set_peer(&tun->endpoint[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL],
-						ggsn_ctrl);
+						to_ctrl);
 	} else if (p->type == GTP_CREATE_PDP_RSP) {
-		/* Find the tunnel created during request */
-		OSMO_ASSERT(sgsn_ctrl);
+		if (p->side_idx != GTPH_SIDE_GGSN) {
+			LOG(LOGL_ERROR, "Wrong side: Create PDP Context"
+			    " Response from the SGSN side");
+			return -1;
+		}
+
+		/* Find the tunnel created during request. This is coming from
+		 * the GGSN side, so to_ctrl corresponds to the SGSN. */
+		OSMO_ASSERT(to_ctrl);
 		tun = gtphub_tun_find(hub, other_side_idx(p->side_idx),
-				      p->plane_idx, sgsn_ctrl, 0, p->header_tei_rx);
+				      p->plane_idx, to_ctrl, 0, p->header_tei_rx);
 
 		if (!tun) {
 			LOG(LOGL_ERROR, "Create PDP Context Response: cannot"
 			    " find matching request from SGSN %s, mapped TEI %x.\n",
-			    gtphub_port_str(sgsn_ctrl), p->header_tei_rx);
+			    gtphub_port_str(to_ctrl), p->header_tei_rx);
 			return -1;
 		}
 	}
@@ -1496,7 +1497,7 @@
 		/* Make sure an entry for this peer address with default port
 		 * exists */
 		struct gtphub_peer_port *peer_from_ie =
-			gtphub_port_have(hub, &from_bind_arr[plane_idx],
+			gtphub_port_have(hub, &hub->to_gsns[side_idx][plane_idx],
 					 &addr_from_ie,
 					 gtphub_plane_idx_default_port[plane_idx]);
 
@@ -1516,8 +1517,8 @@
 		}
 
 		/* Replace the GSN address to reflect gtphub. */
-		rc = gsn_addr_put(&to_bind_arr[plane_idx].local_addr, p,
-				  plane_idx);
+		rc = gsn_addr_put(&hub->to_gsns[other_side_idx(side_idx)][plane_idx].local_addr,
+				  p, plane_idx);
 		if (rc) {
 			LOG(LOGL_ERROR, "Cannot write %s GSN Address IE\n",
 			    gtphub_plane_idx_names[plane_idx]);
@@ -1536,6 +1537,30 @@
 	return 0;
 }
 
+/* Read GSN address IEs from p, and make sure these peer addresses exist in
+ * bind[plane_idx] with default ports, in their respective planes (both Ctrl
+ * and User). Map TEIs announced in IEs, and write mapped TEIs in-place into
+ * the packet p. */
+static int gtphub_handle_pdp_ctx(struct gtphub *hub,
+				 struct gtp_packet_desc *p,
+				 struct gtphub_peer_port *from_ctrl,
+				 struct gtphub_peer_port *to_ctrl)
+{
+	OSMO_ASSERT(p->plane_idx == GTPH_PLANE_CTRL);
+
+	switch (p->type) {
+	case GTP_CREATE_PDP_REQ:
+	case GTP_CREATE_PDP_RSP:
+		return gtphub_handle_create_pdp_ctx(hub, p,
+						    from_ctrl, to_ctrl);
+	default:
+		/* Nothing to do for this message type. */
+		return 0;
+	}
+
+}
+
+
 static int gtphub_write(const struct osmo_fd *to,
 			const struct osmo_sockaddr *to_addr,
 			const uint8_t *buf, size_t buf_len)
@@ -1582,9 +1607,9 @@
 	if (len < 1)
 		return 0;
 
-	len = gtphub_from_ggsns_handle_buf(hub, plane_idx, &from_addr, buf, len,
-					   gtphub_now(),
-					   &reply_buf, &to_ofd, &to_addr);
+	len = gtphub_handle_buf(hub, GTPH_SIDE_GGSN, plane_idx, &from_addr,
+				buf, len, gtphub_now(),
+				&reply_buf, &to_ofd, &to_addr);
 	if (len < 1)
 		return 0;
 
@@ -1705,29 +1730,34 @@
  * sockaddr to *to_addr. The reason for this is that the sockaddr may expire at
  * any moment, while the osmo_fd is guaranteed to persist. Return the number of
  * bytes to forward, 0 or less on failure. */
-int gtphub_from_ggsns_handle_buf(struct gtphub *hub,
-				 unsigned int plane_idx,
-				 const struct osmo_sockaddr *from_addr,
-				 uint8_t *buf,
-				 size_t received,
-				 time_t now,
-				 uint8_t **reply_buf,
-				 struct osmo_fd **to_ofd,
-				 struct osmo_sockaddr *to_addr)
+int gtphub_handle_buf(struct gtphub *hub,
+		      unsigned int side_idx,
+		      unsigned int plane_idx,
+		      const struct osmo_sockaddr *from_addr,
+		      uint8_t *buf,
+		      size_t received,
+		      time_t now,
+		      uint8_t **reply_buf,
+		      struct osmo_fd **to_ofd,
+		      struct osmo_sockaddr *to_addr)
 {
+	/*
 	struct gtphub_bind *from_bind_arr = hub->to_ggsns;
 	struct gtphub_bind *to_bind_arr = hub->to_sgsns;
-	struct gtphub_bind *from_bind = &from_bind_arr[plane_idx];
-	struct gtphub_bind *to_bind = &to_bind_arr[plane_idx];
+	*/
+	struct gtphub_bind *from_bind = &hub->to_gsns[side_idx][plane_idx];
+	struct gtphub_bind *to_bind = &hub->to_gsns[other_side_idx(side_idx)][plane_idx];
 
 	rate_ctr_add(&from_bind->counters_io->ctr[GTPH_CTR_BYTES_IN],
 		     received);
-	LOG(LOGL_DEBUG, "<- rx %s from GGSN %s\n",
+	LOG(LOGL_DEBUG, "%s rx %s from %s %s\n",
+	    (side_idx == GTPH_SIDE_GGSN)? "<-" : "->",
 	    gtphub_plane_idx_names[plane_idx],
+	    gtphub_side_idx_names[side_idx],
 	    osmo_sockaddr_to_str(from_addr));
 
 	static struct gtp_packet_desc p;
-	gtp_decode(buf, received, GTPH_SIDE_GGSN, plane_idx, &p, now);
+	gtp_decode(buf, received, side_idx, plane_idx, &p, now);
 
 	if (p.rc <= 0)
 		return -1;
@@ -1744,7 +1774,9 @@
 		rate_ctr_inc(&from_bind->counters_io->ctr[GTPH_CTR_PKTS_OUT]);
 		rate_ctr_add(&from_bind->counters_io->ctr[GTPH_CTR_BYTES_OUT],
 			     reply_len);
-		LOG(LOGL_DEBUG, "--> Echo response to GGSN: %d bytes to %s\n",
+		LOG(LOGL_DEBUG, "%s Echo response to %s: %d bytes to %s\n",
+		    (side_idx == GTPH_SIDE_GGSN)? "-->" : "<--",
+		    gtphub_side_idx_names[side_idx],
 		    (int)reply_len, osmo_sockaddr_to_str(to_addr));
 		return reply_len;
 	}
@@ -1756,51 +1788,88 @@
 	/* If a GGSN proxy is configured, check that it's indeed that proxy
 	 * talking to us. A proxy is a forced 1:1 connection, e.g. to another
 	 * gtphub, so no-one else is allowed to talk to us from that side. */
-	struct gtphub_peer_port *ggsn = hub->ggsn_proxy[plane_idx];
-	if (ggsn) {
-		if (osmo_sockaddr_cmp(&ggsn->sa, from_addr) != 0) {
+	struct gtphub_peer_port *from_peer = hub->proxy[side_idx][plane_idx];
+	if (from_peer) {
+		if (osmo_sockaddr_cmp(&from_peer->sa, from_addr) != 0) {
 			LOG(LOGL_ERROR,
-			    "Rejecting: GGSN proxy configured, but GTP packet"
-			    " received on GGSN bind is from another sender:"
+			    "Rejecting: %s proxy configured, but GTP packet"
+			    " received on %s bind is from another sender:"
 			    " proxy: %s  sender: %s\n",
-			    gtphub_port_str(ggsn),
+			    gtphub_side_idx_names[side_idx],
+			    gtphub_side_idx_names[side_idx],
+			    gtphub_port_str(from_peer),
 			    osmo_sockaddr_to_str(from_addr));
 			return -1;
 		}
 	}
 
-	if (!ggsn) {
-		/* Find a GGSN peer with a matching address. The sender's port
-		 * may in fact differ. */
-		ggsn = gtphub_known_addr_have_port(from_bind, from_addr);
+	if (!from_peer) {
+		/* Find or create a peer with a matching address. The sender's
+		 * port may in fact differ. */
+		from_peer = gtphub_known_addr_have_port(from_bind, from_addr);
 	}
 
 	/* If any PDP context has been created, we already have an entry for
 	 * this GGSN. If we don't have an entry, the GGSN has nothing to tell
 	 * us about. */
-	if (!ggsn) {
-		LOG(LOGL_ERROR, "Dropping packet: unknown GGSN peer: %s\n",
+	if (!from_peer) {
+		if (side_idx == GTPH_SIDE_GGSN) {
+			LOG(LOGL_ERROR, "Dropping packet: unknown GGSN peer: %s\n",
+			    osmo_sockaddr_to_str(from_addr));
+			return -1;
+		} else {
+			/* SGSN */
+			/* A new peer. If this is on the Ctrl plane, an SGSN
+			 * may make first contact without being known yet, so
+			 * create the peer struct for the current sender. */
+			if (plane_idx != GTPH_PLANE_CTRL) {
+				LOG(LOGL_ERROR,
+				    "Dropping packet: User plane peer was not"
+				    "announced by PDP Context: %s\n",
+				    osmo_sockaddr_to_str(from_addr));
+				return -1;
+			}
+
+			struct gsn_addr from_gsna;
+			uint16_t from_port;
+			if (gsn_addr_from_sockaddr(&from_gsna, &from_port, from_addr) != 0)
+				return -1;
+
+			from_peer = gtphub_port_have(hub, from_bind, &from_gsna, from_port);
+		}
+	}
+
+	if (!from_peer) {
+		/* This could theoretically happen for invalid address data or
+		 * somesuch. */
+		LOG(LOGL_ERROR, "Dropping packet: invalid %s peer: %s\n",
+		    gtphub_side_idx_names[side_idx],
 		    osmo_sockaddr_to_str(from_addr));
 		return -1;
 	}
 
-	LOG(LOGL_DEBUG, "GGSN peer: %s\n", gtphub_port_str(ggsn));
+	LOG(LOGL_DEBUG, "from %s peer: %s\n", gtphub_side_idx_names[side_idx],
+	    gtphub_port_str(from_peer));
 
-	struct gtphub_peer_port *sgsn_from_seq;
-	struct gtphub_peer_port *sgsn;
-	if (gtphub_unmap(hub, &p, ggsn,
-			 hub->sgsn_proxy[plane_idx],
-			 &sgsn, &sgsn_from_seq,
-			 NULL /* not interested, got it in &sgsn already */
+	struct gtphub_peer_port *to_peer_from_seq;
+	struct gtphub_peer_port *to_peer;
+	if (gtphub_unmap(hub, &p, from_peer,
+			 hub->proxy[other_side_idx(side_idx)][plane_idx],
+			 &to_peer, &to_peer_from_seq,
+			 NULL /* not interested, got it in &to_peer already */
 			)
 	    != 0) {
 		return -1;
 	}
 
-	if (!sgsn) {
-		/* A GGSN initiated request would go to a known TEI. So this is
-		 * bogus. */
-		LOG(LOGL_ERROR, "No SGSN to send to. Dropping packet.\n");
+	if ((!to_peer) && (side_idx == GTPH_SIDE_SGSN)) {
+		if (gtphub_resolve_ggsn(hub, &p, &to_peer) < 0)
+			return -1;
+	}
+
+	if (!to_peer) {
+		LOG(LOGL_ERROR, "No %s to send to. Dropping packet.\n",
+		    gtphub_side_idx_names[other_side_idx(side_idx)]);
 		return -1;
 	}
 
@@ -1808,22 +1877,21 @@
 		/* This may be a Create PDP Context response. If it is, there
 		 * are other addresses in the GTP message to set up apart from
 		 * the sender. */
-		if (gtphub_handle_pdp_ctx_ies(hub, from_bind_arr, to_bind_arr,
-					      sgsn, ggsn, &p)
+		if (gtphub_handle_pdp_ctx(hub, &p, from_peer, to_peer)
 		    != 0)
 			return -1;
 	}
 
-	gtphub_check_restart_counter(hub, &p, ggsn);
-	gtphub_map_restart_counter(hub, &p, ggsn, sgsn);
+	gtphub_check_restart_counter(hub, &p, from_peer);
+	gtphub_map_restart_counter(hub, &p, from_peer, to_peer);
 
 	/* If the GGSN is replying to an SGSN request, the sequence nr has
-	 * already been unmapped above (sgsn_from_seq != NULL), and we need not
+	 * already been unmapped above (to_peer_from_seq != NULL), and we need not
 	 * create a new mapping. */
-	if (!sgsn_from_seq)
-		gtphub_map_seq(&p, ggsn, sgsn);
+	if (!to_peer_from_seq)
+		gtphub_map_seq(&p, from_peer, to_peer);
 
-	osmo_sockaddr_copy(to_addr, &sgsn->sa);
+	osmo_sockaddr_copy(to_addr, &to_peer->sa);
 
 	*reply_buf = (uint8_t*)p.data;
 
@@ -1832,7 +1900,9 @@
 		rate_ctr_add(&to_bind->counters_io->ctr[GTPH_CTR_BYTES_OUT],
 			     received);
 	}
-	LOG(LOGL_DEBUG, "<-- Forward to SGSN: %d bytes to %s\n",
+	LOG(LOGL_DEBUG, "%s Forward to %s: %d bytes to %s\n",
+	    (side_idx == GTPH_SIDE_SGSN)? "-->" : "<--",
+	    gtphub_side_idx_names[other_side_idx(side_idx)],
 	    (int)received, osmo_sockaddr_to_str(to_addr));
 	return received;
 }
@@ -1860,169 +1930,15 @@
 	if (len < 1)
 		return 0;
 
-	len = gtphub_from_sgsns_handle_buf(hub, plane_idx, &from_addr, buf, len,
-					   gtphub_now(),
-					   &reply_buf, &to_ofd, &to_addr);
+	len = gtphub_handle_buf(hub, GTPH_SIDE_SGSN, plane_idx, &from_addr,
+				buf, len, gtphub_now(),
+				&reply_buf, &to_ofd, &to_addr);
 	if (len < 1)
 		return 0;
 
 	return gtphub_write(to_ofd, &to_addr, reply_buf, len);
 }
 
-/* Analogous to gtphub_from_ggsns_handle_buf(), see the comment there. */
-int gtphub_from_sgsns_handle_buf(struct gtphub *hub,
-				 unsigned int plane_idx,
-				 const struct osmo_sockaddr *from_addr,
-				 uint8_t *buf,
-				 size_t received,
-				 time_t now,
-				 uint8_t **reply_buf,
-				 struct osmo_fd **to_ofd,
-				 struct osmo_sockaddr *to_addr)
-{
-	struct gtphub_bind *from_bind_arr = hub->to_sgsns;
-	struct gtphub_bind *to_bind_arr = hub->to_ggsns;
-	struct gtphub_bind *from_bind = &from_bind_arr[plane_idx];
-	struct gtphub_bind *to_bind = &to_bind_arr[plane_idx];
-
-	rate_ctr_add(&from_bind->counters_io->ctr[GTPH_CTR_BYTES_IN],
-		     received);
-	LOG(LOGL_DEBUG, "-> rx %s from SGSN %s\n",
-	    gtphub_plane_idx_names[plane_idx],
-	    osmo_sockaddr_to_str(from_addr));
-
-	static struct gtp_packet_desc p;
-	gtp_decode(buf, received, GTPH_SIDE_SGSN, plane_idx, &p, now);
-
-	if (p.rc <= 0)
-		return -1;
-
-	rate_ctr_inc(&from_bind->counters_io->ctr[GTPH_CTR_PKTS_IN]);
-
-	int reply_len;
-	reply_len = gtphub_handle_echo(hub, &p, reply_buf);
-	if (reply_len > 0) {
-		/* It was an echo. Nothing left to do. */
-		osmo_sockaddr_copy(to_addr, from_addr);
-		*to_ofd = &from_bind->ofd;
-
-		rate_ctr_inc(&from_bind->counters_io->ctr[GTPH_CTR_PKTS_OUT]);
-		rate_ctr_add(&from_bind->counters_io->ctr[GTPH_CTR_BYTES_OUT],
-			     reply_len);
-		LOG(LOGL_DEBUG, "<-- Echo response to SGSN: %d bytes to %s\n",
-		    (int)reply_len, osmo_sockaddr_to_str(to_addr));
-		return reply_len;
-	}
-	if (reply_len < 0)
-		return -1;
-
-	*to_ofd = &to_bind->ofd;
-
-	/* If an SGSN proxy is configured, check that it's indeed that proxy
-	 * talking to us. A proxy is a forced 1:1 connection, e.g. to another
-	 * gtphub, so no-one else is allowed to talk to us from that side. */
-	struct gtphub_peer_port *sgsn = hub->sgsn_proxy[plane_idx];
-	if (sgsn) {
-		if (osmo_sockaddr_cmp(&sgsn->sa, from_addr) != 0) {
-			LOG(LOGL_ERROR,
-			    "Rejecting: GGSN proxy configured, but GTP packet"
-			    " received on GGSN bind is from another sender:"
-			    " proxy: %s  sender: %s\n",
-			    gtphub_port_str(sgsn),
-			    osmo_sockaddr_to_str(from_addr));
-			return -1;
-		}
-	}
-
-	if (!sgsn) {
-		/* If any contact has been made before, we already have an
-		 * entry for this SGSN. The port may differ. */
-		sgsn = gtphub_known_addr_have_port(from_bind, from_addr);
-	}
-
-	if (!sgsn) {
-		/* A new peer. If this is on the Ctrl plane, an SGSN may make
-		 * first contact without being known yet, so create the peer
-		 * struct for the current sender. */
-		if (plane_idx != GTPH_PLANE_CTRL) {
-			LOG(LOGL_ERROR,
-			    "User plane peer was not announced by PDP Context,"
-			    " discarding: %s\n",
-			    osmo_sockaddr_to_str(from_addr));
-			return -1;
-		}
-
-		struct gsn_addr from_gsna;
-		uint16_t from_port;
-		if (gsn_addr_from_sockaddr(&from_gsna, &from_port, from_addr) != 0)
-			return -1;
-
-		sgsn = gtphub_port_have(hub, from_bind, &from_gsna, from_port);
-	}
-
-	if (!sgsn) {
-		/* This could theoretically happen for invalid address data or
-		 * somesuch. */
-		LOG(LOGL_ERROR, "Dropping packet: invalid SGSN peer: %s\n",
-		    osmo_sockaddr_to_str(from_addr));
-		return -1;
-	}
-	LOG(LOGL_DEBUG, "SGSN peer: %s\n", gtphub_port_str(sgsn));
-
-	struct gtphub_peer_port *ggsn_from_seq;
-	struct gtphub_peer_port *ggsn;
-	if (gtphub_unmap(hub, &p, sgsn,
-			 hub->ggsn_proxy[plane_idx],
-			 &ggsn, &ggsn_from_seq,
-			 NULL /* not interested, got it in &ggsn already */
-			)
-	    != 0) {
-		return -1;
-	}
-
-	if (!ggsn) {
-		if (gtphub_resolve_ggsn(hub, &p, &ggsn) < 0)
-			return -1;
-	}
-
-	if (!ggsn) {
-		LOG(LOGL_ERROR, "No GGSN to send to. Dropping packet.\n");
-		return -1;
-	}
-
-	if (plane_idx == GTPH_PLANE_CTRL) {
-		/* This may be a Create PDP Context request. If it is, there are
-		 * other addresses in the GTP message to set up apart from the
-		 * sender. */
-		if (gtphub_handle_pdp_ctx_ies(hub, from_bind_arr, to_bind_arr,
-					      sgsn, ggsn, &p)
-		    != 0)
-			return -1;
-	}
-
-	gtphub_check_restart_counter(hub, &p, sgsn);
-	gtphub_map_restart_counter(hub, &p, sgsn, ggsn);
-
-	/* If the SGSN is replying to a GGSN request, the sequence nr has
-	 * already been unmapped above (unmap_ggsn != NULL), and we need not
-	 * create a new outgoing sequence map. */
-	if (!ggsn_from_seq)
-		gtphub_map_seq(&p, sgsn, ggsn);
-
-	osmo_sockaddr_copy(to_addr, &ggsn->sa);
-
-	*reply_buf = (uint8_t*)p.data;
-
-	if (received) {
-		rate_ctr_inc(&to_bind->counters_io->ctr[GTPH_CTR_PKTS_OUT]);
-		rate_ctr_add(&to_bind->counters_io->ctr[GTPH_CTR_BYTES_OUT],
-			     received);
-	}
-	LOG(LOGL_DEBUG, "--> Forward to GGSN: %d bytes to %s\n",
-	    (int)received, osmo_sockaddr_to_str(to_addr));
-	return received;
-}
-
 static void resolved_gssn_del_cb(struct expiring_item *expi)
 {
 	struct gtphub_resolved_ggsn *ggsn;
@@ -2048,7 +1964,7 @@
 	    apn_oi_str, osmo_hexdump((unsigned char*)resolved_addr,
 				     sizeof(*resolved_addr)));
 
-	pp = gtphub_port_have(hub, &hub->to_ggsns[GTPH_PLANE_CTRL],
+	pp = gtphub_port_have(hub, &hub->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL],
 			      resolved_addr, 2123);
 	if (!pp) {
 		LOG(LOGL_ERROR, "Internal: Cannot create/find peer '%s'\n",
@@ -2127,10 +2043,9 @@
 	/* ... */
 
 	if (expired) {
-		int i;
-		for_each_plane(i) {
-			gtphub_gc_bind(&hub->to_sgsns[i]);
-			gtphub_gc_bind(&hub->to_ggsns[i]);
+		int s, p;
+		for_each_side_and_plane(s, p) {
+			gtphub_gc_bind(&hub->to_gsns[s][p]);
 		}
 	}
 }
@@ -2162,16 +2077,16 @@
 
 	nr_pool_init(&hub->tei_pool, 1, 0xffffffff);
 
+	int side_idx;
 	int plane_idx;
-	for_each_plane(plane_idx) {
-		gtphub_bind_init(&hub->to_ggsns[plane_idx]);
-		gtphub_bind_init(&hub->to_sgsns[plane_idx]);
+	for_each_side_and_plane(side_idx, plane_idx) {
+		gtphub_bind_init(&hub->to_gsns[side_idx][plane_idx]);
 	}
 
-	hub->to_sgsns[GTPH_PLANE_CTRL].label = "SGSN Ctrl";
-	hub->to_ggsns[GTPH_PLANE_CTRL].label = "GGSN Ctrl";
-	hub->to_sgsns[GTPH_PLANE_USER].label = "SGSN User";
-	hub->to_ggsns[GTPH_PLANE_USER].label = "GGSN User";
+	hub->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_CTRL].label = "SGSN Ctrl";
+	hub->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL].label = "GGSN Ctrl";
+	hub->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_USER].label = "SGSN User";
+	hub->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_USER].label = "GGSN User";
 }
 
 /* For the test suite, this is kept separate from gtphub_stop(), which also
@@ -2185,22 +2100,20 @@
 	expiry_clear(&hub->expire_quickly);
 	expiry_clear(&hub->expire_slowly);
 
+	int side_idx;
 	int plane_idx;
-	for_each_plane(plane_idx) {
-		gtphub_gc_bind(&hub->to_ggsns[plane_idx]);
-		gtphub_bind_free(&hub->to_ggsns[plane_idx]);
-
-		gtphub_gc_bind(&hub->to_sgsns[plane_idx]);
-		gtphub_bind_free(&hub->to_sgsns[plane_idx]);
+	for_each_side_and_plane(side_idx, plane_idx) {
+		gtphub_gc_bind(&hub->to_gsns[side_idx][plane_idx]);
+		gtphub_bind_free(&hub->to_gsns[side_idx][plane_idx]);
 	}
 }
 
 void gtphub_stop(struct gtphub *hub)
 {
+	int side_idx;
 	int plane_idx;
-	for_each_plane(plane_idx) {
-		gtphub_bind_stop(&hub->to_ggsns[plane_idx]);
-		gtphub_bind_stop(&hub->to_sgsns[plane_idx]);
+	for_each_side_and_plane(side_idx, plane_idx) {
+		gtphub_bind_stop(&hub->to_gsns[side_idx][plane_idx]);
 	}
 	gtphub_free(hub);
 }
@@ -2234,7 +2147,7 @@
 	hub->restart_counter = restart_counter;
 
 	/* If a Ctrl plane proxy is configured, ares will never be used. */
-	if (!cfg->ggsn_proxy[GTPH_PLANE_CTRL].addr_str) {
+	if (!cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL].addr_str) {
 		if (gtphub_ares_init(hub) != 0) {
 			LOG(LOGL_FATAL, "Failed to initialize ares\n");
 			return -1;
@@ -2243,61 +2156,44 @@
 
 	/* TODO set hub->restart_counter from external file. */
 
+	int side_idx;
 	int plane_idx;
-	for_each_plane(plane_idx) {
-		rc = gtphub_bind_start(&hub->to_ggsns[plane_idx],
-				       &cfg->to_ggsns[plane_idx],
-				       from_ggsns_read_cb, hub, plane_idx);
+	for_each_side_and_plane(side_idx, plane_idx) {
+		rc = gtphub_bind_start(&hub->to_gsns[side_idx][plane_idx],
+				       &cfg->to_gsns[side_idx][plane_idx],
+				       (side_idx == GTPH_SIDE_SGSN)
+					       ? from_sgsns_read_cb
+					       : from_ggsns_read_cb,
+				       hub, plane_idx);
 		if (rc) {
-			LOG(LOGL_FATAL, "Failed to bind for GGSNs (%s)\n",
-			    gtphub_plane_idx_names[plane_idx]);
-			return rc;
-		}
-
-		rc = gtphub_bind_start(&hub->to_sgsns[plane_idx],
-				       &cfg->to_sgsns[plane_idx],
-				       from_sgsns_read_cb, hub, plane_idx);
-		if (rc) {
-			LOG(LOGL_FATAL, "Failed to bind for SGSNs (%s)\n",
+			LOG(LOGL_FATAL, "Failed to bind for %ss (%s)\n",
+			    gtphub_side_idx_names[side_idx],
 			    gtphub_plane_idx_names[plane_idx]);
 			return rc;
 		}
 	}
 
-	for_each_plane(plane_idx) {
+	for_each_side_and_plane(side_idx, plane_idx) {
 		if (gtphub_make_proxy(hub,
-				      &hub->sgsn_proxy[plane_idx],
-				      &hub->to_sgsns[plane_idx],
-				      &cfg->sgsn_proxy[plane_idx])
+				      &hub->proxy[side_idx][plane_idx],
+				      &hub->to_gsns[side_idx][plane_idx],
+				      &cfg->proxy[side_idx][plane_idx])
 		    != 0) {
-			LOG(LOGL_FATAL, "Cannot configure SGSN proxy"
+			LOG(LOGL_FATAL, "Cannot configure %s proxy"
 			    " %s port %d.\n",
-			    cfg->sgsn_proxy[plane_idx].addr_str,
-			    (int)cfg->sgsn_proxy[plane_idx].port);
-			return -1;
-		}
-		if (gtphub_make_proxy(hub,
-				      &hub->ggsn_proxy[plane_idx],
-				      &hub->to_ggsns[plane_idx],
-				      &cfg->ggsn_proxy[plane_idx])
-		    != 0) {
-			LOG(LOGL_ERROR, "Cannot configure GGSN proxy.\n");
+			    gtphub_side_idx_names[side_idx],
+			    cfg->proxy[side_idx][plane_idx].addr_str,
+			    (int)cfg->proxy[side_idx][plane_idx].port);
 			return -1;
 		}
 	}
 
-	for_each_plane(plane_idx) {
-		if (hub->sgsn_proxy[plane_idx])
-			LOG(LOGL_NOTICE, "Using SGSN %s proxy %s\n",
+	for_each_side_and_plane(side_idx, plane_idx) {
+		if (hub->proxy[side_idx][plane_idx])
+			LOG(LOGL_NOTICE, "Using %s %s proxy %s\n",
+			    gtphub_side_idx_names[side_idx],
 			    gtphub_plane_idx_names[plane_idx],
-			    gtphub_port_str(hub->sgsn_proxy[plane_idx]));
-	}
-
-	for_each_plane(plane_idx) {
-		if (hub->ggsn_proxy[plane_idx])
-			LOG(LOGL_NOTICE, "Using GGSN %s proxy %s\n",
-			    gtphub_plane_idx_names[plane_idx],
-			    gtphub_port_str(hub->ggsn_proxy[plane_idx]));
+			    gtphub_port_str(hub->proxy[side_idx][plane_idx]));
 	}
 
 	gtphub_gc_start(hub);
diff --git a/openbsc/src/gprs/gtphub_main.c b/openbsc/src/gprs/gtphub_main.c
index 6dd316a..a330b9d 100644
--- a/openbsc/src/gprs/gtphub_main.c
+++ b/openbsc/src/gprs/gtphub_main.c
@@ -83,22 +83,15 @@
 void log_cfg(struct gtphub_cfg *cfg)
 {
 	struct gtphub_cfg_addr *a;
-	a = &cfg->to_sgsns[GTPH_PLANE_CTRL].bind;
-	LOGP(DGTPHUB, LOGL_NOTICE,
-	     "to-SGSNs bind, Control: %s port %d\n",
-	     a->addr_str, a->port);
-	a = &cfg->to_sgsns[GTPH_PLANE_USER].bind;
-	LOGP(DGTPHUB, LOGL_NOTICE,
-	     "to-SGSNs bind, User:    %s port %d\n",
-	     a->addr_str, a->port);
-	a = &cfg->to_ggsns[GTPH_PLANE_CTRL].bind;
-	LOGP(DGTPHUB, LOGL_NOTICE,
-	     "to-GGSNs bind, Control: %s port %d\n",
-	     a->addr_str, a->port);
-	a = &cfg->to_ggsns[GTPH_PLANE_USER].bind;
-	LOGP(DGTPHUB, LOGL_NOTICE,
-	     "to-GGSNs bind, User:    %s port %d\n",
-	     a->addr_str, a->port);
+	int side_idx, plane_idx;
+	for_each_side_and_plane(side_idx, plane_idx) {
+		a = &cfg->to_gsns[side_idx][plane_idx].bind;
+		LOGP(DGTPHUB, LOGL_NOTICE,
+		     "to-%ss bind, %s: %s port %d\n",
+		     gtphub_side_idx_names[side_idx],
+		     gtphub_plane_idx_names[plane_idx],
+		     a->addr_str, a->port);
+	}
 }
 
 static void signal_handler(int signal)
diff --git a/openbsc/src/gprs/gtphub_vty.c b/openbsc/src/gprs/gtphub_vty.c
index 5e5c1dd..e79cdd0 100644
--- a/openbsc/src/gprs/gtphub_vty.c
+++ b/openbsc/src/gprs/gtphub_vty.c
@@ -80,17 +80,17 @@
 	vty_out(vty, "gtphub%s", VTY_NEWLINE);
 
 	write_addrs(vty, "bind-to-sgsns",
-		    &g_cfg->to_sgsns[GTPH_PLANE_CTRL].bind,
-		    &g_cfg->to_sgsns[GTPH_PLANE_USER].bind);
+		    &g_cfg->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_CTRL].bind,
+		    &g_cfg->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_USER].bind);
 
 	write_addrs(vty, "bind-to-ggsns",
-		    &g_cfg->to_ggsns[GTPH_PLANE_CTRL].bind,
-		    &g_cfg->to_ggsns[GTPH_PLANE_USER].bind);
+		    &g_cfg->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL].bind,
+		    &g_cfg->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_USER].bind);
 
-	if (g_cfg->ggsn_proxy[GTPH_PLANE_CTRL].addr_str) {
+	if (g_cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL].addr_str) {
 		write_addrs(vty, "ggsn-proxy",
-			    &g_cfg->ggsn_proxy[GTPH_PLANE_CTRL],
-			    &g_cfg->ggsn_proxy[GTPH_PLANE_USER]);
+			    &g_cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL],
+			    &g_cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_USER]);
 	}
 
 	return CMD_SUCCESS;
@@ -123,9 +123,9 @@
 {
 	int i;
 	for_each_plane(i)
-		g_cfg->to_sgsns[i].bind.addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
-	g_cfg->to_sgsns[GTPH_PLANE_CTRL].bind.port = GTPH_DEFAULT_CONTROL_PORT;
-	g_cfg->to_sgsns[GTPH_PLANE_USER].bind.port = GTPH_DEFAULT_USER_PORT;
+		g_cfg->to_gsns[GTPH_SIDE_SGSN][i].bind.addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
+	g_cfg->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_CTRL].bind.port = GTPH_DEFAULT_CONTROL_PORT;
+	g_cfg->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_USER].bind.port = GTPH_DEFAULT_USER_PORT;
 	return CMD_SUCCESS;
 }
 
@@ -138,9 +138,9 @@
 {
 	int i;
 	for_each_plane(i)
-		g_cfg->to_ggsns[i].bind.addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
-	g_cfg->to_ggsns[GTPH_PLANE_CTRL].bind.port = GTPH_DEFAULT_CONTROL_PORT;
-	g_cfg->to_ggsns[GTPH_PLANE_USER].bind.port = GTPH_DEFAULT_USER_PORT;
+		g_cfg->to_gsns[GTPH_SIDE_GGSN][i].bind.addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
+	g_cfg->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL].bind.port = GTPH_DEFAULT_CONTROL_PORT;
+	g_cfg->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_USER].bind.port = GTPH_DEFAULT_USER_PORT;
 	return CMD_SUCCESS;
 }
 
@@ -161,7 +161,7 @@
 	BIND_DOCS
 	)
 {
-	return handle_binds(g_cfg->to_sgsns, argv);
+	return handle_binds(g_cfg->to_gsns[GTPH_SIDE_SGSN], argv);
 }
 
 DEFUN(cfg_gtphub_bind_to_ggsns, cfg_gtphub_bind_to_ggsns_cmd,
@@ -171,7 +171,7 @@
 	BIND_DOCS
 	)
 {
-	return handle_binds(g_cfg->to_ggsns, argv);
+	return handle_binds(g_cfg->to_gsns[GTPH_SIDE_GGSN], argv);
 }
 
 DEFUN(cfg_gtphub_ggsn_proxy_short, cfg_gtphub_ggsn_proxy_short_cmd,
@@ -181,10 +181,10 @@
 	"Remote IP address (v4 or v6)\n"
 	)
 {
-	g_cfg->ggsn_proxy[GTPH_PLANE_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
-	g_cfg->ggsn_proxy[GTPH_PLANE_CTRL].port = GTPH_DEFAULT_CONTROL_PORT;
-	g_cfg->ggsn_proxy[GTPH_PLANE_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
-	g_cfg->ggsn_proxy[GTPH_PLANE_USER].port = GTPH_DEFAULT_USER_PORT;
+	g_cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
+	g_cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL].port = GTPH_DEFAULT_CONTROL_PORT;
+	g_cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
+	g_cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_USER].port = GTPH_DEFAULT_USER_PORT;
 	return CMD_SUCCESS;
 }
 
@@ -195,10 +195,10 @@
 	BIND_DOCS
 	)
 {
-	g_cfg->ggsn_proxy[GTPH_PLANE_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
-	g_cfg->ggsn_proxy[GTPH_PLANE_CTRL].port = atoi(argv[1]);
-	g_cfg->ggsn_proxy[GTPH_PLANE_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[2]);
-	g_cfg->ggsn_proxy[GTPH_PLANE_USER].port = atoi(argv[3]);
+	g_cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
+	g_cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL].port = atoi(argv[1]);
+	g_cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[2]);
+	g_cfg->proxy[GTPH_SIDE_GGSN][GTPH_PLANE_USER].port = atoi(argv[3]);
 	return CMD_SUCCESS;
 }
 
@@ -209,10 +209,10 @@
 	"Remote IP address (v4 or v6)\n"
 	)
 {
-	g_cfg->sgsn_proxy[GTPH_PLANE_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
-	g_cfg->sgsn_proxy[GTPH_PLANE_CTRL].port = GTPH_DEFAULT_CONTROL_PORT;
-	g_cfg->sgsn_proxy[GTPH_PLANE_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
-	g_cfg->sgsn_proxy[GTPH_PLANE_USER].port = GTPH_DEFAULT_USER_PORT;
+	g_cfg->proxy[GTPH_SIDE_SGSN][GTPH_PLANE_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
+	g_cfg->proxy[GTPH_SIDE_SGSN][GTPH_PLANE_CTRL].port = GTPH_DEFAULT_CONTROL_PORT;
+	g_cfg->proxy[GTPH_SIDE_SGSN][GTPH_PLANE_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
+	g_cfg->proxy[GTPH_SIDE_SGSN][GTPH_PLANE_USER].port = GTPH_DEFAULT_USER_PORT;
 	return CMD_SUCCESS;
 }
 
@@ -223,10 +223,10 @@
 	BIND_DOCS
 	)
 {
-	g_cfg->sgsn_proxy[GTPH_PLANE_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
-	g_cfg->sgsn_proxy[GTPH_PLANE_CTRL].port = atoi(argv[1]);
-	g_cfg->sgsn_proxy[GTPH_PLANE_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[2]);
-	g_cfg->sgsn_proxy[GTPH_PLANE_USER].port = atoi(argv[3]);
+	g_cfg->proxy[GTPH_SIDE_SGSN][GTPH_PLANE_CTRL].addr_str = talloc_strdup(tall_vty_ctx, argv[0]);
+	g_cfg->proxy[GTPH_SIDE_SGSN][GTPH_PLANE_CTRL].port = atoi(argv[1]);
+	g_cfg->proxy[GTPH_SIDE_SGSN][GTPH_PLANE_USER].addr_str = talloc_strdup(tall_vty_ctx, argv[2]);
+	g_cfg->proxy[GTPH_SIDE_SGSN][GTPH_PLANE_USER].port = atoi(argv[3]);
 	return CMD_SUCCESS;
 }
 
@@ -258,13 +258,13 @@
 		vty_out(vty, "- %s Plane:%s",
 			gtphub_plane_idx_names[plane_idx], VTY_NEWLINE);
 
-		struct gtphub_bind *b = &g_hub->to_ggsns[plane_idx];
+		struct gtphub_bind *b = &g_hub->to_gsns[GTPH_SIDE_GGSN][plane_idx];
 		vty_out(vty, "  - to/from GGSNs: %s port %d%s",
 			gsn_addr_to_str(&b->local_addr), (int)b->local_port,
 			VTY_NEWLINE);
 		vty_out_rate_ctr_group(vty, "    ", b->counters_io);
 
-		b = &g_hub->to_sgsns[plane_idx];
+		b = &g_hub->to_gsns[GTPH_SIDE_SGSN][plane_idx];
 		vty_out(vty, "  - to/from SGSNs: %s port %d%s",
 			gsn_addr_to_str(&b->local_addr), (int)b->local_port,
 			VTY_NEWLINE);
diff --git a/openbsc/tests/gtphub/gtphub_test.c b/openbsc/tests/gtphub/gtphub_test.c
index 8347f3b..b0ede11 100644
--- a/openbsc/tests/gtphub/gtphub_test.c
+++ b/openbsc/tests/gtphub/gtphub_test.c
@@ -444,7 +444,7 @@
 					   &resolved_ggsn_addr) == 0);
 
 	struct gtphub_peer_port *pp;
-	pp = gtphub_port_have(hub, &hub->to_ggsns[GTPH_PLANE_CTRL],
+	pp = gtphub_port_have(hub, &hub->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL],
 			      &resolved_gsna, resolved_port);
 	printf("- __wrap_gtphub_resolve_ggsn_addr():\n"
 	       "  returning GGSN addr from imsi %s ni %s: %s\n",
@@ -584,13 +584,13 @@
 	gtphub_init(hub);
 
 	/* Tell this mock gtphub its local address for this test. */
-	LVL2_ASSERT(gsn_addr_from_str(&hub->to_sgsns[GTPH_PLANE_CTRL].local_addr,
+	LVL2_ASSERT(gsn_addr_from_str(&hub->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_CTRL].local_addr,
 				      "127.0.1.1") == 0);
-	LVL2_ASSERT(gsn_addr_from_str(&hub->to_sgsns[GTPH_PLANE_USER].local_addr,
+	LVL2_ASSERT(gsn_addr_from_str(&hub->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_USER].local_addr,
 				      "127.0.1.2") == 0);
-	LVL2_ASSERT(gsn_addr_from_str(&hub->to_ggsns[GTPH_PLANE_CTRL].local_addr,
+	LVL2_ASSERT(gsn_addr_from_str(&hub->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL].local_addr,
 				      "127.0.2.1") == 0);
-	LVL2_ASSERT(gsn_addr_from_str(&hub->to_ggsns[GTPH_PLANE_USER].local_addr,
+	LVL2_ASSERT(gsn_addr_from_str(&hub->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_USER].local_addr,
 				      "127.0.2.2") == 0);
 
 	hub->restart_counter = 0x23;
@@ -604,10 +604,10 @@
 #define GGSNS_USER_FD 2
 #define SGSNS_CTRL_FD 3
 #define SGSNS_USER_FD 4
-	hub->to_ggsns[GTPH_PLANE_CTRL].ofd.priv_nr = GGSNS_CTRL_FD;
-	hub->to_ggsns[GTPH_PLANE_USER].ofd.priv_nr = GGSNS_USER_FD;
-	hub->to_sgsns[GTPH_PLANE_CTRL].ofd.priv_nr = SGSNS_CTRL_FD;
-	hub->to_sgsns[GTPH_PLANE_USER].ofd.priv_nr = SGSNS_USER_FD;
+	hub->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL].ofd.priv_nr = GGSNS_CTRL_FD;
+	hub->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_USER].ofd.priv_nr = GGSNS_USER_FD;
+	hub->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_CTRL].ofd.priv_nr = SGSNS_CTRL_FD;
+	hub->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_USER].ofd.priv_nr = SGSNS_USER_FD;
 
 	return 1;
 }
@@ -619,11 +619,11 @@
 
 	int plane_idx;
 	plane_idx = GTPH_PLANE_CTRL;
-	LVL2_ASSERT(llist_empty(&hub->to_ggsns[plane_idx].peers));
-	LVL2_ASSERT(llist_empty(&hub->to_sgsns[plane_idx].peers));
+	LVL2_ASSERT(llist_empty(&hub->to_gsns[GTPH_SIDE_GGSN][plane_idx].peers));
+	LVL2_ASSERT(llist_empty(&hub->to_gsns[GTPH_SIDE_SGSN][plane_idx].peers));
 	plane_idx = GTPH_PLANE_USER;
-	LVL2_ASSERT(llist_empty(&hub->to_ggsns[plane_idx].peers));
-	LVL2_ASSERT(llist_empty(&hub->to_sgsns[plane_idx].peers));
+	LVL2_ASSERT(llist_empty(&hub->to_gsns[GTPH_SIDE_GGSN][plane_idx].peers));
+	LVL2_ASSERT(llist_empty(&hub->to_gsns[GTPH_SIDE_SGSN][plane_idx].peers));
 
 	gtphub_free(hub);
 	return 1;
@@ -689,16 +689,16 @@
 
 	to_ofd = NULL;
 	ZERO_STRUCT(&to_addr);
-	send = gtphub_from_sgsns_handle_buf(hub, GTPH_PLANE_CTRL, &sgsn_sender,
-					    buf, msg(gtp_ping_from_sgsn), now,
-					    &reply_buf, &to_ofd, &to_addr);
+	send = gtphub_handle_buf(hub, GTPH_SIDE_SGSN, GTPH_PLANE_CTRL,
+				 &sgsn_sender, buf, msg(gtp_ping_from_sgsn),
+				 now, &reply_buf, &to_ofd, &to_addr);
 	OSMO_ASSERT(send > 0);
 	OSMO_ASSERT(to_addr.l);
 	OSMO_ASSERT(same_addr(&to_addr, &sgsn_sender));
 	OSMO_ASSERT(to_ofd && (to_ofd->priv_nr == SGSNS_CTRL_FD));
 	OSMO_ASSERT(reply_is(gtp_pong_to_sgsn));
 
-	pp = gtphub_port_find_sa(&hub->to_sgsns[GTPH_PLANE_CTRL],
+	pp = gtphub_port_find_sa(&hub->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_CTRL],
 				 &sgsn_sender);
 	/* We don't record Echo peers. */
 	OSMO_ASSERT(!pp);
@@ -724,15 +724,15 @@
 
 	to_ofd = NULL;
 	ZERO_STRUCT(&to_addr);
-	send = gtphub_from_ggsns_handle_buf(hub, GTPH_PLANE_CTRL, &ggsn_sender,
-					    buf, msg(gtp_ping_from_ggsn), now,
-					    &reply_buf, &to_ofd, &to_addr);
+	send = gtphub_handle_buf(hub, GTPH_SIDE_GGSN, GTPH_PLANE_CTRL,
+				 &ggsn_sender, buf, msg(gtp_ping_from_ggsn),
+				 now, &reply_buf, &to_ofd, &to_addr);
 	OSMO_ASSERT(send > 0);
 	OSMO_ASSERT(same_addr(&to_addr, &ggsn_sender));
 	OSMO_ASSERT(to_ofd && (to_ofd->priv_nr == GGSNS_CTRL_FD));
 	OSMO_ASSERT(reply_is(gtp_pong_to_ggsn));
 
-	pp = gtphub_port_find_sa(&hub->to_ggsns[GTPH_PLANE_CTRL],
+	pp = gtphub_port_find_sa(&hub->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL],
 				 &sgsn_sender);
 	OSMO_ASSERT(!pp);
 
@@ -741,30 +741,30 @@
 
 	to_ofd = NULL;
 	ZERO_STRUCT(&to_addr);
-	send = gtphub_from_sgsns_handle_buf(hub, GTPH_PLANE_USER, &sgsn_sender,
-					    buf, msg(gtp_ping_from_sgsn), now,
-					    &reply_buf, &to_ofd, &to_addr);
+	send = gtphub_handle_buf(hub, GTPH_SIDE_SGSN, GTPH_PLANE_USER,
+				 &sgsn_sender, buf, msg(gtp_ping_from_sgsn),
+				 now, &reply_buf, &to_ofd, &to_addr);
 	OSMO_ASSERT(send > 0);
 	OSMO_ASSERT(to_addr.l);
 	OSMO_ASSERT(same_addr(&to_addr, &sgsn_sender));
 	OSMO_ASSERT(to_ofd && (to_ofd->priv_nr == SGSNS_USER_FD));
 	OSMO_ASSERT(reply_is(gtp_pong_to_sgsn));
 
-	pp = gtphub_port_find_sa(&hub->to_sgsns[GTPH_PLANE_USER],
+	pp = gtphub_port_find_sa(&hub->to_gsns[GTPH_SIDE_SGSN][GTPH_PLANE_USER],
 				 &sgsn_sender);
 	OSMO_ASSERT(!pp);
 
 	to_ofd = NULL;
 	ZERO_STRUCT(&to_addr);
-	send = gtphub_from_ggsns_handle_buf(hub, GTPH_PLANE_USER, &ggsn_sender,
-					    buf, msg(gtp_ping_from_ggsn), now,
-					    &reply_buf, &to_ofd, &to_addr);
+	send = gtphub_handle_buf(hub, GTPH_SIDE_GGSN, GTPH_PLANE_USER,
+				 &ggsn_sender, buf, msg(gtp_ping_from_ggsn),
+				 now, &reply_buf, &to_ofd, &to_addr);
 	OSMO_ASSERT(send > 0);
 	OSMO_ASSERT(same_addr(&to_addr, &ggsn_sender));
 	OSMO_ASSERT(to_ofd && (to_ofd->priv_nr == GGSNS_USER_FD));
 	OSMO_ASSERT(reply_is(gtp_pong_to_ggsn));
 
-	pp = gtphub_port_find_sa(&hub->to_ggsns[GTPH_PLANE_USER],
+	pp = gtphub_port_find_sa(&hub->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_USER],
 				 &sgsn_sender);
 	OSMO_ASSERT(!pp);
 
@@ -859,9 +859,9 @@
 	struct osmo_fd *ggsn_ofd = NULL;
 	struct osmo_sockaddr ggsn_addr;
 	int send;
-	send = gtphub_from_sgsns_handle_buf(hub, plane_idx, _sgsn_sender, buf,
-					    msg(hex_from_sgsn), now,
-					    &reply_buf, &ggsn_ofd, &ggsn_addr);
+	send = gtphub_handle_buf(hub, GTPH_SIDE_SGSN, plane_idx, _sgsn_sender,
+				 buf, msg(hex_from_sgsn), now,
+				 &reply_buf, &ggsn_ofd, &ggsn_addr);
 	LVL2_ASSERT(send > 0);
 	LVL2_ASSERT(same_addr(&ggsn_addr, ggsn_receiver));
 	LVL2_ASSERT(reply_is(hex_to_ggsn));
@@ -879,9 +879,9 @@
 	struct osmo_fd *sgsn_ofd;
 	struct osmo_sockaddr sgsn_addr;
 	int send;
-	send = gtphub_from_ggsns_handle_buf(hub, plane_idx, ggsn_sender, buf,
-					    msg(msg_from_ggsn), now,
-					    &reply_buf, &sgsn_ofd, &sgsn_addr);
+	send = gtphub_handle_buf(hub, GTPH_SIDE_GGSN, plane_idx, ggsn_sender,
+				 buf, msg(msg_from_ggsn), now,
+				 &reply_buf, &sgsn_ofd, &sgsn_addr);
 	LVL2_ASSERT(send > 0);
 	LVL2_ASSERT(same_addr(&sgsn_addr, sgsn_receiver));
 	LVL2_ASSERT(reply_is(msg_to_sgsn));
@@ -962,7 +962,7 @@
 	OSMO_ASSERT(create_pdp_ctx());
 
 	struct gtphub_peer_port *ggsn_port =
-		gtphub_port_find_sa(&hub->to_ggsns[GTPH_PLANE_CTRL],
+		gtphub_port_find_sa(&hub->to_gsns[GTPH_SIDE_GGSN][GTPH_PLANE_CTRL],
 				    &resolved_ggsn_addr);
 	OSMO_ASSERT(ggsn_port);
 	struct gtphub_peer *ggsn = ggsn_port->peer_addr->peer;