diff mbox

initial commit for libosmo-gtp

Message ID 1444889522-12665-1-git-send-email-pablo@gnumonks.org
State Accepted
Headers show

Commit Message

Pablo Neira Ayuso Oct. 15, 2015, 6:12 a.m. UTC
From: Pablo Neira Ayuso <pablo@gnumonks.org>

I started this libosmo-gtp library after spending quite some time trying to
clean up libgtp and add IPv6 support to it in some clean way.

The result was this library that I started from scratch in my spare time.

The basic idea behind this is to avoid mixing the carrier (socket) code with
the message building and parsing, and avoid overly the complicated callback
scheme of libgtp.

This implements minimal support for GTPv0, it also includes automated tests
by generating GTPv0 messages then hand it over to gtp0_recv() which generates
the reply.

I remember that I also validated this code by sending GTPv0 messages over UDP
to inspect them through wireshark, and they were OK.

It's fairly incomplete. The only client I have for this library is an initial
(fairly incomplete) osmo-gtp daemon using this library to replace opengtp.

BTW, there's a src/libosmocore.c file that contains helper functions to
provide a more consistent way to work with different TLV types.

Probably it can be uploaded to the osmocom repository even if incomplete?

Well, just wanted to get this code out there, it got stuck here and I didn't
find so far more time to make more progress on it, but we'll try to update
it when traveling or something.

 .gitignore                |  43 +++
 Makefile.am               |  13 +
 configure.ac              |  42 +++
 git-version-gen           | 151 +++++++++++
 include/Makefile.am       |   3 +
 include/gtp.h             | 102 +++++++
 include/libosmocore.h     |  20 ++
 include/osmocom/gtp/gtp.h |  49 ++++
 libosmogtp.pc.in          |  11 +
 src/Makefile.am           |  15 ++
 src/gtp.c                 | 669 ++++++++++++++++++++++++++++++++++++++++++++++
 src/libosmocore.c         |  70 +++++
 tests/Makefile.am         |  52 ++++
 tests/gtp/gtp_test.ok     |   0
 tests/gtp_test.c          | 121 +++++++++
 tests/gtp_test.ok         |   4 +
 tests/testsuite.at        |  18 ++
 17 files changed, 1383 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Makefile.am
 create mode 100644 configure.ac
 create mode 100755 git-version-gen
 create mode 100644 include/Makefile.am
 create mode 100644 include/gtp.h
 create mode 100644 include/libosmocore.h
 create mode 100644 include/osmocom/gtp/gtp.h
 create mode 100644 libosmogtp.pc.in
 create mode 100644 src/Makefile.am
 create mode 100644 src/gtp.c
 create mode 100644 src/libosmocore.c
 create mode 100644 tests/Makefile.am
 create mode 100644 tests/gtp/gtp_test.ok
 create mode 100644 tests/gtp_test.c
 create mode 100644 tests/gtp_test.ok
 create mode 100644 tests/testsuite.at

Comments

Neels Hofmeyr Oct. 15, 2015, 1:32 p.m. UTC | #1
Hi Pablo,

I am currently entering the same trajectory as you have: I'm tweaking and
forwarding GTP messages in the openbsc:neels/gtphub branch. The more I try
to use the gtp.h API (from openggsn), the more I am forced to re-implement
it. In that sense, a new and proper library for GTP messages is highly
interesting to me. GTPv0 is in fact less interesting, as seemingly *all*
messages I need to deal with are GTPv1. Adding v1 should actually be
pretty quick, though.

Let me take a look at the patch, comments are inline... I'm not implying
that you should work on it more. That would be great, but if not, they are
remarks for whomever would like to go on with it. Probably me anyway.

Thank you loads for sharing this!

~Neels


comments follow:

On Thu, Oct 15, 2015 at 08:12:02AM +0200, pablo@gnumonks.org wrote:
> From: Pablo Neira Ayuso <pablo@gnumonks.org>
> 
> I started this libosmo-gtp library after spending quite some time trying to
> clean up libgtp and add IPv6 support to it in some clean way.
> 
> The result was this library that I started from scratch in my spare time.
> 
> The basic idea behind this is to avoid mixing the carrier (socket) code with
> the message building and parsing, and avoid overly the complicated callback
> scheme of libgtp.

+1 !!!

> This implements minimal support for GTPv0, it also includes automated tests
> by generating GTPv0 messages then hand it over to gtp0_recv() which generates
> the reply.
> 
> I remember that I also validated this code by sending GTPv0 messages over UDP
> to inspect them through wireshark, and they were OK.

gtphub would be interesting testing grounds: it deals entirely with "real"
GTP messages coming from elsewhere, but also recodes them.

> It's fairly incomplete. The only client I have for this library is an initial
> (fairly incomplete) osmo-gtp daemon using this library to replace opengtp.
> 
> BTW, there's a src/libosmocore.c file that contains helper functions to
> provide a more consistent way to work with different TLV types.
> 
> Probably it can be uploaded to the osmocom repository even if incomplete?
> 
> Well, just wanted to get this code out there, it got stuck here and I didn't
> find so far more time to make more progress on it, but we'll try to update
> it when traveling or something.
> 
>  .gitignore                |  43 +++
>  Makefile.am               |  13 +
>  configure.ac              |  42 +++
>  git-version-gen           | 151 +++++++++++
>  include/Makefile.am       |   3 +
>  include/gtp.h             | 102 +++++++
>  include/libosmocore.h     |  20 ++
>  include/osmocom/gtp/gtp.h |  49 ++++
>  libosmogtp.pc.in          |  11 +
>  src/Makefile.am           |  15 ++
>  src/gtp.c                 | 669 ++++++++++++++++++++++++++++++++++++++++++++++
>  src/libosmocore.c         |  70 +++++
>  tests/Makefile.am         |  52 ++++
>  tests/gtp/gtp_test.ok     |   0
>  tests/gtp_test.c          | 121 +++++++++
>  tests/gtp_test.ok         |   4 +
>  tests/testsuite.at        |  18 ++
>  17 files changed, 1383 insertions(+)
>  create mode 100644 .gitignore
>  create mode 100644 Makefile.am
>  create mode 100644 configure.ac
>  create mode 100755 git-version-gen
>  create mode 100644 include/Makefile.am
>  create mode 100644 include/gtp.h
>  create mode 100644 include/libosmocore.h
>  create mode 100644 include/osmocom/gtp/gtp.h
>  create mode 100644 libosmogtp.pc.in
>  create mode 100644 src/Makefile.am
>  create mode 100644 src/gtp.c
>  create mode 100644 src/libosmocore.c
>  create mode 100644 tests/Makefile.am
>  create mode 100644 tests/gtp/gtp_test.ok
>  create mode 100644 tests/gtp_test.c
>  create mode 100644 tests/gtp_test.ok
>  create mode 100644 tests/testsuite.at

[...]

> diff --git a/include/gtp.h b/include/gtp.h
> new file mode 100644
> index 0000000..3a47de6
> --- /dev/null
> +++ b/include/gtp.h
> @@ -0,0 +1,102 @@
> +#ifndef _GTP_PROTOCOL_H_
> +#define _GTP_PROTOCOL_H_
> +
> +enum gtp_type {
> +	GTP_UNUSED		= 0,	/* GSM 09.60 says for future use */
> +	GTP_ECHO_REQ		= 1,	/* 7.4.1 GSM 09.60 */
> +	GTP_ECHO_RESP		= 2,	/* 7.4.2 GSM 09.60 */
> +	GTP_VERSION_NOTSUPP	= 3,	/* 7.4.3 GSM 09.60 */
> +	GTP_PDP_CREATE_REQ	= 16,	/* 7.5.1 GSM 09.60 */
> +	GTP_PDP_CREATE_RESP	= 17,	/* 7.5.2 GSM 09.60 */
> +	GTP_PDP_UPDATE_REQ	= 18,	/* 7.5.3 GSM 09.60 */
> +	GTP_PDP_UPDATE_RESP	= 19,	/* 7.5.4 GSM 09.60 */
> +	GTP_PDP_DELETE_REQ	= 20,	/* 7.5.5 GSM 09.60 */
> +	GTP_PDP_DELETE_RESP	= 21,	/* 7.5.6 GSM 09.60 */
> +	GTP_TYPE_MAX,
> +};
> +
> +struct gtp0_header {
> +#if BYTE_ORDER == BIG_ENDIAN
> +	uint8_t version:3,
> +		pt:1,
> +		spare:3,
> +		snn:1;
> +#elif BYTE_ORDER == LITTLE_ENDIAN
> +	uint8_t snn:1,
> +		spare:3,
> +		pt:1,
> +		version:3;

Hmm, endianness is about *byte* order, not *bit* order, right?  I suggest
to use one of the existing decoding functions for endianness instead:
- ntohs()/ntohl()
- osmo_loadXXbe()
- decode_big_endian() (yet static in openbsc/src/gprs/gprs_gsup_messages.c,
  see also the first commit it openbsc:neels/sgsn-id-3)
Anyway, this single octet should not be affected by endianness, but those
uint16,32,64_t below are.

> +#else
> +#warn "BYTE_ORDER is not defined, please fix your headers"
> +#endif
> +	uint8_t type;
> +	uint16_t length;
> +	uint16_t seq;
> +	uint16_t flow;
> +	uint8_t number;
> +	uint8_t spare1;
> +	uint8_t spare2;
> +	uint8_t spare3;
> +	uint64_t tid;
> +} __attribute__((packed));
> +
> +/*
> + * Information elements
> + */
> +/* TV */
> +#define GTPV0_IE_CAUSE			1   /* 8 bits */
> +#define GTPV0_IE_QOS_PROFILE		6   /* 24 bits */
> +#define GTPV0_IE_REORDERING_REQ		8   /* 1 bit */
> +#define GTPV0_IE_RECOVERY		14  /* 8 bit */
> +#define GTPV0_IE_SELECT_MODE		15  /* 16 bits */
> +#define GTPV0_IE_FLOW_LABEL_DATA	16  /* 16 bits */
> +#define GTPV0_IE_FLOW_LABEL_SIGNAL	17  /* 16 bits */
> +#define GTPV0_IE_CHARGING_ID		127 /* 32 bits */
> +/* TLV >= 128 */
> +#define GTPV0_IE_ENDUSER_ADDR		128
> +#define GTPV0_IE_AP_NAME		131
> +#define GTPV0_IE_PROTO_CONF_OPTS	132
> +#define GTPV0_IE_GSN_ADDR		133
> +#define GTPV0_IE_MSISDN			134
> +#define GTPV0_IE_CHARGING_GW_ADDR	251
> +
> +/*
> + * Other
> + */
> +#define GTPV0_CAUSE_REQ_ACCEPTED	128	/* GSM 09.60 7.9.1 */
> +
> +#include <netinet/in.h>
> +
> +struct enduser_addr_ie_payload_ipv4 {

heh, amazing name :)

