Import upstream version 0.9.13
diff --git a/src/gprs/Makefile.am b/src/gprs/Makefile.am
new file mode 100644
index 0000000..16c2200
--- /dev/null
+++ b/src/gprs/Makefile.am
@@ -0,0 +1,22 @@
+INCLUDES = $(all_includes) -I$(top_srcdir)/include -I$(top_builddir)
+AM_CFLAGS=-Wall -fno-strict-aliasing $(LIBOSMOCORE_CFLAGS) $(LIBOSMOVTY_CFLAGS) $(COVERAGE_CFLAGS)
+AM_LDFLAGS = $(LIBOSMOCORE_LIBS) $(LIBOSMOVTY_LIBS) $(COVERAGE_LDFLAGS)
+
+noinst_HEADERS = gprs_sndcp.h
+
+if HAVE_LIBGTP
+bin_PROGRAMS = osmo-gbproxy osmo-sgsn
+else
+bin_PROGRAMS = osmo-gbproxy
+endif
+
+osmo_gbproxy_SOURCES = gb_proxy.c gb_proxy_main.c gb_proxy_vty.c
+osmo_gbproxy_LDADD = 	$(top_builddir)/src/libgb/libgb.a \
+			$(top_builddir)/src/libcommon/libcommon.a
+
+osmo_sgsn_SOURCES =	gprs_gmm.c gprs_sgsn.c gprs_sndcp.c gprs_sndcp_vty.c \
+			sgsn_main.c sgsn_vty.c sgsn_libgtp.c \
+			gprs_llc.c gprs_llc_vty.c crc24.c
+osmo_sgsn_LDADD = 	$(top_builddir)/src/libgb/libgb.a \
+			$(top_builddir)/src/libcommon/libcommon.a \
+			-lgtp
diff --git a/src/gprs/Makefile.in b/src/gprs/Makefile.in
new file mode 100644
index 0000000..99ea739
--- /dev/null
+++ b/src/gprs/Makefile.in
@@ -0,0 +1,522 @@
+# Makefile.in generated by automake 1.11.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002,
+# 2003, 2004, 2005, 2006, 2007, 2008, 2009  Free Software Foundation,
+# Inc.
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+VPATH = @srcdir@
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+@HAVE_LIBGTP_FALSE@bin_PROGRAMS = osmo-gbproxy$(EXEEXT)
+@HAVE_LIBGTP_TRUE@bin_PROGRAMS = osmo-gbproxy$(EXEEXT) \
+@HAVE_LIBGTP_TRUE@	osmo-sgsn$(EXEEXT)
+subdir = src/gprs
+DIST_COMMON = $(noinst_HEADERS) $(srcdir)/Makefile.am \
+	$(srcdir)/Makefile.in
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/configure.in
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+	$(ACLOCAL_M4)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/bscconfig.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__installdirs = "$(DESTDIR)$(bindir)"
+PROGRAMS = $(bin_PROGRAMS)
+am_osmo_gbproxy_OBJECTS = gb_proxy.$(OBJEXT) gb_proxy_main.$(OBJEXT) \
+	gb_proxy_vty.$(OBJEXT)
+osmo_gbproxy_OBJECTS = $(am_osmo_gbproxy_OBJECTS)
+osmo_gbproxy_DEPENDENCIES = $(top_builddir)/src/libgb/libgb.a \
+	$(top_builddir)/src/libcommon/libcommon.a
+am_osmo_sgsn_OBJECTS = gprs_gmm.$(OBJEXT) gprs_sgsn.$(OBJEXT) \
+	gprs_sndcp.$(OBJEXT) gprs_sndcp_vty.$(OBJEXT) \
+	sgsn_main.$(OBJEXT) sgsn_vty.$(OBJEXT) sgsn_libgtp.$(OBJEXT) \
+	gprs_llc.$(OBJEXT) gprs_llc_vty.$(OBJEXT) crc24.$(OBJEXT)
+osmo_sgsn_OBJECTS = $(am_osmo_sgsn_OBJECTS)
+osmo_sgsn_DEPENDENCIES = $(top_builddir)/src/libgb/libgb.a \
+	$(top_builddir)/src/libcommon/libcommon.a
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__depfiles_maybe = depfiles
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+	$(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_$(V))
+am__v_CC_ = $(am__v_CC_$(AM_DEFAULT_VERBOSITY))
+am__v_CC_0 = @echo "  CC    " $@;
+AM_V_at = $(am__v_at_$(V))
+am__v_at_ = $(am__v_at_$(AM_DEFAULT_VERBOSITY))
+am__v_at_0 = @
+CCLD = $(CC)
+LINK = $(CCLD) $(AM_CFLAGS) $(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_$(V))
+am__v_CCLD_ = $(am__v_CCLD_$(AM_DEFAULT_VERBOSITY))
+am__v_CCLD_0 = @echo "  CCLD  " $@;
+AM_V_GEN = $(am__v_GEN_$(V))
+am__v_GEN_ = $(am__v_GEN_$(AM_DEFAULT_VERBOSITY))
+am__v_GEN_0 = @echo "  GEN   " $@;
+SOURCES = $(osmo_gbproxy_SOURCES) $(osmo_sgsn_SOURCES)
+DIST_SOURCES = $(osmo_gbproxy_SOURCES) $(osmo_sgsn_SOURCES)
+HEADERS = $(noinst_HEADERS)
+ETAGS = etags
+CTAGS = ctags
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+COVERAGE_CFLAGS = @COVERAGE_CFLAGS@
+COVERAGE_LDFLAGS = @COVERAGE_LDFLAGS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+GPRS_LIBGTP = @GPRS_LIBGTP@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+LDFLAGS = @LDFLAGS@
+LIBOBJS = @LIBOBJS@
+LIBOSMOCORE_CFLAGS = @LIBOSMOCORE_CFLAGS@
+LIBOSMOCORE_LIBS = @LIBOSMOCORE_LIBS@
+LIBOSMOSCCP_CFLAGS = @LIBOSMOSCCP_CFLAGS@
+LIBOSMOSCCP_LIBS = @LIBOSMOSCCP_LIBS@
+LIBOSMOVTY_CFLAGS = @LIBOSMOVTY_CFLAGS@
+LIBOSMOVTY_LIBS = @LIBOSMOVTY_LIBS@
+LIBS = @LIBS@
+LTLIBOBJS = @LTLIBOBJS@
+MAKEINFO = @MAKEINFO@
+MKDIR_P = @MKDIR_P@
+OBJEXT = @OBJEXT@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+RANLIB = @RANLIB@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+STRIP = @STRIP@
+SYMBOL_VISIBILITY = @SYMBOL_VISIBILITY@
+VERSION = @VERSION@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_CC = @ac_ct_CC@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build_alias = @build_alias@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host_alias = @host_alias@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+INCLUDES = $(all_includes) -I$(top_srcdir)/include -I$(top_builddir)
+AM_CFLAGS = -Wall -fno-strict-aliasing $(LIBOSMOCORE_CFLAGS) $(LIBOSMOVTY_CFLAGS) $(COVERAGE_CFLAGS)
+AM_LDFLAGS = $(LIBOSMOCORE_LIBS) $(LIBOSMOVTY_LIBS) $(COVERAGE_LDFLAGS)
+noinst_HEADERS = gprs_sndcp.h
+osmo_gbproxy_SOURCES = gb_proxy.c gb_proxy_main.c gb_proxy_vty.c
+osmo_gbproxy_LDADD = $(top_builddir)/src/libgb/libgb.a \
+			$(top_builddir)/src/libcommon/libcommon.a
+
+osmo_sgsn_SOURCES = gprs_gmm.c gprs_sgsn.c gprs_sndcp.c gprs_sndcp_vty.c \
+			sgsn_main.c sgsn_vty.c sgsn_libgtp.c \
+			gprs_llc.c gprs_llc_vty.c crc24.c
+
+osmo_sgsn_LDADD = $(top_builddir)/src/libgb/libgb.a \
+			$(top_builddir)/src/libcommon/libcommon.a \
+			-lgtp
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .o .obj
+$(srcdir)/Makefile.in:  $(srcdir)/Makefile.am  $(am__configure_deps)
+	@for dep in $?; do \
+	  case '$(am__configure_deps)' in \
+	    *$$dep*) \
+	      ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+	        && { if test -f $@; then exit 0; else break; fi; }; \
+	      exit 1;; \
+	  esac; \
+	done; \
+	echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu src/gprs/Makefile'; \
+	$(am__cd) $(top_srcdir) && \
+	  $(AUTOMAKE) --gnu src/gprs/Makefile
+.PRECIOUS: Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+	@case '$?' in \
+	  *config.status*) \
+	    cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+	  *) \
+	    echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \
+	    cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \
+	esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure:  $(am__configure_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4):  $(am__aclocal_m4_deps)
+	cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+install-binPROGRAMS: $(bin_PROGRAMS)
+	@$(NORMAL_INSTALL)
+	test -z "$(bindir)" || $(MKDIR_P) "$(DESTDIR)$(bindir)"
+	@list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+	for p in $$list; do echo "$$p $$p"; done | \
+	sed 's/$(EXEEXT)$$//' | \
+	while read p p1; do if test -f $$p; \
+	  then echo "$$p"; echo "$$p"; else :; fi; \
+	done | \
+	sed -e 'p;s,.*/,,;n;h' -e 's|.*|.|' \
+	    -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+	sed 'N;N;N;s,\n, ,g' | \
+	$(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+	  { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+	    if ($$2 == $$4) files[d] = files[d] " " $$1; \
+	    else { print "f", $$3 "/" $$4, $$1; } } \
+	  END { for (d in files) print "f", d, files[d] }' | \
+	while read type dir files; do \
+	    if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+	    test -z "$$files" || { \
+	      echo " $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(bindir)$$dir'"; \
+	      $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(bindir)$$dir" || exit $$?; \
+	    } \
+	; done
+
+uninstall-binPROGRAMS:
+	@$(NORMAL_UNINSTALL)
+	@list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+	files=`for p in $$list; do echo "$$p"; done | \
+	  sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+	      -e 's/$$/$(EXEEXT)/' `; \
+	test -n "$$list" || exit 0; \
+	echo " ( cd '$(DESTDIR)$(bindir)' && rm -f" $$files ")"; \
+	cd "$(DESTDIR)$(bindir)" && rm -f $$files
+
+clean-binPROGRAMS:
+	-test -z "$(bin_PROGRAMS)" || rm -f $(bin_PROGRAMS)
+osmo-gbproxy$(EXEEXT): $(osmo_gbproxy_OBJECTS) $(osmo_gbproxy_DEPENDENCIES) 
+	@rm -f osmo-gbproxy$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(osmo_gbproxy_OBJECTS) $(osmo_gbproxy_LDADD) $(LIBS)
+osmo-sgsn$(EXEEXT): $(osmo_sgsn_OBJECTS) $(osmo_sgsn_DEPENDENCIES) 
+	@rm -f osmo-sgsn$(EXEEXT)
+	$(AM_V_CCLD)$(LINK) $(osmo_sgsn_OBJECTS) $(osmo_sgsn_LDADD) $(LIBS)
+
+mostlyclean-compile:
+	-rm -f *.$(OBJEXT)
+
+distclean-compile:
+	-rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/crc24.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/gb_proxy.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/gb_proxy_main.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/gb_proxy_vty.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/gprs_gmm.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/gprs_llc.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/gprs_llc_vty.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/gprs_sgsn.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/gprs_sndcp.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/gprs_sndcp_vty.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sgsn_libgtp.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sgsn_main.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sgsn_vty.Po@am__quote@
+
+.c.o:
+@am__fastdepCC_TRUE@	$(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@	$(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@am__fastdepCC_FALSE@	$(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@	$(COMPILE) -c $<
+
+.c.obj:
+@am__fastdepCC_TRUE@	$(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@	$(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@am__fastdepCC_FALSE@	$(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@	DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@	$(COMPILE) -c `$(CYGPATH_W) '$<'`
+
+ID: $(HEADERS) $(SOURCES) $(LISP) $(TAGS_FILES)
+	list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \
+	unique=`for i in $$list; do \
+	    if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+	  done | \
+	  $(AWK) '{ files[$$0] = 1; nonempty = 1; } \
+	      END { if (nonempty) { for (i in files) print i; }; }'`; \
+	mkid -fID $$unique
+tags: TAGS
+
+TAGS:  $(HEADERS) $(SOURCES)  $(TAGS_DEPENDENCIES) \
+		$(TAGS_FILES) $(LISP)
+	set x; \
+	here=`pwd`; \
+	list='$(SOURCES) $(HEADERS)  $(LISP) $(TAGS_FILES)'; \
+	unique=`for i in $$list; do \
+	    if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+	  done | \
+	  $(AWK) '{ files[$$0] = 1; nonempty = 1; } \
+	      END { if (nonempty) { for (i in files) print i; }; }'`; \
+	shift; \
+	if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+	  test -n "$$unique" || unique=$$empty_fix; \
+	  if test $$# -gt 0; then \
+	    $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+	      "$$@" $$unique; \
+	  else \
+	    $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+	      $$unique; \
+	  fi; \
+	fi
+ctags: CTAGS
+CTAGS:  $(HEADERS) $(SOURCES)  $(TAGS_DEPENDENCIES) \
+		$(TAGS_FILES) $(LISP)
+	list='$(SOURCES) $(HEADERS)  $(LISP) $(TAGS_FILES)'; \
+	unique=`for i in $$list; do \
+	    if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+	  done | \
+	  $(AWK) '{ files[$$0] = 1; nonempty = 1; } \
+	      END { if (nonempty) { for (i in files) print i; }; }'`; \
+	test -z "$(CTAGS_ARGS)$$unique" \
+	  || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+	     $$unique
+
+GTAGS:
+	here=`$(am__cd) $(top_builddir) && pwd` \
+	  && $(am__cd) $(top_srcdir) \
+	  && gtags -i $(GTAGS_ARGS) "$$here"
+
+distclean-tags:
+	-rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(DISTFILES)
+	@srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+	list='$(DISTFILES)'; \
+	  dist_files=`for file in $$list; do echo $$file; done | \
+	  sed -e "s|^$$srcdirstrip/||;t" \
+	      -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+	case $$dist_files in \
+	  */*) $(MKDIR_P) `echo "$$dist_files" | \
+			   sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+			   sort -u` ;; \
+	esac; \
+	for file in $$dist_files; do \
+	  if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+	  if test -d $$d/$$file; then \
+	    dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+	    if test -d "$(distdir)/$$file"; then \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+	      cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+	      find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+	    fi; \
+	    cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+	  else \
+	    test -f "$(distdir)/$$file" \
+	    || cp -p $$d/$$file "$(distdir)/$$file" \
+	    || exit 1; \
+	  fi; \
+	done
+check-am: all-am
+check: check-am
+all-am: Makefile $(PROGRAMS) $(HEADERS)
+installdirs:
+	for dir in "$(DESTDIR)$(bindir)"; do \
+	  test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+	done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+	@$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+	$(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+	  install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+	  `test -z '$(STRIP)' || \
+	    echo "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'"` install
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+	-test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+	-test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+	@echo "This command is intended for maintainers to use"
+	@echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-binPROGRAMS clean-generic mostlyclean-am
+
+distclean: distclean-am
+	-rm -rf ./$(DEPDIR)
+	-rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+	distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-binPROGRAMS
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+	-rm -rf ./$(DEPDIR)
+	-rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-binPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS all all-am check check-am clean clean-binPROGRAMS \
+	clean-generic ctags distclean distclean-compile \
+	distclean-generic distclean-tags distdir dvi dvi-am html \
+	html-am info info-am install install-am install-binPROGRAMS \
+	install-data install-data-am install-dvi install-dvi-am \
+	install-exec install-exec-am install-html install-html-am \
+	install-info install-info-am install-man install-pdf \
+	install-pdf-am install-ps install-ps-am install-strip \
+	installcheck installcheck-am installdirs maintainer-clean \
+	maintainer-clean-generic mostlyclean mostlyclean-compile \
+	mostlyclean-generic pdf pdf-am ps ps-am tags uninstall \
+	uninstall-am uninstall-binPROGRAMS
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/gprs/crc24.c b/src/gprs/crc24.c
new file mode 100644
index 0000000..4d65e6e
--- /dev/null
+++ b/src/gprs/crc24.c
@@ -0,0 +1,68 @@
+/* GPRS LLC CRC-24 Implementation */
+
+/* (C) 2008-2009 by Harald Welte <laforge@gnumonks.org>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <sys/types.h>
+#include <openbsc/crc24.h>
+
+/* CRC24 table - FCS */
+static const u_int32_t tbl_crc24[256] = {
+	0x00000000, 0x00d6a776, 0x00f64557, 0x0020e221, 0x00b78115, 0x00612663, 0x0041c442, 0x00976334,
+	0x00340991, 0x00e2aee7, 0x00c24cc6, 0x0014ebb0, 0x00838884, 0x00552ff2, 0x0075cdd3, 0x00a36aa5,
+	0x00681322, 0x00beb454, 0x009e5675, 0x0048f103, 0x00df9237, 0x00093541, 0x0029d760, 0x00ff7016,
+	0x005c1ab3, 0x008abdc5, 0x00aa5fe4, 0x007cf892, 0x00eb9ba6, 0x003d3cd0, 0x001ddef1, 0x00cb7987,
+	0x00d02644, 0x00068132, 0x00266313, 0x00f0c465, 0x0067a751, 0x00b10027, 0x0091e206, 0x00474570,
+	0x00e42fd5, 0x003288a3, 0x00126a82, 0x00c4cdf4, 0x0053aec0, 0x008509b6, 0x00a5eb97, 0x00734ce1,
+	0x00b83566, 0x006e9210, 0x004e7031, 0x0098d747, 0x000fb473, 0x00d91305, 0x00f9f124, 0x002f5652,
+	0x008c3cf7, 0x005a9b81, 0x007a79a0, 0x00acded6, 0x003bbde2, 0x00ed1a94, 0x00cdf8b5, 0x001b5fc3,
+	0x00fb4733, 0x002de045, 0x000d0264, 0x00dba512, 0x004cc626, 0x009a6150, 0x00ba8371, 0x006c2407,
+	0x00cf4ea2, 0x0019e9d4, 0x00390bf5, 0x00efac83, 0x0078cfb7, 0x00ae68c1, 0x008e8ae0, 0x00582d96,
+	0x00935411, 0x0045f367, 0x00651146, 0x00b3b630, 0x0024d504, 0x00f27272, 0x00d29053, 0x00043725,
+	0x00a75d80, 0x0071faf6, 0x005118d7, 0x0087bfa1, 0x0010dc95, 0x00c67be3, 0x00e699c2, 0x00303eb4,
+	0x002b6177, 0x00fdc601, 0x00dd2420, 0x000b8356, 0x009ce062, 0x004a4714, 0x006aa535, 0x00bc0243,
+	0x001f68e6, 0x00c9cf90, 0x00e92db1, 0x003f8ac7, 0x00a8e9f3, 0x007e4e85, 0x005eaca4, 0x00880bd2,
+	0x00437255, 0x0095d523, 0x00b53702, 0x00639074, 0x00f4f340, 0x00225436, 0x0002b617, 0x00d41161,
+	0x00777bc4, 0x00a1dcb2, 0x00813e93, 0x005799e5, 0x00c0fad1, 0x00165da7, 0x0036bf86, 0x00e018f0,
+	0x00ad85dd, 0x007b22ab, 0x005bc08a, 0x008d67fc, 0x001a04c8, 0x00cca3be, 0x00ec419f, 0x003ae6e9,
+	0x00998c4c, 0x004f2b3a, 0x006fc91b, 0x00b96e6d, 0x002e0d59, 0x00f8aa2f, 0x00d8480e, 0x000eef78,
+	0x00c596ff, 0x00133189, 0x0033d3a8, 0x00e574de, 0x007217ea, 0x00a4b09c, 0x008452bd, 0x0052f5cb,
+	0x00f19f6e, 0x00273818, 0x0007da39, 0x00d17d4f, 0x00461e7b, 0x0090b90d, 0x00b05b2c, 0x0066fc5a,
+	0x007da399, 0x00ab04ef, 0x008be6ce, 0x005d41b8, 0x00ca228c, 0x001c85fa, 0x003c67db, 0x00eac0ad,
+	0x0049aa08, 0x009f0d7e, 0x00bfef5f, 0x00694829, 0x00fe2b1d, 0x00288c6b, 0x00086e4a, 0x00dec93c,
+	0x0015b0bb, 0x00c317cd, 0x00e3f5ec, 0x0035529a, 0x00a231ae, 0x007496d8, 0x005474f9, 0x0082d38f,
+	0x0021b92a, 0x00f71e5c, 0x00d7fc7d, 0x00015b0b, 0x0096383f, 0x00409f49, 0x00607d68, 0x00b6da1e,
+	0x0056c2ee, 0x00806598, 0x00a087b9, 0x007620cf, 0x00e143fb, 0x0037e48d, 0x001706ac, 0x00c1a1da,
+	0x0062cb7f, 0x00b46c09, 0x00948e28, 0x0042295e, 0x00d54a6a, 0x0003ed1c, 0x00230f3d, 0x00f5a84b,
+	0x003ed1cc, 0x00e876ba, 0x00c8949b, 0x001e33ed, 0x008950d9, 0x005ff7af, 0x007f158e, 0x00a9b2f8,
+	0x000ad85d, 0x00dc7f2b, 0x00fc9d0a, 0x002a3a7c, 0x00bd5948, 0x006bfe3e, 0x004b1c1f, 0x009dbb69,
+	0x0086e4aa, 0x005043dc, 0x0070a1fd, 0x00a6068b, 0x003165bf, 0x00e7c2c9, 0x00c720e8, 0x0011879e,
+	0x00b2ed3b, 0x00644a4d, 0x0044a86c, 0x00920f1a, 0x00056c2e, 0x00d3cb58, 0x00f32979, 0x00258e0f,
+	0x00eef788, 0x003850fe, 0x0018b2df, 0x00ce15a9, 0x0059769d, 0x008fd1eb, 0x00af33ca, 0x007994bc,
+	0x00dafe19, 0x000c596f, 0x002cbb4e, 0x00fa1c38, 0x006d7f0c, 0x00bbd87a, 0x009b3a5b, 0x004d9d2d
+};
+
+#define INIT_CRC24	0xffffff
+
+u_int32_t crc24_calc(u_int32_t fcs, u_int8_t *cp, unsigned int len)
+{
+	while (len--)
+		fcs = (fcs >> 8) ^ tbl_crc24[(fcs ^ *cp++) & 0xff];
+	return fcs;
+}
diff --git a/src/gprs/gb_proxy.c b/src/gprs/gb_proxy.c
new file mode 100644
index 0000000..8df93a9
--- /dev/null
+++ b/src/gprs/gb_proxy.c
@@ -0,0 +1,684 @@
+/* NS-over-IP proxy */
+
+/* (C) 2010 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2010 by On-Waves
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <getopt.h>
+#include <errno.h>
+#include <sys/fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <arpa/inet.h>
+
+#include <osmocore/talloc.h>
+#include <osmocore/select.h>
+
+#include <openbsc/signal.h>
+#include <openbsc/debug.h>
+#include <openbsc/gprs_ns.h>
+#include <openbsc/gprs_bssgp.h>
+#include <openbsc/gb_proxy.h>
+
+struct gbprox_peer {
+	struct llist_head list;
+
+	/* NS-VC over which we send/receive data to this BVC */
+	struct gprs_nsvc *nsvc;
+
+	/* BVCI used for Point-to-Point to this peer */
+	uint16_t bvci;
+	int blocked;
+
+	/* Routeing Area that this peer is part of (raw 04.08 encoding) */
+	uint8_t ra[6];
+};
+
+/* Linked list of all Gb peers (except SGSN) */
+static LLIST_HEAD(gbprox_bts_peers);
+
+/* Find the gbprox_peer by its BVCI */
+static struct gbprox_peer *peer_by_bvci(uint16_t bvci)
+{
+	struct gbprox_peer *peer;
+	llist_for_each_entry(peer, &gbprox_bts_peers, list) {
+		if (peer->bvci == bvci)
+			return peer;
+	}
+	return NULL;
+}
+
+static struct gbprox_peer *peer_by_nsvc(struct gprs_nsvc *nsvc)
+{
+	struct gbprox_peer *peer;
+	llist_for_each_entry(peer, &gbprox_bts_peers, list) {
+		if (peer->nsvc == nsvc)
+			return peer;
+	}
+	return NULL;
+}
+
+/* look-up a peer by its Routeing Area Code (RAC) */
+static struct gbprox_peer *peer_by_rac(const uint8_t *ra)
+{
+	struct gbprox_peer *peer;
+	llist_for_each_entry(peer, &gbprox_bts_peers, list) {
+		if (!memcmp(peer->ra, ra, 6))
+			return peer;
+	}
+	return NULL;
+}
+
+/* look-up a peer by its Location Area Code (LAC) */
+static struct gbprox_peer *peer_by_lac(const uint8_t *la)
+{
+	struct gbprox_peer *peer;
+	llist_for_each_entry(peer, &gbprox_bts_peers, list) {
+		if (!memcmp(peer->ra, la, 5))
+			return peer;
+	}
+	return NULL;
+}
+
+static struct gbprox_peer *peer_alloc(uint16_t bvci)
+{
+	struct gbprox_peer *peer;
+
+	peer = talloc_zero(tall_bsc_ctx, struct gbprox_peer);
+	if (!peer)
+		return NULL;
+
+	peer->bvci = bvci;
+	llist_add(&peer->list, &gbprox_bts_peers);
+
+	return peer;
+}
+
+static void peer_free(struct gbprox_peer *peer)
+{
+	llist_del(&peer->list);
+	talloc_free(peer);
+}
+
+/* FIXME: this needs to go to libosmocore/msgb.c */
+static struct msgb *msgb_copy(const struct msgb *msg, const char *name)
+{
+	struct openbsc_msgb_cb *old_cb, *new_cb;
+	struct msgb *new_msg;
+
+	new_msg = msgb_alloc(msg->data_len, name);
+	if (!new_msg)
+		return NULL;
+
+	/* copy data */
+	memcpy(new_msg->_data, msg->_data, new_msg->data_len);
+
+	/* copy header */
+	new_msg->len = msg->len;
+	new_msg->data += msg->data - msg->_data;
+	new_msg->head += msg->head - msg->_data;
+	new_msg->tail += msg->tail - msg->_data;
+
+	new_msg->l1h = new_msg->_data + (msg->l1h - msg->_data);
+	new_msg->l2h = new_msg->_data + (msg->l2h - msg->_data);
+	new_msg->l3h = new_msg->_data + (msg->l3h - msg->_data);
+	new_msg->l4h = new_msg->_data + (msg->l4h - msg->_data);
+
+	/* copy GB specific data */
+	old_cb = OBSC_MSGB_CB(msg);
+	new_cb = OBSC_MSGB_CB(new_msg);
+
+	new_cb->bssgph = new_msg->_data + (old_cb->bssgph - msg->_data);
+	new_cb->llch = new_msg->_data + (old_cb->llch - msg->_data);
+
+	/* bssgp_cell_id is a pointer into the old msgb, so we need to make
+	 * it a pointer into the new msgb */
+	new_cb->bssgp_cell_id = new_msg->_data + (old_cb->bssgp_cell_id - msg->_data);
+	new_cb->nsei = old_cb->nsei;
+	new_cb->bvci = old_cb->bvci;
+	new_cb->tlli = old_cb->tlli;
+
+	return new_msg;
+}
+
+/* strip off the NS header */
+static void strip_ns_hdr(struct msgb *msg)
+{
+	int strip_len = msgb_bssgph(msg) - msg->data;
+	msgb_pull(msg, strip_len);
+}
+
+/* feed a message down the NS-VC associated with the specified peer */
+static int gbprox_relay2sgsn(struct msgb *old_msg, uint16_t ns_bvci)
+{
+	/* create a copy of the message so the old one can
+	 * be free()d safely when we return from gbprox_rcvmsg() */
+	struct msgb *msg = msgb_copy(old_msg, "msgb_relay2sgsn");
+
+	DEBUGP(DGPRS, "NSEI=%u proxying BTS->SGSN (NS_BVCI=%u, NSEI=%u)\n",
+		msgb_nsei(msg), ns_bvci, gbcfg.nsip_sgsn_nsei);
+
+	msgb_bvci(msg) = ns_bvci;
+	msgb_nsei(msg) = gbcfg.nsip_sgsn_nsei;
+
+	strip_ns_hdr(msg);
+
+	return gprs_ns_sendmsg(bssgp_nsi, msg);
+}
+
+/* feed a message down the NS-VC associated with the specified peer */
+static int gbprox_relay2peer(struct msgb *old_msg, struct gbprox_peer *peer,
+			  uint16_t ns_bvci)
+{
+	/* create a copy of the message so the old one can
+	 * be free()d safely when we return from gbprox_rcvmsg() */
+	struct msgb *msg = msgb_copy(old_msg, "msgb_relay2peer");
+
+	DEBUGP(DGPRS, "NSEI=%u proxying SGSN->BSS (NS_BVCI=%u, NSEI=%u)\n",
+		msgb_nsei(msg), ns_bvci, peer->nsvc->nsei);
+
+	msgb_bvci(msg) = ns_bvci;
+	msgb_nsei(msg) = peer->nsvc->nsei;
+
+	/* Strip the old NS header, it will be replaced with a new one */
+	strip_ns_hdr(msg);
+
+	return gprs_ns_sendmsg(bssgp_nsi, msg);
+}
+
+static int block_unblock_peer(uint16_t ptp_bvci, uint8_t pdu_type)
+{
+	struct gbprox_peer *peer;
+
+	peer = peer_by_bvci(ptp_bvci);
+	if (!peer) {
+		LOGP(DGPRS, LOGL_ERROR, "BVCI=%u: Cannot find BSS\n",
+			ptp_bvci);
+		return -ENOENT;
+	}
+
+	switch (pdu_type) {
+	case BSSGP_PDUT_BVC_BLOCK_ACK:
+		peer->blocked = 1;
+		break;
+	case BSSGP_PDUT_BVC_UNBLOCK_ACK:
+		peer->blocked = 0;
+		break;
+	default:
+		break;
+	}
+	return 0;
+}
+
+/* Send a message to a peer identified by ptp_bvci but using ns_bvci
+ * in the NS hdr */
+static int gbprox_relay2bvci(struct msgb *msg, uint16_t ptp_bvci,
+			  uint16_t ns_bvci)
+{
+	struct gbprox_peer *peer;
+
+	peer = peer_by_bvci(ptp_bvci);
+	if (!peer) {
+		LOGP(DGPRS, LOGL_ERROR, "BVCI=%u: Cannot find BSS\n",
+			ptp_bvci);
+		return -ENOENT;
+	}
+
+	return gbprox_relay2peer(msg, peer, ns_bvci);
+}
+
+/* Receive an incoming signalling message from a BSS-side NS-VC */
+static int gbprox_rx_sig_from_bss(struct msgb *msg, struct gprs_nsvc *nsvc,
+				  uint16_t ns_bvci)
+{
+	struct bssgp_normal_hdr *bgph = (struct bssgp_normal_hdr *) msgb_bssgph(msg);
+	struct tlv_parsed tp;
+	uint8_t pdu_type = bgph->pdu_type;
+	int data_len = msgb_bssgp_len(msg) - sizeof(*bgph);
+	struct gbprox_peer *from_peer;
+	struct gprs_ra_id raid;
+
+	if (ns_bvci != 0 && ns_bvci != 1) {
+		LOGP(DGPRS, LOGL_NOTICE, "NSEI=%u BVCI=%u is not signalling\n",
+			nsvc->nsei, ns_bvci);
+		return -EINVAL;
+	}
+
+	/* we actually should never see those two for BVCI == 0, but double-check
+	 * just to make sure  */
+	if (pdu_type == BSSGP_PDUT_UL_UNITDATA ||
+	    pdu_type == BSSGP_PDUT_DL_UNITDATA) {
+		LOGP(DGPRS, LOGL_NOTICE, "NSEI=%u UNITDATA not allowed in "
+			"signalling\n", nsvc->nsei);
+		return -EINVAL;
+	}
+
+	bssgp_tlv_parse(&tp, bgph->data, data_len);
+
+	switch (pdu_type) {
+	case BSSGP_PDUT_SUSPEND:
+	case BSSGP_PDUT_RESUME:
+		/* We implement RAC snooping during SUSPEND/RESUME, since
+		 * it establishes a relationsip between BVCI/peer and the
+		 * routeing area code.  The snooped information is then
+		 * used for routing the {SUSPEND,RESUME}_[N]ACK back to
+		 * the correct BSSGP */
+		if (!TLVP_PRESENT(&tp, BSSGP_IE_ROUTEING_AREA))
+			goto err_mand_ie;
+		from_peer = peer_by_nsvc(nsvc);
+		if (!from_peer)
+			goto err_no_peer;
+		memcpy(from_peer->ra, TLVP_VAL(&tp, BSSGP_IE_ROUTEING_AREA),
+			sizeof(from_peer->ra));
+		gsm48_parse_ra(&raid, from_peer->ra);
+		LOGP(DGPRS, LOGL_INFO, "NSEI=%u BSSGP SUSPEND/RESUME "
+			"RAC snooping: RAC %u-%u-%u-%u behind BVCI=%u, "
+			"NSVCI=%u\n",nsvc->nsei, raid.mcc, raid.mnc, raid.lac,
+			raid.rac , from_peer->bvci, nsvc->nsvci);
+		/* FIXME: This only supports one BSS per RA */
+		break;
+	case BSSGP_PDUT_BVC_RESET:
+		/* If we receive a BVC reset on the signalling endpoint, we
+		 * don't want the SGSN to reset, as the signalling endpoint
+		 * is common for all point-to-point BVCs (and thus all BTS) */
+		if (TLVP_PRESENT(&tp, BSSGP_IE_BVCI)) {
+			uint16_t bvci = ntohs(*(uint16_t *)TLVP_VAL(&tp, BSSGP_IE_BVCI));
+			LOGP(DGPRS, LOGL_INFO, "NSEI=%u Rx BVC RESET (BVCI=%u)\n",
+				nsvc->nsei, bvci);
+			if (bvci == 0) {
+				/* FIXME: only do this if SGSN is alive! */
+				LOGP(DGPRS, LOGL_INFO, "NSEI=%u Tx fake "
+					"BVC RESET ACK of BVCI=0\n", nsvc->nsei);
+				return bssgp_tx_simple_bvci(BSSGP_PDUT_BVC_RESET_ACK,
+							    nsvc->nsei, 0, ns_bvci);
+			}
+			from_peer = peer_by_bvci(bvci);
+			if (!from_peer) {
+				/* if a PTP-BVC is reset, and we don't know that
+				 * PTP-BVCI yet, we should allocate a new peer */
+				LOGP(DGPRS, LOGL_INFO, "Allocationg new peer for "
+				     "BVCI=%u via NSVCI=%u/NSEI=%u\n", bvci,
+				     nsvc->nsvci, nsvc->nsei);
+				from_peer = peer_alloc(bvci);
+				from_peer->nsvc = nsvc;
+			}
+			if (TLVP_PRESENT(&tp, BSSGP_IE_CELL_ID)) {
+				struct gprs_ra_id raid;
+				/* We have a Cell Identifier present in this
+				 * PDU, this means we can extend our local
+				 * state information about this particular cell
+				 * */
+				memcpy(from_peer->ra,
+					TLVP_VAL(&tp, BSSGP_IE_CELL_ID),
+					sizeof(from_peer->ra));
+				gsm48_parse_ra(&raid, from_peer->ra);
+				LOGP(DGPRS, LOGL_INFO, "NSEI=%u/BVCI=%u "
+				     "Cell ID %u-%u-%u-%u\n", nsvc->nsei,
+				     bvci, raid.mcc, raid.mnc, raid.lac,
+				     raid.rac);
+			}
+		}
+		break;
+	}
+
+	/* Normally, we can simply pass on all signalling messages from BSS to
+	 * SGSN */
+	return gbprox_relay2sgsn(msg, ns_bvci);
+err_no_peer:
+	LOGP(DGPRS, LOGL_ERROR, "NSEI=%u(BSS) cannot find peer based on RAC\n",
+		nsvc->nsei);
+	return bssgp_tx_status(BSSGP_CAUSE_UNKNOWN_BVCI, NULL, msg);
+err_mand_ie:
+	LOGP(DGPRS, LOGL_ERROR, "NSEI=%u(BSS) missing mandatory RA IE\n",
+		nsvc->nsei);
+	return bssgp_tx_status(BSSGP_CAUSE_MISSING_MAND_IE, NULL, msg);
+}
+
+/* Receive paging request from SGSN, we need to relay to proper BSS */
+static int gbprox_rx_paging(struct msgb *msg, struct tlv_parsed *tp,
+			    struct gprs_nsvc *nsvc, uint16_t ns_bvci)
+{
+	struct gbprox_peer *peer = NULL;
+
+	LOGP(DGPRS, LOGL_INFO, "NSEI=%u(SGSN) BSSGP PAGING ",
+		nsvc->nsei);
+	if (TLVP_PRESENT(tp, BSSGP_IE_BVCI)) {
+		uint16_t bvci = ntohs(*(uint16_t *)TLVP_VAL(tp, BSSGP_IE_BVCI));
+		LOGPC(DGPRS, LOGL_INFO, "routing by BVCI to peer BVCI=%u\n",
+			bvci);
+	} else if (TLVP_PRESENT(tp, BSSGP_IE_ROUTEING_AREA)) {
+		peer = peer_by_rac(TLVP_VAL(tp, BSSGP_IE_ROUTEING_AREA));
+		LOGPC(DGPRS, LOGL_INFO, "routing by RAC to peer BVCI=%u\n",
+			peer ? peer->bvci : -1);
+	} else if (TLVP_PRESENT(tp, BSSGP_IE_LOCATION_AREA)) {
+		peer = peer_by_lac(TLVP_VAL(tp, BSSGP_IE_LOCATION_AREA));
+		LOGPC(DGPRS, LOGL_INFO, "routing by LAC to peer BVCI=%u\n",
+			peer ? peer->bvci : -1);
+	} else
+		LOGPC(DGPRS, LOGL_INFO, "\n");
+
+	if (!peer) {
+		LOGP(DGPRS, LOGL_ERROR, "NSEI=%u(SGSN) BSSGP PAGING: "
+			"unable to route, missing IE\n", nsvc->nsei);
+		return -EINVAL;
+	}
+	return gbprox_relay2peer(msg, peer, ns_bvci);
+}
+
+/* Receive an incoming BVC-RESET message from the SGSN */
+static int rx_reset_from_sgsn(struct msgb *msg, struct tlv_parsed *tp,
+			      struct gprs_nsvc *nsvc, uint16_t ns_bvci)
+{
+	struct gbprox_peer *peer;
+	uint16_t ptp_bvci;
+
+	if (!TLVP_PRESENT(tp, BSSGP_IE_BVCI)) {
+		return bssgp_tx_status(BSSGP_CAUSE_MISSING_MAND_IE,
+				       NULL, msg);
+	}
+	ptp_bvci = ntohs(*(uint16_t *)TLVP_VAL(tp, BSSGP_IE_BVCI));
+
+	if (ptp_bvci >= 2) {
+		/* A reset for a PTP BVC was received, forward it to its
+		 * respective peer */
+		peer = peer_by_bvci(ptp_bvci);
+		if (!peer) {
+			LOGP(DGPRS, LOGL_ERROR, "NSEI=%u BVCI=%u: Cannot find BSS\n",
+				nsvc->nsei, ptp_bvci);
+			return bssgp_tx_status(BSSGP_CAUSE_UNKNOWN_BVCI,
+					       NULL, msg);
+		}
+		return gbprox_relay2peer(msg, peer, ns_bvci);
+	}
+
+	/* A reset for the Signalling entity has been received
+	 * from the SGSN.  As the signalling BVCI is shared
+	 * among all the BSS's that we multiplex, it needs to
+	 * be relayed  */
+	llist_for_each_entry(peer, &gbprox_bts_peers, list)
+		gbprox_relay2peer(msg, peer, ns_bvci);
+
+	return 0;
+}
+
+/* Receive an incoming signalling message from the SGSN-side NS-VC */
+static int gbprox_rx_sig_from_sgsn(struct msgb *msg, struct gprs_nsvc *nsvc,
+				   uint16_t ns_bvci)
+{
+	struct bssgp_normal_hdr *bgph = (struct bssgp_normal_hdr *) msgb_bssgph(msg);
+	struct tlv_parsed tp;
+	uint8_t pdu_type = bgph->pdu_type;
+	int data_len = msgb_bssgp_len(msg) - sizeof(*bgph);
+	struct gbprox_peer *peer;
+	uint16_t bvci;
+	int rc = 0;
+
+	if (ns_bvci != 0 && ns_bvci != 1) {
+		LOGP(DGPRS, LOGL_NOTICE, "NSEI=%u(SGSN) BVCI=%u is not "
+			"signalling\n", nsvc->nsei, ns_bvci);
+		/* FIXME: Send proper error message */
+		return -EINVAL;
+	}
+
+	/* we actually should never see those two for BVCI == 0, but double-check
+	 * just to make sure  */
+	if (pdu_type == BSSGP_PDUT_UL_UNITDATA ||
+	    pdu_type == BSSGP_PDUT_DL_UNITDATA) {
+		LOGP(DGPRS, LOGL_NOTICE, "NSEI=%u(SGSN) UNITDATA not allowed in "
+			"signalling\n", nsvc->nsei);
+		return bssgp_tx_status(BSSGP_CAUSE_PROTO_ERR_UNSPEC, NULL, msg);
+	}
+
+	rc = bssgp_tlv_parse(&tp, bgph->data, data_len);
+
+	switch (pdu_type) {
+	case BSSGP_PDUT_BVC_RESET:
+		rc = rx_reset_from_sgsn(msg, &tp, nsvc, ns_bvci);
+		break;
+	case BSSGP_PDUT_FLUSH_LL:
+	case BSSGP_PDUT_BVC_RESET_ACK:
+		/* simple case: BVCI IE is mandatory */
+		if (!TLVP_PRESENT(&tp, BSSGP_IE_BVCI))
+			goto err_mand_ie;
+		bvci = ntohs(*(uint16_t *)TLVP_VAL(&tp, BSSGP_IE_BVCI));
+		rc = gbprox_relay2bvci(msg, bvci, ns_bvci);
+		break;
+	case BSSGP_PDUT_PAGING_PS:
+	case BSSGP_PDUT_PAGING_CS:
+		/* process the paging request (LAC/RAC lookup) */
+		rc = gbprox_rx_paging(msg, &tp, nsvc, ns_bvci);
+		break;
+	case BSSGP_PDUT_STATUS:
+		/* Some exception has occurred */
+		LOGP(DGPRS, LOGL_NOTICE,
+			"NSEI=%u(SGSN) BSSGP STATUS ", nsvc->nsei);
+		if (!TLVP_PRESENT(&tp, BSSGP_IE_CAUSE)) {
+			LOGPC(DGPRS, LOGL_NOTICE, "\n");
+			goto err_mand_ie;
+		}
+		LOGPC(DGPRS, LOGL_NOTICE,
+			"cause=0x%02x(%s) ", *TLVP_VAL(&tp, BSSGP_IE_CAUSE),
+			bssgp_cause_str(*TLVP_VAL(&tp, BSSGP_IE_CAUSE)));
+		if (TLVP_PRESENT(&tp, BSSGP_IE_BVCI)) {
+			uint16_t *bvci = (uint16_t *)
+						TLVP_VAL(&tp, BSSGP_IE_BVCI);
+			LOGPC(DGPRS, LOGL_NOTICE,
+				"BVCI=%u\n", ntohs(*bvci));
+		} else
+			LOGPC(DGPRS, LOGL_NOTICE, "\n");
+		break;
+	/* those only exist in the SGSN -> BSS direction */
+	case BSSGP_PDUT_SUSPEND_ACK:
+	case BSSGP_PDUT_SUSPEND_NACK:
+	case BSSGP_PDUT_RESUME_ACK:
+	case BSSGP_PDUT_RESUME_NACK:
+		/* RAC IE is mandatory */
+		if (!TLVP_PRESENT(&tp, BSSGP_IE_ROUTEING_AREA))
+			goto err_mand_ie;
+		peer = peer_by_rac(TLVP_VAL(&tp, BSSGP_IE_ROUTEING_AREA));
+		if (!peer)
+			goto err_no_peer;
+		rc = gbprox_relay2peer(msg, peer, ns_bvci);
+		break;
+	case BSSGP_PDUT_BVC_BLOCK_ACK:
+	case BSSGP_PDUT_BVC_UNBLOCK_ACK:
+		if (!TLVP_PRESENT(&tp, BSSGP_IE_BVCI))
+			goto err_mand_ie;
+		bvci = ntohs(*(uint16_t *)TLVP_VAL(&tp, BSSGP_IE_BVCI));
+		if (bvci == 0) {
+			LOGP(DGPRS, LOGL_NOTICE, "NSEI=%u(SGSN) BSSGP "
+			     "%sBLOCK_ACK for signalling BVCI ?!?\n", nsvc->nsei,
+			     pdu_type == BSSGP_PDUT_BVC_UNBLOCK_ACK ? "UN":"");
+			/* should we send STATUS ? */
+		} else {
+			/* Mark BVC as (un)blocked */
+			block_unblock_peer(bvci, pdu_type);
+		}
+		rc = gbprox_relay2bvci(msg, bvci, ns_bvci);
+		break;
+	case BSSGP_PDUT_SGSN_INVOKE_TRACE:
+		LOGP(DGPRS, LOGL_ERROR,
+		     "NSEI=%u(SGSN) BSSGP INVOKE TRACE not supported\n",nsvc->nsei);
+		rc = bssgp_tx_status(BSSGP_CAUSE_PDU_INCOMP_FEAT, NULL, msg);
+		break;
+	default:
+		LOGP(DGPRS, LOGL_NOTICE, "BSSGP PDU type 0x%02x unknown\n",
+			pdu_type);
+		rc = bssgp_tx_status(BSSGP_CAUSE_PROTO_ERR_UNSPEC, NULL, msg);
+		break;
+	}
+
+	return rc;
+err_mand_ie:
+	LOGP(DGPRS, LOGL_ERROR, "NSEI=%u(SGSN) missing mandatory IE\n",
+		nsvc->nsei);
+	return bssgp_tx_status(BSSGP_CAUSE_MISSING_MAND_IE, NULL, msg);
+err_no_peer:
+	LOGP(DGPRS, LOGL_ERROR, "NSEI=%u(SGSN) cannot find peer based on RAC\n",
+		nsvc->nsei);
+	return bssgp_tx_status(BSSGP_CAUSE_UNKNOWN_BVCI, NULL, msg);
+}
+
+/* Main input function for Gb proxy */
+int gbprox_rcvmsg(struct msgb *msg, struct gprs_nsvc *nsvc, uint16_t ns_bvci)
+{
+	int rc;
+	struct gbprox_peer *peer;
+
+	/* Only BVCI=0 messages need special treatment */
+	if (ns_bvci == 0 || ns_bvci == 1) {
+		if (nsvc->remote_end_is_sgsn)
+			rc = gbprox_rx_sig_from_sgsn(msg, nsvc, ns_bvci);
+		else
+			rc = gbprox_rx_sig_from_bss(msg, nsvc, ns_bvci);
+	} else {
+		/* All other BVCI are PTP and thus can be simply forwarded */
+		if (!nsvc->remote_end_is_sgsn) {
+			return gbprox_relay2sgsn(msg, ns_bvci);
+		}
+		/* else: SGSN -> BSS direction */
+		peer = peer_by_bvci(ns_bvci);
+		if (!peer) {
+			LOGP(DGPRS, LOGL_INFO, "Allocationg new peer for "
+			     "BVCI=%u via NSVC=%u/NSEI=%u\n", ns_bvci,
+			     nsvc->nsvci, nsvc->nsei);
+			peer = peer_alloc(ns_bvci);
+			peer->nsvc = nsvc;
+		}
+		if (peer->blocked) {
+			LOGP(DGPRS, LOGL_NOTICE, "Dropping PDU for "
+			     "blocked BVCI=%u via NSVC=%u/NSEI=%u\n",
+			     ns_bvci, nsvc->nsvci, nsvc->nsei);
+			return bssgp_tx_status(BSSGP_CAUSE_BVCI_BLOCKED, NULL, msg);
+		}
+		rc = gbprox_relay2peer(msg, peer, ns_bvci);
+	}
+
+	return rc;
+}
+
+int gbprox_reset_persistent_nsvcs(struct gprs_ns_inst *nsi)
+{
+	struct gprs_nsvc *nsvc;
+
+	llist_for_each_entry(nsvc, &nsi->gprs_nsvcs, list) {
+		if (!nsvc->persistent)
+			continue;
+		gprs_nsvc_reset(nsvc, NS_CAUSE_OM_INTERVENTION);
+	}
+	return 0;
+}
+
+/* Signal handler for signals from NS layer */
+int gbprox_signal(unsigned int subsys, unsigned int signal,
+		  void *handler_data, void *signal_data)
+{
+	struct ns_signal_data *nssd = signal_data;
+	struct gprs_nsvc *nsvc = nssd->nsvc;
+	struct gbprox_peer *peer;
+
+	if (subsys != SS_NS)
+		return 0;
+
+	if (signal == S_NS_RESET && nsvc->nsei == gbcfg.nsip_sgsn_nsei) {
+		/* We have received a NS-RESET from the NSEI and NSVC
+		 * of the SGSN.  This might happen with SGSN that start
+		 * their own NS-RESET procedure without waiting for our
+		 * NS-RESET */
+		nsvc->remote_end_is_sgsn = 1;
+	}
+
+	if (signal == S_NS_ALIVE_EXP && nsvc->remote_end_is_sgsn) {
+		LOGP(DGPRS, LOGL_NOTICE, "Tns alive expired too often, "
+			"re-starting RESET procedure\n");
+		nsip_connect(nsvc->nsi, &nsvc->ip.bts_addr, nsvc->nsei,
+			     nsvc->nsvci);
+	}
+
+	if (!nsvc->remote_end_is_sgsn) {
+		/* from BSS to SGSN */
+		peer = peer_by_nsvc(nsvc);
+		if (!peer) {
+			LOGP(DGPRS, LOGL_NOTICE, "signal %u for unknown peer "
+			     "NSEI=%u/NSVCI=%u\n", signal, nsvc->nsei,
+			     nsvc->nsvci);
+			return 0;
+		}
+		switch (signal) {
+		case S_NS_RESET:
+		case S_NS_BLOCK:
+			if (!peer->blocked)
+				break;
+			LOGP(DGPRS, LOGL_NOTICE, "Converting NS_RESET from "
+			     "NSEI=%u/NSVCI=%u into BSSGP_BVC_BLOCK to SGSN\n",
+			     nsvc->nsei, nsvc->nsvci);
+			bssgp_tx_simple_bvci(BSSGP_PDUT_BVC_BLOCK, nsvc->nsei,
+					     peer->bvci, 0);
+			break;
+		}
+	} else {
+		/* iterate over all BTS peers and send the respective PDU */
+		llist_for_each_entry(peer, &gbprox_bts_peers, list) {
+			switch (signal) {
+			case S_NS_RESET:
+				gprs_ns_tx_reset(peer->nsvc, nssd->cause);
+				break;
+			case S_NS_BLOCK:
+				gprs_ns_tx_block(peer->nsvc, nssd->cause);
+				break;
+			case S_NS_UNBLOCK:
+				gprs_ns_tx_unblock(peer->nsvc);
+				break;
+			}
+		}
+	}
+	return 0;
+}
+
+
+#include <osmocom/vty/command.h>
+
+gDEFUN(show_gbproxy, show_gbproxy_cmd, "show gbproxy",
+       SHOW_STR "Display information about the Gb proxy")
+{
+	struct gbprox_peer *peer;
+
+	llist_for_each_entry(peer, &gbprox_bts_peers, list) {
+		struct gprs_nsvc *nsvc = peer->nsvc;
+		struct gprs_ra_id raid;
+		gsm48_parse_ra(&raid, peer->ra);
+
+		vty_out(vty, "NSEI %5u, NS-VC %5u, PTP-BVCI %5u, "
+			"RAC %u-%u-%u-%u",
+			nsvc->nsei, nsvc->nsvci, peer->bvci,
+			raid.mcc, raid.mnc, raid.lac, raid.rac);
+		if (nsvc->ll == GPRS_NS_LL_UDP || nsvc->ll == GPRS_NS_LL_FR_GRE)
+			vty_out(vty, " %s:%u",
+				inet_ntoa(nsvc->ip.bts_addr.sin_addr),
+				ntohs(nsvc->ip.bts_addr.sin_port));
+		if (peer->blocked)
+			vty_out(vty, " [BVC-BLOCKED]");
+
+		vty_out(vty, "%s", VTY_NEWLINE);
+	}
+	return CMD_SUCCESS;
+}
diff --git a/src/gprs/gb_proxy_main.c b/src/gprs/gb_proxy_main.c
new file mode 100644
index 0000000..b53e985
--- /dev/null
+++ b/src/gprs/gb_proxy_main.c
@@ -0,0 +1,288 @@
+/* NS-over-IP proxy */
+
+/* (C) 2010 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2010 by On-Waves
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <getopt.h>
+#include <errno.h>
+#include <signal.h>
+#include <sys/fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <osmocore/talloc.h>
+#include <osmocore/select.h>
+#include <osmocore/rate_ctr.h>
+#include <osmocore/process.h>
+
+#include <openbsc/signal.h>
+#include <openbsc/debug.h>
+#include <openbsc/gprs_ns.h>
+#include <openbsc/gprs_bssgp.h>
+#include <openbsc/vty.h>
+#include <openbsc/gb_proxy.h>
+
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/telnet_interface.h>
+
+#include "../../bscconfig.h"
+
+/* this is here for the vty... it will never be called */
+void subscr_put() { abort(); }
+
+#define _GNU_SOURCE
+#include <getopt.h>
+
+void *tall_bsc_ctx;
+
+const char *openbsc_copyright =
+	"Copyright (C) 2010 Harald Welte and On-Waves\r\n"
+	"License AGPLv3+: GNU AGPL version 3 or later <http://gnu.org/licenses/agpl-3.0.html>\r\n"
+	"This is free software: you are free to change and redistribute it.\r\n"
+	"There is NO WARRANTY, to the extent permitted by law.\r\n";
+
+static struct log_target *stderr_target;
+static char *config_file = "osmo_gbproxy.cfg";
+struct gbproxy_config gbcfg;
+static int daemonize = 0;
+
+/* Pointer to the SGSN peer */
+extern struct gbprox_peer *gbprox_peer_sgsn;
+
+/* call-back function for the NS protocol */
+static int proxy_ns_cb(enum gprs_ns_evt event, struct gprs_nsvc *nsvc,
+		      struct msgb *msg, u_int16_t bvci)
+{
+	int rc = 0;
+
+	switch (event) {
+	case GPRS_NS_EVT_UNIT_DATA:
+		rc = gbprox_rcvmsg(msg, nsvc, bvci);
+		break;
+	default:
+		LOGP(DGPRS, LOGL_ERROR, "SGSN: Unknown event %u from NS\n", event);
+		if (msg)
+			talloc_free(msg);
+		rc = -EIO;
+		break;
+	}
+	return rc;
+}
+
+static void signal_handler(int signal)
+{
+	fprintf(stdout, "signal %u received\n", signal);
+
+	switch (signal) {
+	case SIGINT:
+		dispatch_signal(SS_GLOBAL, S_GLOBAL_SHUTDOWN, NULL);
+		sleep(1);
+		exit(0);
+		break;
+	case SIGABRT:
+		/* in case of abort, we want to obtain a talloc report
+		 * and then return to the caller, who will abort the process */
+	case SIGUSR1:
+		talloc_report(tall_vty_ctx, stderr);
+		talloc_report_full(tall_bsc_ctx, stderr);
+		break;
+	case SIGUSR2:
+		talloc_report_full(tall_vty_ctx, stderr);
+		break;
+	default:
+		break;
+	}
+}
+
+static void print_usage()
+{
+	printf("Usage: bsc_hack\n");
+}
+
+static void print_help()
+{
+	printf("  Some useful help...\n");
+	printf("  -h --help this text\n");
+	printf("  -d option --debug=DNS:DGPRS,0:0 enable debugging\n");
+	printf("  -D --daemonize Fork the process into a background daemon\n");
+	printf("  -c --config-file filename The config file to use.\n");
+	printf("  -s --disable-color\n");
+	printf("  -T --timestamp Prefix every log line with a timestamp\n");
+	printf("  -V --version. Print the version of OpenBSC.\n");
+	printf("  -e --log-level number. Set a global loglevel.\n");
+}
+
+static void handle_options(int argc, char **argv)
+{
+	while (1) {
+		int option_index = 0, c;
+		static struct option long_options[] = {
+			{ "help", 0, 0, 'h' },
+			{ "debug", 1, 0, 'd' },
+			{ "daemonize", 0, 0, 'D' },
+			{ "config-file", 1, 0, 'c' },
+			{ "disable-color", 0, 0, 's' },
+			{ "timestamp", 0, 0, 'T' },
+			{ "version", 0, 0, 'V' },
+			{ "log-level", 1, 0, 'e' },
+			{ 0, 0, 0, 0 }
+		};
+
+		c = getopt_long(argc, argv, "hd:Dc:sTVe:",
+				long_options, &option_index);
+		if (c == -1)
+			break;
+
+		switch (c) {
+		case 'h':
+			print_usage();
+			print_help();
+			exit(0);
+		case 's':
+			log_set_use_color(stderr_target, 0);
+			break;
+		case 'd':
+			log_parse_category_mask(stderr_target, optarg);
+			break;
+		case 'D':
+			daemonize = 1;
+			break;
+		case 'c':
+			config_file = strdup(optarg);
+			break;
+		case 'T':
+			log_set_print_timestamp(stderr_target, 1);
+			break;
+		case 'e':
+			log_set_log_level(stderr_target, atoi(optarg));
+			break;
+		case 'V':
+			print_version(1);
+			exit(0);
+			break;
+		default:
+			break;
+		}
+	}
+}
+
+extern void *tall_msgb_ctx;
+
+extern enum node_type bsc_vty_go_parent(struct vty *vty);
+
+static struct vty_app_info vty_info = {
+	.name 		= "OsmoGbProxy",
+	.version	= PACKAGE_VERSION,
+	.go_parent_cb	= bsc_vty_go_parent,
+	.is_config_node	= bsc_vty_is_config_node,
+};
+
+int main(int argc, char **argv)
+{
+	struct gsm_network dummy_network;
+	int rc;
+
+	tall_bsc_ctx = talloc_named_const(NULL, 0, "nsip_proxy");
+	tall_msgb_ctx = talloc_named_const(tall_bsc_ctx, 0, "msgb");
+
+	signal(SIGINT, &signal_handler);
+	signal(SIGABRT, &signal_handler);
+	signal(SIGUSR1, &signal_handler);
+	signal(SIGUSR2, &signal_handler);
+	signal(SIGPIPE, SIG_IGN);
+
+	log_init(&log_info);
+	stderr_target = log_target_create_stderr();
+	log_add_target(stderr_target);
+	log_set_all_filter(stderr_target, 1);
+
+	vty_info.copyright = openbsc_copyright;
+	vty_init(&vty_info);
+	logging_vty_add_cmds();
+	gbproxy_vty_init();
+
+	handle_options(argc, argv);
+
+	rate_ctr_init(tall_bsc_ctx);
+
+	rc = telnet_init(tall_bsc_ctx, &dummy_network, 4246);
+	if (rc < 0)
+		exit(1);
+
+	bssgp_nsi = gprs_ns_instantiate(&proxy_ns_cb);
+	if (!bssgp_nsi) {
+		LOGP(DGPRS, LOGL_ERROR, "Unable to instantiate NS\n");
+		exit(1);
+	}
+	gbcfg.nsi = bssgp_nsi;
+	gprs_ns_vty_init(bssgp_nsi);
+	register_signal_handler(SS_NS, &gbprox_signal, NULL);
+
+	rc = gbproxy_parse_config(config_file, &gbcfg);
+	if (rc < 0) {
+		LOGP(DGPRS, LOGL_FATAL, "Cannot parse config file\n");
+		exit(2);
+	}
+
+	if (!nsvc_by_nsei(gbcfg.nsi, gbcfg.nsip_sgsn_nsei)) {
+		LOGP(DGPRS, LOGL_FATAL, "You cannot proxy to NSEI %u "
+			"without creating that NSEI before\n",
+			gbcfg.nsip_sgsn_nsei);
+		exit(2);
+	}
+
+	rc = gprs_ns_nsip_listen(bssgp_nsi);
+	if (rc < 0) {
+		LOGP(DGPRS, LOGL_FATAL, "Cannot bind/listen on NSIP socket\n");
+		exit(2);
+	}
+
+	rc = gprs_ns_frgre_listen(bssgp_nsi);
+	if (rc < 0) {
+		LOGP(DGPRS, LOGL_FATAL, "Cannot bind/listen GRE "
+			"socket. Do you have CAP_NET_RAW?\n");
+		exit(2);
+	}
+
+	if (daemonize) {
+		rc = osmo_daemonize();
+		if (rc < 0) {
+			perror("Error during daemonize");
+			exit(1);
+		}
+	}
+
+	/* Reset all the persistent NS-VCs that we've read from the config */
+	gbprox_reset_persistent_nsvcs(bssgp_nsi);
+
+	while (1) {
+		rc = bsc_select_main(0);
+		if (rc < 0)
+			exit(3);
+	}
+
+	exit(0);
+}
diff --git a/src/gprs/gb_proxy_vty.c b/src/gprs/gb_proxy_vty.c
new file mode 100644
index 0000000..05f5b1e
--- /dev/null
+++ b/src/gprs/gb_proxy_vty.c
@@ -0,0 +1,104 @@
+/*
+ * (C) 2010 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2010 by On-Waves
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <osmocore/talloc.h>
+
+#include <openbsc/debug.h>
+#include <openbsc/gb_proxy.h>
+#include <openbsc/gprs_ns.h>
+#include <openbsc/vty.h>
+
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/vty.h>
+
+static struct gbproxy_config *g_cfg = NULL;
+
+/*
+ * vty code for mgcp below
+ */
+static struct cmd_node gbproxy_node = {
+	GBPROXY_NODE,
+	"%s(gbproxy)#",
+	1,
+};
+
+static int config_write_gbproxy(struct vty *vty)
+{
+	vty_out(vty, "gbproxy%s", VTY_NEWLINE);
+
+	vty_out(vty, " sgsn nsei %u%s", g_cfg->nsip_sgsn_nsei,
+		VTY_NEWLINE);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_gbproxy,
+      cfg_gbproxy_cmd,
+      "gbproxy",
+      "Configure the Gb proxy")
+{
+	vty->node = GBPROXY_NODE;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_nsip_sgsn_nsei,
+      cfg_nsip_sgsn_nsei_cmd,
+      "sgsn nsei <0-65534>",
+      "Set the NSEI to be used in the connection with the SGSN")
+{
+	unsigned int port = atoi(argv[0]);
+
+	g_cfg->nsip_sgsn_nsei = port;
+	return CMD_SUCCESS;
+}
+
+int gbproxy_vty_init(void)
+{
+	install_element_ve(&show_gbproxy_cmd);
+
+	install_element(CONFIG_NODE, &cfg_gbproxy_cmd);
+	install_node(&gbproxy_node, config_write_gbproxy);
+	install_default(GBPROXY_NODE);
+	install_element(GBPROXY_NODE, &ournode_exit_cmd);
+	install_element(GBPROXY_NODE, &ournode_end_cmd);
+	install_element(GBPROXY_NODE, &cfg_nsip_sgsn_nsei_cmd);
+
+	return 0;
+}
+
+int gbproxy_parse_config(const char *config_file, struct gbproxy_config *cfg)
+{
+	int rc;
+
+	g_cfg = cfg;
+	rc = vty_read_config_file(config_file, NULL);
+	if (rc < 0) {
+		fprintf(stderr, "Failed to parse the config file: '%s'\n", config_file);
+		return rc;
+	}
+
+	return 0;
+}
+
diff --git a/src/gprs/gprs_gmm.c b/src/gprs/gprs_gmm.c
new file mode 100644
index 0000000..949cd96
--- /dev/null
+++ b/src/gprs/gprs_gmm.c
@@ -0,0 +1,1597 @@
+/* GSM Mobile Radio Interface Layer 3 messages on the A-bis interface
+ * 3GPP TS 04.08 version 7.21.0 Release 1998 / ETSI TS 100 940 V7.21.0 */
+
+/* (C) 2009-2010 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2010 by On-Waves
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <errno.h>
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <openbsc/db.h>
+#include <osmocore/msgb.h>
+#include <osmocore/tlv.h>
+#include <osmocore/gsm_utils.h>
+#include <osmocore/signal.h>
+#include <osmocore/talloc.h>
+#include <osmocore/rate_ctr.h>
+
+#include <openbsc/debug.h>
+#include <openbsc/gsm_data.h>
+#include <openbsc/gsm_subscriber.h>
+#include <openbsc/gsm_04_08.h>
+#include <openbsc/gsm_04_08_gprs.h>
+#include <openbsc/paging.h>
+#include <openbsc/transaction.h>
+#include <openbsc/gprs_bssgp.h>
+#include <openbsc/gprs_llc.h>
+#include <openbsc/gprs_sgsn.h>
+#include <openbsc/gprs_gmm.h>
+#include <openbsc/sgsn.h>
+
+#include <pdp.h>
+
+#define PTMSI_ALLOC
+
+/* Section 11.2.2 / Table 11.4 MM timers netowkr side */
+#define GSM0408_T3322_SECS	6	/* DETACH_REQ -> DETACH_ACC */
+#define GSM0408_T3350_SECS	6	/* waiting for ATT/RAU/TMSI COMPL */
+#define GSM0408_T3360_SECS	6	/* waiting for AUTH/CIPH RESP */
+#define GSM0408_T3370_SECS	6	/* waiting for ID RESP */
+
+/* Section 11.2.2 / Table 11.4a MM timers netowkr side */
+#define GSM0408_T3313_SECS	30	/* waiting for paging response */
+#define GSM0408_T3314_SECS	44	/* force to STBY on expiry */
+#define GSM0408_T3316_SECS	44
+
+/* Section 11.3 / Table 11.2d Timers of Session Management - network side */
+#define GSM0408_T3385_SECS	8	/* wait for ACT PDP CTX REQ */
+#define GSM0408_T3386_SECS	8	/* wait for MODIFY PDP CTX ACK */
+#define GSM0408_T3395_SECS	8	/* wait for DEACT PDP CTX ACK */
+#define GSM0408_T3397_SECS	8	/* wait for DEACT AA PDP CTX ACK */
+
+extern struct sgsn_instance *sgsn;
+
+/* Protocol related stuff, should go into libosmocore */
+
+/* 10.5.5.14 GPRS MM Cause / Table 10.5.147 */
+const struct value_string gmm_cause_names[] = {
+	{ GMM_CAUSE_IMSI_UNKNOWN, 	"IMSI unknown in HLR" },
+	{ GMM_CAUSE_ILLEGAL_MS, 	"Illegal MS" },
+	{ GMM_CAUSE_ILLEGAL_ME,		"Illegal ME" },
+	{ GMM_CAUSE_GPRS_NOTALLOWED,	"GPRS services not allowed" },
+	{ GMM_CAUSE_GPRS_OTHER_NOTALLOWED,
+			"GPRS services and non-GPRS services not allowed" },
+	{ GMM_CAUSE_MS_ID_NOT_DERIVED,
+			"MS identity cannot be derived by the network" },
+	{ GMM_CAUSE_IMPL_DETACHED,	"Implicitly detached" },
+	{ GMM_CAUSE_PLMN_NOTALLOWED,	"PLMN not allowed" },
+	{ GMM_CAUSE_LA_NOTALLOWED,	"Location Area not allowed" },
+	{ GMM_CAUSE_ROAMING_NOTALLOWED,
+			"Roaming not allowed in this location area" },
+	{ GMM_CAUSE_NO_GPRS_PLMN,
+				"GPRS services not allowed in this PLMN" },
+	{ GMM_CAUSE_MSC_TEMP_NOTREACH,	"MSC temporarily not reachable" },
+	{ GMM_CAUSE_NET_FAIL,		"Network failure" },
+	{ GMM_CAUSE_CONGESTION,		"Congestion" },
+	{ GMM_CAUSE_SEM_INCORR_MSG,	"Semantically incorrect message" },
+	{ GMM_CAUSE_INV_MAND_INFO, "Invalid mandatory information" },
+	{ GMM_CAUSE_MSGT_NOTEXIST_NOTIMPL,
+			"Message type non-existant or not implemented" },
+	{ GMM_CAUSE_MSGT_INCOMP_P_STATE,
+			"Message type not compatible with protocol state" },
+	{ GMM_CAUSE_IE_NOTEXIST_NOTIMPL,
+			"Information element non-existent or not implemented" },
+	{ GMM_CAUSE_COND_IE_ERR,	"Conditional IE error" },
+	{ GMM_CAUSE_MSG_INCOMP_P_STATE,
+				"Message not compatible with protocol state " },
+	{ GMM_CAUSE_PROTO_ERR_UNSPEC,	"Protocol error, unspecified" },
+	{ 0, NULL }
+};
+
+/* 10.5.6.6 SM Cause / Table 10.5.157 */
+const struct value_string gsm_cause_names[] = {
+	{ GSM_CAUSE_INSUFF_RSRC, "Insufficient resources" },
+	{ GSM_CAUSE_MISSING_APN, "Missing or unknown APN" },
+	{ GSM_CAUSE_UNKNOWN_PDP, "Unknown PDP address or PDP type" },
+	{ GSM_CAUSE_AUTH_FAILED, "User Authentication failed" },
+	{ GSM_CAUSE_ACT_REJ_GGSN, "Activation rejected by GGSN" },
+	{ GSM_CAUSE_ACT_REJ_UNSPEC, "Activation rejected, unspecified" },
+	{ GSM_CAUSE_SERV_OPT_NOTSUPP, "Service option not supported" },
+	{ GSM_CAUSE_REQ_SERV_OPT_NOTSUB,
+				"Requested service option not subscribed" },
+	{ GSM_CAUSE_SERV_OPT_TEMP_OOO,
+				"Service option temporarily out of order" },
+	{ GSM_CAUSE_NSAPI_IN_USE, "NSAPI already used" },
+	{ GSM_CAUSE_DEACT_REGULAR, "Regular deactivation" },
+	{ GSM_CAUSE_QOS_NOT_ACCEPTED, "QoS not accepted" },
+	{ GSM_CAUSE_NET_FAIL, "Network Failure" },
+	{ GSM_CAUSE_REACT_RQD, "Reactivation required" },
+	{ GSM_CAUSE_FEATURE_NOTSUPP, "Feature not supported " },
+	{ GSM_CAUSE_INVALID_TRANS_ID, "Invalid transaction identifier" },
+	{ GSM_CAUSE_SEM_INCORR_MSG, "Semantically incorrect message" },
+	{ GSM_CAUSE_INV_MAND_INFO, "Invalid mandatory information" },
+	{ GSM_CAUSE_MSGT_NOTEXIST_NOTIMPL,
+			"Message type non-existant or not implemented" },
+	{ GSM_CAUSE_MSGT_INCOMP_P_STATE,
+			"Message type not compatible with protocol state" },
+	{ GSM_CAUSE_IE_NOTEXIST_NOTIMPL,
+			"Information element non-existent or not implemented" },
+	{ GSM_CAUSE_COND_IE_ERR, "Conditional IE error" },
+	{ GSM_CAUSE_MSG_INCOMP_P_STATE,
+				"Message not compatible with protocol state " },
+	{ GSM_CAUSE_PROTO_ERR_UNSPEC, "Protocol error, unspecified" },
+	{ 0, NULL }
+};
+
+/* 10.5.5.2 */
+const struct value_string gprs_att_t_strs[] = {
+	{ GPRS_ATT_T_ATTACH, 		"GPRS attach" },
+	{ GPRS_ATT_T_ATT_WHILE_IMSI, 	"GPRS attach while IMSI attached" },
+	{ GPRS_ATT_T_COMBINED, 		"Combined GPRS/IMSI attach" },
+	{ 0, NULL }
+};
+
+const struct value_string gprs_upd_t_strs[] = {
+	{ GPRS_UPD_T_RA,		"RA updating" },
+	{ GPRS_UPD_T_RA_LA,		"combined RA/LA updating" },
+	{ GPRS_UPD_T_RA_LA_IMSI_ATT,	"combined RA/LA updating + IMSI attach" },
+	{ GPRS_UPD_T_PERIODIC,		"periodic updating" },
+	{ 0, NULL }
+};
+
+/* 10.5.5.5 */
+const struct value_string gprs_det_t_mo_strs[] = {
+	{ GPRS_DET_T_MO_GPRS,		"GPRS detach" },
+	{ GPRS_DET_T_MO_IMSI,		"IMSI detach" },
+	{ GPRS_DET_T_MO_COMBINED,	"Combined GPRS/IMSI detach" },
+	{ 0, NULL }
+};
+
+static const struct tlv_definition gsm48_gmm_att_tlvdef = {
+	.def = {
+		[GSM48_IE_GMM_CIPH_CKSN]	= { TLV_TYPE_FIXED, 1 },
+		[GSM48_IE_GMM_TIMER_READY]	= { TLV_TYPE_TV, 1 },
+		[GSM48_IE_GMM_ALLOC_PTMSI]	= { TLV_TYPE_TLV, 0 },
+		[GSM48_IE_GMM_PTMSI_SIG]	= { TLV_TYPE_FIXED, 3 },
+		[GSM48_IE_GMM_AUTH_RAND]	= { TLV_TYPE_FIXED, 16 },
+		[GSM48_IE_GMM_AUTH_SRES]	= { TLV_TYPE_FIXED, 4 },
+		[GSM48_IE_GMM_IMEISV]		= { TLV_TYPE_TLV, 0 },
+		[GSM48_IE_GMM_DRX_PARAM]	= { TLV_TYPE_FIXED, 2 },
+		[GSM48_IE_GMM_MS_NET_CAPA]	= { TLV_TYPE_TLV, 0 },
+		[GSM48_IE_GMM_PDP_CTX_STATUS]	= { TLV_TYPE_TLV, 0 },
+		[GSM48_IE_GMM_PS_LCS_CAPA]	= { TLV_TYPE_TLV, 0 },
+		[GSM48_IE_GMM_GMM_MBMS_CTX_ST]	= { TLV_TYPE_TLV, 0 },
+	},
+};
+
+static const struct tlv_definition gsm48_sm_att_tlvdef = {
+	.def = {
+		[GSM48_IE_GSM_APN]		= { TLV_TYPE_TLV, 0 },
+		[GSM48_IE_GSM_PROTO_CONF_OPT]	= { TLV_TYPE_TLV, 0 },
+		[GSM48_IE_GSM_PDP_ADDR]		= { TLV_TYPE_TLV, 0 },
+		[GSM48_IE_GSM_AA_TMR]		= { TLV_TYPE_TV, 1 },
+		[GSM48_IE_GSM_NAME_FULL]	= { TLV_TYPE_TLV, 0 },
+		[GSM48_IE_GSM_NAME_SHORT]	= { TLV_TYPE_TLV, 0 },
+		[GSM48_IE_GSM_TIMEZONE]		= { TLV_TYPE_FIXED, 1 },
+		[GSM48_IE_GSM_UTC_AND_TZ]	= { TLV_TYPE_FIXED, 7 },
+		[GSM48_IE_GSM_LSA_ID]		= { TLV_TYPE_TLV, 0 },
+	},
+};
+
+/* Our implementation, should be kept in SGSN */
+
+static void mmctx_timer_cb(void *_mm);
+
+static void mmctx_timer_start(struct sgsn_mm_ctx *mm, unsigned int T,
+				unsigned int seconds)
+{
+	if (bsc_timer_pending(&mm->timer))
+		LOGP(DMM, LOGL_ERROR, "Starting MM timer %u while old "
+			"timer %u pending\n", T, mm->T);
+	mm->T = T;
+	mm->num_T_exp = 0;
+
+	/* FIXME: we should do this only once ? */
+	mm->timer.data = mm;
+	mm->timer.cb = &mmctx_timer_cb;
+
+	bsc_schedule_timer(&mm->timer, seconds, 0);
+}
+
+static void mmctx_timer_stop(struct sgsn_mm_ctx *mm, unsigned int T)
+{
+	if (mm->T != T)
+		LOGP(DMM, LOGL_ERROR, "Stopping MM timer %u but "
+			"%u is running\n", T, mm->T);
+	bsc_del_timer(&mm->timer);
+}
+
+/* Send a message through the underlying layer */
+static int gsm48_gmm_sendmsg(struct msgb *msg, int command,
+			     const struct sgsn_mm_ctx *mm)
+{
+	if (mm)
+		rate_ctr_inc(&mm->ctrg->ctr[GMM_CTR_PKTS_SIG_OUT]);
+
+	/* caller needs to provide TLLI, BVCI and NSEI */
+	return gprs_llc_tx_ui(msg, GPRS_SAPI_GMM, command, mm);
+}
+
+/* copy identifiers from old message to new message, this
+ * is required so lower layers can route it correctly */
+static void gmm_copy_id(struct msgb *msg, const struct msgb *old)
+{
+	msgb_tlli(msg) = msgb_tlli(old);
+	msgb_bvci(msg) = msgb_bvci(old);
+	msgb_nsei(msg) = msgb_nsei(old);
+}
+
+/* Store BVCI/NSEI in MM context */
+static void msgid2mmctx(struct sgsn_mm_ctx *mm, const struct msgb *msg)
+{
+	mm->bvci = msgb_bvci(msg);
+	mm->nsei = msgb_nsei(msg);
+}
+
+/* Store BVCI/NSEI in MM context */
+static void mmctx2msgid(struct msgb *msg, const struct sgsn_mm_ctx *mm)
+{
+	msgb_tlli(msg) = mm->tlli;
+	msgb_bvci(msg) = mm->bvci;
+	msgb_nsei(msg) = mm->nsei;
+}
+
+/* Chapter 9.4.18 */
+static int _tx_status(struct msgb *msg, uint8_t cause,
+		      struct sgsn_mm_ctx *mmctx, int sm)
+{
+	struct gsm48_hdr *gh;
+
+	/* MMCTX might be NULL! */
+
+	DEBUGP(DMM, "<- GPRS MM STATUS (cause: %s)\n",
+		get_value_string(gmm_cause_names, cause));
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh) + 1);
+	if (sm) {
+		gh->proto_discr = GSM48_PDISC_SM_GPRS;
+		gh->msg_type = GSM48_MT_GSM_STATUS;
+	} else {
+		gh->proto_discr = GSM48_PDISC_MM_GPRS;
+		gh->msg_type = GSM48_MT_GMM_STATUS;
+	}
+	gh->data[0] = cause;
+
+	return gsm48_gmm_sendmsg(msg, 0, mmctx);
+}
+static int gsm48_tx_gmm_status(struct sgsn_mm_ctx *mmctx, uint8_t cause)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+
+	mmctx2msgid(msg, mmctx);
+	return _tx_status(msg, cause, mmctx, 0);
+};
+static int gsm48_tx_gmm_status_oldmsg(struct msgb *oldmsg, uint8_t cause)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+
+	gmm_copy_id(msg, oldmsg);
+	return _tx_status(msg, cause, NULL, 0);
+}
+static int gsm48_tx_sm_status(struct sgsn_mm_ctx *mmctx, uint8_t cause)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+
+	mmctx2msgid(msg, mmctx);
+	return _tx_status(msg, cause, mmctx, 1);
+};
+static int gsm48_tx_sm_status_oldmsg(struct msgb *oldmsg, uint8_t cause)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+
+	gmm_copy_id(msg, oldmsg);
+	return _tx_status(msg, cause, NULL, 1);
+}
+
+
+static struct gsm48_qos default_qos = {
+	.delay_class = 4,	/* best effort */
+	.reliab_class = GSM48_QOS_RC_LLC_UN_RLC_ACK_DATA_PROT,
+	.peak_tput = GSM48_QOS_PEAK_TPUT_32000bps,
+	.preced_class = GSM48_QOS_PC_NORMAL,
+	.mean_tput = GSM48_QOS_MEAN_TPUT_BEST_EFFORT,
+	.traf_class = GSM48_QOS_TC_INTERACTIVE,
+	.deliv_order = GSM48_QOS_DO_UNORDERED,
+	.deliv_err_sdu = GSM48_QOS_ERRSDU_YES,
+	.max_sdu_size = GSM48_QOS_MAXSDU_1520,
+	.max_bitrate_up = GSM48_QOS_MBRATE_63k,
+	.max_bitrate_down = GSM48_QOS_MBRATE_63k,
+	.resid_ber = GSM48_QOS_RBER_5e_2,
+	.sdu_err_ratio = GSM48_QOS_SERR_1e_2,
+	.handling_prio = 3,
+	.xfer_delay = 0x10,	/* 200ms */
+	.guar_bitrate_up = GSM48_QOS_MBRATE_0k,
+	.guar_bitrate_down = GSM48_QOS_MBRATE_0k,
+	.sig_ind = 0,	/* not optimised for signalling */
+	.max_bitrate_down_ext = 0,	/* use octet 9 */
+	.guar_bitrate_down_ext = 0,	/* use octet 13 */
+};
+
+/* Chapter 9.4.2: Attach accept */
+static int gsm48_tx_gmm_att_ack(struct sgsn_mm_ctx *mm)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+	struct gsm48_attach_ack *aa;
+	uint8_t *ptsig, *mid;
+
+	DEBUGP(DMM, "<- GPRS ATTACH ACCEPT (new P-TMSI=0x%08x)\n", mm->p_tmsi);
+
+	mmctx2msgid(msg, mm);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh));
+	gh->proto_discr = GSM48_PDISC_MM_GPRS;
+	gh->msg_type = GSM48_MT_GMM_ATTACH_ACK;
+
+	aa = (struct gsm48_attach_ack *) msgb_put(msg, sizeof(*aa));
+	aa->force_stby = 0;	/* not indicated */
+	aa->att_result = 1;	/* GPRS only */
+	aa->ra_upd_timer = GPRS_TMR_MINUTE | 10;
+	aa->radio_prio = 4;	/* lowest */
+	gsm48_construct_ra(aa->ra_id.digits, &mm->ra);
+
+#if 0
+	/* Optional: P-TMSI signature */
+	msgb_v_put(msg, GSM48_IE_GMM_PTMSI_SIG);
+	ptsig = msgb_put(msg, 3);
+	ptsig[0] = mm->p_tmsi_sig >> 16;
+	ptsig[1] = mm->p_tmsi_sig >> 8;
+	ptsig[2] = mm->p_tmsi_sig & 0xff;
+
+	/* Optional: Negotiated Ready timer value */
+#endif
+
+#ifdef PTMSI_ALLOC
+	/* Optional: Allocated P-TMSI */
+	mid = msgb_put(msg, GSM48_MID_TMSI_LEN);
+	gsm48_generate_mid_from_tmsi(mid, mm->p_tmsi);
+	mid[0] = GSM48_IE_GMM_ALLOC_PTMSI;
+#endif
+
+	/* Optional: MS-identity (combined attach) */
+	/* Optional: GMM cause (partial attach result for combined attach) */
+
+	return gsm48_gmm_sendmsg(msg, 0, mm);
+}
+
+/* Chapter 9.4.5: Attach reject */
+static int _tx_gmm_att_rej(struct msgb *msg, uint8_t gmm_cause)
+{
+	struct gsm48_hdr *gh;
+
+	DEBUGP(DMM, "<- GPRS ATTACH REJECT\n");
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh) + 1);
+	gh->proto_discr = GSM48_PDISC_MM_GPRS;
+	gh->msg_type = GSM48_MT_GMM_ATTACH_REJ;
+	gh->data[0] = gmm_cause;
+
+	return gsm48_gmm_sendmsg(msg, 0, NULL);
+}
+static int gsm48_tx_gmm_att_rej_oldmsg(const struct msgb *old_msg,
+					uint8_t gmm_cause)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	gmm_copy_id(msg, old_msg);
+	return _tx_gmm_att_rej(msg, gmm_cause);
+}
+static int gsm48_tx_gmm_att_rej(struct sgsn_mm_ctx *mm,
+				uint8_t gmm_cause)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	mmctx2msgid(msg, mm);
+	return _tx_gmm_att_rej(msg, gmm_cause);
+}
+
+/* Chapter 9.4.6.2 Detach accept */
+static int gsm48_tx_gmm_det_ack(struct sgsn_mm_ctx *mm, uint8_t force_stby)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+
+	DEBUGP(DMM, "<- GPRS DETACH ACCEPT\n");
+
+	mmctx2msgid(msg, mm);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh) + 1);
+	gh->proto_discr = GSM48_PDISC_MM_GPRS;
+	gh->msg_type = GSM48_MT_GMM_DETACH_ACK;
+	gh->data[0] = force_stby;
+
+	return gsm48_gmm_sendmsg(msg, 0, mm);
+}
+
+/* Transmit Chapter 9.4.12 Identity Request */
+static int gsm48_tx_gmm_id_req(struct sgsn_mm_ctx *mm, uint8_t id_type)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+
+	DEBUGP(DMM, "<- GPRS IDENTITY REQUEST: mi_type=%02x\n", id_type);
+
+	mmctx2msgid(msg, mm);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh) + 1);
+	gh->proto_discr = GSM48_PDISC_MM_GPRS;
+	gh->msg_type = GSM48_MT_GMM_ID_REQ;
+	/* 10.5.5.9 ID type 2 + identity type and 10.5.5.7 'force to standby' IE */
+	gh->data[0] = id_type & 0xf;
+
+	return gsm48_gmm_sendmsg(msg, 1, mm);
+}
+
+/* Section 9.4.9: Authentication and Ciphering Request */
+static int gsm48_tx_gmm_auth_ciph_req(struct sgsn_mm_ctx *mm, uint8_t *rand,
+				      uint8_t key_seq, uint8_t algo)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+	struct gsm48_auth_ciph_req *acreq;
+	uint8_t *m_rand, *m_cksn;
+
+	DEBUGP(DMM, "<- GPRS AUTH AND CIPHERING REQ (rand = %s)\n",
+		hexdump(rand, 16));
+
+	mmctx2msgid(msg, mm);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh));
+	gh->proto_discr = GSM48_PDISC_MM_GPRS;
+	gh->msg_type = GSM48_MT_GMM_AUTH_CIPH_REQ;
+
+	acreq = (struct gsm48_auth_ciph_req *) msgb_put(msg, sizeof(*acreq));
+	acreq->ciph_alg = algo & 0xf;
+	acreq->imeisv_req = 0x1;
+	acreq->force_stby = 0x0;
+	acreq->ac_ref_nr = 0x0;	/* FIXME: increment this? */
+
+	/* Only if authentication is requested we need to set RAND + CKSN */
+	if (rand) {
+		m_rand = msgb_put(msg, 16+1);
+		m_rand[0] = GSM48_IE_GMM_AUTH_RAND;
+		memcpy(m_rand+1, rand, 16);
+
+		m_cksn = msgb_put(msg, 1+1);
+		m_cksn[0] = GSM48_IE_GMM_CIPH_CKSN;
+		m_cksn[1] = key_seq;
+	}
+
+	/* Start T3360 */
+	mmctx_timer_start(mm, 3360, GSM0408_T3360_SECS);
+
+	/* FIXME: make sure we don't send any other messages to the MS */
+
+	return gsm48_gmm_sendmsg(msg, 1, mm);
+}
+
+/* Section 9.4.11: Authentication and Ciphering Reject */
+static int gsm48_tx_gmm_auth_ciph_rej(struct sgsn_mm_ctx *mm)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+
+	DEBUGP(DMM, "<- GPRS AUTH AND CIPH REJECT\n");
+
+	mmctx2msgid(msg, mm);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh));
+	gh->proto_discr = GSM48_PDISC_MM_GPRS;
+	gh->msg_type = GSM48_MT_GMM_AUTH_CIPH_REJ;
+
+	return gsm48_gmm_sendmsg(msg, 0, mm);
+}
+
+/* Section 9.4.10: Authentication and Ciphering Response */
+static int gsm48_rx_gmm_auth_ciph_resp(struct sgsn_mm_ctx *ctx,
+					struct msgb *msg)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	struct gsm48_auth_ciph_resp *acr = (struct gsm48_auth_ciph_resp *)gh->data;
+	struct tlv_parsed tp;
+	int rc;
+
+	/* FIXME: Stop T3360 */
+
+	rc = tlv_parse(&tp, &gsm48_gmm_att_tlvdef, acr->data,
+			(msg->data + msg->len) - acr->data, 0, 0);
+
+	/* FIXME: compare ac_ref? */
+
+	if (!TLVP_PRESENT(&tp, GSM48_IE_GMM_AUTH_SRES) ||
+	    !TLVP_PRESENT(&tp, GSM48_IE_GMM_IMEISV)) {
+		/* FIXME: missing mandatory IE */
+	}
+
+	/* FIXME: compare SRES with what we expected */
+	/* FIXME: enable LLC cipheirng */
+	return 0;
+}
+
+/* Check if we can already authorize a subscriber */
+static int gsm48_gmm_authorize(struct sgsn_mm_ctx *ctx,
+				enum gprs_t3350_mode t3350_mode)
+{
+	if (strlen(ctx->imei) && strlen(ctx->imsi)) {
+#ifdef PTMSI_ALLOC
+		/* Start T3350 and re-transmit up to 5 times until ATTACH COMPLETE */
+		ctx->t3350_mode = t3350_mode;
+		mmctx_timer_start(ctx, 3350, GSM0408_T3350_SECS);
+#endif
+		ctx->mm_state = GMM_REGISTERED_NORMAL;
+		return gsm48_tx_gmm_att_ack(ctx);
+	} 
+	if (!strlen(ctx->imei)) {
+		ctx->mm_state = GMM_COMMON_PROC_INIT;
+		ctx->t3370_id_type = GSM_MI_TYPE_IMEI;
+		mmctx_timer_start(ctx, 3370, GSM0408_T3370_SECS);
+		return gsm48_tx_gmm_id_req(ctx, GSM_MI_TYPE_IMEI);
+	}
+
+	if (!strlen(ctx->imsi)) {
+		ctx->mm_state = GMM_COMMON_PROC_INIT;
+		ctx->t3370_id_type = GSM_MI_TYPE_IMSI;
+		mmctx_timer_start(ctx, 3370, GSM0408_T3370_SECS);
+		return gsm48_tx_gmm_id_req(ctx, GSM_MI_TYPE_IMSI);
+	}
+
+	return 0;
+}
+
+/* Parse Chapter 9.4.13 Identity Response */
+static int gsm48_rx_gmm_id_resp(struct sgsn_mm_ctx *ctx, struct msgb *msg)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	uint8_t mi_type = gh->data[1] & GSM_MI_TYPE_MASK;
+	char mi_string[GSM48_MI_SIZE];
+
+	gsm48_mi_to_string(mi_string, sizeof(mi_string), &gh->data[1], gh->data[0]);
+	DEBUGP(DMM, "-> GMM IDENTITY RESPONSE: mi_type=0x%02x MI(%s) ",
+		mi_type, mi_string);
+
+	if (!ctx) {
+		DEBUGP(DMM, "from unknown TLLI 0x%08x?!?\n", msgb_tlli(msg));
+		return -EINVAL;
+	}
+
+	if (mi_type == ctx->t3370_id_type)
+		mmctx_timer_stop(ctx, 3370);
+
+	switch (mi_type) {
+	case GSM_MI_TYPE_IMSI:
+		/* we already have a mm context with current TLLI, but no
+		 * P-TMSI / IMSI yet.  What we now need to do is to fill
+		 * this initial context with data from the HLR */
+		if (strlen(ctx->imsi) == 0) {
+			/* Check if we already have a MM context for this IMSI */
+			struct sgsn_mm_ctx *ictx;
+			ictx = sgsn_mm_ctx_by_imsi(mi_string);
+			if (ictx) {
+				DEBUGP(DMM, "Deleting old MM Context for same IMSI ",
+				       "p_tmsi_old=0x%08x, p_tmsi_new=0x%08x\n",
+					ictx->p_tmsi, ctx->p_tmsi);
+				gprs_llgmm_assign(ictx->llme, ictx->tlli,
+						  0xffffffff, GPRS_ALGO_GEA0, NULL);
+				sgsn_mm_ctx_free(ictx);
+			}
+		}
+		strncpy(ctx->imsi, mi_string, sizeof(ctx->imei));
+		break;
+	case GSM_MI_TYPE_IMEI:
+		strncpy(ctx->imei, mi_string, sizeof(ctx->imei));
+		break;
+	case GSM_MI_TYPE_IMEISV:
+		break;
+	}
+
+	DEBUGPC(DMM, "\n");
+	/* Check if we can let the mobile station enter */
+	return gsm48_gmm_authorize(ctx, ctx->t3350_mode);
+}
+
+/* Section 9.4.1 Attach request */
+static int gsm48_rx_gmm_att_req(struct sgsn_mm_ctx *ctx, struct msgb *msg,
+				struct gprs_llc_llme *llme)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	uint8_t *cur = gh->data, *msnc, *mi, *old_ra_info, *ms_ra_acc_cap;
+	uint8_t msnc_len, att_type, mi_len, mi_type, ms_ra_acc_cap_len;
+	uint16_t drx_par;
+	uint32_t tmsi;
+	char mi_string[GSM48_MI_SIZE];
+	struct gprs_ra_id ra_id;
+	uint16_t cid;
+
+	DEBUGP(DMM, "-> GMM ATTACH REQUEST ");
+
+	/* As per TS 04.08 Chapter 4.7.1.4, the attach request arrives either
+	 * with a foreign TLLI (P-TMSI that was allocated to the MS before),
+	 * or with random TLLI. */
+
+	cid = bssgp_parse_cell_id(&ra_id, msgb_bcid(msg));
+
+	/* MS network capability 10.5.5.12 */
+	msnc_len = *cur++;
+	msnc = cur;
+	if (msnc_len > 8)
+		goto err_inval;
+	cur += msnc_len;
+
+	/* aTTACH Type 10.5.5.2 */
+	att_type = *cur++ & 0x0f;
+
+	/* DRX parameter 10.5.5.6 */
+	drx_par = *cur++ << 8;
+	drx_par |= *cur++;
+
+	/* Mobile Identity (P-TMSI or IMSI) 10.5.1.4 */
+	mi_len = *cur++;
+	mi = cur;
+	if (mi_len > 8)
+		goto err_inval;
+	mi_type = *mi & GSM_MI_TYPE_MASK;
+	cur += mi_len;
+
+	gsm48_mi_to_string(mi_string, sizeof(mi_string), mi, mi_len);
+
+	DEBUGPC(DMM, "MI(%s) type=\"%s\" ", mi_string,
+		get_value_string(gprs_att_t_strs, att_type));
+
+	/* Old routing area identification 10.5.5.15 */
+	old_ra_info = cur;
+	cur += 6;
+
+	/* MS Radio Access Capability 10.5.5.12a */
+	ms_ra_acc_cap_len = *cur++;
+	ms_ra_acc_cap = cur;
+	if (ms_ra_acc_cap_len > 51)
+		goto err_inval;
+
+	/* Optional: Old P-TMSI Signature, Requested READY timer, TMSI Status */
+
+	switch (mi_type) {
+	case GSM_MI_TYPE_IMSI:
+		/* Try to find MM context based on IMSI */
+		if (!ctx)
+			ctx = sgsn_mm_ctx_by_imsi(mi_string);
+		if (!ctx) {
+#if 0
+			return gsm48_tx_gmm_att_rej(msg, GMM_CAUSE_IMSI_UNKNOWN);
+#else
+			/* As a temorary hack, we simply assume that the IMSI exists,
+			 * as long as it is part of 'our' network */
+			char mccmnc[16];
+			snprintf(mccmnc, sizeof(mccmnc), "%03d%02d", ra_id.mcc, ra_id.mnc);
+			if (strncmp(mccmnc, mi_string, 5)) {
+				LOGP(DMM, LOGL_INFO, "Rejecting ATTACH REQUESET IMSI=%s\n",
+				     mi_string);
+				return gsm48_tx_gmm_att_rej_oldmsg(msg,
+								GMM_CAUSE_GPRS_NOTALLOWED);
+			}
+			ctx = sgsn_mm_ctx_alloc(0, &ra_id);
+			if (!ctx)
+				return gsm48_tx_gmm_att_rej_oldmsg(msg, GMM_CAUSE_NET_FAIL);
+			strncpy(ctx->imsi, mi_string, sizeof(ctx->imsi));
+#endif
+		}
+		ctx->tlli = msgb_tlli(msg);
+		ctx->llme = llme;
+		msgid2mmctx(ctx, msg);
+		break;
+	case GSM_MI_TYPE_TMSI:
+		memcpy(&tmsi, mi+1, 4);
+		tmsi = ntohl(tmsi);
+		/* Try to find MM context based on P-TMSI */
+		if (!ctx)
+			ctx = sgsn_mm_ctx_by_ptmsi(tmsi);
+		if (!ctx) {
+			/* Allocate a context as most of our code expects one.
+			 * Context will not have an IMSI ultil ID RESP is received */
+			ctx = sgsn_mm_ctx_alloc(msgb_tlli(msg), &ra_id);
+			ctx->p_tmsi = tmsi;
+		}
+		ctx->tlli = msgb_tlli(msg);
+		ctx->llme = llme;
+		msgid2mmctx(ctx, msg);
+		break;
+	default:
+		LOGP(DMM, LOGL_NOTICE, "Rejecting ATTACH REQUEST with "
+			"MI type %u\n", mi_type);
+		return gsm48_tx_gmm_att_rej_oldmsg(msg, GMM_CAUSE_MS_ID_NOT_DERIVED);
+	}
+	/* Update MM Context with currient RA and Cell ID */
+	ctx->ra = ra_id;
+	ctx->cell_id = cid;
+	/* Update MM Context with other data */
+	ctx->drx_parms = drx_par;
+	ctx->ms_radio_access_capa.len = ms_ra_acc_cap_len;
+	memcpy(ctx->ms_radio_access_capa.buf, ms_ra_acc_cap, ms_ra_acc_cap_len);
+	ctx->ms_network_capa.len = msnc_len;
+	memcpy(ctx->ms_network_capa.buf, msnc, msnc_len);
+
+#ifdef PTMSI_ALLOC
+	/* Allocate a new P-TMSI (+ P-TMSI signature) and update TLLI */
+	ctx->p_tmsi_old = ctx->p_tmsi;
+	ctx->p_tmsi = sgsn_alloc_ptmsi();
+#endif
+	/* Even if there is no P-TMSI allocated, the MS will switch from
+	 * foreign TLLI to local TLLI */
+	ctx->tlli_new = gprs_tmsi2tlli(ctx->p_tmsi, TLLI_LOCAL);
+
+	/* Inform LLC layer about new TLLI but keep old active */
+	gprs_llgmm_assign(ctx->llme, ctx->tlli, ctx->tlli_new,
+			  GPRS_ALGO_GEA0, NULL);
+
+	DEBUGPC(DMM, "\n");
+	return ctx ? gsm48_gmm_authorize(ctx, GMM_T3350_MODE_ATT) : 0;
+
+err_inval:
+	DEBUGPC(DMM, "\n");
+	return gsm48_tx_gmm_att_rej_oldmsg(msg, GMM_CAUSE_SEM_INCORR_MSG);
+}
+
+/* Section 4.7.4.1 / 9.4.5.2 MO Detach request */
+static int gsm48_rx_gmm_det_req(struct sgsn_mm_ctx *ctx, struct msgb *msg)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	struct sgsn_pdp_ctx *pdp, *pdp2;
+	uint8_t detach_type, power_off;
+	int rc;
+
+	detach_type = gh->data[0] & 0x7;
+	power_off = gh->data[0] & 0x8;
+
+	/* FIXME: In 24.008 there is an optional P-TMSI and P-TMSI signature IE */
+
+	DEBUGP(DMM, "-> GMM DETACH REQUEST TLLI=0x%08x type=%s %s\n",
+		msgb_tlli(msg), get_value_string(gprs_det_t_mo_strs, detach_type),
+		power_off ? "Power-off" : "");
+
+	/* Mark MM state as deregistered */
+	ctx->mm_state = GMM_DEREGISTERED;
+
+	/* delete all existing PDP contexts for this MS */
+	llist_for_each_entry_safe(pdp, pdp2, &ctx->pdp_list, list) {
+		LOGP(DMM, LOGL_NOTICE, "Dropping PDP context for NSAPI=%u "
+		     "due to GPRS DETACH REQUEST\n", pdp->nsapi);
+		sgsn_delete_pdp_ctx(pdp);
+		/* FIXME: the callback wants to transmit a DEACT PDP CTX ACK,
+		 * which is quite stupid for a MS that has just detached.. */
+	}
+
+	/* force_stby = 0 */
+	rc = gsm48_tx_gmm_det_ack(ctx, 0);
+
+	/* TLLI unassignment */
+	gprs_llgmm_assign(ctx->llme, ctx->tlli, 0xffffffff,
+			  GPRS_ALGO_GEA0, NULL);
+
+	return rc;
+}
+
+/* Chapter 9.4.15: Routing area update accept */
+static int gsm48_tx_gmm_ra_upd_ack(struct sgsn_mm_ctx *mm)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+	struct gsm48_ra_upd_ack *rua;
+	uint8_t *mid;
+
+	DEBUGP(DMM, "<- ROUTING AREA UPDATE ACCEPT\n");
+
+	mmctx2msgid(msg, mm);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh));
+	gh->proto_discr = GSM48_PDISC_MM_GPRS;
+	gh->msg_type = GSM48_MT_GMM_RA_UPD_ACK;
+
+	rua = (struct gsm48_ra_upd_ack *) msgb_put(msg, sizeof(*rua));
+	rua->force_stby = 0;	/* not indicated */
+	rua->upd_result = 0;	/* RA updated */
+	rua->ra_upd_timer = GPRS_TMR_MINUTE | 10;
+
+	gsm48_construct_ra(rua->ra_id.digits, &mm->ra);
+
+#if 0
+	/* Optional: P-TMSI signature */
+	msgb_v_put(msg, GSM48_IE_GMM_PTMSI_SIG);
+	ptsig = msgb_put(msg, 3);
+	ptsig[0] = mm->p_tmsi_sig >> 16;
+	ptsig[1] = mm->p_tmsi_sig >> 8;
+	ptsig[2] = mm->p_tmsi_sig & 0xff;
+#endif
+
+#ifdef PTMSI_ALLOC
+	/* Optional: Allocated P-TMSI */
+	mid = msgb_put(msg, GSM48_MID_TMSI_LEN);
+	gsm48_generate_mid_from_tmsi(mid, mm->p_tmsi);
+	mid[0] = GSM48_IE_GMM_ALLOC_PTMSI;
+#endif
+
+	/* Option: MS ID, ... */
+	return gsm48_gmm_sendmsg(msg, 0, mm);
+}
+
+/* Chapter 9.4.17: Routing area update reject */
+static int gsm48_tx_gmm_ra_upd_rej(struct msgb *old_msg, uint8_t cause)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+
+	DEBUGP(DMM, "<- ROUTING AREA UPDATE REJECT\n");
+
+	gmm_copy_id(msg, old_msg);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh) + 2);
+	gh->proto_discr = GSM48_PDISC_MM_GPRS;
+	gh->msg_type = GSM48_MT_GMM_RA_UPD_REJ;
+	gh->data[0] = cause;
+	gh->data[1] = 0; /* ? */
+
+	/* Option: P-TMSI signature, allocated P-TMSI, MS ID, ... */
+	return gsm48_gmm_sendmsg(msg, 0, NULL);
+}
+
+static void process_ms_ctx_status(struct sgsn_mm_ctx *mmctx,
+				  uint16_t pdp_status)
+{
+	struct sgsn_pdp_ctx *pdp, *pdp2;
+	/* 24.008 4.7.5.1.3: If the PDP context status information element is
+	 * included in ROUTING AREA UPDATE REQUEST message, then the network
+	 * shall deactivate all those PDP contexts locally (without peer to
+	 * peer signalling between the MS and the network), which are not in SM
+	 * state PDP-INACTIVE on network side but are indicated by the MS as
+	 * being in state PDP-INACTIVE. */
+
+	llist_for_each_entry_safe(pdp, pdp2, &mmctx->pdp_list, list) {
+		if (!(pdp_status & (1 << pdp->nsapi))) {
+			LOGP(DMM, LOGL_NOTICE, "Dropping PDP context for NSAPI=%u "
+				"due to PDP CTX STATUS IE= 0x%04x\n",
+				pdp->nsapi, pdp_status);
+			sgsn_delete_pdp_ctx(pdp);
+		}
+	}
+}
+
+/* Chapter 9.4.14: Routing area update request */
+static int gsm48_rx_gmm_ra_upd_req(struct sgsn_mm_ctx *mmctx, struct msgb *msg,
+				   struct gprs_llc_llme *llme)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	uint8_t *cur = gh->data;
+	uint8_t *ms_ra_acc_cap;
+	uint8_t ms_ra_acc_cap_len;
+	struct gprs_ra_id old_ra_id;
+	struct tlv_parsed tp;
+	uint8_t upd_type;
+	int rc;
+
+	/* Update Type 10.5.5.18 */
+	upd_type = *cur++ & 0x0f;
+
+	DEBUGP(DMM, "-> GMM RA UPDATE REQUEST type=\"%s\" ",
+		get_value_string(gprs_upd_t_strs, upd_type));
+
+	/* Old routing area identification 10.5.5.15 */
+	gsm48_parse_ra(&old_ra_id, cur);
+	cur += 6;
+
+	/* MS Radio Access Capability 10.5.5.12a */
+	ms_ra_acc_cap_len = *cur++;
+	ms_ra_acc_cap = cur;
+
+	/* Optional: Old P-TMSI Signature, Requested READY timer, TMSI Status,
+	 * DRX parameter, MS network capability */
+	rc = tlv_parse(&tp, &gsm48_gmm_att_tlvdef, cur,
+			(msg->data + msg->len) - cur, 0, 0);
+
+	switch (upd_type) {
+	case GPRS_UPD_T_RA_LA:
+	case GPRS_UPD_T_RA_LA_IMSI_ATT:
+		DEBUGPC(DMM, " unsupported in Mode III, is your SI13 corrupt?\n");
+		return gsm48_tx_gmm_ra_upd_rej(msg, GMM_CAUSE_PROTO_ERR_UNSPEC);
+		break;
+	case GPRS_UPD_T_RA:
+	case GPRS_UPD_T_PERIODIC:
+		break;
+	}
+
+	/* Look-up the MM context based on old RA-ID and TLLI */
+	mmctx = sgsn_mm_ctx_by_tlli(msgb_tlli(msg), &old_ra_id);
+	if (!mmctx || mmctx->mm_state == GMM_DEREGISTERED) {
+		/* The MS has to perform GPRS attach */
+		DEBUGPC(DMM, " REJECT\n");
+		/* Device is still IMSI atached for CS but initiate GPRS ATTACH */
+		return gsm48_tx_gmm_ra_upd_rej(msg, GMM_CAUSE_MS_ID_NOT_DERIVED);
+	}
+
+	/* Store new BVCI/NSEI in MM context (FIXME: delay until we ack?) */
+	msgid2mmctx(mmctx, msg);
+	/* Bump the statistics of received signalling msgs for this MM context */
+	rate_ctr_inc(&mmctx->ctrg->ctr[GMM_CTR_PKTS_SIG_IN]);
+
+	/* Update the MM context with the new RA-ID */
+	bssgp_parse_cell_id(&mmctx->ra, msgb_bcid(msg));
+	/* Update the MM context with the new (i.e. foreign) TLLI */
+	mmctx->tlli = msgb_tlli(msg);
+	/* FIXME: Update the MM context with the MS radio acc capabilities */
+	/* FIXME: Update the MM context with the MS network capabilities */
+
+	rate_ctr_inc(&mmctx->ctrg->ctr[GMM_CTR_RA_UPDATE]);
+
+	DEBUGPC(DMM, " ACCEPT\n");
+#ifdef PTMSI_ALLOC
+	mmctx->p_tmsi_old = mmctx->p_tmsi;
+	mmctx->p_tmsi = sgsn_alloc_ptmsi();
+	/* Start T3350 and re-transmit up to 5 times until ATTACH COMPLETE */
+	mmctx->t3350_mode = GMM_T3350_MODE_RAU;
+	mmctx_timer_start(mmctx, 3350, GSM0408_T3350_SECS);
+#endif
+	/* Even if there is no P-TMSI allocated, the MS will switch from
+	 * foreign TLLI to local TLLI */
+	mmctx->tlli_new = gprs_tmsi2tlli(mmctx->p_tmsi, TLLI_LOCAL);
+
+	/* Inform LLC layer about new TLLI but keep old active */
+	gprs_llgmm_assign(mmctx->llme, mmctx->tlli, mmctx->tlli_new,
+			  GPRS_ALGO_GEA0, NULL);
+
+	/* Look at PDP Context Status IE and see if MS's view of
+	 * activated/deactivated NSAPIs agrees with our view */
+	if (TLVP_PRESENT(&tp, GSM48_IE_GMM_PDP_CTX_STATUS)) {
+		uint16_t pdp_status = ntohs(*(uint16_t *)
+				TLVP_VAL(&tp, GSM48_IE_GMM_PDP_CTX_STATUS));
+		process_ms_ctx_status(mmctx, pdp_status);
+	}
+
+	/* Send RA UPDATE ACCEPT */
+	return gsm48_tx_gmm_ra_upd_ack(mmctx);
+}
+
+static int gsm48_rx_gmm_status(struct sgsn_mm_ctx *mmctx, struct msgb *msg)
+{
+	struct gsm48_hdr *gh = msgb_l3(msg);
+
+	DEBUGP(DMM, "-> GPRS MM STATUS (cause: %s)\n",
+		get_value_string(gmm_cause_names, gh->data[0]));
+
+	return 0;
+}
+
+/* GPRS Mobility Management */
+static int gsm0408_rcv_gmm(struct sgsn_mm_ctx *mmctx, struct msgb *msg,
+			   struct gprs_llc_llme *llme)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	int rc;
+
+	/* MMCTX can be NULL when called */
+
+	if (!mmctx &&
+	    gh->msg_type != GSM48_MT_GMM_ATTACH_REQ &&
+	    gh->msg_type != GSM48_MT_GMM_RA_UPD_REQ) {
+		LOGP(DMM, LOGL_NOTICE, "Cannot handle GMM for unknown MM CTX\n");
+		return gsm48_tx_gmm_status_oldmsg(msg, GMM_CAUSE_MS_ID_NOT_DERIVED);
+	}
+
+	switch (gh->msg_type) {
+	case GSM48_MT_GMM_RA_UPD_REQ:
+		rc = gsm48_rx_gmm_ra_upd_req(mmctx, msg, llme);
+		break;
+	case GSM48_MT_GMM_ATTACH_REQ:
+		rc = gsm48_rx_gmm_att_req(mmctx, msg, llme);
+		break;
+	case GSM48_MT_GMM_ID_RESP:
+		rc = gsm48_rx_gmm_id_resp(mmctx, msg);
+		break;
+	case GSM48_MT_GMM_STATUS:
+		rc = gsm48_rx_gmm_status(mmctx, msg);
+		break;
+	case GSM48_MT_GMM_DETACH_REQ:
+		rc = gsm48_rx_gmm_det_req(mmctx, msg);
+		break;
+	case GSM48_MT_GMM_ATTACH_COMPL:
+		/* only in case SGSN offered new P-TMSI */
+		DEBUGP(DMM, "-> ATTACH COMPLETE\n");
+		mmctx_timer_stop(mmctx, 3350);
+		mmctx->p_tmsi_old = 0;
+		/* Unassign the old TLLI */
+		mmctx->tlli = mmctx->tlli_new;
+		gprs_llgmm_assign(mmctx->llme, 0xffffffff, mmctx->tlli_new,
+				  GPRS_ALGO_GEA0, NULL);
+		break;
+	case GSM48_MT_GMM_RA_UPD_COMPL:
+		/* only in case SGSN offered new P-TMSI */
+		DEBUGP(DMM, "-> ROUTEING AREA UPDATE COMPLETE\n");
+		mmctx_timer_stop(mmctx, 3350);
+		mmctx->p_tmsi_old = 0;
+		/* Unassign the old TLLI */
+		mmctx->tlli = mmctx->tlli_new;
+		gprs_llgmm_assign(mmctx->llme, 0xffffffff, mmctx->tlli_new,
+				  GPRS_ALGO_GEA0, NULL);
+		break;
+	case GSM48_MT_GMM_PTMSI_REALL_COMPL:
+		DEBUGP(DMM, "-> PTMSI REALLLICATION COMPLETE\n");
+		mmctx_timer_stop(mmctx, 3350);
+		mmctx->p_tmsi_old = 0;
+		/* Unassign the old TLLI */
+		mmctx->tlli = mmctx->tlli_new;
+		//gprs_llgmm_assign(mmctx->llme, 0xffffffff, mmctx->tlli_new, GPRS_ALGO_GEA0, NULL);
+		break;
+	case GSM48_MT_GMM_AUTH_CIPH_RESP:
+		rc = gsm48_rx_gmm_auth_ciph_resp(mmctx, msg);
+		break;
+	default:
+		DEBUGP(DMM, "Unknown GSM 04.08 GMM msg type 0x%02x\n",
+			gh->msg_type);
+		rc = gsm48_tx_gmm_status(mmctx, GMM_CAUSE_MSGT_NOTEXIST_NOTIMPL);
+		break;
+	}
+
+	return rc;
+}
+
+static void mmctx_timer_cb(void *_mm)
+{
+	struct sgsn_mm_ctx *mm = _mm;
+
+	mm->num_T_exp++;
+
+	switch (mm->T) {
+	case 3350:	/* waiting for ATTACH COMPLETE */
+		if (mm->num_T_exp >= 5) {
+			LOGP(DMM, LOGL_NOTICE, "T3350 expired >= 5 times\n");
+			mm->mm_state = GMM_DEREGISTERED;
+			/* FIXME: should we return some error? */
+			break;
+		}
+		/* re-transmit the respective msg and re-start timer */
+		switch (mm->t3350_mode) {
+		case GMM_T3350_MODE_ATT:
+			gsm48_tx_gmm_att_ack(mm);
+			break;
+		case GMM_T3350_MODE_RAU:
+			gsm48_tx_gmm_ra_upd_ack(mm);
+			break;
+		case GMM_T3350_MODE_PTMSI_REALL:
+			/* FIXME */
+			break;
+		}
+		bsc_schedule_timer(&mm->timer, GSM0408_T3350_SECS, 0);
+		break;
+	case 3360:	/* waiting for AUTH AND CIPH RESP */
+		if (mm->num_T_exp >= 5) {
+			LOGP(DMM, LOGL_NOTICE, "T3360 expired >= 5 times\n");
+			mm->mm_state = GMM_DEREGISTERED;
+			break;
+		}
+		/* FIXME: re-transmit the respective msg and re-start timer */
+		bsc_schedule_timer(&mm->timer, GSM0408_T3360_SECS, 0);
+		break;
+	case 3370:	/* waiting for IDENTITY RESPONSE */
+		if (mm->num_T_exp >= 5) {
+			LOGP(DMM, LOGL_NOTICE, "T3370 expired >= 5 times\n");
+			gsm48_tx_gmm_att_rej(mm, GMM_CAUSE_MS_ID_NOT_DERIVED);
+			mm->mm_state = GMM_DEREGISTERED;
+			break;
+		}
+		/* re-tranmit IDENTITY REQUEST and re-start timer */
+		gsm48_tx_gmm_id_req(mm, mm->t3370_id_type);
+		bsc_schedule_timer(&mm->timer, GSM0408_T3370_SECS, 0);
+		break;
+	default:
+		LOGP(DMM, LOGL_ERROR, "timer expired in unknown mode %u\n",
+			mm->T);
+	}
+}
+
+/* GPRS SESSION MANAGEMENT */
+
+static void pdpctx_timer_cb(void *_mm);
+
+static void pdpctx_timer_start(struct sgsn_pdp_ctx *pdp, unsigned int T,
+				unsigned int seconds)
+{
+	if (bsc_timer_pending(&pdp->timer))
+		LOGP(DMM, LOGL_ERROR, "Starting MM timer %u while old "
+			"timer %u pending\n", T, pdp->T);
+	pdp->T = T;
+	pdp->num_T_exp = 0;
+
+	/* FIXME: we should do this only once ? */
+	pdp->timer.data = pdp;
+	pdp->timer.cb = &pdpctx_timer_cb;
+
+	bsc_schedule_timer(&pdp->timer, seconds, 0);
+}
+
+
+static void msgb_put_pdp_addr_ipv4(struct msgb *msg, uint32_t ipaddr)
+{
+	uint8_t v[6];
+
+	v[0] = PDP_TYPE_ORG_IETF;
+	v[1] = PDP_TYPE_N_IETF_IPv4;
+	*(uint32_t *)(v+2) = htonl(ipaddr);
+
+	msgb_tlv_put(msg, GSM48_IE_GSM_PDP_ADDR, sizeof(v), v);
+}
+
+static void msgb_put_pdp_addr_ppp(struct msgb *msg)
+{
+	uint8_t v[2];
+
+	v[0] = PDP_TYPE_ORG_ETSI;
+	v[1] = PDP_TYPE_N_ETSI_PPP;
+
+	msgb_tlv_put(msg, GSM48_IE_GSM_PDP_ADDR, sizeof(v), v);
+}
+
+/* Section 9.5.2: Ativate PDP Context Accept */
+int gsm48_tx_gsm_act_pdp_acc(struct sgsn_pdp_ctx *pdp)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+	uint8_t transaction_id = pdp->ti ^ 0x8; /* flip */
+
+	DEBUGP(DMM, "<- ACTIVATE PDP CONTEXT ACK\n");
+
+	mmctx2msgid(msg, pdp->mm);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh));
+	gh->proto_discr = GSM48_PDISC_SM_GPRS | (transaction_id << 4);
+	gh->msg_type = GSM48_MT_GSM_ACT_PDP_ACK;
+
+	/* Negotiated LLC SAPI */
+	msgb_v_put(msg, pdp->sapi);
+
+	/* FIXME: copy QoS parameters from original request */
+	//msgb_lv_put(msg, pdp->lib->qos_neg.l, pdp->lib->qos_neg.v);
+	msgb_lv_put(msg, sizeof(default_qos), (uint8_t *)&default_qos);
+
+	/* Radio priority 10.5.7.2 */
+	msgb_v_put(msg, pdp->lib->radio_pri);
+
+	/* PDP address */
+	/* Highest 4 bits of first byte need to be set to 1, otherwise
+	 * the IE is identical with the 04.08 PDP Address IE */
+	pdp->lib->eua.v[0] &= ~0xf0;
+	msgb_tlv_put(msg, GSM48_IE_GSM_PDP_ADDR,
+		     pdp->lib->eua.l, pdp->lib->eua.v);
+	pdp->lib->eua.v[0] |= 0xf0;
+
+	/* Optional: Protocol configuration options (FIXME: why 'req') */
+	if (pdp->lib->pco_req.l && pdp->lib->pco_req.v)
+		msgb_tlv_put(msg, GSM48_IE_GSM_PROTO_CONF_OPT,
+			     pdp->lib->pco_req.l, pdp->lib->pco_req.v);
+
+	/* Optional: Packet Flow Identifier */
+
+	return gsm48_gmm_sendmsg(msg, 0, pdp->mm);
+}
+
+/* Section 9.5.3: Activate PDP Context reject */
+int gsm48_tx_gsm_act_pdp_rej(struct sgsn_mm_ctx *mm, uint8_t tid,
+			     uint8_t cause, uint8_t pco_len, uint8_t *pco_v)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+	uint8_t transaction_id = tid ^ 0x8; /* flip */
+
+	DEBUGP(DMM, "<- ACTIVATE PDP CONTEXT REJ(cause=%u)\n", cause);
+
+	mmctx2msgid(msg, mm);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh));
+	gh->proto_discr = GSM48_PDISC_SM_GPRS | (transaction_id << 4);
+	gh->msg_type = GSM48_MT_GSM_ACT_PDP_REJ;
+
+	msgb_v_put(msg, cause);
+	if (pco_len && pco_v)
+		msgb_tlv_put(msg, GSM48_IE_GSM_PROTO_CONF_OPT, pco_len, pco_v);
+
+	return gsm48_gmm_sendmsg(msg, 0, mm);
+}
+
+/* Section 9.5.8: Deactivate PDP Context Request */
+static int _gsm48_tx_gsm_deact_pdp_req(struct sgsn_mm_ctx *mm, uint8_t tid,
+					uint8_t sm_cause)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+	uint8_t transaction_id = tid ^ 0x8; /* flip */
+
+	DEBUGP(DMM, "<- DEACTIVATE PDP CONTEXT REQ\n");
+
+	mmctx2msgid(msg, mm);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh));
+	gh->proto_discr = GSM48_PDISC_SM_GPRS | (transaction_id << 4);
+	gh->msg_type = GSM48_MT_GSM_DEACT_PDP_REQ;
+
+	msgb_v_put(msg, sm_cause);
+
+	return gsm48_gmm_sendmsg(msg, 0, mm);
+}
+int gsm48_tx_gsm_deact_pdp_req(struct sgsn_pdp_ctx *pdp, uint8_t sm_cause)
+{
+	pdpctx_timer_start(pdp, 3395, GSM0408_T3395_SECS);
+
+	return _gsm48_tx_gsm_deact_pdp_req(pdp->mm, pdp->ti, sm_cause);
+}
+
+/* Section 9.5.9: Deactivate PDP Context Accept */
+static int _gsm48_tx_gsm_deact_pdp_acc(struct sgsn_mm_ctx *mm, uint8_t tid)
+{
+	struct msgb *msg = gsm48_msgb_alloc();
+	struct gsm48_hdr *gh;
+	uint8_t transaction_id = tid ^ 0x8; /* flip */
+
+	DEBUGP(DMM, "<- DEACTIVATE PDP CONTEXT ACK\n");
+
+	mmctx2msgid(msg, mm);
+
+	gh = (struct gsm48_hdr *) msgb_put(msg, sizeof(*gh));
+	gh->proto_discr = GSM48_PDISC_SM_GPRS | (transaction_id << 4);
+	gh->msg_type = GSM48_MT_GSM_DEACT_PDP_ACK;
+
+	return gsm48_gmm_sendmsg(msg, 0, mm);
+}
+int gsm48_tx_gsm_deact_pdp_acc(struct sgsn_pdp_ctx *pdp)
+{
+	return _gsm48_tx_gsm_deact_pdp_acc(pdp->mm, pdp->ti);
+}
+
+/* Section 9.5.1: Activate PDP Context Request */
+static int gsm48_rx_gsm_act_pdp_req(struct sgsn_mm_ctx *mmctx,
+				    struct msgb *msg)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	struct gsm48_act_pdp_ctx_req *act_req = (struct gsm48_act_pdp_ctx_req *) gh->data;
+	uint8_t req_qos_len, req_pdpa_len;
+	uint8_t *req_qos, *req_pdpa;
+	struct tlv_parsed tp;
+	uint8_t transaction_id = (gh->proto_discr >> 4);
+	struct sgsn_ggsn_ctx *ggsn;
+	struct sgsn_pdp_ctx *pdp;
+
+	DEBUGP(DMM, "-> ACTIVATE PDP CONTEXT REQ: SAPI=%u NSAPI=%u ",
+		act_req->req_llc_sapi, act_req->req_nsapi);
+
+	/* FIXME: length checks! */
+	req_qos_len = act_req->data[0];
+	req_qos = act_req->data + 1;	/* 10.5.6.5 */
+	req_pdpa_len = act_req->data[1 + req_qos_len];
+	req_pdpa = act_req->data + 1 + req_qos_len + 1;	/* 10.5.6.4 */
+
+	/* Optional: Access Point Name, Protocol Config Options */
+	if (req_pdpa + req_pdpa_len < msg->data + msg->len)
+		tlv_parse(&tp, &gsm48_sm_att_tlvdef, req_pdpa + req_pdpa_len,
+			  (msg->data + msg->len) - (req_pdpa + req_pdpa_len), 0, 0);
+	else
+		memset(&tp, 0, sizeof(tp));
+
+	switch (req_pdpa[0] & 0xf) {
+	case 0x0:
+		DEBUGPC(DMM, "ETSI ");
+		break;
+	case 0x1:
+		DEBUGPC(DMM, "IETF ");
+		break;
+	case 0xf:
+		DEBUGPC(DMM, "Empty ");
+		break;
+	}
+
+	switch (req_pdpa[1]) {
+	case 0x21:
+		DEBUGPC(DMM, "IPv4 ");
+		if (req_pdpa_len >= 6) {
+			struct in_addr ia;
+			ia.s_addr = ntohl(*((uint32_t *) (req_pdpa+2)));
+			DEBUGPC(DMM, "%s ", inet_ntoa(ia));
+		}
+		break;
+	case 0x57:
+		DEBUGPC(DMM, "IPv6 ");
+		if (req_pdpa_len >= 18) {
+			/* FIXME: print IPv6 address */
+		}
+		break;
+	default:	
+		DEBUGPC(DMM, "0x%02x ", req_pdpa[1]);
+		break;
+	}
+
+	DEBUGPC(DMM, "\n");
+
+	/* put the non-TLV elements in the TLV parser structure to
+	 * pass them on to the SGSN / GTP code */
+	tp.lv[OSMO_IE_GSM_REQ_QOS].len = req_qos_len;
+	tp.lv[OSMO_IE_GSM_REQ_QOS].val = req_qos;
+	tp.lv[OSMO_IE_GSM_REQ_PDP_ADDR].len = req_pdpa_len;
+	tp.lv[OSMO_IE_GSM_REQ_PDP_ADDR].val = req_pdpa;
+
+	/* FIXME:  determine GGSN based on APN and subscription options */
+	if (TLVP_PRESENT(&tp, GSM48_IE_GSM_APN)) {}
+
+	/* Check if NSAPI is out of range (TS 04.65 / 7.2) */
+	if (act_req->req_nsapi < 5 || act_req->req_nsapi > 15) {
+		/* Send reject with GSM_CAUSE_INV_MAND_INFO */
+		return gsm48_tx_gsm_act_pdp_rej(mmctx, transaction_id,
+						GSM_CAUSE_INV_MAND_INFO,
+						0, NULL);
+	}
+
+	/* Check if NSAPI is already in use */
+	pdp = sgsn_pdp_ctx_by_nsapi(mmctx, act_req->req_nsapi);
+	if (pdp) {
+		/* We already have a PDP context for this TLLI + NSAPI tuple */
+		if (pdp->sapi == act_req->req_llc_sapi &&
+		    pdp->ti == transaction_id) {
+			/* This apparently is a re-transmission of a PDP CTX
+			 * ACT REQ (our ACT ACK must have got dropped) */
+			return gsm48_tx_gsm_act_pdp_acc(pdp);
+		}
+
+		/* Send reject with GSM_CAUSE_NSAPI_IN_USE */
+		return gsm48_tx_gsm_act_pdp_rej(mmctx, transaction_id,
+						GSM_CAUSE_NSAPI_IN_USE,
+						0, NULL);
+	}
+
+	/* Only increment counter for a real activation, after we checked
+	 * for re-transmissions */
+	rate_ctr_inc(&mmctx->ctrg->ctr[GMM_CTR_PDP_CTX_ACT]);
+
+	ggsn = sgsn_ggsn_ctx_by_id(0);
+	if (!ggsn) {
+		LOGP(DGPRS, LOGL_ERROR, "No GGSN context 0 found!\n");
+		return -EIO;
+	}
+	ggsn->gsn = sgsn->gsn;
+	pdp = sgsn_create_pdp_ctx(ggsn, mmctx, act_req->req_nsapi, &tp);
+	if (!pdp)
+		return -1;
+
+	/* Store SAPI and Transaction Identifier */
+	pdp->sapi = act_req->req_llc_sapi;
+	pdp->ti = transaction_id;
+
+	return 0;
+}
+
+/* Section 9.5.8: Deactivate PDP Context Request */
+static int gsm48_rx_gsm_deact_pdp_req(struct sgsn_mm_ctx *mm, struct msgb *msg)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	uint8_t transaction_id = (gh->proto_discr >> 4);
+	struct sgsn_pdp_ctx *pdp;
+
+	DEBUGP(DMM, "-> DEACTIVATE PDP CONTEXT REQ (cause: %s)\n",
+		get_value_string(gsm_cause_names, gh->data[0]));
+
+	pdp = sgsn_pdp_ctx_by_tid(mm, transaction_id);
+	if (!pdp) {
+		LOGP(DMM, LOGL_NOTICE, "Deactivate PDP Context Request for "
+			"non-existing PDP Context (IMSI=%s, TI=%u)\n",
+			mm->imsi, transaction_id);
+		return _gsm48_tx_gsm_deact_pdp_acc(mm, transaction_id);
+	}
+
+	return sgsn_delete_pdp_ctx(pdp);
+}
+
+/* Section 9.5.9: Deactivate PDP Context Accept */
+static int gsm48_rx_gsm_deact_pdp_ack(struct sgsn_mm_ctx *mm, struct msgb *msg)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	uint8_t transaction_id = (gh->proto_discr >> 4);
+	struct sgsn_pdp_ctx *pdp;
+
+	DEBUGP(DMM, "-> DEACTIVATE PDP CONTEXT ACK\n");
+
+	pdp = sgsn_pdp_ctx_by_tid(mm, transaction_id);
+	if (!pdp) {
+		LOGP(DMM, LOGL_NOTICE, "Deactivate PDP Context Accept for "
+			"non-existing PDP Context (IMSI=%s, TI=%u)\n",
+			mm->imsi, transaction_id);
+		return 0;
+	}
+
+	return sgsn_delete_pdp_ctx(pdp);
+}
+
+static int gsm48_rx_gsm_status(struct sgsn_mm_ctx *ctx, struct msgb *msg)
+{
+	struct gsm48_hdr *gh = msgb_l3(msg);
+
+	DEBUGP(DMM, "-> GPRS SM STATUS (cause: %s)\n",
+		get_value_string(gsm_cause_names, gh->data[0]));
+
+	return 0;
+}
+
+static void pdpctx_timer_cb(void *_pdp)
+{
+	struct sgsn_pdp_ctx *pdp = _pdp;
+
+	pdp->num_T_exp++;
+
+	switch (pdp->T) {
+	case 3395:	/* waiting for PDP CTX DEACT ACK */
+		if (pdp->num_T_exp >= 4) {
+			LOGP(DMM, LOGL_NOTICE, "T3395 expired >= 5 times\n");
+			pdp->state = PDP_STATE_INACTIVE;
+			sgsn_delete_pdp_ctx(pdp);
+			break;
+		}
+		gsm48_tx_gsm_deact_pdp_req(pdp, GSM_CAUSE_NET_FAIL); 
+		bsc_schedule_timer(&pdp->timer, GSM0408_T3395_SECS, 0);
+		break;
+	default:
+		LOGP(DMM, LOGL_ERROR, "timer expired in unknown mode %u\n",
+			pdp->T);
+	}
+}
+
+
+/* GPRS Session Management */
+static int gsm0408_rcv_gsm(struct sgsn_mm_ctx *mmctx, struct msgb *msg,
+			   struct gprs_llc_llme *llme)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	int rc;
+
+	/* MMCTX can be NULL when called */
+
+	if (!mmctx) {
+		LOGP(DMM, LOGL_NOTICE, "Cannot handle SM for unknown MM CTX\n");
+		gsm48_tx_gmm_status_oldmsg(msg, GMM_CAUSE_IMPL_DETACHED);
+		return gsm48_tx_sm_status_oldmsg(msg, GSM_CAUSE_PROTO_ERR_UNSPEC);
+	}
+
+	switch (gh->msg_type) {
+	case GSM48_MT_GSM_ACT_PDP_REQ:
+		rc = gsm48_rx_gsm_act_pdp_req(mmctx, msg);
+		break;
+	case GSM48_MT_GSM_DEACT_PDP_REQ:
+		rc = gsm48_rx_gsm_deact_pdp_req(mmctx, msg);
+		break;
+	case GSM48_MT_GSM_DEACT_PDP_ACK:
+		rc = gsm48_rx_gsm_deact_pdp_ack(mmctx, msg);
+		break;
+	case GSM48_MT_GSM_STATUS:
+		rc = gsm48_rx_gsm_status(mmctx, msg);
+		break;
+	case GSM48_MT_GSM_REQ_PDP_ACT_REJ:
+	case GSM48_MT_GSM_ACT_AA_PDP_REQ:
+	case GSM48_MT_GSM_DEACT_AA_PDP_REQ:
+		DEBUGP(DMM, "Unimplemented GSM 04.08 GSM msg type 0x%02x\n",
+			gh->msg_type);
+		rc = gsm48_tx_sm_status(mmctx, GSM_CAUSE_MSGT_NOTEXIST_NOTIMPL);
+		break;
+	default:
+		DEBUGP(DMM, "Unknown GSM 04.08 GSM msg type 0x%02x\n",
+			gh->msg_type);
+		rc = gsm48_tx_sm_status(mmctx, GSM_CAUSE_MSGT_NOTEXIST_NOTIMPL);
+		break;
+
+	}
+
+	return rc;
+}
+
+/* Main entry point for incoming 04.08 GPRS messages */
+int gsm0408_gprs_rcvmsg(struct msgb *msg, struct gprs_llc_llme *llme)
+{
+	struct gsm48_hdr *gh = (struct gsm48_hdr *) msgb_gmmh(msg);
+	uint8_t pdisc = gh->proto_discr & 0x0f;
+	struct sgsn_mm_ctx *mmctx;
+	struct gprs_ra_id ra_id;
+	int rc = -EINVAL;
+
+	bssgp_parse_cell_id(&ra_id, msgb_bcid(msg));
+	mmctx = sgsn_mm_ctx_by_tlli(msgb_tlli(msg), &ra_id);
+	if (mmctx) {
+		msgid2mmctx(mmctx, msg);
+		rate_ctr_inc(&mmctx->ctrg->ctr[GMM_CTR_PKTS_SIG_IN]);
+		mmctx->llme = llme;
+	}
+
+	/* MMCTX can be NULL */
+
+	switch (pdisc) {
+	case GSM48_PDISC_MM_GPRS:
+		rc = gsm0408_rcv_gmm(mmctx, msg, llme);
+		break;
+	case GSM48_PDISC_SM_GPRS:
+		rc = gsm0408_rcv_gsm(mmctx, msg, llme);
+		break;
+	default:
+		DEBUGP(DMM, "Unknown GSM 04.08 discriminator 0x%02x\n",
+			pdisc);
+		/* FIXME: return status message */
+		break;
+	}
+
+	return rc;
+}
+
+int gprs_gmm_rx_suspend(struct gprs_ra_id *raid, uint32_t tlli)
+{
+	struct sgsn_mm_ctx *mmctx;
+
+	mmctx = sgsn_mm_ctx_by_tlli(tlli, raid);
+	if (!mmctx) {
+		LOGP(DMM, LOGL_NOTICE, "SUSPEND request for unknown "
+			"TLLI=%08x\n", tlli);
+		return -EINVAL;
+	}
+
+	if (mmctx->mm_state != GMM_REGISTERED_NORMAL) {
+		LOGP(DMM, LOGL_NOTICE, "SUSPEND request while state "
+			"!= REGISTERED (TLLI=%08x)\n", tlli);
+		return -EINVAL;
+	}
+
+	/* Transition from REGISTERED_NORMAL to REGISTERED_SUSPENDED */
+	mmctx->mm_state = GMM_REGISTERED_SUSPENDED;
+	return 0;
+}
+
+int gprs_gmm_rx_resume(struct gprs_ra_id *raid, uint32_t tlli,
+		       uint8_t suspend_ref)
+{
+	struct sgsn_mm_ctx *mmctx;
+
+	/* FIXME: make use of suspend reference? */
+
+	mmctx = sgsn_mm_ctx_by_tlli(tlli, raid);
+	if (!mmctx) {
+		LOGP(DMM, LOGL_NOTICE, "RESUME request for unknown "
+			"TLLI=%08x\n", tlli);
+		return -EINVAL;
+	}
+
+	if (mmctx->mm_state != GMM_REGISTERED_SUSPENDED) {
+		LOGP(DMM, LOGL_NOTICE, "RESUME request while state "
+			"!= SUSPENDED (TLLI=%08x)\n", tlli);
+		/* FIXME: should we not simply ignore it? */
+		return -EINVAL;
+	}
+
+	/* Transition from SUSPENDED to NORMAL */
+	mmctx->mm_state = GMM_REGISTERED_NORMAL;
+	return 0;
+}
diff --git a/src/gprs/gprs_llc.c b/src/gprs/gprs_llc.c
new file mode 100644
index 0000000..7991f4c
--- /dev/null
+++ b/src/gprs/gprs_llc.c
@@ -0,0 +1,852 @@
+/* GPRS LLC protocol implementation as per 3GPP TS 04.64 */
+
+/* (C) 2009-2010 by Harald Welte <laforge@gnumonks.org>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <errno.h>
+#include <stdint.h>
+
+#include <osmocore/msgb.h>
+#include <osmocore/linuxlist.h>
+#include <osmocore/timer.h>
+#include <osmocore/talloc.h>
+
+#include <openbsc/gsm_data.h>
+#include <openbsc/debug.h>
+#include <openbsc/gprs_sgsn.h>
+#include <openbsc/gprs_gmm.h>
+#include <openbsc/gprs_bssgp.h>
+#include <openbsc/gprs_llc.h>
+#include <openbsc/crc24.h>
+
+/* Section 8.9.9 LLC layer parameter default values */
+static const struct gprs_llc_params llc_default_params[] = {
+	[1] = {
+		.t200_201	= 5,
+		.n200		= 3,
+		.n201_u		= 400,
+	},
+	[2] = {
+		.t200_201	= 5,
+		.n200		= 3,
+		.n201_u		= 270,
+	},
+	[3] = {
+		.iov_i_exp	= 27,
+		.t200_201	= 5,
+		.n200		= 3,
+		.n201_u		= 500,
+		.n201_i		= 1503,
+		.mD		= 1520,
+		.mU		= 1520,
+		.kD		= 16,
+		.kU		= 16,
+	},
+	[5] = {
+		.iov_i_exp	= 27,
+		.t200_201	= 10,
+		.n200		= 3,
+		.n201_u		= 500,
+		.n201_i		= 1503,
+		.mD		= 760,
+		.mU		= 760,
+		.kD		= 8,
+		.kU		= 8,
+	},
+	[7] = {
+		.t200_201	= 20,
+		.n200		= 3,
+		.n201_u		= 270,
+	},
+	[8] = {
+		.t200_201	= 20,
+		.n200		= 3,
+		.n201_u		= 270,
+	},
+	[9] = {
+		.iov_i_exp	= 27,
+		.t200_201	= 20,
+		.n200		= 3,
+		.n201_u		= 500,
+		.n201_i		= 1503,
+		.mD		= 380,
+		.mU		= 380,
+		.kD		= 4,
+		.kU		= 4,
+	},
+	[11] = {
+		.iov_i_exp	= 27,
+		.t200_201	= 40,
+		.n200		= 3,
+		.n201_u		= 500,
+		.n201_i		= 1503,
+		.mD		= 190,
+		.mU		= 190,
+		.kD		= 2,
+		.kU		= 2,
+	},
+};
+
+LLIST_HEAD(gprs_llc_llmes);
+void *llc_tall_ctx;
+
+/* If the TLLI is foreign, return its local version */
+static inline uint32_t tlli_foreign2local(uint32_t tlli)
+{
+	uint32_t new_tlli;
+
+	if (gprs_tlli_type(tlli) == TLLI_FOREIGN) {
+		new_tlli = tlli | 0x40000000;
+		DEBUGP(DLLC, "TLLI 0x%08x is foreign, converting to "
+			"local TLLI 0x%08x\n", tlli, new_tlli);
+	} else
+		new_tlli = tlli;
+
+	return new_tlli;
+}
+
+/* lookup LLC Entity based on DLCI (TLLI+SAPI tuple) */
+static struct gprs_llc_lle *lle_by_tlli_sapi(uint32_t tlli, uint8_t sapi)
+{
+	struct gprs_llc_llme *llme;
+
+	tlli = tlli_foreign2local(tlli);
+
+	llist_for_each_entry(llme, &gprs_llc_llmes, list) {
+		if (llme->tlli == tlli || llme->old_tlli == tlli)
+			return &llme->lle[sapi];
+	}
+	return NULL;
+}
+
+static void lle_init(struct gprs_llc_llme *llme, uint8_t sapi)
+{
+	struct gprs_llc_lle *lle = &llme->lle[sapi];
+
+	lle->llme = llme;
+	lle->sapi = sapi;
+	lle->state = GPRS_LLES_UNASSIGNED;
+
+	/* Initialize according to parameters */
+	memcpy(&lle->params, &llc_default_params[sapi], sizeof(lle->params));
+}
+
+static struct gprs_llc_llme *llme_alloc(uint32_t tlli)
+{
+	struct gprs_llc_llme *llme;
+	uint32_t i;
+
+	llme = talloc_zero(llc_tall_ctx, struct gprs_llc_llme);
+	if (!llme)
+		return NULL;
+
+	llme->tlli = tlli;
+	llme->old_tlli = 0xffffffff;
+	llme->state = GPRS_LLMS_UNASSIGNED;
+
+	for (i = 0; i < ARRAY_SIZE(llme->lle); i++)
+		lle_init(llme, i);
+
+	llist_add(&llme->list, &gprs_llc_llmes);
+
+	return llme;
+}
+
+static void llme_free(struct gprs_llc_llme *llme)
+{
+	llist_del(&llme->list);
+	talloc_free(llme);
+}
+
+enum gprs_llc_cmd {
+	GPRS_LLC_NULL,
+	GPRS_LLC_RR,
+	GPRS_LLC_ACK,
+	GPRS_LLC_RNR,
+	GPRS_LLC_SACK,
+	GPRS_LLC_DM,
+	GPRS_LLC_DISC,
+	GPRS_LLC_UA,
+	GPRS_LLC_SABM,
+	GPRS_LLC_FRMR,
+	GPRS_LLC_XID,
+	GPRS_LLC_UI,
+};
+
+static const struct value_string llc_cmd_strs[] = {
+	{ GPRS_LLC_NULL,	"NULL" },
+	{ GPRS_LLC_RR,		"RR" },
+	{ GPRS_LLC_ACK,		"ACK" },
+	{ GPRS_LLC_RNR,		"RNR" },
+	{ GPRS_LLC_SACK,	"SACK" },
+	{ GPRS_LLC_DM,		"DM" },
+	{ GPRS_LLC_DISC,	"DISC" },
+	{ GPRS_LLC_UA,		"UA" },
+	{ GPRS_LLC_SABM,	"SABM" },
+	{ GPRS_LLC_FRMR,	"FRMR" },
+	{ GPRS_LLC_XID,		"XID" },
+	{ GPRS_LLC_UI,		"UI" },
+	{ 0, NULL }
+};
+
+struct gprs_llc_hdr_parsed {
+	uint8_t sapi;
+	uint8_t is_cmd:1,
+		 ack_req:1,
+		 is_encrypted:1;
+	uint32_t seq_rx;
+	uint32_t seq_tx;
+	uint32_t fcs;
+	uint32_t fcs_calc;
+	uint8_t *data;
+	uint16_t data_len;
+	uint16_t crc_length;
+	enum gprs_llc_cmd cmd;
+};
+
+#define LLC_ALLOC_SIZE 16384
+#define UI_HDR_LEN	3
+#define N202		4
+#define CRC24_LENGTH	3
+
+static int gprs_llc_fcs(uint8_t *data, unsigned int len)
+{
+	uint32_t fcs_calc;
+
+	fcs_calc = crc24_calc(INIT_CRC24, data, len);
+	fcs_calc = ~fcs_calc;
+	fcs_calc &= 0xffffff;
+
+	return fcs_calc;
+}
+
+static void t200_expired(void *data)
+{
+	struct gprs_llc_lle *lle = data;
+
+	/* 8.5.1.3: Expiry of T200 */
+
+	if (lle->retrans_ctr >= lle->params.n200) {
+		/* FIXME: LLGM-STATUS-IND, LL-RELEASE-IND/CNF */
+		lle->state = GPRS_LLES_ASSIGNED_ADM;
+	}
+
+	switch (lle->state) {
+	case GPRS_LLES_LOCAL_EST:
+		/* FIXME: retransmit SABM */
+		/* FIXME: re-start T200 */
+		lle->retrans_ctr++;
+		break;
+	case GPRS_LLES_LOCAL_REL:
+		/* FIXME: retransmit DISC */
+		/* FIXME: re-start T200 */
+		lle->retrans_ctr++;
+		break;
+	}
+
+}
+
+static void t201_expired(void *data)
+{
+	struct gprs_llc_lle *lle = data;
+
+	if (lle->retrans_ctr < lle->params.n200) {
+		/* FIXME: transmit apropriate supervisory frame (8.6.4.1) */
+		/* FIXME: set timer T201 */
+		lle->retrans_ctr++;
+	}
+}
+
+int gprs_llc_tx_u(struct msgb *msg, uint8_t sapi, int command,
+		  enum gprs_llc_u_cmd u_cmd, int pf_bit)
+{
+	uint8_t *fcs, *llch;
+	uint8_t addr, ctrl;
+	uint32_t fcs_calc;
+
+	/* Identifiers from UP: (TLLI, SAPI) + (BVCI, NSEI) */
+
+	/* Address Field */
+	addr = sapi & 0xf;
+	if (command)
+		addr |= 0x40;
+
+	/* 6.3 Figure 8 */
+	ctrl = 0xe0 | u_cmd;
+	if (pf_bit)
+		ctrl |= 0x10;
+
+	/* prepend LLC UI header */
+	llch = msgb_push(msg, 2);
+	llch[0] = addr;
+	llch[1] = ctrl;
+
+	/* append FCS to end of frame */
+	fcs = msgb_put(msg, 3);
+	fcs_calc = gprs_llc_fcs(llch, fcs - llch);
+	fcs[0] = fcs_calc & 0xff;
+	fcs[1] = (fcs_calc >> 8) & 0xff;
+	fcs[2] = (fcs_calc >> 16) & 0xff;
+
+	/* Identifiers passed down: (BVCI, NSEI) */
+
+	/* Send BSSGP-DL-UNITDATA.req */
+	return gprs_bssgp_tx_dl_ud(msg, NULL);
+}
+
+/* Send XID response to LLE */
+static int gprs_llc_tx_xid(struct gprs_llc_lle *lle, struct msgb *msg)
+{
+	/* copy identifiers from LLE to ensure lower layers can route */
+	msgb_tlli(msg) = lle->llme->tlli;
+	msgb_bvci(msg) = lle->llme->bvci;
+	msgb_nsei(msg) = lle->llme->nsei;
+
+	return gprs_llc_tx_u(msg, lle->sapi, 0, GPRS_LLC_U_XID, 1);
+}
+
+/* Transmit a UI frame over the given SAPI */
+int gprs_llc_tx_ui(struct msgb *msg, uint8_t sapi, int command,
+		   void *mmctx)
+{
+	struct gprs_llc_lle *lle;
+	uint8_t *fcs, *llch;
+	uint8_t addr, ctrl[2];
+	uint32_t fcs_calc;
+	uint16_t nu = 0;
+	uint32_t oc;
+
+	/* Identifiers from UP: (TLLI, SAPI) + (BVCI, NSEI) */
+
+	/* look-up or create the LL Entity for this (TLLI, SAPI) tuple */
+	lle = lle_by_tlli_sapi(msgb_tlli(msg), sapi);
+	if (!lle) {
+		struct gprs_llc_llme *llme;
+		LOGP(DLLC, LOGL_ERROR, "LLC TX: unknown TLLI 0x%08x, "
+			"creating LLME on the fly\n", msgb_tlli(msg));
+		llme = llme_alloc(msgb_tlli(msg));
+		lle = &llme->lle[sapi];
+	}
+
+	if (msg->len > lle->params.n201_u) {
+		LOGP(DLLC, LOGL_ERROR, "Cannot Tx %u bytes (N201-U=%u)\n",
+			msg->len, lle->params.n201_u);
+		return -EFBIG;
+	}
+
+	/* Update LLE's (BVCI, NSEI) tuple */
+	lle->llme->bvci = msgb_bvci(msg);
+	lle->llme->nsei = msgb_nsei(msg);
+
+	/* Obtain current values for N(u) and OC */
+	nu = lle->vu_send;
+	oc = lle->oc_ui_send;
+	/* Increment V(U) */
+	lle->vu_send = (lle->vu_send + 1) % 512;
+	/* Increment Overflow Counter, if needed */
+	if ((lle->vu_send + 1) / 512)
+		lle->oc_ui_send += 512;
+
+	/* Address Field */
+	addr = sapi & 0xf;
+	if (command)
+		addr |= 0x40;
+
+	/* Control Field */
+	ctrl[0] = 0xc0;
+	ctrl[0] |= nu >> 6;
+	ctrl[1] = (nu << 2) & 0xfc;
+	ctrl[1] |= 0x01; /* Protected Mode */
+
+	/* prepend LLC UI header */
+	llch = msgb_push(msg, 3);
+	llch[0] = addr;
+	llch[1] = ctrl[0];
+	llch[2] = ctrl[1];
+
+	/* append FCS to end of frame */
+	fcs = msgb_put(msg, 3);
+	fcs_calc = gprs_llc_fcs(llch, fcs - llch);
+	fcs[0] = fcs_calc & 0xff;
+	fcs[1] = (fcs_calc >> 8) & 0xff;
+	fcs[2] = (fcs_calc >> 16) & 0xff;
+
+	/* encrypt information field + FCS, if needed! */
+	if (lle->llme->algo != GPRS_ALGO_GEA0) {
+		uint32_t iov_ui = 0; /* FIXME: randomly select for TLLI */
+		uint16_t crypt_len = (fcs + 3) - (llch + 3);
+		uint8_t cipher_out[GSM0464_CIPH_MAX_BLOCK];
+		uint32_t iv;
+		int rc, i;
+		uint64_t kc = *(uint64_t *)&lle->llme->kc;
+
+		/* Compute the 'Input' Paraemeter */
+		iv = gprs_cipher_gen_input_ui(iov_ui, sapi, nu, oc);
+
+		/* Compute the keystream that we need to XOR with the data */
+		rc = gprs_cipher_run(cipher_out, crypt_len, lle->llme->algo,
+				     kc, iv, GPRS_CIPH_SGSN2MS);
+		if (rc < 0) {
+			LOGP(DLLC, LOGL_ERROR, "Error crypting UI frame: %d\n", rc);
+			return rc;
+		}
+
+		/* XOR the cipher output with the information field + FCS */
+		for (i = 0; i < crypt_len; i++)
+			*(llch + 3 + i) ^= cipher_out[i];
+
+		/* Mark frame as encrypted */
+		ctrl[1] |= 0x02;
+	}
+
+	/* Identifiers passed down: (BVCI, NSEI) */
+
+	/* Send BSSGP-DL-UNITDATA.req */
+	return gprs_bssgp_tx_dl_ud(msg, mmctx);
+}
+
+static void gprs_llc_hdr_dump(struct gprs_llc_hdr_parsed *gph)
+{
+	DEBUGP(DLLC, "LLC SAPI=%u %c %c FCS=0x%06x",
+		gph->sapi, gph->is_cmd ? 'C' : 'R', gph->ack_req ? 'A' : ' ',
+		gph->fcs);
+
+	if (gph->cmd)
+		DEBUGPC(DLLC, "CMD=%s ", get_value_string(llc_cmd_strs, gph->cmd));
+
+	if (gph->data)
+		DEBUGPC(DLLC, "DATA ");
+
+	DEBUGPC(DLLC, "\n");
+}
+static int gprs_llc_hdr_rx(struct gprs_llc_hdr_parsed *gph,
+			   struct gprs_llc_lle *lle)
+{
+	switch (gph->cmd) {
+	case GPRS_LLC_SABM: /* Section 6.4.1.1 */
+		lle->v_sent = lle->v_ack = lle->v_recv = 0;
+		if (lle->state == GPRS_LLES_ASSIGNED_ADM) {
+			/* start re-establishment (8.7.1) */
+		}
+		lle->state = GPRS_LLES_REMOTE_EST;
+		/* FIXME: Send UA */
+		lle->state = GPRS_LLES_ABM;
+		/* FIXME: process data */
+		break;
+	case GPRS_LLC_DISC: /* Section 6.4.1.2 */
+		/* FIXME: Send UA */
+		/* terminate ABM */
+		lle->state = GPRS_LLES_ASSIGNED_ADM;
+		break;
+	case GPRS_LLC_UA: /* Section 6.4.1.3 */
+		if (lle->state == GPRS_LLES_LOCAL_EST)
+			lle->state = GPRS_LLES_ABM;
+		break;
+	case GPRS_LLC_DM: /* Section 6.4.1.4: ABM cannot be performed */
+		if (lle->state == GPRS_LLES_LOCAL_EST)
+			lle->state = GPRS_LLES_ASSIGNED_ADM;
+		break;
+	case GPRS_LLC_FRMR: /* Section 6.4.1.5 */
+		break;
+	case GPRS_LLC_XID: /* Section 6.4.1.6 */
+		/* FIXME: implement XID negotiation using SNDCP */
+		{
+			struct msgb *resp;
+			uint8_t *xid;
+			resp = msgb_alloc_headroom(4096, 1024, "LLC_XID");
+			xid = msgb_put(resp, gph->data_len);
+			memcpy(xid, gph->data, gph->data_len);
+			gprs_llc_tx_xid(lle, resp);
+		}
+		break;
+	case GPRS_LLC_UI:
+		if (gph->seq_tx < lle->vu_recv) {
+			LOGP(DLLC, LOGL_NOTICE, "TLLI=%08x dropping UI, vurecv %u <= %u\n",
+				lle->llme ? lle->llme->tlli : -1,
+				gph->seq_tx, lle->vu_recv);
+			return -EIO;
+		}
+		/* Increment the sequence number that we expect in the next frame */
+		lle->vu_recv = (gph->seq_tx + 1) % 512;
+		/* Increment Overflow Counter */
+		if ((gph->seq_tx + 1) / 512)
+			lle->oc_ui_recv += 512;
+		break;
+	}
+
+	return 0;
+}
+
+/* parse a GPRS LLC header, also check for invalid frames */
+static int gprs_llc_hdr_parse(struct gprs_llc_hdr_parsed *ghp,
+			      uint8_t *llc_hdr, int len)
+{
+	uint8_t *ctrl = llc_hdr+1;
+	int is_sack = 0;
+
+	if (len <= CRC24_LENGTH)
+		return -EIO;
+
+	ghp->crc_length = len - CRC24_LENGTH;
+
+	ghp->ack_req = 0;
+
+	/* Section 5.5: FCS */
+	ghp->fcs = *(llc_hdr + len - 3);
+	ghp->fcs |= *(llc_hdr + len - 2) << 8;
+	ghp->fcs |= *(llc_hdr + len - 1) << 16;
+
+	/* Section 6.2.1: invalid PD field */
+	if (llc_hdr[0] & 0x80)
+		return -EIO;
+
+	/* This only works for the MS->SGSN direction */
+	if (llc_hdr[0] & 0x40)
+		ghp->is_cmd = 0;
+	else
+		ghp->is_cmd = 1;
+
+	ghp->sapi = llc_hdr[0] & 0xf;
+
+	/* Section 6.2.3: check for reserved SAPI */
+	switch (ghp->sapi) {
+	case 0:
+	case 4:
+	case 6:
+	case 0xa:
+	case 0xc:
+	case 0xd:
+	case 0xf:
+		return -EINVAL;
+	}
+
+	if ((ctrl[0] & 0x80) == 0) {
+		/* I (Information transfer + Supervisory) format */
+		uint8_t k;
+
+		ghp->data = ctrl + 3;
+
+		if (ctrl[0] & 0x40)
+			ghp->ack_req = 1;
+
+		ghp->seq_tx  = (ctrl[0] & 0x1f) << 4;
+		ghp->seq_tx |= (ctrl[1] >> 4);
+
+		ghp->seq_rx  = (ctrl[1] & 0x7) << 6;
+		ghp->seq_rx |= (ctrl[2] >> 2);
+
+		switch (ctrl[2] & 0x03) {
+		case 0:
+			ghp->cmd = GPRS_LLC_RR;
+			break;
+		case 1:
+			ghp->cmd = GPRS_LLC_ACK;
+			break;
+		case 2:
+			ghp->cmd = GPRS_LLC_RNR;
+			break;
+		case 3:
+			ghp->cmd = GPRS_LLC_SACK;
+			k = ctrl[3] & 0x1f;
+			ghp->data += 1 + k;
+			break;
+		}
+		ghp->data_len = (llc_hdr + len - 3) - ghp->data;
+	} else if ((ctrl[0] & 0xc0) == 0x80) {
+		/* S (Supervisory) format */
+		ghp->data = NULL;
+		ghp->data_len = 0;
+
+		if (ctrl[0] & 0x20)
+			ghp->ack_req = 1;
+		ghp->seq_rx  = (ctrl[0] & 0x7) << 6;
+		ghp->seq_rx |= (ctrl[1] >> 2);
+
+		switch (ctrl[1] & 0x03) {
+		case 0:
+			ghp->cmd = GPRS_LLC_RR;
+			break;
+		case 1:
+			ghp->cmd = GPRS_LLC_ACK;
+			break;
+		case 2:
+			ghp->cmd = GPRS_LLC_RNR;
+			break;
+		case 3:
+			ghp->cmd = GPRS_LLC_SACK;
+			break;
+		}
+	} else if ((ctrl[0] & 0xe0) == 0xc0) {
+		/* UI (Unconfirmed Inforamtion) format */
+		ghp->cmd = GPRS_LLC_UI;
+		ghp->data = ctrl + 2;
+		ghp->data_len = (llc_hdr + len - 3) - ghp->data;
+
+		ghp->seq_tx  = (ctrl[0] & 0x7) << 6;
+		ghp->seq_tx |= (ctrl[1] >> 2);
+		if (ctrl[1] & 0x02) {
+			ghp->is_encrypted = 1;
+			/* FIXME: encryption */
+		}
+		if (ctrl[1] & 0x01) {
+			/* FCS over hdr + all inf fields */
+		} else {
+			/* FCS over hdr + N202 octets (4) */
+			if (ghp->crc_length > UI_HDR_LEN + N202)
+				ghp->crc_length = UI_HDR_LEN + N202;
+		}
+	} else {
+		/* U (Unnumbered) format: 1 1 1 P/F M4 M3 M2 M1 */
+		ghp->data = NULL;
+		ghp->data_len = 0;
+
+		switch (ctrl[0] & 0xf) {
+		case GPRS_LLC_U_NULL_CMD:
+			ghp->cmd = GPRS_LLC_NULL;
+			break;
+		case GPRS_LLC_U_DM_RESP:
+			ghp->cmd = GPRS_LLC_DM;
+			break;
+		case GPRS_LLC_U_DISC_CMD:
+			ghp->cmd = GPRS_LLC_DISC;
+			break;
+		case GPRS_LLC_U_UA_RESP:
+			ghp->cmd = GPRS_LLC_UA;
+			break;
+		case GPRS_LLC_U_SABM_CMD:
+			ghp->cmd = GPRS_LLC_SABM;
+			break;
+		case GPRS_LLC_U_FRMR_RESP:
+			ghp->cmd = GPRS_LLC_FRMR;
+			break;
+		case GPRS_LLC_U_XID:
+			ghp->cmd = GPRS_LLC_XID;
+			ghp->data = ctrl + 1;
+			ghp->data_len = (llc_hdr + len - 3) - ghp->data;
+			break;
+		default:
+			return -EIO;
+		}
+	}
+
+	/* FIXME: parse sack frame */
+	if (ghp->cmd == GPRS_LLC_SACK) {
+		LOGP(DLLC, LOGL_NOTICE, "Unsupported SACK frame\n");
+		return -EIO;
+	}
+
+	return 0;
+}
+
+/* receive an incoming LLC PDU (BSSGP-UL-UNITDATA-IND, 7.2.4.2) */
+int gprs_llc_rcvmsg(struct msgb *msg, struct tlv_parsed *tv)
+{
+	struct bssgp_ud_hdr *udh = (struct bssgp_ud_hdr *) msgb_bssgph(msg);
+	struct gprs_llc_hdr *lh = msgb_llch(msg);
+	struct gprs_llc_hdr_parsed llhp;
+	struct gprs_llc_lle *lle;
+	int rc = 0;
+
+	/* Identifiers from DOWN: NSEI, BVCI, TLLI */
+
+	memset(&llhp, 0, sizeof(llhp));
+	rc = gprs_llc_hdr_parse(&llhp, (uint8_t *) lh, TLVP_LEN(tv, BSSGP_IE_LLC_PDU));
+	gprs_llc_hdr_dump(&llhp);
+	if (rc < 0) {
+		LOGP(DLLC, LOGL_NOTICE, "Error during LLC header parsing\n");
+		return rc;
+	}
+
+	switch (gprs_tlli_type(msgb_tlli(msg))) {
+	case TLLI_LOCAL:
+	case TLLI_FOREIGN:
+	case TLLI_RANDOM:
+	case TLLI_AUXILIARY:
+		break;
+	default:
+		LOGP(DLLC, LOGL_ERROR,
+			"Discarding frame with strange TLLI type\n");
+		break;
+	}
+
+	/* find the LLC Entity for this TLLI+SAPI tuple */
+	lle = lle_by_tlli_sapi(msgb_tlli(msg), llhp.sapi);
+
+	/* 7.2.1.1 LLC belonging to unassigned TLLI+SAPI shall be discarded,
+	 * except UID and XID frames with SAPI=1 */
+	if (!lle) {
+		if (llhp.sapi == GPRS_SAPI_GMM &&
+		    (llhp.cmd == GPRS_LLC_XID || llhp.cmd == GPRS_LLC_UI)) {
+			struct gprs_llc_llme *llme;
+			/* FIXME: don't use the TLLI but the 0xFFFF unassigned? */
+			llme = llme_alloc(msgb_tlli(msg));
+			LOGP(DLLC, LOGL_DEBUG, "LLC RX: unknown TLLI 0x08x, "
+				"creating LLME on the fly\n", msgb_tlli(msg));
+			lle = &llme->lle[llhp.sapi];
+		} else {
+			LOGP(DLLC, LOGL_NOTICE,
+				"unknown TLLI/SAPI: Silently dropping\n");
+			return 0;
+		}
+	}
+
+	/* decrypt information field + FCS, if needed! */
+	if (llhp.is_encrypted) {
+		uint32_t iov_ui = 0; /* FIXME: randomly select for TLLI */
+		uint16_t crypt_len = llhp.data_len + 3;
+		uint8_t cipher_out[GSM0464_CIPH_MAX_BLOCK];
+		uint32_t iv;
+		uint64_t kc = *(uint64_t *)&lle->llme->kc;
+		int rc, i;
+
+		if (lle->llme->algo == GPRS_ALGO_GEA0) {
+			LOGP(DLLC, LOGL_NOTICE, "encrypted frame for LLC that "
+				"has no KC/Algo! Dropping.\n");
+			return 0;
+		}
+
+		iv = gprs_cipher_gen_input_ui(iov_ui, lle->sapi, llhp.seq_tx,
+						lle->oc_ui_recv);
+		rc = gprs_cipher_run(cipher_out, crypt_len, lle->llme->algo,
+				     kc, iv, GPRS_CIPH_MS2SGSN);
+		if (rc < 0) {
+			LOGP(DLLC, LOGL_ERROR, "Error decrypting frame: %d\n",
+			     rc);
+			return rc;
+		}
+
+		/* XOR the cipher output with the information field + FCS */
+		for (i = 0; i < crypt_len; i++)
+			*(llhp.data + i) ^= cipher_out[i];
+	} else {
+		if (lle->llme->algo != GPRS_ALGO_GEA0) {
+			LOGP(DLLC, LOGL_NOTICE, "unencrypted frame for LLC "
+				"that is supposed to be encrypted. Dropping.\n");
+			return 0;
+		}
+	}
+
+	/* We have to do the FCS check _after_ decryption */
+	llhp.fcs_calc = gprs_llc_fcs((uint8_t *)lh, llhp.crc_length);
+	if (llhp.fcs != llhp.fcs_calc) {
+		LOGP(DLLC, LOGL_INFO, "Dropping frame with invalid FCS\n");
+		return -EIO;
+	}
+
+	/* Update LLE's (BVCI, NSEI) tuple */
+	lle->llme->bvci = msgb_bvci(msg);
+	lle->llme->nsei = msgb_nsei(msg);
+
+	/* Receive and Process the actual LLC frame */
+	rc = gprs_llc_hdr_rx(&llhp, lle);
+	if (rc < 0)
+		return rc;
+
+	/* llhp.data is only set when we need to send LL_[UNIT]DATA_IND up */
+	if (llhp.data) {
+		msgb_gmmh(msg) = llhp.data;
+		switch (llhp.sapi) {
+		case GPRS_SAPI_GMM:
+			/* send LL_UNITDATA_IND to GMM */
+			rc = gsm0408_gprs_rcvmsg(msg, lle->llme);
+			break;
+		case GPRS_SAPI_SNDCP3:
+		case GPRS_SAPI_SNDCP5:
+		case GPRS_SAPI_SNDCP9:
+		case GPRS_SAPI_SNDCP11:
+			/* send LL_DATA_IND/LL_UNITDATA_IND to SNDCP */
+			rc = sndcp_llunitdata_ind(msg, lle, llhp.data, llhp.data_len);
+			break;
+		case GPRS_SAPI_SMS:
+			/* FIXME */
+		case GPRS_SAPI_TOM2:
+		case GPRS_SAPI_TOM8:
+			/* FIXME: send LL_DATA_IND/LL_UNITDATA_IND to TOM */
+		default:
+			LOGP(DLLC, LOGL_NOTICE, "Unsupported SAPI %u\n", llhp.sapi);
+			rc = -EINVAL;
+			break;
+		}
+	}
+
+	return rc;
+}
+
+/* 04.64 Chapter 7.2.1.1 LLGMM-ASSIGN */
+int gprs_llgmm_assign(struct gprs_llc_llme *llme,
+		      uint32_t old_tlli, uint32_t new_tlli,
+		      enum gprs_ciph_algo alg, const uint8_t *kc)
+{
+	unsigned int i;
+
+	/* Update the crypto parameters */
+	llme->algo = alg;
+	if (alg != GPRS_ALGO_GEA0)
+		memcpy(llme->kc, kc, sizeof(llme->kc));
+
+	if (old_tlli == 0xffffffff && new_tlli != 0xffffffff) {
+		/* TLLI Assignment 8.3.1 */
+		/* New TLLI shall be assigned and used when (re)transmitting LLC frames */
+		/* If old TLLI != 0xffffffff was assigned to LLME, then TLLI
+		 * old is unassigned.  Only TLLI new shall be accepted when
+		 * received from peer. */
+		if (llme->old_tlli != 0xffffffff) {
+			llme->old_tlli = 0xffffffff;
+			llme->tlli = new_tlli;
+		} else {
+			/* If TLLI old == 0xffffffff was assigned to LLME, then this is
+			 * TLLI assignmemt according to 8.3.1 */
+			llme->old_tlli = 0xffffffff;
+			llme->tlli = new_tlli;
+			llme->state = GPRS_LLMS_ASSIGNED;
+			/* 8.5.3.1 For all LLE's */
+			for (i = 0; i < ARRAY_SIZE(llme->lle); i++) {
+				struct gprs_llc_lle *l = &llme->lle[i];
+				l->vu_send = l->vu_recv = 0;
+				l->retrans_ctr = 0;
+				l->state = GPRS_LLES_ASSIGNED_ADM;
+				/* FIXME Set parameters according to table 9 */
+			}
+		}
+	} else if (old_tlli != 0xffffffff && new_tlli != 0xffffffff) {
+		/* TLLI Change 8.3.2 */
+		/* Both TLLI Old and TLLI New are assigned; use New when
+		 * (re)transmitting.  Accept toth Old and New on Rx */
+		llme->old_tlli = llme->tlli;
+		llme->tlli = new_tlli;
+		llme->state = GPRS_LLMS_ASSIGNED;
+	} else if (old_tlli != 0xffffffff && new_tlli == 0xffffffff) {
+		/* TLLI Unassignment 8.3.3) */
+		llme->tlli = llme->old_tlli = 0;
+		llme->state = GPRS_LLMS_UNASSIGNED;
+		for (i = 0; i < ARRAY_SIZE(llme->lle); i++) {
+			struct gprs_llc_lle *l = &llme->lle[i];
+			l->state = GPRS_LLES_UNASSIGNED;
+		}
+		llme_free(llme);
+	} else
+		return -EINVAL;
+
+	return 0;
+}
+
+int gprs_llc_init(const char *cipher_plugin_path)
+{
+	return gprs_cipher_load(cipher_plugin_path);
+}
diff --git a/src/gprs/gprs_llc_vty.c b/src/gprs/gprs_llc_vty.c
new file mode 100644
index 0000000..d4f743b
--- /dev/null
+++ b/src/gprs/gprs_llc_vty.c
@@ -0,0 +1,108 @@
+/* VTY interface for our GPRS LLC implementation */
+
+/* (C) 2010 by Harald Welte <laforge@gnumonks.org>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdint.h>
+
+#include <arpa/inet.h>
+
+#include <openbsc/gsm_data.h>
+#include <osmocore/msgb.h>
+#include <osmocore/tlv.h>
+#include <osmocore/talloc.h>
+#include <osmocore/select.h>
+#include <osmocore/rate_ctr.h>
+#include <openbsc/debug.h>
+#include <openbsc/signal.h>
+#include <openbsc/gprs_llc.h>
+
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+
+struct value_string gprs_llc_state_strs[] = {
+	{ GPRS_LLES_UNASSIGNED, 	"TLLI Unassigned" },
+	{ GPRS_LLES_ASSIGNED_ADM,	"TLLI Assigned" },
+	{ GPRS_LLES_LOCAL_EST,		"Local Establishment" },
+	{ GPRS_LLES_REMOTE_EST,		"Remote Establishment" },
+	{ GPRS_LLES_ABM,		"Asynchronous Balanced Mode" },
+	{ GPRS_LLES_LOCAL_REL,		"Local Release" },
+	{ GPRS_LLES_TIMER_REC,		"Timer Recovery" },
+};
+
+static void vty_dump_lle(struct vty *vty, struct gprs_llc_lle *lle)
+{
+	struct gprs_llc_params *par = &lle->params;
+	vty_out(vty, " SAPI %2u State %s VUsend=%u, VUrecv=%u", lle->sapi, 
+		get_value_string(gprs_llc_state_strs, lle->state),
+		lle->vu_send, lle->vu_recv);
+	vty_out(vty, " Vsent=%u Vack=%u Vrecv=%u, RetransCtr=%u%s",
+		lle->v_sent, lle->v_ack, lle->v_recv,
+		lle->retrans_ctr, VTY_NEWLINE);
+	vty_out(vty, "  T200=%u, N200=%u, N201-U=%u, N201-I=%u, mD=%u, "
+		"mU=%u, kD=%u, kU=%u%s", par->t200_201, par->n200,
+		par->n201_u, par->n201_i, par->mD, par->mU, par->kD,
+		par->kU, VTY_NEWLINE);
+}
+
+static uint8_t valid_sapis[] = { 1, 2, 3, 5, 7, 8, 9, 11 };
+
+static void vty_dump_llme(struct vty *vty, struct gprs_llc_llme *llme)
+{
+	unsigned int i;
+
+	vty_out(vty, "TLLI %08x (Old TLLI %08x) BVCI=%u NSEI=%u: State %s%s",
+		llme->tlli, llme->old_tlli, llme->bvci, llme->nsei,
+		get_value_string(gprs_llc_state_strs, llme->state), VTY_NEWLINE);
+
+	for (i = 0; i < ARRAY_SIZE(valid_sapis); i++) {
+		struct gprs_llc_lle *lle;
+		uint8_t sapi = valid_sapis[i];
+
+		if (sapi >= ARRAY_SIZE(llme->lle))
+			continue;
+
+		lle = &llme->lle[sapi];
+		vty_dump_lle(vty, lle);
+	}
+}
+
+
+DEFUN(show_llc, show_llc_cmd,
+	"show llc",
+	SHOW_STR "Display information about the LLC protocol")
+{
+	struct gprs_llc_llme *llme;
+
+	vty_out(vty, "State of LLC Entities%s", VTY_NEWLINE);
+	llist_for_each_entry(llme, &gprs_llc_llmes, list) {
+		vty_dump_llme(vty, llme);
+	}
+	return CMD_SUCCESS;
+}
+
+int gprs_llc_vty_init(void)
+{
+	install_element_ve(&show_llc_cmd);
+
+	return 0;
+}
diff --git a/src/gprs/gprs_sgsn.c b/src/gprs/gprs_sgsn.c
new file mode 100644
index 0000000..4436554
--- /dev/null
+++ b/src/gprs/gprs_sgsn.c
@@ -0,0 +1,383 @@
+/* GPRS SGSN functionality */
+
+/* (C) 2009 by Harald Welte <laforge@gnumonks.org>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdint.h>
+
+#include <osmocore/linuxlist.h>
+#include <osmocore/talloc.h>
+#include <osmocore/timer.h>
+#include <osmocore/rate_ctr.h>
+#include <openbsc/gsm_subscriber.h>
+#include <openbsc/debug.h>
+#include <openbsc/gprs_sgsn.h>
+#include <openbsc/gprs_ns.h>
+#include <openbsc/gprs_bssgp.h>
+#include <openbsc/sgsn.h>
+#include <openbsc/gsm_04_08_gprs.h>
+#include <openbsc/gprs_gmm.h>
+
+extern struct sgsn_instance *sgsn;
+
+LLIST_HEAD(sgsn_mm_ctxts);
+LLIST_HEAD(sgsn_ggsn_ctxts);
+LLIST_HEAD(sgsn_apn_ctxts);
+LLIST_HEAD(sgsn_pdp_ctxts);
+
+static const struct rate_ctr_desc mmctx_ctr_description[] = {
+	{ "sign.packets.in",	"Signalling Messages ( In)" },
+	{ "sign.packets.out",	"Signalling Messages (Out)" },
+	{ "udata.packets.in",	"User Data  Messages ( In)" },
+	{ "udata.packets.out",	"User Data  Messages (Out)" },
+	{ "udata.bytes.in",	"User Data  Bytes    ( In)" },
+	{ "udata.bytes.out",	"User Data  Bytes    (Out)" },
+	{ "pdp_ctx_act",	"PDP Context Activations  " },
+	{ "suspend",		"SUSPEND Count            " },
+	{ "paging.ps",		"Paging Packet Switched   " },
+	{ "paging.cs",		"Paging Circuit Switched  " },
+	{ "ra_update",		"Routing Area Update      " },
+};
+
+static const struct rate_ctr_group_desc mmctx_ctrg_desc = {
+	.group_name_prefix = "sgsn.mmctx",
+	.group_description = "SGSN MM Context Statistics",
+	.num_ctr = ARRAY_SIZE(mmctx_ctr_description),
+	.ctr_desc = mmctx_ctr_description,
+};
+
+static const struct rate_ctr_desc pdpctx_ctr_description[] = {
+	{ "udata.packets.in",	"User Data  Messages ( In)" },
+	{ "udata.packets.out",	"User Data  Messages (Out)" },
+	{ "udata.bytes.in",	"User Data  Bytes    ( In)" },
+	{ "udata.bytes.out",	"User Data  Bytes    (Out)" },
+};
+
+static const struct rate_ctr_group_desc pdpctx_ctrg_desc = {
+	.group_name_prefix = "sgsn.pdpctx",
+	.group_description = "SGSN PDP Context Statistics",
+	.num_ctr = ARRAY_SIZE(pdpctx_ctr_description),
+	.ctr_desc = pdpctx_ctr_description,
+};
+
+static int ra_id_equals(const struct gprs_ra_id *id1,
+			const struct gprs_ra_id *id2)
+{
+	return (id1->mcc == id2->mcc && id1->mnc == id2->mnc &&
+		id1->lac == id2->lac && id1->rac == id2->rac);
+}
+
+/* See 03.02 Chapter 2.6 */
+static inline uint32_t tlli_foreign(uint32_t tlli)
+{
+	return ((tlli | 0x80000000) & ~0x40000000);	
+}
+
+/* look-up a SGSN MM context based on TLLI + RAI */
+struct sgsn_mm_ctx *sgsn_mm_ctx_by_tlli(uint32_t tlli,
+					const struct gprs_ra_id *raid)
+{
+	struct sgsn_mm_ctx *ctx;
+	int tlli_type;
+
+	llist_for_each_entry(ctx, &sgsn_mm_ctxts, list) {
+		if (tlli == ctx->tlli &&
+		    ra_id_equals(raid, &ctx->ra))
+			return ctx;
+	}
+
+	tlli_type = gprs_tlli_type(tlli);
+	switch (tlli_type) {
+	case TLLI_LOCAL:
+		llist_for_each_entry(ctx, &sgsn_mm_ctxts, list) {
+			if ((ctx->p_tmsi | 0xC0000000) == tlli ||
+			     (ctx->p_tmsi_old && (ctx->p_tmsi_old | 0xC0000000) == tlli)) {
+				ctx->tlli = tlli;
+				return ctx;
+			}
+		}
+		break;
+	case TLLI_FOREIGN:
+		llist_for_each_entry(ctx, &sgsn_mm_ctxts, list) {
+			if (tlli == tlli_foreign(ctx->tlli) &&
+			    ra_id_equals(raid, &ctx->ra))
+				return ctx;
+		}
+		break;
+	default:
+		break;
+	}
+
+	return NULL;
+}
+
+struct sgsn_mm_ctx *sgsn_mm_ctx_by_ptmsi(uint32_t p_tmsi)
+{
+	struct sgsn_mm_ctx *ctx;
+
+	llist_for_each_entry(ctx, &sgsn_mm_ctxts, list) {
+		if (p_tmsi == ctx->p_tmsi ||
+		    (ctx->p_tmsi_old && ctx->p_tmsi_old == p_tmsi))
+			return ctx;
+	}
+	return NULL;
+}
+
+struct sgsn_mm_ctx *sgsn_mm_ctx_by_imsi(const char *imsi)
+{
+	struct sgsn_mm_ctx *ctx;
+
+	llist_for_each_entry(ctx, &sgsn_mm_ctxts, list) {
+		if (!strcmp(imsi, ctx->imsi))
+			return ctx;
+	}
+	return NULL;
+
+}
+
+/* Allocate a new SGSN MM context */
+struct sgsn_mm_ctx *sgsn_mm_ctx_alloc(uint32_t tlli,
+					const struct gprs_ra_id *raid)
+{
+	struct sgsn_mm_ctx *ctx;
+
+	ctx = talloc_zero(tall_bsc_ctx, struct sgsn_mm_ctx);
+	if (!ctx)
+		return NULL;
+
+	memcpy(&ctx->ra, raid, sizeof(ctx->ra));
+	ctx->tlli = tlli;
+	ctx->mm_state = GMM_DEREGISTERED;
+	ctx->ctrg = rate_ctr_group_alloc(ctx, &mmctx_ctrg_desc, tlli);
+	INIT_LLIST_HEAD(&ctx->pdp_list);
+
+	llist_add(&ctx->list, &sgsn_mm_ctxts);
+
+	return ctx;
+}
+
+void sgsn_mm_ctx_free(struct sgsn_mm_ctx *mm)
+{
+	struct sgsn_pdp_ctx *pdp, *pdp2;
+
+	/* Unlink from global list of MM contexts */
+	llist_del(&mm->list);
+
+	/* Free all PDP contexts */
+	llist_for_each_entry_safe(pdp, pdp2, &mm->pdp_list, list)
+		sgsn_pdp_ctx_free(pdp);
+	
+	rate_ctr_group_free(mm->ctrg);
+
+	talloc_free(mm);
+}
+
+/* look up PDP context by MM context and NSAPI */
+struct sgsn_pdp_ctx *sgsn_pdp_ctx_by_nsapi(const struct sgsn_mm_ctx *mm,
+					   uint8_t nsapi)
+{
+	struct sgsn_pdp_ctx *pdp;
+
+	llist_for_each_entry(pdp, &mm->pdp_list, list) {
+		if (pdp->nsapi == nsapi)
+			return pdp;
+	}
+	return NULL;
+}
+
+/* look up PDP context by MM context and transaction ID */
+struct sgsn_pdp_ctx *sgsn_pdp_ctx_by_tid(const struct sgsn_mm_ctx *mm,
+					 uint8_t tid)
+{
+	struct sgsn_pdp_ctx *pdp;
+
+	llist_for_each_entry(pdp, &mm->pdp_list, list) {
+		if (pdp->ti == tid)
+			return pdp;
+	}
+	return NULL;
+}
+
+struct sgsn_pdp_ctx *sgsn_pdp_ctx_alloc(struct sgsn_mm_ctx *mm,
+					uint8_t nsapi)
+{
+	struct sgsn_pdp_ctx *pdp;
+
+	pdp = sgsn_pdp_ctx_by_nsapi(mm, nsapi);
+	if (pdp)
+		return NULL;
+
+	pdp = talloc_zero(tall_bsc_ctx, struct sgsn_pdp_ctx);
+	if (!pdp)
+		return NULL;
+
+	pdp->mm = mm;
+	pdp->nsapi = nsapi;
+	pdp->ctrg = rate_ctr_group_alloc(pdp, &pdpctx_ctrg_desc, nsapi);
+	llist_add(&pdp->list, &mm->pdp_list);
+	llist_add(&pdp->g_list, &sgsn_pdp_ctxts);
+
+	return pdp;
+}
+
+void sgsn_pdp_ctx_free(struct sgsn_pdp_ctx *pdp)
+{
+	rate_ctr_group_free(pdp->ctrg);
+	llist_del(&pdp->list);
+	llist_del(&pdp->g_list);
+	talloc_free(pdp);
+}
+
+/* GGSN contexts */
+
+struct sgsn_ggsn_ctx *sgsn_ggsn_ctx_alloc(uint32_t id)
+{
+	struct sgsn_ggsn_ctx *ggc;
+
+	ggc = talloc_zero(tall_bsc_ctx, struct sgsn_ggsn_ctx);
+	if (!ggc)
+		return NULL;
+
+	ggc->id = id;
+	ggc->gtp_version = 1;
+	ggc->remote_restart_ctr = -1;
+	/* if we are called from config file parse, this gsn doesn't exist yet */
+	ggc->gsn = sgsn->gsn;
+	llist_add(&ggc->list, &sgsn_ggsn_ctxts);
+
+	return ggc;
+}
+
+struct sgsn_ggsn_ctx *sgsn_ggsn_ctx_by_id(uint32_t id)
+{
+	struct sgsn_ggsn_ctx *ggc;
+
+	llist_for_each_entry(ggc, &sgsn_ggsn_ctxts, list) {
+		if (id == ggc->id)
+			return ggc;
+	}
+	return NULL;
+}
+
+struct sgsn_ggsn_ctx *sgsn_ggsn_ctx_by_addr(struct in_addr *addr)
+{
+	struct sgsn_ggsn_ctx *ggc;
+
+	llist_for_each_entry(ggc, &sgsn_ggsn_ctxts, list) {
+		if (!memcmp(addr, &ggc->remote_addr, sizeof(*addr)))
+			return ggc;
+	}
+	return NULL;
+}
+
+
+struct sgsn_ggsn_ctx *sgsn_ggsn_ctx_find_alloc(uint32_t id)
+{
+	struct sgsn_ggsn_ctx *ggc;
+
+	ggc = sgsn_ggsn_ctx_by_id(id);
+	if (!ggc)
+		ggc = sgsn_ggsn_ctx_alloc(id);
+	return ggc;
+}
+
+/* APN contexts */
+
+#if 0
+struct apn_ctx *apn_ctx_alloc(const char *ap_name)
+{
+	struct apn_ctx *actx;
+
+	actx = talloc_zero(talloc_bsc_ctx, struct apn_ctx);
+	if (!actx)
+		return NULL;
+	actx->name = talloc_strdup(actx, ap_name);
+
+	return actx;
+}
+
+struct apn_ctx *apn_ctx_by_name(const char *name)
+{
+	struct apn_ctx *actx;
+
+	llist_for_each_entry(actx, &sgsn_apn_ctxts, list) {
+		if (!strcmp(name, actx->name))
+			return actx;
+	}
+	return NULL;
+}
+
+struct apn_ctx *apn_ctx_find_alloc(const char *name)
+{
+	struct apn_ctx *actx;
+
+	actx = apn_ctx_by_name(name);
+	if (!actx)
+		actx = apn_ctx_alloc(name);
+
+	return actx;
+}
+#endif
+
+uint32_t sgsn_alloc_ptmsi(void)
+{
+	struct sgsn_mm_ctx *mm;
+	uint32_t ptmsi;
+
+restart:
+	ptmsi = rand();
+	llist_for_each_entry(mm, &sgsn_mm_ctxts, list) {
+		if (mm->p_tmsi == ptmsi)
+			goto restart;
+	}
+
+	return ptmsi;
+}
+
+static void drop_one_pdp(struct sgsn_pdp_ctx *pdp)
+{
+	if (pdp->mm->mm_state == GMM_REGISTERED_NORMAL)
+		gsm48_tx_gsm_deact_pdp_req(pdp, GSM_CAUSE_NET_FAIL);
+	else  {
+		/* FIXME: GPRS paging in case MS is SUSPENDED */
+		LOGP(DGPRS, LOGL_NOTICE, "Hard-dropping PDP ctx due to GGSN "
+			"recovery\n");
+		sgsn_pdp_ctx_free(pdp);
+	}
+}
+
+/* High-level function to be called in case a GGSN has disappeared or
+ * ottherwise lost state (recovery procedure) */
+int drop_all_pdp_for_ggsn(struct sgsn_ggsn_ctx *ggsn)
+{
+	struct sgsn_mm_ctx *mm;
+	int num = 0;
+
+	llist_for_each_entry(mm, &sgsn_mm_ctxts, list) {
+		struct sgsn_pdp_ctx *pdp;
+		llist_for_each_entry(pdp, &mm->pdp_list, list) {
+			if (pdp->ggsn == ggsn) {
+				drop_one_pdp(pdp);
+				num++;
+			}
+		}
+	}
+
+	return num;
+}
diff --git a/src/gprs/gprs_sndcp.c b/src/gprs/gprs_sndcp.c
new file mode 100644
index 0000000..4f421e4
--- /dev/null
+++ b/src/gprs/gprs_sndcp.c
@@ -0,0 +1,616 @@
+/* GPRS SNDCP protocol implementation as per 3GPP TS 04.65 */
+
+/* (C) 2010 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2010 by On-Waves
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <errno.h>
+#include <stdint.h>
+
+#include <osmocore/msgb.h>
+#include <osmocore/linuxlist.h>
+#include <osmocore/timer.h>
+#include <osmocore/talloc.h>
+
+#include <openbsc/gsm_data.h>
+#include <openbsc/debug.h>
+#include <openbsc/gprs_bssgp.h>
+#include <openbsc/gprs_llc.h>
+#include <openbsc/sgsn.h>
+
+#include "gprs_sndcp.h"
+
+/* Chapter 7.2: SN-PDU Formats */
+struct sndcp_common_hdr {
+	/* octet 1 */
+	uint8_t nsapi:4;
+	uint8_t more:1;
+	uint8_t type:1;
+	uint8_t first:1;
+	uint8_t spare:1;
+} __attribute__((packed));
+
+/* PCOMP / DCOMP only exist in first fragment */
+struct sndcp_comp_hdr {
+	/* octet 2 */
+	uint8_t pcomp:4;
+	uint8_t dcomp:4;
+} __attribute__((packed));
+
+struct sndcp_udata_hdr {
+	/* octet 3 */
+	uint8_t npdu_high:4;
+	uint8_t seg_nr:4;
+	/* octet 4 */
+	uint8_t npdu_low;
+} __attribute__((packed));
+
+
+static void *tall_sndcp_ctx;
+
+/* A fragment queue entry, containing one framgent of a N-PDU */
+struct defrag_queue_entry {
+	struct llist_head list;
+	/* segment number of this fragment */
+	uint32_t seg_nr;
+	/* length of the data area of this fragment */
+	uint32_t data_len;
+	/* pointer to the data of this fragment */
+	uint8_t *data;
+};
+
+LLIST_HEAD(gprs_sndcp_entities);
+
+/* Enqueue a fragment into the defragment queue */
+static int defrag_enqueue(struct gprs_sndcp_entity *sne, uint8_t seg_nr,
+			  uint8_t *data, uint32_t data_len)
+{
+	struct defrag_queue_entry *dqe;
+
+	dqe = talloc_zero(tall_sndcp_ctx, struct defrag_queue_entry);
+	if (!dqe)
+		return -ENOMEM;
+	dqe->data = talloc_zero_size(dqe, data_len);
+	if (!dqe->data) {
+		talloc_free(dqe);
+		return -ENOMEM;
+	}
+	dqe->seg_nr = seg_nr;
+	dqe->data_len = data_len;
+
+	llist_add(&dqe->list, &sne->defrag.frag_list);
+
+	if (seg_nr > sne->defrag.highest_seg)
+		sne->defrag.highest_seg = seg_nr;
+
+	sne->defrag.seg_have |= (1 << seg_nr);
+	sne->defrag.tot_len += data_len;
+
+	memcpy(dqe->data, data, data_len);
+
+	return 0;
+}
+
+/* return if we have all segments of this N-PDU */
+static int defrag_have_all_segments(struct gprs_sndcp_entity *sne)
+{
+	uint32_t seg_needed = 0;
+	unsigned int i;
+
+	/* create a bitmask of needed segments */
+	for (i = 0; i <= sne->defrag.highest_seg; i++)
+		seg_needed |= (1 << i);
+
+	if (seg_needed == sne->defrag.seg_have)
+		return 1;
+
+	return 0;
+}
+
+static struct defrag_queue_entry *defrag_get_seg(struct gprs_sndcp_entity *sne,
+						 uint32_t seg_nr)
+{
+	struct defrag_queue_entry *dqe;
+
+	llist_for_each_entry(dqe, &sne->defrag.frag_list, list) {
+		if (dqe->seg_nr == seg_nr) {
+			llist_del(&dqe->list);
+			return dqe;
+		}
+	}
+	return NULL;
+}
+
+/* Perform actual defragmentation and create an output packet */
+static int defrag_segments(struct gprs_sndcp_entity *sne)
+{
+	struct msgb *msg;
+	unsigned int seg_nr;
+	uint8_t *npdu;
+
+	LOGP(DSNDCP, LOGL_DEBUG, "TLLI=0x%08x NSAPI=%u: Defragment output PDU %u "
+		"num_seg=%u tot_len=%u\n", sne->lle->llme->tlli, sne->nsapi,
+		sne->defrag.npdu, sne->defrag.highest_seg, sne->defrag.tot_len);
+	msg = msgb_alloc_headroom(sne->defrag.tot_len+256, 128, "SNDCP Defrag");
+	if (!msg)
+		return -ENOMEM;
+
+	/* FIXME: message headers + identifiers */
+
+	npdu = msg->data;
+
+	for (seg_nr = 0; seg_nr <= sne->defrag.highest_seg; seg_nr++) {
+		struct defrag_queue_entry *dqe;
+		uint8_t *data;
+
+		dqe = defrag_get_seg(sne, seg_nr);
+		if (!dqe) {
+			LOGP(DSNDCP, LOGL_ERROR, "Segment %u missing\n", seg_nr);
+			talloc_free(msg);
+			return -EIO;
+		}
+		/* actually append the segment to the N-PDU */
+		data = msgb_put(msg, dqe->data_len);
+		memcpy(data, dqe->data, dqe->data_len);
+
+		/* release memory for the fragment queue entry */
+		talloc_free(dqe);
+	}
+
+	/* FIXME: cancel timer */
+
+	/* actually send the N-PDU to the SGSN core code, which then
+	 * hands it off to the correct GTP tunnel + GGSN via gtp_data_req() */
+	return sgsn_rx_sndcp_ud_ind(&sne->ra_id, sne->lle->llme->tlli,
+				    sne->nsapi, msg, sne->defrag.tot_len, npdu);
+}
+
+static int defrag_input(struct gprs_sndcp_entity *sne, struct msgb *msg, uint8_t *hdr,
+			unsigned int len)
+{
+	struct sndcp_common_hdr *sch;
+	struct sndcp_comp_hdr *scomph = NULL;
+	struct sndcp_udata_hdr *suh;
+	uint16_t npdu_num;
+	uint8_t *data;
+	int rc;
+
+	sch = (struct sndcp_common_hdr *) hdr;
+	if (sch->first) {
+		scomph = (struct sndcp_comp_hdr *) (hdr + 1);
+		suh = (struct sndcp_udata_hdr *) (hdr + 1 + sizeof(struct sndcp_common_hdr));
+	} else
+		suh = (struct sndcp_udata_hdr *) (hdr + sizeof(struct sndcp_common_hdr));
+
+	data = (uint8_t *)suh + sizeof(struct sndcp_udata_hdr);
+
+	npdu_num = (suh->npdu_high << 8) | suh->npdu_low;
+
+	LOGP(DSNDCP, LOGL_DEBUG, "TLLI=0x%08x NSAPI=%u: Input PDU %u Segment %u "
+		"Length %u %s %s\n", sne->lle->llme->tlli, sne->nsapi, npdu_num,
+		suh->seg_nr, len, sch->first ? "F " : "", sch->more ? "M" : "");
+
+	if (sch->first) {
+		/* first segment of a new packet.  Discard all leftover fragments of
+		 * previous packet */
+		if (!llist_empty(&sne->defrag.frag_list)) {
+			struct defrag_queue_entry *dqe, *dqe2;
+			LOGP(DSNDCP, LOGL_INFO, "TLLI=0x%08x NSAPI=%u: Dropping "
+			     "SN-PDU %u due to insufficient segments (%04x)\n",
+			     sne->lle->llme->tlli, sne->nsapi, sne->defrag.npdu,
+			     sne->defrag.seg_have);
+			llist_for_each_entry_safe(dqe, dqe2, &sne->defrag.frag_list, list) {
+				llist_del(&dqe->list);
+				talloc_free(dqe);
+			}
+		}
+		/* store the currently de-fragmented PDU number */
+		sne->defrag.npdu = npdu_num;
+
+		/* Re-set fragmentation state */
+		sne->defrag.no_more = sne->defrag.highest_seg = sne->defrag.seg_have = 0;
+		sne->defrag.tot_len = 0;
+		/* FIXME: (re)start timer */
+	}
+
+	if (sne->defrag.npdu != npdu_num) {
+		LOGP(DSNDCP, LOGL_INFO, "Segment for different SN-PDU "
+			"(%u != %u)\n", npdu_num, sne->defrag.npdu);
+		/* FIXME */
+	}
+
+	/* FIXME: check if seg_nr already exists */
+	/* make sure to subtract length of SNDCP header from 'len' */
+	rc = defrag_enqueue(sne, suh->seg_nr, data, len - (data - hdr));
+	if (rc < 0)
+		return rc;
+
+	if (!sch->more) {
+		/* this is suppsed to be the last segment of the N-PDU, but it
+		 * might well be not the last to arrive */
+		sne->defrag.no_more = 1;
+	}
+
+	if (sne->defrag.no_more) {
+		/* we have already received the last segment before, let's check
+		 * if all the previous segments exist */
+		if (defrag_have_all_segments(sne))
+			return defrag_segments(sne);
+	}
+
+	return 0;
+}
+
+static struct gprs_sndcp_entity *gprs_sndcp_entity_by_lle(const struct gprs_llc_lle *lle,
+						uint8_t nsapi)
+{
+	struct gprs_sndcp_entity *sne;
+
+	llist_for_each_entry(sne, &gprs_sndcp_entities, list) {
+		if (sne->lle == lle && sne->nsapi == nsapi)
+			return sne;
+	}
+	return NULL;
+}
+
+static struct gprs_sndcp_entity *gprs_sndcp_entity_alloc(struct gprs_llc_lle *lle,
+						uint8_t nsapi)
+{
+	struct gprs_sndcp_entity *sne;
+
+	sne = talloc_zero(tall_sndcp_ctx, struct gprs_sndcp_entity);
+	if (!sne)
+		return NULL;
+
+	sne->lle = lle;
+	sne->nsapi = nsapi;
+	sne->defrag.timer.data = sne;
+	//sne->fqueue.timer.cb = FIXME;
+	sne->rx_state = SNDCP_RX_S_FIRST;
+	INIT_LLIST_HEAD(&sne->defrag.frag_list);
+
+	llist_add(&sne->list, &gprs_sndcp_entities);
+
+	return sne;
+}
+
+/* Entry point for the SNSM-ACTIVATE.indication */
+int sndcp_sm_activate_ind(struct gprs_llc_lle *lle, uint8_t nsapi)
+{
+	LOGP(DSNDCP, LOGL_INFO, "SNSM-ACTIVATE.ind (lle=%p TLLI=%08x, "
+	     "SAPI=%u, NSAPI=%u)\n", lle, lle->llme->tlli, lle->sapi, nsapi);
+
+	if (gprs_sndcp_entity_by_lle(lle, nsapi)) {
+		LOGP(DSNDCP, LOGL_ERROR, "Trying to ACTIVATE "
+			"already-existing entity (TLLI=%08x, NSAPI=%u)\n",
+			lle->llme->tlli, nsapi);
+		return -EEXIST;
+	}
+
+	if (!gprs_sndcp_entity_alloc(lle, nsapi)) {
+		LOGP(DSNDCP, LOGL_ERROR, "Out of memory during ACTIVATE\n");
+		return -ENOMEM;
+	}
+
+	return 0;
+}
+
+/* Entry point for the SNSM-DEACTIVATE.indication */
+int sndcp_sm_deactivate_ind(struct gprs_llc_lle *lle, uint8_t nsapi)
+{
+	struct gprs_sndcp_entity *sne;
+
+	LOGP(DSNDCP, LOGL_INFO, "SNSM-DEACTIVATE.ind (lle=%p, TLLI=%08x, "
+	     "SAPI=%u, NSAPI=%u)\n", lle, lle->llme->tlli, lle->sapi, nsapi);
+
+	sne = gprs_sndcp_entity_by_lle(lle, nsapi);
+	if (!sne) {
+		LOGP(DSNDCP, LOGL_ERROR, "SNSM-DEACTIVATE.ind for non-"
+		     "existing TLLI=%08x SAPI=%u NSAPI=%u\n", lle->llme->tlli,
+		     lle->sapi, nsapi);
+		return -ENOENT;
+	}
+	llist_del(&sne->list);
+	/* frag queue entries are hierarchically allocated, so no need to
+	 * free them explicitly here */
+	talloc_free(sne);
+
+	return 0;
+}
+
+/* Fragmenter state */
+struct sndcp_frag_state {
+	uint8_t frag_nr;
+	struct msgb *msg;	/* original message */
+	uint8_t *next_byte;	/* first byte of next fragment */
+
+	struct gprs_sndcp_entity *sne;
+	void *mmcontext;
+};
+
+/* returns '1' if there are more fragments to send, '0' if none */
+static int sndcp_send_ud_frag(struct sndcp_frag_state *fs)
+{
+	struct gprs_sndcp_entity *sne = fs->sne;
+	struct gprs_llc_lle *lle = sne->lle;
+	struct sndcp_common_hdr *sch;
+	struct sndcp_comp_hdr *scomph;
+	struct sndcp_udata_hdr *suh;
+	struct msgb *fmsg;
+	unsigned int max_payload_len;
+	unsigned int len;
+	uint8_t *data;
+	int rc, more;
+
+	fmsg = msgb_alloc_headroom(fs->sne->lle->params.n201_u+256, 128,
+				   "SNDCP Frag");
+	if (!fmsg)
+		return -ENOMEM;
+
+	/* make sure lower layers route the fragment like the original */
+	msgb_tlli(fmsg) = msgb_tlli(fs->msg);
+	msgb_bvci(fmsg) = msgb_bvci(fs->msg);
+	msgb_nsei(fmsg) = msgb_nsei(fs->msg);
+
+	/* prepend common SNDCP header */
+	sch = (struct sndcp_common_hdr *) msgb_put(fmsg, sizeof(*sch));
+	sch->nsapi = sne->nsapi;
+	/* Set FIRST bit if we are the first fragment in a series */
+	if (fs->frag_nr == 0)
+		sch->first = 1;
+	sch->type = 1;
+
+	/* append the compression header for first fragment */
+	if (sch->first) {
+		scomph = (struct sndcp_comp_hdr *)
+				msgb_put(fmsg, sizeof(*scomph));
+		scomph->pcomp = 0;
+		scomph->dcomp = 0;
+	}
+
+	/* append the user-data header */
+	suh = (struct sndcp_udata_hdr *) msgb_put(fmsg, sizeof(*suh));
+	suh->npdu_low = sne->tx_npdu_nr & 0xff;
+	suh->npdu_high = (sne->tx_npdu_nr >> 8) & 0xf;
+	suh->seg_nr = fs->frag_nr % 0xf;
+
+	/* calculate remaining length to be sent */
+	len = (fs->msg->data + fs->msg->len) - fs->next_byte;
+	/* how much payload can we actually send via LLC? */
+	max_payload_len = lle->params.n201_u - (sizeof(*sch) + sizeof(*suh));
+	if (sch->first)
+		max_payload_len -= sizeof(*scomph);
+	/* check if we're exceeding the max */
+	if (len > max_payload_len)
+		len = max_payload_len;
+
+	/* copy the actual fragment data into our fmsg */
+	data = msgb_put(fmsg, len);
+	memcpy(data, fs->next_byte, len);
+
+	/* Increment fragment number and data pointer to next fragment */
+	fs->frag_nr++;
+	fs->next_byte += len;
+
+	/* determine if we have more fragemnts to send */
+	if ((fs->msg->data + fs->msg->len) <= fs->next_byte)
+		more = 0;
+	else
+		more = 1;
+
+	/* set the MORE bit of the SNDCP header accordingly */
+	sch->more = more;
+
+	rc = gprs_llc_tx_ui(fmsg, lle->sapi, 0, fs->mmcontext);
+	if (rc < 0) {
+		/* abort in case of error, do not advance frag_nr / next_byte */
+		msgb_free(fmsg);
+		return rc;
+	}
+
+	if (!more) {
+		/* we've sent all fragments */
+		msgb_free(fs->msg);
+		memset(fs, 0, sizeof(*fs));
+		/* increment NPDU number for next frame */
+		sne->tx_npdu_nr = (sne->tx_npdu_nr + 1) % 0xfff;
+		return 0;
+	}
+
+	/* default: more fragments to send */
+	return 1;
+}
+
+/* Request transmission of a SN-PDU over specified LLC Entity + SAPI */
+int sndcp_unitdata_req(struct msgb *msg, struct gprs_llc_lle *lle, uint8_t nsapi,
+			void *mmcontext)
+{
+	struct gprs_sndcp_entity *sne;
+	struct sndcp_common_hdr *sch;
+	struct sndcp_comp_hdr *scomph;
+	struct sndcp_udata_hdr *suh;
+	struct sndcp_frag_state fs;
+
+	/* Identifiers from UP: (TLLI, SAPI) + (BVCI, NSEI) */
+
+	sne = gprs_sndcp_entity_by_lle(lle, nsapi);
+	if (!sne) {
+		LOGP(DSNDCP, LOGL_ERROR, "Cannot find SNDCP Entity\n");
+		return -EIO;
+	}
+
+	/* Check if we need to fragment this N-PDU into multiple SN-PDUs */
+	if (msg->len > lle->params.n201_u - 
+			(sizeof(*sch) + sizeof(*suh) + sizeof(*scomph))) {
+		/* initialize the fragmenter state */
+		fs.msg = msg;
+		fs.frag_nr = 0;
+		fs.next_byte = msg->data;
+		fs.sne = sne;
+		fs.mmcontext = mmcontext;
+
+		/* call function to generate and send fragments until all
+		 * of the N-PDU has been sent */
+		while (1) {
+			int rc = sndcp_send_ud_frag(&fs);
+			if (rc == 0)
+				return 0;
+			if (rc < 0)
+				return rc;
+		}
+		/* not reached */
+		return 0;
+	}
+
+	/* this is the non-fragmenting case where we only build 1 SN-PDU */
+
+	/* prepend the user-data header */
+	suh = (struct sndcp_udata_hdr *) msgb_push(msg, sizeof(*suh));
+	suh->npdu_low = sne->tx_npdu_nr & 0xff;
+	suh->npdu_high = (sne->tx_npdu_nr >> 8) & 0xf;
+	suh->seg_nr = 0;
+	sne->tx_npdu_nr = (sne->tx_npdu_nr + 1) % 0xfff;
+
+	scomph = (struct sndcp_comp_hdr *) msgb_push(msg, sizeof(*scomph));
+	scomph->pcomp = 0;
+	scomph->dcomp = 0;
+
+	/* prepend common SNDCP header */
+	sch = (struct sndcp_common_hdr *) msgb_push(msg, sizeof(*sch));
+	sch->first = 1;
+	sch->type = 1;
+	sch->nsapi = nsapi;
+
+	return gprs_llc_tx_ui(msg, lle->sapi, 0, mmcontext);
+}
+
+/* Section 5.1.2.17 LL-UNITDATA.ind */
+int sndcp_llunitdata_ind(struct msgb *msg, struct gprs_llc_lle *lle,
+			 uint8_t *hdr, uint16_t len)
+{
+	struct gprs_sndcp_entity *sne;
+	struct sndcp_common_hdr *sch = (struct sndcp_common_hdr *)hdr;
+	struct sndcp_comp_hdr *scomph = NULL;
+	struct sndcp_udata_hdr *suh;
+	uint8_t *npdu;
+	uint16_t npdu_num;
+	int npdu_len;
+
+	sch = (struct sndcp_common_hdr *) hdr;
+	if (sch->first) {
+		scomph = (struct sndcp_comp_hdr *) (hdr + 1);
+		suh = (struct sndcp_udata_hdr *) (hdr + 1 + sizeof(struct sndcp_common_hdr));
+	} else
+		suh = (struct sndcp_udata_hdr *) (hdr + sizeof(struct sndcp_common_hdr));
+
+	if (sch->type == 0) {
+		LOGP(DSNDCP, LOGL_ERROR, "SN-DATA PDU at unitdata_ind() function\n");
+		return -EINVAL;
+	}
+
+	if (len < sizeof(*sch) + sizeof(*suh)) {
+		LOGP(DSNDCP, LOGL_ERROR, "SN-UNITDATA PDU too short (%u)\n", len);
+		return -EIO;
+	}
+
+	sne = gprs_sndcp_entity_by_lle(lle, sch->nsapi);
+	if (!sne) {
+		LOGP(DSNDCP, LOGL_ERROR, "Message for non-existing SNDCP Entity "
+			"(lle=%p, TLLI=%08x, SAPI=%u, NSAPI=%u)\n", lle,
+			lle->llme->tlli, lle->sapi, sch->nsapi);
+		return -EIO;
+	}
+	/* FIXME: move this RA_ID up to the LLME or even higher */
+	bssgp_parse_cell_id(&sne->ra_id, msgb_bcid(msg));
+
+	/* any non-first segment is by definition something to defragment
+	 * as is any segment that tells us there are more segments */
+	if (!sch->first || sch->more)
+		return defrag_input(sne, msg, hdr, len);
+
+	if (scomph && (scomph->pcomp || scomph->dcomp)) {
+		LOGP(DSNDCP, LOGL_ERROR, "We don't support compression yet\n");
+		return -EIO;
+	}
+
+	npdu_num = (suh->npdu_high << 8) | suh->npdu_low;
+	npdu = (uint8_t *)suh + sizeof(*suh);
+	npdu_len = (msg->data + msg->len) - npdu;
+	if (npdu_len <= 0) {
+		LOGP(DSNDCP, LOGL_ERROR, "Short SNDCP N-PDU: %d\n", npdu_len);
+		return -EIO;
+	}
+	/* actually send the N-PDU to the SGSN core code, which then
+	 * hands it off to the correct GTP tunnel + GGSN via gtp_data_req() */
+	return sgsn_rx_sndcp_ud_ind(&sne->ra_id, lle->llme->tlli, sne->nsapi, msg, npdu_len, npdu);
+}
+
+/* Section 5.1.2.1 LL-RESET.ind */
+static int sndcp_ll_reset_ind(struct gprs_sndcp_entity *se)
+{
+	/* treat all outstanding SNDCP-LLC request type primitives as not sent */
+	/* reset all SNDCP XID parameters to default values */
+}
+
+static int sndcp_ll_status_ind()
+{
+	/* inform the SM sub-layer by means of SNSM-STATUS.req */
+}
+
+#if 0
+static struct sndcp_state_list {{
+	uint32_t	states;
+	unsigned int	type;
+	int		(*rout)(struct gprs_sndcp_entity *se, struct msgb *msg);
+} sndcp_state_list[] = {
+	{ ALL_STATES,
+	  LL_RESET_IND, sndcp_ll_reset_ind },
+	{ ALL_STATES,
+	  LL_ESTABLISH_IND, sndcp_ll_est_ind },
+	{ SBIT(SNDCP_S_EST_RQD),
+	  LL_ESTABLISH_RESP, sndcp_ll_est_ind },
+	{ SBIT(SNDCP_S_EST_RQD),
+	  LL_ESTABLISH_CONF, sndcp_ll_est_conf },
+	{ SBIT(SNDCP_S_
+};
+
+static int sndcp_rx_llc_prim()
+{
+	case LL_ESTABLISH_REQ:
+	case LL_RELEASE_REQ:
+	case LL_XID_REQ:
+	case LL_DATA_REQ:
+	LL_UNITDATA_REQ,	/* TLLI, SN-PDU, Ref, QoS, Radio Prio, Ciph */
+
+	switch (prim) {
+	case LL_RESET_IND:
+	case LL_ESTABLISH_IND:
+	case LL_ESTABLISH_RESP:
+	case LL_ESTABLISH_CONF:
+	case LL_RELEASE_IND:
+	case LL_RELEASE_CONF:
+	case LL_XID_IND:
+	case LL_XID_RESP:
+	case LL_XID_CONF:
+	case LL_DATA_IND:
+	case LL_DATA_CONF:
+	case LL_UNITDATA_IND:
+	case LL_STATUS_IND:
+}
+#endif
diff --git a/src/gprs/gprs_sndcp.h b/src/gprs/gprs_sndcp.h
new file mode 100644
index 0000000..e9a50be
--- /dev/null
+++ b/src/gprs/gprs_sndcp.h
@@ -0,0 +1,53 @@
+#ifndef _INT_SNDCP_H
+#define _INT_SNDCP_H
+
+#include <stdint.h>
+#include <osmocore/linuxlist.h>
+
+/* A fragment queue header, maintaining list of fragments for one N-PDU */
+struct defrag_state {
+	/* PDU number for which the defragmentation state applies */
+	uint16_t npdu;
+	/* highest segment number we have received so far */
+	uint8_t highest_seg;
+	/* bitmask of the segments we already have */
+	uint32_t seg_have;
+	/* do we still expect more segments? */
+	unsigned int no_more;
+	/* total length of all segments together */
+	unsigned int tot_len;
+
+	/* linked list of defrag_queue_entry: one for each fragment  */
+	struct llist_head frag_list;
+
+	struct timer_list timer;
+};
+
+/* See 6.7.1.2 Reassembly */
+enum sndcp_rx_state {
+	SNDCP_RX_S_FIRST,
+	SNDCP_RX_S_SUBSEQ,
+	SNDCP_RX_S_DISCARD,
+};
+
+struct gprs_sndcp_entity {
+	struct llist_head list;
+
+	/* FIXME: move this RA_ID up to the LLME or even higher */
+	struct gprs_ra_id ra_id;
+	/* reference to the LLC Entity below this SNDCP entity */
+	struct gprs_llc_lle *lle;
+	/* The NSAPI we shall use on top of LLC */
+	uint8_t nsapi;
+
+	/* NPDU number for the GTP->SNDCP side */
+	uint16_t tx_npdu_nr;
+	/* SNDCP eeceiver state */
+	enum sndcp_rx_state rx_state;
+	/* The defragmentation queue */
+	struct defrag_state defrag;
+};
+
+extern struct llist_head gprs_sndcp_entities;
+
+#endif	/* INT_SNDCP_H */
diff --git a/src/gprs/gprs_sndcp_vty.c b/src/gprs/gprs_sndcp_vty.c
new file mode 100644
index 0000000..5a755d5
--- /dev/null
+++ b/src/gprs/gprs_sndcp_vty.c
@@ -0,0 +1,74 @@
+/* VTY interface for our GPRS SNDCP implementation */
+
+/* (C) 2010 by Harald Welte <laforge@gnumonks.org>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdint.h>
+
+#include <arpa/inet.h>
+
+#include <openbsc/gsm_data.h>
+#include <osmocore/msgb.h>
+#include <osmocore/tlv.h>
+#include <osmocore/talloc.h>
+#include <osmocore/select.h>
+#include <osmocore/rate_ctr.h>
+#include <openbsc/debug.h>
+#include <openbsc/signal.h>
+#include <openbsc/gprs_llc.h>
+
+#include "gprs_sndcp.h"
+
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+
+static void vty_dump_sne(struct vty *vty, struct gprs_sndcp_entity *sne)
+{
+	unsigned int i;
+
+	vty_out(vty, " TLLI %08x SAPI=%u NSAPI=%u:%s",
+		sne->lle->llme->tlli, sne->lle->sapi, sne->nsapi, VTY_NEWLINE);
+	vty_out(vty, "  Defrag: npdu=%u highest_seg=%u seg_have=0x%08x tot_len=%u%s",
+		sne->defrag.npdu, sne->defrag.highest_seg, sne->defrag.seg_have,
+		sne->defrag.tot_len, VTY_NEWLINE);
+}
+
+
+DEFUN(show_sndcp, show_sndcp_cmd,
+	"show sndcp",
+	SHOW_STR "Display information about the SNDCP protocol")
+{
+	struct gprs_sndcp_entity *sne;
+
+	vty_out(vty, "State of SNDCP Entities%s", VTY_NEWLINE);
+	llist_for_each_entry(sne, &gprs_sndcp_entities, list)
+		vty_dump_sne(vty, sne);
+
+	return CMD_SUCCESS;
+}
+
+int gprs_sndcp_vty_init(void)
+{
+	install_element_ve(&show_sndcp_cmd);
+
+	return 0;
+}
diff --git a/src/gprs/sgsn_libgtp.c b/src/gprs/sgsn_libgtp.c
new file mode 100644
index 0000000..7b10a45
--- /dev/null
+++ b/src/gprs/sgsn_libgtp.c
@@ -0,0 +1,610 @@
+/* GPRS SGSN integration with libgtp of OpenGGSN */
+/* libgtp implements the GPRS Tunelling Protocol GTP per TS 09.60 / 29.060 */
+
+/* (C) 2010 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2010 by On-Waves
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <getopt.h>
+#include <errno.h>
+#include <signal.h>
+#include <sys/fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <osmocore/talloc.h>
+#include <osmocore/select.h>
+#include <osmocore/rate_ctr.h>
+#include <openbsc/gsm_04_08_gprs.h>
+
+#include <openbsc/signal.h>
+#include <openbsc/debug.h>
+#include <openbsc/sgsn.h>
+#include <openbsc/gprs_llc.h>
+#include <openbsc/gprs_bssgp.h>
+#include <openbsc/gprs_sgsn.h>
+#include <openbsc/gprs_gmm.h>
+
+#include <gtp.h>
+#include <pdp.h>
+
+const struct value_string gtp_cause_strs[] = {
+	{ GTPCAUSE_REQ_IMSI, "Request IMSI" },
+	{ GTPCAUSE_REQ_IMEI, "Request IMEI" },
+	{ GTPCAUSE_REQ_IMSI_IMEI, "Request IMSI and IMEI" },
+	{ GTPCAUSE_NO_ID_NEEDED, "No identity needed" },
+	{ GTPCAUSE_MS_REFUSES_X, "MS refuses" },
+	{ GTPCAUSE_MS_NOT_RESP_X, "MS is not GPRS responding" },
+	{ GTPCAUSE_ACC_REQ, "Request accepted" },
+	{ GTPCAUSE_NON_EXIST, "Non-existent" },
+	{ GTPCAUSE_INVALID_MESSAGE, "Invalid message format" },
+	{ GTPCAUSE_IMSI_NOT_KNOWN, "IMSI not known" },
+	{ GTPCAUSE_MS_DETACHED, "MS is GPRS detached" },
+	{ GTPCAUSE_MS_NOT_RESP, "MS is not GPRS responding" },
+	{ GTPCAUSE_MS_REFUSES, "MS refuses" },
+	{ GTPCAUSE_NO_RESOURCES, "No resources available" },
+	{ GTPCAUSE_NOT_SUPPORTED, "Service not supported" },
+	{ GTPCAUSE_MAN_IE_INCORRECT, "Mandatory IE incorrect" },
+	{ GTPCAUSE_MAN_IE_MISSING, "Mandatory IE missing" },
+	{ GTPCAUSE_OPT_IE_INCORRECT, "Optional IE incorrect" },
+	{ GTPCAUSE_SYS_FAIL, "System failure" },
+	{ GTPCAUSE_ROAMING_REST, "Roaming restrictions" },
+	{ GTPCAUSE_PTIMSI_MISMATCH, "P-TMSI Signature mismatch" },
+	{ GTPCAUSE_CONN_SUSP, "GPRS connection suspended" },
+	{ GTPCAUSE_AUTH_FAIL, "Authentication failure" },
+	{ GTPCAUSE_USER_AUTH_FAIL, "User authentication failed" },
+	{ GTPCAUSE_CONTEXT_NOT_FOUND, "Context not found" },
+	{ GTPCAUSE_ADDR_OCCUPIED, "All dynamic PDP addresses occupied" },
+	{ GTPCAUSE_NO_MEMORY, "No memory is available" },
+	{ GTPCAUSE_RELOC_FAIL, "Relocation failure" },
+	{ GTPCAUSE_UNKNOWN_MAN_EXTHEADER, "Unknown mandatory ext. header" },
+	{ GTPCAUSE_SEM_ERR_TFT, "Semantic error in TFT operation" },
+	{ GTPCAUSE_SYN_ERR_TFT, "Syntactic error in TFT operation" },
+	{ GTPCAUSE_SEM_ERR_FILTER, "Semantic errors in packet filter" },
+	{ GTPCAUSE_SYN_ERR_FILTER, "Syntactic errors in packet filter" },
+	{ GTPCAUSE_MISSING_APN, "Missing or unknown APN" },
+	{ GTPCAUSE_UNKNOWN_PDP, "Unknown PDP address or PDP type" },
+	{ 0, NULL }
+};
+
+/* Generate the GTP IMSI IE according to 09.60 Section 7.9.2 */
+static uint64_t imsi_str2gtp(char *str)
+{
+	uint64_t imsi64 = 0;
+	unsigned int n;
+	unsigned int imsi_len = strlen(str);
+
+	if (imsi_len > 16) {
+		LOGP(DGPRS, LOGL_NOTICE, "IMSI length > 16 not supported!\n");
+		return 0;
+	}
+
+	for (n = 0; n < 16; n++) {
+		uint64_t val;
+		if (n < imsi_len)
+			val = (str[n]-'0') & 0xf;
+		else
+			val = 0xf;
+		imsi64 |= (val << (n*4));
+	}
+	return imsi64;
+}
+
+/* generate a PDP context based on the IE's from the 04.08 message,
+ * and send the GTP create pdp context request to the GGSN */
+struct sgsn_pdp_ctx *sgsn_create_pdp_ctx(struct sgsn_ggsn_ctx *ggsn,
+					 struct sgsn_mm_ctx *mmctx,
+					 uint16_t nsapi,
+					 struct tlv_parsed *tp)
+{
+	struct sgsn_pdp_ctx *pctx;
+	struct pdp_t *pdp;
+	uint64_t imsi_ui64;
+	int rc;
+
+	LOGP(DGPRS, LOGL_ERROR, "Create PDP Context\n");
+	pctx = sgsn_pdp_ctx_alloc(mmctx, nsapi);
+	if (!pctx) {
+		LOGP(DGPRS, LOGL_ERROR, "Couldn't allocate PDP Ctx\n");
+		return NULL;
+	}
+
+	imsi_ui64 = imsi_str2gtp(mmctx->imsi);
+
+	rc = pdp_newpdp(&pdp, imsi_ui64, nsapi, NULL);
+	if (rc) {
+		LOGP(DGPRS, LOGL_ERROR, "Out of libgtp PDP Contexts\n");
+		return NULL;
+	}
+	pdp->priv = pctx;
+	pctx->lib = pdp;
+	pctx->ggsn = ggsn;
+
+	//pdp->peer =	/* sockaddr_in of GGSN (receive) */
+	//pdp->ipif =	/* not used by library */
+	pdp->version = ggsn->gtp_version;
+	pdp->hisaddr0 =	ggsn->remote_addr;
+	pdp->hisaddr1 = ggsn->remote_addr;
+	//pdp->cch_pdp = 512;	/* Charging Flat Rate */
+
+	/* MS provided APN, subscription not verified */
+	pdp->selmode = 0x01;
+
+	/* IMSI, TEID/TEIC, FLLU/FLLC, TID, NSAPI set in pdp_newpdp */
+
+	/* FIXME: MSISDN in BCD format from mmctx */
+	//pdp->msisdn.l/.v
+
+	/* End User Address from GMM requested PDP address */
+	pdp->eua.l = TLVP_LEN(tp, OSMO_IE_GSM_REQ_PDP_ADDR);
+	if (pdp->eua.l > sizeof(pdp->eua.v))
+		pdp->eua.l = sizeof(pdp->eua.v);
+	memcpy(pdp->eua.v, TLVP_VAL(tp, OSMO_IE_GSM_REQ_PDP_ADDR),
+		pdp->eua.l);
+	/* Highest 4 bits of first byte need to be set to 1, otherwise
+	 * the IE is identical with the 04.08 PDP Address IE */
+	pdp->eua.v[0] |= 0xf0;
+
+	/* APN name from GMM */
+	pdp->apn_use.l = TLVP_LEN(tp, GSM48_IE_GSM_APN);
+	if (pdp->apn_use.l > sizeof(pdp->apn_use.v))
+		pdp->apn_use.l = sizeof(pdp->apn_use.v);
+	memcpy(pdp->apn_use.v, TLVP_VAL(tp, GSM48_IE_GSM_APN),
+		pdp->apn_use.l);
+
+	/* Protocol Configuration Options from GMM */
+	pdp->pco_req.l = TLVP_LEN(tp, GSM48_IE_GSM_PROTO_CONF_OPT);
+	if (pdp->pco_req.l > sizeof(pdp->pco_req.v))
+		pdp->pco_req.l = sizeof(pdp->pco_req.v);
+	memcpy(pdp->pco_req.v, TLVP_VAL(tp, GSM48_IE_GSM_PROTO_CONF_OPT),
+		pdp->pco_req.l);
+
+	/* QoS options from GMM */
+	pdp->qos_req.l = TLVP_LEN(tp, OSMO_IE_GSM_REQ_QOS);
+	if (pdp->qos_req.l > sizeof(pdp->qos_req.v))
+		pdp->qos_req.l = sizeof(pdp->qos_req.v);
+	memcpy(pdp->qos_req.v, TLVP_VAL(tp, OSMO_IE_GSM_REQ_QOS),
+		pdp->qos_req.l);
+
+	/* SGSN address for control plane */
+	pdp->gsnlc.l = sizeof(sgsn->cfg.gtp_listenaddr.sin_addr);
+	memcpy(pdp->gsnlc.v, &sgsn->cfg.gtp_listenaddr.sin_addr,
+		sizeof(sgsn->cfg.gtp_listenaddr.sin_addr));
+
+	/* SGSN address for user plane */
+	pdp->gsnlu.l = sizeof(sgsn->cfg.gtp_listenaddr.sin_addr);
+	memcpy(pdp->gsnlu.v, &sgsn->cfg.gtp_listenaddr.sin_addr,
+		sizeof(sgsn->cfg.gtp_listenaddr.sin_addr));
+
+	/* change pdp state to 'requested' */
+	pctx->state = PDP_STATE_CR_REQ;
+
+	rc = gtp_create_context_req(ggsn->gsn, pdp, pctx);
+	/* FIXME */
+
+	return pctx;
+}
+
+/* SGSN wants to delete a PDP context */
+int sgsn_delete_pdp_ctx(struct sgsn_pdp_ctx *pctx)
+{
+	LOGP(DGPRS, LOGL_ERROR, "Delete PDP Context\n");
+
+	/* FIXME: decide if we need teardown or not ! */
+	return gtp_delete_context_req(pctx->ggsn->gsn, pctx->lib, pctx, 1);
+}
+
+struct cause_map {
+	uint8_t cause_in;
+	uint8_t cause_out;
+};
+
+static uint8_t cause_map(const struct cause_map *map, uint8_t in, uint8_t deflt)
+{
+	const struct cause_map *m;
+
+	for (m = map; m->cause_in && m->cause_out; m++) {
+		if (m->cause_in == in)
+			return m->cause_out;
+	}
+	return deflt;
+}
+
+/* how do we map from gtp cause to SM cause */
+static const struct cause_map gtp2sm_cause_map[] = {
+	{ GTPCAUSE_NO_RESOURCES, 	GSM_CAUSE_INSUFF_RSRC },
+	{ GTPCAUSE_NOT_SUPPORTED,	GSM_CAUSE_SERV_OPT_NOTSUPP },
+	{ GTPCAUSE_MAN_IE_INCORRECT,	GSM_CAUSE_INV_MAND_INFO },
+	{ GTPCAUSE_MAN_IE_MISSING,	GSM_CAUSE_INV_MAND_INFO },
+	{ GTPCAUSE_OPT_IE_INCORRECT,	GSM_CAUSE_PROTO_ERR_UNSPEC },
+	{ GTPCAUSE_SYS_FAIL,		GSM_CAUSE_NET_FAIL },
+	{ GTPCAUSE_ROAMING_REST,	GSM_CAUSE_REQ_SERV_OPT_NOTSUB },
+	{ GTPCAUSE_PTIMSI_MISMATCH,	GSM_CAUSE_PROTO_ERR_UNSPEC },
+	{ GTPCAUSE_CONN_SUSP,		GSM_CAUSE_PROTO_ERR_UNSPEC },
+	{ GTPCAUSE_AUTH_FAIL,		GSM_CAUSE_AUTH_FAILED },
+	{ GTPCAUSE_USER_AUTH_FAIL,	GSM_CAUSE_ACT_REJ_GGSN },
+	{ GTPCAUSE_CONTEXT_NOT_FOUND,	GSM_CAUSE_PROTO_ERR_UNSPEC },
+	{ GTPCAUSE_ADDR_OCCUPIED,	GSM_CAUSE_INSUFF_RSRC },
+	{ GTPCAUSE_NO_MEMORY,		GSM_CAUSE_INSUFF_RSRC },
+	{ GTPCAUSE_RELOC_FAIL,		GSM_CAUSE_PROTO_ERR_UNSPEC },
+	{ GTPCAUSE_UNKNOWN_MAN_EXTHEADER, GSM_CAUSE_PROTO_ERR_UNSPEC },
+	{ GTPCAUSE_MISSING_APN,		GSM_CAUSE_MISSING_APN },
+	{ GTPCAUSE_UNKNOWN_PDP,		GSM_CAUSE_UNKNOWN_PDP },
+	{ 0, 0 }
+};
+
+/* The GGSN has confirmed the creation of a PDP Context */
+static int create_pdp_conf(struct pdp_t *pdp, void *cbp, int cause)
+{
+	struct sgsn_pdp_ctx *pctx = cbp;
+	uint8_t reject_cause;
+	int rc;
+
+	DEBUGP(DGPRS, "Received CREATE PDP CTX CONF, cause=%d(%s)\n",
+		cause, get_value_string(gtp_cause_strs, cause));
+
+	/* Check for cause value if it was really successful */
+	if (cause < 0) {
+		LOGP(DGPRS, LOGL_NOTICE, "Create PDP ctx req timed out\n");
+		if (pdp && pdp->version == 1) {
+			pdp->version = 0;
+			gtp_create_context_req(sgsn->gsn, pdp, cbp);
+			return 0;
+		} else {
+			reject_cause = GSM_CAUSE_NET_FAIL;
+			goto reject;
+		}
+	}
+
+	/* Check for cause value if it was really successful */
+	if (cause != GTPCAUSE_ACC_REQ) {
+		reject_cause = cause_map(gtp2sm_cause_map, cause,
+					 GSM_CAUSE_ACT_REJ_GGSN);
+		goto reject;
+	}
+
+	/* Activate the SNDCP layer */
+	sndcp_sm_activate_ind(&pctx->mm->llme->lle[pctx->sapi], pctx->nsapi);
+
+	/* Send PDP CTX ACT to MS */
+	return gsm48_tx_gsm_act_pdp_acc(pctx);
+
+reject:
+	pctx->state = PDP_STATE_NONE;
+	if (pdp)
+		pdp_freepdp(pdp);
+	/* Send PDP CTX ACT REJ to MS */
+	rc = gsm48_tx_gsm_act_pdp_rej(pctx->mm, pctx->ti, reject_cause,
+					0, NULL);
+	sgsn_pdp_ctx_free(pctx);
+
+	return EOF;
+}
+
+/* Confirmation of a PDP Context Delete */
+static int delete_pdp_conf(struct pdp_t *pdp, void *cbp, int cause)
+{
+	struct sgsn_pdp_ctx *pctx = cbp;
+	int rc;
+
+	DEBUGP(DGPRS, "Received DELETE PDP CTX CONF, cause=%d(%s)\n",
+		cause, get_value_string(gtp_cause_strs, cause));
+
+	/* Deactivate the SNDCP layer */
+	sndcp_sm_deactivate_ind(&pctx->mm->llme->lle[pctx->sapi], pctx->nsapi);
+
+	/* Confirm deactivation of PDP context to MS */
+	rc = gsm48_tx_gsm_deact_pdp_acc(pctx);
+
+	sgsn_pdp_ctx_free(pctx);
+
+	return rc;
+}
+
+/* Confirmation of an GTP ECHO request */
+static int echo_conf(struct pdp_t *pdp, void *cbp, int recovery)
+{
+	if (recovery < 0) {
+		DEBUGP(DGPRS, "GTP Echo Request timed out\n");
+		/* FIXME: if version == 1, retry with version 0 */
+	} else {
+		DEBUGP(DGPRS, "GTP Rx Echo Response\n");
+	}
+	return 0;
+}
+
+/* Any message received by GGSN contains a recovery IE */
+static int cb_recovery(struct sockaddr_in *peer, uint8_t recovery)
+{
+	struct sgsn_ggsn_ctx *ggsn;
+	
+	ggsn = sgsn_ggsn_ctx_by_addr(&peer->sin_addr);
+	if (!ggsn) {
+		DEBUGP(DGPRS, "Received Recovery IE for unknown GGSN\n");
+		return -EINVAL;
+	}
+
+	if (ggsn->remote_restart_ctr == -1) {
+		/* First received ECHO RESPONSE, note the restart ctr */
+		ggsn->remote_restart_ctr = recovery;
+	} else if (ggsn->remote_restart_ctr != recovery) {
+		/* counter has changed (GGSN restart): release all PDP */
+		LOGP(DGPRS, LOGL_NOTICE, "GGSN recovery (%u->%u), "
+		     "releasing all PDP contexts\n",
+		     ggsn->remote_restart_ctr, recovery);
+		ggsn->remote_restart_ctr = recovery;
+		drop_all_pdp_for_ggsn(ggsn);
+	}
+	return 0;
+}
+
+/* libgtp callback for confirmations */
+static int cb_conf(int type, int cause, struct pdp_t *pdp, void *cbp)
+{
+	DEBUGP(DGPRS, "libgtp cb_conf(type=%d, cause=%d, pdp=%p, cbp=%p)\n",
+		type, cause, pdp, cbp);
+
+	if (cause == EOF)
+		LOGP(DGPRS, LOGL_ERROR, "libgtp EOF (type=%u, pdp=%p, cbp=%p)\n",
+			type, pdp, cbp);
+
+	switch (type) {
+	case GTP_ECHO_REQ:
+		/* libgtp hands us the RECOVERY number instead of a cause */
+		return echo_conf(pdp, cbp, cause);
+	case GTP_CREATE_PDP_REQ:
+		return create_pdp_conf(pdp, cbp, cause);
+	case GTP_DELETE_PDP_REQ:
+		return delete_pdp_conf(pdp, cbp, cause);
+	default:
+		break;
+	}
+	return 0;
+}
+
+/* Called whenever a PDP context is deleted for any reason */
+static int cb_delete_context(struct pdp_t *pdp)
+{
+	LOGP(DGPRS, LOGL_INFO, "PDP Context was deleted\n");
+	return 0;
+}
+
+/* Called when we receive a Version Not Supported message */
+static int cb_unsup_ind(struct sockaddr_in *peer)
+{
+	LOGP(DGPRS, LOGL_INFO, "GTP Version not supported Indication "
+		"from %s:%u\n", inet_ntoa(peer->sin_addr),
+		ntohs(peer->sin_port));
+	return 0;
+}
+
+/* Called when we receive a Supported Ext Headers Notification */
+static int cb_extheader_ind(struct sockaddr_in *peer)
+{
+	LOGP(DGPRS, LOGL_INFO, "GTP Supported Ext Headers Noficiation "
+		"from %s:%u\n", inet_ntoa(peer->sin_addr),
+		ntohs(peer->sin_port));
+	return 0;
+}
+
+/* Called whenever we recive a DATA packet */
+static int cb_data_ind(struct pdp_t *lib, void *packet, unsigned int len)
+{
+	struct bssgp_paging_info pinfo;
+	struct sgsn_pdp_ctx *pdp;
+	struct sgsn_mm_ctx *mm;
+	struct msgb *msg;
+	uint8_t *ud;
+	int rc;
+
+	DEBUGP(DGPRS, "GTP DATA IND from GGSN, length=%u\n", len);
+
+	pdp = lib->priv;
+	if (!pdp) {
+		DEBUGP(DGPRS, "GTP DATA IND from GGSN for unknown PDP\n");
+		return -EIO;
+	}
+	mm = pdp->mm;
+
+	msg = msgb_alloc_headroom(len+256, 128, "GTP->SNDCP");
+	ud = msgb_put(msg, len);
+	memcpy(ud, packet, len);
+
+	msgb_tlli(msg) = mm->tlli;
+	msgb_bvci(msg) = mm->bvci;
+	msgb_nsei(msg) = mm->nsei;
+
+	switch (mm->mm_state) {
+	case GMM_REGISTERED_SUSPENDED:
+		/* initiate PS PAGING procedure */
+		memset(&pinfo, 0, sizeof(pinfo));
+		pinfo.mode = BSSGP_PAGING_PS;
+		pinfo.scope = BSSGP_PAGING_BVCI;
+		pinfo.bvci = mm->bvci;
+		pinfo.imsi = mm->imsi;
+		pinfo.ptmsi = &mm->p_tmsi;
+		pinfo.drx_params = mm->drx_parms;
+		pinfo.qos[0] = 0; // FIXME
+		rc = gprs_bssgp_tx_paging(mm->nsei, 0, &pinfo);
+		rate_ctr_inc(&mm->ctrg->ctr[GMM_CTR_PAGING_PS]);
+		/* FIXME: queue the packet we received from GTP */
+		break;
+	case GMM_REGISTERED_NORMAL:
+		break;
+	default:
+		LOGP(DGPRS, LOGL_ERROR, "GTP DATA IND for TLLI %08X in state "
+			"%u\n", mm->tlli, mm->mm_state);
+		msgb_free(msg);
+		return -1;
+	}
+
+	rate_ctr_inc(&pdp->ctrg->ctr[PDP_CTR_PKTS_UDATA_OUT]);
+	rate_ctr_add(&pdp->ctrg->ctr[PDP_CTR_BYTES_UDATA_OUT], len);
+	rate_ctr_inc(&mm->ctrg->ctr[GMM_CTR_PKTS_UDATA_OUT]);
+	rate_ctr_add(&mm->ctrg->ctr[GMM_CTR_BYTES_UDATA_OUT], len);
+
+	return sndcp_unitdata_req(msg, &mm->llme->lle[pdp->sapi],
+				  pdp->nsapi, mm);
+}
+
+/* Called by SNDCP when it has received/re-assembled a N-PDU */
+int sgsn_rx_sndcp_ud_ind(struct gprs_ra_id *ra_id, int32_t tlli, uint8_t nsapi,
+			 struct msgb *msg, uint32_t npdu_len, uint8_t *npdu)
+{
+	struct sgsn_mm_ctx *mmctx;
+	struct sgsn_pdp_ctx *pdp;
+
+	/* look-up the MM context for this message */
+	mmctx = sgsn_mm_ctx_by_tlli(tlli, ra_id);
+	if (!mmctx) {
+		LOGP(DGPRS, LOGL_ERROR,
+			"Cannot find MM CTX for TLLI %08x\n", tlli);
+		return -EIO;
+	}
+	/* look-up the PDP context for this message */
+	pdp = sgsn_pdp_ctx_by_nsapi(mmctx, nsapi);
+	if (!pdp) {
+		LOGP(DGPRS, LOGL_ERROR, "Cannot find PDP CTX for "
+			"TLLI=%08x, NSAPI=%u\n", tlli, nsapi);
+		return -EIO;
+	}
+	if (!pdp->lib) {
+		LOGP(DGPRS, LOGL_ERROR, "PDP CTX without libgtp\n");
+		return -EIO;
+	}
+
+	rate_ctr_inc(&pdp->ctrg->ctr[PDP_CTR_PKTS_UDATA_IN]);
+	rate_ctr_add(&pdp->ctrg->ctr[PDP_CTR_BYTES_UDATA_IN], npdu_len);
+	rate_ctr_inc(&mmctx->ctrg->ctr[GMM_CTR_PKTS_UDATA_IN]);
+	rate_ctr_add(&mmctx->ctrg->ctr[GMM_CTR_BYTES_UDATA_IN], npdu_len);
+
+	return gtp_data_req(pdp->ggsn->gsn, pdp->lib, npdu, npdu_len);
+
+	return gtp_data_req(pdp->ggsn->gsn, pdp->lib, npdu, npdu_len);
+}
+
+/* libgtp select loop integration */
+static int sgsn_gtp_fd_cb(struct bsc_fd *fd, unsigned int what)
+{
+	struct sgsn_instance *sgi = fd->data;
+	int rc;
+
+	if (!(what & BSC_FD_READ))
+		return 0;
+
+	switch (fd->priv_nr) {
+	case 0:
+		rc = gtp_decaps0(sgi->gsn);
+		break;
+	case 1:
+		rc = gtp_decaps1c(sgi->gsn);
+		break;
+	case 2:
+		rc = gtp_decaps1u(sgi->gsn);
+		break;
+	default:
+		rc = -EINVAL;
+		break;
+	}
+	return rc;
+}
+
+static void sgsn_gtp_tmr_start(struct sgsn_instance *sgi)
+{
+	struct timeval next;
+
+	/* Retrieve next retransmission as struct timeval */
+	gtp_retranstimeout(sgi->gsn, &next);
+
+	/* re-schedule the timer */
+	bsc_schedule_timer(&sgi->gtp_timer, next.tv_sec, next.tv_usec/1000);
+}
+
+/* timer callback for libgtp retransmissions and ping */
+static void sgsn_gtp_tmr_cb(void *data)
+{
+	struct sgsn_instance *sgi = data;
+
+	/* Do all the retransmissions as needed */
+	gtp_retrans(sgi->gsn);
+
+	sgsn_gtp_tmr_start(sgi);
+}
+
+int sgsn_gtp_init(struct sgsn_instance *sgi)
+{
+	int rc;
+	struct gsn_t *gsn;
+
+	rc = gtp_new(&sgi->gsn, sgi->cfg.gtp_statedir,
+		     &sgi->cfg.gtp_listenaddr.sin_addr, GTP_MODE_SGSN);
+	if (rc) {
+		LOGP(DGPRS, LOGL_ERROR, "Failed to create GTP: %d\n", rc);
+		return rc;
+	}
+	gsn = sgi->gsn;
+
+	sgi->gtp_fd0.fd = gsn->fd0;
+	sgi->gtp_fd0.priv_nr = 0;
+	sgi->gtp_fd0.data = sgi;
+	sgi->gtp_fd0.when = BSC_FD_READ;
+	sgi->gtp_fd0.cb = sgsn_gtp_fd_cb;
+	rc = bsc_register_fd(&sgi->gtp_fd0);
+	if (rc < 0)
+		return rc;
+
+	sgi->gtp_fd1c.fd = gsn->fd1c;
+	sgi->gtp_fd1c.priv_nr = 1;
+	sgi->gtp_fd1c.data = sgi;
+	sgi->gtp_fd1c.when = BSC_FD_READ;
+	sgi->gtp_fd1c.cb = sgsn_gtp_fd_cb;
+	bsc_register_fd(&sgi->gtp_fd1c);
+	if (rc < 0)
+		return rc;
+
+	sgi->gtp_fd1u.fd = gsn->fd1u;
+	sgi->gtp_fd1u.priv_nr = 2;
+	sgi->gtp_fd1u.data = sgi;
+	sgi->gtp_fd1u.when = BSC_FD_READ;
+	sgi->gtp_fd1u.cb = sgsn_gtp_fd_cb;
+	bsc_register_fd(&sgi->gtp_fd1u);
+	if (rc < 0)
+		return rc;
+
+	/* Start GTP re-transmission timer */
+	sgi->gtp_timer.cb = sgsn_gtp_tmr_cb;
+	sgi->gtp_timer.data = sgi;
+	sgsn_gtp_tmr_start(sgi);
+
+	/* Register callbackcs with libgtp */
+	gtp_set_cb_delete_context(gsn, cb_delete_context);
+	gtp_set_cb_conf(gsn, cb_conf);
+	gtp_set_cb_recovery(gsn, cb_recovery);
+	gtp_set_cb_data_ind(gsn, cb_data_ind);
+	gtp_set_cb_unsup_ind(gsn, cb_unsup_ind);
+	gtp_set_cb_extheader_ind(gsn, cb_extheader_ind);
+
+	return 0;
+}
diff --git a/src/gprs/sgsn_main.c b/src/gprs/sgsn_main.c
new file mode 100644
index 0000000..c59265f
--- /dev/null
+++ b/src/gprs/sgsn_main.c
@@ -0,0 +1,288 @@
+/* GPRS SGSN Implementation */
+
+/* (C) 2010 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2010 by On-Waves
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <getopt.h>
+#include <errno.h>
+#include <signal.h>
+#include <sys/fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <osmocore/talloc.h>
+#include <osmocore/select.h>
+#include <osmocore/rate_ctr.h>
+#include <osmocore/logging.h>
+#include <osmocore/process.h>
+
+#include <osmocom/vty/telnet_interface.h>
+
+#include <openbsc/signal.h>
+#include <openbsc/debug.h>
+#include <openbsc/vty.h>
+#include <openbsc/sgsn.h>
+#include <openbsc/gprs_ns.h>
+#include <openbsc/gprs_bssgp.h>
+#include <openbsc/gprs_llc.h>
+
+#include <gtp.h>
+
+#include "../../bscconfig.h"
+
+/* this is here for the vty... it will never be called */
+void subscr_put() { abort(); }
+
+#define _GNU_SOURCE
+#include <getopt.h>
+
+void *tall_bsc_ctx;
+
+struct gprs_ns_inst *sgsn_nsi;
+static struct log_target *stderr_target;
+static int daemonize = 0;
+const char *openbsc_copyright =
+	"Copyright (C) 2010 Harald Welte and On-Waves\r\n"
+	"License AGPLv3+: GNU AGPL version 2 or later <http://gnu.org/licenses/agpl-3.0.html>\r\n"
+	"This is free software: you are free to change and redistribute it.\r\n"
+	"There is NO WARRANTY, to the extent permitted by law.\r\n";
+
+static struct sgsn_instance sgsn_inst = {
+	.config_file = "osmo_sgsn.cfg",
+	.cfg = {
+		.gtp_statedir = "./",
+	},
+};
+struct sgsn_instance *sgsn = &sgsn_inst;
+
+/* call-back function for the NS protocol */
+static int sgsn_ns_cb(enum gprs_ns_evt event, struct gprs_nsvc *nsvc,
+		      struct msgb *msg, u_int16_t bvci)
+{
+	int rc = 0;
+
+	switch (event) {
+	case GPRS_NS_EVT_UNIT_DATA:
+		/* hand the message into the BSSGP implementation */
+		rc = gprs_bssgp_rcvmsg(msg);
+		break;
+	default:
+		LOGP(DGPRS, LOGL_ERROR, "SGSN: Unknown event %u from NS\n", event);
+		if (msg)
+			talloc_free(msg);
+		rc = -EIO;
+		break;
+	}
+	return rc;
+}
+
+static void signal_handler(int signal)
+{
+	fprintf(stdout, "signal %u received\n", signal);
+
+	switch (signal) {
+	case SIGINT:
+		dispatch_signal(SS_GLOBAL, S_GLOBAL_SHUTDOWN, NULL);
+		sleep(1);
+		exit(0);
+		break;
+	case SIGABRT:
+		/* in case of abort, we want to obtain a talloc report
+		 * and then return to the caller, who will abort the process */
+	case SIGUSR1:
+		talloc_report(tall_vty_ctx, stderr);
+		talloc_report_full(tall_bsc_ctx, stderr);
+		break;
+	case SIGUSR2:
+		talloc_report_full(tall_vty_ctx, stderr);
+		break;
+	default:
+		break;
+	}
+}
+
+/* NSI that BSSGP uses when transmitting on NS */
+extern struct gprs_ns_inst *bssgp_nsi;
+extern void *tall_msgb_ctx;
+
+extern enum node_type bsc_vty_go_parent(struct vty *vty);
+
+static struct vty_app_info vty_info = {
+	.name 		= "OsmoSGSN",
+	.version	= PACKAGE_VERSION,
+	.go_parent_cb	= bsc_vty_go_parent,
+	.is_config_node	= bsc_vty_is_config_node,
+};
+
+static void print_help(void)
+{
+	printf("Some useful help...\n");
+	printf("  -h --help\tthis text\n");
+	printf("  -D --daemonize\tFork the process into a background daemon\n");
+	printf("  -d option --debug\tenable Debugging\n");
+	printf("  -s --disable-color\n");
+	printf("  -c --config-file\tThe config file to use\n");
+	printf("  -e --log-level number\tSet a global log level\n");
+}
+
+static void handle_options(int argc, char **argv)
+{
+	while (1) {
+		int option_index = 0, c;
+		static struct option long_options[] = {
+			{"help", 0, 0, 'h'},
+			{"debug", 1, 0, 'd'},
+			{"daemonize", 0, 0, 'D'},
+			{"config-file", 1, 0, 'c'},
+			{"disable-color", 0, 0, 's'},
+			{"timestamp", 0, 0, 'T'},
+			{"log-level", 1, 0, 'e'},
+			{NULL, 0, 0, 0}
+		};
+
+		c = getopt_long(argc, argv, "hd:Dc:sTe:",
+				long_options, &option_index);
+		if (c == -1)
+			break;
+
+		switch (c) {
+		case 'h':
+			//print_usage();
+			print_help();
+			exit(0);
+		case 's':
+			log_set_use_color(stderr_target, 0);
+			break;
+		case 'd':
+			log_parse_category_mask(stderr_target, optarg);
+			break;
+		case 'D':
+			daemonize = 1;
+			break;
+		case 'c':
+			sgsn_inst.config_file = strdup(optarg);
+			break;
+		case 'T':
+			log_set_print_timestamp(stderr_target, 1);
+			break;
+		case 'e':
+			log_set_log_level(stderr_target, atoi(optarg));
+			break;
+		default:
+			/* ignore */
+			break;
+		}
+	}
+}
+
+int main(int argc, char **argv)
+{
+	struct gsm_network dummy_network;
+	struct sockaddr_in sin;
+	int rc;
+
+	tall_bsc_ctx = talloc_named_const(NULL, 0, "osmo_sgsn");
+	tall_msgb_ctx = talloc_named_const(tall_bsc_ctx, 0, "msgb");
+
+	signal(SIGINT, &signal_handler);
+	signal(SIGABRT, &signal_handler);
+	signal(SIGUSR1, &signal_handler);
+	signal(SIGUSR2, &signal_handler);
+	signal(SIGPIPE, SIG_IGN);
+
+	log_init(&log_info);
+	stderr_target = log_target_create_stderr();
+	log_add_target(stderr_target);
+	log_set_all_filter(stderr_target, 1);
+
+	vty_info.copyright = openbsc_copyright;
+	vty_init(&vty_info);
+	logging_vty_add_cmds();
+        sgsn_vty_init();
+
+	handle_options(argc, argv);
+
+	rate_ctr_init(tall_bsc_ctx);
+	rc = telnet_init(tall_bsc_ctx, &dummy_network, 4245);
+	if (rc < 0)
+		exit(1);
+
+	sgsn_nsi = gprs_ns_instantiate(&sgsn_ns_cb);
+	if (!sgsn_nsi) {
+		LOGP(DGPRS, LOGL_ERROR, "Unable to instantiate NS\n");
+		exit(1);
+	}
+	bssgp_nsi = sgsn_inst.cfg.nsi = sgsn_nsi;
+
+	gprs_llc_init("/usr/local/lib/osmocom/crypt/");
+
+	gprs_ns_vty_init(bssgp_nsi);
+	gprs_bssgp_vty_init();
+	gprs_llc_vty_init();
+	gprs_sndcp_vty_init();
+	/* FIXME: register signal handler for SS_NS */
+
+	rc = sgsn_parse_config(sgsn_inst.config_file, &sgsn_inst.cfg);
+	if (rc < 0) {
+		LOGP(DGPRS, LOGL_FATAL, "Cannot parse config file\n");
+		exit(2);
+	}
+
+	rc = sgsn_gtp_init(&sgsn_inst);
+	if (rc) {
+		LOGP(DGPRS, LOGL_FATAL, "Cannot bind/listen on GTP socket\n");
+		exit(2);
+	}
+
+	rc = gprs_ns_nsip_listen(sgsn_nsi);
+	if (rc < 0) {
+		LOGP(DGPRS, LOGL_FATAL, "Cannot bind/listen on NSIP socket\n");
+		exit(2);
+	}
+
+	rc = gprs_ns_frgre_listen(sgsn_nsi);
+	if (rc < 0) {
+		LOGP(DGPRS, LOGL_FATAL, "Cannot bind/listen GRE "
+			"socket. Do you have CAP_NET_RAW?\n");
+		exit(2);
+	}
+
+	if (daemonize) {
+		rc = osmo_daemonize();
+		if (rc < 0) {
+			perror("Error during daemonize");
+			exit(1);
+		}
+	}
+
+	while (1) {
+		rc = bsc_select_main(0);
+		if (rc < 0)
+			exit(3);
+	}
+
+	exit(0);
+}
diff --git a/src/gprs/sgsn_vty.c b/src/gprs/sgsn_vty.c
new file mode 100644
index 0000000..74669ff
--- /dev/null
+++ b/src/gprs/sgsn_vty.c
@@ -0,0 +1,357 @@
+/*
+ * (C) 2010 by Harald Welte <laforge@gnumonks.org>
+ * (C) 2010 by On-Waves
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <osmocore/talloc.h>
+#include <osmocore/utils.h>
+#include <osmocore/rate_ctr.h>
+
+#include <openbsc/debug.h>
+#include <openbsc/sgsn.h>
+#include <openbsc/gprs_ns.h>
+#include <openbsc/gprs_sgsn.h>
+#include <openbsc/vty.h>
+#include <openbsc/gsm_04_08_gprs.h>
+
+#include <osmocom/vty/command.h>
+#include <osmocom/vty/vty.h>
+
+#include <pdp.h>
+
+static struct sgsn_config *g_cfg = NULL;
+
+
+#define GSM48_MAX_APN_LEN	102	/* 10.5.6.1 */
+static char *gprs_apn2str(uint8_t *apn, unsigned int len)
+{
+	static char apnbuf[GSM48_MAX_APN_LEN+1];
+	unsigned int i;
+
+	if (!apn)
+		return "";
+
+	if (len > sizeof(apnbuf)-1)
+		len = sizeof(apnbuf)-1;
+
+	memcpy(apnbuf, apn, len);
+	apnbuf[len] = '\0';
+
+	/* replace the domain name step sizes with dots */
+	while (i < len) {
+		unsigned int step = apnbuf[i];
+		apnbuf[i] = '.';
+		i += step+1;
+	}
+
+	return apnbuf+1;
+}
+
+static char *gprs_pdpaddr2str(uint8_t *pdpa, uint8_t len)
+{
+	static char str[INET6_ADDRSTRLEN + 10];
+
+	if (!pdpa || len < 2)
+		return "none";
+
+	switch (pdpa[0] & 0x0f) {
+	case PDP_TYPE_ORG_IETF:
+		switch (pdpa[1]) {
+		case PDP_TYPE_N_IETF_IPv4:
+			if (len < 2 + 4)
+				break;
+			strcpy(str, "IPv4 ");
+			inet_ntop(AF_INET, pdpa+2, str+5, sizeof(str)-5);
+			return str;
+		case PDP_TYPE_N_IETF_IPv6:
+			if (len < 2 + 8)
+				break;
+			strcpy(str, "IPv6 ");
+			inet_ntop(AF_INET6, pdpa+2, str+5, sizeof(str)-5);
+			return str;
+		default:
+			break;
+		}
+		break;
+	case PDP_TYPE_ORG_ETSI:
+		if (pdpa[1] == PDP_TYPE_N_ETSI_PPP)
+			return "PPP";
+		break;
+	default:
+		break;
+	}
+
+	return "invalid";
+}
+
+static struct cmd_node sgsn_node = {
+	SGSN_NODE,
+	"%s(sgsn)#",
+	1,
+};
+
+static int config_write_sgsn(struct vty *vty)
+{
+	struct sgsn_ggsn_ctx *gctx;
+
+	vty_out(vty, "sgsn%s", VTY_NEWLINE);
+
+	vty_out(vty, " gtp local-ip %s%s",
+		inet_ntoa(g_cfg->gtp_listenaddr.sin_addr), VTY_NEWLINE);
+
+	llist_for_each_entry(gctx, &sgsn_ggsn_ctxts, list) {
+		vty_out(vty, " ggsn %u remote-ip %s%s", gctx->id,
+			inet_ntoa(gctx->remote_addr), VTY_NEWLINE);
+		vty_out(vty, " ggsn %u gtp-version %u%s", gctx->id,
+			gctx->gtp_version, VTY_NEWLINE);
+	}
+
+	return CMD_SUCCESS;
+}
+
+#define SGSN_STR	"Configure the SGSN"
+
+DEFUN(cfg_sgsn, cfg_sgsn_cmd,
+	"sgsn",
+	SGSN_STR)
+{
+	vty->node = SGSN_NODE;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_sgsn_bind_addr, cfg_sgsn_bind_addr_cmd,
+	"gtp local-ip A.B.C.D",
+	"GTP Parameters\n"
+	"Set the IP address for the local GTP bind\n")
+{
+	inet_aton(argv[0], &g_cfg->gtp_listenaddr.sin_addr);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ggsn_remote_ip, cfg_ggsn_remote_ip_cmd,
+	"ggsn <0-255> remote-ip A.B.C.D",
+	"")
+{
+	uint32_t id = atoi(argv[0]);
+	struct sgsn_ggsn_ctx *ggc = sgsn_ggsn_ctx_find_alloc(id);
+
+	inet_aton(argv[1], &ggc->remote_addr);
+
+	return CMD_SUCCESS;
+}
+
+#if 0
+DEFUN(cfg_ggsn_remote_port, cfg_ggsn_remote_port_cmd,
+	"ggsn <0-255> remote-port <0-65535>",
+	"")
+{
+	uint32_t id = atoi(argv[0]);
+	struct sgsn_ggsn_ctx *ggc = sgsn_ggsn_ctx_find_alloc(id);
+	uint16_t port = atoi(argv[1]);
+
+}
+#endif
+
+DEFUN(cfg_ggsn_gtp_version, cfg_ggsn_gtp_version_cmd,
+	"ggsn <0-255> gtp-version (0|1)",
+	"")
+{
+	uint32_t id = atoi(argv[0]);
+	struct sgsn_ggsn_ctx *ggc = sgsn_ggsn_ctx_find_alloc(id);
+
+	if (atoi(argv[1]))
+		ggc->gtp_version = 1;
+	else
+		ggc->gtp_version = 0;
+
+	return CMD_SUCCESS;
+}
+
+#if 0
+DEFUN(cfg_apn_ggsn, cfg_apn_ggsn_cmd,
+	"apn APNAME ggsn <0-255>",
+	"")
+{
+	struct apn_ctx **
+}
+#endif
+
+const struct value_string gprs_mm_st_strs[] = {
+	{ GMM_DEREGISTERED, "DEREGISTERED" },
+	{ GMM_COMMON_PROC_INIT, "COMMON PROCEDURE (INIT)" },
+	{ GMM_REGISTERED_NORMAL, "REGISTERED (NORMAL)" },
+	{ GMM_REGISTERED_SUSPENDED, "REGISTERED (SUSPENDED)" },
+	{ GMM_DEREGISTERED_INIT, "DEREGISTERED (INIT)" },
+	{ 0, NULL }
+};
+
+static void vty_dump_pdp(struct vty *vty, const char *pfx,
+			 struct sgsn_pdp_ctx *pdp)
+{
+	vty_out(vty, "%sPDP Context IMSI: %s, SAPI: %u, NSAPI: %u%s",
+		pfx, pdp->mm->imsi, pdp->sapi, pdp->nsapi, VTY_NEWLINE);
+	vty_out(vty, "%s  APN: %s%s", pfx,
+		gprs_apn2str(pdp->lib->apn_use.v, pdp->lib->apn_use.l),
+		VTY_NEWLINE);
+	vty_out(vty, "%s  PDP Address: %s%s", pfx,
+		gprs_pdpaddr2str(pdp->lib->eua.v, pdp->lib->eua.l),
+		VTY_NEWLINE);
+	vty_out_rate_ctr_group(vty, " ", pdp->ctrg);
+}
+
+static void vty_dump_mmctx(struct vty *vty, const char *pfx,
+			   struct sgsn_mm_ctx *mm, int pdp)
+{
+	vty_out(vty, "%sMM Context for IMSI %s, IMEI %s, P-TMSI %08x%s",
+		pfx, mm->imsi, mm->imei, mm->p_tmsi, VTY_NEWLINE);
+	vty_out(vty, "%s  MSISDN: %s, TLLI: %08x%s", pfx, mm->msisdn,
+		mm->tlli, VTY_NEWLINE);
+	vty_out(vty, "%s  MM State: %s, Routeing Area: %u-%u-%u-%u, "
+		"Cell ID: %u%s", pfx,
+		get_value_string(gprs_mm_st_strs, mm->mm_state),
+		mm->ra.mcc, mm->ra.mnc, mm->ra.lac, mm->ra.rac,
+		mm->cell_id, VTY_NEWLINE);
+
+	vty_out_rate_ctr_group(vty, " ", mm->ctrg);
+
+	if (pdp) {
+		struct sgsn_pdp_ctx *pdp;
+
+		llist_for_each_entry(pdp, &mm->pdp_list, list)
+			vty_dump_pdp(vty, "  ", pdp);
+	}
+}
+
+DEFUN(show_sgsn, show_sgsn_cmd, "show sgsn",
+      SHOW_STR "Display information about the SGSN")
+{
+	/* FIXME: statistics */
+	return CMD_SUCCESS;
+}
+
+#define MMCTX_STR "MM Context\n"
+#define INCLUDE_PDP_STR "Include PDP Context Information\n"
+
+#if 0
+DEFUN(show_mmctx_tlli, show_mmctx_tlli_cmd,
+	"show mm-context tlli HEX [pdp]",
+	SHOW_STR MMCTX_STR "Identify by TLLI\n" "TLLI\n" INCLUDE_PDP_STR)
+{
+	uint32_t tlli;
+	struct sgsn_mm_ctx *mm;
+
+	tlli = strtoul(argv[0], NULL, 16);
+	mm = sgsn_mm_ctx_by_tlli(tlli);
+	if (!mm) {
+		vty_out(vty, "No MM context for TLLI %08x%s",
+			tlli, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	vty_dump_mmctx(vty, "", mm, argv[1] ? 1 : 0);
+	return CMD_SUCCESS;
+}
+#endif
+
+DEFUN(swow_mmctx_imsi, show_mmctx_imsi_cmd,
+	"show mm-context imsi IMSI [pdp]",
+	SHOW_STR MMCTX_STR "Identify by IMSI\n" "IMSI of the MM Context\n"
+	INCLUDE_PDP_STR)
+{
+	struct sgsn_mm_ctx *mm;
+
+	mm = sgsn_mm_ctx_by_imsi(argv[0]);
+	if (!mm) {
+		vty_out(vty, "No MM context for IMSI %s%s",
+			argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	vty_dump_mmctx(vty, "", mm, argv[1] ? 1 : 0);
+	return CMD_SUCCESS;
+}
+
+DEFUN(swow_mmctx_all, show_mmctx_all_cmd,
+	"show mm-context all [pdp]",
+	SHOW_STR MMCTX_STR "All MM Contexts\n" INCLUDE_PDP_STR)
+{
+	struct sgsn_mm_ctx *mm;
+
+	llist_for_each_entry(mm, &sgsn_mm_ctxts, list)
+		vty_dump_mmctx(vty, "", mm, argv[0] ? 1 : 0);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(show_ggsn, show_ggsn_cmd,
+	"show ggsn",
+	"")
+{
+
+}
+
+DEFUN(show_pdpctx_all, show_pdpctx_all_cmd,
+	"show pdp-context all",
+	SHOW_STR "Display information on PDP Context\n")
+{
+	struct sgsn_pdp_ctx *pdp;
+
+	llist_for_each_entry(pdp, &sgsn_pdp_ctxts, g_list)
+		vty_dump_pdp(vty, "", pdp);
+
+	return CMD_SUCCESS;
+}
+
+int sgsn_vty_init(void)
+{
+	install_element_ve(&show_sgsn_cmd);
+	//install_element_ve(&show_mmctx_tlli_cmd);
+	install_element_ve(&show_mmctx_imsi_cmd);
+	install_element_ve(&show_mmctx_all_cmd);
+	install_element_ve(&show_pdpctx_all_cmd);
+
+	install_element(CONFIG_NODE, &cfg_sgsn_cmd);
+	install_node(&sgsn_node, config_write_sgsn);
+	install_default(SGSN_NODE);
+	install_element(SGSN_NODE, &ournode_exit_cmd);
+	install_element(SGSN_NODE, &ournode_end_cmd);
+	install_element(SGSN_NODE, &cfg_sgsn_bind_addr_cmd);
+	install_element(SGSN_NODE, &cfg_ggsn_remote_ip_cmd);
+	//install_element(SGSN_NODE, &cfg_ggsn_remote_port_cmd);
+	install_element(SGSN_NODE, &cfg_ggsn_gtp_version_cmd);
+
+	return 0;
+}
+
+int sgsn_parse_config(const char *config_file, struct sgsn_config *cfg)
+{
+	int rc;
+
+	g_cfg = cfg;
+	rc = vty_read_config_file(config_file, NULL);
+	if (rc < 0) {
+		fprintf(stderr, "Failed to parse the config file: '%s'\n", config_file);
+		return rc;
+	}
+
+	return 0;
+}