[PATCH] initial commit for libosmo-gtp

This is merely a historical archive of years 2008-2021, before the migration to mailman3.

A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/OpenBSC@lists.osmocom.org/.

Neels Hofmeyr nhofmeyr at sysmocom.de
Thu Oct 15 13:32:02 UTC 2015


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 at gnumonks.org wrote:
> From: Pablo Neira Ayuso <pablo at 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!
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 819 bytes
Desc: Digital signature
URL: <http://lists.osmocom.org/pipermail/openbsc/attachments/20151015/c44bfbfc/attachment.bin>


More information about the OpenBSC mailing list