> +#if BYTE_ORDER == BIG_ENDIAN
> +	uint8_t spare:4,
> +		pdp_org_type:4;
> +#elif BYTE_ORDER == LITTLE_ENDIAN
> +	uint8_t pdp_org_type:4,
> +		spare:4;
> +#else
> +#warn "BYTE_ORDER is not defined, please fix your headers"
> +#endif
> +	uint8_t pdp_type_number;
> +	struct in_addr addr;
> +} __attribute__((packed));
> +
> +struct enduser_addr_ie_payload_ipv6 {
> +#if BYTE_ORDER == BIG_ENDIAN
> +	uint8_t spare:4,
> +		pdp_org_type:4;
> +#elif BYTE_ORDER == LITTLE_ENDIAN
> +	uint8_t pdp_org_type:4,
> +		spare:4;
> +#else
> +#warn "BYTE_ORDER is not defined, please fix your headers"
> +#endif
> +	uint8_t pdp_type_number;
> +	struct in6_addr addr;
> +} __attribute__((packed));
> +
> +#define PDP_ORG_IETF	1
> +#define PDP_TYPE_IPV4	0x21
> +#define PDP_TYPE_IPV6	0x57
> +
> +#endif
> diff --git a/include/libosmocore.h b/include/libosmocore.h
> new file mode 100644
> index 0000000..dab9265
> --- /dev/null
> +++ b/include/libosmocore.h
> @@ -0,0 +1,20 @@
> +#ifndef _GTP_LIBOSMOCORE_H_
> +#define _GTP_LIBOSMOCORE_H_
> +
> +#include <osmocom/gsm/tlv.h>
> +
> +void *msgb_tv_put_be8(struct msgb *msg, int type, uint8_t value);
> +void *msgb_tv_put_be16(struct msgb *msg, int type, uint16_t value);
> +void *msgb_tv_put_be24(struct msgb *msg, int type, uint32_t value);
> +void *msgb_tv_put_be32(struct msgb *msg, int type, uint32_t value);
> +void *msgb_tlv_put_be32(struct msgb *msg, int type, uint32_t data);
> +void *msgb_tlv_put_data(struct msgb *msg, int type, int len, void *data);
> +
> +uint8_t tv_get_be8(struct tlv_parsed *tp, int type);
> +uint16_t tv_get_be16(struct tlv_parsed *tp, int type);
> +uint32_t tv_get_be24(struct tlv_parsed *tp, int type);
> +uint32_t tv_get_be32(struct tlv_parsed *tp, int type);

I like how endianness is dealt with at the msgb put/get level.
The same should probably happen with the header struct above, with
msgb_v_get_beXX().

The old gtp.h also has a "big endian struct"... it's cumbersome as one
cannot simply use the struct fields, but one needs to remember to ntohX()
first. Might make for more optimized code, though, unless read often.

> +const char *tv_get_strdup(struct tlv_parsed *tp, int type);
> +const char *tlv_get_strdup(struct tlv_parsed *tp, int type);
> +
> +#endif
> diff --git a/include/osmocom/gtp/gtp.h b/include/osmocom/gtp/gtp.h
> new file mode 100644
> index 0000000..cb1b362
> --- /dev/null
> +++ b/include/osmocom/gtp/gtp.h
> @@ -0,0 +1,49 @@

[...]

> +enum osmo_gsn_proto {
> +	OSMO_GSN_IPV4	= 0,
> +	OSMO_GSN_IPV6,
> +};

It would be great if IPv6 were handled implicitly, by passing the addr
size around and let getaddrinfo() et al deal with it. Am I missing
something that's standing in the way there?


[...]
> diff --git a/src/gtp.c b/src/gtp.c
> new file mode 100644
> index 0000000..672af5a
> --- /dev/null
> +++ b/src/gtp.c
> @@ -0,0 +1,669 @@
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <stdint.h>
> +#include <stdbool.h>
> +#include <arpa/inet.h>
> +
> +#include <gtp.h>
> +#include <osmocom/core/logging.h>
> +#include <osmocom/core/msgb.h>
> +#include <osmocom/core/write_queue.h>
> +#include <osmocom/gtp/gtp.h>
> +#include <osmocom/gsm/tlv.h>
> +
> +#include "libosmocore.h"
> +
> +/* Generic context pointer to data in the event context */
> +struct osmo_gtp_ctx {
> +	struct osmo_gtp_pdp *pdp;
> +	/* XXX Add more event context here */
> +};
> +
> +/* Events are called from the GTP stack to notify upper layer application */
> +struct gtp_event_cb {
> +	int (*cb)(enum osmo_gtp_event event, struct osmo_gtp_ctx *ctx);
> +};
> +
> +struct osmo_gsn {
> +	struct {
> +		enum osmo_gsn_proto	proto;
> +		bool			reordering_required;
> +		union {
> +			struct in_addr	ip_addr;
> +			struct in6_addr	ip6_addr;
> +		} ggsn;
> +	} cfg;
> +
> +	struct gtp_event_cb	event[OSMO_GTP_EVENT_MAX];
> +
> +	uint8_t restart_counter;
> +};
> +
> +struct osmo_gtp_pdp {
> +	uint32_t		qos_profile:24;
> +	uint16_t		flow_label_data;
> +	uint16_t		flow_label_signal;
> +	uint8_t			sel_mode;
> +	const char		*ap_name;
> +	union {
> +		struct in_addr	ip_addr;
> +		struct in6_addr	ip6_addr;
> +	} enduser;
> +	union {
> +		struct in_addr	ip_addr;
> +		struct in6_addr	ip6_addr;
> +	} sgsn;
> +	const char		*msisdn;
> +};
> +
> +#define OSMO_GTP_MTU		1500
> +
> +#define GTPV0			0	/* GSM 09.60 */
> +#define GTP_PRIME		0	/* GSM 09.60 */
> +#define GTP_NON_PRIME		1	/* GSM 12.15 */
> +#define GTP_SNDCP_NPDU_UNSET	0
> +
> +struct gtp0_header *gtp0_header_put(struct msgb *msg, uint32_t type,
> +				    uint16_t seq, uint16_t flow, uint64_t tid)
> +{
> +	struct gtp0_header *gtp0h = (struct gtp0_header *)msg->data;
> +
> +	gtp0h->version	= GTPV0;
> +	gtp0h->pt	= GTP_NON_PRIME;
> +	gtp0h->spare	= 0x7;
> +	gtp0h->snn	= GTP_SNDCP_NPDU_UNSET;
> +	gtp0h->type	= type;
> +	gtp0h->seq	= htons(seq);
> +	gtp0h->tid	= htobe64(tid);

Ah, here it is: htons() stored in the header struct.
I'd prefer a host-byte-order struct.

> +	gtp0h->spare1	= 0xff;
> +	gtp0h->spare2	= 0xff;
> +	gtp0h->spare3	= 0xff;
> +	gtp0h->number	= 0xff;
> +	gtp0h->flow	= flow;
> +	msgb_put(msg, sizeof(*gtp0h));
> +
> +	return gtp0h;
> +}
> +
> +void gtp0_header_end(struct gtp0_header *gtp0h, struct msgb *msg)
> +{
> +	gtp0h->length = htons(msg->len - sizeof(*gtp0h));

With msgb_alloc_headroom(), it is possible to first write the IEs to the
msgb, and then prepend the header with the correct size later on. See for
example in openbsc, gprs_gsup_msgb_alloc() and ipa_msg_push_header().

[...]
> +static struct msgb *gtp_unsupp(struct msgb *msg)
> +{
> +	struct msgb *reply;
> +	struct gtp0_header *gtp0h;
> +
> +	reply = msgb_alloc(OSMO_GTP_MTU, "gtp");
> +	if (reply == NULL)
> +		return NULL;
> +
> +	gtp0h = gtp0_header_put(reply, GTP_VERSION_NOTSUPP, gtp0h_seq(msg), 0,
> +				gtp0h_tid(msg));
> +	gtp0_header_end(gtp0h, reply);
> +
> +	return reply;
> +}

[...]

> +static int ip_alloc(struct osmo_gtp_pdp *pdp)
> +{
> +	pdp->enduser.ip_addr.s_addr= 0x01020304;
> +	return 0;
> +}
> +
> +static int ip6_alloc(struct osmo_gtp_pdp *pdp)
> +{
> +	return 0;
> +}

Why are these two called *alloc? 

> +static const struct tlv_definition gtp_pdp_create_req_attr_tlvdef = {
> +	.def = {
> +		[GTPV0_IE_QOS_PROFILE]		= { TLV_TYPE_FIXED, 3 },
> +		[GTPV0_IE_RECOVERY]		= { TLV_TYPE_FIXED, 1 },
> +		[GTPV0_IE_SELECT_MODE]		= { TLV_TYPE_FIXED, 1 },
> +		[GTPV0_IE_FLOW_LABEL_DATA]	= { TLV_TYPE_FIXED, 2 },
> +		[GTPV0_IE_FLOW_LABEL_SIGNAL]	= { TLV_TYPE_FIXED, 2 },
> +		[GTPV0_IE_ENDUSER_ADDR]		= { TLV_TYPE_TL16V },
> +		[GTPV0_IE_AP_NAME]		= { TLV_TYPE_TL16V },
> +		[GTPV0_IE_PROTO_CONF_OPTS]	= { TLV_TYPE_TL16V },
> +		[GTPV0_IE_GSN_ADDR]		= { TLV_TYPE_TL16V },
> +		[GTPV0_IE_MSISDN]		= { TLV_TYPE_TL16V },
> +	},
> +};

I can't begin to express how much better this looks than the old gtpie.h!

> +
> +static struct msgb *gtp_pdp_create_req_handler(struct osmo_gsn *gsn,
> +					       struct msgb *msg,
> +					       struct osmo_gtp_err *err)
> +{
> +	struct msgb *reply;
> +	struct tlv_parsed tp;
> +	struct osmo_gtp_pdp _pdp, *pdp = &_pdp;
> +	int ret;
> +
> +	memset(pdp, 0, sizeof(_pdp));
> +
> +	ret = tlv_parse(&tp, &gtp_pdp_create_req_attr_tlvdef,
> +			msg->data + sizeof(struct gtp0_header),
> +			msg->len - sizeof(struct gtp0_header), 0, 0);
> +	if (ret < 0) {
> +		LOGP(DLINP, LOGL_ERROR, "cannot parse TLVs: %u\n", ret);
> +		return NULL;
> +	}
> +
> +	/* Mandatory attributes:
> +	 *
> +	 * 1) 7.9.6. QoS profile. 32 bits. GSM 04.08
> +	 * 2) 7.9.13. Selection mode. 8 bits & 0x3.
> +	 * 3) 7.9.14. Flow label data. 16 bits
> +	 * 4) 7.9.15. Flow label signalling. 16 bits
> +	 * 5) 7.9.18. End User Address (see struct enduser_addr_ie_payload).
> +	 * 6) 7.9.21. Access Point (AP) Name (variable length)
> +	 * 7) 7.9.23. GSN address (variable length)
> +	 * 8) 7.9.24. MSISDN (variable length)
> +	 */
> +	if (!TLVP_PRESENT(&tp, GTPV0_IE_QOS_PROFILE) 		||
> +	    !TLVP_PRESENT(&tp, GTPV0_IE_SELECT_MODE) 		||
> +	    !TLVP_PRESENT(&tp, GTPV0_IE_FLOW_LABEL_DATA)	||
> +	    !TLVP_PRESENT(&tp, GTPV0_IE_FLOW_LABEL_SIGNAL)	||
> +	    !TLVP_PRESENT(&tp, GTPV0_IE_ENDUSER_ADDR)		||
> +	    !TLVP_PRESENT(&tp, GTPV0_IE_AP_NAME)		||
> +	    !TLVP_PRESENT(&tp, GTPV0_IE_GSN_ADDR)		||
> +	    !TLVP_PRESENT(&tp, GTPV0_IE_MSISDN)) {
> +		LOGP(DLINP, LOGL_ERROR, "missing mandatory TLV\n");
> +		return NULL;
> +	}

Oh, how pleasing to the openggsn ridden eye :)

> +
> +	pdp->qos_profile = tv_get_be24(&tp, GTPV0_IE_QOS_PROFILE);
> +
> +	/* Validate spare 6 bits to one (7.9.13)? */
> +	pdp->sel_mode = tv_get_be8(&tp, GTPV0_IE_SELECT_MODE) & 0x03;
> +	pdp->flow_label_data = tv_get_be16(&tp, GTPV0_IE_FLOW_LABEL_DATA);
> +	pdp->flow_label_signal = tv_get_be16(&tp, GTPV0_IE_FLOW_LABEL_SIGNAL);
> +	pdp->ap_name = tv_get_strdup(&tp, GTPV0_IE_AP_NAME);
> +
> +	if (gtp0_parse_enduser_addr(&tp, pdp) < 0 ||
> +	    gtp0_parse_gsn_addr(&tp, pdp) < 0)
> +		return NULL;
> +
> +	pdp->msisdn = tlv_get_strdup(&tp, GTPV0_IE_MSISDN);
> +
> +	/* TODO: Optional attributes:
> +	 * 1) 7.9.21. Protocol configuration options. GSM 04.08.
> +	 */
> +	if (TLVP_PRESENT(&tp, GTPV0_IE_PROTO_CONF_OPTS)) {
> +	}
> +
> +	reply = gtp_pdp_ctx_create_resp(gsn, msg, pdp);

For gtphub, I would need to have the response composition separate from
parsing. Well, not much left to do, is there, with a nice and clean API
like this ;)

In fact, I'd like to have message decoding/encoding in an entirely
separate header/c file pair from message handling.

> +	if (reply == NULL)
> +		return NULL;
> +
> +	return reply;
> +}
> +

[...]

> +static struct osmo_gtp_handler gtp0_handler[GTP_TYPE_MAX] = {
> +	[GTP_ECHO_REQ]		= {
> +		.request	= true,
> +		.handler	= gtp_echo_req_handler,
> +	},
> +	[GTP_PDP_CREATE_REQ]	= {
> +		.request	= true,
> +		.handler	= gtp_pdp_create_req_handler,
> +	},
> +	[GTP_PDP_UPDATE_REQ]	= {
> +		.request	= true,
> +		.handler	= gtp_pdp_update_req_handler,
> +	},
> +	[GTP_PDP_DELETE_REQ]	= {
> +		.request	= true,
> +		.handler	= gtp_pdp_delete_req_handler,
> +	},
> +};

nice. Though it seems to be an implementation detail for the client you
were writing (I'm always thinking in terms of gtphub, which doesn't
usually take own actions, but just forwards tweaked GTP data).

> +struct msgb *gtp0_recv(struct osmo_gsn *gsn, struct msgb *msg,
> +		       struct osmo_gtp_err *err)
> +{
> +	struct gtp0_header *gtp0h = (struct gtp0_header *)msg->data;
> +	struct msgb *reply = NULL;
> +
> +	if (gtp0h->version != 0) {
> +		LOGP(DLINP, LOGL_ERROR, "wrong GTP packet version %u\n",
> +		     gtp0h->version);
> +		return gtp_unsupp(msg);
> +	}
> +	if (msg->len < sizeof(*gtp0h)) {
> +		LOGP(DLINP, LOGL_ERROR, "GTPv0 packet too short msg->len %u\n",
> +		     msg->len);
> +		return NULL;
> +	}
> +	if (msg->len != ntohs(gtp0h->length) + sizeof(*gtp0h)) {
> +		LOGP(DLINP, LOGL_ERROR,
> +		     "truncated GTPv0 header msg->len %u != %u\n",
> +		     msg->len, ntohs(gtp0h->length) + (int)sizeof(*gtp0h));
> +		return NULL;
> +	}

Heh, this is pretty much 1:1 what I've pasted/written for gtphub :)

> +
> +	if ((gtp0h->type < GTP_TYPE_MAX) && gtp0_handler[gtp0h->type].handler)
> +		reply = gtp0_handler[gtp0h->type].handler(gsn, msg, err);
> +
> +	return reply;
> +}
> +
[...]

> diff --git a/src/libosmocore.c b/src/libosmocore.c
> new file mode 100644
> index 0000000..28b60d0
> --- /dev/null
> +++ b/src/libosmocore.c
> @@ -0,0 +1,70 @@
> +#include <osmocom/gsm/tlv.h>

[...]

> +void *msgb_tv_put_be24(struct msgb *msg, int type, uint32_t value)
> +{
> +	value = htonl(value & 0x00ffffff) >> 8;

heh, interesting :)
This won't work on a BE system. You can't portably do a
host CPU shift-right on a network byte order value.

Imagining a class: Big Endian 101, Professor asks: "Can anyone explain
the results of this code on a big endian and a little endian system?"
Half an hour of vigorous discussion follows.

> +	return msgb_tv_fixed_put(msg, type, 3, (uint8_t *)&value);
> +}

[...]

Thanks again!
Harald Welte Oct. 17, 2015, 8:53 a.m. UTC | #2
Hi Neels and Pablo,

first of all, it is great to see this code appear, I never liked libgtp
much either...

On Thu, Oct 15, 2015 at 03:32:02PM +0200, Neels Hofmeyr wrote:
> > +struct gtp0_header {
> > +#if BYTE_ORDER == BIG_ENDIAN
> > +	uint8_t version:3,
> > +		pt:1,
> > +		spare:3,
> > +		snn:1;
> > +#elif BYTE_ORDER == LITTLE_ENDIAN
> > +	uint8_t snn:1,
> > +		spare:3,
> > +		pt:1,
> > +		version:3;
> 
> Hmm, endianness is about *byte* order, not *bit* order, right?  

Life would be too simple for that.  It there is both byte-endinanness
and bit-endianness.  And if you define bit-fields using the c syntax,
then (at least on all platforms I know), you have to split your
definition like above.  Look at the definitions of IP header and TCP
header in your /usr/include/netinet/ip.h or /usr/include/netinet/tcp.h

> to use one of the existing decoding functions for endianness instead:
> - ntohs()/ntohl()

that doesn't help you with what pablo defined, as it is about an uint8_t ;)

> > +	gtp0h->seq	= htons(seq);
> > +	gtp0h->tid	= htobe64(tid);
> 
> Ah, here it is: htons() stored in the header struct.
> I'd prefer a host-byte-order struct.

The struct itself (for 16bit/32bit members of the struct) dosen't care.
And the function you refer to takes host-byte-order input and then
pushes the values as network byte ordre to the msgb, which I think is
fine.

> > +void gtp0_header_end(struct gtp0_header *gtp0h, struct msgb *msg)
> > +{
> > +	gtp0h->length = htons(msg->len - sizeof(*gtp0h));
> 
> With msgb_alloc_headroom(), it is possible to first write the IEs to the
> msgb, and then prepend the header with the correct size later on. See for
> example in openbsc, gprs_gsup_msgb_alloc() and ipa_msg_push_header().

yes, that is the preferred method.

> > +static const struct tlv_definition gtp_pdp_create_req_attr_tlvdef = {
> > +	.def = {
> > +		[GTPV0_IE_QOS_PROFILE]		= { TLV_TYPE_FIXED, 3 },
> > +};
> 
> I can't begin to express how much better this looks than the old gtpie.h!

Well, that's how we generaly deal with TLVs in libosmocore based
programs :)

> > +void *msgb_tv_put_be24(struct msgb *msg, int type, uint32_t value)
> > +{
> > +	value = htonl(value & 0x00ffffff) >> 8;
> 
> heh, interesting :)
> This won't work on a BE system. You can't portably do a
> host CPU shift-right on a network byte order value.

On a BE system the htonl() will evaporate and you end up with doning
only a shift-right by 8, which is probably not what you wanted, indeed.

> Imagining a class: Big Endian 101, Professor asks: "Can anyone explain
> the results of this code on a big endian and a little endian system?"
> Half an hour of vigorous discussion follows.

Beware, Pablo is a CS Professor at Sevilla University ;)
Neels Hofmeyr Oct. 17, 2015, 8:52 p.m. UTC | #3
On Sat, Oct 17, 2015 at 10:53:53AM +0200, Harald Welte wrote:
> Hi Neels and Pablo,
> 
> first of all, it is great to see this code appear, I never liked libgtp
> much either...
> 
> On Thu, Oct 15, 2015 at 03:32:02PM +0200, Neels Hofmeyr wrote:
> > > +struct gtp0_header {
> > > +#if BYTE_ORDER == BIG_ENDIAN
> > > +	uint8_t version:3,
> > > +		pt:1,
> > > +		spare:3,
> > > +		snn:1;
> > > +#elif BYTE_ORDER == LITTLE_ENDIAN
> > > +	uint8_t snn:1,
> > > +		spare:3,
> > > +		pt:1,
> > > +		version:3;
>
> /usr/include/netinet/ip.h or /usr/include/netinet/tcp.h

Learning never ends indeed. Thanks!

And then, I take it, the bits within each bitfield element are also
reversed, but since it matches the host, they are correct and ready...

I'm confused by the fact that the bitfields' order is "reversed" on BE.
Is that consistent with shift left and right operators?? Don't tell me
that you have to reverse those as well on a BE system.

Either way, would be great if C had a way that doesn't need everything
written twice...


[... TLVs]
> Well, that's how we generaly deal with TLVs in libosmocore based
> programs :)

About that: I see that values are referenced from the struct tlv_parsed.
That's mighty fast. But GTP does have duplicate IEs: more than one LV
with the same T. I need to loop tlv_parse_one() myself then, right? :/

I noticed because I found code in libgtp that targets the second occurence
of an IE type. The one instance I know of so far is in

 etsi/GSM/by_chapter/29.060.pdf  7.3.2 Create PDP Context Request

which has NSAPI and Linked NSAPI both referencing 7.7.17, as well as two
SGSN addresses both referencing 7.7.32 as IE description.

> Beware, Pablo is a CS Professor at Sevilla University ;)

/me bows

I actually picked some oranges in the Sevilla Monasteria once - delicious!
It was free, too. And I had una thervesa on the Alameda :)

~Neels
Harald Welte Oct. 18, 2015, 8:23 a.m. UTC | #4
Hi Neels,

On Sat, Oct 17, 2015 at 10:52:07PM +0200, Neels Hofmeyr wrote:
> [... TLVs]
> > Well, that's how we generaly deal with TLVs in libosmocore based
> > programs :)
> 
> About that: I see that values are referenced from the struct tlv_parsed.
> That's mighty fast. But GTP does have duplicate IEs: more than one LV
> with the same T. I need to loop tlv_parse_one() myself then, right? :/

Indeed.  At least for those messages where that might happen. I don't
really see a clean way to handle that, other than having a function
where the caller hands in multiple struct tlv_parsed (or an array of
them), which is equally ugly.
Jacob Erlbeck Oct. 19, 2015, 12:42 p.m. UTC | #5
On 17.10.2015 22:52, Neels Hofmeyr wrote:
> On Sat, Oct 17, 2015 at 10:53:53AM +0200, Harald Welte wrote:
>> Hi Neels and Pablo,
>>
>> first of all, it is great to see this code appear, I never liked libgtp
>> much either...
>>
>> On Thu, Oct 15, 2015 at 03:32:02PM +0200, Neels Hofmeyr wrote:
>>>> +struct gtp0_header {
>>>> +#if BYTE_ORDER == BIG_ENDIAN
>>>> +	uint8_t version:3,
>>>> +		pt:1,
>>>> +		spare:3,
>>>> +		snn:1;
>>>> +#elif BYTE_ORDER == LITTLE_ENDIAN
>>>> +	uint8_t snn:1,
>>>> +		spare:3,
>>>> +		pt:1,
>>>> +		version:3;
>>
>> /usr/include/netinet/ip.h or /usr/include/netinet/tcp.h
> 
> Learning never ends indeed. Thanks!
> 
> And then, I take it, the bits within each bitfield element are also
> reversed, but since it matches the host, they are correct and ready...
> 
> I'm confused by the fact that the bitfields' order is "reversed" on BE.
> Is that consistent with shift left and right operators?? Don't tell me
> that you have to reverse those as well on a BE system.

The ordering of bitfields is not necessarily tied to the byte ordering.

> 
> Either way, would be great if C had a way that doesn't need everything
> written twice...

In fact you have to write it two times only, because we know we are
using gcc. The C99 spec leaves it open to the compiler implementer
whether the bit fields within a unit are "high-order to
low-order or low-order to high-order" [C99: 6.7.2.1 (10)]. Have a look
at the definition of the 'ms_struct' and 'gcc_struct' variable
attributes in the gcc documentation for further details. Supporting both
ABIs would lead to four variants.

So this approach to do serialisation/deserialisation is not really
portable with respect to the specification.

Jacob
diff mbox

Patch

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ee984ce
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,43 @@ 
+Makefile
+Makefile.in
+.deps
+.libs
+*.o
+*.lo
+*.la
+*.pc
+aclocal.m4
+m4/*.m4
+autom4te.cache
+config.h*
+config.sub
+config.log
+config.status
+config.guess
+configure
+compile
+depcomp
+missing
+ltmain.sh
+install-sh
+stamp-h1
+libtool
+#libosmo-abis-*
+tests/*_test
+
+.tarball-version
+.version
+.dirstamp
+
+# tests
+tests/atconfig
+tests/package.m4
+tests/testsuite
+tests/testsuite.log
+tests/testsuite.dir/
+tests/gtp_test
+
+
+
+# vi/vim files
+*.sw?
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..5ab1484
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,13 @@ 
+ACLOCAL_AMFLAGS = -I m4
+
+AM_CPPFLAGS = $(all_includes) -I$(top_srcdir)/include
+SUBDIRS = include src tests
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA = libosmogtp.pc
+
+BUILT_SOURCES = $(top_srcdir)/.version
+$(top_srcdir)/.version:
+	echo $(VERSION) > $@-t && mv $@-t $@
+dist-hook:
+	echo $(VERSION) > $(distdir)/.tarball-version
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..c22fab4
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,42 @@ 
+AC_INIT([libosmogtp],
+	m4_esyscmd([./git-version-gen .tarball-version]),
+	[gprs@lists.osmocom.org])
+
+AM_INIT_AUTOMAKE([foreign dist-bzip2 no-dist-gzip 1.6])
+AC_CONFIG_TESTDIR(tests)
+
+dnl kernel style compile messages
+m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])])
+
+dnl checks for programs
+AC_PROG_MAKE_SET
+AC_PROG_CC
+AC_PROG_INSTALL
+LT_INIT([pic-only])
+
+AC_CONFIG_MACRO_DIR([m4])
+
+# The following test is taken from WebKit's webkit.m4
+saved_CFLAGS="$CFLAGS"
+CFLAGS="$CFLAGS -fvisibility=hidden "
+AC_MSG_CHECKING([if ${CC} supports -fvisibility=hidden])
+AC_COMPILE_IFELSE([AC_LANG_SOURCE([char foo;])],
+      [ AC_MSG_RESULT([yes])
+        SYMBOL_VISIBILITY="-fvisibility=hidden"],
+        AC_MSG_RESULT([no]))
+CFLAGS="$saved_CFLAGS"
+AC_SUBST(SYMBOL_VISIBILITY)
+
+dnl Generate the output
+AM_CONFIG_HEADER(config.h)
+
+PKG_CHECK_MODULES(LIBOSMOCORE, libosmocore >= 0.3.0)
+PKG_CHECK_MODULES(LIBOSMOVTY, libosmovty >= 0.3.0)
+PKG_CHECK_MODULES(LIBOSMOGSM, libosmogsm >= 0.3.10)
+
+AC_OUTPUT(
+	libosmogtp.pc
+	include/Makefile
+	src/Makefile
+	tests/Makefile
+	Makefile)
diff --git a/git-version-gen b/git-version-gen
new file mode 100755
index 0000000..42cf3d2
--- /dev/null
+++ b/git-version-gen
@@ -0,0 +1,151 @@ 
+#!/bin/sh
+# Print a version string.
+scriptversion=2010-01-28.01
+
+# Copyright (C) 2007-2010 Free Software Foundation, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# This script is derived from GIT-VERSION-GEN from GIT: http://git.or.cz/.
+# It may be run two ways:
+# - from a git repository in which the "git describe" command below
+#   produces useful output (thus requiring at least one signed tag)
+# - from a non-git-repo directory containing a .tarball-version file, which
+#   presumes this script is invoked like "./git-version-gen .tarball-version".
+
+# In order to use intra-version strings in your project, you will need two
+# separate generated version string files:
+#
+# .tarball-version - present only in a distribution tarball, and not in
+#   a checked-out repository.  Created with contents that were learned at
+#   the last time autoconf was run, and used by git-version-gen.  Must not
+#   be present in either $(srcdir) or $(builddir) for git-version-gen to
+#   give accurate answers during normal development with a checked out tree,
+#   but must be present in a tarball when there is no version control system.
+#   Therefore, it cannot be used in any dependencies.  GNUmakefile has
+#   hooks to force a reconfigure at distribution time to get the value
+#   correct, without penalizing normal development with extra reconfigures.
+#
+# .version - present in a checked-out repository and in a distribution
+#   tarball.  Usable in dependencies, particularly for files that don't
+#   want to depend on config.h but do want to track version changes.
+#   Delete this file prior to any autoconf run where you want to rebuild
+#   files to pick up a version string change; and leave it stale to
+#   minimize rebuild time after unrelated changes to configure sources.
+#
+# It is probably wise to add these two files to .gitignore, so that you
+# don't accidentally commit either generated file.
+#
+# Use the following line in your configure.ac, so that $(VERSION) will
+# automatically be up-to-date each time configure is run (and note that
+# since configure.ac no longer includes a version string, Makefile rules
+# should not depend on configure.ac for version updates).
+#
+# AC_INIT([GNU project],
+#         m4_esyscmd([build-aux/git-version-gen .tarball-version]),
+#         [bug-project@example])
+#
+# Then use the following lines in your Makefile.am, so that .version
+# will be present for dependencies, and so that .tarball-version will
+# exist in distribution tarballs.
+#
+# BUILT_SOURCES = $(top_srcdir)/.version
+# $(top_srcdir)/.version:
+#	echo $(VERSION) > $@-t && mv $@-t $@
+# dist-hook:
+#	echo $(VERSION) > $(distdir)/.tarball-version
+
+case $# in
+    1) ;;
+    *) echo 1>&2 "Usage: $0 \$srcdir/.tarball-version"; exit 1;;
+esac
+
+tarball_version_file=$1
+nl='
+'
+
+# First see if there is a tarball-only version file.
+# then try "git describe", then default.
+if test -f $tarball_version_file
+then
+    v=`cat $tarball_version_file` || exit 1
+    case $v in
+	*$nl*) v= ;; # reject multi-line output
+	[0-9]*) ;;
+	*) v= ;;
+    esac
+    test -z "$v" \
+	&& echo "$0: WARNING: $tarball_version_file seems to be damaged" 1>&2
+fi
+
+if test -n "$v"
+then
+    : # use $v
+elif
+       v=`git describe --abbrev=4 --match='v*' HEAD 2>/dev/null \
+	  || git describe --abbrev=4 HEAD 2>/dev/null` \
+    && case $v in
+	 [0-9]*) ;;
+	 v[0-9]*) ;;
+	 *) (exit 1) ;;
+       esac
+then
+    # Is this a new git that lists number of commits since the last
+    # tag or the previous older version that did not?
+    #   Newer: v6.10-77-g0f8faeb
+    #   Older: v6.10-g0f8faeb
+    case $v in
+	*-*-*) : git describe is okay three part flavor ;;
+	*-*)
+	    : git describe is older two part flavor
+	    # Recreate the number of commits and rewrite such that the
+	    # result is the same as if we were using the newer version
+	    # of git describe.
+	    vtag=`echo "$v" | sed 's/-.*//'`
+	    numcommits=`git rev-list "$vtag"..HEAD | wc -l`
+	    v=`echo "$v" | sed "s/\(.*\)-\(.*\)/\1-$numcommits-\2/"`;
+	    ;;
+    esac
+
+    # Change the first '-' to a '.', so version-comparing tools work properly.
+    # Remove the "g" in git describe's output string, to save a byte.
+    v=`echo "$v" | sed 's/-/./;s/\(.*\)-g/\1-/'`;
+else
+    v=UNKNOWN
+fi
+
+v=`echo "$v" |sed 's/^v//'`
+
+# Don't declare a version "dirty" merely because a time stamp has changed.
+git status > /dev/null 2>&1
+
+dirty=`sh -c 'git diff-index --name-only HEAD' 2>/dev/null` || dirty=
+case "$dirty" in
+    '') ;;
+    *) # Append the suffix only if there isn't one already.
+	case $v in
+	  *-dirty) ;;
+	  *) v="$v-dirty" ;;
+	esac ;;
+esac
+
+# Omit the trailing newline, so that m4_esyscmd can use the result directly.
+echo "$v" | tr -d '\012'
+
+# Local variables:
+# eval: (add-hook 'write-file-hooks 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-end: "$"
+# End:
diff --git a/include/Makefile.am b/include/Makefile.am
new file mode 100644
index 0000000..d04cac0
--- /dev/null
+++ b/include/Makefile.am
@@ -0,0 +1,3 @@ 
+noinst_HEADERS = gtp.h libosmocore.h
+
+nobase_include_HEADERS = osmocom/gtp/gtp.h
diff --git a/include/gtp.h b/include/gtp.h
new file mode 100644
index 0000000..3a47de6
--- /dev/null
+++ b/include/gtp.h
@@ -0,0 +1,102 @@ 
+#ifndef _GTP_PROTOCOL_H_
+#define _GTP_PROTOCOL_H_
+
+enum gtp_type {
+	GTP_UNUSED		= 0,	/* GSM 09.60 says for future use */
+	GTP_ECHO_REQ		= 1,	/* 7.4.1 GSM 09.60 */
+	GTP_ECHO_RESP		= 2,	/* 7.4.2 GSM 09.60 */
+	GTP_VERSION_NOTSUPP	= 3,	/* 7.4.3 GSM 09.60 */
+	GTP_PDP_CREATE_REQ	= 16,	/* 7.5.1 GSM 09.60 */
+	GTP_PDP_CREATE_RESP	= 17,	/* 7.5.2 GSM 09.60 */
+	GTP_PDP_UPDATE_REQ	= 18,	/* 7.5.3 GSM 09.60 */
+	GTP_PDP_UPDATE_RESP	= 19,	/* 7.5.4 GSM 09.60 */
+	GTP_PDP_DELETE_REQ	= 20,	/* 7.5.5 GSM 09.60 */
+	GTP_PDP_DELETE_RESP	= 21,	/* 7.5.6 GSM 09.60 */
+	GTP_TYPE_MAX,
+};
+
+struct gtp0_header {
+#if BYTE_ORDER == BIG_ENDIAN
+	uint8_t version:3,
+		pt:1,
+		spare:3,
+		snn:1;
+#elif BYTE_ORDER == LITTLE_ENDIAN
+	uint8_t snn:1,
+		spare:3,
+		pt:1,
+		version:3;
+#else
+#warn "BYTE_ORDER is not defined, please fix your headers"
+#endif
+	uint8_t type;
+	uint16_t length;
+	uint16_t seq;
+	uint16_t flow;
+	uint8_t number;
+	uint8_t spare1;
+	uint8_t spare2;
+	uint8_t spare3;
+	uint64_t tid;
+} __attribute__((packed));
+
+/*
+ * Information elements
+ */
+/* TV */
+#define GTPV0_IE_CAUSE			1   /* 8 bits */
+#define GTPV0_IE_QOS_PROFILE		6   /* 24 bits */
+#define GTPV0_IE_REORDERING_REQ		8   /* 1 bit */
+#define GTPV0_IE_RECOVERY		14  /* 8 bit */
+#define GTPV0_IE_SELECT_MODE		15  /* 16 bits */
+#define GTPV0_IE_FLOW_LABEL_DATA	16  /* 16 bits */
+#define GTPV0_IE_FLOW_LABEL_SIGNAL	17  /* 16 bits */
+#define GTPV0_IE_CHARGING_ID		127 /* 32 bits */
+/* TLV >= 128 */
+#define GTPV0_IE_ENDUSER_ADDR		128
+#define GTPV0_IE_AP_NAME		131
+#define GTPV0_IE_PROTO_CONF_OPTS	132
+#define GTPV0_IE_GSN_ADDR		133
+#define GTPV0_IE_MSISDN			134
+#define GTPV0_IE_CHARGING_GW_ADDR	251
+
+/*
+ * Other
+ */
+#define GTPV0_CAUSE_REQ_ACCEPTED	128	/* GSM 09.60 7.9.1 */
+
+#include <netinet/in.h>
+
+struct enduser_addr_ie_payload_ipv4 {
+#if BYTE_ORDER == BIG_ENDIAN
+	uint8_t spare:4,
+		pdp_org_type:4;
+#elif BYTE_ORDER == LITTLE_ENDIAN
+	uint8_t pdp_org_type:4,
+		spare:4;
+#else
+#warn "BYTE_ORDER is not defined, please fix your headers"
+#endif
+	uint8_t pdp_type_number;
+	struct in_addr addr;
+} __attribute__((packed));
+
+struct enduser_addr_ie_payload_ipv6 {
+#if BYTE_ORDER == BIG_ENDIAN
+	uint8_t spare:4,
+		pdp_org_type:4;
+#elif BYTE_ORDER == LITTLE_ENDIAN
+	uint8_t pdp_org_type:4,
+		spare:4;
+#else
+#warn "BYTE_ORDER is not defined, please fix your headers"
+#endif
+	uint8_t pdp_type_number;
+	struct in6_addr addr;
+} __attribute__((packed));
+
+#define PDP_ORG_IETF	1
+#define PDP_TYPE_IPV4	0x21
+#define PDP_TYPE_IPV6	0x57
+
+#endif
diff --git a/include/libosmocore.h b/include/libosmocore.h
new file mode 100644
index 0000000..dab9265
--- /dev/null
+++ b/include/libosmocore.h
@@ -0,0 +1,20 @@ 
+#ifndef _GTP_LIBOSMOCORE_H_
+#define _GTP_LIBOSMOCORE_H_
+
+#include <osmocom/gsm/tlv.h>
+
+void *msgb_tv_put_be8(struct msgb *msg, int type, uint8_t value);
+void *msgb_tv_put_be16(struct msgb *msg, int type, uint16_t value);
+void *msgb_tv_put_be24(struct msgb *msg, int type, uint32_t value);
+void *msgb_tv_put_be32(struct msgb *msg, int type, uint32_t value);
+void *msgb_tlv_put_be32(struct msgb *msg, int type, uint32_t data);
+void *msgb_tlv_put_data(struct msgb *msg, int type, int len, void *data);
+
+uint8_t tv_get_be8(struct tlv_parsed *tp, int type);
+uint16_t tv_get_be16(struct tlv_parsed *tp, int type);
+uint32_t tv_get_be24(struct tlv_parsed *tp, int type);
+uint32_t tv_get_be32(struct tlv_parsed *tp, int type);
+const char *tv_get_strdup(struct tlv_parsed *tp, int type);
+const char *tlv_get_strdup(struct tlv_parsed *tp, int type);
+
+#endif
diff --git a/include/osmocom/gtp/gtp.h b/include/osmocom/gtp/gtp.h
new file mode 100644
index 0000000..cb1b362
--- /dev/null
+++ b/include/osmocom/gtp/gtp.h
@@ -0,0 +1,49 @@ 
+#ifndef _OSMO_GTP_H_
+#define _OSMO_GTP_H_
+
+enum osmo_gtp_event {
+	OSMO_GTP_PDP_NEW,
+	OSMO_GTP_PDP_UPD,
+	OSMO_GTP_PDP_DEL,
+	/* XXX add more events here */
+	OSMO_GTP_EVENT_MAX,
+};
+
+enum gtp_err_type {
+	GTP_ENOSUPP	= 0,
+	GTP_ETOOSHORT,
+	GTP_ETRUNCATED,
+	GTP_EMALFORMED,
+	GTP_EMAX
+};
+
+struct osmo_gtp_err {
+	const char		*file;
+	int			line;
+	enum gtp_err_type	type;
+};
+
+struct osmo_gsn;
+
+struct osmo_gsn *osmo_gsn_alloc(void);
+
+struct msgb *gtp0_recv(struct osmo_gsn *gsn, struct msgb *msg,
+		       struct osmo_gtp_err *err);
+
+enum {
+	OSMO_GTP_DEFAULT = 0,
+	OSMO_GTP_HEX,
+};
+
+void osmo_gtp0_fprintf(FILE *fd, struct msgb *msg, uint32_t type);
+
+struct gtp0_header *gtp0_header_put(struct msgb *msg, uint32_t type,
+				    uint16_t seq, uint16_t flow, uint64_t tid);
+void gtp0_header_end(struct gtp0_header *gtp0h, struct msgb *msg);
+
+enum osmo_gsn_proto {
+	OSMO_GSN_IPV4	= 0,
+	OSMO_GSN_IPV6,
+};
+
+#endif
diff --git a/libosmogtp.pc.in b/libosmogtp.pc.in
new file mode 100644
index 0000000..f8998ea
--- /dev/null
+++ b/libosmogtp.pc.in
@@ -0,0 +1,11 @@ 
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: GTP Core Library
+Description: C Utility Library
+Version: @VERSION@
+Libs: -L${libdir} -losmogtp
+Cflags: -I${includedir}/
+
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..cad0c75
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,15 @@ 
+# This is _NOT_ the library release version, it's an API version.
+# Please read Chapter 6 "Library interface versions" of the libtool documentation before making any modification
+LIBVERSION=0:0:0
+
+AM_CPPFLAGS = $(all_includes) -I$(top_srcdir)/include -I$(top_builddir)
+AM_CFLAGS= -Wall $(LIBOSMOCORE_CFLAGS) $(LIBOSMOGSM_CFLAGS) $(LIBOSMOVTY_CFLAGS) $(COVERAGE_CFLAGS)
+AM_LDFLAGS = $(COVERAGE_LDFLAGS)
+COMMONLIBS = $(LIBOSMOCORE_LIBS) $(LIBOSMOGSM_LIBS) $(LIBOSMOVTY_LIBS)
+
+lib_LTLIBRARIES = libosmogtp.la
+
+libosmogtp_la_LDFLAGS = $(AM_LDFLAGS) -version-info $(LIBVERSION)
+libosmogtp_la_LIBADD = $(COMMONLIBS)
+libosmogtp_la_SOURCES = gtp.c		\
+			libosmocore.c
diff --git a/src/gtp.c b/src/gtp.c
new file mode 100644
index 0000000..672af5a
--- /dev/null
+++ b/src/gtp.c
@@ -0,0 +1,669 @@ 
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <arpa/inet.h>
+
+#include <gtp.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/write_queue.h>
+#include <osmocom/gtp/gtp.h>
+#include <osmocom/gsm/tlv.h>
+
+#include "libosmocore.h"
+
+/* Generic context pointer to data in the event context */
+struct osmo_gtp_ctx {
+	struct osmo_gtp_pdp *pdp;
+	/* XXX Add more event context here */
+};
+
+/* Events are called from the GTP stack to notify upper layer application */
+struct gtp_event_cb {
+	int (*cb)(enum osmo_gtp_event event, struct osmo_gtp_ctx *ctx);
+};
+
+struct osmo_gsn {
+	struct {
+		enum osmo_gsn_proto	proto;
+		bool			reordering_required;
+		union {
+			struct in_addr	ip_addr;
+			struct in6_addr	ip6_addr;
+		} ggsn;
+	} cfg;
+
+	struct gtp_event_cb	event[OSMO_GTP_EVENT_MAX];
+
+	uint8_t restart_counter;
+};
+
+struct osmo_gtp_pdp {
+	uint32_t		qos_profile:24;
+	uint16_t		flow_label_data;
+	uint16_t		flow_label_signal;
+	uint8_t			sel_mode;
+	const char		*ap_name;
+	union {
+		struct in_addr	ip_addr;
+		struct in6_addr	ip6_addr;
+	} enduser;
+	union {
+		struct in_addr	ip_addr;
+		struct in6_addr	ip6_addr;
+	} sgsn;
+	const char		*msisdn;
+};
+
+#define OSMO_GTP_MTU		1500
+
+#define GTPV0			0	/* GSM 09.60 */
+#define GTP_PRIME		0	/* GSM 09.60 */
+#define GTP_NON_PRIME		1	/* GSM 12.15 */
+#define GTP_SNDCP_NPDU_UNSET	0
+
+struct gtp0_header *gtp0_header_put(struct msgb *msg, uint32_t type,
+				    uint16_t seq, uint16_t flow, uint64_t tid)
+{
+	struct gtp0_header *gtp0h = (struct gtp0_header *)msg->data;
+
+	gtp0h->version	= GTPV0;
+	gtp0h->pt	= GTP_NON_PRIME;
+	gtp0h->spare	= 0x7;
+	gtp0h->snn	= GTP_SNDCP_NPDU_UNSET;
+	gtp0h->type	= type;
+	gtp0h->seq	= htons(seq);
+	gtp0h->tid	= htobe64(tid);
+	gtp0h->spare1	= 0xff;
+	gtp0h->spare2	= 0xff;
+	gtp0h->spare3	= 0xff;
+	gtp0h->number	= 0xff;
+	gtp0h->flow	= flow;
+	msgb_put(msg, sizeof(*gtp0h));
+
+	return gtp0h;
+}
+
+void gtp0_header_end(struct gtp0_header *gtp0h, struct msgb *msg)
+{
+	gtp0h->length = htons(msg->len - sizeof(*gtp0h));
+}
+
+uint64_t gtp0h_tid(const struct msgb *msg)
+{
+	const struct gtp0_header *gtp0h =
+		(const struct gtp0_header *)msg->data;
+
+	return be64toh(gtp0h->tid);
+}
+
+uint16_t gtp0h_seq(const struct msgb *msg)
+{
+	const struct gtp0_header *gtp0h =
+		(const struct gtp0_header *)msg->data;
+
+	return ntohs(gtp0h->seq);
+}
+
+struct osmo_gsn *osmo_gsn_alloc(void)
+{
+	return calloc(1, sizeof(struct osmo_gsn));
+}
+
+static struct msgb *gtp_unsupp(struct msgb *msg)
+{
+	struct msgb *reply;
+	struct gtp0_header *gtp0h;
+
+	reply = msgb_alloc(OSMO_GTP_MTU, "gtp");
+	if (reply == NULL)
+		return NULL;
+
+	gtp0h = gtp0_header_put(reply, GTP_VERSION_NOTSUPP, gtp0h_seq(msg), 0,
+				gtp0h_tid(msg));
+	gtp0_header_end(gtp0h, reply);
+
+	return reply;
+}
+
+struct osmo_gtp_handler {
+	bool request;
+	struct msgb *(*handler)(struct osmo_gsn *gsn, struct msgb *msg,
+				struct osmo_gtp_err *err);
+};
+
+static struct msgb *gtp_echo_req_handler(struct osmo_gsn *gsn,
+					 struct msgb *msg,
+					 struct osmo_gtp_err *err)
+{
+	struct msgb *reply;
+	struct gtp0_header *gtp0h;
+
+	reply = msgb_alloc(OSMO_GTP_MTU, "gtp");
+	if (reply == NULL)
+		return NULL;
+
+	gtp0h = gtp0_header_put(reply, GTP_ECHO_RESP, gtp0h_seq(msg), 0,
+				gtp0h_tid(msg));
+	msgb_tv_put(reply, GTPV0_IE_RECOVERY, gsn->restart_counter);
+
+	/* TODO: Private extension. 7.9.26 (Optional) */
+	/* TODO: IE T=255 Length ExtensionID=(8 bits) ExtValue=(variable) */
+
+	gtp0_header_end(gtp0h, reply);
+
+	return reply;
+}
+
+static int ip_alloc(struct osmo_gtp_pdp *pdp)
+{
+	pdp->enduser.ip_addr.s_addr= 0x01020304;
+	return 0;
+}
+
+static int ip6_alloc(struct osmo_gtp_pdp *pdp)
+{
+	return 0;
+}
+
+static void build_enduser_addr(struct msgb *reply, struct osmo_gsn *gsn,
+			       struct osmo_gtp_pdp *pdp)
+{
+	switch (gsn->cfg.proto) {
+	case OSMO_GSN_IPV4: {
+		struct enduser_addr_ie_payload_ipv4 ie_ipv4;
+
+		ip_alloc(pdp);
+
+		ie_ipv4.spare = 0xf;
+		ie_ipv4.pdp_org_type = PDP_ORG_IETF;
+		ie_ipv4.pdp_type_number = PDP_TYPE_IPV4;
+		ie_ipv4.addr = pdp->enduser.ip_addr;
+
+		msgb_tlv_put_data(reply, GTPV0_IE_ENDUSER_ADDR,
+				  sizeof(ie_ipv4), &ie_ipv4);
+		break;
+		}
+	case OSMO_GSN_IPV6: {
+		struct enduser_addr_ie_payload_ipv6 ie_ipv6;
+
+		ip6_alloc(pdp);
+
+		ie_ipv6.spare = 0xf;
+		ie_ipv6.pdp_org_type = PDP_ORG_IETF;
+		ie_ipv6.pdp_type_number = PDP_TYPE_IPV6;
+		ie_ipv6.addr = pdp->enduser.ip6_addr;
+
+		msgb_tlv_put_data(reply, GTPV0_IE_ENDUSER_ADDR,
+				  sizeof(ie_ipv6), &ie_ipv6);
+		}
+		break;
+	}
+}
+
+static void build_gsn_addr(struct msgb *reply, struct osmo_gsn *gsn,
+			   struct osmo_gtp_pdp *pdp)
+{
+	switch (gsn->cfg.proto) {
+	case OSMO_GSN_IPV4:
+		msgb_tlv_put_data(reply, GTPV0_IE_GSN_ADDR,
+				  sizeof(struct in_addr),
+				  &gsn->cfg.ggsn.ip_addr);
+		break;
+	case OSMO_GSN_IPV6:
+		msgb_tlv_put_data(reply, GTPV0_IE_GSN_ADDR,
+			     	  sizeof(struct in6_addr),
+				  &gsn->cfg.ggsn.ip6_addr);
+		break;
+	}
+}
+
+static struct msgb *gtp_pdp_ctx_create_resp(struct osmo_gsn *gsn,
+					    const struct msgb *msg,
+					    struct osmo_gtp_pdp *pdp)
+{
+	struct msgb *reply;
+	struct gtp0_header *gtp0h;
+
+	reply = msgb_alloc(OSMO_GTP_MTU, "gtp");
+	if (reply == NULL)
+		return NULL;
+
+	gtp0h = gtp0_header_put(reply, GTP_PDP_CREATE_RESP, gtp0h_seq(msg), 0,
+				gtp0h_tid(msg));
+
+	/* TODO: implement reject PDP request + reason */
+	msgb_tv_put_be8(reply, GTPV0_IE_CAUSE, GTPV0_CAUSE_REQ_ACCEPTED);
+
+	if (GTPV0_CAUSE_REQ_ACCEPTED == GTPV0_CAUSE_REQ_ACCEPTED) {
+		/* Quality of service profile. 7.9.6 (Conditional) */
+
+		/* broken u24 */
+		msgb_tv_put_be24(reply, GTPV0_IE_QOS_PROFILE, pdp->qos_profile);
+
+		/* Reordering required. 7.9.7 (Conditional) */
+		msgb_tv_put_be8(reply, GTPV0_IE_REORDERING_REQ,
+				gsn->cfg.reordering_required | 0xfe);
+
+		/* Recovery. 7.9.12 (Optional) */
+		msgb_tv_put_be8(reply, GTPV0_IE_RECOVERY, gsn->restart_counter);
+
+		int val = 0xa;
+
+		/* Flow label signalling. 7.9.15 (Conditional) */
+		msgb_tv_put_be16(reply, GTPV0_IE_FLOW_LABEL_DATA, val);
+		/* Flow label data I. 7.9.14 (Conditional) */
+		msgb_tv_put_be16(reply, GTPV0_IE_FLOW_LABEL_SIGNAL, val);
+
+		/* Charging ID. 7.9.17 (Conditional) */
+		msgb_tv_put_be32(reply, GTPV0_IE_CHARGING_ID, val);
+
+		/* End user address. 7.9.18 (Conditional) */
+		build_enduser_addr(reply, gsn, pdp);
+
+		/* TODO: Protocol configuration options. 7.9.22 (Optional) */
+
+		/* TODO: GGSN Address for signalling. 7.9.23 (Conditional) */
+		/* TODO: GGSN Address for user traffic. 7.9.23 (Conditional) */
+		build_gsn_addr(reply, gsn, pdp);
+
+		/* TODO: Charging Gateway Address. 7.9.25 (Optional) */
+		msgb_tlv_put_data(reply, GTPV0_IE_CHARGING_GW_ADDR, sizeof(val), &val);
+
+		/* TODO: Private extension. 7.9.26 (Optional) */
+	}
+
+	gtp0_header_end(gtp0h, reply);
+
+	return reply;
+}
+
+static const struct tlv_definition gtp_pdp_create_req_attr_tlvdef = {
+	.def = {
+		[GTPV0_IE_QOS_PROFILE]		= { TLV_TYPE_FIXED, 3 },
+		[GTPV0_IE_RECOVERY]		= { TLV_TYPE_FIXED, 1 },
+		[GTPV0_IE_SELECT_MODE]		= { TLV_TYPE_FIXED, 1 },
+		[GTPV0_IE_FLOW_LABEL_DATA]	= { TLV_TYPE_FIXED, 2 },
+		[GTPV0_IE_FLOW_LABEL_SIGNAL]	= { TLV_TYPE_FIXED, 2 },
+		[GTPV0_IE_ENDUSER_ADDR]		= { TLV_TYPE_TL16V },
+		[GTPV0_IE_AP_NAME]		= { TLV_TYPE_TL16V },
+		[GTPV0_IE_PROTO_CONF_OPTS]	= { TLV_TYPE_TL16V },
+		[GTPV0_IE_GSN_ADDR]		= { TLV_TYPE_TL16V },
+		[GTPV0_IE_MSISDN]		= { TLV_TYPE_TL16V },
+	},
+};
+
+struct enduser_addr_ie_payload {
+#if BYTE_ORDER == BIG_ENDIAN
+	uint8_t spare:4,
+		pdp_org_type:4;
+#elif BYTE_ORDER == LITTLE_ENDIAN
+	uint8_t pdp_org_type:4,
+		spare:4;
+#else
+#warn "BYTE_ORDER is not defined, please fix your headers"
+#endif
+	uint8_t pdp_type_number;
+	char addr[0];
+} __attribute__((packed));
+
+static int gtp0_parse_enduser_addr(struct tlv_parsed *tp,
+				   struct osmo_gtp_pdp *pdp)
+{
+	struct enduser_addr_ie_payload *ie =
+		(struct enduser_addr_ie_payload *)
+			TLVP_VAL(tp, GTPV0_IE_ENDUSER_ADDR);
+	int attrlen = TLVP_LEN(tp, GTPV0_IE_ENDUSER_ADDR);
+	int hdrlen = sizeof(struct enduser_addr_ie_payload);
+
+	if (ie->pdp_org_type != PDP_ORG_IETF) {
+		LOGP(DLINP, LOGL_ERROR, "Unsupported PDP org type %d",
+		     ie->pdp_org_type);
+		return -1;
+	}
+
+	switch (ie->pdp_type_number) {
+	case PDP_TYPE_IPV4:
+		if (attrlen != hdrlen + sizeof(struct in_addr)) {
+			LOGP(DLINP, LOGL_ERROR,
+			     "bad enduser address length %d for IPv4",
+			     attrlen);
+			return -1;
+		}
+		memcpy(&pdp->enduser.ip_addr, ie->addr, sizeof(struct in_addr));
+		break;
+	case PDP_TYPE_IPV6:
+		if (attrlen != hdrlen + sizeof(struct in6_addr)) {
+			LOGP(DLINP, LOGL_ERROR,
+			     "bad enduser address length %d for IPv6",
+			     attrlen);
+			return -1;
+		}
+		memcpy(&pdp->enduser.ip6_addr, ie->addr, sizeof(struct in6_addr));
+		break;
+	default:
+		LOGP(DLINP, LOGL_ERROR, "Unsupported PDP type number %d\n",
+		     ie->pdp_type_number);
+		break;
+	}
+
+	return 0;
+}
+
+static int gtp0_parse_gsn_addr(struct tlv_parsed *tp,
+			       struct osmo_gtp_pdp *pdp)
+{
+	switch (TLVP_LEN(tp, GTPV0_IE_GSN_ADDR)) {
+	case sizeof(struct in_addr):
+		memcpy(&pdp->sgsn, TLVP_VAL(tp, GTPV0_IE_GSN_ADDR),
+		       sizeof(struct in_addr));
+		break;
+	case sizeof(struct in6_addr):
+		memcpy(&pdp->sgsn, TLVP_VAL(tp, GTPV0_IE_GSN_ADDR),
+		       sizeof(struct in6_addr));
+		break;
+	default:
+		return -1;
+	}
+	return 0;
+}
+
+static struct msgb *gtp_pdp_create_req_handler(struct osmo_gsn *gsn,
+					       struct msgb *msg,
+					       struct osmo_gtp_err *err)
+{
+	struct msgb *reply;
+	struct tlv_parsed tp;
+	struct osmo_gtp_pdp _pdp, *pdp = &_pdp;
+	int ret;
+
+	memset(pdp, 0, sizeof(_pdp));
+
+	ret = tlv_parse(&tp, &gtp_pdp_create_req_attr_tlvdef,
+			msg->data + sizeof(struct gtp0_header),
+			msg->len - sizeof(struct gtp0_header), 0, 0);
+	if (ret < 0) {
+		LOGP(DLINP, LOGL_ERROR, "cannot parse TLVs: %u\n", ret);
+		return NULL;
+	}
+
+	/* Mandatory attributes:
+	 *
+	 * 1) 7.9.6. QoS profile. 32 bits. GSM 04.08
+	 * 2) 7.9.13. Selection mode. 8 bits & 0x3.
+	 * 3) 7.9.14. Flow label data. 16 bits
+	 * 4) 7.9.15. Flow label signalling. 16 bits
+	 * 5) 7.9.18. End User Address (see struct enduser_addr_ie_payload).
+	 * 6) 7.9.21. Access Point (AP) Name (variable length)
+	 * 7) 7.9.23. GSN address (variable length)
+	 * 8) 7.9.24. MSISDN (variable length)
+	 */
+	if (!TLVP_PRESENT(&tp, GTPV0_IE_QOS_PROFILE) 		||
+	    !TLVP_PRESENT(&tp, GTPV0_IE_SELECT_MODE) 		||
+	    !TLVP_PRESENT(&tp, GTPV0_IE_FLOW_LABEL_DATA)	||
+	    !TLVP_PRESENT(&tp, GTPV0_IE_FLOW_LABEL_SIGNAL)	||
+	    !TLVP_PRESENT(&tp, GTPV0_IE_ENDUSER_ADDR)		||
+	    !TLVP_PRESENT(&tp, GTPV0_IE_AP_NAME)		||
+	    !TLVP_PRESENT(&tp, GTPV0_IE_GSN_ADDR)		||
+	    !TLVP_PRESENT(&tp, GTPV0_IE_MSISDN)) {
+		LOGP(DLINP, LOGL_ERROR, "missing mandatory TLV\n");
+		return NULL;
+	}
+
+	pdp->qos_profile = tv_get_be24(&tp, GTPV0_IE_QOS_PROFILE);
+
+	/* Validate spare 6 bits to one (7.9.13)? */
+	pdp->sel_mode = tv_get_be8(&tp, GTPV0_IE_SELECT_MODE) & 0x03;
+	pdp->flow_label_data = tv_get_be16(&tp, GTPV0_IE_FLOW_LABEL_DATA);
+	pdp->flow_label_signal = tv_get_be16(&tp, GTPV0_IE_FLOW_LABEL_SIGNAL);
+	pdp->ap_name = tv_get_strdup(&tp, GTPV0_IE_AP_NAME);
+
+	if (gtp0_parse_enduser_addr(&tp, pdp) < 0 ||
+	    gtp0_parse_gsn_addr(&tp, pdp) < 0)
+		return NULL;
+
+	pdp->msisdn = tlv_get_strdup(&tp, GTPV0_IE_MSISDN);
+
+	/* TODO: Optional attributes:
+	 * 1) 7.9.21. Protocol configuration options. GSM 04.08.
+	 */
+	if (TLVP_PRESENT(&tp, GTPV0_IE_PROTO_CONF_OPTS)) {
+	}
+
+	reply = gtp_pdp_ctx_create_resp(gsn, msg, pdp);
+	if (reply == NULL)
+		return NULL;
+
+	return reply;
+}
+
+static const struct tlv_definition gtp_pdp_update_req_attr_tlvdef = {
+	.def = {
+		[GTPV0_IE_QOS_PROFILE]		= { TLV_TYPE_FIXED, 3 },
+		[GTPV0_IE_RECOVERY]		= { TLV_TYPE_FIXED, 1 },
+		[GTPV0_IE_FLOW_LABEL_DATA]	= { TLV_TYPE_FIXED, 2 },
+		[GTPV0_IE_FLOW_LABEL_SIGNAL]	= { TLV_TYPE_FIXED, 2 },
+		[GTPV0_IE_GSN_ADDR]		= { TLV_TYPE_TL16V },
+	},
+};
+
+static struct msgb *gtp_pdp_ctx_update_resp(struct osmo_gsn *gsn,
+					    const struct msgb *msg,
+					    struct osmo_gtp_pdp *pdp)
+{
+	struct msgb *reply;
+	struct gtp0_header *gtp0h;
+
+	reply = msgb_alloc(OSMO_GTP_MTU, "gtp");
+	if (reply == NULL)
+		return NULL;
+
+	gtp0h = gtp0_header_put(reply, GTP_PDP_UPDATE_RESP, gtp0h_seq(msg), 0,
+				gtp0h_tid(msg));
+
+	/* TODO: implement reject PDP request + reason */
+	msgb_tv_put_be8(reply, GTPV0_IE_CAUSE, GTPV0_CAUSE_REQ_ACCEPTED);
+
+	if (GTPV0_CAUSE_REQ_ACCEPTED == GTPV0_CAUSE_REQ_ACCEPTED) {
+		/* Quality of service profile. 7.9.6 (Conditional) */
+
+		/* broken u24 */
+		msgb_tv_put_be24(reply, GTPV0_IE_QOS_PROFILE, pdp->qos_profile);
+
+		/* Recovery. 7.9.12 (Optional) */
+		msgb_tv_put_be8(reply, GTPV0_IE_RECOVERY, gsn->restart_counter);
+
+		int val = 0xa;
+
+		/* Flow label signalling. 7.9.15 (Conditional) */
+		msgb_tv_put_be16(reply, GTPV0_IE_FLOW_LABEL_DATA, val);
+		/* Flow label data I. 7.9.14 (Conditional) */
+		msgb_tv_put_be16(reply, GTPV0_IE_FLOW_LABEL_SIGNAL, val);
+
+		/* Charging ID. 7.9.17 (Conditional) */
+		msgb_tv_put_be32(reply, GTPV0_IE_CHARGING_ID, val);
+
+		/* TODO: Protocol configuration options. 7.9.22 (Optional) */
+		/* TODO: GGSN Address for signalling. 7.9.23 (Conditional) */
+		/* TODO: GGSN Address for user traffic. 7.9.23 (Conditional) */
+		build_gsn_addr(reply, gsn, pdp);
+
+		/* TODO: Charging Gateway Address. 7.9.25 (Optional) */
+		msgb_tlv_put_data(reply, GTPV0_IE_CHARGING_GW_ADDR, sizeof(val), &val);
+
+		/* TODO: Private extension. 7.9.26 (Optional) */
+	}
+
+	gtp0_header_end(gtp0h, reply);
+
+	return reply;
+}
+
+static struct msgb *gtp_pdp_update_req_handler(struct osmo_gsn *gsn,
+					       struct msgb *msg,
+					       struct osmo_gtp_err *err)
+{
+	struct msgb *reply;
+	struct tlv_parsed tp;
+	struct osmo_gtp_pdp _pdp, *pdp = &_pdp;
+	int ret;
+
+	memset(pdp, 0, sizeof(_pdp));
+
+	ret = tlv_parse(&tp, &gtp_pdp_update_req_attr_tlvdef,
+			msg->data + sizeof(struct gtp0_header),
+			msg->len - sizeof(struct gtp0_header), 0, 0);
+	if (ret < 0) {
+		LOGP(DLINP, LOGL_ERROR, "cannot parse TLVs: %u\n", ret);
+		return NULL;
+	}
+
+	/* Mandatory attributes:
+	 *
+	 * 1) 7.9.6. QoS profile. 32 bits. GSM 04.08
+	 * 2) 7.9.14. Flow label data. 16 bits
+	 * 3) 7.9.15. Flow label signalling. 16 bits
+	 * 4) 7.9.23. GSN address (variable length)
+	 * 5) 7.9.24. MSISDN (variable length)
+	 */
+	if (!TLVP_PRESENT(&tp, GTPV0_IE_QOS_PROFILE) 		||
+	    !TLVP_PRESENT(&tp, GTPV0_IE_FLOW_LABEL_DATA)	||
+	    !TLVP_PRESENT(&tp, GTPV0_IE_FLOW_LABEL_SIGNAL)	||
+	    !TLVP_PRESENT(&tp, GTPV0_IE_GSN_ADDR)) {
+		LOGP(DLINP, LOGL_ERROR, "missing mandatory TLV\n");
+		return NULL;
+	}
+
+	pdp->qos_profile = tv_get_be24(&tp, GTPV0_IE_QOS_PROFILE);
+
+	pdp->flow_label_data = tv_get_be16(&tp, GTPV0_IE_FLOW_LABEL_DATA);
+	pdp->flow_label_signal = tv_get_be16(&tp, GTPV0_IE_FLOW_LABEL_SIGNAL);
+	if (gtp0_parse_gsn_addr(&tp, pdp) < 0)
+		return NULL;
+
+	reply = gtp_pdp_ctx_update_resp(gsn, msg, pdp);
+	if (reply == NULL)
+		return NULL;
+
+	return reply;
+}
+
+static struct msgb *gtp_pdp_ctx_delete_resp(struct osmo_gsn *gsn,
+					    const struct msgb *msg,
+					    struct osmo_gtp_pdp *pdp)
+{
+	struct msgb *reply;
+	struct gtp0_header *gtp0h;
+
+	reply = msgb_alloc(OSMO_GTP_MTU, "gtp");
+	if (reply == NULL)
+		return NULL;
+
+	gtp0h = gtp0_header_put(reply, GTP_PDP_DELETE_RESP, gtp0h_seq(msg), 0,
+				gtp0h_tid(msg));
+
+	/* TODO: implement reject PDP request + reason */
+	msgb_tv_put_be8(reply, GTPV0_IE_CAUSE, GTPV0_CAUSE_REQ_ACCEPTED);
+
+	gtp0_header_end(gtp0h, reply);
+
+	return reply;
+}
+
+static struct msgb *gtp_pdp_delete_req_handler(struct osmo_gsn *gsn,
+					       struct msgb *msg,
+					       struct osmo_gtp_err *err)
+{
+	struct msgb *reply;
+	struct osmo_gtp_pdp _pdp, *pdp = &_pdp;
+
+	reply = gtp_pdp_ctx_delete_resp(gsn, msg, pdp);
+	if (reply == NULL)
+		return NULL;
+
+	return reply;
+}
+
+static struct osmo_gtp_handler gtp0_handler[GTP_TYPE_MAX] = {
+	[GTP_ECHO_REQ]		= {
+		.request	= true,
+		.handler	= gtp_echo_req_handler,
+	},
+	[GTP_PDP_CREATE_REQ]	= {
+		.request	= true,
+		.handler	= gtp_pdp_create_req_handler,
+	},
+	[GTP_PDP_UPDATE_REQ]	= {
+		.request	= true,
+		.handler	= gtp_pdp_update_req_handler,
+	},
+	[GTP_PDP_DELETE_REQ]	= {
+		.request	= true,
+		.handler	= gtp_pdp_delete_req_handler,
+	},
+};
+
+struct msgb *gtp0_recv(struct osmo_gsn *gsn, struct msgb *msg,
+		       struct osmo_gtp_err *err)
+{
+	struct gtp0_header *gtp0h = (struct gtp0_header *)msg->data;
+	struct msgb *reply = NULL;
+
+	if (gtp0h->version != 0) {
+		LOGP(DLINP, LOGL_ERROR, "wrong GTP packet version %u\n",
+		     gtp0h->version);
+		return gtp_unsupp(msg);
+	}
+	if (msg->len < sizeof(*gtp0h)) {
+		LOGP(DLINP, LOGL_ERROR, "GTPv0 packet too short msg->len %u\n",
+		     msg->len);
+		return NULL;
+	}
+	if (msg->len != ntohs(gtp0h->length) + sizeof(*gtp0h)) {
+		LOGP(DLINP, LOGL_ERROR,
+		     "truncated GTPv0 header msg->len %u != %u\n",
+		     msg->len, ntohs(gtp0h->length) + (int)sizeof(*gtp0h));
+		return NULL;
+	}
+
+	if ((gtp0h->type < GTP_TYPE_MAX) && gtp0_handler[gtp0h->type].handler)
+		reply = gtp0_handler[gtp0h->type].handler(gsn, msg, err);
+
+	return reply;
+}
+
+struct osmo_gtp_pdp *osmo_gtp_get_pdp(struct osmo_gtp_ctx *ctx)
+{
+	return ctx->pdp;
+}
+
+void osmo_gtp0_fprintf(FILE *fd, struct msgb *msg, uint32_t type)
+{
+	struct gtp0_header *gtp0h = (struct gtp0_header *)msg->data;
+	int i;
+
+	if (msg->len < sizeof(*gtp0h))
+		return;
+
+	switch (type) {
+	case OSMO_GTP_DEFAULT:
+		printf("version pt spare snn\n%x %x %x %x\n",
+		       gtp0h->version, gtp0h->pt, gtp0h->spare, gtp0h->snn);
+		printf("type seq tid flow\n%u %x %llx %x\n",
+		       gtp0h->type, ntohs(gtp0h->seq),
+		       (unsigned long long)be64toh(gtp0h->tid), gtp0h->flow);
+
+		for (i = sizeof(*gtp0h); i < msg->len; i++) {
+			printf("(%d) %.2x ", i, msg->data[i] & 0xff);
+		}
+		printf("\n");
+		break;
+	case OSMO_GTP_HEX:
+		for (i = 0; i < msg->len; i++)
+			printf("\\x%.2x", msg->data[i] & 0xff);
+
+		printf("\n");
+		break;
+	}
+}
diff --git a/src/libosmocore.c b/src/libosmocore.c
new file mode 100644
index 0000000..28b60d0
--- /dev/null
+++ b/src/libosmocore.c
@@ -0,0 +1,70 @@ 
+#include <osmocom/gsm/tlv.h>
+#include "libosmocore.h"
+#include <arpa/inet.h>
+
+void *msgb_tv_put_be8(struct msgb *msg, int type, uint8_t value)
+{
+	return msgb_tv_fixed_put(msg, type, 1, (uint8_t *)&value);
+}
+
+void *msgb_tv_put_be16(struct msgb *msg, int type, uint16_t value)
+{
+	value = htons(value);
+	return msgb_tv_fixed_put(msg, type, 2, (uint8_t *)&value);
+}
+
+void *msgb_tv_put_be24(struct msgb *msg, int type, uint32_t value)
+{
+	value = htonl(value & 0x00ffffff) >> 8;
+	return msgb_tv_fixed_put(msg, type, 3, (uint8_t *)&value);
+}
+
+void *msgb_tv_put_be32(struct msgb *msg, int type, uint32_t value)
+{
+	value = htonl(value);
+	return msgb_tv_fixed_put(msg, type, 4, (uint8_t *)&value);
+}
+
+void *msgb_tlv_put_be32(struct msgb *msg, int type, uint32_t value)
+{
+	value = htonl(value);
+	return msgb_tlv_put(msg, type, sizeof(uint32_t), (uint8_t *)&value);
+}
+
+void *msgb_tlv_put_data(struct msgb *msg, int type, int len, void *data)
+{
+	return msgb_tl16v_put(msg, type, len, data);
+}
+
+uint8_t tv_get_be8(struct tlv_parsed *tp, int type)
+{
+	return *((uint8_t *)TLVP_VAL(tp, type));
+}
+
+uint16_t tv_get_be16(struct tlv_parsed *tp, int type)
+{
+	return ntohs(*((uint16_t *)TLVP_VAL(tp, type)));
+}
+
+uint32_t tv_get_be24(struct tlv_parsed *tp, int type)
+{
+	uint32_t tmp = *((uint32_t *)TLVP_VAL(tp, type));
+
+	return ntohl(tmp) >> 8;
+}
+
+uint32_t tv_get_be32(struct tlv_parsed *tp, int type)
+{
+	return ntohl(*((uint32_t *)TLVP_VAL(tp, type)));
+}
+
+const char *tv_get_strdup(struct tlv_parsed *tp, int type)
+{
+	return strndup((const char *)TLVP_VAL(tp, type), TLVP_LEN(tp, type));
+}
+
+const char *tlv_get_strdup(struct tlv_parsed *tp, int type)
+{
+	/* XXX validate length */
+	return strndup((const char *)TLVP_VAL(tp, type), TLVP_LEN(tp, type));
+}
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 0000000..9dfe830
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,52 @@ 
+AM_CPPFLAGS = $(all_includes) -I$(top_srcdir)/include
+AM_CFLAGS=-Wall -g $(LIBOSMOCORE_CFLAGS) $(LIBOSMOGSM_CFLAGS) $(COVERAGE_CFLAGS)
+AM_LDFLAGS = $(COVERAGE_LDFLAGS)
+
+check_PROGRAMS = gtp_test
+
+gtp_test_SOURCES = gtp_test.c
+gtp_test_LDADD = $(top_builddir)/src/libosmogtp.la \
+		 $(LIBOSMOCORE_LIBS) $(LIBOSMOGSM_LIBS)
+
+# boilerplate for the tests
+# The `:;' works around a Bash 3.2 bug when the output is not writeable.
+$(srcdir)/package.m4: $(top_srcdir)/configure.ac
+	:;{ \
+               echo '# Signature of the current package.' && \
+               echo 'm4_define([AT_PACKAGE_NAME],' && \
+               echo '  [$(PACKAGE_NAME)])' && \
+               echo 'm4_define([AT_PACKAGE_TARNAME],' && \
+               echo '  [$(PACKAGE_TARNAME)])' && \
+               echo 'm4_define([AT_PACKAGE_VERSION],' && \
+               echo '  [$(PACKAGE_VERSION)])' && \
+               echo 'm4_define([AT_PACKAGE_STRING],' && \
+               echo '  [$(PACKAGE_STRING)])' && \
+               echo 'm4_define([AT_PACKAGE_BUGREPORT],' && \
+               echo '  [$(PACKAGE_BUGREPORT)])'; \
+               echo 'm4_define([AT_PACKAGE_URL],' && \
+               echo '  [$(PACKAGE_URL)])'; \
+             } >'$(srcdir)/package.m4'
+
+EXTRA_DIST	= testsuite.at $(srcdir)/package.m4 $(TESTSUITE) \
+		  gtp/gtp_test.ok
+
+TESTSUITE = $(srcdir)/testsuite
+
+DISTCLEANFILES = atconfig
+
+check-local: atconfig $(TESTSUITE)
+	$(SHELL) '$(TESTSUITE)' $(TESTSUITEFLAGS)
+
+installcheck-local: atconfig $(TESTSUITE)
+	$(SHELL) '$(TESTSUITE)' AUTOTEST_PATH='$(bindir)' \
+		$(TESTSUITEFLAGS)
+
+clean-local:
+	test ! -f '$(TESTSUITE)' || \
+		$(SHELL) '$(TESTSUITE)' --clean
+
+AUTOM4TE = $(SHELL) $(top_srcdir)/missing --run autom4te
+AUTOTEST = $(AUTOM4TE) --language=autotest
+$(TESTSUITE): $(srcdir)/testsuite.at $(srcdir)/package.m4
+	$(AUTOTEST) -I '$(srcdir)' -o $@.tmp $@.at
+	mv $@.tmp $@
diff --git a/tests/gtp/gtp_test.ok b/tests/gtp/gtp_test.ok
new file mode 100644
index 0000000..e69de29
diff --git a/tests/gtp_test.c b/tests/gtp_test.c
new file mode 100644
index 0000000..53be1da
--- /dev/null
+++ b/tests/gtp_test.c
@@ -0,0 +1,121 @@ 
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <gtp.h>
+#include <osmocom/core/msgb.h>
+#include <osmocom/gtp/gtp.h>
+#include <osmocom/gsm/tlv.h>
+
+#include "libosmocore.h"
+
+int main(void)
+{
+	struct osmo_gsn *gsn;
+	struct msgb *msg, *reply;
+	struct osmo_gtp_err err;
+	struct gtp0_header *gtp0h;
+
+	gsn = osmo_gsn_alloc();
+
+	msg = msgb_alloc(1500, "gtp-test");
+
+	/*
+	 * GTP echo request
+	 */
+	gtp0h = gtp0_header_put(msg, GTP_ECHO_REQ, 0x1234, 0, 0);
+	gtp0_header_end(gtp0h, msg);
+
+	reply = gtp0_recv(gsn, msg, &err);
+	if (reply)
+		osmo_gtp0_fprintf(stdout, reply, OSMO_GTP_HEX);
+	else
+		printf("failure\n");
+
+	msgb_free(reply);
+	msgb_reset(msg);
+
+	/*
+	 * GTP PDP context create request
+	 */
+	int val = 0x654321;
+
+	gtp0h = gtp0_header_put(msg, GTP_PDP_CREATE_REQ, 0x1234, 0, 10);
+	msgb_tv_put_be24(msg, GTPV0_IE_QOS_PROFILE, val);
+	msgb_tv_put_be8(msg, GTPV0_IE_SELECT_MODE, 0);
+	msgb_tv_put_be16(msg, GTPV0_IE_FLOW_LABEL_DATA, 1);
+	msgb_tv_put_be16(msg, GTPV0_IE_FLOW_LABEL_SIGNAL, 2);
+
+	const char *test = "test";
+
+	struct enduser_addr_ie_payload_ipv4 ie_ipv4 = {
+		.spare			= 0xf,
+		.pdp_org_type		= PDP_ORG_IETF,
+		.pdp_type_number 	= PDP_TYPE_IPV4,
+		.addr			= {
+			.s_addr = 0x04030201,
+		},
+	};
+	msgb_tlv_put_data(msg, GTPV0_IE_ENDUSER_ADDR, sizeof(ie_ipv4),
+			  &ie_ipv4);
+	msgb_tlv_put_data(msg, GTPV0_IE_AP_NAME, strlen(test), (uint8_t *)test);
+	val = 0x01020304;
+	msgb_tlv_put_data(msg, GTPV0_IE_GSN_ADDR, sizeof(val), (uint8_t *)&val);
+	val = 0x0a0b0c0d;
+
+	const char *msisdn = "650102030";
+	msgb_tlv_put_data(msg, GTPV0_IE_MSISDN, strlen(msisdn), (uint8_t *)msisdn);
+
+	gtp0_header_end(gtp0h, msg);
+
+	reply = gtp0_recv(gsn, msg, &err);
+	if (reply)
+		osmo_gtp0_fprintf(stdout, reply, OSMO_GTP_HEX);
+	else
+		printf("failure\n");
+
+	msgb_free(reply);
+	msgb_reset(msg);
+
+	/*
+	 * GTP Update PDP context request
+	 */
+	val = 0x654321;
+
+	gtp0h = gtp0_header_put(msg, GTP_PDP_UPDATE_REQ, 0x1234, 0, 10);
+	msgb_tv_put_be24(msg, GTPV0_IE_QOS_PROFILE, val);
+	/* Recovery 7.9.12 optional */
+	msgb_tv_put_be16(msg, GTPV0_IE_FLOW_LABEL_DATA, 1);
+	msgb_tv_put_be16(msg, GTPV0_IE_FLOW_LABEL_SIGNAL, 2);
+	val = 0x01020304;
+	msgb_tlv_put_data(msg, GTPV0_IE_GSN_ADDR, sizeof(val), (uint8_t *)&val);
+
+	gtp0_header_end(gtp0h, msg);
+
+	reply = gtp0_recv(gsn, msg, &err);
+	if (reply)
+		osmo_gtp0_fprintf(stdout, reply, OSMO_GTP_HEX);
+	else
+		printf("failure\n");
+
+	msgb_free(reply);
+	msgb_reset(msg);
+
+	/*
+	 * GTP delete PDP context request
+	 */
+	val = 0x654321;
+
+	gtp0h = gtp0_header_put(msg, GTP_PDP_DELETE_REQ, 0x1234, 0, 10);
+	gtp0_header_end(gtp0h, msg);
+
+	reply = gtp0_recv(gsn, msg, &err);
+	if (reply)
+		osmo_gtp0_fprintf(stdout, reply, OSMO_GTP_HEX);
+	else
+		printf("failure\n");
+
+	msgb_free(reply);
+	msgb_reset(msg);
+
+	return EXIT_SUCCESS;
+}
diff --git a/tests/gtp_test.ok b/tests/gtp_test.ok
new file mode 100644
index 0000000..6860a40
--- /dev/null
+++ b/tests/gtp_test.ok
@@ -0,0 +1,4 @@ 
+version pt spare snn
+0 1 7 0
+ntype seq tid flow
+2 3412 0 0(20) e (21) 0 
diff --git a/tests/testsuite.at b/tests/testsuite.at
new file mode 100644
index 0000000..fb55669
--- /dev/null
+++ b/tests/testsuite.at
@@ -0,0 +1,18 @@ 
+AT_INIT
+AT_BANNER([Regression tests.])
+
+# Example for tests.. copy and uncomment. This creates a new category
+# and test. It will copy the expected output to expout and then run
+# the given test. The stdout will be compared with the expout to determine
+# if the test was successfull.
+# AT_SETUP([NAME])
+# AT_KEYWORDS([NAME])
+# cat $abs_srcdir/NAME/NAME_test.ok > expout
+# AT_CHECK([$abs_top_builddir/tests/NAME/NAME_test], [], [expout])
+# AT_CLEANUP
+
+AT_SETUP([gtp_test])
+AT_KEYWORDS([gtp_test])
+cat $abs_srcdir/gtp_test.ok > expout
+AT_CHECK([$abs_top_builddir/tests/gtp_test], [], [expout], [ignore])
+AT_CLEANUP